1- import { Fragment , useEffect , useMemo , useState } from 'react' ;
1+ import { Fragment , useMemo , useState } from 'react' ;
22import styled from '@emotion/styled' ;
33
44import { Container , Flex } from '@sentry/scraps/layout' ;
55import { Heading , Text } from '@sentry/scraps/text' ;
66
77import { Breadcrumbs } from 'sentry/components/breadcrumbs' ;
8- import { Alert } from 'sentry/components/core/alert' ;
98import { Button } from 'sentry/components/core/button' ;
109import { Checkbox } from 'sentry/components/core/checkbox' ;
1110import { Disclosure } from 'sentry/components/core/disclosure' ;
@@ -32,8 +31,6 @@ import useOrganization from 'sentry/utils/useOrganization';
3231import { useUser } from 'sentry/utils/useUser' ;
3332import { useUserTeams } from 'sentry/utils/useUserTeams' ;
3433
35- const STORAGE_KEY = 'dynamic-grouping-cluster-data' ;
36-
3734interface AssignedEntity {
3835 email : string | null ;
3936 id : string ;
@@ -52,11 +49,14 @@ interface ClusterSummary {
5249 group_ids : number [ ] ;
5350 issue_titles : string [ ] ;
5451 project_ids : number [ ] ;
55- summary : string ;
5652 tags : string [ ] ;
5753 title : string ;
5854}
5955
56+ interface TopIssuesResponse {
57+ data : ClusterSummary [ ] ;
58+ }
59+
6060// Compact issue preview for dynamic grouping - no short ID or quick fix icon
6161function CompactIssuePreview ( { group} : { group : Group } ) {
6262 const organization = useOrganization ( ) ;
@@ -215,7 +215,7 @@ function ClusterCard({
215215 </ TitleHeading >
216216 </ Disclosure . Title >
217217 < Disclosure . Content >
218- < SummaryText variant = "muted" > { cluster . summary } </ SummaryText >
218+ < SummaryText variant = "muted" > { cluster . description } </ SummaryText >
219219 { cluster . fixability_score !== null && (
220220 < ConfidenceText size = "sm" variant = "muted" >
221221 { t ( '%s%% confidence' , Math . round ( cluster . fixability_score * 100 ) ) }
@@ -267,72 +267,34 @@ function DynamicGrouping() {
267267 const organization = useOrganization ( ) ;
268268 const user = useUser ( ) ;
269269 const { teams} = useUserTeams ( ) ;
270- const [ jsonInput , setJsonInput ] = useState ( '' ) ;
271- const [ clusterData , setClusterData ] = useState < ClusterSummary [ ] > ( [ ] ) ;
272- const [ parseError , setParseError ] = useState < string | null > ( null ) ;
273- const [ showInput , setShowInput ] = useState ( true ) ;
274270 const [ filterByAssignedToMe , setFilterByAssignedToMe ] = useState ( true ) ;
275271 const [ minFixabilityScore , setMinFixabilityScore ] = useState ( 50 ) ;
276272 const [ removingClusterId , setRemovingClusterId ] = useState < number | null > ( null ) ;
273+ const [ removedClusterIds , setRemovedClusterIds ] = useState < Set < number > > ( new Set ( ) ) ;
277274
278- // Load from localStorage on mount
279- useEffect ( ( ) => {
280- const stored = localStorage . getItem ( STORAGE_KEY ) ;
281- if ( stored ) {
282- try {
283- const parsed = JSON . parse ( stored ) ;
284- setClusterData ( parsed ) ;
285- setJsonInput ( stored ) ;
286- setShowInput ( false ) ;
287- } catch {
288- // If stored data is invalid, clear it
289- localStorage . removeItem ( STORAGE_KEY ) ;
290- }
291- }
292- } , [ ] ) ;
293-
294- const handleJsonSubmit = ( ) => {
295- try {
296- const parsed = JSON . parse ( jsonInput ) ;
297- if ( ! Array . isArray ( parsed ) ) {
298- setParseError ( t ( 'JSON must be an array of cluster summaries' ) ) ;
299- return ;
300- }
301- setClusterData ( parsed ) ;
302- setParseError ( null ) ;
303- setShowInput ( false ) ;
304- localStorage . setItem ( STORAGE_KEY , jsonInput ) ;
305- } catch ( error ) {
306- setParseError ( error instanceof Error ? error . message : t ( 'Invalid JSON format' ) ) ;
275+ // Fetch cluster data from API
276+ const { data : topIssuesResponse , isPending} = useApiQuery < TopIssuesResponse > (
277+ [ `/organizations/${ organization . slug } /top-issues/` ] ,
278+ {
279+ staleTime : 60000 , // Cache for 1 minute
307280 }
308- } ;
281+ ) ;
309282
310- const handleClear = ( ) => {
311- setClusterData ( [ ] ) ;
312- setJsonInput ( '' ) ;
313- setParseError ( null ) ;
314- setShowInput ( true ) ;
315- localStorage . removeItem ( STORAGE_KEY ) ;
316- } ;
283+ const clusterData = useMemo (
284+ ( ) => topIssuesResponse ?. data ?? [ ] ,
285+ [ topIssuesResponse ?. data ]
286+ ) ;
317287
318288 const handleRemoveCluster = ( clusterId : number ) => {
319289 // Start animation
320290 setRemovingClusterId ( clusterId ) ;
321291
322292 // Wait for animation to complete before removing
323293 setTimeout ( ( ) => {
324- const updatedClusters = clusterData . filter (
325- cluster => cluster . cluster_id !== clusterId
326- ) ;
327- setClusterData ( updatedClusters ) ;
294+ const updatedRemovedIds = new Set ( removedClusterIds ) ;
295+ updatedRemovedIds . add ( clusterId ) ;
296+ setRemovedClusterIds ( updatedRemovedIds ) ;
328297 setRemovingClusterId ( null ) ;
329-
330- // Update localStorage with the new data
331- if ( updatedClusters . length > 0 ) {
332- localStorage . setItem ( STORAGE_KEY , JSON . stringify ( updatedClusters ) ) ;
333- } else {
334- localStorage . removeItem ( STORAGE_KEY ) ;
335- }
336298 } , 300 ) ; // Match the animation duration
337299 } ;
338300
@@ -364,6 +326,11 @@ function DynamicGrouping() {
364326 const filteredAndSortedClusters = useMemo ( ( ) => {
365327 return [ ...clusterData ]
366328 . filter ( ( cluster : ClusterSummary ) => {
329+ // Filter out removed clusters
330+ if ( removedClusterIds . has ( cluster . cluster_id ) ) {
331+ return false ;
332+ }
333+
367334 // Filter by fixability score - hide clusters below threshold
368335 const fixabilityScore = ( cluster . fixability_score ?? 0 ) * 100 ;
369336 if ( fixabilityScore < minFixabilityScore ) {
@@ -380,7 +347,13 @@ function DynamicGrouping() {
380347 ( a : ClusterSummary , b : ClusterSummary ) =>
381348 ( b . fixability_score ?? 0 ) - ( a . fixability_score ?? 0 )
382349 ) ;
383- } , [ clusterData , filterByAssignedToMe , minFixabilityScore , isClusterAssignedToMe ] ) ;
350+ } , [
351+ clusterData ,
352+ removedClusterIds ,
353+ filterByAssignedToMe ,
354+ minFixabilityScore ,
355+ isClusterAssignedToMe ,
356+ ] ) ;
384357
385358 const totalIssues = useMemo ( ( ) => {
386359 return filteredAndSortedClusters . reduce (
@@ -409,52 +382,15 @@ function DynamicGrouping() {
409382 />
410383
411384 < PageHeader >
412- < Flex justify = "between" align = "start" gap = "sm" >
413- < div style = { { flex : 1 } } >
414- < Heading as = "h1" > { t ( 'Top Issues' ) } </ Heading >
415- </ div >
416- { clusterData . length > 0 && ! showInput && (
417- < Flex gap = "sm" style = { { flexShrink : 0 } } >
418- < Button size = "sm" onClick = { ( ) => setShowInput ( true ) } >
419- { t ( 'Update Data' ) }
420- </ Button >
421- < Button size = "sm" priority = "danger" onClick = { handleClear } >
422- { t ( 'Clear' ) }
423- </ Button >
424- </ Flex >
425- ) }
426- </ Flex >
385+ < Heading as = "h1" > { t ( 'Top Issues' ) } </ Heading >
427386 </ PageHeader >
428387
429- { showInput || clusterData . length === 0 ? (
430- < Container
431- padding = "lg"
432- border = "primary"
433- radius = "md"
434- marginBottom = "lg"
435- background = "primary"
436- >
437- < Flex direction = "column" gap = "md" >
438- < Text size = "sm" variant = "muted" >
439- { t ( 'Paste cluster summaries JSON data below:' ) }
440- </ Text >
441- < JsonTextarea
442- value = { jsonInput }
443- onChange = { e => setJsonInput ( e . target . value ) }
444- placeholder = { t ( 'Paste JSON array here...' ) }
445- rows = { 10 }
446- />
447- { parseError && < Alert type = "error" > { parseError } </ Alert > }
448- < Flex gap = "sm" >
449- < Button priority = "primary" onClick = { handleJsonSubmit } >
450- { t ( 'Load Data' ) }
451- </ Button >
452- { clusterData . length > 0 && (
453- < Button onClick = { ( ) => setShowInput ( false ) } > { t ( 'Cancel' ) } </ Button >
454- ) }
455- </ Flex >
456- </ Flex >
457- </ Container >
388+ { isPending ? (
389+ < Flex direction = "column" gap = "md" marginTop = "lg" >
390+ { [ 0 , 1 , 2 ] . map ( i => (
391+ < Placeholder key = { i } height = "200px" />
392+ ) ) }
393+ </ Flex >
458394 ) : (
459395 < Fragment >
460396 < Flex marginBottom = "lg" >
@@ -512,16 +448,24 @@ function DynamicGrouping() {
512448 </ Disclosure >
513449 </ Container >
514450
515- < CardsGrid >
516- { filteredAndSortedClusters . map ( ( cluster : ClusterSummary ) => (
517- < ClusterCard
518- key = { cluster . cluster_id }
519- cluster = { cluster }
520- onRemove = { handleRemoveCluster }
521- isRemoving = { removingClusterId === cluster . cluster_id }
522- />
523- ) ) }
524- </ CardsGrid >
451+ { filteredAndSortedClusters . length === 0 ? (
452+ < Container padding = "lg" border = "primary" radius = "md" background = "primary" >
453+ < Text variant = "muted" style = { { textAlign : 'center' } } >
454+ { t ( 'No clusters match the current filters' ) }
455+ </ Text >
456+ </ Container >
457+ ) : (
458+ < CardsGrid >
459+ { filteredAndSortedClusters . map ( ( cluster : ClusterSummary ) => (
460+ < ClusterCard
461+ key = { cluster . cluster_id }
462+ cluster = { cluster }
463+ onRemove = { handleRemoveCluster }
464+ isRemoving = { removingClusterId === cluster . cluster_id }
465+ />
466+ ) ) }
467+ </ CardsGrid >
468+ ) }
525469 </ Fragment >
526470 ) }
527471 </ PageContainer >
@@ -742,30 +686,6 @@ const EventCount = styled('span')`
742686 font-weight: ${ p => p . theme . fontWeight . bold } ;
743687` ;
744688
745- const JsonTextarea = styled ( 'textarea' ) `
746- width: 100%;
747- min-height: 300px;
748- padding: ${ space ( 1.5 ) } ;
749- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
750- font-size: 13px;
751- line-height: 1.5;
752- background: ${ p => p . theme . backgroundSecondary } ;
753- border: 1px solid ${ p => p . theme . border } ;
754- border-radius: ${ p => p . theme . borderRadius } ;
755- color: ${ p => p . theme . textColor } ;
756- resize: vertical;
757-
758- &:focus {
759- outline: none;
760- border-color: ${ p => p . theme . purple300 } ;
761- box-shadow: 0 0 0 1px ${ p => p . theme . purple300 } ;
762- }
763-
764- &::placeholder {
765- color: ${ p => p . theme . subText } ;
766- }
767- ` ;
768-
769689const AdvancedFilterContent = styled ( 'div' ) `
770690 padding: ${ space ( 2 ) } 0;
771691 display: flex;
0 commit comments