TopNav: Page can now support dashboard page layouts (Both new and old) (#52039)

* Scenes: Support new top nav

* Page: Make Page component support new and old dashboard page layouts

* Pass scrollbar props

* Fixing flex layout for dashboard

* Updated title handling and test
This commit is contained in:
Torkel Ödegaard
2022-07-14 20:52:03 +02:00
committed by GitHub
parent 7947629f82
commit a472e094e1
8 changed files with 209 additions and 137 deletions
+57 -16
View File
@@ -11,11 +11,21 @@ import { PageHeader } from '../PageHeader/PageHeader';
import { Page as NewPage } from '../PageNew/Page';
import { PageContents } from './PageContents';
import { PageType } from './types';
import { PageLayoutType, PageType } from './types';
import { usePageNav } from './usePageNav';
import { usePageTitle } from './usePageTitle';
export const OldPage: PageType = ({ navId, navModel: oldNavProp, pageNav, children, className, ...otherProps }) => {
export const OldPage: PageType = ({
navId,
navModel: oldNavProp,
pageNav,
children,
className,
toolbar,
scrollRef,
scrollTop,
layout = PageLayoutType.Default,
}) => {
const styles = useStyles2(getStyles);
const navModel = usePageNav(navId, oldNavProp);
@@ -24,14 +34,26 @@ export const OldPage: PageType = ({ navId, navModel: oldNavProp, pageNav, childr
const pageHeaderNav = pageNav ?? navModel?.main;
return (
<div {...otherProps} className={cx(styles.wrapper, className)}>
<CustomScrollbar autoHeightMin={'100%'}>
<div className="page-scrollbar-content">
{pageHeaderNav && <PageHeader navItem={pageHeaderNav} />}
{children}
<Footer />
</div>
</CustomScrollbar>
<div className={cx(styles.wrapper, className)}>
{layout === PageLayoutType.Default && (
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
<div className="page-scrollbar-content">
{pageHeaderNav && <PageHeader navItem={pageHeaderNav} />}
{children}
<Footer />
</div>
</CustomScrollbar>
)}
{layout === PageLayoutType.Dashboard && (
<>
{toolbar}
<div className={styles.scrollWrapper}>
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
<div className={cx(styles.content, !toolbar && styles.contentWithoutToolbar)}>{children}</div>
</CustomScrollbar>
</div>
</>
)}
</div>
);
};
@@ -41,10 +63,29 @@ OldPage.Contents = PageContents;
export const Page: PageType = config.featureToggles.topnav ? NewPage : OldPage;
const getStyles = (_: GrafanaTheme2) => ({
wrapper: css`
width: 100%;
flex-grow: 1;
min-height: 0;
`,
const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css({
width: '100%',
height: '100%',
display: 'flex',
flex: '1 1 0',
flexDirection: 'column',
minHeight: 0,
}),
scrollWrapper: css({
width: '100%',
flexGrow: 1,
minHeight: 0,
display: 'flex',
}),
content: css({
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(0, 2, 2, 2),
flexBasis: '100%',
flexGrow: 1,
}),
contentWithoutToolbar: css({
padding: theme.spacing(2),
}),
});
+13 -1
View File
@@ -1,4 +1,4 @@
import { FC, HTMLAttributes } from 'react';
import { FC, HTMLAttributes, RefCallback } from 'react';
import { NavModel, NavModelItem } from '@grafana/data';
@@ -11,6 +11,18 @@ export interface PageProps extends HTMLAttributes<HTMLDivElement> {
navId?: string;
navModel?: NavModel;
pageNav?: NavModelItem;
layout?: PageLayoutType;
/** Something we can remove when we remove the old nav. */
toolbar?: React.ReactNode;
/** Can be used to get the scroll container element to access scroll position */
scrollRef?: RefCallback<HTMLDivElement>;
/** Can be used to update the current scroll position */
scrollTop?: number;
}
export enum PageLayoutType {
Default,
Dashboard,
}
export interface PageType extends FC<PageProps> {
+47 -17
View File
@@ -8,7 +8,7 @@ import { CustomScrollbar, useStyles2 } from '@grafana/ui';
// Components
import { appChromeService } from '../AppChrome/AppChromeService';
import { Footer } from '../Footer/Footer';
import { PageType } from '../Page/types';
import { PageLayoutType, PageType } from '../Page/types';
import { usePageNav } from '../Page/usePageNav';
import { usePageTitle } from '../Page/usePageTitle';
@@ -17,7 +17,17 @@ import { PageHeader } from './PageHeader';
import { PageTabs } from './PageTabs';
import { SectionNav } from './SectionNav';
export const Page: PageType = ({ navId, navModel: oldNavProp, pageNav, children, className, ...otherProps }) => {
export const Page: PageType = ({
navId,
navModel: oldNavProp,
pageNav,
children,
className,
layout = PageLayoutType.Default,
toolbar,
scrollTop,
scrollRef,
}) => {
const styles = useStyles2(getStyles);
const navModel = usePageNav(navId, oldNavProp);
@@ -26,26 +36,39 @@ export const Page: PageType = ({ navId, navModel: oldNavProp, pageNav, children,
const pageHeaderNav = pageNav ?? navModel?.node;
useEffect(() => {
if (navModel || pageNav) {
appChromeService.update({ sectionNav: navModel?.node, pageNav });
if (navModel) {
appChromeService.update({
sectionNav: navModel.node,
...(pageNav && { pageNav }),
});
}
}, [navModel, pageNav]);
return (
<div {...otherProps} className={cx(styles.wrapper, className)}>
<div className={styles.panes}>
{navModel && navModel.main.children && <SectionNav model={navModel} />}
<div className={styles.pageContent}>
<CustomScrollbar autoHeightMin={'100%'}>
<div className={styles.pageInner}>
{pageHeaderNav && <PageHeader navItem={pageHeaderNav} />}
{pageNav && pageNav.children && <PageTabs navItem={pageNav} />}
{children}
</div>
<Footer />
</CustomScrollbar>
<div className={cx(styles.wrapper, className)}>
{layout === PageLayoutType.Default && (
<div className={styles.panes}>
{navModel && navModel.main.children && <SectionNav model={navModel} />}
<div className={styles.pageContent}>
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
<div className={styles.pageInner}>
{pageHeaderNav && <PageHeader navItem={pageHeaderNav} />}
{pageNav && pageNav.children && <PageTabs navItem={pageNav} />}
{children}
</div>
<Footer />
</CustomScrollbar>
</div>
</div>
</div>
)}
{layout === PageLayoutType.Dashboard && (
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
<div className={styles.dashboardContent}>
{toolbar}
{children}
</div>
</CustomScrollbar>
)}
</div>
);
};
@@ -89,5 +112,12 @@ const getStyles = (theme: GrafanaTheme2) => {
flexDirection: 'column',
flexGrow: 1,
}),
dashboardContent: css({
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(2),
flexBasis: '100%',
flexGrow: 1,
}),
};
};
@@ -8,7 +8,7 @@ import { mockToolkitActionCreator } from 'test/core/redux/mocks';
import { createTheme } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { locationService, setDataSourceSrv } from '@grafana/runtime';
import { config, locationService, setDataSourceSrv } from '@grafana/runtime';
import { notifyApp } from 'app/core/actions';
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
import { DashboardInitPhase, DashboardRoutes } from 'app/types';
@@ -103,6 +103,8 @@ function dashboardPageScenario(description: string, scenarioFn: (ctx: ScenarioCo
setupFn = fn;
},
mount: (propOverrides?: Partial<Props>) => {
config.bootData.navTree = [{ text: 'Dashboards', id: 'dashboards' }];
const store = configureStore();
const props: Props = {
...getRouteComponentProps({
@@ -189,7 +191,7 @@ describe('DashboardPage', () => {
});
it('Should update title', () => {
expect(document.title).toBe('My dashboard - Grafana');
expect(document.title).toBe('My dashboard - Dashboards - Grafana');
});
});
@@ -1,14 +1,14 @@
import { css } from '@emotion/css';
import classnames from 'classnames';
import React, { PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { GrafanaTheme2, TimeRange } from '@grafana/data';
import { locationUtil, NavModelItem, TimeRange } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config, locationService } from '@grafana/runtime';
import { CustomScrollbar, stylesFactory, Themeable2, withTheme2 } from '@grafana/ui';
import { locationService } from '@grafana/runtime';
import { Themeable2, withTheme2 } from '@grafana/ui';
import { notifyApp } from 'app/core/actions';
import { Branding } from 'app/core/components/Branding/Branding';
import { Page } from 'app/core/components/Page/Page';
import { PageLayoutType } from 'app/core/components/Page/types';
import { createErrorNotification } from 'app/core/copy/appNotification';
import { getKioskMode } from 'app/core/navigation/kiosk';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
@@ -92,6 +92,7 @@ export interface State {
export class UnthemedDashboardPage extends PureComponent<Props, State> {
private forceRouteReloadCounter = 0;
state: State = this.getCleanState();
pageNav?: NavModelItem;
getCleanState(): State {
return {
@@ -148,9 +149,27 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
return;
}
// if we just got dashboard update title
if (prevProps.dashboard !== dashboard) {
document.title = dashboard.title + ' - ' + Branding.AppTitle;
// Update page nav
if (!this.pageNav || dashboard.title !== this.pageNav.text) {
this.pageNav = {
text: dashboard.title,
url: locationUtil.getUrlForPartial(this.props.history.location, {
editview: null,
editPanel: null,
viewPanel: null,
}),
};
}
// Check if folder changed
if (
dashboard.meta.folderTitle &&
(!this.pageNav.parentItem || this.pageNav.parentItem.text !== dashboard.meta.folderTitle)
) {
this.pageNav.parentItem = {
text: dashboard.meta.folderTitle,
url: `/dashboards/f/${dashboard.meta.folderUid}`,
};
}
if (
@@ -321,97 +340,61 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
}
render() {
const { dashboard, initError, queryParams, theme, isPublic } = this.props;
const { dashboard, initError, queryParams, isPublic } = this.props;
const { editPanel, viewPanel, updateScrollTop } = this.state;
const kioskMode = !isPublic ? getKioskMode() : KioskMode.Full;
const styles = getStyles(theme, kioskMode);
if (!dashboard) {
return <DashboardLoading initPhase={this.props.initPhase} />;
}
const inspectPanel = this.getInspectPanel();
const containerClassNames = classnames(styles.dashboardContainer, {
'panel-in-fullscreen': viewPanel,
});
const containerClassNames = classnames({ 'panel-in-fullscreen': viewPanel });
const showSubMenu = !editPanel && kioskMode === KioskMode.Off && !this.props.queryParams.editview;
const toolbar = kioskMode !== KioskMode.Full && (
<header data-testid={selectors.pages.Dashboard.DashNav.navV2}>
<DashNav
dashboard={dashboard}
title={dashboard.title}
folderTitle={dashboard.meta.folderTitle}
isFullscreen={!!viewPanel}
onAddPanel={this.onAddPanel}
kioskMode={kioskMode}
hideTimePicker={dashboard.timepicker.hidden}
/>
</header>
);
return (
<div className={containerClassNames}>
{kioskMode !== KioskMode.Full && (
<header data-testid={selectors.pages.Dashboard.DashNav.navV2}>
<DashNav
dashboard={dashboard}
title={dashboard.title}
folderTitle={dashboard.meta.folderTitle}
isFullscreen={!!viewPanel}
onAddPanel={this.onAddPanel}
kioskMode={kioskMode}
hideTimePicker={dashboard.timepicker.hidden}
/>
</header>
)}
<Page
navId="dashboards"
pageNav={this.pageNav}
layout={PageLayoutType.Dashboard}
toolbar={toolbar}
className={containerClassNames}
scrollRef={this.setScrollRef}
scrollTop={updateScrollTop}
>
<DashboardPrompt dashboard={dashboard} />
<div className={styles.dashboardScroll}>
<CustomScrollbar
autoHeightMin="100%"
scrollRefCallback={this.setScrollRef}
scrollTop={updateScrollTop}
hideHorizontalTrack={true}
updateAfterMountMs={500}
>
<div className={styles.dashboardContent}>
{initError && <DashboardFailed />}
{showSubMenu && (
<section aria-label={selectors.pages.Dashboard.SubMenu.submenu}>
<SubMenu dashboard={dashboard} annotations={dashboard.annotations.list} links={dashboard.links} />
</section>
)}
{initError && <DashboardFailed />}
{showSubMenu && (
<section aria-label={selectors.pages.Dashboard.SubMenu.submenu}>
<SubMenu dashboard={dashboard} annotations={dashboard.annotations.list} links={dashboard.links} />
</section>
)}
<DashboardGrid dashboard={dashboard} viewPanel={viewPanel} editPanel={editPanel} />
</div>
</CustomScrollbar>
</div>
<DashboardGrid dashboard={dashboard} viewPanel={viewPanel} editPanel={editPanel} />
{inspectPanel && <PanelInspector dashboard={dashboard} panel={inspectPanel} />}
{editPanel && <PanelEditor dashboard={dashboard} sourcePanel={editPanel} tab={this.props.queryParams.tab} />}
{queryParams.editview && <DashboardSettings dashboard={dashboard} editview={queryParams.editview} />}
</div>
</Page>
);
}
}
/*
* Styles
*/
export const getStyles = stylesFactory((theme: GrafanaTheme2, kioskMode: KioskMode) => {
const contentPadding =
kioskMode === KioskMode.Full || config.featureToggles.topnav ? theme.spacing(2) : theme.spacing(0, 2, 2);
return {
dashboardContainer: css`
width: 100%;
height: 100%;
display: flex;
flex: 1 1 0;
flex-direction: column;
min-height: 0;
`,
dashboardScroll: css`
width: 100%;
flex-grow: 1;
min-height: 0;
display: flex;
`,
dashboardContent: css`
padding: ${contentPadding};
flex-basis: 100%;
flex-grow: 1;
`,
};
});
export const DashboardPage = withTheme2(UnthemedDashboardPage);
DashboardPage.displayName = 'DashboardPage';
export default connector(DashboardPage);
+1 -1
View File
@@ -14,7 +14,7 @@ export const SceneListPage: FC<Props> = ({}) => {
const scenes = getScenes();
return (
<Page>
<Page navId="scenes">
<Page.Contents>
<Stack direction="column">
{scenes.map((scene) => (
+1 -5
View File
@@ -14,11 +14,7 @@ export const ScenePage: FC<Props> = (props) => {
return <h2>Scene not found</h2>;
}
return (
<div style={{ height: '100%', display: 'flex', width: '100%' }}>
<scene.Component model={scene} />
</div>
);
return <scene.Component model={scene} />;
};
export default ScenePage;
+26 -18
View File
@@ -1,6 +1,10 @@
import React from 'react';
import { config } from '@grafana/runtime';
import { PageToolbar, ToolbarButton } from '@grafana/ui';
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
import { Page } from 'app/core/components/Page/Page';
import { PageLayoutType } from 'app/core/components/Page/types';
import { SceneObjectBase } from '../core/SceneObjectBase';
import { SceneComponentProps, SceneObjectState, SceneObject } from '../core/types';
@@ -31,24 +35,28 @@ export class Scene extends SceneObjectBase<SceneState> {
function SceneRenderer({ model }: SceneComponentProps<Scene>) {
const { title, layout, actions = [], isEditing, $editor } = model.useState();
const toolbarActions = (actions ?? []).map((action) => <action.Component key={action.state.key} model={action} />);
if ($editor) {
toolbarActions.push(
<ToolbarButton
icon="cog"
variant={isEditing ? 'primary' : 'default'}
onClick={() => model.setState({ isEditing: !model.state.isEditing })}
/>
);
}
const pageToolbar = config.featureToggles.topnav ? (
<AppChromeUpdate pageNav={{ text: title }} actions={toolbarActions} />
) : (
<PageToolbar title={title}>{toolbarActions}</PageToolbar>
);
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', flex: '1 1 0', minHeight: 0 }}>
<PageToolbar title={title}>
{actions.map((action) => (
<action.Component key={action.state.key} model={action} />
))}
{$editor && (
<ToolbarButton
icon="cog"
variant={isEditing ? 'primary' : 'default'}
onClick={() => model.setState({ isEditing: !model.state.isEditing })}
/>
)}
</PageToolbar>
<div style={{ flexGrow: 1, display: 'flex', padding: '16px', gap: '8px', paddingTop: 0, overflow: 'auto' }}>
<layout.Component model={layout} isEditing={isEditing} />
{$editor && <$editor.Component model={$editor} isEditing={isEditing} />}
</div>
</div>
<Page navId="scenes" layout={PageLayoutType.Dashboard} toolbar={pageToolbar}>
<layout.Component model={layout} isEditing={isEditing} />
{$editor && <$editor.Component model={$editor} isEditing={isEditing} />}
</Page>
);
}