You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by yj...@apache.org on 2021/02/28 18:11:22 UTC
[superset] branch master updated: refactor(explore): convert
ControlPanelsContainer to typescript (#13221)
This is an automated email from the ASF dual-hosted git repository.
yjc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 3c62069 refactor(explore): convert ControlPanelsContainer to typescript (#13221)
3c62069 is described below
commit 3c62069bbb0d07ff0194fbb159c2e04fd98fafc2
Author: Jesse Yang <je...@airbnb.com>
AuthorDate: Sun Feb 28 08:10:15 2021 -1000
refactor(explore): convert ControlPanelsContainer to typescript (#13221)
---
superset-frontend/.eslintrc.js | 2 +
...advanced.test.ts => advanced_analytics.test.ts} | 41 -----
.../{advanced.test.ts => annotations.test.ts} | 53 +-----
.../explore/visualizations/pivot_table.test.js | 2 +-
...er_spec.jsx => ControlPanelsContainer_spec.tsx} | 21 ++-
superset-frontend/src/constants.ts | 1 -
.../src/explore/actions/exploreActions.ts | 8 +-
...elsContainer.jsx => ControlPanelsContainer.tsx} | 180 ++++++++++-----------
.../components/{ControlRow.jsx => ControlRow.tsx} | 19 +--
.../explore/components/ExploreViewContainer.jsx | 3 -
.../components/controls/CollectionControl.jsx | 6 -
.../DateFilterControl/DateFilterControl.tsx | 40 ++---
.../controls/FilterControl/AdhocFilterControl.jsx | 32 ++--
.../MetricControl/AdhocMetricEditPopover.jsx | 3 -
.../controls/MetricControl/AdhocMetricOption.jsx | 2 -
.../MetricControl/AdhocMetricPopoverTrigger.tsx | 2 -
.../MetricControl/MetricDefinitionValue.jsx | 9 +-
.../controls/MetricControl/MetricsControl.jsx | 8 +-
.../explore/components/controls/TextControl.tsx | 88 +++++-----
.../src/explore/{constants.js => constants.ts} | 10 +-
.../controlUtils/getFormDataFromControls.ts} | 36 ++---
.../{controlUtils.js => controlUtils/index.js} | 13 +-
superset-frontend/src/explore/controls.jsx | 6 +-
.../src/explore/reducers/exploreReducer.js | 43 +++--
.../{getInitialState.js => getInitialState.ts} | 69 +++++---
superset-frontend/src/types/Chart.ts | 3 +-
superset-frontend/src/types/bootstrapTypes.ts | 9 ++
27 files changed, 313 insertions(+), 396 deletions(-)
diff --git a/superset-frontend/.eslintrc.js b/superset-frontend/.eslintrc.js
index 46970952..5b79778 100644
--- a/superset-frontend/.eslintrc.js
+++ b/superset-frontend/.eslintrc.js
@@ -104,6 +104,7 @@ module.exports = {
'no-mixed-operators': 0,
'no-multi-assign': 0,
'no-multi-spaces': 0,
+ 'no-nested-ternary': 0,
'no-prototype-builtins': 0,
'no-restricted-properties': 0,
'no-restricted-imports': [
@@ -226,6 +227,7 @@ module.exports = {
'no-mixed-operators': 0,
'no-multi-assign': 0,
'no-multi-spaces': 0,
+ 'no-nested-ternary': 0,
'no-prototype-builtins': 0,
'no-restricted-properties': 0,
'no-restricted-imports': [
diff --git a/superset-frontend/cypress-base/cypress/integration/explore/advanced.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/advanced_analytics.test.ts
similarity index 63%
copy from superset-frontend/cypress-base/cypress/integration/explore/advanced.test.ts
copy to superset-frontend/cypress-base/cypress/integration/explore/advanced_analytics.test.ts
index 066ae86..77ebfbe 100644
--- a/superset-frontend/cypress-base/cypress/integration/explore/advanced.test.ts
+++ b/superset-frontend/cypress-base/cypress/integration/explore/advanced_analytics.test.ts
@@ -55,44 +55,3 @@ describe('Advanced analytics', () => {
.contains('1 year');
});
});
-
-describe('Annotations', () => {
- beforeEach(() => {
- cy.login();
- cy.intercept('GET', '/superset/explore_json/**').as('getJson');
- cy.intercept('POST', '/superset/explore_json/**').as('postJson');
- });
-
- it('Create formula annotation y-axis goal line', () => {
- cy.visitChartByName('Num Births Trend');
- cy.verifySliceSuccess({ waitAlias: '@postJson' });
-
- cy.get('[data-test=annotation_layers]').within(() => {
- cy.get('button').click();
- });
-
- cy.get('[data-test="popover-content"]').within(() => {
- cy.get('[data-test=annotation-layer-name-header]')
- .siblings()
- .first()
- .within(() => {
- cy.get('input').type('Goal line');
- });
- cy.get('[data-test=annotation-layer-value-header]')
- .siblings()
- .first()
- .within(() => {
- cy.get('input').type('y=1400000');
- });
- cy.get('button').contains('OK').click({ force: true });
- });
-
- cy.get('button[data-test="run-query-button"]').click();
- cy.verifySliceSuccess({
- waitAlias: '@postJson',
- chartSelector: 'svg',
- });
-
- cy.get('.nv-legend-text').should('have.length', 2);
- });
-});
diff --git a/superset-frontend/cypress-base/cypress/integration/explore/advanced.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/annotations.test.ts
similarity index 55%
rename from superset-frontend/cypress-base/cypress/integration/explore/advanced.test.ts
rename to superset-frontend/cypress-base/cypress/integration/explore/annotations.test.ts
index 066ae86..ebbaddf 100644
--- a/superset-frontend/cypress-base/cypress/integration/explore/advanced.test.ts
+++ b/superset-frontend/cypress-base/cypress/integration/explore/annotations.test.ts
@@ -16,46 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
-describe('Advanced analytics', () => {
- beforeEach(() => {
- cy.login();
- cy.intercept('GET', '/superset/explore_json/**').as('getJson');
- cy.intercept('POST', '/superset/explore_json/**').as('postJson');
- });
-
- it('Create custom time compare', () => {
- cy.visitChartByName('Num Births Trend');
- cy.verifySliceSuccess({ waitAlias: '@postJson' });
-
- cy.get('.ant-collapse-header').contains('Advanced Analytics').click();
-
- cy.get('[data-test=time_compare]').find('.Select__control').click();
- cy.get('[data-test=time_compare]')
- .find('input[type=text]')
- .type('28 days{enter}');
-
- cy.get('[data-test=time_compare]')
- .find('input[type=text]')
- .type('1 year{enter}');
-
- cy.get('button[data-test="run-query-button"]').click();
- cy.wait('@postJson');
- cy.reload();
- cy.verifySliceSuccess({
- waitAlias: '@postJson',
- chartSelector: 'svg',
- });
-
- cy.get('.ant-collapse-header').contains('Advanced Analytics').click();
- cy.get('[data-test=time_compare]')
- .find('.Select__multi-value__label')
- .contains('28 days');
- cy.get('[data-test=time_compare]')
- .find('.Select__multi-value__label')
- .contains('1 year');
- });
-});
-
describe('Annotations', () => {
beforeEach(() => {
cy.login();
@@ -67,16 +27,16 @@ describe('Annotations', () => {
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@postJson' });
- cy.get('[data-test=annotation_layers]').within(() => {
- cy.get('button').click();
- });
+ const layerLabel = 'Goal line';
+
+ cy.get('[data-test=annotation_layers] button').click();
cy.get('[data-test="popover-content"]').within(() => {
cy.get('[data-test=annotation-layer-name-header]')
.siblings()
.first()
.within(() => {
- cy.get('input').type('Goal line');
+ cy.get('input').type(layerLabel);
});
cy.get('[data-test=annotation-layer-value-header]')
.siblings()
@@ -84,15 +44,16 @@ describe('Annotations', () => {
.within(() => {
cy.get('input').type('y=1400000');
});
- cy.get('button').contains('OK').click({ force: true });
+ cy.get('button').contains('OK').click();
});
cy.get('button[data-test="run-query-button"]').click();
+ cy.get('[data-test=annotation_layers]').contains(layerLabel);
+
cy.verifySliceSuccess({
waitAlias: '@postJson',
chartSelector: 'svg',
});
-
cy.get('.nv-legend-text').should('have.length', 2);
});
});
diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/pivot_table.test.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/pivot_table.test.js
index 1346b89..14de08d 100644
--- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/pivot_table.test.js
+++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/pivot_table.test.js
@@ -28,7 +28,7 @@ describe('Visualization > Pivot Table', () => {
adhoc_filters: [],
groupby: ['name'],
columns: ['state'],
- row_limit: 50000,
+ row_limit: 5000,
pandas_aggfunc: 'sum',
pivot_margins: true,
number_format: '.3s',
diff --git a/superset-frontend/spec/javascripts/explore/components/ControlPanelsContainer_spec.jsx b/superset-frontend/spec/javascripts/explore/components/ControlPanelsContainer_spec.tsx
similarity index 86%
rename from superset-frontend/spec/javascripts/explore/components/ControlPanelsContainer_spec.jsx
rename to superset-frontend/spec/javascripts/explore/components/ControlPanelsContainer_spec.tsx
index 35de5b6..9b2fe59 100644
--- a/superset-frontend/spec/javascripts/explore/components/ControlPanelsContainer_spec.jsx
+++ b/superset-frontend/spec/javascripts/explore/components/ControlPanelsContainer_spec.tsx
@@ -18,10 +18,17 @@
*/
import React from 'react';
import { shallow } from 'enzyme';
-import { getChartControlPanelRegistry, t } from '@superset-ui/core';
+import {
+ DatasourceType,
+ getChartControlPanelRegistry,
+ t,
+} from '@superset-ui/core';
import { defaultControls } from 'src/explore/store';
import { getFormDataFromControls } from 'src/explore/controlUtils';
-import { ControlPanelsContainer } from 'src/explore/components/ControlPanelsContainer';
+import {
+ ControlPanelsContainer,
+ ControlPanelsContainerProps,
+} from 'src/explore/components/ControlPanelsContainer';
import Collapse from 'src/common/components/Collapse';
describe('ControlPanelsContainer', () => {
@@ -78,15 +85,15 @@ describe('ControlPanelsContainer', () => {
});
function getDefaultProps() {
+ const controls = defaultControls as ControlPanelsContainerProps['controls'];
return {
- datasource_type: 'table',
+ datasource_type: DatasourceType.Table,
actions: {},
- controls: defaultControls,
- // Note: default viz_type is table
- form_data: getFormDataFromControls(defaultControls),
+ controls,
+ form_data: getFormDataFromControls(controls),
isDatasourceMetaLoading: false,
exploreState: {},
- };
+ } as ControlPanelsContainerProps;
}
it('renders ControlPanelSections', () => {
diff --git a/superset-frontend/src/constants.ts b/superset-frontend/src/constants.ts
index 0a7f156..ad76836 100644
--- a/superset-frontend/src/constants.ts
+++ b/superset-frontend/src/constants.ts
@@ -17,7 +17,6 @@
* under the License.
*/
export const DATETIME_WITH_TIME_ZONE = 'YYYY-MM-DD HH:mm:ssZ';
-
export const TIME_WITH_MS = 'HH:mm:ss.SSS';
export const BOOL_TRUE_DISPLAY = 'True';
diff --git a/superset-frontend/src/explore/actions/exploreActions.ts b/superset-frontend/src/explore/actions/exploreActions.ts
index 4923ae3..7ebfc45 100644
--- a/superset-frontend/src/explore/actions/exploreActions.ts
+++ b/superset-frontend/src/explore/actions/exploreActions.ts
@@ -99,7 +99,7 @@ export const SET_FIELD_VALUE = 'SET_FIELD_VALUE';
export function setControlValue(
controlName: string,
value: any,
- validationErrors: any[],
+ validationErrors?: any[],
) {
return { type: SET_FIELD_VALUE, controlName, value, validationErrors };
}
@@ -109,11 +109,6 @@ export function setExploreControls(formData: QueryFormData) {
return { type: SET_EXPLORE_CONTROLS, formData };
}
-export const REMOVE_CONTROL_PANEL_ALERT = 'REMOVE_CONTROL_PANEL_ALERT';
-export function removeControlPanelAlert() {
- return { type: REMOVE_CONTROL_PANEL_ALERT };
-}
-
export const UPDATE_CHART_TITLE = 'UPDATE_CHART_TITLE';
export function updateChartTitle(sliceName: string) {
return { type: UPDATE_CHART_TITLE, sliceName };
@@ -154,7 +149,6 @@ export const exploreActions = {
saveFaveStar,
setControlValue,
setExploreControls,
- removeControlPanelAlert,
updateChartTitle,
createNewSlice,
sliceUpdated,
diff --git a/superset-frontend/src/explore/components/ControlPanelsContainer.jsx b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
similarity index 69%
rename from superset-frontend/src/explore/components/ControlPanelsContainer.jsx
rename to superset-frontend/src/explore/components/ControlPanelsContainer.tsx
index cc61ab2..fc738e9 100644
--- a/superset-frontend/src/explore/components/ControlPanelsContainer.jsx
+++ b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
@@ -18,30 +18,47 @@
*/
/* eslint camelcase: 0 */
import React from 'react';
-import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
-import { t, styled, getChartControlPanelRegistry } from '@superset-ui/core';
+import {
+ t,
+ styled,
+ getChartControlPanelRegistry,
+ QueryFormData,
+ DatasourceType,
+} from '@superset-ui/core';
import Tabs from 'src/common/components/Tabs';
-import Alert from 'src/components/Alert';
import Collapse from 'src/common/components/Collapse';
import { PluginContext } from 'src/components/DynamicPlugins';
import Loading from 'src/components/Loading';
-import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
+import {
+ ControlPanelSectionConfig,
+ ControlState,
+ CustomControlItem,
+ ExpandedControlItem,
+ InfoTooltipWithTrigger,
+} from '@superset-ui/chart-controls';
import ControlRow from './ControlRow';
import Control from './Control';
import { sectionsToRender } from '../controlUtils';
-import { exploreActions } from '../actions/exploreActions';
+import { ExploreActions, exploreActions } from '../actions/exploreActions';
+import { ExploreState } from '../reducers/getInitialState';
-const propTypes = {
- actions: PropTypes.object.isRequired,
- alert: PropTypes.string,
- datasource_type: PropTypes.string.isRequired,
- exploreState: PropTypes.object.isRequired,
- controls: PropTypes.object.isRequired,
- form_data: PropTypes.object.isRequired,
- isDatasourceMetaLoading: PropTypes.bool.isRequired,
+export type ControlPanelsContainerProps = {
+ actions: ExploreActions;
+ datasource_type: DatasourceType;
+ exploreState: Record<string, any>;
+ controls: Record<string, ControlState>;
+ form_data: QueryFormData;
+ isDatasourceMetaLoading: boolean;
+};
+
+export type ExpandedControlPanelSectionConfig = Omit<
+ ControlPanelSectionConfig,
+ 'controlSetRows'
+> & {
+ controlSetRows: ExpandedControlItem[][];
};
const Styles = styled.div`
@@ -50,9 +67,6 @@ const Styles = styled.div`
overflow: auto;
overflow-x: visible;
overflow-y: auto;
- .remove-alert {
- cursor: pointer;
- }
#controlSections {
min-height: 100%;
overflow: visible;
@@ -80,60 +94,34 @@ const ControlPanelsTabs = styled(Tabs)`
height: 100%;
}
`;
-class ControlPanelsContainer extends React.Component {
+
+class ControlPanelsContainer extends React.Component<ControlPanelsContainerProps> {
// trigger updates to the component when async plugins load
static contextType = PluginContext;
- constructor(props) {
+ constructor(props: ControlPanelsContainerProps) {
super(props);
-
- this.removeAlert = this.removeAlert.bind(this);
this.renderControl = this.renderControl.bind(this);
this.renderControlPanelSection = this.renderControlPanelSection.bind(this);
}
- componentDidUpdate(prevProps) {
- const {
- actions: { setControlValue },
- } = this.props;
- if (this.props.form_data.datasource !== prevProps.form_data.datasource) {
- const defaultValues = [
- 'MetricsControl',
- 'AdhocFilterControl',
- 'TextControl',
- 'SelectControl',
- 'CheckboxControl',
- 'AnnotationLayerControl',
- ];
- Object.entries(this.props.controls).forEach(([controlName, control]) => {
- const { type, default: defaultValue } = control;
- if (defaultValues.includes(type)) {
- setControlValue(controlName, defaultValue);
- }
- });
- }
- }
-
- sectionsToRender() {
+ sectionsToRender(): ExpandedControlPanelSectionConfig[] {
return sectionsToRender(
this.props.form_data.viz_type,
this.props.datasource_type,
);
}
- sectionsToExpand(sections) {
+ sectionsToExpand(sections: ControlPanelSectionConfig[]) {
return sections.reduce(
- (acc, cur) => (cur.expanded ? [...acc, cur.label] : acc),
- [],
+ (acc, section) =>
+ section.expanded ? [...acc, String(section.label)] : acc,
+ [] as string[],
);
}
- removeAlert() {
- this.props.actions.removeControlPanelAlert();
- }
-
- renderControl({ name, config }) {
- const { actions, controls, form_data: formData } = this.props;
+ renderControl({ name, config }: CustomControlItem) {
+ const { actions, controls } = this.props;
const { visibility } = config;
// If the control item is not an object, we have to look up the control data from
@@ -144,11 +132,9 @@ class ControlPanelsContainer extends React.Component {
...controls[name],
name,
};
- const {
- validationErrors,
- provideFormDataToProps,
- ...restProps
- } = controlData;
+ const { validationErrors, ...restProps } = controlData as ControlState & {
+ validationErrors?: any[];
+ };
// if visibility check says the config is not visible, don't render it
if (visibility && !visibility.call(config, this.props, controlData)) {
@@ -160,30 +146,42 @@ class ControlPanelsContainer extends React.Component {
name={name}
validationErrors={validationErrors}
actions={actions}
- formData={provideFormDataToProps ? formData : null}
- datasource={formData?.datasource}
{...restProps}
/>
);
}
- renderControlPanelSection(section) {
+ renderControlPanelSection(section: ExpandedControlPanelSectionConfig) {
const { controls } = this.props;
const { label, description } = section;
+ // Section label can be a ReactNode but in some places we want to
+ // have a string ID. Using forced type conversion for now,
+ // should probably add a `id` field to sections in the future.
+ const sectionId = String(label);
+
const hasErrors = section.controlSetRows.some(rows =>
- rows.some(
- s =>
- controls[s] &&
- controls[s].validationErrors &&
- controls[s].validationErrors.length > 0,
- ),
+ rows.some(item => {
+ const controlName =
+ typeof item === 'string'
+ ? item
+ : item && 'name' in item
+ ? item.name
+ : null;
+ return (
+ controlName &&
+ controlName in controls &&
+ controls[controlName].validationErrors &&
+ controls[controlName].validationErrors.length > 0
+ );
+ }),
);
const PanelHeader = () => (
<span>
<span>{label}</span>{' '}
{description && (
- <InfoTooltipWithTrigger label={label} tooltip={description} />
+ // label is only used in tooltip id (should probably call this prop `id`)
+ <InfoTooltipWithTrigger label={sectionId} tooltip={description} />
)}
{hasErrors && (
<InfoTooltipWithTrigger
@@ -199,7 +197,7 @@ class ControlPanelsContainer extends React.Component {
<Collapse.Panel
className="control-panel-section"
header={PanelHeader()}
- key={section.label}
+ key={sectionId}
>
{section.controlSetRows.map((controlSets, i) => {
const renderedControls = controlSets
@@ -229,7 +227,6 @@ class ControlPanelsContainer extends React.Component {
return (
<ControlRow
key={`controlsetrow-${i}`}
- className="control-row"
controls={renderedControls}
/>
);
@@ -247,8 +244,8 @@ class ControlPanelsContainer extends React.Component {
return <Loading />;
}
- const querySectionsToRender = [];
- const displaySectionsToRender = [];
+ const querySectionsToRender: ExpandedControlPanelSectionConfig[] = [];
+ const displaySectionsToRender: ExpandedControlPanelSectionConfig[] = [];
this.sectionsToRender().forEach(section => {
// if at least one control in the section is not `renderTrigger`
// or asks to be displayed at the Data tab
@@ -258,6 +255,8 @@ class ControlPanelsContainer extends React.Component {
rows.some(
control =>
control &&
+ typeof control === 'object' &&
+ 'config' in control &&
control.config &&
(!control.config.renderTrigger ||
control.config.tabOverride === 'data'),
@@ -277,14 +276,6 @@ class ControlPanelsContainer extends React.Component {
);
return (
<Styles>
- {this.props.alert && (
- <Alert
- type="warning"
- message={this.props.alert}
- closable
- onClose={this.removeAlert}
- />
- )}
<ControlPanelsTabs
id="controlSections"
data-test="control-tabs"
@@ -318,26 +309,19 @@ class ControlPanelsContainer extends React.Component {
}
}
-ControlPanelsContainer.propTypes = propTypes;
-
-function mapStateToProps({ explore }) {
- return {
- alert: explore.controlPanelAlert,
- isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
- controls: explore.controls,
- exploreState: explore,
- };
-}
-
-function mapDispatchToProps(dispatch) {
- return {
- actions: bindActionCreators(exploreActions, dispatch),
- };
-}
-
export { ControlPanelsContainer };
export default connect(
- mapStateToProps,
- mapDispatchToProps,
+ function mapStateToProps({ explore }: ExploreState) {
+ return {
+ isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
+ controls: explore.controls,
+ exploreState: explore,
+ };
+ },
+ function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators(exploreActions, dispatch),
+ };
+ },
)(ControlPanelsContainer);
diff --git a/superset-frontend/src/explore/components/ControlRow.jsx b/superset-frontend/src/explore/components/ControlRow.tsx
similarity index 76%
rename from superset-frontend/src/explore/components/ControlRow.jsx
rename to superset-frontend/src/explore/components/ControlRow.tsx
index 47a0a70..5574af0 100644
--- a/superset-frontend/src/explore/components/ControlRow.jsx
+++ b/superset-frontend/src/explore/components/ControlRow.tsx
@@ -16,20 +16,20 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { ExpandedControlItem } from '@superset-ui/chart-controls';
import React from 'react';
-import PropTypes from 'prop-types';
const NUM_COLUMNS = 12;
-const propTypes = {
- controls: PropTypes.arrayOf(PropTypes.object).isRequired,
-};
-
-function ControlSetRow(props) {
- const colSize = NUM_COLUMNS / props.controls.length;
+export default function ControlRow({
+ controls,
+}: {
+ controls: ExpandedControlItem[];
+}) {
+ const colSize = NUM_COLUMNS / controls.length;
return (
<div className="row space-1">
- {props.controls.map((control, i) => (
+ {controls.map((control, i) => (
<div className={`col-lg-${colSize} col-xs-12`} key={i}>
{control}
</div>
@@ -37,6 +37,3 @@ function ControlSetRow(props) {
</div>
);
}
-
-ControlSetRow.propTypes = propTypes;
-export default ControlSetRow;
diff --git a/superset-frontend/src/explore/components/ExploreViewContainer.jsx b/superset-frontend/src/explore/components/ExploreViewContainer.jsx
index 71d163a..cd72ac7 100644
--- a/superset-frontend/src/explore/components/ExploreViewContainer.jsx
+++ b/superset-frontend/src/explore/components/ExploreViewContainer.jsx
@@ -222,10 +222,7 @@ function ExploreViewContainer(props) {
}
function onQuery() {
- // remove alerts when query
- props.actions.removeControlPanelAlert();
props.actions.triggerQuery(true, props.chart.id);
-
addHistory();
}
diff --git a/superset-frontend/src/explore/components/controls/CollectionControl.jsx b/superset-frontend/src/explore/components/controls/CollectionControl.jsx
index 46b0966..198df6d 100644
--- a/superset-frontend/src/explore/components/controls/CollectionControl.jsx
+++ b/superset-frontend/src/explore/components/controls/CollectionControl.jsx
@@ -69,12 +69,6 @@ export default class CollectionControl extends React.Component {
this.onAdd = this.onAdd.bind(this);
}
- componentDidUpdate(prevProps) {
- if (prevProps.datasource.name !== this.props.datasource.name) {
- this.props.onChange([]);
- }
- }
-
onChange(i, value) {
Object.assign(this.props.value[i], value);
this.props.onChange(this.props.value);
diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterControl.tsx b/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterControl.tsx
index 0e84bc3..f15da37 100644
--- a/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterControl.tsx
+++ b/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterControl.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useMemo } from 'react';
import rison from 'rison';
import {
SupersetClient,
@@ -25,6 +25,7 @@ import {
t,
TimeRangeEndpoints,
} from '@superset-ui/core';
+import { DatasourceMeta } from '@superset-ui/chart-controls';
import {
buildTimeRangeString,
formatTimeRange,
@@ -38,6 +39,8 @@ import { Divider } from 'src/common/components';
import Icon from 'src/components/Icon';
import { Select } from 'src/components/Select';
import { Tooltip } from 'src/common/components/Tooltip';
+import { DEFAULT_TIME_RANGE } from 'src/explore/constants';
+
import { SelectOptionType, FrameType } from './types';
import {
COMMON_RANGE_VALUES_SET,
@@ -165,28 +168,27 @@ const IconWrapper = styled.span`
}
`;
-interface DateFilterLabelProps {
+interface DateFilterControlProps {
name: string;
onChange: (timeRange: string) => void;
value?: string;
endpoints?: TimeRangeEndpoints;
- datasource?: string;
+ datasource?: DatasourceMeta;
}
-export default function DateFilterControl(props: DateFilterLabelProps) {
- const { value = 'Last week', endpoints, onChange, datasource } = props;
+export default function DateFilterControl(props: DateFilterControlProps) {
+ const { value = DEFAULT_TIME_RANGE, endpoints, onChange } = props;
const [actualTimeRange, setActualTimeRange] = useState<string>(value);
const [show, setShow] = useState<boolean>(false);
- const [frame, setFrame] = useState<FrameType>(guessFrame(value));
- const [isMounted, setIsMounted] = useState<boolean>(false);
+ const guessedFrame = useMemo(() => guessFrame(value), [value]);
+ const [frame, setFrame] = useState<FrameType>(guessedFrame);
const [timeRangeValue, setTimeRangeValue] = useState(value);
const [validTimeRange, setValidTimeRange] = useState<boolean>(false);
const [evalResponse, setEvalResponse] = useState<string>(value);
const [tooltipTitle, setTooltipTitle] = useState<string>(value);
useEffect(() => {
- if (!isMounted) setIsMounted(true);
fetchTimeRange(value, endpoints).then(({ value: actualRange, error }) => {
if (error) {
setEvalResponse(error || '');
@@ -205,9 +207,9 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
+--------------+------+----------+--------+----------+-----------+
*/
if (
- frame === 'Common' ||
- frame === 'Calendar' ||
- frame === 'No filter'
+ guessedFrame === 'Common' ||
+ guessedFrame === 'Calendar' ||
+ guessedFrame === 'No filter'
) {
setActualTimeRange(value);
setTooltipTitle(actualRange || '');
@@ -221,14 +223,6 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
}, [value]);
useEffect(() => {
- if (isMounted) {
- onChange('Last week');
- setTimeRangeValue('Last week');
- setFrame(guessFrame('Last week'));
- }
- }, [datasource]);
-
- useEffect(() => {
fetchTimeRange(timeRangeValue, endpoints).then(({ value, error }) => {
if (error) {
setEvalResponse(error || '');
@@ -247,13 +241,13 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
function onOpen() {
setTimeRangeValue(value);
- setFrame(guessFrame(value));
+ setFrame(guessedFrame);
setShow(true);
}
function onHide() {
setTimeRangeValue(value);
- setFrame(guessFrame(value));
+ setFrame(guessedFrame);
setShow(false);
}
@@ -265,7 +259,7 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
}
};
- function onFrame(option: SelectOptionType) {
+ function onChangeFrame(option: SelectOptionType) {
if (option.value === 'No filter') {
setTimeRangeValue('No filter');
}
@@ -278,7 +272,7 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
<Select
options={FRAME_OPTIONS}
value={FRAME_OPTIONS.filter(({ value }) => value === frame)}
- onChange={onFrame}
+ onChange={onChangeFrame}
className="frame-dropdown"
/>
{frame !== 'No filter' && <Divider />}
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl.jsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl.jsx
index 9ae816a..0fd2505 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl.jsx
+++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl.jsx
@@ -18,7 +18,13 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
-import { t, logging, SupersetClient, withTheme } from '@superset-ui/core';
+import {
+ t,
+ logging,
+ SupersetClient,
+ withTheme,
+ ensureIsArray,
+} from '@superset-ui/core';
import ControlHeader from 'src/explore/components/ControlHeader';
import adhocMetricType from 'src/explore/components/controls/MetricControl/adhocMetricType';
@@ -39,6 +45,11 @@ import AdhocFilterOption from './AdhocFilterOption';
import AdhocFilter, { CLAUSES, EXPRESSION_TYPES } from './AdhocFilter';
import adhocFilterType from './adhocFilterType';
+const selectedMetricType = PropTypes.oneOfType([
+ PropTypes.string,
+ adhocMetricType,
+]);
+
const propTypes = {
name: PropTypes.string,
onChange: PropTypes.func,
@@ -46,12 +57,10 @@ const propTypes = {
datasource: PropTypes.object,
columns: PropTypes.arrayOf(columnType),
savedMetrics: PropTypes.arrayOf(savedMetricType),
- formData: PropTypes.shape({
- metric: PropTypes.oneOfType([PropTypes.string, adhocMetricType]),
- metrics: PropTypes.arrayOf(
- PropTypes.oneOfType([PropTypes.string, adhocMetricType]),
- ),
- }),
+ selectedMetrics: PropTypes.oneOfType([
+ selectedMetricType,
+ PropTypes.arrayOf(selectedMetricType),
+ ]),
isLoading: PropTypes.bool,
};
@@ -60,7 +69,7 @@ const defaultProps = {
onChange: () => {},
columns: [],
savedMetrics: [],
- formData: {},
+ selectedMetrics: [],
};
function isDictionaryForAdhocFilter(value) {
@@ -141,10 +150,7 @@ class AdhocFilterControl extends React.Component {
}
UNSAFE_componentWillReceiveProps(nextProps) {
- if (
- this.props.columns !== nextProps.columns ||
- this.props.formData !== nextProps.formData
- ) {
+ if (this.props.columns !== nextProps.columns) {
this.setState({ options: this.optionsForSelect(nextProps) });
}
if (this.props.value !== nextProps.value) {
@@ -270,7 +276,7 @@ class AdhocFilterControl extends React.Component {
optionsForSelect(props) {
const options = [
...props.columns,
- ...[...(props.formData?.metrics || []), props.formData?.metric].map(
+ ...ensureIsArray(props.selectedMetrics).map(
metric =>
metric &&
(typeof metric === 'string'
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover.jsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover.jsx
index 4d2e3a8..81405e5 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover.jsx
@@ -123,9 +123,6 @@ export default class AdhocMetricEditPopover extends React.PureComponent {
adhocMetricLabel: this.state.adhocMetric?.getDefaultLabel(),
});
}
- if (prevProps.datasource !== this.props.datasource) {
- this.props.onChange(null);
- }
}
componentWillUnmount() {
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.jsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.jsx
index 7d3e4c1..bc5a781 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.jsx
@@ -60,7 +60,6 @@ class AdhocMetricOption extends React.PureComponent {
onMoveLabel,
onDropLabel,
index,
- datasource,
} = this.props;
return (
@@ -70,7 +69,6 @@ class AdhocMetricOption extends React.PureComponent {
columns={columns}
savedMetricsOptions={savedMetricsOptions}
savedMetric={savedMetric}
- datasource={datasource}
datasourceType={datasourceType}
>
<OptionControlLabel
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger.tsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger.tsx
index 9964861..dd7b4bf 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger.tsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger.tsx
@@ -33,7 +33,6 @@ export type AdhocMetricPopoverTriggerProps = {
savedMetricsOptions: savedMetricType[];
savedMetric: savedMetricType;
datasourceType: string;
- datasource: string;
children: ReactNode;
createNew?: boolean;
};
@@ -160,7 +159,6 @@ class AdhocMetricPopoverTrigger extends React.PureComponent<
columns={this.props.columns}
savedMetricsOptions={this.props.savedMetricsOptions}
savedMetric={this.props.savedMetric}
- datasource={this.props.datasource}
datasourceType={this.props.datasourceType}
onResize={this.onPopoverResize}
onClose={this.closePopover}
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx b/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx
index 6ef84a3..5abca1f 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx
@@ -38,7 +38,6 @@ const propTypes = {
savedMetricsOptions: PropTypes.arrayOf(savedMetricType),
multi: PropTypes.bool,
datasourceType: PropTypes.string,
- datasource: PropTypes.string,
};
export default function MetricDefinitionValue({
@@ -52,16 +51,15 @@ export default function MetricDefinitionValue({
onMoveLabel,
onDropLabel,
index,
- datasource,
}) {
const getSavedMetricByName = metricName =>
savedMetrics.find(metric => metric.metric_name === metricName);
let savedMetric;
- if (option.metric_name) {
- savedMetric = option;
- } else if (typeof option === 'string') {
+ if (typeof option === 'string') {
savedMetric = getSavedMetricByName(option);
+ } else if (option.metric_name) {
+ savedMetric = option;
}
if (option instanceof AdhocMetric || savedMetric) {
@@ -79,7 +77,6 @@ export default function MetricDefinitionValue({
onDropLabel,
index,
savedMetric: savedMetric ?? {},
- datasource,
};
return <AdhocMetricOption {...metricOptionProps} />;
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.jsx b/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.jsx
index 89bec24..50591c3 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.jsx
@@ -179,10 +179,10 @@ class MetricsControl extends React.PureComponent {
) {
this.setState({ options: this.optionsForSelect(nextProps) });
- // Remove metrics if selected value no longer a column
- const containsAllMetrics = columnsContainAllMetrics(value, nextProps);
-
- if (!containsAllMetrics) {
+ // Remove all metrics if selected value no longer a valid column
+ // in the dataset. Must use `nextProps` here because Redux reducers may
+ // have already updated the value for this control.
+ if (!columnsContainAllMetrics(nextProps.value, nextProps)) {
this.props.onChange([]);
}
}
diff --git a/superset-frontend/src/explore/components/controls/TextControl.tsx b/superset-frontend/src/explore/components/controls/TextControl.tsx
index 43c8c77..93c69e5 100644
--- a/superset-frontend/src/explore/components/controls/TextControl.tsx
+++ b/superset-frontend/src/explore/components/controls/TextControl.tsx
@@ -17,71 +17,55 @@
* under the License.
*/
import React from 'react';
-import { FormGroup, FormControl } from 'react-bootstrap';
+import { FormGroup, FormControl, FormControlProps } from 'react-bootstrap';
import { legacyValidateNumber, legacyValidateInteger } from '@superset-ui/core';
import debounce from 'lodash/debounce';
-import ControlHeader from '../ControlHeader';
+import { FAST_DEBOUNCE } from 'src/constants';
+import ControlHeader from 'src/explore/components/ControlHeader';
-interface TextControlProps {
+type InputValueType = string | number;
+
+export interface TextControlProps<T extends InputValueType = InputValueType> {
disabled?: boolean;
isFloat?: boolean;
isInt?: boolean;
- onChange?: (value: any, errors: any) => {};
+ onChange?: (value: T, errors: any) => {};
onFocus?: () => {};
placeholder?: string;
- value?: string | number;
+ value?: T | null;
controlId?: string;
renderTrigger?: boolean;
- datasource?: string;
}
-interface TextControlState {
+export interface TextControlState {
controlId: string;
- currentDatasource?: string;
- value?: string | number;
+ value: string;
}
const generateControlId = (controlId?: string) =>
`formInlineName_${controlId ?? (Math.random() * 1000000).toFixed()}`;
-export default class TextControl extends React.Component<
- TextControlProps,
- TextControlState
-> {
- debouncedOnChange = debounce((inputValue: string) => {
- this.onChange(inputValue);
- }, 500);
+const safeStringify = (value?: InputValueType | null) =>
+ value == null ? '' : String(value);
- static getDerivedStateFromProps(
- props: TextControlProps,
- state: TextControlState,
- ) {
- // reset value when datasource changes
- // props.datasource and props.value don't update in the same re-render,
- // so we need to synchronize them to update the state with correct values
- if (
- props.value !== state.value &&
- props.datasource !== state.currentDatasource
- ) {
- return { value: props.value, currentDatasource: props.datasource };
- }
- return null;
- }
+export default class TextControl<
+ T extends InputValueType = InputValueType
+> extends React.Component<TextControlProps<T>, TextControlState> {
+ initialValue?: TextControlProps['value'];
- constructor(props: TextControlProps) {
+ constructor(props: TextControlProps<T>) {
super(props);
-
- // if there's no control id provided, generate a random
- // number to prevent rendering elements with same ids
+ this.initialValue = props.value;
this.state = {
+ // if there's no control id provided, generate a random
+ // number to prevent rendering elements with same ids
controlId: generateControlId(props.controlId),
- value: props.value,
- currentDatasource: props.datasource,
+ value: safeStringify(this.initialValue),
};
}
onChange = (inputValue: string) => {
- let parsedValue: string | number = inputValue;
+ let parsedValue: InputValueType = inputValue;
// Validation & casting
const errors = [];
if (inputValue !== '' && this.props.isFloat) {
@@ -102,26 +86,26 @@ export default class TextControl extends React.Component<
parsedValue = parseInt(inputValue, 10);
}
}
- this.props.onChange?.(parsedValue, errors);
+ this.props.onChange?.(parsedValue as T, errors);
};
- onChangeWrapper = (event: any) => {
- const { value } = event.target;
- this.setState({ value });
+ debouncedOnChange = debounce((inputValue: string) => {
+ this.onChange(inputValue);
+ }, FAST_DEBOUNCE);
- // use debounce when change takes effect immediately after user starts typing
- const onChange = this.props.renderTrigger
- ? this.debouncedOnChange
- : this.onChange;
- onChange(value);
+ onChangeWrapper: FormControlProps['onChange'] = event => {
+ const { value } = event.target as HTMLInputElement;
+ this.setState({ value }, () => {
+ this.debouncedOnChange(value);
+ });
};
render = () => {
- const { value: rawValue } = this.state;
- const value =
- typeof rawValue !== 'undefined' && rawValue !== null
- ? rawValue.toString()
- : '';
+ let { value } = this.state;
+ if (this.initialValue !== this.props.value) {
+ this.initialValue = this.props.value;
+ value = safeStringify(this.props.value);
+ }
return (
<div>
<ControlHeader {...this.props} />
diff --git a/superset-frontend/src/explore/constants.js b/superset-frontend/src/explore/constants.ts
similarity index 94%
rename from superset-frontend/src/explore/constants.js
rename to superset-frontend/src/explore/constants.ts
index d9f083b..06fa62c 100644
--- a/superset-frontend/src/explore/constants.js
+++ b/superset-frontend/src/explore/constants.ts
@@ -55,12 +55,7 @@ export const HAVING_OPERATORS = [
OPERATORS['>='],
OPERATORS['<='],
];
-export const MULTI_OPERATORS = new Set([
- OPERATORS.in,
- OPERATORS['not in'],
- OPERATORS.IN,
- OPERATORS['NOT IN'],
-]);
+export const MULTI_OPERATORS = new Set([OPERATORS.IN, OPERATORS['NOT IN']]);
// CUSTOM_OPERATORS will show operator in simple mode,
// but will generate customized sqlExpression
export const CUSTOM_OPERATORS = new Set([OPERATORS['LATEST PARTITION']]);
@@ -103,3 +98,6 @@ export const TIME_FILTER_MAP = {
druid_time_origin: '__time_origin',
granularity: '__granularity',
};
+
+// TODO: make this configurable per Superset installation
+export const DEFAULT_TIME_RANGE = 'Last week';
diff --git a/superset-frontend/src/types/bootstrapTypes.ts b/superset-frontend/src/explore/controlUtils/getFormDataFromControls.ts
similarity index 63%
copy from superset-frontend/src/types/bootstrapTypes.ts
copy to superset-frontend/src/explore/controlUtils/getFormDataFromControls.ts
index cb8fb1e..f5ffa52 100644
--- a/superset-frontend/src/types/bootstrapTypes.ts
+++ b/superset-frontend/src/explore/controlUtils/getFormDataFromControls.ts
@@ -16,29 +16,19 @@
* specific language governing permissions and limitations
* under the License.
*/
-export type User = {
- createdOn: string;
- email: string;
- firstName: string;
- isActive: boolean;
- lastName: string;
- userId: number;
- username: string;
-};
+import { QueryFormData } from '@superset-ui/core';
+import { ControlStateMapping } from '@superset-ui/chart-controls';
-export interface UserWithPermissionsAndRoles extends User {
- permissions: {
- database_access?: string[];
- datasource_access?: string[];
+export function getFormDataFromControls(
+ controlsState: ControlStateMapping,
+): QueryFormData {
+ const formData: QueryFormData = {
+ viz_type: 'table',
+ datasource: '',
};
- roles: Record<string, string[][]>;
+ Object.keys(controlsState).forEach(controlName => {
+ const control = controlsState[controlName];
+ formData[controlName] = control.value;
+ });
+ return formData;
}
-
-export type Dashboard = {
- dttm: number;
- id: number;
- url: string;
- title: string;
- creator?: string;
- creator_url?: string;
-};
diff --git a/superset-frontend/src/explore/controlUtils.js b/superset-frontend/src/explore/controlUtils/index.js
similarity index 95%
rename from superset-frontend/src/explore/controlUtils.js
rename to superset-frontend/src/explore/controlUtils/index.js
index ae6dcc7..4e1f0ac 100644
--- a/superset-frontend/src/explore/controlUtils.js
+++ b/superset-frontend/src/explore/controlUtils/index.js
@@ -19,16 +19,9 @@
import memoizeOne from 'memoize-one';
import { getChartControlPanelRegistry } from '@superset-ui/core';
import { expandControlConfig } from '@superset-ui/chart-controls';
-import * as SECTIONS from './controlPanels/sections';
-
-export function getFormDataFromControls(controlsState) {
- const formData = {};
- Object.keys(controlsState).forEach(controlName => {
- const control = controlsState[controlName];
- formData[controlName] = control.value;
- });
- return formData;
-}
+import * as SECTIONS from '../controlPanels/sections';
+
+export * from './getFormDataFromControls';
export function validateControl(control, processedState) {
const { validators } = control;
diff --git a/superset-frontend/src/explore/controls.jsx b/superset-frontend/src/explore/controls.jsx
index dfbfd21..c37f5e7 100644
--- a/superset-frontend/src/explore/controls.jsx
+++ b/superset-frontend/src/explore/controls.jsx
@@ -355,8 +355,9 @@ export const controls = {
"using the engine's local timezone. Note one can explicitly set the timezone " +
'per the ISO 8601 format if specifying either the start and/or end time.',
),
- mapStateToProps: state => ({
- endpoints: state.form_data ? state.form_data.time_range_endpoints : null,
+ mapStateToProps: ({ form_data: formData }) => ({
+ // eslint-disable-next-line camelcase
+ endpoints: formData?.time_range_endpoints,
}),
},
@@ -474,7 +475,6 @@ export const controls = {
savedMetrics: state.datasource ? state.datasource.metrics : [],
datasource: state.datasource,
}),
- provideFormDataToProps: true,
},
color_scheme: {
diff --git a/superset-frontend/src/explore/reducers/exploreReducer.js b/superset-frontend/src/explore/reducers/exploreReducer.js
index 3076727..7727049 100644
--- a/superset-frontend/src/explore/reducers/exploreReducer.js
+++ b/superset-frontend/src/explore/reducers/exploreReducer.js
@@ -18,13 +18,14 @@
*/
/* eslint camelcase: 0 */
import { DYNAMIC_PLUGIN_CONTROLS_READY } from 'src/chart/chartAction';
-import { getControlsState } from '../store';
+import { DEFAULT_TIME_RANGE } from 'src/explore/constants';
+import { getControlsState } from 'src/explore/store';
import {
getControlConfig,
getFormDataFromControls,
getControlStateFromControlConfig,
-} from '../controlUtils';
-import * as actions from '../actions/exploreActions';
+} from 'src/explore/controlUtils';
+import * as actions from 'src/explore/actions/exploreActions';
export default function exploreReducer(state = {}, action) {
const actionHandlers = {
@@ -61,8 +62,38 @@ export default function exploreReducer(state = {}, action) {
delete newFormData.time_grain_sqla;
}
}
+
+ const controls = { ...state.controls };
+ if (
+ action.datasource.id !== state.datasource.id ||
+ action.datasource.type !== state.datasource.type
+ ) {
+ // reset time range filter to default
+ newFormData.time_range = DEFAULT_TIME_RANGE;
+
+ // reset control values for column/metric related controls
+ Object.entries(controls).forEach(([controlName, controlState]) => {
+ if (
+ // for direct column select controls
+ controlState.valueKey === 'column_name' ||
+ // for all other controls
+ 'columns' in controlState
+ ) {
+ // if a control use datasource columns, reset its value to `undefined`,
+ // then `getControlsState` will pick up the default.
+ // TODO: filter out only invalid columns and keep others
+ controls[controlName] = {
+ ...controlState,
+ value: undefined,
+ };
+ newFormData[controlName] = undefined;
+ }
+ });
+ }
+
const newState = {
...state,
+ controls,
datasource: action.datasource,
datasource_id: action.datasource.id,
datasource_type: action.datasource.type,
@@ -85,12 +116,6 @@ export default function exploreReducer(state = {}, action) {
datasources: action.datasources,
};
},
- [actions.REMOVE_CONTROL_PANEL_ALERT]() {
- return {
- ...state,
- controlPanelAlert: null,
- };
- },
[actions.SET_FIELD_VALUE]() {
const new_form_data = state.form_data;
const { controlName, value, validationErrors } = action;
diff --git a/superset-frontend/src/explore/reducers/getInitialState.js b/superset-frontend/src/explore/reducers/getInitialState.ts
similarity index 53%
rename from superset-frontend/src/explore/reducers/getInitialState.js
rename to superset-frontend/src/explore/reducers/getInitialState.ts
index 0774340..9cb02a6 100644
--- a/superset-frontend/src/explore/reducers/getInitialState.js
+++ b/superset-frontend/src/explore/reducers/getInitialState.ts
@@ -17,41 +17,72 @@
* under the License.
*/
import shortid from 'shortid';
+import {
+ Datasource,
+ DatasourceType,
+ JsonObject,
+ QueryFormData,
+} from '@superset-ui/core';
+import { Slice } from 'src/types/Chart';
+import { CommonBootstrapData } from 'src/types/bootstrapTypes';
-import getToastsFromPyFlashMessages from '../../messageToasts/utils/getToastsFromPyFlashMessages';
-import { getChartKey } from '../exploreUtils';
-import { getControlsState } from '../store';
+import getToastsFromPyFlashMessages from 'src/messageToasts/utils/getToastsFromPyFlashMessages';
+import { getChartKey } from 'src/explore/exploreUtils';
+import { getControlsState } from 'src/explore/store';
import {
getFormDataFromControls,
applyMapStateToPropsToControl,
-} from '../controlUtils';
+} from 'src/explore/controlUtils';
+import { ControlStateMapping } from '@superset-ui/chart-controls';
+
+export interface ExlorePageBootstrapData extends JsonObject {
+ can_add: boolean;
+ can_download: boolean;
+ can_overwrite: boolean;
+ datasource: Datasource;
+ form_data: QueryFormData;
+ datasource_id: number;
+ datasource_type: DatasourceType;
+ slice: Slice | null;
+ standalone: boolean;
+ user_id: number;
+ forced_height: string | null;
+ common: CommonBootstrapData;
+}
-export default function getInitialState(bootstrapData) {
- const { form_data: rawFormData } = bootstrapData;
+export default function getInitialState(
+ bootstrapData: ExlorePageBootstrapData,
+) {
+ const { form_data: initialFormData } = bootstrapData;
const { slice } = bootstrapData;
const sliceName = slice ? slice.slice_name : null;
- const bootstrappedState = {
+
+ const exploreState = {
+ // note this will add `form_data` to state,
+ // which will be manipulatable by future reducers.
...bootstrapData,
sliceName,
common: {
flash_messages: bootstrapData.common.flash_messages,
conf: bootstrapData.common.conf,
},
- rawFormData,
- filterColumnOpts: [],
isDatasourceMetaLoading: false,
isStarred: false,
+ // Initial control state will skip `control.mapStateToProps`
+ // because `bootstrapData.controls` is undefined.
+ controls: getControlsState(
+ bootstrapData,
+ initialFormData,
+ ) as ControlStateMapping,
};
- const controls = getControlsState(bootstrappedState, rawFormData);
- bootstrappedState.controls = controls;
// apply initial mapStateToProps for all controls, must execute AFTER
- // bootstrappedState has initialized `controls`. Order of execution is not
+ // bootstrapState has initialized `controls`. Order of execution is not
// guaranteed, so controls shouldn't rely on the each other's mapped state.
- Object.entries(controls).forEach(([key, controlState]) => {
- controls[key] = applyMapStateToPropsToControl(
+ Object.entries(exploreState.controls).forEach(([key, controlState]) => {
+ exploreState.controls[key] = applyMapStateToPropsToControl(
controlState,
- bootstrappedState,
+ exploreState,
);
});
@@ -59,7 +90,7 @@ export default function getInitialState(bootstrapData) {
? getFormDataFromControls(getControlsState(bootstrapData, slice.form_data))
: null;
- const chartKey = getChartKey(bootstrappedState);
+ const chartKey: number = getChartKey(bootstrapData);
return {
charts: {
@@ -69,7 +100,7 @@ export default function getInitialState(bootstrapData) {
chartStatus: null,
chartUpdateEndTime: null,
chartUpdateStartTime: 0,
- latestQueryFormData: getFormDataFromControls(controls),
+ latestQueryFormData: getFormDataFromControls(exploreState.controls),
sliceFormData,
queryController: null,
queriesResponse: null,
@@ -81,10 +112,12 @@ export default function getInitialState(bootstrapData) {
dashboards: [],
saveModalAlert: null,
},
- explore: bootstrappedState,
+ explore: exploreState,
impressionId: shortid.generate(),
messageToasts: getToastsFromPyFlashMessages(
(bootstrapData.common || {}).flash_messages || [],
),
};
}
+
+export type ExploreState = ReturnType<typeof getInitialState>;
diff --git a/superset-frontend/src/types/Chart.ts b/superset-frontend/src/types/Chart.ts
index cf78dab..02b9025 100644
--- a/superset-frontend/src/types/Chart.ts
+++ b/superset-frontend/src/types/Chart.ts
@@ -21,6 +21,7 @@
* The Chart model as returned from the API
*/
+import { QueryFormData } from '@superset-ui/core';
import Owner from './Owner';
export interface Chart {
@@ -45,7 +46,7 @@ export type Slice = {
slice_name: string;
description: string | null;
cache_timeout: number | null;
- url?: string;
+ form_data?: QueryFormData;
};
export default Chart;
diff --git a/superset-frontend/src/types/bootstrapTypes.ts b/superset-frontend/src/types/bootstrapTypes.ts
index cb8fb1e..7ef7caa 100644
--- a/superset-frontend/src/types/bootstrapTypes.ts
+++ b/superset-frontend/src/types/bootstrapTypes.ts
@@ -1,3 +1,5 @@
+import { JsonObject, Locale } from '@superset-ui/core';
+
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@@ -42,3 +44,10 @@ export type Dashboard = {
creator?: string;
creator_url?: string;
};
+
+export interface CommonBootstrapData {
+ flash_messages: string[][];
+ conf: JsonObject;
+ locale: Locale;
+ feature_flags: Record<string, boolean>;
+}