Skip to content

Commit 8034317

Browse files
authored
feat(nav): Stop using config for primary navigation (#83666)
1 parent b7ce4af commit 8034317

4 files changed

Lines changed: 341 additions & 227 deletions

File tree

‎static/app/components/nav/index.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import styled from '@emotion/styled';
22

33
import {DeprecatedNavContextProvider} from 'sentry/components/nav/contextDeprecated';
44
import MobileTopbar from 'sentry/components/nav/mobileTopbar';
5-
import Sidebar from 'sentry/components/nav/sidebar';
5+
import {Sidebar} from 'sentry/components/nav/sidebar';
66
import {useBreakpoints} from 'sentry/utils/metrics/useBreakpoints';
77

88
function Nav() {

‎static/app/components/nav/mobileTopbar.tsx‎

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

5-
import {SidebarItems} from 'sentry/components/nav/sidebar';
5+
import {PrimaryNavigationItems} from 'sentry/components/nav/primary';
66
import {IconClose, IconMenu, IconSentry} from 'sentry/icons';
77
import {space} from 'sentry/styles/space';
88
import theme from 'sentry/utils/theme';
@@ -35,7 +35,7 @@ function MobileTopbar() {
3535
</button>
3636
{view !== 'closed' ? (
3737
<OverlayPortal>
38-
<SidebarItems />
38+
<PrimaryNavigationItems />
3939
</OverlayPortal>
4040
) : null}
4141
</Topbar>
Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
import {Fragment, type MouseEventHandler, useCallback} from 'react';
2+
import styled from '@emotion/styled';
3+
4+
import {openHelpSearchModal} from 'sentry/actionCreators/modal';
5+
import Feature from 'sentry/components/acl/feature';
6+
import {DropdownMenu} from 'sentry/components/dropdownMenu';
7+
import InteractionStateLayer from 'sentry/components/interactionStateLayer';
8+
import Link from 'sentry/components/links/link';
9+
import {linkStyles} from 'sentry/components/links/styles';
10+
import {
11+
isNavItemActive,
12+
isSubmenuItemActive,
13+
makeLinkPropsFromTo,
14+
type NavSidebarItem,
15+
resolveNavItemTo,
16+
} from 'sentry/components/nav/utils';
17+
import {
18+
IconDashboard,
19+
IconGraph,
20+
IconIssues,
21+
IconLightning,
22+
IconProject,
23+
IconQuestion,
24+
IconSearch,
25+
IconSettings,
26+
IconSiren,
27+
} from 'sentry/icons';
28+
import {t} from 'sentry/locale';
29+
import ConfigStore from 'sentry/stores/configStore';
30+
import {space} from 'sentry/styles/space';
31+
import {trackAnalytics} from 'sentry/utils/analytics';
32+
import {useLocation} from 'sentry/utils/useLocation';
33+
import useOrganization from 'sentry/utils/useOrganization';
34+
35+
interface SidebarItemProps {
36+
item: NavSidebarItem;
37+
children?: React.ReactNode;
38+
onClick?: MouseEventHandler<HTMLElement>;
39+
}
40+
41+
function SidebarBody({children}: {children: React.ReactNode}) {
42+
return <SidebarItemList>{children}</SidebarItemList>;
43+
}
44+
45+
function SidebarFooter({children}: {children: React.ReactNode}) {
46+
return (
47+
<SidebarFooterWrapper>
48+
<SidebarItemList>{children}</SidebarItemList>
49+
</SidebarFooterWrapper>
50+
);
51+
}
52+
53+
function SidebarItem({item}: SidebarItemProps) {
54+
const to = resolveNavItemTo(item);
55+
const SidebarChild = to ? SidebarLink : SidebarMenu;
56+
const organization = useOrganization();
57+
58+
const FeatureGuard = item.feature ? Feature : Fragment;
59+
const featureGuardProps: any = item.feature ?? {};
60+
61+
const recordAnalytics = useCallback(
62+
() =>
63+
trackAnalytics('growth.clicked_sidebar', {item: item.analyticsKey, organization}),
64+
[organization, item.analyticsKey]
65+
);
66+
67+
return (
68+
<FeatureGuard {...featureGuardProps}>
69+
<SidebarItemWrapper>
70+
<SidebarChild item={item} key={item.label} onClick={recordAnalytics}>
71+
{item.icon}
72+
<span>{item.label}</span>
73+
</SidebarChild>
74+
</SidebarItemWrapper>
75+
</FeatureGuard>
76+
);
77+
}
78+
79+
function SidebarMenu({item, children, onClick}: SidebarItemProps) {
80+
if (!item.dropdown) {
81+
throw new Error(
82+
`Nav item "${item.label}" must have either a \`dropdown\` or \`to\` value!`
83+
);
84+
}
85+
return (
86+
<DropdownMenu
87+
position="right-end"
88+
trigger={(props, isOpen) => {
89+
return (
90+
<NavButton
91+
{...props}
92+
onClick={event => {
93+
onClick?.(event);
94+
props.onClick?.(event);
95+
}}
96+
>
97+
<InteractionStateLayer hasSelectedBackground={isOpen} />
98+
{children}
99+
</NavButton>
100+
);
101+
}}
102+
items={item.dropdown}
103+
/>
104+
);
105+
}
106+
107+
function SidebarLink({children, item, onClick}: SidebarItemProps) {
108+
const location = useLocation();
109+
const isActive = isNavItemActive(item, location);
110+
const isSubmenuActive = isSubmenuItemActive(item, location);
111+
const to = resolveNavItemTo(item);
112+
if (!to) {
113+
throw new Error(
114+
`Nav item "${item.label}" must have either a \`dropdown\` or \`to\` value!`
115+
);
116+
}
117+
const linkProps = makeLinkPropsFromTo(to);
118+
119+
return (
120+
<NavLink
121+
{...linkProps}
122+
onClick={onClick}
123+
className={isActive || isSubmenuActive ? 'active' : undefined}
124+
aria-current={isActive ? 'page' : undefined}
125+
>
126+
<InteractionStateLayer hasSelectedBackground={isActive || isSubmenuActive} />
127+
{children}
128+
</NavLink>
129+
);
130+
}
131+
132+
export function PrimaryNavigationItems() {
133+
const organization = useOrganization();
134+
const prefix = `organizations/${organization.slug}`;
135+
136+
return (
137+
<Fragment>
138+
<SidebarBody>
139+
<SidebarItem
140+
item={{
141+
label: t('Issues'),
142+
icon: <IconIssues />,
143+
analyticsKey: 'issues',
144+
to: `/${prefix}/issues/`,
145+
}}
146+
/>
147+
<SidebarItem
148+
item={{
149+
label: t('Projects'),
150+
icon: <IconProject />,
151+
analyticsKey: 'projects',
152+
to: `/${prefix}/projects/`,
153+
}}
154+
/>
155+
<SidebarItem
156+
item={{
157+
label: t('Explore'),
158+
icon: <IconSearch />,
159+
analyticsKey: 'explore',
160+
to: `/${prefix}/traces/`,
161+
}}
162+
/>
163+
<Feature features={['performance-view']}>
164+
<SidebarItem
165+
item={{
166+
label: t('Insights'),
167+
icon: <IconGraph />,
168+
analyticsKey: 'insights-domains',
169+
to: `/${prefix}/insights/frontend/`,
170+
}}
171+
/>
172+
</Feature>
173+
<Feature
174+
features={['performance-view']}
175+
hookName="feature-disabled:performance-sidebar-item"
176+
>
177+
<SidebarItem
178+
item={{
179+
label: t('Perf.'),
180+
icon: <IconLightning />,
181+
analyticsKey: 'performance',
182+
to: `/${prefix}/performance/`,
183+
}}
184+
/>
185+
</Feature>
186+
<Feature
187+
features={['discover', 'discover-query', 'dashboards-basic', 'dashboards-edit']}
188+
hookName="feature-disabled:dashboards-sidebar-item"
189+
requireAll={false}
190+
>
191+
<SidebarItem
192+
item={{
193+
label: t('Boards'),
194+
icon: <IconDashboard />,
195+
analyticsKey: 'customizable-dashboards',
196+
to: `/${prefix}/dashboards/`,
197+
}}
198+
/>
199+
</Feature>
200+
<SidebarItem
201+
item={{
202+
label: t('Alerts'),
203+
icon: <IconSiren />,
204+
analyticsKey: 'alerts',
205+
to: `/${prefix}/alerts/rules/`,
206+
}}
207+
/>
208+
</SidebarBody>
209+
<SidebarFooter>
210+
<SidebarItem
211+
item={{
212+
label: t('Help'),
213+
icon: <IconQuestion />,
214+
analyticsKey: 'help',
215+
dropdown: [
216+
{
217+
key: 'search',
218+
label: t('Search Support, Docs and More'),
219+
onAction() {
220+
openHelpSearchModal({organization});
221+
},
222+
},
223+
{
224+
key: 'help',
225+
label: t('Visit Help Center'),
226+
to: 'https://sentry.zendesk.com/hc/en-us',
227+
},
228+
{
229+
key: 'discord',
230+
label: t('Join our Discord'),
231+
to: 'https://discord.com/invite/sentry',
232+
},
233+
{
234+
key: 'support',
235+
label: t('Contact Support'),
236+
to: `mailto:${ConfigStore.get('supportEmail')}`,
237+
},
238+
],
239+
}}
240+
/>
241+
<SidebarItem
242+
item={{
243+
label: t('Settings'),
244+
icon: <IconSettings />,
245+
analyticsKey: 'settings',
246+
to: `${prefix}/settings/${organization.slug}/`,
247+
}}
248+
/>
249+
</SidebarFooter>
250+
</Fragment>
251+
);
252+
}
253+
254+
const SidebarItemList = styled('ul')`
255+
position: relative;
256+
list-style: none;
257+
margin: 0;
258+
padding: 0;
259+
padding-top: ${space(1)};
260+
display: flex;
261+
flex-direction: column;
262+
width: 100%;
263+
color: rgba(255, 255, 255, 0.85);
264+
265+
@media screen and (min-width: ${p => p.theme.breakpoints.medium}) {
266+
gap: ${space(1)};
267+
}
268+
`;
269+
270+
const SidebarItemWrapper = styled('li')`
271+
svg {
272+
--size: 14px;
273+
width: var(--size);
274+
height: var(--size);
275+
276+
@media (min-width: ${p => p.theme.breakpoints.medium}) {
277+
--size: 18px;
278+
padding-top: ${space(0.5)};
279+
}
280+
}
281+
> a,
282+
button {
283+
display: flex;
284+
flex-direction: row;
285+
height: 40px;
286+
gap: ${space(1.5)};
287+
align-items: center;
288+
padding: auto ${space(1.5)};
289+
color: var(--color, currentColor);
290+
font-size: ${p => p.theme.fontSizeMedium};
291+
font-weight: ${p => p.theme.fontWeightNormal};
292+
line-height: 177.75%;
293+
294+
& > * {
295+
pointer-events: none;
296+
}
297+
298+
@media (min-width: ${p => p.theme.breakpoints.medium}) {
299+
flex-direction: column;
300+
justify-content: center;
301+
height: 52px;
302+
padding: ${space(0.5)} ${space(0.75)};
303+
border-radius: ${p => p.theme.borderRadius};
304+
font-size: ${p => p.theme.fontSizeExtraSmall};
305+
margin-inline: ${space(1)};
306+
gap: ${space(0.5)};
307+
}
308+
}
309+
`;
310+
311+
const SidebarFooterWrapper = styled('div')`
312+
position: relative;
313+
border-top: 1px solid ${p => p.theme.translucentGray200};
314+
display: flex;
315+
flex-direction: row;
316+
align-items: stretch;
317+
padding-bottom: ${space(0.5)};
318+
margin-top: auto;
319+
`;
320+
321+
const NavLink = styled(Link)`
322+
position: relative;
323+
`;
324+
325+
const NavButton = styled('button')`
326+
border: none;
327+
position: relative;
328+
background: transparent;
329+
min-width: 58px;
330+
331+
${linkStyles}
332+
`;

0 commit comments

Comments
 (0)