teamNotifications.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import {Fragment} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  5. import {hasEveryAccess} from 'sentry/components/acl/access';
  6. import AsyncComponent from 'sentry/components/asyncComponent';
  7. import {Button} from 'sentry/components/button';
  8. import Confirm from 'sentry/components/confirm';
  9. import EmptyMessage from 'sentry/components/emptyMessage';
  10. import TextField from 'sentry/components/forms/fields/textField';
  11. import ExternalLink from 'sentry/components/links/externalLink';
  12. import {Panel, PanelBody, PanelHeader} from 'sentry/components/panels';
  13. import {Tooltip} from 'sentry/components/tooltip';
  14. import {IconDelete} from 'sentry/icons';
  15. import {t, tct} from 'sentry/locale';
  16. import {space} from 'sentry/styles/space';
  17. import {ExternalTeam, Integration, Organization, Team} from 'sentry/types';
  18. import {toTitleCase} from 'sentry/utils';
  19. import withOrganization from 'sentry/utils/withOrganization';
  20. import AsyncView from 'sentry/views/asyncView';
  21. import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
  22. type Props = RouteComponentProps<{teamId: string}, {}> & {
  23. organization: Organization;
  24. team: Team;
  25. };
  26. type State = AsyncView['state'] & {
  27. integrations: Integration[];
  28. teamDetails: Team;
  29. };
  30. const DOCS_LINK =
  31. 'https://docs.sentry.io/product/integrations/notification-incidents/slack/#team-notifications';
  32. const NOTIFICATION_PROVIDERS = ['slack'];
  33. class TeamNotificationSettings extends AsyncView<Props, State> {
  34. getTitle() {
  35. return 'Team Notification Settings';
  36. }
  37. getEndpoints(): ReturnType<AsyncComponent['getEndpoints']> {
  38. const {organization, team} = this.props;
  39. return [
  40. [
  41. 'teamDetails',
  42. `/teams/${organization.slug}/${team.slug}/`,
  43. {query: {expand: ['externalTeams']}},
  44. ],
  45. [
  46. 'integrations',
  47. `/organizations/${organization.slug}/integrations/`,
  48. {query: {includeConfig: 0}},
  49. ],
  50. ];
  51. }
  52. handleDelete = async (mapping: ExternalTeam) => {
  53. try {
  54. const {organization, team} = this.props;
  55. const endpoint = `/teams/${organization.slug}/${team.slug}/external-teams/${mapping.id}/`;
  56. await this.api.requestPromise(endpoint, {
  57. method: 'DELETE',
  58. });
  59. addSuccessMessage(t('Deletion successful'));
  60. this.fetchData();
  61. } catch {
  62. addErrorMessage(t('An error occurred'));
  63. }
  64. };
  65. renderBody() {
  66. const {team} = this.props;
  67. return (
  68. <Fragment>
  69. <PermissionAlert access={['team:write']} team={team} />
  70. <Panel>
  71. <PanelHeader>{t('Notifications')}</PanelHeader>
  72. <PanelBody>{this.renderPanelBody()}</PanelBody>
  73. </Panel>
  74. </Fragment>
  75. );
  76. }
  77. renderPanelBody() {
  78. const {organization, team} = this.props;
  79. const {teamDetails, integrations} = this.state;
  80. const notificationIntegrations = integrations.filter(integration =>
  81. NOTIFICATION_PROVIDERS.includes(integration.provider.key)
  82. );
  83. if (!notificationIntegrations.length) {
  84. return (
  85. <EmptyMessage>
  86. {t('No Notification Integrations have been installed yet.')}
  87. </EmptyMessage>
  88. );
  89. }
  90. const externalTeams = (teamDetails.externalTeams || []).filter(externalTeam =>
  91. NOTIFICATION_PROVIDERS.includes(externalTeam.provider)
  92. );
  93. if (!externalTeams.length) {
  94. return (
  95. <EmptyMessage>
  96. <div>{t('No teams have been linked yet.')}</div>
  97. <NotDisabledSubText>
  98. {tct('Head over to Slack and type [code] to get started. [link].', {
  99. code: <code>/sentry link team</code>,
  100. link: <ExternalLink href={DOCS_LINK}>{t('Learn more')}</ExternalLink>,
  101. })}
  102. </NotDisabledSubText>
  103. </EmptyMessage>
  104. );
  105. }
  106. const integrationsById = Object.fromEntries(
  107. notificationIntegrations.map(integration => [integration.id, integration])
  108. );
  109. const hasWriteAccess = hasEveryAccess(['team:write'], {organization, team});
  110. return externalTeams.map(externalTeam => (
  111. <FormFieldWrapper key={externalTeam.id}>
  112. <StyledFormField
  113. disabled
  114. label={
  115. <div>
  116. <NotDisabledText>
  117. {toTitleCase(externalTeam.provider)}:
  118. {integrationsById[externalTeam.integrationId].name}
  119. </NotDisabledText>
  120. <NotDisabledSubText>
  121. {tct('Unlink this channel in Slack with [code]. [link].', {
  122. code: <code>/sentry unlink team</code>,
  123. link: <ExternalLink href={DOCS_LINK}>{t('Learn more')}</ExternalLink>,
  124. })}
  125. </NotDisabledSubText>
  126. </div>
  127. }
  128. labelText={t('Unlink this channel in slack with `/slack unlink team`')}
  129. name="externalName"
  130. value={externalTeam.externalName}
  131. />
  132. <DeleteButtonWrapper>
  133. <Tooltip
  134. title={t(
  135. 'You must be an organization owner, manager or admin to remove a Slack team link'
  136. )}
  137. disabled={hasWriteAccess}
  138. >
  139. <Confirm
  140. disabled={!hasWriteAccess}
  141. onConfirm={() => this.handleDelete(externalTeam)}
  142. message={t('Are you sure you want to remove this Slack team link?')}
  143. >
  144. <Button
  145. size="sm"
  146. icon={<IconDelete size="md" />}
  147. aria-label={t('delete')}
  148. disabled={!hasWriteAccess}
  149. />
  150. </Confirm>
  151. </Tooltip>
  152. </DeleteButtonWrapper>
  153. </FormFieldWrapper>
  154. ));
  155. }
  156. }
  157. export default withOrganization(TeamNotificationSettings);
  158. const NotDisabledText = styled('div')`
  159. color: ${p => p.theme.textColor};
  160. line-height: ${space(2)};
  161. `;
  162. const NotDisabledSubText = styled('div')`
  163. color: ${p => p.theme.subText};
  164. font-size: ${p => p.theme.fontSizeRelativeSmall};
  165. line-height: 1.4;
  166. margin-top: ${space(1)};
  167. `;
  168. const FormFieldWrapper = styled('div')`
  169. display: flex;
  170. align-items: center;
  171. justify-content: flex-start;
  172. `;
  173. const StyledFormField = styled(TextField)`
  174. flex: 1;
  175. `;
  176. const DeleteButtonWrapper = styled('div')`
  177. margin-right: ${space(4)};
  178. padding-right: ${space(0.5)};
  179. `;