From 5ce25509a12f75e9b5fb4639ded4abde5016bba0 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Mon, 12 Apr 2021 16:50:49 +0200 Subject: [PATCH] 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. --- .../components/PanelChrome/ErrorIndicator.tsx | 47 +++++++++++++ .../PanelChrome/LoadingIndicator.tsx | 19 ++++- .../PanelChrome/PanelChrome.story.tsx | 70 ++++++++++--------- .../components/PanelChrome/PanelChrome.tsx | 55 ++++++++------- .../src/components/PanelChrome/index.ts | 17 +++++ packages/grafana-ui/src/components/index.ts | 11 ++- packages/grafana-ui/src/types/icon.ts | 1 + 7 files changed, 158 insertions(+), 62 deletions(-) create mode 100644 packages/grafana-ui/src/components/PanelChrome/ErrorIndicator.tsx diff --git a/packages/grafana-ui/src/components/PanelChrome/ErrorIndicator.tsx b/packages/grafana-ui/src/components/PanelChrome/ErrorIndicator.tsx new file mode 100644 index 00000000000..44a74d1d37e --- /dev/null +++ b/packages/grafana-ui/src/components/PanelChrome/ErrorIndicator.tsx @@ -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 = ({ error, onClick }) => { + const styles = useStyles(getStyles); + + if (!error) { + return null; + } + + return ( + + + + ); +}; + +const getStyles = (theme: GrafanaTheme) => { + return { + clickable: css` + cursor: pointer; + `, + icon: css` + color: ${theme.palette.red88}; + `, + }; +}; diff --git a/packages/grafana-ui/src/components/PanelChrome/LoadingIndicator.tsx b/packages/grafana-ui/src/components/PanelChrome/LoadingIndicator.tsx index 3ba2bea7dca..92bd024b6de 100644 --- a/packages/grafana-ui/src/components/PanelChrome/LoadingIndicator.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/LoadingIndicator.tsx @@ -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 = ({ onCancel, loading }) => { + const styles = useStyles(getStyles); + if (!loading) { return null; } @@ -19,7 +26,7 @@ export const LoadingIndicator: React.FC = ({ onCancel, lo return ( = ({ onCancel, lo ); }; + +const getStyles = () => { + return { + clickable: css` + cursor: pointer; + `, + }; +}; diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx index 7b71f84feb3..71341560718 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx @@ -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 ( - - {(innerWidth, innerHeight) => { - return ( -
- ); - }} -
+
+ + {(innerWidth, innerHeight) => { + return ( +
+ ); + }} +
+
); }; 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 setLoading(false)} />; }; -const mapToItems = (selected: string): React.ReactNode[] | undefined => { - switch (selected) { - case 'loading': - return []; - default: - return; - } +const mapToItems = (selected: string[]): React.ReactNode[] => { + return selected.map((s) => { + switch (s) { + case 'loading': + return ; + case 'error': + return {}} />; + default: + return null; + } + }); }; diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index 3541a84ff79..e13dc1a9687 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -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 = ({ ); }; +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 }; -}; diff --git a/packages/grafana-ui/src/components/PanelChrome/index.ts b/packages/grafana-ui/src/components/PanelChrome/index.ts index 738d07347de..52fde79ecfb 100644 --- a/packages/grafana-ui/src/components/PanelChrome/index.ts +++ b/packages/grafana-ui/src/components/PanelChrome/index.ts @@ -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 { LoadingIndicator: typeof LoadingIndicator; + ErrorIndicator: typeof ErrorIndicator; } /** @@ -19,3 +21,18 @@ export interface PanelChromeType extends React.FC { */ 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'; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index bb04c76f3bf..3b742578208 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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'; diff --git a/packages/grafana-ui/src/types/icon.ts b/packages/grafana-ui/src/types/icon.ts index 6f4f430af9c..ab4beeea00f 100644 --- a/packages/grafana-ui/src/types/icon.ts +++ b/packages/grafana-ui/src/types/icon.ts @@ -56,6 +56,7 @@ export type IconName = | 'envelope' | 'exchange-alt' | 'exclamation-triangle' + | 'exclamation' | 'external-link-alt' | 'eye-slash' | 'eye'