ignore.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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. ResolutionStatus,
  15. ResolutionStatusDetails,
  16. SelectValue,
  17. UpdateResolutionStatus,
  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: UpdateResolutionStatus) => 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) => {
  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={() => onUpdate({status: ResolutionStatus.UNRESOLVED})}
  64. aria-label={t('Unignore')}
  65. icon={<IconMute size="xs" />}
  66. />
  67. </Tooltip>
  68. );
  69. }
  70. const openCustomIgnoreDuration = () =>
  71. openModal(deps => (
  72. <CustomIgnoreDurationModal
  73. {...deps}
  74. onSelected={details => onCustomIgnore(details)}
  75. />
  76. ));
  77. const openCustomIgnoreCount = () =>
  78. openModal(deps => (
  79. <CustomIgnoreCountModal
  80. {...deps}
  81. onSelected={details => onCustomIgnore(details)}
  82. label={t('Ignore this issue until it occurs again\u2026')}
  83. countLabel={t('Number of times')}
  84. countName="ignoreCount"
  85. windowName="ignoreWindow"
  86. windowOptions={IGNORE_WINDOWS}
  87. />
  88. ));
  89. const openCustomIgnoreUserCount = () =>
  90. openModal(deps => (
  91. <CustomIgnoreCountModal
  92. {...deps}
  93. onSelected={details => onCustomIgnore(details)}
  94. label={t('Ignore this issue until it affects an additional\u2026')}
  95. countLabel={t('Number of users')}
  96. countName="ignoreUserCount"
  97. windowName="ignoreUserWindow"
  98. windowOptions={IGNORE_WINDOWS}
  99. />
  100. ));
  101. const dropdownItems = [
  102. {
  103. key: 'for',
  104. label: t('For\u2026'),
  105. isSubmenu: true,
  106. children: [
  107. ...IGNORE_DURATIONS.map(duration => ({
  108. key: `for-${duration}`,
  109. label: <Duration seconds={duration * 60} />,
  110. onAction: () => onIgnore({ignoreDuration: duration}),
  111. })),
  112. {
  113. key: 'for-custom',
  114. label: t('Custom'),
  115. onAction: () => openCustomIgnoreDuration(),
  116. },
  117. ],
  118. },
  119. {
  120. key: 'until-reoccur',
  121. label: t('Until this occurs again\u2026'),
  122. isSubmenu: true,
  123. children: [
  124. ...IGNORE_COUNTS.map(count => ({
  125. key: `until-reoccur-${count}-times`,
  126. label:
  127. count === 1
  128. ? t('one time\u2026') // This is intentional as unbalanced string formatters are problematic
  129. : tn('%s time\u2026', '%s times\u2026', count),
  130. isSubmenu: true,
  131. children: [
  132. {
  133. key: `until-reoccur-${count}-times-from-now`,
  134. label: t('from now'),
  135. onAction: () => onIgnore({ignoreCount: count}),
  136. },
  137. ...IGNORE_WINDOWS.map(({value, label}) => ({
  138. key: `until-reoccur-${count}-times-from-${label}`,
  139. label,
  140. onAction: () =>
  141. onIgnore({
  142. ignoreCount: count,
  143. ignoreWindow: value,
  144. }),
  145. })),
  146. ],
  147. })),
  148. {
  149. key: 'until-reoccur-custom',
  150. label: t('Custom'),
  151. onAction: () => openCustomIgnoreCount(),
  152. },
  153. ],
  154. },
  155. {
  156. key: 'until-affect',
  157. label: t('Until this affects an additional\u2026'),
  158. isSubmenu: true,
  159. children: [
  160. ...IGNORE_COUNTS.map(count => ({
  161. key: `until-affect-${count}-users`,
  162. label:
  163. count === 1
  164. ? t('one user\u2026') // This is intentional as unbalanced string formatters are problematic
  165. : tn('%s user\u2026', '%s users\u2026', count),
  166. isSubmenu: true,
  167. children: [
  168. {
  169. key: `until-affect-${count}-users-from-now`,
  170. label: t('from now'),
  171. onAction: () => onIgnore({ignoreUserCount: count}),
  172. },
  173. ...IGNORE_WINDOWS.map(({value, label}) => ({
  174. key: `until-affect-${count}-users-from-${label}`,
  175. label,
  176. onAction: () =>
  177. onIgnore({
  178. ignoreUserCount: count,
  179. ignoreUserWindow: value,
  180. }),
  181. })),
  182. ],
  183. })),
  184. {
  185. key: 'until-affect-custom',
  186. label: t('Custom'),
  187. onAction: () => openCustomIgnoreUserCount(),
  188. },
  189. ],
  190. },
  191. ];
  192. return (
  193. <ButtonBar merged>
  194. <IgnoreButton
  195. size="xs"
  196. tooltipProps={{delay: 300, disabled}}
  197. title={t(
  198. 'Silences alerts for this issue and removes it from the issue stream by default.'
  199. )}
  200. icon={<IconMute size="xs" />}
  201. onClick={() => onIgnore()}
  202. disabled={disabled}
  203. >
  204. {t('Ignore')}
  205. </IgnoreButton>
  206. <DropdownMenuControl
  207. size="sm"
  208. trigger={({props: triggerProps, ref: triggerRef}) => (
  209. <DropdownTrigger
  210. ref={triggerRef}
  211. {...triggerProps}
  212. aria-label={t('Ignore options')}
  213. size="xs"
  214. icon={<IconChevron direction="down" size="xs" />}
  215. disabled={disabled}
  216. />
  217. )}
  218. menuTitle={t('Ignore')}
  219. items={dropdownItems}
  220. isDisabled={disabled}
  221. />
  222. </ButtonBar>
  223. );
  224. };
  225. export default IgnoreActions;
  226. const IgnoreButton = styled(Button)`
  227. box-shadow: none;
  228. border-radius: ${p => p.theme.borderRadiusLeft};
  229. `;
  230. const DropdownTrigger = styled(Button)`
  231. box-shadow: none;
  232. border-radius: ${p => p.theme.borderRadiusRight};
  233. border-left: none;
  234. `;