Navigation: Expose new props to extend Page/PluginPage (#58465)

* add extensions and customisation to `Page`

* adjust alignment
This commit is contained in:
Ashley Harrison 2022-11-09 09:05:01 +00:00 committed by GitHub
parent 9778d642df
commit b3c761aaa7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 162 additions and 50 deletions

View File

@ -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;

View File

@ -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 && (
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
<div className={cx('page-scrollbar-content', className)}>
{pageHeaderNav && <PageHeader navItem={pageHeaderNav} />}
{pageHeaderNav && (
<PageHeader
actions={actions}
info={info}
navItem={pageHeaderNav}
renderTitle={renderTitle}
subTitle={subTitle}
/>
)}
{children}
<Footer />
</div>
@ -67,7 +78,6 @@ export const OldPage: PageType = ({
);
};
OldPage.Header = PageHeader;
OldPage.Contents = PageContents;
OldPage.OldNavOnly = OldNavOnly;

View File

@ -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<HTMLDivElement> {
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<PageProps> {
Header: typeof PageHeader;
OldNavOnly: typeof OldNavOnly;
Contents: typeof PageContents;
}

View File

@ -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<Props> = ({ navItem: model }) => {
export const PageHeader: FC<Props> = ({ 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 (
<div className="page-header__inner">
<span className="page-header__logo">
{icon && <Icon name={icon} size="xxxl" style={{ marginTop }} />}
{main.img && <img className="page-header__img" src={main.img} alt={`logo of ${main.text}`} />}
</span>
<div className={cx('page-header__info-block', styles.headerText)}>
{renderTitle
? 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 />}
{actions && <div className={styles.actions}>{actions}</div>}
</div>
</div>
);
};
return (
<div className={styles.headerCanvas}>
<div className="page-container">
<div className="page-header">
{renderHeaderTitle(model)}
{renderHeader(model)}
{model.children && model.children.length > 0 && <Navigation>{model.children}</Navigation>}
</div>
</div>
@ -99,27 +130,11 @@ export const PageHeader: FC<Props> = ({ navItem: model }) => {
);
};
function renderHeaderTitle(main: NavModelItem) {
const marginTop = main.icon === 'grafana' ? 12 : 14;
const icon = main.icon && toIconName(main.icon);
return (
<div className="page-header__inner">
<span className="page-header__logo">
{icon && <Icon name={icon} size="xxxl" style={{ marginTop }} />}
{main.img && <img className="page-header__img" src={main.img} alt={`logo of ${main.text}`} />}
</span>
<div className="page-header__info-block">
{renderTitle(main.text, main.breadcrumbs ?? [], main.highlightText)}
{main.subTitle && <div className="page-header__sub-title">{main.subTitle}</div>}
{main.headerExtra && <main.headerExtra />}
</div>
</div>
);
}
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};
`,

View File

@ -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 = ({
<div className={styles.pageContainer}>
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
<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} />}
<div className={styles.pageContent}>{children}</div>
</div>
@ -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

View File

@ -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) : <h1 className={styles.pageTitle}>{title}</h1>;
return (
<>
<h1 className={styles.pageTitle}>
{navItem.img && <img className={styles.pageImg} src={navItem.img} alt={`logo for ${navItem.text}`} />}
{getNavTitle(navItem.id) ?? navItem.text}
</h1>
{sub && <div className={styles.pageSubTitle}>{sub}</div>}
<div className={styles.pageHeader}>
<div className={styles.topRow}>
<div className={styles.titleInfoContainer}>
<div className={styles.title}>
{navItem.img && <img className={styles.img} src={navItem.img} alt={`logo for ${navItem.text}`} />}
{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 />}
</>
</div>
);
}
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),

View File

@ -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 (
<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>
);

View File

@ -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};