notificationActionManager.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import {Fragment, useMemo, useState} from 'react';
  2. import capitalize from 'lodash/capitalize';
  3. import DropdownButton from 'sentry/components/dropdownButton';
  4. import {DropdownMenu, MenuItemProps} from 'sentry/components/dropdownMenu';
  5. import NotificationActionItem from 'sentry/components/notificationActions/notificationActionItem';
  6. import {Tooltip} from 'sentry/components/tooltip';
  7. import {IconAdd} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import {Project} from 'sentry/types';
  10. import {
  11. AvailableNotificationAction,
  12. NotificationAction,
  13. NotificationActionService,
  14. } from 'sentry/types/notificationActions';
  15. type NotificationActionManagerProps = {
  16. /**
  17. * The list of existing notification actions
  18. */
  19. actions: NotificationAction[];
  20. /**
  21. * The list of available notification actions
  22. */
  23. availableActions: AvailableNotificationAction[];
  24. /**
  25. * The project associated with the notification actions
  26. * TODO(enterprise): refactor to account for multiple projects
  27. */
  28. project: Project;
  29. /**
  30. * Updates the notification alert count for this project
  31. */
  32. updateAlertCount: (projectId: number, alertCount: number) => void;
  33. disabled?: boolean;
  34. /**
  35. * Optional list of roles to display as recipients of Sentry notifications
  36. */
  37. recipientRoles?: string[];
  38. };
  39. function NotificationActionManager({
  40. actions,
  41. availableActions,
  42. recipientRoles,
  43. project,
  44. updateAlertCount = () => {},
  45. disabled = false,
  46. }: NotificationActionManagerProps) {
  47. const [notificationActions, setNotificationActions] =
  48. useState<Partial<NotificationAction>[]>(actions);
  49. const removeNotificationAction = (index: number) => {
  50. // Removes notif action from state using the index
  51. const updatedActions = [...notificationActions];
  52. updatedActions.splice(index, 1);
  53. setNotificationActions(updatedActions);
  54. updateAlertCount(parseInt(project.id, 10), updatedActions.length);
  55. };
  56. const updateNotificationAction = (index: number, updatedAction: NotificationAction) => {
  57. // Updates notif action from state using the index
  58. const updatedActions = [...notificationActions];
  59. updatedActions.splice(index, 1, updatedAction);
  60. setNotificationActions(updatedActions);
  61. };
  62. // Lists the available actions for each service
  63. const availableServices: Record<
  64. NotificationActionService,
  65. AvailableNotificationAction[]
  66. > = useMemo(() => {
  67. const availableServicesMap: Record<
  68. NotificationActionService,
  69. AvailableNotificationAction[]
  70. > = {
  71. [NotificationActionService.SENTRY_NOTIFICATION]: [],
  72. [NotificationActionService.EMAIL]: [],
  73. [NotificationActionService.SLACK]: [],
  74. [NotificationActionService.PAGERDUTY]: [],
  75. [NotificationActionService.MSTEAMS]: [],
  76. [NotificationActionService.SENTRY_APP]: [],
  77. [NotificationActionService.OPSGENIE]: [],
  78. [NotificationActionService.DISCORD]: [],
  79. };
  80. availableActions.forEach(a => {
  81. availableServicesMap[a.action.serviceType as NotificationActionService].push(a);
  82. });
  83. return availableServicesMap;
  84. }, [availableActions]);
  85. // Groups the notification actions together by service
  86. // Will render the notif actions in the order the keys are listed in
  87. const actionsMap: Record<
  88. NotificationActionService,
  89. {action: NotificationAction; index: number}[]
  90. > = useMemo(() => {
  91. const notificationActionsMap: Record<
  92. NotificationActionService,
  93. {action: NotificationAction; index: number}[]
  94. > = {
  95. [NotificationActionService.SENTRY_NOTIFICATION]: [],
  96. [NotificationActionService.EMAIL]: [],
  97. [NotificationActionService.SLACK]: [],
  98. [NotificationActionService.PAGERDUTY]: [],
  99. [NotificationActionService.MSTEAMS]: [],
  100. [NotificationActionService.SENTRY_APP]: [],
  101. [NotificationActionService.OPSGENIE]: [],
  102. [NotificationActionService.DISCORD]: [],
  103. };
  104. notificationActions.forEach((action, index) => {
  105. if (action.serviceType) {
  106. notificationActionsMap[action.serviceType].push({action, index});
  107. }
  108. });
  109. return notificationActionsMap;
  110. }, [notificationActions]);
  111. // Groups the pagerduty integrations with their corresponding allowed services
  112. const pagerdutyIntegrations: Record<number, AvailableNotificationAction[]> =
  113. useMemo(() => {
  114. const integrations: Record<number, AvailableNotificationAction[]> = {};
  115. availableServices[NotificationActionService.PAGERDUTY].forEach(service => {
  116. const integrationId = service.action.integrationId;
  117. if (integrationId) {
  118. if (integrationId in integrations) {
  119. integrations[integrationId].push(service);
  120. } else {
  121. integrations[integrationId] = [service];
  122. }
  123. }
  124. });
  125. return integrations;
  126. }, [availableServices]);
  127. const opsgenieIntegrations: Record<number, AvailableNotificationAction[]> =
  128. useMemo(() => {
  129. const integrations: Record<number, AvailableNotificationAction[]> = {};
  130. availableServices[NotificationActionService.OPSGENIE].forEach(team => {
  131. const integrationId = team.action.integrationId;
  132. if (integrationId) {
  133. if (integrationId in integrations) {
  134. integrations[integrationId].push(team);
  135. } else {
  136. integrations[integrationId] = [team];
  137. }
  138. }
  139. });
  140. return integrations;
  141. }, [availableServices]);
  142. const renderNotificationActions = () => {
  143. if (!notificationActions) {
  144. return [];
  145. }
  146. // Renders the notif actions grouped together by kind
  147. return Object.keys(actionsMap).map(serviceType => {
  148. const services = actionsMap[serviceType];
  149. return services.map(({action, index}) => (
  150. <NotificationActionItem
  151. key={index}
  152. index={index}
  153. defaultEdit={!action.id}
  154. action={action}
  155. recipientRoles={recipientRoles}
  156. availableActions={availableServices[serviceType]}
  157. opsgenieIntegrations={opsgenieIntegrations}
  158. pagerdutyIntegrations={pagerdutyIntegrations}
  159. project={project}
  160. onDelete={removeNotificationAction}
  161. onUpdate={updateNotificationAction}
  162. disabled={disabled}
  163. />
  164. ));
  165. });
  166. };
  167. const getLabel = (serviceType: string) => {
  168. switch (serviceType) {
  169. case NotificationActionService.SENTRY_NOTIFICATION:
  170. return t('Send a Sentry notification');
  171. case NotificationActionService.OPSGENIE:
  172. return t('Send an Opsgenie notification');
  173. default:
  174. return t('Send a %s notification', capitalize(serviceType));
  175. }
  176. };
  177. const getMenuItems = () => {
  178. const menuItems: MenuItemProps[] = [];
  179. Object.entries(availableServices).forEach(([serviceType, validActions]) => {
  180. if (validActions.length === 0) {
  181. return;
  182. }
  183. // Cannot have more than one Sentry notification
  184. if (
  185. serviceType === NotificationActionService.SENTRY_NOTIFICATION &&
  186. actionsMap[serviceType].length === 1
  187. ) {
  188. return;
  189. }
  190. const label = getLabel(serviceType);
  191. menuItems.push({
  192. key: serviceType,
  193. label,
  194. onAction: () => {
  195. // Add notification action
  196. const updatedActions = [...notificationActions, validActions[0].action];
  197. setNotificationActions(updatedActions);
  198. updateAlertCount(parseInt(project.id, 10), updatedActions.length);
  199. },
  200. });
  201. });
  202. return menuItems;
  203. };
  204. const addAlertButton = (
  205. <Tooltip
  206. disabled={!disabled}
  207. title={t('You do not have permission to add notification actions for this project')}
  208. >
  209. <DropdownMenu
  210. items={getMenuItems()}
  211. trigger={(triggerProps, isOpen) => (
  212. <DropdownButton
  213. {...triggerProps}
  214. isOpen={isOpen}
  215. aria-label={t('Add Action')}
  216. size="xs"
  217. icon={<IconAdd isCircled color="gray300" />}
  218. disabled={disabled}
  219. >
  220. {t('Add Action')}
  221. </DropdownButton>
  222. )}
  223. isDisabled={disabled}
  224. data-test-id="add-action-button"
  225. />
  226. </Tooltip>
  227. );
  228. return (
  229. <Fragment>
  230. {renderNotificationActions()}
  231. {addAlertButton}
  232. </Fragment>
  233. );
  234. }
  235. export default NotificationActionManager;