diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7bb4781513d95f..a2f08ede5ed774 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -678,6 +678,7 @@ tests/sentry/api/endpoints/test_organization_attribute_mappings.py @get /static/app/components/events/highlights/ @getsentry/issue-workflow /static/app/components/issues/ @getsentry/issue-workflow /static/app/components/stackTrace/ @getsentry/issue-workflow +/static/app/components/stream/supergroupRow.tsx @getsentry/issue-detection-frontend /static/app/views/issueList/ @getsentry/issue-workflow /static/app/views/issueList/issueListSeerComboBox.tsx @getsentry/issue-workflow @getsentry/machine-learning-ai /static/app/views/issueList/pages/supergroups.tsx @getsentry/issue-detection-frontend diff --git a/static/app/components/group/issueSuperGroup.tsx b/static/app/components/group/issueSuperGroup.tsx deleted file mode 100644 index 62a717a650a618..00000000000000 --- a/static/app/components/group/issueSuperGroup.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import styled from '@emotion/styled'; - -import {Container, Flex, Stack} from '@sentry/scraps/layout'; -import {Text} from '@sentry/scraps/text'; -import {Tooltip} from '@sentry/scraps/tooltip'; - -import {useDrawer} from 'sentry/components/globalDrawer'; -import {IconFocus, IconStack} from 'sentry/icons'; -import {t, tn} from 'sentry/locale'; -import {SupergroupDetailDrawer} from 'sentry/views/issueList/supergroups/supergroupDrawer'; -import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types'; - -interface Props { - supergroup: SupergroupDetail; -} - -/** - * Show a badge indicating an issue belongs to a supergroup. - */ -export function IssueSuperGroup({supergroup}: Props) { - const {openDrawer} = useDrawer(); - - const tooltipTitle = ( - - - - {supergroup.title} - - - {tn('%s issue', '%s issues', supergroup.group_ids.length)} - - - - - {supergroup.error_type ? ( - - - {t('Error:')} - - - {supergroup.error_type} - - - ) : null} - {supergroup.code_area ? ( - - - {t('Location:')} - - - {supergroup.code_area} - - - ) : null} - - - {supergroup.summary ? ( - - - - - - {t('Root Cause')} - - - - {supergroup.summary} - - - - ) : null} - - ); - - const handleClick = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - openDrawer(() => , { - ariaLabel: t('Supergroup details'), - drawerKey: 'supergroup-drawer', - }); - }; - - return ( - - - - {supergroup.group_ids.length > 50 ? '50+' : supergroup.group_ids.length} - - - ); -} - -const SuperGroupButton = styled('button')` - display: inline-flex; - align-items: center; - background: none; - border: none; - padding: 0; - cursor: pointer; - color: ${p => p.theme.colors.gray500}; - font-size: ${p => p.theme.font.size.sm}; - gap: 0 ${p => p.theme.space.xs}; - position: relative; - - &:hover { - color: ${p => p.theme.tokens.interactive.link.accent.hover}; - } -`; diff --git a/static/app/components/groupMetaRow.tsx b/static/app/components/groupMetaRow.tsx index 1a4eac3eac4821..125c66974d7f49 100644 --- a/static/app/components/groupMetaRow.tsx +++ b/static/app/components/groupMetaRow.tsx @@ -13,7 +13,6 @@ import {TimesTag} from 'sentry/components/group/inboxBadges/timesTag'; import {UnhandledTag} from 'sentry/components/group/inboxBadges/unhandledTag'; import {IssueReplayCount} from 'sentry/components/group/issueReplayCount'; import {IssueSeerBadge} from 'sentry/components/group/issueSeerBadge'; -import {IssueSuperGroup} from 'sentry/components/group/issueSuperGroup'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import {extractSelectionParameters} from 'sentry/components/pageFilters/parse'; import {Placeholder} from 'sentry/components/placeholder'; @@ -24,7 +23,6 @@ import {defined} from 'sentry/utils'; import {getTitle} from 'sentry/utils/events'; import {useReplayCountForIssues} from 'sentry/utils/replayCount/useReplayCountForIssues'; import {projectCanLinkToReplay} from 'sentry/utils/replays/projectSupportsReplay'; -import {useSuperGroupForIssues} from 'sentry/utils/supergroup/useSuperGroupForIssues'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -75,7 +73,6 @@ export function GroupMetaRow({data, showAssignee, showLifetime = true}: Props) { const issuesPath = `/organizations/${organization.slug}/issues/`; const {getReplayCountForIssue} = useReplayCountForIssues(); - const {getSuperGroupForIssue} = useSuperGroupForIssues(); const showReplayCount = organization.features.includes('session-replay') && @@ -83,10 +80,6 @@ export function GroupMetaRow({data, showAssignee, showLifetime = true}: Props) { data.issueCategory && !!getReplayCountForIssue(data.id, data.issueCategory); - const supergroup = organization.features.includes('top-issues-ui') - ? getSuperGroupForIssue(data.id) - : undefined; - const autofixRunExists = getAutofixRunExists(data); const seerFixable = isIssueQuickFixable(data); const showSeer = @@ -126,7 +119,6 @@ export function GroupMetaRow({data, showAssignee, showLifetime = true}: Props) { ) : null, showReplayCount ? : null, - supergroup ? : null, showSeer ? : null, logger ? ( diff --git a/static/app/components/stream/supergroupRow.tsx b/static/app/components/stream/supergroupRow.tsx new file mode 100644 index 00000000000000..6f07d00c579e51 --- /dev/null +++ b/static/app/components/stream/supergroupRow.tsx @@ -0,0 +1,267 @@ +import {useState} from 'react'; +import styled from '@emotion/styled'; + +import InteractionStateLayer from '@sentry/scraps/interactionStateLayer'; +import {Text} from '@sentry/scraps/text'; + +import {GroupStatusChart} from 'sentry/components/charts/groupStatusChart'; +import {Count} from 'sentry/components/count'; +import {useDrawer} from 'sentry/components/globalDrawer'; +import {PanelItem} from 'sentry/components/panels/panelItem'; +import {Placeholder} from 'sentry/components/placeholder'; +import {TimeSince} from 'sentry/components/timeSince'; +import {IconStack} from 'sentry/icons'; +import {t} from 'sentry/locale'; +import type {AggregatedSupergroupStats} from 'sentry/utils/supergroup/aggregateSupergroupStats'; +import {COLUMN_BREAKPOINTS} from 'sentry/views/issueList/actions/utils'; +import {SupergroupDetailDrawer} from 'sentry/views/issueList/supergroups/supergroupDrawer'; +import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types'; + +interface SupergroupRowProps { + matchedCount: number; + supergroup: SupergroupDetail; + aggregatedStats?: AggregatedSupergroupStats | null; +} + +export function SupergroupRow({ + supergroup, + matchedCount, + aggregatedStats, +}: SupergroupRowProps) { + const {openDrawer, isDrawerOpen} = useDrawer(); + const [isActive, setIsActive] = useState(false); + const handleClick = () => { + setIsActive(true); + openDrawer(() => , { + ariaLabel: t('Supergroup details'), + drawerKey: 'supergroup-drawer', + onClose: () => setIsActive(false), + }); + }; + + const highlighted = isActive && isDrawerOpen; + + return ( + + + + + + + {supergroup.error_type ? ( + + {supergroup.error_type} + + ) : null} + + {supergroup.title} + + + {supergroup.code_area ? ( + + {supergroup.code_area} + + ) : null} + {supergroup.code_area && matchedCount > 0 ? : null} + {matchedCount > 0 ? ( + + {matchedCount} / {supergroup.group_ids.length} {t('issues matched')} + + ) : null} + + + + + {aggregatedStats?.lastSeen ? ( + + ) : ( + + )} + + + + {aggregatedStats?.firstSeen ? ( + + ) : ( + + )} + + + + {aggregatedStats?.mergedStats && aggregatedStats.mergedStats.length > 0 ? ( + + ) : ( + + )} + + + + {aggregatedStats ? ( + + ) : ( + + )} + + + + {aggregatedStats ? ( + + ) : ( + + )} + + + + + + ); +} + +const Wrapper = styled(PanelItem)<{highlighted: boolean}>` + position: relative; + line-height: 1.1; + padding: ${p => p.theme.space.md} 0; + cursor: pointer; + min-height: 82px; + background: ${p => + p.highlighted ? p.theme.tokens.background.secondary : 'transparent'}; +`; + +const Summary = styled('div')` + overflow: hidden; + margin-left: ${p => p.theme.space.md}; + margin-right: ${p => p.theme.space['3xl']}; + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + gap: ${p => p.theme.space.xs}; + font-size: ${p => p.theme.font.size.md}; +`; + +const IconArea = styled('div')` + align-self: flex-start; + width: 32px; + display: flex; + align-items: center; + justify-content: flex-end; + flex-shrink: 0; + padding-top: ${p => p.theme.space.sm}; +`; + +const AccentIcon = styled(IconStack)` + color: ${p => p.theme.tokens.graphics.accent.vibrant}; +`; + +const MetaRow = styled('div')` + display: inline-grid; + grid-auto-flow: column dense; + gap: ${p => p.theme.space.sm}; + justify-content: start; + align-items: center; + color: ${p => p.theme.tokens.content.secondary}; + font-size: ${p => p.theme.font.size.sm}; + white-space: nowrap; + line-height: 1.2; + min-height: ${p => p.theme.space.xl}; +`; + +const Dot = styled('div')` + width: 3px; + height: 3px; + border-radius: 50%; + background: currentcolor; + flex-shrink: 0; +`; + +const LastSeenColumn = styled('div')` + display: flex; + align-items: center; + justify-content: flex-end; + width: 86px; + padding-right: ${p => p.theme.space.xl}; + margin-right: ${p => p.theme.space.xl}; + + @container (width < ${COLUMN_BREAKPOINTS.LAST_SEEN}) { + display: none; + } +`; + +const FirstSeenColumn = styled('div')` + display: flex; + align-items: center; + justify-content: flex-end; + width: 50px; + padding-right: ${p => p.theme.space.xl}; + margin-right: ${p => p.theme.space.xl}; + + @container (width < ${COLUMN_BREAKPOINTS.FIRST_SEEN}) { + display: none; + } +`; + +const ChartColumn = styled('div')` + width: 175px; + align-self: center; + margin-right: ${p => p.theme.space.xl}; + + @container (width < ${COLUMN_BREAKPOINTS.TREND}) { + display: none; + } +`; + +const DataColumn = styled('div')` + display: flex; + justify-content: flex-end; + text-align: right; + align-items: center; + align-self: center; + padding-right: ${p => p.theme.space.xl}; + margin-right: ${p => p.theme.space.xl}; + width: 60px; +`; + +const EventsColumn = styled(DataColumn)` + @container (width < ${COLUMN_BREAKPOINTS.EVENTS}) { + display: none; + } +`; + +const UsersColumn = styled(DataColumn)` + @container (width < ${COLUMN_BREAKPOINTS.USERS}) { + display: none; + } +`; + +const PrimaryCount = styled(Count)` + font-size: ${p => p.theme.font.size.md}; + display: flex; + justify-content: right; + margin-bottom: ${p => p.theme.space['2xs']}; + font-variant-numeric: tabular-nums; +`; + +// Empty spacers to match StreamGroup column widths and keep alignment +const PrioritySpacer = styled('div')` + width: 64px; + padding-right: ${p => p.theme.space.xl}; + margin-right: ${p => p.theme.space.xl}; + + @container (width < ${COLUMN_BREAKPOINTS.PRIORITY}) { + display: none; + } +`; + +const AssigneeSpacer = styled('div')` + width: 66px; + padding-right: ${p => p.theme.space.xl}; + margin-right: ${p => p.theme.space.xl}; + + @container (width < ${COLUMN_BREAKPOINTS.ASSIGNEE}) { + display: none; + } +`; diff --git a/static/app/utils/supergroup/aggregateSupergroupStats.ts b/static/app/utils/supergroup/aggregateSupergroupStats.ts new file mode 100644 index 00000000000000..09e7013875168f --- /dev/null +++ b/static/app/utils/supergroup/aggregateSupergroupStats.ts @@ -0,0 +1,58 @@ +import type {TimeseriesValue} from 'sentry/types/core'; +import type {Group} from 'sentry/types/group'; + +export interface AggregatedSupergroupStats { + eventCount: number; + firstSeen: string | null; + lastSeen: string | null; + mergedStats: TimeseriesValue[]; + userCount: number; +} + +/** + * Aggregate stats from member groups for display in a supergroup row. + * Sums event/user counts, takes min firstSeen and max lastSeen, + * and point-wise sums the trend data. + */ +export function aggregateSupergroupStats( + groups: Group[], + statsPeriod: string +): AggregatedSupergroupStats | null { + if (groups.length === 0) { + return null; + } + + let eventCount = 0; + let userCount = 0; + let firstSeen: string | null = null; + let lastSeen: string | null = null; + let mergedStats: TimeseriesValue[] = []; + + for (const group of groups) { + eventCount += parseInt(group.count, 10) || 0; + userCount += group.userCount || 0; + + const gFirstSeen = group.lifetime?.firstSeen ?? group.firstSeen; + if (gFirstSeen && (!firstSeen || gFirstSeen < firstSeen)) { + firstSeen = gFirstSeen; + } + + const gLastSeen = group.lifetime?.lastSeen ?? group.lastSeen; + if (gLastSeen && (!lastSeen || gLastSeen > lastSeen)) { + lastSeen = gLastSeen; + } + + const stats = group.stats?.[statsPeriod]; + if (stats) { + if (mergedStats.length === 0) { + mergedStats = stats.map(([ts, val]) => [ts, val] as TimeseriesValue); + } else { + for (let i = 0; i < Math.min(mergedStats.length, stats.length); i++) { + mergedStats[i] = [mergedStats[i]![0], mergedStats[i]![1] + stats[i]![1]]; + } + } + } + } + + return {eventCount, userCount, firstSeen, lastSeen, mergedStats}; +} diff --git a/static/app/utils/supergroup/useSuperGroupForIssues.tsx b/static/app/utils/supergroup/useSuperGroupForIssues.tsx deleted file mode 100644 index bab747aae8994b..00000000000000 --- a/static/app/utils/supergroup/useSuperGroupForIssues.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import {useCallback} from 'react'; - -import type {ApiResult} from 'sentry/api'; -import {getApiUrl} from 'sentry/utils/api/getApiUrl'; -import {useAggregatedQueryKeys} from 'sentry/utils/api/useAggregatedQueryKeys'; -import type {ApiQueryKey} from 'sentry/utils/queryClient'; -import {useOrganization} from 'sentry/utils/useOrganization'; -import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types'; - -type SupergroupState = Record; - -function supergroupReducer( - prevState: undefined | SupergroupState, - response: ApiResult<{data: SupergroupDetail[]}>, - aggregates: readonly string[] -): undefined | SupergroupState { - const defaults = Object.fromEntries( - aggregates.map(id => [id, null]) - ) as SupergroupState; - const supergroups = response[0].data; - const byGroupId: SupergroupState = Object.fromEntries( - supergroups.flatMap(sg => sg.group_ids.map(groupId => [String(groupId), sg])) - ); - return {...defaults, ...prevState, ...byGroupId}; -} - -/** - * Query results for whether an Issue/Group belongs to a supergroup. - */ -export function useSuperGroupForIssues() { - const organization = useOrganization(); - - const cache = useAggregatedQueryKeys({ - cacheKey: `/organizations/${organization.slug}/seer/supergroups/by-group/`, - bufferLimit: 25, - getQueryKey: useCallback( - (ids: readonly string[]): ApiQueryKey => [ - getApiUrl('/organizations/$organizationIdOrSlug/seer/supergroups/by-group/', { - path: {organizationIdOrSlug: organization.slug}, - }), - { - query: { - group_id: ids, - }, - }, - ], - [organization.slug] - ), - responseReducer: supergroupReducer, - }); - - const getSuperGroupForIssue = useCallback( - (id: string): SupergroupDetail | null | undefined => { - cache.buffer([id]); - return cache.data?.[id]; - }, - [cache] - ); - - return {getSuperGroupForIssue}; -} diff --git a/static/app/utils/supergroup/useSuperGroups.tsx b/static/app/utils/supergroup/useSuperGroups.tsx new file mode 100644 index 00000000000000..664ea33df0a662 --- /dev/null +++ b/static/app/utils/supergroup/useSuperGroups.tsx @@ -0,0 +1,58 @@ +import {useMemo} from 'react'; + +import {getApiUrl} from 'sentry/utils/api/getApiUrl'; +import {useApiQuery} from 'sentry/utils/queryClient'; +import {useOrganization} from 'sentry/utils/useOrganization'; +import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types'; + +export type SupergroupLookup = Record; + +/** + * Fetch supergroup assignments for a batch of group IDs. + * Returns a lookup map and loading state so callers can block rendering + * until the data is available, preventing pop-in when issues are regrouped. + */ +export function useSuperGroups(groupIds: string[]): { + data: SupergroupLookup; + isLoading: boolean; +} { + const organization = useOrganization(); + const hasTopIssuesUI = organization.features.includes('top-issues-ui'); + const enabled = hasTopIssuesUI && groupIds.length > 0; + + const {data: response, isLoading} = useApiQuery<{data: SupergroupDetail[]}>( + [ + getApiUrl('/organizations/$organizationIdOrSlug/seer/supergroups/by-group/', { + path: {organizationIdOrSlug: organization.slug}, + }), + { + query: { + group_id: groupIds, + }, + }, + ], + { + staleTime: 30_000, + enabled, + } + ); + + const lookup = useMemo(() => { + if (!response?.data) { + return {}; + } + + const result: SupergroupLookup = Object.fromEntries(groupIds.map(id => [id, null])); + for (const sg of response.data) { + for (const groupId of sg.group_ids) { + result[String(groupId)] = sg; + } + } + return result; + }, [response, groupIds]); + + return { + data: lookup, + isLoading: enabled && isLoading, + }; +} diff --git a/static/app/views/issueList/groupListBody.tsx b/static/app/views/issueList/groupListBody.tsx index b7fe8cd7f099ad..8690b712f67d45 100644 --- a/static/app/views/issueList/groupListBody.tsx +++ b/static/app/views/issueList/groupListBody.tsx @@ -1,3 +1,4 @@ +import {useMemo} from 'react'; import {useTheme} from '@emotion/react'; import type {IndexedMembersByProject} from 'sentry/actionCreators/members'; @@ -5,12 +6,16 @@ import type {GroupListColumn} from 'sentry/components/issues/groupList'; import {LoadingError} from 'sentry/components/loadingError'; import {PanelBody} from 'sentry/components/panels/panelBody'; import {LoadingStreamGroup, StreamGroup} from 'sentry/components/stream/group'; +import {SupergroupRow} from 'sentry/components/stream/supergroupRow'; import {GroupStore} from 'sentry/stores/groupStore'; import type {Group} from 'sentry/types/group'; +import {aggregateSupergroupStats} from 'sentry/utils/supergroup/aggregateSupergroupStats'; +import type {SupergroupLookup} from 'sentry/utils/supergroup/useSuperGroups'; import {useApi} from 'sentry/utils/useApi'; import {useMedia} from 'sentry/utils/useMedia'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useSyncedLocalStorageState} from 'sentry/utils/useSyncedLocalStorageState'; +import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types'; import type {IssueUpdateData} from 'sentry/views/issueList/types'; import {NoGroupsHandler} from './noGroupsHandler'; @@ -28,6 +33,7 @@ type GroupListBodyProps = { query: string; refetchGroups: () => void; selectedProjectIds: number[]; + supergroupLookup?: SupergroupLookup; }; type GroupListProps = { @@ -37,6 +43,7 @@ type GroupListProps = { memberList: IndexedMembersByProject; onActionTaken: (itemIds: string[], data: IssueUpdateData) => void; query: string; + supergroupLookup?: SupergroupLookup; }; const COLUMNS: GroupListColumn[] = [ @@ -50,6 +57,10 @@ const COLUMNS: GroupListColumn[] = [ 'lastTriggered', ]; +type RenderItem = + | {id: string; type: 'issue'} + | {matchingIds: string[]; supergroup: SupergroupDetail; type: 'supergroup'}; + function LoadingSkeleton({ pageSize, displayReprocessingLayout, @@ -82,6 +93,7 @@ export function GroupListBody({ selectedProjectIds, pageSize, onActionTaken, + supergroupLookup, }: GroupListBodyProps) { const api = useApi(); const organization = useOrganization(); @@ -119,10 +131,42 @@ export function GroupListBody({ displayReprocessingLayout={displayReprocessingLayout} groupStatsPeriod={groupStatsPeriod} onActionTaken={onActionTaken} + supergroupLookup={supergroupLookup} /> ); } +function buildRenderItems( + groupIds: string[], + getSuperGroupForIssue: (id: string) => SupergroupDetail | null | undefined, + enabled: boolean +): RenderItem[] { + if (!enabled) { + return groupIds.map(id => ({type: 'issue' as const, id})); + } + + const seen = new Map(); + const items: RenderItem[] = []; + + for (const id of groupIds) { + const sg = getSuperGroupForIssue(id); + if (sg && sg.group_ids.length > 1) { + const existing = seen.get(sg.id); + if (existing) { + existing.push(id); + } else { + const matchingIds = [id]; + seen.set(sg.id, matchingIds); + items.push({type: 'supergroup', supergroup: sg, matchingIds}); + } + } else { + items.push({type: 'issue', id}); + } + } + + return items; +} + function GroupList({ groupIds, memberList, @@ -130,8 +174,10 @@ function GroupList({ displayReprocessingLayout, groupStatsPeriod, onActionTaken, + supergroupLookup, }: GroupListProps) { const theme = useTheme(); + const organization = useOrganization(); const [isSavedSearchesOpen] = useSyncedLocalStorageState( SAVED_SEARCHES_SIDEBAR_OPEN_LOCALSTORAGE_KEY, false @@ -141,29 +187,58 @@ function GroupList({ `(width < ${isSavedSearchesOpen ? theme.breakpoints.xl : theme.breakpoints.md})` ); + const hasTopIssuesUI = organization.features.includes('top-issues-ui'); + const renderItems = useMemo( + () => + buildRenderItems( + groupIds, + (id: string) => supergroupLookup?.[id] ?? null, + hasTopIssuesUI + ), + [groupIds, supergroupLookup, hasTopIssuesUI] + ); + + const renderStreamGroup = (id: string, columns: GroupListColumn[]) => { + const group = GroupStore.get(id) as Group | undefined; + if (!group) { + return null; + } + return ( + onActionTaken([id], {priority})} + withColumns={columns} + /> + ); + }; + return ( - {groupIds.map(id => { - const hasGuideAnchor = id === topIssue; - const group = GroupStore.get(id) as Group | undefined; - - if (!group) { - return null; + {renderItems.map(item => { + if (item.type === 'issue') { + return renderStreamGroup(item.id, COLUMNS); } + const {supergroup, matchingIds} = item; + const memberGroups = matchingIds + .map(id => GroupStore.get(id) as Group | undefined) + .filter((g): g is Group => g !== undefined); + const stats = aggregateSupergroupStats(memberGroups, groupStatsPeriod); + return ( - onActionTaken([id], {priority})} - withColumns={COLUMNS} + ); })} diff --git a/static/app/views/issueList/issueListTable.tsx b/static/app/views/issueList/issueListTable.tsx index c3419f993bbe0b..b81579c6ed50ac 100644 --- a/static/app/views/issueList/issueListTable.tsx +++ b/static/app/views/issueList/issueListTable.tsx @@ -10,6 +10,7 @@ import {t} from 'sentry/locale'; import type {PageFilters} from 'sentry/types/core'; import {DemoTourElement, DemoTourStep} from 'sentry/utils/demoMode/demoTours'; import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry'; +import type {SupergroupLookup} from 'sentry/utils/supergroup/useSuperGroups'; import {useLocation} from 'sentry/utils/useLocation'; import {IssueListActions} from 'sentry/views/issueList/actions'; import {GroupListBody} from 'sentry/views/issueList/groupListBody'; @@ -40,6 +41,7 @@ interface IssueListTableProps { selection: PageFilters; statsLoading: boolean; statsPeriod: string; + supergroupLookup?: SupergroupLookup; } export function IssueListTable({ @@ -64,6 +66,7 @@ export function IssueListTable({ paginationAnalyticsEvent, issuesSuccessfullyLoaded, pageSize, + supergroupLookup, }: IssueListTableProps) { const location = useLocation(); @@ -124,6 +127,7 @@ export function IssueListTable({ pageSize={pageSize} refetchGroups={refetchGroups} onActionTaken={onActionTaken} + supergroupLookup={supergroupLookup} /> diff --git a/static/app/views/issueList/overview.tsx b/static/app/views/issueList/overview.tsx index fc9dd16ae81b3d..7701534b39a2be 100644 --- a/static/app/views/issueList/overview.tsx +++ b/static/app/views/issueList/overview.tsx @@ -37,6 +37,7 @@ import type {RequestError} from 'sentry/utils/requestError/requestError'; import {useDisableRouteAnalytics} from 'sentry/utils/routeAnalytics/useDisableRouteAnalytics'; import {useRouteAnalyticsEventNames} from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames'; import {useRouteAnalyticsParams} from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams'; +import {useSuperGroups} from 'sentry/utils/supergroup/useSuperGroups'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useApi} from 'sentry/utils/useApi'; import {useLocation} from 'sentry/utils/useLocation'; @@ -163,6 +164,9 @@ function IssueListOverview({ useIssuesINPObserver(); + const {data: supergroupLookup, isLoading: supergroupsLoading} = + useSuperGroups(groupIds); + const onRealtimePoll = useCallback( (data: any, {queryCount: newQueryCount}: {queryCount: number}) => { // Note: We do not update state with cursors from polling, @@ -900,8 +904,9 @@ function IssueListOverview({ displayReprocessingActions={displayReprocessingActions} memberList={memberList} selectedProjectIds={selection.projects} - issuesLoading={issuesLoading} + issuesLoading={issuesLoading || supergroupsLoading} statsLoading={statsLoading} + supergroupLookup={supergroupLookup} error={error} refetchGroups={fetchData} paginationCaption={ diff --git a/static/app/views/issueList/supergroups/supergroupDrawer.tsx b/static/app/views/issueList/supergroups/supergroupDrawer.tsx index a238000fad6326..885bdd9524464a 100644 --- a/static/app/views/issueList/supergroups/supergroupDrawer.tsx +++ b/static/app/views/issueList/supergroups/supergroupDrawer.tsx @@ -22,10 +22,7 @@ import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types'; export function SupergroupDetailDrawer({supergroup}: {supergroup: SupergroupDetail}) { const organization = useOrganization(); const placeholderRows = Math.min(supergroup.group_ids.length, 10); - const issueIdQuery = - supergroup.group_ids.length === 1 - ? `issue.id:${supergroup.group_ids[0]}` - : `issue.id:[${supergroup.group_ids.join(',')}]`; + const issueIdQuery = `issue.id:[${supergroup.group_ids.join(',')}]`; return (