123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447 |
- import {useMemo} from 'react';
- import styled from '@emotion/styled';
- import color from 'color';
- import type {Location} from 'history';
- import partition from 'lodash/partition';
- import GuideAnchor from 'sentry/components/assistant/guideAnchor';
- import {Button} from 'sentry/components/button';
- import Collapsible from 'sentry/components/collapsible';
- import GlobalSelectionLink from 'sentry/components/globalSelectionLink';
- import Panel from 'sentry/components/panels/panel';
- import PanelHeader from 'sentry/components/panels/panelHeader';
- import TextOverflow from 'sentry/components/textOverflow';
- import TimeSince from 'sentry/components/timeSince';
- import {Tooltip} from 'sentry/components/tooltip';
- import Version from 'sentry/components/version';
- import {t, tct, tn} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {Organization, PageFilters, Release} from 'sentry/types';
- import type {Threshold, ThresholdStatus} from '../../utils/types';
- import type {ReleasesDisplayOption} from '../releasesDisplayOptions';
- import type {ReleasesRequestRenderProps} from '../releasesRequest';
- import ReleaseCardCommits from './releaseCardCommits';
- import ReleaseCardProjectRow from './releaseCardProjectRow';
- import ReleaseCardStatsPeriod from './releaseCardStatsPeriod';
- function getReleaseProjectId(release: Release, selection: PageFilters) {
- // if a release has only one project
- if (release.projects.length === 1) {
- return release.projects[0].id;
- }
- // if only one project is selected in global header and release has it (second condition will prevent false positives like -1)
- if (
- selection.projects.length === 1 &&
- release.projects.map(p => p.id).includes(selection.projects[0])
- ) {
- return selection.projects[0];
- }
- // project selector on release detail page will pick it up
- return undefined;
- }
- type Props = {
- activeDisplay: ReleasesDisplayOption;
- getHealthData: ReleasesRequestRenderProps['getHealthData'];
- isTopRelease: boolean;
- location: Location;
- organization: Organization;
- release: Release;
- reloading: boolean;
- selection: PageFilters;
- showHealthPlaceholders: boolean;
- showReleaseAdoptionStages: boolean;
- thresholdStatuses: {[key: string]: ThresholdStatus[]};
- thresholds: Threshold[];
- };
- function ReleaseCard({
- release,
- organization,
- activeDisplay,
- location,
- reloading,
- selection,
- showHealthPlaceholders,
- isTopRelease,
- getHealthData,
- showReleaseAdoptionStages,
- thresholdStatuses,
- thresholds,
- }: Props) {
- const {
- version,
- commitCount,
- lastDeploy,
- dateCreated,
- versionInfo,
- adoptionStages,
- projects,
- } = release;
- const [projectsToShow, projectsToHide] = useMemo(() => {
- // sort health rows inside release card alphabetically by project name,
- // show only the ones that are selected in global header
- return partition(
- projects.sort((a, b) => a.slug.localeCompare(b.slug)),
- p =>
- // do not filter for My Projects & All Projects
- selection.projects.length > 0 && !selection.projects.includes(-1)
- ? selection.projects.includes(p.id)
- : true
- );
- }, [projects, selection.projects]);
- const hasThresholds = thresholds.length > 0;
- const getHiddenProjectsTooltip = () => {
- const limitedProjects = projectsToHide.map(p => p.slug).slice(0, 5);
- const remainderLength = projectsToHide.length - limitedProjects.length;
- if (remainderLength) {
- limitedProjects.push(tn('and %s more', 'and %s more', remainderLength));
- }
- return limitedProjects.join(', ');
- };
- return (
- <StyledPanel reloading={reloading ? 1 : 0} data-test-id="release-panel">
- <ReleaseInfo>
- {/* Header/info is the table sidecard */}
- <ReleaseInfoHeader>
- <GlobalSelectionLink
- to={{
- pathname: `/organizations/${
- organization.slug
- }/releases/${encodeURIComponent(version)}/`,
- query: {project: getReleaseProjectId(release, selection)},
- }}
- >
- <GuideAnchor
- disabled={!isTopRelease || projectsToShow.length > 1}
- target="release_version"
- >
- <VersionWrapper>
- <StyledVersion version={version} tooltipRawVersion anchor={false} />
- </VersionWrapper>
- </GuideAnchor>
- </GlobalSelectionLink>
- {commitCount > 0 && (
- <ReleaseCardCommits release={release} withHeading={false} />
- )}
- </ReleaseInfoHeader>
- <ReleaseInfoSubheader>
- {versionInfo?.package && (
- <PackageName>
- <TextOverflow ellipsisDirection="left">{versionInfo.package}</TextOverflow>
- </PackageName>
- )}
- <TimeSince date={lastDeploy?.dateFinished || dateCreated} />
- {lastDeploy?.dateFinished && ` \u007C ${lastDeploy.environment}`}
- </ReleaseInfoSubheader>
- </ReleaseInfo>
- <ReleaseProjects>
- {/* projects is the table */}
- <ReleaseProjectsHeader lightText>
- <ReleaseProjectsLayout
- showReleaseAdoptionStages={showReleaseAdoptionStages}
- hasThresholds={hasThresholds}
- >
- <ReleaseProjectColumn>{t('Project Name')}</ReleaseProjectColumn>
- {showReleaseAdoptionStages && (
- <AdoptionStageColumn>{t('Adoption Stage')}</AdoptionStageColumn>
- )}
- <AdoptionColumn>
- <span>{t('Adoption')}</span>
- <ReleaseCardStatsPeriod location={location} />
- </AdoptionColumn>
- <CrashFreeRateColumn>{t('Crash Free Rate')}</CrashFreeRateColumn>
- <DisplaySmallCol>{t('Crashes')}</DisplaySmallCol>
- <NewIssuesColumn>{t('New Issues')}</NewIssuesColumn>
- {hasThresholds && <DisplaySmallCol>{t('Thresholds')}</DisplaySmallCol>}
- </ReleaseProjectsLayout>
- </ReleaseProjectsHeader>
- <ProjectRows>
- <Collapsible
- expandButton={({onExpand, numberOfHiddenItems}) => (
- <ExpandButtonWrapper>
- <Button priority="primary" size="xs" onClick={onExpand}>
- {tct('Show [numberOfHiddenItems] More', {numberOfHiddenItems})}
- </Button>
- </ExpandButtonWrapper>
- )}
- collapseButton={({onCollapse}) => (
- <CollapseButtonWrapper>
- <Button priority="primary" size="xs" onClick={onCollapse}>
- {t('Collapse')}
- </Button>
- </CollapseButtonWrapper>
- )}
- >
- {projectsToShow.map((project, index) => {
- const key = `${project.slug}-${version}`;
- const projectThresholds = thresholds.filter(
- threshold => threshold.project.slug === project.slug
- );
- return (
- <ReleaseCardProjectRow
- key={`${key}-row`}
- activeDisplay={activeDisplay}
- adoptionStages={adoptionStages}
- getHealthData={getHealthData}
- hasThresholds={hasThresholds}
- expectedThresholds={projectThresholds.length}
- index={index}
- isTopRelease={isTopRelease}
- location={location}
- organization={organization}
- project={project}
- releaseVersion={version}
- lastDeploy={lastDeploy}
- showPlaceholders={showHealthPlaceholders}
- showReleaseAdoptionStages={showReleaseAdoptionStages}
- thresholdStatuses={hasThresholds ? thresholdStatuses[`${key}`] : []}
- />
- );
- })}
- </Collapsible>
- </ProjectRows>
- {projectsToHide.length > 0 && (
- <HiddenProjectsMessage data-test-id="hidden-projects">
- <Tooltip title={getHiddenProjectsTooltip()}>
- <TextOverflow>
- {projectsToHide.length === 1
- ? tct('[number:1] hidden project', {number: <strong />})
- : tct('[number] hidden projects', {
- number: <strong>{projectsToHide.length}</strong>,
- })}
- </TextOverflow>
- </Tooltip>
- </HiddenProjectsMessage>
- )}
- </ReleaseProjects>
- </StyledPanel>
- );
- }
- const VersionWrapper = styled('div')`
- display: flex;
- align-items: center;
- `;
- const StyledVersion = styled(Version)`
- ${p => p.theme.overflowEllipsis};
- `;
- const StyledPanel = styled(Panel)<{reloading: number}>`
- opacity: ${p => (p.reloading ? 0.5 : 1)};
- pointer-events: ${p => (p.reloading ? 'none' : 'auto')};
- @media (min-width: ${p => p.theme.breakpoints.medium}) {
- display: flex;
- }
- `;
- const ReleaseInfo = styled('div')`
- padding: ${space(1.5)} ${space(2)};
- flex-shrink: 0;
- @media (min-width: ${p => p.theme.breakpoints.medium}) {
- border-right: 1px solid ${p => p.theme.border};
- min-width: 260px;
- width: 22%;
- max-width: 300px;
- }
- `;
- const ReleaseInfoSubheader = styled('div')`
- font-size: ${p => p.theme.fontSizeSmall};
- color: ${p => p.theme.gray400};
- `;
- const PackageName = styled('div')`
- font-size: ${p => p.theme.fontSizeMedium};
- color: ${p => p.theme.textColor};
- display: flex;
- align-items: center;
- gap: ${space(0.5)};
- `;
- const ReleaseProjects = styled('div')`
- border-top: 1px solid ${p => p.theme.border};
- flex-grow: 1;
- display: grid;
- @media (min-width: ${p => p.theme.breakpoints.medium}) {
- border-top: none;
- }
- `;
- const ReleaseInfoHeader = styled('div')`
- font-size: ${p => p.theme.fontSizeExtraLarge};
- display: grid;
- grid-template-columns: minmax(0, 1fr) max-content;
- gap: ${space(2)};
- align-items: center;
- `;
- const ReleaseProjectsHeader = styled(PanelHeader)`
- border-top-left-radius: 0;
- padding: ${space(1.5)} ${space(2)};
- font-size: ${p => p.theme.fontSizeSmall};
- `;
- const ProjectRows = styled('div')`
- position: relative;
- `;
- const ExpandButtonWrapper = styled('div')`
- position: absolute;
- width: 100%;
- bottom: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- background-image: linear-gradient(
- 180deg,
- ${p => color(p.theme.background).alpha(0).string()} 0,
- ${p => p.theme.background}
- );
- background-repeat: repeat-x;
- border-bottom: ${space(1)} solid ${p => p.theme.background};
- border-top: ${space(1)} solid transparent;
- border-bottom-right-radius: ${p => p.theme.borderRadius};
- @media (max-width: ${p => p.theme.breakpoints.medium}) {
- border-bottom-left-radius: ${p => p.theme.borderRadius};
- }
- `;
- const CollapseButtonWrapper = styled('div')`
- display: flex;
- align-items: center;
- justify-content: center;
- height: 41px;
- `;
- export const ReleaseProjectsLayout = styled('div')<{
- hasThresholds?: boolean;
- showReleaseAdoptionStages?: boolean;
- }>`
- display: grid;
- grid-template-columns: 1fr 1.4fr 0.6fr 0.7fr;
- grid-column-gap: ${space(1)};
- align-items: center;
- width: 100%;
- @media (min-width: ${p => p.theme.breakpoints.small}) {
- ${p => {
- const thresholdSize = p.hasThresholds ? '0.5fr' : '';
- return `grid-template-columns: 1fr 1fr 1fr 0.5fr 0.5fr ${thresholdSize} 0.5fr`;
- }}
- }
- @media (min-width: ${p => p.theme.breakpoints.medium}) {
- ${p => {
- const thresholdSize = p.hasThresholds ? '0.5fr' : '';
- return `grid-template-columns: 1fr 1fr 1fr 0.5fr 0.5fr ${thresholdSize} 0.5fr`;
- }}
- }
- @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
- ${p => {
- const adoptionStagesSize = p.showReleaseAdoptionStages ? '0.7fr' : '';
- const thresholdSize = p.hasThresholds ? '0.7fr' : '';
- return `grid-template-columns: 1fr ${adoptionStagesSize} 1fr 1fr 0.7fr 0.7fr ${thresholdSize} 0.5fr`;
- }}
- }
- `;
- export const ReleaseProjectColumn = styled('div')`
- ${p => p.theme.overflowEllipsis};
- line-height: 20px;
- `;
- export const NewIssuesColumn = styled(ReleaseProjectColumn)`
- font-variant-numeric: tabular-nums;
- @media (min-width: ${p => p.theme.breakpoints.small}) {
- text-align: right;
- }
- `;
- export const AdoptionColumn = styled(ReleaseProjectColumn)`
- display: none;
- font-variant-numeric: tabular-nums;
- @media (min-width: ${p => p.theme.breakpoints.small}) {
- display: flex;
- /* Chart tooltips need overflow */
- overflow: visible;
- }
- & > * {
- flex: 1;
- }
- `;
- export const AdoptionStageColumn = styled(ReleaseProjectColumn)`
- display: none;
- font-variant-numeric: tabular-nums;
- @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
- display: flex;
- /* Need to show the edges of the tags */
- overflow: visible;
- }
- `;
- export const CrashFreeRateColumn = styled(ReleaseProjectColumn)`
- font-variant-numeric: tabular-nums;
- @media (min-width: ${p => p.theme.breakpoints.small}) {
- text-align: center;
- }
- @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
- text-align: right;
- }
- `;
- export const DisplaySmallCol = styled(ReleaseProjectColumn)`
- display: none;
- font-variant-numeric: tabular-nums;
- @media (min-width: ${p => p.theme.breakpoints.small}) {
- display: block;
- text-align: right;
- }
- `;
- const HiddenProjectsMessage = styled('div')`
- display: flex;
- align-items: center;
- font-size: ${p => p.theme.fontSizeSmall};
- padding: 0 ${space(2)};
- border-top: 1px solid ${p => p.theme.border};
- overflow: hidden;
- height: 24px;
- line-height: 24px;
- color: ${p => p.theme.gray300};
- background-color: ${p => p.theme.backgroundSecondary};
- border-bottom-right-radius: ${p => p.theme.borderRadius};
- @media (max-width: ${p => p.theme.breakpoints.medium}) {
- border-bottom-left-radius: ${p => p.theme.borderRadius};
- }
- `;
- export default ReleaseCard;
|