diff --git a/superset-frontend/src/SqlLab/App.jsx b/superset-frontend/src/SqlLab/App.jsx
index be0a982a4951d..dbbab0d184bad 100644
--- a/superset-frontend/src/SqlLab/App.jsx
+++ b/superset-frontend/src/SqlLab/App.jsx
@@ -26,10 +26,12 @@ import { initFeatureFlags, isFeatureEnabled } from 'src/featureFlags';
import { setupStore } from 'src/views/store';
import setupExtensions from 'src/setup/setupExtensions';
import getBootstrapData from 'src/utils/getBootstrapData';
+import { api } from 'src/hooks/apiResources/queryApi';
import getInitialState from './reducers/getInitialState';
import { reducers } from './reducers/index';
import App from './components/App';
import {
+ emptyTablePersistData,
emptyQueryResults,
clearQueryEditors,
} from './utils/reduxStateToLocalStorageHelper';
@@ -62,6 +64,7 @@ const sqlLabPersistStateConfig = {
if (path === 'sqlLab') {
subset[path] = {
...state[path],
+ tables: emptyTablePersistData(state[path].tables),
queries: emptyQueryResults(state[path].queries),
queryEditors: clearQueryEditors(state[path].queryEditors),
unsavedQueryEditor: clearQueryEditors([
@@ -119,6 +122,28 @@ export const store = setupStore({
}),
});
+// Rehydrate server side persisted table metadata
+initialState.sqlLab.tables.forEach(
+ ({ name: table, schema, dbId, persistData }) => {
+ if (dbId && schema && table && persistData?.columns) {
+ store.dispatch(
+ api.util.upsertQueryData(
+ 'tableMetadata',
+ { dbId, schema, table },
+ persistData,
+ ),
+ );
+ store.dispatch(
+ api.util.upsertQueryData(
+ 'tableExtendedMetadata',
+ { dbId, schema, table },
+ {},
+ ),
+ );
+ }
+ },
+);
+
// Highlight the navbar menu
const menus = document.querySelectorAll('.nav.navbar-nav li.dropdown');
const sqlLabMenu = Array.prototype.slice
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js
index 8b908d2666311..addad284e7024 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.js
@@ -1100,65 +1100,7 @@ export function mergeTable(table, query, prepend) {
return { type: MERGE_TABLE, table, query, prepend };
}
-function getTableMetadata(table, query, dispatch) {
- return SupersetClient.get({
- endpoint: encodeURI(
- `/api/v1/database/${query.dbId}/table/${encodeURIComponent(
- table.name,
- )}/${encodeURIComponent(table.schema)}/`,
- ),
- })
- .then(({ json }) => {
- const newTable = {
- ...table,
- ...json,
- expanded: true,
- isMetadataLoading: false,
- };
- dispatch(mergeTable(newTable)); // Merge table to tables in state
- return newTable;
- })
- .catch(() =>
- Promise.all([
- dispatch(
- mergeTable({
- ...table,
- isMetadataLoading: false,
- }),
- ),
- dispatch(
- addDangerToast(t('An error occurred while fetching table metadata')),
- ),
- ]),
- );
-}
-
-function getTableExtendedMetadata(table, query, dispatch) {
- return SupersetClient.get({
- endpoint: encodeURI(
- `/api/v1/database/${query.dbId}/table_extra/` +
- `${encodeURIComponent(table.name)}/${encodeURIComponent(
- table.schema,
- )}/`,
- ),
- })
- .then(({ json }) => {
- dispatch(
- mergeTable({ ...table, ...json, isExtraMetadataLoading: false }),
- );
- return json;
- })
- .catch(() =>
- Promise.all([
- dispatch(mergeTable({ ...table, isExtraMetadataLoading: false })),
- dispatch(
- addDangerToast(t('An error occurred while fetching table metadata')),
- ),
- ]),
- );
-}
-
-export function addTable(queryEditor, database, tableName, schemaName) {
+export function addTable(queryEditor, tableName, schemaName) {
return function (dispatch, getState) {
const query = getUpToDateQuery(getState(), queryEditor, queryEditor.id);
const table = {
@@ -1171,67 +1113,90 @@ export function addTable(queryEditor, database, tableName, schemaName) {
mergeTable(
{
...table,
- isMetadataLoading: true,
- isExtraMetadataLoading: true,
+ id: shortid.generate(),
expanded: true,
},
null,
true,
),
);
+ };
+}
- return Promise.all([
- getTableMetadata(table, query, dispatch),
- getTableExtendedMetadata(table, query, dispatch),
- ]).then(([newTable, json]) => {
- const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
- ? SupersetClient.post({
- endpoint: encodeURI('/tableschemaview/'),
- postPayload: { table: { ...newTable, ...json } },
- })
- : Promise.resolve({ json: { id: shortid.generate() } });
-
- if (!database.disable_data_preview && database.id === query.dbId) {
- const dataPreviewQuery = {
- id: shortid.generate(),
- dbId: query.dbId,
- sql: newTable.selectStar,
- tableName: table.name,
- sqlEditorId: null,
- tab: '',
- runAsync: database.allow_run_async,
- ctas: false,
- isDataPreview: true,
- };
- Promise.all([
- dispatch(
- mergeTable(
- {
- ...newTable,
- dataPreviewQueryId: dataPreviewQuery.id,
- },
- dataPreviewQuery,
- ),
+export function runTablePreviewQuery(newTable) {
+ return function (dispatch, getState) {
+ const {
+ sqlLab: { databases },
+ } = getState();
+ const database = databases[newTable.dbId];
+ const { dbId } = newTable;
+
+ if (database && !database.disable_data_preview) {
+ const dataPreviewQuery = {
+ id: shortid.generate(),
+ dbId,
+ sql: newTable.selectStar,
+ tableName: newTable.name,
+ sqlEditorId: null,
+ tab: '',
+ runAsync: database.allow_run_async,
+ ctas: false,
+ isDataPreview: true,
+ };
+ return Promise.all([
+ dispatch(
+ mergeTable(
+ {
+ id: newTable.id,
+ dbId: newTable.dbId,
+ schema: newTable.schema,
+ name: newTable.name,
+ queryEditorId: newTable.queryEditorId,
+ dataPreviewQueryId: dataPreviewQuery.id,
+ },
+ dataPreviewQuery,
),
- dispatch(runQuery(dataPreviewQuery)),
- ]);
- }
+ ),
+ dispatch(runQuery(dataPreviewQuery)),
+ ]);
+ }
+ return Promise.resolve();
+ };
+}
- return sync
- .then(({ json: resultJson }) =>
- dispatch(mergeTable({ ...table, id: resultJson.id })),
- )
- .catch(() =>
- dispatch(
- addDangerToast(
- t(
- 'An error occurred while fetching table metadata. ' +
- 'Please contact your administrator.',
- ),
+export function syncTable(table, tableMetadata) {
+ return function (dispatch) {
+ const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
+ ? SupersetClient.post({
+ endpoint: encodeURI('/tableschemaview/'),
+ postPayload: { table: { ...tableMetadata, ...table } },
+ })
+ : Promise.resolve({ json: { id: table.id } });
+
+ return sync
+ .then(({ json: resultJson }) => {
+ const newTable = { ...table, id: resultJson.id };
+ dispatch(
+ mergeTable({
+ ...newTable,
+ expanded: true,
+ initialized: true,
+ }),
+ );
+ if (!table.dataPreviewQueryId) {
+ dispatch(runTablePreviewQuery({ ...tableMetadata, ...newTable }));
+ }
+ })
+ .catch(() =>
+ dispatch(
+ addDangerToast(
+ t(
+ 'An error occurred while fetching table metadata. ' +
+ 'Please contact your administrator.',
),
),
- );
- });
+ ),
+ );
};
}
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
index 01886d6f77d08..2f26ec16d520a 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
@@ -821,43 +821,7 @@ describe('async actions', () => {
});
describe('addTable', () => {
- it('updates the table schema state in the backend', () => {
- expect.assertions(6);
-
- const database = { disable_data_preview: true };
- const tableName = 'table';
- const schemaName = 'schema';
- const store = mockStore(initialState);
- const expectedActionTypes = [
- actions.MERGE_TABLE, // addTable
- actions.MERGE_TABLE, // getTableMetadata
- actions.MERGE_TABLE, // getTableExtendedMetadata
- actions.MERGE_TABLE, // addTable
- ];
- 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(store.getActions()[0].prepend).toBeTruthy();
- expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
- expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1);
- expect(fetchMock.calls(getExtraTableMetadataEndpoint)).toHaveLength(
- 1,
- );
-
- // tab state is not updated, since no query was run
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
- });
- });
-
- it('fetches table schema state from unsaved change', () => {
- const database = { disable_data_preview: true };
+ it('dispatches table state from unsaved change', () => {
const tableName = 'table';
const schemaName = 'schema';
const expectedDbId = 473892;
@@ -871,31 +835,47 @@ describe('async actions', () => {
},
},
});
- const request = actions.addTable(
- query,
- database,
- tableName,
- schemaName,
+ const request = actions.addTable(query, tableName, schemaName);
+ request(store.dispatch, store.getState);
+ expect(store.getActions()[0]).toEqual(
+ expect.objectContaining({
+ table: expect.objectContaining({
+ name: tableName,
+ schema: schemaName,
+ dbId: expectedDbId,
+ }),
+ }),
);
+ });
+ });
+
+ describe('syncTable', () => {
+ it('updates the table schema state in the backend', () => {
+ expect.assertions(4);
+
+ const tableName = 'table';
+ const schemaName = 'schema';
+ const store = mockStore(initialState);
+ const expectedActionTypes = [
+ actions.MERGE_TABLE, // syncTable
+ ];
+ const request = actions.syncTable(query, tableName, schemaName);
return request(store.dispatch, store.getState).then(() => {
- expect(
- fetchMock.calls(
- `glob:**/api/v1/database/${expectedDbId}/table/*/*/`,
- ),
- ).toHaveLength(1);
- expect(
- fetchMock.calls(
- `glob:**/api/v1/database/${expectedDbId}/table_extra/*/*/`,
- ),
- ).toHaveLength(1);
+ expect(store.getActions().map(a => a.type)).toEqual(
+ expectedActionTypes,
+ );
+ expect(store.getActions()[0].prepend).toBeFalsy();
+ expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
// tab state is not updated, since no query was run
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
});
});
+ });
+ describe('runTablePreviewQuery', () => {
it('updates and runs data preview query when configured', () => {
- expect.assertions(5);
+ expect.assertions(3);
const results = {
data: mockBigNumber,
@@ -906,34 +886,32 @@ describe('async actions', () => {
overwriteRoutes: true,
});
- const database = { disable_data_preview: false, id: 1 };
const tableName = 'table';
const schemaName = 'schema';
- const store = mockStore(initialState);
+ const store = mockStore({
+ ...initialState,
+ sqlLab: {
+ ...initialState.sqlLab,
+ databases: {
+ 1: { disable_data_preview: false },
+ },
+ },
+ });
const expectedActionTypes = [
- actions.MERGE_TABLE, // addTable
- actions.MERGE_TABLE, // getTableMetadata
- actions.MERGE_TABLE, // getTableExtendedMetadata
actions.MERGE_TABLE, // addTable (data preview)
actions.START_QUERY, // runQuery (data preview)
- actions.MERGE_TABLE, // addTable
actions.QUERY_SUCCESS, // querySuccess
];
- const request = actions.addTable(
- query,
- database,
- tableName,
- schemaName,
- );
+ const request = actions.runTablePreviewQuery({
+ dbId: 1,
+ name: tableName,
+ schema: 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,
- );
+ expect(fetchMock.calls(runQueryEndpoint)).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
index 7638003e9025c..c69b92346db8a 100644
--- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx
+++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx
@@ -51,7 +51,6 @@ const setup = (queryEditor: QueryEditor, store?: Store) =>
queryEditorId={queryEditor.id}
height="100px"
hotkeys={[]}
- database={{}}
onChange={jest.fn()}
onBlur={jest.fn()}
autocomplete
diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx
index 5f7d92e943a80..1db1cc5d9b492 100644
--- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx
+++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx
@@ -55,7 +55,6 @@ type AceEditorWrapperProps = {
onBlur: (sql: string) => void;
onChange: (sql: string) => void;
queryEditorId: string;
- database: any;
extendedTables?: Array<{ name: string; columns: any[] }>;
height: string;
hotkeys: HotKey[];
@@ -86,7 +85,6 @@ const AceEditorWrapper = ({
onBlur = () => {},
onChange = () => {},
queryEditorId,
- database,
extendedTables = [],
height,
hotkeys,
@@ -258,9 +256,7 @@ const AceEditorWrapper = ({
const completer = {
insertMatch: (editor: Editor, data: any) => {
if (data.meta === 'table') {
- dispatch(
- addTable(queryEditor, database, data.value, queryEditor.schema),
- );
+ dispatch(addTable(queryEditor, data.value, queryEditor.schema));
}
let { caption } = data;
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
index 6399baa1cdc63..b07c31fd75fde 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
@@ -666,7 +666,6 @@ const SqlEditor = ({
onBlur={setQueryEditorAndSaveSql}
onChange={onSqlChanged}
queryEditorId={queryEditor.id}
- database={database}
extendedTables={tables}
height={`${aceEditorHeight}px`}
hotkeys={hotkeys}
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
index 453d80f112f4d..e89bdc57b7fb3 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
@@ -164,9 +164,9 @@ const SqlEditorLeftBar = ({
return true;
});
- tablesToAdd.forEach(tableName =>
- dispatch(addTable(queryEditor, database, tableName, schemaName)),
- );
+ tablesToAdd.forEach(tableName => {
+ dispatch(addTable(queryEditor, tableName, schemaName));
+ });
dispatch(removeTables(currentTables));
};
diff --git a/superset-frontend/src/SqlLab/components/TableElement/TableElement.test.jsx b/superset-frontend/src/SqlLab/components/TableElement/TableElement.test.jsx
deleted file mode 100644
index 60efa3a59677f..0000000000000
--- a/superset-frontend/src/SqlLab/components/TableElement/TableElement.test.jsx
+++ /dev/null
@@ -1,161 +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 { mount } from 'enzyme';
-import { Provider } from 'react-redux';
-import configureStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import { supersetTheme, ThemeProvider } from '@superset-ui/core';
-import Collapse from 'src/components/Collapse';
-import { IconTooltip } from 'src/components/IconTooltip';
-import TableElement from 'src/SqlLab/components/TableElement';
-import ColumnElement from 'src/SqlLab/components/ColumnElement';
-import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
-import { initialState, table } from 'src/SqlLab/fixtures';
-
-const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-
-describe('TableElement', () => {
- const store = mockStore(initialState);
- const mockedProps = {
- table,
- timeout: 0,
- };
- it('renders', () => {
- expect(React.isValidElement(
{JSON.stringify(ix, null, ' ')}))} triggerNode={