diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/chart/Chart.jsx index a9dd0ff3b7a20..07d041e63a155 100644 --- a/superset/assets/src/chart/Chart.jsx +++ b/superset/assets/src/chart/Chart.jsx @@ -3,12 +3,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Tooltip } from 'react-bootstrap'; +import SuperChart from '../superset-ui-core/chart/components/SuperChart'; import ChartBody from './ChartBody'; import Loading from '../components/Loading'; import { Logger, LOG_ACTIONS_RENDER_CHART } from '../logger'; import StackTraceMessage from '../components/StackTraceMessage'; import RefreshChartOverlay from '../components/RefreshChartOverlay'; -import visPromiseLookup from '../visualizations'; +// import visPromiseLookup from '../visualizations'; import sandboxedEval from '../modules/sandbox'; import './chart.css'; @@ -127,19 +128,19 @@ class Chart extends React.PureComponent { } loadAsyncVis(visType) { - this.visPromise = visPromiseLookup[visType]; - - this.visPromise() - .then((renderVis) => { - // ensure Component is still mounted - if (this.visPromise) { - this.setState({ renderVis }, this.renderVis); - } - }) - .catch((error) => { - console.warn(error); // eslint-disable-line - this.props.actions.chartRenderingFailed(error, this.props.chartId); - }); + // this.visPromise = visPromiseLookup[visType]; + + // this.visPromise() + // .then((renderVis) => { + // // ensure Component is still mounted + // if (this.visPromise) { + // this.setState({ renderVis }, this.renderVis); + // } + // }) + // .catch((error) => { + // console.warn(error); // eslint-disable-line + // this.props.actions.chartRenderingFailed(error, this.props.chartId); + // }); } addFilter(col, vals, merge = true, refresh = true) { @@ -228,6 +229,19 @@ class Chart extends React.PureComponent { // this allows to be positioned in the middle of the chart const containerStyles = isLoading ? { height: this.height(), width: this.width() } : null; + const { + className, + vizType, + faded, + queryResponse, + setControlValue, + } = this.props; + const classNames = [ + className, + vizType, + faded ? 'faded' : '', + ].join(' '); + return (
{this.renderTooltip()} @@ -254,7 +268,19 @@ class Chart extends React.PureComponent { /> )} - {!isLoading && + {!this.props.chartAlert && ( + + )} + {/* {!isLoading && !this.props.chartAlert && ( - )} + )} */}
); } diff --git a/superset/assets/src/superset-ui-core/chart/components/ErrorMessageFactory.jsx b/superset/assets/src/superset-ui-core/chart/components/ErrorMessageFactory.jsx new file mode 100644 index 0000000000000..990ea05fe1848 --- /dev/null +++ b/superset/assets/src/superset-ui-core/chart/components/ErrorMessageFactory.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const propTypes = { + id: PropTypes.string, + className: PropTypes.string, +}; +const defaultProps = { + id: '', + className: '', +}; + +export function createErrorMessage(type, error) { + function ErrorMessage(props) { + const { id, className } = props; + return ( +
+
+ ERROR + Chart type: {type} — + {error} +
+
+ ); + } + ErrorMessage.propTypes = propTypes; + ErrorMessage.defaultProps = defaultProps; + + return ErrorMessage; +} diff --git a/superset/assets/src/superset-ui-core/chart/components/SuperChart.jsx b/superset/assets/src/superset-ui-core/chart/components/SuperChart.jsx new file mode 100644 index 0000000000000..4421342174f09 --- /dev/null +++ b/superset/assets/src/superset-ui-core/chart/components/SuperChart.jsx @@ -0,0 +1,125 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { createErrorMessage } from './ErrorMessageFactory'; +import { loadChart } from '../registries/ChartLoaderRegistry'; +import { loadTransformProps } from '../registries/TransformPropsLoaderRegistry'; + +const IDENTITY = x => x; + +const propTypes = { + id: PropTypes.string, + className: PropTypes.string, + type: PropTypes.string.isRequired, + preTransformProps: PropTypes.func, + overrideTransformProps: PropTypes.func, + postTransformProps: PropTypes.func, +}; +const defaultProps = { + id: '', + className: '', + preTransformProps: IDENTITY, + overrideTransformProps: undefined, + postTransformProps: IDENTITY, +}; + +class SuperChart extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + Renderer: null, + transformProps: null, + }; + this.loading = false; + } + + componentDidMount() { + const { type, overrideTransformProps } = this.props; + this.loadChartType(type, overrideTransformProps); + } + + componentWillReceiveProps(nextProps) { + const { type, overrideTransformProps } = this.props; + if (nextProps.type !== type + || nextProps.overrideTransformProps !== overrideTransformProps + ) { + this.loadChartType(nextProps.type, nextProps.overrideTransformProps); + } + } + + loadChartType(type, overrideTransformProps) { + // Clear state + this.setState({ + Renderer: null, + transformProps: null, + }); + this.loading = false; + + if (type) { + console.log('loadChart', loadChart); + const componentPromise = loadChart(type); + const transformPropsPromise = overrideTransformProps + ? Promise.resolve(overrideTransformProps) + : loadTransformProps(type); + + this.loading = Promise.all([componentPromise, transformPropsPromise]) + .then( + // on success + ([Renderer, transformProps]) => { + this.setState({ Renderer, transformProps }); + }, + // on failure + (error) => { + this.setState({ + Renderer: createErrorMessage(type, error), + transformProps: IDENTITY, + }); + }, + ); + } + } + + render() { + const { + id, + className, + preTransformProps, + overrideTransformProps, + postTransformProps, + ...otherProps + } = this.props; + const type = this.props.type; + + const { Renderer } = this.state; + + // Loaded (both success and failure) + if (Renderer && this.transformProps) { + return ( + + ); + } + + // Loading state + if (this.loading) { + return ( +
+ Loading... +
+ ); + } + + // Initial state + return ( +
+ ); + } +} + +SuperChart.propTypes = propTypes; +SuperChart.defaultProps = defaultProps; + +export default SuperChart; diff --git a/superset/assets/src/superset-ui-core/chart/models/ChartMetadata.js b/superset/assets/src/superset-ui-core/chart/models/ChartMetadata.js new file mode 100644 index 0000000000000..5233960f40597 --- /dev/null +++ b/superset/assets/src/superset-ui-core/chart/models/ChartMetadata.js @@ -0,0 +1,20 @@ +export default class ChartMetadata { + constructor({ + name, + description, + thumbnail, + show = true, + }) { + this.name = name; + this.description = description; + this.thumbnail = thumbnail; + this.show = show; + this.variations = []; + } + + addVariation(variation) { + variation.setParent(this); + this.variations.push(variation); + return this; + } +} diff --git a/superset/assets/src/superset-ui-core/chart/models/ChartVariationMetadata.js b/superset/assets/src/superset-ui-core/chart/models/ChartVariationMetadata.js new file mode 100644 index 0000000000000..c7541e7bbd69e --- /dev/null +++ b/superset/assets/src/superset-ui-core/chart/models/ChartVariationMetadata.js @@ -0,0 +1,22 @@ +export default class ChartVariationMetadata { + constructor({ + variationKey, + name, + description, + thumbnail, + show = true, + defaultParams, + } = {}) { + this.parent = null; + this.variationKey = variationKey; + this.name = name; + this.description = description; + this.thumbnail = thumbnail; + this.show = show; + this.defaultParams = defaultParams; + } + + setParent(parent) { + this.parent = parent; + } +} diff --git a/superset/assets/src/superset-ui-core/chart/plugins/ChartPlugin.js b/superset/assets/src/superset-ui-core/chart/plugins/ChartPlugin.js new file mode 100644 index 0000000000000..6a2abd8e492fc --- /dev/null +++ b/superset/assets/src/superset-ui-core/chart/plugins/ChartPlugin.js @@ -0,0 +1,50 @@ +import Plugin from '../../platform/Plugin'; +import * as BuildQueryLoaderRegistry from '../registries/BuildQueryLoaderRegistry'; +import * as ChartMetadataRegistry from '../registries/ChartMetadataRegistry'; +import * as ChartLoaderRegistry from '../registries/ChartLoaderRegistry'; +import * as TransformPropsLoaderRegistry from '../registries/TransformPropsLoaderRegistry'; + +const IDENTITY = x => x; + +export default class ChartPlugin extends Plugin { + constructor({ + key, + metadata, + + // use buildQuery for immediate value + buildQuery = IDENTITY, + // use loadBuildQuery for dynamic import (lazy-loading) + loadBuildQuery, + + // use transformProps for immediate value + transformProps = IDENTITY, + // use loadTransformProps for dynamic import (lazy-loading) + loadTransformProps, + + // use Chart for immediate value + Chart, + // use loadChart for dynamic import (lazy-loading) + loadChart, + } = {}) { + super(key); + this.metadata = metadata; + this.loadBuildQuery = loadBuildQuery || (() => buildQuery); + this.loadTransformProps = loadTransformProps || (() => transformProps); + + if (loadChart) { + this.loadChart = loadChart; + } else if (Chart) { + this.loadChart = () => Chart; + } else { + throw new Error('Chart or loadChart is required'); + } + } + + install(key = this.key) { + super.setInstalledKey(key); + BuildQueryLoaderRegistry.registerLoader(key, this.loadBuildQuery); + ChartMetadataRegistry.register(key, this.metadata); + ChartLoaderRegistry.registerLoader(key, this.loadChart); + TransformPropsLoaderRegistry.registerLoader(key, this.loadTransformProps); + } +} diff --git a/superset/assets/src/superset-ui-core/chart/registries/BuildQueryLoaderRegistry.js b/superset/assets/src/superset-ui-core/chart/registries/BuildQueryLoaderRegistry.js new file mode 100644 index 0000000000000..c46c9802e32e7 --- /dev/null +++ b/superset/assets/src/superset-ui-core/chart/registries/BuildQueryLoaderRegistry.js @@ -0,0 +1,28 @@ +import LoaderRegistry from '../../platform/LoaderRegistry'; +import makeSingleton from '../../utils/makeSingleton'; + +class BuildQueryLoaderRegistry extends LoaderRegistry { + constructor() { + super('BuildQuery'); + } +} + +const { + getInstance, + has, + register, + registerLoader, + load, +} = makeSingleton(BuildQueryLoaderRegistry); + +// alias +const loadBuildQuery = load; + +export { + getInstance, + has, + register, + registerLoader, + load, + loadBuildQuery, +}; diff --git a/superset/assets/src/superset-ui-core/chart/registries/ChartLoaderRegistry.js b/superset/assets/src/superset-ui-core/chart/registries/ChartLoaderRegistry.js new file mode 100644 index 0000000000000..1d841807b0ff6 --- /dev/null +++ b/superset/assets/src/superset-ui-core/chart/registries/ChartLoaderRegistry.js @@ -0,0 +1,30 @@ +import LoaderRegistry from '../../platform/LoaderRegistry'; +import makeSingleton from '../../utils/makeSingleton'; + +class ChartLoaderRegistry extends LoaderRegistry { + constructor() { + super('Chart'); + } +} + +const { + getInstance, + has, + register, + registerLoader, + load, +} = makeSingleton(ChartLoaderRegistry); + +console.log('load', load, has, getInstance); + +// alias +const loadChart = load; + +export { + getInstance, + has, + register, + registerLoader, + load, + loadChart, +}; diff --git a/superset/assets/src/superset-ui-core/chart/registries/ChartMetadataRegistry.js b/superset/assets/src/superset-ui-core/chart/registries/ChartMetadataRegistry.js new file mode 100644 index 0000000000000..36a0579efa380 --- /dev/null +++ b/superset/assets/src/superset-ui-core/chart/registries/ChartMetadataRegistry.js @@ -0,0 +1,26 @@ +import Registry from '../../platform/Registry'; +import makeSingleton from '../../utils/makeSingleton'; + +class ChartMetadataRegistry extends Registry { + constructor() { + super('ChartMetadata'); + } +} + +const { + getInstance, + has, + register, + get, +} = makeSingleton(ChartMetadataRegistry); + +// alias +const getMetadata = get; + +export { + getInstance, + has, + register, + get, + getMetadata, +}; diff --git a/superset/assets/src/superset-ui-core/chart/registries/TransformPropsLoaderRegistry.js b/superset/assets/src/superset-ui-core/chart/registries/TransformPropsLoaderRegistry.js new file mode 100644 index 0000000000000..82c2b1d2ccf50 --- /dev/null +++ b/superset/assets/src/superset-ui-core/chart/registries/TransformPropsLoaderRegistry.js @@ -0,0 +1,28 @@ +import LoaderRegistry from '../../platform/LoaderRegistry'; +import makeSingleton from '../../utils/makeSingleton'; + +class TransformPropsLoaderRegistry extends LoaderRegistry { + constructor() { + super('TransformProps'); + } +} + +const { + getInstance, + has, + register, + registerLoader, + load, +} = makeSingleton(TransformPropsLoaderRegistry); + +// alias +const loadTransformProps = load; + +export { + getInstance, + has, + register, + registerLoader, + load, + loadTransformProps, +}; diff --git a/superset/assets/src/superset-ui-core/platform/LoaderRegistry.js b/superset/assets/src/superset-ui-core/platform/LoaderRegistry.js new file mode 100644 index 0000000000000..48722f0cd838a --- /dev/null +++ b/superset/assets/src/superset-ui-core/platform/LoaderRegistry.js @@ -0,0 +1,25 @@ +import Registry from './Registry'; + +export default class LoaderRegistry extends Registry { + register(key, value) { + this.items[key] = () => value; + } + + registerLoader(key, loader) { + this.items[key] = loader; + } + + load(key) { + const promise = this.promises[key]; + if (promise) { + return promise; + } + const loader = this.get(key); + if (loader) { + const newPromise = Promise.resolve(loader()); + this.promises[key] = newPromise; + return newPromise; + } + return Promise.reject(`[${this.name}Registry] Item with key "${key}" is not registered.`); + } +} diff --git a/superset/assets/src/superset-ui-core/platform/Plugin.js b/superset/assets/src/superset-ui-core/platform/Plugin.js new file mode 100644 index 0000000000000..3a969502ce741 --- /dev/null +++ b/superset/assets/src/superset-ui-core/platform/Plugin.js @@ -0,0 +1,15 @@ +import isRequired from '../utils/isRequired'; + +export default class Plugin { + constructor(key = isRequired('key')) { + this.key = key; + } + + setInstalledKey(key) { + this.installedKey = key; + } + + install(key = this.key) { + this.setInstalledKey(key); + } +} diff --git a/superset/assets/src/superset-ui-core/platform/Preset.js b/superset/assets/src/superset-ui-core/platform/Preset.js new file mode 100644 index 0000000000000..43f6ba414440a --- /dev/null +++ b/superset/assets/src/superset-ui-core/platform/Preset.js @@ -0,0 +1,32 @@ +export default class Preset { + constructor({ name, namespace = '', presets = [], plugins = [] }) { + this.name = name; + this.namespace = namespace; + this.presets = presets + .map(preset => (preset instanceof Preset) ? preset : new Preset(preset)); + this.plugins = plugins; + } + + expandPlugins() { + const allPlugins = {}; + const addPlugin = (plugin) => { + const key = `${this.namespace}${plugin.name}`; + allPlugins[key] = plugin; + }; + this.presets + .map(preset => preset.expandPlugins()) + .forEach((plugins) => { + plugins.forEach(addPlugin); + }); + this.plugins.forEach(addPlugin); + return allPlugins; + } + + install() { + const allPlugins = this.expandPlugins(); + Object.keys(allPlugins) + .forEach((key) => { + allPlugins[key].install(key); + }); + } +} diff --git a/superset/assets/src/superset-ui-core/platform/Registry.js b/superset/assets/src/superset-ui-core/platform/Registry.js new file mode 100644 index 0000000000000..28b6573844ccd --- /dev/null +++ b/superset/assets/src/superset-ui-core/platform/Registry.js @@ -0,0 +1,38 @@ +export default class Registry { + constructor(name) { + this.name = name; + this.items = {}; + this.promises = {}; + } + + has(key) { + const item = this.items[key]; + return item !== null && item !== undefined; + } + + register(key, value) { + this.items[key] = value; + } + + get(key) { + const item = this.items[key]; + if (item) { + return item; + } + return null; + } + + getAsPromise(key) { + const promise = this.promises[key]; + if (promise) { + return promise; + } + const item = this.get(key); + if (item) { + const newPromise = Promise.resolve(item); + this.promises[key] = newPromise; + return newPromise; + } + return Promise.reject(`[${this.name}Registry] Item with key "${key}" is not registered.`); + } +} diff --git a/superset/assets/src/superset-ui-core/utils/isRequired.js b/superset/assets/src/superset-ui-core/utils/isRequired.js new file mode 100644 index 0000000000000..d5878a5127a08 --- /dev/null +++ b/superset/assets/src/superset-ui-core/utils/isRequired.js @@ -0,0 +1,3 @@ +export default function isRequired(field) { + throw new Error(`${field} is required`); +} diff --git a/superset/assets/src/superset-ui-core/utils/makeSingleton.js b/superset/assets/src/superset-ui-core/utils/makeSingleton.js new file mode 100644 index 0000000000000..bd5acf8f93237 --- /dev/null +++ b/superset/assets/src/superset-ui-core/utils/makeSingleton.js @@ -0,0 +1,22 @@ +export default function makeSingleton(BaseClass) { + let singleton; + + function getInstance() { + if (!singleton) { + singleton = new BaseClass(); + } + return singleton; + } + + const staticFunctions = Object.getOwnPropertyNames(BaseClass.prototype) + .filter(fn => fn !== 'constructor') + .reduce((all, fn) => { + console.log('fn', fn); + const functions = all; + functions[fn] = function (...args) { + return getInstance()[fn](...args); + }; + return functions; + }, { getInstance }); + return staticFunctions; +} diff --git a/superset/assets/src/visualizations/BigNumber/index.js b/superset/assets/src/visualizations/BigNumber/index.js deleted file mode 100644 index 3aaef63c3da06..0000000000000 --- a/superset/assets/src/visualizations/BigNumber/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import adaptor from './adaptor'; -import BigNumber from './BigNumber'; - -export { BigNumber }; -export default adaptor; diff --git a/superset/assets/src/visualizations/BigNumber/metadata.js b/superset/assets/src/visualizations/BigNumber/metadata.js new file mode 100644 index 0000000000000..5fcec0bbb20d3 --- /dev/null +++ b/superset/assets/src/visualizations/BigNumber/metadata.js @@ -0,0 +1,7 @@ +import ChartMetadata from '../../superset-ui-core/chart/models/ChartMetadata'; + +export default new ChartMetadata({ + name: 'Big Number', + description: 'Big Number', + thumbnail: '', +}); diff --git a/superset/assets/src/visualizations/BigNumber/pluginFull.js b/superset/assets/src/visualizations/BigNumber/pluginFull.js new file mode 100644 index 0000000000000..bc6dc434b4d74 --- /dev/null +++ b/superset/assets/src/visualizations/BigNumber/pluginFull.js @@ -0,0 +1,11 @@ +import ChartPlugin from '../../superset-ui-core/chart/plugins/ChartPlugin'; +import BigNumber from './BigNumber'; +import transformProps from './transformProps'; +import metadata from './metadata'; + +export default new ChartPlugin({ + key: 'big-number', + metadata, + transformProps, + Chart: BigNumber, +}); diff --git a/superset/assets/src/visualizations/BigNumber/pluginLazy.js b/superset/assets/src/visualizations/BigNumber/pluginLazy.js new file mode 100644 index 0000000000000..ced4755198976 --- /dev/null +++ b/superset/assets/src/visualizations/BigNumber/pluginLazy.js @@ -0,0 +1,9 @@ +import ChartPlugin from '../../superset-ui-core/chart/plugins/ChartPlugin'; +import metadata from './metadata'; + +export default new ChartPlugin({ + key: 'big-number', + metadata, + loadTransformProps: () => import('./transformProps.js'), + loadChart: () => import('./BigNumber.jsx'), +}); diff --git a/superset/assets/src/visualizations/BigNumber/adaptor.jsx b/superset/assets/src/visualizations/BigNumber/transformProps.js similarity index 74% rename from superset/assets/src/visualizations/BigNumber/adaptor.jsx rename to superset/assets/src/visualizations/BigNumber/transformProps.js index 40a3a0f3d3601..91d533ee4ffd7 100644 --- a/superset/assets/src/visualizations/BigNumber/adaptor.jsx +++ b/superset/assets/src/visualizations/BigNumber/transformProps.js @@ -1,13 +1,10 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; import * as color from 'd3-color'; - -import BigNumberVis, { renderTooltipFactory } from './BigNumber'; +import { renderTooltipFactory } from './BigNumber'; import { d3FormatPreset } from '../../modules/utils'; const TIME_COLUMN = '__timestamp'; -function transform(data, formData) { +function transformData(data, formData) { let bigNumber; let trendlineData; const metricName = formData.metric.label || formData.metric; @@ -55,10 +52,10 @@ function transform(data, formData) { }; } -function adaptor(slice, payload) { +export default function transformProps({ slice, payload }) { const { formData, containerId } = slice; - const transformedData = transform(payload.data, formData); + const transformedData = transformData(payload.data, formData); const startYAxisAtZero = formData.start_y_axis_at_zero; const formatValue = d3FormatPreset(formData.y_axis_format); let userColor; @@ -67,19 +64,14 @@ function adaptor(slice, payload) { userColor = color.rgb(r, g, b).hex(); } - ReactDOM.render( - , - document.getElementById(containerId), - ); + return { + width: slice.width(), + height: slice.height(), + formatBigNumber: formatValue, + startYAxisAtZero, + mainColor: userColor, + gradientId: `big_number_${containerId}`, + renderTooltip: renderTooltipFactory(formatValue), + ...transformedData, + }; } - -export default adaptor; diff --git a/superset/assets/src/visualizations/index.js b/superset/assets/src/visualizations/index.js index 91f425bd7edea..e587a29bb9531 100644 --- a/superset/assets/src/visualizations/index.js +++ b/superset/assets/src/visualizations/index.js @@ -64,10 +64,10 @@ const loadNvd3 = () => loadVis(import(/* webpackChunkName: "nvd3_vis" */ './nvd3 const vizMap = { [VIZ_TYPES.area]: loadNvd3, [VIZ_TYPES.bar]: loadNvd3, - [VIZ_TYPES.big_number]: () => - loadVis(import(/* webpackChunkName: 'big_number' */ './BigNumber/index.js')), - [VIZ_TYPES.big_number_total]: () => - loadVis(import(/* webpackChunkName: "big_number" */ './BigNumber/index.js')), + // [VIZ_TYPES.big_number]: () => + // loadVis(import(/* webpackChunkName: 'big_number' */ './BigNumber/index.js')), + // [VIZ_TYPES.big_number_total]: () => + // loadVis(import(/* webpackChunkName: "big_number" */ './BigNumber/index.js')), [VIZ_TYPES.box_plot]: loadNvd3, [VIZ_TYPES.bubble]: loadNvd3, [VIZ_TYPES.bullet]: loadNvd3,