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>;
+}