Skip to content

Commit 4598b29

Browse files
authored
ref(top-issues): small cluster cards (#104535)
Experimenting on making the cluster cards smaller via hiding issues by default, and showing the description. <img width="617" height="349" alt="Screenshot 2025-12-08 at 11 26 03 AM" src="https://github.com/user-attachments/assets/90614477-6beb-413e-ba7e-044bbe1ecbb0" />
1 parent 641076a commit 4598b29

File tree

1 file changed

+126
-122
lines changed

1 file changed

+126
-122
lines changed

static/app/views/issueList/pages/dynamicGrouping.tsx

Lines changed: 126 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ interface ClusterSummary {
9494
group_ids: number[];
9595
issue_titles: string[];
9696
project_ids: number[];
97+
summary: string | null;
9798
tags: string[];
9899
title: string;
99100
code_area_tags?: string[];
@@ -110,9 +111,9 @@ function formatClusterInfoForClipboard(cluster: ClusterSummary): string {
110111
lines.push(`## ${cluster.title}`);
111112
lines.push('');
112113

113-
if (cluster.description) {
114+
if (cluster.summary) {
114115
lines.push('### Summary');
115-
lines.push(cluster.description);
116+
lines.push(cluster.summary);
116117
lines.push('');
117118
}
118119

@@ -347,7 +348,7 @@ function ClusterCard({
347348
const api = useApi();
348349
const organization = useOrganization();
349350
const {selection} = usePageFilters();
350-
const [showDescription, setShowDescription] = useState(false);
351+
const [activeTab, setActiveTab] = useState<'summary' | 'issues'>('summary');
351352
const clusterStats = useClusterStats(cluster.group_ids);
352353
const {copy} = useCopyToClipboard();
353354

@@ -439,100 +440,97 @@ function ClusterCard({
439440
onTagClick={onTagClick}
440441
selectedTags={selectedTags}
441442
/>
442-
{cluster.description && (
443-
<Fragment>
444-
{showDescription ? (
445-
<Fragment>
446-
<DescriptionText>{cluster.description}</DescriptionText>
447-
<ReadMoreButton onClick={() => setShowDescription(false)}>
448-
{t('Collapse summary')}
449-
</ReadMoreButton>
450-
</Fragment>
443+
<ClusterStats>
444+
{cluster.fixability_score !== null &&
445+
cluster.fixability_score !== undefined && (
446+
<StatItem>
447+
<IconFix size="xs" color="gray300" />
448+
<Text size="xs">
449+
<Text size="xs" bold as="span">
450+
{Math.round(cluster.fixability_score * 100)}%
451+
</Text>{' '}
452+
{t('relevance')}
453+
</Text>
454+
</StatItem>
455+
)}
456+
<StatItem>
457+
<IconFire size="xs" color="gray300" />
458+
{clusterStats.isPending ? (
459+
<Text size="xs" variant="muted">
460+
461+
</Text>
451462
) : (
452-
<ReadMoreButton onClick={() => setShowDescription(true)}>
453-
{t('View summary')}
454-
</ReadMoreButton>
463+
<Text size="xs">
464+
<Text size="xs" bold as="span">
465+
{clusterStats.totalEvents.toLocaleString()}
466+
</Text>{' '}
467+
{tn('event', 'events', clusterStats.totalEvents)}
468+
</Text>
455469
)}
456-
</Fragment>
457-
)}
458-
</CardHeader>
459-
460-
<ClusterStatsBar>
461-
{cluster.fixability_score !== null && cluster.fixability_score !== undefined && (
470+
</StatItem>
462471
<StatItem>
463-
<IconFix size="xs" color="gray300" />
464-
<Text size="xs">
465-
<Text size="xs" bold as="span">
466-
{Math.round(cluster.fixability_score * 100)}%
467-
</Text>{' '}
468-
{t('relevance')}
469-
</Text>
472+
<IconUser size="xs" color="gray300" />
473+
{clusterStats.isPending ? (
474+
<Text size="xs" variant="muted">
475+
476+
</Text>
477+
) : (
478+
<Text size="xs">
479+
<Text size="xs" bold as="span">
480+
{clusterStats.totalUsers.toLocaleString()}
481+
</Text>{' '}
482+
{tn('user', 'users', clusterStats.totalUsers)}
483+
</Text>
484+
)}
470485
</StatItem>
471-
)}
472-
<StatItem>
473-
<IconFire size="xs" color="gray300" />
474-
{clusterStats.isPending ? (
475-
<Text size="xs" variant="muted">
476-
477-
</Text>
478-
) : (
479-
<Text size="xs">
480-
<Text size="xs" bold as="span">
481-
{clusterStats.totalEvents.toLocaleString()}
482-
</Text>{' '}
483-
{tn('event', 'events', clusterStats.totalEvents)}
484-
</Text>
486+
{!clusterStats.isPending && clusterStats.lastSeen && (
487+
<StatItem>
488+
<IconClock size="xs" color="gray300" />
489+
<TimeSince
490+
tooltipPrefix={t('Last Seen')}
491+
date={clusterStats.lastSeen}
492+
suffix={t('ago')}
493+
unitStyle="short"
494+
/>
495+
</StatItem>
485496
)}
486-
</StatItem>
487-
<StatItem>
488-
<IconUser size="xs" color="gray300" />
489-
{clusterStats.isPending ? (
490-
<Text size="xs" variant="muted">
491-
492-
</Text>
493-
) : (
494-
<Text size="xs">
495-
<Text size="xs" bold as="span">
496-
{clusterStats.totalUsers.toLocaleString()}
497-
</Text>{' '}
498-
{tn('user', 'users', clusterStats.totalUsers)}
499-
</Text>
497+
{!clusterStats.isPending && clusterStats.firstSeen && (
498+
<StatItem>
499+
<IconCalendar size="xs" color="gray300" />
500+
<TimeSince
501+
tooltipPrefix={t('First Seen')}
502+
date={clusterStats.firstSeen}
503+
suffix={t('old')}
504+
unitStyle="short"
505+
/>
506+
</StatItem>
500507
)}
501-
</StatItem>
502-
{!clusterStats.isPending && clusterStats.lastSeen && (
503-
<StatItem>
504-
<IconClock size="xs" color="gray300" />
505-
<TimeSince
506-
tooltipPrefix={t('Last Seen')}
507-
date={clusterStats.lastSeen}
508-
suffix={t('ago')}
509-
unitStyle="short"
510-
/>
511-
</StatItem>
512-
)}
513-
{!clusterStats.isPending && clusterStats.firstSeen && (
514-
<StatItem>
515-
<IconCalendar size="xs" color="gray300" />
516-
<TimeSince
517-
tooltipPrefix={t('First Seen')}
518-
date={clusterStats.firstSeen}
519-
suffix={t('old')}
520-
unitStyle="short"
521-
/>
522-
</StatItem>
523-
)}
524-
</ClusterStatsBar>
508+
</ClusterStats>
509+
</CardHeader>
525510

526-
<IssuesSection>
527-
<IssuesSectionHeader>
528-
<Text size="sm" bold uppercase>
511+
<TabSection>
512+
<TabBar>
513+
<Tab isActive={activeTab === 'summary'} onClick={() => setActiveTab('summary')}>
514+
{t('Summary')}
515+
</Tab>
516+
<Tab isActive={activeTab === 'issues'} onClick={() => setActiveTab('issues')}>
529517
{t('Preview Issues')}
530-
</Text>
531-
</IssuesSectionHeader>
532-
<IssuesList>
533-
<ClusterIssues groupIds={cluster.group_ids} />
534-
</IssuesList>
535-
</IssuesSection>
518+
</Tab>
519+
</TabBar>
520+
<TabContent>
521+
{activeTab === 'summary' ? (
522+
cluster.summary ? (
523+
<DescriptionText>{cluster.summary}</DescriptionText>
524+
) : (
525+
<Text size="sm" variant="muted">
526+
{t('No summary available')}
527+
</Text>
528+
)
529+
) : (
530+
<ClusterIssues groupIds={cluster.group_ids} />
531+
)}
532+
</TabContent>
533+
</TabSection>
536534

537535
<CardFooter>
538536
<ButtonBar merged gap="0">
@@ -1134,15 +1132,12 @@ const ClusterTitle = styled('h3')`
11341132
word-break: break-word;
11351133
`;
11361134

1137-
// Horizontal stats bar below header
1138-
const ClusterStatsBar = styled('div')`
1135+
// Stats row within header
1136+
const ClusterStats = styled('div')`
11391137
display: flex;
11401138
flex-wrap: wrap;
11411139
align-items: center;
11421140
gap: ${space(2)};
1143-
padding: ${space(1.5)} ${space(3)};
1144-
border-top: 1px solid ${p => p.theme.innerBorder};
1145-
border-bottom: 1px solid ${p => p.theme.innerBorder};
11461141
font-size: ${p => p.theme.fontSize.sm};
11471142
color: ${p => p.theme.subText};
11481143
`;
@@ -1153,24 +1148,48 @@ const StatItem = styled('div')`
11531148
gap: ${space(0.5)};
11541149
`;
11551150

1156-
// Zone 3: Issues list with clear containment
1157-
const IssuesSection = styled('div')`
1158-
padding: ${space(2)} ${space(3)};
1159-
flex: 1;
1151+
// Tab section for Summary / Preview Issues
1152+
const TabSection = styled('div')``;
1153+
1154+
const TabBar = styled('div')`
11601155
display: flex;
1161-
flex-direction: column;
1156+
gap: ${space(0.5)};
1157+
padding: ${space(1)} ${space(3)} 0;
1158+
border-bottom: 1px solid ${p => p.theme.innerBorder};
11621159
`;
11631160

1164-
const IssuesSectionHeader = styled('div')`
1165-
margin-bottom: ${space(1.5)};
1166-
color: ${p => p.theme.subText};
1167-
letter-spacing: 0.5px;
1161+
const Tab = styled('button')<{isActive: boolean}>`
1162+
background: none;
1163+
border: none;
1164+
padding: ${space(1)} ${space(1.5)};
1165+
font-size: ${p => p.theme.fontSize.sm};
1166+
font-weight: 500;
1167+
color: ${p => (p.isActive ? p.theme.textColor : p.theme.subText)};
1168+
cursor: pointer;
1169+
position: relative;
1170+
margin-bottom: -1px;
1171+
1172+
${p =>
1173+
p.isActive &&
1174+
`
1175+
&::after {
1176+
content: '';
1177+
position: absolute;
1178+
left: 0;
1179+
right: 0;
1180+
bottom: 0;
1181+
height: 2px;
1182+
background: ${p.theme.purple300};
1183+
}
1184+
`}
1185+
1186+
&:hover {
1187+
color: ${p => p.theme.textColor};
1188+
}
11681189
`;
11691190

1170-
const IssuesList = styled('div')`
1171-
display: flex;
1172-
flex-direction: column;
1173-
gap: ${space(1.5)};
1191+
const TabContent = styled('div')`
1192+
padding: ${space(2)} ${space(3)};
11741193
`;
11751194

11761195
// Zone 4: Footer with actions
@@ -1242,21 +1261,6 @@ const MetaSeparator = styled('div')`
12421261
background-color: ${p => p.theme.innerBorder};
12431262
`;
12441263

1245-
const ReadMoreButton = styled('button')`
1246-
background: none;
1247-
border: none;
1248-
padding: 0;
1249-
font-size: ${p => p.theme.fontSize.sm};
1250-
color: ${p => p.theme.subText};
1251-
cursor: pointer;
1252-
text-align: left;
1253-
1254-
&:hover {
1255-
color: ${p => p.theme.textColor};
1256-
text-decoration: underline;
1257-
}
1258-
`;
1259-
12601264
const DescriptionText = styled('p')`
12611265
margin: 0;
12621266
font-size: ${p => p.theme.fontSize.sm};

0 commit comments

Comments
 (0)