Skip to content

Commit

Permalink
feat(threads): Add sorting to the thread selector (#83512)
Browse files Browse the repository at this point in the history
Resolves #83367

Adds sorting to the threads selector by the few attributes we display
here.


https://github.com/user-attachments/assets/95640d72-9217-4d63-a4f4-0e316490b1f4
  • Loading branch information
leeandher authored Jan 16, 2025
1 parent 97c9c5f commit 836e4f3
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import getRelevantFrame from './getRelevantFrame';
import getThreadException from './getThreadException';
import getThreadStacktrace from './getThreadStacktrace';

type ThreadInfo = {
export type ThreadInfo = {
crashedInfo?: EntryData;
filename?: string;
label?: string;
Expand Down
188 changes: 155 additions & 33 deletions static/app/components/events/interfaces/threads/threadSelector/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import {useMemo} from 'react';
import {useMemo, useState} from 'react';
import styled from '@emotion/styled';

import {CompactSelect} from 'sentry/components/compactSelect';
import {Flex} from 'sentry/components/container/flex';
import {IconArrow} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Event, ExceptionType, Frame, Thread} from 'sentry/types/event';
import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
import useOrganization from 'sentry/utils/useOrganization';

import filterThreadInfo from './filterThreadInfo';
import filterThreadInfo, {type ThreadInfo} from './filterThreadInfo';
import Option from './option';
import {ThreadSelectorGrid, ThreadSelectorGridCell} from './styles';
import {getMappedThreadState} from './threadStates';
Expand All @@ -22,19 +24,14 @@ type Props = {
/**
* Expects threads to be sorted by crashed first
*/
threads: Thread[];
threads: Readonly<Thread[]>;
};

function Header({hasThreadStates}: {hasThreadStates: boolean}) {
return (
<StyledGrid hasThreadStates={hasThreadStates}>
<ThreadSelectorGridCell />
<ThreadSelectorGridCell>{t('ID')}</ThreadSelectorGridCell>
<ThreadSelectorGridCell>{t('Name')}</ThreadSelectorGridCell>
<ThreadSelectorGridCell>{t('Label')}</ThreadSelectorGridCell>
{hasThreadStates && <ThreadSelectorGridCell>{t('State')}</ThreadSelectorGridCell>}
</StyledGrid>
);
const enum SortAttribute {
ID = 'id',
NAME = 'name',
LABEL = 'label',
STATE = 'state',
}

function getThreadLabel(
Expand All @@ -50,28 +47,76 @@ function getThreadLabel(

function ThreadSelector({threads, event, exception, activeThread, onChange}: Props) {
const organization = useOrganization({allowNull: true});
const [currentThread, setCurrentThread] = useState<Thread>(activeThread);
const [sortAttribute, setSortAttribute] = useState<SortAttribute>(SortAttribute.ID);
const [isSortAscending, setIsSortAscending] = useState<boolean>(true);

const hasThreadStates = threads.some(thread =>
defined(getMappedThreadState(thread.state))
);
const threadInfoMap = useMemo(() => {
return threads.reduce<Record<number, ThreadInfo>>((acc, thread) => {
acc[thread.id] = filterThreadInfo(event, thread, exception);
return acc;
}, {});
}, [threads, event, exception]);

const items = useMemo(() => {
return threads.map((thread: Thread) => {
const threadInfo = filterThreadInfo(event, thread, exception);
return {
value: thread.id,
textValue: `#${thread.id}: ${thread.name} ${threadInfo.label} ${threadInfo.filename}`,
label: (
<Option
thread={thread}
details={threadInfo}
crashedInfo={threadInfo.crashedInfo}
hasThreadStates={hasThreadStates}
/>
),
};
const orderedThreads = useMemo(() => {
const sortedThreads: Readonly<Thread[]> = threads.toSorted((threadA, threadB) => {
const threadInfoA = threadInfoMap[threadA.id] ?? {};
const threadInfoB = threadInfoMap[threadB.id] ?? {};

switch (sortAttribute) {
case SortAttribute.ID:
return isSortAscending ? threadA.id - threadB.id : threadB.id - threadA.id;
case SortAttribute.NAME:
return isSortAscending
? threadA.name?.localeCompare(threadB.name ?? '') ?? 0
: threadB.name?.localeCompare(threadA.name ?? '') ?? 0;
case SortAttribute.LABEL:
return isSortAscending
? threadInfoA.label?.localeCompare(threadInfoB.label ?? '') ?? 0
: threadInfoB.label?.localeCompare(threadInfoA.label ?? '') ?? 0;
case SortAttribute.STATE:
return isSortAscending
? threadInfoA.state?.localeCompare(threadInfoB.state ?? '') ?? 0
: threadInfoB.state?.localeCompare(threadInfoA.state ?? '') ?? 0;
default:
return 0;
}
});
}, [threads, event, exception, hasThreadStates]);
const currentThreadIndex = sortedThreads.findIndex(
thread => thread.id === currentThread.id
);
return [
sortedThreads[currentThreadIndex],
...sortedThreads.slice(0, currentThreadIndex),
...sortedThreads.slice(currentThreadIndex + 1),
].filter(defined);
}, [threads, sortAttribute, isSortAscending, currentThread, threadInfoMap]);

const items = orderedThreads.map((thread: Thread) => {
const threadInfo = threadInfoMap[thread.id] ?? {};
return {
value: thread.id,
textValue: `#${thread.id}: ${thread.name ?? ''} ${threadInfo.label ?? ''} ${threadInfo.filename ?? ''} ${threadInfo.state ?? ''}`,
label: (
<Option
thread={thread}
details={threadInfo}
crashedInfo={threadInfo?.crashedInfo}
hasThreadStates={hasThreadStates}
/>
),
};
});

const sortIcon = (
<IconArrow
direction={isSortAscending ? 'down' : 'up'}
style={{height: 10, width: 10}}
/>
);

return (
<CompactSelect
Expand Down Expand Up @@ -100,7 +145,65 @@ function ThreadSelector({threads, event, exception, activeThread, onChange}: Pro
</ActiveThreadName>
</ThreadName>
}
menuBody={<Header hasThreadStates={hasThreadStates} />}
menuBody={
<StyledGrid hasThreadStates={hasThreadStates}>
<ThreadSelectorGridCell />
<SortableThreadSelectorGridCell
onClick={() => {
setSortAttribute(SortAttribute.ID);
setIsSortAscending(
sortAttribute === SortAttribute.ID ? !isSortAscending : true
);
}}
>
<HeaderText>
{t('ID')}
{sortAttribute === SortAttribute.ID && sortIcon}
</HeaderText>
</SortableThreadSelectorGridCell>
<SortableThreadSelectorGridCell
onClick={() => {
setSortAttribute(SortAttribute.NAME);
setIsSortAscending(
sortAttribute === SortAttribute.NAME ? !isSortAscending : true
);
}}
>
<HeaderText>
{t('Name')}
{sortAttribute === SortAttribute.NAME && sortIcon}
</HeaderText>
</SortableThreadSelectorGridCell>
<SortableThreadSelectorGridCell
onClick={() => {
setSortAttribute(SortAttribute.LABEL);
setIsSortAscending(
sortAttribute === SortAttribute.LABEL ? !isSortAscending : true
);
}}
>
<HeaderText>
{t('Label')}
{sortAttribute === SortAttribute.LABEL && sortIcon}
</HeaderText>
</SortableThreadSelectorGridCell>
{hasThreadStates && (
<SortableThreadSelectorGridCell
onClick={() => {
setSortAttribute(SortAttribute.STATE);
setIsSortAscending(
sortAttribute === SortAttribute.STATE ? !isSortAscending : true
);
}}
>
<HeaderText>
{t('State')}
{sortAttribute === SortAttribute.STATE && sortIcon}
</HeaderText>
</SortableThreadSelectorGridCell>
)}
</StyledGrid>
}
onChange={selected => {
const threadIndex = threads.findIndex(th => th.id === selected.value);
const thread = threads[threadIndex];
Expand All @@ -119,6 +222,7 @@ function ThreadSelector({threads, event, exception, activeThread, onChange}: Pro
0,
});
onChange(thread);
setCurrentThread(thread);
}
}}
/>
Expand All @@ -140,10 +244,28 @@ const ActiveThreadName = styled('span')`
`;

const StyledGrid = styled(ThreadSelectorGrid)`
padding-left: 40px;
padding-right: 40px;
padding-left: 36px;
padding-right: 20px;
color: ${p => p.theme.subText};
font-weight: ${p => p.theme.fontWeightBold};
border-bottom: 1px solid ${p => p.theme.border};
margin-bottom: 2px;
margin-bottom: ${space(0.5)};
`;

const SortableThreadSelectorGridCell = styled(ThreadSelectorGridCell)`
margin-bottom: ${space(0.5)};
cursor: pointer;
user-select: none;
border-radius: ${p => p.theme.borderRadius};
&:hover {
background-color: ${p => p.theme.backgroundSecondary};
}
`;

const HeaderText = styled(Flex)`
display: flex;
align-items: center;
justify-content: flex-start;
gap: ${space(0.5)};
padding: 0 ${space(0.5)};
`;

0 comments on commit 836e4f3

Please sign in to comment.