mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Navigation: Expose new props to extend Page
/PluginPage
(#58465)
* add extensions and customisation to `Page` * adjust alignment
This commit is contained in:
parent
9778d642df
commit
b3c761aaa7
@ -2,7 +2,18 @@ import React from 'react';
|
|||||||
|
|
||||||
import { NavModelItem, PageLayoutType } from '@grafana/data';
|
import { NavModelItem, PageLayoutType } from '@grafana/data';
|
||||||
|
|
||||||
|
export interface PageInfoItem {
|
||||||
|
label: string;
|
||||||
|
value: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PluginPageProps {
|
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 */
|
/** Shown under main heading */
|
||||||
subTitle?: React.ReactNode;
|
subTitle?: React.ReactNode;
|
||||||
pageNav?: NavModelItem;
|
pageNav?: NavModelItem;
|
||||||
|
@ -26,7 +26,10 @@ export const OldPage: PageType = ({
|
|||||||
scrollRef,
|
scrollRef,
|
||||||
scrollTop,
|
scrollTop,
|
||||||
layout = PageLayoutType.Standard,
|
layout = PageLayoutType.Standard,
|
||||||
|
renderTitle,
|
||||||
subTitle,
|
subTitle,
|
||||||
|
actions,
|
||||||
|
info,
|
||||||
...otherProps
|
...otherProps
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
@ -41,7 +44,15 @@ export const OldPage: PageType = ({
|
|||||||
{layout === PageLayoutType.Standard && (
|
{layout === PageLayoutType.Standard && (
|
||||||
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
|
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
|
||||||
<div className={cx('page-scrollbar-content', className)}>
|
<div className={cx('page-scrollbar-content', className)}>
|
||||||
{pageHeaderNav && <PageHeader navItem={pageHeaderNav} />}
|
{pageHeaderNav && (
|
||||||
|
<PageHeader
|
||||||
|
actions={actions}
|
||||||
|
info={info}
|
||||||
|
navItem={pageHeaderNav}
|
||||||
|
renderTitle={renderTitle}
|
||||||
|
subTitle={subTitle}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{children}
|
{children}
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
@ -67,7 +78,6 @@ export const OldPage: PageType = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
OldPage.Header = PageHeader;
|
|
||||||
OldPage.Contents = PageContents;
|
OldPage.Contents = PageContents;
|
||||||
OldPage.OldNavOnly = OldNavOnly;
|
OldPage.OldNavOnly = OldNavOnly;
|
||||||
|
|
||||||
|
@ -2,8 +2,6 @@ import React, { FC, HTMLAttributes, RefCallback } from 'react';
|
|||||||
|
|
||||||
import { NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
|
import { NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
|
||||||
|
|
||||||
import { PageHeader } from '../PageHeader/PageHeader';
|
|
||||||
|
|
||||||
import { OldNavOnly } from './OldNavOnly';
|
import { OldNavOnly } from './OldNavOnly';
|
||||||
import { PageContents } from './PageContents';
|
import { PageContents } from './PageContents';
|
||||||
|
|
||||||
@ -12,7 +10,15 @@ export interface PageProps extends HTMLAttributes<HTMLDivElement> {
|
|||||||
navId?: string;
|
navId?: string;
|
||||||
navModel?: NavModel;
|
navModel?: NavModel;
|
||||||
pageNav?: NavModelItem;
|
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;
|
subTitle?: React.ReactNode;
|
||||||
|
/** Control the page layout. */
|
||||||
layout?: PageLayoutType;
|
layout?: PageLayoutType;
|
||||||
/** Something we can remove when we remove the old nav. */
|
/** Something we can remove when we remove the old nav. */
|
||||||
toolbar?: React.ReactNode;
|
toolbar?: React.ReactNode;
|
||||||
@ -28,7 +34,6 @@ export interface PageInfoItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PageType extends FC<PageProps> {
|
export interface PageType extends FC<PageProps> {
|
||||||
Header: typeof PageHeader;
|
|
||||||
OldNavOnly: typeof OldNavOnly;
|
OldNavOnly: typeof OldNavOnly;
|
||||||
Contents: typeof PageContents;
|
Contents: typeof PageContents;
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,20 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { NavModelItem, NavModelBreadcrumb, GrafanaTheme2 } from '@grafana/data';
|
import { NavModelItem, NavModelBreadcrumb, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Tab, TabsBar, Icon, useStyles2, toIconName } from '@grafana/ui';
|
import { Tab, TabsBar, Icon, useStyles2, toIconName } from '@grafana/ui';
|
||||||
import { PanelHeaderMenuItem } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem';
|
import { PanelHeaderMenuItem } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem';
|
||||||
|
|
||||||
|
import { PageInfoItem } from '../Page/types';
|
||||||
|
import { PageInfo } from '../PageInfo/PageInfo';
|
||||||
import { ProBadge } from '../Upgrade/ProBadge';
|
import { ProBadge } from '../Upgrade/ProBadge';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
navItem: NavModelItem;
|
navItem: NavModelItem;
|
||||||
|
renderTitle?: (title: string) => React.ReactNode;
|
||||||
|
actions?: React.ReactNode;
|
||||||
|
info?: PageInfoItem[];
|
||||||
|
subTitle?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectNav = ({ children, customCss }: { children: NavModelItem[]; customCss: string }) => {
|
const SelectNav = ({ children, customCss }: { children: NavModelItem[]; customCss: string }) => {
|
||||||
@ -80,28 +86,17 @@ const Navigation = ({ children }: { children: NavModelItem[] }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageHeader: FC<Props> = ({ navItem: model }) => {
|
export const PageHeader: FC<Props> = ({ navItem: model, renderTitle, actions, info, subTitle }) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const renderHeader = (main: NavModelItem) => {
|
||||||
<div className={styles.headerCanvas}>
|
|
||||||
<div className="page-container">
|
|
||||||
<div className="page-header">
|
|
||||||
{renderHeaderTitle(model)}
|
|
||||||
{model.children && model.children.length > 0 && <Navigation>{model.children}</Navigation>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function renderHeaderTitle(main: NavModelItem) {
|
|
||||||
const marginTop = main.icon === 'grafana' ? 12 : 14;
|
const marginTop = main.icon === 'grafana' ? 12 : 14;
|
||||||
const icon = main.icon && toIconName(main.icon);
|
const icon = main.icon && toIconName(main.icon);
|
||||||
|
const sub = subTitle ?? main.subTitle;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-header__inner">
|
<div className="page-header__inner">
|
||||||
@ -110,16 +105,36 @@ function renderHeaderTitle(main: NavModelItem) {
|
|||||||
{main.img && <img className="page-header__img" src={main.img} alt={`logo of ${main.text}`} />}
|
{main.img && <img className="page-header__img" src={main.img} alt={`logo of ${main.text}`} />}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="page-header__info-block">
|
<div className={cx('page-header__info-block', styles.headerText)}>
|
||||||
{renderTitle(main.text, main.breadcrumbs ?? [], main.highlightText)}
|
{renderTitle
|
||||||
{main.subTitle && <div className="page-header__sub-title">{main.subTitle}</div>}
|
? renderTitle(main.text)
|
||||||
|
: renderHeaderTitle(main.text, main.breadcrumbs ?? [], main.highlightText)}
|
||||||
|
{info && <PageInfo info={info} />}
|
||||||
|
{sub && <div className="page-header__sub-title">{sub}</div>}
|
||||||
{main.headerExtra && <main.headerExtra />}
|
{main.headerExtra && <main.headerExtra />}
|
||||||
|
{actions && <div className={styles.actions}>{actions}</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function renderTitle(title: string, breadcrumbs: NavModelBreadcrumb[], highlightText: NavModelItem['highlightText']) {
|
return (
|
||||||
|
<div className={styles.headerCanvas}>
|
||||||
|
<div className="page-container">
|
||||||
|
<div className="page-header">
|
||||||
|
{renderHeader(model)}
|
||||||
|
{model.children && model.children.length > 0 && <Navigation>{model.children}</Navigation>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderHeaderTitle(
|
||||||
|
title: string,
|
||||||
|
breadcrumbs: NavModelBreadcrumb[],
|
||||||
|
highlightText: NavModelItem['highlightText']
|
||||||
|
) {
|
||||||
if (!title && (!breadcrumbs || breadcrumbs.length === 0)) {
|
if (!title && (!breadcrumbs || breadcrumbs.length === 0)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -158,6 +173,16 @@ function renderTitle(title: string, breadcrumbs: NavModelBreadcrumb[], highlight
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
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`
|
headerCanvas: css`
|
||||||
background: ${theme.colors.background.canvas};
|
background: ${theme.colors.background.canvas};
|
||||||
`,
|
`,
|
||||||
|
@ -20,9 +20,12 @@ export const Page: PageType = ({
|
|||||||
navId,
|
navId,
|
||||||
navModel: oldNavProp,
|
navModel: oldNavProp,
|
||||||
pageNav,
|
pageNav,
|
||||||
|
renderTitle,
|
||||||
|
actions,
|
||||||
subTitle,
|
subTitle,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
info,
|
||||||
layout = PageLayoutType.Standard,
|
layout = PageLayoutType.Standard,
|
||||||
toolbar,
|
toolbar,
|
||||||
scrollTop,
|
scrollTop,
|
||||||
@ -54,7 +57,15 @@ export const Page: PageType = ({
|
|||||||
<div className={styles.pageContainer}>
|
<div className={styles.pageContainer}>
|
||||||
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
|
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
|
||||||
<div className={styles.pageInner}>
|
<div className={styles.pageInner}>
|
||||||
{pageHeaderNav && <PageHeader navItem={pageHeaderNav} subTitle={subTitle} />}
|
{pageHeaderNav && (
|
||||||
|
<PageHeader
|
||||||
|
actions={actions}
|
||||||
|
navItem={pageHeaderNav}
|
||||||
|
renderTitle={renderTitle}
|
||||||
|
info={info}
|
||||||
|
subTitle={subTitle}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{pageNav && pageNav.children && <PageTabs navItem={pageNav} />}
|
{pageNav && pageNav.children && <PageTabs navItem={pageNav} />}
|
||||||
<div className={styles.pageContent}>{children}</div>
|
<div className={styles.pageContent}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -81,12 +92,11 @@ export const Page: PageType = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const OldNavOnly = () => null;
|
|
||||||
OldNavOnly.displayName = 'OldNavOnly';
|
|
||||||
|
|
||||||
Page.Header = PageHeader;
|
|
||||||
Page.Contents = PageContents;
|
Page.Contents = PageContents;
|
||||||
Page.OldNavOnly = OldNavOnly;
|
|
||||||
|
Page.OldNavOnly = function OldNavOnly() {
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
const shadow = theme.isDark
|
const shadow = theme.isDark
|
||||||
|
@ -5,41 +5,84 @@ import { NavModelItem, GrafanaTheme2 } from '@grafana/data';
|
|||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { getNavSubTitle, getNavTitle } from '../NavBar/navBarItem-translations';
|
import { getNavSubTitle, getNavTitle } from '../NavBar/navBarItem-translations';
|
||||||
|
import { PageInfoItem } from '../Page/types';
|
||||||
|
import { PageInfo } from '../PageInfo/PageInfo';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
navItem: NavModelItem;
|
navItem: NavModelItem;
|
||||||
|
renderTitle?: (title: string) => React.ReactNode;
|
||||||
|
actions?: React.ReactNode;
|
||||||
|
info?: PageInfoItem[];
|
||||||
subTitle?: React.ReactNode;
|
subTitle?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PageHeader({ navItem, subTitle }: Props) {
|
export function PageHeader({ navItem, renderTitle, actions, info, subTitle }: Props) {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const sub = subTitle ?? getNavSubTitle(navItem.id) ?? navItem.subTitle;
|
const sub = subTitle ?? getNavSubTitle(navItem.id) ?? navItem.subTitle;
|
||||||
|
|
||||||
|
const title = getNavTitle(navItem.id) ?? navItem.text;
|
||||||
|
const titleElement = renderTitle ? renderTitle(title) : <h1 className={styles.pageTitle}>{title}</h1>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.pageHeader}>
|
||||||
<h1 className={styles.pageTitle}>
|
<div className={styles.topRow}>
|
||||||
{navItem.img && <img className={styles.pageImg} src={navItem.img} alt={`logo for ${navItem.text}`} />}
|
<div className={styles.titleInfoContainer}>
|
||||||
{getNavTitle(navItem.id) ?? navItem.text}
|
<div className={styles.title}>
|
||||||
</h1>
|
{navItem.img && <img className={styles.img} src={navItem.img} alt={`logo for ${navItem.text}`} />}
|
||||||
{sub && <div className={styles.pageSubTitle}>{sub}</div>}
|
{titleElement}
|
||||||
|
</div>
|
||||||
|
{info && <PageInfo info={info} />}
|
||||||
|
</div>
|
||||||
|
<div className={styles.actions}>{actions}</div>
|
||||||
|
</div>
|
||||||
|
{sub && <div className={styles.subTitle}>{sub}</div>}
|
||||||
{navItem.headerExtra && <navItem.headerExtra />}
|
{navItem.headerExtra && <navItem.headerExtra />}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
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({
|
pageTitle: css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
marginBottom: theme.spacing(3),
|
marginBottom: 0,
|
||||||
}),
|
}),
|
||||||
pageSubTitle: css({
|
subTitle: css({
|
||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
top: theme.spacing(-1),
|
|
||||||
color: theme.colors.text.secondary,
|
color: theme.colors.text.secondary,
|
||||||
}),
|
}),
|
||||||
pageImg: css({
|
img: css({
|
||||||
width: '32px',
|
width: '32px',
|
||||||
height: '32px',
|
height: '32px',
|
||||||
marginRight: theme.spacing(2),
|
marginRight: theme.spacing(2),
|
||||||
|
@ -5,11 +5,19 @@ import { PluginPageContext } from 'app/features/plugins/components/PluginPageCon
|
|||||||
|
|
||||||
import { Page } from '../Page/Page';
|
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);
|
const context = useContext(PluginPageContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={context.sectionNav} pageNav={pageNav} layout={layout} subTitle={subTitle}>
|
<Page
|
||||||
|
navModel={context.sectionNav}
|
||||||
|
pageNav={pageNav}
|
||||||
|
layout={layout}
|
||||||
|
actions={actions}
|
||||||
|
renderTitle={renderTitle}
|
||||||
|
info={info}
|
||||||
|
subTitle={subTitle}
|
||||||
|
>
|
||||||
<Page.Contents>{children}</Page.Contents>
|
<Page.Contents>{children}</Page.Contents>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
@ -72,7 +72,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
margin-bottom: ${theme.spacing(1)};
|
margin-bottom: ${theme.spacing(1)};
|
||||||
`,
|
`,
|
||||||
description: css`
|
description: css`
|
||||||
margin: ${theme.spacing(-1, 0, 1)};
|
margin-bottom: ${theme.spacing(1)};
|
||||||
`,
|
`,
|
||||||
breadcrumb: css`
|
breadcrumb: css`
|
||||||
font-size: ${theme.typography.h2.fontSize};
|
font-size: ${theme.typography.h2.fontSize};
|
||||||
|
Loading…
Reference in New Issue
Block a user