resolve.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import {Component} from 'react';
  2. import styled from '@emotion/styled';
  3. import {openModal} from 'sentry/actionCreators/modal';
  4. import Button from 'sentry/components/button';
  5. import ButtonBar from 'sentry/components/buttonBar';
  6. import {openConfirmModal} from 'sentry/components/confirm';
  7. import CustomCommitsResolutionModal from 'sentry/components/customCommitsResolutionModal';
  8. import CustomResolutionModal from 'sentry/components/customResolutionModal';
  9. import DropdownMenuControl from 'sentry/components/dropdownMenuControl';
  10. import Tooltip from 'sentry/components/tooltip';
  11. import {IconCheckmark, IconChevron} from 'sentry/icons';
  12. import {t} from 'sentry/locale';
  13. import {
  14. GroupStatusResolution,
  15. Organization,
  16. Release,
  17. ResolutionStatus,
  18. ResolutionStatusDetails,
  19. } from 'sentry/types';
  20. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  21. import {formatVersion} from 'sentry/utils/formatters';
  22. import withOrganization from 'sentry/utils/withOrganization';
  23. const defaultProps = {
  24. isResolved: false,
  25. isAutoResolved: false,
  26. confirmLabel: t('Resolve'),
  27. };
  28. type Props = {
  29. hasRelease: boolean;
  30. onUpdate: (data: GroupStatusResolution) => void;
  31. orgSlug: string;
  32. organization: Organization;
  33. confirmMessage?: React.ReactNode;
  34. disableDropdown?: boolean;
  35. disabled?: boolean;
  36. latestRelease?: Release;
  37. projectFetchError?: boolean;
  38. projectSlug?: string;
  39. shouldConfirm?: boolean;
  40. } & Partial<typeof defaultProps>;
  41. class ResolveActions extends Component<Props> {
  42. static defaultProps = defaultProps;
  43. handleCommitResolution(statusDetails: ResolutionStatusDetails) {
  44. const {onUpdate} = this.props;
  45. onUpdate({
  46. status: ResolutionStatus.RESOLVED,
  47. statusDetails,
  48. });
  49. }
  50. handleAnotherExistingReleaseResolution(statusDetails: ResolutionStatusDetails) {
  51. const {organization, onUpdate} = this.props;
  52. onUpdate({
  53. status: ResolutionStatus.RESOLVED,
  54. statusDetails,
  55. });
  56. trackAdvancedAnalyticsEvent('resolve_issue', {
  57. organization,
  58. release: 'anotherExisting',
  59. });
  60. }
  61. handleCurrentReleaseResolution = () => {
  62. const {onUpdate, organization, hasRelease, latestRelease} = this.props;
  63. hasRelease &&
  64. onUpdate({
  65. status: ResolutionStatus.RESOLVED,
  66. statusDetails: {
  67. inRelease: latestRelease ? latestRelease.version : 'latest',
  68. },
  69. });
  70. trackAdvancedAnalyticsEvent('resolve_issue', {
  71. organization,
  72. release: 'current',
  73. });
  74. };
  75. handleNextReleaseResolution = () => {
  76. const {onUpdate, organization, hasRelease} = this.props;
  77. hasRelease &&
  78. onUpdate({
  79. status: ResolutionStatus.RESOLVED,
  80. statusDetails: {
  81. inNextRelease: true,
  82. },
  83. });
  84. trackAdvancedAnalyticsEvent('resolve_issue', {
  85. organization,
  86. release: 'next',
  87. });
  88. };
  89. renderResolved() {
  90. const {isAutoResolved, onUpdate} = this.props;
  91. return (
  92. <Tooltip
  93. title={
  94. isAutoResolved
  95. ? t(
  96. 'This event is resolved due to the Auto Resolve configuration for this project'
  97. )
  98. : t('Unresolve')
  99. }
  100. >
  101. <Button
  102. priority="primary"
  103. size="xs"
  104. icon={<IconCheckmark size="xs" />}
  105. aria-label={t('Unresolve')}
  106. disabled={isAutoResolved}
  107. onClick={() =>
  108. onUpdate({status: ResolutionStatus.UNRESOLVED, statusDetails: {}})
  109. }
  110. />
  111. </Tooltip>
  112. );
  113. }
  114. renderDropdownMenu() {
  115. const {
  116. projectSlug,
  117. isResolved,
  118. hasRelease,
  119. latestRelease,
  120. confirmMessage,
  121. shouldConfirm,
  122. disabled,
  123. confirmLabel,
  124. disableDropdown,
  125. } = this.props;
  126. if (isResolved) {
  127. return this.renderResolved();
  128. }
  129. const actionTitle = !hasRelease
  130. ? t('Set up release tracking in order to use this feature.')
  131. : '';
  132. const onActionOrConfirm = onAction => {
  133. openConfirmModal({
  134. bypass: !shouldConfirm,
  135. onConfirm: onAction,
  136. message: confirmMessage,
  137. confirmText: confirmLabel,
  138. });
  139. };
  140. const items = [
  141. {
  142. key: 'next-release',
  143. label: t('The next release'),
  144. details: actionTitle,
  145. onAction: () => onActionOrConfirm(this.handleNextReleaseResolution),
  146. showDividers: !actionTitle,
  147. },
  148. {
  149. key: 'current-release',
  150. label: latestRelease
  151. ? t('The current release (%s)', formatVersion(latestRelease.version))
  152. : t('The current release'),
  153. details: actionTitle,
  154. onAction: () => onActionOrConfirm(this.handleCurrentReleaseResolution),
  155. showDividers: !actionTitle,
  156. },
  157. {
  158. key: 'another-release',
  159. label: t('Another existing release\u2026'),
  160. onAction: () => this.openCustomReleaseModal(),
  161. showDividers: !actionTitle,
  162. },
  163. {
  164. key: 'a-commit',
  165. label: t('A commit\u2026'),
  166. onAction: () => this.openCustomCommitModal(),
  167. showDividers: !actionTitle,
  168. },
  169. ];
  170. const isDisabled = !projectSlug ? disabled : disableDropdown;
  171. return (
  172. <DropdownMenuControl
  173. size="sm"
  174. items={items}
  175. trigger={({props: triggerProps, ref: triggerRef}) => (
  176. <DropdownTrigger
  177. ref={triggerRef}
  178. {...triggerProps}
  179. aria-label={t('More resolve options')}
  180. size="xs"
  181. icon={<IconChevron direction="down" size="xs" />}
  182. disabled={isDisabled}
  183. />
  184. )}
  185. disabledKeys={
  186. disabled || !hasRelease
  187. ? ['next-release', 'current-release', 'another-release']
  188. : []
  189. }
  190. menuTitle={t('Resolved In')}
  191. isDisabled={isDisabled}
  192. />
  193. );
  194. }
  195. openCustomCommitModal() {
  196. const {orgSlug, projectSlug} = this.props;
  197. openModal(deps => (
  198. <CustomCommitsResolutionModal
  199. {...deps}
  200. onSelected={(statusDetails: ResolutionStatusDetails) =>
  201. this.handleCommitResolution(statusDetails)
  202. }
  203. orgSlug={orgSlug}
  204. projectSlug={projectSlug}
  205. />
  206. ));
  207. }
  208. openCustomReleaseModal() {
  209. const {orgSlug, projectSlug} = this.props;
  210. openModal(deps => (
  211. <CustomResolutionModal
  212. {...deps}
  213. onSelected={(statusDetails: ResolutionStatusDetails) =>
  214. this.handleAnotherExistingReleaseResolution(statusDetails)
  215. }
  216. orgSlug={orgSlug}
  217. projectSlug={projectSlug}
  218. />
  219. ));
  220. }
  221. render() {
  222. const {
  223. isResolved,
  224. onUpdate,
  225. confirmMessage,
  226. shouldConfirm,
  227. disabled,
  228. confirmLabel,
  229. projectFetchError,
  230. } = this.props;
  231. if (isResolved) {
  232. return this.renderResolved();
  233. }
  234. const onResolve = () =>
  235. openConfirmModal({
  236. bypass: !shouldConfirm,
  237. onConfirm: () => onUpdate({status: ResolutionStatus.RESOLVED, statusDetails: {}}),
  238. message: confirmMessage,
  239. confirmText: confirmLabel,
  240. });
  241. return (
  242. <Tooltip disabled={!projectFetchError} title={t('Error fetching project')}>
  243. <ButtonBar merged>
  244. <ResolveButton
  245. size="xs"
  246. title={t(
  247. 'Resolves the issue. The issue will get unresolved if it happens again.'
  248. )}
  249. tooltipProps={{delay: 300, disabled}}
  250. icon={<IconCheckmark size="xs" />}
  251. onClick={onResolve}
  252. disabled={disabled}
  253. >
  254. {t('Resolve')}
  255. </ResolveButton>
  256. {this.renderDropdownMenu()}
  257. </ButtonBar>
  258. </Tooltip>
  259. );
  260. }
  261. }
  262. export default withOrganization(ResolveActions);
  263. const ResolveButton = styled(Button)`
  264. box-shadow: none;
  265. border-radius: ${p => p.theme.borderRadiusLeft};
  266. `;
  267. const DropdownTrigger = styled(Button)`
  268. box-shadow: none;
  269. border-radius: ${p => p.theme.borderRadiusRight};
  270. border-left: none;
  271. `;