PanelChrome: adding support for displaying error messages. (#32748)

* adding support to display panel error.

* adding error indicator.

* renaming back to left.

* fixing docs error issues.

* adding release tag.
This commit is contained in:
Marcus Andersson 2021-04-12 16:50:49 +02:00 committed by GitHub
parent 258578766b
commit 5ce25509a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 158 additions and 62 deletions

View File

@ -0,0 +1,47 @@
import React from 'react';
import { css, cx } from '@emotion/css';
import { Icon } from '../Icon/Icon';
import { Tooltip } from '../Tooltip/Tooltip';
import { useStyles } from '../../themes';
import { GrafanaTheme } from '@grafana/data';
/**
* @internal
*/
export type ErrorIndicatorProps = {
error?: string;
onClick?: () => void;
};
/**
* @internal
*/
export const ErrorIndicator: React.FC<ErrorIndicatorProps> = ({ error, onClick }) => {
const styles = useStyles(getStyles);
if (!error) {
return null;
}
return (
<Tooltip theme="error" content={error}>
<Icon
onClick={onClick}
className={cx(styles.icon, { [styles.clickable]: !!onClick })}
size="sm"
name="exclamation-triangle"
/>
</Tooltip>
);
};
const getStyles = (theme: GrafanaTheme) => {
return {
clickable: css`
cursor: pointer;
`,
icon: css`
color: ${theme.palette.red88};
`,
};
};

View File

@ -1,9 +1,14 @@
import React from 'react';
import { css, cx } from '@emotion/css';
import { selectors } from '@grafana/e2e-selectors';
import { Icon } from '../Icon/Icon';
import { Tooltip } from '../Tooltip/Tooltip';
import { useStyles } from '../../themes';
type LoadingIndicatorProps = {
/**
* @internal
*/
export type LoadingIndicatorProps = {
loading: boolean;
onCancel: () => void;
};
@ -12,6 +17,8 @@ type LoadingIndicatorProps = {
* @internal
*/
export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({ onCancel, loading }) => {
const styles = useStyles(getStyles);
if (!loading) {
return null;
}
@ -19,7 +26,7 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({ onCancel, lo
return (
<Tooltip content="Cancel query">
<Icon
className="spin-clockwise"
className={cx('spin-clockwise', { [styles.clickable]: !!onCancel })}
name="sync"
size="sm"
onClick={onCancel}
@ -28,3 +35,11 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({ onCancel, lo
</Tooltip>
);
};
const getStyles = () => {
return {
clickable: css`
cursor: pointer;
`,
};
};

View File

@ -1,22 +1,23 @@
import React, { useState } from 'react';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { withCenteredStory, withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
import { useInterval } from 'react-use';
import { PanelChrome, PanelPadding } from './PanelChrome';
import { LoadingIndicator } from './LoadingIndicator';
import { ErrorIndicator } from './ErrorIndicator';
import { useTheme } from '../../themes/ThemeContext';
export default {
title: 'Visualizations/PanelChrome',
component: PanelChrome,
decorators: [withCenteredStory],
decorators: [withCenteredStory, withHorizontallyCenteredStory],
parameters: {
docs: {},
},
argTypes: {
leftItems: {
control: {
type: 'select',
options: ['none', 'loading'],
type: 'multi-select',
options: ['none', 'loading', 'error'],
},
},
width: {
@ -33,18 +34,18 @@ export default {
};
type PanelChromeStoryProps = {
leftItems: string;
leftItems: string[];
title: string | undefined;
padding: PanelPadding;
};
export const StandardPanel = (props: PanelChromeStoryProps) => {
const theme = useTheme();
const { title, padding } = props;
const leftItems = mapToItems(props.leftItems);
return (
<PanelChrome width={400} height={230} leftItems={leftItems} title={title} padding={padding}>
<div style={{ display: 'flex', height: '500px', alignItems: 'center' }}>
<PanelChrome {...props} width={400} height={230} leftItems={leftItems}>
{(innerWidth, innerHeight) => {
return (
<div
@ -62,11 +63,12 @@ export const StandardPanel = (props: PanelChromeStoryProps) => {
);
}}
</PanelChrome>
</div>
);
};
StandardPanel.args = {
leftItems: 'none',
leftItems: ['none'],
title: 'Very long title that should get ellipsis when there is no more space',
};
@ -77,11 +79,15 @@ const LoadingItem = () => {
return <LoadingIndicator loading={loading} onCancel={() => setLoading(false)} />;
};
const mapToItems = (selected: string): React.ReactNode[] | undefined => {
switch (selected) {
const mapToItems = (selected: string[]): React.ReactNode[] => {
return selected.map((s) => {
switch (s) {
case 'loading':
return [<LoadingItem key="loading" />];
return <LoadingItem key="loading" />;
case 'error':
return <ErrorIndicator error="Could not find datasource with id: 12345" onClick={() => {}} />;
default:
return;
return null;
}
});
};

View File

@ -1,6 +1,6 @@
import React, { CSSProperties, ReactNode } from 'react';
import { css } from '@emotion/css';
import { useStyles, useTheme } from '../../themes';
import { useTheme, useStyles } from '../../themes';
import { GrafanaTheme } from '@grafana/data';
/**
@ -11,7 +11,7 @@ export interface PanelChromeProps {
height: number;
title?: string;
padding?: PanelPadding;
leftItems?: React.ReactNode[];
leftItems?: React.ReactNode[]; // rightItems will be added later (actions links etc.)
children: (innerWidth: number, innerHeight: number) => React.ReactNode;
}
@ -57,6 +57,31 @@ export const PanelChrome: React.FC<PanelChromeProps> = ({
);
};
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 };
};
const getStyles = (theme: GrafanaTheme) => {
return {
container: css`
@ -89,32 +114,8 @@ const getStyles = (theme: GrafanaTheme) => {
flex-grow: 1;
`,
leftItems: css`
display: flex;
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

@ -1,5 +1,6 @@
import React from 'react';
import { LoadingIndicator } from './LoadingIndicator';
import { ErrorIndicator } from './ErrorIndicator';
import { PanelChrome as PanelChromeComponent, PanelChromeProps } from './PanelChrome';
/**
@ -12,6 +13,7 @@ export { PanelChromeProps, PanelPadding } from './PanelChrome';
*/
export interface PanelChromeType extends React.FC<PanelChromeProps> {
LoadingIndicator: typeof LoadingIndicator;
ErrorIndicator: typeof ErrorIndicator;
}
/**
@ -19,3 +21,18 @@ export interface PanelChromeType extends React.FC<PanelChromeProps> {
*/
export const PanelChrome = PanelChromeComponent as PanelChromeType;
PanelChrome.LoadingIndicator = LoadingIndicator;
PanelChrome.ErrorIndicator = ErrorIndicator;
/**
* Exporting the components for extensibility and since it is a good practice
* according to the api-extractor.
*/
export {
LoadingIndicator as PanelChromeLoadingIndicator,
LoadingIndicatorProps as PanelChromeLoadingIndicatorProps,
} from './LoadingIndicator';
export {
ErrorIndicator as PanelChromeErrorIndicator,
ErrorIndicatorProps as PanelChromeErrorIndicatorProps,
} from './ErrorIndicator';

View File

@ -77,7 +77,16 @@ 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 {
PanelChrome,
PanelChromeProps,
PanelPadding,
PanelChromeType,
PanelChromeLoadingIndicator,
PanelChromeLoadingIndicatorProps,
PanelChromeErrorIndicator,
PanelChromeErrorIndicatorProps,
} from './PanelChrome';
export { VizLayout, VizLayoutComponentType, VizLayoutLegendProps, VizLayoutProps } from './VizLayout/VizLayout';
export { VizLegendItem } from './VizLegend/types';
export { LegendPlacement, LegendDisplayMode, VizLegendOptions } from './VizLegend/models.gen';

View File

@ -56,6 +56,7 @@ export type IconName =
| 'envelope'
| 'exchange-alt'
| 'exclamation-triangle'
| 'exclamation'
| 'external-link-alt'
| 'eye-slash'
| 'eye'