Skip to content

Commit

Permalink
feat: allow no results component, don't require series (#936)
Browse files Browse the repository at this point in the history
  • Loading branch information
nickofthyme authored Dec 4, 2020
1 parent 94534a5 commit 4766c23
Show file tree
Hide file tree
Showing 14 changed files with 195 additions and 35 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
{
Expand Down
6 changes: 3 additions & 3 deletions api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1599,6 +1600,7 @@ export interface SettingsSpec extends Spec {
legendMaxDepth?: number;
legendPosition: Position;
minBrushDelta?: number;
noResults?: ComponentType | ReactChild;
// (undocumented)
onBrushEnd?: BrushEndListener;
// (undocumented)
Expand Down Expand Up @@ -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<Omit<SettingsSpec, 'chartType' | 'specType' | 'id' | 'externalPointerEvents'>> & {
externalPointerEvents?: RecursivePartial<SettingsSpec['externalPointerEvents']>;
};
export type SettingsSpecProps = Partial<Omit<SettingsSpec, 'chartType' | 'specType' | 'id'>>;

// Warning: (ae-missing-release-tag) "SharedGeometryStateStyle" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
Expand Down
6 changes: 4 additions & 2 deletions integration/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ function encodeString(string: string) {
const storiesToSkip: Record<string, string[]> = {
// Interactions: ['Some story name'],
Legend: ['Actions'],
'Test Cases': ['No Series'],
};

/**
Expand Down Expand Up @@ -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);
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 39 additions & 0 deletions integration/tests/test_cases_stories.test.ts
Original file line number Diff line number Diff line change
@@ -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' },
);
});
});
2 changes: 1 addition & 1 deletion src/components/_unavailable_chart.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.echReactiveChart_unavailable {
.echReactiveChart_noResults {
display: flex;
align-items: center;
justify-content: center;
Expand Down
1 change: 1 addition & 0 deletions src/components/chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ export class Chart extends React.Component<ChartProps, ChartState> {
<ChartStatus />
<ChartResizer />
<Legend />
{/* TODO: Add renderFn to error boundary */}
<ErrorBoundary>
<SpecsParser>{this.props.children}</SpecsParser>
<div className="echContainer">
Expand Down
41 changes: 24 additions & 17 deletions src/components/chart_container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: (
Expand Down Expand Up @@ -169,21 +171,22 @@ class ChartContainerComponent extends React.Component<ReactiveChartProps> {
};

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 (
<div className="echReactiveChart_unavailable">
<p>No data to display</p>
</div>
);

if (
status === InitStatus.ChartNotInitialized ||
status === InitStatus.MissingChartType ||
status === InitStatus.SpecNotInitialized ||
isChartEmpty
) {
return <NoResults renderFn={settings?.noResults} />;
}

const { pointerCursor, internalChartRenderer, getChartContainerRef, forwardStageRef } = this.props;
return (
<div
Expand Down Expand Up @@ -214,26 +217,30 @@ const mapDispatchToProps = (dispatch: Dispatch): ChartContainerComponentDispatch
);
const mapStateToProps = (state: GlobalChartState): ChartContainerComponentStateProps => {
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,
};
};

Expand Down
9 changes: 4 additions & 5 deletions src/components/error_boundary/error_boundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -51,11 +54,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt

render() {
if (this.hasError) {
return (
<div className="echReactiveChart_unavailable">
<p>No data to display</p>
</div>
);
return <NoResults renderFn={this.props.renderFn} />;
}

return this.props.children;
Expand Down
32 changes: 32 additions & 0 deletions src/components/no_results.tsx
Original file line number Diff line number Diff line change
@@ -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<NoResultsProps> = ({ renderFn }) => (
<Suspense fallback={() => null}>
<div className="echReactiveChart_noResults">{renderFn ?? <p>No data to display</p>}</div>
</Suspense>
);
14 changes: 7 additions & 7 deletions src/specs/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -424,6 +424,10 @@ export interface SettingsSpec extends Spec {
* Orders ordinal x values
*/
orderOrdinalBinsBy?: OrderBy;
/**
* Render component for no results UI
*/
noResults?: ComponentType | ReactChild;
}

/**
Expand Down Expand Up @@ -454,11 +458,7 @@ export type DefaultSettingsProps =
| 'minBrushDelta'
| 'externalPointerEvents';

export type SettingsSpecProps = Partial<
Omit<SettingsSpec, 'chartType' | 'specType' | 'id' | 'externalPointerEvents'>
> & {
externalPointerEvents?: RecursivePartial<SettingsSpec['externalPointerEvents']>;
};
export type SettingsSpecProps = Partial<Omit<SettingsSpec, 'chartType' | 'specType' | 'id'>>;

export const Settings: React.FunctionComponent<SettingsSpecProps> = getConnect()(
specComponentFactory<SettingsSpec, DefaultSettingsProps>(DEFAULT_SETTINGS_SPEC),
Expand Down
50 changes: 50 additions & 0 deletions stories/test_cases/1_no_series.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<EuiFlexItem>
<EuiFlexGroup direction="column" alignItems="center" justifyContent="center">
<EuiIcon type="visualizeApp" />
<EuiSpacer size="s" />
<p>{msg}</p>
</EuiFlexGroup>
</EuiFlexItem>
);

/**
* 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 (
<Chart className="story-chart">
<Axis id="count" title="count" position={Position.Left} />
<Axis id="x" title="goods" position={Position.Bottom} />
<Settings noResults={customNoResults ? <NoResults msg={noResultsMsg} /> : undefined} />
</Chart>
);
};
29 changes: 29 additions & 0 deletions stories/test_cases/test_cases.stories.tsx
Original file line number Diff line number Diff line change
@@ -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';

0 comments on commit 4766c23

Please sign in to comment.