releaseActions.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import {Fragment} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import {archiveRelease, restoreRelease} from 'sentry/actionCreators/release';
  6. import {Client} from 'sentry/api';
  7. import ButtonBar from 'sentry/components/buttonBar';
  8. import {openConfirmModal} from 'sentry/components/confirm';
  9. import DropdownMenuControl from 'sentry/components/dropdownMenuControl';
  10. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  11. import NavigationButtonGroup from 'sentry/components/navigationButtonGroup';
  12. import TextOverflow from 'sentry/components/textOverflow';
  13. import Tooltip from 'sentry/components/tooltip';
  14. import {IconEllipsis} from 'sentry/icons';
  15. import {t, tct, tn} from 'sentry/locale';
  16. import space from 'sentry/styles/space';
  17. import {Organization, Release, ReleaseMeta} from 'sentry/types';
  18. import {trackAnalyticsEvent} from 'sentry/utils/analytics';
  19. import {formatVersion} from 'sentry/utils/formatters';
  20. import {isReleaseArchived} from '../../utils';
  21. type Props = {
  22. location: Location;
  23. organization: Organization;
  24. projectSlug: string;
  25. refetchData: () => void;
  26. release: Release;
  27. releaseMeta: ReleaseMeta;
  28. };
  29. function ReleaseActions({
  30. location,
  31. organization,
  32. projectSlug,
  33. release,
  34. releaseMeta,
  35. refetchData,
  36. }: Props) {
  37. async function handleArchive() {
  38. try {
  39. await archiveRelease(new Client(), {
  40. orgSlug: organization.slug,
  41. projectSlug,
  42. releaseVersion: release.version,
  43. });
  44. browserHistory.push(`/organizations/${organization.slug}/releases/`);
  45. } catch {
  46. // do nothing, action creator is already displaying error message
  47. }
  48. }
  49. async function handleRestore() {
  50. try {
  51. await restoreRelease(new Client(), {
  52. orgSlug: organization.slug,
  53. projectSlug,
  54. releaseVersion: release.version,
  55. });
  56. refetchData();
  57. } catch {
  58. // do nothing, action creator is already displaying error message
  59. }
  60. }
  61. function getProjectList() {
  62. const maxVisibleProjects = 5;
  63. const visibleProjects = releaseMeta.projects.slice(0, maxVisibleProjects);
  64. const numberOfCollapsedProjects =
  65. releaseMeta.projects.length - visibleProjects.length;
  66. return (
  67. <Fragment>
  68. {visibleProjects.map(project => (
  69. <ProjectBadge key={project.slug} project={project} avatarSize={18} />
  70. ))}
  71. {numberOfCollapsedProjects > 0 && (
  72. <span>
  73. <Tooltip
  74. title={release.projects
  75. .slice(maxVisibleProjects)
  76. .map(p => p.slug)
  77. .join(', ')}
  78. >
  79. + {tn('%s other project', '%s other projects', numberOfCollapsedProjects)}
  80. </Tooltip>
  81. </span>
  82. )}
  83. </Fragment>
  84. );
  85. }
  86. function getModalHeader(title: React.ReactNode) {
  87. return (
  88. <h4>
  89. <TextOverflow>{title}</TextOverflow>
  90. </h4>
  91. );
  92. }
  93. function getModalMessage(message: React.ReactNode) {
  94. return (
  95. <Fragment>
  96. {message}
  97. <ProjectsWrapper>{getProjectList()}</ProjectsWrapper>
  98. {t('Are you sure you want to do this?')}
  99. </Fragment>
  100. );
  101. }
  102. function replaceReleaseUrl(toRelease: string | null) {
  103. return toRelease
  104. ? {
  105. pathname: location.pathname
  106. .replace(encodeURIComponent(release.version), toRelease)
  107. .replace(release.version, toRelease),
  108. query: {...location.query, activeRepo: undefined},
  109. }
  110. : '';
  111. }
  112. function handleNavigationClick(direction: string) {
  113. trackAnalyticsEvent({
  114. eventKey: `release_detail.pagination`,
  115. eventName: `Release Detail: Pagination`,
  116. organization_id: parseInt(organization.id, 10),
  117. direction,
  118. });
  119. }
  120. const menuItems = [
  121. isReleaseArchived(release)
  122. ? {
  123. key: 'restore',
  124. label: t('Restore'),
  125. onAction: () =>
  126. openConfirmModal({
  127. onConfirm: handleRestore,
  128. header: getModalHeader(
  129. tct('Restore Release [release]', {
  130. release: formatVersion(release.version),
  131. })
  132. ),
  133. message: getModalMessage(
  134. tn(
  135. 'You are restoring this release for the following project:',
  136. 'By restoring this release, you are also restoring it for the following projects:',
  137. releaseMeta.projects.length
  138. )
  139. ),
  140. cancelText: t('Nevermind'),
  141. confirmText: t('Restore'),
  142. }),
  143. }
  144. : {
  145. key: 'archive',
  146. label: t('Archive'),
  147. onAction: () =>
  148. openConfirmModal({
  149. onConfirm: handleArchive,
  150. header: getModalHeader(
  151. tct('Archive Release [release]', {
  152. release: formatVersion(release.version),
  153. })
  154. ),
  155. message: getModalMessage(
  156. tn(
  157. 'You are archiving this release for the following project:',
  158. 'By archiving this release, you are also archiving it for the following projects:',
  159. releaseMeta.projects.length
  160. )
  161. ),
  162. cancelText: t('Nevermind'),
  163. confirmText: t('Archive'),
  164. }),
  165. },
  166. ];
  167. const {
  168. nextReleaseVersion,
  169. prevReleaseVersion,
  170. firstReleaseVersion,
  171. lastReleaseVersion,
  172. } = release.currentProjectMeta;
  173. return (
  174. <ButtonBar gap={1}>
  175. <NavigationButtonGroup
  176. size="sm"
  177. hasPrevious={!!prevReleaseVersion}
  178. hasNext={!!nextReleaseVersion}
  179. links={[
  180. replaceReleaseUrl(firstReleaseVersion),
  181. replaceReleaseUrl(prevReleaseVersion),
  182. replaceReleaseUrl(nextReleaseVersion),
  183. replaceReleaseUrl(lastReleaseVersion),
  184. ]}
  185. onOldestClick={() => handleNavigationClick('oldest')}
  186. onOlderClick={() => handleNavigationClick('older')}
  187. onNewerClick={() => handleNavigationClick('newer')}
  188. onNewestClick={() => handleNavigationClick('newest')}
  189. />
  190. <DropdownMenuControl
  191. size="sm"
  192. items={menuItems}
  193. triggerProps={{
  194. showChevron: false,
  195. icon: <IconEllipsis size="xs" />,
  196. 'aria-label': t('Actions'),
  197. }}
  198. position="bottom-end"
  199. />
  200. </ButtonBar>
  201. );
  202. }
  203. const ProjectsWrapper = styled('div')`
  204. margin: ${space(2)} 0 ${space(2)} ${space(2)};
  205. display: grid;
  206. gap: ${space(0.5)};
  207. img {
  208. border: none !important;
  209. box-shadow: none !important;
  210. }
  211. `;
  212. export default ReleaseActions;