chore(sqllab): migrate to typescript (#26171)

This commit is contained in:
JUST.in DO IT 2024-02-06 11:26:50 -08:00 committed by GitHub
parent 5d46d3a5d3
commit 14f88e3f89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 415 additions and 432 deletions

View File

@ -328,6 +328,7 @@ export type Query = {
actions: Record<string, any>; actions: Record<string, any>;
type: DatasourceType; type: DatasourceType;
columns: QueryColumn[]; columns: QueryColumn[];
runAsync?: boolean;
}; };
export type QueryResults = { export type QueryResults = {

View File

@ -39,8 +39,8 @@ const common = { ...bootstrapData.common };
const user = { ...bootstrapData.user }; const user = { ...bootstrapData.user };
const noopReducer = const noopReducer =
(initialState: unknown) => <STATE = unknown>(initialState: STATE) =>
(state = initialState) => (state: STATE = initialState) =>
state; state;
export default { export default {

View File

@ -17,7 +17,7 @@
* under the License. * under the License.
*/ */
import React from 'react'; import React from 'react';
import { combineReducers } from 'redux'; import { AnyAction, combineReducers } from 'redux';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import { render } from 'spec/helpers/testing-library'; import { render } from 'spec/helpers/testing-library';
@ -38,18 +38,15 @@ jest.mock('src/SqlLab/components/QueryAutoRefresh', () => () => (
<div data-test="mock-query-auto-refresh" /> <div data-test="mock-query-auto-refresh" />
)); ));
const sqlLabReducer = combineReducers(reducers); const sqlLabReducer = combineReducers({
localStorageUsageInKilobytes: reducers.localStorageUsageInKilobytes,
});
const mockAction = {} as AnyAction;
describe('SqlLab App', () => { describe('SqlLab App', () => {
const middlewares = [thunk]; const middlewares = [thunk];
const mockStore = configureStore(middlewares); const mockStore = configureStore(middlewares);
const store = mockStore(sqlLabReducer(undefined, {}), {}); const store = mockStore(sqlLabReducer(undefined, mockAction));
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('is valid', () => { it('is valid', () => {
expect(React.isValidElement(<App />)).toBe(true); expect(React.isValidElement(<App />)).toBe(true);
@ -61,15 +58,13 @@ describe('SqlLab App', () => {
expect(getByTestId('mock-tabbed-sql-editors')).toBeInTheDocument(); 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 localStorageUsageInKilobytes = LOCALSTORAGE_MAX_USAGE_KB + 10;
const initialState = {
localStorageUsageInKilobytes,
};
const storeExceedLocalStorage = mockStore( const storeExceedLocalStorage = mockStore(
sqlLabReducer( sqlLabReducer(initialState, mockAction),
{
localStorageUsageInKilobytes,
},
{},
),
); );
const { rerender } = render(<App />, { 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 localStorageUsageInKilobytes = LOCALSTORAGE_MAX_USAGE_KB - 10;
const storeExceedLocalStorage = mockStore( const storeExceedLocalStorage = mockStore(
sqlLabReducer( sqlLabReducer(
{ {
localStorageUsageInKilobytes, localStorageUsageInKilobytes,
}, },
{}, mockAction,
), ),
); );

View File

@ -17,8 +17,6 @@
* under the License. * under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import { css, styled, t } from '@superset-ui/core'; import { css, styled, t } from '@superset-ui/core';
@ -28,7 +26,8 @@ import {
LOCALSTORAGE_WARNING_THRESHOLD, LOCALSTORAGE_WARNING_THRESHOLD,
LOCALSTORAGE_WARNING_MESSAGE_THROTTLE_MS, LOCALSTORAGE_WARNING_MESSAGE_THROTTLE_MS,
} from 'src/SqlLab/constants'; } 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 { logEvent } from 'src/logger/actions';
import { import {
LOG_ACTIONS_SQLLAB_WARN_LOCAL_STORAGE_USAGE, LOG_ACTIONS_SQLLAB_WARN_LOCAL_STORAGE_USAGE,
@ -100,8 +99,21 @@ const SqlLabStyles = styled.div`
`}; `};
`; `;
class App extends React.PureComponent { type PureProps = {
constructor(props) { // 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); super(props);
this.state = { this.state = {
hash: window.location.hash, hash: window.location.hash,
@ -125,7 +137,7 @@ class App extends React.PureComponent {
componentDidUpdate() { componentDidUpdate() {
const { localStorageUsageInKilobytes, actions, queries } = this.props; const { localStorageUsageInKilobytes, actions, queries } = this.props;
const queryCount = queries?.lenghth || 0; const queryCount = Object.keys(queries || {}).length || 0;
if ( if (
localStorageUsageInKilobytes >= localStorageUsageInKilobytes >=
LOCALSTORAGE_WARNING_THRESHOLD * LOCALSTORAGE_MAX_USAGE_KB LOCALSTORAGE_WARNING_THRESHOLD * LOCALSTORAGE_MAX_USAGE_KB
@ -159,7 +171,7 @@ class App extends React.PureComponent {
this.setState({ hash: window.location.hash }); this.setState({ hash: window.location.hash });
} }
showLocalStorageUsageWarning(currentUsage, queryCount) { showLocalStorageUsageWarning(currentUsage: number, queryCount: number) {
this.props.actions.addDangerToast( this.props.actions.addDangerToast(
t( t(
"SQL Lab uses your browser's local storage to store queries and results." + "SQL Lab uses your browser's local storage to store queries and results." +
@ -190,7 +202,6 @@ class App extends React.PureComponent {
<Redirect <Redirect
to={{ to={{
pathname: '/sqllab/history/', pathname: '/sqllab/history/',
replace: true,
}} }}
/> />
); );
@ -207,13 +218,7 @@ class App extends React.PureComponent {
} }
} }
App.propTypes = { function mapStateToProps(state: SqlLabRootState) {
actions: PropTypes.object,
common: PropTypes.object,
localStorageUsageInKilobytes: PropTypes.number.isRequired,
};
function mapStateToProps(state) {
const { common, localStorageUsageInKilobytes, sqlLab } = state; const { common, localStorageUsageInKilobytes, sqlLab } = state;
return { return {
common, 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 { return {
actions: bindActionCreators({ ...Actions, logEvent }, dispatch), ...state,
...stateProps,
actions: dispatchProps,
}; };
} }
export default connect(mapStateToProps, mapDispatchToProps)(App); export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(App);

View File

@ -103,7 +103,7 @@ import SaveQuery, { QueryPayload } from '../SaveQuery';
import ScheduleQueryButton from '../ScheduleQueryButton'; import ScheduleQueryButton from '../ScheduleQueryButton';
import EstimateQueryCostButton from '../EstimateQueryCostButton'; import EstimateQueryCostButton from '../EstimateQueryCostButton';
import ShareSqlLabQuery from '../ShareSqlLabQuery'; import ShareSqlLabQuery from '../ShareSqlLabQuery';
import SqlEditorLeftBar, { ExtendedTable } from '../SqlEditorLeftBar'; import SqlEditorLeftBar from '../SqlEditorLeftBar';
import AceEditorWrapper from '../AceEditorWrapper'; import AceEditorWrapper from '../AceEditorWrapper';
import RunQueryActionButton from '../RunQueryActionButton'; import RunQueryActionButton from '../RunQueryActionButton';
import QueryLimitSelect from '../QueryLimitSelect'; import QueryLimitSelect from '../QueryLimitSelect';
@ -215,7 +215,6 @@ const StyledSqlEditor = styled.div`
const extensionsRegistry = getExtensionsRegistry(); const extensionsRegistry = getExtensionsRegistry();
export type Props = { export type Props = {
tables: ExtendedTable[];
queryEditor: QueryEditor; queryEditor: QueryEditor;
defaultQueryLimit: number; defaultQueryLimit: number;
maxRow: number; maxRow: number;
@ -235,7 +234,6 @@ const elementStyle = (
}); });
const SqlEditor: React.FC<Props> = ({ const SqlEditor: React.FC<Props> = ({
tables,
queryEditor, queryEditor,
defaultQueryLimit, defaultQueryLimit,
maxRow, maxRow,
@ -839,7 +837,6 @@ const SqlEditor: React.FC<Props> = ({
<SqlEditorLeftBar <SqlEditorLeftBar
database={database} database={database}
queryEditorId={queryEditor.id} queryEditorId={queryEditor.id}
tables={tables}
setEmptyState={bool => setShowEmptyState(bool)} setEmptyState={bool => setShowEmptyState(bool)}
/> />
</StyledSidebar> </StyledSidebar>

View File

@ -20,16 +20,19 @@ import React from 'react';
import fetchMock from 'fetch-mock'; import fetchMock from 'fetch-mock';
import { render, screen, waitFor } from 'spec/helpers/testing-library'; import { render, screen, waitFor } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux'; import SqlEditorLeftBar, {
import '@testing-library/jest-dom/extend-expect'; SqlEditorLeftBarProps,
import SqlEditorLeftBar from 'src/SqlLab/components/SqlEditorLeftBar'; } from 'src/SqlLab/components/SqlEditorLeftBar';
import { table, initialState, defaultQueryEditor } from 'src/SqlLab/fixtures'; import {
import { api } from 'src/hooks/apiResources/queryApi'; table,
import { setupStore } from 'src/views/store'; initialState,
import reducers from 'spec/helpers/reducerIndex'; defaultQueryEditor,
extraQueryEditor1,
} from 'src/SqlLab/fixtures';
import type { RootState } from 'src/views/store';
import type { Store } from 'redux';
const mockedProps = { const mockedProps = {
tables: [table],
queryEditorId: defaultQueryEditor.id, queryEditorId: defaultQueryEditor.id,
database: { database: {
id: 1, id: 1,
@ -39,115 +42,117 @@ const mockedProps = {
height: 0, 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(() => { beforeEach(() => {
store = createStore(initialState);
actions = [];
fetchMock.get('glob:*/api/v1/database/?*', { result: [] }); fetchMock.get('glob:*/api/v1/database/?*', { result: [] });
fetchMock.get('glob:*/api/v1/database/*/schemas/?*', { fetchMock.get('glob:*/api/v1/database/*/schemas/?*', {
count: 2, count: 2,
result: ['main', 'new_schema'], result: ['main', 'new_schema'],
}); });
fetchMock.get('glob:*/api/v1/database/*/tables/*', { fetchMock.get('glob:*/api/v1/database/*/tables/*', {
count: 1, count: 2,
result: [ result: [
{ {
label: 'ab_user', label: 'ab_user',
value: '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(() => { afterEach(() => {
fetchMock.restore(); fetchMock.restore();
store.dispatch(api.util.resetApiState());
jest.clearAllMocks(); jest.clearAllMocks();
}); });
const renderAndWait = (props, store) => const renderAndWait = (
props: SqlEditorLeftBarProps,
store?: Store,
initialState?: RootState,
) =>
waitFor(() => waitFor(() =>
render(<SqlEditorLeftBar {...props} />, { render(<SqlEditorLeftBar {...props} />, {
useRedux: true, useRedux: true,
initialState,
...(store && { store }), ...(store && { store }),
}), }),
); );
test('is valid', () => {
expect(
React.isValidElement(
<Provider store={store}>
<SqlEditorLeftBar {...mockedProps} />
</Provider>,
),
).toBe(true);
});
test('renders a TableElement', async () => { test('renders a TableElement', async () => {
await renderAndWait(mockedProps, store); const { findByText, getAllByTestId } = await renderAndWait(
expect(await screen.findByText(/Database/i)).toBeInTheDocument(); mockedProps,
const tableElement = screen.getAllByTestId('table-element'); undefined,
{ ...initialState, sqlLab: { ...initialState.sqlLab, tables: [table] } },
);
expect(await findByText(/Database/i)).toBeInTheDocument();
const tableElement = getAllByTestId('table-element');
expect(tableElement.length).toBeGreaterThanOrEqual(1); expect(tableElement.length).toBeGreaterThanOrEqual(1);
}); });
test('table should be visible when expanded is true', async () => { 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', 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', name: 'Select schema or type to search schemas',
}); });
const dropdown = screen.getByText(/Table/i); const dropdown = getByText(/Table/i);
const abUser = screen.queryAllByText(/ab_user/i); const abUser = queryAllByText(/ab_user/i);
await waitFor(() => { expect(getByText(/Database/i)).toBeInTheDocument();
expect(screen.getByText(/Database/i)).toBeInTheDocument(); expect(dbSelect).toBeInTheDocument();
expect(dbSelect).toBeInTheDocument(); expect(schemaSelect).toBeInTheDocument();
expect(schemaSelect).toBeInTheDocument(); expect(dropdown).toBeInTheDocument();
expect(dropdown).toBeInTheDocument(); expect(abUser).toHaveLength(2);
expect(abUser).toHaveLength(2); expect(
expect( container.querySelector('.ant-collapse-content-active'),
container.querySelector('.ant-collapse-content-active'), ).toBeInTheDocument();
).toBeInTheDocument(); table.columns.forEach(({ name }) => {
expect(getByText(name)).toBeInTheDocument();
}); });
}); });
test('should toggle the table when the header is clicked', async () => { 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(); expect(header).toBeInTheDocument();
userEvent.click(header); if (header) {
userEvent.click(header);
}
await waitFor(() => { await waitFor(() =>
expect(actions[actions.length - 1].type).toEqual('COLLAPSE_TABLE'); expect(
}); container.querySelector('.ant-collapse-content-inactive'),
).toBeInTheDocument(),
);
}); });
test('When changing database the table list must be updated', async () => { test('When changing database the table list must be updated', async () => {
store = createStore({ const { rerender } = await renderAndWait(mockedProps, undefined, {
...initialState, ...initialState,
sqlLab: { sqlLab: {
...initialState.sqlLab, ...initialState.sqlLab,
@ -155,9 +160,25 @@ test('When changing database the table list must be updated', async () => {
id: defaultQueryEditor.id, id: defaultQueryEditor.id,
schema: 'new_schema', 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(/main/i)[0]).toBeInTheDocument();
expect(screen.getAllByText(/ab_user/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', database_name: 'new_db',
backend: 'postgresql', backend: 'postgresql',
}} }}
queryEditorId={defaultQueryEditor.id} queryEditorId={extraQueryEditor1.id}
tables={[{ ...mockedProps.tables[0], dbId: 2, name: 'new_table' }]}
/>, />,
{
useRedux: true,
store,
},
); );
expect(await screen.findByText(/new_db/i)).toBeInTheDocument(); const updatedDbSelector = await screen.findAllByText(/new_db/i);
expect(await screen.findByText(/new_table/i)).toBeInTheDocument(); 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 () => { test('ignore schema api when current schema is deprecated', async () => {
const invalidSchemaName = 'None'; const invalidSchemaName = 'None';
store = createStore({ await renderAndWait(mockedProps, undefined, {
...initialState, ...initialState,
sqlLab: { sqlLab: {
...initialState.sqlLab, ...initialState.sqlLab,
@ -192,9 +210,9 @@ test('ignore schema api when current schema is deprecated', async () => {
id: defaultQueryEditor.id, id: defaultQueryEditor.id,
schema: invalidSchemaName, schema: invalidSchemaName,
}, },
tables: [table],
}, },
}); });
const { rerender } = await renderAndWait(mockedProps, store);
expect(await screen.findByText(/Database/i)).toBeInTheDocument(); expect(await screen.findByText(/Database/i)).toBeInTheDocument();
expect(fetchMock.calls()).not.toContainEqual( 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 // Deselect the deprecated schema selection
await waitFor(() => await waitFor(() =>
expect(screen.queryByText(/None/i)).not.toBeInTheDocument(), expect(screen.queryByText(/None/i)).not.toBeInTheDocument(),

View File

@ -24,10 +24,10 @@ import React, {
Dispatch, Dispatch,
SetStateAction, SetStateAction,
} from 'react'; } from 'react';
import { useDispatch } from 'react-redux'; import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import querystring from 'query-string'; import querystring from 'query-string';
import { Table } from 'src/SqlLab/types'; import { SqlLabRootState, Table } from 'src/SqlLab/types';
import { import {
queryEditorSetDb, queryEditorSetDb,
addTable, addTable,
@ -55,16 +55,11 @@ import {
} from 'src/utils/localStorageHelpers'; } from 'src/utils/localStorageHelpers';
import TableElement from '../TableElement'; import TableElement from '../TableElement';
export interface ExtendedTable extends Table { export interface SqlEditorLeftBarProps {
expanded: boolean;
}
interface SqlEditorLeftBarProps {
queryEditorId: string; queryEditorId: string;
height?: number; height?: number;
tables?: ExtendedTable[];
database?: DatabaseObject; database?: DatabaseObject;
setEmptyState: Dispatch<SetStateAction<boolean>>; setEmptyState?: Dispatch<SetStateAction<boolean>>;
} }
const StyledScrollbarContainer = styled.div` const StyledScrollbarContainer = styled.div`
@ -111,10 +106,14 @@ const LeftBarStyles = styled.div`
const SqlEditorLeftBar = ({ const SqlEditorLeftBar = ({
database, database,
queryEditorId, queryEditorId,
tables = [],
height = 500, height = 500,
setEmptyState, setEmptyState,
}: SqlEditorLeftBarProps) => { }: SqlEditorLeftBarProps) => {
const tables = useSelector<SqlLabRootState, Table[]>(
({ sqlLab }) =>
sqlLab.tables.filter(table => table.queryEditorId === queryEditorId),
shallowEqual,
);
const dispatch = useDispatch(); const dispatch = useDispatch();
const queryEditor = useQueryEditor(queryEditorId, ['dbId', 'schema']); const queryEditor = useQueryEditor(queryEditorId, ['dbId', 'schema']);
@ -144,7 +143,7 @@ const SqlEditorLeftBar = ({
}; };
const onDbChange = ({ id: dbId }: { id: number }) => { const onDbChange = ({ id: dbId }: { id: number }) => {
setEmptyState(false); setEmptyState?.(false);
dispatch(queryEditorSetDb(queryEditor, dbId)); dispatch(queryEditorSetDb(queryEditor, dbId));
}; };
@ -177,7 +176,7 @@ const SqlEditorLeftBar = ({
}; };
const onToggleTable = (updatedTables: string[]) => { const onToggleTable = (updatedTables: string[]) => {
tables.forEach((table: ExtendedTable) => { tables.forEach(table => {
if (!updatedTables.includes(table.id.toString()) && table.expanded) { if (!updatedTables.includes(table.id.toString()) && table.expanded) {
dispatch(collapseTable(table)); dispatch(collapseTable(table));
} else if ( } else if (

View File

@ -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);
});
});

View File

@ -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();
});

View File

@ -18,11 +18,10 @@
*/ */
import React from 'react'; import React from 'react';
import { pick } from 'lodash'; import { pick } from 'lodash';
import PropTypes from 'prop-types';
import { EditableTabs } from 'src/components/Tabs'; import { EditableTabs } from 'src/components/Tabs';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import URI from 'urijs'; import URI from 'urijs';
import type { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
import { FeatureFlag, styled, t, isFeatureEnabled } from '@superset-ui/core'; import { FeatureFlag, styled, t, isFeatureEnabled } from '@superset-ui/core';
import { Tooltip } from 'src/components/Tooltip'; import { Tooltip } from 'src/components/Tooltip';
import { detectOS } from 'src/utils/common'; import { detectOS } from 'src/utils/common';
@ -33,22 +32,7 @@ import { locationContext } from 'src/pages/SqlLab/LocationContext';
import SqlEditor from '../SqlEditor'; import SqlEditor from '../SqlEditor';
import SqlEditorTabHeader from '../SqlEditorTabHeader'; import SqlEditorTabHeader from '../SqlEditorTabHeader';
const propTypes = { const DEFAULT_PROPS = {
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 = {
queryEditors: [], queryEditors: [],
offline: false, offline: false,
saveQueryWarning: null, saveQueryWarning: null,
@ -73,15 +57,14 @@ const TabTitle = styled.span`
// Get the user's OS // Get the user's OS
const userOS = detectOS(); const userOS = detectOS();
class TabbedSqlEditors extends React.PureComponent { type TabbedSqlEditorsProps = ReturnType<typeof mergeProps>;
constructor(props) {
const SQL_LAB_URL = '/sqllab';
class TabbedSqlEditors extends React.PureComponent<TabbedSqlEditorsProps> {
constructor(props: TabbedSqlEditorsProps) {
super(props); super(props);
const sqlLabUrl = '/sqllab';
this.state = {
sqlLabUrl,
};
this.removeQueryEditor = this.removeQueryEditor.bind(this); this.removeQueryEditor = this.removeQueryEditor.bind(this);
this.duplicateQueryEditor = this.duplicateQueryEditor.bind(this);
this.handleSelect = this.handleSelect.bind(this); this.handleSelect = this.handleSelect.bind(this);
this.handleEdit = this.handleEdit.bind(this); this.handleEdit = this.handleEdit.bind(this);
} }
@ -136,7 +119,7 @@ class TabbedSqlEditors extends React.PureComponent {
...this.context.requestedQuery, ...this.context.requestedQuery,
...bootstrapData.requested_query, ...bootstrapData.requested_query,
...queryParameters, ...queryParameters,
}; } as Record<string, string>;
// Popping a new tab based on the querystring // Popping a new tab based on the querystring
if (id || sql || savedQueryId || datasourceKey || queryId) { if (id || sql || savedQueryId || datasourceKey || queryId) {
@ -149,7 +132,7 @@ class TabbedSqlEditors extends React.PureComponent {
} else if (datasourceKey) { } else if (datasourceKey) {
this.props.actions.popDatasourceQuery(datasourceKey, sql); this.props.actions.popDatasourceQuery(datasourceKey, sql);
} else if (sql) { } else if (sql) {
let databaseId = dbid; let databaseId: string | number = dbid;
if (databaseId) { if (databaseId) {
databaseId = parseInt(databaseId, 10); databaseId = parseInt(databaseId, 10);
} else { } else {
@ -177,11 +160,11 @@ class TabbedSqlEditors extends React.PureComponent {
this.newQueryEditor(); this.newQueryEditor();
if (isNewQuery) { if (isNewQuery) {
window.history.replaceState({}, document.title, this.state.sqlLabUrl); window.history.replaceState({}, document.title, SQL_LAB_URL);
} }
} else { } else {
const qe = this.activeQueryEditor(); const qe = this.activeQueryEditor();
const latestQuery = this.props.queries[qe.latestQueryId]; const latestQuery = this.props.queries[qe?.latestQueryId || ''];
if ( if (
isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) && isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) &&
latestQuery && latestQuery &&
@ -197,9 +180,9 @@ class TabbedSqlEditors extends React.PureComponent {
} }
} }
popNewTab(urlParams) { popNewTab(urlParams: Record<string, string>) {
// Clean the url in browser history // 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); window.history.replaceState({}, document.title, updatedUrl);
} }
@ -215,7 +198,7 @@ class TabbedSqlEditors extends React.PureComponent {
this.props.actions.addNewQueryEditor(); this.props.actions.addNewQueryEditor();
} }
handleSelect(key) { handleSelect(key: string) {
const qeid = this.props.tabHistory[this.props.tabHistory.length - 1]; const qeid = this.props.tabHistory[this.props.tabHistory.length - 1];
if (key !== qeid) { if (key !== qeid) {
const queryEditor = this.props.queryEditors.find(qe => qe.id === key); 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') { if (action === 'remove') {
const qe = this.props.queryEditors.find(qe => qe.id === key); const qe = this.props.queryEditors.find(qe => qe.id === key);
this.removeQueryEditor(qe); if (qe) {
this.removeQueryEditor(qe);
}
} }
if (action === 'add') { if (action === 'add') {
this.newQueryEditor(); this.newQueryEditor();
} }
} }
removeQueryEditor(qe) { removeQueryEditor(qe: QueryEditor) {
this.props.actions.removeQueryEditor(qe); this.props.actions.removeQueryEditor(qe);
} }
duplicateQueryEditor(qe) {
this.props.actions.cloneQueryToNewTab(qe, false);
}
render() { render() {
const noQueryEditors = this.props.queryEditors?.length === 0; const noQueryEditors = this.props.queryEditors?.length === 0;
const editors = this.props.queryEditors?.map(qe => ( const editors = this.props.queryEditors?.map(qe => (
@ -257,7 +238,6 @@ class TabbedSqlEditors extends React.PureComponent {
data-key={qe.id} data-key={qe.id}
> >
<SqlEditor <SqlEditor
tables={this.props.tables.filter(xt => xt.queryEditorId === qe.id)}
queryEditor={qe} queryEditor={qe}
defaultQueryLimit={this.props.defaultQueryLimit} defaultQueryLimit={this.props.defaultQueryLimit}
maxRow={this.props.maxRow} maxRow={this.props.maxRow}
@ -332,30 +312,45 @@ class TabbedSqlEditors extends React.PureComponent {
); );
} }
} }
TabbedSqlEditors.propTypes = propTypes;
TabbedSqlEditors.defaultProps = defaultProps;
TabbedSqlEditors.contextType = locationContext; TabbedSqlEditors.contextType = locationContext;
function mapStateToProps({ sqlLab, common }) { export function mapStateToProps({ sqlLab, common }: SqlLabRootState) {
return { return {
databases: sqlLab.databases, databases: sqlLab.databases,
queryEditors: sqlLab.queryEditors, queryEditors: sqlLab.queryEditors ?? DEFAULT_PROPS.queryEditors,
queries: sqlLab.queries, queries: sqlLab.queries,
tabHistory: sqlLab.tabHistory, tabHistory: sqlLab.tabHistory,
tables: sqlLab.tables, tables: sqlLab.tables,
defaultDbId: common.conf.SQLLAB_DEFAULT_DBID, defaultDbId: common.conf.SQLLAB_DEFAULT_DBID,
displayLimit: common.conf.DISPLAY_MAX_ROW, displayLimit: common.conf.DISPLAY_MAX_ROW,
offline: sqlLab.offline, offline: sqlLab.offline ?? DEFAULT_PROPS.offline,
defaultQueryLimit: common.conf.DEFAULT_SQLLAB_LIMIT, defaultQueryLimit: common.conf.DEFAULT_SQLLAB_LIMIT,
maxRow: common.conf.SQL_MAX_ROW, maxRow: common.conf.SQL_MAX_ROW,
saveQueryWarning: common.conf.SQLLAB_SAVE_WARNING_MESSAGE, saveQueryWarning:
scheduleQueryWarning: common.conf.SQLLAB_SCHEDULE_WARNING_MESSAGE, common.conf.SQLLAB_SAVE_WARNING_MESSAGE ?? DEFAULT_PROPS.saveQueryWarning,
}; scheduleQueryWarning:
} common.conf.SQLLAB_SCHEDULE_WARNING_MESSAGE ??
function mapDispatchToProps(dispatch) { DEFAULT_PROPS.scheduleQueryWarning,
return {
actions: bindActionCreators(Actions, dispatch),
}; };
} }
export default connect(mapStateToProps, mapDispatchToProps)(TabbedSqlEditors); const mapDispatchToProps = {
...Actions,
};
function mergeProps(
stateProps: ReturnType<typeof mapStateToProps>,
dispatchProps: typeof mapDispatchToProps,
) {
return {
...stateProps,
actions: dispatchProps,
};
}
export default connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
)(TabbedSqlEditors);

View File

@ -35,7 +35,7 @@ export const alert = { bsStyle: 'danger', msg: 'Ooops', id: 'lksvmcx32' };
export const table = { export const table = {
dbId: 1, dbId: 1,
selectStar: 'SELECT * FROM ab_user', selectStar: 'SELECT * FROM ab_user',
queryEditorId: 'rJ-KP47a', queryEditorId: 'dfsadfs',
schema: 'superset', schema: 'superset',
name: 'ab_user', name: 'ab_user',
id: 'r11Vgt60', id: 'r11Vgt60',

View File

@ -16,11 +16,15 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { JsonObject, QueryResponse } from '@superset-ui/core'; import { QueryResponse } from '@superset-ui/core';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; import {
CommonBootstrapData,
UserWithPermissionsAndRoles,
} from 'src/types/bootstrapTypes';
import { ToastType } from 'src/components/MessageToasts/types'; import { ToastType } from 'src/components/MessageToasts/types';
import { DropdownButtonProps } from 'src/components/DropdownButton'; import { DropdownButtonProps } from 'src/components/DropdownButton';
import { ButtonProps } from 'src/components/Button'; import { ButtonProps } from 'src/components/Button';
import type { TableMetaData } from 'src/hooks/apiResources';
export type QueryButtonProps = DropdownButtonProps | ButtonProps; export type QueryButtonProps = DropdownButtonProps | ButtonProps;
@ -81,8 +85,10 @@ export interface Table {
name: string; name: string;
queryEditorId: QueryEditor['id']; queryEditorId: QueryEditor['id'];
dataPreviewQueryId: string | null; dataPreviewQueryId: string | null;
expanded?: boolean; expanded: boolean;
initialized?: boolean; initialized?: boolean;
inLocalStorage?: boolean;
persistData?: TableMetaData;
} }
export type SqlLabRootState = { export type SqlLabRootState = {
@ -92,7 +98,7 @@ export type SqlLabRootState = {
databases: Record<string, any>; databases: Record<string, any>;
dbConnect: boolean; dbConnect: boolean;
offline: boolean; offline: boolean;
queries: Record<string, QueryResponse>; queries: Record<string, QueryResponse & { inLocalStorage?: boolean }>;
queryEditors: QueryEditor[]; queryEditors: QueryEditor[];
tabHistory: string[]; // default is activeTab ? [activeTab.id.toString()] : [] tabHistory: string[]; // default is activeTab ? [activeTab.id.toString()] : []
tables: Table[]; tables: Table[];
@ -105,10 +111,7 @@ export type SqlLabRootState = {
localStorageUsageInKilobytes: number; localStorageUsageInKilobytes: number;
messageToasts: toastState[]; messageToasts: toastState[];
user: UserWithPermissionsAndRoles; user: UserWithPermissionsAndRoles;
common: { common: CommonBootstrapData;
flash_messages: string[];
conf: JsonObject;
};
}; };
export enum DatasetRadioState { export enum DatasetRadioState {

View File

@ -16,6 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import type { QueryResponse } from '@superset-ui/core';
import { import {
emptyQueryResults, emptyQueryResults,
clearQueryEditors, clearQueryEditors,
@ -43,7 +44,7 @@ describe('reduxStateToLocalStorageHelper', () => {
expect(Date.now() - startDttm).toBeGreaterThan( expect(Date.now() - startDttm).toBeGreaterThan(
LOCALSTORAGE_MAX_QUERY_AGE_MS, LOCALSTORAGE_MAX_QUERY_AGE_MS,
); );
expect(Object.keys(oldQuery.results)).toContain('data'); expect(Object.keys(oldQuery.results || {})).toContain('data');
const emptiedQuery = emptyQueryResults(queriesObj); const emptiedQuery = emptyQueryResults(queriesObj);
expect(emptiedQuery[id].startDttm).toBe(startDttm); expect(emptiedQuery[id].startDttm).toBe(startDttm);
@ -55,7 +56,7 @@ describe('reduxStateToLocalStorageHelper', () => {
...queries[0], ...queries[0],
startDttm: Date.now(), startDttm: Date.now(),
results: { data: [{ a: 1 }] }, results: { data: [{ a: 1 }] },
}; } as unknown as QueryResponse;
const largeQuery = { const largeQuery = {
...queries[1], ...queries[1],
startDttm: Date.now(), startDttm: Date.now(),
@ -70,7 +71,7 @@ describe('reduxStateToLocalStorageHelper', () => {
}, },
], ],
}, },
}; } as unknown as QueryResponse;
expect(Object.keys(largeQuery.results)).toContain('data'); expect(Object.keys(largeQuery.results)).toContain('data');
const emptiedQuery = emptyQueryResults({ const emptiedQuery = emptyQueryResults({
[reasonableSizeQuery.id]: reasonableSizeQuery, [reasonableSizeQuery.id]: reasonableSizeQuery,

View File

@ -16,6 +16,9 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * 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 { pick } from 'lodash';
import { tableApiUtil } from 'src/hooks/apiResources/tables'; import { tableApiUtil } from 'src/hooks/apiResources/tables';
import { import {
@ -44,7 +47,7 @@ const PERSISTENT_QUERY_EDITOR_KEYS = new Set([
'hideLeftBar', 'hideLeftBar',
]); ]);
function shouldEmptyQueryResults(query) { function shouldEmptyQueryResults(query: QueryResponse) {
const { startDttm, results } = query; const { startDttm, results } = query;
return ( return (
Date.now() - startDttm > LOCALSTORAGE_MAX_QUERY_AGE_MS || 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 return tables
.map(table => .map(table =>
pick(table, [ pick(table, [
@ -68,7 +71,9 @@ export function emptyTablePersistData(tables) {
.filter(({ queryEditorId }) => Boolean(queryEditorId)); .filter(({ queryEditorId }) => Boolean(queryEditorId));
} }
export function emptyQueryResults(queries) { export function emptyQueryResults(
queries: SqlLabRootState['sqlLab']['queries'],
) {
return Object.keys(queries).reduce((accu, key) => { return Object.keys(queries).reduce((accu, key) => {
const { results } = queries[key]; const { results } = queries[key];
const query = { const query = {
@ -84,7 +89,7 @@ export function emptyQueryResults(queries) {
}, {}); }, {});
} }
export function clearQueryEditors(queryEditors) { export function clearQueryEditors(queryEditors: QueryEditor[]) {
return queryEditors.map(editor => return queryEditors.map(editor =>
// only return selected keys // only return selected keys
Object.keys(editor) 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 // Rehydrate server side persisted table metadata
state.sqlLab.tables.forEach(({ name: table, schema, dbId, persistData }) => { state.sqlLab.tables.forEach(({ name: table, schema, dbId, persistData }) => {
if (dbId && schema && table && persistData?.columns) { if (dbId && schema && table && persistData?.columns) {

View File

@ -29,7 +29,7 @@ export enum EmptyStateSize {
} }
export interface EmptyStateSmallProps { export interface EmptyStateSmallProps {
title: ReactNode; title?: ReactNode;
description?: ReactNode; description?: ReactNode;
image?: ReactNode; image?: ReactNode;
} }

View File

@ -160,6 +160,7 @@ export interface BootstrapData {
embedded?: { embedded?: {
dashboard_id: string; dashboard_id: string;
}; };
requested_query?: JsonObject;
} }
export function isUser(user: any): user is User { export function isUser(user: any): user is User {