ignore.tsx 8.2 KB

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