ignore.tsx 8.1 KB

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