Skip to content

Commit 1366cda

Browse files
scttcperandrewshie-sentry
authored andcommitted
feat(issues): Add thread navigators, compact dropdown (#80969)
1 parent 783ef19 commit 1366cda

File tree

9 files changed

+226
-271
lines changed

9 files changed

+226
-271
lines changed

static/app/components/events/interfaces/threads.spec.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,6 +1267,18 @@ describe('Threads', function () {
12671267
within(threadSelector).getByText('ViewController.causeCrash');
12681268
});
12691269

1270+
it('can navigate to next/previous thread', async function () {
1271+
render(<Threads {...props} />, {organization});
1272+
const threadSelector = await screen.findByTestId('thread-selector');
1273+
expect(threadSelector).toHaveTextContent('Thread #0');
1274+
await userEvent.click(await screen.findByRole('button', {name: 'Next Thread'}));
1275+
expect(threadSelector).toHaveTextContent('Thread #1');
1276+
await userEvent.click(
1277+
await screen.findByRole('button', {name: 'Previous Thread'})
1278+
);
1279+
expect(threadSelector).toHaveTextContent('Thread #0');
1280+
});
1281+
12701282
it('renders raw stack trace', async function () {
12711283
MockApiClient.addMockResponse({
12721284
url: `/projects/${organization.slug}/${project.slug}/events/${event.id}/apple-crash-report?minified=false`,

static/app/components/events/interfaces/threads.tsx

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import {Fragment, useEffect, useState} from 'react';
1+
import {Fragment, useEffect, useMemo, useState} from 'react';
22
import styled from '@emotion/styled';
33

4+
import {Button} from 'sentry/components/button';
5+
import ButtonBar from 'sentry/components/buttonBar';
46
import {CommitRow} from 'sentry/components/commitRow';
57
import {Flex} from 'sentry/components/container/flex';
68
import ErrorBoundary from 'sentry/components/errorBoundary';
@@ -15,7 +17,14 @@ import Pill from 'sentry/components/pill';
1517
import Pills from 'sentry/components/pills';
1618
import QuestionTooltip from 'sentry/components/questionTooltip';
1719
import TextOverflow from 'sentry/components/textOverflow';
18-
import {IconClock, IconInfo, IconLock, IconPlay, IconTimer} from 'sentry/icons';
20+
import {
21+
IconChevron,
22+
IconClock,
23+
IconInfo,
24+
IconLock,
25+
IconPlay,
26+
IconTimer,
27+
} from 'sentry/icons';
1928
import {t, tn} from 'sentry/locale';
2029
import {space} from 'sentry/styles/space';
2130
import type {Event, Thread} from 'sentry/types/event';
@@ -101,15 +110,22 @@ const useActiveThreadState = (
101110
};
102111

103112
export function Threads({data, event, projectSlug, groupingCurrentLevel, group}: Props) {
104-
const threads = data.values ?? [];
113+
// Sort threads by crashed first
114+
const threads = useMemo(
115+
() => (data.values ?? []).toSorted((a, b) => Number(b.crashed) - Number(a.crashed)),
116+
[data.values]
117+
);
105118
const hasStreamlinedUI = useHasStreamlinedUI();
106119
const [activeThread, setActiveThread] = useActiveThreadState(event, threads);
107120

108121
const stackTraceNotFound = !threads.length;
109122

110123
const hasMoreThanOneThread = threads.length > 1;
111124

112-
const exception = getThreadException(event, activeThread);
125+
const exception = useMemo(
126+
() => getThreadException(event, activeThread),
127+
[event, activeThread]
128+
);
113129

114130
const entryIndex = exception
115131
? event.entries.findIndex(entry => entry.type === EntryType.EXCEPTION)
@@ -226,6 +242,18 @@ export function Threads({data, event, projectSlug, groupingCurrentLevel, group}:
226242
const {id: activeThreadId, name: activeThreadName} = activeThread ?? {};
227243
const hideThreadTags = activeThreadId === undefined || !activeThreadName;
228244

245+
function handleChangeThread(direction: 'previous' | 'next') {
246+
const currentIndex = threads.findIndex(thread => thread.id === activeThreadId);
247+
let nextIndex = direction === 'previous' ? currentIndex - 1 : currentIndex + 1;
248+
if (nextIndex < 0) {
249+
nextIndex = threads.length - 1;
250+
} else if (nextIndex >= threads.length) {
251+
nextIndex = 0;
252+
}
253+
254+
setActiveThread(threads[nextIndex]);
255+
}
256+
229257
const threadComponent = (
230258
<Fragment>
231259
{hasMoreThanOneThread && (
@@ -235,6 +263,28 @@ export function Threads({data, event, projectSlug, groupingCurrentLevel, group}:
235263
<ThreadHeading>{t('Threads')}</ThreadHeading>
236264
{activeThread && (
237265
<Wrapper>
266+
<ButtonBar merged>
267+
<Button
268+
title={t('Previous Thread')}
269+
tooltipProps={{delay: 1000}}
270+
icon={<IconChevron direction="left" />}
271+
aria-label={t('Previous Thread')}
272+
size="xs"
273+
onClick={() => {
274+
handleChangeThread('previous');
275+
}}
276+
/>
277+
<Button
278+
title={t('Next Thread')}
279+
tooltipProps={{delay: 1000}}
280+
icon={<IconChevron direction="right" />}
281+
aria-label={t('Next Thread')}
282+
size="xs"
283+
onClick={() => {
284+
handleChangeThread('next');
285+
}}
286+
/>
287+
</ButtonBar>
238288
<ThreadSelector
239289
threads={threads}
240290
activeThread={activeThread}
@@ -398,6 +448,8 @@ const LockReason = styled(TextOverflow)`
398448
`;
399449

400450
const Wrapper = styled('div')`
451+
display: flex;
452+
gap: ${space(1)};
401453
align-items: center;
402454
flex-wrap: wrap;
403455
flex-grow: 1;

static/app/components/events/interfaces/threads/threadSelector/filterThreadInfo.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type {StacktraceType} from 'sentry/types/stacktrace';
88
import getRelevantFrame from './getRelevantFrame';
99
import getThreadException from './getThreadException';
1010
import getThreadStacktrace from './getThreadStacktrace';
11-
import trimFilename from './trimFilename';
1211

1312
type ThreadInfo = {
1413
crashedInfo?: EntryData;
@@ -17,6 +16,11 @@ type ThreadInfo = {
1716
state?: ThreadStates;
1817
};
1918

19+
function trimFilename(filename: string) {
20+
const pieces = filename.split(/\//g);
21+
return pieces[pieces.length - 1];
22+
}
23+
2024
function filterThreadInfo(
2125
event: Event,
2226
thread: Thread,

static/app/components/events/interfaces/threads/threadSelector/header.tsx

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)