diff --git a/superset/assets/package.json b/superset/assets/package.json index d5571e08bb97d..0b7dc9c8b51a2 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -79,6 +79,7 @@ "jed": "^1.1.1", "jquery": "3.1.1", "json-bigint": "^0.3.0", + "lodash": "^4.17.11", "lodash.throttle": "^4.1.1", "mapbox-gl": "^0.45.0", "mathjs": "^3.20.2", diff --git a/superset/assets/spec/javascripts/utils/convertKeysToCamelCase_spec.js b/superset/assets/spec/javascripts/utils/convertKeysToCamelCase_spec.js new file mode 100644 index 0000000000000..6cae3c1874649 --- /dev/null +++ b/superset/assets/spec/javascripts/utils/convertKeysToCamelCase_spec.js @@ -0,0 +1,29 @@ +import { it, describe } from 'mocha'; +import { expect } from 'chai'; +import convertKeysToCamelCase from '../../../src/utils/convertKeysToCamelCase'; + +describe.only('convertKeysToCamelCase(object)', () => { + it('returns undefined for undefined input', () => { + expect(convertKeysToCamelCase(undefined)).to.equal(undefined); + }); + it('returns null for null input', () => { + expect(convertKeysToCamelCase(null)).to.equal(null); + }); + it('returns a new object that has all keys in camelCase', () => { + const input = { + is_happy: true, + 'is-angry': false, + isHungry: false, + }; + expect(convertKeysToCamelCase(input)).to.deep.equal({ + isHappy: true, + isAngry: false, + isHungry: false, + }); + }); + it('throws error if input is not a plain object', () => { + expect(() => { convertKeysToCamelCase({}); }).to.not.throw(); + expect(() => { convertKeysToCamelCase(''); }).to.throw(); + expect(() => { convertKeysToCamelCase(new Map()); }).to.throw(); + }); +}); diff --git a/superset/assets/src/utils/convertKeysToCamelCase.js b/superset/assets/src/utils/convertKeysToCamelCase.js new file mode 100644 index 0000000000000..c1071e649438c --- /dev/null +++ b/superset/assets/src/utils/convertKeysToCamelCase.js @@ -0,0 +1,11 @@ +import { mapKeys, camelCase, isPlainObject } from 'lodash/fp'; + +export default function convertKeysToCamelCase(object) { + if (object === null || object === undefined) { + return object; + } + if (isPlainObject(object)) { + return mapKeys(k => camelCase(k), object); + } + throw new Error(`Cannot convert input that is not a plain object: ${object}`); +} diff --git a/superset/assets/src/utils/createAdaptor.jsx b/superset/assets/src/utils/createAdaptor.jsx new file mode 100644 index 0000000000000..0de1d25dc8f24 --- /dev/null +++ b/superset/assets/src/utils/createAdaptor.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import BasicChartInput from '../visualizations/models/BasicChartInput'; + +const IDENTITY = x => x; + +export default function createAdaptor(Component, transformProps = IDENTITY) { + return function adaptor(slice, payload, setControlValue) { + const basicChartInput = new BasicChartInput(slice, payload, setControlValue); + ReactDOM.render( + , + document.querySelector(slice.selector), + ); + }; +} diff --git a/superset/assets/src/utils/reactify.jsx b/superset/assets/src/utils/reactify.jsx new file mode 100644 index 0000000000000..d52c81694fca3 --- /dev/null +++ b/superset/assets/src/utils/reactify.jsx @@ -0,0 +1,54 @@ +import React from 'react'; + +export default function reactify(renderFn) { + class ReactifiedComponent extends React.Component { + constructor(props) { + super(props); + this.setContainerRef = this.setContainerRef.bind(this); + } + + componentDidMount() { + this.execute(); + } + + componentDidUpdate() { + this.execute(); + } + + componentWillUnmount() { + this.container = null; + } + + setContainerRef(c) { + this.container = c; + } + + execute() { + if (this.container) { + renderFn(this.container, this.props); + } + } + + render() { + const { id, className } = this.props; + return ( +
+ ); + } + } + + if (renderFn.displayName) { + ReactifiedComponent.displayName = renderFn.displayName; + } + if (renderFn.propTypes) { + ReactifiedComponent.propTypes = renderFn.propTypes; + } + if (renderFn.defaultProps) { + ReactifiedComponent.defaultProps = renderFn.defaultProps; + } + return ReactifiedComponent; +} diff --git a/superset/assets/src/visualizations/WorldMap/ReactWorldMap.js b/superset/assets/src/visualizations/WorldMap/ReactWorldMap.js new file mode 100644 index 0000000000000..bc17f82b0be7f --- /dev/null +++ b/superset/assets/src/visualizations/WorldMap/ReactWorldMap.js @@ -0,0 +1,4 @@ +import reactify from '../../utils/reactify'; +import WorldMap from './WorldMap'; + +export default reactify(WorldMap); diff --git a/superset/assets/src/visualizations/world_map.css b/superset/assets/src/visualizations/WorldMap/WorldMap.css similarity index 100% rename from superset/assets/src/visualizations/world_map.css rename to superset/assets/src/visualizations/WorldMap/WorldMap.css diff --git a/superset/assets/src/visualizations/world_map.js b/superset/assets/src/visualizations/WorldMap/WorldMap.js similarity index 86% rename from superset/assets/src/visualizations/world_map.js rename to superset/assets/src/visualizations/WorldMap/WorldMap.js index 6c4948a7dd6c1..d83d79411b0d7 100644 --- a/superset/assets/src/visualizations/world_map.js +++ b/superset/assets/src/visualizations/WorldMap/WorldMap.js @@ -1,7 +1,7 @@ import d3 from 'd3'; import PropTypes from 'prop-types'; import Datamap from 'datamaps'; -import './world_map.css'; +import './WorldMap.css'; const propTypes = { data: PropTypes.arrayOf(PropTypes.shape({ @@ -109,20 +109,4 @@ function WorldMap(element, props) { WorldMap.propTypes = propTypes; -function adaptor(slice, payload) { - const { selector, formData } = slice; - const { - max_bubble_size: maxBubbleSize, - show_bubbles: showBubbles, - } = formData; - const element = document.querySelector(selector); - - return WorldMap(element, { - data: payload.data, - height: slice.height(), - maxBubbleSize: parseInt(maxBubbleSize, 10), - showBubbles, - }); -} - -export default adaptor; +export default WorldMap; diff --git a/superset/assets/src/visualizations/WorldMap/adaptor.jsx b/superset/assets/src/visualizations/WorldMap/adaptor.jsx new file mode 100644 index 0000000000000..30d0400f35a11 --- /dev/null +++ b/superset/assets/src/visualizations/WorldMap/adaptor.jsx @@ -0,0 +1,5 @@ +import createAdaptor from '../../utils/createAdaptor'; +import WorldMap from './ReactWorldMap'; +import transformProps from './transformProps'; + +export default createAdaptor(WorldMap, transformProps); diff --git a/superset/assets/src/visualizations/WorldMap/transformProps.js b/superset/assets/src/visualizations/WorldMap/transformProps.js new file mode 100644 index 0000000000000..4e56b03c06be7 --- /dev/null +++ b/superset/assets/src/visualizations/WorldMap/transformProps.js @@ -0,0 +1,10 @@ +export default function transformProps(basicChartInput) { + const { formData, payload } = basicChartInput; + const { maxBubbleSize, showBubbles } = formData; + + return { + data: payload.data, + maxBubbleSize: parseInt(maxBubbleSize, 10), + showBubbles, + }; +} diff --git a/superset/assets/src/visualizations/index.js b/superset/assets/src/visualizations/index.js index 31feffc2abba4..e924dd4d12e06 100644 --- a/superset/assets/src/visualizations/index.js +++ b/superset/assets/src/visualizations/index.js @@ -108,7 +108,7 @@ const vizMap = { [VIZ_TYPES.word_cloud]: () => loadVis(import(/* webpackChunkName: "word_cloud" */ './wordcloud/WordCloud.js')), [VIZ_TYPES.world_map]: () => - loadVis(import(/* webpackChunkName: "world_map" */ './world_map.js')), + loadVis(import(/* webpackChunkName: "world_map" */ './WorldMap/adaptor.jsx')), [VIZ_TYPES.dual_line]: loadNvd3, [VIZ_TYPES.event_flow]: () => loadVis(import(/* webpackChunkName: "EventFlow" */ './EventFlow.jsx')), diff --git a/superset/assets/src/visualizations/models/BasicChartInput.js b/superset/assets/src/visualizations/models/BasicChartInput.js new file mode 100644 index 0000000000000..de4add533639b --- /dev/null +++ b/superset/assets/src/visualizations/models/BasicChartInput.js @@ -0,0 +1,10 @@ +import convertKeysToCamelCase from '../../utils/convertKeysToCamelCase'; + +export default class BasicChartInput { + constructor(slice, payload, setControlValue) { + this.annotationData = slice.annotationData; + this.formData = convertKeysToCamelCase(slice.formData); + this.payload = payload; + this.setControlValue = setControlValue; + } +} diff --git a/superset/assets/yarn.lock b/superset/assets/yarn.lock index e0fc1ea6f194f..b8c06f94a728b 100644 --- a/superset/assets/yarn.lock +++ b/superset/assets/yarn.lock @@ -7487,6 +7487,10 @@ lodash@4.17.10, lodash@^4.0.1, lodash@^4.0.8, lodash@^4.13.1, lodash@^4.14.0, lo version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" +lodash@^4.17.11: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + log-symbols@2.2.0, log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"