From 8c5d329c10cefab5f31697809ba6bbe1f0b65a81 Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Mon, 1 Dec 2025 10:55:54 -0800 Subject: [PATCH 1/2] feat: add more specific tags to UI --- .../views/issueList/pages/dynamicGrouping.tsx | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/static/app/views/issueList/pages/dynamicGrouping.tsx b/static/app/views/issueList/pages/dynamicGrouping.tsx index 5ee81f99cac7fd..15c703a4bb2546 100644 --- a/static/app/views/issueList/pages/dynamicGrouping.tsx +++ b/static/app/views/issueList/pages/dynamicGrouping.tsx @@ -53,9 +53,12 @@ interface ClusterSummary { fixability_score: number | null; group_ids: number[]; issue_titles: string[]; // unused - project_ids: number[]; // unused - tags: string[]; // unused + project_ids: number[]; + tags: string[]; title: string; + code_area_tags?: string[]; + error_type_tags?: string[]; + service_tags?: string[]; } interface TopIssuesResponse { @@ -177,6 +180,27 @@ function useClusterStats(groupIds: number[]): ClusterStats { }, [groups, isPending]); } +function ClusterTags({cluster}: {cluster: ClusterSummary}) { + const hasServiceTags = cluster.service_tags && cluster.service_tags.length > 0; + const hasErrorTypeTags = cluster.error_type_tags && cluster.error_type_tags.length > 0; + const hasCodeAreaTags = cluster.code_area_tags && cluster.code_area_tags.length > 0; + + if (!hasServiceTags && !hasErrorTypeTags && !hasCodeAreaTags) { + return null; + } + + return ( + + {hasServiceTags && + cluster.service_tags!.map(tag => {tag})} + {hasErrorTypeTags && + cluster.error_type_tags!.map(tag => {tag})} + {hasCodeAreaTags && + cluster.code_area_tags!.map(tag => {tag})} + + ); +} + function ClusterIssues({groupIds}: {groupIds: number[]}) { const organization = useOrganization(); const previewGroupIds = groupIds.slice(0, 3); @@ -244,13 +268,7 @@ function ClusterCard({ )} )} - {cluster.tags && cluster.tags.length > 0 && ( - - {cluster.tags.map(tag => ( - {tag} - ))} - - )} + {issueCount} From f5886532358afdfc6dcb14537f20fc0b0ee4be78 Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Mon, 1 Dec 2025 10:59:59 -0800 Subject: [PATCH 2/2] feat: make tags filterable and clickable --- .../views/issueList/pages/dynamicGrouping.tsx | 157 +++++++++++++++++- 1 file changed, 152 insertions(+), 5 deletions(-) diff --git a/static/app/views/issueList/pages/dynamicGrouping.tsx b/static/app/views/issueList/pages/dynamicGrouping.tsx index 15c703a4bb2546..d2465e729ad593 100644 --- a/static/app/views/issueList/pages/dynamicGrouping.tsx +++ b/static/app/views/issueList/pages/dynamicGrouping.tsx @@ -180,7 +180,13 @@ function useClusterStats(groupIds: number[]): ClusterStats { }, [groups, isPending]); } -function ClusterTags({cluster}: {cluster: ClusterSummary}) { +interface ClusterTagsProps { + cluster: ClusterSummary; + onTagClick?: (tag: string) => void; + selectedTags?: Set; +} + +function ClusterTags({cluster, onTagClick, selectedTags}: ClusterTagsProps) { const hasServiceTags = cluster.service_tags && cluster.service_tags.length > 0; const hasErrorTypeTags = cluster.error_type_tags && cluster.error_type_tags.length > 0; const hasCodeAreaTags = cluster.code_area_tags && cluster.code_area_tags.length > 0; @@ -189,14 +195,30 @@ function ClusterTags({cluster}: {cluster: ClusterSummary}) { return null; } + const renderTag = (tag: string, key: string) => { + const isSelected = selectedTags?.has(tag); + return ( + { + e.stopPropagation(); + onTagClick?.(tag); + }} + isSelected={isSelected} + > + {tag} + + ); + }; + return ( {hasServiceTags && - cluster.service_tags!.map(tag => {tag})} + cluster.service_tags!.map(tag => renderTag(tag, `service-${tag}`))} {hasErrorTypeTags && - cluster.error_type_tags!.map(tag => {tag})} + cluster.error_type_tags!.map(tag => renderTag(tag, `error-${tag}`))} {hasCodeAreaTags && - cluster.code_area_tags!.map(tag => {tag})} + cluster.code_area_tags!.map(tag => renderTag(tag, `code-${tag}`))} ); } @@ -241,9 +263,13 @@ function ClusterIssues({groupIds}: {groupIds: number[]}) { function ClusterCard({ cluster, onRemove, + onTagClick, + selectedTags, }: { cluster: ClusterSummary; onRemove: (clusterId: number) => void; + onTagClick?: (tag: string) => void; + selectedTags?: Set; }) { const organization = useOrganization(); const issueCount = cluster.group_ids.length; @@ -268,7 +294,11 @@ function ClusterCard({ )} )} - + {issueCount} @@ -364,6 +394,7 @@ function DynamicGrouping() { const {teams: userTeams} = useUserTeams(); const [filterByAssignedToMe, setFilterByAssignedToMe] = useState(true); const [selectedTeamIds, setSelectedTeamIds] = useState>(new Set()); + const [selectedTags, setSelectedTags] = useState>(new Set()); const [minFixabilityScore, setMinFixabilityScore] = useState(50); const [removedClusterIds, setRemovedClusterIds] = useState(new Set()); const [showJsonInput, setShowJsonInput] = useState(false); @@ -447,6 +478,43 @@ function DynamicGrouping() { setRemovedClusterIds(prev => new Set([...prev, clusterId])); }; + const handleTagClick = (tag: string) => { + setSelectedTags(prev => { + const next = new Set(prev); + if (next.has(tag)) { + next.delete(tag); + } else { + next.add(tag); + } + return next; + }); + }; + + const handleClearTagFilter = (tag: string) => { + setSelectedTags(prev => { + const next = new Set(prev); + next.delete(tag); + return next; + }); + }; + + const handleClearAllTagFilters = () => { + setSelectedTags(new Set()); + }; + + // Helper to check if a cluster has any of the selected tags + const clusterHasSelectedTags = (cluster: ClusterSummary): boolean => { + if (selectedTags.size === 0) return true; + + const allClusterTags = [ + ...(cluster.service_tags ?? []), + ...(cluster.error_type_tags ?? []), + ...(cluster.code_area_tags ?? []), + ]; + + return Array.from(selectedTags).every(tag => allClusterTags.includes(tag)); + }; + // When using custom JSON data with filters disabled, skip all filtering and sorting const shouldSkipFilters = isUsingCustomData && disableFilters; const filteredAndSortedClusters = shouldSkipFilters @@ -458,6 +526,9 @@ function DynamicGrouping() { const fixabilityScore = (cluster.fixability_score ?? 0) * 100; if (fixabilityScore < minFixabilityScore) return false; + // Filter by selected tags + if (!clusterHasSelectedTags(cluster)) return false; + if (filterByAssignedToMe) { if (!cluster.assignedTo?.length) return false; return cluster.assignedTo.some( @@ -578,6 +649,31 @@ function DynamicGrouping() { {shouldSkipFilters && ` ${t('(filters disabled)')}`} + {selectedTags.size > 0 && ( + + + {t('Filtering by tags:')} + + + {Array.from(selectedTags).map(tag => ( + + {tag} + + + + )} + {!shouldSkipFilters && ( ))} @@ -853,4 +951,53 @@ const CustomDataBadge = styled('div')` color: ${p => p.theme.yellow400}; `; +const ClickableTag = styled(Tag)<{isSelected?: boolean}>` + cursor: pointer; + transition: + background 0.15s ease, + border-color 0.15s ease, + transform 0.1s ease, + box-shadow 0.15s ease; + user-select: none; + + ${p => + p.isSelected && + ` + background: ${p.theme.purple100}; + border-color: ${p.theme.purple300}; + color: ${p.theme.purple400}; + `} + + &:hover { + background: ${p => (p.isSelected ? p.theme.purple200 : p.theme.gray100)}; + border-color: ${p => (p.isSelected ? p.theme.purple400 : p.theme.gray300)}; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + &:active { + transform: translateY(0); + box-shadow: none; + } +`; + +const ActiveTagFilters = styled('div')` + display: flex; + align-items: center; + gap: ${space(1)}; + margin-top: ${space(1.5)}; + flex-wrap: wrap; +`; + +const ActiveTagChip = styled('div')` + display: flex; + align-items: center; + gap: ${space(0.5)}; + padding: ${space(0.25)} ${space(0.5)} ${space(0.25)} ${space(1)}; + background: ${p => p.theme.purple100}; + border: 1px solid ${p => p.theme.purple200}; + border-radius: ${p => p.theme.borderRadius}; + color: ${p => p.theme.purple400}; +`; + export default DynamicGrouping;