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