diff --git a/static/app/components/stream/supergroups/supergroupCheckbox.tsx b/static/app/components/stream/supergroups/supergroupCheckbox.tsx deleted file mode 100644 index d0aba581ff380a..00000000000000 --- a/static/app/components/stream/supergroups/supergroupCheckbox.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import {useCallback} from 'react'; -import styled from '@emotion/styled'; - -import {Checkbox} from '@sentry/scraps/checkbox'; - -import {t} from 'sentry/locale'; -import {useOptionalIssueSelectionActions} from 'sentry/views/issueList/issueSelectionContext'; - -interface SupergroupCheckboxProps { - matchedGroupIds: string[]; - selectedCount: number; -} - -export function SupergroupCheckbox({ - matchedGroupIds, - selectedCount, -}: SupergroupCheckboxProps) { - const actions = useOptionalIssueSelectionActions(); - - const checkedState = - selectedCount === 0 - ? false - : selectedCount === matchedGroupIds.length - ? true - : ('indeterminate' as const); - - const handleChange = useCallback( - (evt: React.MouseEvent) => { - evt.stopPropagation(); - const nextValue = checkedState !== true; - actions?.setSelectionForIds(matchedGroupIds, nextValue); - }, - [actions, matchedGroupIds, checkedState] - ); - - if (!actions) { - return null; - } - - return ( - - {}} - onClick={handleChange} - /> - - ); -} - -export const CheckboxLabel = styled('label')` - margin: 0; - display: flex; - align-items: center; - justify-content: center; -`; - -const CheckboxWithBackground = styled(Checkbox)` - background-color: ${p => p.theme.tokens.background.primary}; -`; diff --git a/static/app/components/stream/supergroups/supergroupRow.tsx b/static/app/components/stream/supergroups/supergroupRow.tsx index a94a23bcfb5995..9ded1f077c0fbe 100644 --- a/static/app/components/stream/supergroups/supergroupRow.tsx +++ b/static/app/components/stream/supergroups/supergroupRow.tsx @@ -1,4 +1,4 @@ -import {useMemo, useState} from 'react'; +import {useState} from 'react'; import styled from '@emotion/styled'; import InteractionStateLayer from '@sentry/scraps/interactionStateLayer'; @@ -11,21 +11,15 @@ 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 { - CheckboxLabel, - SupergroupCheckbox, -} from 'sentry/components/stream/supergroups/supergroupCheckbox'; import {TimeSince} from 'sentry/components/timeSince'; import {IconStack} from 'sentry/icons'; import {t} from 'sentry/locale'; import {COLUMN_BREAKPOINTS} from 'sentry/views/issueList/actions/utils'; -import {useOptionalIssueSelectionSummary} from 'sentry/views/issueList/issueSelectionContext'; import type {AggregatedSupergroupStats} from 'sentry/views/issueList/supergroups/aggregateSupergroupStats'; import {SupergroupDetailDrawer} from 'sentry/views/issueList/supergroups/supergroupDrawer'; import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types'; interface SupergroupRowProps { - matchedGroupIds: string[]; supergroup: SupergroupDetail; aggregatedStats?: AggregatedSupergroupStats | null; memberList?: IndexedMembersByProject; @@ -33,11 +27,9 @@ interface SupergroupRowProps { export function SupergroupRow({ supergroup, - matchedGroupIds, aggregatedStats, memberList, }: SupergroupRowProps) { - const matchedCount = matchedGroupIds.length; const {openDrawer, isDrawerOpen} = useDrawer(); const [isActive, setIsActive] = useState(false); const handleClick = () => { @@ -46,8 +38,8 @@ export function SupergroupRow({ () => ( ), { @@ -58,20 +50,6 @@ export function SupergroupRow({ ); }; - const summary = useOptionalIssueSelectionSummary(); - const selectedCount = useMemo(() => { - if (!summary) { - return 0; - } - let count = 0; - for (const id of matchedGroupIds) { - if (summary.records.get(id)) { - count++; - } - } - return count; - }, [summary, matchedGroupIds]); - const highlighted = isActive && isDrawerOpen; return ( @@ -79,38 +57,24 @@ 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 && matchedCount > 0 ? : null} - {matchedCount > 0 ? ( - 0 ? 'primary' : 'muted'} - bold={selectedCount > 0} - > - {selectedCount > 0 - ? `${selectedCount} / ${supergroup.group_ids.length} ${t('issues selected')}` - : `${matchedCount} / ${supergroup.group_ids.length} ${t('issues matched')}`} - - ) : null} + {supergroup.code_area ? : null} + + {`${supergroup.group_ids.length} ${t('issues')}`} + @@ -196,12 +160,6 @@ const Wrapper = styled(PanelItem)<{highlighted: boolean}>` min-height: 82px; background: ${p => p.highlighted ? p.theme.tokens.background.secondary : 'transparent'}; - - &:not(:hover):not(:has(input:checked)):not(:has(input:indeterminate)) { - ${CheckboxLabel} { - ${p => p.theme.visuallyHidden}; - } - } `; const Summary = styled('div')` diff --git a/static/app/views/issueDetails/streamline/sidebar/supergroupSection.tsx b/static/app/views/issueDetails/streamline/sidebar/supergroupSection.tsx index 1701ae8fb67348..dbe9342e6be0d0 100644 --- a/static/app/views/issueDetails/streamline/sidebar/supergroupSection.tsx +++ b/static/app/views/issueDetails/streamline/sidebar/supergroupSection.tsx @@ -35,15 +35,10 @@ export function SupergroupSection({group}: SupergroupSectionProps) { const issueCount = supergroup.group_ids.length; const handleClick = () => { - 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..a4bb65dbc2bf4b 100644 --- a/static/app/views/issueList/groupListBody.tsx +++ b/static/app/views/issueList/groupListBody.tsx @@ -237,7 +237,6 @@ function GroupList({ diff --git a/static/app/views/issueList/supergroups/supergroupDrawer.tsx b/static/app/views/issueList/supergroups/supergroupDrawer.tsx index 45028396f09d25..8e5abada896b48 100644 --- a/static/app/views/issueList/supergroups/supergroupDrawer.tsx +++ b/static/app/views/issueList/supergroups/supergroupDrawer.tsx @@ -1,13 +1,19 @@ -import {Fragment, useMemo, useState} from 'react'; -import {css} from '@emotion/react'; +import {Fragment, useCallback, useMemo, useState} from 'react'; import styled from '@emotion/styled'; import {Badge} from '@sentry/scraps/badge'; import {Button} from '@sentry/scraps/button'; +import {Checkbox} from '@sentry/scraps/checkbox'; import {inlineCodeStyles} from '@sentry/scraps/code'; import {Container, Flex, Stack} from '@sentry/scraps/layout'; import {Heading, Text} from '@sentry/scraps/text'; +import {bulkDelete, bulkUpdate, mergeGroups} from 'sentry/actionCreators/group'; +import { + addErrorMessage, + addLoadingMessage, + clearIndicators, +} from 'sentry/actionCreators/indicator'; import type {IndexedMembersByProject} from 'sentry/actionCreators/members'; import { CrumbContainer, @@ -16,29 +22,40 @@ import { } from 'sentry/components/events/eventDrawer'; import {DrawerBody, DrawerHeader} from 'sentry/components/globalDrawer/components'; import type {GroupListColumn} from 'sentry/components/issues/groupList'; -import {GroupListHeader} from 'sentry/components/issues/groupListHeader'; +import {IssueStreamHeaderLabel} from 'sentry/components/IssueStreamHeaderLabel'; import {ALL_ACCESS_PROJECTS} from 'sentry/components/pageFilters/constants'; +import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; import {Panel} from 'sentry/components/panels/panel'; import {PanelBody} from 'sentry/components/panels/panelBody'; -import {Placeholder} from 'sentry/components/placeholder'; import { DEFAULT_STREAM_GROUP_STATS_PERIOD, + LoadingStreamGroup, StreamGroup, } from 'sentry/components/stream/group'; -import {IconChevron, IconFocus} from 'sentry/icons'; +import {IconChevron, IconFilter, 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 {apiOptions} from 'sentry/utils/api/apiOptions'; +import {uniq} from 'sentry/utils/array/uniq'; import {MarkedText} from 'sentry/utils/marked/markedText'; -import {useApiQuery} from 'sentry/utils/queryClient'; +import {useQuery, useQueryClient} from 'sentry/utils/queryClient'; +import {useApi} from 'sentry/utils/useApi'; +import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; +import {ActionSet} from 'sentry/views/issueList/actions/actionSet'; +import {COLUMN_BREAKPOINTS, ConfirmAction} from 'sentry/views/issueList/actions/utils'; +import { + IssueSelectionProvider, + useIssueSelectionActions, + useIssueSelectionSummary, +} from 'sentry/views/issueList/issueSelectionContext'; import {SupergroupFeedback} from 'sentry/views/issueList/supergroups/supergroupFeedback'; import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types'; - -const PAGE_SIZE = 20; +import type {IssueUpdateData} from 'sentry/views/issueList/types'; const DRAWER_COLUMNS: GroupListColumn[] = [ + 'graph', 'event', 'users', 'assignee', @@ -47,15 +64,15 @@ const DRAWER_COLUMNS: GroupListColumn[] = [ ]; interface SupergroupDetailDrawerProps { - matchedGroupIds: string[]; supergroup: SupergroupDetail; + filterWithCurrentSearch?: boolean; memberList?: IndexedMembersByProject; } export function SupergroupDetailDrawer({ supergroup, - matchedGroupIds, memberList, + filterWithCurrentSearch, }: SupergroupDetailDrawerProps) { return ( @@ -127,8 +144,8 @@ export function SupergroupDetailDrawer({ )} @@ -137,121 +154,144 @@ export function SupergroupDetailDrawer({ ); } +const PAGE_SIZE = 25; + function SupergroupIssueList({ groupIds, - matchedGroupIds, memberList, + filterWithCurrentSearch, }: { groupIds: number[]; - matchedGroupIds: string[]; + filterWithCurrentSearch?: boolean; memberList?: IndexedMembersByProject; }) { 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); - } - } + const { + query: searchQuery, + project, + environment, + statsPeriod, + start, + end, + } = location.query; + const query = typeof searchQuery === 'string' ? searchQuery : ''; + 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); - return {sortedGroupIds: [...matched, ...loaded, ...unloaded], loadedIds: cachedIds}; - }, [groupIds, matchedGroupIds]); - - 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 {data: fetchedGroups, isPending} = useApiQuery( - [ - getApiUrl('/organizations/$organizationIdOrSlug/issues/', { - path: {organizationIdOrSlug: organization.slug}, - }), - { - query: { - group: pageUnloadedIds.map(String), - project: ALL_ACCESS_PROJECTS, - }, + // Fetch all groups on this page + const {data: allGroups, isPending: allPending} = useQuery( + apiOptions.as()('/organizations/$organizationIdOrSlug/issues/', { + path: {organizationIdOrSlug: organization.slug}, + query: { + group: pageGroupIds.map(String), + project: ALL_ACCESS_PROJECTS, }, - ], - { staleTime: 30_000, - enabled: pageUnloadedIds.length > 0, - } + }) ); + // Search with the stream query to find which ones match + const {data: matchedGroups, isPending: matchPending} = useQuery({ + ...apiOptions.as()('/organizations/$organizationIdOrSlug/issues/', { + path: {organizationIdOrSlug: organization.slug}, + query: { + project, + environment, + statsPeriod, + start, + end, + query: `${query} ${issueIdFilter}`, + limit: groupIds.length, + }, + staleTime: 30_000, + }), + enabled: !!filterWithCurrentSearch, + }); + + const isPending = allPending || (!!filterWithCurrentSearch && matchPending); + + if (isPending) { + return ( + + + {t('Issue')} + + + + {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; + }); + + const visibleGroupIds = sortedGroups.map(g => g.id); + 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) { + + + + + {sortedGroups.map(group => { const members = memberList?.[group.project?.slug] ? memberList[group.project.slug] : undefined; return ( - + + {matchedIds.has(group.id) && ( + + + + )} - + ); - } - - if (isPending) { - return ( - - - - ); - } - - return null; - })} - - + })} + + + {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}`}