resolve.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  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. Organization,
  15. Release,
  16. ResolutionStatus,
  17. ResolutionStatusDetails,
  18. UpdateResolutionStatus,
  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: UpdateResolutionStatus) => 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={() => onUpdate({status: ResolutionStatus.UNRESOLVED})}
  108. />
  109. </Tooltip>
  110. );
  111. }
  112. renderDropdownMenu() {
  113. const {
  114. projectSlug,
  115. isResolved,
  116. hasRelease,
  117. latestRelease,
  118. confirmMessage,
  119. shouldConfirm,
  120. disabled,
  121. confirmLabel,
  122. disableDropdown,
  123. } = this.props;
  124. if (isResolved) {
  125. return this.renderResolved();
  126. }
  127. const actionTitle = !hasRelease
  128. ? t('Set up release tracking in order to use this feature.')
  129. : '';
  130. const onActionOrConfirm = onAction => {
  131. openConfirmModal({
  132. bypass: !shouldConfirm,
  133. onConfirm: onAction,
  134. message: confirmMessage,
  135. confirmText: confirmLabel,
  136. });
  137. };
  138. const items = [
  139. {
  140. key: 'next-release',
  141. label: t('The next release'),
  142. details: actionTitle,
  143. onAction: () => onActionOrConfirm(this.handleNextReleaseResolution),
  144. showDividers: !hasRelease,
  145. },
  146. {
  147. key: 'current-release',
  148. label: latestRelease
  149. ? t('The current release (%s)', formatVersion(latestRelease.version))
  150. : t('The current release'),
  151. details: actionTitle,
  152. onAction: () => onActionOrConfirm(this.handleCurrentReleaseResolution),
  153. showDividers: !hasRelease,
  154. },
  155. {
  156. key: 'another-release',
  157. label: t('Another existing release\u2026'),
  158. onAction: () => this.openCustomReleaseModal(),
  159. },
  160. {
  161. key: 'a-commit',
  162. label: t('A commit\u2026'),
  163. onAction: () => this.openCustomCommitModal(),
  164. },
  165. ];
  166. const isDisabled = !projectSlug ? disabled : disableDropdown;
  167. return (
  168. <DropdownMenuControl
  169. items={items}
  170. trigger={({props: triggerProps, ref: triggerRef}) => (
  171. <DropdownTrigger
  172. ref={triggerRef}
  173. {...triggerProps}
  174. aria-label={t('More resolve options')}
  175. size="xs"
  176. icon={<IconChevron direction="down" size="xs" />}
  177. disabled={isDisabled}
  178. />
  179. )}
  180. disabledKeys={
  181. disabled || !hasRelease
  182. ? ['next-release', 'current-release', 'another-release']
  183. : []
  184. }
  185. menuTitle={t('Resolved In')}
  186. isDisabled={isDisabled}
  187. />
  188. );
  189. }
  190. openCustomCommitModal() {
  191. const {orgSlug, projectSlug} = this.props;
  192. openModal(deps => (
  193. <CustomCommitsResolutionModal
  194. {...deps}
  195. onSelected={(statusDetails: ResolutionStatusDetails) =>
  196. this.handleCommitResolution(statusDetails)
  197. }
  198. orgSlug={orgSlug}
  199. projectSlug={projectSlug}
  200. />
  201. ));
  202. }
  203. openCustomReleaseModal() {
  204. const {orgSlug, projectSlug} = this.props;
  205. openModal(deps => (
  206. <CustomResolutionModal
  207. {...deps}
  208. onSelected={(statusDetails: ResolutionStatusDetails) =>
  209. this.handleAnotherExistingReleaseResolution(statusDetails)
  210. }
  211. orgSlug={orgSlug}
  212. projectSlug={projectSlug}
  213. />
  214. ));
  215. }
  216. render() {
  217. const {
  218. isResolved,
  219. onUpdate,
  220. confirmMessage,
  221. shouldConfirm,
  222. disabled,
  223. confirmLabel,
  224. projectFetchError,
  225. } = this.props;
  226. if (isResolved) {
  227. return this.renderResolved();
  228. }
  229. const onResolve = () =>
  230. openConfirmModal({
  231. bypass: !shouldConfirm,
  232. onConfirm: () => onUpdate({status: ResolutionStatus.RESOLVED}),
  233. message: confirmMessage,
  234. confirmText: confirmLabel,
  235. });
  236. return (
  237. <Tooltip disabled={!projectFetchError} title={t('Error fetching project')}>
  238. <ButtonBar merged>
  239. <ResolveButton
  240. size="xs"
  241. title={t(
  242. 'Resolves the issue. The issue will get unresolved if it happens again.'
  243. )}
  244. tooltipProps={{delay: 300, disabled}}
  245. icon={<IconCheckmark size="xs" />}
  246. onClick={onResolve}
  247. disabled={disabled}
  248. >
  249. {t('Resolve')}
  250. </ResolveButton>
  251. {this.renderDropdownMenu()}
  252. </ButtonBar>
  253. </Tooltip>
  254. );
  255. }
  256. }
  257. export default withOrganization(ResolveActions);
  258. const ResolveButton = styled(Button)`
  259. box-shadow: none;
  260. border-radius: ${p => p.theme.borderRadiusLeft};
  261. `;
  262. const DropdownTrigger = styled(Button)`
  263. box-shadow: none;
  264. border-radius: ${p => p.theme.borderRadiusRight};
  265. border-left: none;
  266. `;