From ab949318dec8d48991f53a2c291bc5d4f9b93291 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Thu, 9 Apr 2026 16:17:54 -0700 Subject: [PATCH 1/7] ref(supergroups): Use issues search to load and highlight supergroup drawer issues Replace the `group=id` param approach with `issue.id:[ids]` search queries. The drawer now makes two requests: one to load all supergroup issues, and a second with the current stream query + filters to determine which ones match. Matched issues sort to the top and get highlighted. When opened from issue details (no stream context), only the load request fires. Co-Authored-By: Claude Opus 4.6 --- .../stream/supergroups/supergroupRow.tsx | 22 +-- .../streamline/sidebar/supergroupSection.tsx | 13 +- static/app/views/issueList/groupListBody.tsx | 1 + .../supergroups/supergroupDrawer.tsx | 175 ++++++++++-------- 4 files changed, 109 insertions(+), 102 deletions(-) diff --git a/static/app/components/stream/supergroups/supergroupRow.tsx b/static/app/components/stream/supergroups/supergroupRow.tsx index a94a23bcfb5995..2fa318c7b0039c 100644 --- a/static/app/components/stream/supergroups/supergroupRow.tsx +++ b/static/app/components/stream/supergroups/supergroupRow.tsx @@ -26,6 +26,7 @@ import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types'; interface SupergroupRowProps { matchedGroupIds: string[]; + query: string; supergroup: SupergroupDetail; aggregatedStats?: AggregatedSupergroupStats | null; memberList?: IndexedMembersByProject; @@ -36,6 +37,7 @@ export function SupergroupRow({ matchedGroupIds, aggregatedStats, memberList, + query, }: SupergroupRowProps) { const matchedCount = matchedGroupIds.length; const {openDrawer, isDrawerOpen} = useDrawer(); @@ -46,8 +48,8 @@ export function SupergroupRow({ () => ( ), { @@ -85,20 +87,16 @@ export function SupergroupRow({ /> - {supergroup.error_type ? ( - - {supergroup.error_type} - - ) : null} - + {supergroup.title} + + {supergroup.error_type} + - {supergroup.code_area ? ( - - {supergroup.code_area} - - ) : null} + + {supergroup.code_area} + {supergroup.code_area && matchedCount > 0 ? : null} {matchedCount > 0 ? ( { - openDrawer( - () => ( - - ), - { - ariaLabel: t('Supergroup details'), - drawerKey: 'supergroup-drawer', - } - ); + openDrawer(() => , { + ariaLabel: t('Supergroup details'), + drawerKey: 'supergroup-drawer', + }); }; return ( diff --git a/static/app/views/issueList/groupListBody.tsx b/static/app/views/issueList/groupListBody.tsx index cb7d48cadff080..471b9fe21044fb 100644 --- a/static/app/views/issueList/groupListBody.tsx +++ b/static/app/views/issueList/groupListBody.tsx @@ -240,6 +240,7 @@ function GroupList({ matchedGroupIds={matchingIds} aggregatedStats={stats} memberList={memberList} + query={query} /> ); })} diff --git a/static/app/views/issueList/supergroups/supergroupDrawer.tsx b/static/app/views/issueList/supergroups/supergroupDrawer.tsx index 45028396f09d25..6ef15cb78a16fb 100644 --- a/static/app/views/issueList/supergroups/supergroupDrawer.tsx +++ b/static/app/views/issueList/supergroups/supergroupDrawer.tsx @@ -1,4 +1,4 @@ -import {Fragment, useMemo, useState} from 'react'; +import {Fragment, useState} from 'react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; @@ -27,17 +27,15 @@ import { } from 'sentry/components/stream/group'; import {IconChevron, IconFocus} from 'sentry/icons'; import {t} from 'sentry/locale'; -import {GroupStore} from 'sentry/stores/groupStore'; import type {Group} from 'sentry/types/group'; import {getApiUrl} from 'sentry/utils/api/getApiUrl'; import {MarkedText} from 'sentry/utils/marked/markedText'; import {useApiQuery} from 'sentry/utils/queryClient'; +import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {SupergroupFeedback} from 'sentry/views/issueList/supergroups/supergroupFeedback'; import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types'; -const PAGE_SIZE = 20; - const DRAWER_COLUMNS: GroupListColumn[] = [ 'event', 'users', @@ -47,15 +45,15 @@ const DRAWER_COLUMNS: GroupListColumn[] = [ ]; interface SupergroupDetailDrawerProps { - matchedGroupIds: string[]; supergroup: SupergroupDetail; memberList?: IndexedMembersByProject; + query?: string; } export function SupergroupDetailDrawer({ supergroup, - matchedGroupIds, memberList, + query, }: SupergroupDetailDrawerProps) { return ( @@ -127,8 +125,8 @@ export function SupergroupDetailDrawer({ )} @@ -137,121 +135,136 @@ export function SupergroupDetailDrawer({ ); } +const PAGE_SIZE = 25; + function SupergroupIssueList({ groupIds, - matchedGroupIds, memberList, + query, }: { groupIds: number[]; - matchedGroupIds: string[]; memberList?: IndexedMembersByProject; + query?: string; }) { const organization = useOrganization(); + const location = useLocation(); const [page, setPage] = useState(0); - // Sort: matched first, then other loaded groups, then unloaded - const {sortedGroupIds, loadedIds} = useMemo(() => { - const matched: number[] = []; - const loaded: number[] = []; - const cachedIds = new Set(); - const unloaded: number[] = []; - - for (const id of groupIds) { - const strId = String(id); - if (GroupStore.get(strId)) { - cachedIds.add(strId); - if (matchedGroupIds.includes(strId)) { - matched.push(id); - } else { - loaded.push(id); - } - } else { - unloaded.push(id); - } - } - - return {sortedGroupIds: [...matched, ...loaded, ...unloaded], loadedIds: cachedIds}; - }, [groupIds, matchedGroupIds]); + const hasQuery = query !== undefined; + const issueIdFilter = `issue.id:[${groupIds.join(',')}]`; + const totalPages = Math.ceil(groupIds.length / PAGE_SIZE); + const pageGroupIds = groupIds.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE); - const totalPages = Math.ceil(sortedGroupIds.length / PAGE_SIZE); - const pageGroupIds = sortedGroupIds.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE); - const pageUnloadedIds = pageGroupIds.filter(id => !loadedIds.has(String(id))); + const {project, environment, statsPeriod, start, end} = location.query; - const {data: fetchedGroups, isPending} = useApiQuery( + // Fetch all groups on this page + const {data: allGroups, isPending: allPending} = useApiQuery( [ getApiUrl('/organizations/$organizationIdOrSlug/issues/', { path: {organizationIdOrSlug: organization.slug}, }), { query: { - group: pageUnloadedIds.map(String), + query: issueIdFilter, project: ALL_ACCESS_PROJECTS, + limit: PAGE_SIZE, + }, + }, + ], + {staleTime: 30_000} + ); + + // Search with the stream query to find which ones match + const {data: matchedGroups, isPending: matchPending} = useApiQuery( + [ + getApiUrl('/organizations/$organizationIdOrSlug/issues/', { + path: {organizationIdOrSlug: organization.slug}, + }), + { + query: { + project, + environment, + statsPeriod, + start, + end, + query: `${query} ${issueIdFilter}`, + limit: groupIds.length, }, }, ], - { - staleTime: 30_000, - enabled: pageUnloadedIds.length > 0, - } + {staleTime: 30_000, enabled: hasQuery} ); + const isPending = allPending || (hasQuery && matchPending); + + if (isPending) { + return ( + + + + {pageGroupIds.map(id => ( + + + + ))} + + + ); + } + + const matchedIds = new Set(matchedGroups?.map(g => g.id)); + const groupMap = new Map(allGroups?.map(g => [g.id, g])); + + // Sort: matched first, then the rest + const sortedGroups = [...pageGroupIds] + .map(id => groupMap.get(String(id))) + .filter((g): g is Group => g !== undefined) + .sort((a, b) => { + const aMatched = matchedIds.has(a.id); + const bMatched = matchedIds.has(b.id); + if (aMatched !== bMatched) { + return aMatched ? -1 : 1; + } + return 0; + }); + return ( - {matchedGroupIds.length > 0 && ( + {matchedIds.size > 0 && ( - {t('Visible in current results')} + {t('Matches current filters')} )} - {pageGroupIds.map(id => { - const strId = String(id); - const group = - (GroupStore.get(strId) as Group | undefined) ?? - fetchedGroups?.find(g => g.id === strId); - - if (group) { - const members = memberList?.[group.project?.slug] - ? memberList[group.project.slug] - : undefined; - return ( - - - - ); - } - - if (isPending) { - return ( - - - - ); - } - - return null; + {sortedGroups.map(group => { + const members = memberList?.[group.project?.slug] + ? memberList[group.project.slug] + : undefined; + return ( + + + + ); })} {totalPages > 1 && ( - {`${page * PAGE_SIZE + 1}-${Math.min((page + 1) * PAGE_SIZE, sortedGroupIds.length)} of ${sortedGroupIds.length}`} + {`${page * PAGE_SIZE + 1}-${Math.min((page + 1) * PAGE_SIZE, groupIds.length)} of ${groupIds.length}`}