PanelChrome: new logic-less emotion based component with no dependency on PanelModel or DashboardModel (#29456)

* WIP: Started work on a new panel chrome component

* Minor progress

* Next icons & state

* adding support for leftItems.

* fixing duplicated exports of PanelChrome.

* adding examples on loading indicator in storybook.

* adding API stability docs.

* removed dependency on stylesFactory.

* fixed docs errors.

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>
This commit is contained in:
Torkel Ödegaard 2021-03-18 13:22:19 +01:00 committed by GitHub
parent b0d7e3dbee
commit 1454c3723d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 275 additions and 0 deletions

View File

@ -0,0 +1,30 @@
import React from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { Icon } from '../Icon/Icon';
import { Tooltip } from '../Tooltip/Tooltip';
type LoadingIndicatorProps = {
loading: boolean;
onCancel: () => void;
};
/**
* @internal
*/
export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({ onCancel, loading }) => {
if (!loading) {
return null;
}
return (
<Tooltip content="Cancel query">
<Icon
className="spin-clockwise"
name="sync"
size="sm"
onClick={onCancel}
aria-label={selectors.components.LoadingIndicator.icon}
/>
</Tooltip>
);
};

View File

@ -0,0 +1,103 @@
import React, { CSSProperties, useState } from 'react';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { PanelChrome, useTheme, PanelChromeProps } from '@grafana/ui';
import { HorizontalGroup, VerticalGroup } from '../Layout/Layout';
import { merge } from 'lodash';
import { GrafanaTheme } from '@grafana/data';
import { useInterval } from 'react-use';
export default {
title: 'Visualizations/PanelChrome',
component: PanelChrome,
decorators: [withCenteredStory],
parameters: {
docs: {},
},
};
function renderPanel(name: string, overrides: Partial<PanelChromeProps>, theme: GrafanaTheme) {
const props: PanelChromeProps = {
width: 400,
height: 130,
title: 'Default title',
children: () => undefined,
};
merge(props, overrides);
const contentStyle: CSSProperties = {
background: theme.colors.bg2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
};
return (
<PanelChrome {...props}>
{(innerWidth, innerHeight) => {
return <div style={{ width: innerWidth, height: innerHeight, ...contentStyle }}>{name}</div>;
}}
</PanelChrome>
);
}
export const StandardPanel = () => {
const theme = useTheme();
const [loading, setLoading] = useState(true);
useInterval(() => setLoading(true), 5000);
return (
<div style={{ background: theme.colors.dashboardBg, padding: 100 }}>
<HorizontalGroup spacing="md">
<VerticalGroup spacing="md">
{renderPanel('Default panel', {}, theme)}
{renderPanel('No padding', { padding: 'none' }, theme)}
</VerticalGroup>
<VerticalGroup spacing="md">
{renderPanel('No title', { title: '' }, theme)}
{renderPanel(
'Very long title',
{ title: 'Very long title that should get ellipsis when there is no more space' },
theme
)}
</VerticalGroup>
</HorizontalGroup>
<div style={{ marginTop: theme.spacing.md }} />
<HorizontalGroup spacing="md">
<VerticalGroup spacing="md">
{renderPanel(
'No title and loading indicator',
{
title: '',
leftItems: [
<PanelChrome.LoadingIndicator
loading={loading}
onCancel={() => setLoading(false)}
key="loading-indicator"
/>,
],
},
theme
)}
</VerticalGroup>
<VerticalGroup spacing="md">
{renderPanel(
'Very long title',
{
title: 'Very long title that should get ellipsis when there is no more space',
leftItems: [
<PanelChrome.LoadingIndicator
loading={loading}
onCancel={() => setLoading(false)}
key="loading-indicator"
/>,
],
},
theme
)}
</VerticalGroup>
</HorizontalGroup>
</div>
);
};

View File

@ -0,0 +1,120 @@
import React, { CSSProperties, ReactNode } from 'react';
import { css } from 'emotion';
import { useStyles, useTheme } from '../../themes';
import { GrafanaTheme } from '@grafana/data';
/**
* @internal
*/
export interface PanelChromeProps {
width: number;
height: number;
title?: string;
padding?: PanelPadding;
leftItems?: React.ReactNode[];
children: (innerWidth: number, innerHeight: number) => React.ReactNode;
}
/**
* @internal
*/
export type PanelPadding = 'none' | 'md';
/**
* @internal
*/
export const PanelChrome: React.FC<PanelChromeProps> = ({
title = '',
children,
width,
height,
padding = 'md',
leftItems = [],
}) => {
const theme = useTheme();
const styles = useStyles(getStyles);
const headerHeight = getHeaderHeight(theme, title, leftItems);
const { contentStyle, innerWidth, innerHeight } = getContentStyle(padding, theme, width, headerHeight, height);
const headerStyles: CSSProperties = {
height: theme.panelHeaderHeight,
};
const containerStyles: CSSProperties = { width, height };
return (
<div className={styles.container} style={containerStyles}>
<div className={styles.header} style={headerStyles}>
<div className={styles.headerTitle}>{title}</div>
{itemsRenderer(leftItems, (items) => {
return <div className={styles.leftItems}>{items}</div>;
})}
</div>
<div className={styles.content} style={contentStyle}>
{children(innerWidth, innerHeight)}
</div>
</div>
);
};
const getStyles = (theme: GrafanaTheme) => {
return {
container: css`
label: panel-container;
background-color: ${theme.colors.panelBg};
border: 1px solid ${theme.colors.panelBorder};
position: relative;
border-radius: 3px;
height: 100%;
display: flex;
flex-direction: column;
flex: 0 0 0;
`,
content: css`
label: panel-content;
width: 100%;
flex-grow: 1;
`,
header: css`
label: panel-header;
display: flex;
align-items: center;
`,
headerTitle: css`
label: panel-header;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
padding-left: ${theme.panelPadding}px;
flex-grow: 1;
`,
leftItems: css`
padding-right: ${theme.panelPadding}px;
`,
};
};
const itemsRenderer = (items: ReactNode[], renderer: (items: ReactNode[]) => ReactNode): ReactNode => {
const toRender = React.Children.toArray(items).filter(Boolean);
return toRender.length > 0 ? renderer(toRender) : null;
};
const getHeaderHeight = (theme: GrafanaTheme, title: string, items: ReactNode[]) => {
if (title.length > 0 || items.length > 0) {
return theme.panelHeaderHeight;
}
return 0;
};
const getContentStyle = (padding: string, theme: GrafanaTheme, width: number, headerHeight: number, height: number) => {
const chromePadding = padding === 'md' ? theme.panelPadding : 0;
const panelBorder = 1 * 2;
const innerWidth = width - chromePadding * 2 - panelBorder;
const innerHeight = height - headerHeight - chromePadding * 2 - panelBorder;
const contentStyle: CSSProperties = {
padding: chromePadding,
};
return { contentStyle, innerWidth, innerHeight };
};

View File

@ -0,0 +1,21 @@
import React from 'react';
import { LoadingIndicator } from './LoadingIndicator';
import { PanelChrome as PanelChromeComponent, PanelChromeProps } from './PanelChrome';
/**
* @internal
*/
export { PanelChromeProps, PanelPadding } from './PanelChrome';
/**
* @internal
*/
export interface PanelChromeType extends React.FC<PanelChromeProps> {
LoadingIndicator: typeof LoadingIndicator;
}
/**
* @internal
*/
export const PanelChrome = PanelChromeComponent as PanelChromeType;
PanelChrome.LoadingIndicator = LoadingIndicator;

View File

@ -81,6 +81,7 @@ export { BarGauge, BarGaugeDisplayMode } from './BarGauge/BarGauge';
export { GraphTooltipOptions } from './Graph/GraphTooltip/types';
export { VizRepeater, VizRepeaterRenderValueProps } from './VizRepeater/VizRepeater';
export { graphTimeFormat, graphTickFormatter } from './Graph/utils';
export { PanelChrome, PanelChromeProps, PanelPadding, PanelChromeType } from './PanelChrome';
export { VizLayout, VizLayoutComponentType, VizLayoutLegendProps, VizLayoutProps } from './VizLayout/VizLayout';
export { VizLegendItem, LegendPlacement, LegendDisplayMode, VizLegendOptions } from './VizLegend/types';
export { VizLegend } from './VizLegend/VizLegend';