From 84c03a9f8516e9fb846e25a920aa82b00c660a02 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 20 Jun 2023 17:58:32 +0700 Subject: [PATCH] feat: add renderError property (#175) * fix(main): reset "No data" error on change data * feat(main): Add renderErrorView prop for custom render and handle errors --- src/components/ChartKit.tsx | 2 +- .../ErrorBoundary/ErrorBoundary.tsx | 34 +++++++- src/components/ErrorView/ErrorView.tsx | 14 ---- .../__stories__/components/ChartStory.tsx | 27 +++++-- .../custom-error-render.stories.tsx | 73 +++++++++++++++++ .../__stories__/no-data/no-data.stories.tsx | 30 +++++++ .../highcharts/mocks/custom-error-render.ts | 80 +++++++++++++++++++ src/plugins/highcharts/mocks/no-data.ts | 80 +++++++++++++++++++ src/types/index.ts | 11 ++- src/utils/getErrorMessage.ts | 7 ++ 10 files changed, 333 insertions(+), 25 deletions(-) delete mode 100644 src/components/ErrorView/ErrorView.tsx create mode 100644 src/plugins/highcharts/__stories__/custom-error-render/custom-error-render.stories.tsx create mode 100644 src/plugins/highcharts/__stories__/no-data/no-data.stories.tsx create mode 100644 src/plugins/highcharts/mocks/custom-error-render.ts create mode 100644 src/plugins/highcharts/mocks/no-data.ts create mode 100644 src/utils/getErrorMessage.ts diff --git a/src/components/ChartKit.tsx b/src/components/ChartKit.tsx index ad4b07e8..9cb6c326 100644 --- a/src/components/ChartKit.tsx +++ b/src/components/ChartKit.tsx @@ -61,7 +61,7 @@ const ChartKitComponentWithErrorBoundary = React.forwardRef< ChartKitProps >(function ChartKitComponentWithErrorBoundary(props, ref) { return ( - + ); diff --git a/src/components/ErrorBoundary/ErrorBoundary.tsx b/src/components/ErrorBoundary/ErrorBoundary.tsx index bb2c5822..32143de4 100644 --- a/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -1,10 +1,13 @@ import React from 'react'; import type {ChartKitError} from '../../libs'; -import type {ChartKitOnError} from '../../types'; -import {ErrorView} from '../ErrorView/ErrorView'; +import type {ChartKitOnError, ChartKitType, ChartKitWidget, RenderError} from '../../types'; +import {getErrorMessage} from '../../utils/getErrorMessage'; +import {CHARTKIT_ERROR_CODE} from '../../libs'; type Props = { onError?: ChartKitOnError; + data: ChartKitWidget[ChartKitType]['data']; + renderError?: RenderError; }; type State = { @@ -28,13 +31,38 @@ export class ErrorBoundary extends React.Component { } } + componentDidUpdate(prevProps: Readonly) { + if (prevProps.data !== this.props.data) { + const {error} = this.state; + if (error && 'code' in error && error.code === CHARTKIT_ERROR_CODE.NO_DATA) { + this.resetError(); + } + } + } + render() { const {error} = this.state; if (error) { - return ; + const message = getErrorMessage(error); + + if (this.props.renderError) { + return this.props.renderError({ + error, + message, + resetError: this.resetError, + }); + } + + return
{message}
; } return this.props.children; } + + resetError = () => { + if (this.state.error) { + this.setState({error: undefined}); + } + }; } diff --git a/src/components/ErrorView/ErrorView.tsx b/src/components/ErrorView/ErrorView.tsx deleted file mode 100644 index 37f62bd7..00000000 --- a/src/components/ErrorView/ErrorView.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import {i18n} from '../../i18n'; -import type {ChartKitError} from '../../libs'; - -type Props = { - error: ChartKitError | Error; -}; - -export const ErrorView = ({error}: Props) => { - const code = 'code' in error && error.code; - const message = error.message || code || i18n('error', 'label_unknown-error'); - - return
{message}
; -}; diff --git a/src/plugins/highcharts/__stories__/components/ChartStory.tsx b/src/plugins/highcharts/__stories__/components/ChartStory.tsx index af7ad3a0..505cde9e 100644 --- a/src/plugins/highcharts/__stories__/components/ChartStory.tsx +++ b/src/plugins/highcharts/__stories__/components/ChartStory.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {Button} from '@gravity-ui/uikit'; -import {ChartKitRef} from '../../../../types'; +import {ChartKitRef, RenderError} from '../../../../types'; import {settings} from '../../../../libs'; import {HighchartsPlugin} from '../../index'; import holidays from '../../mocks/holidays'; @@ -11,19 +11,29 @@ const DEFAULT_STORY_HEIGHT = '300px'; const DEFAULT_STORY_WIDTH = '100%'; export type ChartStoryProps = { + data: HighchartsWidgetData; + + withoutPlugin?: boolean; + visible?: boolean; height?: string; width?: string; - - data: HighchartsWidgetData; + renderError?: RenderError; }; export const ChartStory: React.FC = (props: ChartStoryProps) => { const {height, width, data} = props; - const [visible, setVisible] = React.useState(false); + const initRef = React.useRef(false); + const [visible, setVisible] = React.useState(Boolean(props.visible)); const chartKitRef = React.useRef(); + if (!initRef.current) { + if (!props.withoutPlugin) { + settings.set({plugins: [HighchartsPlugin], extra: {holidays}}); + } + initRef.current = true; + } + if (!visible) { - settings.set({plugins: [HighchartsPlugin], extra: {holidays}}); return ; } @@ -34,7 +44,12 @@ export const ChartStory: React.FC = (props: ChartStoryProps) => width: width || DEFAULT_STORY_WIDTH, }} > - + ); }; diff --git a/src/plugins/highcharts/__stories__/custom-error-render/custom-error-render.stories.tsx b/src/plugins/highcharts/__stories__/custom-error-render/custom-error-render.stories.tsx new file mode 100644 index 00000000..40db4b83 --- /dev/null +++ b/src/plugins/highcharts/__stories__/custom-error-render/custom-error-render.stories.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import {Meta, Story} from '@storybook/react'; +import {ChartKit} from '../../../../components/ChartKit'; +import {ChartStory} from '../components/ChartStory'; +import {Button} from '@gravity-ui/uikit'; +import {CHARTKIT_ERROR_CODE, settings} from '../../../../libs'; +import {HighchartsPlugin} from '../../index'; +import holidays from '../../mocks/holidays'; +import {noData, filledData} from '../../mocks/custom-error-render'; +import {RenderError} from '../../../../types'; + +export default { + title: 'Plugins/Highcharts/CustomErrorRender', + component: ChartKit, +} as Meta; + +const Template: Story = () => { + const [data, setData] = React.useState(noData); + + const renderErrorView: RenderError = React.useCallback(({error, message, resetError}) => { + function renderFixButton() { + if (!('code' in error)) { + return null; + } + + switch (error.code) { + case CHARTKIT_ERROR_CODE.UNKNOWN_PLUGIN: + return ( + + ); + case CHARTKIT_ERROR_CODE.NO_DATA: + return ( + + ); + default: + return null; + } + } + + return ( +
+

{message}

+ {renderFixButton()} +
+ ); + }, []); + + return ( +
+ +
+ ); +}; + +export const CustomErrorRender = Template.bind({}); diff --git a/src/plugins/highcharts/__stories__/no-data/no-data.stories.tsx b/src/plugins/highcharts/__stories__/no-data/no-data.stories.tsx new file mode 100644 index 00000000..39e8e72c --- /dev/null +++ b/src/plugins/highcharts/__stories__/no-data/no-data.stories.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import {Meta, Story} from '@storybook/react'; +import {ChartKit} from '../../../../components/ChartKit'; +import {noData, filledData} from '../../mocks/no-data'; +import {ChartStory} from '../components/ChartStory'; +import {Button} from '@gravity-ui/uikit'; + +export default { + title: 'Plugins/Highcharts/NoData', + component: ChartKit, +} as Meta; + +const Template: Story = () => { + const [data, setData] = React.useState(noData); + + const handleUpdateData = React.useCallback(() => { + setData(filledData); + }, []); + + return ( +
+
+ +
+ +
+ ); +}; + +export const NoData = Template.bind({}); diff --git a/src/plugins/highcharts/mocks/custom-error-render.ts b/src/plugins/highcharts/mocks/custom-error-render.ts new file mode 100644 index 00000000..1e4185c0 --- /dev/null +++ b/src/plugins/highcharts/mocks/custom-error-render.ts @@ -0,0 +1,80 @@ +import type {HighchartsWidgetData} from '../types'; +import _ from 'lodash'; + +const baseData: Omit = { + config: { + hideHolidays: false, + normalizeDiv: false, + normalizeSub: false, + }, + libraryConfig: { + chart: { + type: 'arearange', + }, + title: { + text: 'Temperature variation by day', + }, + xAxis: { + type: 'datetime', + }, + tooltip: { + valueSuffix: '°C', + }, + }, +}; + +export const noData: HighchartsWidgetData = { + ...baseData, + data: { + graphs: [ + { + name: 'Temperatures', + data: [], + }, + ], + }, +}; + +export const filledData: HighchartsWidgetData = { + ...baseData, + data: { + graphs: [ + { + name: 'Temperatures', + data: [ + [1246406400000, 10.4, 17], + [1246492800000, 10.3, 28.6], + [1246579200000, 14.8, 18.4], + [1246665600000, 11.5, 25.8], + [1246752000000, 11.1, 24.4], + [1246838400000, 17.7, 19.6], + [1246924800000, 15.1, 18.1], + [1247011200000, 15.1, 27.2], + [1247097600000, 17, 17.5], + [1247184000000, 12.6, 18.5], + [1247270400000, 12.2, 26], + [1247356800000, 15.9, 22.9], + [1247443200000, 17.1, 18.1], + [1247529600000, 13.3, 24.2], + [1247616000000, 17, 28.1], + [1247702400000, 16.2, 22.6], + [1247788800000, 10.6, 19], + [1247875200000, 11.3, 19.7], + [1247961600000, 14.1, 24.6], + [1248048000000, 14.2, 22.5], + [1248134400000, 14.1, 28.5], + [1248220800000, 14, 27], + [1248307200000, 10.2, 20.6], + [1248393600000, 13.1, 29.9], + [1248480000000, 13.7, 21.1], + [1248566400000, 15, 28.6], + [1248652800000, 12, 17.5], + [1248739200000, 17.8, 24.4], + [1248825600000, 11.7, 25.9], + [1248912000000, 13.6, 25.6], + [1248998400000, 17.3, 22.2], + ], + }, + ], + }, +}; diff --git a/src/plugins/highcharts/mocks/no-data.ts b/src/plugins/highcharts/mocks/no-data.ts new file mode 100644 index 00000000..1e4185c0 --- /dev/null +++ b/src/plugins/highcharts/mocks/no-data.ts @@ -0,0 +1,80 @@ +import type {HighchartsWidgetData} from '../types'; +import _ from 'lodash'; + +const baseData: Omit = { + config: { + hideHolidays: false, + normalizeDiv: false, + normalizeSub: false, + }, + libraryConfig: { + chart: { + type: 'arearange', + }, + title: { + text: 'Temperature variation by day', + }, + xAxis: { + type: 'datetime', + }, + tooltip: { + valueSuffix: '°C', + }, + }, +}; + +export const noData: HighchartsWidgetData = { + ...baseData, + data: { + graphs: [ + { + name: 'Temperatures', + data: [], + }, + ], + }, +}; + +export const filledData: HighchartsWidgetData = { + ...baseData, + data: { + graphs: [ + { + name: 'Temperatures', + data: [ + [1246406400000, 10.4, 17], + [1246492800000, 10.3, 28.6], + [1246579200000, 14.8, 18.4], + [1246665600000, 11.5, 25.8], + [1246752000000, 11.1, 24.4], + [1246838400000, 17.7, 19.6], + [1246924800000, 15.1, 18.1], + [1247011200000, 15.1, 27.2], + [1247097600000, 17, 17.5], + [1247184000000, 12.6, 18.5], + [1247270400000, 12.2, 26], + [1247356800000, 15.9, 22.9], + [1247443200000, 17.1, 18.1], + [1247529600000, 13.3, 24.2], + [1247616000000, 17, 28.1], + [1247702400000, 16.2, 22.6], + [1247788800000, 10.6, 19], + [1247875200000, 11.3, 19.7], + [1247961600000, 14.1, 24.6], + [1248048000000, 14.2, 22.5], + [1248134400000, 14.1, 28.5], + [1248220800000, 14, 27], + [1248307200000, 10.2, 20.6], + [1248393600000, 13.1, 29.9], + [1248480000000, 13.7, 21.1], + [1248566400000, 15, 28.6], + [1248652800000, 12, 17.5], + [1248739200000, 17.8, 24.4], + [1248825600000, 11.7, 25.9], + [1248912000000, 13.6, 25.6], + [1248998400000, 17.3, 22.2], + ], + }, + ], + }, +}; diff --git a/src/types/index.ts b/src/types/index.ts index 0e928bdc..754197fd 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,7 @@ import React from 'react'; import type {ChartKitWidget} from './widget'; +import {ChartKitError} from '../libs'; export type {ChartKitHolidays} from './misc'; @@ -47,7 +48,7 @@ export type ChartKitProps = { * @param data */ onChartLoad?: (data: ChartKitOnChartLoad) => void; - + renderError?: RenderError; onError?: ChartKitOnError; } & {[key in keyof Omit]: ChartKitWidget[T][key]}; @@ -56,4 +57,12 @@ export type ChartKitPlugin = { renderer: React.LazyExoticComponent; }; +export type RenderErrorOpts = { + message: string; + error: ChartKitError | Error; + resetError: () => void; +}; + +export type RenderError = (opts: RenderErrorOpts) => React.ReactNode; + export type {ChartKitWidget}; diff --git a/src/utils/getErrorMessage.ts b/src/utils/getErrorMessage.ts new file mode 100644 index 00000000..f1ec644c --- /dev/null +++ b/src/utils/getErrorMessage.ts @@ -0,0 +1,7 @@ +import type {ChartKitError} from '../libs'; +import {i18n} from '../i18n'; + +export function getErrorMessage(error: ChartKitError | Error) { + const code = 'code' in error && error.code; + return (error.message || code || i18n('error', 'label_unknown-error')).toString(); +}