Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stacked Nav #77665

Merged
merged 46 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
9f8655f
feat(nav): new nav components
natemoo-re Sep 13, 2024
e21aa1a
feat(nav): update app layout
natemoo-re Sep 13, 2024
94e6fd4
fix(nav): aria-current logic
natemoo-re Sep 13, 2024
02c4037
fix(nav): remove AnimatePresence
natemoo-re Sep 13, 2024
a782803
feat(issues): add issue group consts
natemoo-re Sep 17, 2024
f1c29a9
refactor(nav): support feature flagged nav items
natemoo-re Sep 17, 2024
ab001ec
fix(nav): adjust insights display
natemoo-re Sep 17, 2024
64820fc
wip(nav): mobile layout
natemoo-re Sep 18, 2024
6e2b4d2
feat(nav): add top-level mobile menu
natemoo-re Sep 19, 2024
0de0a47
fix(nav): adjust mobile layout
natemoo-re Sep 19, 2024
d1cc909
fix(nav): issue group highlighting
natemoo-re Sep 19, 2024
567a016
temp(nav): hardcode feature flag
natemoo-re Sep 20, 2024
c581439
fix(nav): improve issue filter ux
natemoo-re Sep 20, 2024
0a410e8
test(nav): add basic test
natemoo-re Sep 20, 2024
5443884
fix(nav): improve accessibility
natemoo-re Sep 20, 2024
d57fb4d
fix(nav): revert hardcoded flag
natemoo-re Sep 20, 2024
fb9623e
tmp(nav): remove non-link footer items
natemoo-re Sep 20, 2024
ff968f8
chore(nav): add braces to if statement
natemoo-re Sep 20, 2024
a31522d
test(nav): fix test assertion
natemoo-re Sep 20, 2024
01f5bfc
refactor(nav): update util naming
natemoo-re Sep 23, 2024
ec11c47
refactor(nav): prefer pure function for location descriptor util
natemoo-re Sep 23, 2024
a0ffd32
Update static/app/types/hooks.tsx
natemoo-re Sep 23, 2024
368b7d7
refactor(hooks): update type name
natemoo-re Sep 23, 2024
074555a
Update static/app/components/nav/config.tsx
natemoo-re Sep 23, 2024
92db803
Update static/app/components/nav/index.spec.tsx
natemoo-re Sep 23, 2024
c9ef43a
:hammer_and_wrench: apply pre-commit fixes
getsantry[bot] Sep 23, 2024
3d7f477
refactor(nav): simplify mobile effect logic
natemoo-re Sep 24, 2024
e360eed
fix(nav): adjust types
natemoo-re Sep 24, 2024
69aaec5
refactor(nav): rename hook, remove useState, respect `prefers-reduced…
natemoo-re Sep 24, 2024
4cdf4bb
refactor(nav): use standard Feature interface
natemoo-re Sep 24, 2024
0581985
refactor(nav): simplify config, add jsdoc comments
natemoo-re Sep 24, 2024
41a03f0
test(nav): improve tests
natemoo-re Sep 24, 2024
a115884
fix(nav): fix edge cases
natemoo-re Sep 24, 2024
9356c17
chore: remove querySelector caching
natemoo-re Sep 25, 2024
940a746
Update static/app/components/nav/utils.tsx
natemoo-re Sep 25, 2024
bf4783a
chore: prefer strict union
natemoo-re Sep 25, 2024
09bd87c
chore(nav): use helper for submenu logic
natemoo-re Sep 25, 2024
ad4bff0
chore(nav): destructure nav config
natemoo-re Sep 25, 2024
23e9fae
chore(nav): use URLSearchParams to encode search
natemoo-re Sep 25, 2024
eb959b1
chore(nav): add comment
natemoo-re Sep 25, 2024
7740326
chore(nav): update hardcoded values
natemoo-re Sep 25, 2024
429a566
refactor(nav): simplify overlay
natemoo-re Sep 25, 2024
56ab2e9
refactor(nav): move helper up
natemoo-re Sep 25, 2024
253b9dd
refactor(nav): use less generic selector
natemoo-re Sep 25, 2024
c09074f
chore(nav): add comment
natemoo-re Sep 25, 2024
5cf995f
fix(nav): wrap with useCallback
natemoo-re Sep 25, 2024
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
174 changes: 174 additions & 0 deletions static/app/components/nav/config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import type {NavConfig} from 'sentry/components/nav/utils';
import {
IconDashboard,
IconGraph,
IconIssues,
IconLightning,
IconProject,
IconSearch,
IconSettings,
IconSiren,
} from 'sentry/icons';
import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import {getDiscoverLandingUrl} from 'sentry/utils/discover/urls';
import {MODULE_BASE_URLS} from 'sentry/views/insights/common/utils/useModuleURL';
import {MODULE_SIDEBAR_TITLE as MODULE_TITLE_HTTP} from 'sentry/views/insights/http/settings';
import {INSIGHTS_BASE_URL, MODULE_TITLES} from 'sentry/views/insights/settings';
import {getSearchForIssueGroup, IssueGroup} from 'sentry/views/issueList/utils';

/**
* Global nav settings for all Sentry users.
* Links are generated per-organization with the proper `/organization/:slug/` prefix.
*
* To permission-gate certain items, include props to be passed to the `<Feature>` component
*/
export function createNavConfig({organization}: {organization: Organization}): NavConfig {
const prefix = `organizations/${organization.slug}`;
const insightsPrefix = `${prefix}/${INSIGHTS_BASE_URL}`;

return {
main: [
{
label: t('Issues'),
icon: <IconIssues />,
submenu: [
{
label: t('All'),
to: `/${prefix}/issues/?query=is:unresolved`,
},
{
label: t('Error & Outage'),
to: `/${prefix}/issues/${getSearchForIssueGroup(IssueGroup.ERROR_OUTAGE)}`,
},
{
label: t('Trend'),
to: `/${prefix}/issues/${getSearchForIssueGroup(IssueGroup.TREND)}`,
},
{
label: t('Craftsmanship'),
to: `/${prefix}/issues/${getSearchForIssueGroup(IssueGroup.CRAFTSMANSHIP)}`,
},
{
label: t('Security'),
to: `/${prefix}/issues/${getSearchForIssueGroup(IssueGroup.SECURITY)}`,
},
{label: t('Feedback'), to: `/${prefix}/feedback/`},
],
},
{label: t('Projects'), to: `/${prefix}/projects/`, icon: <IconProject />},
{
label: t('Explore'),
icon: <IconSearch />,
submenu: [
{
label: t('Traces'),
to: `/${prefix}/traces/`,
feature: {features: 'performance-trace-explorer'},
},
{
label: t('Metrics'),
to: `/${prefix}/metrics/`,
feature: {features: 'custom-metrics'},
},
{
label: t('Profiles'),
to: `/${prefix}/profiling/`,
feature: {
features: 'profiling',
hookName: 'feature-disabled:profiling-sidebar-item',
requireAll: false,
},
},
{
label: t('Replays'),
to: `/${prefix}/replays/`,
feature: {
features: 'session-replay-ui',
hookName: 'feature-disabled:replay-sidebar-item',
},
},
{
label: t('Discover'),
to: getDiscoverLandingUrl(organization),
feature: {
features: 'discover-basic',
hookName: 'feature-disabled:discover2-sidebar-item',
},
},
{label: t('Releases'), to: `/${prefix}/releases/`},
{label: t('Crons'), to: `/${prefix}/crons/`},
],
},
{
label: t('Insights'),
icon: <IconGraph />,
feature: {features: 'insights-entry-points'},
submenu: [
{
label: MODULE_TITLE_HTTP,
to: `/${insightsPrefix}/${MODULE_BASE_URLS.http}/`,
},
{label: MODULE_TITLES.db, to: `/${insightsPrefix}/${MODULE_BASE_URLS.db}/`},
{
label: MODULE_TITLES.resource,
to: `/${insightsPrefix}/${MODULE_BASE_URLS.resource}/`,
},
{
label: MODULE_TITLES.app_start,
to: `/${insightsPrefix}/${MODULE_BASE_URLS.app_start}/`,
},
{
label: MODULE_TITLES['mobile-screens'],
to: `/${insightsPrefix}/${MODULE_BASE_URLS['mobile-screens']}/`,
feature: {features: 'insights-mobile-screens-module'},
},
{
label: MODULE_TITLES.vital,
to: `/${insightsPrefix}/${MODULE_BASE_URLS.vital}/`,
},
{
label: MODULE_TITLES.cache,
to: `/${insightsPrefix}/${MODULE_BASE_URLS.cache}/`,
},
{
label: MODULE_TITLES.queue,
to: `/${insightsPrefix}/${MODULE_BASE_URLS.queue}/`,
},
{
label: MODULE_TITLES.ai,
to: `/${insightsPrefix}/${MODULE_BASE_URLS.ai}/`,
feature: {features: 'insights-entry-points'},
},
],
},
{
label: t('Perf.'),
to: '/performance/',
icon: <IconLightning />,
feature: {
features: 'performance-view',
hookName: 'feature-disabled:performance-sidebar-item',
},
},
{
label: t('Boards'),
to: '/dashboards/',
icon: <IconDashboard />,
feature: {
features: ['discover', 'discover-query', 'dashboards-basic', 'dashboards-edit'],
hookName: 'feature-disabled:dashboards-sidebar-item',
requireAll: false,
},
},
{label: t('Alerts'), to: `/${prefix}/alerts/rules/`, icon: <IconSiren />},
],
footer: [
{
label: t('Settings'),
to: `/settings/${organization.slug}/`,
icon: <IconSettings />,
},
],
};
}
61 changes: 61 additions & 0 deletions static/app/components/nav/context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {createContext, useContext, useMemo} from 'react';

import {createNavConfig} from 'sentry/components/nav/config';
import type {
NavConfig,
NavItemLayout,
NavSidebarItem,
NavSubmenuItem,
} from 'sentry/components/nav/utils';
import {isNavItemActive, isSubmenuItemActive} from 'sentry/components/nav/utils';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';

export interface NavContext {
/** Raw config for entire nav items */
config: Readonly<NavConfig>;
/** Currently active submenu items, if any */
submenu?: Readonly<NavItemLayout<NavSubmenuItem>>;
}

const NavContext = createContext<NavContext>({config: {main: []}});

export function useNavContext(): NavContext {
const navContext = useContext(NavContext);
return navContext;
}

export function NavContextProvider({children}) {
const organization = useOrganization();
const location = useLocation();
/** Raw nav configuration values */
const config = useMemo(() => createNavConfig({organization}), [organization]);
/**
* Active submenu items derived from the nav config and current `location`.
* These are returned in a normalized layout format for ease of use.
*/
const submenu = useMemo<NavContext['submenu']>(() => {
for (const item of config.main) {
if (isNavItemActive(item, location) || isSubmenuItemActive(item, location)) {
return normalizeSubmenu(item.submenu);
}
}
if (config.footer) {
for (const item of config.footer) {
if (isNavItemActive(item, location) || isSubmenuItemActive(item, location)) {
return normalizeSubmenu(item.submenu);
}
}
}
return undefined;
}, [config, location]);

return <NavContext.Provider value={{config, submenu}}>{children}</NavContext.Provider>;
}

const normalizeSubmenu = (submenu: NavSidebarItem['submenu']): NavContext['submenu'] => {
if (Array.isArray(submenu)) {
return {main: submenu};
}
return submenu;
};
Loading
Loading