@@ -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-
12601264const DescriptionText = styled ( 'p' ) `
12611265 margin: 0;
12621266 font-size: ${ p => p . theme . fontSize . sm } ;
0 commit comments