mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Navigation: Collapsible section nav implementation (#55995)
* initial collapsible section nav implementation * fix unit tests * automatically collapse sectionnav when below lg size * fix unit tests * only register 1 event listener each time * fix display name for SectionNavToggle
This commit is contained in:
parent
4087ad413f
commit
317b353b34
@ -6,12 +6,10 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||
import CSSTransition from 'react-transition-group/CSSTransition';
|
||||
|
||||
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { CustomScrollbar, Icon, IconButton, useTheme2 } from '@grafana/ui';
|
||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||
|
||||
import { TOP_BAR_LEVEL_HEIGHT } from '../AppChrome/types';
|
||||
import { NavBarToggle } from '../NavBar/NavBarToggle';
|
||||
|
||||
import { NavBarMenuItemWrapper } from './NavBarMenuItemWrapper';
|
||||
|
||||
@ -75,14 +73,6 @@ export function NavBarMenu({ activeItem, navItems, searchBarHidden, onClose }: P
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
<NavBarToggle
|
||||
className={styles.menuCollapseIcon}
|
||||
isExpanded={true}
|
||||
onClick={() => {
|
||||
reportInteraction('grafana_navigation_collapsed');
|
||||
onMenuClose();
|
||||
}}
|
||||
/>
|
||||
<nav className={styles.content}>
|
||||
<CustomScrollbar showScrollIndicators hideHorizontalTrack>
|
||||
<ul className={styles.itemList}>
|
||||
@ -162,12 +152,6 @@ const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => {
|
||||
gridTemplateColumns: `minmax(${MENU_WIDTH}, auto)`,
|
||||
minWidth: MENU_WIDTH,
|
||||
}),
|
||||
menuCollapseIcon: css({
|
||||
position: 'absolute',
|
||||
top: '20px',
|
||||
right: '0px',
|
||||
transform: `translateX(50%)`,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
// Libraries
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
||||
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
|
||||
import { CustomScrollbar, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||
|
||||
import { Footer } from '../Footer/Footer';
|
||||
@ -15,6 +16,7 @@ import { PageContents } from './PageContents';
|
||||
import { PageHeader } from './PageHeader';
|
||||
import { PageTabs } from './PageTabs';
|
||||
import { SectionNav } from './SectionNav';
|
||||
import { SectionNavToggle } from './SectionNavToggle';
|
||||
|
||||
export const Page: PageType = ({
|
||||
navId,
|
||||
@ -29,14 +31,29 @@ export const Page: PageType = ({
|
||||
scrollRef,
|
||||
...otherProps
|
||||
}) => {
|
||||
const theme = useTheme2();
|
||||
const styles = useStyles2(getStyles);
|
||||
const navModel = usePageNav(navId, oldNavProp);
|
||||
const { chrome } = useGrafana();
|
||||
|
||||
const isSmallScreen = window.matchMedia(`(max-width: ${theme.breakpoints.values.lg}px)`).matches;
|
||||
const [navExpandedPreference, setNavExpandedPreference] = useLocalStorage<boolean>(
|
||||
'grafana.sectionNav.expanded',
|
||||
!isSmallScreen
|
||||
);
|
||||
const [isNavExpanded, setNavExpanded] = useState(!isSmallScreen && navExpandedPreference);
|
||||
|
||||
usePageTitle(navModel, pageNav);
|
||||
|
||||
const pageHeaderNav = pageNav ?? navModel?.node;
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia(`(max-width: ${theme.breakpoints.values.lg}px)`);
|
||||
const onMediaQueryChange = (e: MediaQueryListEvent) => setNavExpanded(e.matches ? false : navExpandedPreference);
|
||||
mediaQuery.addEventListener('change', onMediaQueryChange);
|
||||
return () => mediaQuery.removeEventListener('change', onMediaQueryChange);
|
||||
}, [navExpandedPreference, theme.breakpoints.values.lg]);
|
||||
|
||||
useEffect(() => {
|
||||
if (navModel) {
|
||||
chrome.update({
|
||||
@ -46,11 +63,25 @@ export const Page: PageType = ({
|
||||
}
|
||||
}, [navModel, pageNav, chrome]);
|
||||
|
||||
const onToggleSectionNav = () => {
|
||||
setNavExpandedPreference(!isNavExpanded);
|
||||
setNavExpanded(!isNavExpanded);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cx(styles.wrapper, className)} {...otherProps}>
|
||||
{layout === PageLayoutType.Standard && (
|
||||
<div className={styles.panes}>
|
||||
{navModel && <SectionNav model={navModel} />}
|
||||
{navModel && (
|
||||
<>
|
||||
<SectionNav model={navModel} isExpanded={Boolean(isNavExpanded)} />
|
||||
<SectionNavToggle
|
||||
className={styles.collapseIcon}
|
||||
isExpanded={Boolean(isNavExpanded)}
|
||||
onClick={onToggleSectionNav}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className={styles.pageContent}>
|
||||
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
|
||||
<div className={styles.pageInner}>
|
||||
@ -94,6 +125,18 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
: '0 0.6px 1.5px -1px rgb(0 0 0 / 8%),0 2px 4px rgb(0 0 0 / 6%),0 5px 10px -1px rgb(0 0 0 / 5%)';
|
||||
|
||||
return {
|
||||
collapseIcon: css({
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
transform: 'translateX(50%)',
|
||||
top: theme.spacing(8),
|
||||
right: theme.spacing(-1),
|
||||
|
||||
[theme.breakpoints.down('md')]: {
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, 50%) rotate(90deg)',
|
||||
top: theme.spacing(2),
|
||||
},
|
||||
}),
|
||||
wrapper: css`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { NavModel, GrafanaTheme2 } from '@grafana/data';
|
||||
@ -8,9 +8,10 @@ import { SectionNavItem } from './SectionNavItem';
|
||||
|
||||
export interface Props {
|
||||
model: NavModel;
|
||||
isExpanded: boolean;
|
||||
}
|
||||
|
||||
export function SectionNav({ model }: Props) {
|
||||
export function SectionNav({ model, isExpanded }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
if (!Boolean(model.main?.children?.length)) {
|
||||
@ -18,7 +19,11 @@ export function SectionNav({ model }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className={styles.nav}>
|
||||
<nav
|
||||
className={cx(styles.nav, {
|
||||
[styles.navExpanded]: isExpanded,
|
||||
})}
|
||||
>
|
||||
<CustomScrollbar showScrollIndicators>
|
||||
<div className={styles.items} role="tablist">
|
||||
<SectionNavItem item={model.main} />
|
||||
@ -35,14 +40,27 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
flexDirection: 'column',
|
||||
background: theme.colors.background.canvas,
|
||||
flexShrink: 0,
|
||||
transition: theme.transitions.create(['width', 'max-height']),
|
||||
[theme.breakpoints.up('md')]: {
|
||||
width: 0,
|
||||
},
|
||||
[theme.breakpoints.down('md')]: {
|
||||
maxHeight: 0,
|
||||
},
|
||||
}),
|
||||
navExpanded: css({
|
||||
[theme.breakpoints.up('md')]: {
|
||||
width: '250px',
|
||||
},
|
||||
[theme.breakpoints.down('md')]: {
|
||||
maxHeight: '50vh',
|
||||
},
|
||||
}),
|
||||
items: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: theme.spacing(4.5, 1, 2, 2),
|
||||
minWidth: '250px',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
39
public/app/core/components/PageNew/SectionNavToggle.tsx
Normal file
39
public/app/core/components/PageNew/SectionNavToggle.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { css } from '@emotion/css';
|
||||
import classnames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { IconButton, useTheme2 } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
className?: string;
|
||||
isExpanded: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const SectionNavToggle = ({ className, isExpanded, onClick }: Props) => {
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={isExpanded ? 'Close section navigation' : 'Open section navigation'}
|
||||
name={isExpanded ? 'angle-left' : 'angle-right'}
|
||||
className={classnames(className, styles.icon)}
|
||||
size="xl"
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
SectionNavToggle.displayName = 'SectionNavToggle';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
icon: css({
|
||||
backgroundColor: theme.colors.background.secondary,
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
borderRadius: '50%',
|
||||
marginRight: 0,
|
||||
zIndex: 1,
|
||||
}),
|
||||
});
|
@ -57,7 +57,7 @@ describe('VersionSettings', () => {
|
||||
// Need to use delay: null here to work with fakeTimers
|
||||
// see https://github.com/testing-library/user-event/issues/833
|
||||
user = userEvent.setup({ delay: null });
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
@ -103,7 +103,7 @@ describe('VersionSettings', () => {
|
||||
historySrv.getHistoryList.mockResolvedValue(versions.slice(0, VERSIONS_FETCH_LIMIT - 5));
|
||||
setup();
|
||||
|
||||
expect(screen.queryByRole('button', { name: /show more versions|/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /show more versions/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /compare versions/i })).not.toBeInTheDocument();
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('table')).toBeInTheDocument());
|
||||
|
Loading…
Reference in New Issue
Block a user