resolve.tsx 7.0 KB

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