diff --git a/packages/grafana-runtime/src/components/PluginPage.tsx b/packages/grafana-runtime/src/components/PluginPage.tsx index 23b0de448d6..de886f12e8e 100644 --- a/packages/grafana-runtime/src/components/PluginPage.tsx +++ b/packages/grafana-runtime/src/components/PluginPage.tsx @@ -2,7 +2,18 @@ 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; diff --git a/public/app/core/components/Page/Page.tsx b/public/app/core/components/Page/Page.tsx index 496c20696d3..a8e620942df 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}
@@ -67,7 +78,6 @@ export const OldPage: PageType = ({ ); }; -OldPage.Header = PageHeader; OldPage.Contents = PageContents; OldPage.OldNavOnly = OldNavOnly; diff --git a/public/app/core/components/Page/types.ts b/public/app/core/components/Page/types.ts index 0e23c966835..1cbf15c54d4 100644 --- a/public/app/core/components/Page/types.ts +++ b/public/app/core/components/Page/types.ts @@ -2,8 +2,6 @@ import React, { FC, HTMLAttributes, RefCallback } from 'react'; import { NavModel, NavModelItem, PageLayoutType } from '@grafana/data'; -import { PageHeader } from '../PageHeader/PageHeader'; - import { OldNavOnly } from './OldNavOnly'; import { PageContents } from './PageContents'; @@ -12,7 +10,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; @@ -28,7 +34,6 @@ export interface PageInfoItem { } export interface PageType extends FC { - Header: typeof PageHeader; OldNavOnly: typeof OldNavOnly; Contents: typeof PageContents; } diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx index c98ba70d26c..9309c5561de 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 }) => { @@ -80,18 +86,43 @@ 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}
} + {main.headerExtra && } + {actions &&
{actions}
} +
+
+ ); + }; + return (
- {renderHeaderTitle(model)} + {renderHeader(model)} {model.children && model.children.length > 0 && {model.children}}
@@ -99,27 +130,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}
} - {main.headerExtra && } -
-
- ); -} - -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; } @@ -158,6 +173,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 2b7e33c0364..e221db268b7 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,11 @@ 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 ea134842bb2..0534a7416a2 100644 --- a/public/app/core/components/PageNew/PageHeader.tsx +++ b/public/app/core/components/PageNew/PageHeader.tsx @@ -5,41 +5,84 @@ import { NavModelItem, GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; import { getNavSubTitle, getNavTitle } from '../NavBar/navBarItem-translations'; +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 ?? getNavSubTitle(navItem.id) ?? navItem.subTitle; + const title = getNavTitle(navItem.id) ?? navItem.text; + const titleElement = renderTitle ? renderTitle(title) :

{title}

; + return ( - <> -

- {navItem.img && {`logo} - {getNavTitle(navItem.id) ?? navItem.text} -

- {sub &&
{sub}
} +
+
+
+
+ {navItem.img && {`logo} + {titleElement} +
+ {info && } +
+
{actions}
+
+ {sub &&
{sub}
} {navItem.headerExtra && } - +
); } 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 41189849d1d..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, subTitle }: PluginPageProps) { +export function PluginPage({ actions, children, info, pageNav, layout, renderTitle, subTitle }: PluginPageProps) { const context = useContext(PluginPageContext); return ( - + {children} ); diff --git a/public/app/features/plugins/admin/components/PluginDetailsHeader.tsx b/public/app/features/plugins/admin/components/PluginDetailsHeader.tsx index 91f0cff634d..87d89760cff 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsHeader.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsHeader.tsx @@ -72,7 +72,7 @@ export const getStyles = (theme: GrafanaTheme2) => { margin-bottom: ${theme.spacing(1)}; `, description: css` - margin: ${theme.spacing(-1, 0, 1)}; + margin-bottom: ${theme.spacing(1)}; `, breadcrumb: css` font-size: ${theme.typography.h2.fontSize};