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:
@@ -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),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user