Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
415 changes: 0 additions & 415 deletions static/app/stores/groupingStore.spec.tsx

This file was deleted.

406 changes: 0 additions & 406 deletions static/app/stores/groupingStore.tsx

This file was deleted.

39 changes: 15 additions & 24 deletions static/app/views/issueDetails/groupMerged/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,28 @@ import {GroupFixture} from 'sentry-fixture/group';
import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen} from 'sentry-test/reactTestingLibrary';

import {GroupingStore} from 'sentry/stores/groupingStore';
import {GroupMergedView} from 'sentry/views/issueDetails/groupMerged';

describe('Issues -> Merged View', () => {
const events = DetailedEventsFixture();
const group = GroupFixture();
const mockData = {
merged: [
{
latestEvent: events[0],
state: 'unlocked',
id: '2c4887696f708c476a81ce4e834c4b02',
mergedBySeer: true,
},
{
latestEvent: events[1],
state: 'unlocked',
id: 'e05da55328a860b21f62e371f0a7507d',
},
],
};
const mergedFingerprints = [
{
latestEvent: events[0],
id: '2c4887696f708c476a81ce4e834c4b02',
mergedBySeer: true,
},
{
latestEvent: events[1],
id: 'e05da55328a860b21f62e371f0a7507d',
},
];

beforeEach(() => {
GroupingStore.init();
MockApiClient.clearMockResponses();
MockApiClient.addMockResponse({
url: `/organizations/org-slug/issues/${group.id}/hashes/?limit=50&query=`,
body: mockData.merged,
url: `/organizations/org-slug/issues/${group.id}/hashes/`,
body: mergedFingerprints,
});
});

Expand All @@ -49,15 +43,12 @@ describe('Issues -> Merged View', () => {
}
);

// Wait for the component to load
await screen.findByText('Fingerprints included in this issue');
const links = await screen.findAllByRole('button', {name: 'View latest event'});
expect(links).toHaveLength(mergedFingerprints.length);

const title = await screen.findByText('Fingerprints included in this issue');
expect(title.parentElement).toHaveTextContent(
'Fingerprints included in this issue (2)'
);

const links = await screen.findAllByRole('button', {name: 'View latest event'});
expect(links).toHaveLength(mockData.merged.length);
});
});
136 changes: 73 additions & 63 deletions static/app/views/issueDetails/groupMerged/index.tsx
Original file line number Diff line number Diff line change
@@ -1,98 +1,103 @@
import {Fragment, useCallback, useEffect, useState} from 'react';
import {Fragment} from 'react';
import styled from '@emotion/styled';
import {useQuery} from '@tanstack/react-query';
import type {Location} from 'history';
import * as qs from 'query-string';

import {LoadingError} from 'sentry/components/loadingError';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {QueryCount} from 'sentry/components/queryCount';
import {t, tct} from 'sentry/locale';
import type {Fingerprint} from 'sentry/stores/groupingStore';
import {GroupingStore} from 'sentry/stores/groupingStore';
import type {Group} from 'sentry/types/group';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {trackAnalytics} from 'sentry/utils/analytics';
import {useOrganization} from 'sentry/utils/useOrganization';

import {MergedList} from './mergedList';
import {
type Fingerprint,
useGroupMergedHashes,
useGroupMergedState,
} from './useGroupMerged';

type Props = {
groupId: Group['id'];
location: Location;
project: Project;
};

export function GroupMergedView(props: Props) {
interface GroupMergedContentProps {
error: boolean;
fingerprints: Fingerprint[];
groupId: Group['id'];
loading: boolean;
organization: Organization;
project: Project;
refetch: () => void;
pageLinks?: string;
}

export function GroupMergedView({project, groupId, location}: Props) {
const organization = useOrganization();
const [mergedItems, setMergedItems] = useState<Fingerprint[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(false);
const [mergedLinks, setMergedLinks] = useState<string | undefined>(undefined);
const {project, groupId, location} = props;
const {dataUpdatedAt, error, fingerprints, loading, pageLinks, refetch} =
useGroupMergedHashes({
groupId,
location,
organization,
});

const onGroupingChange = useCallback(
({
mergedItems: items,
mergedLinks: links,
loading: l,
error: e,
}: ReturnType<typeof GroupingStore.getState>) => {
if (items) {
setMergedItems(items);
setMergedLinks(links);
setIsLoading(l === undefined ? false : l);
setError(e === undefined ? false : e);
}
},
[]
return (
<GroupMergedContent
key={`${groupId}:${dataUpdatedAt}`}
error={error}
fingerprints={fingerprints}
groupId={groupId}
loading={loading}
organization={organization}
pageLinks={pageLinks}
project={project}
refetch={refetch}
/>
);
}

useEffect(() => {
const unsubscribe = GroupingStore.listen(onGroupingChange, undefined);
return () => {
unsubscribe();
};
}, [onGroupingChange]);

const {refetch} = useQuery({
queryKey: [
`/organizations/${organization.slug}/issues/${groupId}/hashes/`,
{query: {...location.query, limit: 50, query: location.query.query ?? ''}},
] as const,
queryFn: ({queryKey}) => {
// Not sure why query params are encoded into the "endpoint", but keeping behavior the same
const endpoint = `${queryKey[0]}?${qs.stringify(queryKey[1].query)}`;
// TODO: GroupingStore.onFetch is a nightmare, useQuery here is helping convert from class component.
return GroupingStore.onFetch([{endpoint, dataKey: 'merged'}]);
},
staleTime: 30_000,
retry: false,
});
function GroupMergedContent({
error,
fingerprints,
groupId,
loading,
organization,
pageLinks,
project,
refetch,
}: GroupMergedContentProps) {
const {
enableFingerprintCompare,
fingerprintsWithLatestEvent,
selectedEventIds,
state,
toggleAllCollapsed,
toggleCollapsed,
toggleSelected,
unmerge,
unmergeDisabled,
} = useGroupMergedState({fingerprints, groupId, organization});

const handleUnmerge = () => {
GroupingStore.onUnmerge({
groupId,
orgSlug: organization.slug,
unmerge({
loadingMessage: t('Unmerging events\u2026'),
successMessage: t('Events successfully queued for unmerging.'),
errorMessage: t('Unable to queue events for unmerging.'),
});
const unmergeKeys = [...GroupingStore.getState().unmergeList.values()];
trackAnalytics('issue_details.merged_tab.unmerge_clicked', {
organization,
group_id: groupId,
event_ids_unmerged: unmergeKeys.join(','),
total_unmerged: unmergeKeys.length,
event_ids_unmerged: selectedEventIds.join(','),
total_unmerged: selectedEventIds.length,
});
};

const isError = error && !isLoading;
const isLoadedSuccessfully = !isError && !isLoading;

const fingerprintsWithLatestEvent = mergedItems.filter(
({latestEvent}) => !!latestEvent
);
const isError = error && !loading;
const isLoadedSuccessfully = !isError && !loading;

return (
<Fragment>
Expand All @@ -113,7 +118,7 @@ export function GroupMergedView(props: Props) {
</small>
</HeaderWrapper>

{isLoading && <LoadingIndicator />}
{loading && <LoadingIndicator />}
{isError && (
<LoadingError
message={t('Unable to load merged events, please try again later')}
Expand All @@ -124,11 +129,16 @@ export function GroupMergedView(props: Props) {
{isLoadedSuccessfully && (
<MergedList
project={project}
fingerprints={mergedItems}
pageLinks={mergedLinks}
fingerprints={fingerprints}
pageLinks={pageLinks}
groupId={groupId}
enableFingerprintCompare={enableFingerprintCompare}
state={state}
toggleCollapsed={toggleCollapsed}
toggleSelected={toggleSelected}
unmergeDisabled={unmergeDisabled}
onUnmerge={handleUnmerge}
onToggleCollapse={GroupingStore.onToggleCollapseFingerprints}
onToggleCollapse={toggleAllCollapsed}
/>
)}
</Fragment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,17 @@ describe('MergedIssuesDrawer', () => {
const project = ProjectFixture();
const group = GroupFixture();
const event = EventFixture();
let mockMergedIssues: jest.Mock;

beforeEach(() => {
MockApiClient.clearMockResponses();
ProjectsStore.loadInitialData([project]);
GroupStore.init();

mockMergedIssues = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/issues/${group.id}/hashes/?limit=50&query=`,
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/issues/${group.id}/hashes/`,
body: [
{
latestEvent: event,
state: 'unlocked',
id: '2c4887696f708c476a81ce4e834c4b02',
mergedBySeer: true,
},
Expand All @@ -42,7 +40,6 @@ describe('MergedIssuesDrawer', () => {
await screen.findByRole('heading', {name: 'Merged Issues'})
).toBeInTheDocument();
expect(screen.getByText('Fingerprints included in this issue')).toBeInTheDocument();
expect(mockMergedIssues).toHaveBeenCalled();
expect(screen.getByRole('button', {name: 'Close Drawer'})).toBeInTheDocument();
});
});
Loading
Loading