123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- import {Fragment} from 'react';
- import styled from '@emotion/styled';
- import type {Location} from 'history';
- import {archiveRelease, restoreRelease} from 'sentry/actionCreators/release';
- import {Client} from 'sentry/api';
- import {LinkButton} from 'sentry/components/button';
- import ButtonBar from 'sentry/components/buttonBar';
- import {openConfirmModal} from 'sentry/components/confirm';
- import {DropdownMenu} from 'sentry/components/dropdownMenu';
- import ProjectBadge from 'sentry/components/idBadge/projectBadge';
- import TextOverflow from 'sentry/components/textOverflow';
- import {Tooltip} from 'sentry/components/tooltip';
- import {IconEllipsis, IconNext, IconPrevious} from 'sentry/icons';
- import {t, tct, tn} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {Organization} from 'sentry/types/organization';
- import type {Release, ReleaseMeta} from 'sentry/types/release';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import {browserHistory} from 'sentry/utils/browserHistory';
- import normalizeUrl from 'sentry/utils/url/normalizeUrl';
- import {formatVersion} from 'sentry/utils/versions/formatVersion';
- import {isReleaseArchived} from '../../utils';
- type Props = {
- location: Location;
- organization: Organization;
- projectSlug: string;
- refetchData: () => void;
- release: Release;
- releaseMeta: ReleaseMeta;
- };
- function ReleaseActions({
- location,
- organization,
- projectSlug,
- release,
- releaseMeta,
- refetchData,
- }: Props) {
- async function handleArchive() {
- try {
- await archiveRelease(new Client(), {
- orgSlug: organization.slug,
- projectSlug,
- releaseVersion: release.version,
- });
- browserHistory.push(normalizeUrl(`/organizations/${organization.slug}/releases/`));
- } catch {
- // do nothing, action creator is already displaying error message
- }
- }
- async function handleRestore() {
- try {
- await restoreRelease(new Client(), {
- orgSlug: organization.slug,
- projectSlug,
- releaseVersion: release.version,
- });
- refetchData();
- } catch {
- // do nothing, action creator is already displaying error message
- }
- }
- function getProjectList() {
- const maxVisibleProjects = 5;
- const visibleProjects = releaseMeta.projects.slice(0, maxVisibleProjects);
- const numberOfCollapsedProjects =
- releaseMeta.projects.length - visibleProjects.length;
- return (
- <Fragment>
- {visibleProjects.map(project => (
- <ProjectBadge key={project.slug} project={project} avatarSize={18} />
- ))}
- {numberOfCollapsedProjects > 0 && (
- <span>
- <Tooltip
- title={release.projects
- .slice(maxVisibleProjects)
- .map(p => p.slug)
- .join(', ')}
- >
- + {tn('%s other project', '%s other projects', numberOfCollapsedProjects)}
- </Tooltip>
- </span>
- )}
- </Fragment>
- );
- }
- function getModalHeader(title: React.ReactNode) {
- return (
- <h4>
- <TextOverflow>{title}</TextOverflow>
- </h4>
- );
- }
- function getModalMessage(message: React.ReactNode) {
- return (
- <Fragment>
- {message}
- <ProjectsWrapper>{getProjectList()}</ProjectsWrapper>
- {t('Are you sure you want to do this?')}
- </Fragment>
- );
- }
- function replaceReleaseUrl(toRelease: string | null) {
- return toRelease
- ? {
- pathname: location.pathname
- .replace(encodeURIComponent(release.version), encodeURIComponent(toRelease))
- .replace(release.version, encodeURIComponent(toRelease)),
- query: {...location.query, activeRepo: undefined},
- }
- : '';
- }
- function handleNavigationClick(direction: string) {
- trackAnalytics(`release_detail.pagination`, {
- organization,
- direction,
- });
- }
- const menuItems = [
- isReleaseArchived(release)
- ? {
- key: 'restore',
- label: t('Restore'),
- onAction: () =>
- openConfirmModal({
- onConfirm: handleRestore,
- header: getModalHeader(
- tct('Restore Release [release]', {
- release: formatVersion(release.version),
- })
- ),
- message: getModalMessage(
- tn(
- 'You are restoring this release for the following project:',
- 'By restoring this release, you are also restoring it for the following projects:',
- releaseMeta.projects.length
- )
- ),
- cancelText: t('Nevermind'),
- confirmText: t('Restore'),
- }),
- }
- : {
- key: 'archive',
- label: t('Archive'),
- onAction: () =>
- openConfirmModal({
- onConfirm: handleArchive,
- header: getModalHeader(
- tct('Archive Release [release]', {
- release: formatVersion(release.version),
- })
- ),
- message: getModalMessage(
- tn(
- 'You are archiving this release for the following project:',
- 'By archiving this release, you are also archiving it for the following projects:',
- releaseMeta.projects.length
- )
- ),
- cancelText: t('Nevermind'),
- confirmText: t('Archive'),
- }),
- },
- ];
- const hasPrevious = !!release.currentProjectMeta.prevReleaseVersion;
- const hasNext = !!release.currentProjectMeta.nextReleaseVersion;
- return (
- <ButtonBar gap={1}>
- <ButtonBar merged>
- <LinkButton
- size="sm"
- to={replaceReleaseUrl(release.currentProjectMeta.firstReleaseVersion)}
- disabled={!hasPrevious}
- aria-label={t('Oldest')}
- icon={<IconPrevious />}
- onClick={() => handleNavigationClick('oldest')}
- />
- <LinkButton
- size="sm"
- to={replaceReleaseUrl(release.currentProjectMeta.prevReleaseVersion)}
- disabled={!hasPrevious}
- onClick={() => handleNavigationClick('older')}
- >
- {t('Older')}
- </LinkButton>
- <LinkButton
- size="sm"
- to={replaceReleaseUrl(release.currentProjectMeta.nextReleaseVersion)}
- disabled={!hasNext}
- onClick={() => handleNavigationClick('newer')}
- >
- {t('Newer')}
- </LinkButton>
- <LinkButton
- size="sm"
- to={replaceReleaseUrl(release.currentProjectMeta.lastReleaseVersion)}
- disabled={!hasNext}
- aria-label={t('Newest')}
- icon={<IconNext />}
- onClick={() => handleNavigationClick('newest')}
- />
- </ButtonBar>
- <DropdownMenu
- size="sm"
- items={menuItems}
- triggerProps={{
- showChevron: false,
- icon: <IconEllipsis />,
- 'aria-label': t('Actions'),
- }}
- position="bottom-end"
- />
- </ButtonBar>
- );
- }
- const ProjectsWrapper = styled('div')`
- margin: ${space(2)} 0 ${space(2)} ${space(2)};
- display: grid;
- gap: ${space(0.5)};
- img {
- border: none !important;
- box-shadow: none !important;
- }
- `;
- export default ReleaseActions;
|