mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Navigation: Rough implementation of new navbar design (#46909)
This commit is contained in:
parent
4449439a41
commit
f486b54b84
@ -30,7 +30,7 @@ export const CollapsableSection: FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [open, toggleOpen] = useState<boolean>(isOpen);
|
const [open, toggleOpen] = useState<boolean>(isOpen);
|
||||||
const styles = useStyles2(collapsableSectionStyles);
|
const styles = useStyles2(collapsableSectionStyles);
|
||||||
const tooltip = `Click to ${open ? 'collapse' : 'expand'}`;
|
|
||||||
const onClick = (e: React.MouseEvent) => {
|
const onClick = (e: React.MouseEvent) => {
|
||||||
if (e.target instanceof HTMLElement && e.target.tagName === 'A') {
|
if (e.target instanceof HTMLElement && e.target.tagName === 'A') {
|
||||||
return;
|
return;
|
||||||
@ -48,7 +48,7 @@ export const CollapsableSection: FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div onClick={onClick} className={cx(styles.header, className)} title={tooltip}>
|
<div onClick={onClick} className={cx(styles.header, className)}>
|
||||||
<button
|
<button
|
||||||
id={`collapse-button-${id}`}
|
id={`collapse-button-${id}`}
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
@ -88,9 +88,6 @@ const collapsableSectionStyles = (theme: GrafanaTheme2) => ({
|
|||||||
padding: `${theme.spacing(0.5)} 0`,
|
padding: `${theme.spacing(0.5)} 0`,
|
||||||
'&:focus-within': getFocusStyles(theme),
|
'&:focus-within': getFocusStyles(theme),
|
||||||
}),
|
}),
|
||||||
headerClosed: css({
|
|
||||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
|
||||||
}),
|
|
||||||
button: css({
|
button: css({
|
||||||
all: 'unset',
|
all: 'unset',
|
||||||
'&:focus-visible': {
|
'&:focus-visible': {
|
||||||
|
@ -43,7 +43,9 @@ export const focusCss = (theme: GrafanaTheme) => `
|
|||||||
outline: 2px dotted transparent;
|
outline: 2px dotted transparent;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
box-shadow: 0 0 0 2px ${theme.colors.bodyBg}, 0 0 0px 4px ${theme.colors.formFocusOutline};
|
box-shadow: 0 0 0 2px ${theme.colors.bodyBg}, 0 0 0px 4px ${theme.colors.formFocusOutline};
|
||||||
transition: all 0.2s cubic-bezier(0.19, 1, 0.22, 1);
|
transition-property: outline, outline-offset, box-shadow;
|
||||||
|
transition-duration: 0.2s;
|
||||||
|
transition-timing-function: cubic-bezier(0.19, 1, 0.22, 1);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function getMouseFocusStyles(theme: GrafanaTheme2): CSSObject {
|
export function getMouseFocusStyles(theme: GrafanaTheme2): CSSObject {
|
||||||
@ -58,7 +60,9 @@ export function getFocusStyles(theme: GrafanaTheme2): CSSObject {
|
|||||||
outline: '2px dotted transparent',
|
outline: '2px dotted transparent',
|
||||||
outlineOffset: '2px',
|
outlineOffset: '2px',
|
||||||
boxShadow: `0 0 0 2px ${theme.colors.background.canvas}, 0 0 0px 4px ${theme.colors.primary.main}`,
|
boxShadow: `0 0 0 2px ${theme.colors.background.canvas}, 0 0 0px 4px ${theme.colors.primary.main}`,
|
||||||
transition: `all 0.2s cubic-bezier(0.19, 1, 0.22, 1)`,
|
transitionTimingFunction: `cubic-bezier(0.19, 1, 0.22, 1)`,
|
||||||
|
transitionDuration: '0.2s',
|
||||||
|
transitionProperty: 'outline, outline-offset, box-shadow',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import { ConfigContext, ThemeProvider } from './core/utils/ConfigProvider';
|
|||||||
import { RouteDescriptor } from './core/navigation/types';
|
import { RouteDescriptor } from './core/navigation/types';
|
||||||
import { contextSrv } from './core/services/context_srv';
|
import { contextSrv } from './core/services/context_srv';
|
||||||
import { NavBar } from './core/components/NavBar/NavBar';
|
import { NavBar } from './core/components/NavBar/NavBar';
|
||||||
import { NavBarNext } from './core/components/NavBar/NavBarNext';
|
import { NavBarNext } from './core/components/NavBar/Next/NavBarNext';
|
||||||
import { GrafanaRoute } from './core/navigation/GrafanaRoute';
|
import { GrafanaRoute } from './core/navigation/GrafanaRoute';
|
||||||
import { AppNotificationList } from './core/components/AppNotifications/AppNotificationList';
|
import { AppNotificationList } from './core/components/AppNotifications/AppNotificationList';
|
||||||
import { SearchWrapper } from 'app/features/search';
|
import { SearchWrapper } from 'app/features/search';
|
||||||
|
@ -6,9 +6,6 @@ import { useDialog } from '@react-aria/dialog';
|
|||||||
import { useOverlay } from '@react-aria/overlays';
|
import { useOverlay } from '@react-aria/overlays';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { NavBarMenuItem } from './NavBarMenuItem';
|
import { NavBarMenuItem } from './NavBarMenuItem';
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { togglePin } from 'app/core/reducers/navBarTree';
|
|
||||||
import { getConfig } from 'app/core/config';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
activeItem?: NavModelItem;
|
activeItem?: NavModelItem;
|
||||||
@ -17,11 +14,6 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NavBarMenu({ activeItem, navItems, onClose }: Props) {
|
export function NavBarMenu({ activeItem, navItems, onClose }: Props) {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const toggleItemPin = (id: string) => {
|
|
||||||
dispatch(togglePin({ id }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
@ -35,7 +27,6 @@ export function NavBarMenu({ activeItem, navItems, onClose }: Props) {
|
|||||||
ref
|
ref
|
||||||
);
|
);
|
||||||
|
|
||||||
const newNavigationEnabled = getConfig().featureToggles.newNavigation;
|
|
||||||
return (
|
return (
|
||||||
<FocusScope contain restoreFocus autoFocus>
|
<FocusScope contain restoreFocus autoFocus>
|
||||||
<div data-testid="navbarmenu" className={styles.container} ref={ref} {...overlayProps} {...dialogProps}>
|
<div data-testid="navbarmenu" className={styles.container} ref={ref} {...overlayProps} {...dialogProps}>
|
||||||
@ -59,9 +50,6 @@ export function NavBarMenu({ activeItem, navItems, onClose }: Props) {
|
|||||||
text={link.text}
|
text={link.text}
|
||||||
url={link.url}
|
url={link.url}
|
||||||
isMobile={true}
|
isMobile={true}
|
||||||
pinned={!link.hideFromNavbar}
|
|
||||||
canPin={newNavigationEnabled && link.id !== 'search'}
|
|
||||||
onTogglePin={() => link.id && toggleItemPin(link.id)}
|
|
||||||
/>
|
/>
|
||||||
{link.children?.map(
|
{link.children?.map(
|
||||||
(childLink) =>
|
(childLink) =>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Icon, IconButton, IconName, Link, useTheme2 } from '@grafana/ui';
|
import { Icon, IconName, Link, useTheme2 } from '@grafana/ui';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
icon?: IconName;
|
icon?: IconName;
|
||||||
@ -14,9 +14,6 @@ export interface Props {
|
|||||||
url?: string;
|
url?: string;
|
||||||
adjustHeightForBorder?: boolean;
|
adjustHeightForBorder?: boolean;
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
canPin?: boolean;
|
|
||||||
pinned?: boolean;
|
|
||||||
onTogglePin?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NavBarMenuItem({
|
export function NavBarMenuItem({
|
||||||
@ -29,20 +26,10 @@ export function NavBarMenuItem({
|
|||||||
text,
|
text,
|
||||||
url,
|
url,
|
||||||
isMobile = false,
|
isMobile = false,
|
||||||
canPin = false,
|
|
||||||
pinned = false,
|
|
||||||
onTogglePin,
|
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme, isActive, styleOverrides);
|
const styles = getStyles(theme, isActive, styleOverrides);
|
||||||
|
|
||||||
const onClickPin = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
onTogglePin?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const linkContent = (
|
const linkContent = (
|
||||||
<div className={styles.linkContent}>
|
<div className={styles.linkContent}>
|
||||||
<div>
|
<div>
|
||||||
@ -78,17 +65,7 @@ export function NavBarMenuItem({
|
|||||||
return isDivider ? (
|
return isDivider ? (
|
||||||
<li data-testid="dropdown-child-divider" className={styles.divider} tabIndex={-1} aria-disabled />
|
<li data-testid="dropdown-child-divider" className={styles.divider} tabIndex={-1} aria-disabled />
|
||||||
) : (
|
) : (
|
||||||
<li className={styles.listItem}>
|
<li className={styles.listItem}>{element}</li>
|
||||||
{element}
|
|
||||||
{canPin && (
|
|
||||||
<IconButton
|
|
||||||
name="anchor"
|
|
||||||
className={cx('pin-button', styles.pinButton, { [styles.visible]: pinned })}
|
|
||||||
onClick={onClickPin}
|
|
||||||
tooltip={`${pinned ? 'Unpin' : 'Pin'} menu item`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,187 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
import { css, cx } from '@emotion/css';
|
|
||||||
import { cloneDeep } from 'lodash';
|
|
||||||
import { GrafanaTheme2, NavModelItem, NavSection } from '@grafana/data';
|
|
||||||
import { Icon, IconName, useTheme2 } from '@grafana/ui';
|
|
||||||
import { locationService } from '@grafana/runtime';
|
|
||||||
import { getKioskMode } from 'app/core/navigation/kiosk';
|
|
||||||
import config from 'app/core/config';
|
|
||||||
import { KioskMode, StoreState } from 'app/types';
|
|
||||||
import { enrichConfigItems, getActiveItem, isMatchOrChildMatch, isSearchActive, SEARCH_ITEM_ID } from './utils';
|
|
||||||
import { OrgSwitcher } from '../OrgSwitcher';
|
|
||||||
import { NavBarSection } from './NavBarSection';
|
|
||||||
import { NavBarMenu } from './NavBarMenu';
|
|
||||||
import NavBarItem from './NavBarItem';
|
|
||||||
import { NavBarItemWithoutMenu } from './NavBarItemWithoutMenu';
|
|
||||||
import { Branding } from '../Branding/Branding';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
const onOpenSearch = () => {
|
|
||||||
locationService.partial({ search: 'open' });
|
|
||||||
};
|
|
||||||
|
|
||||||
const searchItem: NavModelItem = {
|
|
||||||
id: SEARCH_ITEM_ID,
|
|
||||||
onClick: onOpenSearch,
|
|
||||||
text: 'Search dashboards',
|
|
||||||
icon: 'search',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NavBarNext = React.memo(() => {
|
|
||||||
const navBarTree = useSelector((state: StoreState) => state.navBarTree);
|
|
||||||
const homeUrl = config.appSubUrl || '/';
|
|
||||||
const theme = useTheme2();
|
|
||||||
const styles = getStyles(theme);
|
|
||||||
const location = useLocation();
|
|
||||||
const kiosk = getKioskMode();
|
|
||||||
const [showSwitcherModal, setShowSwitcherModal] = useState(false);
|
|
||||||
const toggleSwitcherModal = () => {
|
|
||||||
setShowSwitcherModal(!showSwitcherModal);
|
|
||||||
};
|
|
||||||
const navTree = cloneDeep(navBarTree);
|
|
||||||
|
|
||||||
// Here we need to hack in a "home" NavModelItem since this is constructed in the frontend
|
|
||||||
const homeLink: NavModelItem = {
|
|
||||||
text: 'Home',
|
|
||||||
url: config.appSubUrl || '/',
|
|
||||||
};
|
|
||||||
navTree.unshift(homeLink);
|
|
||||||
|
|
||||||
const coreItems = navTree.filter((item) => item.section === NavSection.Core);
|
|
||||||
const pluginItems = navTree.filter((item) => item.section === NavSection.Plugin);
|
|
||||||
const configItems = enrichConfigItems(
|
|
||||||
navTree.filter((item) => item.section === NavSection.Config),
|
|
||||||
location,
|
|
||||||
toggleSwitcherModal
|
|
||||||
);
|
|
||||||
const activeItem = isSearchActive(location) ? searchItem : getActiveItem(navTree, location.pathname);
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
|
||||||
|
|
||||||
if (kiosk !== KioskMode.Off) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav className={cx(styles.sidemenu, 'sidemenu')} data-testid="sidemenu" aria-label="Main menu">
|
|
||||||
<div className={styles.mobileSidemenuLogo} onClick={() => setMenuOpen(!menuOpen)} key="hamburger">
|
|
||||||
<Icon name="bars" size="xl" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<NavBarSection>
|
|
||||||
<NavBarItemWithoutMenu
|
|
||||||
isActive={isMatchOrChildMatch(homeLink, activeItem)}
|
|
||||||
label="Home"
|
|
||||||
className={styles.grafanaLogo}
|
|
||||||
url={homeUrl}
|
|
||||||
>
|
|
||||||
<Branding.MenuLogo />
|
|
||||||
</NavBarItemWithoutMenu>
|
|
||||||
<NavBarItem className={styles.search} isActive={activeItem === searchItem} link={searchItem}>
|
|
||||||
<Icon name="search" size="xl" />
|
|
||||||
</NavBarItem>
|
|
||||||
</NavBarSection>
|
|
||||||
|
|
||||||
<NavBarSection>
|
|
||||||
{coreItems.map((link, index) => (
|
|
||||||
<NavBarItem
|
|
||||||
key={`${link.id}-${index}`}
|
|
||||||
isActive={isMatchOrChildMatch(link, activeItem)}
|
|
||||||
link={{ ...link, subTitle: undefined, onClick: undefined }}
|
|
||||||
>
|
|
||||||
{link.icon && <Icon name={link.icon as IconName} size="xl" />}
|
|
||||||
{link.img && <img src={link.img} alt={`${link.text} logo`} />}
|
|
||||||
</NavBarItem>
|
|
||||||
))}
|
|
||||||
</NavBarSection>
|
|
||||||
|
|
||||||
<NavBarSection>
|
|
||||||
{pluginItems.map((link, index) => (
|
|
||||||
<NavBarItem key={`${link.id}-${index}`} isActive={isMatchOrChildMatch(link, activeItem)} link={link}>
|
|
||||||
{link.icon && <Icon name={link.icon as IconName} size="xl" />}
|
|
||||||
{link.img && <img src={link.img} alt={`${link.text} logo`} />}
|
|
||||||
</NavBarItem>
|
|
||||||
))}
|
|
||||||
</NavBarSection>
|
|
||||||
|
|
||||||
<div className={styles.spacer} />
|
|
||||||
|
|
||||||
<NavBarSection>
|
|
||||||
{configItems.map((link, index) => (
|
|
||||||
<NavBarItem
|
|
||||||
key={`${link.id}-${index}`}
|
|
||||||
isActive={isMatchOrChildMatch(link, activeItem)}
|
|
||||||
reverseMenuDirection
|
|
||||||
link={link}
|
|
||||||
>
|
|
||||||
{link.icon && <Icon name={link.icon as IconName} size="xl" />}
|
|
||||||
{link.img && <img src={link.img} alt={`${link.text} logo`} />}
|
|
||||||
</NavBarItem>
|
|
||||||
))}
|
|
||||||
</NavBarSection>
|
|
||||||
|
|
||||||
{showSwitcherModal && <OrgSwitcher onDismiss={toggleSwitcherModal} />}
|
|
||||||
{menuOpen && (
|
|
||||||
<NavBarMenu
|
|
||||||
activeItem={activeItem}
|
|
||||||
navItems={[searchItem, ...coreItems, ...pluginItems, ...configItems]}
|
|
||||||
onClose={() => setMenuOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
NavBarNext.displayName = 'NavBarNext';
|
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
|
||||||
search: css`
|
|
||||||
display: none;
|
|
||||||
margin-top: 0;
|
|
||||||
|
|
||||||
${theme.breakpoints.up('md')} {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
sidemenu: css`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
position: fixed;
|
|
||||||
z-index: ${theme.zIndex.sidemenu};
|
|
||||||
|
|
||||||
${theme.breakpoints.up('md')} {
|
|
||||||
background: ${theme.colors.background.primary};
|
|
||||||
border-right: 1px solid ${theme.components.panel.borderColor};
|
|
||||||
position: relative;
|
|
||||||
width: ${theme.components.sidemenu.width}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidemenu-hidden & {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
grafanaLogo: css`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
img {
|
|
||||||
height: ${theme.spacing(3)};
|
|
||||||
width: ${theme.spacing(3)};
|
|
||||||
}
|
|
||||||
justify-content: center;
|
|
||||||
`,
|
|
||||||
mobileSidemenuLogo: css`
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: ${theme.spacing(2)};
|
|
||||||
|
|
||||||
${theme.breakpoints.up('md')} {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
spacer: css`
|
|
||||||
flex: 1;
|
|
||||||
`,
|
|
||||||
});
|
|
135
public/app/core/components/NavBar/Next/NavBarItem.tsx
Normal file
135
public/app/core/components/NavBar/Next/NavBarItem.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { Item } from '@react-stately/collections';
|
||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
import { GrafanaTheme2, locationUtil, NavMenuItemType, NavModelItem } from '@grafana/data';
|
||||||
|
import { IconName, useTheme2 } from '@grafana/ui';
|
||||||
|
import { locationService } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { NavBarMenuItem } from './NavBarMenuItem';
|
||||||
|
import { getNavBarItemWithoutMenuStyles, NavBarItemWithoutMenu } from './NavBarItemWithoutMenu';
|
||||||
|
import { NavBarItemMenuTrigger } from './NavBarItemMenuTrigger';
|
||||||
|
import { NavBarItemMenu } from '../NavBarItemMenu';
|
||||||
|
import { getNavModelItemKey } from '../utils';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import menuItemTranslations from '../navBarItem-translations';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
isActive?: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
reverseMenuDirection?: boolean;
|
||||||
|
showMenu?: boolean;
|
||||||
|
link: NavModelItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NavBarItem = ({
|
||||||
|
isActive = false,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
reverseMenuDirection = false,
|
||||||
|
showMenu = true,
|
||||||
|
link,
|
||||||
|
}: Props) => {
|
||||||
|
const { i18n } = useLingui();
|
||||||
|
const theme = useTheme2();
|
||||||
|
const menuItems = link.children ?? [];
|
||||||
|
|
||||||
|
// Spreading `menuItems` here as otherwise we'd be mutating props
|
||||||
|
const menuItemsSorted = reverseMenuDirection ? [...menuItems].reverse() : menuItems;
|
||||||
|
const filteredItems = menuItemsSorted
|
||||||
|
.filter((item) => !item.hideFromMenu)
|
||||||
|
.map((i) => ({ ...i, menuItemType: NavMenuItemType.Item }));
|
||||||
|
const adjustHeightForBorder = filteredItems.length === 0;
|
||||||
|
const styles = getStyles(theme, adjustHeightForBorder, isActive);
|
||||||
|
const section: NavModelItem = {
|
||||||
|
...link,
|
||||||
|
children: filteredItems,
|
||||||
|
menuItemType: NavMenuItemType.Section,
|
||||||
|
};
|
||||||
|
const items: NavModelItem[] = [section].concat(filteredItems);
|
||||||
|
|
||||||
|
const onNavigate = (item: NavModelItem) => {
|
||||||
|
const { url, target, onClick } = item;
|
||||||
|
if (!url) {
|
||||||
|
onClick?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target && url.startsWith('/')) {
|
||||||
|
locationService.push(locationUtil.stripBaseFromUrl(url));
|
||||||
|
} else {
|
||||||
|
window.open(url, target);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const translationKey = link.id && menuItemTranslations[link.id];
|
||||||
|
const linkText = translationKey ? i18n._(translationKey) : link.text;
|
||||||
|
|
||||||
|
if (!showMenu) {
|
||||||
|
return (
|
||||||
|
<NavBarItemWithoutMenu
|
||||||
|
label={link.text}
|
||||||
|
className={className}
|
||||||
|
isActive={isActive}
|
||||||
|
url={link.url}
|
||||||
|
onClick={link.onClick}
|
||||||
|
target={link.target}
|
||||||
|
highlightText={link.highlightText}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</NavBarItemWithoutMenu>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<li className={cx(styles.container, className)}>
|
||||||
|
<NavBarItemMenuTrigger item={section} isActive={isActive} label={linkText}>
|
||||||
|
<NavBarItemMenu
|
||||||
|
items={items}
|
||||||
|
reverseMenuDirection={reverseMenuDirection}
|
||||||
|
adjustHeightForBorder={adjustHeightForBorder}
|
||||||
|
disabledKeys={['divider', 'subtitle']}
|
||||||
|
aria-label={section.text}
|
||||||
|
onNavigate={onNavigate}
|
||||||
|
>
|
||||||
|
{(item: NavModelItem) => {
|
||||||
|
const translationKey = item.id && menuItemTranslations[item.id];
|
||||||
|
const itemText = translationKey ? i18n._(translationKey) : item.text;
|
||||||
|
const isSection = item.menuItemType === NavMenuItemType.Section;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Item key={getNavModelItemKey(item)} textValue={item.text}>
|
||||||
|
<NavBarMenuItem
|
||||||
|
isDivider={!isSection && item.divider}
|
||||||
|
icon={isSection ? undefined : (item.icon as IconName)}
|
||||||
|
target={item.target}
|
||||||
|
text={itemText}
|
||||||
|
url={item.url}
|
||||||
|
onClick={item.onClick}
|
||||||
|
styleOverrides={cx(styles.primaryText, { [styles.header]: isSection })}
|
||||||
|
/>
|
||||||
|
</Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</NavBarItemMenu>
|
||||||
|
</NavBarItemMenuTrigger>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavBarItem;
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2, adjustHeightForBorder: boolean, isActive?: boolean) => ({
|
||||||
|
...getNavBarItemWithoutMenuStyles(theme, isActive),
|
||||||
|
primaryText: css({
|
||||||
|
color: theme.colors.text.primary,
|
||||||
|
}),
|
||||||
|
header: css({
|
||||||
|
height: `calc(${theme.spacing(6)} - ${adjustHeightForBorder ? 2 : 1}px)`,
|
||||||
|
fontSize: theme.typography.h4.fontSize,
|
||||||
|
fontWeight: theme.typography.h4.fontWeight,
|
||||||
|
padding: `${theme.spacing(1)} ${theme.spacing(2)}`,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
width: '100%',
|
||||||
|
}),
|
||||||
|
});
|
217
public/app/core/components/NavBar/Next/NavBarItemMenuTrigger.tsx
Normal file
217
public/app/core/components/NavBar/Next/NavBarItemMenuTrigger.tsx
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
import React, { ReactElement, useEffect, useState } from 'react';
|
||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
import { Icon, IconName, Link, useTheme2 } from '@grafana/ui';
|
||||||
|
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
|
||||||
|
import { MenuTriggerProps } from '@react-types/menu';
|
||||||
|
import { useMenuTriggerState } from '@react-stately/menu';
|
||||||
|
import { useMenuTrigger } from '@react-aria/menu';
|
||||||
|
import { useFocusWithin, useHover, useKeyboard } from '@react-aria/interactions';
|
||||||
|
import { useButton } from '@react-aria/button';
|
||||||
|
import { useDialog } from '@react-aria/dialog';
|
||||||
|
import { DismissButton, useOverlay } from '@react-aria/overlays';
|
||||||
|
import { FocusScope } from '@react-aria/focus';
|
||||||
|
|
||||||
|
import { NavBarItemMenuContext } from '../context';
|
||||||
|
import { NavFeatureHighlight } from '../NavFeatureHighlight';
|
||||||
|
import { reportExperimentView } from '@grafana/runtime';
|
||||||
|
|
||||||
|
export interface NavBarItemMenuTriggerProps extends MenuTriggerProps {
|
||||||
|
children: ReactElement;
|
||||||
|
item: NavModelItem;
|
||||||
|
isActive?: boolean;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NavBarItemMenuTrigger(props: NavBarItemMenuTriggerProps): ReactElement {
|
||||||
|
const { item, isActive, label, children: menu, ...rest } = props;
|
||||||
|
const [menuHasFocus, setMenuHasFocus] = useState(false);
|
||||||
|
const theme = useTheme2();
|
||||||
|
const styles = getStyles(theme, isActive);
|
||||||
|
|
||||||
|
// Create state based on the incoming props
|
||||||
|
const state = useMenuTriggerState({ ...rest });
|
||||||
|
|
||||||
|
// Get props for the menu trigger and menu elements
|
||||||
|
const ref = React.useRef<HTMLElement>(null);
|
||||||
|
const { menuTriggerProps, menuProps } = useMenuTrigger({}, state, ref);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (item.highlightId) {
|
||||||
|
reportExperimentView(`feature-highlights-${item.highlightId}-nav`, 'test', '');
|
||||||
|
}
|
||||||
|
}, [item.highlightId]);
|
||||||
|
|
||||||
|
const { hoverProps } = useHover({
|
||||||
|
onHoverChange: (isHovering) => {
|
||||||
|
if (isHovering) {
|
||||||
|
state.open();
|
||||||
|
} else {
|
||||||
|
state.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { focusWithinProps } = useFocusWithin({
|
||||||
|
onFocusWithinChange: (isFocused) => {
|
||||||
|
if (isFocused) {
|
||||||
|
state.open();
|
||||||
|
}
|
||||||
|
if (!isFocused) {
|
||||||
|
state.close();
|
||||||
|
setMenuHasFocus(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { keyboardProps } = useKeyboard({
|
||||||
|
onKeyDown: (e) => {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowRight':
|
||||||
|
if (!state.isOpen) {
|
||||||
|
state.open();
|
||||||
|
}
|
||||||
|
setMenuHasFocus(true);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get props for the button based on the trigger props from useMenuTrigger
|
||||||
|
const { buttonProps } = useButton(menuTriggerProps, ref);
|
||||||
|
const Wrapper = item.highlightText ? NavFeatureHighlight : React.Fragment;
|
||||||
|
const itemContent = (
|
||||||
|
<Wrapper>
|
||||||
|
<span className={styles.icon}>
|
||||||
|
{item?.icon && <Icon name={item.icon as IconName} size="xl" />}
|
||||||
|
{item?.img && <img src={item.img} alt={`${item.text} logo`} />}
|
||||||
|
</span>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
let element = (
|
||||||
|
<button
|
||||||
|
className={styles.element}
|
||||||
|
{...buttonProps}
|
||||||
|
{...keyboardProps}
|
||||||
|
ref={ref as React.RefObject<HTMLButtonElement>}
|
||||||
|
onClick={item?.onClick}
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
|
{itemContent}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (item?.url) {
|
||||||
|
element =
|
||||||
|
!item.target && item.url.startsWith('/') ? (
|
||||||
|
<Link
|
||||||
|
{...buttonProps}
|
||||||
|
{...keyboardProps}
|
||||||
|
ref={ref as React.RefObject<HTMLAnchorElement>}
|
||||||
|
href={item.url}
|
||||||
|
target={item.target}
|
||||||
|
onClick={item?.onClick}
|
||||||
|
className={styles.element}
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
|
{itemContent}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
href={item.url}
|
||||||
|
target={item.target}
|
||||||
|
onClick={item?.onClick}
|
||||||
|
{...buttonProps}
|
||||||
|
{...keyboardProps}
|
||||||
|
ref={ref as React.RefObject<HTMLAnchorElement>}
|
||||||
|
className={styles.element}
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
|
{itemContent}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlayRef = React.useRef(null);
|
||||||
|
const { dialogProps } = useDialog({}, overlayRef);
|
||||||
|
const { overlayProps } = useOverlay(
|
||||||
|
{
|
||||||
|
onClose: () => state.close(),
|
||||||
|
isOpen: state.isOpen,
|
||||||
|
isDismissable: true,
|
||||||
|
},
|
||||||
|
overlayRef
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx(styles.element, 'dropdown')} {...focusWithinProps} {...hoverProps}>
|
||||||
|
{element}
|
||||||
|
{state.isOpen && (
|
||||||
|
<NavBarItemMenuContext.Provider
|
||||||
|
value={{
|
||||||
|
menuProps,
|
||||||
|
menuHasFocus,
|
||||||
|
onClose: () => state.close(),
|
||||||
|
onLeft: () => {
|
||||||
|
setMenuHasFocus(false);
|
||||||
|
ref.current?.focus();
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FocusScope restoreFocus>
|
||||||
|
<div {...overlayProps} {...dialogProps} ref={overlayRef}>
|
||||||
|
<DismissButton onDismiss={() => state.close()} />
|
||||||
|
{menu}
|
||||||
|
<DismissButton onDismiss={() => state.close()} />
|
||||||
|
</div>
|
||||||
|
</FocusScope>
|
||||||
|
</NavBarItemMenuContext.Provider>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2, isActive?: boolean) => ({
|
||||||
|
element: css({
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
color: 'inherit',
|
||||||
|
display: 'grid',
|
||||||
|
padding: 0,
|
||||||
|
placeContent: 'center',
|
||||||
|
height: theme.spacing(6),
|
||||||
|
width: theme.spacing(7),
|
||||||
|
|
||||||
|
'&::before': {
|
||||||
|
display: isActive ? 'block' : 'none',
|
||||||
|
content: '" "',
|
||||||
|
position: 'absolute',
|
||||||
|
left: theme.spacing(1),
|
||||||
|
top: theme.spacing(1.5),
|
||||||
|
bottom: theme.spacing(1.5),
|
||||||
|
width: theme.spacing(0.5),
|
||||||
|
borderRadius: theme.shape.borderRadius(1),
|
||||||
|
backgroundImage: theme.colors.gradients.brandVertical,
|
||||||
|
},
|
||||||
|
|
||||||
|
'&:focus-visible': {
|
||||||
|
backgroundColor: theme.colors.action.hover,
|
||||||
|
boxShadow: 'none',
|
||||||
|
color: theme.colors.text.primary,
|
||||||
|
outline: `${theme.shape.borderRadius(1)} solid ${theme.colors.primary.main}`,
|
||||||
|
outlineOffset: `-${theme.shape.borderRadius(1)}`,
|
||||||
|
transition: 'none',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
icon: css({
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
|
||||||
|
img: {
|
||||||
|
borderRadius: '50%',
|
||||||
|
height: theme.spacing(3),
|
||||||
|
width: theme.spacing(3),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
128
public/app/core/components/NavBar/Next/NavBarItemWithoutMenu.tsx
Normal file
128
public/app/core/components/NavBar/Next/NavBarItemWithoutMenu.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { Link, useTheme2 } from '@grafana/ui';
|
||||||
|
import { NavFeatureHighlight } from '../NavFeatureHighlight';
|
||||||
|
|
||||||
|
export interface NavBarItemWithoutMenuProps {
|
||||||
|
label: string;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
elClassName?: string;
|
||||||
|
url?: string;
|
||||||
|
target?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
highlightText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NavBarItemWithoutMenu({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
url,
|
||||||
|
target,
|
||||||
|
isActive = false,
|
||||||
|
onClick,
|
||||||
|
highlightText,
|
||||||
|
className,
|
||||||
|
elClassName,
|
||||||
|
}: NavBarItemWithoutMenuProps) {
|
||||||
|
const theme = useTheme2();
|
||||||
|
const styles = getNavBarItemWithoutMenuStyles(theme, isActive);
|
||||||
|
|
||||||
|
const content = highlightText ? (
|
||||||
|
<NavFeatureHighlight>
|
||||||
|
<span className={styles.icon}>{children}</span>
|
||||||
|
</NavFeatureHighlight>
|
||||||
|
) : (
|
||||||
|
<span className={styles.icon}>{children}</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
const elStyle = cx(styles.element, elClassName);
|
||||||
|
|
||||||
|
const renderContents = () => {
|
||||||
|
if (!url) {
|
||||||
|
return (
|
||||||
|
<button className={elStyle} onClick={onClick} aria-label={label}>
|
||||||
|
{content}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
} else if (!target && url.startsWith('/')) {
|
||||||
|
return (
|
||||||
|
<Link className={elStyle} href={url} target={target} aria-label={label} onClick={onClick} aria-haspopup="true">
|
||||||
|
{content}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<a href={url} target={target} className={elStyle} onClick={onClick} aria-label={label}>
|
||||||
|
{content}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <div className={cx(styles.container, className)}>{renderContents()}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNavBarItemWithoutMenuStyles(theme: GrafanaTheme2, isActive?: boolean) {
|
||||||
|
return {
|
||||||
|
container: css({
|
||||||
|
position: 'relative',
|
||||||
|
color: isActive ? theme.colors.text.primary : theme.colors.text.secondary,
|
||||||
|
display: 'grid',
|
||||||
|
placeItems: 'center',
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.colors.action.hover,
|
||||||
|
color: theme.colors.text.primary,
|
||||||
|
|
||||||
|
// TODO don't use a hardcoded class here, use isVisible in NavBarDropdown
|
||||||
|
'.navbar-dropdown': {
|
||||||
|
opacity: 1,
|
||||||
|
visibility: 'visible',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
element: css({
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
color: 'inherit',
|
||||||
|
display: 'block',
|
||||||
|
padding: 0,
|
||||||
|
textAlign: 'center',
|
||||||
|
|
||||||
|
'&::before': {
|
||||||
|
display: isActive ? 'block' : 'none',
|
||||||
|
content: "' '",
|
||||||
|
position: 'absolute',
|
||||||
|
left: theme.spacing(1),
|
||||||
|
top: theme.spacing(1.5),
|
||||||
|
bottom: theme.spacing(1.5),
|
||||||
|
width: theme.spacing(0.5),
|
||||||
|
borderRadius: theme.shape.borderRadius(1),
|
||||||
|
backgroundImage: theme.colors.gradients.brandVertical,
|
||||||
|
},
|
||||||
|
|
||||||
|
'&:focus-visible': {
|
||||||
|
backgroundColor: theme.colors.action.hover,
|
||||||
|
boxShadow: 'none',
|
||||||
|
color: theme.colors.text.primary,
|
||||||
|
outline: `${theme.shape.borderRadius(1)} solid ${theme.colors.primary.main}`,
|
||||||
|
outlineOffset: `-${theme.shape.borderRadius(1)}`,
|
||||||
|
transition: 'none',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
icon: css({
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
|
||||||
|
img: {
|
||||||
|
borderRadius: '50%',
|
||||||
|
height: theme.spacing(3),
|
||||||
|
width: theme.spacing(3),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
291
public/app/core/components/NavBar/Next/NavBarMenu.tsx
Normal file
291
public/app/core/components/NavBar/Next/NavBarMenu.tsx
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
|
||||||
|
import { CollapsableSection, CustomScrollbar, Icon, IconName, useStyles2 } from '@grafana/ui';
|
||||||
|
import { FocusScope } from '@react-aria/focus';
|
||||||
|
import { useDialog } from '@react-aria/dialog';
|
||||||
|
import { useOverlay } from '@react-aria/overlays';
|
||||||
|
import { css, cx, keyframes } from '@emotion/css';
|
||||||
|
import { NavBarMenuItem } from './NavBarMenuItem';
|
||||||
|
import { NavBarItemWithoutMenu } from './NavBarItemWithoutMenu';
|
||||||
|
import { isMatchOrChildMatch } from '../utils';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
activeItem?: NavModelItem;
|
||||||
|
navItems: NavModelItem[];
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NavBarMenu({ activeItem, navItems, onClose }: Props) {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const ref = useRef(null);
|
||||||
|
const { dialogProps } = useDialog({}, ref);
|
||||||
|
const { overlayProps } = useOverlay(
|
||||||
|
{
|
||||||
|
isDismissable: true,
|
||||||
|
isOpen: true,
|
||||||
|
onClose,
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FocusScope contain restoreFocus autoFocus>
|
||||||
|
<div data-testid="navbarmenu" className={styles.container} ref={ref} {...overlayProps} {...dialogProps}>
|
||||||
|
<nav className={styles.content}>
|
||||||
|
<CustomScrollbar hideHorizontalTrack>
|
||||||
|
<ul className={styles.itemList}>
|
||||||
|
{navItems.map((link) => (
|
||||||
|
<NavItem link={link} onClose={onClose} activeItem={activeItem} key={link.text} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</CustomScrollbar>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</FocusScope>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NavBarMenu.displayName = 'NavBarMenu';
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
const fadeIn = keyframes`
|
||||||
|
from {
|
||||||
|
background-color: ${theme.colors.background.primary};
|
||||||
|
width: ${theme.spacing(7)};
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background-color: ${theme.colors.background.canvas};
|
||||||
|
width: 300px;
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
container: css({
|
||||||
|
animation: `150ms ease-in 0s 1 normal forwards ${fadeIn}`,
|
||||||
|
bottom: 0,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
left: 0,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
marginRight: theme.spacing(1.5),
|
||||||
|
right: 0,
|
||||||
|
zIndex: 9999,
|
||||||
|
top: 0,
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
borderRight: `1px solid ${theme.colors.border.weak}`,
|
||||||
|
right: 'unset',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
content: css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'auto',
|
||||||
|
}),
|
||||||
|
itemList: css({
|
||||||
|
display: 'grid',
|
||||||
|
gridAutoRows: `minmax(${theme.spacing(6)}, auto)`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function NavItem({
|
||||||
|
link,
|
||||||
|
activeItem,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
link: NavModelItem;
|
||||||
|
activeItem?: NavModelItem;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const styles = useStyles2(getNavItemStyles);
|
||||||
|
|
||||||
|
if (linkHasChildren(link)) {
|
||||||
|
return (
|
||||||
|
<CollapsibleNavItem link={link} isActive={isMatchOrChildMatch(link, activeItem)}>
|
||||||
|
<ul>
|
||||||
|
{link.children.map(
|
||||||
|
(childLink) =>
|
||||||
|
!childLink.divider && (
|
||||||
|
<NavBarMenuItem
|
||||||
|
key={`${link.text}-${childLink.text}`}
|
||||||
|
isActive={activeItem === childLink}
|
||||||
|
isDivider={childLink.divider}
|
||||||
|
onClick={() => {
|
||||||
|
childLink.onClick?.();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
styleOverrides={styles.item}
|
||||||
|
target={childLink.target}
|
||||||
|
text={childLink.text}
|
||||||
|
url={childLink.url}
|
||||||
|
isMobile={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</CollapsibleNavItem>
|
||||||
|
);
|
||||||
|
} else if (link.id === 'saved-items') {
|
||||||
|
return (
|
||||||
|
<CollapsibleNavItem link={link} isActive={isMatchOrChildMatch(link, activeItem)} className={styles.savedItems}>
|
||||||
|
<em className={styles.savedItemsText}>No saved items</em>
|
||||||
|
</CollapsibleNavItem>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<li className={styles.flex}>
|
||||||
|
<NavBarItemWithoutMenu
|
||||||
|
className={styles.itemWithoutMenu}
|
||||||
|
elClassName={styles.fullWidth}
|
||||||
|
label={link.text}
|
||||||
|
url={link.url}
|
||||||
|
target={link.target}
|
||||||
|
onClick={() => {
|
||||||
|
link.onClick?.();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
isActive={link === activeItem}
|
||||||
|
>
|
||||||
|
<div className={styles.savedItemsMenuItemWrapper}>
|
||||||
|
{link.img && (
|
||||||
|
<img src={link.img} alt={`${link.text} logo`} height="24" width="24" style={{ borderRadius: '50%' }} />
|
||||||
|
)}
|
||||||
|
{link.icon && <Icon name={link.icon as IconName} size="xl" />}
|
||||||
|
<span className={styles.linkText}>{link.text}</span>
|
||||||
|
</div>
|
||||||
|
</NavBarItemWithoutMenu>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNavItemStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
item: css({
|
||||||
|
padding: `${theme.spacing(1)} 0`,
|
||||||
|
'&::before': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
savedItems: css({
|
||||||
|
background: theme.colors.background.secondary,
|
||||||
|
}),
|
||||||
|
savedItemsText: css({
|
||||||
|
display: 'block',
|
||||||
|
paddingBottom: theme.spacing(2),
|
||||||
|
color: theme.colors.text.secondary,
|
||||||
|
}),
|
||||||
|
flex: css({
|
||||||
|
display: 'flex',
|
||||||
|
}),
|
||||||
|
itemWithoutMenu: css({
|
||||||
|
position: 'relative',
|
||||||
|
placeItems: 'inherit',
|
||||||
|
justifyContent: 'start',
|
||||||
|
display: 'flex',
|
||||||
|
flexGrow: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
}),
|
||||||
|
fullWidth: css({
|
||||||
|
width: '100%',
|
||||||
|
}),
|
||||||
|
savedItemsMenuItemWrapper: css({
|
||||||
|
display: 'grid',
|
||||||
|
gridAutoFlow: 'column',
|
||||||
|
gridTemplateColumns: `${theme.spacing(7)} auto`,
|
||||||
|
alignItems: 'center',
|
||||||
|
}),
|
||||||
|
linkText: css({
|
||||||
|
fontSize: theme.typography.pxToRem(14),
|
||||||
|
justifySelf: 'start',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
function CollapsibleNavItem({
|
||||||
|
link,
|
||||||
|
isActive,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
link: NavModelItem;
|
||||||
|
isActive?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const styles = useStyles2(getCollapsibleStyles);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={cx(styles.menuItem, className)}>
|
||||||
|
<NavBarItemWithoutMenu
|
||||||
|
isActive={isActive}
|
||||||
|
label={link.text}
|
||||||
|
url={link.url}
|
||||||
|
target={link.target}
|
||||||
|
onClick={link.onClick}
|
||||||
|
className={styles.collapsibleMenuItem}
|
||||||
|
>
|
||||||
|
{link.img && (
|
||||||
|
<img src={link.img} alt={`${link.text} logo`} height="24" width="24" style={{ borderRadius: '50%' }} />
|
||||||
|
)}
|
||||||
|
{link.icon && <Icon name={link.icon as IconName} size="xl" />}
|
||||||
|
</NavBarItemWithoutMenu>
|
||||||
|
<div className={styles.collapsibleSectionWrapper}>
|
||||||
|
<CollapsableSection
|
||||||
|
isOpen={false}
|
||||||
|
className={styles.collapseWrapper}
|
||||||
|
contentClassName={styles.collapseContent}
|
||||||
|
label={
|
||||||
|
<div className={cx(styles.labelWrapper, { [styles.primary]: isActive })}>
|
||||||
|
<span className={styles.linkText}>{link.text}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CollapsableSection>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCollapsibleStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
menuItem: css({
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
}),
|
||||||
|
collapsibleMenuItem: css({
|
||||||
|
height: theme.spacing(6),
|
||||||
|
width: theme.spacing(7),
|
||||||
|
display: 'grid',
|
||||||
|
placeContent: 'center',
|
||||||
|
}),
|
||||||
|
collapsibleSectionWrapper: css({
|
||||||
|
display: 'flex',
|
||||||
|
flexGrow: 1,
|
||||||
|
alignSelf: 'start',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}),
|
||||||
|
collapseWrapper: css({
|
||||||
|
borderRadius: theme.shape.borderRadius(2),
|
||||||
|
paddingRight: theme.spacing(4.25),
|
||||||
|
height: theme.spacing(6),
|
||||||
|
alignItems: 'center',
|
||||||
|
}),
|
||||||
|
collapseContent: css({
|
||||||
|
padding: 0,
|
||||||
|
paddingLeft: theme.spacing(1.25),
|
||||||
|
}),
|
||||||
|
labelWrapper: css({
|
||||||
|
fontSize: '15px',
|
||||||
|
color: theme.colors.text.secondary,
|
||||||
|
}),
|
||||||
|
primary: css({
|
||||||
|
color: theme.colors.text.primary,
|
||||||
|
}),
|
||||||
|
linkText: css({
|
||||||
|
fontSize: theme.typography.pxToRem(14),
|
||||||
|
justifySelf: 'start',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
function linkHasChildren(link: NavModelItem): link is NavModelItem & { children: NavModelItem[] } {
|
||||||
|
return Boolean(link.children && link.children.length > 0);
|
||||||
|
}
|
166
public/app/core/components/NavBar/Next/NavBarMenuItem.tsx
Normal file
166
public/app/core/components/NavBar/Next/NavBarMenuItem.tsx
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { Icon, IconName, Link, useTheme2 } from '@grafana/ui';
|
||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
icon?: IconName;
|
||||||
|
isActive?: boolean;
|
||||||
|
isDivider?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
styleOverrides?: string;
|
||||||
|
target?: HTMLAnchorElement['target'];
|
||||||
|
text: React.ReactNode;
|
||||||
|
url?: string;
|
||||||
|
adjustHeightForBorder?: boolean;
|
||||||
|
isMobile?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NavBarMenuItem({
|
||||||
|
icon,
|
||||||
|
isActive,
|
||||||
|
isDivider,
|
||||||
|
onClick,
|
||||||
|
styleOverrides,
|
||||||
|
target,
|
||||||
|
text,
|
||||||
|
url,
|
||||||
|
isMobile = false,
|
||||||
|
}: Props) {
|
||||||
|
const theme = useTheme2();
|
||||||
|
const styles = getStyles(theme, isActive);
|
||||||
|
const elStyle = cx(styles.element, styleOverrides);
|
||||||
|
|
||||||
|
const linkContent = (
|
||||||
|
<div className={styles.linkContent}>
|
||||||
|
<span>{text}</span>
|
||||||
|
{target === '_blank' && (
|
||||||
|
<Icon data-testid="external-link-icon" name="external-link-alt" className={styles.externalLinkIcon} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
let element = (
|
||||||
|
<button className={elStyle} onClick={onClick} tabIndex={-1}>
|
||||||
|
{linkContent}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
element =
|
||||||
|
!target && url.startsWith('/') ? (
|
||||||
|
<Link className={elStyle} href={url} target={target} onClick={onClick} tabIndex={!isMobile ? -1 : 0}>
|
||||||
|
{linkContent}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<a href={url} target={target} className={elStyle} onClick={onClick} tabIndex={!isMobile ? -1 : 0}>
|
||||||
|
{linkContent}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return isDivider ? (
|
||||||
|
<li data-testid="dropdown-child-divider" className={styles.divider} tabIndex={-1} aria-disabled />
|
||||||
|
) : (
|
||||||
|
<li className={styles.listItem}>{element}</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDivider ? (
|
||||||
|
<div data-testid="dropdown-child-divider" className={styles.divider} tabIndex={-1} aria-disabled />
|
||||||
|
) : (
|
||||||
|
<div style={{ position: 'relative' }}>{element}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NavBarMenuItem.displayName = 'NavBarMenuItem';
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive']) => ({
|
||||||
|
linkContent: css({
|
||||||
|
display: 'grid',
|
||||||
|
placeItems: 'center',
|
||||||
|
gridAutoFlow: 'column',
|
||||||
|
gap: '0.5rem',
|
||||||
|
}),
|
||||||
|
externalLinkIcon: css({
|
||||||
|
color: theme.colors.text.secondary,
|
||||||
|
gridColumnStart: 3,
|
||||||
|
}),
|
||||||
|
element: css({
|
||||||
|
alignItems: 'center',
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
color: isActive ? theme.colors.text.primary : theme.colors.text.secondary,
|
||||||
|
display: 'flex',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
height: '100%',
|
||||||
|
padding: '5px 12px 5px 10px',
|
||||||
|
textAlign: 'left',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
|
||||||
|
'&:focus-visible + .pin-button': {
|
||||||
|
opacity: '100%',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&:focus-visible': {
|
||||||
|
outline: 'none',
|
||||||
|
boxShadow: 'none',
|
||||||
|
|
||||||
|
'&::after': {
|
||||||
|
boxShadow: 'none',
|
||||||
|
outline: `${theme.shape.borderRadius} solid ${theme.colors.primary.main}`,
|
||||||
|
outlineOffset: `-${theme.shape.borderRadius(1)}`,
|
||||||
|
transition: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'&::before': {
|
||||||
|
display: isActive ? 'block' : 'none',
|
||||||
|
content: '" "',
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: theme.spacing(0.5),
|
||||||
|
borderRadius: theme.shape.borderRadius(1),
|
||||||
|
backgroundImage: theme.colors.gradients.brandVertical,
|
||||||
|
},
|
||||||
|
|
||||||
|
'&::after': {
|
||||||
|
position: 'absolute',
|
||||||
|
content: '" "',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
listItem: css({
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
|
||||||
|
'&:hover, &:focus-within': {
|
||||||
|
color: theme.colors.text.primary,
|
||||||
|
|
||||||
|
'> *:first-child::after': {
|
||||||
|
backgroundColor: theme.colors.action.hover,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'> .pin-button': {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
'&:hover > .pin-button, &:focusVisible > .pin-button': {
|
||||||
|
opacity: '100%',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
divider: css({
|
||||||
|
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||||
|
height: '1px',
|
||||||
|
margin: `${theme.spacing(1)} 0`,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}),
|
||||||
|
});
|
@ -4,7 +4,7 @@ import { Router } from 'react-router-dom';
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { locationService } from '@grafana/runtime';
|
import { locationService } from '@grafana/runtime';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
import TestProvider from '../../../../test/helpers/TestProvider';
|
import TestProvider from '../../../../../test/helpers/TestProvider';
|
||||||
import { NavBarNext } from './NavBarNext';
|
import { NavBarNext } from './NavBarNext';
|
||||||
|
|
||||||
jest.mock('app/core/services/context_srv', () => ({
|
jest.mock('app/core/services/context_srv', () => ({
|
237
public/app/core/components/NavBar/Next/NavBarNext.tsx
Normal file
237
public/app/core/components/NavBar/Next/NavBarNext.tsx
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { GrafanaTheme2, NavModelItem, NavSection } from '@grafana/data';
|
||||||
|
import { Icon, IconButton, IconName, useTheme2 } from '@grafana/ui';
|
||||||
|
import { config, locationService } from '@grafana/runtime';
|
||||||
|
import { getKioskMode } from 'app/core/navigation/kiosk';
|
||||||
|
import { KioskMode, StoreState } from 'app/types';
|
||||||
|
import { enrichConfigItems, getActiveItem, isMatchOrChildMatch, isSearchActive, SEARCH_ITEM_ID } from '../utils';
|
||||||
|
import { OrgSwitcher } from '../../OrgSwitcher';
|
||||||
|
import { NavBarMenu } from './NavBarMenu';
|
||||||
|
import NavBarItem from './NavBarItem';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { NavBarItemWithoutMenu } from './NavBarItemWithoutMenu';
|
||||||
|
|
||||||
|
const onOpenSearch = () => {
|
||||||
|
locationService.partial({ search: 'open' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchItem: NavModelItem = {
|
||||||
|
id: SEARCH_ITEM_ID,
|
||||||
|
onClick: onOpenSearch,
|
||||||
|
text: 'Search dashboards',
|
||||||
|
icon: 'search',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Here we need to hack in a "home" NavModelItem since this is constructed in the frontend
|
||||||
|
const homeItem: NavModelItem = {
|
||||||
|
id: 'home',
|
||||||
|
text: 'Home',
|
||||||
|
url: config.appSubUrl || '/',
|
||||||
|
icon: 'grafana',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NavBarNext = React.memo(() => {
|
||||||
|
const navBarTree = useSelector((state: StoreState) => state.navBarTree);
|
||||||
|
const theme = useTheme2();
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
const location = useLocation();
|
||||||
|
const kiosk = getKioskMode();
|
||||||
|
const [showSwitcherModal, setShowSwitcherModal] = useState(false);
|
||||||
|
const toggleSwitcherModal = () => {
|
||||||
|
setShowSwitcherModal(!showSwitcherModal);
|
||||||
|
};
|
||||||
|
const navTree = cloneDeep(navBarTree);
|
||||||
|
navTree.unshift(homeItem);
|
||||||
|
|
||||||
|
const coreItems = navTree.filter((item) => item.section === NavSection.Core);
|
||||||
|
const pluginItems = navTree.filter((item) => item.section === NavSection.Plugin);
|
||||||
|
const configItems = enrichConfigItems(
|
||||||
|
navTree.filter((item) => item.section === NavSection.Config),
|
||||||
|
location,
|
||||||
|
toggleSwitcherModal
|
||||||
|
);
|
||||||
|
const activeItem = isSearchActive(location) ? searchItem : getActiveItem(navTree, location.pathname);
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
if (kiosk !== KioskMode.Off) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.navWrapper}>
|
||||||
|
<nav className={cx(styles.sidemenu, 'sidemenu')} data-testid="sidemenu" aria-label="Main menu">
|
||||||
|
<div className={styles.mobileSidemenuLogo} onClick={() => setMenuOpen(!menuOpen)} key="hamburger">
|
||||||
|
<Icon name="bars" size="xl" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className={styles.itemList}>
|
||||||
|
<NavBarItemWithoutMenu
|
||||||
|
isActive={isMatchOrChildMatch(homeItem, activeItem)}
|
||||||
|
label="Home"
|
||||||
|
className={styles.grafanaLogo}
|
||||||
|
url={homeItem.url}
|
||||||
|
>
|
||||||
|
<Icon name="grafana" size="xl" />
|
||||||
|
</NavBarItemWithoutMenu>
|
||||||
|
<NavBarItem className={styles.search} isActive={activeItem === searchItem} link={searchItem}>
|
||||||
|
<Icon name="search" size="xl" />
|
||||||
|
</NavBarItem>
|
||||||
|
|
||||||
|
{coreItems.map((link, index) => (
|
||||||
|
<NavBarItem
|
||||||
|
key={`${link.id}-${index}`}
|
||||||
|
isActive={isMatchOrChildMatch(link, activeItem)}
|
||||||
|
link={{ ...link, subTitle: undefined, onClick: undefined }}
|
||||||
|
>
|
||||||
|
{link.icon && <Icon name={link.icon as IconName} size="xl" />}
|
||||||
|
{link.img && <img src={link.img} alt={`${link.text} logo`} />}
|
||||||
|
</NavBarItem>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{pluginItems.length > 0 &&
|
||||||
|
pluginItems.map((link, index) => (
|
||||||
|
<NavBarItem key={`${link.id}-${index}`} isActive={isMatchOrChildMatch(link, activeItem)} link={link}>
|
||||||
|
{link.icon && <Icon name={link.icon as IconName} size="xl" />}
|
||||||
|
{link.img && <img src={link.img} alt={`${link.text} logo`} />}
|
||||||
|
</NavBarItem>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{configItems.map((link, index) => (
|
||||||
|
<NavBarItem
|
||||||
|
key={`${link.id}-${index}`}
|
||||||
|
isActive={isMatchOrChildMatch(link, activeItem)}
|
||||||
|
reverseMenuDirection
|
||||||
|
link={link}
|
||||||
|
className={cx({ [styles.verticalSpacer]: index === 0 })}
|
||||||
|
>
|
||||||
|
{link.icon && <Icon name={link.icon as IconName} size="xl" />}
|
||||||
|
{link.img && <img src={link.img} alt={`${link.text} logo`} />}
|
||||||
|
</NavBarItem>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{showSwitcherModal && <OrgSwitcher onDismiss={toggleSwitcherModal} />}
|
||||||
|
<div className={styles.menuWrapper}>
|
||||||
|
{menuOpen && (
|
||||||
|
<NavBarMenu
|
||||||
|
activeItem={activeItem}
|
||||||
|
navItems={[homeItem, searchItem, ...coreItems, ...pluginItems, ...configItems]}
|
||||||
|
onClose={() => setMenuOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
name={menuOpen ? 'angle-left' : 'angle-right'}
|
||||||
|
className={cx(styles.menuToggle, { [styles.menuOpen]: menuOpen })}
|
||||||
|
size="xl"
|
||||||
|
onClick={() => setMenuOpen(!menuOpen)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
NavBarNext.displayName = 'NavBarNext';
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
navWrapper: css({
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
}),
|
||||||
|
sidemenu: css({
|
||||||
|
label: 'sidemenu',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
backgroundColor: theme.colors.background.primary,
|
||||||
|
zIndex: theme.zIndex.sidemenu,
|
||||||
|
padding: `${theme.spacing(1)} 0`,
|
||||||
|
position: 'relative',
|
||||||
|
width: theme.spacing(7),
|
||||||
|
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
position: 'fixed',
|
||||||
|
paddingTop: '0px',
|
||||||
|
backgroundColor: 'inherit',
|
||||||
|
},
|
||||||
|
|
||||||
|
'.sidemenu-hidden &': {
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
mobileSidemenuLogo: css({
|
||||||
|
alignItems: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
itemList: css({
|
||||||
|
backgroundColor: 'inherit',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
'> *': {
|
||||||
|
height: theme.spacing(6),
|
||||||
|
},
|
||||||
|
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
grafanaLogo: css({
|
||||||
|
alignItems: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
img: {
|
||||||
|
height: theme.spacing(3),
|
||||||
|
width: theme.spacing(3),
|
||||||
|
},
|
||||||
|
justifyContent: 'center',
|
||||||
|
}),
|
||||||
|
search: css({
|
||||||
|
display: 'none',
|
||||||
|
marginTop: 0,
|
||||||
|
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
display: 'grid',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
verticalSpacer: css({
|
||||||
|
marginTop: 'auto',
|
||||||
|
}),
|
||||||
|
hideFromMobile: css({
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
menuWrapper: css({
|
||||||
|
position: 'fixed',
|
||||||
|
display: 'grid',
|
||||||
|
gridAutoFlow: 'column',
|
||||||
|
height: '100%',
|
||||||
|
zIndex: 9999,
|
||||||
|
}),
|
||||||
|
menuToggle: css({
|
||||||
|
position: 'absolute',
|
||||||
|
marginRight: 0,
|
||||||
|
top: '43px',
|
||||||
|
right: '0px',
|
||||||
|
zIndex: 9999,
|
||||||
|
transform: `translateX(calc(${theme.spacing(7)} + 50%))`,
|
||||||
|
background: 'gray',
|
||||||
|
borderRadius: '50%',
|
||||||
|
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
menuOpen: css({
|
||||||
|
transform: 'translateX(0%)',
|
||||||
|
}),
|
||||||
|
});
|
@ -65,14 +65,14 @@ describe('VariablesUnknownTable', () => {
|
|||||||
const { getUnknownsNetworkSpy } = await getTestContext();
|
const { getUnknownsNetworkSpy } = await getTestContext();
|
||||||
|
|
||||||
userEvent.click(screen.getByRole('heading', { name: /renamed or missing variables/i }));
|
userEvent.click(screen.getByRole('heading', { name: /renamed or missing variables/i }));
|
||||||
await waitFor(() => expect(screen.getByTitle('Click to collapse')).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true'));
|
||||||
expect(getUnknownsNetworkSpy).toHaveBeenCalledTimes(1);
|
expect(getUnknownsNetworkSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
userEvent.click(screen.getByRole('heading', { name: /renamed or missing variables/i }));
|
userEvent.click(screen.getByRole('heading', { name: /renamed or missing variables/i }));
|
||||||
await waitFor(() => expect(screen.getByTitle('Click to expand')).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false'));
|
||||||
|
|
||||||
userEvent.click(screen.getByRole('heading', { name: /renamed or missing variables/i }));
|
userEvent.click(screen.getByRole('heading', { name: /renamed or missing variables/i }));
|
||||||
await waitFor(() => expect(screen.getByTitle('Click to collapse')).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true'));
|
||||||
|
|
||||||
expect(getUnknownsNetworkSpy).toHaveBeenCalledTimes(1);
|
expect(getUnknownsNetworkSpy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user