1- import { Fragment , useMemo , useState } from 'react' ;
1+ import { Fragment , useCallback , useMemo , useState } from 'react' ;
22import styled from '@emotion/styled' ;
33
44import { Container , Flex } from '@sentry/scraps/layout' ;
@@ -9,6 +9,7 @@ import {Checkbox} from 'sentry/components/core/checkbox';
99import { Disclosure } from 'sentry/components/core/disclosure' ;
1010import { NumberInput } from 'sentry/components/core/input/numberInput' ;
1111import { Link } from 'sentry/components/core/link' ;
12+ import { TextArea } from 'sentry/components/core/textarea' ;
1213import EventOrGroupTitle from 'sentry/components/eventOrGroupTitle' ;
1314import EventMessage from 'sentry/components/events/eventMessage' ;
1415import TimesTag from 'sentry/components/group/inboxBadges/timesTag' ;
@@ -17,7 +18,14 @@ import ProjectBadge from 'sentry/components/idBadge/projectBadge';
1718import LoadingIndicator from 'sentry/components/loadingIndicator' ;
1819import Redirect from 'sentry/components/redirect' ;
1920import TimeSince from 'sentry/components/timeSince' ;
20- import { IconCalendar , IconClock , IconFire , IconFix } from 'sentry/icons' ;
21+ import {
22+ IconCalendar ,
23+ IconClock ,
24+ IconClose ,
25+ IconFire ,
26+ IconFix ,
27+ IconUpload ,
28+ } from 'sentry/icons' ;
2129import { t , tn } from 'sentry/locale' ;
2230import { space } from 'sentry/styles/space' ;
2331import type { Group } from 'sentry/types/group' ;
@@ -331,17 +339,48 @@ function DynamicGrouping() {
331339 const [ filterByAssignedToMe , setFilterByAssignedToMe ] = useState ( true ) ;
332340 const [ selectedTeamIds , setSelectedTeamIds ] = useState < Set < string > > ( new Set ( ) ) ;
333341 const [ minFixabilityScore , setMinFixabilityScore ] = useState ( 50 ) ;
334- const [ removedClusterIds , setRemovedClusterIds ] = useState < Set < number > > ( new Set ( ) ) ;
342+ const [ removedClusterIds , setRemovedClusterIds ] = useState ( new Set < number > ( ) ) ;
343+ const [ showJsonInput , setShowJsonInput ] = useState ( false ) ;
344+ const [ jsonInputValue , setJsonInputValue ] = useState ( '' ) ;
345+ const [ customClusterData , setCustomClusterData ] = useState < ClusterSummary [ ] | null > (
346+ null
347+ ) ;
348+ const [ jsonError , setJsonError ] = useState < string | null > ( null ) ;
335349
336350 // Fetch cluster data from API
337351 const { data : topIssuesResponse , isPending} = useApiQuery < TopIssuesResponse > (
338352 [ `/organizations/${ organization . slug } /top-issues/` ] ,
339353 {
340354 staleTime : 60000 ,
355+ enabled : customClusterData === null , // Only fetch if no custom data
341356 }
342357 ) ;
343358
344- const clusterData = topIssuesResponse ?. data ?? [ ] ;
359+ const handleParseJson = useCallback ( ( ) => {
360+ try {
361+ const parsed = JSON . parse ( jsonInputValue ) ;
362+ // Support both {data: [...]} format and direct array format
363+ const clusters = Array . isArray ( parsed ) ? parsed : parsed ?. data ;
364+ if ( ! Array . isArray ( clusters ) ) {
365+ setJsonError ( t ( 'JSON must be an array or have a "data" property with an array' ) ) ;
366+ return ;
367+ }
368+ setCustomClusterData ( clusters as ClusterSummary [ ] ) ;
369+ setJsonError ( null ) ;
370+ setShowJsonInput ( false ) ;
371+ } catch ( e ) {
372+ setJsonError ( t ( 'Invalid JSON: %s' , e instanceof Error ? e . message : String ( e ) ) ) ;
373+ }
374+ } , [ jsonInputValue ] ) ;
375+
376+ const handleClearCustomData = useCallback ( ( ) => {
377+ setCustomClusterData ( null ) ;
378+ setJsonInputValue ( '' ) ;
379+ setJsonError ( null ) ;
380+ } , [ ] ) ;
381+
382+ const clusterData = customClusterData ?? topIssuesResponse ?. data ?? [ ] ;
383+ const isUsingCustomData = customClusterData !== null ;
345384
346385 // Extract all unique teams from the cluster data
347386 const teamsInData = useMemo ( ( ) => {
@@ -417,9 +456,72 @@ function DynamicGrouping() {
417456 return (
418457 < PageWrapper >
419458 < HeaderSection >
420- < Heading as = "h1" style = { { marginBottom : space ( 2 ) } } >
421- { t ( 'Top Issues' ) }
422- </ Heading >
459+ < Flex align = "center" gap = "md" style = { { marginBottom : space ( 2 ) } } >
460+ < Heading as = "h1" > { t ( 'Top Issues' ) } </ Heading >
461+ { isUsingCustomData && (
462+ < CustomDataBadge >
463+ < Text size = "xs" bold >
464+ { t ( 'Using Custom Data' ) }
465+ </ Text >
466+ < Button
467+ size = "zero"
468+ borderless
469+ icon = { < IconClose size = "xs" /> }
470+ aria-label = { t ( 'Clear custom data' ) }
471+ onClick = { handleClearCustomData }
472+ />
473+ </ CustomDataBadge >
474+ ) }
475+ </ Flex >
476+
477+ < Flex gap = "sm" style = { { marginBottom : space ( 2 ) } } >
478+ < Button
479+ size = "sm"
480+ icon = { < IconUpload size = "xs" /> }
481+ onClick = { ( ) => setShowJsonInput ( ! showJsonInput ) }
482+ >
483+ { showJsonInput ? t ( 'Hide JSON Input' ) : t ( 'Paste JSON' ) }
484+ </ Button >
485+ </ Flex >
486+
487+ { showJsonInput && (
488+ < JsonInputContainer >
489+ < Text size = "sm" variant = "muted" style = { { marginBottom : space ( 1 ) } } >
490+ { t (
491+ 'Paste cluster JSON data below. Accepts either a raw array of clusters or an object with a "data" property.'
492+ ) }
493+ </ Text >
494+ < TextArea
495+ value = { jsonInputValue }
496+ onChange = { ( e : React . ChangeEvent < HTMLTextAreaElement > ) => {
497+ setJsonInputValue ( e . target . value ) ;
498+ setJsonError ( null ) ;
499+ } }
500+ placeholder = { t ( 'Paste JSON here...' ) }
501+ rows = { 8 }
502+ monospace
503+ />
504+ { jsonError && (
505+ < Text size = "sm" style = { { color : 'var(--red400)' , marginTop : space ( 1 ) } } >
506+ { jsonError }
507+ </ Text >
508+ ) }
509+ < Flex gap = "sm" style = { { marginTop : space ( 1 ) } } >
510+ < Button size = "sm" priority = "primary" onClick = { handleParseJson } >
511+ { t ( 'Parse and Load' ) }
512+ </ Button >
513+ < Button
514+ size = "sm"
515+ onClick = { ( ) => {
516+ setShowJsonInput ( false ) ;
517+ setJsonError ( null ) ;
518+ } }
519+ >
520+ { t ( 'Cancel' ) }
521+ </ Button >
522+ </ Flex >
523+ </ JsonInputContainer >
524+ ) }
423525
424526 { isPending ? null : (
425527 < Fragment >
@@ -686,4 +788,23 @@ const FilterLabel = styled('span')<{disabled?: boolean}>`
686788 color: ${ p => ( p . disabled ? p . theme . disabled : p . theme . subText ) } ;
687789` ;
688790
791+ const JsonInputContainer = styled ( 'div' ) `
792+ margin-bottom: ${ space ( 2 ) } ;
793+ padding: ${ space ( 2 ) } ;
794+ background: ${ p => p . theme . backgroundSecondary } ;
795+ border: 1px solid ${ p => p . theme . border } ;
796+ border-radius: ${ p => p . theme . borderRadius } ;
797+ ` ;
798+
799+ const CustomDataBadge = styled ( 'div' ) `
800+ display: flex;
801+ align-items: center;
802+ gap: ${ space ( 0.5 ) } ;
803+ padding: ${ space ( 0.5 ) } ${ space ( 1 ) } ;
804+ background: ${ p => p . theme . yellow100 } ;
805+ border: 1px solid ${ p => p . theme . yellow300 } ;
806+ border-radius: ${ p => p . theme . borderRadius } ;
807+ color: ${ p => p . theme . yellow400 } ;
808+ ` ;
809+
689810export default DynamicGrouping ;
0 commit comments