ignore.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. import * as React from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {openModal} from 'app/actionCreators/modal';
  5. import ActionLink from 'app/components/actions/actionLink';
  6. import ButtonBar from 'app/components/buttonBar';
  7. import CustomIgnoreCountModal from 'app/components/customIgnoreCountModal';
  8. import CustomIgnoreDurationModal from 'app/components/customIgnoreDurationModal';
  9. import DropdownLink from 'app/components/dropdownLink';
  10. import Duration from 'app/components/duration';
  11. import Tooltip from 'app/components/tooltip';
  12. import {IconChevron, IconMute} from 'app/icons';
  13. import {t, tn} from 'app/locale';
  14. import space from 'app/styles/space';
  15. import {
  16. ResolutionStatus,
  17. ResolutionStatusDetails,
  18. UpdateResolutionStatus,
  19. } from 'app/types';
  20. import ActionButton from './button';
  21. import MenuHeader from './menuHeader';
  22. const IGNORE_DURATIONS = [30, 120, 360, 60 * 24, 60 * 24 * 7];
  23. const IGNORE_COUNTS = [1, 10, 100, 1000, 10000, 100000];
  24. const IGNORE_WINDOWS: [number, string][] = [
  25. [60, t('per hour')],
  26. [24 * 60, t('per day')],
  27. [24 * 7 * 60, t('per week')],
  28. ];
  29. type Props = {
  30. onUpdate: (params: UpdateResolutionStatus) => void;
  31. disabled?: boolean;
  32. shouldConfirm?: boolean;
  33. confirmMessage?: React.ReactNode;
  34. confirmLabel?: string;
  35. isIgnored?: boolean;
  36. };
  37. const IgnoreActions = ({
  38. onUpdate,
  39. disabled,
  40. shouldConfirm,
  41. confirmMessage,
  42. confirmLabel = t('Ignore'),
  43. isIgnored = false,
  44. }: Props) => {
  45. const onIgnore = (statusDetails: ResolutionStatusDetails) => {
  46. return onUpdate({
  47. status: ResolutionStatus.IGNORED,
  48. statusDetails: statusDetails || {},
  49. });
  50. };
  51. const onCustomIgnore = (statusDetails: ResolutionStatusDetails) => {
  52. onIgnore(statusDetails);
  53. };
  54. const actionLinkProps = {
  55. shouldConfirm,
  56. title: t('Ignore'),
  57. message: confirmMessage,
  58. confirmLabel,
  59. disabled,
  60. };
  61. if (isIgnored) {
  62. return (
  63. <Tooltip title={t('Change status to unresolved')}>
  64. <ActionButton
  65. priority="primary"
  66. onClick={() => onUpdate({status: ResolutionStatus.UNRESOLVED})}
  67. label={t('Unignore')}
  68. icon={<IconMute size="xs" />}
  69. />
  70. </Tooltip>
  71. );
  72. }
  73. const openCustomIgnoreDuration = () =>
  74. openModal(deps => (
  75. <CustomIgnoreDurationModal
  76. {...deps}
  77. onSelected={details => onCustomIgnore(details)}
  78. />
  79. ));
  80. const openCustomIngoreCount = () =>
  81. openModal(deps => (
  82. <CustomIgnoreCountModal
  83. {...deps}
  84. onSelected={details => onCustomIgnore(details)}
  85. label={t('Ignore this issue until it occurs again\u2026')}
  86. countLabel={t('Number of times')}
  87. countName="ignoreCount"
  88. windowName="ignoreWindow"
  89. windowChoices={IGNORE_WINDOWS}
  90. />
  91. ));
  92. const openCustomIgnoreUserCount = () =>
  93. openModal(deps => (
  94. <CustomIgnoreCountModal
  95. {...deps}
  96. onSelected={details => onCustomIgnore(details)}
  97. label={t('Ignore this issue until it affects an additional\u2026')}
  98. countLabel={t('Number of users')}
  99. countName="ignoreUserCount"
  100. windowName="ignoreUserWindow"
  101. windowChoices={IGNORE_WINDOWS}
  102. />
  103. ));
  104. return (
  105. <ButtonBar merged>
  106. <ActionLink
  107. {...actionLinkProps}
  108. type="button"
  109. title={t('Ignore')}
  110. onAction={() => onUpdate({status: ResolutionStatus.IGNORED})}
  111. icon={<IconMute size="xs" />}
  112. >
  113. {t('Ignore')}
  114. </ActionLink>
  115. <StyledDropdownLink
  116. customTitle={
  117. <ActionButton
  118. disabled={disabled}
  119. icon={<IconChevron direction="down" size="xs" />}
  120. />
  121. }
  122. alwaysRenderMenu
  123. disabled={disabled}
  124. >
  125. <MenuHeader>{t('Ignore')}</MenuHeader>
  126. <DropdownMenuItem>
  127. <DropdownLink
  128. title={
  129. <ActionSubMenu>
  130. {t('For\u2026')}
  131. <SubMenuChevron>
  132. <IconChevron direction="right" size="xs" />
  133. </SubMenuChevron>
  134. </ActionSubMenu>
  135. }
  136. caret={false}
  137. isNestedDropdown
  138. alwaysRenderMenu
  139. >
  140. {IGNORE_DURATIONS.map(duration => (
  141. <DropdownMenuItem key={duration}>
  142. <StyledForActionLink
  143. {...actionLinkProps}
  144. onAction={() => onIgnore({ignoreDuration: duration})}
  145. >
  146. <ActionSubMenu>
  147. <Duration seconds={duration * 60} />
  148. </ActionSubMenu>
  149. </StyledForActionLink>
  150. </DropdownMenuItem>
  151. ))}
  152. <DropdownMenuItem>
  153. <ActionSubMenu>
  154. <a onClick={openCustomIgnoreDuration}>{t('Custom')}</a>
  155. </ActionSubMenu>
  156. </DropdownMenuItem>
  157. </DropdownLink>
  158. </DropdownMenuItem>
  159. <DropdownMenuItem>
  160. <DropdownLink
  161. title={
  162. <ActionSubMenu>
  163. {t('Until this occurs again\u2026')}
  164. <SubMenuChevron>
  165. <IconChevron direction="right" size="xs" />
  166. </SubMenuChevron>
  167. </ActionSubMenu>
  168. }
  169. caret={false}
  170. isNestedDropdown
  171. alwaysRenderMenu
  172. >
  173. {IGNORE_COUNTS.map(count => (
  174. <DropdownMenuItem key={count}>
  175. <DropdownLink
  176. title={
  177. <ActionSubMenu>
  178. {count === 1
  179. ? t('one time\u2026') // This is intentional as unbalanced string formatters are problematic
  180. : tn('%s time\u2026', '%s times\u2026', count)}
  181. <SubMenuChevron>
  182. <IconChevron direction="right" size="xs" />
  183. </SubMenuChevron>
  184. </ActionSubMenu>
  185. }
  186. caret={false}
  187. isNestedDropdown
  188. alwaysRenderMenu
  189. >
  190. <DropdownMenuItem>
  191. <StyledActionLink
  192. {...actionLinkProps}
  193. onAction={() => onIgnore({ignoreCount: count})}
  194. >
  195. {t('from now')}
  196. </StyledActionLink>
  197. </DropdownMenuItem>
  198. {IGNORE_WINDOWS.map(([hours, label]) => (
  199. <DropdownMenuItem key={hours}>
  200. <StyledActionLink
  201. {...actionLinkProps}
  202. onAction={() =>
  203. onIgnore({
  204. ignoreCount: count,
  205. ignoreWindow: hours,
  206. })
  207. }
  208. >
  209. {label}
  210. </StyledActionLink>
  211. </DropdownMenuItem>
  212. ))}
  213. </DropdownLink>
  214. </DropdownMenuItem>
  215. ))}
  216. <DropdownMenuItem>
  217. <ActionSubMenu>
  218. <a onClick={openCustomIngoreCount}>{t('Custom')}</a>
  219. </ActionSubMenu>
  220. </DropdownMenuItem>
  221. </DropdownLink>
  222. </DropdownMenuItem>
  223. <DropdownMenuItem>
  224. <DropdownLink
  225. title={
  226. <ActionSubMenu>
  227. {t('Until this affects an additional\u2026')}
  228. <SubMenuChevron>
  229. <IconChevron direction="right" size="xs" />
  230. </SubMenuChevron>
  231. </ActionSubMenu>
  232. }
  233. caret={false}
  234. isNestedDropdown
  235. alwaysRenderMenu
  236. >
  237. {IGNORE_COUNTS.map(count => (
  238. <DropdownMenuItem key={count}>
  239. <DropdownLink
  240. title={
  241. <ActionSubMenu>
  242. {tn('one user\u2026', '%s users\u2026', count)}
  243. <SubMenuChevron>
  244. <IconChevron direction="right" size="xs" />
  245. </SubMenuChevron>
  246. </ActionSubMenu>
  247. }
  248. caret={false}
  249. isNestedDropdown
  250. alwaysRenderMenu
  251. >
  252. <DropdownMenuItem>
  253. <StyledActionLink
  254. {...actionLinkProps}
  255. onAction={() => onIgnore({ignoreUserCount: count})}
  256. >
  257. {t('from now')}
  258. </StyledActionLink>
  259. </DropdownMenuItem>
  260. {IGNORE_WINDOWS.map(([hours, label]) => (
  261. <DropdownMenuItem key={hours}>
  262. <StyledActionLink
  263. {...actionLinkProps}
  264. onAction={() =>
  265. onIgnore({
  266. ignoreUserCount: count,
  267. ignoreUserWindow: hours,
  268. })
  269. }
  270. >
  271. {label}
  272. </StyledActionLink>
  273. </DropdownMenuItem>
  274. ))}
  275. </DropdownLink>
  276. </DropdownMenuItem>
  277. ))}
  278. <DropdownMenuItem>
  279. <ActionSubMenu>
  280. <a onClick={openCustomIgnoreUserCount}>{t('Custom')}</a>
  281. </ActionSubMenu>
  282. </DropdownMenuItem>
  283. </DropdownLink>
  284. </DropdownMenuItem>
  285. </StyledDropdownLink>
  286. </ButtonBar>
  287. );
  288. };
  289. export default IgnoreActions;
  290. const actionLinkCss = p => css`
  291. color: ${p.theme.subText};
  292. &:hover {
  293. border-radius: ${p.theme.borderRadius};
  294. background: ${p.theme.bodyBackground} !important;
  295. }
  296. `;
  297. const StyledActionLink = styled(ActionLink)`
  298. padding: 7px 10px !important;
  299. ${actionLinkCss};
  300. `;
  301. const StyledForActionLink = styled(ActionLink)`
  302. padding: ${space(0.5)} 0;
  303. ${actionLinkCss};
  304. `;
  305. const StyledDropdownLink = styled(DropdownLink)`
  306. transition: none;
  307. border-top-left-radius: 0 !important;
  308. border-bottom-left-radius: 0 !important;
  309. `;
  310. const DropdownMenuItem = styled('li')`
  311. :not(:last-child) {
  312. border-bottom: 1px solid ${p => p.theme.innerBorder};
  313. }
  314. > span {
  315. display: block;
  316. > ul {
  317. border-radius: ${p => p.theme.borderRadius};
  318. top: 5px;
  319. left: 100%;
  320. margin-top: -5px;
  321. margin-left: -1px;
  322. &:after,
  323. &:before {
  324. display: none !important;
  325. }
  326. }
  327. }
  328. &:hover > span {
  329. background: ${p => p.theme.focus};
  330. }
  331. `;
  332. const ActionSubMenu = styled('span')`
  333. display: grid;
  334. grid-template-columns: 200px 1fr;
  335. grid-column-start: 1;
  336. grid-column-end: 4;
  337. gap: ${space(1)};
  338. padding: ${space(0.5)} 0;
  339. color: ${p => p.theme.textColor};
  340. a {
  341. color: ${p => p.theme.textColor};
  342. }
  343. `;
  344. const SubMenuChevron = styled('span')`
  345. display: grid;
  346. align-self: center;
  347. color: ${p => p.theme.gray300};
  348. transition: 0.1s color linear;
  349. &:hover,
  350. &:active {
  351. color: ${p => p.theme.subText};
  352. }
  353. `;