parent
157482955e
commit
8d1b7ecfde
|
|
@ -47,7 +47,7 @@ type Options = Omit<RenderOptions, 'queries'> & {
|
||||||
store?: Store;
|
store?: Store;
|
||||||
};
|
};
|
||||||
|
|
||||||
function createWrapper(options?: Options) {
|
export function createWrapper(options?: Options) {
|
||||||
const {
|
const {
|
||||||
useDnd,
|
useDnd,
|
||||||
useRedux,
|
useRedux,
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ jest.mock('src/components/AsyncAceEditor', () => ({
|
||||||
const setup = (queryEditor: QueryEditor, store?: Store) =>
|
const setup = (queryEditor: QueryEditor, store?: Store) =>
|
||||||
render(
|
render(
|
||||||
<AceEditorWrapper
|
<AceEditorWrapper
|
||||||
queryEditor={queryEditor}
|
queryEditorId={queryEditor.id}
|
||||||
height="100px"
|
height="100px"
|
||||||
hotkeys={[]}
|
hotkeys={[]}
|
||||||
database={{}}
|
database={{}}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ import {
|
||||||
AceCompleterKeyword,
|
AceCompleterKeyword,
|
||||||
FullSQLEditor as AceEditor,
|
FullSQLEditor as AceEditor,
|
||||||
} from 'src/components/AsyncAceEditor';
|
} from 'src/components/AsyncAceEditor';
|
||||||
import { QueryEditor } from 'src/SqlLab/types';
|
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
|
||||||
|
|
||||||
type HotKey = {
|
type HotKey = {
|
||||||
key: string;
|
key: string;
|
||||||
|
|
@ -50,7 +50,7 @@ type AceEditorWrapperProps = {
|
||||||
autocomplete: boolean;
|
autocomplete: boolean;
|
||||||
onBlur: (sql: string) => void;
|
onBlur: (sql: string) => void;
|
||||||
onChange: (sql: string) => void;
|
onChange: (sql: string) => void;
|
||||||
queryEditor: QueryEditor;
|
queryEditorId: string;
|
||||||
database: any;
|
database: any;
|
||||||
extendedTables?: Array<{ name: string; columns: any[] }>;
|
extendedTables?: Array<{ name: string; columns: any[] }>;
|
||||||
height: string;
|
height: string;
|
||||||
|
|
@ -61,7 +61,7 @@ const AceEditorWrapper = ({
|
||||||
autocomplete,
|
autocomplete,
|
||||||
onBlur = () => {},
|
onBlur = () => {},
|
||||||
onChange = () => {},
|
onChange = () => {},
|
||||||
queryEditor,
|
queryEditorId,
|
||||||
database,
|
database,
|
||||||
extendedTables = [],
|
extendedTables = [],
|
||||||
height,
|
height,
|
||||||
|
|
@ -69,7 +69,17 @@ const AceEditorWrapper = ({
|
||||||
}: AceEditorWrapperProps) => {
|
}: AceEditorWrapperProps) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const { sql: currentSql } = queryEditor;
|
const queryEditor = useQueryEditor(queryEditorId, [
|
||||||
|
'id',
|
||||||
|
'dbId',
|
||||||
|
'sql',
|
||||||
|
'functionNames',
|
||||||
|
'schemaOptions',
|
||||||
|
'tableOptions',
|
||||||
|
'validationResult',
|
||||||
|
'schema',
|
||||||
|
]);
|
||||||
|
const currentSql = queryEditor.sql ?? '';
|
||||||
const functionNames = queryEditor.functionNames ?? [];
|
const functionNames = queryEditor.functionNames ?? [];
|
||||||
const schemas = queryEditor.schemaOptions ?? [];
|
const schemas = queryEditor.schemaOptions ?? [];
|
||||||
const tables = queryEditor.tableOptions ?? [];
|
const tables = queryEditor.tableOptions ?? [];
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ const defaultQueryLimit = 100;
|
||||||
const setup = (props?: Partial<QueryLimitSelectProps>, store?: Store) =>
|
const setup = (props?: Partial<QueryLimitSelectProps>, store?: Store) =>
|
||||||
render(
|
render(
|
||||||
<QueryLimitSelect
|
<QueryLimitSelect
|
||||||
queryEditor={defaultQueryEditor}
|
queryEditorId={defaultQueryEditor.id}
|
||||||
maxRow={100000}
|
maxRow={100000}
|
||||||
defaultQueryLimit={defaultQueryLimit}
|
defaultQueryLimit={defaultQueryLimit}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -67,12 +67,20 @@ describe('QueryLimitSelect', () => {
|
||||||
const queryLimit = 10;
|
const queryLimit = 10;
|
||||||
const { getByText } = setup(
|
const { getByText } = setup(
|
||||||
{
|
{
|
||||||
queryEditor: {
|
queryEditorId: defaultQueryEditor.id,
|
||||||
...defaultQueryEditor,
|
|
||||||
queryLimit,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
mockStore(initialState),
|
mockStore({
|
||||||
|
...initialState,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
queryEditors: [
|
||||||
|
{
|
||||||
|
...defaultQueryEditor,
|
||||||
|
queryLimit,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
expect(getByText(queryLimit)).toBeInTheDocument();
|
expect(getByText(queryLimit)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
@ -129,7 +137,9 @@ describe('QueryLimitSelect', () => {
|
||||||
{
|
{
|
||||||
type: 'QUERY_EDITOR_SET_QUERY_LIMIT',
|
type: 'QUERY_EDITOR_SET_QUERY_LIMIT',
|
||||||
queryLimit: LIMIT_DROPDOWN[expectedIndex],
|
queryLimit: LIMIT_DROPDOWN[expectedIndex],
|
||||||
queryEditor: defaultQueryEditor,
|
queryEditor: {
|
||||||
|
id: defaultQueryEditor.id,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -17,16 +17,16 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { styled, useTheme } from '@superset-ui/core';
|
import { styled, useTheme } from '@superset-ui/core';
|
||||||
import { AntdDropdown } from 'src/components';
|
import { AntdDropdown } from 'src/components';
|
||||||
import { Menu } from 'src/components/Menu';
|
import { Menu } from 'src/components/Menu';
|
||||||
import Icons from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
import { SqlLabRootState, QueryEditor } from 'src/SqlLab/types';
|
|
||||||
import { queryEditorSetQueryLimit } from 'src/SqlLab/actions/sqlLab';
|
import { queryEditorSetQueryLimit } from 'src/SqlLab/actions/sqlLab';
|
||||||
|
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
|
||||||
|
|
||||||
export interface QueryLimitSelectProps {
|
export interface QueryLimitSelectProps {
|
||||||
queryEditor: QueryEditor;
|
queryEditorId: string;
|
||||||
maxRow: number;
|
maxRow: number;
|
||||||
defaultQueryLimit: number;
|
defaultQueryLimit: number;
|
||||||
}
|
}
|
||||||
|
|
@ -79,19 +79,12 @@ function renderQueryLimit(
|
||||||
}
|
}
|
||||||
|
|
||||||
const QueryLimitSelect = ({
|
const QueryLimitSelect = ({
|
||||||
queryEditor,
|
queryEditorId,
|
||||||
maxRow,
|
maxRow,
|
||||||
defaultQueryLimit,
|
defaultQueryLimit,
|
||||||
}: QueryLimitSelectProps) => {
|
}: QueryLimitSelectProps) => {
|
||||||
const queryLimit = useSelector<SqlLabRootState, number>(
|
const queryEditor = useQueryEditor(queryEditorId, ['id', 'queryLimit']);
|
||||||
({ sqlLab: { unsavedQueryEditor } }) => {
|
const queryLimit = queryEditor.queryLimit || defaultQueryLimit;
|
||||||
const updatedQueryEditor = {
|
|
||||||
...queryEditor,
|
|
||||||
...(unsavedQueryEditor.id === queryEditor.id && unsavedQueryEditor),
|
|
||||||
};
|
|
||||||
return updatedQueryEditor.queryLimit || defaultQueryLimit;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const setQueryLimit = (updatedQueryLimit: number) =>
|
const setQueryLimit = (updatedQueryLimit: number) =>
|
||||||
dispatch(queryEditorSetQueryLimit(queryEditor, updatedQueryLimit));
|
dispatch(queryEditorSetQueryLimit(queryEditor, updatedQueryLimit));
|
||||||
|
|
|
||||||
|
|
@ -41,13 +41,13 @@ jest.mock('src/components/Select/AsyncSelect', () => () => (
|
||||||
));
|
));
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
queryEditor: defaultQueryEditor,
|
queryEditorId: defaultQueryEditor.id,
|
||||||
allowAsync: false,
|
allowAsync: false,
|
||||||
dbId: 1,
|
dbId: 1,
|
||||||
queryState: 'ready',
|
queryState: 'ready',
|
||||||
runQuery: jest.fn(),
|
runQuery: () => {},
|
||||||
selectedText: null,
|
selectedText: null,
|
||||||
stopQuery: jest.fn(),
|
stopQuery: () => {},
|
||||||
overlayCreateAsMenu: null,
|
overlayCreateAsMenu: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -57,95 +57,104 @@ const setup = (props?: Partial<Props>, store?: Store) =>
|
||||||
...(store && { store }),
|
...(store && { store }),
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('RunQueryActionButton', () => {
|
it('renders a single Button', () => {
|
||||||
beforeEach(() => {
|
const { getByRole } = setup({}, mockStore(initialState));
|
||||||
defaultProps.runQuery.mockReset();
|
expect(getByRole('button')).toBeInTheDocument();
|
||||||
defaultProps.stopQuery.mockReset();
|
});
|
||||||
});
|
|
||||||
|
it('renders a label for Run Query', () => {
|
||||||
it('renders a single Button', () => {
|
const { getByText } = setup({}, mockStore(initialState));
|
||||||
const { getByRole } = setup({}, mockStore(initialState));
|
expect(getByText('Run')).toBeInTheDocument();
|
||||||
expect(getByRole('button')).toBeInTheDocument();
|
});
|
||||||
});
|
|
||||||
|
it('renders a label for Selected Query', () => {
|
||||||
it('renders a label for Run Query', () => {
|
const { getByText } = setup(
|
||||||
const { getByText } = setup({}, mockStore(initialState));
|
{},
|
||||||
expect(getByText('Run')).toBeInTheDocument();
|
mockStore({
|
||||||
});
|
...initialState,
|
||||||
|
sqlLab: {
|
||||||
it('renders a label for Selected Query', () => {
|
...initialState.sqlLab,
|
||||||
const { getByText } = setup(
|
unsavedQueryEditor: {
|
||||||
{},
|
id: defaultQueryEditor.id,
|
||||||
mockStore({
|
selectedText: 'select * from\n-- this is comment\nwhere',
|
||||||
...initialState,
|
},
|
||||||
sqlLab: {
|
},
|
||||||
...initialState.sqlLab,
|
}),
|
||||||
unsavedQueryEditor: {
|
);
|
||||||
id: defaultQueryEditor.id,
|
expect(getByText('Run selection')).toBeInTheDocument();
|
||||||
selectedText: 'FROM',
|
});
|
||||||
},
|
|
||||||
},
|
it('disable button when sql from unsaved changes is empty', () => {
|
||||||
}),
|
const { getByRole } = setup(
|
||||||
);
|
{},
|
||||||
expect(getByText('Run selection')).toBeInTheDocument();
|
mockStore({
|
||||||
});
|
...initialState,
|
||||||
|
sqlLab: {
|
||||||
it('disable button when sql from unsaved changes is empty', () => {
|
...initialState.sqlLab,
|
||||||
const { getByRole } = setup(
|
unsavedQueryEditor: {
|
||||||
{},
|
id: defaultQueryEditor.id,
|
||||||
mockStore({
|
sql: '',
|
||||||
...initialState,
|
},
|
||||||
sqlLab: {
|
},
|
||||||
...initialState.sqlLab,
|
}),
|
||||||
unsavedQueryEditor: {
|
);
|
||||||
id: defaultQueryEditor.id,
|
const button = getByRole('button');
|
||||||
sql: '',
|
expect(button).toBeDisabled();
|
||||||
},
|
});
|
||||||
},
|
|
||||||
}),
|
it('disable button when selectedText only contains blank contents', () => {
|
||||||
);
|
const { getByRole } = setup(
|
||||||
const button = getByRole('button');
|
{},
|
||||||
expect(button).toBeDisabled();
|
mockStore({
|
||||||
});
|
...initialState,
|
||||||
|
sqlLab: {
|
||||||
it('enable default button for unrelated unsaved changes', () => {
|
...initialState.sqlLab,
|
||||||
const { getByRole } = setup(
|
unsavedQueryEditor: {
|
||||||
{},
|
id: defaultQueryEditor.id,
|
||||||
mockStore({
|
selectedText: '-- this is comment\n\n \t',
|
||||||
...initialState,
|
},
|
||||||
sqlLab: {
|
},
|
||||||
...initialState.sqlLab,
|
}),
|
||||||
unsavedQueryEditor: {
|
);
|
||||||
id: `${defaultQueryEditor.id}-other`,
|
const button = getByRole('button');
|
||||||
sql: '',
|
expect(button).toBeDisabled();
|
||||||
},
|
});
|
||||||
},
|
|
||||||
}),
|
it('enable default button for unrelated unsaved changes', () => {
|
||||||
);
|
const { getByRole } = setup(
|
||||||
const button = getByRole('button');
|
{},
|
||||||
expect(button).toBeEnabled();
|
mockStore({
|
||||||
});
|
...initialState,
|
||||||
|
sqlLab: {
|
||||||
it('dispatch runQuery on click', async () => {
|
...initialState.sqlLab,
|
||||||
const { getByRole } = setup({}, mockStore(initialState));
|
unsavedQueryEditor: {
|
||||||
const button = getByRole('button');
|
id: `${defaultQueryEditor.id}-other`,
|
||||||
expect(defaultProps.runQuery).toHaveBeenCalledTimes(0);
|
sql: '',
|
||||||
fireEvent.click(button);
|
},
|
||||||
await waitFor(() => expect(defaultProps.runQuery).toHaveBeenCalledTimes(1));
|
},
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
describe('on running state', () => {
|
const button = getByRole('button');
|
||||||
it('dispatch stopQuery on click', async () => {
|
expect(button).toBeEnabled();
|
||||||
const { getByRole } = setup(
|
});
|
||||||
{ queryState: 'running' },
|
|
||||||
mockStore(initialState),
|
it('dispatch runQuery on click', async () => {
|
||||||
);
|
const runQuery = jest.fn();
|
||||||
const button = getByRole('button');
|
const { getByRole } = setup({ runQuery }, mockStore(initialState));
|
||||||
expect(defaultProps.stopQuery).toHaveBeenCalledTimes(0);
|
const button = getByRole('button');
|
||||||
fireEvent.click(button);
|
expect(runQuery).toHaveBeenCalledTimes(0);
|
||||||
await waitFor(() =>
|
fireEvent.click(button);
|
||||||
expect(defaultProps.stopQuery).toHaveBeenCalledTimes(1),
|
await waitFor(() => expect(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));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,11 @@ import Button from 'src/components/Button';
|
||||||
import Icons from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
import { DropdownButton } from 'src/components/DropdownButton';
|
import { DropdownButton } from 'src/components/DropdownButton';
|
||||||
import { detectOS } from 'src/utils/common';
|
import { detectOS } from 'src/utils/common';
|
||||||
import { shallowEqual, useSelector } from 'react-redux';
|
import { QueryButtonProps } from 'src/SqlLab/types';
|
||||||
import {
|
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
|
||||||
QueryEditor,
|
|
||||||
SqlLabRootState,
|
|
||||||
QueryButtonProps,
|
|
||||||
} from 'src/SqlLab/types';
|
|
||||||
import { getUpToDateQuery } from 'src/SqlLab/actions/sqlLab';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
queryEditor: QueryEditor;
|
queryEditorId: string;
|
||||||
allowAsync: boolean;
|
allowAsync: boolean;
|
||||||
queryState?: string;
|
queryState?: string;
|
||||||
runQuery: (c?: boolean) => void;
|
runQuery: (c?: boolean) => void;
|
||||||
|
|
@ -86,29 +81,21 @@ const StyledButton = styled.span`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const RunQueryActionButton = ({
|
const RunQueryActionButton: React.FC<Props> = ({
|
||||||
allowAsync = false,
|
allowAsync = false,
|
||||||
queryEditor,
|
queryEditorId,
|
||||||
queryState,
|
queryState,
|
||||||
overlayCreateAsMenu,
|
overlayCreateAsMenu,
|
||||||
runQuery,
|
runQuery,
|
||||||
stopQuery,
|
stopQuery,
|
||||||
}: Props) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const userOS = detectOS();
|
const userOS = detectOS();
|
||||||
const { selectedText, sql } = useSelector<
|
|
||||||
SqlLabRootState,
|
const { selectedText, sql } = useQueryEditor(queryEditorId, [
|
||||||
Pick<QueryEditor, 'selectedText' | 'sql'>
|
'selectedText',
|
||||||
>(rootState => {
|
'sql',
|
||||||
const currentQueryEditor = getUpToDateQuery(
|
]);
|
||||||
rootState,
|
|
||||||
queryEditor,
|
|
||||||
) as unknown as QueryEditor;
|
|
||||||
return {
|
|
||||||
selectedText: currentQueryEditor.selectedText,
|
|
||||||
sql: currentQueryEditor.sql,
|
|
||||||
};
|
|
||||||
}, shallowEqual);
|
|
||||||
|
|
||||||
const shouldShowStopBtn =
|
const shouldShowStopBtn =
|
||||||
!!queryState && ['running', 'pending'].indexOf(queryState) > -1;
|
!!queryState && ['running', 'pending'].indexOf(queryState) > -1;
|
||||||
|
|
@ -117,7 +104,10 @@ const RunQueryActionButton = ({
|
||||||
? (DropdownButton as React.FC)
|
? (DropdownButton as React.FC)
|
||||||
: Button;
|
: Button;
|
||||||
|
|
||||||
const isDisabled = !sql || !sql.trim();
|
const sqlContent = selectedText || sql || '';
|
||||||
|
const isDisabled =
|
||||||
|
!sqlContent ||
|
||||||
|
!sqlContent.replace(/(\/\*[^*]*\*\/)|(\/\/[^*]*)|(--[^.].*)/gm, '').trim();
|
||||||
|
|
||||||
const stopButtonTooltipText = useMemo(
|
const stopButtonTooltipText = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,28 @@ import SaveQuery from 'src/SqlLab/components/SaveQuery';
|
||||||
import { initialState, databases } from 'src/SqlLab/fixtures';
|
import { initialState, databases } from 'src/SqlLab/fixtures';
|
||||||
|
|
||||||
const mockedProps = {
|
const mockedProps = {
|
||||||
queryEditor: {
|
queryEditorId: '123',
|
||||||
dbId: 1,
|
|
||||||
schema: 'main',
|
|
||||||
sql: 'SELECT * FROM t',
|
|
||||||
},
|
|
||||||
animation: false,
|
animation: false,
|
||||||
database: databases.result[0],
|
database: databases.result[0],
|
||||||
onUpdate: () => {},
|
onUpdate: () => {},
|
||||||
onSave: () => {},
|
onSave: () => {},
|
||||||
|
saveQueryWarning: null,
|
||||||
|
columns: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockState = {
|
||||||
|
...initialState,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
queryEditors: [
|
||||||
|
{
|
||||||
|
id: mockedProps.queryEditorId,
|
||||||
|
dbId: 1,
|
||||||
|
schema: 'main',
|
||||||
|
sql: 'SELECT * FROM t',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const splitSaveBtnProps = {
|
const splitSaveBtnProps = {
|
||||||
|
|
@ -51,7 +64,7 @@ describe('SavedQuery', () => {
|
||||||
it('renders a non-split save button when allows_virtual_table_explore is not enabled', () => {
|
it('renders a non-split save button when allows_virtual_table_explore is not enabled', () => {
|
||||||
render(<SaveQuery {...mockedProps} />, {
|
render(<SaveQuery {...mockedProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
store: mockStore(initialState),
|
store: mockStore(mockState),
|
||||||
});
|
});
|
||||||
|
|
||||||
const saveBtn = screen.getByRole('button', { name: /save/i });
|
const saveBtn = screen.getByRole('button', { name: /save/i });
|
||||||
|
|
@ -62,7 +75,7 @@ describe('SavedQuery', () => {
|
||||||
it('renders a save query modal when user clicks save button', () => {
|
it('renders a save query modal when user clicks save button', () => {
|
||||||
render(<SaveQuery {...mockedProps} />, {
|
render(<SaveQuery {...mockedProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
store: mockStore(initialState),
|
store: mockStore(mockState),
|
||||||
});
|
});
|
||||||
|
|
||||||
const saveBtn = screen.getByRole('button', { name: /save/i });
|
const saveBtn = screen.getByRole('button', { name: /save/i });
|
||||||
|
|
@ -78,7 +91,7 @@ describe('SavedQuery', () => {
|
||||||
it('renders the save query modal UI', () => {
|
it('renders the save query modal UI', () => {
|
||||||
render(<SaveQuery {...mockedProps} />, {
|
render(<SaveQuery {...mockedProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
store: mockStore(initialState),
|
store: mockStore(mockState),
|
||||||
});
|
});
|
||||||
|
|
||||||
const saveBtn = screen.getByRole('button', { name: /save/i });
|
const saveBtn = screen.getByRole('button', { name: /save/i });
|
||||||
|
|
@ -111,16 +124,18 @@ describe('SavedQuery', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a "save as new" and "update" button if query already exists', () => {
|
it('renders a "save as new" and "update" button if query already exists', () => {
|
||||||
const props = {
|
render(<SaveQuery {...mockedProps} />, {
|
||||||
...mockedProps,
|
|
||||||
queryEditor: {
|
|
||||||
...mockedProps.query,
|
|
||||||
remoteId: '42',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
render(<SaveQuery {...props} />, {
|
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
store: mockStore(initialState),
|
store: mockStore({
|
||||||
|
...mockState,
|
||||||
|
sqlLab: {
|
||||||
|
...mockState.sqlLab,
|
||||||
|
unsavedQueryEditor: {
|
||||||
|
id: mockedProps.queryEditorId,
|
||||||
|
remoteId: '42',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const saveBtn = screen.getByRole('button', { name: /save/i });
|
const saveBtn = screen.getByRole('button', { name: /save/i });
|
||||||
|
|
@ -136,7 +151,7 @@ describe('SavedQuery', () => {
|
||||||
it('renders a split save button when allows_virtual_table_explore is enabled', async () => {
|
it('renders a split save button when allows_virtual_table_explore is enabled', async () => {
|
||||||
render(<SaveQuery {...splitSaveBtnProps} />, {
|
render(<SaveQuery {...splitSaveBtnProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
store: mockStore(initialState),
|
store: mockStore(mockState),
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|
@ -151,7 +166,7 @@ describe('SavedQuery', () => {
|
||||||
it('renders a save dataset modal when user clicks "save dataset" menu item', async () => {
|
it('renders a save dataset modal when user clicks "save dataset" menu item', async () => {
|
||||||
render(<SaveQuery {...splitSaveBtnProps} />, {
|
render(<SaveQuery {...splitSaveBtnProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
store: mockStore(initialState),
|
store: mockStore(mockState),
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|
@ -170,7 +185,7 @@ describe('SavedQuery', () => {
|
||||||
it('renders the save dataset modal UI', async () => {
|
it('renders the save dataset modal UI', async () => {
|
||||||
render(<SaveQuery {...splitSaveBtnProps} />, {
|
render(<SaveQuery {...splitSaveBtnProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
store: mockStore(initialState),
|
store: mockStore(mockState),
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|
@ -16,8 +16,7 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { useSelector, shallowEqual } from 'react-redux';
|
|
||||||
import { Row, Col } from 'src/components';
|
import { Row, Col } from 'src/components';
|
||||||
import { Input, TextArea } from 'src/components/Input';
|
import { Input, TextArea } from 'src/components/Input';
|
||||||
import { t, styled } from '@superset-ui/core';
|
import { t, styled } from '@superset-ui/core';
|
||||||
|
|
@ -31,10 +30,11 @@ import {
|
||||||
ISaveableDatasource,
|
ISaveableDatasource,
|
||||||
} from 'src/SqlLab/components/SaveDatasetModal';
|
} from 'src/SqlLab/components/SaveDatasetModal';
|
||||||
import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils';
|
import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils';
|
||||||
import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
|
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
|
||||||
|
import { QueryEditor } from 'src/SqlLab/types';
|
||||||
|
|
||||||
interface SaveQueryProps {
|
interface SaveQueryProps {
|
||||||
queryEditor: QueryEditor;
|
queryEditorId: string;
|
||||||
columns: ISaveableDatasource['columns'];
|
columns: ISaveableDatasource['columns'];
|
||||||
onSave: (arg0: QueryPayload) => void;
|
onSave: (arg0: QueryPayload) => void;
|
||||||
onUpdate: (arg0: QueryPayload) => void;
|
onUpdate: (arg0: QueryPayload) => void;
|
||||||
|
|
@ -43,30 +43,22 @@ interface SaveQueryProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueryPayload = {
|
type QueryPayload = {
|
||||||
autorun: boolean;
|
name: string;
|
||||||
dbId: number;
|
|
||||||
description?: string;
|
description?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
latestQueryId: string;
|
} & Pick<
|
||||||
queryLimit: number;
|
QueryEditor,
|
||||||
remoteId: number;
|
| 'autorun'
|
||||||
schema: string;
|
| 'dbId'
|
||||||
schemaOptions: Array<{
|
| 'schema'
|
||||||
label: string;
|
| 'sql'
|
||||||
title: string;
|
| 'selectedText'
|
||||||
value: string;
|
| 'remoteId'
|
||||||
}>;
|
| 'latestQueryId'
|
||||||
selectedText: string | null;
|
| 'queryLimit'
|
||||||
sql: string;
|
| 'tableOptions'
|
||||||
tableOptions: Array<{
|
| 'schemaOptions'
|
||||||
label: string;
|
>;
|
||||||
schema: string;
|
|
||||||
title: string;
|
|
||||||
type: string;
|
|
||||||
value: string;
|
|
||||||
}>;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Styles = styled.span`
|
const Styles = styled.span`
|
||||||
span[role='img'] {
|
span[role='img'] {
|
||||||
|
|
@ -81,20 +73,33 @@ const Styles = styled.span`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function SaveQuery({
|
export default function SaveQuery({
|
||||||
queryEditor,
|
queryEditorId,
|
||||||
onSave = () => {},
|
onSave = () => {},
|
||||||
onUpdate,
|
onUpdate,
|
||||||
saveQueryWarning = null,
|
saveQueryWarning = null,
|
||||||
database,
|
database,
|
||||||
columns,
|
columns,
|
||||||
}: SaveQueryProps) {
|
}: SaveQueryProps) {
|
||||||
const query = useSelector<SqlLabRootState, QueryEditor>(
|
const queryEditor = useQueryEditor(queryEditorId, [
|
||||||
({ sqlLab: { unsavedQueryEditor } }) => ({
|
'autorun',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'remoteId',
|
||||||
|
'dbId',
|
||||||
|
'latestQueryId',
|
||||||
|
'queryLimit',
|
||||||
|
'schema',
|
||||||
|
'schemaOptions',
|
||||||
|
'selectedText',
|
||||||
|
'sql',
|
||||||
|
'tableOptions',
|
||||||
|
]);
|
||||||
|
const query = useMemo(
|
||||||
|
() => ({
|
||||||
...queryEditor,
|
...queryEditor,
|
||||||
...(queryEditor.id === unsavedQueryEditor.id && unsavedQueryEditor),
|
|
||||||
columns,
|
columns,
|
||||||
}),
|
}),
|
||||||
shallowEqual,
|
[queryEditor, columns],
|
||||||
);
|
);
|
||||||
const defaultLabel = query.name || query.description || t('Undefined');
|
const defaultLabel = query.name || query.description || t('Undefined');
|
||||||
const [description, setDescription] = useState<string>(
|
const [description, setDescription] = useState<string>(
|
||||||
|
|
@ -114,12 +119,12 @@ export default function SaveQuery({
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|
||||||
const queryPayload = () =>
|
const queryPayload = () => ({
|
||||||
({
|
...query,
|
||||||
...query,
|
name: label,
|
||||||
name: label,
|
description,
|
||||||
description,
|
dbId: query.dbId ?? 0,
|
||||||
} as any as QueryPayload);
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isSaved) setLabel(defaultLabel);
|
if (!isSaved) setLabel(defaultLabel);
|
||||||
|
|
|
||||||
|
|
@ -31,30 +31,42 @@ import ShareSqlLabQuery from 'src/SqlLab/components/ShareSqlLabQuery';
|
||||||
import { initialState } from 'src/SqlLab/fixtures';
|
import { initialState } from 'src/SqlLab/fixtures';
|
||||||
|
|
||||||
const mockStore = configureStore([thunk]);
|
const mockStore = configureStore([thunk]);
|
||||||
const store = mockStore(initialState);
|
const defaultProps = {
|
||||||
let isFeatureEnabledMock;
|
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 standardProvider = ({ children }) => (
|
const mockState = {
|
||||||
|
...initialState,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
queryEditors: [mockQueryEditor, disabled],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const store = mockStore(mockState);
|
||||||
|
let isFeatureEnabledMock: jest.SpyInstance;
|
||||||
|
|
||||||
|
const standardProvider: React.FC = ({ children }) => (
|
||||||
<ThemeProvider theme={supersetTheme}>
|
<ThemeProvider theme={supersetTheme}>
|
||||||
<Provider store={store}>{children}</Provider>
|
<Provider store={store}>{children}</Provider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
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 = {
|
const unsavedQueryEditor = {
|
||||||
id: defaultProps.queryEditor.id,
|
id: defaultProps.queryEditorId,
|
||||||
dbId: 9888,
|
dbId: 9888,
|
||||||
name: 'query title changed',
|
name: 'query title changed',
|
||||||
schema: 'query_schema_updated',
|
schema: 'query_schema_updated',
|
||||||
|
|
@ -62,7 +74,7 @@ const unsavedQueryEditor = {
|
||||||
autorun: true,
|
autorun: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const standardProviderWithUnsaved = ({ children }) => (
|
const standardProviderWithUnsaved: React.FC = ({ children }) => (
|
||||||
<ThemeProvider theme={supersetTheme}>
|
<ThemeProvider theme={supersetTheme}>
|
||||||
<Provider
|
<Provider
|
||||||
store={mockStore({
|
store={mockStore({
|
||||||
|
|
@ -100,7 +112,7 @@ describe('ShareSqlLabQuery', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
isFeatureEnabledMock.restore();
|
isFeatureEnabledMock.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls storeQuery() with the query when getCopyUrl() is called', async () => {
|
it('calls storeQuery() with the query when getCopyUrl() is called', async () => {
|
||||||
|
|
@ -110,7 +122,7 @@ describe('ShareSqlLabQuery', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const button = screen.getByRole('button');
|
const button = screen.getByRole('button');
|
||||||
const { id, remoteId, ...expected } = defaultProps.queryEditor;
|
const { id, remoteId, ...expected } = mockQueryEditor;
|
||||||
const storeQuerySpy = jest.spyOn(utils, 'storeQuery');
|
const storeQuerySpy = jest.spyOn(utils, 'storeQuery');
|
||||||
userEvent.click(button);
|
userEvent.click(button);
|
||||||
expect(storeQuerySpy.mock.calls).toHaveLength(1);
|
expect(storeQuerySpy.mock.calls).toHaveLength(1);
|
||||||
|
|
@ -142,7 +154,7 @@ describe('ShareSqlLabQuery', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
isFeatureEnabledMock.restore();
|
isFeatureEnabledMock.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not call storeQuery() with the query when getCopyUrl() is called and feature is not enabled', async () => {
|
it('does not call storeQuery() with the query when getCopyUrl() is called and feature is not enabled', async () => {
|
||||||
|
|
@ -160,10 +172,7 @@ describe('ShareSqlLabQuery', () => {
|
||||||
|
|
||||||
it('button is disabled and there is a request to save the query', async () => {
|
it('button is disabled and there is a request to save the query', async () => {
|
||||||
const updatedProps = {
|
const updatedProps = {
|
||||||
queryEditor: {
|
queryEditorId: disabled.id,
|
||||||
...defaultProps.queryEditor,
|
|
||||||
remoteId: undefined,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render(<ShareSqlLabQuery {...updatedProps} />, {
|
render(<ShareSqlLabQuery {...updatedProps} />, {
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallowEqual, useSelector } from 'react-redux';
|
|
||||||
import { t, useTheme, styled } from '@superset-ui/core';
|
import { t, useTheme, styled } from '@superset-ui/core';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
import Icons from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
|
|
@ -26,10 +25,10 @@ import CopyToClipboard from 'src/components/CopyToClipboard';
|
||||||
import { storeQuery } from 'src/utils/common';
|
import { storeQuery } from 'src/utils/common';
|
||||||
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||||
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
|
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
|
||||||
import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
|
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
|
||||||
|
|
||||||
interface ShareSqlLabQueryPropTypes {
|
interface ShareSqlLabQueryPropTypes {
|
||||||
queryEditor: QueryEditor;
|
queryEditorId: string;
|
||||||
addDangerToast: (msg: string) => void;
|
addDangerToast: (msg: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,21 +43,15 @@ const StyledIcon = styled(Icons.Link)`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function ShareSqlLabQuery({
|
function ShareSqlLabQuery({
|
||||||
queryEditor,
|
queryEditorId,
|
||||||
addDangerToast,
|
addDangerToast,
|
||||||
}: ShareSqlLabQueryPropTypes) {
|
}: ShareSqlLabQueryPropTypes) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const { dbId, name, schema, autorun, sql, remoteId } = useSelector<
|
const { dbId, name, schema, autorun, sql, remoteId } = useQueryEditor(
|
||||||
SqlLabRootState,
|
queryEditorId,
|
||||||
Partial<QueryEditor>
|
['dbId', 'name', 'schema', 'autorun', 'sql', 'remoteId'],
|
||||||
>(({ 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 getCopyUrlForKvStore = (callback: Function) => {
|
||||||
const sharedQuery = { dbId, name, schema, autorun, sql };
|
const sharedQuery = { dbId, name, schema, autorun, sql };
|
||||||
|
|
|
||||||
|
|
@ -163,13 +163,8 @@ const SqlEditor = ({
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const { currentQueryEditor, database, latestQuery, hideLeftBar } =
|
const { database, latestQuery, hideLeftBar } = useSelector(
|
||||||
useSelector(({ sqlLab: { unsavedQueryEditor, databases, queries } }) => {
|
({ sqlLab: { unsavedQueryEditor, databases, queries } }) => {
|
||||||
const currentQueryEditor = {
|
|
||||||
...queryEditor,
|
|
||||||
...(queryEditor.id === unsavedQueryEditor.id && unsavedQueryEditor),
|
|
||||||
};
|
|
||||||
|
|
||||||
let { dbId, latestQueryId, hideLeftBar } = queryEditor;
|
let { dbId, latestQueryId, hideLeftBar } = queryEditor;
|
||||||
if (unsavedQueryEditor.id === queryEditor.id) {
|
if (unsavedQueryEditor.id === queryEditor.id) {
|
||||||
dbId = unsavedQueryEditor.dbId || dbId;
|
dbId = unsavedQueryEditor.dbId || dbId;
|
||||||
|
|
@ -177,12 +172,12 @@ const SqlEditor = ({
|
||||||
hideLeftBar = unsavedQueryEditor.hideLeftBar || hideLeftBar;
|
hideLeftBar = unsavedQueryEditor.hideLeftBar || hideLeftBar;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
currentQueryEditor,
|
|
||||||
database: databases[dbId],
|
database: databases[dbId],
|
||||||
latestQuery: queries[latestQueryId],
|
latestQuery: queries[latestQueryId],
|
||||||
hideLeftBar,
|
hideLeftBar,
|
||||||
};
|
};
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const queryEditors = useSelector(({ sqlLab }) => sqlLab.queryEditors);
|
const queryEditors = useSelector(({ sqlLab }) => sqlLab.queryEditors);
|
||||||
|
|
||||||
|
|
@ -540,7 +535,7 @@ const SqlEditor = ({
|
||||||
<span>
|
<span>
|
||||||
<RunQueryActionButton
|
<RunQueryActionButton
|
||||||
allowAsync={database ? database.allow_run_async : false}
|
allowAsync={database ? database.allow_run_async : false}
|
||||||
queryEditor={queryEditor}
|
queryEditorId={queryEditor.id}
|
||||||
queryState={latestQuery?.state}
|
queryState={latestQuery?.state}
|
||||||
runQuery={startQuery}
|
runQuery={startQuery}
|
||||||
stopQuery={stopQuery}
|
stopQuery={stopQuery}
|
||||||
|
|
@ -559,7 +554,7 @@ const SqlEditor = ({
|
||||||
)}
|
)}
|
||||||
<span>
|
<span>
|
||||||
<QueryLimitSelect
|
<QueryLimitSelect
|
||||||
queryEditor={queryEditor}
|
queryEditorId={queryEditor.id}
|
||||||
maxRow={maxRow}
|
maxRow={maxRow}
|
||||||
defaultQueryLimit={defaultQueryLimit}
|
defaultQueryLimit={defaultQueryLimit}
|
||||||
/>
|
/>
|
||||||
|
|
@ -576,7 +571,7 @@ const SqlEditor = ({
|
||||||
<div className="rightItems">
|
<div className="rightItems">
|
||||||
<span>
|
<span>
|
||||||
<SaveQuery
|
<SaveQuery
|
||||||
queryEditor={queryEditor}
|
queryEditorId={queryEditor.id}
|
||||||
columns={latestQuery?.results?.columns || []}
|
columns={latestQuery?.results?.columns || []}
|
||||||
onSave={onSaveQuery}
|
onSave={onSaveQuery}
|
||||||
onUpdate={query => dispatch(updateSavedQuery(query))}
|
onUpdate={query => dispatch(updateSavedQuery(query))}
|
||||||
|
|
@ -585,7 +580,7 @@ const SqlEditor = ({
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<ShareSqlLabQuery queryEditor={queryEditor} />
|
<ShareSqlLabQuery queryEditorId={queryEditor.id} />
|
||||||
</span>
|
</span>
|
||||||
<AntdDropdown overlay={renderDropdown()} trigger="click">
|
<AntdDropdown overlay={renderDropdown()} trigger="click">
|
||||||
<Icons.MoreHoriz iconColor={theme.colors.grayscale.base} />
|
<Icons.MoreHoriz iconColor={theme.colors.grayscale.base} />
|
||||||
|
|
@ -616,7 +611,7 @@ const SqlEditor = ({
|
||||||
autocomplete={autocompleteEnabled}
|
autocomplete={autocompleteEnabled}
|
||||||
onBlur={setQueryEditorAndSaveSql}
|
onBlur={setQueryEditorAndSaveSql}
|
||||||
onChange={onSqlChanged}
|
onChange={onSqlChanged}
|
||||||
queryEditor={currentQueryEditor}
|
queryEditorId={queryEditor.id}
|
||||||
database={database}
|
database={database}
|
||||||
extendedTables={tables}
|
extendedTables={tables}
|
||||||
height={`${aceEditorHeight}px`}
|
height={`${aceEditorHeight}px`}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ import React, {
|
||||||
Dispatch,
|
Dispatch,
|
||||||
SetStateAction,
|
SetStateAction,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import querystring from 'query-string';
|
import querystring from 'query-string';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
import { t, styled, css, SupersetTheme } from '@superset-ui/core';
|
import { t, styled, css, SupersetTheme } from '@superset-ui/core';
|
||||||
|
|
@ -33,7 +32,8 @@ import Collapse from 'src/components/Collapse';
|
||||||
import Icons from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
import { TableSelectorMultiple } from 'src/components/TableSelector';
|
import { TableSelectorMultiple } from 'src/components/TableSelector';
|
||||||
import { IconTooltip } from 'src/components/IconTooltip';
|
import { IconTooltip } from 'src/components/IconTooltip';
|
||||||
import { QueryEditor, SchemaOption, SqlLabRootState } from 'src/SqlLab/types';
|
import { QueryEditor, SchemaOption } from 'src/SqlLab/types';
|
||||||
|
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
|
||||||
import { DatabaseObject } from 'src/components/DatabaseSelector';
|
import { DatabaseObject } from 'src/components/DatabaseSelector';
|
||||||
import { EmptyStateSmall } from 'src/components/EmptyState';
|
import { EmptyStateSmall } from 'src/components/EmptyState';
|
||||||
import {
|
import {
|
||||||
|
|
@ -117,15 +117,7 @@ export default function SqlEditorLeftBar({
|
||||||
const [userSelectedDb, setUserSelected] = useState<DatabaseObject | null>(
|
const [userSelectedDb, setUserSelected] = useState<DatabaseObject | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const schema = useSelector<SqlLabRootState, string>(
|
const { schema } = useQueryEditor(queryEditor.id, ['schema']);
|
||||||
({ sqlLab: { unsavedQueryEditor } }) => {
|
|
||||||
const updatedQueryEditor = {
|
|
||||||
...queryEditor,
|
|
||||||
...(unsavedQueryEditor.id === queryEditor.id && unsavedQueryEditor),
|
|
||||||
};
|
|
||||||
return updatedQueryEditor.schema;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const bool = querystring.parse(window.location.search).db;
|
const bool = querystring.parse(window.location.search).db;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
/**
|
||||||
|
* 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<T extends keyof QueryEditor>(
|
||||||
|
sqlEditorId: string,
|
||||||
|
attributes: ReadonlyArray<T>,
|
||||||
|
) {
|
||||||
|
return useSelector<SqlLabRootState, Pick<QueryEditor, T | 'id'>>(
|
||||||
|
({ sqlLab: { unsavedQueryEditor, queryEditors } }) =>
|
||||||
|
pick(
|
||||||
|
{
|
||||||
|
...queryEditors.find(({ id }) => id === sqlEditorId),
|
||||||
|
...(sqlEditorId === unsavedQueryEditor.id && unsavedQueryEditor),
|
||||||
|
},
|
||||||
|
['id'].concat(attributes),
|
||||||
|
) as Pick<QueryEditor, T | 'id'>,
|
||||||
|
shallowEqual,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
});
|
||||||
|
|
@ -449,7 +449,7 @@ export default function sqlLabReducer(state = {}, action) {
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
...(action.queryEditor.id === state.unsavedQueryEditor.id
|
...(action.queryEditor.id === state.unsavedQueryEditor.id
|
||||||
? alterInObject(
|
? alterInArr(
|
||||||
mergeUnsavedState,
|
mergeUnsavedState,
|
||||||
'queryEditors',
|
'queryEditors',
|
||||||
action.queryEditor,
|
action.queryEditor,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue