Skip to content

Commit 3ed5dab

Browse files
authored
feat(issues): Redesign streamline event nav (#79656)
1 parent 213aecb commit 3ed5dab

File tree

15 files changed

+714
-548
lines changed

15 files changed

+714
-548
lines changed

static/app/components/dropdownMenu/index.spec.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,4 +249,19 @@ describe('DropdownMenu', function () {
249249
// Items should not appear
250250
expect(screen.queryByRole('menuitemradio')).not.toBeInTheDocument();
251251
});
252+
253+
it('closes after clicking link', async function () {
254+
render(
255+
<DropdownMenu
256+
items={[{key: 'item1', label: 'Item One', to: '/test'}]}
257+
triggerLabel="Menu"
258+
/>
259+
);
260+
261+
await userEvent.click(screen.getByRole('button', {name: 'Menu'}));
262+
await userEvent.click(screen.getByRole('menuitemradio', {name: 'Item One'}));
263+
await waitFor(() => {
264+
expect(screen.queryByRole('menuitemradio')).not.toBeInTheDocument();
265+
});
266+
});
252267
});

static/app/components/dropdownMenu/item.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,15 @@ function BaseDropdownMenuItem(
118118
const {key, onAction, to, label, isSubmenu, trailingItems, ...itemProps} =
119119
node.value ?? {};
120120
const {size} = node.props;
121+
const {rootOverlayState} = useContext(DropdownMenuContext);
121122

122123
const actionHandler = () => {
123124
if (to) {
125+
// Close the menu after the click event has bubbled to the link
126+
// Only needed on links that do not unmount the menu
127+
if (closeOnSelect) {
128+
requestAnimationFrame(() => rootOverlayState?.close());
129+
}
124130
return;
125131
}
126132
if (isSubmenu) {
@@ -178,7 +184,6 @@ function BaseDropdownMenuItem(
178184
});
179185

180186
// Manage interactive events & create aria attributes
181-
const {rootOverlayState} = useContext(DropdownMenuContext);
182187
const {menuItemProps, labelProps, descriptionProps} = useMenuItem(
183188
{
184189
key: node.key,

static/app/components/events/autofix/autofixDrawer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type {Group} from 'sentry/types/group';
1919
import type {Project} from 'sentry/types/project';
2020
import {getShortEventId} from 'sentry/utils/events';
2121
import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
22-
import {MIN_NAV_HEIGHT} from 'sentry/views/issueDetails/streamline/eventNavigation';
22+
import {MIN_NAV_HEIGHT} from 'sentry/views/issueDetails/streamline/eventTitle';
2323

2424
interface AutofixStartBoxProps {
2525
groupId: string;

static/app/components/events/eventDrawer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {Breadcrumbs as NavigationBreadcrumbs} from 'sentry/components/breadcrumb
44
import {DrawerBody, DrawerHeader} from 'sentry/components/globalDrawer/components';
55
import {InputGroup} from 'sentry/components/inputGroup';
66
import {space} from 'sentry/styles/space';
7-
import {MIN_NAV_HEIGHT} from 'sentry/views/issueDetails/streamline/eventNavigation';
7+
import {MIN_NAV_HEIGHT} from 'sentry/views/issueDetails/streamline/eventTitle';
88

99
export const Header = styled('h3')`
1010
display: block;

static/app/components/tabs/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export interface TabsProps<T>
3131
* selected tab item.
3232
*/
3333
defaultValue?: T;
34+
/**
35+
* Disable tabs from being put in the overflow menu.
36+
*/
37+
disableOverflow?: boolean;
3438
disabled?: boolean;
3539
/**
3640
* Callback when the selected tab changes.

static/app/components/tabs/tabList.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,23 @@ export function useOverflowTabs({
3030
tabListRef,
3131
tabItemsRef,
3232
tabItems,
33+
disabled,
3334
}: {
35+
/**
36+
* Prevent tabs from being put in the overflow menu.
37+
*/
38+
disabled: boolean | undefined;
3439
tabItems: TabListItemProps[];
3540
tabItemsRef: React.RefObject<Record<string | number, HTMLLIElement | null>>;
3641
tabListRef: React.RefObject<HTMLUListElement>;
3742
}) {
3843
const [overflowTabs, setOverflowTabs] = useState<Array<string | number>>([]);
3944

4045
useEffect(() => {
46+
if (disabled) {
47+
return () => {};
48+
}
49+
4150
const options = {
4251
root: tabListRef.current,
4352
// Nagative right margin to account for overflow menu's trigger button
@@ -70,7 +79,7 @@ export function useOverflowTabs({
7079
);
7180

7281
return () => observer.disconnect();
73-
}, [tabListRef, tabItemsRef]);
82+
}, [tabListRef, tabItemsRef, disabled]);
7483

7584
const tabItemKeyToHiddenMap = tabItems.reduce(
7685
(acc, next) => ({
@@ -142,6 +151,7 @@ function BaseTabList({
142151
disabled,
143152
orientation = 'horizontal',
144153
keyboardActivation = 'manual',
154+
disableOverflow,
145155
...otherRootProps
146156
} = rootProps;
147157

@@ -178,6 +188,7 @@ function BaseTabList({
178188
tabListRef,
179189
tabItemsRef,
180190
tabItems: props.items,
191+
disabled: disableOverflow,
181192
});
182193

183194
const overflowMenuItems = useMemo(() => {

static/app/views/issueDetails/groupEventDetails/groupEventDetails.tsx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ import {css} from '@emotion/react';
33
import styled from '@emotion/styled';
44
import isEqual from 'lodash/isEqual';
55

6+
import Feature from 'sentry/components/acl/feature';
67
import ArchivedBox from 'sentry/components/archivedBox';
8+
import ErrorBoundary from 'sentry/components/errorBoundary';
79
import GroupEventDetailsLoadingError from 'sentry/components/errors/groupEventDetailsLoadingError';
810
import {withMeta} from 'sentry/components/events/meta/metaProxy';
11+
import {GroupSummary} from 'sentry/components/group/groupSummary';
912
import HookOrDefault from 'sentry/components/hookOrDefault';
1013
import * as Layout from 'sentry/components/layouts/thirds';
1114
import LoadingIndicator from 'sentry/components/loadingIndicator';
1215
import {TransactionProfileIdProvider} from 'sentry/components/profiling/transactionProfileIdProvider';
1316
import ResolutionBox from 'sentry/components/resolutionBox';
17+
import {t} from 'sentry/locale';
1418
import useSentryAppComponentsData from 'sentry/stores/useSentryAppComponentsData';
1519
import {space} from 'sentry/styles/space';
1620
import type {Event} from 'sentry/types/event';
@@ -29,6 +33,8 @@ import GroupEventDetailsContent from 'sentry/views/issueDetails/groupEventDetail
2933
import GroupEventHeader from 'sentry/views/issueDetails/groupEventHeader';
3034
import GroupSidebar from 'sentry/views/issueDetails/groupSidebar';
3135
import {EventDetailsHeader} from 'sentry/views/issueDetails/streamline/eventDetailsHeader';
36+
import {IssueEventNavigation} from 'sentry/views/issueDetails/streamline/eventNavigation';
37+
import {useEventQuery} from 'sentry/views/issueDetails/streamline/eventSearch';
3238
import StreamlinedSidebar from 'sentry/views/issueDetails/streamline/sidebar';
3339

3440
import ReprocessingProgress from '../reprocessingProgress';
@@ -74,6 +80,7 @@ function GroupEventDetails(props: GroupEventDetailsProps) {
7480
const prevEnvironment = usePrevious(environments);
7581
const prevEvent = usePrevious(event);
7682
const hasStreamlinedUI = useHasStreamlinedUI();
83+
const searchQuery = useEventQuery({group});
7784

7885
const [sidebarOpen, _] = useSyncedLocalStorageState('issue-details-sidebar-open', true);
7986

@@ -205,9 +212,29 @@ function GroupEventDetails(props: GroupEventDetailsProps) {
205212
<EventDetailsHeader event={event} group={group} />
206213
) : null}
207214
{hasStreamlinedUI ? (
208-
<GroupContent>{renderContent()}</GroupContent>
215+
<GroupContent>
216+
<PageErrorBoundary
217+
mini
218+
message={t('There was an error loading the issue summary')}
219+
>
220+
<Feature features={['organizations:ai-summary']}>
221+
<GroupSummary
222+
groupId={group.id}
223+
groupCategory={group.issueCategory}
224+
/>
225+
</Feature>
226+
</PageErrorBoundary>
227+
<div>
228+
<IssueEventNavigation
229+
event={event}
230+
group={group}
231+
query={searchQuery}
232+
/>
233+
{renderContent()}
234+
</div>
235+
</GroupContent>
209236
) : (
210-
renderContent()
237+
<GroupContent>{renderContent()}</GroupContent>
211238
)}
212239
</MainLayoutComponent>
213240
{hasStreamlinedUI ? (
@@ -306,4 +333,9 @@ const StyledLayoutSide = styled(Layout.Side)<{hasStreamlinedUi: boolean}>`
306333
}
307334
`;
308335

336+
const PageErrorBoundary = styled(ErrorBoundary)`
337+
margin: 0;
338+
border: 1px solid ${p => p.theme.translucentBorder};
339+
`;
340+
309341
export default GroupEventDetails;

static/app/views/issueDetails/streamline/eventDetails.spec.tsx

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ describe('EventDetails', function () {
2222
const event = EventFixture({id: 'event-id'});
2323
const defaultProps = {project, group, event};
2424

25-
let mockActionableItems: jest.Mock;
2625
let mockList: jest.Mock;
2726
let mockListMeta: jest.Mock;
2827

@@ -38,11 +37,6 @@ describe('EventDetails', function () {
3837
);
3938
ProjectsStore.loadInitialData([project]);
4039
MockApiClient.clearMockResponses();
41-
mockActionableItems = MockApiClient.addMockResponse({
42-
url: `/projects/${organization.slug}/${project.slug}/events/${event.id}/actionable-items/`,
43-
body: {errors: []},
44-
method: 'GET',
45-
});
4640
MockApiClient.addMockResponse({
4741
url: `/organizations/${organization.slug}/issues/${group.id}/tags/`,
4842
body: TagsFixture(),
@@ -83,22 +77,6 @@ describe('EventDetails', function () {
8377
});
8478
});
8579

86-
it('displays all basic components', async function () {
87-
render(<EventDetails {...defaultProps} />, {organization});
88-
await screen.findByText(event.id);
89-
90-
// Navigation
91-
expect(screen.getByRole('tab', {name: 'Recommended Event'})).toBeInTheDocument();
92-
expect(screen.getByRole('tab', {name: 'First Event'})).toBeInTheDocument();
93-
expect(screen.getByRole('button', {name: 'Next Event'})).toBeInTheDocument();
94-
expect(screen.getByRole('button', {name: 'View All Events'})).toBeInTheDocument();
95-
// Content
96-
expect(mockActionableItems).toHaveBeenCalled();
97-
// All Events (should not query initially)
98-
expect(mockList).not.toHaveBeenCalled();
99-
expect(mockListMeta).not.toHaveBeenCalled();
100-
});
101-
10280
it('should display the events list', async function () {
10381
const router = RouterFixture({
10482
location: LocationFixture({
@@ -108,8 +86,7 @@ describe('EventDetails', function () {
10886
});
10987
render(<EventDetails {...defaultProps} />, {organization, router});
11088

111-
expect(await screen.findByRole('button', {name: 'Close'})).toBeInTheDocument();
112-
expect(screen.getByText('All Events')).toBeInTheDocument();
89+
expect(await screen.findByText('All Events')).toBeInTheDocument();
11390

11491
expect(mockList).toHaveBeenCalled();
11592
expect(mockListMeta).toHaveBeenCalled();

static/app/views/issueDetails/streamline/eventDetails.tsx

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import {useLayoutEffect, useState} from 'react';
22
import {useTheme} from '@emotion/react';
33
import styled from '@emotion/styled';
44

5-
import Feature from 'sentry/components/acl/feature';
65
import ErrorBoundary from 'sentry/components/errorBoundary';
7-
import {GroupSummary} from 'sentry/components/group/groupSummary';
86
import {t} from 'sentry/locale';
97
import {space} from 'sentry/styles/space';
108
import type {Event} from 'sentry/types/event';
@@ -21,8 +19,7 @@ import {
2119
useEventDetailsReducer,
2220
} from 'sentry/views/issueDetails/streamline/context';
2321
import {EventList} from 'sentry/views/issueDetails/streamline/eventList';
24-
import {EventNavigation} from 'sentry/views/issueDetails/streamline/eventNavigation';
25-
import {useEventQuery} from 'sentry/views/issueDetails/streamline/eventSearch';
22+
import {EventTitle} from 'sentry/views/issueDetails/streamline/eventTitle';
2623
import {Tab} from 'sentry/views/issueDetails/types';
2724
import {useGroupDetailsRoute} from 'sentry/views/issueDetails/useGroupDetailsRoute';
2825

@@ -32,17 +29,11 @@ export function EventDetails({
3229
project,
3330
}: Required<EventDetailsContentProps>) {
3431
const {eventDetails, dispatch} = useEventDetailsReducer();
35-
const searchQuery = useEventQuery({group});
3632

3733
const {currentTab} = useGroupDetailsRoute();
3834

3935
return (
4036
<EventDetailsContext.Provider value={{...eventDetails, dispatch}}>
41-
<PageErrorBoundary mini message={t('There was an error loading the issue summary')}>
42-
<Feature features={['organizations:ai-summary']}>
43-
<GroupSummary groupId={group.id} groupCategory={group.issueCategory} />
44-
</Feature>
45-
</PageErrorBoundary>
4637
{/* TODO(issues): We should use the router for this */}
4738
{currentTab === Tab.EVENTS && (
4839
<PageErrorBoundary mini message={t('There was an error loading the event list')}>
@@ -57,27 +48,21 @@ export function EventDetails({
5748
mini
5849
message={t('There was an error loading the event content')}
5950
>
60-
<GroupContent>
61-
<StickyEventNav event={event} group={group} searchQuery={searchQuery} />
62-
<ContentPadding>
63-
<EventDetailsContent group={group} event={event} project={project} />
64-
</ContentPadding>
65-
</GroupContent>
51+
<div>
52+
<GroupContent>
53+
<StickyEventNav event={event} group={group} />
54+
<ContentPadding>
55+
<EventDetailsContent group={group} event={event} project={project} />
56+
</ContentPadding>
57+
</GroupContent>
58+
</div>
6659
</PageErrorBoundary>
6760
)}
6861
</EventDetailsContext.Provider>
6962
);
7063
}
7164

72-
function StickyEventNav({
73-
event,
74-
group,
75-
searchQuery,
76-
}: {
77-
event: Event;
78-
group: Group;
79-
searchQuery: string;
80-
}) {
65+
function StickyEventNav({event, group}: {event: Event; group: Group}) {
8166
const theme = useTheme();
8267
const [nav, setNav] = useState<HTMLDivElement | null>(null);
8368
const isStuck = useIsStuck(nav);
@@ -102,13 +87,12 @@ function StickyEventNav({
10287
event={event}
10388
group={group}
10489
ref={setNav}
105-
query={searchQuery}
10690
data-stuck={isStuck}
10791
/>
10892
);
10993
}
11094

111-
const FloatingEventNavigation = styled(EventNavigation)`
95+
const FloatingEventNavigation = styled(EventTitle)`
11296
position: sticky;
11397
top: 0;
11498
@media (max-width: ${p => p.theme.breakpoints.medium}) {

static/app/views/issueDetails/streamline/eventList.spec.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ describe('EventList', () => {
126126

127127
expect(screen.getByRole('button', {name: 'Previous Page'})).toBeInTheDocument();
128128
expect(screen.getByRole('button', {name: 'Next Page'})).toBeInTheDocument();
129-
expect(screen.getByRole('button', {name: 'Close'})).toBeInTheDocument();
130129
expect(
131130
await screen.findByText(
132131
`Showing 0-${MOCK_EVENTS_TABLE_DATA.length} of ${totalCount}`

0 commit comments

Comments
 (0)