diff --git a/superset-frontend/spec/helpers/testing-library.tsx b/superset-frontend/spec/helpers/testing-library.tsx index 25b0324fe1864..d489ec2deaf2a 100644 --- a/superset-frontend/spec/helpers/testing-library.tsx +++ b/superset-frontend/spec/helpers/testing-library.tsx @@ -47,7 +47,7 @@ type Options = Omit & { store?: Store; }; -export function createWrapper(options?: Options) { +function createWrapper(options?: Options) { const { useDnd, useRedux, diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx b/superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx index 7638003e9025c..0fd5c7d3e86d8 100644 --- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx +++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx @@ -48,7 +48,7 @@ jest.mock('src/components/AsyncAceEditor', () => ({ const setup = (queryEditor: QueryEditor, store?: Store) => render( void; onChange: (sql: string) => void; - queryEditorId: string; + queryEditor: QueryEditor; database: any; extendedTables?: Array<{ name: string; columns: any[] }>; height: string; @@ -61,7 +61,7 @@ const AceEditorWrapper = ({ autocomplete, onBlur = () => {}, onChange = () => {}, - queryEditorId, + queryEditor, database, extendedTables = [], height, @@ -69,17 +69,7 @@ const AceEditorWrapper = ({ }: AceEditorWrapperProps) => { const dispatch = useDispatch(); - const queryEditor = useQueryEditor(queryEditorId, [ - 'id', - 'dbId', - 'sql', - 'functionNames', - 'schemaOptions', - 'tableOptions', - 'validationResult', - 'schema', - ]); - const currentSql = queryEditor.sql ?? ''; + const { sql: currentSql } = queryEditor; const functionNames = queryEditor.functionNames ?? []; const schemas = queryEditor.schemaOptions ?? []; const tables = queryEditor.tableOptions ?? []; diff --git a/superset-frontend/src/SqlLab/components/QueryLimitSelect/QueryLimitSelect.test.tsx b/superset-frontend/src/SqlLab/components/QueryLimitSelect/QueryLimitSelect.test.tsx index 317a8d02ba51f..c640d5a9dfed7 100644 --- a/superset-frontend/src/SqlLab/components/QueryLimitSelect/QueryLimitSelect.test.tsx +++ b/superset-frontend/src/SqlLab/components/QueryLimitSelect/QueryLimitSelect.test.tsx @@ -51,7 +51,7 @@ const defaultQueryLimit = 100; const setup = (props?: Partial, store?: Store) => render( { const queryLimit = 10; const { getByText } = setup( { - queryEditorId: defaultQueryEditor.id, - }, - mockStore({ - ...initialState, - sqlLab: { - ...initialState.sqlLab, - queryEditors: [ - { - ...defaultQueryEditor, - queryLimit, - }, - ], + queryEditor: { + ...defaultQueryEditor, + queryLimit, }, - }), + }, + mockStore(initialState), ); expect(getByText(queryLimit)).toBeInTheDocument(); }); @@ -137,9 +129,7 @@ describe('QueryLimitSelect', () => { { type: 'QUERY_EDITOR_SET_QUERY_LIMIT', queryLimit: LIMIT_DROPDOWN[expectedIndex], - queryEditor: { - id: defaultQueryEditor.id, - }, + queryEditor: defaultQueryEditor, }, ]), ); diff --git a/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx b/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx index 886e139a98e5e..f438ebc59edc0 100644 --- a/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx +++ b/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx @@ -17,16 +17,16 @@ * under the License. */ import React from 'react'; -import { useDispatch } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import { styled, useTheme } from '@superset-ui/core'; import { AntdDropdown } from 'src/components'; import { Menu } from 'src/components/Menu'; import Icons from 'src/components/Icons'; +import { SqlLabRootState, QueryEditor } from 'src/SqlLab/types'; import { queryEditorSetQueryLimit } from 'src/SqlLab/actions/sqlLab'; -import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor'; export interface QueryLimitSelectProps { - queryEditorId: string; + queryEditor: QueryEditor; maxRow: number; defaultQueryLimit: number; } @@ -79,12 +79,19 @@ function renderQueryLimit( } const QueryLimitSelect = ({ - queryEditorId, + queryEditor, maxRow, defaultQueryLimit, }: QueryLimitSelectProps) => { - const queryEditor = useQueryEditor(queryEditorId, ['id', 'queryLimit']); - const queryLimit = queryEditor.queryLimit || defaultQueryLimit; + const queryLimit = useSelector( + ({ sqlLab: { unsavedQueryEditor } }) => { + const updatedQueryEditor = { + ...queryEditor, + ...(unsavedQueryEditor.id === queryEditor.id && unsavedQueryEditor), + }; + return updatedQueryEditor.queryLimit || defaultQueryLimit; + }, + ); const dispatch = useDispatch(); const setQueryLimit = (updatedQueryLimit: number) => dispatch(queryEditorSetQueryLimit(queryEditor, updatedQueryLimit)); diff --git a/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx b/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx index 7062ad694a2e2..189a5fd2f7ab9 100644 --- a/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx +++ b/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx @@ -41,13 +41,13 @@ jest.mock('src/components/Select/AsyncSelect', () => () => ( )); const defaultProps = { - queryEditorId: defaultQueryEditor.id, + queryEditor: defaultQueryEditor, allowAsync: false, dbId: 1, queryState: 'ready', - runQuery: () => {}, + runQuery: jest.fn(), selectedText: null, - stopQuery: () => {}, + stopQuery: jest.fn(), overlayCreateAsMenu: null, }; @@ -57,104 +57,95 @@ const setup = (props?: Partial, store?: Store) => ...(store && { store }), }); -it('renders a single Button', () => { - const { getByRole } = setup({}, mockStore(initialState)); - expect(getByRole('button')).toBeInTheDocument(); -}); +describe('RunQueryActionButton', () => { + beforeEach(() => { + defaultProps.runQuery.mockReset(); + defaultProps.stopQuery.mockReset(); + }); -it('renders a label for Run Query', () => { - const { getByText } = setup({}, mockStore(initialState)); - expect(getByText('Run')).toBeInTheDocument(); -}); + it('renders a single Button', () => { + const { getByRole } = setup({}, mockStore(initialState)); + expect(getByRole('button')).toBeInTheDocument(); + }); -it('renders a label for Selected Query', () => { - const { getByText } = setup( - {}, - mockStore({ - ...initialState, - sqlLab: { - ...initialState.sqlLab, - unsavedQueryEditor: { - id: defaultQueryEditor.id, - selectedText: 'select * from\n-- this is comment\nwhere', - }, - }, - }), - ); - expect(getByText('Run selection')).toBeInTheDocument(); -}); + it('renders a label for Run Query', () => { + const { getByText } = setup({}, mockStore(initialState)); + expect(getByText('Run')).toBeInTheDocument(); + }); -it('disable button when sql from unsaved changes is empty', () => { - const { getByRole } = setup( - {}, - mockStore({ - ...initialState, - sqlLab: { - ...initialState.sqlLab, - unsavedQueryEditor: { - id: defaultQueryEditor.id, - sql: '', + it('renders a label for Selected Query', () => { + const { getByText } = setup( + {}, + mockStore({ + ...initialState, + sqlLab: { + ...initialState.sqlLab, + unsavedQueryEditor: { + id: defaultQueryEditor.id, + selectedText: 'FROM', + }, }, - }, - }), - ); - const button = getByRole('button'); - expect(button).toBeDisabled(); -}); + }), + ); + expect(getByText('Run selection')).toBeInTheDocument(); + }); -it('disable button when selectedText only contains blank contents', () => { - const { getByRole } = setup( - {}, - mockStore({ - ...initialState, - sqlLab: { - ...initialState.sqlLab, - unsavedQueryEditor: { - id: defaultQueryEditor.id, - selectedText: '-- this is comment\n\n \t', + it('disable button when sql from unsaved changes is empty', () => { + const { getByRole } = setup( + {}, + mockStore({ + ...initialState, + sqlLab: { + ...initialState.sqlLab, + unsavedQueryEditor: { + id: defaultQueryEditor.id, + sql: '', + }, }, - }, - }), - ); - const button = getByRole('button'); - expect(button).toBeDisabled(); -}); + }), + ); + const button = getByRole('button'); + expect(button).toBeDisabled(); + }); -it('enable default button for unrelated unsaved changes', () => { - const { getByRole } = setup( - {}, - mockStore({ - ...initialState, - sqlLab: { - ...initialState.sqlLab, - unsavedQueryEditor: { - id: `${defaultQueryEditor.id}-other`, - sql: '', + it('enable default button for unrelated unsaved changes', () => { + const { getByRole } = setup( + {}, + mockStore({ + ...initialState, + sqlLab: { + ...initialState.sqlLab, + unsavedQueryEditor: { + id: `${defaultQueryEditor.id}-other`, + sql: '', + }, }, - }, - }), - ); - const button = getByRole('button'); - expect(button).toBeEnabled(); -}); + }), + ); + const button = getByRole('button'); + expect(button).toBeEnabled(); + }); -it('dispatch runQuery on click', async () => { - const runQuery = jest.fn(); - const { getByRole } = setup({ runQuery }, mockStore(initialState)); - const button = getByRole('button'); - expect(runQuery).toHaveBeenCalledTimes(0); - fireEvent.click(button); - await waitFor(() => expect(runQuery).toHaveBeenCalledTimes(1)); -}); + it('dispatch runQuery on click', async () => { + const { getByRole } = setup({}, mockStore(initialState)); + const button = getByRole('button'); + expect(defaultProps.runQuery).toHaveBeenCalledTimes(0); + fireEvent.click(button); + await waitFor(() => expect(defaultProps.runQuery).toHaveBeenCalledTimes(1)); + }); -it('dispatch stopQuery on click while running state', async () => { - const stopQuery = jest.fn(); - const { getByRole } = setup( - { queryState: 'running', stopQuery }, - mockStore(initialState), - ); - const button = getByRole('button'); - expect(stopQuery).toHaveBeenCalledTimes(0); - fireEvent.click(button); - await waitFor(() => expect(stopQuery).toHaveBeenCalledTimes(1)); + describe('on running state', () => { + it('dispatch stopQuery on click', async () => { + const { getByRole } = setup( + { queryState: 'running' }, + mockStore(initialState), + ); + const button = getByRole('button'); + expect(defaultProps.stopQuery).toHaveBeenCalledTimes(0); + fireEvent.click(button); + await waitFor(() => + expect(defaultProps.stopQuery).toHaveBeenCalledTimes(1), + ); + }); + }); }); diff --git a/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx b/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx index a85b4b6b7bb88..5cc453f5ee93e 100644 --- a/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx +++ b/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx @@ -24,11 +24,16 @@ import Button from 'src/components/Button'; import Icons from 'src/components/Icons'; import { DropdownButton } from 'src/components/DropdownButton'; import { detectOS } from 'src/utils/common'; -import { QueryButtonProps } from 'src/SqlLab/types'; -import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor'; +import { shallowEqual, useSelector } from 'react-redux'; +import { + QueryEditor, + SqlLabRootState, + QueryButtonProps, +} from 'src/SqlLab/types'; +import { getUpToDateQuery } from 'src/SqlLab/actions/sqlLab'; export interface Props { - queryEditorId: string; + queryEditor: QueryEditor; allowAsync: boolean; queryState?: string; runQuery: (c?: boolean) => void; @@ -81,21 +86,29 @@ const StyledButton = styled.span` } `; -const RunQueryActionButton: React.FC = ({ +const RunQueryActionButton = ({ allowAsync = false, - queryEditorId, + queryEditor, queryState, overlayCreateAsMenu, runQuery, stopQuery, -}) => { +}: Props) => { const theme = useTheme(); const userOS = detectOS(); - - const { selectedText, sql } = useQueryEditor(queryEditorId, [ - 'selectedText', - 'sql', - ]); + const { selectedText, sql } = useSelector< + SqlLabRootState, + Pick + >(rootState => { + const currentQueryEditor = getUpToDateQuery( + rootState, + queryEditor, + ) as unknown as QueryEditor; + return { + selectedText: currentQueryEditor.selectedText, + sql: currentQueryEditor.sql, + }; + }, shallowEqual); const shouldShowStopBtn = !!queryState && ['running', 'pending'].indexOf(queryState) > -1; @@ -104,10 +117,7 @@ const RunQueryActionButton: React.FC = ({ ? (DropdownButton as React.FC) : Button; - const sqlContent = selectedText || sql || ''; - const isDisabled = - !sqlContent || - !sqlContent.replace(/(\/\*[^*]*\*\/)|(\/\/[^*]*)|(--[^.].*)/gm, '').trim(); + const isDisabled = !sql || !sql.trim(); const stopButtonTooltipText = useMemo( () => diff --git a/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx b/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.jsx similarity index 89% rename from superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx rename to superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.jsx index f321a54ec4dbe..2a5fcf3eb79fd 100644 --- a/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx +++ b/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.jsx @@ -25,28 +25,15 @@ import SaveQuery from 'src/SqlLab/components/SaveQuery'; import { initialState, databases } from 'src/SqlLab/fixtures'; const mockedProps = { - queryEditorId: '123', + queryEditor: { + dbId: 1, + schema: 'main', + sql: 'SELECT * FROM t', + }, animation: false, database: databases.result[0], onUpdate: () => {}, onSave: () => {}, - saveQueryWarning: null, - columns: [], -}; - -const mockState = { - ...initialState, - sqlLab: { - ...initialState.sqlLab, - queryEditors: [ - { - id: mockedProps.queryEditorId, - dbId: 1, - schema: 'main', - sql: 'SELECT * FROM t', - }, - ], - }, }; const splitSaveBtnProps = { @@ -64,7 +51,7 @@ describe('SavedQuery', () => { it('renders a non-split save button when allows_virtual_table_explore is not enabled', () => { render(, { useRedux: true, - store: mockStore(mockState), + store: mockStore(initialState), }); const saveBtn = screen.getByRole('button', { name: /save/i }); @@ -75,7 +62,7 @@ describe('SavedQuery', () => { it('renders a save query modal when user clicks save button', () => { render(, { useRedux: true, - store: mockStore(mockState), + store: mockStore(initialState), }); const saveBtn = screen.getByRole('button', { name: /save/i }); @@ -91,7 +78,7 @@ describe('SavedQuery', () => { it('renders the save query modal UI', () => { render(, { useRedux: true, - store: mockStore(mockState), + store: mockStore(initialState), }); const saveBtn = screen.getByRole('button', { name: /save/i }); @@ -124,18 +111,16 @@ describe('SavedQuery', () => { }); it('renders a "save as new" and "update" button if query already exists', () => { - render(, { + const props = { + ...mockedProps, + queryEditor: { + ...mockedProps.query, + remoteId: '42', + }, + }; + render(, { useRedux: true, - store: mockStore({ - ...mockState, - sqlLab: { - ...mockState.sqlLab, - unsavedQueryEditor: { - id: mockedProps.queryEditorId, - remoteId: '42', - }, - }, - }), + store: mockStore(initialState), }); const saveBtn = screen.getByRole('button', { name: /save/i }); @@ -151,7 +136,7 @@ describe('SavedQuery', () => { it('renders a split save button when allows_virtual_table_explore is enabled', async () => { render(, { useRedux: true, - store: mockStore(mockState), + store: mockStore(initialState), }); await waitFor(() => { @@ -166,7 +151,7 @@ describe('SavedQuery', () => { it('renders a save dataset modal when user clicks "save dataset" menu item', async () => { render(, { useRedux: true, - store: mockStore(mockState), + store: mockStore(initialState), }); await waitFor(() => { @@ -185,7 +170,7 @@ describe('SavedQuery', () => { it('renders the save dataset modal UI', async () => { render(, { useRedux: true, - store: mockStore(mockState), + store: mockStore(initialState), }); await waitFor(() => { diff --git a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx index eab37eceb375f..38cd5625ecd68 100644 --- a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx +++ b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect } from 'react'; +import { useSelector, shallowEqual } from 'react-redux'; import { Row, Col } from 'src/components'; import { Input, TextArea } from 'src/components/Input'; import { t, styled } from '@superset-ui/core'; @@ -30,11 +31,10 @@ import { ISaveableDatasource, } from 'src/SqlLab/components/SaveDatasetModal'; import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils'; -import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor'; -import { QueryEditor } from 'src/SqlLab/types'; +import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types'; interface SaveQueryProps { - queryEditorId: string; + queryEditor: QueryEditor; columns: ISaveableDatasource['columns']; onSave: (arg0: QueryPayload) => void; onUpdate: (arg0: QueryPayload) => void; @@ -43,22 +43,30 @@ interface SaveQueryProps { } type QueryPayload = { - name: string; + autorun: boolean; + dbId: number; description?: string; id?: string; -} & Pick< - QueryEditor, - | 'autorun' - | 'dbId' - | 'schema' - | 'sql' - | 'selectedText' - | 'remoteId' - | 'latestQueryId' - | 'queryLimit' - | 'tableOptions' - | 'schemaOptions' ->; + latestQueryId: string; + queryLimit: number; + remoteId: number; + schema: string; + schemaOptions: Array<{ + label: string; + title: string; + value: string; + }>; + selectedText: string | null; + sql: string; + tableOptions: Array<{ + label: string; + schema: string; + title: string; + type: string; + value: string; + }>; + name: string; +}; const Styles = styled.span` span[role='img'] { @@ -73,33 +81,20 @@ const Styles = styled.span` `; export default function SaveQuery({ - queryEditorId, + queryEditor, onSave = () => {}, onUpdate, saveQueryWarning = null, database, columns, }: SaveQueryProps) { - const queryEditor = useQueryEditor(queryEditorId, [ - 'autorun', - 'name', - 'description', - 'remoteId', - 'dbId', - 'latestQueryId', - 'queryLimit', - 'schema', - 'schemaOptions', - 'selectedText', - 'sql', - 'tableOptions', - ]); - const query = useMemo( - () => ({ + const query = useSelector( + ({ sqlLab: { unsavedQueryEditor } }) => ({ ...queryEditor, + ...(queryEditor.id === unsavedQueryEditor.id && unsavedQueryEditor), columns, }), - [queryEditor, columns], + shallowEqual, ); const defaultLabel = query.name || query.description || t('Undefined'); const [description, setDescription] = useState( @@ -119,12 +114,12 @@ export default function SaveQuery({ ); - const queryPayload = () => ({ - ...query, - name: label, - description, - dbId: query.dbId ?? 0, - }); + const queryPayload = () => + ({ + ...query, + name: label, + description, + } as any as QueryPayload); useEffect(() => { if (!isSaved) setLabel(defaultLabel); diff --git a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/ShareSqlLabQuery.test.tsx b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/ShareSqlLabQuery.test.jsx similarity index 85% rename from superset-frontend/src/SqlLab/components/ShareSqlLabQuery/ShareSqlLabQuery.test.tsx rename to superset-frontend/src/SqlLab/components/ShareSqlLabQuery/ShareSqlLabQuery.test.jsx index 2c2038f5c4138..22913894fbc39 100644 --- a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/ShareSqlLabQuery.test.tsx +++ b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/ShareSqlLabQuery.test.jsx @@ -31,42 +31,30 @@ import ShareSqlLabQuery from 'src/SqlLab/components/ShareSqlLabQuery'; import { initialState } from 'src/SqlLab/fixtures'; const mockStore = configureStore([thunk]); -const defaultProps = { - queryEditorId: 'qe1', - addDangerToast: jest.fn(), -}; -const mockQueryEditor = { - id: defaultProps.queryEditorId, - dbId: 0, - name: 'query title', - schema: 'query_schema', - autorun: false, - sql: 'SELECT * FROM ...', - remoteId: 999, -}; -const disabled = { - id: 'disabledEditorId', - remoteId: undefined, -}; - -const mockState = { - ...initialState, - sqlLab: { - ...initialState.sqlLab, - queryEditors: [mockQueryEditor, disabled], - }, -}; -const store = mockStore(mockState); -let isFeatureEnabledMock: jest.SpyInstance; +const store = mockStore(initialState); +let isFeatureEnabledMock; -const standardProvider: React.FC = ({ children }) => ( +const standardProvider = ({ children }) => ( {children} ); +const defaultProps = { + queryEditor: { + id: 'qe1', + dbId: 0, + name: 'query title', + schema: 'query_schema', + autorun: false, + sql: 'SELECT * FROM ...', + remoteId: 999, + }, + addDangerToast: jest.fn(), +}; + const unsavedQueryEditor = { - id: defaultProps.queryEditorId, + id: defaultProps.queryEditor.id, dbId: 9888, name: 'query title changed', schema: 'query_schema_updated', @@ -74,7 +62,7 @@ const unsavedQueryEditor = { autorun: true, }; -const standardProviderWithUnsaved: React.FC = ({ children }) => ( +const standardProviderWithUnsaved = ({ children }) => ( { }); afterAll(() => { - isFeatureEnabledMock.mockReset(); + isFeatureEnabledMock.restore(); }); it('calls storeQuery() with the query when getCopyUrl() is called', async () => { @@ -122,7 +110,7 @@ describe('ShareSqlLabQuery', () => { }); }); const button = screen.getByRole('button'); - const { id, remoteId, ...expected } = mockQueryEditor; + const { id, remoteId, ...expected } = defaultProps.queryEditor; const storeQuerySpy = jest.spyOn(utils, 'storeQuery'); userEvent.click(button); expect(storeQuerySpy.mock.calls).toHaveLength(1); @@ -154,7 +142,7 @@ describe('ShareSqlLabQuery', () => { }); afterAll(() => { - isFeatureEnabledMock.mockReset(); + isFeatureEnabledMock.restore(); }); it('does not call storeQuery() with the query when getCopyUrl() is called and feature is not enabled', async () => { @@ -172,7 +160,10 @@ describe('ShareSqlLabQuery', () => { it('button is disabled and there is a request to save the query', async () => { const updatedProps = { - queryEditorId: disabled.id, + queryEditor: { + ...defaultProps.queryEditor, + remoteId: undefined, + }, }; render(, { diff --git a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx index f1e6d13c41ac0..37481bdee9249 100644 --- a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx +++ b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx @@ -17,6 +17,7 @@ * under the License. */ import React from 'react'; +import { shallowEqual, useSelector } from 'react-redux'; import { t, useTheme, styled } from '@superset-ui/core'; import Button from 'src/components/Button'; import Icons from 'src/components/Icons'; @@ -25,10 +26,10 @@ import CopyToClipboard from 'src/components/CopyToClipboard'; import { storeQuery } from 'src/utils/common'; import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; -import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor'; +import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types'; interface ShareSqlLabQueryPropTypes { - queryEditorId: string; + queryEditor: QueryEditor; addDangerToast: (msg: string) => void; } @@ -43,15 +44,21 @@ const StyledIcon = styled(Icons.Link)` `; function ShareSqlLabQuery({ - queryEditorId, + queryEditor, addDangerToast, }: ShareSqlLabQueryPropTypes) { const theme = useTheme(); - const { dbId, name, schema, autorun, sql, remoteId } = useQueryEditor( - queryEditorId, - ['dbId', 'name', 'schema', 'autorun', 'sql', 'remoteId'], - ); + const { dbId, name, schema, autorun, sql, remoteId } = useSelector< + SqlLabRootState, + Partial + >(({ sqlLab: { unsavedQueryEditor } }) => { + const { dbId, name, schema, autorun, sql, remoteId } = { + ...queryEditor, + ...(unsavedQueryEditor.id === queryEditor.id && unsavedQueryEditor), + }; + return { dbId, name, schema, autorun, sql, remoteId }; + }, shallowEqual); const getCopyUrlForKvStore = (callback: Function) => { const sharedQuery = { dbId, name, schema, autorun, sql }; diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx index bc4cf013d5cfb..3e8ee84f6c500 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx @@ -163,8 +163,13 @@ const SqlEditor = ({ const theme = useTheme(); const dispatch = useDispatch(); - const { database, latestQuery, hideLeftBar } = useSelector( - ({ sqlLab: { unsavedQueryEditor, databases, queries } }) => { + const { currentQueryEditor, database, latestQuery, hideLeftBar } = + useSelector(({ sqlLab: { unsavedQueryEditor, databases, queries } }) => { + const currentQueryEditor = { + ...queryEditor, + ...(queryEditor.id === unsavedQueryEditor.id && unsavedQueryEditor), + }; + let { dbId, latestQueryId, hideLeftBar } = queryEditor; if (unsavedQueryEditor.id === queryEditor.id) { dbId = unsavedQueryEditor.dbId || dbId; @@ -172,12 +177,12 @@ const SqlEditor = ({ hideLeftBar = unsavedQueryEditor.hideLeftBar || hideLeftBar; } return { + currentQueryEditor, database: databases[dbId], latestQuery: queries[latestQueryId], hideLeftBar, }; - }, - ); + }); const queryEditors = useSelector(({ sqlLab }) => sqlLab.queryEditors); @@ -535,7 +540,7 @@ const SqlEditor = ({ @@ -571,7 +576,7 @@ const SqlEditor = ({
dispatch(updateSavedQuery(query))} @@ -580,7 +585,7 @@ const SqlEditor = ({ /> - + @@ -611,7 +616,7 @@ const SqlEditor = ({ autocomplete={autocompleteEnabled} onBlur={setQueryEditorAndSaveSql} onChange={onSqlChanged} - queryEditorId={queryEditor.id} + queryEditor={currentQueryEditor} database={database} extendedTables={tables} height={`${aceEditorHeight}px`} diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx index 06a31711db4a9..1a24edc65701d 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx @@ -25,6 +25,7 @@ import React, { Dispatch, SetStateAction, } from 'react'; +import { useSelector } from 'react-redux'; import querystring from 'query-string'; import Button from 'src/components/Button'; import { t, styled, css, SupersetTheme } from '@superset-ui/core'; @@ -32,8 +33,7 @@ import Collapse from 'src/components/Collapse'; import Icons from 'src/components/Icons'; import { TableSelectorMultiple } from 'src/components/TableSelector'; import { IconTooltip } from 'src/components/IconTooltip'; -import { QueryEditor, SchemaOption } from 'src/SqlLab/types'; -import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor'; +import { QueryEditor, SchemaOption, SqlLabRootState } from 'src/SqlLab/types'; import { DatabaseObject } from 'src/components/DatabaseSelector'; import { EmptyStateSmall } from 'src/components/EmptyState'; import { @@ -117,7 +117,15 @@ export default function SqlEditorLeftBar({ const [userSelectedDb, setUserSelected] = useState( null, ); - const { schema } = useQueryEditor(queryEditor.id, ['schema']); + const schema = useSelector( + ({ sqlLab: { unsavedQueryEditor } }) => { + const updatedQueryEditor = { + ...queryEditor, + ...(unsavedQueryEditor.id === queryEditor.id && unsavedQueryEditor), + }; + return updatedQueryEditor.schema; + }, + ); useEffect(() => { const bool = querystring.parse(window.location.search).db; diff --git a/superset-frontend/src/SqlLab/hooks/useQueryEditor/index.ts b/superset-frontend/src/SqlLab/hooks/useQueryEditor/index.ts deleted file mode 100644 index 7044e77798fd2..0000000000000 --- a/superset-frontend/src/SqlLab/hooks/useQueryEditor/index.ts +++ /dev/null @@ -1,38 +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 pick from 'lodash/pick'; -import { shallowEqual, useSelector } from 'react-redux'; -import { SqlLabRootState, QueryEditor } from 'src/SqlLab/types'; - -export default function useQueryEditor( - sqlEditorId: string, - attributes: ReadonlyArray, -) { - return useSelector>( - ({ sqlLab: { unsavedQueryEditor, queryEditors } }) => - pick( - { - ...queryEditors.find(({ id }) => id === sqlEditorId), - ...(sqlEditorId === unsavedQueryEditor.id && unsavedQueryEditor), - }, - ['id'].concat(attributes), - ) as Pick, - shallowEqual, - ); -} diff --git a/superset-frontend/src/SqlLab/hooks/useQueryEditor/useQueryEditor.test.ts b/superset-frontend/src/SqlLab/hooks/useQueryEditor/useQueryEditor.test.ts deleted file mode 100644 index 23de4d68226cf..0000000000000 --- a/superset-frontend/src/SqlLab/hooks/useQueryEditor/useQueryEditor.test.ts +++ /dev/null @@ -1,92 +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 configureStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures'; -import { renderHook } from '@testing-library/react-hooks'; -import { createWrapper } from 'spec/helpers/testing-library'; - -import useQueryEditor from '.'; - -const middlewares = [thunk]; -const mockStore = configureStore(middlewares); - -test('returns selected queryEditor values', () => { - const { result } = renderHook( - () => - useQueryEditor(defaultQueryEditor.id, [ - 'id', - 'name', - 'dbId', - 'schemaOptions', - ]), - { - wrapper: createWrapper({ - useRedux: true, - store: mockStore(initialState), - }), - }, - ); - expect(result.current).toEqual({ - id: defaultQueryEditor.id, - name: defaultQueryEditor.name, - dbId: defaultQueryEditor.dbId, - schemaOptions: defaultQueryEditor.schemaOptions, - }); -}); - -test('includes id implicitly', () => { - const { result } = renderHook( - () => useQueryEditor(defaultQueryEditor.id, ['name']), - { - wrapper: createWrapper({ - useRedux: true, - store: mockStore(initialState), - }), - }, - ); - expect(result.current).toEqual({ - id: defaultQueryEditor.id, - name: defaultQueryEditor.name, - }); -}); - -test('returns updated values from unsaved change', () => { - const expectedSql = 'SELECT updated_column\nFROM updated_table\nWHERE'; - const { result } = renderHook( - () => useQueryEditor(defaultQueryEditor.id, ['id', 'sql']), - { - wrapper: createWrapper({ - useRedux: true, - store: mockStore({ - ...initialState, - sqlLab: { - ...initialState.sqlLab, - unsavedQueryEditor: { - id: defaultQueryEditor.id, - sql: expectedSql, - }, - }, - }), - }), - }, - ); - expect(result.current.id).toEqual(defaultQueryEditor.id); - expect(result.current.sql).toEqual(expectedSql); -}); diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.js b/superset-frontend/src/SqlLab/reducers/sqlLab.js index 164691c666381..bab832d6c61f9 100644 --- a/superset-frontend/src/SqlLab/reducers/sqlLab.js +++ b/superset-frontend/src/SqlLab/reducers/sqlLab.js @@ -449,7 +449,7 @@ export default function sqlLabReducer(state = {}, action) { ); return { ...(action.queryEditor.id === state.unsavedQueryEditor.id - ? alterInArr( + ? alterInObject( mergeUnsavedState, 'queryEditors', action.queryEditor,