diff --git a/packages/grafana-runtime/src/components/PluginPage.tsx b/packages/grafana-runtime/src/components/PluginPage.tsx index 601a0fb56af..de886f12e8e 100644 --- a/packages/grafana-runtime/src/components/PluginPage.tsx +++ b/packages/grafana-runtime/src/components/PluginPage.tsx @@ -2,7 +2,20 @@ import React from 'react'; import { NavModelItem, PageLayoutType } from '@grafana/data'; +export interface PageInfoItem { + label: string; + value: React.ReactNode; +} + export interface PluginPageProps { + /** Can be used to place actions inline with the heading */ + info?: PageInfoItem[]; + /** Can be used to place actions inline with the heading */ + actions?: React.ReactNode; + /** Can be used to customize rendering of title */ + renderTitle?: (title: string) => React.ReactNode; + /** Shown under main heading */ + subTitle?: React.ReactNode; pageNav?: NavModelItem; children: React.ReactNode; layout?: PageLayoutType; diff --git a/public/app/core/components/Page/Page.tsx b/public/app/core/components/Page/Page.tsx index 496c20696d3..968e5f32142 100644 --- a/public/app/core/components/Page/Page.tsx +++ b/public/app/core/components/Page/Page.tsx @@ -26,7 +26,10 @@ export const OldPage: PageType = ({ scrollRef, scrollTop, layout = PageLayoutType.Standard, + renderTitle, subTitle, + actions, + info, ...otherProps }) => { const styles = useStyles2(getStyles); @@ -41,7 +44,15 @@ export const OldPage: PageType = ({ {layout === PageLayoutType.Standard && (
- {pageHeaderNav && } + {pageHeaderNav && ( + + )} {children}
diff --git a/public/app/core/components/Page/types.ts b/public/app/core/components/Page/types.ts index 0e23c966835..6d64fa2b8cb 100644 --- a/public/app/core/components/Page/types.ts +++ b/public/app/core/components/Page/types.ts @@ -12,7 +12,15 @@ export interface PageProps extends HTMLAttributes { navId?: string; navModel?: NavModel; pageNav?: NavModelItem; + /** Can be used to place info inline with the heading */ + info?: PageInfoItem[]; + /** Can be used to place actions inline with the heading */ + actions?: React.ReactNode; + /** Can be used to customize rendering of title */ + renderTitle?: (title: string) => React.ReactNode; + /** Can be used to customize or customize and set a page sub title */ subTitle?: React.ReactNode; + /** Control the page layout. */ layout?: PageLayoutType; /** Something we can remove when we remove the old nav. */ toolbar?: React.ReactNode; diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx index e9d6d292c03..9c5867c2350 100644 --- a/public/app/core/components/PageHeader/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -1,14 +1,20 @@ -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import React, { FC } from 'react'; import { NavModelItem, NavModelBreadcrumb, GrafanaTheme2 } from '@grafana/data'; import { Tab, TabsBar, Icon, useStyles2, toIconName } from '@grafana/ui'; import { PanelHeaderMenuItem } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem'; +import { PageInfoItem } from '../Page/types'; +import { PageInfo } from '../PageInfo/PageInfo'; import { ProBadge } from '../Upgrade/ProBadge'; export interface Props { navItem: NavModelItem; + renderTitle?: (title: string) => React.ReactNode; + actions?: React.ReactNode; + info?: PageInfoItem[]; + subTitle?: React.ReactNode; } const SelectNav = ({ children, customCss }: { children: NavModelItem[]; customCss: string }) => { @@ -75,18 +81,42 @@ const Navigation = ({ children }: { children: NavModelItem[] }) => { ); }; -export const PageHeader: FC = ({ navItem: model }) => { +export const PageHeader: FC = ({ navItem: model, renderTitle, actions, info, subTitle }) => { const styles = useStyles2(getStyles); if (!model) { return null; } + const renderHeader = (main: NavModelItem) => { + const marginTop = main.icon === 'grafana' ? 12 : 14; + const icon = main.icon && toIconName(main.icon); + const sub = subTitle ?? main.subTitle; + + return ( +
+ + {icon && } + {main.img && {`logo} + + +
+ {renderTitle + ? renderTitle(main.text) + : renderHeaderTitle(main.text, main.breadcrumbs ?? [], main.highlightText)} + {info && } + {sub &&
{sub}
} + {actions &&
{actions}
} +
+
+ ); + }; + return (
- {renderHeaderTitle(model)} + {renderHeader(model)} {model.children && model.children.length > 0 && {model.children}}
@@ -94,26 +124,11 @@ export const PageHeader: FC = ({ navItem: model }) => { ); }; -function renderHeaderTitle(main: NavModelItem) { - const marginTop = main.icon === 'grafana' ? 12 : 14; - const icon = main.icon && toIconName(main.icon); - - return ( -
- - {icon && } - {main.img && {`logo} - - -
- {renderTitle(main.text, main.breadcrumbs ?? [], main.highlightText)} - {main.subTitle &&
{main.subTitle}
} -
-
- ); -} - -function renderTitle(title: string, breadcrumbs: NavModelBreadcrumb[], highlightText: NavModelItem['highlightText']) { +function renderHeaderTitle( + title: string, + breadcrumbs: NavModelBreadcrumb[], + highlightText: NavModelItem['highlightText'] +) { if (!title && (!breadcrumbs || breadcrumbs.length === 0)) { return null; } @@ -152,6 +167,16 @@ function renderTitle(title: string, breadcrumbs: NavModelBreadcrumb[], highlight } const getStyles = (theme: GrafanaTheme2) => ({ + actions: css({ + display: 'flex', + flexDirection: 'row', + gap: theme.spacing(1), + }), + headerText: css({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1), + }), headerCanvas: css` background: ${theme.colors.background.canvas}; `, diff --git a/public/app/core/components/PageNew/Page.tsx b/public/app/core/components/PageNew/Page.tsx index 1ad41a503b4..59497768267 100644 --- a/public/app/core/components/PageNew/Page.tsx +++ b/public/app/core/components/PageNew/Page.tsx @@ -20,9 +20,12 @@ export const Page: PageType = ({ navId, navModel: oldNavProp, pageNav, + renderTitle, + actions, subTitle, children, className, + info, layout = PageLayoutType.Standard, toolbar, scrollTop, @@ -54,7 +57,15 @@ export const Page: PageType = ({
- {pageHeaderNav && } + {pageHeaderNav && ( + + )} {pageNav && pageNav.children && } {children}
@@ -81,12 +92,12 @@ export const Page: PageType = ({ ); }; -const OldNavOnly = () => null; -OldNavOnly.displayName = 'OldNavOnly'; - Page.Header = PageHeader; Page.Contents = PageContents; -Page.OldNavOnly = OldNavOnly; + +Page.OldNavOnly = function OldNavOnly() { + return null; +}; const getStyles = (theme: GrafanaTheme2) => { const shadow = theme.isDark diff --git a/public/app/core/components/PageNew/PageHeader.tsx b/public/app/core/components/PageNew/PageHeader.tsx index 930e8e5a1b4..9ab2709ce68 100644 --- a/public/app/core/components/PageNew/PageHeader.tsx +++ b/public/app/core/components/PageNew/PageHeader.tsx @@ -4,39 +4,83 @@ import React from 'react'; import { NavModelItem, GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; +import { PageInfoItem } from '../Page/types'; +import { PageInfo } from '../PageInfo/PageInfo'; + export interface Props { navItem: NavModelItem; + renderTitle?: (title: string) => React.ReactNode; + actions?: React.ReactNode; + info?: PageInfoItem[]; subTitle?: React.ReactNode; } -export function PageHeader({ navItem, subTitle }: Props) { +export function PageHeader({ navItem, renderTitle, actions, info, subTitle }: Props) { const styles = useStyles2(getStyles); - const sub = subTitle ?? navItem.description; + const sub = subTitle ?? navItem.subTitle; + + const title = navItem.text; + const titleElement = renderTitle ? renderTitle(title) :

{title}

; return ( - <> -

- {navItem.img && {`logo} - {navItem.text} -

- {sub &&
{sub}
} - +
+
+
+
+ {navItem.img && {`logo} + {titleElement} +
+ {info && } +
+
{actions}
+
+ {sub &&
{sub}
} +
); } const getStyles = (theme: GrafanaTheme2) => { return { + topRow: css({ + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + gap: theme.spacing(1, 2), + }), + title: css({ + display: 'flex', + flexDirection: 'row', + }), + actions: css({ + display: 'flex', + flexDirection: 'row', + gap: theme.spacing(1), + }), + titleInfoContainer: css({ + display: 'flex', + label: 'title-info-container', + flex: 1, + flexWrap: 'wrap', + gap: theme.spacing(1, 4), + justifyContent: 'space-between', + maxWidth: '100%', + }), + pageHeader: css({ + label: 'page-header', + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1), + }), pageTitle: css({ display: 'flex', - marginBottom: theme.spacing(3), + marginBottom: 0, }), - pageSubTitle: css({ + subTitle: css({ marginBottom: theme.spacing(2), position: 'relative', - top: theme.spacing(-1), color: theme.colors.text.secondary, }), - pageImg: css({ + img: css({ width: '32px', height: '32px', marginRight: theme.spacing(2), diff --git a/public/app/core/components/PageNew/PluginPage.tsx b/public/app/core/components/PageNew/PluginPage.tsx index f620545e9ae..2872786852a 100644 --- a/public/app/core/components/PageNew/PluginPage.tsx +++ b/public/app/core/components/PageNew/PluginPage.tsx @@ -5,11 +5,19 @@ import { PluginPageContext } from 'app/features/plugins/components/PluginPageCon import { Page } from '../Page/Page'; -export function PluginPage({ children, pageNav, layout }: PluginPageProps) { +export function PluginPage({ actions, children, info, pageNav, layout, renderTitle, subTitle }: PluginPageProps) { const context = useContext(PluginPageContext); return ( - + {children} );