diff --git a/.eslintrc.js b/.eslintrc.js index 3c2fbf6dbc..4e1c82e3c9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -256,6 +256,7 @@ module.exports = { 'unicorn/prefer-node-append': 0, 'unicorn/no-zero-fractions': 0, 'unicorn/prefer-node-remove': 0, // not IE11 compatible + 'unicorn/no-unreadable-array-destructuring': 0, 'unicorn/filename-case': [ 'error', { diff --git a/api/charts.api.md b/api/charts.api.md index d88a2c6e6d..04fede6004 100644 --- a/api/charts.api.md +++ b/api/charts.api.md @@ -7,6 +7,7 @@ import { $Values } from 'utility-types'; import { ComponentType } from 'react'; import React from 'react'; +import { ReactChild } from 'react'; // @public export type Accessor = AccessorObjectKey | AccessorArrayIndex; @@ -1599,6 +1600,7 @@ export interface SettingsSpec extends Spec { legendMaxDepth?: number; legendPosition: Position; minBrushDelta?: number; + noResults?: ComponentType | ReactChild; // (undocumented) onBrushEnd?: BrushEndListener; // (undocumented) @@ -1646,9 +1648,7 @@ export interface SettingsSpec extends Spec { // Warning: (ae-missing-release-tag) "SettingsSpecProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type SettingsSpecProps = Partial> & { - externalPointerEvents?: RecursivePartial; -}; +export type SettingsSpecProps = Partial>; // Warning: (ae-missing-release-tag) "SharedGeometryStateStyle" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/integration/helpers.ts b/integration/helpers.ts index babc5aab93..62362d3c45 100644 --- a/integration/helpers.ts +++ b/integration/helpers.ts @@ -73,6 +73,7 @@ function encodeString(string: string) { const storiesToSkip: Record = { // Interactions: ['Some story name'], Legend: ['Actions'], + 'Test Cases': ['No Series'], }; /** @@ -101,6 +102,7 @@ export function getStorybookInfo(): StoryGroupInfo[] { const encodedGroup = encodeString(group); - return [group, encodedGroup, stories]; - }); + return [group, encodedGroup, stories] as StoryGroupInfo; + }) + .filter(([, , stories]) => stories.length > 0); } diff --git a/integration/tests/__image_snapshots__/test-cases-stories-test-ts-test-cases-stories-should-render-custom-no-results-component-1-snap.png b/integration/tests/__image_snapshots__/test-cases-stories-test-ts-test-cases-stories-should-render-custom-no-results-component-1-snap.png new file mode 100644 index 0000000000..b9d58594c3 Binary files /dev/null and b/integration/tests/__image_snapshots__/test-cases-stories-test-ts-test-cases-stories-should-render-custom-no-results-component-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/test-cases-stories-test-ts-test-cases-stories-should-render-default-no-results-component-1-snap.png b/integration/tests/__image_snapshots__/test-cases-stories-test-ts-test-cases-stories-should-render-default-no-results-component-1-snap.png new file mode 100644 index 0000000000..d4f9f35020 Binary files /dev/null and b/integration/tests/__image_snapshots__/test-cases-stories-test-ts-test-cases-stories-should-render-default-no-results-component-1-snap.png differ diff --git a/integration/tests/test_cases_stories.test.ts b/integration/tests/test_cases_stories.test.ts new file mode 100644 index 0000000000..3813ee7dde --- /dev/null +++ b/integration/tests/test_cases_stories.test.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { common } from '../page_objects'; + +describe('Test cases stories', () => { + it('should render custom no results component', async () => { + await common.expectChartAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/test-cases--no-series&knob-Show custom no results=true', + { + waitSelector: '.echReactiveChart_noResults .euiIcon:not(.euiIcon-isLoading)', + delay: 500, // wait for icon to load + }, + ); + }); + + it('should render default no results component', async () => { + await common.expectChartAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/test-cases--no-series&knob-Show custom no results=false', + { waitSelector: '.echReactiveChart_noResults' }, + ); + }); +}); diff --git a/src/components/_unavailable_chart.scss b/src/components/_unavailable_chart.scss index b86d636310..a64eefad80 100644 --- a/src/components/_unavailable_chart.scss +++ b/src/components/_unavailable_chart.scss @@ -1,4 +1,4 @@ -.echReactiveChart_unavailable { +.echReactiveChart_noResults { display: flex; align-items: center; justify-content: center; diff --git a/src/components/chart.tsx b/src/components/chart.tsx index 5863897c14..578eaa23ba 100644 --- a/src/components/chart.tsx +++ b/src/components/chart.tsx @@ -172,6 +172,7 @@ export class Chart extends React.Component { + {/* TODO: Add renderFn to error boundary */} {this.props.children}
diff --git a/src/components/chart_container.tsx b/src/components/chart_container.tsx index 69cd25a348..ff837dc666 100644 --- a/src/components/chart_container.tsx +++ b/src/components/chart_container.tsx @@ -37,12 +37,14 @@ import { getInternalIsInitializedSelector, InitStatus } from '../state/selectors import { getSettingsSpecSelector } from '../state/selectors/get_settings_specs'; import { isInternalChartEmptySelector } from '../state/selectors/is_chart_empty'; import { deepEqual } from '../utils/fast_deep_equal'; +import { NoResults } from './no_results'; interface ChartContainerComponentStateProps { - initialized: InitStatus; + status: InitStatus; isChartEmpty?: boolean; pointerCursor: string; isBrushing: boolean; + initalized?: boolean; isBrushingAvailable: boolean; settings?: SettingsSpec; internalChartRenderer: ( @@ -169,21 +171,22 @@ class ChartContainerComponent extends React.Component { }; render() { - const { initialized, isChartEmpty } = this.props; - if ( - initialized === InitStatus.ParentSizeInvalid || - initialized === InitStatus.SpecNotInitialized || - initialized === InitStatus.ChartNotInitialized - ) { + const { status, isChartEmpty, settings, initalized } = this.props; + + if (!initalized || status === InitStatus.ParentSizeInvalid) { + // TODO: Display error on chart return null; } - if (initialized === InitStatus.MissingChartType || isChartEmpty === true) { - return ( -
-

No data to display

-
- ); + + if ( + status === InitStatus.ChartNotInitialized || + status === InitStatus.MissingChartType || + status === InitStatus.SpecNotInitialized || + isChartEmpty + ) { + return ; } + const { pointerCursor, internalChartRenderer, getChartContainerRef, forwardStageRef } = this.props; return (
{ const status = getInternalIsInitializedSelector(state); + const settings = getSettingsSpecSelector(state); + const initalized = !state.specParsing && state.specsInitialized; if (status !== InitStatus.Initialized) { return { - initialized: status, - isChartEmpty: undefined, + status, + initalized, pointerCursor: 'default', isBrushingAvailable: false, isBrushing: false, internalChartRenderer: () => null, + settings, }; } return { - initialized: status, + status, + initalized, isChartEmpty: isInternalChartEmptySelector(state), pointerCursor: getInternalPointerCursor(state), isBrushingAvailable: getInternalIsBrushingAvailableSelector(state), isBrushing: getInternalIsBrushingSelector(state), internalChartRenderer: getInternalChartRendererSelector(state), - settings: getSettingsSpecSelector(state), + settings, }; }; diff --git a/src/components/error_boundary/error_boundary.tsx b/src/components/error_boundary/error_boundary.tsx index 878a959fd3..482cef0f35 100644 --- a/src/components/error_boundary/error_boundary.tsx +++ b/src/components/error_boundary/error_boundary.tsx @@ -19,10 +19,13 @@ import React, { Component, ReactNode } from 'react'; +import { SettingsSpecProps } from '../../specs'; +import { NoResults } from '../no_results'; import { isGracefulError } from './errors'; type ErrorBoundaryProps = { children: ReactNode; + renderFn?: SettingsSpecProps['noResults']; }; interface ErrorBoundaryState { @@ -51,11 +54,7 @@ export class ErrorBoundary extends Component -

No data to display

-
- ); + return ; } return this.props.children; diff --git a/src/components/no_results.tsx b/src/components/no_results.tsx new file mode 100644 index 0000000000..1b72052d7f --- /dev/null +++ b/src/components/no_results.tsx @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC, Suspense } from 'react'; + +import { SettingsSpecProps } from '../specs'; + +interface NoResultsProps { + renderFn?: SettingsSpecProps['noResults']; +} + +export const NoResults: FC = ({ renderFn }) => ( + null}> +
{renderFn ??

No data to display

}
+
+); diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index 544a515ea3..527f94269e 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { ComponentType } from 'react'; +import React, { ComponentType, ReactChild } from 'react'; import { Spec } from '.'; import { Cell } from '../chart_types/heatmap/layout/types/viewmodel_types'; @@ -30,7 +30,7 @@ import { CustomTooltip } from '../components/tooltip/types'; import { ScaleContinuousType, ScaleOrdinalType } from '../scales'; import { getConnect, specComponentFactory } from '../state/spec_factory'; import { Accessor } from '../utils/accessor'; -import { Color, Position, RecursivePartial, Rendering, Rotation } from '../utils/commons'; +import { Color, Position, Rendering, Rotation } from '../utils/commons'; import { Domain } from '../utils/domain'; import { GeometryValue } from '../utils/geometry'; import { GroupId } from '../utils/ids'; @@ -424,6 +424,10 @@ export interface SettingsSpec extends Spec { * Orders ordinal x values */ orderOrdinalBinsBy?: OrderBy; + /** + * Render component for no results UI + */ + noResults?: ComponentType | ReactChild; } /** @@ -454,11 +458,7 @@ export type DefaultSettingsProps = | 'minBrushDelta' | 'externalPointerEvents'; -export type SettingsSpecProps = Partial< - Omit -> & { - externalPointerEvents?: RecursivePartial; -}; +export type SettingsSpecProps = Partial>; export const Settings: React.FunctionComponent = getConnect()( specComponentFactory(DEFAULT_SETTINGS_SPEC), diff --git a/stories/test_cases/1_no_series.tsx b/stories/test_cases/1_no_series.tsx new file mode 100644 index 0000000000..bf1d31ef40 --- /dev/null +++ b/stories/test_cases/1_no_series.tsx @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui'; +import { boolean, text } from '@storybook/addon-knobs'; +import React, { FC } from 'react'; + +import { Chart, Settings, Axis, Position } from '../../src'; + +const NoResults: FC<{ msg: string }> = ({ msg }) => ( + + + + +

{msg}

+
+
+); + +/** + * Should render no data value + */ +export const Example = () => { + const customNoResults = boolean('Show custom no results', true); + const noResultsMsg = text('Custom No Results message', 'No Results'); + + return ( + + + + : undefined} /> + + ); +}; diff --git a/stories/test_cases/test_cases.stories.tsx b/stories/test_cases/test_cases.stories.tsx new file mode 100644 index 0000000000..25ab139ab4 --- /dev/null +++ b/stories/test_cases/test_cases.stories.tsx @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SB_SOURCE_PANEL } from '../utils/storybook'; + +export default { + title: 'Test Cases', + parameters: { + options: { selectedPanel: SB_SOURCE_PANEL }, + }, +}; + +export { Example as noSeries } from './1_no_series';