grafana/public/app/core/components/AppChrome/AppChrome.tsx

239 lines
7.8 KiB
TypeScript

import { css, cx } from '@emotion/css';
import classNames from 'classnames';
import { PropsWithChildren, useEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { config, locationSearchToObject, locationService } from '@grafana/runtime';
import { useStyles2, LinkButton, useTheme2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange';
import store from 'app/core/store';
import { CommandPalette } from 'app/features/commandPalette/CommandPalette';
import { KioskMode } from 'app/types';
import { AppChromeMenu } from './AppChromeMenu';
import { DOCKED_LOCAL_STORAGE_KEY, DOCKED_MENU_OPEN_LOCAL_STORAGE_KEY } from './AppChromeService';
import { MegaMenu } from './MegaMenu/MegaMenu';
import { NavToolbar } from './NavToolbar/NavToolbar';
import { ReturnToPrevious } from './ReturnToPrevious/ReturnToPrevious';
import { TopSearchBar } from './TopBar/TopSearchBar';
import { TOP_BAR_LEVEL_HEIGHT } from './types';
export interface Props extends PropsWithChildren<{}> {}
export function AppChrome({ children }: Props) {
const { chrome } = useGrafana();
const state = chrome.useState();
const searchBarHidden = state.searchBarHidden || state.kioskMode === KioskMode.TV;
const theme = useTheme2();
const styles = useStyles2(getStyles, searchBarHidden);
const dockedMenuBreakpoint = theme.breakpoints.values.xl;
const dockedMenuLocalStorageState = store.getBool(DOCKED_LOCAL_STORAGE_KEY, true);
const menuDockedAndOpen = !state.chromeless && state.megaMenuDocked && state.megaMenuOpen;
useMediaQueryChange({
breakpoint: dockedMenuBreakpoint,
onChange: (e) => {
if (dockedMenuLocalStorageState) {
chrome.setMegaMenuDocked(e.matches, false);
chrome.setMegaMenuOpen(
e.matches ? store.getBool(DOCKED_MENU_OPEN_LOCAL_STORAGE_KEY, state.megaMenuOpen) : false
);
}
},
});
const contentClass = cx({
[styles.content]: true,
[styles.contentNoSearchBar]: searchBarHidden,
[styles.contentChromeless]: state.chromeless,
});
const handleMegaMenu = () => {
chrome.setMegaMenuOpen(!state.megaMenuOpen);
};
const { pathname, search } = locationService.getLocation();
const url = pathname + search;
const shouldShowReturnToPrevious = state.returnToPrevious && url !== state.returnToPrevious.href;
// Clear returnToPrevious when the page is manually navigated to
useEffect(() => {
if (state.returnToPrevious && url === state.returnToPrevious.href) {
chrome.clearReturnToPrevious('auto_dismissed');
}
// We only want to pay attention when the location changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chrome, url]);
// Sync updates from kiosk mode query string back into app chrome
useEffect(() => {
const queryParams = locationSearchToObject(search);
chrome.setKioskModeFromUrl(queryParams.kiosk);
}, [chrome, search]);
// Chromeless routes are without topNav, mega menu, search & command palette
// We check chromeless twice here instead of having a separate path so {children}
// doesn't get re-mounted when chromeless goes from true to false.
return (
<div
className={classNames('main-view', {
'main-view--search-bar-hidden': searchBarHidden && !state.chromeless,
'main-view--chrome-hidden': state.chromeless,
})}
>
{!state.chromeless && (
<>
<LinkButton className={styles.skipLink} href="#pageContent">
Skip to main content
</LinkButton>
<header className={cx(styles.topNav)}>
{!searchBarHidden && <TopSearchBar />}
<NavToolbar
searchBarHidden={searchBarHidden}
sectionNav={state.sectionNav.node}
pageNav={state.pageNav}
actions={state.actions}
onToggleSearchBar={chrome.onToggleSearchBar}
onToggleMegaMenu={handleMegaMenu}
onToggleKioskMode={chrome.onToggleKioskMode}
/>
</header>
</>
)}
<div className={contentClass}>
<div className={styles.panes}>
{menuDockedAndOpen && (
<MegaMenu className={styles.dockedMegaMenu} onClose={() => chrome.setMegaMenuOpen(false)} />
)}
<main
className={cx(styles.pageContainer, {
[styles.pageContainerMenuDocked]: config.featureToggles.bodyScrolling && menuDockedAndOpen,
})}
id="pageContent"
>
{children}
</main>
</div>
</div>
{!state.chromeless && !state.megaMenuDocked && <AppChromeMenu />}
{!state.chromeless && <CommandPalette />}
{shouldShowReturnToPrevious && state.returnToPrevious && (
<ReturnToPrevious href={state.returnToPrevious.href} title={state.returnToPrevious.title} />
)}
</div>
);
}
const getStyles = (theme: GrafanaTheme2, searchBarHidden: boolean) => {
return {
content: css({
display: 'flex',
flexDirection: 'column',
paddingTop: TOP_BAR_LEVEL_HEIGHT * 2,
flexGrow: 1,
height: config.featureToggles.bodyScrolling ? 'auto' : '100%',
}),
contentNoSearchBar: css({
paddingTop: TOP_BAR_LEVEL_HEIGHT,
}),
contentChromeless: css({
paddingTop: 0,
}),
dockedMegaMenu: css(
config.featureToggles.bodyScrolling
? {
background: theme.colors.background.primary,
borderRight: `1px solid ${theme.colors.border.weak}`,
display: 'none',
position: 'fixed',
height: `calc(100% - ${searchBarHidden ? TOP_BAR_LEVEL_HEIGHT : TOP_BAR_LEVEL_HEIGHT * 2}px)`,
zIndex: theme.zIndex.navbarFixed,
[theme.breakpoints.up('xl')]: {
display: 'block',
},
}
: {
background: theme.colors.background.primary,
borderRight: `1px solid ${theme.colors.border.weak}`,
display: 'none',
zIndex: theme.zIndex.navbarFixed,
[theme.breakpoints.up('xl')]: {
display: 'block',
},
}
),
topNav: css({
display: 'flex',
position: 'fixed',
zIndex: theme.zIndex.navbarFixed,
left: 0,
right: 0,
background: theme.colors.background.primary,
flexDirection: 'column',
}),
panes: css(
config.featureToggles.bodyScrolling
? {
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
label: 'page-panes',
}
: {
label: 'page-panes',
display: 'flex',
height: '100%',
width: '100%',
flexGrow: 1,
minHeight: 0,
flexDirection: 'column',
[theme.breakpoints.up('md')]: {
flexDirection: 'row',
},
}
),
pageContainerMenuDocked: css({
paddingLeft: '300px',
}),
pageContainer: css(
config.featureToggles.bodyScrolling
? {
label: 'page-container',
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
}
: {
label: 'page-container',
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
minHeight: 0,
minWidth: 0,
overflow: 'auto',
'@media print': {
overflow: 'visible',
},
'@page': {
margin: 0,
size: 'auto',
padding: 0,
},
}
),
skipLink: css({
position: 'fixed',
top: -1000,
':focus': {
left: theme.spacing(1),
top: theme.spacing(1),
zIndex: theme.zIndex.portal,
},
}),
};
};