mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
b0d7e3dbee
commit
1454c3723d
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
120
packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx
Normal file
120
packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx
Normal 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 };
|
||||||
|
};
|
21
packages/grafana-ui/src/components/PanelChrome/index.ts
Normal file
21
packages/grafana-ui/src/components/PanelChrome/index.ts
Normal 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;
|
@ -81,6 +81,7 @@ export { BarGauge, BarGaugeDisplayMode } from './BarGauge/BarGauge';
|
|||||||
export { GraphTooltipOptions } from './Graph/GraphTooltip/types';
|
export { GraphTooltipOptions } from './Graph/GraphTooltip/types';
|
||||||
export { VizRepeater, VizRepeaterRenderValueProps } from './VizRepeater/VizRepeater';
|
export { VizRepeater, VizRepeaterRenderValueProps } from './VizRepeater/VizRepeater';
|
||||||
export { graphTimeFormat, graphTickFormatter } from './Graph/utils';
|
export { graphTimeFormat, graphTickFormatter } from './Graph/utils';
|
||||||
|
export { PanelChrome, PanelChromeProps, PanelPadding, PanelChromeType } from './PanelChrome';
|
||||||
export { VizLayout, VizLayoutComponentType, VizLayoutLegendProps, VizLayoutProps } from './VizLayout/VizLayout';
|
export { VizLayout, VizLayoutComponentType, VizLayoutLegendProps, VizLayoutProps } from './VizLayout/VizLayout';
|
||||||
export { VizLegendItem, LegendPlacement, LegendDisplayMode, VizLegendOptions } from './VizLegend/types';
|
export { VizLegendItem, LegendPlacement, LegendDisplayMode, VizLegendOptions } from './VizLegend/types';
|
||||||
export { VizLegend } from './VizLegend/VizLegend';
|
export { VizLegend } from './VizLegend/VizLegend';
|
||||||
|
Loading…
Reference in New Issue
Block a user