This is an automated email from the ASF dual-hosted git repository.
maximebeauchemin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push:
new 46579b1 Refactor out controlUtils.js module + unit tests (#7350)
46579b1 is described below
commit 46579b1bd67b798f19e0242f997cd8df60843e40
Author: Maxime Beauchemin <[email protected]>
AuthorDate: Tue Apr 30 11:45:06 2019 -0700
Refactor out controlUtils.js module + unit tests (#7350)
* [WiP]refactor out a controlUtils.js file
* unit tests
* add missing license
* Addressing comments
---
.../components/ControlPanelsContainer_spec.jsx | 3 +-
.../spec/javascripts/explore/controlUtils_spec.jsx | 164 +++++++++++++++++++++
.../assets/spec/javascripts/explore/store_spec.jsx | 66 +++++++++
.../explore/components/ExploreViewContainer.jsx | 2 +-
superset/assets/src/explore/controlUtils.js | 124 ++++++++++++++++
.../assets/src/explore/reducers/exploreReducer.js | 13 +-
.../assets/src/explore/reducers/getInitialState.js | 3 +-
superset/assets/src/explore/store.js | 127 +++-------------
superset/assets/src/visualizations/deckgl/utils.js | 3 +
9 files changed, 392 insertions(+), 113 deletions(-)
diff --git
a/superset/assets/spec/javascripts/explore/components/ControlPanelsContainer_spec.jsx
b/superset/assets/spec/javascripts/explore/components/ControlPanelsContainer_spec.jsx
index 842c5e2..cda5e0a 100644
---
a/superset/assets/spec/javascripts/explore/components/ControlPanelsContainer_spec.jsx
+++
b/superset/assets/spec/javascripts/explore/components/ControlPanelsContainer_spec.jsx
@@ -18,7 +18,8 @@
*/
import React from 'react';
import { shallow } from 'enzyme';
-import { getFormDataFromControls, defaultControls } from 'src/explore/store';
+import { defaultControls } from 'src/explore/store';
+import { getFormDataFromControls } from 'src/explore/controlUtils';
import { ControlPanelsContainer } from
'src/explore/components/ControlPanelsContainer';
import ControlPanelSection from 'src/explore/components/ControlPanelSection';
import * as featureFlags from 'src/featureFlags';
diff --git a/superset/assets/spec/javascripts/explore/controlUtils_spec.jsx
b/superset/assets/spec/javascripts/explore/controlUtils_spec.jsx
new file mode 100644
index 0000000..50f766a
--- /dev/null
+++ b/superset/assets/spec/javascripts/explore/controlUtils_spec.jsx
@@ -0,0 +1,164 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 {
+ getControlConfig,
+ getControlState,
+ getControlKeys,
+ applyMapStateToPropsToControl,
+} from '../../../src/explore/controlUtils';
+
+describe('controlUtils', () => {
+ const state = {
+ datasource: {
+ columns: [
+ 'a', 'b', 'c',
+ ],
+ metrics: [
+ { metric_name: 'first' },
+ { metric_name: 'second' },
+ ],
+ },
+ };
+
+ describe('getControlConfig', () => {
+ it('returns a valid spatial controlConfig', () => {
+ const spatialControl = getControlConfig('spatial', 'deck_grid');
+ expect(spatialControl.type).toEqual('SpatialControl');
+ expect(spatialControl.validators).toHaveLength(1);
+ });
+ it('overrides according to vizType', () => {
+ let control = getControlConfig('metric', 'line');
+ expect(control.type).toEqual('MetricsControl');
+ expect(control.validators).toHaveLength(1);
+
+ // deck_polygon overrides and removes validators
+ control = getControlConfig('metric', 'deck_polygon');
+ expect(control.type).toEqual('MetricsControl');
+ expect(control.validators).toHaveLength(0);
+ });
+ });
+
+ describe('getControlKeys', () => {
+
+ window.featureFlags = {
+ SCOPED_FILTER: false,
+ };
+
+ it('gets only strings, even when React components are in conf', () => {
+ const keys = getControlKeys('filter_box');
+ expect(keys.every(k => typeof k === 'string')).toEqual(true);
+ expect(keys).toHaveLength(16);
+ });
+ it('gets the right set of controlKeys for filter_box', () => {
+ const keys = getControlKeys('filter_box');
+ expect(keys.sort()).toEqual([
+ 'adhoc_filters',
+ 'cache_timeout',
+ 'datasource',
+ 'date_filter',
+ 'druid_time_origin',
+ 'filter_configs',
+ 'granularity',
+ 'instant_filtering',
+ 'show_druid_time_granularity',
+ 'show_druid_time_origin',
+ 'show_sqla_time_column',
+ 'show_sqla_time_granularity',
+ 'slice_id',
+ 'time_range',
+ 'url_params',
+ 'viz_type',
+ ]);
+ });
+ });
+
+ describe('applyMapStateToPropsToControl,', () => {
+ it('applies state to props as expected', () => {
+ let control = getControlConfig('all_columns', 'table');
+ control = applyMapStateToPropsToControl(control, state);
+ expect(control.options).toEqual(['a', 'b', 'c']);
+ });
+
+ it('removes the mapStateToProps key from the object', () => {
+ let control = getControlConfig('all_columns', 'table');
+ control = applyMapStateToPropsToControl(control, state);
+ expect(control.mapStateToProps).toBe(undefined);
+ });
+
+ });
+
+ describe('getControlState', () => {
+
+ it('to be function free', () => {
+ const control = getControlState('all_columns', 'table', state, ['a']);
+ expect(control.mapStateToProps).toBe(undefined);
+ expect(control.validators).toBe(undefined);
+ });
+
+ it('to fix multi with non-array values', () => {
+ const control = getControlState('all_columns', 'table', state, 'a');
+ expect(control.value).toEqual(['a']);
+ });
+
+ it('removes missing/invalid choice', () => {
+ let control = getControlState('stacked_style', 'area', state, 'stack');
+ expect(control.value).toBe('stack');
+
+ control = getControlState('stacked_style', 'area', state, 'FOO');
+ expect(control.value).toBe(null);
+ });
+
+ it('applies the default function for metrics', () => {
+ const control = getControlState('metrics', 'table', state);
+ expect(control.default).toEqual(['first']);
+ });
+
+ it('applies the default function for metric', () => {
+ const control = getControlState('metric', 'table', state);
+ expect(control.default).toEqual('first');
+ });
+
+ it('applies the default function, prefers count if it exists', () => {
+ const stateWithCount = {
+ ...state,
+ datasource: {
+ ...state.datasource,
+ metrics: [
+ { metric_name: 'first' },
+ { metric_name: 'second' },
+ { metric_name: 'count' },
+ ],
+ },
+ };
+ const control = getControlState('metrics', 'table', stateWithCount);
+ expect(control.default).toEqual(['count']);
+ });
+
+ });
+
+ describe('validateControl', () => {
+
+ it('validates the control, returns an error if empty', () => {
+ const control = getControlState('metric', 'table', state, null);
+ expect(control.validationErrors).toEqual(['cannot be empty']);
+ });
+
+ });
+
+});
diff --git a/superset/assets/spec/javascripts/explore/store_spec.jsx
b/superset/assets/spec/javascripts/explore/store_spec.jsx
new file mode 100644
index 0000000..4884ed1
--- /dev/null
+++ b/superset/assets/spec/javascripts/explore/store_spec.jsx
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 { applyDefaultFormData } from '../../../src/explore/store';
+
+describe('store', () => {
+
+ describe('applyDefaultFormData', () => {
+
+ window.featureFlags = {
+ SCOPED_FILTER: false,
+ };
+
+ it('applies default to formData if the key is missing', () => {
+ const inputFormData = {
+ datasource: '11_table',
+ viz_type: 'table',
+ };
+ let outputFormData = applyDefaultFormData(inputFormData);
+ expect(outputFormData.row_limit).toEqual(10000);
+
+ const inputWithRowLimit = {
+ ...inputFormData,
+ row_limit: 888,
+ };
+ outputFormData = applyDefaultFormData(inputWithRowLimit);
+ expect(outputFormData.row_limit).toEqual(888);
+ });
+
+ it('keeps null if key is defined with null', () => {
+ const inputFormData = {
+ datasource: '11_table',
+ viz_type: 'table',
+ row_limit: null,
+ };
+ const outputFormData = applyDefaultFormData(inputFormData);
+ expect(outputFormData.row_limit).toBe(null);
+ });
+
+ it('removes out of scope, or deprecated keys', () => {
+ const inputFormData = {
+ datasource: '11_table',
+ viz_type: 'table',
+ this_should_no_be_here: true,
+ };
+ const outputFormData = applyDefaultFormData(inputFormData);
+ expect(outputFormData.this_should_no_be_here).toBe(undefined);
+ });
+
+ });
+});
diff --git a/superset/assets/src/explore/components/ExploreViewContainer.jsx
b/superset/assets/src/explore/components/ExploreViewContainer.jsx
index 431f252..9e6647f 100644
--- a/superset/assets/src/explore/components/ExploreViewContainer.jsx
+++ b/superset/assets/src/explore/components/ExploreViewContainer.jsx
@@ -29,7 +29,7 @@ import SaveModal from './SaveModal';
import QueryAndSaveBtns from './QueryAndSaveBtns';
import { getExploreUrlAndPayload, getExploreLongUrl } from '../exploreUtils';
import { areObjectsEqual } from '../../reduxUtils';
-import { getFormDataFromControls } from '../store';
+import { getFormDataFromControls } from '../controlUtils';
import { chartPropShape } from '../../dashboard/util/propShapes';
import * as exploreActions from '../actions/exploreActions';
import * as saveModalActions from '../actions/saveModalActions';
diff --git a/superset/assets/src/explore/controlUtils.js
b/superset/assets/src/explore/controlUtils.js
new file mode 100644
index 0000000..65e8c47
--- /dev/null
+++ b/superset/assets/src/explore/controlUtils.js
@@ -0,0 +1,124 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 controlPanelConfigs, { sectionsToRender } from './controlPanels';
+import controls from './controls';
+
+export function getFormDataFromControls(controlsState) {
+ const formData = {};
+ Object.keys(controlsState).forEach((controlName) => {
+ formData[controlName] = controlsState[controlName].value;
+ });
+ return formData;
+}
+
+export function validateControl(control) {
+ const validators = control.validators;
+ if (validators && validators.length > 0) {
+ const validatedControl = { ...control };
+ const validationErrors = [];
+ validators.forEach((f) => {
+ const v = f(control.value);
+ if (v) {
+ validationErrors.push(v);
+ }
+ });
+ delete validatedControl.validators;
+ return { ...validatedControl, validationErrors };
+ }
+ return control;
+}
+
+export function getControlKeys(vizType, datasourceType) {
+ const controlKeys = [];
+ sectionsToRender(vizType, datasourceType).forEach(
+ section => section.controlSetRows.forEach(
+ fieldsetRow => fieldsetRow.forEach(
+ (field) => {
+ if (typeof field === 'string') {
+ controlKeys.push(field);
+ }
+ })));
+ return controlKeys;
+}
+
+export function getControlConfig(controlKey, vizType) {
+ // Gets the control definition, applies overrides, and executes
+ // the mapStatetoProps
+ const vizConf = controlPanelConfigs[vizType] || {};
+ const controlOverrides = vizConf.controlOverrides || {};
+ const control = {
+ ...controls[controlKey],
+ ...controlOverrides[controlKey],
+ };
+ return control;
+}
+
+export function applyMapStateToPropsToControl(control, state) {
+ if (control.mapStateToProps) {
+ const appliedControl = { ...control };
+ if (state) {
+ Object.assign(appliedControl, control.mapStateToProps(state, control));
+ }
+ delete appliedControl.mapStateToProps;
+ return appliedControl;
+ }
+ return control;
+}
+
+function handleMissingChoice(controlKey, control) {
+ // If the value is not valid anymore based on choices, clear it
+ const value = control.value;
+ if (
+ control.type === 'SelectControl' &&
+ !control.freeForm &&
+ control.choices &&
+ value
+ ) {
+ const alteredControl = { ...control };
+ const choiceValues = control.choices.map(c => c[0]);
+ if (control.multi && value.length > 0) {
+ alteredControl.value = value.filter(el => choiceValues.indexOf(el) > -1);
+ return alteredControl;
+ } else if (!control.multi && choiceValues.indexOf(value) < 0) {
+ alteredControl.value = null;
+ return alteredControl;
+ }
+ }
+ return control;
+}
+
+export function getControlState(controlKey, vizType, state, value) {
+ let controlValue = value;
+ const controlConfig = getControlConfig(controlKey, vizType);
+ let controlState = { ...controlConfig };
+ controlState = applyMapStateToPropsToControl(controlState, state);
+
+ // If default is a function, evaluate it
+ if (typeof controlState.default === 'function') {
+ controlState.default = controlState.default(controlState);
+ }
+
+ // If a choice control went from multi=false to true, wrap value in array
+ if (controlConfig.multi && value && !Array.isArray(value)) {
+ controlValue = [value];
+ }
+ controlState.value = controlValue === undefined ? controlState.default :
controlValue;
+ controlState = handleMissingChoice(controlKey, controlState);
+ return validateControl(controlState);
+}
diff --git a/superset/assets/src/explore/reducers/exploreReducer.js
b/superset/assets/src/explore/reducers/exploreReducer.js
index 6f6a1a9..44d5a72 100644
--- a/superset/assets/src/explore/reducers/exploreReducer.js
+++ b/superset/assets/src/explore/reducers/exploreReducer.js
@@ -17,8 +17,10 @@
* under the License.
*/
/* eslint camelcase: 0 */
-import { validateControl, getControlsState, getFormDataFromControls } from
'../store';
-import controls from '../controls';
+import {
+ getControlsState,
+} from '../store';
+import { getControlState, getFormDataFromControls } from '../controlUtils';
import * as actions from '../actions/exploreActions';
export default function exploreReducer(state = {}, action) {
@@ -78,11 +80,10 @@ export default function exploreReducer(state = {}, action) {
[actions.SET_FIELD_VALUE]() {
// These errors are reported from the Control components
let errors = action.validationErrors || [];
- let control = {
- ...controls[action.controlName],
- value: action.value,
+ const vizType = state.form_data.viz_type;
+ const control = {
+ ...getControlState(action.controlName, vizType, state, action.value),
};
- control = validateControl(control);
// These errors are based on control config `validators`
errors = errors.concat(control.validationErrors || []);
diff --git a/superset/assets/src/explore/reducers/getInitialState.js
b/superset/assets/src/explore/reducers/getInitialState.js
index 98b9799..892224d 100644
--- a/superset/assets/src/explore/reducers/getInitialState.js
+++ b/superset/assets/src/explore/reducers/getInitialState.js
@@ -20,7 +20,8 @@ import shortid from 'shortid';
import getToastsFromPyFlashMessages from
'../../messageToasts/utils/getToastsFromPyFlashMessages';
import { getChartKey } from '../exploreUtils';
-import { getControlsState, getFormDataFromControls } from '../store';
+import { getControlsState } from '../store';
+import { getFormDataFromControls } from '../controlUtils';
export default function getInitialState(bootstrapData) {
const controls = getControlsState(bootstrapData, bootstrapData.form_data);
diff --git a/superset/assets/src/explore/store.js
b/superset/assets/src/explore/store.js
index df456c2..6d58b8d 100644
--- a/superset/assets/src/explore/store.js
+++ b/superset/assets/src/explore/store.js
@@ -17,44 +17,13 @@
* under the License.
*/
/* eslint camelcase: 0 */
-import React from 'react';
+import {
+ getControlState,
+ getControlKeys,
+ getFormDataFromControls,
+} from './controlUtils';
import controls from './controls';
-import controlPanelConfigs, { sectionsToRender } from './controlPanels';
-
-export function getFormDataFromControls(controlsState) {
- const formData = {};
- Object.keys(controlsState).forEach((controlName) => {
- formData[controlName] = controlsState[controlName].value;
- });
- return formData;
-}
-
-export function validateControl(control) {
- const validators = control.validators;
- const validationErrors = [];
- if (validators && validators.length > 0) {
- validators.forEach((f) => {
- const v = f(control.value);
- if (v) {
- validationErrors.push(v);
- }
- });
- }
- if (validationErrors.length > 0) {
- return { ...control, validationErrors };
- }
- return control;
-}
-
-
-export function getControlNames(vizType, datasourceType) {
- const controlNames = [];
- sectionsToRender(vizType, datasourceType).forEach(
- section => section.controlSetRows.forEach(
- fsr => fsr.forEach(
- f => controlNames.push(f))));
- return controlNames;
-}
+import controlPanelConfigs from './controlPanels';
function handleDeprecatedControls(formData) {
// Reacffectation / handling of deprecated controls
@@ -66,104 +35,54 @@ function handleDeprecatedControls(formData) {
}
}
-export function getControlsState(state, form_data) {
+export function getControlsState(state, inputFormData) {
/*
* Gets a new controls object to put in the state. The controls object
* is similar to the configuration control with only the controls
* related to the current viz_type, materializes mapStateToProps functions,
- * adds value keys coming from form_data passed here. This can't be an action
creator
+ * adds value keys coming from inputFormData passed here. This can't be an
action creator
* just yet because it's used in both the explore and dashboard views.
* */
// Getting a list of active control names for the current viz
- const formData = Object.assign({}, form_data);
+ const formData = Object.assign({}, inputFormData);
const vizType = formData.viz_type || 'table';
handleDeprecatedControls(formData);
- const controlNames = getControlNames(vizType, state.datasource.type);
+ const controlNames = getControlKeys(vizType, state.datasource.type);
const viz = controlPanelConfigs[vizType] || {};
- const controlOverrides = viz.controlOverrides || {};
const controlsState = {};
- controlNames.forEach((k) => {
- if (React.isValidElement(k)) {
- // no state
- return;
- }
- const control = Object.assign({}, controls[k], controlOverrides[k]);
- if (control.mapStateToProps) {
- Object.assign(control, control.mapStateToProps(state, control));
- delete control.mapStateToProps;
- }
-
- formData[k] = (control.multi && formData[k] &&
!Array.isArray(formData[k])) ? [formData[k]]
- : formData[k];
-
- // If the value is not valid anymore based on choices, clear it
- if (
- control.type === 'SelectControl' &&
- !control.freeForm &&
- control.choices &&
- k !== 'datasource' &&
- formData[k]
- ) {
- const choiceValues = control.choices.map(c => c[0]);
- if (control.multi && formData[k].length > 0) {
- formData[k] = formData[k].filter(el => choiceValues.indexOf(el) > -1);
- } else if (!control.multi && choiceValues.indexOf(formData[k]) < 0) {
- delete formData[k];
- }
- }
- if (typeof control.default === 'function') {
- control.default = control.default(control);
- }
- control.validationErrors = [];
- control.value = control.default;
- // formData[k]'s type should match control value type
- if (formData[k] !== undefined &&
- (Array.isArray(formData[k]) && control.multi || !control.multi)
- ) {
- control.value = formData[k];
- }
- controlsState[k] = validateControl(control);
+ controlNames.forEach((k) => {
+ const control = getControlState(k, vizType, state, formData[k]);
+ controlsState[k] = control;
+ formData[k] = control.value;
});
+
if (viz.onInit) {
return viz.onInit(controlsState);
}
return controlsState;
}
-export function applyDefaultFormData(form_data) {
- const datasourceType = form_data.datasource.split('__')[1];
- const vizType = form_data.viz_type || 'table';
- const viz = controlPanelConfigs[vizType] || {};
- const controlNames = getControlNames(vizType, datasourceType);
- const controlOverrides = viz.controlOverrides || {};
+export function applyDefaultFormData(inputFormData) {
+ const datasourceType = inputFormData.datasource.split('__')[1];
+ const vizType = inputFormData.viz_type;
+ const controlNames = getControlKeys(vizType, datasourceType);
const formData = {};
controlNames.forEach((k) => {
- const control = Object.assign({}, controls[k]);
- if (controlOverrides[k]) {
- Object.assign(control, controlOverrides[k]);
- }
- if (form_data[k] === undefined) {
- if (typeof control.default === 'function') {
- formData[k] = control.default(controls[k]);
- } else {
- formData[k] = control.default;
- }
+ const controlState = getControlState(k, vizType, null, inputFormData[k]);
+ if (inputFormData[k] === undefined) {
+ formData[k] = controlState.value;
} else {
- formData[k] = form_data[k];
+ formData[k] = inputFormData[k];
}
});
return formData;
}
-export const autoQueryControls = [
- 'datasource',
- 'viz_type',
-];
const defaultControls = Object.assign({}, controls);
Object.keys(controls).forEach((f) => {
diff --git a/superset/assets/src/visualizations/deckgl/utils.js
b/superset/assets/src/visualizations/deckgl/utils.js
index b2b130a..62024ef 100644
--- a/superset/assets/src/visualizations/deckgl/utils.js
+++ b/superset/assets/src/visualizations/deckgl/utils.js
@@ -34,6 +34,9 @@ export function getBreakPoints({
// compute evenly distributed break points based on number of buckets
const numBuckets = formDataNumBuckets ? parseInt(formDataNumBuckets, 10) :
DEFAULT_NUM_BUCKETS;
const [minValue, maxValue] = extent(features, accessor);
+ if (minValue === undefined) {
+ return [];
+ }
const delta = (maxValue - minValue) / numBuckets;
const precision = delta === 0
? 0