You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by hu...@apache.org on 2022/09/01 18:01:40 UTC
[superset] 01/01: Revert "perf(sqllab): Rendering perf improvement using immutable state (#20877)"
This is an automated email from the ASF dual-hosted git repository.
hugh pushed a commit to branch revert-f77b910e2cc9f1bd90ac0f3a9097ec5d394b582d
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 15c713e90fca3478e48544a95377cbd0f180c4ff
Author: hughhhh <hu...@gmail.com>
AuthorDate: Thu Sep 1 10:58:40 2022 -0700
Revert "perf(sqllab): Rendering perf improvement using immutable state (#20877)"
This reverts commit f77b910e2cc9f1bd90ac0f3a9097ec5d394b582d.
---
.../cypress/integration/sqllab/tabs.test.ts | 6 +-
superset-frontend/src/SqlLab/App.jsx | 9 -
superset-frontend/src/SqlLab/actions/sqlLab.js | 102 +-----
.../src/SqlLab/actions/sqlLab.test.js | 122 +++----
.../AceEditorWrapper/AceEditorWrapper.test.tsx | 129 -------
.../SqlLab/components/AceEditorWrapper/index.tsx | 51 +--
.../EstimateQueryCostButton.test.tsx | 93 -----
.../components/EstimateQueryCostButton/index.tsx | 32 +-
.../QueryLimitSelect/QueryLimitSelect.test.tsx | 137 --------
.../SqlLab/components/QueryLimitSelect/index.tsx | 118 -------
.../RunQueryActionButton.test.jsx | 53 +++
.../RunQueryActionButton.test.tsx | 151 ---------
.../components/RunQueryActionButton/index.tsx | 32 +-
.../SqlLab/components/SaveQuery/SaveQuery.test.jsx | 47 +--
.../src/SqlLab/components/SaveQuery/index.tsx | 35 +-
.../SqlLab/components/SqlEditor/SqlEditor.test.jsx | 53 +--
.../src/SqlLab/components/SqlEditor/index.jsx | 165 ++++++---
.../SqlLab/components/SqlEditorLeftBar/index.tsx | 19 +-
.../SqlEditorTabHeader/SqlEditorTabHeader.test.tsx | 220 ------------
.../SqlLab/components/SqlEditorTabHeader/index.tsx | 147 --------
.../TabbedSqlEditors/TabbedSqlEditors.test.jsx | 17 +-
.../SqlLab/components/TabbedSqlEditors/index.jsx | 142 ++++++--
.../TemplateParamsEditor.test.tsx | 90 +----
.../components/TemplateParamsEditor/index.tsx | 22 +-
superset-frontend/src/SqlLab/fixtures.ts | 29 +-
.../src/SqlLab/reducers/getInitialState.js | 38 +--
superset-frontend/src/SqlLab/reducers/sqlLab.js | 376 ++++++---------------
.../src/SqlLab/reducers/sqlLab.test.js | 67 +---
superset-frontend/src/SqlLab/types.ts | 26 +-
.../src/SqlLab/utils/newQueryTabName.test.ts | 2 -
.../src/components/Dropdown/index.tsx | 2 +-
.../src/components/TableSelector/index.tsx | 3 +-
32 files changed, 606 insertions(+), 1929 deletions(-)
diff --git a/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.ts b/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.ts
index c7f1a23e47..0e85664cb7 100644
--- a/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.ts
+++ b/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.ts
@@ -30,10 +30,8 @@ describe('SqlLab query tabs', () => {
const initialUntitledCount = Math.max(
0,
...tabs
- .map(
- (i, tabItem) =>
- Number(tabItem.textContent?.match(/Untitled Query (\d+)/)?.[1]) ||
- 0,
+ .map((i, tabItem) =>
+ Number(tabItem.textContent?.match(/Untitled Query (\d+)/)?.[1]),
)
.toArray(),
);
diff --git a/superset-frontend/src/SqlLab/App.jsx b/superset-frontend/src/SqlLab/App.jsx
index 39f784a5c5..02a4df2a6f 100644
--- a/superset-frontend/src/SqlLab/App.jsx
+++ b/superset-frontend/src/SqlLab/App.jsx
@@ -67,9 +67,6 @@ const sqlLabPersistStateConfig = {
...state[path],
queries: emptyQueryResults(state[path].queries),
queryEditors: clearQueryEditors(state[path].queryEditors),
- unsavedQueryEditor: clearQueryEditors([
- state[path].unsavedQueryEditor,
- ])[0],
};
}
});
@@ -94,12 +91,6 @@ const sqlLabPersistStateConfig = {
const result = {
...initialState,
...persistedState,
- sqlLab: {
- ...(persistedState?.sqlLab || {}),
- // Overwrite initialState over persistedState for sqlLab
- // since a logic in getInitialState overrides the value from persistedState
- ...initialState.sqlLab,
- },
};
// Filter out any user data that may have been persisted in an older version.
// Get user from bootstrap data instead, every time
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js
index cb6f3c4095..df21e32df3 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.js
@@ -123,17 +123,6 @@ const fieldConverter = mapping => obj =>
const convertQueryToServer = fieldConverter(queryServerMapping);
const convertQueryToClient = fieldConverter(queryClientMapping);
-export function getUpToDateQuery(rootState, queryEditor, key) {
- const {
- sqlLab: { unsavedQueryEditor },
- } = rootState;
- const id = key ?? queryEditor.id;
- return {
- ...queryEditor,
- ...(id === unsavedQueryEditor.id && unsavedQueryEditor),
- };
-}
-
export function resetState() {
return { type: RESET_STATE };
}
@@ -178,26 +167,24 @@ export function scheduleQuery(query) {
);
}
-export function estimateQueryCost(queryEditor) {
- return (dispatch, getState) => {
- const { dbId, schema, sql, selectedText, templateParams } =
- getUpToDateQuery(getState(), queryEditor);
- const requestSql = selectedText || sql;
- const endpoint =
- schema === null
- ? `/superset/estimate_query_cost/${dbId}/`
- : `/superset/estimate_query_cost/${dbId}/${schema}/`;
- return Promise.all([
- dispatch({ type: COST_ESTIMATE_STARTED, query: queryEditor }),
+export function estimateQueryCost(query) {
+ const { dbId, schema, sql, templateParams } = query;
+ const endpoint =
+ schema === null
+ ? `/superset/estimate_query_cost/${dbId}/`
+ : `/superset/estimate_query_cost/${dbId}/${schema}/`;
+ return dispatch =>
+ Promise.all([
+ dispatch({ type: COST_ESTIMATE_STARTED, query }),
SupersetClient.post({
endpoint,
postPayload: {
- sql: requestSql,
+ sql,
templateParams: JSON.parse(templateParams || '{}'),
},
})
.then(({ json }) =>
- dispatch({ type: COST_ESTIMATE_RETURNED, query: queryEditor, json }),
+ dispatch({ type: COST_ESTIMATE_RETURNED, query, json }),
)
.catch(response =>
getClientErrorObject(response).then(error => {
@@ -207,13 +194,12 @@ export function estimateQueryCost(queryEditor) {
t('Failed at retrieving results');
return dispatch({
type: COST_ESTIMATE_FAILED,
- query: queryEditor,
+ query,
error: message,
});
}),
),
]);
- };
}
export function startQuery(query) {
@@ -371,34 +357,6 @@ export function runQuery(query) {
};
}
-export function runQueryFromSqlEditor(
- database,
- queryEditor,
- defaultQueryLimit,
- tempTable,
- ctas,
- ctasMethod,
-) {
- return function (dispatch, getState) {
- const qe = getUpToDateQuery(getState(), queryEditor, queryEditor.id);
- const query = {
- dbId: qe.dbId,
- sql: qe.selectedText || qe.sql,
- sqlEditorId: qe.id,
- tab: qe.name,
- schema: qe.schema,
- tempTable,
- templateParams: qe.templateParams,
- queryLimit: qe.queryLimit || defaultQueryLimit,
- runAsync: database ? database.allow_run_async : false,
- ctas,
- ctas_method: ctasMethod,
- updateTabState: !qe.selectedText,
- };
- dispatch(runQuery(query));
- };
-}
-
export function reRunQuery(query) {
// run Query with a new id
return function (dispatch) {
@@ -406,23 +364,8 @@ export function reRunQuery(query) {
};
}
-export function validateQuery(queryEditor, sql) {
- return function (dispatch, getState) {
- const {
- sqlLab: { unsavedQueryEditor },
- } = getState();
- const qe = {
- ...queryEditor,
- ...(queryEditor.id === unsavedQueryEditor.id && unsavedQueryEditor),
- };
-
- const query = {
- dbId: qe.dbId,
- sql,
- sqlEditorId: qe.id,
- schema: qe.schema,
- templateParams: qe.templateParams,
- };
+export function validateQuery(query) {
+ return function (dispatch) {
dispatch(startQueryValidation(query));
const postPayload = {
@@ -677,7 +620,6 @@ export function switchQueryEditor(queryEditor, displayLimit) {
return function (dispatch) {
if (
isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) &&
- queryEditor &&
!queryEditor.loaded
) {
SupersetClient.get({
@@ -781,17 +723,6 @@ export function removeQueryEditor(queryEditor) {
};
}
-export function removeAllOtherQueryEditors(queryEditor) {
- return function (dispatch, getState) {
- const { sqlLab } = getState();
- sqlLab.queryEditors?.forEach(otherQueryEditor => {
- if (otherQueryEditor.id !== queryEditor.id) {
- dispatch(removeQueryEditor(otherQueryEditor));
- }
- });
- };
-}
-
export function removeQuery(query) {
return function (dispatch) {
const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
@@ -990,9 +921,8 @@ export function queryEditorSetSql(queryEditor, sql) {
return { type: QUERY_EDITOR_SET_SQL, queryEditor, sql };
}
-export function queryEditorSetAndSaveSql(targetQueryEditor, sql) {
- return function (dispatch, getState) {
- const queryEditor = getUpToDateQuery(getState(), targetQueryEditor);
+export function queryEditorSetAndSaveSql(queryEditor, sql) {
+ return function (dispatch) {
// saved query and set tab state use this action
dispatch(queryEditorSetSql(queryEditor, sql));
if (isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)) {
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
index 4dadb9ea89..1c4509b3a1 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
@@ -24,7 +24,7 @@ import thunk from 'redux-thunk';
import shortid from 'shortid';
import * as featureFlags from 'src/featureFlags';
import * as actions from 'src/SqlLab/actions/sqlLab';
-import { defaultQueryEditor, query, initialState } from 'src/SqlLab/fixtures';
+import { defaultQueryEditor, query } from '../fixtures';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
@@ -32,13 +32,14 @@ const mockStore = configureMockStore(middlewares);
describe('async actions', () => {
const mockBigNumber = '9223372036854775807';
const queryEditor = {
- ...defaultQueryEditor,
id: 'abcd',
autorun: false,
+ dbId: null,
latestQueryId: null,
+ selectedText: null,
sql: 'SELECT *\nFROM\nWHERE',
name: 'Untitled Query 1',
- schemaOptions: [{ value: 'main', label: 'main', title: 'main' }],
+ schemaOptions: [{ value: 'main', label: 'main', name: 'main' }],
};
let dispatch;
@@ -64,20 +65,20 @@ describe('async actions', () => {
const makeRequest = () => {
const request = actions.saveQuery(query);
- return request(dispatch, () => initialState);
+ return request(dispatch);
};
it('posts to the correct url', () => {
expect.assertions(1);
- const store = mockStore(initialState);
+ const store = mockStore({});
return store.dispatch(actions.saveQuery(query)).then(() => {
expect(fetchMock.calls(saveQueryEndpoint)).toHaveLength(1);
});
});
it('posts the correct query object', () => {
- const store = mockStore(initialState);
+ const store = mockStore({});
return store.dispatch(actions.saveQuery(query)).then(() => {
const call = fetchMock.calls(saveQueryEndpoint)[0];
const formData = call[1].body;
@@ -106,7 +107,7 @@ describe('async actions', () => {
it('onSave calls QUERY_EDITOR_SAVED and QUERY_EDITOR_SET_TITLE', () => {
expect.assertions(1);
- const store = mockStore(initialState);
+ const store = mockStore({});
const expectedActionTypes = [
actions.QUERY_EDITOR_SAVED,
actions.QUERY_EDITOR_SET_TITLE,
@@ -190,7 +191,7 @@ describe('async actions', () => {
describe('runQuery without query params', () => {
const makeRequest = () => {
const request = actions.runQuery(query);
- return request(dispatch, () => initialState);
+ return request(dispatch);
};
it('makes the fetch request', () => {
@@ -223,9 +224,7 @@ describe('async actions', () => {
const store = mockStore({});
const expectedActionTypes = [actions.START_QUERY, actions.QUERY_SUCCESS];
- const { dispatch } = store;
- const request = actions.runQuery(query);
- return request(dispatch, () => initialState).then(() => {
+ return store.dispatch(actions.runQuery(query)).then(() => {
expect(store.getActions().map(a => a.type)).toEqual(
expectedActionTypes,
);
@@ -243,9 +242,7 @@ describe('async actions', () => {
const store = mockStore({});
const expectedActionTypes = [actions.START_QUERY, actions.QUERY_FAILED];
- const { dispatch } = store;
- const request = actions.runQuery(query);
- return request(dispatch, () => initialState).then(() => {
+ return store.dispatch(actions.runQuery(query)).then(() => {
expect(store.getActions().map(a => a.type)).toEqual(
expectedActionTypes,
);
@@ -268,19 +265,15 @@ describe('async actions', () => {
const makeRequest = () => {
const request = actions.runQuery(query);
- return request(dispatch, () => initialState);
+ return request(dispatch);
};
- it('makes the fetch request', async () => {
- const runQueryEndpointWithParams = 'glob:*/superset/sql_json/?foo=bar';
- fetchMock.post(
- runQueryEndpointWithParams,
- `{ "data": ${mockBigNumber} }`,
- );
- await makeRequest().then(() => {
- expect(fetchMock.calls(runQueryEndpointWithParams)).toHaveLength(1);
- });
- });
+ it('makes the fetch request', () =>
+ makeRequest().then(() => {
+ expect(
+ fetchMock.calls('glob:*/superset/sql_json/?foo=bar'),
+ ).toHaveLength(1);
+ }));
});
describe('reRunQuery', () => {
@@ -298,12 +291,10 @@ describe('async actions', () => {
sqlLab: {
tabHistory: [id],
queryEditors: [{ id, name: 'Dummy query editor' }],
- unsavedQueryEditor: {},
},
};
const store = mockStore(state);
- const request = actions.reRunQuery(query);
- request(store.dispatch, store.getState);
+ store.dispatch(actions.reRunQuery(query));
expect(store.getActions()[0].query.id).toEqual('abcd');
});
});
@@ -360,7 +351,6 @@ describe('async actions', () => {
sqlLab: {
tabHistory: [id],
queryEditors: [{ id, name: 'Dummy query editor' }],
- unsavedQueryEditor: {},
},
};
const store = mockStore(state);
@@ -379,10 +369,11 @@ describe('async actions', () => {
},
},
];
- const request = actions.cloneQueryToNewTab(query, true);
- return request(store.dispatch, store.getState).then(() => {
- expect(store.getActions()).toEqual(expectedActions);
- });
+ return store
+ .dispatch(actions.cloneQueryToNewTab(query, true))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
});
});
@@ -398,17 +389,18 @@ describe('async actions', () => {
it('creates new query editor', () => {
expect.assertions(1);
- const store = mockStore(initialState);
+ const store = mockStore({});
const expectedActions = [
{
type: actions.ADD_QUERY_EDITOR,
queryEditor,
},
];
- const request = actions.addQueryEditor(defaultQueryEditor);
- return request(store.dispatch, store.getState).then(() => {
- expect(store.getActions()).toEqual(expectedActions);
- });
+ return store
+ .dispatch(actions.addQueryEditor(defaultQueryEditor))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
});
});
@@ -656,12 +648,14 @@ describe('async actions', () => {
it('updates the tab state in the backend', () => {
expect.assertions(2);
- const store = mockStore(initialState);
- const request = actions.queryEditorSetAndSaveSql(queryEditor, sql);
- return request(store.dispatch, store.getState).then(() => {
- expect(store.getActions()).toEqual(expectedActions);
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
- });
+ const store = mockStore({});
+
+ return store
+ .dispatch(actions.queryEditorSetAndSaveSql(queryEditor, sql))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
+ });
});
});
describe('with backend persistence flag off', () => {
@@ -672,9 +666,9 @@ describe('async actions', () => {
feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'),
);
- const store = mockStore(initialState);
- const request = actions.queryEditorSetAndSaveSql(queryEditor, sql);
- request(store.dispatch, store.getState);
+ const store = mockStore({});
+
+ store.dispatch(actions.queryEditorSetAndSaveSql(queryEditor, sql));
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
@@ -776,7 +770,7 @@ describe('async actions', () => {
const database = { disable_data_preview: false, id: 1 };
const tableName = 'table';
const schemaName = 'schema';
- const store = mockStore(initialState);
+ const store = mockStore({});
const expectedActionTypes = [
actions.MERGE_TABLE, // addTable
actions.MERGE_TABLE, // getTableMetadata
@@ -786,24 +780,20 @@ describe('async actions', () => {
actions.MERGE_TABLE, // addTable
actions.QUERY_SUCCESS, // querySuccess
];
- const request = actions.addTable(
- query,
- database,
- tableName,
- schemaName,
- );
- return request(store.dispatch, store.getState).then(() => {
- expect(store.getActions().map(a => a.type)).toEqual(
- expectedActionTypes,
- );
- expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
- expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1);
- expect(fetchMock.calls(getExtraTableMetadataEndpoint)).toHaveLength(
- 1,
- );
- // tab state is not updated, since the query is a data preview
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
- });
+ return store
+ .dispatch(actions.addTable(query, database, tableName, schemaName))
+ .then(() => {
+ expect(store.getActions().map(a => a.type)).toEqual(
+ expectedActionTypes,
+ );
+ expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
+ expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1);
+ expect(fetchMock.calls(getExtraTableMetadataEndpoint)).toHaveLength(
+ 1,
+ );
+ // tab state is not updated, since the query is a data preview
+ expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
+ });
});
});
diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx b/superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx
deleted file mode 100644
index 6bfefc0a3d..0000000000
--- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import React from 'react';
-import configureStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import { render, waitFor } from 'spec/helpers/testing-library';
-import { QueryEditor } from 'src/SqlLab/types';
-import { Store } from 'redux';
-import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
-import {
- queryEditorSetSelectedText,
- queryEditorSetFunctionNames,
- addTable,
-} from 'src/SqlLab/actions/sqlLab';
-import AceEditorWrapper from 'src/SqlLab/components/AceEditorWrapper';
-import { AsyncAceEditorProps } from 'src/components/AsyncAceEditor';
-
-const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-
-jest.mock('src/components/Select', () => () => (
- <div data-test="mock-deprecated-select" />
-));
-jest.mock('src/components/Select/Select', () => () => (
- <div data-test="mock-deprecated-select-select" />
-));
-jest.mock('src/components/Select/AsyncSelect', () => () => (
- <div data-test="mock-deprecated-async-select" />
-));
-
-jest.mock('src/components/AsyncAceEditor', () => ({
- FullSQLEditor: (props: AsyncAceEditorProps) => (
- <div data-test="react-ace">{JSON.stringify(props)}</div>
- ),
-}));
-
-const setup = (queryEditor: QueryEditor, store?: Store) =>
- render(
- <AceEditorWrapper
- queryEditor={queryEditor}
- actions={{
- queryEditorSetSelectedText,
- queryEditorSetFunctionNames,
- addTable,
- }}
- height="100px"
- hotkeys={[]}
- database={{}}
- onChange={jest.fn()}
- onBlur={jest.fn()}
- autocomplete
- />,
- {
- useRedux: true,
- ...(store && { store }),
- },
- );
-
-describe('AceEditorWrapper', () => {
- it('renders ace editor including sql value', async () => {
- const { getByTestId } = setup(defaultQueryEditor, mockStore(initialState));
- await waitFor(() => expect(getByTestId('react-ace')).toBeInTheDocument());
-
- expect(getByTestId('react-ace')).toHaveTextContent(
- JSON.stringify({ value: defaultQueryEditor.sql }).slice(1, -1),
- );
- });
-
- it('renders sql from unsaved change', () => {
- const expectedSql = 'SELECT updated_column\nFROM updated_table\nWHERE';
- const { getByTestId } = setup(
- defaultQueryEditor,
- mockStore({
- ...initialState,
- sqlLab: {
- ...initialState.sqlLab,
- unsavedQueryEditor: {
- id: defaultQueryEditor.id,
- sql: expectedSql,
- },
- },
- }),
- );
-
- expect(getByTestId('react-ace')).toHaveTextContent(
- JSON.stringify({ value: expectedSql }).slice(1, -1),
- );
- });
-
- it('renders current sql for unrelated unsaved changes', () => {
- const expectedSql = 'SELECT updated_column\nFROM updated_table\nWHERE';
- const { getByTestId } = setup(
- defaultQueryEditor,
- mockStore({
- ...initialState,
- sqlLab: {
- ...initialState.sqlLab,
- unsavedQueryEditor: {
- id: `${defaultQueryEditor.id}-other`,
- sql: expectedSql,
- },
- },
- }),
- );
-
- expect(getByTestId('react-ace')).not.toHaveTextContent(
- JSON.stringify({ value: expectedSql }).slice(1, -1),
- );
- expect(getByTestId('react-ace')).toHaveTextContent(
- JSON.stringify({ value: defaultQueryEditor.sql }).slice(1, -1),
- );
- });
-});
diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx
index 6b70be228b..53ec3f808a 100644
--- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx
+++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx
@@ -17,7 +17,6 @@
* under the License.
*/
import React from 'react';
-import { connect } from 'react-redux';
import { areArraysShallowEqual } from 'src/reduxUtils';
import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
import {
@@ -31,7 +30,7 @@ import {
AceCompleterKeyword,
FullSQLEditor as AceEditor,
} from 'src/components/AsyncAceEditor';
-import { QueryEditor, SchemaOption, SqlLabRootState } from 'src/SqlLab/types';
+import { QueryEditor } from 'src/SqlLab/types';
type HotKey = {
key: string;
@@ -40,13 +39,7 @@ type HotKey = {
func: () => void;
};
-type OwnProps = {
- queryEditor: QueryEditor;
- extendedTables: Array<{ name: string; columns: any[] }>;
- autocomplete: boolean;
- onChange: (sql: string) => void;
- onBlur: (sql: string) => void;
- database: any;
+interface Props {
actions: {
queryEditorSetSelectedText: (edit: any, text: null | string) => void;
queryEditorSetFunctionNames: (queryEditor: object, dbId: number) => void;
@@ -57,19 +50,19 @@ type OwnProps = {
schema: any,
) => void;
};
- hotkeys: HotKey[];
- height: string;
-};
-
-type ReduxProps = {
- queryEditor: QueryEditor;
+ autocomplete: boolean;
+ onBlur: (sql: string) => void;
sql: string;
- schemas: SchemaOption[];
+ database: any;
+ schemas: any[];
tables: any[];
functionNames: string[];
-};
-
-type Props = ReduxProps & OwnProps;
+ extendedTables: Array<{ name: string; columns: any[] }>;
+ queryEditor: QueryEditor;
+ height: string;
+ hotkeys: HotKey[];
+ onChange: (sql: string) => void;
+}
interface State {
sql: string;
@@ -293,22 +286,4 @@ class AceEditorWrapper extends React.PureComponent<Props, State> {
}
}
-function mapStateToProps(
- { sqlLab: { unsavedQueryEditor } }: SqlLabRootState,
- { queryEditor }: OwnProps,
-) {
- const currentQueryEditor = {
- ...queryEditor,
- ...(queryEditor.id === unsavedQueryEditor.id && unsavedQueryEditor),
- };
- return {
- queryEditor: currentQueryEditor,
- sql: currentQueryEditor.sql,
- schemas: currentQueryEditor.schemaOptions || [],
- tables: currentQueryEditor.tableOptions,
- functionNames: currentQueryEditor.functionNames,
- };
-}
-export default connect<ReduxProps, {}, OwnProps>(mapStateToProps)(
- AceEditorWrapper,
-);
+export default AceEditorWrapper;
diff --git a/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/EstimateQueryCostButton.test.tsx b/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/EstimateQueryCostButton.test.tsx
deleted file mode 100644
index 5a8eaeaa11..0000000000
--- a/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/EstimateQueryCostButton.test.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import React from 'react';
-import configureStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import { render } from 'spec/helpers/testing-library';
-import { Store } from 'redux';
-import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
-
-import EstimateQueryCostButton, {
- EstimateQueryCostButtonProps,
-} from 'src/SqlLab/components/EstimateQueryCostButton';
-
-const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-
-jest.mock('src/components/Select', () => () => (
- <div data-test="mock-deprecated-select" />
-));
-jest.mock('src/components/Select/Select', () => () => (
- <div data-test="mock-deprecated-select-select" />
-));
-jest.mock('src/components/Select/AsyncSelect', () => () => (
- <div data-test="mock-deprecated-async-select" />
-));
-
-const setup = (props: Partial<EstimateQueryCostButtonProps>, store?: Store) =>
- render(
- <EstimateQueryCostButton
- queryEditor={defaultQueryEditor}
- getEstimate={jest.fn()}
- {...props}
- />,
- {
- useRedux: true,
- ...(store && { store }),
- },
- );
-
-describe('EstimateQueryCostButton', () => {
- it('renders EstimateQueryCostButton', async () => {
- const { queryByText } = setup({}, mockStore(initialState));
-
- expect(queryByText('Estimate cost')).toBeTruthy();
- });
-
- it('renders label for selected query', async () => {
- const queryEditorWithSelectedText = {
- ...defaultQueryEditor,
- selectedText: 'SELECT',
- };
- const { queryByText } = setup(
- { queryEditor: queryEditorWithSelectedText },
- mockStore(initialState),
- );
-
- expect(queryByText('Estimate selected query cost')).toBeTruthy();
- });
-
- it('renders label for selected query from unsaved', async () => {
- const { queryByText } = setup(
- {},
- mockStore({
- ...initialState,
- sqlLab: {
- ...initialState.sqlLab,
- unsavedQueryEditor: {
- id: defaultQueryEditor.id,
- selectedText: 'SELECT',
- },
- },
- }),
- );
-
- expect(queryByText('Estimate selected query cost')).toBeTruthy();
- });
-});
diff --git a/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/index.tsx b/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/index.tsx
index dc2c23c406..d7f2d7dd6d 100644
--- a/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/index.tsx
+++ b/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/index.tsx
@@ -24,37 +24,23 @@ import Button from 'src/components/Button';
import Loading from 'src/components/Loading';
import ModalTrigger from 'src/components/ModalTrigger';
import { EmptyWrapperType } from 'src/components/TableView/TableView';
-import {
- SqlLabRootState,
- QueryCostEstimate,
- QueryEditor,
-} from 'src/SqlLab/types';
-import { getUpToDateQuery } from 'src/SqlLab/actions/sqlLab';
-import { useSelector } from 'react-redux';
-export interface EstimateQueryCostButtonProps {
+interface EstimateQueryCostButtonProps {
getEstimate: Function;
- queryEditor: QueryEditor;
+ queryCostEstimate: Record<string, any>;
+ selectedText?: string;
tooltip?: string;
disabled?: boolean;
}
const EstimateQueryCostButton = ({
getEstimate,
- queryEditor,
+ queryCostEstimate = {},
+ selectedText,
tooltip = '',
disabled = false,
}: EstimateQueryCostButtonProps) => {
- const queryCostEstimate = useSelector<
- SqlLabRootState,
- QueryCostEstimate | undefined
- >(state => state.sqlLab.queryCostEstimates?.[queryEditor.id]);
- const selectedText = useSelector<SqlLabRootState, string | undefined>(
- rootState =>
- (getUpToDateQuery(rootState, queryEditor) as unknown as QueryEditor)
- .selectedText,
- );
- const { cost } = queryCostEstimate || {};
+ const { cost } = queryCostEstimate;
const tableData = useMemo(() => (Array.isArray(cost) ? cost : []), [cost]);
const columns = useMemo(
() =>
@@ -71,16 +57,16 @@ const EstimateQueryCostButton = ({
};
const renderModalBody = () => {
- if (queryCostEstimate?.error) {
+ if (queryCostEstimate.error !== null) {
return (
<Alert
key="query-estimate-error"
type="error"
- message={queryCostEstimate?.error}
+ message={queryCostEstimate.error}
/>
);
}
- if (queryCostEstimate?.completed) {
+ if (queryCostEstimate.completed) {
return (
<TableView
columns={columns}
diff --git a/superset-frontend/src/SqlLab/components/QueryLimitSelect/QueryLimitSelect.test.tsx b/superset-frontend/src/SqlLab/components/QueryLimitSelect/QueryLimitSelect.test.tsx
deleted file mode 100644
index 8b3d1b7f09..0000000000
--- a/superset-frontend/src/SqlLab/components/QueryLimitSelect/QueryLimitSelect.test.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import React from 'react';
-import configureStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import { Store } from 'redux';
-
-import { render, fireEvent, waitFor } from 'spec/helpers/testing-library';
-import userEvent from '@testing-library/user-event';
-import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
-import QueryLimitSelect, {
- LIMIT_DROPDOWN,
- QueryLimitSelectProps,
- convertToNumWithSpaces,
-} from 'src/SqlLab/components/QueryLimitSelect';
-
-const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-
-jest.mock('src/components/Select', () => () => (
- <div data-test="mock-deprecated-select" />
-));
-jest.mock('src/components/Select/Select', () => () => (
- <div data-test="mock-deprecated-select-select" />
-));
-jest.mock('src/components/Select/AsyncSelect', () => () => (
- <div data-test="mock-deprecated-async-select" />
-));
-jest.mock('src/components/Icons/Icon', () => () => (
- <div data-test="mock-icons-icon" />
-));
-
-const defaultQueryLimit = 100;
-
-const setup = (props?: Partial<QueryLimitSelectProps>, store?: Store) =>
- render(
- <QueryLimitSelect
- queryEditor={defaultQueryEditor}
- maxRow={100000}
- defaultQueryLimit={defaultQueryLimit}
- {...props}
- />,
- {
- useRedux: true,
- ...(store && { store }),
- },
- );
-
-describe('QueryLimitSelect', () => {
- it('renders current query limit size', () => {
- const queryLimit = 10;
- const { getByText } = setup(
- {
- queryEditor: {
- ...defaultQueryEditor,
- queryLimit,
- },
- },
- mockStore(initialState),
- );
- expect(getByText(queryLimit)).toBeInTheDocument();
- });
-
- it('renders default query limit for initial queryEditor', () => {
- const { getByText } = setup({}, mockStore(initialState));
- expect(getByText(defaultQueryLimit)).toBeInTheDocument();
- });
-
- it('renders queryLimit from unsavedQueryEditor', () => {
- const queryLimit = 10000;
- const { getByText } = setup(
- {},
- mockStore({
- ...initialState,
- sqlLab: {
- ...initialState.sqlLab,
- unsavedQueryEditor: {
- id: defaultQueryEditor.id,
- queryLimit,
- },
- },
- }),
- );
- expect(getByText(convertToNumWithSpaces(queryLimit))).toBeInTheDocument();
- });
-
- it('renders dropdown select', async () => {
- const { baseElement, getByRole } = setup({}, mockStore(initialState));
- const dropdown = baseElement.getElementsByClassName(
- 'ant-dropdown-trigger',
- )[0];
-
- userEvent.click(dropdown);
- await waitFor(() => expect(getByRole('menu')).toBeInTheDocument());
- });
-
- it('dispatches QUERY_EDITOR_SET_QUERY_LIMIT action on dropdown menu click', async () => {
- const store = mockStore(initialState);
- const expectedIndex = 1;
- const { baseElement, getAllByRole, getByRole } = setup({}, store);
- const dropdown = baseElement.getElementsByClassName(
- 'ant-dropdown-trigger',
- )[0];
-
- userEvent.click(dropdown);
- await waitFor(() => expect(getByRole('menu')).toBeInTheDocument());
-
- const menu = getAllByRole('menuitem')[expectedIndex];
- expect(store.getActions()).toEqual([]);
- fireEvent.click(menu);
- await waitFor(() =>
- expect(store.getActions()).toEqual([
- {
- type: 'QUERY_EDITOR_SET_QUERY_LIMIT',
- queryLimit: LIMIT_DROPDOWN[expectedIndex],
- queryEditor: defaultQueryEditor,
- },
- ]),
- );
- });
-});
diff --git a/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx b/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx
deleted file mode 100644
index f438ebc59e..0000000000
--- a/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import React from 'react';
-import { useSelector, useDispatch } from 'react-redux';
-import { styled, useTheme } from '@superset-ui/core';
-import { AntdDropdown } from 'src/components';
-import { Menu } from 'src/components/Menu';
-import Icons from 'src/components/Icons';
-import { SqlLabRootState, QueryEditor } from 'src/SqlLab/types';
-import { queryEditorSetQueryLimit } from 'src/SqlLab/actions/sqlLab';
-
-export interface QueryLimitSelectProps {
- queryEditor: QueryEditor;
- maxRow: number;
- defaultQueryLimit: number;
-}
-
-export const LIMIT_DROPDOWN = [10, 100, 1000, 10000, 100000];
-
-export function convertToNumWithSpaces(num: number) {
- return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1 ');
-}
-
-const LimitSelectStyled = styled.span`
- ${({ theme }) => `
- .ant-dropdown-trigger {
- align-items: center;
- color: ${theme.colors.grayscale.dark2};
- display: flex;
- font-size: 12px;
- margin-right: ${theme.gridUnit * 2}px;
- text-decoration: none;
- border: 0;
- background: transparent;
- span {
- display: inline-block;
- margin-right: ${theme.gridUnit * 2}px;
- &:last-of-type: {
- margin-right: ${theme.gridUnit * 4}px;
- }
- }
- }
- `}
-`;
-
-function renderQueryLimit(
- maxRow: number,
- setQueryLimit: (limit: number) => void,
-) {
- // Adding SQL_MAX_ROW value to dropdown
- LIMIT_DROPDOWN.push(maxRow);
-
- return (
- <Menu>
- {[...new Set(LIMIT_DROPDOWN)].map(limit => (
- <Menu.Item key={`${limit}`} onClick={() => setQueryLimit(limit)}>
- {/* // eslint-disable-line no-use-before-define */}
- <a role="button">{convertToNumWithSpaces(limit)}</a>{' '}
- </Menu.Item>
- ))}
- </Menu>
- );
-}
-
-const QueryLimitSelect = ({
- queryEditor,
- maxRow,
- defaultQueryLimit,
-}: QueryLimitSelectProps) => {
- const queryLimit = useSelector<SqlLabRootState, number>(
- ({ sqlLab: { unsavedQueryEditor } }) => {
- const updatedQueryEditor = {
- ...queryEditor,
- ...(unsavedQueryEditor.id === queryEditor.id && unsavedQueryEditor),
- };
- return updatedQueryEditor.queryLimit || defaultQueryLimit;
- },
- );
- const dispatch = useDispatch();
- const setQueryLimit = (updatedQueryLimit: number) =>
- dispatch(queryEditorSetQueryLimit(queryEditor, updatedQueryLimit));
- const theme = useTheme();
-
- return (
- <LimitSelectStyled>
- <AntdDropdown
- overlay={renderQueryLimit(maxRow, setQueryLimit)}
- trigger={['click']}
- >
- <button type="button" onClick={e => e.preventDefault()}>
- <span>LIMIT:</span>
- <span className="limitDropdown">
- {convertToNumWithSpaces(queryLimit)}
- </span>
- <Icons.TriangleDown iconColor={theme.colors.grayscale.base} />
- </button>
- </AntdDropdown>
- </LimitSelectStyled>
- );
-};
-
-export default QueryLimitSelect;
diff --git a/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.jsx b/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.jsx
new file mode 100644
index 0000000000..823f10741a
--- /dev/null
+++ b/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.jsx
@@ -0,0 +1,53 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import { mount } from 'enzyme';
+import { supersetTheme, ThemeProvider } from '@superset-ui/core';
+import RunQueryActionButton from 'src/SqlLab/components/RunQueryActionButton';
+import Button from 'src/components/Button';
+
+describe('RunQueryActionButton', () => {
+ let wrapper;
+ const defaultProps = {
+ allowAsync: false,
+ dbId: 1,
+ queryState: 'pending',
+ runQuery: () => {}, // eslint-disable-line
+ selectedText: null,
+ stopQuery: () => {}, // eslint-disable-line
+ sql: '',
+ };
+
+ beforeEach(() => {
+ wrapper = mount(<RunQueryActionButton {...defaultProps} />, {
+ wrappingComponent: ThemeProvider,
+ wrappingComponentProps: { theme: supersetTheme },
+ });
+ });
+
+ it('is a valid react element', () => {
+ expect(
+ React.isValidElement(<RunQueryActionButton {...defaultProps} />),
+ ).toBe(true);
+ });
+
+ it('renders a single Button', () => {
+ expect(wrapper.find(Button)).toExist();
+ });
+});
diff --git a/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx b/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx
deleted file mode 100644
index 7802989689..0000000000
--- a/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import React from 'react';
-import configureStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import { Store } from 'redux';
-
-import { render, fireEvent, waitFor } from 'spec/helpers/testing-library';
-import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
-import RunQueryActionButton, {
- Props,
-} from 'src/SqlLab/components/RunQueryActionButton';
-
-const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-
-jest.mock('src/components/Select', () => () => (
- <div data-test="mock-deprecated-select" />
-));
-jest.mock('src/components/Select/Select', () => () => (
- <div data-test="mock-deprecated-select-select" />
-));
-jest.mock('src/components/Select/AsyncSelect', () => () => (
- <div data-test="mock-deprecated-async-select" />
-));
-
-const defaultProps = {
- queryEditor: defaultQueryEditor,
- allowAsync: false,
- dbId: 1,
- queryState: 'ready',
- runQuery: jest.fn(),
- selectedText: null,
- stopQuery: jest.fn(),
- overlayCreateAsMenu: null,
-};
-
-const setup = (props?: Partial<Props>, store?: Store) =>
- render(<RunQueryActionButton {...defaultProps} {...props} />, {
- useRedux: true,
- ...(store && { store }),
- });
-
-describe('RunQueryActionButton', () => {
- beforeEach(() => {
- defaultProps.runQuery.mockReset();
- defaultProps.stopQuery.mockReset();
- });
-
- it('renders a single Button', () => {
- const { getByRole } = setup({}, mockStore(initialState));
- expect(getByRole('button')).toBeInTheDocument();
- });
-
- it('renders a label for Run Query', () => {
- const { getByText } = setup({}, mockStore(initialState));
- expect(getByText('Run')).toBeInTheDocument();
- });
-
- it('renders a label for Selected Query', () => {
- const { getByText } = setup(
- {},
- mockStore({
- ...initialState,
- sqlLab: {
- ...initialState.sqlLab,
- unsavedQueryEditor: {
- id: defaultQueryEditor.id,
- selectedText: 'FROM',
- },
- },
- }),
- );
- expect(getByText('Run selection')).toBeInTheDocument();
- });
-
- it('disable button when sql from unsaved changes is empty', () => {
- const { getByRole } = setup(
- {},
- mockStore({
- ...initialState,
- sqlLab: {
- ...initialState.sqlLab,
- unsavedQueryEditor: {
- id: defaultQueryEditor.id,
- sql: '',
- },
- },
- }),
- );
- const button = getByRole('button');
- expect(button).toBeDisabled();
- });
-
- it('enable default button for unrelated unsaved changes', () => {
- const { getByRole } = setup(
- {},
- mockStore({
- ...initialState,
- sqlLab: {
- ...initialState.sqlLab,
- unsavedQueryEditor: {
- id: `${defaultQueryEditor.id}-other`,
- sql: '',
- },
- },
- }),
- );
- const button = getByRole('button');
- expect(button).toBeEnabled();
- });
-
- it('dispatch runQuery on click', async () => {
- const { getByRole } = setup({}, mockStore(initialState));
- const button = getByRole('button');
- expect(defaultProps.runQuery).toHaveBeenCalledTimes(0);
- fireEvent.click(button);
- await waitFor(() => expect(defaultProps.runQuery).toHaveBeenCalledTimes(1));
- });
-
- describe('on running state', () => {
- it('dispatch stopQuery on click', async () => {
- const { getByRole } = setup(
- { queryState: 'running' },
- mockStore(initialState),
- );
- const button = getByRole('button');
- expect(defaultProps.stopQuery).toHaveBeenCalledTimes(0);
- fireEvent.click(button);
- await waitFor(() =>
- expect(defaultProps.stopQuery).toHaveBeenCalledTimes(1),
- );
- });
- });
-});
diff --git a/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx b/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx
index 5cc453f5ee..19bf778756 100644
--- a/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx
+++ b/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx
@@ -24,20 +24,15 @@ import Button from 'src/components/Button';
import Icons from 'src/components/Icons';
import { DropdownButton } from 'src/components/DropdownButton';
import { detectOS } from 'src/utils/common';
-import { shallowEqual, useSelector } from 'react-redux';
-import {
- QueryEditor,
- SqlLabRootState,
- QueryButtonProps,
-} from 'src/SqlLab/types';
-import { getUpToDateQuery } from 'src/SqlLab/actions/sqlLab';
+import { QueryButtonProps } from 'src/SqlLab/types';
-export interface Props {
- queryEditor: QueryEditor;
+interface Props {
allowAsync: boolean;
queryState?: string;
runQuery: (c?: boolean) => void;
+ selectedText?: string;
stopQuery: () => void;
+ sql: string;
overlayCreateAsMenu: typeof Menu | null;
}
@@ -88,27 +83,16 @@ const StyledButton = styled.span`
const RunQueryActionButton = ({
allowAsync = false,
- queryEditor,
queryState,
+ selectedText,
+ sql = '',
overlayCreateAsMenu,
runQuery,
stopQuery,
}: Props) => {
const theme = useTheme();
+
const userOS = detectOS();
- const { selectedText, sql } = useSelector<
- SqlLabRootState,
- Pick<QueryEditor, 'selectedText' | 'sql'>
- >(rootState => {
- const currentQueryEditor = getUpToDateQuery(
- rootState,
- queryEditor,
- ) as unknown as QueryEditor;
- return {
- selectedText: currentQueryEditor.selectedText,
- sql: currentQueryEditor.sql,
- };
- }, shallowEqual);
const shouldShowStopBtn =
!!queryState && ['running', 'pending'].indexOf(queryState) > -1;
@@ -117,7 +101,7 @@ const RunQueryActionButton = ({
? (DropdownButton as React.FC)
: Button;
- const isDisabled = !sql || !sql.trim();
+ const isDisabled = !sql.trim();
const stopButtonTooltipText = useMemo(
() =>
diff --git a/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.jsx b/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.jsx
index 2a5fcf3eb7..94b7e2780b 100644
--- a/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.jsx
+++ b/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.jsx
@@ -17,19 +17,18 @@
* under the License.
*/
import React from 'react';
-import configureStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import SaveQuery from 'src/SqlLab/components/SaveQuery';
-import { initialState, databases } from 'src/SqlLab/fixtures';
+import { databases } from 'src/SqlLab/fixtures';
const mockedProps = {
- queryEditor: {
+ query: {
dbId: 1,
schema: 'main',
sql: 'SELECT * FROM t',
},
+ defaultLabel: 'untitled',
animation: false,
database: databases.result[0],
onUpdate: () => {},
@@ -44,15 +43,9 @@ const splitSaveBtnProps = {
},
};
-const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-
describe('SavedQuery', () => {
it('renders a non-split save button when allows_virtual_table_explore is not enabled', () => {
- render(<SaveQuery {...mockedProps} />, {
- useRedux: true,
- store: mockStore(initialState),
- });
+ render(<SaveQuery {...mockedProps} />, { useRedux: true });
const saveBtn = screen.getByRole('button', { name: /save/i });
@@ -60,10 +53,7 @@ describe('SavedQuery', () => {
});
it('renders a save query modal when user clicks save button', () => {
- render(<SaveQuery {...mockedProps} />, {
- useRedux: true,
- store: mockStore(initialState),
- });
+ render(<SaveQuery {...mockedProps} />, { useRedux: true });
const saveBtn = screen.getByRole('button', { name: /save/i });
userEvent.click(saveBtn);
@@ -76,10 +66,7 @@ describe('SavedQuery', () => {
});
it('renders the save query modal UI', () => {
- render(<SaveQuery {...mockedProps} />, {
- useRedux: true,
- store: mockStore(initialState),
- });
+ render(<SaveQuery {...mockedProps} />, { useRedux: true });
const saveBtn = screen.getByRole('button', { name: /save/i });
userEvent.click(saveBtn);
@@ -113,15 +100,12 @@ describe('SavedQuery', () => {
it('renders a "save as new" and "update" button if query already exists', () => {
const props = {
...mockedProps,
- queryEditor: {
+ query: {
...mockedProps.query,
remoteId: '42',
},
};
- render(<SaveQuery {...props} />, {
- useRedux: true,
- store: mockStore(initialState),
- });
+ render(<SaveQuery {...props} />, { useRedux: true });
const saveBtn = screen.getByRole('button', { name: /save/i });
userEvent.click(saveBtn);
@@ -134,10 +118,7 @@ describe('SavedQuery', () => {
});
it('renders a split save button when allows_virtual_table_explore is enabled', async () => {
- render(<SaveQuery {...splitSaveBtnProps} />, {
- useRedux: true,
- store: mockStore(initialState),
- });
+ render(<SaveQuery {...splitSaveBtnProps} />, { useRedux: true });
await waitFor(() => {
const saveBtn = screen.getByRole('button', { name: /save/i });
@@ -149,10 +130,7 @@ describe('SavedQuery', () => {
});
it('renders a save dataset modal when user clicks "save dataset" menu item', async () => {
- render(<SaveQuery {...splitSaveBtnProps} />, {
- useRedux: true,
- store: mockStore(initialState),
- });
+ render(<SaveQuery {...splitSaveBtnProps} />, { useRedux: true });
await waitFor(() => {
const caretBtn = screen.getByRole('button', { name: /caret-down/i });
@@ -168,10 +146,7 @@ describe('SavedQuery', () => {
});
it('renders the save dataset modal UI', async () => {
- render(<SaveQuery {...splitSaveBtnProps} />, {
- useRedux: true,
- store: mockStore(initialState),
- });
+ render(<SaveQuery {...splitSaveBtnProps} />, { useRedux: true });
await waitFor(() => {
const caretBtn = screen.getByRole('button', { name: /caret-down/i });
diff --git a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
index 38cd5625ec..554514f4db 100644
--- a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
@@ -17,7 +17,6 @@
* under the License.
*/
import React, { useState, useEffect } from 'react';
-import { useSelector, shallowEqual } from 'react-redux';
import { Row, Col } from 'src/components';
import { Input, TextArea } from 'src/components/Input';
import { t, styled } from '@superset-ui/core';
@@ -26,16 +25,12 @@ import { Menu } from 'src/components/Menu';
import { Form, FormItem } from 'src/components/Form';
import Modal from 'src/components/Modal';
import SaveDatasetActionButton from 'src/SqlLab/components/SaveDatasetActionButton';
-import {
- SaveDatasetModal,
- ISaveableDatasource,
-} from 'src/SqlLab/components/SaveDatasetModal';
+import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils';
-import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
interface SaveQueryProps {
- queryEditor: QueryEditor;
- columns: ISaveableDatasource['columns'];
+ query: QueryPayload;
+ defaultLabel: string;
onSave: (arg0: QueryPayload) => void;
onUpdate: (arg0: QueryPayload) => void;
saveQueryWarning: string | null;
@@ -81,22 +76,13 @@ const Styles = styled.span`
`;
export default function SaveQuery({
- queryEditor,
+ query,
+ defaultLabel = t('Undefined'),
onSave = () => {},
onUpdate,
saveQueryWarning = null,
database,
- columns,
}: SaveQueryProps) {
- const query = useSelector<SqlLabRootState, QueryEditor>(
- ({ sqlLab: { unsavedQueryEditor } }) => ({
- ...queryEditor,
- ...(queryEditor.id === unsavedQueryEditor.id && unsavedQueryEditor),
- columns,
- }),
- shallowEqual,
- );
- const defaultLabel = query.name || query.description || t('Undefined');
const [description, setDescription] = useState<string>(
query.description || '',
);
@@ -114,12 +100,11 @@ export default function SaveQuery({
</Menu>
);
- const queryPayload = () =>
- ({
- ...query,
- name: label,
- description,
- } as any as QueryPayload);
+ const queryPayload = () => ({
+ ...query,
+ name: label,
+ description,
+ });
useEffect(() => {
if (!isSaved) setLabel(defaultLabel);
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx
index 735264b957..d946c675cc 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx
@@ -40,12 +40,7 @@ import {
} from 'src/SqlLab/actions/sqlLab';
import { EmptyStateBig } from 'src/components/EmptyState';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
-import {
- initialState,
- queries,
- table,
- defaultQueryEditor,
-} from 'src/SqlLab/fixtures';
+import { initialState, queries, table } from 'src/SqlLab/fixtures';
const MOCKED_SQL_EDITOR_HEIGHT = 500;
@@ -53,31 +48,7 @@ fetchMock.get('glob:*/api/v1/database/*', { result: [] });
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
-const store = mockStore({
- ...initialState,
- sqlLab: {
- ...initialState.sqlLab,
- databases: {
- dbid1: {
- allow_ctas: false,
- allow_cvas: false,
- allow_dml: false,
- allow_file_upload: false,
- allow_multi_schema_metadata_fetch: false,
- allow_run_async: false,
- backend: 'postgresql',
- database_name: 'examples',
- expose_in_sqllab: true,
- force_ctas_schema: null,
- id: 1,
- },
- },
- unsavedQueryEditor: {
- id: defaultQueryEditor.id,
- dbId: 'dbid1',
- },
- },
-});
+const store = mockStore(initialState);
describe('SqlEditor', () => {
const mockedProps = {
@@ -86,9 +57,21 @@ describe('SqlEditor', () => {
queryEditorSetSelectedText,
queryEditorSetSchemaOptions,
addDangerToast: jest.fn(),
- removeDataPreview: jest.fn(),
},
- queryEditor: initialState.sqlLab.queryEditors[0],
+ database: {
+ allow_ctas: false,
+ allow_cvas: false,
+ allow_dml: false,
+ allow_file_upload: false,
+ allow_multi_schema_metadata_fetch: false,
+ allow_run_async: false,
+ backend: 'postgresql',
+ database_name: 'examples',
+ expose_in_sqllab: true,
+ force_ctas_schema: null,
+ id: 1,
+ },
+ queryEditorId: initialState.sqlLab.queryEditors[0].id,
latestQuery: queries[0],
tables: [table],
getHeight: () => '100px',
@@ -111,8 +94,8 @@ describe('SqlEditor', () => {
);
it('does not render SqlEditor if no db selected', () => {
- const queryEditor = initialState.sqlLab.queryEditors[1];
- const updatedProps = { ...mockedProps, queryEditor };
+ const database = {};
+ const updatedProps = { ...mockedProps, database };
const wrapper = buildWrapper(updatedProps);
expect(wrapper.find(EmptyStateBig)).toExist();
});
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
index d813b3b52d..bdda237158 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
@@ -43,10 +43,10 @@ import {
persistEditorHeight,
postStopQuery,
queryEditorSetAutorun,
+ queryEditorSetQueryLimit,
queryEditorSetSql,
queryEditorSetAndSaveSql,
queryEditorSetTemplateParams,
- runQueryFromSqlEditor,
runQuery,
saveQuery,
addSavedQueryToTabState,
@@ -79,8 +79,8 @@ import SqlEditorLeftBar from '../SqlEditorLeftBar';
import AceEditorWrapper from '../AceEditorWrapper';
import RunQueryActionButton from '../RunQueryActionButton';
import { newQueryTabName } from '../../utils/newQueryTabName';
-import QueryLimitSelect from '../QueryLimitSelect';
+const LIMIT_DROPDOWN = [10, 100, 1000, 10000, 100000];
const SQL_EDITOR_PADDING = 10;
const INITIAL_NORTH_PERCENT = 30;
const INITIAL_SOUTH_PERCENT = 70;
@@ -96,6 +96,26 @@ const validatorMap =
bootstrapData?.common?.conf?.SQL_VALIDATORS_BY_ENGINE || {};
const scheduledQueriesConf = bootstrapData?.common?.conf?.SCHEDULED_QUERIES;
+const LimitSelectStyled = styled.span`
+ ${({ theme }) => `
+ .ant-dropdown-trigger {
+ align-items: center;
+ color: ${theme.colors.grayscale.dark2};
+ display: flex;
+ font-size: 12px;
+ margin-right: ${theme.gridUnit * 2}px;
+ text-decoration: none;
+ span {
+ display: inline-block;
+ margin-right: ${theme.gridUnit * 2}px;
+ &:last-of-type: {
+ margin-right: ${theme.gridUnit * 4}px;
+ }
+ }
+ }
+ `}
+`;
+
const StyledToolbar = styled.div`
padding: ${({ theme }) => theme.gridUnit * 2}px;
background: ${({ theme }) => theme.colors.grayscale.light5};
@@ -134,7 +154,7 @@ const propTypes = {
tables: PropTypes.array.isRequired,
editorQueries: PropTypes.array.isRequired,
dataPreviewQueries: PropTypes.array.isRequired,
- queryEditor: PropTypes.object.isRequired,
+ queryEditorId: PropTypes.string.isRequired,
hideLeftBar: PropTypes.bool,
defaultQueryLimit: PropTypes.number.isRequired,
maxRow: PropTypes.number.isRequired,
@@ -185,6 +205,7 @@ class SqlEditor extends React.PureComponent {
);
this.queryPane = this.queryPane.bind(this);
this.getHotkeyConfig = this.getHotkeyConfig.bind(this);
+ this.renderQueryLimit = this.renderQueryLimit.bind(this);
this.getAceEditorAndSouthPaneHeights =
this.getAceEditorAndSouthPaneHeights.bind(this);
this.getSqlEditorHeight = this.getSqlEditorHeight.bind(this);
@@ -361,10 +382,21 @@ class SqlEditor extends React.PureComponent {
this.props.queryEditorSetAndSaveSql(this.props.queryEditor, sql);
}
+ setQueryLimit(queryLimit) {
+ this.props.queryEditorSetQueryLimit(this.props.queryEditor, queryLimit);
+ }
+
getQueryCostEstimate() {
if (this.props.database) {
const qe = this.props.queryEditor;
- this.props.estimateQueryCost(qe);
+ const query = {
+ dbId: qe.dbId,
+ sql: qe.selectedText ? qe.selectedText : this.props.queryEditor.sql,
+ sqlEditorId: qe.id,
+ schema: qe.schema,
+ templateParams: qe.templateParams,
+ };
+ this.props.estimateQueryCost(query);
}
}
@@ -393,9 +425,16 @@ class SqlEditor extends React.PureComponent {
}
requestValidation(sql) {
- const { database, queryEditor, validateQuery } = this.props;
- if (database) {
- validateQuery(queryEditor, sql);
+ if (this.props.database) {
+ const qe = this.props.queryEditor;
+ const query = {
+ dbId: qe.dbId,
+ sql,
+ sqlEditorId: qe.id,
+ schema: qe.schema,
+ templateParams: qe.templateParams,
+ };
+ this.props.validateQuery(query);
}
}
@@ -419,22 +458,25 @@ class SqlEditor extends React.PureComponent {
}
startQuery(ctas = false, ctas_method = CtasEnum.TABLE) {
- const {
- database,
- runQueryFromSqlEditor,
- setActiveSouthPaneTab,
- queryEditor,
- defaultQueryLimit,
- } = this.props;
- runQueryFromSqlEditor(
- database,
- queryEditor,
- defaultQueryLimit,
- ctas ? this.state.ctas : '',
+ const qe = this.props.queryEditor;
+ const query = {
+ dbId: qe.dbId,
+ sql: qe.selectedText ? qe.selectedText : qe.sql,
+ sqlEditorId: qe.id,
+ tab: qe.name,
+ schema: qe.schema,
+ tempTable: ctas ? this.state.ctas : '',
+ templateParams: qe.templateParams,
+ queryLimit: qe.queryLimit || this.props.defaultQueryLimit,
+ runAsync: this.props.database
+ ? this.props.database.allow_run_async
+ : false,
ctas,
ctas_method,
- );
- setActiveSouthPaneTab('Results');
+ updateTabState: !qe.selectedText,
+ };
+ this.props.runQuery(query);
+ this.props.setActiveSouthPaneTab('Results');
}
stopQuery() {
@@ -487,7 +529,11 @@ class SqlEditor extends React.PureComponent {
onBlur={this.setQueryEditorSql}
onChange={this.onSqlChanged}
queryEditor={this.props.queryEditor}
+ sql={this.props.queryEditor.sql}
database={this.props.database}
+ schemas={this.props.queryEditor.schemaOptions}
+ tables={this.props.queryEditor.tableOptions}
+ functionNames={this.props.queryEditor.functionNames}
extendedTables={this.props.tables}
height={`${aceEditorHeight}px`}
hotkeys={hotkeys}
@@ -531,7 +577,7 @@ class SqlEditor extends React.PureComponent {
onChange={params => {
this.props.actions.queryEditorSetTemplateParams(qe, params);
}}
- queryEditor={qe}
+ code={qe.templateParams}
/>
</Menu.Item>
)}
@@ -553,6 +599,25 @@ class SqlEditor extends React.PureComponent {
);
}
+ renderQueryLimit() {
+ // Adding SQL_MAX_ROW value to dropdown
+ const { maxRow } = this.props;
+ LIMIT_DROPDOWN.push(maxRow);
+
+ return (
+ <Menu>
+ {[...new Set(LIMIT_DROPDOWN)].map(limit => (
+ <Menu.Item key={`${limit}`} onClick={() => this.setQueryLimit(limit)}>
+ {/* // eslint-disable-line no-use-before-define */}
+ <a role="button" styling="link">
+ {this.convertToNumWithSpaces(limit)}
+ </a>{' '}
+ </Menu.Item>
+ ))}
+ </Menu>
+ );
+ }
+
async saveQuery(query) {
const { queryEditor: qe, actions } = this.props;
const savedQuery = await actions.saveQuery(query);
@@ -608,10 +673,11 @@ class SqlEditor extends React.PureComponent {
? this.props.database.allow_run_async
: false
}
- queryEditor={qe}
queryState={this.props.latestQuery?.state}
runQuery={this.runQuery}
+ selectedText={qe.selectedText}
stopQuery={this.stopQuery}
+ sql={this.props.queryEditor.sql}
overlayCreateAsMenu={showMenu ? runMenuBtn : null}
/>
</span>
@@ -621,17 +687,27 @@ class SqlEditor extends React.PureComponent {
<span>
<EstimateQueryCostButton
getEstimate={this.getQueryCostEstimate}
- queryEditor={qe}
+ queryCostEstimate={qe.queryCostEstimate}
+ selectedText={qe.selectedText}
tooltip={t('Estimate the cost before running a query')}
/>
</span>
)}
<span>
- <QueryLimitSelect
- queryEditor={this.props.queryEditor}
- maxRow={this.props.maxRow}
- defaultQueryLimit={this.props.defaultQueryLimit}
- />
+ <LimitSelectStyled>
+ <AntdDropdown overlay={this.renderQueryLimit()} trigger="click">
+ <a onClick={e => e.preventDefault()}>
+ <span>LIMIT:</span>
+ <span className="limitDropdown">
+ {this.convertToNumWithSpaces(
+ this.props.queryEditor.queryLimit ||
+ this.props.defaultQueryLimit,
+ )}
+ </span>
+ <Icons.TriangleDown iconColor={theme.colors.grayscale.base} />
+ </a>
+ </AntdDropdown>
+ </LimitSelectStyled>
</span>
{this.props.latestQuery && (
<Timer
@@ -645,8 +721,11 @@ class SqlEditor extends React.PureComponent {
<div className="rightItems">
<span>
<SaveQuery
- queryEditor={qe}
- columns={this.props.latestQuery?.results?.columns || []}
+ query={{
+ ...qe,
+ columns: this.props.latestQuery?.results?.columns || [],
+ }}
+ defaultLabel={qe.name || qe.description}
onSave={this.saveQuery}
onUpdate={this.props.actions.updateSavedQuery}
saveQueryWarning={this.props.saveQueryWarning}
@@ -753,22 +832,12 @@ class SqlEditor extends React.PureComponent {
SqlEditor.defaultProps = defaultProps;
SqlEditor.propTypes = propTypes;
-function mapStateToProps({ sqlLab }, { queryEditor }) {
- let { latestQueryId, dbId } = queryEditor;
- if (sqlLab.unsavedQueryEditor.id === queryEditor.id) {
- const { latestQueryId: unsavedQID, dbId: unsavedDBID } =
- sqlLab.unsavedQueryEditor;
- latestQueryId = unsavedQID || latestQueryId;
- dbId = unsavedDBID || dbId;
- }
- const database = sqlLab.databases[dbId];
- const latestQuery = sqlLab.queries[latestQueryId];
-
- return {
- queryEditors: sqlLab.queryEditors,
- latestQuery,
- database,
- };
+function mapStateToProps({ sqlLab }, props) {
+ const queryEditor = sqlLab.queryEditors.find(
+ editor => editor.id === props.queryEditorId,
+ );
+
+ return { sqlLab, ...props, queryEditor, queryEditors: sqlLab.queryEditors };
}
function mapDispatchToProps(dispatch) {
@@ -779,10 +848,10 @@ function mapDispatchToProps(dispatch) {
persistEditorHeight,
postStopQuery,
queryEditorSetAutorun,
+ queryEditorSetQueryLimit,
queryEditorSetSql,
queryEditorSetAndSaveSql,
queryEditorSetTemplateParams,
- runQueryFromSqlEditor,
runQuery,
saveQuery,
addSavedQueryToTabState,
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
index 97d80a4e88..fbe782534e 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
@@ -32,7 +32,7 @@ import Collapse from 'src/components/Collapse';
import Icons from 'src/components/Icons';
import { TableSelectorMultiple } from 'src/components/TableSelector';
import { IconTooltip } from 'src/components/IconTooltip';
-import { QueryEditor, SchemaOption } from 'src/SqlLab/types';
+import { QueryEditor } from 'src/SqlLab/types';
import { DatabaseObject } from 'src/components/DatabaseSelector';
import { EmptyStateSmall } from 'src/components/EmptyState';
import {
@@ -55,10 +55,7 @@ interface actionsTypes {
setDatabases: (arg0: any) => {};
addDangerToast: (msg: string) => void;
queryEditorSetSchema: (queryEditor: QueryEditor, schema?: string) => void;
- queryEditorSetSchemaOptions: (
- queryEditor: QueryEditor,
- options: SchemaOption[],
- ) => void;
+ queryEditorSetSchemaOptions: () => void;
queryEditorSetTableOptions: (
queryEditor: QueryEditor,
options: Array<any>,
@@ -73,6 +70,7 @@ interface SqlEditorLeftBarProps {
actions: actionsTypes & TableElementProps['actions'];
database: DatabaseObject;
setEmptyState: Dispatch<SetStateAction<boolean>>;
+ showDisabled: boolean;
}
const StyledScrollbarContainer = styled.div`
@@ -241,15 +239,6 @@ export default function SqlEditorLeftBar({
[actions],
);
- const handleSchemasLoad = React.useCallback(
- (options: Array<any>) => {
- if (queryEditorRef.current) {
- actions.queryEditorSetSchemaOptions(queryEditorRef.current, options);
- }
- },
- [actions],
- );
-
return (
<div className="SqlEditorLeftBar">
<TableSelectorMultiple
@@ -260,7 +249,7 @@ export default function SqlEditorLeftBar({
handleError={actions.addDangerToast}
onDbChange={onDbChange}
onSchemaChange={handleSchemaChange}
- onSchemasLoad={handleSchemasLoad}
+ onSchemasLoad={actions.queryEditorSetSchemaOptions}
onTableSelectChange={onTablesChange}
onTablesLoad={handleTablesLoad}
schema={queryEditor.schema}
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/SqlEditorTabHeader.test.tsx b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/SqlEditorTabHeader.test.tsx
deleted file mode 100644
index 5216129bdf..0000000000
--- a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/SqlEditorTabHeader.test.tsx
+++ /dev/null
@@ -1,220 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import React from 'react';
-import configureStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import {
- fireEvent,
- screen,
- render,
- waitFor,
-} from 'spec/helpers/testing-library';
-import userEvent from '@testing-library/user-event';
-import { QueryEditor } from 'src/SqlLab/types';
-import {
- initialState,
- defaultQueryEditor,
- extraQueryEditor1,
- extraQueryEditor2,
-} from 'src/SqlLab/fixtures';
-import { Store } from 'redux';
-import {
- REMOVE_QUERY_EDITOR,
- QUERY_EDITOR_SET_TITLE,
- ADD_QUERY_EDITOR,
- QUERY_EDITOR_TOGGLE_LEFT_BAR,
-} from 'src/SqlLab/actions/sqlLab';
-import SqlEditorTabHeader from 'src/SqlLab/components/SqlEditorTabHeader';
-
-jest.mock('src/components/Select', () => () => (
- <div data-test="mock-deprecated-select" />
-));
-jest.mock('src/components/Select/Select', () => () => (
- <div data-test="mock-deprecated-select-select" />
-));
-jest.mock('src/components/Select/AsyncSelect', () => () => (
- <div data-test="mock-async-select" />
-));
-
-const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-const setup = (queryEditor: QueryEditor, store?: Store) =>
- render(<SqlEditorTabHeader queryEditor={queryEditor} />, {
- useRedux: true,
- ...(store && { store }),
- });
-
-describe('SqlEditorTabHeader', () => {
- it('renders name', () => {
- const { queryByText } = setup(defaultQueryEditor, mockStore(initialState));
- expect(queryByText(defaultQueryEditor.name)).toBeTruthy();
- expect(queryByText(extraQueryEditor1.name)).toBeFalsy();
- expect(queryByText(extraQueryEditor2.name)).toBeFalsy();
- });
-
- it('renders name from unsaved changes', () => {
- const expectedTitle = 'updated title';
- const { queryByText } = setup(
- defaultQueryEditor,
- mockStore({
- ...initialState,
- sqlLab: {
- ...initialState.sqlLab,
- unsavedQueryEditor: {
- id: defaultQueryEditor.id,
- name: expectedTitle,
- },
- },
- }),
- );
- expect(queryByText(expectedTitle)).toBeTruthy();
- expect(queryByText(defaultQueryEditor.name)).toBeFalsy();
- expect(queryByText(extraQueryEditor1.name)).toBeFalsy();
- expect(queryByText(extraQueryEditor2.name)).toBeFalsy();
- });
-
- it('renders current name for unrelated unsaved changes', () => {
- const unrelatedTitle = 'updated title';
- const { queryByText } = setup(
- defaultQueryEditor,
- mockStore({
- ...initialState,
- sqlLab: {
- ...initialState.sqlLab,
- unsavedQueryEditor: {
- id: `${defaultQueryEditor.id}-other`,
- name: unrelatedTitle,
- },
- },
- }),
- );
- expect(queryByText(defaultQueryEditor.name)).toBeTruthy();
- expect(queryByText(unrelatedTitle)).toBeFalsy();
- expect(queryByText(extraQueryEditor1.name)).toBeFalsy();
- expect(queryByText(extraQueryEditor2.name)).toBeFalsy();
- });
-
- describe('with dropdown menus', () => {
- let store = mockStore();
- beforeEach(async () => {
- store = mockStore(initialState);
- const { getByTestId } = setup(defaultQueryEditor, store);
- const dropdown = getByTestId('dropdown-trigger');
-
- userEvent.click(dropdown);
- });
-
- it('should dispatch removeQueryEditor action', async () => {
- await waitFor(() =>
- expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
- );
-
- fireEvent.click(screen.getByTestId('close-tab-menu-option'));
-
- const actions = store.getActions();
- await waitFor(() =>
- expect(actions[0]).toEqual({
- type: REMOVE_QUERY_EDITOR,
- queryEditor: defaultQueryEditor,
- }),
- );
- });
-
- it('should dispatch queryEditorSetTitle action', async () => {
- await waitFor(() =>
- expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
- );
- const expectedTitle = 'typed text';
- const mockPrompt = jest
- .spyOn(window, 'prompt')
- .mockImplementation(() => expectedTitle);
- fireEvent.click(screen.getByTestId('rename-tab-menu-option'));
-
- const actions = store.getActions();
- await waitFor(() =>
- expect(actions[0]).toEqual({
- type: QUERY_EDITOR_SET_TITLE,
- name: expectedTitle,
- queryEditor: expect.objectContaining({
- id: defaultQueryEditor.id,
- }),
- }),
- );
- mockPrompt.mockClear();
- });
-
- it('should dispatch toggleLeftBar action', async () => {
- await waitFor(() =>
- expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
- );
- fireEvent.click(screen.getByTestId('toggle-menu-option'));
-
- const actions = store.getActions();
- await waitFor(() =>
- expect(actions[0]).toEqual({
- type: QUERY_EDITOR_TOGGLE_LEFT_BAR,
- hideLeftBar: !defaultQueryEditor.hideLeftBar,
- queryEditor: expect.objectContaining({
- id: defaultQueryEditor.id,
- }),
- }),
- );
- });
-
- it('should dispatch removeAllOtherQueryEditors action', async () => {
- await waitFor(() =>
- expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
- );
- fireEvent.click(screen.getByTestId('close-all-other-menu-option'));
-
- const actions = store.getActions();
- await waitFor(() =>
- expect(actions).toEqual([
- {
- type: REMOVE_QUERY_EDITOR,
- queryEditor: initialState.sqlLab.queryEditors[1],
- },
- {
- type: REMOVE_QUERY_EDITOR,
- queryEditor: initialState.sqlLab.queryEditors[2],
- },
- ]),
- );
- });
-
- it('should dispatch cloneQueryToNewTab action', async () => {
- await waitFor(() =>
- expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
- );
- fireEvent.click(screen.getByTestId('clone-tab-menu-option'));
-
- const actions = store.getActions();
- await waitFor(() =>
- expect(actions[0]).toEqual({
- type: ADD_QUERY_EDITOR,
- queryEditor: expect.objectContaining({
- name: `Copy of ${defaultQueryEditor.name}`,
- sql: defaultQueryEditor.sql,
- autorun: false,
- }),
- }),
- );
- });
- });
-});
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx
deleted file mode 100644
index dce2f2700e..0000000000
--- a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import React, { useMemo } from 'react';
-import { bindActionCreators } from 'redux';
-import { useSelector, useDispatch, shallowEqual } from 'react-redux';
-import { Dropdown } from 'src/components/Dropdown';
-import { Menu } from 'src/components/Menu';
-import { styled, t, QueryState } from '@superset-ui/core';
-import {
- removeQueryEditor,
- removeAllOtherQueryEditors,
- queryEditorSetTitle,
- cloneQueryToNewTab,
- toggleLeftBar,
-} from 'src/SqlLab/actions/sqlLab';
-import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
-import TabStatusIcon from '../TabStatusIcon';
-
-const TabTitleWrapper = styled.div`
- display: flex;
- align-items: center;
-`;
-const TabTitle = styled.span`
- margin-right: ${({ theme }) => theme.gridUnit * 2}px;
- text-transform: none;
-`;
-
-interface Props {
- queryEditor: QueryEditor;
-}
-
-const SqlEditorTabHeader: React.FC<Props> = ({ queryEditor }) => {
- const qe = useSelector<SqlLabRootState, QueryEditor>(
- ({ sqlLab: { unsavedQueryEditor } }) => ({
- ...queryEditor,
- ...(queryEditor.id === unsavedQueryEditor.id && unsavedQueryEditor),
- }),
- shallowEqual,
- );
- const queryStatus = useSelector<SqlLabRootState, QueryState>(
- ({ sqlLab }) => sqlLab.queries[qe.latestQueryId || '']?.state || '',
- );
- const dispatch = useDispatch();
- const actions = useMemo(
- () =>
- bindActionCreators(
- {
- removeQueryEditor,
- removeAllOtherQueryEditors,
- queryEditorSetTitle,
- cloneQueryToNewTab,
- toggleLeftBar,
- },
- dispatch,
- ),
- [dispatch],
- );
-
- function renameTab() {
- const newTitle = prompt(t('Enter a new title for the tab'));
- if (newTitle) {
- actions.queryEditorSetTitle(qe, newTitle);
- }
- }
-
- return (
- <TabTitleWrapper>
- <Dropdown
- trigger={['click']}
- overlay={
- <Menu style={{ width: 176 }}>
- <Menu.Item
- className="close-btn"
- key="1"
- onClick={() => actions.removeQueryEditor(qe)}
- data-test="close-tab-menu-option"
- >
- <div className="icon-container">
- <i className="fa fa-close" />
- </div>
- {t('Close tab')}
- </Menu.Item>
- <Menu.Item
- key="2"
- onClick={renameTab}
- data-test="rename-tab-menu-option"
- >
- <div className="icon-container">
- <i className="fa fa-i-cursor" />
- </div>
- {t('Rename tab')}
- </Menu.Item>
- <Menu.Item
- key="3"
- onClick={() => actions.toggleLeftBar(qe)}
- data-test="toggle-menu-option"
- >
- <div className="icon-container">
- <i className="fa fa-cogs" />
- </div>
- {qe.hideLeftBar ? t('Expand tool bar') : t('Hide tool bar')}
- </Menu.Item>
- <Menu.Item
- key="4"
- onClick={() => actions.removeAllOtherQueryEditors(qe)}
- data-test="close-all-other-menu-option"
- >
- <div className="icon-container">
- <i className="fa fa-times-circle-o" />
- </div>
- {t('Close all other tabs')}
- </Menu.Item>
- <Menu.Item
- key="5"
- onClick={() => actions.cloneQueryToNewTab(qe, false)}
- data-test="clone-tab-menu-option"
- >
- <div className="icon-container">
- <i className="fa fa-files-o" />
- </div>
- {t('Duplicate tab')}
- </Menu.Item>
- </Menu>
- }
- />
- <TabTitle>{qe.name}</TabTitle> <TabStatusIcon tabState={queryStatus} />{' '}
- </TabTitleWrapper>
- );
-};
-
-export default SqlEditorTabHeader;
diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx
index bc290673be..5de36bc99a 100644
--- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx
+++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx
@@ -30,7 +30,6 @@ import { EditableTabs } from 'src/components/Tabs';
import TabbedSqlEditors from 'src/SqlLab/components/TabbedSqlEditors';
import SqlEditor from 'src/SqlLab/components/SqlEditor';
import { table, initialState } from 'src/SqlLab/fixtures';
-import { newQueryTabName } from 'src/SqlLab/utils/newQueryTabName';
fetchMock.get('glob:*/api/v1/database/*', {});
fetchMock.get('glob:*/savedqueryviewapi/api/get/*', {});
@@ -151,6 +150,18 @@ describe('TabbedSqlEditors', () => {
);
});
});
+ it('should rename Tab', () => {
+ global.prompt = () => 'new title';
+ wrapper = getWrapper();
+ sinon.stub(wrapper.instance().props.actions, 'queryEditorSetTitle');
+
+ wrapper.instance().renameTab(queryEditors[0]);
+ expect(
+ wrapper.instance().props.actions.queryEditorSetTitle.getCall(0).args[1],
+ ).toBe('new title');
+
+ delete global.prompt;
+ });
it('should removeQueryEditor', () => {
wrapper = getWrapper();
sinon.stub(wrapper.instance().props.actions, 'removeQueryEditor');
@@ -172,11 +183,11 @@ describe('TabbedSqlEditors', () => {
it('should properly increment query tab name', () => {
wrapper = getWrapper();
sinon.stub(wrapper.instance().props.actions, 'addQueryEditor');
- const newTitle = newQueryTabName(wrapper.instance().props.queryEditors);
+
wrapper.instance().newQueryEditor();
expect(
wrapper.instance().props.actions.addQueryEditor.getCall(0).args[0].name,
- ).toContain(newTitle);
+ ).toContain('Untitled Query 2');
});
it('should duplicate query editor', () => {
wrapper = getWrapper();
diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx
index c5b94f8b35..c97080c6e9 100644
--- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx
+++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx
@@ -18,7 +18,9 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
+import { Dropdown } from 'src/components/Dropdown';
import { EditableTabs } from 'src/components/Tabs';
+import { Menu } from 'src/components/Menu';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import URI from 'urijs';
@@ -31,7 +33,7 @@ import * as Actions from 'src/SqlLab/actions/sqlLab';
import { EmptyStateBig } from 'src/components/EmptyState';
import { newQueryTabName } from '../../utils/newQueryTabName';
import SqlEditor from '../SqlEditor';
-import SqlEditorTabHeader from '../SqlEditorTabHeader';
+import TabStatusIcon from '../TabStatusIcon';
const propTypes = {
actions: PropTypes.object.isRequired,
@@ -42,6 +44,7 @@ const propTypes = {
databases: PropTypes.object.isRequired,
queries: PropTypes.object.isRequired,
queryEditors: PropTypes.array,
+ requestedQuery: PropTypes.object,
tabHistory: PropTypes.array.isRequired,
tables: PropTypes.array.isRequired,
offline: PropTypes.bool,
@@ -51,10 +54,16 @@ const propTypes = {
const defaultProps = {
queryEditors: [],
offline: false,
+ requestedQuery: null,
saveQueryWarning: null,
scheduleQueryWarning: null,
};
+const TabTitleWrapper = styled.div`
+ display: flex;
+ align-items: center;
+`;
+
const StyledTab = styled.span`
line-height: 24px;
`;
@@ -77,6 +86,10 @@ class TabbedSqlEditors extends React.PureComponent {
dataPreviewQueries: [],
};
this.removeQueryEditor = this.removeQueryEditor.bind(this);
+ this.renameTab = this.renameTab.bind(this);
+ this.toggleLeftBar = this.toggleLeftBar.bind(this);
+ this.removeAllOtherQueryEditors =
+ this.removeAllOtherQueryEditors.bind(this);
this.duplicateQueryEditor = this.duplicateQueryEditor.bind(this);
this.handleSelect = this.handleSelect.bind(this);
this.handleEdit = this.handleEdit.bind(this);
@@ -223,6 +236,14 @@ class TabbedSqlEditors extends React.PureComponent {
window.history.replaceState({}, document.title, this.state.sqlLabUrl);
}
+ renameTab(qe) {
+ /* eslint no-alert: 0 */
+ const newTitle = prompt(t('Enter a new title for the tab'));
+ if (newTitle) {
+ this.props.actions.queryEditorSetTitle(qe, newTitle);
+ }
+ }
+
activeQueryEditor() {
if (this.props.tabHistory.length === 0) {
return this.props.queryEditors[0];
@@ -283,34 +304,106 @@ class TabbedSqlEditors extends React.PureComponent {
this.props.actions.removeQueryEditor(qe);
}
+ removeAllOtherQueryEditors(cqe) {
+ this.props.queryEditors.forEach(
+ qe => qe !== cqe && this.removeQueryEditor(qe),
+ );
+ }
+
duplicateQueryEditor(qe) {
this.props.actions.cloneQueryToNewTab(qe, false);
}
+ toggleLeftBar(qe) {
+ this.props.actions.toggleLeftBar(qe);
+ }
+
render() {
const noQueryEditors = this.props.queryEditors?.length === 0;
- const editors = this.props.queryEditors?.map(qe => (
- <EditableTabs.TabPane
- key={qe.id}
- tab={<SqlEditorTabHeader queryEditor={qe} />}
- // for tests - key prop isn't handled by enzyme well bcs it's a react keyword
- data-key={qe.id}
- >
- <SqlEditor
- tables={this.props.tables.filter(xt => xt.queryEditorId === qe.id)}
- queryEditor={qe}
- editorQueries={this.state.queriesArray}
- dataPreviewQueries={this.state.dataPreviewQueries}
- actions={this.props.actions}
- hideLeftBar={qe.hideLeftBar}
- defaultQueryLimit={this.props.defaultQueryLimit}
- maxRow={this.props.maxRow}
- displayLimit={this.props.displayLimit}
- saveQueryWarning={this.props.saveQueryWarning}
- scheduleQueryWarning={this.props.scheduleQueryWarning}
- />
- </EditableTabs.TabPane>
- ));
+ const editors = this.props.queryEditors.map(qe => {
+ let latestQuery;
+ if (qe.latestQueryId) {
+ latestQuery = this.props.queries[qe.latestQueryId];
+ }
+ let database;
+ if (qe.dbId) {
+ database = this.props.databases[qe.dbId];
+ }
+ const state = latestQuery ? latestQuery.state : '';
+
+ const menu = (
+ <Menu style={{ width: 176 }}>
+ <Menu.Item
+ className="close-btn"
+ key="1"
+ onClick={() => this.removeQueryEditor(qe)}
+ data-test="close-tab-menu-option"
+ >
+ <div className="icon-container">
+ <i className="fa fa-close" />
+ </div>
+ {t('Close tab')}
+ </Menu.Item>
+ <Menu.Item key="2" onClick={() => this.renameTab(qe)}>
+ <div className="icon-container">
+ <i className="fa fa-i-cursor" />
+ </div>
+ {t('Rename tab')}
+ </Menu.Item>
+ <Menu.Item key="3" onClick={() => this.toggleLeftBar(qe)}>
+ <div className="icon-container">
+ <i className="fa fa-cogs" />
+ </div>
+ {qe.hideLeftBar ? t('Expand tool bar') : t('Hide tool bar')}
+ </Menu.Item>
+ <Menu.Item
+ key="4"
+ onClick={() => this.removeAllOtherQueryEditors(qe)}
+ >
+ <div className="icon-container">
+ <i className="fa fa-times-circle-o" />
+ </div>
+ {t('Close all other tabs')}
+ </Menu.Item>
+ <Menu.Item key="5" onClick={() => this.duplicateQueryEditor(qe)}>
+ <div className="icon-container">
+ <i className="fa fa-files-o" />
+ </div>
+ {t('Duplicate tab')}
+ </Menu.Item>
+ </Menu>
+ );
+ const tabHeader = (
+ <TabTitleWrapper>
+ <Dropdown overlay={menu} trigger={['click']} />
+ <TabTitle>{qe.name}</TabTitle> <TabStatusIcon tabState={state} />{' '}
+ </TabTitleWrapper>
+ );
+ return (
+ <EditableTabs.TabPane
+ key={qe.id}
+ tab={tabHeader}
+ // for tests - key prop isn't handled by enzyme well bcs it's a react keyword
+ data-key={qe.id}
+ >
+ <SqlEditor
+ tables={this.props.tables.filter(xt => xt.queryEditorId === qe.id)}
+ queryEditorId={qe.id}
+ editorQueries={this.state.queriesArray}
+ dataPreviewQueries={this.state.dataPreviewQueries}
+ latestQuery={latestQuery}
+ database={database}
+ actions={this.props.actions}
+ hideLeftBar={qe.hideLeftBar}
+ defaultQueryLimit={this.props.defaultQueryLimit}
+ maxRow={this.props.maxRow}
+ displayLimit={this.props.displayLimit}
+ saveQueryWarning={this.props.saveQueryWarning}
+ scheduleQueryWarning={this.props.scheduleQueryWarning}
+ />
+ </EditableTabs.TabPane>
+ );
+ });
const emptyTab = (
<StyledTab>
@@ -379,7 +472,7 @@ class TabbedSqlEditors extends React.PureComponent {
TabbedSqlEditors.propTypes = propTypes;
TabbedSqlEditors.defaultProps = defaultProps;
-function mapStateToProps({ sqlLab, common }) {
+function mapStateToProps({ sqlLab, common, requestedQuery }) {
return {
databases: sqlLab.databases,
queryEditors: sqlLab.queryEditors,
@@ -393,6 +486,7 @@ function mapStateToProps({ sqlLab, common }) {
maxRow: common.conf.SQL_MAX_ROW,
saveQueryWarning: common.conf.SQLLAB_SAVE_WARNING_MESSAGE,
scheduleQueryWarning: common.conf.SQLLAB_SCHEDULE_WARNING_MESSAGE,
+ requestedQuery,
};
}
function mapDispatchToProps(dispatch) {
diff --git a/superset-frontend/src/SqlLab/components/TemplateParamsEditor/TemplateParamsEditor.test.tsx b/superset-frontend/src/SqlLab/components/TemplateParamsEditor/TemplateParamsEditor.test.tsx
index a82b1e5859..bc04030d28 100644
--- a/superset-frontend/src/SqlLab/components/TemplateParamsEditor/TemplateParamsEditor.test.tsx
+++ b/superset-frontend/src/SqlLab/components/TemplateParamsEditor/TemplateParamsEditor.test.tsx
@@ -17,100 +17,38 @@
* under the License.
*/
-import React from 'react';
-import configureStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import { Store } from 'redux';
+import React, { ReactNode } from 'react';
import {
render,
fireEvent,
getByText,
waitFor,
} from 'spec/helpers/testing-library';
-import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
+import { ThemeProvider, supersetTheme } from '@superset-ui/core';
-import TemplateParamsEditor, {
- Props,
-} from 'src/SqlLab/components/TemplateParamsEditor';
+import TemplateParamsEditor from 'src/SqlLab/components/TemplateParamsEditor';
-jest.mock('src/components/Select', () => () => (
- <div data-test="mock-deprecated-select" />
-));
-jest.mock('src/components/Select/Select', () => () => (
- <div data-test="mock-deprecated-select-select" />
-));
-jest.mock('src/components/Select/AsyncSelect', () => () => (
- <div data-test="mock-async-select" />
-));
-jest.mock('src/components/AsyncAceEditor', () => ({
- ConfigEditor: ({ value }: { value: string }) => (
- <div data-test="mock-async-ace-editor">{value}</div>
- ),
-}));
-
-const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-const setup = (otherProps: Partial<Props> = {}, store?: Store) =>
- render(
- <TemplateParamsEditor
- language="json"
- onChange={() => {}}
- queryEditor={defaultQueryEditor}
- {...otherProps}
- />,
- {
- useRedux: true,
- store: mockStore(initialState),
- ...(store && { store }),
- },
- );
+const ThemeWrapper = ({ children }: { children: ReactNode }) => (
+ <ThemeProvider theme={supersetTheme}>{children}</ThemeProvider>
+);
describe('TemplateParamsEditor', () => {
it('should render with a title', () => {
- const { container } = setup();
+ const { container } = render(
+ <TemplateParamsEditor code="FOO" language="json" onChange={() => {}} />,
+ { wrapper: ThemeWrapper },
+ );
expect(container.querySelector('div[role="button"]')).toBeInTheDocument();
});
it('should open a modal with the ace editor', async () => {
- const { container, getByTestId } = setup();
- fireEvent.click(getByText(container, 'Parameters'));
- await waitFor(() => {
- expect(getByTestId('mock-async-ace-editor')).toBeInTheDocument();
- });
- });
-
- it('renders templateParams', async () => {
- const { container, getByTestId } = setup();
- fireEvent.click(getByText(container, 'Parameters'));
- await waitFor(() => {
- expect(getByTestId('mock-async-ace-editor')).toBeInTheDocument();
- });
- expect(getByTestId('mock-async-ace-editor')).toHaveTextContent(
- defaultQueryEditor.templateParams,
- );
- });
-
- it('renders code from unsaved changes', async () => {
- const expectedCode = 'custom code value';
- const { container, getByTestId } = setup(
- {},
- mockStore({
- ...initialState,
- sqlLab: {
- ...initialState.sqlLab,
- unsavedQueryEditor: {
- id: defaultQueryEditor.id,
- templateParams: expectedCode,
- },
- },
- }),
+ const { container, baseElement } = render(
+ <TemplateParamsEditor code="FOO" language="json" onChange={() => {}} />,
+ { wrapper: ThemeWrapper },
);
fireEvent.click(getByText(container, 'Parameters'));
await waitFor(() => {
- expect(getByTestId('mock-async-ace-editor')).toBeInTheDocument();
+ expect(baseElement.querySelector('#ace-editor')).toBeInTheDocument();
});
- expect(getByTestId('mock-async-ace-editor')).toHaveTextContent(
- expectedCode,
- );
});
});
diff --git a/superset-frontend/src/SqlLab/components/TemplateParamsEditor/index.tsx b/superset-frontend/src/SqlLab/components/TemplateParamsEditor/index.tsx
index 4eea10da05..62d0a7209d 100644
--- a/superset-frontend/src/SqlLab/components/TemplateParamsEditor/index.tsx
+++ b/superset-frontend/src/SqlLab/components/TemplateParamsEditor/index.tsx
@@ -26,9 +26,6 @@ import ModalTrigger from 'src/components/ModalTrigger';
import { ConfigEditor } from 'src/components/AsyncAceEditor';
import { FAST_DEBOUNCE } from 'src/constants';
import { Tooltip } from 'src/components/Tooltip';
-import { useSelector } from 'react-redux';
-import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
-import { getUpToDateQuery } from 'src/SqlLab/actions/sqlLab';
const StyledConfigEditor = styled(ConfigEditor)`
&.ace_editor {
@@ -36,24 +33,17 @@ const StyledConfigEditor = styled(ConfigEditor)`
}
`;
-export type Props = {
- queryEditor: QueryEditor;
- language: 'yaml' | 'json';
- onChange: () => void;
-};
-
function TemplateParamsEditor({
- queryEditor,
+ code = '{}',
language,
onChange = () => {},
-}: Props) {
+}: {
+ code: string;
+ language: 'yaml' | 'json';
+ onChange: () => void;
+}) {
const [parsedJSON, setParsedJSON] = useState({});
const [isValid, setIsValid] = useState(true);
- const code = useSelector<SqlLabRootState, string>(
- rootState =>
- (getUpToDateQuery(rootState, queryEditor) as unknown as QueryEditor)
- .templateParams || '{}',
- );
useEffect(() => {
try {
diff --git a/superset-frontend/src/SqlLab/fixtures.ts b/superset-frontend/src/SqlLab/fixtures.ts
index 72c1c0d50f..ea0fbd1bb8 100644
--- a/superset-frontend/src/SqlLab/fixtures.ts
+++ b/superset-frontend/src/SqlLab/fixtures.ts
@@ -178,38 +178,18 @@ export const table = {
export const defaultQueryEditor = {
id: 'dfsadfs',
autorun: false,
- dbId: undefined,
+ dbId: null,
latestQueryId: null,
- selectedText: undefined,
+ selectedText: null,
sql: 'SELECT *\nFROM\nWHERE',
name: 'Untitled Query 1',
- schema: 'main',
- remoteId: null,
- tableOptions: [],
- functionNames: [],
- hideLeftBar: false,
schemaOptions: [
{
value: 'main',
label: 'main',
- title: 'main',
+ name: 'main',
},
],
- templateParams: '{}',
-};
-
-export const extraQueryEditor1 = {
- ...defaultQueryEditor,
- id: 'diekd23',
- sql: 'SELECT *\nFROM\nWHERE\nLIMIT',
- name: 'Untitled Query 2',
-};
-
-export const extraQueryEditor2 = {
- ...defaultQueryEditor,
- id: 'owkdi998',
- sql: 'SELECT *\nFROM\nWHERE\nGROUP BY',
- name: 'Untitled Query 3',
};
export const queries = [
@@ -660,14 +640,13 @@ export const initialState = {
alerts: [],
queries: {},
databases: {},
- queryEditors: [defaultQueryEditor, extraQueryEditor1, extraQueryEditor2],
+ queryEditors: [defaultQueryEditor],
tabHistory: [defaultQueryEditor.id],
tables: [],
workspaceQueries: [],
queriesLastUpdate: 0,
activeSouthPaneTab: 'Results',
user: { user },
- unsavedQueryEditor: {},
},
messageToasts: [],
common: {
diff --git a/superset-frontend/src/SqlLab/reducers/getInitialState.js b/superset-frontend/src/SqlLab/reducers/getInitialState.js
index 2d00d3e0d6..21c367844d 100644
--- a/superset-frontend/src/SqlLab/reducers/getInitialState.js
+++ b/superset-frontend/src/SqlLab/reducers/getInitialState.js
@@ -37,7 +37,7 @@ export default function getInitialState({
* To allow for a transparent migration, the initial state is a combination
* of the backend state (if any) with the browser state (if any).
*/
- let queryEditors = {};
+ const queryEditors = [];
const defaultQueryEditor = {
id: null,
loaded: true,
@@ -55,9 +55,13 @@ export default function getInitialState({
errors: [],
completed: false,
},
+ queryCostEstimate: {
+ cost: null,
+ completed: false,
+ error: null,
+ },
hideLeftBar: false,
};
- let unsavedQueryEditor = {};
/**
* Load state from the backend. This will be empty if the feature flag
@@ -98,10 +102,7 @@ export default function getInitialState({
name: label,
};
}
- queryEditors = {
- ...queryEditors,
- [queryEditor.id]: queryEditor,
- };
+ queryEditors.push(queryEditor);
});
const tabHistory = activeTab ? [activeTab.id.toString()] : [];
@@ -159,22 +160,15 @@ export default function getInitialState({
// migration was successful
localStorage.removeItem('redux');
} else {
- unsavedQueryEditor = sqlLab.unsavedQueryEditor || {};
// add query editors and tables to state with a special flag so they can
// be migrated if the `SQLLAB_BACKEND_PERSISTENCE` feature flag is on
- sqlLab.queryEditors.forEach(qe => {
- queryEditors = {
- ...queryEditors,
- [qe.id]: {
- ...queryEditors[qe.id],
- ...qe,
- name: qe.title || qe.name,
- ...(unsavedQueryEditor.id === qe.id && unsavedQueryEditor),
- inLocalStorage: true,
- loaded: true,
- },
- };
- });
+ sqlLab.queryEditors.forEach(qe =>
+ queryEditors.push({
+ ...qe,
+ inLocalStorage: true,
+ loaded: true,
+ }),
+ );
sqlLab.tables.forEach(table =>
tables.push({ ...table, inLocalStorage: true }),
);
@@ -192,13 +186,11 @@ export default function getInitialState({
databases,
offline: false,
queries,
- queryEditors: Object.values(queryEditors),
+ queryEditors,
tabHistory,
tables,
queriesLastUpdate: Date.now(),
user,
- unsavedQueryEditor,
- queryCostEstimates: {},
},
requestedQuery,
messageToasts: getToastsFromPyFlashMessages(
diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.js b/superset-frontend/src/SqlLab/reducers/sqlLab.js
index a12db71a37..f87c8ce9c1 100644
--- a/superset-frontend/src/SqlLab/reducers/sqlLab.js
+++ b/superset-frontend/src/SqlLab/reducers/sqlLab.js
@@ -31,28 +31,12 @@ import {
extendArr,
} from '../../reduxUtils';
-function alterUnsavedQueryEditorState(state, updatedState, id) {
- return {
- ...(state.unsavedQueryEditor.id === id && state.unsavedQueryEditor),
- ...(id ? { id, ...updatedState } : state.unsavedQueryEditor),
- };
-}
-
export default function sqlLabReducer(state = {}, action) {
const actionHandlers = {
[actions.ADD_QUERY_EDITOR]() {
- const mergeUnsavedState = alterInArr(
- state,
- 'queryEditors',
- state.unsavedQueryEditor,
- {
- ...state.unsavedQueryEditor,
- },
- );
- const newState = {
- ...mergeUnsavedState,
- tabHistory: [...state.tabHistory, action.queryEditor.id],
- };
+ const tabHistory = state.tabHistory.slice();
+ tabHistory.push(action.queryEditor.id);
+ const newState = { ...state, tabHistory };
return addToArr(newState, 'queryEditors', action.queryEditor);
},
[actions.QUERY_EDITOR_SAVED]() {
@@ -82,14 +66,9 @@ export default function sqlLabReducer(state = {}, action) {
);
},
[actions.CLONE_QUERY_TO_NEW_TAB]() {
- const queryEditor = state.queryEditors.find(
+ const progenitor = state.queryEditors.find(
qe => qe.id === state.tabHistory[state.tabHistory.length - 1],
);
- const progenitor = {
- ...queryEditor,
- ...(state.unsavedQueryEditor.id === queryEditor.id &&
- state.unsavedQueryEditor),
- };
const qe = {
remoteId: progenitor.remoteId,
name: t('Copy of %s', progenitor.name),
@@ -100,14 +79,7 @@ export default function sqlLabReducer(state = {}, action) {
queryLimit: action.query.queryLimit,
maxRow: action.query.maxRow,
};
- const stateWithoutUnsavedState = {
- ...state,
- unsavedQueryEditor: {},
- };
- return sqlLabReducer(
- stateWithoutUnsavedState,
- actions.addQueryEditor(qe),
- );
+ return sqlLabReducer(state, actions.addQueryEditor(qe));
},
[actions.REMOVE_QUERY_EDITOR]() {
let newState = removeFromArr(state, 'queryEditors', action.queryEditor);
@@ -211,20 +183,16 @@ export default function sqlLabReducer(state = {}, action) {
};
},
[actions.START_QUERY_VALIDATION]() {
- return {
- ...state,
- unsavedQueryEditor: alterUnsavedQueryEditorState(
- state,
- {
- validationResult: {
- id: action.query.id,
- errors: [],
- completed: false,
- },
- },
- action.query.sqlEditorId,
- ),
- };
+ let newState = { ...state };
+ const sqlEditor = { id: action.query.sqlEditorId };
+ newState = alterInArr(newState, 'queryEditors', sqlEditor, {
+ validationResult: {
+ id: action.query.id,
+ errors: [],
+ completed: false,
+ },
+ });
+ return newState;
},
[actions.QUERY_VALIDATION_RETURNED]() {
// If the server is very slow about answering us, we might get validation
@@ -234,29 +202,21 @@ export default function sqlLabReducer(state = {}, action) {
// We don't care about any but the most recent because validations are
// only valid for the SQL text they correspond to -- once the SQL has
// changed, the old validation doesn't tell us anything useful anymore.
- const qe = {
- ...getFromArr(state.queryEditors, action.query.sqlEditorId),
- ...(state.unsavedQueryEditor.id === action.query.sqlEditorId &&
- state.unsavedQueryEditor),
- };
+ const qe = getFromArr(state.queryEditors, action.query.sqlEditorId);
if (qe.validationResult.id !== action.query.id) {
return state;
}
// Otherwise, persist the results on the queryEditor state
- return {
- ...state,
- unsavedQueryEditor: alterUnsavedQueryEditorState(
- state,
- {
- validationResult: {
- id: action.query.id,
- errors: action.results,
- completed: true,
- },
- },
- action.query.sqlEditorId,
- ),
- };
+ let newState = { ...state };
+ const sqlEditor = { id: action.query.sqlEditorId };
+ newState = alterInArr(newState, 'queryEditors', sqlEditor, {
+ validationResult: {
+ id: action.query.id,
+ errors: action.results,
+ completed: true,
+ },
+ });
+ return newState;
},
[actions.QUERY_VALIDATION_FAILED]() {
// If the server is very slow about answering us, we might get validation
@@ -290,52 +250,45 @@ export default function sqlLabReducer(state = {}, action) {
return newState;
},
[actions.COST_ESTIMATE_STARTED]() {
- return {
- ...state,
- queryCostEstimates: {
- ...state.queryCostEstimates,
- [action.query.sqlEditorId]: {
- completed: false,
- cost: null,
- error: null,
- },
+ let newState = { ...state };
+ const sqlEditor = { id: action.query.sqlEditorId };
+ newState = alterInArr(newState, 'queryEditors', sqlEditor, {
+ queryCostEstimate: {
+ completed: false,
+ cost: null,
+ error: null,
},
- };
+ });
+ return newState;
},
[actions.COST_ESTIMATE_RETURNED]() {
- return {
- ...state,
- queryCostEstimates: {
- ...state.queryCostEstimates,
- [action.query.sqlEditorId]: {
- completed: true,
- cost: action.json,
- error: null,
- },
+ let newState = { ...state };
+ const sqlEditor = { id: action.query.sqlEditorId };
+ newState = alterInArr(newState, 'queryEditors', sqlEditor, {
+ queryCostEstimate: {
+ completed: true,
+ cost: action.json,
+ error: null,
},
- };
+ });
+ return newState;
},
[actions.COST_ESTIMATE_FAILED]() {
- return {
- ...state,
- queryCostEstimates: {
- ...state.queryCostEstimates,
- [action.query.sqlEditorId]: {
- completed: false,
- cost: null,
- error: action.error,
- },
+ let newState = { ...state };
+ const sqlEditor = { id: action.query.sqlEditorId };
+ newState = alterInArr(newState, 'queryEditors', sqlEditor, {
+ queryCostEstimate: {
+ completed: false,
+ cost: null,
+ error: action.error,
},
- };
+ });
+ return newState;
},
[actions.START_QUERY]() {
let newState = { ...state };
if (action.query.sqlEditorId) {
- const qe = {
- ...getFromArr(state.queryEditors, action.query.sqlEditorId),
- ...(action.query.sqlEditorId === state.unsavedQueryEditor.id &&
- state.unsavedQueryEditor),
- };
+ const qe = getFromArr(state.queryEditors, action.query.sqlEditorId);
if (qe.latestQueryId && state.queries[qe.latestQueryId]) {
const newResults = {
...state.queries[qe.latestQueryId].results,
@@ -350,17 +303,10 @@ export default function sqlLabReducer(state = {}, action) {
newState.activeSouthPaneTab = action.query.id;
}
newState = addToObject(newState, 'queries', action.query);
-
- return {
- ...newState,
- unsavedQueryEditor: alterUnsavedQueryEditorState(
- state,
- {
- latestQueryId: action.query.id,
- },
- action.query.sqlEditorId,
- ),
- };
+ const sqlEditor = { id: action.query.sqlEditorId };
+ return alterInArr(newState, 'queryEditors', sqlEditor, {
+ latestQueryId: action.query.id,
+ });
},
[actions.STOP_QUERY]() {
return alterInObject(state, 'queries', action.query, {
@@ -425,41 +371,14 @@ export default function sqlLabReducer(state = {}, action) {
qeIds.indexOf(action.queryEditor?.id) > -1 &&
state.tabHistory[state.tabHistory.length - 1] !== action.queryEditor.id
) {
- const mergeUnsavedState = alterInArr(
- state,
- 'queryEditors',
- state.unsavedQueryEditor,
- {
- ...state.unsavedQueryEditor,
- },
- );
- return {
- ...(action.queryEditor.id === state.unsavedQueryEditor.id
- ? alterInObject(
- mergeUnsavedState,
- 'queryEditors',
- action.queryEditor,
- {
- ...action.queryEditor,
- ...state.unsavedQueryEditor,
- },
- )
- : mergeUnsavedState),
- tabHistory: [...state.tabHistory, action.queryEditor.id],
- };
+ const tabHistory = state.tabHistory.slice();
+ tabHistory.push(action.queryEditor.id);
+ return { ...state, tabHistory };
}
return state;
},
[actions.LOAD_QUERY_EDITOR]() {
- const mergeUnsavedState = alterInArr(
- state,
- 'queryEditors',
- state.unsavedQueryEditor,
- {
- ...state.unsavedQueryEditor,
- },
- );
- return alterInArr(mergeUnsavedState, 'queryEditors', action.queryEditor, {
+ return alterInArr(state, 'queryEditors', action.queryEditor, {
...action.queryEditor,
});
},
@@ -522,161 +441,70 @@ export default function sqlLabReducer(state = {}, action) {
return { ...state, queries };
},
[actions.QUERY_EDITOR_SETDB]() {
- return {
- ...state,
- unsavedQueryEditor: alterUnsavedQueryEditorState(
- state,
- {
- dbId: action.dbId,
- },
- action.queryEditor.id,
- ),
- };
+ return alterInArr(state, 'queryEditors', action.queryEditor, {
+ dbId: action.dbId,
+ });
},
[actions.QUERY_EDITOR_SET_FUNCTION_NAMES]() {
- return {
- ...state,
- unsavedQueryEditor: alterUnsavedQueryEditorState(
- state,
- {
- functionNames: action.functionNames,
- },
- action.queryEditor.id,
- ),
- };
+ return alterInArr(state, 'queryEditors', action.queryEditor, {
+ functionNames: action.functionNames,
+ });
},
[actions.QUERY_EDITOR_SET_SCHEMA]() {
- return {
- ...state,
- unsavedQueryEditor: alterUnsavedQueryEditorState(
- state,
- {
- schema: action.schema,
- },
- action.queryEditor.id,
- ),
- };
+ return alterInArr(state, 'queryEditors', action.queryEditor, {
+ schema: action.schema,
+ });
},
[actions.QUERY_EDITOR_SET_SCHEMA_OPTIONS]() {
- return {
- ...state,
- unsavedQueryEditor: alterUnsavedQueryEditorState(
- state,
- {
- schemaOptions: action.options,
- },
- action.queryEditor.id,
- ),
- };
+ return alterInArr(state, 'queryEditors', action.queryEditor, {
+ schemaOptions: action.options,
+ });
},
[actions.QUERY_EDITOR_SET_TABLE_OPTIONS]() {
- return {
- ...state,
- unsavedQueryEditor: alterUnsavedQueryEditorState(
- state,
- {
- tableOptions: action.options,
- },
- action.queryEditor.id,
- ),
- };
+ return alterInArr(state, 'queryEditors', action.queryEditor, {
+ tableOptions: action.options,
+ });
},
[actions.QUERY_EDITOR_SET_TITLE]() {
- return {
- ...state,
- unsavedQueryEditor: alterUnsavedQueryEditorState(
- state,
- {
- name: action.name,
- },
- action.queryEditor.id,
- ),
- };
+ return alterInArr(state, 'queryEditors', action.queryEditor, {
+ name: action.name,
+ });
},
[actions.QUERY_EDITOR_SET_SQL]() {
- return {
- ...state,
- unsavedQueryEditor: alterUnsavedQueryEditorState(
- state,
- {
- sql: action.sql,
- },
- action.queryEditor.id,
- ),
- };
+ return alterInArr(state, 'queryEditors', action.queryEditor, {
+ sql: action.sql,
+ });
},
[actions.QUERY_EDITOR_SET_QUERY_LIMIT]() {
- return {
- ...state,
- unsavedQueryEditor: alterUnsavedQueryEditorState(
- state,
- {
- queryLimit: action.queryLimit,
- },
- action.queryEditor.id,
- ),
- };
+ return alterInArr(state, 'queryEditors', action.queryEditor, {
+ queryLimit: action.queryLimit,
+ });
},
[actions.QUERY_EDITOR_SET_TEMPLATE_PARAMS]() {
- return {
- ...state,
- unsavedQueryEditor: alterUnsavedQueryEditorState(
- state,
- {
- templateParams: action.templateParams,
- },
- action.queryEditor.id,
- ),
- };
+ return alterInArr(state, 'queryEditors', action.queryEditor, {
+ templateParams: action.templateParams,
+ });
},
[actions.QUERY_EDITOR_SET_SELECTED_TEXT]() {
- return {
- ...state,
- unsavedQueryEditor: alterUnsavedQueryEditorState(
- state,
- {
- selectedText: action.sql,
- },
- action.queryEditor.id,
- ),
- };
+ return alterInArr(state, 'queryEditors', action.queryEditor, {
+ selectedText: action.sql,
+ });
},
[actions.QUERY_EDITOR_SET_AUTORUN]() {
- return {
- ...state,
- unsavedQueryEditor: alterUnsavedQueryEditorState(
- state,
- {
- autorun: action.autorun,
- },
- action.queryEditor.id,
- ),
- };
+ return alterInArr(state, 'queryEditors', action.queryEditor, {
+ autorun: action.autorun,
+ });
},
[actions.QUERY_EDITOR_PERSIST_HEIGHT]() {
- return {
- ...state,
- unsavedQueryEditor: alterUnsavedQueryEditorState(
- state,
- {
- northPercent: action.northPercent,
- southPercent: action.southPercent,
- },
- action.queryEditor.id,
- ),
- };
+ return alterInArr(state, 'queryEditors', action.queryEditor, {
+ northPercent: action.northPercent,
+ southPercent: action.southPercent,
+ });
},
[actions.QUERY_EDITOR_TOGGLE_LEFT_BAR]() {
- return {
- ...state,
- unsavedQueryEditor: alterUnsavedQueryEditorState(
- state,
- {
- hideLeftBar: action.hideLeftBar,
- },
- action.queryEditor.id,
- ),
- };
+ return alterInArr(state, 'queryEditors', action.queryEditor, {
+ hideLeftBar: action.hideLeftBar,
+ });
},
[actions.SET_DATABASES]() {
const databases = {};
diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.test.js b/superset-frontend/src/SqlLab/reducers/sqlLab.test.js
index 82483712f8..4c92e13247 100644
--- a/superset-frontend/src/SqlLab/reducers/sqlLab.test.js
+++ b/superset-frontend/src/SqlLab/reducers/sqlLab.test.js
@@ -39,55 +39,23 @@ describe('sqlLabReducer', () => {
qe = newState.queryEditors.find(e => e.id === 'abcd');
});
it('should add a query editor', () => {
- expect(newState.queryEditors).toHaveLength(
- initialState.queryEditors.length + 1,
- );
- });
- it('should merge the current unsaved changes when adding a query editor', () => {
- const expectedTitle = 'new updated title';
- const updateAction = {
- type: actions.QUERY_EDITOR_SET_TITLE,
- queryEditor: initialState.queryEditors[0],
- name: expectedTitle,
- };
- newState = sqlLabReducer(newState, updateAction);
- const addAction = {
- type: actions.ADD_QUERY_EDITOR,
- queryEditor: { ...initialState.queryEditors[0], id: 'efgh' },
- };
- newState = sqlLabReducer(newState, addAction);
-
- expect(newState.queryEditors[0].name).toEqual(expectedTitle);
- expect(
- newState.queryEditors[newState.queryEditors.length - 1].id,
- ).toEqual('efgh');
+ expect(newState.queryEditors).toHaveLength(2);
});
it('should remove a query editor', () => {
- expect(newState.queryEditors).toHaveLength(
- initialState.queryEditors.length + 1,
- );
+ expect(newState.queryEditors).toHaveLength(2);
const action = {
type: actions.REMOVE_QUERY_EDITOR,
queryEditor: qe,
};
newState = sqlLabReducer(newState, action);
- expect(newState.queryEditors).toHaveLength(
- initialState.queryEditors.length,
- );
+ expect(newState.queryEditors).toHaveLength(1);
});
it('should set q query editor active', () => {
- const expectedTitle = 'new updated title';
const addQueryEditorAction = {
type: actions.ADD_QUERY_EDITOR,
queryEditor: { ...initialState.queryEditors[0], id: 'abcd' },
};
newState = sqlLabReducer(newState, addQueryEditorAction);
- const updateAction = {
- type: actions.QUERY_EDITOR_SET_TITLE,
- queryEditor: initialState.queryEditors[1],
- name: expectedTitle,
- };
- newState = sqlLabReducer(newState, updateAction);
const setActiveQueryEditorAction = {
type: actions.SET_ACTIVE_QUERY_EDITOR,
queryEditor: defaultQueryEditor,
@@ -96,7 +64,6 @@ describe('sqlLabReducer', () => {
expect(newState.tabHistory[newState.tabHistory.length - 1]).toBe(
defaultQueryEditor.id,
);
- expect(newState.queryEditors[1].name).toEqual(expectedTitle);
});
it('should not fail while setting DB', () => {
const dbId = 9;
@@ -106,8 +73,7 @@ describe('sqlLabReducer', () => {
dbId,
};
newState = sqlLabReducer(newState, action);
- expect(newState.unsavedQueryEditor.dbId).toBe(dbId);
- expect(newState.unsavedQueryEditor.id).toBe(qe.id);
+ expect(newState.queryEditors[1].dbId).toBe(dbId);
});
it('should not fail while setting schema', () => {
const schema = 'foo';
@@ -117,8 +83,7 @@ describe('sqlLabReducer', () => {
schema,
};
newState = sqlLabReducer(newState, action);
- expect(newState.unsavedQueryEditor.schema).toBe(schema);
- expect(newState.unsavedQueryEditor.id).toBe(qe.id);
+ expect(newState.queryEditors[1].schema).toBe(schema);
});
it('should not fail while setting autorun', () => {
const action = {
@@ -126,22 +91,19 @@ describe('sqlLabReducer', () => {
queryEditor: qe,
};
newState = sqlLabReducer(newState, { ...action, autorun: false });
- expect(newState.unsavedQueryEditor.autorun).toBe(false);
- expect(newState.unsavedQueryEditor.id).toBe(qe.id);
+ expect(newState.queryEditors[1].autorun).toBe(false);
newState = sqlLabReducer(newState, { ...action, autorun: true });
- expect(newState.unsavedQueryEditor.autorun).toBe(true);
- expect(newState.unsavedQueryEditor.id).toBe(qe.id);
+ expect(newState.queryEditors[1].autorun).toBe(true);
});
it('should not fail while setting title', () => {
const title = 'Untitled Query 1';
const action = {
type: actions.QUERY_EDITOR_SET_TITLE,
queryEditor: qe,
- name: title,
+ title,
};
newState = sqlLabReducer(newState, action);
- expect(newState.unsavedQueryEditor.name).toBe(title);
- expect(newState.unsavedQueryEditor.id).toBe(qe.id);
+ expect(newState.queryEditors[0].name).toBe(title);
});
it('should not fail while setting Sql', () => {
const sql = 'SELECT nothing from dev_null';
@@ -151,8 +113,7 @@ describe('sqlLabReducer', () => {
sql,
};
newState = sqlLabReducer(newState, action);
- expect(newState.unsavedQueryEditor.sql).toBe(sql);
- expect(newState.unsavedQueryEditor.id).toBe(qe.id);
+ expect(newState.queryEditors[1].sql).toBe(sql);
});
it('should not fail while setting queryLimit', () => {
const queryLimit = 101;
@@ -162,8 +123,7 @@ describe('sqlLabReducer', () => {
queryLimit,
};
newState = sqlLabReducer(newState, action);
- expect(newState.unsavedQueryEditor.queryLimit).toBe(queryLimit);
- expect(newState.unsavedQueryEditor.id).toBe(qe.id);
+ expect(newState.queryEditors[1].queryLimit).toEqual(queryLimit);
});
it('should set selectedText', () => {
const selectedText = 'TEST';
@@ -172,10 +132,9 @@ describe('sqlLabReducer', () => {
queryEditor: newState.queryEditors[0],
sql: selectedText,
};
- expect(newState.queryEditors[0].selectedText).toBeFalsy();
+ expect(newState.queryEditors[0].selectedText).toBeNull();
newState = sqlLabReducer(newState, action);
- expect(newState.unsavedQueryEditor.selectedText).toBe(selectedText);
- expect(newState.unsavedQueryEditor.id).toBe(newState.queryEditors[0].id);
+ expect(newState.queryEditors[0].selectedText).toBe(selectedText);
});
});
describe('Tables', () => {
diff --git a/superset-frontend/src/SqlLab/types.ts b/superset-frontend/src/SqlLab/types.ts
index 18c6773ae6..8bb3e73d28 100644
--- a/superset-frontend/src/SqlLab/types.ts
+++ b/superset-frontend/src/SqlLab/types.ts
@@ -32,26 +32,16 @@ export type QueryDictionary = {
};
export interface QueryEditor {
- id: string;
dbId?: number;
name: string;
schema: string;
autorun: boolean;
sql: string;
remoteId: number | null;
- tableOptions: any[];
- schemaOptions?: SchemaOption[];
- functionNames: string[];
validationResult?: {
completed: boolean;
errors: SupersetError[];
};
- hideLeftBar?: boolean;
- latestQueryId?: string | null;
- templateParams?: string;
- selectedText?: string;
- queryLimit?: number;
- description?: string;
}
export type toastState = {
@@ -69,15 +59,13 @@ export type SqlLabRootState = {
databases: Record<string, any>;
dbConnect: boolean;
offline: boolean;
- queries: Record<string, Query>;
+ queries: Query[];
queryEditors: QueryEditor[];
tabHistory: string[]; // default is activeTab ? [activeTab.id.toString()] : []
tables: Record<string, any>[];
queriesLastUpdate: number;
user: UserWithPermissionsAndRoles;
errorMessage: string | null;
- unsavedQueryEditor: Partial<QueryEditor>;
- queryCostEstimates?: Record<string, QueryCostEstimate>;
};
localStorageUsageInKilobytes: number;
messageToasts: toastState[];
@@ -125,15 +113,3 @@ export interface DatasetOptionAutocomplete {
datasetId: number;
owners: [DatasetOwner];
}
-
-export interface SchemaOption {
- value: string;
- label: string;
- title: string;
-}
-
-export interface QueryCostEstimate {
- completed: string;
- cost: Record<string, any>[];
- error: string;
-}
diff --git a/superset-frontend/src/SqlLab/utils/newQueryTabName.test.ts b/superset-frontend/src/SqlLab/utils/newQueryTabName.test.ts
index 33eec73781..6f2af5ebb7 100644
--- a/superset-frontend/src/SqlLab/utils/newQueryTabName.test.ts
+++ b/superset-frontend/src/SqlLab/utils/newQueryTabName.test.ts
@@ -17,11 +17,9 @@
* under the License.
*/
-import { defaultQueryEditor } from 'src/SqlLab/fixtures';
import { newQueryTabName } from './newQueryTabName';
const emptyEditor = {
- ...defaultQueryEditor,
title: '',
schema: '',
autorun: false,
diff --git a/superset-frontend/src/components/Dropdown/index.tsx b/superset-frontend/src/components/Dropdown/index.tsx
index bd01aabb4d..e5d5f9f852 100644
--- a/superset-frontend/src/components/Dropdown/index.tsx
+++ b/superset-frontend/src/components/Dropdown/index.tsx
@@ -66,7 +66,7 @@ const MenuDotsWrapper = styled.div`
padding-left: ${({ theme }) => theme.gridUnit}px;
`;
-export interface DropdownProps extends DropDownProps {
+export interface DropdownProps {
overlay: React.ReactElement;
}
diff --git a/superset-frontend/src/components/TableSelector/index.tsx b/superset-frontend/src/components/TableSelector/index.tsx
index a41c4aa645..4c07e256cc 100644
--- a/superset-frontend/src/components/TableSelector/index.tsx
+++ b/superset-frontend/src/components/TableSelector/index.tsx
@@ -36,7 +36,6 @@ import RefreshLabel from 'src/components/RefreshLabel';
import CertifiedBadge from 'src/components/CertifiedBadge';
import WarningIconWithTooltip from 'src/components/WarningIconWithTooltip';
import { useToasts } from 'src/components/MessageToasts/withToasts';
-import { SchemaOption } from 'src/SqlLab/types';
const TableSelectorWrapper = styled.div`
${({ theme }) => `
@@ -90,7 +89,7 @@ interface TableSelectorProps {
isDatabaseSelectEnabled?: boolean;
onDbChange?: (db: DatabaseObject) => void;
onSchemaChange?: (schema?: string) => void;
- onSchemasLoad?: (schemaOptions: SchemaOption[]) => void;
+ onSchemasLoad?: () => void;
onTablesLoad?: (options: Array<any>) => void;
readOnly?: boolean;
schema?: string;