From a472e094e115e903b6ca1cf9aea19a598c203d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 14 Jul 2022 20:52:03 +0200 Subject: [PATCH] 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 --- public/app/core/components/Page/Page.tsx | 73 ++++++++-- public/app/core/components/Page/types.ts | 14 +- public/app/core/components/PageNew/Page.tsx | 64 +++++--- .../containers/DashboardPage.test.tsx | 6 +- .../dashboard/containers/DashboardPage.tsx | 137 ++++++++---------- public/app/features/scenes/SceneListPage.tsx | 2 +- public/app/features/scenes/ScenePage.tsx | 6 +- .../app/features/scenes/components/Scene.tsx | 44 +++--- 8 files changed, 209 insertions(+), 137 deletions(-) diff --git a/public/app/core/components/Page/Page.tsx b/public/app/core/components/Page/Page.tsx index 643c9ee281e..52c96ecd0a3 100644 --- a/public/app/core/components/Page/Page.tsx +++ b/public/app/core/components/Page/Page.tsx @@ -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 ( -
- -
- {pageHeaderNav && } - {children} -
-
-
+
+ {layout === PageLayoutType.Default && ( + +
+ {pageHeaderNav && } + {children} +
+
+
+ )} + {layout === PageLayoutType.Dashboard && ( + <> + {toolbar} +
+ +
{children}
+
+
+ + )}
); }; @@ -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), + }), }); diff --git a/public/app/core/components/Page/types.ts b/public/app/core/components/Page/types.ts index bc8340f22fc..5a2413f3f0e 100644 --- a/public/app/core/components/Page/types.ts +++ b/public/app/core/components/Page/types.ts @@ -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 { 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; + /** Can be used to update the current scroll position */ + scrollTop?: number; +} + +export enum PageLayoutType { + Default, + Dashboard, } export interface PageType extends FC { diff --git a/public/app/core/components/PageNew/Page.tsx b/public/app/core/components/PageNew/Page.tsx index 72bf56d951a..60ee99235d0 100644 --- a/public/app/core/components/PageNew/Page.tsx +++ b/public/app/core/components/PageNew/Page.tsx @@ -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 ( -
-
- {navModel && navModel.main.children && } -
- -
- {pageHeaderNav && } - {pageNav && pageNav.children && } - {children} -
-
- +
+ {layout === PageLayoutType.Default && ( +
+ {navModel && navModel.main.children && } +
+ +
+ {pageHeaderNav && } + {pageNav && pageNav.children && } + {children} +
+
+ +
-
+ )} + {layout === PageLayoutType.Dashboard && ( + +
+ {toolbar} + {children} +
+
+ )}
); }; @@ -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, + }), }; }; diff --git a/public/app/features/dashboard/containers/DashboardPage.test.tsx b/public/app/features/dashboard/containers/DashboardPage.test.tsx index 9a9b7439af0..992a5e9d4b8 100644 --- a/public/app/features/dashboard/containers/DashboardPage.test.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.test.tsx @@ -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) => { + 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'); }); }); diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 017e6adc8a1..bdc6c2acdf4 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -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 { private forceRouteReloadCounter = 0; state: State = this.getCleanState(); + pageNav?: NavModelItem; getCleanState(): State { return { @@ -148,9 +149,27 @@ export class UnthemedDashboardPage extends PureComponent { 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 { } 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 ; } 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 && ( +
+ +
+ ); return ( -
- {kioskMode !== KioskMode.Full && ( -
- -
- )} - + -
- -
- {initError && } - {showSubMenu && ( -
- -
- )} + {initError && } + {showSubMenu && ( +
+ +
+ )} - -
-
-
+ {inspectPanel && } {editPanel && } {queryParams.editview && } -
+ ); } } -/* - * 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); diff --git a/public/app/features/scenes/SceneListPage.tsx b/public/app/features/scenes/SceneListPage.tsx index 0ed740640f0..2cf1e6ad489 100644 --- a/public/app/features/scenes/SceneListPage.tsx +++ b/public/app/features/scenes/SceneListPage.tsx @@ -14,7 +14,7 @@ export const SceneListPage: FC = ({}) => { const scenes = getScenes(); return ( - + {scenes.map((scene) => ( diff --git a/public/app/features/scenes/ScenePage.tsx b/public/app/features/scenes/ScenePage.tsx index 14cf1133635..c8a0fbbdfdc 100644 --- a/public/app/features/scenes/ScenePage.tsx +++ b/public/app/features/scenes/ScenePage.tsx @@ -14,11 +14,7 @@ export const ScenePage: FC = (props) => { return

Scene not found

; } - return ( -
- -
- ); + return ; }; export default ScenePage; diff --git a/public/app/features/scenes/components/Scene.tsx b/public/app/features/scenes/components/Scene.tsx index a5e52ad0612..3d2194d967d 100644 --- a/public/app/features/scenes/components/Scene.tsx +++ b/public/app/features/scenes/components/Scene.tsx @@ -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 { function SceneRenderer({ model }: SceneComponentProps) { const { title, layout, actions = [], isEditing, $editor } = model.useState(); + const toolbarActions = (actions ?? []).map((action) => ); + + if ($editor) { + toolbarActions.push( + model.setState({ isEditing: !model.state.isEditing })} + /> + ); + } + + const pageToolbar = config.featureToggles.topnav ? ( + + ) : ( + {toolbarActions} + ); + return ( -
- - {actions.map((action) => ( - - ))} - {$editor && ( - model.setState({ isEditing: !model.state.isEditing })} - /> - )} - -
- - {$editor && <$editor.Component model={$editor} isEditing={isEditing} />} -
-
+ + + {$editor && <$editor.Component model={$editor} isEditing={isEditing} />} + ); }