chore: Remove actions prop and refactor code in SQL Lab (#22231)

This commit is contained in:
EugeneTorap 2022-12-05 14:12:52 +03:00 committed by GitHub
parent b2d909f529
commit f3bf3ec2ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 127 additions and 247 deletions

View File

@ -296,6 +296,7 @@ export type Query = {
errorMessage: string | null;
extra: {
progress: string | null;
errors?: SupersetError[];
};
id: string;
isDataPreview: boolean;

View File

@ -20,16 +20,8 @@ import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import QueryHistory from 'src/SqlLab/components/QueryHistory';
const NOOP = () => {};
const mockedProps = {
queries: [],
actions: {
queryEditorSetAndSaveSql: NOOP,
cloneQueryToNewTab: NOOP,
fetchQueryResults: NOOP,
clearQueryResults: NOOP,
removeQuery: NOOP,
},
displayLimit: 1000,
latestQueryId: 'yhMUZCGb',
};

View File

@ -23,13 +23,6 @@ import QueryTable from 'src/SqlLab/components/QueryTable';
interface QueryHistoryProps {
queries: QueryResponse[];
actions: {
queryEditorSetAndSaveSql: Function;
cloneQueryToNewTab: Function;
fetchQueryResults: Function;
clearQueryResults: Function;
removeQuery: Function;
};
displayLimit: number;
latestQueryId: string | undefined;
}
@ -47,7 +40,6 @@ const StyledEmptyStateWrapper = styled.div`
const QueryHistory = ({
queries,
actions,
displayLimit,
latestQueryId,
}: QueryHistoryProps) =>
@ -64,7 +56,6 @@ const QueryHistory = ({
'actions',
]}
queries={queries}
actions={actions}
displayLimit={displayLimit}
latestQueryId={latestQueryId}
/>

View File

@ -43,7 +43,6 @@ fetchMock.get(DATABASE_ENDPOINT, []);
describe('QuerySearch', () => {
const mockedProps = {
actions: { addDangerToast: jest.fn() },
displayLimit: 50,
};

View File

@ -17,6 +17,9 @@
* under the License.
*/
import React, { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { setDatabases, addDangerToast } from 'src/SqlLab/actions/sqlLab';
import Button from 'src/components/Button';
import Select from 'src/components/DeprecatedSelect';
import { styled, t, SupersetClient, QueryResponse } from '@superset-ui/core';
@ -33,15 +36,6 @@ import { STATUS_OPTIONS, TIME_OPTIONS } from 'src/SqlLab/constants';
import QueryTable from '../QueryTable';
interface QuerySearchProps {
actions: {
addDangerToast: (msg: string) => void;
setDatabases: (data: Record<string, any>) => Record<string, any>;
queryEditorSetAndSaveSql: Function;
cloneQueryToNewTab: Function;
fetchQueryResults: Function;
clearQueryResults: Function;
removeQuery: Function;
};
displayLimit: number;
}
@ -77,7 +71,10 @@ const TableStyles = styled.div`
const StyledTableStylesContainer = styled.div`
overflow: auto;
`;
function QuerySearch({ actions, displayLimit }: QuerySearchProps) {
const QuerySearch = ({ displayLimit }: QuerySearchProps) => {
const dispatch = useDispatch();
const [databaseId, setDatabaseId] = useState<string>('');
const [userId, setUserId] = useState<string>('');
const [searchText, setSearchText] = useState<string>('');
@ -133,7 +130,7 @@ function QuerySearch({ actions, displayLimit }: QuerySearchProps) {
const queries = Object.values(response.json);
setQueriesArray(queries);
} catch (err) {
actions.addDangerToast(t('An error occurred when refreshing queries'));
dispatch(addDangerToast(t('An error occurred when refreshing queries')));
} finally {
setQueriesLoading(false);
}
@ -178,10 +175,10 @@ function QuerySearch({ actions, displayLimit }: QuerySearchProps) {
value: id,
label: database_name,
}));
actions.setDatabases(result);
dispatch(setDatabases(result));
if (result.length === 0) {
actions.addDangerToast(
t("It seems you don't have access to any database"),
dispatch(
addDangerToast(t("It seems you don't have access to any database")),
);
}
return options;
@ -280,7 +277,6 @@ function QuerySearch({ actions, displayLimit }: QuerySearchProps) {
onUserClicked={onUserClicked}
onDbClicked={onDbClicked}
queries={queriesArray}
actions={actions}
displayLimit={displayLimit}
/>
</TableStyles>
@ -288,5 +284,6 @@ function QuerySearch({ actions, displayLimit }: QuerySearchProps) {
</StyledTableStylesContainer>
</TableWrapper>
);
}
};
export default QuerySearch;

View File

@ -25,13 +25,11 @@ import TableView from 'src/components/TableView';
import TableCollection from 'src/components/TableCollection';
import { Provider } from 'react-redux';
import { queries, user } from 'src/SqlLab/fixtures';
import * as actions from 'src/SqlLab/actions/sqlLab';
describe('QueryTable', () => {
const mockedProps = {
queries,
displayLimit: 100,
actions,
latestQueryId: 'ryhMUZCGb',
};
it('is valid', () => {

View File

@ -22,7 +22,15 @@ import Card from 'src/components/Card';
import ProgressBar from 'src/components/ProgressBar';
import Label from 'src/components/Label';
import { t, useTheme, QueryResponse } from '@superset-ui/core';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import {
queryEditorSetAndSaveSql,
cloneQueryToNewTab,
fetchQueryResults,
clearQueryResults,
removeQuery,
} from 'src/SqlLab/actions/sqlLab';
import TableView from 'src/components/TableView';
import Button from 'src/components/Button';
import { fDuration } from 'src/utils/dates';
@ -45,13 +53,6 @@ interface QueryTableQuery
interface QueryTableProps {
columns?: string[];
actions: {
queryEditorSetAndSaveSql: Function;
cloneQueryToNewTab: Function;
fetchQueryResults: Function;
clearQueryResults: Function;
removeQuery: Function;
};
queries?: QueryResponse[];
onUserClicked?: Function;
onDbClicked?: Function;
@ -66,7 +67,6 @@ const openQuery = (id: number) => {
const QueryTable = ({
columns = ['started', 'duration', 'rows'],
actions,
queries = [],
onUserClicked = () => undefined,
onDbClicked = () => undefined,
@ -74,6 +74,7 @@ const QueryTable = ({
latestQueryId,
}: QueryTableProps) => {
const theme = useTheme();
const dispatch = useDispatch();
const setHeaders = (column: string) => {
if (column === 'sql') {
@ -93,25 +94,17 @@ const QueryTable = ({
const user = useSelector<SqlLabRootState, User>(state => state.sqlLab.user);
const {
queryEditorSetAndSaveSql,
cloneQueryToNewTab,
fetchQueryResults,
clearQueryResults,
removeQuery,
} = actions;
const data = useMemo(() => {
const restoreSql = (query: QueryResponse) => {
queryEditorSetAndSaveSql({ id: query.sqlEditorId }, query.sql);
dispatch(queryEditorSetAndSaveSql({ id: query.sqlEditorId }, query.sql));
};
const openQueryInNewTab = (query: QueryResponse) => {
cloneQueryToNewTab(query, true);
dispatch(cloneQueryToNewTab(query, true));
};
const openAsyncResults = (query: QueryResponse, displayLimit: number) => {
fetchQueryResults(query, displayLimit);
dispatch(fetchQueryResults(query, displayLimit));
};
const statusAttributes = {
@ -239,7 +232,7 @@ const QueryTable = ({
}
modalTitle={t('Data preview')}
beforeOpen={() => openAsyncResults(query, displayLimit)}
onExit={() => clearQueryResults(query)}
onExit={() => dispatch(clearQueryResults(query))}
modalBody={
<ResultSet
showSql
@ -293,7 +286,7 @@ const QueryTable = ({
{q.id !== latestQueryId && (
<StyledTooltip
tooltip={t('Remove query from log')}
onClick={() => removeQuery(query)}
onClick={() => dispatch(removeQuery(query))}
>
<Icons.Trash iconSize="xl" />
</StyledTooltip>
@ -303,19 +296,7 @@ const QueryTable = ({
return q;
})
.reverse();
}, [
queries,
onUserClicked,
onDbClicked,
user,
displayLimit,
actions,
clearQueryResults,
cloneQueryToNewTab,
fetchQueryResults,
queryEditorSetAndSaveSql,
removeQuery,
]);
}, [queries, onUserClicked, onDbClicked, user, displayLimit]);
return (
<div className="QueryTable">

View File

@ -23,15 +23,15 @@ import { DropdownButton } from 'src/components/DropdownButton';
import Button from 'src/components/Button';
import { DropdownButtonProps } from 'antd/lib/dropdown';
interface Props {
interface SaveDatasetActionButtonProps {
setShowSave: (arg0: boolean) => void;
overlayMenu: JSX.Element | null;
}
export default function SaveDatasetActionButton({
const SaveDatasetActionButton = ({
setShowSave,
overlayMenu,
}: Props) {
}: SaveDatasetActionButtonProps) => {
const theme = useTheme();
const StyledDropdownButton = styled(
@ -80,4 +80,6 @@ export default function SaveDatasetActionButton({
{t('Save')}
</StyledDropdownButton>
);
}
};
export default SaveDatasetActionButton;

View File

@ -19,47 +19,30 @@
import React from 'react';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { styledShallow as shallow } from 'spec/helpers/theming';
import { render, screen, act } from 'spec/helpers/testing-library';
import SouthPaneContainer from 'src/SqlLab/components/SouthPane/state';
import ResultSet from 'src/SqlLab/components/ResultSet';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import SouthPane from 'src/SqlLab/components/SouthPane';
import '@testing-library/jest-dom/extend-expect';
import { STATUS_OPTIONS } from 'src/SqlLab/constants';
import { initialState, table, defaultQueryEditor } from 'src/SqlLab/fixtures';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
const NOOP = () => {};
const mockedProps = {
queryEditorId: defaultQueryEditor.id,
latestQueryId: 'LCly_kkIN',
actions: {},
activeSouthPaneTab: '',
height: 1,
displayLimit: 1,
databases: {},
defaultQueryLimit: 100,
};
const mockedEmptyProps = {
queryEditorId: 'random_id',
latestQueryId: '',
actions: {
queryEditorSetAndSaveSql: NOOP,
cloneQueryToNewTab: NOOP,
fetchQueryResults: NOOP,
clearQueryResults: NOOP,
removeQuery: NOOP,
setActiveSouthPaneTab: NOOP,
},
activeSouthPaneTab: '',
height: 100,
databases: '',
displayLimit: 100,
user: UserWithPermissionsAndRoles,
defaultQueryLimit: 100,
};
const latestQueryProgressMsg = 'LATEST QUERY MESSAGE - LCly_kkIN';
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
const store = mockStore({
@ -84,6 +67,7 @@ const store = mockStore({
id: 'LCly_kkIN',
startDttm: Date.now(),
sqlEditorId: defaultQueryEditor.id,
extra: { progress: latestQueryProgressMsg },
},
lXJa7F9_r: {
cached: false,
@ -115,48 +99,40 @@ const store = mockStore({
},
},
});
const setup = (overrides = {}) => (
<SouthPaneContainer store={store} {...mockedProps} {...overrides} />
);
describe('SouthPane - Enzyme', () => {
const getWrapper = () => shallow(setup()).dive();
let wrapper;
it('should render offline when the state is offline', () => {
wrapper = getWrapper().dive();
wrapper.setProps({ offline: true });
expect(wrapper.childAt(0).text()).toBe(STATUS_OPTIONS.offline);
const setup = (props, store) =>
render(<SouthPane {...props} />, {
useRedux: true,
...(store && { store }),
});
it('should pass latest query down to ResultSet component', () => {
wrapper = getWrapper().dive();
expect(wrapper.find(ResultSet)).toExist();
// for editorQueries
expect(wrapper.find(ResultSet).first().props().query.id).toEqual(
mockedProps.latestQueryId,
);
// for dataPreviewQueries
expect(wrapper.find(ResultSet).last().props().query.id).toEqual(
'2g2_iRFMl',
);
});
});
describe('SouthPane', () => {
const renderAndWait = (props, store) =>
waitFor(async () => setup(props, store));
describe('SouthPane - RTL', () => {
const renderAndWait = overrides => {
const mounted = act(async () => {
render(setup(overrides));
});
return mounted;
};
it('Renders an empty state for results', async () => {
await renderAndWait(mockedEmptyProps);
await renderAndWait(mockedEmptyProps, store);
const emptyStateText = screen.getByText(/run a query to display results/i);
expect(emptyStateText).toBeVisible();
});
it('should render offline when the state is offline', async () => {
await renderAndWait(
mockedEmptyProps,
mockStore({
...initialState,
sqlLab: {
...initialState.sqlLab,
offline: true,
},
}),
);
expect(screen.getByText(STATUS_OPTIONS.offline)).toBeVisible();
});
it('should pass latest query down to ResultSet component', async () => {
await renderAndWait(mockedProps, store);
expect(screen.getByText(latestQueryProgressMsg)).toBeVisible();
});
});

View File

@ -17,16 +17,18 @@
* under the License.
*/
import React, { createRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import shortid from 'shortid';
import Alert from 'src/components/Alert';
import Tabs from 'src/components/Tabs';
import { EmptyStateMedium } from 'src/components/EmptyState';
import { t, styled } from '@superset-ui/core';
import { setActiveSouthPaneTab } from 'src/SqlLab/actions/sqlLab';
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import Label from 'src/components/Label';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import { SqlLabRootState } from 'src/SqlLab/types';
import QueryHistory from '../QueryHistory';
import ResultSet from '../ResultSet';
import {
@ -39,27 +41,13 @@ const TAB_HEIGHT = 140;
/*
editorQueries are queries executed by users passed from SqlEditor component
dataPrebiewQueries are all queries executed for preview of table data (from SqlEditorLeft)
dataPreviewQueries are all queries executed for preview of table data (from SqlEditorLeft)
*/
export interface SouthPanePropTypes {
export interface SouthPaneProps {
queryEditorId: string;
editorQueries: any[];
latestQueryId?: string;
dataPreviewQueries: any[];
actions: {
queryEditorSetAndSaveSql: Function;
cloneQueryToNewTab: Function;
fetchQueryResults: Function;
clearQueryResults: Function;
removeQuery: Function;
setActiveSouthPaneTab: Function;
};
activeSouthPaneTab?: string;
height: number;
databases: Record<string, any>;
offline?: boolean;
displayLimit: number;
user: UserWithPermissionsAndRoles;
defaultQueryLimit: number;
}
@ -111,23 +99,49 @@ const StyledEmptyStateWrapper = styled.div`
}
`;
export default function SouthPane({
editorQueries,
const SouthPane = ({
queryEditorId,
latestQueryId,
dataPreviewQueries,
actions,
activeSouthPaneTab = 'Results',
height,
databases,
offline = false,
displayLimit,
user,
defaultQueryLimit,
}: SouthPanePropTypes) {
}: SouthPaneProps) => {
const dispatch = useDispatch();
const { editorQueries, dataPreviewQueries, databases, offline, user } =
useSelector(({ sqlLab }: SqlLabRootState) => {
const { databases, offline, user, queries, tables } = sqlLab;
const dataPreviewQueries = tables
.filter(
({ dataPreviewQueryId, queryEditorId: qeId }) =>
dataPreviewQueryId &&
queryEditorId === qeId &&
queries[dataPreviewQueryId],
)
.map(({ name, dataPreviewQueryId }) => ({
...queries[dataPreviewQueryId],
tableName: name,
}));
const editorQueries = Object.values(queries).filter(
({ sqlEditorId }) => sqlEditorId === queryEditorId,
);
return {
editorQueries,
dataPreviewQueries,
databases,
offline: offline ?? false,
user,
};
});
const activeSouthPaneTab =
useSelector<SqlLabRootState, string>(
state => state.sqlLab.activeSouthPaneTab as string,
) ?? 'Results';
const innerTabContentHeight = height - TAB_HEIGHT;
const southPaneRef = createRef<HTMLDivElement>();
const switchTab = (id: string) => {
actions.setActiveSouthPaneTab(id);
dispatch(setActiveSouthPaneTab(id));
};
const renderOfflineStatus = () => (
<Label className="m-r-3" type={STATE_TYPE_MAP[STATUS_OPTIONS.offline]}>
@ -209,7 +223,12 @@ export default function SouthPane({
return offline ? (
renderOfflineStatus()
) : (
<StyledPane className="SouthPane" height={height} ref={southPaneRef}>
<StyledPane
data-test="south-pane"
className="SouthPane"
height={height}
ref={southPaneRef}
>
<Tabs
activeKey={activeSouthPaneTab}
className="SouthPaneTabs"
@ -224,7 +243,6 @@ export default function SouthPane({
<Tabs.TabPane tab={t('Query history')} key="History">
<QueryHistory
queries={editorQueries}
actions={actions}
displayLimit={displayLimit}
latestQueryId={latestQueryId}
/>
@ -233,4 +251,6 @@ export default function SouthPane({
</Tabs>
</StyledPane>
);
}
};
export default SouthPane;

View File

@ -1,61 +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 { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import * as Actions from 'src/SqlLab/actions/sqlLab';
import { SqlLabRootState } from 'src/SqlLab/types';
import SouthPane, { SouthPanePropTypes } from '.';
function mapStateToProps(
{ sqlLab }: SqlLabRootState,
{ queryEditorId }: SouthPanePropTypes,
) {
const { databases, activeSouthPaneTab, offline, user, queries, tables } =
sqlLab;
const dataPreviewQueries = tables
.filter(
({ dataPreviewQueryId, queryEditorId: qeId }) =>
dataPreviewQueryId &&
queryEditorId === qeId &&
queries[dataPreviewQueryId],
)
.map(({ name, dataPreviewQueryId }) => ({
...queries[dataPreviewQueryId],
tableName: name,
}));
const editorQueries = Object.values(queries).filter(
({ sqlEditorId }) => sqlEditorId === queryEditorId,
);
return {
editorQueries,
dataPreviewQueries,
activeSouthPaneTab,
databases,
offline,
user,
};
}
function mapDispatchToProps(dispatch: Dispatch) {
return {
actions: bindActionCreators<any, any>(Actions, dispatch),
};
}
export default connect<any>(mapStateToProps, mapDispatchToProps)(SouthPane);

View File

@ -30,14 +30,9 @@ import {
SQL_TOOLBAR_HEIGHT,
} from 'src/SqlLab/constants';
import AceEditorWrapper from 'src/SqlLab/components/AceEditorWrapper';
import ConnectedSouthPane from 'src/SqlLab/components/SouthPane/state';
import SouthPane from 'src/SqlLab/components/SouthPane';
import SqlEditor from 'src/SqlLab/components/SqlEditor';
import QueryProvider from 'src/views/QueryProvider';
import {
queryEditorSetFunctionNames,
queryEditorSetSelectedText,
queryEditorSetSchemaOptions,
} from 'src/SqlLab/actions/sqlLab';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
import {
initialState,
@ -97,13 +92,6 @@ const setup = (props = {}, store) =>
describe('SqlEditor', () => {
const mockedProps = {
actions: {
queryEditorSetFunctionNames,
queryEditorSetSelectedText,
queryEditorSetSchemaOptions,
addDangerToast: jest.fn(),
removeDataPreview: jest.fn(),
},
queryEditor: initialState.sqlLab.queryEditors[0],
latestQuery: queries[0],
tables: [table],
@ -238,7 +226,7 @@ describe('SqlEditor', () => {
await waitForComponentToPaint(wrapper);
const totalSize =
parseFloat(wrapper.find(AceEditorWrapper).props().height) +
wrapper.find(ConnectedSouthPane).props().height +
wrapper.find(SouthPane).props().height +
SQL_TOOLBAR_HEIGHT +
SQL_EDITOR_GUTTER_MARGIN * 2 +
SQL_EDITOR_GUTTER_HEIGHT;
@ -252,7 +240,7 @@ describe('SqlEditor', () => {
await waitForComponentToPaint(wrapper);
const totalSize =
parseFloat(wrapper.find(AceEditorWrapper).props().height) +
wrapper.find(ConnectedSouthPane).props().height +
wrapper.find(SouthPane).props().height +
SQL_TOOLBAR_HEIGHT +
SQL_EDITOR_GUTTER_MARGIN * 2 +
SQL_EDITOR_GUTTER_HEIGHT;

View File

@ -76,7 +76,7 @@ import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { EmptyStateBig } from 'src/components/EmptyState';
import { isEmpty } from 'lodash';
import TemplateParamsEditor from '../TemplateParamsEditor';
import ConnectedSouthPane from '../SouthPane/state';
import SouthPane from '../SouthPane';
import SaveQuery from '../SaveQuery';
import ScheduleQueryButton from '../ScheduleQueryButton';
import EstimateQueryCostButton from '../EstimateQueryCostButton';
@ -135,7 +135,6 @@ const StyledSidebar = styled.div`
`;
const propTypes = {
actions: PropTypes.object.isRequired,
tables: PropTypes.array.isRequired,
queryEditor: PropTypes.object.isRequired,
defaultQueryLimit: PropTypes.number.isRequired,
@ -146,7 +145,6 @@ const propTypes = {
};
const SqlEditor = ({
actions,
tables,
queryEditor,
defaultQueryLimit,
@ -617,10 +615,9 @@ const SqlEditor = ({
/>
{renderEditorBottomBar(hotkeys)}
</div>
<ConnectedSouthPane
<SouthPane
queryEditorId={queryEditor.id}
latestQueryId={latestQuery?.id}
actions={actions}
height={southPaneHeight}
displayLimit={displayLimit}
defaultQueryLimit={defaultQueryLimit}

View File

@ -264,7 +264,6 @@ class TabbedSqlEditors extends React.PureComponent {
<SqlEditor
tables={this.props.tables.filter(xt => xt.queryEditorId === qe.id)}
queryEditor={qe}
actions={this.props.actions}
defaultQueryLimit={this.props.defaultQueryLimit}
maxRow={this.props.maxRow}
displayLimit={this.props.displayLimit}

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { JsonObject, Query, QueryResponse } from '@superset-ui/core';
import { JsonObject, QueryResponse } from '@superset-ui/core';
import { SupersetError } from 'src/components/ErrorMessage/types';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import { ToastType } from 'src/components/MessageToasts/types';
@ -69,7 +69,7 @@ export type SqlLabRootState = {
databases: Record<string, any>;
dbConnect: boolean;
offline: boolean;
queries: Record<string, Query>;
queries: Record<string, QueryResponse>;
queryEditors: QueryEditor[];
tabHistory: string[]; // default is activeTab ? [activeTab.id.toString()] : []
tables: Record<string, any>[];