You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2024/02/06 19:26:57 UTC
(superset) branch master updated: chore(sqllab): migrate to typescript (#26171)
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 14f88e3f89 chore(sqllab): migrate to typescript (#26171)
14f88e3f89 is described below
commit 14f88e3f895d9d0c94dcf3a12eafdd8cf7337c94
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Tue Feb 6 11:26:50 2024 -0800
chore(sqllab): migrate to typescript (#26171)
---
.../superset-ui-core/src/query/types/Query.ts | 1 +
superset-frontend/spec/helpers/reducerIndex.ts | 4 +-
.../components/App/{App.test.jsx => App.test.tsx} | 31 ++-
.../SqlLab/components/App/{index.jsx => index.tsx} | 52 +++--
.../src/SqlLab/components/SqlEditor/index.tsx | 5 +-
...rLeftBar.test.jsx => SqlEditorLeftBar.test.tsx} | 177 ++++++++--------
.../SqlLab/components/SqlEditorLeftBar/index.tsx | 23 +--
.../TabbedSqlEditors/TabbedSqlEditors.test.jsx | 228 ---------------------
.../TabbedSqlEditors/TabbedSqlEditors.test.tsx | 178 ++++++++++++++++
.../TabbedSqlEditors/{index.jsx => index.tsx} | 95 ++++-----
superset-frontend/src/SqlLab/fixtures.ts | 2 +-
superset-frontend/src/SqlLab/types.ts | 19 +-
...ryResults.test.js => emptyQueryResults.test.ts} | 7 +-
...Helper.js => reduxStateToLocalStorageHelper.ts} | 18 +-
.../src/components/EmptyState/index.tsx | 2 +-
superset-frontend/src/types/bootstrapTypes.ts | 1 +
16 files changed, 413 insertions(+), 430 deletions(-)
diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
index 488caaa600..8999a2b574 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
@@ -328,6 +328,7 @@ export type Query = {
actions: Record<string, any>;
type: DatasourceType;
columns: QueryColumn[];
+ runAsync?: boolean;
};
export type QueryResults = {
diff --git a/superset-frontend/spec/helpers/reducerIndex.ts b/superset-frontend/spec/helpers/reducerIndex.ts
index 95fe4d3f1c..a4e0021839 100644
--- a/superset-frontend/spec/helpers/reducerIndex.ts
+++ b/superset-frontend/spec/helpers/reducerIndex.ts
@@ -39,8 +39,8 @@ const common = { ...bootstrapData.common };
const user = { ...bootstrapData.user };
const noopReducer =
- (initialState: unknown) =>
- (state = initialState) =>
+ <STATE = unknown>(initialState: STATE) =>
+ (state: STATE = initialState) =>
state;
export default {
diff --git a/superset-frontend/src/SqlLab/components/App/App.test.jsx b/superset-frontend/src/SqlLab/components/App/App.test.tsx
similarity index 86%
rename from superset-frontend/src/SqlLab/components/App/App.test.jsx
rename to superset-frontend/src/SqlLab/components/App/App.test.tsx
index d3db1d5fb8..b609419cb1 100644
--- a/superset-frontend/src/SqlLab/components/App/App.test.jsx
+++ b/superset-frontend/src/SqlLab/components/App/App.test.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
import React from 'react';
-import { combineReducers } from 'redux';
+import { AnyAction, combineReducers } from 'redux';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { render } from 'spec/helpers/testing-library';
@@ -38,18 +38,15 @@ jest.mock('src/SqlLab/components/QueryAutoRefresh', () => () => (
<div data-test="mock-query-auto-refresh" />
));
-const sqlLabReducer = combineReducers(reducers);
+const sqlLabReducer = combineReducers({
+ localStorageUsageInKilobytes: reducers.localStorageUsageInKilobytes,
+});
+const mockAction = {} as AnyAction;
describe('SqlLab App', () => {
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
- const store = mockStore(sqlLabReducer(undefined, {}), {});
- beforeEach(() => {
- jest.useFakeTimers();
- });
- afterEach(() => {
- jest.useRealTimers();
- });
+ const store = mockStore(sqlLabReducer(undefined, mockAction));
it('is valid', () => {
expect(React.isValidElement(<App />)).toBe(true);
@@ -61,15 +58,13 @@ describe('SqlLab App', () => {
expect(getByTestId('mock-tabbed-sql-editors')).toBeInTheDocument();
});
- it('logs current usage warning', () => {
+ it('logs current usage warning', async () => {
const localStorageUsageInKilobytes = LOCALSTORAGE_MAX_USAGE_KB + 10;
+ const initialState = {
+ localStorageUsageInKilobytes,
+ };
const storeExceedLocalStorage = mockStore(
- sqlLabReducer(
- {
- localStorageUsageInKilobytes,
- },
- {},
- ),
+ sqlLabReducer(initialState, mockAction),
);
const { rerender } = render(<App />, {
@@ -87,14 +82,14 @@ describe('SqlLab App', () => {
);
});
- it('logs current local storage usage', () => {
+ it('logs current local storage usage', async () => {
const localStorageUsageInKilobytes = LOCALSTORAGE_MAX_USAGE_KB - 10;
const storeExceedLocalStorage = mockStore(
sqlLabReducer(
{
localStorageUsageInKilobytes,
},
- {},
+ mockAction,
),
);
diff --git a/superset-frontend/src/SqlLab/components/App/index.jsx b/superset-frontend/src/SqlLab/components/App/index.tsx
similarity index 85%
rename from superset-frontend/src/SqlLab/components/App/index.jsx
rename to superset-frontend/src/SqlLab/components/App/index.tsx
index b830454e19..4d2e1d222c 100644
--- a/superset-frontend/src/SqlLab/components/App/index.jsx
+++ b/superset-frontend/src/SqlLab/components/App/index.tsx
@@ -17,8 +17,6 @@
* under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
-import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { css, styled, t } from '@superset-ui/core';
@@ -28,7 +26,8 @@ import {
LOCALSTORAGE_WARNING_THRESHOLD,
LOCALSTORAGE_WARNING_MESSAGE_THROTTLE_MS,
} from 'src/SqlLab/constants';
-import * as Actions from 'src/SqlLab/actions/sqlLab';
+import { addDangerToast } from 'src/components/MessageToasts/actions';
+import type { SqlLabRootState } from 'src/SqlLab/types';
import { logEvent } from 'src/logger/actions';
import {
LOG_ACTIONS_SQLLAB_WARN_LOCAL_STORAGE_USAGE,
@@ -100,8 +99,21 @@ const SqlLabStyles = styled.div`
`};
`;
-class App extends React.PureComponent {
- constructor(props) {
+type PureProps = {
+ // add this for testing componentDidUpdate spec
+ updated?: boolean;
+};
+
+type AppProps = ReturnType<typeof mergeProps> & PureProps;
+
+interface AppState {
+ hash: string;
+}
+
+class App extends React.PureComponent<AppProps, AppState> {
+ hasLoggedLocalStorageUsage: boolean;
+
+ constructor(props: AppProps) {
super(props);
this.state = {
hash: window.location.hash,
@@ -125,7 +137,7 @@ class App extends React.PureComponent {
componentDidUpdate() {
const { localStorageUsageInKilobytes, actions, queries } = this.props;
- const queryCount = queries?.lenghth || 0;
+ const queryCount = Object.keys(queries || {}).length || 0;
if (
localStorageUsageInKilobytes >=
LOCALSTORAGE_WARNING_THRESHOLD * LOCALSTORAGE_MAX_USAGE_KB
@@ -159,7 +171,7 @@ class App extends React.PureComponent {
this.setState({ hash: window.location.hash });
}
- showLocalStorageUsageWarning(currentUsage, queryCount) {
+ showLocalStorageUsageWarning(currentUsage: number, queryCount: number) {
this.props.actions.addDangerToast(
t(
"SQL Lab uses your browser's local storage to store queries and results." +
@@ -190,7 +202,6 @@ class App extends React.PureComponent {
<Redirect
to={{
pathname: '/sqllab/history/',
- replace: true,
}}
/>
);
@@ -207,13 +218,7 @@ class App extends React.PureComponent {
}
}
-App.propTypes = {
- actions: PropTypes.object,
- common: PropTypes.object,
- localStorageUsageInKilobytes: PropTypes.number.isRequired,
-};
-
-function mapStateToProps(state) {
+function mapStateToProps(state: SqlLabRootState) {
const { common, localStorageUsageInKilobytes, sqlLab } = state;
return {
common,
@@ -223,10 +228,21 @@ function mapStateToProps(state) {
};
}
-function mapDispatchToProps(dispatch) {
+const mapDispatchToProps = {
+ addDangerToast,
+ logEvent,
+};
+
+function mergeProps(
+ stateProps: ReturnType<typeof mapStateToProps>,
+ dispatchProps: typeof mapDispatchToProps,
+ state: PureProps,
+) {
return {
- actions: bindActionCreators({ ...Actions, logEvent }, dispatch),
+ ...state,
+ ...stateProps,
+ actions: dispatchProps,
};
}
-export default connect(mapStateToProps, mapDispatchToProps)(App);
+export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(App);
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
index 8213253685..23de528066 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
@@ -103,7 +103,7 @@ import SaveQuery, { QueryPayload } from '../SaveQuery';
import ScheduleQueryButton from '../ScheduleQueryButton';
import EstimateQueryCostButton from '../EstimateQueryCostButton';
import ShareSqlLabQuery from '../ShareSqlLabQuery';
-import SqlEditorLeftBar, { ExtendedTable } from '../SqlEditorLeftBar';
+import SqlEditorLeftBar from '../SqlEditorLeftBar';
import AceEditorWrapper from '../AceEditorWrapper';
import RunQueryActionButton from '../RunQueryActionButton';
import QueryLimitSelect from '../QueryLimitSelect';
@@ -215,7 +215,6 @@ const StyledSqlEditor = styled.div`
const extensionsRegistry = getExtensionsRegistry();
export type Props = {
- tables: ExtendedTable[];
queryEditor: QueryEditor;
defaultQueryLimit: number;
maxRow: number;
@@ -235,7 +234,6 @@ const elementStyle = (
});
const SqlEditor: React.FC<Props> = ({
- tables,
queryEditor,
defaultQueryLimit,
maxRow,
@@ -839,7 +837,6 @@ const SqlEditor: React.FC<Props> = ({
<SqlEditorLeftBar
database={database}
queryEditorId={queryEditor.id}
- tables={tables}
setEmptyState={bool => setShowEmptyState(bool)}
/>
</StyledSidebar>
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.jsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx
similarity index 54%
rename from superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.jsx
rename to superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx
index 6665091572..f89c842b15 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.jsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx
@@ -20,16 +20,19 @@ import React from 'react';
import fetchMock from 'fetch-mock';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
-import { Provider } from 'react-redux';
-import '@testing-library/jest-dom/extend-expect';
-import SqlEditorLeftBar from 'src/SqlLab/components/SqlEditorLeftBar';
-import { table, initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
-import { api } from 'src/hooks/apiResources/queryApi';
-import { setupStore } from 'src/views/store';
-import reducers from 'spec/helpers/reducerIndex';
+import SqlEditorLeftBar, {
+ SqlEditorLeftBarProps,
+} from 'src/SqlLab/components/SqlEditorLeftBar';
+import {
+ table,
+ initialState,
+ defaultQueryEditor,
+ extraQueryEditor1,
+} from 'src/SqlLab/fixtures';
+import type { RootState } from 'src/views/store';
+import type { Store } from 'redux';
const mockedProps = {
- tables: [table],
queryEditorId: defaultQueryEditor.id,
database: {
id: 1,
@@ -39,115 +42,117 @@ const mockedProps = {
height: 0,
};
-let store;
-let actions;
-
-const logAction = () => next => action => {
- if (typeof action === 'function') {
- return next(action);
- }
- actions.push(action);
- return next(action);
-};
-
-const createStore = initState =>
- setupStore({
- disableDegugger: true,
- initialState: initState,
- rootReducers: reducers,
- middleware: getDefaultMiddleware =>
- getDefaultMiddleware().concat(api.middleware, logAction),
- });
-
beforeEach(() => {
- store = createStore(initialState);
- actions = [];
fetchMock.get('glob:*/api/v1/database/?*', { result: [] });
fetchMock.get('glob:*/api/v1/database/*/schemas/?*', {
count: 2,
result: ['main', 'new_schema'],
});
fetchMock.get('glob:*/api/v1/database/*/tables/*', {
- count: 1,
+ count: 2,
result: [
{
label: 'ab_user',
value: 'ab_user',
},
+ {
+ label: 'new_table',
+ value: 'new_table',
+ },
],
});
+ fetchMock.get('glob:*/api/v1/database/*/table/*/*', {
+ status: 200,
+ body: {
+ columns: table.columns,
+ },
+ });
+ fetchMock.get('glob:*/api/v1/database/*/table_extra/*/*', {
+ status: 200,
+ body: {},
+ });
});
afterEach(() => {
fetchMock.restore();
- store.dispatch(api.util.resetApiState());
jest.clearAllMocks();
});
-const renderAndWait = (props, store) =>
+const renderAndWait = (
+ props: SqlEditorLeftBarProps,
+ store?: Store,
+ initialState?: RootState,
+) =>
waitFor(() =>
render(<SqlEditorLeftBar {...props} />, {
useRedux: true,
+ initialState,
...(store && { store }),
}),
);
-test('is valid', () => {
- expect(
- React.isValidElement(
- <Provider store={store}>
- <SqlEditorLeftBar {...mockedProps} />
- </Provider>,
- ),
- ).toBe(true);
-});
-
test('renders a TableElement', async () => {
- await renderAndWait(mockedProps, store);
- expect(await screen.findByText(/Database/i)).toBeInTheDocument();
- const tableElement = screen.getAllByTestId('table-element');
+ const { findByText, getAllByTestId } = await renderAndWait(
+ mockedProps,
+ undefined,
+ { ...initialState, sqlLab: { ...initialState.sqlLab, tables: [table] } },
+ );
+ expect(await findByText(/Database/i)).toBeInTheDocument();
+ const tableElement = getAllByTestId('table-element');
expect(tableElement.length).toBeGreaterThanOrEqual(1);
});
test('table should be visible when expanded is true', async () => {
- const { container } = await renderAndWait(mockedProps, store);
+ const { container, getByText, getByRole, queryAllByText } =
+ await renderAndWait(mockedProps, undefined, {
+ ...initialState,
+ sqlLab: { ...initialState.sqlLab, tables: [table] },
+ });
- const dbSelect = screen.getByRole('combobox', {
+ const dbSelect = getByRole('combobox', {
name: 'Select database or type to search databases',
});
- const schemaSelect = screen.getByRole('combobox', {
+ const schemaSelect = getByRole('combobox', {
name: 'Select schema or type to search schemas',
});
- const dropdown = screen.getByText(/Table/i);
- const abUser = screen.queryAllByText(/ab_user/i);
-
- await waitFor(() => {
- expect(screen.getByText(/Database/i)).toBeInTheDocument();
- expect(dbSelect).toBeInTheDocument();
- expect(schemaSelect).toBeInTheDocument();
- expect(dropdown).toBeInTheDocument();
- expect(abUser).toHaveLength(2);
- expect(
- container.querySelector('.ant-collapse-content-active'),
- ).toBeInTheDocument();
+ const dropdown = getByText(/Table/i);
+ const abUser = queryAllByText(/ab_user/i);
+
+ expect(getByText(/Database/i)).toBeInTheDocument();
+ expect(dbSelect).toBeInTheDocument();
+ expect(schemaSelect).toBeInTheDocument();
+ expect(dropdown).toBeInTheDocument();
+ expect(abUser).toHaveLength(2);
+ expect(
+ container.querySelector('.ant-collapse-content-active'),
+ ).toBeInTheDocument();
+ table.columns.forEach(({ name }) => {
+ expect(getByText(name)).toBeInTheDocument();
});
});
test('should toggle the table when the header is clicked', async () => {
- await renderAndWait(mockedProps, store);
+ const { container } = await renderAndWait(mockedProps, undefined, {
+ ...initialState,
+ sqlLab: { ...initialState.sqlLab, tables: [table] },
+ });
- const header = (await screen.findAllByText(/ab_user/))[1];
+ const header = container.querySelector('.ant-collapse-header');
expect(header).toBeInTheDocument();
- userEvent.click(header);
+ if (header) {
+ userEvent.click(header);
+ }
- await waitFor(() => {
- expect(actions[actions.length - 1].type).toEqual('COLLAPSE_TABLE');
- });
+ await waitFor(() =>
+ expect(
+ container.querySelector('.ant-collapse-content-inactive'),
+ ).toBeInTheDocument(),
+ );
});
test('When changing database the table list must be updated', async () => {
- store = createStore({
+ const { rerender } = await renderAndWait(mockedProps, undefined, {
...initialState,
sqlLab: {
...initialState.sqlLab,
@@ -155,9 +160,25 @@ test('When changing database the table list must be updated', async () => {
id: defaultQueryEditor.id,
schema: 'new_schema',
},
+ queryEditors: [
+ defaultQueryEditor,
+ {
+ ...extraQueryEditor1,
+ schema: 'new_schema',
+ dbId: 2,
+ },
+ ],
+ tables: [
+ table,
+ {
+ ...table,
+ dbId: 2,
+ name: 'new_table',
+ queryEditorId: extraQueryEditor1.id,
+ },
+ ],
},
});
- const { rerender } = await renderAndWait(mockedProps, store);
expect(screen.getAllByText(/main/i)[0]).toBeInTheDocument();
expect(screen.getAllByText(/ab_user/i)[0]).toBeInTheDocument();
@@ -170,21 +191,18 @@ test('When changing database the table list must be updated', async () => {
database_name: 'new_db',
backend: 'postgresql',
}}
- queryEditorId={defaultQueryEditor.id}
- tables={[{ ...mockedProps.tables[0], dbId: 2, name: 'new_table' }]}
+ queryEditorId={extraQueryEditor1.id}
/>,
- {
- useRedux: true,
- store,
- },
);
- expect(await screen.findByText(/new_db/i)).toBeInTheDocument();
- expect(await screen.findByText(/new_table/i)).toBeInTheDocument();
+ const updatedDbSelector = await screen.findAllByText(/new_db/i);
+ expect(updatedDbSelector[0]).toBeInTheDocument();
+ const updatedTableSelector = await screen.findAllByText(/new_table/i);
+ expect(updatedTableSelector[0]).toBeInTheDocument();
});
test('ignore schema api when current schema is deprecated', async () => {
const invalidSchemaName = 'None';
- store = createStore({
+ await renderAndWait(mockedProps, undefined, {
...initialState,
sqlLab: {
...initialState.sqlLab,
@@ -192,9 +210,9 @@ test('ignore schema api when current schema is deprecated', async () => {
id: defaultQueryEditor.id,
schema: invalidSchemaName,
},
+ tables: [table],
},
});
- const { rerender } = await renderAndWait(mockedProps, store);
expect(await screen.findByText(/Database/i)).toBeInTheDocument();
expect(fetchMock.calls()).not.toContainEqual(
@@ -204,7 +222,6 @@ test('ignore schema api when current schema is deprecated', async () => {
),
]),
);
- rerender();
// Deselect the deprecated schema selection
await waitFor(() =>
expect(screen.queryByText(/None/i)).not.toBeInTheDocument(),
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
index eff67d49b1..15a1735626 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
@@ -24,10 +24,10 @@ import React, {
Dispatch,
SetStateAction,
} from 'react';
-import { useDispatch } from 'react-redux';
+import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import querystring from 'query-string';
-import { Table } from 'src/SqlLab/types';
+import { SqlLabRootState, Table } from 'src/SqlLab/types';
import {
queryEditorSetDb,
addTable,
@@ -55,16 +55,11 @@ import {
} from 'src/utils/localStorageHelpers';
import TableElement from '../TableElement';
-export interface ExtendedTable extends Table {
- expanded: boolean;
-}
-
-interface SqlEditorLeftBarProps {
+export interface SqlEditorLeftBarProps {
queryEditorId: string;
height?: number;
- tables?: ExtendedTable[];
database?: DatabaseObject;
- setEmptyState: Dispatch<SetStateAction<boolean>>;
+ setEmptyState?: Dispatch<SetStateAction<boolean>>;
}
const StyledScrollbarContainer = styled.div`
@@ -111,10 +106,14 @@ const LeftBarStyles = styled.div`
const SqlEditorLeftBar = ({
database,
queryEditorId,
- tables = [],
height = 500,
setEmptyState,
}: SqlEditorLeftBarProps) => {
+ const tables = useSelector<SqlLabRootState, Table[]>(
+ ({ sqlLab }) =>
+ sqlLab.tables.filter(table => table.queryEditorId === queryEditorId),
+ shallowEqual,
+ );
const dispatch = useDispatch();
const queryEditor = useQueryEditor(queryEditorId, ['dbId', 'schema']);
@@ -144,7 +143,7 @@ const SqlEditorLeftBar = ({
};
const onDbChange = ({ id: dbId }: { id: number }) => {
- setEmptyState(false);
+ setEmptyState?.(false);
dispatch(queryEditorSetDb(queryEditor, dbId));
};
@@ -177,7 +176,7 @@ const SqlEditorLeftBar = ({
};
const onToggleTable = (updatedTables: string[]) => {
- tables.forEach((table: ExtendedTable) => {
+ tables.forEach(table => {
if (!updatedTables.includes(table.id.toString()) && table.expanded) {
dispatch(collapseTable(table));
} else if (
diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx
deleted file mode 100644
index 5d782590a1..0000000000
--- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx
+++ /dev/null
@@ -1,228 +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 URI from 'urijs';
-import { Provider } from 'react-redux';
-import { shallow, mount } from 'enzyme';
-import { fireEvent, render, waitFor } from 'spec/helpers/testing-library';
-import sinon from 'sinon';
-import { act } from 'react-dom/test-utils';
-import fetchMock from 'fetch-mock';
-import { supersetTheme, ThemeProvider } from '@superset-ui/core';
-import { EditableTabs } from 'src/components/Tabs';
-import TabbedSqlEditors from 'src/SqlLab/components/TabbedSqlEditors';
-import SqlEditor from 'src/SqlLab/components/SqlEditor';
-import { initialState } from 'src/SqlLab/fixtures';
-import { newQueryTabName } from 'src/SqlLab/utils/newQueryTabName';
-
-fetchMock.get('glob:*/api/v1/database/*', {});
-fetchMock.get('glob:*/api/v1/saved_query/*', {});
-fetchMock.get('glob:*/kv/*', {});
-
-describe('TabbedSqlEditors', () => {
- const middlewares = [thunk];
- const mockStore = configureStore(middlewares);
- const store = mockStore(initialState);
-
- const queryEditors = [
- {
- autorun: false,
- dbId: 1,
- id: 'newEditorId',
- latestQueryId: 'B1-VQU1zW',
- schema: null,
- selectedText: null,
- sql: 'SELECT ds...',
- name: 'Untitled Query',
- },
- ];
- const mockedProps = {
- actions: {},
- databases: {},
- tables: [],
- queries: {},
- queryEditors: initialState.sqlLab.queryEditors,
- tabHistory: initialState.sqlLab.tabHistory,
- editorHeight: '',
- getHeight: () => '100px',
- database: {},
- defaultQueryLimit: 1000,
- maxRow: 100000,
- };
-
- const getWrapper = () =>
- shallow(<TabbedSqlEditors store={store} {...mockedProps} />)
- .dive()
- .dive();
-
- const mountWithAct = async () =>
- act(async () => {
- mount(
- <Provider store={store}>
- <TabbedSqlEditors {...mockedProps} />
- </Provider>,
- {
- wrappingComponent: ThemeProvider,
- wrappingComponentProps: { theme: supersetTheme },
- },
- );
- });
- const setup = (props = {}, overridesStore) =>
- render(<TabbedSqlEditors {...props} />, {
- useRedux: true,
- store: overridesStore || store,
- });
-
- let wrapper;
- it('is valid', () => {
- expect(React.isValidElement(<TabbedSqlEditors {...mockedProps} />)).toBe(
- true,
- );
- });
- describe('componentDidMount', () => {
- let uriStub;
- beforeEach(() => {
- sinon.stub(window.history, 'replaceState');
- uriStub = sinon.stub(URI.prototype, 'search');
- });
- afterEach(() => {
- window.history.replaceState.restore();
- uriStub.restore();
- });
- it('should handle id', async () => {
- uriStub.returns({ id: 1 });
- await mountWithAct();
- expect(window.history.replaceState.getCall(0).args[2]).toBe('/sqllab');
- });
- it('should handle savedQueryId', async () => {
- uriStub.returns({ savedQueryId: 1 });
- await mountWithAct();
- expect(window.history.replaceState.getCall(0).args[2]).toBe('/sqllab');
- });
- it('should handle sql', async () => {
- uriStub.returns({ sql: 1, dbid: 1 });
- await mountWithAct();
- expect(window.history.replaceState.getCall(0).args[2]).toBe('/sqllab');
- });
- it('should handle custom url params', async () => {
- uriStub.returns({
- sql: 1,
- dbid: 1,
- custom_value: 'str',
- extra_attr1: 'true',
- });
- await mountWithAct();
- expect(window.history.replaceState.getCall(0).args[2]).toBe(
- '/sqllab?custom_value=str&extra_attr1=true',
- );
- });
- });
- it('should removeQueryEditor', () => {
- wrapper = getWrapper();
- sinon.stub(wrapper.instance().props.actions, 'removeQueryEditor');
-
- wrapper.instance().removeQueryEditor(queryEditors[0]);
- expect(
- wrapper.instance().props.actions.removeQueryEditor.getCall(0).args[0],
- ).toBe(queryEditors[0]);
- });
- it('should add new query editor', async () => {
- const { getAllByLabelText } = setup(mockedProps);
- fireEvent.click(getAllByLabelText('Add tab')[0]);
- const actions = store.getActions();
- await waitFor(() =>
- expect(actions).toContainEqual({
- type: 'ADD_QUERY_EDITOR',
- queryEditor: expect.objectContaining({
- name: expect.stringMatching(/Untitled Query (\d+)+/),
- }),
- }),
- );
- });
- it('should properly increment query tab name', async () => {
- const { getAllByLabelText } = setup(mockedProps);
- const newTitle = newQueryTabName(store.getState().sqlLab.queryEditors);
- fireEvent.click(getAllByLabelText('Add tab')[0]);
- const actions = store.getActions();
- await waitFor(() =>
- expect(actions).toContainEqual({
- type: 'ADD_QUERY_EDITOR',
- queryEditor: expect.objectContaining({
- name: newTitle,
- }),
- }),
- );
- });
- it('should duplicate query editor', () => {
- wrapper = getWrapper();
- sinon.stub(wrapper.instance().props.actions, 'cloneQueryToNewTab');
-
- wrapper.instance().duplicateQueryEditor(queryEditors[0]);
- expect(
- wrapper.instance().props.actions.cloneQueryToNewTab.getCall(0).args[0],
- ).toBe(queryEditors[0]);
- });
- it('should handle select', () => {
- const mockEvent = {
- target: {
- getAttribute: () => null,
- },
- };
- wrapper = getWrapper();
- sinon.stub(wrapper.instance().props.actions, 'switchQueryEditor');
-
- // cannot switch to current tab, switchQueryEditor never gets called
- wrapper.instance().handleSelect('dfsadfs', mockEvent);
- expect(
- wrapper.instance().props.actions.switchQueryEditor.callCount,
- ).toEqual(0);
- });
- it('should handle add tab', () => {
- wrapper = getWrapper();
- sinon.spy(wrapper.instance(), 'newQueryEditor');
-
- wrapper.instance().handleEdit('1', 'add');
- expect(wrapper.instance().newQueryEditor.callCount).toBe(1);
- wrapper.instance().newQueryEditor.restore();
- });
- it('should render', () => {
- wrapper = getWrapper();
- wrapper.setState({ hideLeftBar: true });
-
- const firstTab = wrapper.find(EditableTabs.TabPane).first();
- expect(firstTab.props()['data-key']).toContain(
- initialState.sqlLab.queryEditors[0].id,
- );
- expect(firstTab.find(SqlEditor)).toHaveLength(1);
- });
- it('should disable new tab when offline', () => {
- wrapper = getWrapper();
- expect(wrapper.find('#a11y-query-editor-tabs').props().hideAdd).toBe(false);
- wrapper.setProps({ offline: true });
- expect(wrapper.find('#a11y-query-editor-tabs').props().hideAdd).toBe(true);
- });
- it('should have an empty state when query editors is empty', () => {
- wrapper = getWrapper();
- wrapper.setProps({ queryEditors: [] });
- const firstTab = wrapper.find(EditableTabs.TabPane).first();
- expect(firstTab.props()['data-key']).toBe(0);
- });
-});
diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.tsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.tsx
new file mode 100644
index 0000000000..6b048830e8
--- /dev/null
+++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.tsx
@@ -0,0 +1,178 @@
+/**
+ * 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 URI from 'urijs';
+import { fireEvent, render, waitFor } from 'spec/helpers/testing-library';
+import fetchMock from 'fetch-mock';
+import TabbedSqlEditors from 'src/SqlLab/components/TabbedSqlEditors';
+import { initialState } from 'src/SqlLab/fixtures';
+import { newQueryTabName } from 'src/SqlLab/utils/newQueryTabName';
+import { Store } from 'redux';
+import { RootState } from 'src/views/store';
+import { SET_ACTIVE_QUERY_EDITOR } from 'src/SqlLab/actions/sqlLab';
+
+fetchMock.get('glob:*/api/v1/database/*', {});
+fetchMock.get('glob:*/api/v1/saved_query/*', {});
+fetchMock.get('glob:*/kv/*', {});
+
+jest.mock('src/SqlLab/components/SqlEditor', () => () => (
+ <div data-test="mock-sql-editor" />
+));
+
+const middlewares = [thunk];
+const mockStore = configureStore(middlewares);
+const store = mockStore(initialState);
+
+const setup = (overridesStore?: Store, initialState?: RootState) =>
+ render(<TabbedSqlEditors />, {
+ useRedux: true,
+ initialState,
+ ...(overridesStore && { store: overridesStore }),
+ });
+
+beforeEach(() => {
+ store.clearActions();
+});
+
+describe('componentDidMount', () => {
+ let uriStub = jest.spyOn(URI.prototype, 'search');
+ let replaceState = jest.spyOn(window.history, 'replaceState');
+ beforeEach(() => {
+ replaceState = jest.spyOn(window.history, 'replaceState');
+ uriStub = jest.spyOn(URI.prototype, 'search');
+ });
+ afterEach(() => {
+ replaceState.mockReset();
+ uriStub.mockReset();
+ });
+ test('should handle id', () => {
+ uriStub.mockReturnValue({ id: 1 });
+ setup(store);
+ expect(replaceState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ '/sqllab',
+ );
+ });
+ test('should handle savedQueryId', () => {
+ uriStub.mockReturnValue({ savedQueryId: 1 });
+ setup(store);
+ expect(replaceState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ '/sqllab',
+ );
+ });
+ test('should handle sql', () => {
+ uriStub.mockReturnValue({ sql: 1, dbid: 1 });
+ setup(store);
+ expect(replaceState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ '/sqllab',
+ );
+ });
+ test('should handle custom url params', () => {
+ uriStub.mockReturnValue({
+ sql: 1,
+ dbid: 1,
+ custom_value: 'str',
+ extra_attr1: 'true',
+ });
+ setup(store);
+ expect(replaceState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ '/sqllab?custom_value=str&extra_attr1=true',
+ );
+ });
+});
+
+test('should removeQueryEditor', async () => {
+ const { getByRole, getAllByRole, queryByText } = setup(
+ undefined,
+ initialState,
+ );
+ const tabCount = getAllByRole('tab').length;
+ const tabList = getByRole('tablist');
+ const closeButton = tabList.getElementsByTagName('button')[0];
+ expect(closeButton).toBeInTheDocument();
+ if (closeButton) {
+ fireEvent.click(closeButton);
+ }
+ await waitFor(() => expect(getAllByRole('tab').length).toEqual(tabCount - 1));
+ expect(queryByText(initialState.sqlLab.queryEditors[0].name)).toBeFalsy();
+});
+test('should add new query editor', async () => {
+ const { getAllByLabelText, getAllByRole } = setup(undefined, initialState);
+ const tabCount = getAllByRole('tab').length;
+ fireEvent.click(getAllByLabelText('Add tab')[0]);
+ await waitFor(() => expect(getAllByRole('tab').length).toEqual(tabCount + 1));
+ expect(getAllByRole('tab')[tabCount]).toHaveTextContent(
+ /Untitled Query (\d+)+/,
+ );
+});
+test('should properly increment query tab name', async () => {
+ const { getAllByLabelText, getAllByRole } = setup(undefined, initialState);
+ const tabCount = getAllByRole('tab').length;
+ const newTitle = newQueryTabName(initialState.sqlLab.queryEditors);
+ fireEvent.click(getAllByLabelText('Add tab')[0]);
+ await waitFor(() => expect(getAllByRole('tab').length).toEqual(tabCount + 1));
+ expect(getAllByRole('tab')[tabCount]).toHaveTextContent(newTitle);
+});
+test('should handle select', async () => {
+ const { getAllByRole } = setup(store);
+ const tabs = getAllByRole('tab');
+ fireEvent.click(tabs[1]);
+ await waitFor(() => expect(store.getActions()).toHaveLength(1));
+ expect(store.getActions()[0]).toEqual(
+ expect.objectContaining({
+ type: SET_ACTIVE_QUERY_EDITOR,
+ queryEditor: initialState.sqlLab.queryEditors[1],
+ }),
+ );
+});
+test('should render', () => {
+ const { getAllByRole } = setup(store);
+ const tabs = getAllByRole('tab');
+ expect(tabs).toHaveLength(initialState.sqlLab.queryEditors.length);
+});
+test('should disable new tab when offline', () => {
+ const { queryAllByLabelText } = setup(undefined, {
+ ...initialState,
+ sqlLab: {
+ ...initialState.sqlLab,
+ offline: true,
+ },
+ });
+ expect(queryAllByLabelText('Add tab').length).toEqual(0);
+});
+test('should have an empty state when query editors is empty', () => {
+ const { getByText } = setup(undefined, {
+ ...initialState,
+ sqlLab: {
+ ...initialState.sqlLab,
+ queryEditors: [],
+ tabHistory: [],
+ },
+ });
+ expect(getByText('Add a new tab to create SQL Query')).toBeInTheDocument();
+});
diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx
similarity index 83%
rename from superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx
rename to superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx
index 0bd60fff7e..62ecfb5dcc 100644
--- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx
+++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx
@@ -18,11 +18,10 @@
*/
import React from 'react';
import { pick } from 'lodash';
-import PropTypes from 'prop-types';
import { EditableTabs } from 'src/components/Tabs';
import { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
import URI from 'urijs';
+import type { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
import { FeatureFlag, styled, t, isFeatureEnabled } from '@superset-ui/core';
import { Tooltip } from 'src/components/Tooltip';
import { detectOS } from 'src/utils/common';
@@ -33,22 +32,7 @@ import { locationContext } from 'src/pages/SqlLab/LocationContext';
import SqlEditor from '../SqlEditor';
import SqlEditorTabHeader from '../SqlEditorTabHeader';
-const propTypes = {
- actions: PropTypes.object.isRequired,
- defaultDbId: PropTypes.number,
- displayLimit: PropTypes.number,
- defaultQueryLimit: PropTypes.number.isRequired,
- maxRow: PropTypes.number.isRequired,
- databases: PropTypes.object.isRequired,
- queries: PropTypes.object.isRequired,
- queryEditors: PropTypes.array,
- tabHistory: PropTypes.array.isRequired,
- tables: PropTypes.array.isRequired,
- offline: PropTypes.bool,
- saveQueryWarning: PropTypes.string,
- scheduleQueryWarning: PropTypes.string,
-};
-const defaultProps = {
+const DEFAULT_PROPS = {
queryEditors: [],
offline: false,
saveQueryWarning: null,
@@ -73,15 +57,14 @@ const TabTitle = styled.span`
// Get the user's OS
const userOS = detectOS();
-class TabbedSqlEditors extends React.PureComponent {
- constructor(props) {
+type TabbedSqlEditorsProps = ReturnType<typeof mergeProps>;
+
+const SQL_LAB_URL = '/sqllab';
+
+class TabbedSqlEditors extends React.PureComponent<TabbedSqlEditorsProps> {
+ constructor(props: TabbedSqlEditorsProps) {
super(props);
- const sqlLabUrl = '/sqllab';
- this.state = {
- sqlLabUrl,
- };
this.removeQueryEditor = this.removeQueryEditor.bind(this);
- this.duplicateQueryEditor = this.duplicateQueryEditor.bind(this);
this.handleSelect = this.handleSelect.bind(this);
this.handleEdit = this.handleEdit.bind(this);
}
@@ -136,7 +119,7 @@ class TabbedSqlEditors extends React.PureComponent {
...this.context.requestedQuery,
...bootstrapData.requested_query,
...queryParameters,
- };
+ } as Record<string, string>;
// Popping a new tab based on the querystring
if (id || sql || savedQueryId || datasourceKey || queryId) {
@@ -149,7 +132,7 @@ class TabbedSqlEditors extends React.PureComponent {
} else if (datasourceKey) {
this.props.actions.popDatasourceQuery(datasourceKey, sql);
} else if (sql) {
- let databaseId = dbid;
+ let databaseId: string | number = dbid;
if (databaseId) {
databaseId = parseInt(databaseId, 10);
} else {
@@ -177,11 +160,11 @@ class TabbedSqlEditors extends React.PureComponent {
this.newQueryEditor();
if (isNewQuery) {
- window.history.replaceState({}, document.title, this.state.sqlLabUrl);
+ window.history.replaceState({}, document.title, SQL_LAB_URL);
}
} else {
const qe = this.activeQueryEditor();
- const latestQuery = this.props.queries[qe.latestQueryId];
+ const latestQuery = this.props.queries[qe?.latestQueryId || ''];
if (
isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) &&
latestQuery &&
@@ -197,9 +180,9 @@ class TabbedSqlEditors extends React.PureComponent {
}
}
- popNewTab(urlParams) {
+ popNewTab(urlParams: Record<string, string>) {
// Clean the url in browser history
- const updatedUrl = `${URI(this.state.sqlLabUrl).query(urlParams)}`;
+ const updatedUrl = `${URI(SQL_LAB_URL).query(urlParams)}`;
window.history.replaceState({}, document.title, updatedUrl);
}
@@ -215,7 +198,7 @@ class TabbedSqlEditors extends React.PureComponent {
this.props.actions.addNewQueryEditor();
}
- handleSelect(key) {
+ handleSelect(key: string) {
const qeid = this.props.tabHistory[this.props.tabHistory.length - 1];
if (key !== qeid) {
const queryEditor = this.props.queryEditors.find(qe => qe.id === key);
@@ -229,24 +212,22 @@ class TabbedSqlEditors extends React.PureComponent {
}
}
- handleEdit(key, action) {
+ handleEdit(key: string, action: string) {
if (action === 'remove') {
const qe = this.props.queryEditors.find(qe => qe.id === key);
- this.removeQueryEditor(qe);
+ if (qe) {
+ this.removeQueryEditor(qe);
+ }
}
if (action === 'add') {
this.newQueryEditor();
}
}
- removeQueryEditor(qe) {
+ removeQueryEditor(qe: QueryEditor) {
this.props.actions.removeQueryEditor(qe);
}
- duplicateQueryEditor(qe) {
- this.props.actions.cloneQueryToNewTab(qe, false);
- }
-
render() {
const noQueryEditors = this.props.queryEditors?.length === 0;
const editors = this.props.queryEditors?.map(qe => (
@@ -257,7 +238,6 @@ class TabbedSqlEditors extends React.PureComponent {
data-key={qe.id}
>
<SqlEditor
- tables={this.props.tables.filter(xt => xt.queryEditorId === qe.id)}
queryEditor={qe}
defaultQueryLimit={this.props.defaultQueryLimit}
maxRow={this.props.maxRow}
@@ -332,30 +312,45 @@ class TabbedSqlEditors extends React.PureComponent {
);
}
}
-TabbedSqlEditors.propTypes = propTypes;
-TabbedSqlEditors.defaultProps = defaultProps;
+
TabbedSqlEditors.contextType = locationContext;
-function mapStateToProps({ sqlLab, common }) {
+export function mapStateToProps({ sqlLab, common }: SqlLabRootState) {
return {
databases: sqlLab.databases,
- queryEditors: sqlLab.queryEditors,
+ queryEditors: sqlLab.queryEditors ?? DEFAULT_PROPS.queryEditors,
queries: sqlLab.queries,
tabHistory: sqlLab.tabHistory,
tables: sqlLab.tables,
defaultDbId: common.conf.SQLLAB_DEFAULT_DBID,
displayLimit: common.conf.DISPLAY_MAX_ROW,
- offline: sqlLab.offline,
+ offline: sqlLab.offline ?? DEFAULT_PROPS.offline,
defaultQueryLimit: common.conf.DEFAULT_SQLLAB_LIMIT,
maxRow: common.conf.SQL_MAX_ROW,
- saveQueryWarning: common.conf.SQLLAB_SAVE_WARNING_MESSAGE,
- scheduleQueryWarning: common.conf.SQLLAB_SCHEDULE_WARNING_MESSAGE,
+ saveQueryWarning:
+ common.conf.SQLLAB_SAVE_WARNING_MESSAGE ?? DEFAULT_PROPS.saveQueryWarning,
+ scheduleQueryWarning:
+ common.conf.SQLLAB_SCHEDULE_WARNING_MESSAGE ??
+ DEFAULT_PROPS.scheduleQueryWarning,
};
}
-function mapDispatchToProps(dispatch) {
+
+const mapDispatchToProps = {
+ ...Actions,
+};
+
+function mergeProps(
+ stateProps: ReturnType<typeof mapStateToProps>,
+ dispatchProps: typeof mapDispatchToProps,
+) {
return {
- actions: bindActionCreators(Actions, dispatch),
+ ...stateProps,
+ actions: dispatchProps,
};
}
-export default connect(mapStateToProps, mapDispatchToProps)(TabbedSqlEditors);
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+ mergeProps,
+)(TabbedSqlEditors);
diff --git a/superset-frontend/src/SqlLab/fixtures.ts b/superset-frontend/src/SqlLab/fixtures.ts
index 24d512a1b3..845e2209b5 100644
--- a/superset-frontend/src/SqlLab/fixtures.ts
+++ b/superset-frontend/src/SqlLab/fixtures.ts
@@ -35,7 +35,7 @@ export const alert = { bsStyle: 'danger', msg: 'Ooops', id: 'lksvmcx32' };
export const table = {
dbId: 1,
selectStar: 'SELECT * FROM ab_user',
- queryEditorId: 'rJ-KP47a',
+ queryEditorId: 'dfsadfs',
schema: 'superset',
name: 'ab_user',
id: 'r11Vgt60',
diff --git a/superset-frontend/src/SqlLab/types.ts b/superset-frontend/src/SqlLab/types.ts
index 6b150c2ba5..cac9ceb5d9 100644
--- a/superset-frontend/src/SqlLab/types.ts
+++ b/superset-frontend/src/SqlLab/types.ts
@@ -16,11 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { JsonObject, QueryResponse } from '@superset-ui/core';
-import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
+import { QueryResponse } from '@superset-ui/core';
+import {
+ CommonBootstrapData,
+ UserWithPermissionsAndRoles,
+} from 'src/types/bootstrapTypes';
import { ToastType } from 'src/components/MessageToasts/types';
import { DropdownButtonProps } from 'src/components/DropdownButton';
import { ButtonProps } from 'src/components/Button';
+import type { TableMetaData } from 'src/hooks/apiResources';
export type QueryButtonProps = DropdownButtonProps | ButtonProps;
@@ -81,8 +85,10 @@ export interface Table {
name: string;
queryEditorId: QueryEditor['id'];
dataPreviewQueryId: string | null;
- expanded?: boolean;
+ expanded: boolean;
initialized?: boolean;
+ inLocalStorage?: boolean;
+ persistData?: TableMetaData;
}
export type SqlLabRootState = {
@@ -92,7 +98,7 @@ export type SqlLabRootState = {
databases: Record<string, any>;
dbConnect: boolean;
offline: boolean;
- queries: Record<string, QueryResponse>;
+ queries: Record<string, QueryResponse & { inLocalStorage?: boolean }>;
queryEditors: QueryEditor[];
tabHistory: string[]; // default is activeTab ? [activeTab.id.toString()] : []
tables: Table[];
@@ -105,10 +111,7 @@ export type SqlLabRootState = {
localStorageUsageInKilobytes: number;
messageToasts: toastState[];
user: UserWithPermissionsAndRoles;
- common: {
- flash_messages: string[];
- conf: JsonObject;
- };
+ common: CommonBootstrapData;
};
export enum DatasetRadioState {
diff --git a/superset-frontend/src/SqlLab/utils/emptyQueryResults.test.js b/superset-frontend/src/SqlLab/utils/emptyQueryResults.test.ts
similarity index 94%
rename from superset-frontend/src/SqlLab/utils/emptyQueryResults.test.js
rename to superset-frontend/src/SqlLab/utils/emptyQueryResults.test.ts
index f08fccbef7..ca7f60af20 100644
--- a/superset-frontend/src/SqlLab/utils/emptyQueryResults.test.js
+++ b/superset-frontend/src/SqlLab/utils/emptyQueryResults.test.ts
@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
+import type { QueryResponse } from '@superset-ui/core';
import {
emptyQueryResults,
clearQueryEditors,
@@ -43,7 +44,7 @@ describe('reduxStateToLocalStorageHelper', () => {
expect(Date.now() - startDttm).toBeGreaterThan(
LOCALSTORAGE_MAX_QUERY_AGE_MS,
);
- expect(Object.keys(oldQuery.results)).toContain('data');
+ expect(Object.keys(oldQuery.results || {})).toContain('data');
const emptiedQuery = emptyQueryResults(queriesObj);
expect(emptiedQuery[id].startDttm).toBe(startDttm);
@@ -55,7 +56,7 @@ describe('reduxStateToLocalStorageHelper', () => {
...queries[0],
startDttm: Date.now(),
results: { data: [{ a: 1 }] },
- };
+ } as unknown as QueryResponse;
const largeQuery = {
...queries[1],
startDttm: Date.now(),
@@ -70,7 +71,7 @@ describe('reduxStateToLocalStorageHelper', () => {
},
],
},
- };
+ } as unknown as QueryResponse;
expect(Object.keys(largeQuery.results)).toContain('data');
const emptiedQuery = emptyQueryResults({
[reasonableSizeQuery.id]: reasonableSizeQuery,
diff --git a/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.js b/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts
similarity index 83%
rename from superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.js
rename to superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts
index 5b7a31b304..8b7f41f9f7 100644
--- a/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.js
+++ b/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts
@@ -16,6 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
+import type { QueryResponse } from '@superset-ui/core';
+import type { QueryEditor, SqlLabRootState, Table } from 'src/SqlLab/types';
+import type { ThunkDispatch } from 'redux-thunk';
import { pick } from 'lodash';
import { tableApiUtil } from 'src/hooks/apiResources/tables';
import {
@@ -44,7 +47,7 @@ const PERSISTENT_QUERY_EDITOR_KEYS = new Set([
'hideLeftBar',
]);
-function shouldEmptyQueryResults(query) {
+function shouldEmptyQueryResults(query: QueryResponse) {
const { startDttm, results } = query;
return (
Date.now() - startDttm > LOCALSTORAGE_MAX_QUERY_AGE_MS ||
@@ -53,7 +56,7 @@ function shouldEmptyQueryResults(query) {
);
}
-export function emptyTablePersistData(tables) {
+export function emptyTablePersistData(tables: Table[]) {
return tables
.map(table =>
pick(table, [
@@ -68,7 +71,9 @@ export function emptyTablePersistData(tables) {
.filter(({ queryEditorId }) => Boolean(queryEditorId));
}
-export function emptyQueryResults(queries) {
+export function emptyQueryResults(
+ queries: SqlLabRootState['sqlLab']['queries'],
+) {
return Object.keys(queries).reduce((accu, key) => {
const { results } = queries[key];
const query = {
@@ -84,7 +89,7 @@ export function emptyQueryResults(queries) {
}, {});
}
-export function clearQueryEditors(queryEditors) {
+export function clearQueryEditors(queryEditors: QueryEditor[]) {
return queryEditors.map(editor =>
// only return selected keys
Object.keys(editor)
@@ -99,7 +104,10 @@ export function clearQueryEditors(queryEditors) {
);
}
-export function rehydratePersistedState(dispatch, state) {
+export function rehydratePersistedState(
+ dispatch: ThunkDispatch<SqlLabRootState, unknown, any>,
+ state: SqlLabRootState,
+) {
// Rehydrate server side persisted table metadata
state.sqlLab.tables.forEach(({ name: table, schema, dbId, persistData }) => {
if (dbId && schema && table && persistData?.columns) {
diff --git a/superset-frontend/src/components/EmptyState/index.tsx b/superset-frontend/src/components/EmptyState/index.tsx
index 95c454b0ae..9a3e22cf1b 100644
--- a/superset-frontend/src/components/EmptyState/index.tsx
+++ b/superset-frontend/src/components/EmptyState/index.tsx
@@ -29,7 +29,7 @@ export enum EmptyStateSize {
}
export interface EmptyStateSmallProps {
- title: ReactNode;
+ title?: ReactNode;
description?: ReactNode;
image?: ReactNode;
}
diff --git a/superset-frontend/src/types/bootstrapTypes.ts b/superset-frontend/src/types/bootstrapTypes.ts
index 80570f5d22..332acf0e82 100644
--- a/superset-frontend/src/types/bootstrapTypes.ts
+++ b/superset-frontend/src/types/bootstrapTypes.ts
@@ -160,6 +160,7 @@ export interface BootstrapData {
embedded?: {
dashboard_id: string;
};
+ requested_query?: JsonObject;
}
export function isUser(user: any): user is User {