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 <laura.fernandez@grafana.com>
pull/76212/head
Ashley Harrison 2 years ago committed by GitHub
parent de2d8f50e8
commit 930c753340
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
  2. 1
      packages/grafana-data/src/types/icon.ts
  3. 2
      pkg/services/featuremgmt/registry.go
  4. 2
      pkg/services/featuremgmt/toggles_gen.csv
  5. 14
      public/app/core/components/AppChrome/AppChrome.tsx
  6. 4
      public/app/core/components/AppChrome/AppChromeMenu.tsx
  7. 24
      public/app/core/components/AppChrome/AppChromeService.tsx
  8. 2
      public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.test.tsx
  9. 49
      public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx
  10. 13
      public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItem.tsx
  11. 2
      public/app/core/components/AppChrome/MegaMenu/MegaMenu.test.tsx
  12. 4
      public/app/core/components/AppChrome/MegaMenu/NavBarMenu.tsx
  13. 3
      public/app/core/components/Page/Page.tsx
  14. 5
      public/locales/de-DE/grafana.json
  15. 5
      public/locales/en-US/grafana.json
  16. 5
      public/locales/es-ES/grafana.json
  17. 5
      public/locales/fr-FR/grafana.json
  18. 5
      public/locales/pseudo-LOCALE/grafana.json
  19. 5
      public/locales/zh-Hans/grafana.json

@ -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 |

@ -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,

@ -164,7 +164,7 @@ var (
{
Name: "dockedMegaMenu",
Description: "Enable support for a persistent (docked) navigation menu",
Stage: FeatureStagePublicPreview,
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaFrontendPlatformSquad,
},

@ -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

1 Name Stage Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
22 logRequestsInstrumentedAsUnknown experimental @grafana/hosted-grafana-team false false false false
23 dataConnectionsConsole GA @grafana/plugins-platform-backend false false false false
24 topnav deprecated @grafana/grafana-frontend-platform false false false false
25 dockedMegaMenu preview experimental @grafana/grafana-frontend-platform false false false true
26 grpcServer preview @grafana/grafana-app-platform-squad false false false false
27 entityStore experimental @grafana/grafana-app-platform-squad true false false false
28 cloudWatchCrossAccountQuerying GA @grafana/aws-datasources false false false false

@ -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}
/>
</div>
@ -64,6 +65,9 @@ export function AppChrome({ children }: Props) {
{state.layout === PageLayoutType.Standard && state.sectionNav && !config.featureToggles.dockedMegaMenu && (
<SectionNav model={state.sectionNav} />
)}
{config.featureToggles.dockedMegaMenu && !state.chromeless && state.megaMenu === 'docked' && (
<DockedMegaMenu className={styles.dockedMegaMenu} onClose={() => chrome.setMegaMenu('closed')} />
)}
<div className={styles.pageContainer} id="pageContent">
{children}
</div>
@ -74,7 +78,7 @@ export function AppChrome({ children }: Props) {
{config.featureToggles.dockedMegaMenu ? (
<AppChromeMenu />
) : (
<MegaMenu searchBarHidden={searchBarHidden} onClose={() => chrome.setMegaMenu(false)} />
<MegaMenu searchBarHidden={searchBarHidden} onClose={() => chrome.setMegaMenu('closed')} />
)}
<CommandPalette />
</>
@ -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',

@ -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(
{

@ -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 = () => {

@ -35,7 +35,7 @@ const setup = () => {
];
const grafanaContext = getGrafanaContextMock();
grafanaContext.chrome.onToggleMegaMenu();
grafanaContext.chrome.setMegaMenu('open');
return render(
<TestProvider storeState={{ navBarTree }} grafanaContext={grafanaContext}>

@ -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 (
<div data-testid="navbarmenu" ref={ref} {...restProps}>
<div className={styles.mobileHeader}>
<Icon name="bars" size="xl" />
<IconButton
aria-label="Close navigation menu"
tooltip="Close menu"
tooltip={t('navigation.megamenu.close', 'Close menu')}
name="times"
onClick={onClose}
size="xl"
@ -48,8 +56,27 @@ export const MegaMenu = React.memo(
<nav className={styles.content}>
<CustomScrollbar showScrollIndicators hideHorizontalTrack>
<ul className={styles.itemList}>
{navItems.map((link) => (
<MegaMenuItem link={link} onClose={onClose} activeItem={activeItem} key={link.text} />
{navItems.map((link, index) => (
<Flex key={link.text} direction="row" alignItems="center">
<MegaMenuItem
link={link}
onClick={state.megaMenu === 'open' ? onClose : undefined}
activeItem={activeItem}
/>
{index === 0 && (
<IconButton
className={styles.dockMenuButton}
tooltip={
state.megaMenu === 'docked'
? t('navigation.megamenu.undock', 'Undock menu')
: t('navigation.megamenu.dock', 'Dock menu')
}
name="web-section-alt"
onClick={handleDockedMenu}
variant="secondary"
/>
)}
</Flex>
))}
</ul>
</CustomScrollbar>
@ -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),
}),
});

@ -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 (
<li>
<li className={styles.listItem}>
<div className={styles.collapsibleSectionWrapper}>
<MegaMenuItemText
isActive={isActive}
onClick={() => {
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,

@ -29,7 +29,7 @@ const setup = () => {
];
const grafanaContext = getGrafanaContextMock();
grafanaContext.chrome.onToggleMegaMenu();
grafanaContext.chrome.setMegaMenu('open');
return render(
<TestProvider storeState={{ navBarTree }} grafanaContext={grafanaContext}>

@ -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 (
<OverlayContainer>

@ -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),
},
}),

@ -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",

@ -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",

@ -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ú",

@ -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",

@ -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ū",

@ -716,6 +716,11 @@
"kiosk": {
"tv-alert": "按 ESC 退出 kiosk 模式"
},
"megamenu": {
"close": "",
"dock": "",
"undock": ""
},
"toolbar": {
"enable-kiosk": "启用 kiosk 模式",
"toggle-menu": "切换菜单",

Loading…
Cancel
Save