teamNotifications.tsx 5.9 KB

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