This is an automated email from the ASF dual-hosted git repository.

lyndsi 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 ba0c37d3df feat(explore): Frontend implementation of dataset creation 
from infobox (#19855)
ba0c37d3df is described below

commit ba0c37d3df85b1af39404af1d578daeb0ff2d278
Author: Lyndsi Kay Williams <[email protected]>
AuthorDate: Tue Jun 7 15:03:45 2022 -0500

    feat(explore): Frontend implementation of dataset creation from infobox 
(#19855)
    
    * Frontend implementation of create dataset from infobox
    
    * Fixed sl_dataset type
    
    * Fix test
    
    * Fixed sl_dataset type (forgot to save)
    
    * RTL testing
    
    * Adjusted styling/text on infobox and save dataset modal
    
    * Appease lint
    
    * Make infobox invisible and fix tests
    
    * Remove unnecessary placeholder
    
    * Move types to sql lab
    
    * Moved logic into save dataset modal
    
    * Change DatasourceMeta type to Dataset
    
    * Add ExploreDatasource union type to save dataset modal
    
    * Get user info from redux inside save dataset modal
    
    * Addressed comments
    
    * Adjusting to new query type
    
    * Fixed save dataset in explore and union type
    
    * Added testing
    
    * Defined d for queries
    
    * Remove dataset from SaveDatasetModal
    
    * Clarify useSelector parameter
    
    * Fix dndControls union type
    
    * Fix shared-controls union type
    
    * Fix controlPanel union type
    
    * Move ExploreRootState to explore type file
    
    * Remove unnecessary testing playground
    
    * Move datasource type check in DatasourcePanel to a function
    
    * Make all sqllab Query imports reference @superset-ui/core Query type
    
    * Deconstruct query props in ResultSet
    
    * Fix union type in /legacy-plugin-chart-heatmap/src/controlPanel
    
    * Change SaveDatasetModal tests to RTL
    
    * Cleaned datasourceTypeCheck
    
    * Fix infobox styling
    
    * Fix SaveDatasetModal test
    
    * Fix query fixture in sqllab and Query type in SaveDatasetModal test
    
    * Fix Query type and make test query fixture
    
    * Added columns to Query type, separated results property, created 
QueryResponse union type, and fixed all types affected
    
    * Fixed a couple missed broken types
    
    * Added ExploreDatasource to SqlLab type file
    
    * Removed unneeded Query import from DatasourcePanel
    
    * Address PR comments
    
    * Fix columnChoices
    
    * Fix all incorrect column property checks
    
    * Fix logic on dndGroupByControl
    
    * Dry up savedMetrics type check
    
    * Fixed TIME_COLUMN_OPTION
    
    * Dried savedMetrics type check even further
    
    * Change savedMetricsTypeCheck to defineSavedMetrics
    
    * Change datasourceTypeCheck to isValidDatasourceType
    
    * Fix Query path in groupByControl
    
    * dnd_granularity_sqla now sorts Query types with is_dttm at the top
    
    * Fixed/cleaned query sort
    
    * Add sortedQueryColumns and proper optional chaining to granularity_sqla
    
    * Move testQuery to core-ui, add test coverage for Queries in columnChoices
    
    * Moved DEFAULT_METRICS to core-ui and wrote a test for defineSavedMetrics
    
    * Add license and clean dataset test object
    
    * Change DatasourceType.Dataset to dataset
---
 .../superset-ui-chart-controls/src/constants.ts    |  17 +-
 .../src/shared-controls/dndControls.tsx            |  62 ++-
 .../src/shared-controls/index.tsx                  |  58 ++-
 .../superset-ui-chart-controls/src/types.ts        |   7 +-
 .../src/utils/columnChoices.ts                     |  24 +-
 .../src/utils/defineSavedMetrics.ts}               |  17 +-
 .../superset-ui-chart-controls/src/utils/index.ts  |   1 +
 .../test/utils/columnChoices.test.tsx              |  13 +-
 ...hoices.test.tsx => defineSavedMetrics.test.tsx} |  51 +--
 .../superset-ui-core/src/query/types/Datasource.ts |  11 +
 .../superset-ui-core/src/query/types/Query.ts      | 210 ++++++++++
 .../src/controlPanel.tsx                           |   2 +-
 .../src/utilities/controls.jsx                     |   2 +-
 .../src/plugin/controls/columns.tsx                |  12 +-
 .../src/plugin/controls/metrics.tsx                |  13 +-
 .../src/plugin/controls/orderBy.tsx                |   6 +-
 .../src/plugin/controlPanel.tsx                    |   7 +-
 .../plugin-chart-table/src/controlPanel.tsx        |  31 +-
 .../components/ExploreCtasResultsButton/index.tsx  |   4 +-
 .../src/SqlLab/components/QueryHistory/index.tsx   |   5 +-
 .../src/SqlLab/components/QuerySearch/index.tsx    |   5 +-
 .../SqlLab/components/QueryStateLabel/index.tsx    |   2 +-
 .../src/SqlLab/components/QueryTable/index.tsx     |  16 +-
 .../src/SqlLab/components/ResultSet/index.tsx      | 360 ++---------------
 .../SaveDatasetModal/SaveDatasetModal.test.tsx     |  86 +++--
 .../SqlLab/components/SaveDatasetModal/index.tsx   | 425 +++++++++++++++------
 .../src/SqlLab/components/TabStatusIcon/index.tsx  |   2 +-
 superset-frontend/src/SqlLab/types.ts              | 114 +++---
 .../dashboard/components/FiltersBadge/selectors.ts |   4 +-
 .../FiltersConfigModal/FiltersConfigForm/utils.ts  |   8 +-
 superset-frontend/src/dashboard/types.ts           |   4 +-
 .../src/explore/actions/exploreActions.ts          |   6 +-
 .../explore/components/ControlPanelsContainer.tsx  |   8 +-
 .../DatasourcePanel/DatasourcePanel.test.tsx       |  74 +++-
 .../explore/components/DatasourcePanel/index.tsx   |  66 +++-
 .../components/ExploreViewContainer/index.jsx      |   1 +
 .../getControlValuesCompatibleWithDatasource.ts    |  10 +-
 superset-frontend/src/explore/fixtures.tsx         |   2 +-
 .../src/explore/reducers/getInitialState.ts        |   7 +-
 superset-frontend/src/explore/types.ts             |  32 +-
 40 files changed, 1069 insertions(+), 716 deletions(-)

diff --git 
a/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts 
b/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts
index 5e16956c60..265874f5e6 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts
@@ -16,7 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { t, QueryMode, DTTM_ALIAS, GenericDataType } from '@superset-ui/core';
+import {
+  t,
+  QueryMode,
+  DTTM_ALIAS,
+  GenericDataType,
+  QueryColumn,
+  DatasourceType,
+} from '@superset-ui/core';
 import { ColumnMeta } from './types';
 
 // eslint-disable-next-line import/prefer-default-export
@@ -32,7 +39,7 @@ export const COLUMN_NAME_ALIASES: Record<string, string> = {
   [DTTM_ALIAS]: t('Time'),
 };
 
-export const TIME_COLUMN_OPTION: ColumnMeta = {
+export const DATASET_TIME_COLUMN_OPTION: ColumnMeta = {
   verbose_name: COLUMN_NAME_ALIASES[DTTM_ALIAS],
   column_name: DTTM_ALIAS,
   type_generic: GenericDataType.TEMPORAL,
@@ -41,6 +48,12 @@ export const TIME_COLUMN_OPTION: ColumnMeta = {
   ),
 };
 
+export const QUERY_TIME_COLUMN_OPTION: QueryColumn = {
+  name: DTTM_ALIAS,
+  type: DatasourceType.Query,
+  is_dttm: false,
+};
+
 export const QueryModeLabel = {
   [QueryMode.aggregate]: t('Aggregate'),
   [QueryMode.raw]: t('Raw records'),
diff --git 
a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx
 
b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx
index 44e0d2fb63..ce63590f74 100644
--- 
a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx
+++ 
b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx
@@ -20,11 +20,14 @@
 import {
   FeatureFlag,
   isFeatureEnabled,
+  QueryColumn,
+  QueryResponse,
   t,
   validateNonEmpty,
 } from '@superset-ui/core';
-import { ExtraControlProps, SharedControlConfig } from '../types';
-import { TIME_COLUMN_OPTION, TIME_FILTER_LABELS } from '../constants';
+import { ExtraControlProps, SharedControlConfig, Dataset } from '../types';
+import { DATASET_TIME_COLUMN_OPTION, TIME_FILTER_LABELS } from '../constants';
+import { QUERY_TIME_COLUMN_OPTION, defineSavedMetrics } from '..';
 
 export const dndGroupByControl: SharedControlConfig<'DndColumnSelect'> = {
   type: 'DndColumnSelect',
@@ -36,15 +39,25 @@ export const dndGroupByControl: 
SharedControlConfig<'DndColumnSelect'> = {
   ),
   mapStateToProps(state, { includeTime }) {
     const newState: ExtraControlProps = {};
-    if (state.datasource) {
-      const options = state.datasource.columns.filter(c => c.groupby);
+    const { datasource } = state;
+    if (datasource?.columns[0]?.hasOwnProperty('groupby')) {
+      const options = (datasource as Dataset).columns.filter(c => c.groupby);
       if (includeTime) {
-        options.unshift(TIME_COLUMN_OPTION);
+        options.unshift(DATASET_TIME_COLUMN_OPTION);
       }
       newState.options = Object.fromEntries(
         options.map(option => [option.column_name, option]),
       );
-      newState.savedMetrics = state.datasource.metrics || [];
+      newState.savedMetrics = (datasource as Dataset).metrics || [];
+    } else {
+      const options = datasource?.columns;
+      if (includeTime) {
+        (options as QueryColumn[])?.unshift(QUERY_TIME_COLUMN_OPTION);
+      }
+      newState.options = Object.fromEntries(
+        (options as QueryColumn[])?.map(option => [option.name, option]),
+      );
+      newState.options = datasource?.columns;
     }
     return newState;
   },
@@ -83,8 +96,10 @@ export const dnd_adhoc_filters: 
SharedControlConfig<'DndFilterSelect'> = {
   default: [],
   description: '',
   mapStateToProps: ({ datasource, form_data }) => ({
-    columns: datasource?.columns.filter(c => c.filterable) || [],
-    savedMetrics: datasource?.metrics || [],
+    columns: datasource?.columns[0]?.hasOwnProperty('filterable')
+      ? (datasource as Dataset)?.columns.filter(c => c.filterable)
+      : datasource?.columns || [],
+    savedMetrics: defineSavedMetrics(datasource),
     // current active adhoc metrics
     selectedMetrics:
       form_data.metrics || (form_data.metric ? [form_data.metric] : []),
@@ -99,8 +114,8 @@ export const dnd_adhoc_metrics: 
SharedControlConfig<'DndMetricSelect'> = {
   label: t('Metrics'),
   validators: [validateNonEmpty],
   mapStateToProps: ({ datasource }) => ({
-    columns: datasource ? datasource.columns : [],
-    savedMetrics: datasource ? datasource.metrics : [],
+    columns: datasource?.columns || [],
+    savedMetrics: defineSavedMetrics(datasource),
     datasource,
     datasourceType: datasource?.type,
   }),
@@ -130,7 +145,7 @@ export const dnd_sort_by: 
SharedControlConfig<'DndMetricSelect'> = {
   ),
   mapStateToProps: ({ datasource }) => ({
     columns: datasource?.columns || [],
-    savedMetrics: datasource?.metrics || [],
+    savedMetrics: defineSavedMetrics(datasource),
     datasource,
     datasourceType: datasource?.type,
   }),
@@ -178,14 +193,31 @@ export const dnd_granularity_sqla: typeof 
dndGroupByControl = {
       : 'Drop temporal column here',
   ),
   mapStateToProps: ({ datasource }) => {
-    const temporalColumns = datasource?.columns.filter(c => c.is_dttm) ?? [];
+    if (datasource?.columns[0]?.hasOwnProperty('column_name')) {
+      const temporalColumns =
+        (datasource as Dataset)?.columns?.filter(c => c.is_dttm) ?? [];
+      const options = Object.fromEntries(
+        temporalColumns.map(option => [option.column_name, option]),
+      );
+      return {
+        options,
+        default:
+          (datasource as Dataset)?.main_dttm_col ||
+          temporalColumns[0]?.column_name ||
+          null,
+        isTemporal: true,
+      };
+    }
+
+    const sortedQueryColumns = (datasource as QueryResponse)?.columns?.sort(
+      query => (query?.is_dttm ? -1 : 1),
+    );
     const options = Object.fromEntries(
-      temporalColumns.map(option => [option.column_name, option]),
+      sortedQueryColumns.map(option => [option.name, option]),
     );
     return {
       options,
-      default:
-        datasource?.main_dttm_col || temporalColumns[0]?.column_name || null,
+      default: sortedQueryColumns[0]?.name || null,
       isTemporal: true,
     };
   },
diff --git 
a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx
 
b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx
index 713fbf1c2d..5ff32d50b0 100644
--- 
a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx
+++ 
b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx
@@ -45,6 +45,8 @@ import {
   legacyValidateInteger,
   validateNonEmpty,
   ComparisionType,
+  QueryResponse,
+  QueryColumn,
 } from '@superset-ui/core';
 
 import {
@@ -55,14 +57,16 @@ import {
   D3_TIME_FORMAT_DOCS,
   DEFAULT_TIME_FORMAT,
   DEFAULT_NUMBER_FORMAT,
+  defineSavedMetrics,
 } from '../utils';
-import { TIME_FILTER_LABELS, TIME_COLUMN_OPTION } from '../constants';
+import { TIME_FILTER_LABELS, DATASET_TIME_COLUMN_OPTION } from '../constants';
 import {
   Metric,
   SharedControlConfig,
   ColumnMeta,
   ExtraControlProps,
   SelectControlConfig,
+  Dataset,
 } from '../types';
 import { ColumnOption } from '../components/ColumnOption';
 
@@ -82,6 +86,7 @@ import {
   dndSeries,
   dnd_adhoc_metric_2,
 } from './dndControls';
+import { QUERY_TIME_COLUMN_OPTION } from '..';
 
 const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
 const sequentialSchemeRegistry = getSequentialSchemeRegistry();
@@ -131,11 +136,14 @@ const groupByControl: 
SharedControlConfig<'SelectControl', ColumnMeta> = {
   promptTextCreator: (label: unknown) => label,
   mapStateToProps(state, { includeTime }) {
     const newState: ExtraControlProps = {};
-    if (state.datasource) {
-      const options = state.datasource.columns.filter(c => c.groupby);
-      if (includeTime) {
-        options.unshift(TIME_COLUMN_OPTION);
-      }
+    const { datasource } = state;
+    if (datasource?.columns[0]?.hasOwnProperty('groupby')) {
+      const options = (datasource as Dataset).columns.filter(c => c.groupby);
+      if (includeTime) options.unshift(DATASET_TIME_COLUMN_OPTION);
+      newState.options = options;
+    } else {
+      const options = (datasource as QueryResponse).columns;
+      if (includeTime) options.unshift(QUERY_TIME_COLUMN_OPTION);
       newState.options = options;
     }
     return newState;
@@ -149,8 +157,8 @@ const metrics: SharedControlConfig<'MetricsControl'> = {
   label: t('Metrics'),
   validators: [validateNonEmpty],
   mapStateToProps: ({ datasource }) => ({
-    columns: datasource ? datasource.columns : [],
-    savedMetrics: datasource ? datasource.metrics : [],
+    columns: datasource?.columns || [],
+    savedMetrics: defineSavedMetrics(datasource),
     datasource,
     datasourceType: datasource?.type,
   }),
@@ -292,15 +300,23 @@ const granularity_sqla: 
SharedControlConfig<'SelectControl', ColumnMeta> = {
   valueRenderer: c => <ColumnOption column={c} />,
   valueKey: 'column_name',
   mapStateToProps: state => {
-    const props: Partial<SelectControlConfig<ColumnMeta>> = {};
-    if (state.datasource) {
-      props.options = state.datasource.columns.filter(c => c.is_dttm);
+    const props: Partial<SelectControlConfig<ColumnMeta | QueryColumn>> = {};
+    const { datasource } = state;
+    if (datasource?.columns[0]?.hasOwnProperty('main_dttm_col')) {
+      const dataset = datasource as Dataset;
+      props.options = dataset.columns.filter((c: ColumnMeta) => c.is_dttm);
       props.default = null;
-      if (state.datasource.main_dttm_col) {
-        props.default = state.datasource.main_dttm_col;
-      } else if (props.options && props.options.length > 0) {
-        props.default = props.options[0].column_name;
+      if (dataset.main_dttm_col) {
+        props.default = dataset.main_dttm_col;
+      } else if (props?.options) {
+        props.default = (props.options[0] as ColumnMeta).column_name;
       }
+    } else {
+      const sortedQueryColumns = (datasource as QueryResponse)?.columns?.sort(
+        query => (query?.is_dttm ? -1 : 1),
+      );
+      props.options = sortedQueryColumns;
+      if (props?.options) props.default = props.options[0]?.name;
     }
     return props;
   },
@@ -318,7 +334,7 @@ const time_grain_sqla: SharedControlConfig<'SelectControl'> 
= {
       'engine basis in the Superset source code.',
   ),
   mapStateToProps: ({ datasource }) => ({
-    choices: datasource?.time_grain_sqla || null,
+    choices: (datasource as Dataset)?.time_grain_sqla || null,
   }),
 };
 
@@ -335,7 +351,7 @@ const time_range: SharedControlConfig<'DateFilterControl'> 
= {
       "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: ({ datasource, form_data }) => ({
+  mapStateToProps: ({ datasource }) => ({
     datasource,
   }),
 };
@@ -401,7 +417,7 @@ const sort_by: SharedControlConfig<'MetricsControl'> = {
   ),
   mapStateToProps: ({ datasource }) => ({
     columns: datasource?.columns || [],
-    savedMetrics: datasource?.metrics || [],
+    savedMetrics: defineSavedMetrics(datasource),
     datasource,
     datasourceType: datasource?.type,
   }),
@@ -493,8 +509,10 @@ const adhoc_filters: 
SharedControlConfig<'AdhocFilterControl'> = {
   default: [],
   description: '',
   mapStateToProps: ({ datasource, form_data }) => ({
-    columns: datasource?.columns.filter(c => c.filterable) || [],
-    savedMetrics: datasource?.metrics || [],
+    columns: datasource?.columns[0]?.hasOwnProperty('filterable')
+      ? (datasource as Dataset)?.columns.filter(c => c.filterable)
+      : datasource?.columns || [],
+    savedMetrics: defineSavedMetrics(datasource),
     // current active adhoc metrics
     selectedMetrics:
       form_data.metrics || (form_data.metric ? [form_data.metric] : []),
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts 
b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
index 7926ffd759..a7bd128be9 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
@@ -25,6 +25,7 @@ import type {
   JsonValue,
   Metric,
   QueryFormData,
+  QueryResponse,
   QueryFormMetric,
   QueryFormColumn,
 } from '@superset-ui/core';
@@ -53,7 +54,7 @@ export type ColumnMeta = Omit<Column, 'id'> & {
   id?: number;
 } & AnyDict;
 
-export interface DatasourceMeta {
+export interface Dataset {
   id: number;
   type: DatasourceType;
   columns: ColumnMeta[];
@@ -71,7 +72,7 @@ export interface DatasourceMeta {
 
 export interface ControlPanelState {
   form_data: QueryFormData;
-  datasource: DatasourceMeta | null;
+  datasource: Dataset | QueryResponse | null;
   controls: ControlStateMapping;
 }
 
@@ -90,7 +91,7 @@ export interface ActionDispatcher<
  * Mapping of action dispatchers
  */
 export interface ControlPanelActionDispatchers {
-  setDatasource: ActionDispatcher<[DatasourceMeta]>;
+  setDatasource: ActionDispatcher<[Dataset]>;
 }
 
 /**
diff --git 
a/superset-frontend/packages/superset-ui-chart-controls/src/utils/columnChoices.ts
 
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/columnChoices.ts
index 3725f175e7..0387717ff7 100644
--- 
a/superset-frontend/packages/superset-ui-chart-controls/src/utils/columnChoices.ts
+++ 
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/columnChoices.ts
@@ -16,20 +16,30 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { DatasourceMeta } from '../types';
+import { QueryResponse } from '@superset-ui/core';
+import { Dataset } from '../types';
 
 /**
  * Convert Datasource columns to column choices
  */
 export default function columnChoices(
-  datasource?: DatasourceMeta | null,
+  datasource?: Dataset | QueryResponse | null,
 ): [string, string][] {
+  if (datasource?.columns[0]?.hasOwnProperty('column_name')) {
+    return (
+      (datasource as Dataset)?.columns
+        .map((col): [string, string] => [
+          col.column_name,
+          col.verbose_name || col.column_name,
+        ])
+        .sort((opt1, opt2) =>
+          opt1[1].toLowerCase() > opt2[1].toLowerCase() ? 1 : -1,
+        ) || []
+    );
+  }
   return (
-    datasource?.columns
-      .map((col): [string, string] => [
-        col.column_name,
-        col.verbose_name || col.column_name,
-      ])
+    (datasource as QueryResponse)?.columns
+      .map((col): [string, string] => [col.name, col.name])
       .sort((opt1, opt2) =>
         opt1[1].toLowerCase() > opt2[1].toLowerCase() ? 1 : -1,
       ) || []
diff --git a/superset-frontend/src/SqlLab/components/TabStatusIcon/index.tsx 
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/defineSavedMetrics.ts
similarity index 71%
copy from superset-frontend/src/SqlLab/components/TabStatusIcon/index.tsx
copy to 
superset-frontend/packages/superset-ui-chart-controls/src/utils/defineSavedMetrics.ts
index 070e749288..431b6cb4be 100644
--- a/superset-frontend/src/SqlLab/components/TabStatusIcon/index.tsx
+++ 
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/defineSavedMetrics.ts
@@ -1,3 +1,4 @@
+/* eslint-disable camelcase */
 /**
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -16,13 +17,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React from 'react';
-import { QueryState } from 'src/SqlLab/types';
 
-interface TabStatusIconProps {
-  tabState: QueryState;
-}
+import { QueryResponse, DEFAULT_METRICS } from '@superset-ui/core';
+import { Dataset } from '../types';
 
-export default function TabStatusIcon({ tabState }: TabStatusIconProps) {
-  return <div className={`circle ${tabState}`} />;
-}
+export const defineSavedMetrics = (
+  datasource: Dataset | QueryResponse | null,
+) =>
+  datasource?.hasOwnProperty('metrics')
+    ? (datasource as Dataset)?.metrics || []
+    : DEFAULT_METRICS;
diff --git 
a/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts 
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts
index bfb5b5e824..11c03e4ca1 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts
@@ -22,3 +22,4 @@ export * from './expandControlConfig';
 export * from './getColorFormatters';
 export { default as mainMetric } from './mainMetric';
 export { default as columnChoices } from './columnChoices';
+export * from './defineSavedMetrics';
diff --git 
a/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx
 
b/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx
index d4e34c79c7..3224bbcc26 100644
--- 
a/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx
+++ 
b/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx
@@ -16,11 +16,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { DatasourceType } from '@superset-ui/core';
+import { DatasourceType, QueryResponse, testQuery } from '@superset-ui/core';
 import { columnChoices } from '../../src';
 
 describe('columnChoices()', () => {
-  it('should convert columns to choices', () => {
+  it('should convert columns to choices when source is a Dataset', () => {
     expect(
       columnChoices({
         id: 1,
@@ -56,4 +56,13 @@ describe('columnChoices()', () => {
   it('should return empty array when no columns', () => {
     expect(columnChoices(undefined)).toEqual([]);
   });
+
+  it('should convert columns to choices when source is a Query', () => {
+    expect(columnChoices(testQuery as QueryResponse)).toEqual([
+      ['Column 1', 'Column 1'],
+      ['Column 2', 'Column 2'],
+      ['Column 3', 'Column 3'],
+    ]);
+    expect.anything();
+  });
 });
diff --git 
a/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx
 
b/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx
similarity index 58%
copy from 
superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx
copy to 
superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx
index d4e34c79c7..59036bf604 100644
--- 
a/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx
+++ 
b/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx
@@ -16,44 +16,45 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { DatasourceType } from '@superset-ui/core';
-import { columnChoices } from '../../src';
+import {
+  DatasourceType,
+  DEFAULT_METRICS,
+  QueryResponse,
+  testQuery,
+} from '@superset-ui/core';
+import { defineSavedMetrics } from '@superset-ui/chart-controls';
 
-describe('columnChoices()', () => {
-  it('should convert columns to choices', () => {
+describe('defineSavedMetrics', () => {
+  it('defines saved metrics if source is a Dataset', () => {
     expect(
-      columnChoices({
+      defineSavedMetrics({
         id: 1,
-        metrics: [],
-        type: DatasourceType.Table,
-        main_dttm_col: 'test',
-        time_grain_sqla: 'P1D',
-        columns: [
-          {
-            column_name: 'fiz',
-          },
+        metrics: [
           {
-            column_name: 'about',
-            verbose_name: 'right',
-          },
-          {
-            column_name: 'foo',
-            verbose_name: 'bar',
+            metric_name: 'COUNT(*) non-default-dataset-metric',
+            expression: 'COUNT(*) non-default-dataset-metric',
           },
         ],
+        type: DatasourceType.Table,
+        main_dttm_col: 'test',
+        time_grain_sqla: 'P1D',
+        columns: [],
         verbose_map: {},
-        column_format: { fiz: 'NUMERIC', about: 'STRING', foo: 'DATE' },
+        column_format: {},
         datasource_name: 'my_datasource',
         description: 'this is my datasource',
       }),
     ).toEqual([
-      ['foo', 'bar'],
-      ['fiz', 'fiz'],
-      ['about', 'right'],
+      {
+        metric_name: 'COUNT(*) non-default-dataset-metric',
+        expression: 'COUNT(*) non-default-dataset-metric',
+      },
     ]);
   });
 
-  it('should return empty array when no columns', () => {
-    expect(columnChoices(undefined)).toEqual([]);
+  it('returns default saved metrics if souce is a Query', () => {
+    expect(defineSavedMetrics(testQuery as QueryResponse)).toEqual(
+      DEFAULT_METRICS,
+    );
   });
 });
diff --git 
a/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts 
b/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts
index 073bdf90c1..03916dee5e 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts
@@ -22,6 +22,10 @@ import { Metric } from './Metric';
 export enum DatasourceType {
   Table = 'table',
   Druid = 'druid',
+  Query = 'query',
+  Dataset = 'dataset',
+  SlTable = 'sl_table',
+  SavedQuery = 'saved_query',
 }
 
 /**
@@ -43,4 +47,11 @@ export interface Datasource {
   };
 }
 
+export const DEFAULT_METRICS = [
+  {
+    metric_name: 'COUNT(*)',
+    expression: 'COUNT(*)',
+  },
+];
+
 export default {};
diff --git 
a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts 
b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
index c9961cc7cb..d4b672a7a3 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
@@ -165,4 +165,214 @@ export interface QueryContext {
   form_data?: QueryFormData;
 }
 
+export const ErrorTypeEnum = {
+  // Frontend errors
+  FRONTEND_CSRF_ERROR: 'FRONTEND_CSRF_ERROR',
+  FRONTEND_NETWORK_ERROR: 'FRONTEND_NETWORK_ERROR',
+  FRONTEND_TIMEOUT_ERROR: 'FRONTEND_TIMEOUT_ERROR',
+
+  // DB Engine errors
+  GENERIC_DB_ENGINE_ERROR: 'GENERIC_DB_ENGINE_ERROR',
+  COLUMN_DOES_NOT_EXIST_ERROR: 'COLUMN_DOES_NOT_EXIST_ERROR',
+  TABLE_DOES_NOT_EXIST_ERROR: 'TABLE_DOES_NOT_EXIST_ERROR',
+  SCHEMA_DOES_NOT_EXIST_ERROR: 'SCHEMA_DOES_NOT_EXIST_ERROR',
+  CONNECTION_INVALID_USERNAME_ERROR: 'CONNECTION_INVALID_USERNAME_ERROR',
+  CONNECTION_INVALID_PASSWORD_ERROR: 'CONNECTION_INVALID_PASSWORD_ERROR',
+  CONNECTION_INVALID_HOSTNAME_ERROR: 'CONNECTION_INVALID_HOSTNAME_ERROR',
+  CONNECTION_PORT_CLOSED_ERROR: 'CONNECTION_PORT_CLOSED_ERROR',
+  CONNECTION_INVALID_PORT_ERROR: 'CONNECTION_INVALID_PORT_ERROR',
+  CONNECTION_HOST_DOWN_ERROR: 'CONNECTION_HOST_DOWN_ERROR',
+  CONNECTION_ACCESS_DENIED_ERROR: 'CONNECTION_ACCESS_DENIED_ERROR',
+  CONNECTION_UNKNOWN_DATABASE_ERROR: 'CONNECTION_UNKNOWN_DATABASE_ERROR',
+  CONNECTION_DATABASE_PERMISSIONS_ERROR:
+    'CONNECTION_DATABASE_PERMISSIONS_ERROR',
+  CONNECTION_MISSING_PARAMETERS_ERRORS: 'CONNECTION_MISSING_PARAMETERS_ERRORS',
+  OBJECT_DOES_NOT_EXIST_ERROR: 'OBJECT_DOES_NOT_EXIST_ERROR',
+  SYNTAX_ERROR: 'SYNTAX_ERROR',
+
+  // Viz errors
+  VIZ_GET_DF_ERROR: 'VIZ_GET_DF_ERROR',
+  UNKNOWN_DATASOURCE_TYPE_ERROR: 'UNKNOWN_DATASOURCE_TYPE_ERROR',
+  FAILED_FETCHING_DATASOURCE_INFO_ERROR:
+    'FAILED_FETCHING_DATASOURCE_INFO_ERROR',
+
+  // Security access errors
+  TABLE_SECURITY_ACCESS_ERROR: 'TABLE_SECURITY_ACCESS_ERROR',
+  DATASOURCE_SECURITY_ACCESS_ERROR: 'DATASOURCE_SECURITY_ACCESS_ERROR',
+  DATABASE_SECURITY_ACCESS_ERROR: 'DATABASE_SECURITY_ACCESS_ERROR',
+  QUERY_SECURITY_ACCESS_ERROR: 'QUERY_SECURITY_ACCESS_ERROR',
+  MISSING_OWNERSHIP_ERROR: 'MISSING_OWNERSHIP_ERROR',
+
+  // Other errors
+  BACKEND_TIMEOUT_ERROR: 'BACKEND_TIMEOUT_ERROR',
+  DATABASE_NOT_FOUND_ERROR: 'DATABASE_NOT_FOUND_ERROR',
+
+  // Sqllab error
+  MISSING_TEMPLATE_PARAMS_ERROR: 'MISSING_TEMPLATE_PARAMS_ERROR',
+  INVALID_TEMPLATE_PARAMS_ERROR: 'INVALID_TEMPLATE_PARAMS_ERROR',
+  RESULTS_BACKEND_NOT_CONFIGURED_ERROR: 'RESULTS_BACKEND_NOT_CONFIGURED_ERROR',
+  DML_NOT_ALLOWED_ERROR: 'DML_NOT_ALLOWED_ERROR',
+  INVALID_CTAS_QUERY_ERROR: 'INVALID_CTAS_QUERY_ERROR',
+  INVALID_CVAS_QUERY_ERROR: 'INVALID_CVAS_QUERY_ERROR',
+  SQLLAB_TIMEOUT_ERROR: 'SQLLAB_TIMEOUT_ERROR',
+  RESULTS_BACKEND_ERROR: 'RESULTS_BACKEND_ERROR',
+  ASYNC_WORKERS_ERROR: 'ASYNC_WORKERS_ERROR',
+
+  // Generic errors
+  GENERIC_COMMAND_ERROR: 'GENERIC_COMMAND_ERROR',
+  GENERIC_BACKEND_ERROR: 'GENERIC_BACKEND_ERROR',
+
+  // API errors
+  INVALID_PAYLOAD_FORMAT_ERROR: 'INVALID_PAYLOAD_FORMAT_ERROR',
+  INVALID_PAYLOAD_SCHEMA_ERROR: 'INVALID_PAYLOAD_SCHEMA_ERROR',
+} as const;
+
+type ValueOf<T> = T[keyof T];
+
+export type ErrorType = ValueOf<typeof ErrorTypeEnum>;
+
+// Keep in sync with superset/views/errors.py
+export type ErrorLevel = 'info' | 'warning' | 'error';
+
+export type ErrorSource = 'dashboard' | 'explore' | 'sqllab';
+
+export type SupersetError<ExtraType = Record<string, any> | null> = {
+  error_type: ErrorType;
+  extra: ExtraType;
+  level: ErrorLevel;
+  message: string;
+};
+
+export const CtasEnum = {
+  TABLE: 'TABLE',
+  VIEW: 'VIEW',
+};
+
+export type QueryColumn = {
+  name: string;
+  type: string | null;
+  is_dttm: boolean;
+};
+
+export type QueryState =
+  | 'stopped'
+  | 'failed'
+  | 'pending'
+  | 'running'
+  | 'scheduled'
+  | 'success'
+  | 'fetching'
+  | 'timed_out';
+
+export type Query = {
+  cached: boolean;
+  ctas: boolean;
+  ctas_method?: keyof typeof CtasEnum;
+  dbId: number;
+  errors?: SupersetError[];
+  errorMessage: string | null;
+  extra: {
+    progress: string | null;
+  };
+  id: string;
+  isDataPreview: boolean;
+  link?: string;
+  progress: number;
+  resultsKey: string | null;
+  schema?: string;
+  sql: string;
+  sqlEditorId: string;
+  state: QueryState;
+  tab: string | null;
+  tempSchema: string | null;
+  tempTable: string;
+  trackingUrl: string | null;
+  templateParams: any;
+  rows: number;
+  queryLimit: number;
+  limitingFactor: string;
+  endDttm: number;
+  duration: string;
+  startDttm: number;
+  time: Record<string, any>;
+  user: Record<string, any>;
+  userId: number;
+  db: Record<string, any>;
+  started: string;
+  querylink: Record<string, any>;
+  queryId: number;
+  executedSql: string;
+  output: string | Record<string, any>;
+  actions: Record<string, any>;
+  type: DatasourceType.Query;
+  columns: QueryColumn[];
+};
+
+export type QueryResults = {
+  results: {
+    displayLimitReached: boolean;
+    columns: QueryColumn[];
+    data: Record<string, unknown>[];
+    expanded_columns: QueryColumn[];
+    selected_columns: QueryColumn[];
+    query: { limit: number };
+  };
+};
+
+export type QueryResponse = Query & QueryResults;
+
+export const testQuery: Query = {
+  id: 'clientId2353',
+  dbId: 1,
+  sql: 'SELECT * FROM something',
+  sqlEditorId: 'dfsadfs',
+  tab: 'unimportant',
+  tempTable: '',
+  ctas: false,
+  cached: false,
+  errorMessage: null,
+  extra: { progress: null },
+  isDataPreview: false,
+  progress: 0,
+  resultsKey: null,
+  state: 'success',
+  tempSchema: null,
+  trackingUrl: null,
+  templateParams: null,
+  rows: 42,
+  queryLimit: 100,
+  limitingFactor: '',
+  endDttm: 1476910579693,
+  duration: '',
+  startDttm: 1476910566092.96,
+  time: {},
+  user: {},
+  userId: 1,
+  db: {},
+  started: '',
+  querylink: {},
+  queryId: 1,
+  executedSql: '',
+  output: '',
+  actions: {},
+  type: DatasourceType.Query,
+  columns: [
+    {
+      name: 'Column 1',
+      type: DatasourceType.Query,
+      is_dttm: false,
+    },
+    {
+      name: 'Column 2',
+      type: DatasourceType.Query,
+      is_dttm: true,
+    },
+    {
+      name: 'Column 3',
+      type: DatasourceType.Query,
+      is_dttm: false,
+    },
+  ],
+};
+
 export default {};
diff --git 
a/superset-frontend/plugins/legacy-plugin-chart-event-flow/src/controlPanel.tsx 
b/superset-frontend/plugins/legacy-plugin-chart-event-flow/src/controlPanel.tsx
index 11a0b88987..22d5c8ce4e 100644
--- 
a/superset-frontend/plugins/legacy-plugin-chart-event-flow/src/controlPanel.tsx
+++ 
b/superset-frontend/plugins/legacy-plugin-chart-event-flow/src/controlPanel.tsx
@@ -109,7 +109,7 @@ const config: ControlPanelConfig = {
               valueKey: 'column_name',
               allowAll: true,
               mapStateToProps: state => ({
-                options: state.datasource ? state.datasource.columns : [],
+                options: state.datasource?.columns || [],
               }),
               commaChoosesOption: false,
               freeForm: true,
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/controls.jsx
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/controls.jsx
index 03092e7316..9e6d2b0d84 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/controls.jsx
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/controls.jsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 export function columnChoices(datasource) {
-  if (datasource && datasource.columns) {
+  if (datasource?.columns) {
     return datasource.columns
       .map(col => [col.column_name, col.verbose_name || col.column_name])
       .sort((opt1, opt2) =>
diff --git 
a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/columns.tsx
 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/columns.tsx
index fd24bb75fb..3aec61dc40 100644
--- 
a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/columns.tsx
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/columns.tsx
@@ -21,6 +21,8 @@ import {
   ControlSetItem,
   ExtraControlProps,
   sharedControls,
+  Dataset,
+  ColumnMeta,
 } from '@superset-ui/chart-controls';
 import {
   ensureIsArray,
@@ -63,10 +65,12 @@ const dndAllColumns: typeof sharedControls.groupby = {
   mapStateToProps({ datasource, controls }, controlState) {
     const newState: ExtraControlProps = {};
     if (datasource) {
-      const options = datasource.columns;
-      newState.options = Object.fromEntries(
-        options.map(option => [option.column_name, option]),
-      );
+      if (datasource?.columns[0]?.hasOwnProperty('filterable')) {
+        const options = (datasource as Dataset).columns;
+        newState.options = Object.fromEntries(
+          options.map((option: ColumnMeta) => [option.column_name, option]),
+        );
+      } else newState.options = datasource.columns;
     }
     newState.queryMode = getQueryMode(controls);
     newState.externalValidationErrors =
diff --git 
a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx
 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx
index 7df35e6a66..96eab55f92 100644
--- 
a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx
@@ -21,6 +21,9 @@ import {
   ControlSetItem,
   ControlState,
   sharedControls,
+  Dataset,
+  ColumnMeta,
+  defineSavedMetrics,
 } from '@superset-ui/chart-controls';
 import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core';
 import { getQueryMode, isAggMode, validateAggControlValues } from './shared';
@@ -36,7 +39,7 @@ const percentMetrics: typeof sharedControls.metrics = {
   resetOnHide: false,
   mapStateToProps: ({ datasource, controls }, controlState) => ({
     columns: datasource?.columns || [],
-    savedMetrics: datasource?.metrics || [],
+    savedMetrics: defineSavedMetrics(datasource),
     datasource,
     datasourceType: datasource?.type,
     queryMode: getQueryMode(controls),
@@ -74,8 +77,12 @@ export const metricsControlSetItem: ControlSetItem = {
       { controls, datasource, form_data }: ControlPanelState,
       controlState: ControlState,
     ) => ({
-      columns: datasource?.columns.filter(c => c.filterable) || [],
-      savedMetrics: datasource?.metrics || [],
+      columns: datasource?.columns[0]?.hasOwnProperty('filterable')
+        ? (datasource as Dataset)?.columns?.filter(
+            (c: ColumnMeta) => c.filterable,
+          )
+        : datasource?.columns,
+      savedMetrics: defineSavedMetrics(datasource),
       // current active adhoc metrics
       selectedMetrics:
         form_data.metrics || (form_data.metric ? [form_data.metric] : []),
diff --git 
a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/orderBy.tsx
 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/orderBy.tsx
index b7c8f8e240..93002bd49b 100644
--- 
a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/orderBy.tsx
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/orderBy.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ControlSetItem } from '@superset-ui/chart-controls';
+import { ControlSetItem, Dataset } from '@superset-ui/chart-controls';
 import { t } from '@superset-ui/core';
 import { isAggMode, isRawMode } from './shared';
 
@@ -29,7 +29,9 @@ export const orderByControlSetItem: ControlSetItem = {
     multi: true,
     default: [],
     mapStateToProps: ({ datasource }) => ({
-      choices: datasource?.order_by_choices || [],
+      choices: datasource?.hasOwnProperty('order_by_choices')
+        ? (datasource as Dataset)?.order_by_choices
+        : datasource?.columns || [],
     }),
     visibility: isRawMode,
     resetOnHide: false,
diff --git 
a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx
 
b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx
index acbdb04bc7..ce09ae1345 100644
--- 
a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx
+++ 
b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx
@@ -31,6 +31,7 @@ import {
   sections,
   sharedControls,
   emitFilterControl,
+  Dataset,
 } from '@superset-ui/chart-controls';
 import { MetricsLayoutEnum } from '../types';
 
@@ -350,7 +351,11 @@ const config: ControlPanelConfig = {
                 const values =
                   (explore?.controls?.metrics?.value as QueryFormMetric[]) ??
                   [];
-                const verboseMap = explore?.datasource?.verbose_map ?? {};
+                const verboseMap = explore?.datasource?.hasOwnProperty(
+                  'verbose_map',
+                )
+                  ? (explore?.datasource as Dataset)?.verbose_map
+                  : explore?.datasource?.columns ?? {};
                 const metricColumn = values.map(value => {
                   if (typeof value === 'string') {
                     return { value, label: verboseMap[value] ?? value };
diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx 
b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
index 4f5530f6b9..427f671c1a 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
@@ -44,6 +44,9 @@ import {
   ExtraControlProps,
   ControlState,
   emitFilterControl,
+  Dataset,
+  ColumnMeta,
+  defineSavedMetrics,
 } from '@superset-ui/chart-controls';
 
 import i18n from './i18n';
@@ -127,12 +130,12 @@ const dnd_all_columns: typeof sharedControls.groupby = {
   default: [],
   mapStateToProps({ datasource, controls }, controlState) {
     const newState: ExtraControlProps = {};
-    if (datasource) {
-      const options = datasource.columns;
+    if (datasource?.columns[0]?.hasOwnProperty('column_name')) {
+      const options = (datasource as Dataset).columns;
       newState.options = Object.fromEntries(
-        options.map(option => [option.column_name, option]),
+        options.map((option: ColumnMeta) => [option.column_name, option]),
       );
-    }
+    } else newState.options = datasource?.columns;
     newState.queryMode = getQueryMode(controls);
     newState.externalValidationErrors =
       isRawMode({ controls }) && ensureIsArray(controlState.value).length === 0
@@ -155,7 +158,7 @@ const percent_metrics: typeof sharedControls.metrics = {
   resetOnHide: false,
   mapStateToProps: ({ datasource, controls }, controlState) => ({
     columns: datasource?.columns || [],
-    savedMetrics: datasource?.metrics || [],
+    savedMetrics: defineSavedMetrics(datasource),
     datasource,
     datasourceType: datasource?.type,
     queryMode: getQueryMode(controls),
@@ -229,8 +232,12 @@ const config: ControlPanelConfig = {
                 { controls, datasource, form_data }: ControlPanelState,
                 controlState: ControlState,
               ) => ({
-                columns: datasource?.columns.filter(c => c.filterable) || [],
-                savedMetrics: datasource?.metrics || [],
+                columns: datasource?.columns[0]?.hasOwnProperty('filterable')
+                  ? (datasource as Dataset)?.columns?.filter(
+                      (c: ColumnMeta) => c.filterable,
+                    )
+                  : datasource?.columns,
+                savedMetrics: defineSavedMetrics(datasource),
                 // current active adhoc metrics
                 selectedMetrics:
                   form_data.metrics ||
@@ -280,7 +287,9 @@ const config: ControlPanelConfig = {
               multi: true,
               default: [],
               mapStateToProps: ({ datasource }) => ({
-                choices: datasource?.order_by_choices || [],
+                choices: datasource?.hasOwnProperty('order_by_choices')
+                  ? (datasource as Dataset)?.order_by_choices
+                  : datasource?.columns || [],
               }),
               visibility: isRawMode,
               resetOnHide: false,
@@ -505,7 +514,11 @@ const config: ControlPanelConfig = {
                 return true;
               },
               mapStateToProps(explore, _, chart) {
-                const verboseMap = explore?.datasource?.verbose_map ?? {};
+                const verboseMap = explore?.datasource?.hasOwnProperty(
+                  'verbose_map',
+                )
+                  ? (explore?.datasource as Dataset)?.verbose_map
+                  : explore?.datasource?.columns ?? {};
                 const { colnames, coltypes } =
                   chart?.queriesResponse?.[0] ?? {};
                 const numericColumns =
diff --git 
a/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/index.tsx 
b/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/index.tsx
index 9adb5dc402..fbcdc15bc5 100644
--- a/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/index.tsx
+++ b/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/index.tsx
@@ -22,7 +22,7 @@ import { t } from '@superset-ui/core';
 import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
 import Button from 'src/components/Button';
 import { exploreChart } from 'src/explore/exploreUtils';
-import { RootState } from 'src/SqlLab/types';
+import { SqlLabRootState } from 'src/SqlLab/types';
 
 interface ExploreCtasResultsButtonProps {
   actions: {
@@ -45,7 +45,7 @@ const ExploreCtasResultsButton = ({
 }: ExploreCtasResultsButtonProps) => {
   const { createCtasDatasource, addInfoToast, addDangerToast } = actions;
   const errorMessage = useSelector(
-    (state: RootState) => state.sqlLab.errorMessage,
+    (state: SqlLabRootState) => state.sqlLab.errorMessage,
   );
 
   const buildVizOptions = {
diff --git a/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx 
b/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx
index c41ace1ead..86f2806920 100644
--- a/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx
@@ -18,12 +18,11 @@
  */
 import React from 'react';
 import { EmptyStateMedium } from 'src/components/EmptyState';
-import { t, styled } from '@superset-ui/core';
-import { Query } from 'src/SqlLab/types';
+import { t, styled, QueryResponse } from '@superset-ui/core';
 import QueryTable from 'src/SqlLab/components/QueryTable';
 
 interface QueryHistoryProps {
-  queries: Query[];
+  queries: QueryResponse[];
   actions: {
     queryEditorSetAndSaveSql: Function;
     cloneQueryToNewTab: Function;
diff --git a/superset-frontend/src/SqlLab/components/QuerySearch/index.tsx 
b/superset-frontend/src/SqlLab/components/QuerySearch/index.tsx
index e1e994133a..635603e255 100644
--- a/superset-frontend/src/SqlLab/components/QuerySearch/index.tsx
+++ b/superset-frontend/src/SqlLab/components/QuerySearch/index.tsx
@@ -19,7 +19,7 @@
 import React, { useState, useEffect } from 'react';
 import Button from 'src/components/Button';
 import Select from 'src/components/Select';
-import { styled, t, SupersetClient } from '@superset-ui/core';
+import { styled, t, SupersetClient, QueryResponse } from '@superset-ui/core';
 import { debounce } from 'lodash';
 import Loading from 'src/components/Loading';
 import {
@@ -29,7 +29,6 @@ import {
   epochTimeXYearsAgo,
 } from 'src/utils/dates';
 import AsyncSelect from 'src/components/AsyncSelect';
-import { Query } from 'src/SqlLab/types';
 import { STATUS_OPTIONS, TIME_OPTIONS } from 'src/SqlLab/constants';
 import QueryTable from '../QueryTable';
 
@@ -85,7 +84,7 @@ function QuerySearch({ actions, displayLimit }: 
QuerySearchProps) {
   const [from, setFrom] = useState<string>('28 days ago');
   const [to, setTo] = useState<string>('now');
   const [status, setStatus] = useState<string>('success');
-  const [queriesArray, setQueriesArray] = useState<Query[]>([]);
+  const [queriesArray, setQueriesArray] = useState<QueryResponse[]>([]);
   const [queriesLoading, setQueriesLoading] = useState<boolean>(true);
 
   const getTimeFromSelection = (selection: string) => {
diff --git a/superset-frontend/src/SqlLab/components/QueryStateLabel/index.tsx 
b/superset-frontend/src/SqlLab/components/QueryStateLabel/index.tsx
index b2704843df..6168a2af71 100644
--- a/superset-frontend/src/SqlLab/components/QueryStateLabel/index.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryStateLabel/index.tsx
@@ -19,7 +19,7 @@
 import React from 'react';
 import Label from 'src/components/Label';
 import { STATE_TYPE_MAP } from 'src/SqlLab/constants';
-import { Query } from 'src/SqlLab/types';
+import { Query } from '@superset-ui/core';
 
 interface QueryStateLabelProps {
   query: Query;
diff --git a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx 
b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
index 90d2219497..54edb7f97e 100644
--- a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
@@ -21,14 +21,14 @@ import moment from 'moment';
 import Card from 'src/components/Card';
 import ProgressBar from 'src/components/ProgressBar';
 import Label from 'src/components/Label';
-import { t, useTheme } from '@superset-ui/core';
+import { t, useTheme, QueryResponse } from '@superset-ui/core';
 import { useSelector } from 'react-redux';
 import TableView from 'src/components/TableView';
 import Button from 'src/components/Button';
 import { fDuration } from 'src/utils/dates';
 import Icons from 'src/components/Icons';
 import { Tooltip } from 'src/components/Tooltip';
-import { Query, RootState } from 'src/SqlLab/types';
+import { SqlLabRootState } from 'src/SqlLab/types';
 import ModalTrigger from 'src/components/ModalTrigger';
 import { UserWithPermissionsAndRoles as User } from 'src/types/bootstrapTypes';
 import ResultSet from '../ResultSet';
@@ -36,7 +36,7 @@ import HighlightedSql from '../HighlightedSql';
 import { StaticPosition, verticalAlign, StyledTooltip } from './styles';
 
 interface QueryTableQuery
-  extends Omit<Query, 'state' | 'sql' | 'progress' | 'results'> {
+  extends Omit<QueryResponse, 'state' | 'sql' | 'progress' | 'results'> {
   state?: Record<string, any>;
   sql?: Record<string, any>;
   progress?: Record<string, any>;
@@ -52,7 +52,7 @@ interface QueryTableProps {
     clearQueryResults: Function;
     removeQuery: Function;
   };
-  queries?: Query[];
+  queries?: QueryResponse[];
   onUserClicked?: Function;
   onDbClicked?: Function;
   displayLimit: number;
@@ -91,7 +91,7 @@ const QueryTable = ({
     [columns],
   );
 
-  const user = useSelector<RootState, User>(state => state.sqlLab.user);
+  const user = useSelector<SqlLabRootState, User>(state => state.sqlLab.user);
 
   const {
     queryEditorSetAndSaveSql,
@@ -102,15 +102,15 @@ const QueryTable = ({
   } = actions;
 
   const data = useMemo(() => {
-    const restoreSql = (query: Query) => {
+    const restoreSql = (query: QueryResponse) => {
       queryEditorSetAndSaveSql({ id: query.sqlEditorId }, query.sql);
     };
 
-    const openQueryInNewTab = (query: Query) => {
+    const openQueryInNewTab = (query: QueryResponse) => {
       cloneQueryToNewTab(query, true);
     };
 
-    const openAsyncResults = (query: Query, displayLimit: number) => {
+    const openAsyncResults = (query: QueryResponse, displayLimit: number) => {
       fetchQueryResults(query, displayLimit);
     };
 
diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx 
b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
index 39c897c8d4..339303ba57 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
@@ -19,19 +19,9 @@
 import React, { CSSProperties } from 'react';
 import ButtonGroup from 'src/components/ButtonGroup';
 import Alert from 'src/components/Alert';
-import moment from 'moment';
-import { RadioChangeEvent } from 'src/components';
 import Button from 'src/components/Button';
 import shortid from 'shortid';
-import rison from 'rison';
-import {
-  styled,
-  t,
-  makeApi,
-  SupersetClient,
-  JsonResponse,
-} from '@superset-ui/core';
-import { debounce } from 'lodash';
+import { styled, t, QueryResponse } from '@superset-ui/core';
 import ErrorMessageWithStackTrace from 
'src/components/ErrorMessage/ErrorMessageWithStackTrace';
 import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
 import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
@@ -42,26 +32,12 @@ import FilterableTable, {
 } from 'src/components/FilterableTable';
 import CopyToClipboard from 'src/components/CopyToClipboard';
 import { prepareCopyToClipboardTabularData } from 'src/utils/common';
-import { exploreChart } from 'src/explore/exploreUtils';
 import { CtasEnum } from 'src/SqlLab/actions/sqlLab';
-import { Query } from 'src/SqlLab/types';
 import ExploreCtasResultsButton from '../ExploreCtasResultsButton';
 import ExploreResultsButton from '../ExploreResultsButton';
 import HighlightedSql from '../HighlightedSql';
 import QueryStateLabel from '../QueryStateLabel';
 
-enum DatasetRadioState {
-  SAVE_NEW = 1,
-  OVERWRITE_DATASET = 2,
-}
-
-const EXPLORE_CHART_DEFAULT = {
-  metrics: [],
-  groupby: [],
-  time_range: 'No filter',
-  viz_type: 'table',
-};
-
 enum LIMITING_FACTOR {
   QUERY = 'QUERY',
   QUERY_AND_DROPDOWN = 'QUERY_AND_DROPDOWN',
@@ -71,19 +47,6 @@ enum LIMITING_FACTOR {
 
 const LOADING_STYLES: CSSProperties = { position: 'relative', minHeight: 100 };
 
-interface DatasetOwner {
-  first_name: string;
-  id: number;
-  last_name: string;
-  username: string;
-}
-
-interface DatasetOptionAutocomplete {
-  value: string;
-  datasetId: number;
-  owners: [DatasetOwner];
-}
-
 interface ResultSetProps {
   showControls?: boolean;
   actions: Record<string, any>;
@@ -92,7 +55,7 @@ interface ResultSetProps {
   database?: Record<string, any>;
   displayLimit: number;
   height: number;
-  query: Query;
+  query: QueryResponse;
   search?: boolean;
   showSql?: boolean;
   visualize?: boolean;
@@ -105,12 +68,6 @@ interface ResultSetState {
   showExploreResultsButton: boolean;
   data: Record<string, any>[];
   showSaveDatasetModal: boolean;
-  newSaveDatasetName: string;
-  saveDatasetRadioBtnState: number;
-  shouldOverwriteDataSet: boolean;
-  datasetToOverwrite: Record<string, any>;
-  saveModalAutocompleteValue: string;
-  userDatasetOptions: DatasetOptionAutocomplete[];
   alertIsOpen: boolean;
 }
 
@@ -145,44 +102,6 @@ const ResultSetErrorMessage = styled.div`
   padding-top: ${({ theme }) => 4 * theme.gridUnit}px;
 `;
 
-const ResultSetRowsReturned = styled.span`
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  width: 100%;
-  overflow: hidden;
-  display: inline-block;
-`;
-
-const LimitMessage = styled.span`
-  color: ${({ theme }) => theme.colors.secondary.light1};
-  margin-left: ${({ theme }) => theme.gridUnit * 2}px;
-`;
-
-const updateDataset = async (
-  dbId: number,
-  datasetId: number,
-  sql: string,
-  columns: Array<Record<string, any>>,
-  owners: [number],
-  overrideColumns: boolean,
-) => {
-  const endpoint = 
`api/v1/dataset/${datasetId}?override_columns=${overrideColumns}`;
-  const headers = { 'Content-Type': 'application/json' };
-  const body = JSON.stringify({
-    sql,
-    columns,
-    owners,
-    database_id: dbId,
-  });
-
-  const data: JsonResponse = await SupersetClient.put({
-    endpoint,
-    headers,
-    body,
-  });
-  return data.json.result;
-};
-
 export default class ResultSet extends React.PureComponent<
   ResultSetProps,
   ResultSetState
@@ -203,12 +122,6 @@ export default class ResultSet extends React.PureComponent<
       showExploreResultsButton: false,
       data: [],
       showSaveDatasetModal: false,
-      newSaveDatasetName: this.getDefaultDatasetName(),
-      saveDatasetRadioBtnState: DatasetRadioState.SAVE_NEW,
-      shouldOverwriteDataSet: false,
-      datasetToOverwrite: {},
-      saveModalAutocompleteValue: '',
-      userDatasetOptions: [],
       alertIsOpen: false,
     };
     this.changeSearch = this.changeSearch.bind(this);
@@ -217,31 +130,11 @@ export default class ResultSet extends 
React.PureComponent<
     this.reFetchQueryResults = this.reFetchQueryResults.bind(this);
     this.toggleExploreResultsButton =
       this.toggleExploreResultsButton.bind(this);
-    this.handleSaveInDataset = this.handleSaveInDataset.bind(this);
-    this.handleHideSaveModal = this.handleHideSaveModal.bind(this);
-    this.handleDatasetNameChange = this.handleDatasetNameChange.bind(this);
-    this.handleSaveDatasetRadioBtnState =
-      this.handleSaveDatasetRadioBtnState.bind(this);
-    this.handleOverwriteCancel = this.handleOverwriteCancel.bind(this);
-    this.handleOverwriteDataset = this.handleOverwriteDataset.bind(this);
-    this.handleOverwriteDatasetOption =
-      this.handleOverwriteDatasetOption.bind(this);
-    this.handleSaveDatasetModalSearch = debounce(
-      this.handleSaveDatasetModalSearch.bind(this),
-      1000,
-    );
-    this.handleFilterAutocompleteOption =
-      this.handleFilterAutocompleteOption.bind(this);
-    this.handleOnChangeAutoComplete =
-      this.handleOnChangeAutoComplete.bind(this);
-    this.handleExploreBtnClick = this.handleExploreBtnClick.bind(this);
   }
 
   async componentDidMount() {
     // only do this the first time the component is rendered/mounted
     this.reRunQueryIfSessionTimeoutErrorOnMount();
-    const userDatasetsOwned = await this.getUserDatasets();
-    this.setState({ userDatasetOptions: userDatasetsOwned });
   }
 
   UNSAFE_componentWillReceiveProps(nextProps: ResultSetProps) {
@@ -273,186 +166,7 @@ export default class ResultSet extends 
React.PureComponent<
     }
   };
 
-  getDefaultDatasetName = () =>
-    `${this.props.query.tab} ${moment().format('MM/DD/YYYY HH:mm:ss')}`;
-
-  handleOnChangeAutoComplete = () => {
-    this.setState({ datasetToOverwrite: {} });
-  };
-
-  handleOverwriteDataset = async () => {
-    const { sql, results, dbId } = this.props.query;
-    const { datasetToOverwrite } = this.state;
-
-    await updateDataset(
-      dbId,
-      datasetToOverwrite.datasetId,
-      sql,
-      results.selected_columns.map(d => ({
-        column_name: d.name,
-        type: d.type,
-        is_dttm: d.is_dttm,
-      })),
-      datasetToOverwrite.owners.map((o: DatasetOwner) => o.id),
-      true,
-    );
-
-    this.setState({
-      showSaveDatasetModal: false,
-      shouldOverwriteDataSet: false,
-      datasetToOverwrite: {},
-      newSaveDatasetName: this.getDefaultDatasetName(),
-    });
-
-    exploreChart({
-      ...EXPLORE_CHART_DEFAULT,
-      datasource: `${datasetToOverwrite.datasetId}__table`,
-      all_columns: results.selected_columns.map(d => d.name),
-    });
-  };
-
-  handleSaveInDataset = () => {
-    // if user wants to overwrite a dataset we need to prompt them
-    if (
-      this.state.saveDatasetRadioBtnState ===
-      DatasetRadioState.OVERWRITE_DATASET
-    ) {
-      this.setState({ shouldOverwriteDataSet: true });
-      return;
-    }
-
-    const { schema, sql, dbId } = this.props.query;
-    let { templateParams } = this.props.query;
-    const selectedColumns = this.props.query?.results?.selected_columns || [];
-
-    // The filters param is only used to test jinja templates.
-    // Remove the special filters entry from the templateParams
-    // before saving the dataset.
-    if (templateParams) {
-      const p = JSON.parse(templateParams);
-      /* eslint-disable-next-line no-underscore-dangle */
-      if (p._filters) {
-        /* eslint-disable-next-line no-underscore-dangle */
-        delete p._filters;
-        templateParams = JSON.stringify(p);
-      }
-    }
-
-    this.props.actions
-      .createDatasource({
-        schema,
-        sql,
-        dbId,
-        templateParams,
-        datasourceName: this.state.newSaveDatasetName,
-        columns: selectedColumns,
-      })
-      .then((data: { table_id: number }) => {
-        exploreChart({
-          datasource: `${data.table_id}__table`,
-          metrics: [],
-          groupby: [],
-          time_range: 'No filter',
-          viz_type: 'table',
-          all_columns: selectedColumns.map(c => c.name),
-          row_limit: 1000,
-        });
-      })
-      .catch(() => {
-        this.props.actions.addDangerToast(
-          t('An error occurred saving dataset'),
-        );
-      });
-
-    this.setState({
-      showSaveDatasetModal: false,
-      newSaveDatasetName: this.getDefaultDatasetName(),
-    });
-  };
-
-  handleOverwriteDatasetOption = (
-    _data: string,
-    option: Record<string, any>,
-  ) => {
-    this.setState({ datasetToOverwrite: option });
-  };
-
-  handleDatasetNameChange = (e: React.FormEvent<HTMLInputElement>) => {
-    // @ts-expect-error
-    this.setState({ newSaveDatasetName: e.target.value });
-  };
-
-  handleHideSaveModal = () => {
-    this.setState({
-      showSaveDatasetModal: false,
-      shouldOverwriteDataSet: false,
-    });
-  };
-
-  handleSaveDatasetRadioBtnState = (e: RadioChangeEvent) => {
-    this.setState({ saveDatasetRadioBtnState: Number(e.target.value) });
-  };
-
-  handleOverwriteCancel = () => {
-    this.setState({ shouldOverwriteDataSet: false, datasetToOverwrite: {} });
-  };
-
-  handleExploreBtnClick = () => {
-    this.setState({
-      showSaveDatasetModal: true,
-    });
-  };
-
-  getUserDatasets = async (searchText = '') => {
-    // Making sure that autocomplete input has a value before rendering the 
dropdown
-    // Transforming the userDatasetsOwned data for SaveModalComponent)
-    const { userId } = this.props.user;
-    if (userId) {
-      const queryParams = rison.encode({
-        filters: [
-          {
-            col: 'table_name',
-            opr: 'ct',
-            value: searchText,
-          },
-          {
-            col: 'owners',
-            opr: 'rel_m_m',
-            value: userId,
-          },
-        ],
-        order_column: 'changed_on_delta_humanized',
-        order_direction: 'desc',
-      });
-
-      const response = await makeApi({
-        method: 'GET',
-        endpoint: '/api/v1/dataset',
-      })(`q=${queryParams}`);
-
-      return response.result.map(
-        (r: { table_name: string; id: number; owners: [DatasetOwner] }) => ({
-          value: r.table_name,
-          datasetId: r.id,
-          owners: r.owners,
-        }),
-      );
-    }
-
-    return null;
-  };
-
-  handleSaveDatasetModalSearch = async (searchText: string) => {
-    const userDatasetsOwned = await this.getUserDatasets(searchText);
-    this.setState({ userDatasetOptions: userDatasetsOwned });
-  };
-
-  handleFilterAutocompleteOption = (
-    inputValue: string,
-    option: { value: string; datasetId: number },
-  ) => option.value.toLowerCase().includes(inputValue.toLowerCase());
-
-  clearQueryResults(query: Query) {
+  clearQueryResults(query: QueryResponse) {
     this.props.actions.clearQueryResults(query);
   }
 
@@ -477,11 +191,11 @@ export default class ResultSet extends 
React.PureComponent<
     this.setState({ searchText: event.target.value });
   }
 
-  fetchResults(query: Query) {
+  fetchResults(query: QueryResponse) {
     this.props.actions.fetchQueryResults(query, this.props.displayLimit);
   }
 
-  reFetchQueryResults(query: Query) {
+  reFetchQueryResults(query: QueryResponse) {
     this.props.actions.reFetchQueryResults(query);
   }
 
@@ -503,55 +217,31 @@ export default class ResultSet extends 
React.PureComponent<
       }
       const { columns } = this.props.query.results;
       // Added compute logic to stop user from being able to Save & Explore
-      const {
-        saveDatasetRadioBtnState,
-        newSaveDatasetName,
-        datasetToOverwrite,
-        saveModalAutocompleteValue,
-        shouldOverwriteDataSet,
-        userDatasetOptions,
-        showSaveDatasetModal,
-      } = this.state;
-      const disableSaveAndExploreBtn =
-        (saveDatasetRadioBtnState === DatasetRadioState.SAVE_NEW &&
-          newSaveDatasetName.length === 0) ||
-        (saveDatasetRadioBtnState === DatasetRadioState.OVERWRITE_DATASET &&
-          Object.keys(datasetToOverwrite).length === 0 &&
-          saveModalAutocompleteValue.length === 0);
+      const { showSaveDatasetModal } = this.state;
+      const { query } = this.props;
 
       return (
         <ResultSetControls>
           <SaveDatasetModal
             visible={showSaveDatasetModal}
-            onOk={this.handleSaveInDataset}
-            saveDatasetRadioBtnState={saveDatasetRadioBtnState}
-            shouldOverwriteDataset={shouldOverwriteDataSet}
-            defaultCreateDatasetValue={newSaveDatasetName}
-            userDatasetOptions={userDatasetOptions}
-            disableSaveAndExploreBtn={disableSaveAndExploreBtn}
-            onHide={this.handleHideSaveModal}
-            handleDatasetNameChange={this.handleDatasetNameChange}
-            
handleSaveDatasetRadioBtnState={this.handleSaveDatasetRadioBtnState}
-            handleOverwriteCancel={this.handleOverwriteCancel}
-            handleOverwriteDataset={this.handleOverwriteDataset}
-            handleOverwriteDatasetOption={this.handleOverwriteDatasetOption}
-            handleSaveDatasetModalSearch={this.handleSaveDatasetModalSearch}
-            filterAutocompleteOption={this.handleFilterAutocompleteOption}
-            onChangeAutoComplete={this.handleOnChangeAutoComplete}
+            onHide={() => this.setState({ showSaveDatasetModal: false })}
+            buttonTextOnSave={t('Save & Explore')}
+            buttonTextOnOverwrite={t('Overwrite & Explore')}
+            modalDescription={t(
+              'Save this query as a virtual dataset to continue exploring',
+            )}
+            datasource={query}
           />
           <ResultSetButtons>
             {this.props.visualize &&
               this.props.database?.allows_virtual_table_explore && (
                 <ExploreResultsButton
                   database={this.props.database}
-                  onClick={this.handleExploreBtnClick}
+                  onClick={() => this.setState({ showSaveDatasetModal: true })}
                 />
               )}
             {this.props.csv && (
-              <Button
-                buttonSize="small"
-                href={`/superset/csv/${this.props.query.id}`}
-              >
+              <Button buttonSize="small" href={`/superset/csv/${query.id}`}>
                 <i className="fa fa-file-text-o" /> {t('Download to CSV')}
               </Button>
             )}
@@ -587,10 +277,6 @@ export default class ResultSet extends React.PureComponent<
     return <div />;
   }
 
-  onAlertClose = () => {
-    this.setState({ alertIsOpen: false });
-  };
-
   renderRowsReturned() {
     const { results, rows, queryLimit, limitingFactor } = this.props.query;
     let limitMessage;
@@ -646,17 +332,17 @@ export default class ResultSet extends 
React.PureComponent<
     return (
       <ReturnedRows>
         {!limitReached && !shouldUseDefaultDropdownAlert && (
-          <ResultSetRowsReturned title={tooltipText}>
+          <span title={tooltipText}>
             {rowsReturnedMessage}
-            <LimitMessage>{limitMessage}</LimitMessage>
-          </ResultSetRowsReturned>
+            <span>{limitMessage}</span>
+          </span>
         )}
         {!limitReached && shouldUseDefaultDropdownAlert && (
           <div ref={this.calculateAlertRefHeight}>
             <Alert
               type="warning"
               message={t('%(rows)d rows returned', { rows })}
-              onClose={this.onAlertClose}
+              onClose={() => this.setState({ alertIsOpen: false })}
               description={t(
                 'The number of rows displayed is limited to %s by the 
dropdown.',
                 rows,
@@ -668,7 +354,7 @@ export default class ResultSet extends React.PureComponent<
           <div ref={this.calculateAlertRefHeight}>
             <Alert
               type="warning"
-              onClose={this.onAlertClose}
+              onClose={() => this.setState({ alertIsOpen: false })}
               message={t('%(rows)d rows returned', { rows: rowsCount })}
               description={
                 isAdmin
@@ -691,9 +377,7 @@ export default class ResultSet extends React.PureComponent<
       exploreDBId = this.props.database.explore_database_id;
     }
 
-    if (this.props.showSql) {
-      sql = <HighlightedSql sql={query.sql} />;
-    }
+    if (this.props.showSql) sql = <HighlightedSql sql={query.sql} />;
 
     if (query.state === 'stopped') {
       return <Alert type="warning" message={t('Query was stopped')} />;
diff --git 
a/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
 
b/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
index cab5559994..c35b5eb2b6 100644
--- 
a/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
+++ 
b/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
@@ -17,44 +17,60 @@
  * under the License.
  */
 import React from 'react';
-import { shallow } from 'enzyme';
-import { Radio } from 'src/components/Radio';
-import { AutoComplete } from 'src/components';
-import { Input } from 'src/components/Input';
+import { QueryResponse, testQuery } from '@superset-ui/core';
 import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
+import { render, screen } from 'spec/helpers/testing-library';
 
-describe('SaveDatasetModal', () => {
-  const mockedProps = {
-    visible: false,
-    onOk: () => {},
-    onHide: () => {},
-    handleDatasetNameChange: () => {},
-    handleSaveDatasetRadioBtnState: () => {},
-    saveDatasetRadioBtnState: 1,
-    handleOverwriteCancel: () => {},
-    handleOverwriteDataset: () => {},
-    handleOverwriteDatasetOption: () => {},
-    defaultCreateDatasetValue: 'someDatasets',
-    shouldOverwriteDataset: false,
-    userDatasetOptions: [],
-    disableSaveAndExploreBtn: false,
-    handleSaveDatasetModalSearch: () => Promise,
-    filterAutocompleteOption: () => false,
-    onChangeAutoComplete: () => {},
-  };
-  it('renders a radio group btn', () => {
-    // @ts-ignore
-    const wrapper = shallow(<SaveDatasetModal {...mockedProps} />);
-    expect(wrapper.find(Radio.Group)).toExist();
+const mockedProps = {
+  visible: true,
+  onHide: () => {},
+  buttonTextOnSave: 'Save',
+  buttonTextOnOverwrite: 'Overwrite',
+  datasource: testQuery as QueryResponse,
+};
+
+describe('SaveDatasetModal RTL', () => {
+  it('renders a "Save as new" field', () => {
+    render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
+
+    const saveRadioBtn = screen.getByRole('radio', {
+      name: /save as new unimportant/i,
+    });
+    const fieldLabel = screen.getByText(/save as new/i);
+    const inputField = screen.getByRole('textbox');
+    const inputFieldText = screen.getByDisplayValue(/unimportant/i);
+
+    expect(saveRadioBtn).toBeVisible();
+    expect(fieldLabel).toBeVisible();
+    expect(inputField).toBeVisible();
+    expect(inputFieldText).toBeVisible();
   });
-  it('renders a autocomplete', () => {
-    // @ts-ignore
-    const wrapper = shallow(<SaveDatasetModal {...mockedProps} />);
-    expect(wrapper.find(AutoComplete)).toExist();
+
+  it('renders an "Overwrite existing" field', () => {
+    render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
+
+    const overwriteRadioBtn = screen.getByRole('radio', {
+      name: /overwrite existing select or type dataset name/i,
+    });
+    const fieldLabel = screen.getByText(/overwrite existing/i);
+    const inputField = screen.getByRole('combobox');
+    const placeholderText = screen.getByText(/select or type dataset name/i);
+
+    expect(overwriteRadioBtn).toBeVisible();
+    expect(fieldLabel).toBeVisible();
+    expect(inputField).toBeVisible();
+    expect(placeholderText).toBeVisible();
   });
-  it('renders an input form', () => {
-    // @ts-ignore
-    const wrapper = shallow(<SaveDatasetModal {...mockedProps} />);
-    expect(wrapper.find(Input)).toExist();
+
+  it('renders a save button', () => {
+    render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
+
+    expect(screen.getByRole('button', { name: /save/i })).toBeVisible();
+  });
+
+  it('renders a close button', () => {
+    render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
+
+    expect(screen.getByRole('button', { name: /close/i })).toBeVisible();
   });
 });
diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx 
b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
index 21884dbe8f..94b67a6fb3 100644
--- a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
@@ -17,153 +17,356 @@
  * under the License.
  */
 
-import React, { FunctionComponent } from 'react';
-import { AutoCompleteProps } from 'antd/lib/auto-complete';
+import React, { FunctionComponent, useState } from 'react';
 import { Radio } from 'src/components/Radio';
 import { AutoComplete, RadioChangeEvent } from 'src/components';
 import { Input } from 'src/components/Input';
 import StyledModal from 'src/components/Modal';
 import Button from 'src/components/Button';
-import { styled, t } from '@superset-ui/core';
+import {
+  styled,
+  t,
+  SupersetClient,
+  makeApi,
+  JsonResponse,
+  JsonObject,
+  QueryResponse,
+} from '@superset-ui/core';
+import { useSelector, useDispatch } from 'react-redux';
+import moment from 'moment';
+import rison from 'rison';
+import { createDatasource } from 'src/SqlLab/actions/sqlLab';
+import { addDangerToast } from 'src/components/MessageToasts/actions';
+import { UserWithPermissionsAndRoles as User } from 'src/types/bootstrapTypes';
+import {
+  DatasetRadioState,
+  EXPLORE_CHART_DEFAULT,
+  DatasetOwner,
+  DatasetOptionAutocomplete,
+  SqlLabExploreRootState,
+  getInitialState,
+  ExploreDatasource,
+} from 'src/SqlLab/types';
+import { exploreChart } from 'src/explore/exploreUtils';
 
 interface SaveDatasetModalProps {
   visible: boolean;
-  onOk: () => void;
   onHide: () => void;
-  handleDatasetNameChange: (e: React.FormEvent<HTMLInputElement>) => void;
-  handleSaveDatasetModalSearch: (searchText: string) => Promise<void>;
-  filterAutocompleteOption: (
-    inputValue: string,
-    option: { value: string; datasetId: number },
-  ) => boolean;
-  handleSaveDatasetRadioBtnState: (e: RadioChangeEvent) => void;
-  handleOverwriteCancel: () => void;
-  handleOverwriteDataset: () => void;
-  handleOverwriteDatasetOption: (
-    data: string,
-    option: Record<string, any>,
-  ) => void;
-  onChangeAutoComplete: () => void;
-  defaultCreateDatasetValue: string;
-  disableSaveAndExploreBtn: boolean;
-  saveDatasetRadioBtnState: number;
-  shouldOverwriteDataset: boolean;
-  userDatasetOptions: AutoCompleteProps['options'];
+  buttonTextOnSave: string;
+  buttonTextOnOverwrite: string;
+  modalDescription?: string;
+  datasource: ExploreDatasource;
 }
 
 const Styles = styled.div`
-  .smd-body {
+  .sdm-body {
     margin: 0 8px;
   }
-  .smd-input {
+  .sdm-input {
     margin-left: 45px;
     width: 401px;
   }
-  .smd-autocomplete {
+  .sdm-autocomplete {
     margin-left: 8px;
     width: 401px;
   }
-  .smd-radio {
+  .sdm-radio {
     display: block;
     height: 30px;
     margin: 10px 0px;
     line-height: 30px;
   }
-  .smd-overwrite-msg {
+  .sdm-overwrite-msg {
     margin: 7px;
   }
 `;
 
+const updateDataset = async (
+  dbId: number,
+  datasetId: number,
+  sql: string,
+  columns: Array<Record<string, any>>,
+  owners: [number],
+  overrideColumns: boolean,
+) => {
+  const endpoint = 
`api/v1/dataset/${datasetId}?override_columns=${overrideColumns}`;
+  const headers = { 'Content-Type': 'application/json' };
+  const body = JSON.stringify({
+    sql,
+    columns,
+    owners,
+    database_id: dbId,
+  });
+
+  const data: JsonResponse = await SupersetClient.put({
+    endpoint,
+    headers,
+    body,
+  });
+  return data.json.result;
+};
+
 // eslint-disable-next-line no-empty-pattern
 export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
   visible,
-  onOk,
   onHide,
-  handleDatasetNameChange,
-  handleSaveDatasetRadioBtnState,
-  saveDatasetRadioBtnState,
-  shouldOverwriteDataset,
-  handleOverwriteCancel,
-  handleOverwriteDataset,
-  handleOverwriteDatasetOption,
-  defaultCreateDatasetValue,
-  disableSaveAndExploreBtn,
-  handleSaveDatasetModalSearch,
-  filterAutocompleteOption,
-  userDatasetOptions,
-  onChangeAutoComplete,
-}) => (
-  <StyledModal
-    show={visible}
-    title="Save or Overwrite Dataset"
-    onHide={onHide}
-    footer={
-      <>
-        {!shouldOverwriteDataset && (
-          <Button
-            disabled={disableSaveAndExploreBtn}
-            buttonStyle="primary"
-            onClick={onOk}
-          >
-            {t('Save & Explore')}
-          </Button>
-        )}
-        {shouldOverwriteDataset && (
-          <>
-            <Button onClick={handleOverwriteCancel}>Back</Button>
+  buttonTextOnSave,
+  buttonTextOnOverwrite,
+  modalDescription,
+  datasource,
+}) => {
+  const query = datasource as QueryResponse;
+  const getDefaultDatasetName = () =>
+    `${query.tab} ${moment().format('MM/DD/YYYY HH:mm:ss')}`;
+  const [datasetName, setDatasetName] = useState(getDefaultDatasetName());
+  const [newOrOverwrite, setNewOrOverwrite] = useState(
+    DatasetRadioState.SAVE_NEW,
+  );
+  const [shouldOverwriteDataset, setShouldOverwriteDataset] = useState(false);
+  const [userDatasetOptions, setUserDatasetOptions] = useState<
+    DatasetOptionAutocomplete[]
+  >([]);
+  const [datasetToOverwrite, setDatasetToOverwrite] = useState<
+    Record<string, any>
+  >({});
+  const [autocompleteValue, setAutocompleteValue] = useState('');
+
+  const user = useSelector<SqlLabExploreRootState, User>(user =>
+    getInitialState(user),
+  );
+  const dispatch = useDispatch<(dispatch: any) => Promise<JsonObject>>();
+
+  const handleOverwriteDataset = async () => {
+    await updateDataset(
+      query.dbId,
+      datasetToOverwrite.datasetId,
+      query.sql,
+      query.results.selected_columns.map(
+        (d: { name: string; type: string; is_dttm: boolean }) => ({
+          column_name: d.name,
+          type: d.type,
+          is_dttm: d.is_dttm,
+        }),
+      ),
+      datasetToOverwrite.owners.map((o: DatasetOwner) => o.id),
+      true,
+    );
+
+    setShouldOverwriteDataset(false);
+    setDatasetToOverwrite({});
+    setDatasetName(getDefaultDatasetName());
+
+    exploreChart({
+      ...EXPLORE_CHART_DEFAULT,
+      datasource: `${datasetToOverwrite.datasetId}__table`,
+      all_columns: query.results.selected_columns.map(
+        (d: { name: string; type: string; is_dttm: boolean }) => d.name,
+      ),
+    });
+  };
+
+  const getUserDatasets = async (searchText = '') => {
+    // Making sure that autocomplete input has a value before rendering the 
dropdown
+    // Transforming the userDatasetsOwned data for SaveModalComponent)
+    const { userId } = user;
+    if (userId) {
+      const queryParams = rison.encode({
+        filters: [
+          {
+            col: 'table_name',
+            opr: 'ct',
+            value: searchText,
+          },
+          {
+            col: 'owners',
+            opr: 'rel_m_m',
+            value: userId,
+          },
+        ],
+        order_column: 'changed_on_delta_humanized',
+        order_direction: 'desc',
+      });
+
+      const response = await makeApi({
+        method: 'GET',
+        endpoint: '/api/v1/dataset',
+      })(`q=${queryParams}`);
+
+      return response.result.map(
+        (r: { table_name: string; id: number; owners: [DatasetOwner] }) => ({
+          value: r.table_name,
+          datasetId: r.id,
+          owners: r.owners,
+        }),
+      );
+    }
+
+    return null;
+  };
+
+  const handleSaveInDataset = () => {
+    // if user wants to overwrite a dataset we need to prompt them
+    if (newOrOverwrite === DatasetRadioState.OVERWRITE_DATASET) {
+      setShouldOverwriteDataset(true);
+      return;
+    }
+
+    const selectedColumns = query.results.selected_columns || [];
+
+    // The filters param is only used to test jinja templates.
+    // Remove the special filters entry from the templateParams
+    // before saving the dataset.
+    if (query.templateParams) {
+      const p = JSON.parse(query.templateParams);
+      /* eslint-disable-next-line no-underscore-dangle */
+      if (p._filters) {
+        /* eslint-disable-next-line no-underscore-dangle */
+        delete p._filters;
+        // eslint-disable-next-line no-param-reassign
+        query.templateParams = JSON.stringify(p);
+      }
+    }
+
+    dispatch(
+      createDatasource({
+        schema: query.schema,
+        sql: query.sql,
+        dbId: query.dbId,
+        templateParams: query.templateParams,
+        datasourceName: datasetName,
+        columns: selectedColumns,
+      }),
+    )
+      .then((data: { table_id: number }) => {
+        exploreChart({
+          datasource: `${data.table_id}__table`,
+          metrics: [],
+          groupby: [],
+          time_range: 'No filter',
+          viz_type: 'table',
+          all_columns: selectedColumns.map(c => c.name),
+          row_limit: 1000,
+        });
+      })
+      .catch(() => {
+        addDangerToast(t('An error occurred saving dataset'));
+      });
+
+    setDatasetName(getDefaultDatasetName());
+    onHide();
+  };
+
+  const handleSaveDatasetModalSearch = async (searchText: string) => {
+    const userDatasetsOwned = await getUserDatasets(searchText);
+    setUserDatasetOptions(userDatasetsOwned);
+  };
+
+  const handleOverwriteDatasetOption = (
+    _data: string,
+    option: Record<string, any>,
+  ) => setDatasetToOverwrite(option);
+
+  const handleDatasetNameChange = (e: React.FormEvent<HTMLInputElement>) => {
+    // @ts-expect-error
+    setDatasetName(e.target.value);
+  };
+
+  const handleOverwriteCancel = () => {
+    setShouldOverwriteDataset(false);
+    setDatasetToOverwrite({});
+  };
+
+  const disableSaveAndExploreBtn =
+    (newOrOverwrite === DatasetRadioState.SAVE_NEW &&
+      datasetName.length === 0) ||
+    (newOrOverwrite === DatasetRadioState.OVERWRITE_DATASET &&
+      Object.keys(datasetToOverwrite).length === 0 &&
+      autocompleteValue.length === 0);
+
+  const filterAutocompleteOption = (
+    inputValue: string,
+    option: { value: string; datasetId: number },
+  ) => option.value.toLowerCase().includes(inputValue.toLowerCase());
+
+  return (
+    <StyledModal
+      show={visible}
+      title={t('Save or Overwrite Dataset')}
+      onHide={onHide}
+      footer={
+        <>
+          {!shouldOverwriteDataset && (
             <Button
-              className="md"
-              buttonStyle="primary"
-              onClick={handleOverwriteDataset}
               disabled={disableSaveAndExploreBtn}
+              buttonStyle="primary"
+              onClick={handleSaveInDataset}
             >
-              {t('Overwrite & Explore')}
+              {buttonTextOnSave}
             </Button>
-          </>
+          )}
+          {shouldOverwriteDataset && (
+            <>
+              <Button onClick={handleOverwriteCancel}>Back</Button>
+              <Button
+                className="md"
+                buttonStyle="primary"
+                onClick={handleOverwriteDataset}
+                disabled={disableSaveAndExploreBtn}
+              >
+                {buttonTextOnOverwrite}
+              </Button>
+            </>
+          )}
+        </>
+      }
+    >
+      <Styles>
+        {!shouldOverwriteDataset && (
+          <div className="sdm-body">
+            {modalDescription && (
+              <div className="sdm-prompt">{modalDescription}</div>
+            )}
+            <Radio.Group
+              onChange={(e: RadioChangeEvent) => {
+                setNewOrOverwrite(Number(e.target.value));
+              }}
+              value={newOrOverwrite}
+            >
+              <Radio className="sdm-radio" value={1}>
+                {t('Save as new')}
+                <Input
+                  className="sdm-input"
+                  defaultValue={datasetName}
+                  onChange={handleDatasetNameChange}
+                  disabled={newOrOverwrite !== 1}
+                />
+              </Radio>
+              <Radio className="sdm-radio" value={2}>
+                {t('Overwrite existing')}
+                <AutoComplete
+                  className="sdm-autocomplete"
+                  options={userDatasetOptions}
+                  onSelect={handleOverwriteDatasetOption}
+                  onSearch={handleSaveDatasetModalSearch}
+                  onChange={value => {
+                    setDatasetToOverwrite({});
+                    setAutocompleteValue(value);
+                  }}
+                  placeholder={t('Select or type dataset name')}
+                  filterOption={filterAutocompleteOption}
+                  disabled={newOrOverwrite !== 2}
+                  value={autocompleteValue}
+                />
+              </Radio>
+            </Radio.Group>
+          </div>
         )}
-      </>
-    }
-  >
-    <Styles>
-      {!shouldOverwriteDataset && (
-        <div className="smd-body">
-          <div className="smd-prompt">
-            Save this query as a virtual dataset to continue exploring
+        {shouldOverwriteDataset && (
+          <div className="sdm-overwrite-msg">
+            {t('Are you sure you want to overwrite this dataset?')}
           </div>
-          <Radio.Group
-            onChange={handleSaveDatasetRadioBtnState}
-            value={saveDatasetRadioBtnState}
-          >
-            <Radio className="smd-radio" value={1}>
-              Save as new
-              <Input
-                className="smd-input"
-                defaultValue={defaultCreateDatasetValue}
-                onChange={handleDatasetNameChange}
-                disabled={saveDatasetRadioBtnState !== 1}
-              />
-            </Radio>
-            <Radio className="smd-radio" value={2}>
-              Overwrite existing
-              <AutoComplete
-                className="smd-autocomplete"
-                options={userDatasetOptions}
-                onSelect={handleOverwriteDatasetOption}
-                onSearch={handleSaveDatasetModalSearch}
-                onChange={onChangeAutoComplete}
-                placeholder="Select or type dataset name"
-                filterOption={filterAutocompleteOption}
-                disabled={saveDatasetRadioBtnState !== 2}
-              />
-            </Radio>
-          </Radio.Group>
-        </div>
-      )}
-      {shouldOverwriteDataset && (
-        <div className="smd-overwrite-msg">
-          Are you sure you want to overwrite this dataset?
-        </div>
-      )}
-    </Styles>
-  </StyledModal>
-);
+        )}
+      </Styles>
+    </StyledModal>
+  );
+};
diff --git a/superset-frontend/src/SqlLab/components/TabStatusIcon/index.tsx 
b/superset-frontend/src/SqlLab/components/TabStatusIcon/index.tsx
index 070e749288..799124fb9c 100644
--- a/superset-frontend/src/SqlLab/components/TabStatusIcon/index.tsx
+++ b/superset-frontend/src/SqlLab/components/TabStatusIcon/index.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 import React from 'react';
-import { QueryState } from 'src/SqlLab/types';
+import { QueryState } from '@superset-ui/core';
 
 interface TabStatusIconProps {
   tabState: QueryState;
diff --git a/superset-frontend/src/SqlLab/types.ts 
b/superset-frontend/src/SqlLab/types.ts
index e171479163..fb3993fe84 100644
--- a/superset-frontend/src/SqlLab/types.ts
+++ b/superset-frontend/src/SqlLab/types.ts
@@ -17,76 +17,13 @@
  * under the License.
  */
 import { SupersetError } from 'src/components/ErrorMessage/types';
-import { CtasEnum } from 'src/SqlLab/actions/sqlLab';
 import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
 import { ToastType } from 'src/components/MessageToasts/types';
+import { Dataset } from '@superset-ui/chart-controls';
+import { Query, QueryResponse } from '@superset-ui/core';
+import { ExploreRootState } from 'src/explore/types';
 
-// same as superset.result_set.ResultSetColumnType
-export type Column = {
-  name: string;
-  type: string | null;
-  is_dttm: boolean;
-};
-
-export type QueryState =
-  | 'stopped'
-  | 'failed'
-  | 'pending'
-  | 'running'
-  | 'scheduled'
-  | 'success'
-  | 'fetching'
-  | 'timed_out';
-
-export type Query = {
-  cached: boolean;
-  ctas: boolean;
-  ctas_method?: keyof typeof CtasEnum;
-  dbId: number;
-  errors?: SupersetError[];
-  errorMessage: string | null;
-  extra: {
-    progress: string | null;
-  };
-  id: string;
-  isDataPreview: boolean;
-  link?: string;
-  progress: number;
-  results: {
-    displayLimitReached: boolean;
-    columns: Column[];
-    data: Record<string, unknown>[];
-    expanded_columns: Column[];
-    selected_columns: Column[];
-    query: { limit: number };
-  };
-  resultsKey: string | null;
-  schema?: string;
-  sql: string;
-  sqlEditorId: string;
-  state: QueryState;
-  tab: string | null;
-  tempSchema: string | null;
-  tempTable: string;
-  trackingUrl: string | null;
-  templateParams: any;
-  rows: number;
-  queryLimit: number;
-  limitingFactor: string;
-  endDttm: number;
-  duration: string;
-  startDttm: number;
-  time: Record<string, any>;
-  user: Record<string, any>;
-  userId: number;
-  db: Record<string, any>;
-  started: string;
-  querylink: Record<string, any>;
-  queryId: number;
-  executedSql: string;
-  output: string | Record<string, any>;
-  actions: Record<string, any>;
-};
+export type ExploreDatasource = Dataset | QueryResponse;
 
 export interface QueryEditor {
   dbId?: number;
@@ -109,7 +46,7 @@ export type toastState = {
   noDuplicate: boolean;
 };
 
-export type RootState = {
+export type SqlLabRootState = {
   sqlLab: {
     activeSouthPaneTab: string | number; // default is string; 
action.newQuery.id is number
     alerts: any[];
@@ -128,3 +65,44 @@ export type RootState = {
   messageToasts: toastState[];
   common: {};
 };
+
+export type SqlLabExploreRootState = SqlLabRootState | ExploreRootState;
+
+export const getInitialState = (state: SqlLabExploreRootState) => {
+  if (state.hasOwnProperty('sqlLab')) {
+    const {
+      sqlLab: { user },
+    } = state as SqlLabRootState;
+    return user;
+  }
+
+  const {
+    explore: { user },
+  } = state as ExploreRootState;
+  return user;
+};
+
+export enum DatasetRadioState {
+  SAVE_NEW = 1,
+  OVERWRITE_DATASET = 2,
+}
+
+export const EXPLORE_CHART_DEFAULT = {
+  metrics: [],
+  groupby: [],
+  time_range: 'No filter',
+  viz_type: 'table',
+};
+
+export interface DatasetOwner {
+  first_name: string;
+  id: number;
+  last_name: string;
+  username: string;
+}
+
+export interface DatasetOptionAutocomplete {
+  value: string;
+  datasetId: number;
+  owners: [DatasetOwner];
+}
diff --git 
a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts 
b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts
index 0b84c73411..6d9ab6fe9c 100644
--- a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts
+++ b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts
@@ -42,8 +42,8 @@ export enum IndicatorStatus {
 
 const TIME_GRANULARITY_FIELDS = new Set(Object.values(TIME_FILTER_MAP));
 
-// As of 2020-09-28, the DatasourceMeta type in superset-ui is incorrect.
-// Should patch it here until the DatasourceMeta type is updated.
+// As of 2020-09-28, the Dataset type in superset-ui is incorrect.
+// Should patch it here until the Dataset type is updated.
 type Datasource = {
   time_grain_sqla?: [string, string][];
   granularity?: [string, string][];
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/utils.ts
 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/utils.ts
index e946cb6714..8fdb3b0325 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/utils.ts
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/utils.ts
@@ -19,7 +19,7 @@
 import { flatMapDeep } from 'lodash';
 import { FormInstance } from 'src/components';
 import React from 'react';
-import { CustomControlItem, DatasourceMeta } from 
'@superset-ui/chart-controls';
+import { CustomControlItem, Dataset } from '@superset-ui/chart-controls';
 import { Column, ensureIsArray, GenericDataType } from '@superset-ui/core';
 import { DatasourcesState, ChartsState } from 'src/dashboard/types';
 
@@ -80,16 +80,16 @@ type DatasetSelectValue = {
 };
 
 export const datasetToSelectOption = (
-  item: DatasourceMeta & { table_name: string },
+  item: Dataset & { table_name: string },
 ): DatasetSelectValue => ({
   value: item.id,
   label: item.table_name,
 });
 
-// TODO: add column_types field to DatasourceMeta
+// TODO: add column_types field to Dataset
 // We return true if column_types is undefined or empty as a precaution 
against backend failing to return column_types
 export const hasTemporalColumns = (
-  dataset: DatasourceMeta & { column_types: GenericDataType[] },
+  dataset: Dataset & { column_types: GenericDataType[] },
 ) => {
   const columnTypes = ensureIsArray(dataset?.column_types);
   return (
diff --git a/superset-frontend/src/dashboard/types.ts 
b/superset-frontend/src/dashboard/types.ts
index c0b312d434..e4b8227689 100644
--- a/superset-frontend/src/dashboard/types.ts
+++ b/superset-frontend/src/dashboard/types.ts
@@ -24,7 +24,7 @@ import {
   JsonObject,
   NativeFiltersState,
 } from '@superset-ui/core';
-import { DatasourceMeta } from '@superset-ui/chart-controls';
+import { Dataset } from '@superset-ui/chart-controls';
 import { chart } from 'src/components/Chart/chartReducer';
 import componentTypes from 'src/dashboard/util/componentTypes';
 
@@ -84,7 +84,7 @@ export type DashboardInfo = {
 
 export type ChartsState = { [key: string]: Chart };
 
-export type Datasource = DatasourceMeta & {
+export type Datasource = Dataset & {
   uid: string;
   column_types: GenericDataType[];
   table_name: string;
diff --git a/superset-frontend/src/explore/actions/exploreActions.ts 
b/superset-frontend/src/explore/actions/exploreActions.ts
index fe45d0b63e..2b6c0f5490 100644
--- a/superset-frontend/src/explore/actions/exploreActions.ts
+++ b/superset-frontend/src/explore/actions/exploreActions.ts
@@ -17,7 +17,7 @@
  * under the License.
  */
 /* eslint camelcase: 0 */
-import { DatasourceMeta } from '@superset-ui/chart-controls';
+import { Dataset } from '@superset-ui/chart-controls';
 import {
   t,
   SupersetClient,
@@ -39,12 +39,12 @@ export function setDatasourceType(datasourceType: 
DatasourceType) {
 }
 
 export const SET_DATASOURCE = 'SET_DATASOURCE';
-export function setDatasource(datasource: DatasourceMeta) {
+export function setDatasource(datasource: Dataset) {
   return { type: SET_DATASOURCE, datasource };
 }
 
 export const SET_DATASOURCES = 'SET_DATASOURCES';
-export function setDatasources(datasources: DatasourceMeta[]) {
+export function setDatasources(datasources: Dataset[]) {
   return { type: SET_DATASOURCES, datasources };
 }
 
diff --git 
a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx 
b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
index ef9517e4dd..6ed73f2b38 100644
--- a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
+++ b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
@@ -40,7 +40,7 @@ import {
   ControlPanelSectionConfig,
   ControlState,
   CustomControlItem,
-  DatasourceMeta,
+  Dataset,
   ExpandedControlItem,
   InfoTooltipWithTrigger,
   sections,
@@ -174,13 +174,13 @@ const isTimeSection = (section: 
ControlPanelSectionConfig): boolean =>
   (sections.legacyRegularTime.label === section.label ||
     sections.legacyTimeseriesTime.label === section.label);
 
-const hasTimeColumn = (datasource: DatasourceMeta): boolean =>
+const hasTimeColumn = (datasource: Dataset): boolean =>
   datasource?.columns?.some(c => c.is_dttm) ||
   datasource.type === DatasourceType.Druid;
 
 const sectionsToExpand = (
   sections: ControlPanelSectionConfig[],
-  datasource: DatasourceMeta,
+  datasource: Dataset,
 ): string[] =>
   // avoid expanding time section if datasource doesn't include time column
   sections.reduce(
@@ -193,7 +193,7 @@ const sectionsToExpand = (
 
 function getState(
   vizType: string,
-  datasource: DatasourceMeta,
+  datasource: Dataset,
   datasourceType: DatasourceType,
 ) {
   const querySections: ControlPanelSectionConfig[] = [];
diff --git 
a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx
 
b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx
index 99c596b80e..4b19c5b2f3 100644
--- 
a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx
+++ 
b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx
@@ -92,19 +92,19 @@ function search(value: string, input: HTMLElement) {
 }
 
 test('should render', () => {
-  const { container } = render(setup(props));
+  const { container } = render(setup(props), { useRedux: true });
   expect(container).toBeVisible();
 });
 
 test('should display items in controls', () => {
-  render(setup(props));
+  render(setup(props), { useRedux: true });
   expect(screen.getByText('birth_names')).toBeInTheDocument();
   expect(screen.getByText('Metrics')).toBeInTheDocument();
   expect(screen.getByText('Columns')).toBeInTheDocument();
 });
 
 test('should render the metrics', () => {
-  render(setup(props));
+  render(setup(props), { useRedux: true });
   const metricsNum = metrics.length;
   metrics.forEach(metric =>
     expect(screen.getByText(metric.metric_name)).toBeInTheDocument(),
@@ -115,7 +115,7 @@ test('should render the metrics', () => {
 });
 
 test('should render the columns', () => {
-  render(setup(props));
+  render(setup(props), { useRedux: true });
   const columnsNum = columns.length;
   columns.forEach(col =>
     expect(screen.getByText(col.column_name)).toBeInTheDocument(),
@@ -126,7 +126,7 @@ test('should render the columns', () => {
 });
 
 test('should render 0 search results', async () => {
-  render(setup(props));
+  render(setup(props), { useRedux: true });
   const searchInput = screen.getByPlaceholderText('Search Metrics & Columns');
 
   search('nothing', searchInput);
@@ -134,7 +134,7 @@ test('should render 0 search results', async () => {
 });
 
 test('should search and render matching columns', async () => {
-  render(setup(props));
+  render(setup(props), { useRedux: true });
   const searchInput = screen.getByPlaceholderText('Search Metrics & Columns');
 
   search(columns[0].column_name, searchInput);
@@ -146,7 +146,7 @@ test('should search and render matching columns', async () 
=> {
 });
 
 test('should search and render matching metrics', async () => {
-  render(setup(props));
+  render(setup(props), { useRedux: true });
   const searchInput = screen.getByPlaceholderText('Search Metrics & Columns');
 
   search(metrics[0].metric_name, searchInput);
@@ -174,8 +174,68 @@ test('should render a warning', async () => {
         },
       },
     }),
+    { useRedux: true },
   );
   expect(
     await screen.findByRole('img', { name: 'alert-solid' }),
   ).toBeInTheDocument();
 });
+
+test('should render a create dataset infobox', () => {
+  render(
+    setup({
+      ...props,
+      datasource: {
+        ...datasource,
+        type: DatasourceType.Query,
+      },
+    }),
+    { useRedux: true },
+  );
+
+  const createButton = screen.getByRole('button', {
+    name: /create a dataset/i,
+  });
+  const infoboxText = screen.getByText(/to edit or add columns and metrics./i);
+
+  expect(createButton).toBeVisible();
+  expect(infoboxText).toBeVisible();
+});
+
+test('should render a save dataset modal when "Create a dataset" is clicked', 
() => {
+  render(
+    setup({
+      ...props,
+      datasource: {
+        ...datasource,
+        type: DatasourceType.Query,
+      },
+    }),
+    { useRedux: true },
+  );
+
+  const createButton = screen.getByRole('button', {
+    name: /create a dataset/i,
+  });
+
+  userEvent.click(createButton);
+
+  const saveDatasetModalTitle = screen.getByText(/save or overwrite dataset/i);
+
+  expect(saveDatasetModalTitle).toBeVisible();
+});
+
+test('should not render a save dataset modal when datasource is not query or 
dataset', () => {
+  render(
+    setup({
+      ...props,
+      datasource: {
+        ...datasource,
+        type: DatasourceType.Table,
+      },
+    }),
+    { useRedux: true },
+  );
+
+  expect(screen.queryByText(/create a dataset/i)).toBe(null);
+});
diff --git a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx 
b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
index 8bd39aa52f..e8ea306814 100644
--- a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
+++ b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
@@ -17,32 +17,33 @@
  * under the License.
  */
 import React, { useEffect, useMemo, useRef, useState } from 'react';
-import { css, styled, t } from '@superset-ui/core';
+import { css, styled, t, DatasourceType } from '@superset-ui/core';
 import {
   ControlConfig,
-  DatasourceMeta,
+  Dataset,
   ColumnMeta,
 } from '@superset-ui/chart-controls';
 import { debounce } from 'lodash';
 import { matchSorter, rankings } from 'match-sorter';
 import Collapse from 'src/components/Collapse';
+import Alert from 'src/components/Alert';
+import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
 import { Input } from 'src/components/Input';
 import { FAST_DEBOUNCE } from 'src/constants';
 import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
 import { ExploreActions } from 'src/explore/actions/exploreActions';
 import Control from 'src/explore/components/Control';
-import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
+import { ExploreDatasource } from 'src/SqlLab/types';
 import DatasourcePanelDragOption from './DatasourcePanelDragOption';
 import { DndItemType } from '../DndItemType';
 import { StyledColumnOption, StyledMetricOption } from '../optionRenderers';
 
 interface DatasourceControl extends ControlConfig {
-  datasource?: DatasourceMeta;
-  user: UserWithPermissionsAndRoles;
+  datasource?: ExploreDatasource;
 }
 
 export interface Props {
-  datasource: DatasourceMeta;
+  datasource: Dataset;
   controls: {
     datasource: DatasourceControl;
   };
@@ -154,6 +155,16 @@ const SectionHeader = styled.span`
   `}
 `;
 
+const StyledInfoboxWrapper = styled.div`
+  ${({ theme }) => css`
+    margin: 0 ${theme.gridUnit * 2.5}px;
+
+    span {
+      text-decoration: underline;
+    }
+  `}
+`;
+
 const LabelContainer = (props: {
   children: React.ReactElement;
   className: string;
@@ -192,6 +203,7 @@ export default function DataSourcePanel({
     [_columns],
   );
 
+  const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false);
   const [inputValue, setInputValue] = useState('');
   const [lists, setList] = useState({
     columns,
@@ -279,6 +291,7 @@ export default function DataSourcePanel({
         : lists.metrics.slice(0, DEFAULT_MAX_METRICS_LENGTH),
     [lists.metrics, showAllMetrics],
   );
+
   const columnSlice = useMemo(
     () =>
       showAllColumns
@@ -289,6 +302,17 @@ export default function DataSourcePanel({
     [lists.columns, showAllColumns],
   );
 
+  const showInfoboxCheck = () => {
+    if (sessionStorage.getItem('showInfobox') === 'false') return false;
+    return true;
+  };
+
+  const isValidDatasourceType =
+    datasource.type === DatasourceType.Dataset ||
+    datasource.type === DatasourceType.SlTable ||
+    datasource.type === DatasourceType.SavedQuery ||
+    datasource.type === DatasourceType.Query;
+
   const mainBody = useMemo(
     () => (
       <>
@@ -303,6 +327,29 @@ export default function DataSourcePanel({
           placeholder={t('Search Metrics & Columns')}
         />
         <div className="field-selections">
+          {isValidDatasourceType && showInfoboxCheck() && (
+            <StyledInfoboxWrapper>
+              <Alert
+                closable
+                onClose={() => sessionStorage.setItem('showInfobox', 'false')}
+                type="info"
+                message=""
+                description={
+                  <>
+                    <span
+                      role="button"
+                      tabIndex={0}
+                      onClick={() => setShowSaveDatasetModal(true)}
+                      className="add-dataset-alert-description"
+                    >
+                      {t('Create a dataset')}
+                    </span>
+                    {t(' to edit or add columns and metrics.')}
+                  </>
+                }
+              />
+            </StyledInfoboxWrapper>
+          )}
           <Collapse
             defaultActiveKey={['metrics', 'column']}
             expandIconPosition="right"
@@ -399,6 +446,13 @@ export default function DataSourcePanel({
 
   return (
     <DatasourceContainer>
+      <SaveDatasetModal
+        visible={showSaveDatasetModal}
+        onHide={() => setShowSaveDatasetModal(false)}
+        buttonTextOnSave={t('Save')}
+        buttonTextOnOverwrite={t('Overwrite')}
+        datasource={datasource}
+      />
       <Control {...datasourceControl} name="datasource" actions={actions} />
       {datasource.id != null && mainBody}
     </DatasourceContainer>
diff --git 
a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx 
b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
index e102f2dc97..9f33e9df82 100644
--- a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
@@ -622,6 +622,7 @@ function ExploreViewContainer(props) {
             controls={props.controls}
             actions={props.actions}
             shouldForceUpdate={shouldForceUpdate}
+            user={props.user}
           />
         </Resizable>
         {isCollapsed ? (
diff --git 
a/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.ts
 
b/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.ts
index efd594cb7f..e070e82464 100644
--- 
a/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.ts
+++ 
b/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.ts
@@ -17,11 +17,7 @@
  * under the License.
  */
 
-import {
-  ControlState,
-  DatasourceMeta,
-  Metric,
-} from '@superset-ui/chart-controls';
+import { ControlState, Dataset, Metric } from '@superset-ui/chart-controls';
 import {
   Column,
   isAdhocMetricSimple,
@@ -33,7 +29,7 @@ import {
 import AdhocMetric from 
'src/explore/components/controls/MetricControl/AdhocMetric';
 
 const isControlValueCompatibleWithDatasource = (
-  datasource: DatasourceMeta,
+  datasource: Dataset,
   controlState: ControlState,
   value: any,
 ) => {
@@ -78,7 +74,7 @@ const isControlValueCompatibleWithDatasource = (
 };
 
 export const getControlValuesCompatibleWithDatasource = (
-  datasource: DatasourceMeta,
+  datasource: Dataset,
   controlState: ControlState,
   value: JsonValue,
 ) => {
diff --git a/superset-frontend/src/explore/fixtures.tsx 
b/superset-frontend/src/explore/fixtures.tsx
index 7ce2626069..78579b82e4 100644
--- a/superset-frontend/src/explore/fixtures.tsx
+++ b/superset-frontend/src/explore/fixtures.tsx
@@ -99,7 +99,7 @@ export const controlPanelSectionsChartOptionsTable: 
ControlPanelSectionConfig[]
               optionRenderer: c => <ColumnOption column={c} showType />,
               valueKey: 'column_name',
               mapStateToProps: stateRef => ({
-                options: stateRef.datasource ? stateRef.datasource.columns : 
[],
+                options: stateRef.datasource?.columns || [],
               }),
               freeForm: true,
             } as ControlConfig<'SelectControl', ColumnMeta>,
diff --git a/superset-frontend/src/explore/reducers/getInitialState.ts 
b/superset-frontend/src/explore/reducers/getInitialState.ts
index 45440f6f5b..4d598fb722 100644
--- a/superset-frontend/src/explore/reducers/getInitialState.ts
+++ b/superset-frontend/src/explore/reducers/getInitialState.ts
@@ -18,10 +18,7 @@
  */
 import shortid from 'shortid';
 import { DatasourceType, JsonObject, QueryFormData } from '@superset-ui/core';
-import {
-  ControlStateMapping,
-  DatasourceMeta,
-} from '@superset-ui/chart-controls';
+import { ControlStateMapping, Dataset } from '@superset-ui/chart-controls';
 import {
   CommonBootstrapData,
   UserWithPermissionsAndRoles,
@@ -41,7 +38,7 @@ export interface ExplorePageBootstrapData extends JsonObject {
   can_download: boolean;
   can_overwrite: boolean;
   common: CommonBootstrapData;
-  datasource: DatasourceMeta;
+  datasource: Dataset;
   datasource_id: number;
   datasource_type: DatasourceType;
   forced_height: string | null;
diff --git a/superset-frontend/src/explore/types.ts 
b/superset-frontend/src/explore/types.ts
index fe6436ab86..4d50b449c5 100644
--- a/superset-frontend/src/explore/types.ts
+++ b/superset-frontend/src/explore/types.ts
@@ -22,8 +22,10 @@ import {
   AnnotationData,
   AdhocMetric,
 } from '@superset-ui/core';
-import { ColumnMeta, DatasourceMeta } from '@superset-ui/chart-controls';
+import { ColumnMeta, Dataset } from '@superset-ui/chart-controls';
 import { DatabaseObject } from 'src/views/CRUD/types';
+import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
+import { toastState } from 'src/SqlLab/types';
 
 export { Slice, Chart } from 'src/types/Chart';
 
@@ -56,9 +58,35 @@ export type OptionSortType = Partial<
   ColumnMeta & AdhocMetric & { saved_metric_name: string }
 >;
 
-export type Datasource = DatasourceMeta & {
+export type Datasource = Dataset & {
   database?: DatabaseObject;
   datasource?: string;
   schema?: string;
   is_sqllab_view?: boolean;
 };
+
+export type ExploreRootState = {
+  explore: {
+    can_add: boolean;
+    can_download: boolean;
+    common: object;
+    controls: object;
+    controlsTransferred: object;
+    datasource: object;
+    datasource_id: number;
+    datasource_type: string;
+    force: boolean;
+    forced_height: object;
+    form_data: object;
+    isDatasourceMetaLoading: boolean;
+    isStarred: boolean;
+    slice: object;
+    sliceName: string;
+    standalone: boolean;
+    timeFormattedColumns: object;
+    user: UserWithPermissionsAndRoles;
+  };
+  localStorageUsageInKilobytes: number;
+  messageToasts: toastState[];
+  common: {};
+};

Reply via email to