mirror of
https://github.com/grafana/grafana.git
synced 2025-02-11 16:15:42 -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 { 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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -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,40 +34,41 @@ 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}>
|
||||
{(innerWidth, innerHeight) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: innerWidth,
|
||||
height: innerHeight,
|
||||
...{
|
||||
background: theme.colors.bg2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}}
|
||||
></div>
|
||||
);
|
||||
}}
|
||||
</PanelChrome>
|
||||
<div style={{ display: 'flex', height: '500px', alignItems: 'center' }}>
|
||||
<PanelChrome {...props} width={400} height={230} leftItems={leftItems}>
|
||||
{(innerWidth, innerHeight) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: innerWidth,
|
||||
height: innerHeight,
|
||||
...{
|
||||
background: theme.colors.bg2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}}
|
||||
></div>
|
||||
);
|
||||
}}
|
||||
</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) {
|
||||
case 'loading':
|
||||
return [<LoadingItem key="loading" />];
|
||||
default:
|
||||
return;
|
||||
}
|
||||
const mapToItems = (selected: string[]): React.ReactNode[] => {
|
||||
return selected.map((s) => {
|
||||
switch (s) {
|
||||
case 'loading':
|
||||
return <LoadingItem key="loading" />;
|
||||
case 'error':
|
||||
return <ErrorIndicator error="Could not find datasource with id: 12345" onClick={() => {}} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -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 };
|
||||
};
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -56,6 +56,7 @@ export type IconName =
|
||||
| 'envelope'
|
||||
| 'exchange-alt'
|
||||
| 'exclamation-triangle'
|
||||
| 'exclamation'
|
||||
| 'external-link-alt'
|
||||
| 'eye-slash'
|
||||
| 'eye'
|
||||
|
Loading…
Reference in New Issue
Block a user