mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
448 lines
14 KiB
TypeScript
448 lines
14 KiB
TypeScript
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;
|