mirror of https://github.com/apache/superset.git
chore(sqllab): migrate to typescript
This commit is contained in:
parent
2cd7135a51
commit
81f74703e7
|
@ -328,6 +328,7 @@ export type Query = {
|
|||
actions: Record<string, any>;
|
||||
type: DatasourceType;
|
||||
columns: QueryColumn[];
|
||||
runAsync?: boolean;
|
||||
};
|
||||
|
||||
export type QueryResults = {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
@ -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);
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
const dropdown = getByText(/Table/i);
|
||||
const abUser = 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();
|
||||
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(),
|
|
@ -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 (
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
saveQueryWarning:
|
||||
common.conf.SQLLAB_SAVE_WARNING_MESSAGE ?? DEFAULT_PROPS.saveQueryWarning,
|
||||
scheduleQueryWarning:
|
||||
common.conf.SQLLAB_SCHEDULE_WARNING_MESSAGE ??
|
||||
DEFAULT_PROPS.scheduleQueryWarning,
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
|
@ -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',
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
|
@ -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) {
|
|
@ -29,7 +29,7 @@ export enum EmptyStateSize {
|
|||
}
|
||||
|
||||
export interface EmptyStateSmallProps {
|
||||
title: ReactNode;
|
||||
title?: ReactNode;
|
||||
description?: ReactNode;
|
||||
image?: ReactNode;
|
||||
}
|
||||
|
|
|
@ -160,6 +160,7 @@ export interface BootstrapData {
|
|||
embedded?: {
|
||||
dashboard_id: string;
|
||||
};
|
||||
requested_query?: JsonObject;
|
||||
}
|
||||
|
||||
export function isUser(user: any): user is User {
|
||||
|
|
Loading…
Reference in New Issue