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}
+
+ >
+ )}
);
};
@@ -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} />}
+
);
}