mirror of
https://github.com/grafana/grafana.git
synced 2024-11-26 02:40:26 -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 { 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';
|
||||
|
Loading…
Reference in New Issue
Block a user