mirror of
https://github.com/grafana/grafana.git
synced 2025-02-12 00:25:46 -06:00
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:
parent
258578766b
commit
5ce25509a1
@ -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};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
@ -1,9 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { css, cx } from '@emotion/css';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { Icon } from '../Icon/Icon';
|
import { Icon } from '../Icon/Icon';
|
||||||
import { Tooltip } from '../Tooltip/Tooltip';
|
import { Tooltip } from '../Tooltip/Tooltip';
|
||||||
|
import { useStyles } from '../../themes';
|
||||||
|
|
||||||
type LoadingIndicatorProps = {
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type LoadingIndicatorProps = {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
};
|
};
|
||||||
@ -12,6 +17,8 @@ type LoadingIndicatorProps = {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({ onCancel, loading }) => {
|
export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({ onCancel, loading }) => {
|
||||||
|
const styles = useStyles(getStyles);
|
||||||
|
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -19,7 +26,7 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({ onCancel, lo
|
|||||||
return (
|
return (
|
||||||
<Tooltip content="Cancel query">
|
<Tooltip content="Cancel query">
|
||||||
<Icon
|
<Icon
|
||||||
className="spin-clockwise"
|
className={cx('spin-clockwise', { [styles.clickable]: !!onCancel })}
|
||||||
name="sync"
|
name="sync"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
@ -28,3 +35,11 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({ onCancel, lo
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getStyles = () => {
|
||||||
|
return {
|
||||||
|
clickable: css`
|
||||||
|
cursor: pointer;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
import { withCenteredStory, withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||||
import { useInterval } from 'react-use';
|
import { useInterval } from 'react-use';
|
||||||
import { PanelChrome, PanelPadding } from './PanelChrome';
|
import { PanelChrome, PanelPadding } from './PanelChrome';
|
||||||
import { LoadingIndicator } from './LoadingIndicator';
|
import { LoadingIndicator } from './LoadingIndicator';
|
||||||
|
import { ErrorIndicator } from './ErrorIndicator';
|
||||||
import { useTheme } from '../../themes/ThemeContext';
|
import { useTheme } from '../../themes/ThemeContext';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Visualizations/PanelChrome',
|
title: 'Visualizations/PanelChrome',
|
||||||
component: PanelChrome,
|
component: PanelChrome,
|
||||||
decorators: [withCenteredStory],
|
decorators: [withCenteredStory, withHorizontallyCenteredStory],
|
||||||
parameters: {
|
parameters: {
|
||||||
docs: {},
|
docs: {},
|
||||||
},
|
},
|
||||||
argTypes: {
|
argTypes: {
|
||||||
leftItems: {
|
leftItems: {
|
||||||
control: {
|
control: {
|
||||||
type: 'select',
|
type: 'multi-select',
|
||||||
options: ['none', 'loading'],
|
options: ['none', 'loading', 'error'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
width: {
|
width: {
|
||||||
@ -33,18 +34,18 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type PanelChromeStoryProps = {
|
type PanelChromeStoryProps = {
|
||||||
leftItems: string;
|
leftItems: string[];
|
||||||
title: string | undefined;
|
title: string | undefined;
|
||||||
padding: PanelPadding;
|
padding: PanelPadding;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StandardPanel = (props: PanelChromeStoryProps) => {
|
export const StandardPanel = (props: PanelChromeStoryProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { title, padding } = props;
|
|
||||||
const leftItems = mapToItems(props.leftItems);
|
const leftItems = mapToItems(props.leftItems);
|
||||||
|
|
||||||
return (
|
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) => {
|
{(innerWidth, innerHeight) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -62,11 +63,12 @@ export const StandardPanel = (props: PanelChromeStoryProps) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</PanelChrome>
|
</PanelChrome>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
StandardPanel.args = {
|
StandardPanel.args = {
|
||||||
leftItems: 'none',
|
leftItems: ['none'],
|
||||||
title: 'Very long title that should get ellipsis when there is no more space',
|
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)} />;
|
return <LoadingIndicator loading={loading} onCancel={() => setLoading(false)} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapToItems = (selected: string): React.ReactNode[] | undefined => {
|
const mapToItems = (selected: string[]): React.ReactNode[] => {
|
||||||
switch (selected) {
|
return selected.map((s) => {
|
||||||
|
switch (s) {
|
||||||
case 'loading':
|
case 'loading':
|
||||||
return [<LoadingItem key="loading" />];
|
return <LoadingItem key="loading" />;
|
||||||
|
case 'error':
|
||||||
|
return <ErrorIndicator error="Could not find datasource with id: 12345" onClick={() => {}} />;
|
||||||
default:
|
default:
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { CSSProperties, ReactNode } from 'react';
|
import React, { CSSProperties, ReactNode } from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { useStyles, useTheme } from '../../themes';
|
import { useTheme, useStyles } from '../../themes';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -11,7 +11,7 @@ export interface PanelChromeProps {
|
|||||||
height: number;
|
height: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
padding?: PanelPadding;
|
padding?: PanelPadding;
|
||||||
leftItems?: React.ReactNode[];
|
leftItems?: React.ReactNode[]; // rightItems will be added later (actions links etc.)
|
||||||
children: (innerWidth: number, innerHeight: number) => React.ReactNode;
|
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) => {
|
const getStyles = (theme: GrafanaTheme) => {
|
||||||
return {
|
return {
|
||||||
container: css`
|
container: css`
|
||||||
@ -89,32 +114,8 @@ const getStyles = (theme: GrafanaTheme) => {
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
`,
|
`,
|
||||||
leftItems: css`
|
leftItems: css`
|
||||||
|
display: flex;
|
||||||
padding-right: ${theme.panelPadding}px;
|
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 };
|
|
||||||
};
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { LoadingIndicator } from './LoadingIndicator';
|
import { LoadingIndicator } from './LoadingIndicator';
|
||||||
|
import { ErrorIndicator } from './ErrorIndicator';
|
||||||
import { PanelChrome as PanelChromeComponent, PanelChromeProps } from './PanelChrome';
|
import { PanelChrome as PanelChromeComponent, PanelChromeProps } from './PanelChrome';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -12,6 +13,7 @@ export { PanelChromeProps, PanelPadding } from './PanelChrome';
|
|||||||
*/
|
*/
|
||||||
export interface PanelChromeType extends React.FC<PanelChromeProps> {
|
export interface PanelChromeType extends React.FC<PanelChromeProps> {
|
||||||
LoadingIndicator: typeof LoadingIndicator;
|
LoadingIndicator: typeof LoadingIndicator;
|
||||||
|
ErrorIndicator: typeof ErrorIndicator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,3 +21,18 @@ export interface PanelChromeType extends React.FC<PanelChromeProps> {
|
|||||||
*/
|
*/
|
||||||
export const PanelChrome = PanelChromeComponent as PanelChromeType;
|
export const PanelChrome = PanelChromeComponent as PanelChromeType;
|
||||||
PanelChrome.LoadingIndicator = LoadingIndicator;
|
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';
|
||||||
|
@ -77,7 +77,16 @@ 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 {
|
||||||
|
PanelChrome,
|
||||||
|
PanelChromeProps,
|
||||||
|
PanelPadding,
|
||||||
|
PanelChromeType,
|
||||||
|
PanelChromeLoadingIndicator,
|
||||||
|
PanelChromeLoadingIndicatorProps,
|
||||||
|
PanelChromeErrorIndicator,
|
||||||
|
PanelChromeErrorIndicatorProps,
|
||||||
|
} from './PanelChrome';
|
||||||
export { VizLayout, VizLayoutComponentType, VizLayoutLegendProps, VizLayoutProps } from './VizLayout/VizLayout';
|
export { VizLayout, VizLayoutComponentType, VizLayoutLegendProps, VizLayoutProps } from './VizLayout/VizLayout';
|
||||||
export { VizLegendItem } from './VizLegend/types';
|
export { VizLegendItem } from './VizLegend/types';
|
||||||
export { LegendPlacement, LegendDisplayMode, VizLegendOptions } from './VizLegend/models.gen';
|
export { LegendPlacement, LegendDisplayMode, VizLegendOptions } from './VizLegend/models.gen';
|
||||||
|
@ -56,6 +56,7 @@ export type IconName =
|
|||||||
| 'envelope'
|
| 'envelope'
|
||||||
| 'exchange-alt'
|
| 'exchange-alt'
|
||||||
| 'exclamation-triangle'
|
| 'exclamation-triangle'
|
||||||
|
| 'exclamation'
|
||||||
| 'external-link-alt'
|
| 'external-link-alt'
|
||||||
| 'eye-slash'
|
| 'eye-slash'
|
||||||
| 'eye'
|
| 'eye'
|
||||||
|
Loading…
Reference in New Issue
Block a user