From a9b85248271a59f46958ba8fa223dbecc00f2dc9 Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Mon, 24 Nov 2025 08:34:50 -0800 Subject: [PATCH 1/2] use endpoint instead of copy pasting --- .../views/issueList/pages/dynamicGrouping.tsx | 201 +++++++----------- 1 file changed, 77 insertions(+), 124 deletions(-) diff --git a/static/app/views/issueList/pages/dynamicGrouping.tsx b/static/app/views/issueList/pages/dynamicGrouping.tsx index 9c7a5875bb3086..400d37861f94de 100644 --- a/static/app/views/issueList/pages/dynamicGrouping.tsx +++ b/static/app/views/issueList/pages/dynamicGrouping.tsx @@ -1,11 +1,10 @@ -import {Fragment, useEffect, useMemo, useState} from 'react'; +import {Fragment, useMemo, useState} from 'react'; import styled from '@emotion/styled'; import {Container, Flex} from '@sentry/scraps/layout'; import {Heading, Text} from '@sentry/scraps/text'; import {Breadcrumbs} from 'sentry/components/breadcrumbs'; -import {Alert} from 'sentry/components/core/alert'; import {Button} from 'sentry/components/core/button'; import {Checkbox} from 'sentry/components/core/checkbox'; import {Disclosure} from 'sentry/components/core/disclosure'; @@ -32,7 +31,7 @@ import useOrganization from 'sentry/utils/useOrganization'; import {useUser} from 'sentry/utils/useUser'; import {useUserTeams} from 'sentry/utils/useUserTeams'; -const STORAGE_KEY = 'dynamic-grouping-cluster-data'; +const REMOVED_CLUSTERS_KEY = 'dynamic-grouping-removed-clusters'; interface AssignedEntity { email: string | null; @@ -52,11 +51,14 @@ interface ClusterSummary { group_ids: number[]; issue_titles: string[]; project_ids: number[]; - summary: string; tags: string[]; title: string; } +interface TopIssuesResponse { + data: ClusterSummary[]; +} + // Compact issue preview for dynamic grouping - no short ID or quick fix icon function CompactIssuePreview({group}: {group: Group}) { const organization = useOrganization(); @@ -215,7 +217,7 @@ function ClusterCard({ - {cluster.summary} + {cluster.description} {cluster.fixability_score !== null && ( {t('%s%% confidence', Math.round(cluster.fixability_score * 100))} @@ -267,52 +269,38 @@ function DynamicGrouping() { const organization = useOrganization(); const user = useUser(); const {teams} = useUserTeams(); - const [jsonInput, setJsonInput] = useState(''); - const [clusterData, setClusterData] = useState([]); - const [parseError, setParseError] = useState(null); - const [showInput, setShowInput] = useState(true); const [filterByAssignedToMe, setFilterByAssignedToMe] = useState(true); const [minFixabilityScore, setMinFixabilityScore] = useState(50); const [removingClusterId, setRemovingClusterId] = useState(null); - - // Load from localStorage on mount - useEffect(() => { - const stored = localStorage.getItem(STORAGE_KEY); + const [removedClusterIds, setRemovedClusterIds] = useState>(() => { + // Load removed cluster IDs from localStorage on mount + const stored = localStorage.getItem(REMOVED_CLUSTERS_KEY); if (stored) { try { - const parsed = JSON.parse(stored); - setClusterData(parsed); - setJsonInput(stored); - setShowInput(false); + return new Set(JSON.parse(stored)); } catch { - // If stored data is invalid, clear it - localStorage.removeItem(STORAGE_KEY); + return new Set(); } } - }, []); - - const handleJsonSubmit = () => { - try { - const parsed = JSON.parse(jsonInput); - if (!Array.isArray(parsed)) { - setParseError(t('JSON must be an array of cluster summaries')); - return; - } - setClusterData(parsed); - setParseError(null); - setShowInput(false); - localStorage.setItem(STORAGE_KEY, jsonInput); - } catch (error) { - setParseError(error instanceof Error ? error.message : t('Invalid JSON format')); + return new Set(); + }); + + // Fetch cluster data from API + const {data: topIssuesResponse, isPending} = useApiQuery( + [`/organizations/${organization.slug}/top-issues/`], + { + staleTime: 60000, // Cache for 1 minute } - }; + ); - const handleClear = () => { - setClusterData([]); - setJsonInput(''); - setParseError(null); - setShowInput(true); - localStorage.removeItem(STORAGE_KEY); + const clusterData = useMemo( + () => topIssuesResponse?.data ?? [], + [topIssuesResponse?.data] + ); + + const handleClearRemovedClusters = () => { + setRemovedClusterIds(new Set()); + localStorage.removeItem(REMOVED_CLUSTERS_KEY); }; const handleRemoveCluster = (clusterId: number) => { @@ -321,18 +309,16 @@ function DynamicGrouping() { // Wait for animation to complete before removing setTimeout(() => { - const updatedClusters = clusterData.filter( - cluster => cluster.cluster_id !== clusterId - ); - setClusterData(updatedClusters); + const updatedRemovedIds = new Set(removedClusterIds); + updatedRemovedIds.add(clusterId); + setRemovedClusterIds(updatedRemovedIds); setRemovingClusterId(null); - // Update localStorage with the new data - if (updatedClusters.length > 0) { - localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedClusters)); - } else { - localStorage.removeItem(STORAGE_KEY); - } + // Persist removed cluster IDs to localStorage + localStorage.setItem( + REMOVED_CLUSTERS_KEY, + JSON.stringify(Array.from(updatedRemovedIds)) + ); }, 300); // Match the animation duration }; @@ -364,6 +350,11 @@ function DynamicGrouping() { const filteredAndSortedClusters = useMemo(() => { return [...clusterData] .filter((cluster: ClusterSummary) => { + // Filter out removed clusters + if (removedClusterIds.has(cluster.cluster_id)) { + return false; + } + // Filter by fixability score - hide clusters below threshold const fixabilityScore = (cluster.fixability_score ?? 0) * 100; if (fixabilityScore < minFixabilityScore) { @@ -380,7 +371,13 @@ function DynamicGrouping() { (a: ClusterSummary, b: ClusterSummary) => (b.fixability_score ?? 0) - (a.fixability_score ?? 0) ); - }, [clusterData, filterByAssignedToMe, minFixabilityScore, isClusterAssignedToMe]); + }, [ + clusterData, + removedClusterIds, + filterByAssignedToMe, + minFixabilityScore, + isClusterAssignedToMe, + ]); const totalIssues = useMemo(() => { return filteredAndSortedClusters.reduce( @@ -413,48 +410,20 @@ function DynamicGrouping() {
{t('Top Issues')}
- {clusterData.length > 0 && !showInput && ( - - - - + {removedClusterIds.size > 0 && ( + )} - {showInput || clusterData.length === 0 ? ( - - - - {t('Paste cluster summaries JSON data below:')} - - setJsonInput(e.target.value)} - placeholder={t('Paste JSON array here...')} - rows={10} - /> - {parseError && {parseError}} - - - {clusterData.length > 0 && ( - - )} - - - + {isPending ? ( + + {[0, 1, 2].map(i => ( + + ))} + ) : ( @@ -512,16 +481,24 @@ function DynamicGrouping() { - - {filteredAndSortedClusters.map((cluster: ClusterSummary) => ( - - ))} - + {filteredAndSortedClusters.length === 0 ? ( + + + {t('No clusters match the current filters')} + + + ) : ( + + {filteredAndSortedClusters.map((cluster: ClusterSummary) => ( + + ))} + + )} )} @@ -742,30 +719,6 @@ const EventCount = styled('span')` font-weight: ${p => p.theme.fontWeight.bold}; `; -const JsonTextarea = styled('textarea')` - width: 100%; - min-height: 300px; - padding: ${space(1.5)}; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; - font-size: 13px; - line-height: 1.5; - background: ${p => p.theme.backgroundSecondary}; - border: 1px solid ${p => p.theme.border}; - border-radius: ${p => p.theme.borderRadius}; - color: ${p => p.theme.textColor}; - resize: vertical; - - &:focus { - outline: none; - border-color: ${p => p.theme.purple300}; - box-shadow: 0 0 0 1px ${p => p.theme.purple300}; - } - - &::placeholder { - color: ${p => p.theme.subText}; - } -`; - const AdvancedFilterContent = styled('div')` padding: ${space(2)} 0; display: flex; From b79f4c8c2b2d18fe4cceb50229b9dca5401815c2 Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Mon, 24 Nov 2025 08:36:33 -0800 Subject: [PATCH 2/2] remove localstorage stuff --- .../views/issueList/pages/dynamicGrouping.tsx | 37 +------------------ 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/static/app/views/issueList/pages/dynamicGrouping.tsx b/static/app/views/issueList/pages/dynamicGrouping.tsx index 400d37861f94de..57ba9ccfe20d19 100644 --- a/static/app/views/issueList/pages/dynamicGrouping.tsx +++ b/static/app/views/issueList/pages/dynamicGrouping.tsx @@ -31,8 +31,6 @@ import useOrganization from 'sentry/utils/useOrganization'; import {useUser} from 'sentry/utils/useUser'; import {useUserTeams} from 'sentry/utils/useUserTeams'; -const REMOVED_CLUSTERS_KEY = 'dynamic-grouping-removed-clusters'; - interface AssignedEntity { email: string | null; id: string; @@ -272,18 +270,7 @@ function DynamicGrouping() { const [filterByAssignedToMe, setFilterByAssignedToMe] = useState(true); const [minFixabilityScore, setMinFixabilityScore] = useState(50); const [removingClusterId, setRemovingClusterId] = useState(null); - const [removedClusterIds, setRemovedClusterIds] = useState>(() => { - // Load removed cluster IDs from localStorage on mount - const stored = localStorage.getItem(REMOVED_CLUSTERS_KEY); - if (stored) { - try { - return new Set(JSON.parse(stored)); - } catch { - return new Set(); - } - } - return new Set(); - }); + const [removedClusterIds, setRemovedClusterIds] = useState>(new Set()); // Fetch cluster data from API const {data: topIssuesResponse, isPending} = useApiQuery( @@ -298,11 +285,6 @@ function DynamicGrouping() { [topIssuesResponse?.data] ); - const handleClearRemovedClusters = () => { - setRemovedClusterIds(new Set()); - localStorage.removeItem(REMOVED_CLUSTERS_KEY); - }; - const handleRemoveCluster = (clusterId: number) => { // Start animation setRemovingClusterId(clusterId); @@ -313,12 +295,6 @@ function DynamicGrouping() { updatedRemovedIds.add(clusterId); setRemovedClusterIds(updatedRemovedIds); setRemovingClusterId(null); - - // Persist removed cluster IDs to localStorage - localStorage.setItem( - REMOVED_CLUSTERS_KEY, - JSON.stringify(Array.from(updatedRemovedIds)) - ); }, 300); // Match the animation duration }; @@ -406,16 +382,7 @@ function DynamicGrouping() { /> - -
- {t('Top Issues')} -
- {removedClusterIds.size > 0 && ( - - )} -
+ {t('Top Issues')}
{isPending ? (