Skip to content

Commit

Permalink
Add SuperChart (apache#68)
Browse files Browse the repository at this point in the history
* Add superchart and convert to ts

* fix compilation error

* fix build issue

* unit test working

* Add default width and height to ChartProps

* export SuperChart

* Add null check and update unit tests

* update unit tests

* complete test coverage

* Add generic

* reduce timeout
  • Loading branch information
kristw authored and xtinec committed Dec 20, 2018
1 parent 556a508 commit 9c48747
Show file tree
Hide file tree
Showing 9 changed files with 458 additions and 32 deletions.
222 changes: 222 additions & 0 deletions packages/superset-ui-chart/src/components/SuperChart.tsx
@@ -0,0 +1,222 @@
import * as React from 'react';
import { createSelector } from 'reselect';
import getChartComponentRegistry from '../registries/ChartComponentRegistrySingleton';
import getChartTransformPropsRegistry from '../registries/ChartTransformPropsRegistrySingleton';
import ChartProps from '../models/ChartProps';
import createLoadableRenderer, { LoadableRenderer } from './createLoadableRenderer';

const IDENTITY = (x: any) => x;

const EMPTY = () => null;

/* eslint-disable sort-keys */
const defaultProps = {
id: '',
className: '',
preTransformProps: IDENTITY,
overrideTransformProps: undefined,
postTransformProps: IDENTITY,
onRenderSuccess() {},
onRenderFailure() {},
};
/* eslint-enable sort-keys */

type TransformFunction<Input = PlainProps, Output = PlainProps> = (x: Input) => Output;
type HandlerFunction = (...args: any[]) => void;

interface LoadingProps {
error: any;
}

interface PlainProps {
[key: string]: any;
}

interface LoadedModules {
Chart: React.Component | { default: React.Component };
transformProps: TransformFunction | { default: TransformFunction };
}

interface RenderProps {
chartProps: ChartProps;
preTransformProps?: TransformFunction<ChartProps>;
postTransformProps?: TransformFunction;
}

const BLANK_CHART_PROPS = new ChartProps();

export interface SuperChartProps {
id?: string;
className?: string;
chartProps?: ChartProps | null;
chartType: string;
preTransformProps?: TransformFunction<ChartProps>;
overrideTransformProps?: TransformFunction;
postTransformProps?: TransformFunction;
onRenderSuccess?: HandlerFunction;
onRenderFailure?: HandlerFunction;
}

function getModule<T>(value: any): T {
return (value.default ? value.default : value) as T;
}

export default class SuperChart extends React.PureComponent<SuperChartProps, {}> {
static defaultProps = defaultProps;

constructor(props: SuperChartProps) {
super(props);

this.renderChart = this.renderChart.bind(this);
this.renderLoading = this.renderLoading.bind(this);

// memoized function so it will not recompute
// and return previous value
// unless one of
// - preTransformProps
// - transformProps
// - postTransformProps
// - chartProps
// is changed.
this.processChartProps = createSelector(
input => input.preTransformProps,
input => input.transformProps,
input => input.postTransformProps,
input => input.chartProps,
(pre = IDENTITY, transform = IDENTITY, post = IDENTITY, chartProps) =>
post(transform(pre(chartProps))),
);

const componentRegistry = getChartComponentRegistry();
const transformPropsRegistry = getChartTransformPropsRegistry();

// memoized function so it will not recompute
// and return previous value
// unless one of
// - chartType
// - overrideTransformProps
// is changed.
this.createLoadableRenderer = createSelector(
input => input.chartType,
input => input.overrideTransformProps,
(chartType, overrideTransformProps) => {
if (chartType) {
const Renderer = createLoadableRenderer({
loader: {
Chart: () => componentRegistry.getAsPromise(chartType),
transformProps: overrideTransformProps
? () => Promise.resolve(overrideTransformProps)
: () => transformPropsRegistry.getAsPromise(chartType),
},
loading: (loadingProps: LoadingProps) => this.renderLoading(loadingProps, chartType),
render: this.renderChart,
});

// Trigger preloading.
Renderer.preload();

return Renderer;
}

return EMPTY;
},
);
}

processChartProps: (
input: {
chartProps: ChartProps;
preTransformProps?: TransformFunction<ChartProps>;
transformProps?: TransformFunction;
postTransformProps?: TransformFunction;
},
) => any;

createLoadableRenderer: (
input: {
chartType: string;
overrideTransformProps?: TransformFunction;
},
) => LoadableRenderer<RenderProps, LoadedModules> | (() => null);

renderChart(loaded: LoadedModules, props: RenderProps) {
const Chart = getModule<typeof React.Component>(loaded.Chart);
const transformProps = getModule<TransformFunction>(loaded.transformProps);
const { chartProps, preTransformProps, postTransformProps } = props;

return (
<Chart
{...this.processChartProps({
/* eslint-disable sort-keys */
chartProps,
preTransformProps,
transformProps,
postTransformProps,
/* eslint-enable sort-keys */
})}
/>
);
}

renderLoading(loadingProps: LoadingProps, chartType: string) {
const { error } = loadingProps;

if (error) {
return (
<div className="alert alert-warning" role="alert">
<strong>ERROR</strong>&nbsp;
<code>chartType=&quot;{chartType}&quot;</code> &mdash;
{error.toString()}
</div>
);
}

return null;
}

render() {
const {
id,
className,
preTransformProps,
postTransformProps,
chartProps = BLANK_CHART_PROPS,
onRenderSuccess,
onRenderFailure,
} = this.props;

// Create LoadableRenderer and start preloading
// the lazy-loaded Chart components
const Renderer = this.createLoadableRenderer(this.props);

// Do not render if chartProps is set to null.
// but the pre-loading has been started in this.createLoadableRenderer
// to prepare for rendering once chartProps becomes available.
if (chartProps === null) {
return null;
}

const containerProps: {
id?: string;
className?: string;
} = {};
if (id) {
containerProps.id = id;
}
if (className) {
containerProps.className = className;
}

return (
<div {...containerProps}>
<Renderer
preTransformProps={preTransformProps}
postTransformProps={postTransformProps}
chartProps={chartProps}
onRenderSuccess={onRenderSuccess}
onRenderFailure={onRenderFailure}
/>
</div>
);
}
}
7 changes: 4 additions & 3 deletions packages/superset-ui-chart/src/index.ts
@@ -1,15 +1,16 @@
export { ChartClient, ChartClientConfig } from './clients/ChartClient';
export { ChartMetadata, ChartMetadataConfig } from './models/ChartMetadata';
export { default as ChartMetadata, ChartMetadataConfig } from './models/ChartMetadata';
export {
ChartPlugin,
default as ChartPlugin,
ChartPluginConfig,
BuildQueryFunction,
TransformPropsFunction,
} from './models/ChartPlugin';
export { ChartProps, ChartPropsConfig } from './models/ChartProps';
export { default as ChartProps, ChartPropsConfig } from './models/ChartProps';

export { default as createLoadableRenderer } from './components/createLoadableRenderer';
export { default as reactify } from './components/reactify';
export { default as SuperChart } from './components/SuperChart';

export {
default as getChartBuildQueryRegistry,
Expand Down
2 changes: 1 addition & 1 deletion packages/superset-ui-chart/src/models/ChartMetadata.ts
Expand Up @@ -12,7 +12,7 @@ export interface ChartMetadataConfig {
thumbnail: string;
}

export class ChartMetadata {
export default class ChartMetadata {
name: string;
credits: Array<string>;
description: string;
Expand Down
16 changes: 9 additions & 7 deletions packages/superset-ui-chart/src/models/ChartPlugin.ts
@@ -1,6 +1,6 @@
import { isRequired, Plugin } from '@superset-ui/core';
import { ChartMetadata } from './ChartMetadata';
import { ChartProps } from './ChartProps';
import ChartMetadata from './ChartMetadata';
import ChartProps from './ChartProps';
import { FormData } from '../query/FormData';
import { QueryContext } from '../query/buildQueryContext';
import getChartMetadataRegistry from '../registries/ChartMetadataRegistrySingleton';
Expand All @@ -11,7 +11,7 @@ import getChartTransformPropsRegistry from '../registries/ChartTransformPropsReg
const IDENTITY = (x: any) => x;

type PromiseOrValue<T> = Promise<T> | T;
type PromiseOrValueLoader<T> = () => PromiseOrValue<T>;
type PromiseOrValueLoader<T> = () => PromiseOrValue<T> | PromiseOrValue<{ default: T }>;

export type BuildQueryFunction = (formData: FormData) => QueryContext;

Expand All @@ -37,7 +37,7 @@ export interface ChartPluginConfig {
loadChart?: PromiseOrValueLoader<Function>;
}

export class ChartPlugin extends Plugin {
export default class ChartPlugin extends Plugin {
metadata: ChartMetadata;
loadBuildQuery?: PromiseOrValueLoader<BuildQueryFunction>;
loadTransformProps: PromiseOrValueLoader<TransformPropsFunction>;
Expand Down Expand Up @@ -67,7 +67,7 @@ export class ChartPlugin extends Plugin {
}
}

register(): ChartPlugin {
register() {
const { key = isRequired('config.key') } = this.config;
getChartMetadataRegistry().registerValue(key, this.metadata);
getChartBuildQueryRegistry().registerLoader(key, this.loadBuildQuery);
Expand All @@ -77,7 +77,9 @@ export class ChartPlugin extends Plugin {
return this;
}

configure(config: { [key: string]: any }): ChartPlugin {
return super.configure(config);
configure(config: { [key: string]: any }, replace?: boolean) {
super.configure(config, replace);

return this;
}
}
54 changes: 35 additions & 19 deletions packages/superset-ui-chart/src/models/ChartProps.ts
Expand Up @@ -19,19 +19,22 @@ export interface ChartPropsConfig {
annotationData?: AnnotationData;
datasource?: SnakeCaseDatasource;
filters?: Filters;
formData: SnakeCaseFormData;
height: number;
formData?: SnakeCaseFormData;
height?: number;
onAddFilter?: HandlerFunction;
onError?: HandlerFunction;
payload: QueryData;
payload?: QueryData;
setControlValue?: HandlerFunction;
setTooltip?: HandlerFunction;
width: number;
width?: number;
}

function NOOP() {}

export class ChartProps {
const DEFAULT_WIDTH = 800;
const DEFAULT_HEIGHT = 600;

export default class ChartProps {
static createSelector: () => ChartPropsSelector;

annotationData: AnnotationData;
Expand All @@ -48,20 +51,33 @@ export class ChartProps {
setTooltip: HandlerFunction;
width: number;

constructor(config: ChartPropsConfig) {
this.width = config.width;
this.height = config.height;
this.annotationData = config.annotationData || {};
this.datasource = convertKeysToCamelCase(config.datasource);
this.rawDatasource = config.datasource || {};
this.filters = config.filters || [];
this.formData = convertKeysToCamelCase(config.formData);
this.rawFormData = config.formData;
this.onAddFilter = config.onAddFilter || NOOP;
this.onError = config.onError || NOOP;
this.payload = config.payload;
this.setControlValue = config.setControlValue || NOOP;
this.setTooltip = config.setTooltip || NOOP;
constructor(config: ChartPropsConfig = {}) {
const {
annotationData = {},
datasource = {},
filters = [],
formData = {},
onAddFilter = NOOP,
onError = NOOP,
payload = {},
setControlValue = NOOP,
setTooltip = NOOP,
width = DEFAULT_WIDTH,
height = DEFAULT_HEIGHT,
} = config;
this.width = width;
this.height = height;
this.annotationData = annotationData;
this.datasource = convertKeysToCamelCase(datasource);
this.rawDatasource = datasource;
this.filters = filters;
this.formData = convertKeysToCamelCase(formData);
this.rawFormData = formData;
this.onAddFilter = onAddFilter;
this.onError = onError;
this.payload = payload;
this.setControlValue = setControlValue;
this.setTooltip = setTooltip;
}
}

Expand Down
13 changes: 13 additions & 0 deletions packages/superset-ui-chart/test/components/SuperChart.test.jsx
@@ -0,0 +1,13 @@
import React from 'react';
import { shallow } from 'enzyme';
import { SuperChart } from '../../src';

describe('SuperChart', () => {
it('does not render if chartType is not set', done => {
const wrapper = shallow(<SuperChart />);
setTimeout(() => {
expect(wrapper.render().children()).toHaveLength(0);
done();
}, 5);
});
});

0 comments on commit 9c48747

Please sign in to comment.