From 930c7533409ab366d8d6d874a26901ff768f9a12 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 10 Oct 2023 14:55:52 +0100 Subject: [PATCH] Navigation: Implement logic for docking nav menu (#76188) * Create a state for dockedMegaMenu and the function to manage it * Add the dockedMenu icon and handle the status when clicking it * Add Megamenu to section nav area when it is docked * get logic working * fix mobile * refactor state + persist in localStorage * adjust icon and don't use position absolute * restore old rudderstack tracking * use Flex instead * adjust feature toggle to be experimental * extract out localStorage handling into utils * don't need separate file * use store.set/get instead --------- Co-authored-by: eledobleefe --- .../feature-toggles/index.md | 2 +- packages/grafana-data/src/types/icon.ts | 1 + pkg/services/featuremgmt/registry.go | 2 +- pkg/services/featuremgmt/toggles_gen.csv | 2 +- .../core/components/AppChrome/AppChrome.tsx | 14 +++++- .../components/AppChrome/AppChromeMenu.tsx | 4 +- .../components/AppChrome/AppChromeService.tsx | 24 +++++---- .../DockedMegaMenu/MegaMenu.test.tsx | 2 +- .../AppChrome/DockedMegaMenu/MegaMenu.tsx | 49 ++++++++++++++++--- .../AppChrome/DockedMegaMenu/MegaMenuItem.tsx | 13 +++-- .../AppChrome/MegaMenu/MegaMenu.test.tsx | 2 +- .../AppChrome/MegaMenu/NavBarMenu.tsx | 4 +- public/app/core/components/Page/Page.tsx | 3 +- public/locales/de-DE/grafana.json | 5 ++ public/locales/en-US/grafana.json | 5 ++ public/locales/es-ES/grafana.json | 5 ++ public/locales/fr-FR/grafana.json | 5 ++ public/locales/pseudo-LOCALE/grafana.json | 5 ++ public/locales/zh-Hans/grafana.json | 5 ++ 19 files changed, 117 insertions(+), 35 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 342e22368fb..06727f9e537 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -64,7 +64,6 @@ Some features are enabled by default. You can disable these feature by setting t | `newDBLibrary` | Use jmoiron/sqlx rather than xorm for a few backend services | | `autoMigrateOldPanels` | Migrate old angular panels to supported versions (graph, table-old, worldmap, etc) | | `disableAngular` | Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime. | -| `dockedMegaMenu` | Enable support for a persistent (docked) navigation menu | | `grpcServer` | Run the GRPC server | | `accessControlOnCall` | Access control primitives for OnCall | | `nestedFolders` | Enable folder nesting | @@ -97,6 +96,7 @@ Experimental features might be changed or removed without prior notice. | `scenes` | Experimental framework to build interactive dashboards | | `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables | | `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown | +| `dockedMegaMenu` | Enable support for a persistent (docked) navigation menu | | `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema | | `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query | | `alertingBacktesting` | Rule backtesting API for alerting | diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts index 9d957baf273..d2e9d9303bf 100644 --- a/packages/grafana-data/src/types/icon.ts +++ b/packages/grafana-data/src/types/icon.ts @@ -225,6 +225,7 @@ export const availableIconsIndex = { 'vertical-align-bottom': true, 'vertical-align-center': true, 'vertical-align-top': true, + 'web-section-alt': true, 'wrap-text': true, rss: true, x: true, diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 37fa797b436..cde567f677d 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -164,7 +164,7 @@ var ( { Name: "dockedMegaMenu", Description: "Enable support for a persistent (docked) navigation menu", - Stage: FeatureStagePublicPreview, + Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaFrontendPlatformSquad, }, diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 0a3882cb846..539bf40ec1f 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -22,7 +22,7 @@ disableSecretsCompatibility,experimental,@grafana/hosted-grafana-team,false,fals logRequestsInstrumentedAsUnknown,experimental,@grafana/hosted-grafana-team,false,false,false,false dataConnectionsConsole,GA,@grafana/plugins-platform-backend,false,false,false,false topnav,deprecated,@grafana/grafana-frontend-platform,false,false,false,false -dockedMegaMenu,preview,@grafana/grafana-frontend-platform,false,false,false,true +dockedMegaMenu,experimental,@grafana/grafana-frontend-platform,false,false,false,true grpcServer,preview,@grafana/grafana-app-platform-squad,false,false,false,false entityStore,experimental,@grafana/grafana-app-platform-squad,true,false,false,false cloudWatchCrossAccountQuerying,GA,@grafana/aws-datasources,false,false,false,false diff --git a/public/app/core/components/AppChrome/AppChrome.tsx b/public/app/core/components/AppChrome/AppChrome.tsx index 5c2f458f9c1..ae02e558f86 100644 --- a/public/app/core/components/AppChrome/AppChrome.tsx +++ b/public/app/core/components/AppChrome/AppChrome.tsx @@ -10,6 +10,7 @@ import { CommandPalette } from 'app/features/commandPalette/CommandPalette'; import { KioskMode } from 'app/types'; import { AppChromeMenu } from './AppChromeMenu'; +import { MegaMenu as DockedMegaMenu } from './DockedMegaMenu/MegaMenu'; import { MegaMenu } from './MegaMenu/MegaMenu'; import { NavToolbar } from './NavToolbar/NavToolbar'; import { SectionNav } from './SectionNav/SectionNav'; @@ -53,7 +54,7 @@ export function AppChrome({ children }: Props) { pageNav={state.pageNav} actions={state.actions} onToggleSearchBar={chrome.onToggleSearchBar} - onToggleMegaMenu={chrome.onToggleMegaMenu} + onToggleMegaMenu={() => chrome.setMegaMenu(state.megaMenu === 'closed' ? 'open' : 'closed')} onToggleKioskMode={chrome.onToggleKioskMode} /> @@ -64,6 +65,9 @@ export function AppChrome({ children }: Props) { {state.layout === PageLayoutType.Standard && state.sectionNav && !config.featureToggles.dockedMegaMenu && ( )} + {config.featureToggles.dockedMegaMenu && !state.chromeless && state.megaMenu === 'docked' && ( + chrome.setMegaMenu('closed')} /> + )}
{children}
@@ -74,7 +78,7 @@ export function AppChrome({ children }: Props) { {config.featureToggles.dockedMegaMenu ? ( ) : ( - chrome.setMegaMenu(false)} /> + chrome.setMegaMenu('closed')} /> )} @@ -102,6 +106,12 @@ const getStyles = (theme: GrafanaTheme2) => { contentChromeless: css({ paddingTop: 0, }), + dockedMegaMenu: css({ + background: theme.colors.background.primary, + borderRight: `1px solid ${theme.colors.border.weak}`, + borderTop: `1px solid ${theme.colors.border.weak}`, + zIndex: theme.zIndex.navbarFixed, + }), topNav: css({ display: 'flex', position: 'fixed', diff --git a/public/app/core/components/AppChrome/AppChromeMenu.tsx b/public/app/core/components/AppChrome/AppChromeMenu.tsx index ab164b0639d..a204e7fc9b3 100644 --- a/public/app/core/components/AppChrome/AppChromeMenu.tsx +++ b/public/app/core/components/AppChrome/AppChromeMenu.tsx @@ -27,8 +27,8 @@ export function AppChromeMenu({}: Props) { const animationSpeed = theme.transitions.duration.shortest; const animationStyles = useStyles2(getAnimStyles, animationSpeed); - const isOpen = state.megaMenuOpen; - const onClose = () => chrome.setMegaMenu(false); + const isOpen = state.megaMenu === 'open'; + const onClose = () => chrome.setMegaMenu('closed'); const { overlayProps, underlayProps } = useOverlay( { diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx index dafbbdcaf68..c39a25766d0 100644 --- a/public/app/core/components/AppChrome/AppChromeService.tsx +++ b/public/app/core/components/AppChrome/AppChromeService.tsx @@ -2,7 +2,7 @@ import { useObservable } from 'react-use'; import { BehaviorSubject } from 'rxjs'; import { AppEvents, NavModel, NavModelItem, PageLayoutType, UrlQueryValue } from '@grafana/data'; -import { locationService, reportInteraction } from '@grafana/runtime'; +import { config, locationService, reportInteraction } from '@grafana/runtime'; import appEvents from 'app/core/app_events'; import { t } from 'app/core/internationalization'; import store from 'app/core/store'; @@ -17,11 +17,13 @@ export interface AppChromeState { pageNav?: NavModelItem; actions?: React.ReactNode; searchBarHidden?: boolean; - megaMenuOpen?: boolean; + megaMenu: 'open' | 'closed' | 'docked'; kioskMode: KioskMode | null; layout: PageLayoutType; } +const DOCKED_LOCAL_STORAGE_KEY = 'grafana.navigation.docked'; + export class AppChromeService { searchBarStorageKey = 'SearchBar_Hidden'; private currentRoute?: RouteDescriptor; @@ -31,6 +33,8 @@ export class AppChromeService { chromeless: true, // start out hidden to not flash it on pages without chrome sectionNav: { node: { text: t('nav.home.title', 'Home') }, main: { text: '' } }, searchBarHidden: store.getBool(this.searchBarStorageKey, false), + megaMenu: + config.featureToggles.dockedMegaMenu && store.getBool(DOCKED_LOCAL_STORAGE_KEY, false) ? 'docked' : 'closed', kioskMode: null, layout: PageLayoutType.Canvas, }); @@ -93,14 +97,14 @@ export class AppChromeService { return useObservable(this.state, this.state.getValue()); } - public onToggleMegaMenu = () => { - const isOpen = !this.state.getValue().megaMenuOpen; - reportInteraction('grafana_toggle_menu_clicked', { action: isOpen ? 'open' : 'close' }); - this.update({ megaMenuOpen: isOpen }); - }; - - public setMegaMenu = (megaMenuOpen: boolean) => { - this.update({ megaMenuOpen }); + public setMegaMenu = (newMegaMenuState: AppChromeState['megaMenu']) => { + if (config.featureToggles.dockedMegaMenu) { + store.set(DOCKED_LOCAL_STORAGE_KEY, newMegaMenuState === 'docked'); + reportInteraction('grafana_mega_menu_state', { state: newMegaMenuState }); + } else { + reportInteraction('grafana_toggle_menu_clicked', { action: newMegaMenuState === 'open' ? 'open' : 'close' }); + } + this.update({ megaMenu: newMegaMenuState }); }; public onToggleSearchBar = () => { diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.test.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.test.tsx index ad5dd4e8bb0..a914fda4cac 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.test.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.test.tsx @@ -35,7 +35,7 @@ const setup = () => { ]; const grafanaContext = getGrafanaContextMock(); - grafanaContext.chrome.onToggleMegaMenu(); + grafanaContext.chrome.setMegaMenu('open'); return render( diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx index 6915a9a2270..d4459cb11ad 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx @@ -6,6 +6,9 @@ import { useLocation } from 'react-router-dom'; import { GrafanaTheme2 } from '@grafana/data'; import { CustomScrollbar, Icon, IconButton, useStyles2 } from '@grafana/ui'; +import { Flex } from '@grafana/ui/src/unstable'; +import { useGrafana } from 'app/core/context/GrafanaContext'; +import { t } from 'app/core/internationalization'; import { useSelector } from 'app/types'; import { MegaMenuItem } from './MegaMenuItem'; @@ -22,6 +25,8 @@ export const MegaMenu = React.memo( const navBarTree = useSelector((state) => state.navBarTree); const styles = useStyles2(getStyles); const location = useLocation(); + const { chrome } = useGrafana(); + const state = chrome.useState(); const navTree = cloneDeep(navBarTree); @@ -32,13 +37,16 @@ export const MegaMenu = React.memo( const activeItem = getActiveItem(navItems, location.pathname); + const handleDockedMenu = () => { + chrome.setMegaMenu(state.megaMenu === 'docked' ? 'closed' : 'docked'); + }; + return (
    - {navItems.map((link) => ( - + {navItems.map((link, index) => ( + + + {index === 0 && ( + + )} + ))}
@@ -65,8 +92,9 @@ const getStyles = (theme: GrafanaTheme2) => ({ content: css({ display: 'flex', flexDirection: 'column', - flexGrow: 1, + height: '100%', minHeight: 0, + position: 'relative', }), mobileHeader: css({ display: 'flex', @@ -79,10 +107,15 @@ const getStyles = (theme: GrafanaTheme2) => ({ }, }), itemList: css({ - display: 'grid', - gridAutoRows: `minmax(${theme.spacing(6)}, auto)`, - gridTemplateColumns: `minmax(${MENU_WIDTH}, auto)`, + display: 'flex', + flexDirection: 'column', listStyleType: 'none', minWidth: MENU_WIDTH, + [theme.breakpoints.up('md')]: { + width: MENU_WIDTH, + }, + }), + dockMenuButton: css({ + marginRight: theme.spacing(2), }), }); diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItem.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItem.tsx index c99000e5431..2518d035889 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItem.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItem.tsx @@ -15,11 +15,11 @@ import { hasChildMatch } from './utils'; interface Props { link: NavModelItem; activeItem?: NavModelItem; - onClose?: () => void; + onClick?: () => void; level?: number; } -export function MegaMenuItem({ link, activeItem, level = 0, onClose }: Props) { +export function MegaMenuItem({ link, activeItem, level = 0, onClick }: Props) { const styles = useStyles2(getStyles); const FeatureHighlightWrapper = link.highlightText ? FeatureHighlight : React.Fragment; const isActive = link === activeItem; @@ -29,13 +29,13 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClose }: Props) { const showExpandButton = linkHasChildren(link) || link.emptyMessage; return ( -
  • +
  • { link.onClick?.(); - onClose?.(); + onClick?.(); }} target={link.target} url={link.url} @@ -75,7 +75,7 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClose }: Props) { key={`${link.text}-${childLink.text}`} link={childLink} activeItem={activeItem} - onClose={onClose} + onClick={onClick} level={level + 1} /> )) @@ -121,6 +121,9 @@ const getStyles = (theme: GrafanaTheme2) => ({ alignItems: 'center', fontWeight: theme.typography.fontWeightMedium, }), + listItem: css({ + flex: 1, + }), isActive: css({ color: theme.colors.text.primary, diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.test.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.test.tsx index a411c977ff9..59551594e72 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.test.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.test.tsx @@ -29,7 +29,7 @@ const setup = () => { ]; const grafanaContext = getGrafanaContextMock(); - grafanaContext.chrome.onToggleMegaMenu(); + grafanaContext.chrome.setMegaMenu('open'); return render( diff --git a/public/app/core/components/AppChrome/MegaMenu/NavBarMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/NavBarMenu.tsx index 6879e4f1cda..18e6411a900 100644 --- a/public/app/core/components/AppChrome/MegaMenu/NavBarMenu.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/NavBarMenu.tsx @@ -46,10 +46,10 @@ export function NavBarMenu({ activeItem, navItems, searchBarHidden, onClose }: P ); useEffect(() => { - if (state.megaMenuOpen) { + if (state.megaMenu === 'open') { setIsOpen(true); } - }, [state.megaMenuOpen]); + }, [state.megaMenu]); return ( diff --git a/public/app/core/components/Page/Page.tsx b/public/app/core/components/Page/Page.tsx index 595cd466199..f2490a2d91f 100644 --- a/public/app/core/components/Page/Page.tsx +++ b/public/app/core/components/Page/Page.tsx @@ -3,6 +3,7 @@ import { css, cx } from '@emotion/css'; import React, { useLayoutEffect } from 'react'; import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { CustomScrollbar, useStyles2 } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; @@ -108,7 +109,7 @@ const getStyles = (theme: GrafanaTheme2) => { margin: theme.spacing(0, 0, 0, 0), [theme.breakpoints.up('md')]: { - margin: theme.spacing(2, 2, 0, 1), + margin: theme.spacing(2, 2, 0, config.featureToggles.dockedMegaMenu ? 2 : 1), padding: theme.spacing(3), }, }), diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 6b13a287858..d4a27b44448 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -722,6 +722,11 @@ "kiosk": { "tv-alert": "Drücke ESC, um den Kiosk-Modus zu verlassen" }, + "megamenu": { + "close": "", + "dock": "", + "undock": "" + }, "toolbar": { "enable-kiosk": "Kiosk-Modus aktivieren", "toggle-menu": "Menü umschalten", diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 9d4ae262e9d..2680839a488 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -722,6 +722,11 @@ "kiosk": { "tv-alert": "Press ESC to exit kiosk mode" }, + "megamenu": { + "close": "Close menu", + "dock": "Dock menu", + "undock": "Undock menu" + }, "toolbar": { "enable-kiosk": "Enable kiosk mode", "toggle-menu": "Toggle menu", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 55fbec548ea..efdcacb0dad 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -728,6 +728,11 @@ "kiosk": { "tv-alert": "Pulse ESC para salir del modo de quiosco" }, + "megamenu": { + "close": "", + "dock": "", + "undock": "" + }, "toolbar": { "enable-kiosk": "Activar el modo de quiosco", "toggle-menu": "Activar o desactivar menú", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index fa76398f0ba..dcdca2e1f2d 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -728,6 +728,11 @@ "kiosk": { "tv-alert": "Appuyez sur ESC pour quitter le mode kiosque" }, + "megamenu": { + "close": "", + "dock": "", + "undock": "" + }, "toolbar": { "enable-kiosk": "Activer le mode kiosque", "toggle-menu": "Afficher/Masquer le menu", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index e54ce1ec676..d8e037af51e 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -722,6 +722,11 @@ "kiosk": { "tv-alert": "Přęşş ĒŜC ŧő ęχįŧ ĸįőşĸ mőđę" }, + "megamenu": { + "close": "Cľőşę męʼnū", + "dock": "Đőčĸ męʼnū", + "undock": "Ůʼnđőčĸ męʼnū" + }, "toolbar": { "enable-kiosk": "Ēʼnäþľę ĸįőşĸ mőđę", "toggle-menu": "Ŧőģģľę męʼnū", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index f7bcbfbe56f..061da99ba8b 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -716,6 +716,11 @@ "kiosk": { "tv-alert": "按 ESC 退出 kiosk 模式" }, + "megamenu": { + "close": "", + "dock": "", + "undock": "" + }, "toolbar": { "enable-kiosk": "启用 kiosk 模式", "toggle-menu": "切换菜单",