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

200 lines
6.4 KiB
TypeScript

import {Fragment} 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 Confirm from 'sentry/components/confirm';
import type DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
import EmptyMessage from 'sentry/components/emptyMessage';
import TextField from 'sentry/components/forms/fields/textField';
import ExternalLink from 'sentry/components/links/externalLink';
import Panel from 'sentry/components/panels/panel';
import PanelBody from 'sentry/components/panels/panelBody';
import PanelHeader from 'sentry/components/panels/panelHeader';
import {Tooltip} from 'sentry/components/tooltip';
import {IconDelete} from 'sentry/icons';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {ExternalTeam, Integration, Organization, Team} from 'sentry/types';
import {toTitleCase} from 'sentry/utils';
import withOrganization from 'sentry/utils/withOrganization';
import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
type Props = RouteComponentProps<{teamId: string}, {}> & {
organization: Organization;
team: Team;
};
type State = DeprecatedAsyncView['state'] & {
integrations: Integration[];
teamDetails: Team;
};
const DOCS_LINK =
'https://docs.sentry.io/product/integrations/notification-incidents/slack/#team-notifications';
const NOTIFICATION_PROVIDERS = ['slack'];
class TeamNotificationSettings extends DeprecatedAsyncView<Props, State> {
getTitle() {
return 'Team Notification Settings';
}
getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
const {organization, team} = this.props;
return [
[
'teamDetails',
`/teams/${organization.slug}/${team.slug}/`,
{query: {expand: ['externalTeams']}},
],
[
'integrations',
`/organizations/${organization.slug}/integrations/`,
{query: {includeConfig: 0}},
],
];
}
handleDelete = async (mapping: ExternalTeam) => {
try {
const {organization, team} = this.props;
const endpoint = `/teams/${organization.slug}/${team.slug}/external-teams/${mapping.id}/`;
await this.api.requestPromise(endpoint, {
method: 'DELETE',
});
addSuccessMessage(t('Deletion successful'));
this.fetchData();
} catch {
addErrorMessage(t('An error occurred'));
}
};
renderBody() {
const {team} = this.props;
return (
<Fragment>
<PermissionAlert access={['team:write']} team={team} />
<Panel>
<PanelHeader>{t('Notifications')}</PanelHeader>
<PanelBody>{this.renderPanelBody()}</PanelBody>
</Panel>
</Fragment>
);
}
renderPanelBody() {
const {organization, team} = this.props;
const {teamDetails, integrations} = this.state;
const notificationIntegrations = integrations.filter(integration =>
NOTIFICATION_PROVIDERS.includes(integration.provider.key)
);
if (!notificationIntegrations.length) {
return (
<EmptyMessage>
{t('No Notification Integrations have been installed yet.')}
</EmptyMessage>
);
}
const externalTeams = (teamDetails.externalTeams || []).filter(externalTeam =>
NOTIFICATION_PROVIDERS.includes(externalTeam.provider)
);
if (!externalTeams.length) {
return (
<EmptyMessage>
<div>{t('No teams have been linked yet.')}</div>
<NotDisabledSubText>
{tct('Head over to Slack and type [code] to get started. [link].', {
code: <code>/sentry link team</code>,
link: <ExternalLink href={DOCS_LINK}>{t('Learn more')}</ExternalLink>,
})}
</NotDisabledSubText>
</EmptyMessage>
);
}
const integrationsById = Object.fromEntries(
notificationIntegrations.map(integration => [integration.id, integration])
);
const hasWriteAccess = hasEveryAccess(['team:write'], {organization, team});
return externalTeams.map(externalTeam => (
<FormFieldWrapper key={externalTeam.id}>
<StyledFormField
disabled
label={
<div>
<NotDisabledText>
{toTitleCase(externalTeam.provider)}:
{integrationsById[externalTeam.integrationId].name}
</NotDisabledText>
<NotDisabledSubText>
{tct('Unlink this channel in Slack with [code]. [link].', {
code: <code>/sentry unlink team</code>,
link: <ExternalLink href={DOCS_LINK}>{t('Learn more')}</ExternalLink>,
})}
</NotDisabledSubText>
</div>
}
labelText={t('Unlink this channel in slack with `/slack unlink team`')}
name="externalName"
value={externalTeam.externalName}
/>
<DeleteButtonWrapper>
<Tooltip
title={t(
'You must be an organization owner, manager or admin to remove a Slack team link'
)}
disabled={hasWriteAccess}
>
<Confirm
disabled={!hasWriteAccess}
onConfirm={() => this.handleDelete(externalTeam)}
message={t('Are you sure you want to remove this Slack team link?')}
>
<Button
size="sm"
icon={<IconDelete size="md" />}
aria-label={t('delete')}
disabled={!hasWriteAccess}
/>
</Confirm>
</Tooltip>
</DeleteButtonWrapper>
</FormFieldWrapper>
));
}
}
export default withOrganization(TeamNotificationSettings);
const NotDisabledText = styled('div')`
color: ${p => p.theme.textColor};
line-height: ${space(2)};
`;
const NotDisabledSubText = styled('div')`
color: ${p => p.theme.subText};
font-size: ${p => p.theme.fontSizeRelativeSmall};
line-height: 1.4;
margin-top: ${space(1)};
`;
const FormFieldWrapper = styled('div')`
display: flex;
align-items: center;
justify-content: flex-start;
`;
const StyledFormField = styled(TextField)`
flex: 1;
`;
const DeleteButtonWrapper = styled('div')`
margin-right: ${space(4)};
padding-right: ${space(0.5)};
`;