Skip to content

Commit f7ac1bc

Browse files
authored
feat(issues): allow pasting json top issues (#104022)
1 parent dffc673 commit f7ac1bc

File tree

1 file changed

+128
-7
lines changed

1 file changed

+128
-7
lines changed

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

Lines changed: 128 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Fragment, useMemo, useState} from 'react';
1+
import {Fragment, useCallback, useMemo, useState} from 'react';
22
import styled from '@emotion/styled';
33

44
import {Container, Flex} from '@sentry/scraps/layout';
@@ -9,6 +9,7 @@ import {Checkbox} from 'sentry/components/core/checkbox';
99
import {Disclosure} from 'sentry/components/core/disclosure';
1010
import {NumberInput} from 'sentry/components/core/input/numberInput';
1111
import {Link} from 'sentry/components/core/link';
12+
import {TextArea} from 'sentry/components/core/textarea';
1213
import EventOrGroupTitle from 'sentry/components/eventOrGroupTitle';
1314
import EventMessage from 'sentry/components/events/eventMessage';
1415
import TimesTag from 'sentry/components/group/inboxBadges/timesTag';
@@ -17,7 +18,14 @@ import ProjectBadge from 'sentry/components/idBadge/projectBadge';
1718
import LoadingIndicator from 'sentry/components/loadingIndicator';
1819
import Redirect from 'sentry/components/redirect';
1920
import 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';
2129
import {t, tn} from 'sentry/locale';
2230
import {space} from 'sentry/styles/space';
2331
import 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+
689810
export default DynamicGrouping;

0 commit comments

Comments
 (0)