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}`}