From d6a37c37da4476da746bdf0ec309debf96997cd5 Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Tue, 25 Nov 2025 10:28:28 -0800 Subject: [PATCH] pasting json --- .../views/issueList/pages/dynamicGrouping.tsx | 135 +++++++++++++++++- 1 file changed, 128 insertions(+), 7 deletions(-) diff --git a/static/app/views/issueList/pages/dynamicGrouping.tsx b/static/app/views/issueList/pages/dynamicGrouping.tsx index a9e1b8a7e71f3a..72b6b4f62e41aa 100644 --- a/static/app/views/issueList/pages/dynamicGrouping.tsx +++ b/static/app/views/issueList/pages/dynamicGrouping.tsx @@ -1,4 +1,4 @@ -import {Fragment, useMemo, useState} from 'react'; +import {Fragment, useCallback, useMemo, useState} from 'react'; import styled from '@emotion/styled'; import {Container, Flex} from '@sentry/scraps/layout'; @@ -9,6 +9,7 @@ import {Checkbox} from 'sentry/components/core/checkbox'; import {Disclosure} from 'sentry/components/core/disclosure'; import {NumberInput} from 'sentry/components/core/input/numberInput'; import {Link} from 'sentry/components/core/link'; +import {TextArea} from 'sentry/components/core/textarea'; import EventOrGroupTitle from 'sentry/components/eventOrGroupTitle'; import EventMessage from 'sentry/components/events/eventMessage'; import TimesTag from 'sentry/components/group/inboxBadges/timesTag'; @@ -17,7 +18,14 @@ import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import Redirect from 'sentry/components/redirect'; import TimeSince from 'sentry/components/timeSince'; -import {IconCalendar, IconClock, IconFire, IconFix} from 'sentry/icons'; +import { + IconCalendar, + IconClock, + IconClose, + IconFire, + IconFix, + IconUpload, +} from 'sentry/icons'; import {t, tn} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Group} from 'sentry/types/group'; @@ -331,17 +339,48 @@ function DynamicGrouping() { const [filterByAssignedToMe, setFilterByAssignedToMe] = useState(true); const [selectedTeamIds, setSelectedTeamIds] = useState>(new Set()); const [minFixabilityScore, setMinFixabilityScore] = useState(50); - const [removedClusterIds, setRemovedClusterIds] = useState>(new Set()); + const [removedClusterIds, setRemovedClusterIds] = useState(new Set()); + const [showJsonInput, setShowJsonInput] = useState(false); + const [jsonInputValue, setJsonInputValue] = useState(''); + const [customClusterData, setCustomClusterData] = useState( + null + ); + const [jsonError, setJsonError] = useState(null); // Fetch cluster data from API const {data: topIssuesResponse, isPending} = useApiQuery( [`/organizations/${organization.slug}/top-issues/`], { staleTime: 60000, + enabled: customClusterData === null, // Only fetch if no custom data } ); - const clusterData = topIssuesResponse?.data ?? []; + const handleParseJson = useCallback(() => { + try { + const parsed = JSON.parse(jsonInputValue); + // Support both {data: [...]} format and direct array format + const clusters = Array.isArray(parsed) ? parsed : parsed?.data; + if (!Array.isArray(clusters)) { + setJsonError(t('JSON must be an array or have a "data" property with an array')); + return; + } + setCustomClusterData(clusters as ClusterSummary[]); + setJsonError(null); + setShowJsonInput(false); + } catch (e) { + setJsonError(t('Invalid JSON: %s', e instanceof Error ? e.message : String(e))); + } + }, [jsonInputValue]); + + const handleClearCustomData = useCallback(() => { + setCustomClusterData(null); + setJsonInputValue(''); + setJsonError(null); + }, []); + + const clusterData = customClusterData ?? topIssuesResponse?.data ?? []; + const isUsingCustomData = customClusterData !== null; // Extract all unique teams from the cluster data const teamsInData = useMemo(() => { @@ -417,9 +456,72 @@ function DynamicGrouping() { return ( - - {t('Top Issues')} - + + {t('Top Issues')} + {isUsingCustomData && ( + + + {t('Using Custom Data')} + + + + + {showJsonInput && ( + + + {t( + 'Paste cluster JSON data below. Accepts either a raw array of clusters or an object with a "data" property.' + )} + +