mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
200 lines
6.4 KiB
TypeScript
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)};
|
|
`;
|