diff --git a/pkg/api/index.go b/pkg/api/index.go
index a4069de409b..98bf217821e 100644
--- a/pkg/api/index.go
+++ b/pkg/api/index.go
@@ -105,7 +105,7 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
}
if hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
- appLink.Url = path.Join(hs.Cfg.AppSubURL, "a", plugin.ID)
+ appLink.Url = hs.Cfg.AppSubURL + "/a/" + plugin.ID
} else {
appLink.Url = path.Join(hs.Cfg.AppSubURL, plugin.DefaultNavURL)
}
@@ -521,6 +521,7 @@ func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b
Text: "Browse", Id: "dashboards/browse", Url: hs.Cfg.AppSubURL + "/dashboards", Icon: "sitemap",
})
}
+
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "Playlists", Id: "dashboards/playlists", Url: hs.Cfg.AppSubURL + "/playlists", Icon: "presentation-play",
})
diff --git a/public/app/core/components/MegaMenu/MegaMenu.test.tsx b/public/app/core/components/MegaMenu/MegaMenu.test.tsx
index 6ca38ce58a2..c8cdd3b531c 100644
--- a/public/app/core/components/MegaMenu/MegaMenu.test.tsx
+++ b/public/app/core/components/MegaMenu/MegaMenu.test.tsx
@@ -56,8 +56,8 @@ describe('MegaMenu', () => {
setup();
expect(await screen.findByTestId('navbarmenu')).toBeInTheDocument();
- expect(await screen.findByLabelText('Home')).toBeInTheDocument();
- expect(screen.queryAllByLabelText('Section name').length).toBe(2);
+ expect(await screen.findByRole('link', { name: 'Home' })).toBeInTheDocument();
+ expect(await screen.findByRole('link', { name: 'Section name' })).toBeInTheDocument();
});
it('should filter out profile', async () => {
diff --git a/public/app/core/components/MegaMenu/NavBarMenu.tsx b/public/app/core/components/MegaMenu/NavBarMenu.tsx
index ca81a5a15c4..95be7b68ac3 100644
--- a/public/app/core/components/MegaMenu/NavBarMenu.tsx
+++ b/public/app/core/components/MegaMenu/NavBarMenu.tsx
@@ -11,9 +11,10 @@ import { CustomScrollbar, Icon, IconButton, useTheme2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { TOP_BAR_LEVEL_HEIGHT } from '../AppChrome/types';
-import { NavItem } from '../NavBar/NavBarMenu';
import { NavBarToggle } from '../NavBar/NavBarToggle';
+import { NavBarMenuItemWrapper } from './NavBarMenuItemWrapper';
+
const MENU_WIDTH = '350px';
export interface Props {
@@ -59,7 +60,7 @@ export function NavBarMenu({ activeItem, navItems, searchBarHidden, onClose }: P
in={isOpen}
unmountOnExit={true}
classNames={animStyles.overlay}
- timeout={animationSpeed}
+ timeout={{ enter: animationSpeed, exit: 0 }}
onExited={onClose}
>
@@ -86,7 +87,7 @@ export function NavBarMenu({ activeItem, navItems, searchBarHidden, onClose }: P
{navItems.map((link) => (
-
+
))}
@@ -121,18 +122,17 @@ const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => {
position: 'fixed',
right: 0,
top: topPosition,
- zIndex: theme.zIndex.navbarFixed - 2,
+ zIndex: theme.zIndex.modalBackdrop,
}),
container: css({
display: 'flex',
bottom: 0,
flexDirection: 'column',
left: 0,
- paddingTop: theme.spacing(1),
marginRight: theme.spacing(1.5),
right: 0,
// Needs to below navbar should we change the navbarFixed? add add a new level?
- zIndex: theme.zIndex.navbarFixed - 1,
+ zIndex: theme.zIndex.modal,
position: 'fixed',
top: topPosition,
backgroundColor: theme.colors.background.primary,
@@ -151,7 +151,7 @@ const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => {
borderBottom: `1px solid ${theme.colors.border.weak}`,
display: 'flex',
justifyContent: 'space-between',
- padding: theme.spacing(1, 2, 2),
+ padding: theme.spacing(1, 2),
[theme.breakpoints.up('md')]: {
display: 'none',
},
@@ -159,11 +159,12 @@ const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => {
itemList: css({
display: 'grid',
gridAutoRows: `minmax(${theme.spacing(6)}, auto)`,
+ gridTemplateColumns: `minmax(${MENU_WIDTH}, auto)`,
minWidth: MENU_WIDTH,
}),
menuCollapseIcon: css({
position: 'absolute',
- top: '43px',
+ top: '20px',
right: '0px',
transform: `translateX(50%)`,
}),
@@ -219,15 +220,11 @@ const getAnimStyles = (theme: GrafanaTheme2, animationDuration: number) => {
enter: css(backdropClosed),
enterActive: css(backdropTransition, backdropOpen),
enterDone: css(backdropOpen),
- exit: css(backdropOpen),
- exitActive: css(backdropTransition, backdropClosed),
},
overlay: {
enter: css(overlayClosed),
enterActive: css(overlayTransition, overlayOpen),
enterDone: css(overlayOpen),
- exit: css(overlayOpen),
- exitActive: css(overlayTransition, overlayClosed),
},
};
};
diff --git a/public/app/core/components/MegaMenu/NavBarMenuItem.tsx b/public/app/core/components/MegaMenu/NavBarMenuItem.tsx
new file mode 100644
index 00000000000..c3dbe7b08b0
--- /dev/null
+++ b/public/app/core/components/MegaMenu/NavBarMenuItem.tsx
@@ -0,0 +1,122 @@
+import { css, cx } from '@emotion/css';
+import React from 'react';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { Icon, IconName, Link, useTheme2 } from '@grafana/ui';
+
+export interface Props {
+ children: React.ReactNode;
+ icon?: IconName;
+ isActive?: boolean;
+ isChild?: boolean;
+ onClick?: () => void;
+ target?: HTMLAnchorElement['target'];
+ url?: string;
+}
+
+export function NavBarMenuItem({ children, icon, isActive, isChild, onClick, target, url }: Props) {
+ const theme = useTheme2();
+ const styles = getStyles(theme, isActive, isChild);
+
+ const linkContent = (
+
+ {icon &&
}
+
+
{children}
+
+ {target === '_blank' && (
+
+ )}
+
+ );
+
+ let element = (
+
+ );
+
+ if (url) {
+ element =
+ !target && url.startsWith('/') ? (
+
+ {linkContent}
+
+ ) : (
+
+ {linkContent}
+
+ );
+ }
+
+ return
{element};
+}
+
+NavBarMenuItem.displayName = 'NavBarMenuItem';
+
+const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive'], isChild: Props['isActive']) => ({
+ button: css({
+ backgroundColor: 'unset',
+ borderStyle: 'unset',
+ }),
+ linkContent: css({
+ alignItems: 'center',
+ display: 'flex',
+ gap: '0.5rem',
+ height: '100%',
+ width: '100%',
+ }),
+ linkText: css({
+ textOverflow: 'ellipsis',
+ overflow: 'hidden',
+ whiteSpace: 'nowrap',
+ }),
+ externalLinkIcon: css({
+ color: theme.colors.text.secondary,
+ }),
+ element: css({
+ alignItems: 'center',
+ boxSizing: 'border-box',
+ position: 'relative',
+ color: isActive ? theme.colors.text.primary : theme.colors.text.secondary,
+ padding: theme.spacing(1, 1, 1, isChild ? 5 : 0),
+ ...(isChild && {
+ borderRadius: theme.shape.borderRadius(),
+ }),
+ width: '100%',
+ '&:hover, &:focus-visible': {
+ ...(isChild && {
+ background: theme.colors.emphasize(theme.colors.background.primary, 0.03),
+ }),
+ textDecoration: 'underline',
+ color: theme.colors.text.primary,
+ },
+ '&:focus-visible': {
+ boxShadow: 'none',
+ outline: `2px solid ${theme.colors.primary.main}`,
+ outlineOffset: '-2px',
+ transition: 'none',
+ },
+ '&::before': {
+ display: isActive ? 'block' : 'none',
+ content: '" "',
+ height: theme.spacing(3),
+ position: 'absolute',
+ left: theme.spacing(1),
+ top: '50%',
+ transform: 'translateY(-50%)',
+ width: theme.spacing(0.5),
+ borderRadius: theme.shape.borderRadius(1),
+ backgroundImage: theme.colors.gradients.brandVertical,
+ },
+ }),
+ listItem: css({
+ boxSizing: 'border-box',
+ position: 'relative',
+ display: 'flex',
+ width: '100%',
+ ...(isChild && {
+ padding: theme.spacing(0, 2),
+ }),
+ }),
+});
diff --git a/public/app/core/components/MegaMenu/NavBarMenuItemWrapper.tsx b/public/app/core/components/MegaMenu/NavBarMenuItemWrapper.tsx
new file mode 100644
index 00000000000..deba44a0ed0
--- /dev/null
+++ b/public/app/core/components/MegaMenu/NavBarMenuItemWrapper.tsx
@@ -0,0 +1,112 @@
+import { css } from '@emotion/css';
+import { useLingui } from '@lingui/react';
+import React from 'react';
+
+import { GrafanaTheme2, NavModelItem } from '@grafana/data';
+import { toIconName, useStyles2 } from '@grafana/ui';
+
+import menuItemTranslations from '../NavBar/navBarItem-translations';
+import { isMatchOrChildMatch } from '../NavBar/utils';
+
+import { NavBarMenuItem } from './NavBarMenuItem';
+import { NavBarMenuSection } from './NavBarMenuSection';
+
+export function NavBarMenuItemWrapper({
+ link,
+ activeItem,
+ onClose,
+}: {
+ link: NavModelItem;
+ activeItem?: NavModelItem;
+ onClose: () => void;
+}) {
+ const { i18n } = useLingui();
+ const styles = useStyles2(getStyles);
+
+ if (link.emptyMessageId && !linkHasChildren(link)) {
+ const emptyMessageTranslated = i18n._(menuItemTranslations[link.emptyMessageId]);
+ return (
+
+
+ {emptyMessageTranslated}
+
+
+ );
+ }
+
+ return (
+
+ {linkHasChildren(link) && (
+
+ {link.children.map((childLink) => {
+ const icon = childLink.icon ? toIconName(childLink.icon) : undefined;
+ return (
+ !childLink.divider && (
+ {
+ childLink.onClick?.();
+ onClose();
+ }}
+ target={childLink.target}
+ url={childLink.url}
+ >
+ {childLink.text}
+
+ )
+ );
+ })}
+
+ )}
+
+ );
+}
+
+const getStyles = (theme: GrafanaTheme2) => ({
+ children: css({
+ display: 'flex',
+ flexDirection: 'column',
+ }),
+ flex: css({
+ display: 'flex',
+ }),
+ itemWithoutMenu: css({
+ position: 'relative',
+ placeItems: 'inherit',
+ justifyContent: 'start',
+ display: 'flex',
+ flexGrow: 1,
+ alignItems: 'center',
+ }),
+ fullWidth: css({
+ height: '100%',
+ width: '100%',
+ }),
+ iconContainer: css({
+ display: 'flex',
+ placeContent: 'center',
+ }),
+ itemWithoutMenuContent: css({
+ display: 'grid',
+ gridAutoFlow: 'column',
+ gridTemplateColumns: `${theme.spacing(7)} auto`,
+ alignItems: 'center',
+ height: '100%',
+ }),
+ linkText: css({
+ fontSize: theme.typography.pxToRem(14),
+ justifySelf: 'start',
+ }),
+ emptyMessage: css({
+ color: theme.colors.text.secondary,
+ fontStyle: 'italic',
+ padding: theme.spacing(1, 1.5, 1, 7),
+ }),
+});
+
+function linkHasChildren(link: NavModelItem): link is NavModelItem & { children: NavModelItem[] } {
+ return Boolean(link.children && link.children.length > 0);
+}
diff --git a/public/app/core/components/MegaMenu/NavBarMenuSection.tsx b/public/app/core/components/MegaMenu/NavBarMenuSection.tsx
new file mode 100644
index 00000000000..626bcff47dd
--- /dev/null
+++ b/public/app/core/components/MegaMenu/NavBarMenuSection.tsx
@@ -0,0 +1,116 @@
+import { css, cx } from '@emotion/css';
+import React, { useState } from 'react';
+
+import { GrafanaTheme2, NavModelItem } from '@grafana/data';
+import { Button, Icon, useStyles2 } from '@grafana/ui';
+
+import { NavBarItemIcon } from '../NavBar/NavBarItemIcon';
+import { NavFeatureHighlight } from '../NavBar/NavFeatureHighlight';
+import { hasChildMatch } from '../NavBar/utils';
+
+import { NavBarMenuItem } from './NavBarMenuItem';
+
+export function NavBarMenuSection({
+ link,
+ activeItem,
+ children,
+ className,
+ onClose,
+}: {
+ link: NavModelItem;
+ activeItem?: NavModelItem;
+ children: React.ReactNode;
+ className?: string;
+ onClose?: () => void;
+}) {
+ const styles = useStyles2(getStyles);
+ const FeatureHighlightWrapper = link.highlightText ? NavFeatureHighlight : React.Fragment;
+ const isActive = link === activeItem;
+ const hasActiveChild = hasChildMatch(link, activeItem);
+ const [sectionExpanded, setSectionExpanded] = useState(Boolean(hasActiveChild));
+
+ return (
+ <>
+
+
{
+ link.onClick?.();
+ onClose?.();
+ }}
+ target={link.target}
+ url={link.url}
+ >
+
+
+
+
+ {link.text}
+
+
+ {Boolean(link.children?.length) && (
+
+ )}
+
+ {sectionExpanded && children}
+ >
+ );
+}
+
+const getStyles = (theme: GrafanaTheme2) => ({
+ collapsibleSectionWrapper: css({
+ alignItems: 'center',
+ display: 'flex',
+ }),
+ collapseButton: css({
+ color: theme.colors.text.disabled,
+ padding: theme.spacing(0, 0.5),
+ marginRight: theme.spacing(1),
+ }),
+ collapseWrapperActive: css({
+ backgroundColor: theme.colors.action.disabledBackground,
+ }),
+ collapseContent: css({
+ padding: 0,
+ }),
+ labelWrapper: css({
+ display: 'grid',
+ fontSize: theme.typography.pxToRem(14),
+ gridAutoFlow: 'column',
+ gridTemplateColumns: `${theme.spacing(7)} auto`,
+ placeItems: 'center',
+ fontWeight: theme.typography.fontWeightMedium,
+ }),
+ isActive: css({
+ color: theme.colors.text.primary,
+
+ '&::before': {
+ display: 'block',
+ content: '" "',
+ height: theme.spacing(3),
+ position: 'absolute',
+ left: theme.spacing(1),
+ top: '50%',
+ transform: 'translateY(-50%)',
+ width: theme.spacing(0.5),
+ borderRadius: theme.shape.borderRadius(1),
+ backgroundImage: theme.colors.gradients.brandVertical,
+ },
+ }),
+ hasActiveChild: css({
+ color: theme.colors.text.primary,
+ }),
+});
diff --git a/public/app/core/components/NavBar/utils.ts b/public/app/core/components/NavBar/utils.ts
index 05935e3603c..24eeaa5212f 100644
--- a/public/app/core/components/NavBar/utils.ts
+++ b/public/app/core/components/NavBar/utils.ts
@@ -100,7 +100,19 @@ export const enrichWithInteractionTracking = (item: NavModelItem, expandedState:
};
export const isMatchOrChildMatch = (itemToCheck: NavModelItem, searchItem?: NavModelItem) => {
- return Boolean(itemToCheck === searchItem || itemToCheck.children?.some((child) => child === searchItem));
+ return Boolean(itemToCheck === searchItem || hasChildMatch(itemToCheck, searchItem));
+};
+
+export const hasChildMatch = (itemToCheck: NavModelItem, searchItem?: NavModelItem): boolean => {
+ return Boolean(
+ itemToCheck.children?.some((child) => {
+ if (child === searchItem) {
+ return true;
+ } else {
+ return hasChildMatch(child, searchItem);
+ }
+ })
+ );
};
const stripQueryParams = (url?: string) => {
diff --git a/public/app/core/components/PageNew/SectionNavItem.tsx b/public/app/core/components/PageNew/SectionNavItem.tsx
index bf6034685ea..ff7b3348866 100644
--- a/public/app/core/components/PageNew/SectionNavItem.tsx
+++ b/public/app/core/components/PageNew/SectionNavItem.tsx
@@ -68,7 +68,7 @@ const getStyles = (theme: GrafanaTheme2) => {
activeStyle: css`
label: activeTabStyle;
color: ${theme.colors.text.primary};
- background-color: ${theme.colors.action.disabledBackground};
+ background: ${theme.colors.emphasize(theme.colors.background.canvas, 0.03)};
border-radius: ${theme.shape.borderRadius(2)};
font-weight: ${theme.typography.fontWeightMedium};
diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx
index 4680ec0d367..5d39368ca06 100644
--- a/public/app/features/dashboard/containers/DashboardPage.tsx
+++ b/public/app/features/dashboard/containers/DashboardPage.tsx
@@ -4,11 +4,10 @@ import { connect, ConnectedProps } from 'react-redux';
import { locationUtil, NavModel, NavModelItem, TimeRange, PageLayoutType } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
-import { locationService } from '@grafana/runtime';
+import { config, locationService } from '@grafana/runtime';
import { Themeable2, withTheme2 } from '@grafana/ui';
import { notifyApp } from 'app/core/actions';
import { Page } from 'app/core/components/Page/Page';
-import { config } from 'app/core/config';
import { GrafanaContext } from 'app/core/context/GrafanaContext';
import { createErrorNotification } from 'app/core/copy/appNotification';
import { getKioskMode } from 'app/core/navigation/kiosk';