ignore.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. import styled from '@emotion/styled';
  2. import {openModal} from 'sentry/actionCreators/modal';
  3. import Button from 'sentry/components/button';
  4. import ButtonBar from 'sentry/components/buttonBar';
  5. import {openConfirmModal} from 'sentry/components/confirm';
  6. import CustomIgnoreCountModal from 'sentry/components/customIgnoreCountModal';
  7. import CustomIgnoreDurationModal from 'sentry/components/customIgnoreDurationModal';
  8. import DropdownMenuControl from 'sentry/components/dropdownMenuControl';
  9. import Duration from 'sentry/components/duration';
  10. import Tooltip from 'sentry/components/tooltip';
  11. import {IconChevron, IconMute} from 'sentry/icons';
  12. import {t, tn} from 'sentry/locale';
  13. import {
  14. GroupStatusResolution,
  15. ResolutionStatus,
  16. ResolutionStatusDetails,
  17. SelectValue,
  18. } from 'sentry/types';
  19. const IGNORE_DURATIONS = [30, 120, 360, 60 * 24, 60 * 24 * 7];
  20. const IGNORE_COUNTS = [1, 10, 100, 1000, 10000, 100000];
  21. const IGNORE_WINDOWS: SelectValue<number>[] = [
  22. {value: 60, label: t('per hour')},
  23. {value: 24 * 60, label: t('per day')},
  24. {value: 24 * 7 * 60, label: t('per week')},
  25. ];
  26. type Props = {
  27. onUpdate: (params: GroupStatusResolution) => void;
  28. confirmLabel?: string;
  29. confirmMessage?: React.ReactNode;
  30. disabled?: boolean;
  31. isIgnored?: boolean;
  32. shouldConfirm?: boolean;
  33. };
  34. const IgnoreActions = ({
  35. onUpdate,
  36. disabled,
  37. shouldConfirm,
  38. confirmMessage,
  39. confirmLabel = t('Ignore'),
  40. isIgnored = false,
  41. }: Props) => {
  42. const onIgnore = (statusDetails: ResolutionStatusDetails | undefined = {}) => {
  43. openConfirmModal({
  44. bypass: !shouldConfirm,
  45. onConfirm: () =>
  46. onUpdate({
  47. status: ResolutionStatus.IGNORED,
  48. statusDetails,
  49. }),
  50. message: confirmMessage,
  51. confirmText: confirmLabel,
  52. });
  53. };
  54. const onCustomIgnore = (statusDetails: ResolutionStatusDetails) => {
  55. onIgnore(statusDetails);
  56. };
  57. if (isIgnored) {
  58. return (
  59. <Tooltip title={t('Change status to unresolved')}>
  60. <Button
  61. priority="primary"
  62. size="xs"
  63. onClick={() =>
  64. onUpdate({status: ResolutionStatus.UNRESOLVED, statusDetails: {}})
  65. }
  66. aria-label={t('Unignore')}
  67. icon={<IconMute size="xs" />}
  68. />
  69. </Tooltip>
  70. );
  71. }
  72. const openCustomIgnoreDuration = () =>
  73. openModal(deps => (
  74. <CustomIgnoreDurationModal
  75. {...deps}
  76. onSelected={details => onCustomIgnore(details)}
  77. />
  78. ));
  79. const openCustomIgnoreCount = () =>
  80. openModal(deps => (
  81. <CustomIgnoreCountModal
  82. {...deps}
  83. onSelected={details => onCustomIgnore(details)}
  84. label={t('Ignore this issue until it occurs again\u2026')}
  85. countLabel={t('Number of times')}
  86. countName="ignoreCount"
  87. windowName="ignoreWindow"
  88. windowOptions={IGNORE_WINDOWS}
  89. />
  90. ));
  91. const openCustomIgnoreUserCount = () =>
  92. openModal(deps => (
  93. <CustomIgnoreCountModal
  94. {...deps}
  95. onSelected={details => onCustomIgnore(details)}
  96. label={t('Ignore this issue until it affects an additional\u2026')}
  97. countLabel={t('Number of users')}
  98. countName="ignoreUserCount"
  99. windowName="ignoreUserWindow"
  100. windowOptions={IGNORE_WINDOWS}
  101. />
  102. ));
  103. const dropdownItems = [
  104. {
  105. key: 'for',
  106. label: t('For\u2026'),
  107. isSubmenu: true,
  108. children: [
  109. ...IGNORE_DURATIONS.map(duration => ({
  110. key: `for-${duration}`,
  111. label: <Duration seconds={duration * 60} />,
  112. onAction: () => onIgnore({ignoreDuration: duration}),
  113. })),
  114. {
  115. key: 'for-custom',
  116. label: t('Custom'),
  117. onAction: () => openCustomIgnoreDuration(),
  118. },
  119. ],
  120. },
  121. {
  122. key: 'until-reoccur',
  123. label: t('Until this occurs again\u2026'),
  124. isSubmenu: true,
  125. children: [
  126. ...IGNORE_COUNTS.map(count => ({
  127. key: `until-reoccur-${count}-times`,
  128. label:
  129. count === 1
  130. ? t('one time\u2026') // This is intentional as unbalanced string formatters are problematic
  131. : tn('%s time\u2026', '%s times\u2026', count),
  132. isSubmenu: true,
  133. children: [
  134. {
  135. key: `until-reoccur-${count}-times-from-now`,
  136. label: t('from now'),
  137. onAction: () => onIgnore({ignoreCount: count}),
  138. },
  139. ...IGNORE_WINDOWS.map(({value, label}) => ({
  140. key: `until-reoccur-${count}-times-from-${label}`,
  141. label,
  142. onAction: () =>
  143. onIgnore({
  144. ignoreCount: count,
  145. ignoreWindow: value,
  146. }),
  147. })),
  148. ],
  149. })),
  150. {
  151. key: 'until-reoccur-custom',
  152. label: t('Custom'),
  153. onAction: () => openCustomIgnoreCount(),
  154. },
  155. ],
  156. },
  157. {
  158. key: 'until-affect',
  159. label: t('Until this affects an additional\u2026'),
  160. isSubmenu: true,
  161. children: [
  162. ...IGNORE_COUNTS.map(count => ({
  163. key: `until-affect-${count}-users`,
  164. label:
  165. count === 1
  166. ? t('one user\u2026') // This is intentional as unbalanced string formatters are problematic
  167. : tn('%s user\u2026', '%s users\u2026', count),
  168. isSubmenu: true,
  169. children: [
  170. {
  171. key: `until-affect-${count}-users-from-now`,
  172. label: t('from now'),
  173. onAction: () => onIgnore({ignoreUserCount: count}),
  174. },
  175. ...IGNORE_WINDOWS.map(({value, label}) => ({
  176. key: `until-affect-${count}-users-from-${label}`,
  177. label,
  178. onAction: () =>
  179. onIgnore({
  180. ignoreUserCount: count,
  181. ignoreUserWindow: value,
  182. }),
  183. })),
  184. ],
  185. })),
  186. {
  187. key: 'until-affect-custom',
  188. label: t('Custom'),
  189. onAction: () => openCustomIgnoreUserCount(),
  190. },
  191. ],
  192. },
  193. ];
  194. return (
  195. <ButtonBar merged>
  196. <IgnoreButton
  197. size="xs"
  198. tooltipProps={{delay: 300, disabled}}
  199. title={t(
  200. 'Silences alerts for this issue and removes it from the issue stream by default.'
  201. )}
  202. icon={<IconMute size="xs" />}
  203. onClick={() => onIgnore()}
  204. disabled={disabled}
  205. >
  206. {t('Ignore')}
  207. </IgnoreButton>
  208. <DropdownMenuControl
  209. size="sm"
  210. trigger={({props: triggerProps, ref: triggerRef}) => (
  211. <DropdownTrigger
  212. ref={triggerRef}
  213. {...triggerProps}
  214. aria-label={t('Ignore options')}
  215. size="xs"
  216. icon={<IconChevron direction="down" size="xs" />}
  217. disabled={disabled}
  218. />
  219. )}
  220. menuTitle={t('Ignore')}
  221. items={dropdownItems}
  222. isDisabled={disabled}
  223. />
  224. </ButtonBar>
  225. );
  226. };
  227. export default IgnoreActions;
  228. const IgnoreButton = styled(Button)`
  229. box-shadow: none;
  230. border-radius: ${p => p.theme.borderRadiusLeft};
  231. `;
  232. const DropdownTrigger = styled(Button)`
  233. box-shadow: none;
  234. border-radius: ${p => p.theme.borderRadiusRight};
  235. border-left: none;
  236. `;