Files
2024-02-15 17:01:50 +00:00

201 lines
7.0 KiB
TypeScript

import {Fragment, useState} from 'react';
import type {RouteComponentProps} from 'react-router';
import styled from '@emotion/styled';
import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
import {hasEveryAccess} from 'sentry/components/acl/access';
import {Button} from 'sentry/components/button';
import DropdownAutoComplete from 'sentry/components/dropdownAutoComplete';
import DropdownButton from 'sentry/components/dropdownButton';
import EmptyMessage from 'sentry/components/emptyMessage';
import LoadingError from 'sentry/components/loadingError';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import Pagination from 'sentry/components/pagination';
import Panel from 'sentry/components/panels/panel';
import PanelBody from 'sentry/components/panels/panelBody';
import PanelHeader from 'sentry/components/panels/panelHeader';
import PanelItem from 'sentry/components/panels/panelItem';
import {Tooltip} from 'sentry/components/tooltip';
import {IconFlag, IconSubtract} from 'sentry/icons';
import {t} from 'sentry/locale';
import ProjectsStore from 'sentry/stores/projectsStore';
import {space} from 'sentry/styles/space';
import type {Project, Team} from 'sentry/types';
import {sortProjects} from 'sentry/utils';
import {useApiQuery} from 'sentry/utils/queryClient';
import useApi from 'sentry/utils/useApi';
import useOrganization from 'sentry/utils/useOrganization';
import ProjectListItem from 'sentry/views/settings/components/settingsProjectItem';
import TextBlock from 'sentry/views/settings/components/text/textBlock';
import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
interface TeamProjectsProps extends RouteComponentProps<{teamId: string}, {}> {
team: Team;
}
function TeamProjects({team, location, params}: TeamProjectsProps) {
const organization = useOrganization();
const api = useApi({persistInFlight: true});
const [query, setQuery] = useState<string>('');
const teamId = params.teamId;
const {
data: linkedProjects,
isError: linkedProjectsError,
isLoading: linkedProjectsLoading,
getResponseHeader: linkedProjectsHeaders,
refetch: refetchLinkedProjects,
} = useApiQuery<Project[]>(
[
`/organizations/${organization.slug}/projects/`,
{
query: {
query: `team:${teamId}`,
cursor: location.query.cursor,
},
},
],
{staleTime: 0}
);
const {
data: unlinkedProjects = [],
isLoading: loadingUnlinkedProjects,
refetch: refetchUnlinkedProjects,
} = useApiQuery<Project[]>(
[
`/organizations/${organization.slug}/projects/`,
{
query: {query: query ? `!team:${teamId} ${query}` : `!team:${teamId}`},
},
],
{staleTime: 0}
);
const handleLinkProject = (project: Project, action: string) => {
api.request(`/projects/${organization.slug}/${project.slug}/teams/${teamId}/`, {
method: action === 'add' ? 'POST' : 'DELETE',
success: resp => {
refetchLinkedProjects();
refetchUnlinkedProjects();
ProjectsStore.onUpdateSuccess(resp);
addSuccessMessage(
action === 'add'
? t('Successfully added project to team.')
: t('Successfully removed project from team')
);
},
error: () => {
addErrorMessage(t("Wasn't able to change project association."));
},
});
};
const linkedProjectsPageLinks = linkedProjectsHeaders?.('Link');
const hasWriteAccess = hasEveryAccess(['team:write'], {organization, team});
const otherProjects = unlinkedProjects
.filter(p => p.access.includes('project:write'))
.map(p => ({
value: p.id,
searchKey: p.slug,
label: <ProjectListElement>{p.slug}</ProjectListElement>,
}));
return (
<Fragment>
<TextBlock>
{t(
'If you have Team Admin permissions for other projects, you can associate them with this team.'
)}
</TextBlock>
<PermissionAlert access={['team:write']} team={team} />
<Panel>
<PanelHeader hasButtons>
<div>{t('Projects')}</div>
<div style={{textTransform: 'none', fontWeight: 'normal'}}>
{!hasWriteAccess ? (
<DropdownButton
disabled
title={t('You do not have enough permission to associate a project.')}
size="xs"
>
{t('Add Project')}
</DropdownButton>
) : (
<DropdownAutoComplete
items={otherProjects}
onChange={evt => setQuery(evt.target.value)}
onSelect={selection => {
const project = unlinkedProjects.find(p => p.id === selection.value);
if (project) {
handleLinkProject(project, 'add');
}
}}
onClose={() => setQuery('')}
busy={loadingUnlinkedProjects}
emptyMessage={t('You are not an admin for any other projects')}
alignMenu="right"
>
{({isOpen}) => (
<DropdownButton isOpen={isOpen} size="xs">
{t('Add Project')}
</DropdownButton>
)}
</DropdownAutoComplete>
)}
</div>
</PanelHeader>
<PanelBody>
{linkedProjectsError && (
<LoadingError onRetry={() => refetchLinkedProjects()} />
)}
{linkedProjectsLoading && <LoadingIndicator />}
{linkedProjects?.length ? (
sortProjects(linkedProjects).map(project => (
<StyledPanelItem key={project.id}>
<ProjectListItem project={project} organization={organization} />
<Tooltip
disabled={hasWriteAccess}
title={t(
'You do not have enough permission to change project association.'
)}
>
<Button
size="sm"
disabled={!hasWriteAccess}
icon={<IconSubtract isCircled />}
aria-label={t('Remove')}
onClick={() => {
handleLinkProject(project, 'remove');
}}
>
{t('Remove')}
</Button>
</Tooltip>
</StyledPanelItem>
))
) : linkedProjectsLoading ? null : (
<EmptyMessage size="large" icon={<IconFlag size="xl" />}>
{t("This team doesn't have access to any projects.")}
</EmptyMessage>
)}
</PanelBody>
</Panel>
<Pagination pageLinks={linkedProjectsPageLinks} />
</Fragment>
);
}
const StyledPanelItem = styled(PanelItem)`
display: flex;
align-items: center;
justify-content: space-between;
padding: ${space(2)};
max-width: 100%;
`;
const ProjectListElement = styled('div')`
padding: ${space(0.25)} 0;
`;
export default TeamProjects;