mirror of
https://github.com/apache/superset.git
synced 2026-04-27 20:14:54 +00:00
perf(sqllab): Rendering perf improvement using immutable state (#20877)
* perf(sqllab): Rendering perf improvement using immutable state - keep queryEditors immutable during active state - add unsavedQueryEditor to store all active changes - refactor each component to subscribe the related unsaved editor state only * revert ISaveableDatasource type cast * missing trigger prop * a default of an empty object and optional operator
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { render, waitFor } from 'spec/helpers/testing-library';
|
||||
import { QueryEditor } from 'src/SqlLab/types';
|
||||
import { Store } from 'redux';
|
||||
import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
|
||||
import {
|
||||
queryEditorSetSelectedText,
|
||||
queryEditorSetFunctionNames,
|
||||
addTable,
|
||||
} from 'src/SqlLab/actions/sqlLab';
|
||||
import AceEditorWrapper from 'src/SqlLab/components/AceEditorWrapper';
|
||||
import { AsyncAceEditorProps } from 'src/components/AsyncAceEditor';
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureStore(middlewares);
|
||||
|
||||
jest.mock('src/components/Select', () => () => (
|
||||
<div data-test="mock-deprecated-select" />
|
||||
));
|
||||
jest.mock('src/components/Select/Select', () => () => (
|
||||
<div data-test="mock-deprecated-select-select" />
|
||||
));
|
||||
jest.mock('src/components/Select/AsyncSelect', () => () => (
|
||||
<div data-test="mock-deprecated-async-select" />
|
||||
));
|
||||
|
||||
jest.mock('src/components/AsyncAceEditor', () => ({
|
||||
FullSQLEditor: (props: AsyncAceEditorProps) => (
|
||||
<div data-test="react-ace">{JSON.stringify(props)}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const setup = (queryEditor: QueryEditor, store?: Store) =>
|
||||
render(
|
||||
<AceEditorWrapper
|
||||
queryEditor={queryEditor}
|
||||
actions={{
|
||||
queryEditorSetSelectedText,
|
||||
queryEditorSetFunctionNames,
|
||||
addTable,
|
||||
}}
|
||||
height="100px"
|
||||
hotkeys={[]}
|
||||
database={{}}
|
||||
onChange={jest.fn()}
|
||||
onBlur={jest.fn()}
|
||||
autocomplete
|
||||
/>,
|
||||
{
|
||||
useRedux: true,
|
||||
...(store && { store }),
|
||||
},
|
||||
);
|
||||
|
||||
describe('AceEditorWrapper', () => {
|
||||
it('renders ace editor including sql value', async () => {
|
||||
const { getByTestId } = setup(defaultQueryEditor, mockStore(initialState));
|
||||
await waitFor(() => expect(getByTestId('react-ace')).toBeInTheDocument());
|
||||
|
||||
expect(getByTestId('react-ace')).toHaveTextContent(
|
||||
JSON.stringify({ value: defaultQueryEditor.sql }).slice(1, -1),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders sql from unsaved change', () => {
|
||||
const expectedSql = 'SELECT updated_column\nFROM updated_table\nWHERE';
|
||||
const { getByTestId } = setup(
|
||||
defaultQueryEditor,
|
||||
mockStore({
|
||||
...initialState,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
unsavedQueryEditor: {
|
||||
id: defaultQueryEditor.id,
|
||||
sql: expectedSql,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(getByTestId('react-ace')).toHaveTextContent(
|
||||
JSON.stringify({ value: expectedSql }).slice(1, -1),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders current sql for unrelated unsaved changes', () => {
|
||||
const expectedSql = 'SELECT updated_column\nFROM updated_table\nWHERE';
|
||||
const { getByTestId } = setup(
|
||||
defaultQueryEditor,
|
||||
mockStore({
|
||||
...initialState,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
unsavedQueryEditor: {
|
||||
id: `${defaultQueryEditor.id}-other`,
|
||||
sql: expectedSql,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(getByTestId('react-ace')).not.toHaveTextContent(
|
||||
JSON.stringify({ value: expectedSql }).slice(1, -1),
|
||||
);
|
||||
expect(getByTestId('react-ace')).toHaveTextContent(
|
||||
JSON.stringify({ value: defaultQueryEditor.sql }).slice(1, -1),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { areArraysShallowEqual } from 'src/reduxUtils';
|
||||
import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
|
||||
import {
|
||||
@@ -30,7 +31,7 @@ import {
|
||||
AceCompleterKeyword,
|
||||
FullSQLEditor as AceEditor,
|
||||
} from 'src/components/AsyncAceEditor';
|
||||
import { QueryEditor } from 'src/SqlLab/types';
|
||||
import { QueryEditor, SchemaOption, SqlLabRootState } from 'src/SqlLab/types';
|
||||
|
||||
type HotKey = {
|
||||
key: string;
|
||||
@@ -39,7 +40,13 @@ type HotKey = {
|
||||
func: () => void;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
type OwnProps = {
|
||||
queryEditor: QueryEditor;
|
||||
extendedTables: Array<{ name: string; columns: any[] }>;
|
||||
autocomplete: boolean;
|
||||
onChange: (sql: string) => void;
|
||||
onBlur: (sql: string) => void;
|
||||
database: any;
|
||||
actions: {
|
||||
queryEditorSetSelectedText: (edit: any, text: null | string) => void;
|
||||
queryEditorSetFunctionNames: (queryEditor: object, dbId: number) => void;
|
||||
@@ -50,19 +57,19 @@ interface Props {
|
||||
schema: any,
|
||||
) => void;
|
||||
};
|
||||
autocomplete: boolean;
|
||||
onBlur: (sql: string) => void;
|
||||
hotkeys: HotKey[];
|
||||
height: string;
|
||||
};
|
||||
|
||||
type ReduxProps = {
|
||||
queryEditor: QueryEditor;
|
||||
sql: string;
|
||||
database: any;
|
||||
schemas: any[];
|
||||
schemas: SchemaOption[];
|
||||
tables: any[];
|
||||
functionNames: string[];
|
||||
extendedTables: Array<{ name: string; columns: any[] }>;
|
||||
queryEditor: QueryEditor;
|
||||
height: string;
|
||||
hotkeys: HotKey[];
|
||||
onChange: (sql: string) => void;
|
||||
}
|
||||
};
|
||||
|
||||
type Props = ReduxProps & OwnProps;
|
||||
|
||||
interface State {
|
||||
sql: string;
|
||||
@@ -286,4 +293,22 @@ class AceEditorWrapper extends React.PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export default AceEditorWrapper;
|
||||
function mapStateToProps(
|
||||
{ sqlLab: { unsavedQueryEditor } }: SqlLabRootState,
|
||||
{ queryEditor }: OwnProps,
|
||||
) {
|
||||
const currentQueryEditor = {
|
||||
...queryEditor,
|
||||
...(queryEditor.id === unsavedQueryEditor.id && unsavedQueryEditor),
|
||||
};
|
||||
return {
|
||||
queryEditor: currentQueryEditor,
|
||||
sql: currentQueryEditor.sql,
|
||||
schemas: currentQueryEditor.schemaOptions || [],
|
||||
tables: currentQueryEditor.tableOptions,
|
||||
functionNames: currentQueryEditor.functionNames,
|
||||
};
|
||||
}
|
||||
export default connect<ReduxProps, {}, OwnProps>(mapStateToProps)(
|
||||
AceEditorWrapper,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { render } from 'spec/helpers/testing-library';
|
||||
import { Store } from 'redux';
|
||||
import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
|
||||
|
||||
import EstimateQueryCostButton, {
|
||||
EstimateQueryCostButtonProps,
|
||||
} from 'src/SqlLab/components/EstimateQueryCostButton';
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureStore(middlewares);
|
||||
|
||||
jest.mock('src/components/Select', () => () => (
|
||||
<div data-test="mock-deprecated-select" />
|
||||
));
|
||||
jest.mock('src/components/Select/Select', () => () => (
|
||||
<div data-test="mock-deprecated-select-select" />
|
||||
));
|
||||
jest.mock('src/components/Select/AsyncSelect', () => () => (
|
||||
<div data-test="mock-deprecated-async-select" />
|
||||
));
|
||||
|
||||
const setup = (props: Partial<EstimateQueryCostButtonProps>, store?: Store) =>
|
||||
render(
|
||||
<EstimateQueryCostButton
|
||||
queryEditor={defaultQueryEditor}
|
||||
getEstimate={jest.fn()}
|
||||
{...props}
|
||||
/>,
|
||||
{
|
||||
useRedux: true,
|
||||
...(store && { store }),
|
||||
},
|
||||
);
|
||||
|
||||
describe('EstimateQueryCostButton', () => {
|
||||
it('renders EstimateQueryCostButton', async () => {
|
||||
const { queryByText } = setup({}, mockStore(initialState));
|
||||
|
||||
expect(queryByText('Estimate cost')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders label for selected query', async () => {
|
||||
const queryEditorWithSelectedText = {
|
||||
...defaultQueryEditor,
|
||||
selectedText: 'SELECT',
|
||||
};
|
||||
const { queryByText } = setup(
|
||||
{ queryEditor: queryEditorWithSelectedText },
|
||||
mockStore(initialState),
|
||||
);
|
||||
|
||||
expect(queryByText('Estimate selected query cost')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders label for selected query from unsaved', async () => {
|
||||
const { queryByText } = setup(
|
||||
{},
|
||||
mockStore({
|
||||
...initialState,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
unsavedQueryEditor: {
|
||||
id: defaultQueryEditor.id,
|
||||
selectedText: 'SELECT',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(queryByText('Estimate selected query cost')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -24,23 +24,37 @@ import Button from 'src/components/Button';
|
||||
import Loading from 'src/components/Loading';
|
||||
import ModalTrigger from 'src/components/ModalTrigger';
|
||||
import { EmptyWrapperType } from 'src/components/TableView/TableView';
|
||||
import {
|
||||
SqlLabRootState,
|
||||
QueryCostEstimate,
|
||||
QueryEditor,
|
||||
} from 'src/SqlLab/types';
|
||||
import { getUpToDateQuery } from 'src/SqlLab/actions/sqlLab';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
interface EstimateQueryCostButtonProps {
|
||||
export interface EstimateQueryCostButtonProps {
|
||||
getEstimate: Function;
|
||||
queryCostEstimate: Record<string, any>;
|
||||
selectedText?: string;
|
||||
queryEditor: QueryEditor;
|
||||
tooltip?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const EstimateQueryCostButton = ({
|
||||
getEstimate,
|
||||
queryCostEstimate = {},
|
||||
selectedText,
|
||||
queryEditor,
|
||||
tooltip = '',
|
||||
disabled = false,
|
||||
}: EstimateQueryCostButtonProps) => {
|
||||
const { cost } = queryCostEstimate;
|
||||
const queryCostEstimate = useSelector<
|
||||
SqlLabRootState,
|
||||
QueryCostEstimate | undefined
|
||||
>(state => state.sqlLab.queryCostEstimates?.[queryEditor.id]);
|
||||
const selectedText = useSelector<SqlLabRootState, string | undefined>(
|
||||
rootState =>
|
||||
(getUpToDateQuery(rootState, queryEditor) as unknown as QueryEditor)
|
||||
.selectedText,
|
||||
);
|
||||
const { cost } = queryCostEstimate || {};
|
||||
const tableData = useMemo(() => (Array.isArray(cost) ? cost : []), [cost]);
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
@@ -57,16 +71,16 @@ const EstimateQueryCostButton = ({
|
||||
};
|
||||
|
||||
const renderModalBody = () => {
|
||||
if (queryCostEstimate.error !== null) {
|
||||
if (queryCostEstimate?.error) {
|
||||
return (
|
||||
<Alert
|
||||
key="query-estimate-error"
|
||||
type="error"
|
||||
message={queryCostEstimate.error}
|
||||
message={queryCostEstimate?.error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (queryCostEstimate.completed) {
|
||||
if (queryCostEstimate?.completed) {
|
||||
return (
|
||||
<TableView
|
||||
columns={columns}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { Store } from 'redux';
|
||||
|
||||
import { render, fireEvent, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
|
||||
import QueryLimitSelect, {
|
||||
LIMIT_DROPDOWN,
|
||||
QueryLimitSelectProps,
|
||||
convertToNumWithSpaces,
|
||||
} from 'src/SqlLab/components/QueryLimitSelect';
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureStore(middlewares);
|
||||
|
||||
jest.mock('src/components/Select', () => () => (
|
||||
<div data-test="mock-deprecated-select" />
|
||||
));
|
||||
jest.mock('src/components/Select/Select', () => () => (
|
||||
<div data-test="mock-deprecated-select-select" />
|
||||
));
|
||||
jest.mock('src/components/Select/AsyncSelect', () => () => (
|
||||
<div data-test="mock-deprecated-async-select" />
|
||||
));
|
||||
jest.mock('src/components/Icons/Icon', () => () => (
|
||||
<div data-test="mock-icons-icon" />
|
||||
));
|
||||
|
||||
const defaultQueryLimit = 100;
|
||||
|
||||
const setup = (props?: Partial<QueryLimitSelectProps>, store?: Store) =>
|
||||
render(
|
||||
<QueryLimitSelect
|
||||
queryEditor={defaultQueryEditor}
|
||||
maxRow={100000}
|
||||
defaultQueryLimit={defaultQueryLimit}
|
||||
{...props}
|
||||
/>,
|
||||
{
|
||||
useRedux: true,
|
||||
...(store && { store }),
|
||||
},
|
||||
);
|
||||
|
||||
describe('QueryLimitSelect', () => {
|
||||
it('renders current query limit size', () => {
|
||||
const queryLimit = 10;
|
||||
const { getByText } = setup(
|
||||
{
|
||||
queryEditor: {
|
||||
...defaultQueryEditor,
|
||||
queryLimit,
|
||||
},
|
||||
},
|
||||
mockStore(initialState),
|
||||
);
|
||||
expect(getByText(queryLimit)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders default query limit for initial queryEditor', () => {
|
||||
const { getByText } = setup({}, mockStore(initialState));
|
||||
expect(getByText(defaultQueryLimit)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders queryLimit from unsavedQueryEditor', () => {
|
||||
const queryLimit = 10000;
|
||||
const { getByText } = setup(
|
||||
{},
|
||||
mockStore({
|
||||
...initialState,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
unsavedQueryEditor: {
|
||||
id: defaultQueryEditor.id,
|
||||
queryLimit,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(getByText(convertToNumWithSpaces(queryLimit))).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders dropdown select', async () => {
|
||||
const { baseElement, getByRole } = setup({}, mockStore(initialState));
|
||||
const dropdown = baseElement.getElementsByClassName(
|
||||
'ant-dropdown-trigger',
|
||||
)[0];
|
||||
|
||||
userEvent.click(dropdown);
|
||||
await waitFor(() => expect(getByRole('menu')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('dispatches QUERY_EDITOR_SET_QUERY_LIMIT action on dropdown menu click', async () => {
|
||||
const store = mockStore(initialState);
|
||||
const expectedIndex = 1;
|
||||
const { baseElement, getAllByRole, getByRole } = setup({}, store);
|
||||
const dropdown = baseElement.getElementsByClassName(
|
||||
'ant-dropdown-trigger',
|
||||
)[0];
|
||||
|
||||
userEvent.click(dropdown);
|
||||
await waitFor(() => expect(getByRole('menu')).toBeInTheDocument());
|
||||
|
||||
const menu = getAllByRole('menuitem')[expectedIndex];
|
||||
expect(store.getActions()).toEqual([]);
|
||||
fireEvent.click(menu);
|
||||
await waitFor(() =>
|
||||
expect(store.getActions()).toEqual([
|
||||
{
|
||||
type: 'QUERY_EDITOR_SET_QUERY_LIMIT',
|
||||
queryLimit: LIMIT_DROPDOWN[expectedIndex],
|
||||
queryEditor: defaultQueryEditor,
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { 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';
|
||||
|
||||
export interface QueryLimitSelectProps {
|
||||
queryEditor: QueryEditor;
|
||||
maxRow: number;
|
||||
defaultQueryLimit: number;
|
||||
}
|
||||
|
||||
export const LIMIT_DROPDOWN = [10, 100, 1000, 10000, 100000];
|
||||
|
||||
export function convertToNumWithSpaces(num: number) {
|
||||
return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1 ');
|
||||
}
|
||||
|
||||
const LimitSelectStyled = styled.span`
|
||||
${({ theme }) => `
|
||||
.ant-dropdown-trigger {
|
||||
align-items: center;
|
||||
color: ${theme.colors.grayscale.dark2};
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
margin-right: ${theme.gridUnit * 2}px;
|
||||
text-decoration: none;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
span {
|
||||
display: inline-block;
|
||||
margin-right: ${theme.gridUnit * 2}px;
|
||||
&:last-of-type: {
|
||||
margin-right: ${theme.gridUnit * 4}px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
function renderQueryLimit(
|
||||
maxRow: number,
|
||||
setQueryLimit: (limit: number) => void,
|
||||
) {
|
||||
// Adding SQL_MAX_ROW value to dropdown
|
||||
LIMIT_DROPDOWN.push(maxRow);
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
{[...new Set(LIMIT_DROPDOWN)].map(limit => (
|
||||
<Menu.Item key={`${limit}`} onClick={() => setQueryLimit(limit)}>
|
||||
{/* // eslint-disable-line no-use-before-define */}
|
||||
<a role="button">{convertToNumWithSpaces(limit)}</a>{' '}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
const QueryLimitSelect = ({
|
||||
queryEditor,
|
||||
maxRow,
|
||||
defaultQueryLimit,
|
||||
}: QueryLimitSelectProps) => {
|
||||
const queryLimit = useSelector<SqlLabRootState, number>(
|
||||
({ 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));
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<LimitSelectStyled>
|
||||
<AntdDropdown
|
||||
overlay={renderQueryLimit(maxRow, setQueryLimit)}
|
||||
trigger={['click']}
|
||||
>
|
||||
<button type="button" onClick={e => e.preventDefault()}>
|
||||
<span>LIMIT:</span>
|
||||
<span className="limitDropdown">
|
||||
{convertToNumWithSpaces(queryLimit)}
|
||||
</span>
|
||||
<Icons.TriangleDown iconColor={theme.colors.grayscale.base} />
|
||||
</button>
|
||||
</AntdDropdown>
|
||||
</LimitSelectStyled>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryLimitSelect;
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||
import RunQueryActionButton from 'src/SqlLab/components/RunQueryActionButton';
|
||||
import Button from 'src/components/Button';
|
||||
|
||||
describe('RunQueryActionButton', () => {
|
||||
let wrapper;
|
||||
const defaultProps = {
|
||||
allowAsync: false,
|
||||
dbId: 1,
|
||||
queryState: 'pending',
|
||||
runQuery: () => {}, // eslint-disable-line
|
||||
selectedText: null,
|
||||
stopQuery: () => {}, // eslint-disable-line
|
||||
sql: '',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mount(<RunQueryActionButton {...defaultProps} />, {
|
||||
wrappingComponent: ThemeProvider,
|
||||
wrappingComponentProps: { theme: supersetTheme },
|
||||
});
|
||||
});
|
||||
|
||||
it('is a valid react element', () => {
|
||||
expect(
|
||||
React.isValidElement(<RunQueryActionButton {...defaultProps} />),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('renders a single Button', () => {
|
||||
expect(wrapper.find(Button)).toExist();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { Store } from 'redux';
|
||||
|
||||
import { render, fireEvent, waitFor } from 'spec/helpers/testing-library';
|
||||
import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
|
||||
import RunQueryActionButton, {
|
||||
Props,
|
||||
} from 'src/SqlLab/components/RunQueryActionButton';
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureStore(middlewares);
|
||||
|
||||
jest.mock('src/components/Select', () => () => (
|
||||
<div data-test="mock-deprecated-select" />
|
||||
));
|
||||
jest.mock('src/components/Select/Select', () => () => (
|
||||
<div data-test="mock-deprecated-select-select" />
|
||||
));
|
||||
jest.mock('src/components/Select/AsyncSelect', () => () => (
|
||||
<div data-test="mock-deprecated-async-select" />
|
||||
));
|
||||
|
||||
const defaultProps = {
|
||||
queryEditor: defaultQueryEditor,
|
||||
allowAsync: false,
|
||||
dbId: 1,
|
||||
queryState: 'ready',
|
||||
runQuery: jest.fn(),
|
||||
selectedText: null,
|
||||
stopQuery: jest.fn(),
|
||||
overlayCreateAsMenu: null,
|
||||
};
|
||||
|
||||
const setup = (props?: Partial<Props>, store?: Store) =>
|
||||
render(<RunQueryActionButton {...defaultProps} {...props} />, {
|
||||
useRedux: true,
|
||||
...(store && { store }),
|
||||
});
|
||||
|
||||
describe('RunQueryActionButton', () => {
|
||||
beforeEach(() => {
|
||||
defaultProps.runQuery.mockReset();
|
||||
defaultProps.stopQuery.mockReset();
|
||||
});
|
||||
|
||||
it('renders a single Button', () => {
|
||||
const { getByRole } = setup({}, mockStore(initialState));
|
||||
expect(getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a label for Run Query', () => {
|
||||
const { getByText } = setup({}, mockStore(initialState));
|
||||
expect(getByText('Run')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a label for Selected Query', () => {
|
||||
const { getByText } = setup(
|
||||
{},
|
||||
mockStore({
|
||||
...initialState,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
unsavedQueryEditor: {
|
||||
id: defaultQueryEditor.id,
|
||||
selectedText: 'FROM',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(getByText('Run selection')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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));
|
||||
});
|
||||
|
||||
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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -24,15 +24,20 @@ 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 { shallowEqual, useSelector } from 'react-redux';
|
||||
import {
|
||||
QueryEditor,
|
||||
SqlLabRootState,
|
||||
QueryButtonProps,
|
||||
} from 'src/SqlLab/types';
|
||||
import { getUpToDateQuery } from 'src/SqlLab/actions/sqlLab';
|
||||
|
||||
interface Props {
|
||||
export interface Props {
|
||||
queryEditor: QueryEditor;
|
||||
allowAsync: boolean;
|
||||
queryState?: string;
|
||||
runQuery: (c?: boolean) => void;
|
||||
selectedText?: string;
|
||||
stopQuery: () => void;
|
||||
sql: string;
|
||||
overlayCreateAsMenu: typeof Menu | null;
|
||||
}
|
||||
|
||||
@@ -83,16 +88,27 @@ const StyledButton = styled.span`
|
||||
|
||||
const RunQueryActionButton = ({
|
||||
allowAsync = false,
|
||||
queryEditor,
|
||||
queryState,
|
||||
selectedText,
|
||||
sql = '',
|
||||
overlayCreateAsMenu,
|
||||
runQuery,
|
||||
stopQuery,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const userOS = detectOS();
|
||||
const { selectedText, sql } = useSelector<
|
||||
SqlLabRootState,
|
||||
Pick<QueryEditor, 'selectedText' | 'sql'>
|
||||
>(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;
|
||||
@@ -101,7 +117,7 @@ const RunQueryActionButton = ({
|
||||
? (DropdownButton as React.FC)
|
||||
: Button;
|
||||
|
||||
const isDisabled = !sql.trim();
|
||||
const isDisabled = !sql || !sql.trim();
|
||||
|
||||
const stopButtonTooltipText = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -17,18 +17,19 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import SaveQuery from 'src/SqlLab/components/SaveQuery';
|
||||
import { databases } from 'src/SqlLab/fixtures';
|
||||
import { initialState, databases } from 'src/SqlLab/fixtures';
|
||||
|
||||
const mockedProps = {
|
||||
query: {
|
||||
queryEditor: {
|
||||
dbId: 1,
|
||||
schema: 'main',
|
||||
sql: 'SELECT * FROM t',
|
||||
},
|
||||
defaultLabel: 'untitled',
|
||||
animation: false,
|
||||
database: databases.result[0],
|
||||
onUpdate: () => {},
|
||||
@@ -43,9 +44,15 @@ const splitSaveBtnProps = {
|
||||
},
|
||||
};
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureStore(middlewares);
|
||||
|
||||
describe('SavedQuery', () => {
|
||||
it('renders a non-split save button when allows_virtual_table_explore is not enabled', () => {
|
||||
render(<SaveQuery {...mockedProps} />, { useRedux: true });
|
||||
render(<SaveQuery {...mockedProps} />, {
|
||||
useRedux: true,
|
||||
store: mockStore(initialState),
|
||||
});
|
||||
|
||||
const saveBtn = screen.getByRole('button', { name: /save/i });
|
||||
|
||||
@@ -53,7 +60,10 @@ describe('SavedQuery', () => {
|
||||
});
|
||||
|
||||
it('renders a save query modal when user clicks save button', () => {
|
||||
render(<SaveQuery {...mockedProps} />, { useRedux: true });
|
||||
render(<SaveQuery {...mockedProps} />, {
|
||||
useRedux: true,
|
||||
store: mockStore(initialState),
|
||||
});
|
||||
|
||||
const saveBtn = screen.getByRole('button', { name: /save/i });
|
||||
userEvent.click(saveBtn);
|
||||
@@ -66,7 +76,10 @@ describe('SavedQuery', () => {
|
||||
});
|
||||
|
||||
it('renders the save query modal UI', () => {
|
||||
render(<SaveQuery {...mockedProps} />, { useRedux: true });
|
||||
render(<SaveQuery {...mockedProps} />, {
|
||||
useRedux: true,
|
||||
store: mockStore(initialState),
|
||||
});
|
||||
|
||||
const saveBtn = screen.getByRole('button', { name: /save/i });
|
||||
userEvent.click(saveBtn);
|
||||
@@ -100,12 +113,15 @@ describe('SavedQuery', () => {
|
||||
it('renders a "save as new" and "update" button if query already exists', () => {
|
||||
const props = {
|
||||
...mockedProps,
|
||||
query: {
|
||||
queryEditor: {
|
||||
...mockedProps.query,
|
||||
remoteId: '42',
|
||||
},
|
||||
};
|
||||
render(<SaveQuery {...props} />, { useRedux: true });
|
||||
render(<SaveQuery {...props} />, {
|
||||
useRedux: true,
|
||||
store: mockStore(initialState),
|
||||
});
|
||||
|
||||
const saveBtn = screen.getByRole('button', { name: /save/i });
|
||||
userEvent.click(saveBtn);
|
||||
@@ -118,7 +134,10 @@ describe('SavedQuery', () => {
|
||||
});
|
||||
|
||||
it('renders a split save button when allows_virtual_table_explore is enabled', async () => {
|
||||
render(<SaveQuery {...splitSaveBtnProps} />, { useRedux: true });
|
||||
render(<SaveQuery {...splitSaveBtnProps} />, {
|
||||
useRedux: true,
|
||||
store: mockStore(initialState),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const saveBtn = screen.getByRole('button', { name: /save/i });
|
||||
@@ -130,7 +149,10 @@ describe('SavedQuery', () => {
|
||||
});
|
||||
|
||||
it('renders a save dataset modal when user clicks "save dataset" menu item', async () => {
|
||||
render(<SaveQuery {...splitSaveBtnProps} />, { useRedux: true });
|
||||
render(<SaveQuery {...splitSaveBtnProps} />, {
|
||||
useRedux: true,
|
||||
store: mockStore(initialState),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const caretBtn = screen.getByRole('button', { name: /caret-down/i });
|
||||
@@ -146,7 +168,10 @@ describe('SavedQuery', () => {
|
||||
});
|
||||
|
||||
it('renders the save dataset modal UI', async () => {
|
||||
render(<SaveQuery {...splitSaveBtnProps} />, { useRedux: true });
|
||||
render(<SaveQuery {...splitSaveBtnProps} />, {
|
||||
useRedux: true,
|
||||
store: mockStore(initialState),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const caretBtn = screen.getByRole('button', { name: /caret-down/i });
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
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';
|
||||
@@ -25,12 +26,16 @@ import { Menu } from 'src/components/Menu';
|
||||
import { Form, FormItem } from 'src/components/Form';
|
||||
import Modal from 'src/components/Modal';
|
||||
import SaveDatasetActionButton from 'src/SqlLab/components/SaveDatasetActionButton';
|
||||
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
|
||||
import {
|
||||
SaveDatasetModal,
|
||||
ISaveableDatasource,
|
||||
} from 'src/SqlLab/components/SaveDatasetModal';
|
||||
import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils';
|
||||
import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
|
||||
|
||||
interface SaveQueryProps {
|
||||
query: QueryPayload;
|
||||
defaultLabel: string;
|
||||
queryEditor: QueryEditor;
|
||||
columns: ISaveableDatasource['columns'];
|
||||
onSave: (arg0: QueryPayload) => void;
|
||||
onUpdate: (arg0: QueryPayload) => void;
|
||||
saveQueryWarning: string | null;
|
||||
@@ -76,13 +81,22 @@ const Styles = styled.span`
|
||||
`;
|
||||
|
||||
export default function SaveQuery({
|
||||
query,
|
||||
defaultLabel = t('Undefined'),
|
||||
queryEditor,
|
||||
onSave = () => {},
|
||||
onUpdate,
|
||||
saveQueryWarning = null,
|
||||
database,
|
||||
columns,
|
||||
}: SaveQueryProps) {
|
||||
const query = useSelector<SqlLabRootState, QueryEditor>(
|
||||
({ sqlLab: { unsavedQueryEditor } }) => ({
|
||||
...queryEditor,
|
||||
...(queryEditor.id === unsavedQueryEditor.id && unsavedQueryEditor),
|
||||
columns,
|
||||
}),
|
||||
shallowEqual,
|
||||
);
|
||||
const defaultLabel = query.name || query.description || t('Undefined');
|
||||
const [description, setDescription] = useState<string>(
|
||||
query.description || '',
|
||||
);
|
||||
@@ -100,11 +114,12 @@ export default function SaveQuery({
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const queryPayload = () => ({
|
||||
...query,
|
||||
name: label,
|
||||
description,
|
||||
});
|
||||
const queryPayload = () =>
|
||||
({
|
||||
...query,
|
||||
name: label,
|
||||
description,
|
||||
} as any as QueryPayload);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSaved) setLabel(defaultLabel);
|
||||
|
||||
@@ -40,7 +40,12 @@ import {
|
||||
} from 'src/SqlLab/actions/sqlLab';
|
||||
import { EmptyStateBig } from 'src/components/EmptyState';
|
||||
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||
import { initialState, queries, table } from 'src/SqlLab/fixtures';
|
||||
import {
|
||||
initialState,
|
||||
queries,
|
||||
table,
|
||||
defaultQueryEditor,
|
||||
} from 'src/SqlLab/fixtures';
|
||||
|
||||
const MOCKED_SQL_EDITOR_HEIGHT = 500;
|
||||
|
||||
@@ -48,7 +53,31 @@ fetchMock.get('glob:*/api/v1/database/*', { result: [] });
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureStore(middlewares);
|
||||
const store = mockStore(initialState);
|
||||
const store = mockStore({
|
||||
...initialState,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
databases: {
|
||||
dbid1: {
|
||||
allow_ctas: false,
|
||||
allow_cvas: false,
|
||||
allow_dml: false,
|
||||
allow_file_upload: false,
|
||||
allow_multi_schema_metadata_fetch: false,
|
||||
allow_run_async: false,
|
||||
backend: 'postgresql',
|
||||
database_name: 'examples',
|
||||
expose_in_sqllab: true,
|
||||
force_ctas_schema: null,
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
unsavedQueryEditor: {
|
||||
id: defaultQueryEditor.id,
|
||||
dbId: 'dbid1',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('SqlEditor', () => {
|
||||
const mockedProps = {
|
||||
@@ -57,21 +86,9 @@ describe('SqlEditor', () => {
|
||||
queryEditorSetSelectedText,
|
||||
queryEditorSetSchemaOptions,
|
||||
addDangerToast: jest.fn(),
|
||||
removeDataPreview: jest.fn(),
|
||||
},
|
||||
database: {
|
||||
allow_ctas: false,
|
||||
allow_cvas: false,
|
||||
allow_dml: false,
|
||||
allow_file_upload: false,
|
||||
allow_multi_schema_metadata_fetch: false,
|
||||
allow_run_async: false,
|
||||
backend: 'postgresql',
|
||||
database_name: 'examples',
|
||||
expose_in_sqllab: true,
|
||||
force_ctas_schema: null,
|
||||
id: 1,
|
||||
},
|
||||
queryEditorId: initialState.sqlLab.queryEditors[0].id,
|
||||
queryEditor: initialState.sqlLab.queryEditors[0],
|
||||
latestQuery: queries[0],
|
||||
tables: [table],
|
||||
getHeight: () => '100px',
|
||||
@@ -94,8 +111,8 @@ describe('SqlEditor', () => {
|
||||
);
|
||||
|
||||
it('does not render SqlEditor if no db selected', () => {
|
||||
const database = {};
|
||||
const updatedProps = { ...mockedProps, database };
|
||||
const queryEditor = initialState.sqlLab.queryEditors[1];
|
||||
const updatedProps = { ...mockedProps, queryEditor };
|
||||
const wrapper = buildWrapper(updatedProps);
|
||||
expect(wrapper.find(EmptyStateBig)).toExist();
|
||||
});
|
||||
|
||||
@@ -43,10 +43,10 @@ import {
|
||||
persistEditorHeight,
|
||||
postStopQuery,
|
||||
queryEditorSetAutorun,
|
||||
queryEditorSetQueryLimit,
|
||||
queryEditorSetSql,
|
||||
queryEditorSetAndSaveSql,
|
||||
queryEditorSetTemplateParams,
|
||||
runQueryFromSqlEditor,
|
||||
runQuery,
|
||||
saveQuery,
|
||||
addSavedQueryToTabState,
|
||||
@@ -79,8 +79,8 @@ import SqlEditorLeftBar from '../SqlEditorLeftBar';
|
||||
import AceEditorWrapper from '../AceEditorWrapper';
|
||||
import RunQueryActionButton from '../RunQueryActionButton';
|
||||
import { newQueryTabName } from '../../utils/newQueryTabName';
|
||||
import QueryLimitSelect from '../QueryLimitSelect';
|
||||
|
||||
const LIMIT_DROPDOWN = [10, 100, 1000, 10000, 100000];
|
||||
const SQL_EDITOR_PADDING = 10;
|
||||
const INITIAL_NORTH_PERCENT = 30;
|
||||
const INITIAL_SOUTH_PERCENT = 70;
|
||||
@@ -96,26 +96,6 @@ const validatorMap =
|
||||
bootstrapData?.common?.conf?.SQL_VALIDATORS_BY_ENGINE || {};
|
||||
const scheduledQueriesConf = bootstrapData?.common?.conf?.SCHEDULED_QUERIES;
|
||||
|
||||
const LimitSelectStyled = styled.span`
|
||||
${({ theme }) => `
|
||||
.ant-dropdown-trigger {
|
||||
align-items: center;
|
||||
color: ${theme.colors.grayscale.dark2};
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
margin-right: ${theme.gridUnit * 2}px;
|
||||
text-decoration: none;
|
||||
span {
|
||||
display: inline-block;
|
||||
margin-right: ${theme.gridUnit * 2}px;
|
||||
&:last-of-type: {
|
||||
margin-right: ${theme.gridUnit * 4}px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledToolbar = styled.div`
|
||||
padding: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
background: ${({ theme }) => theme.colors.grayscale.light5};
|
||||
@@ -154,7 +134,7 @@ const propTypes = {
|
||||
tables: PropTypes.array.isRequired,
|
||||
editorQueries: PropTypes.array.isRequired,
|
||||
dataPreviewQueries: PropTypes.array.isRequired,
|
||||
queryEditorId: PropTypes.string.isRequired,
|
||||
queryEditor: PropTypes.object.isRequired,
|
||||
hideLeftBar: PropTypes.bool,
|
||||
defaultQueryLimit: PropTypes.number.isRequired,
|
||||
maxRow: PropTypes.number.isRequired,
|
||||
@@ -205,7 +185,6 @@ class SqlEditor extends React.PureComponent {
|
||||
);
|
||||
this.queryPane = this.queryPane.bind(this);
|
||||
this.getHotkeyConfig = this.getHotkeyConfig.bind(this);
|
||||
this.renderQueryLimit = this.renderQueryLimit.bind(this);
|
||||
this.getAceEditorAndSouthPaneHeights =
|
||||
this.getAceEditorAndSouthPaneHeights.bind(this);
|
||||
this.getSqlEditorHeight = this.getSqlEditorHeight.bind(this);
|
||||
@@ -382,21 +361,10 @@ class SqlEditor extends React.PureComponent {
|
||||
this.props.queryEditorSetAndSaveSql(this.props.queryEditor, sql);
|
||||
}
|
||||
|
||||
setQueryLimit(queryLimit) {
|
||||
this.props.queryEditorSetQueryLimit(this.props.queryEditor, queryLimit);
|
||||
}
|
||||
|
||||
getQueryCostEstimate() {
|
||||
if (this.props.database) {
|
||||
const qe = this.props.queryEditor;
|
||||
const query = {
|
||||
dbId: qe.dbId,
|
||||
sql: qe.selectedText ? qe.selectedText : this.props.queryEditor.sql,
|
||||
sqlEditorId: qe.id,
|
||||
schema: qe.schema,
|
||||
templateParams: qe.templateParams,
|
||||
};
|
||||
this.props.estimateQueryCost(query);
|
||||
this.props.estimateQueryCost(qe);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,16 +393,9 @@ class SqlEditor extends React.PureComponent {
|
||||
}
|
||||
|
||||
requestValidation(sql) {
|
||||
if (this.props.database) {
|
||||
const qe = this.props.queryEditor;
|
||||
const query = {
|
||||
dbId: qe.dbId,
|
||||
sql,
|
||||
sqlEditorId: qe.id,
|
||||
schema: qe.schema,
|
||||
templateParams: qe.templateParams,
|
||||
};
|
||||
this.props.validateQuery(query);
|
||||
const { database, queryEditor, validateQuery } = this.props;
|
||||
if (database) {
|
||||
validateQuery(queryEditor, sql);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,25 +419,22 @@ class SqlEditor extends React.PureComponent {
|
||||
}
|
||||
|
||||
startQuery(ctas = false, ctas_method = CtasEnum.TABLE) {
|
||||
const qe = this.props.queryEditor;
|
||||
const query = {
|
||||
dbId: qe.dbId,
|
||||
sql: qe.selectedText ? qe.selectedText : qe.sql,
|
||||
sqlEditorId: qe.id,
|
||||
tab: qe.name,
|
||||
schema: qe.schema,
|
||||
tempTable: ctas ? this.state.ctas : '',
|
||||
templateParams: qe.templateParams,
|
||||
queryLimit: qe.queryLimit || this.props.defaultQueryLimit,
|
||||
runAsync: this.props.database
|
||||
? this.props.database.allow_run_async
|
||||
: false,
|
||||
const {
|
||||
database,
|
||||
runQueryFromSqlEditor,
|
||||
setActiveSouthPaneTab,
|
||||
queryEditor,
|
||||
defaultQueryLimit,
|
||||
} = this.props;
|
||||
runQueryFromSqlEditor(
|
||||
database,
|
||||
queryEditor,
|
||||
defaultQueryLimit,
|
||||
ctas ? this.state.ctas : '',
|
||||
ctas,
|
||||
ctas_method,
|
||||
updateTabState: !qe.selectedText,
|
||||
};
|
||||
this.props.runQuery(query);
|
||||
this.props.setActiveSouthPaneTab('Results');
|
||||
);
|
||||
setActiveSouthPaneTab('Results');
|
||||
}
|
||||
|
||||
stopQuery() {
|
||||
@@ -529,11 +487,7 @@ class SqlEditor extends React.PureComponent {
|
||||
onBlur={this.setQueryEditorSql}
|
||||
onChange={this.onSqlChanged}
|
||||
queryEditor={this.props.queryEditor}
|
||||
sql={this.props.queryEditor.sql}
|
||||
database={this.props.database}
|
||||
schemas={this.props.queryEditor.schemaOptions}
|
||||
tables={this.props.queryEditor.tableOptions}
|
||||
functionNames={this.props.queryEditor.functionNames}
|
||||
extendedTables={this.props.tables}
|
||||
height={`${aceEditorHeight}px`}
|
||||
hotkeys={hotkeys}
|
||||
@@ -577,7 +531,7 @@ class SqlEditor extends React.PureComponent {
|
||||
onChange={params => {
|
||||
this.props.actions.queryEditorSetTemplateParams(qe, params);
|
||||
}}
|
||||
code={qe.templateParams}
|
||||
queryEditor={qe}
|
||||
/>
|
||||
</Menu.Item>
|
||||
)}
|
||||
@@ -599,25 +553,6 @@ class SqlEditor extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
renderQueryLimit() {
|
||||
// Adding SQL_MAX_ROW value to dropdown
|
||||
const { maxRow } = this.props;
|
||||
LIMIT_DROPDOWN.push(maxRow);
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
{[...new Set(LIMIT_DROPDOWN)].map(limit => (
|
||||
<Menu.Item key={`${limit}`} onClick={() => this.setQueryLimit(limit)}>
|
||||
{/* // eslint-disable-line no-use-before-define */}
|
||||
<a role="button" styling="link">
|
||||
{this.convertToNumWithSpaces(limit)}
|
||||
</a>{' '}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
async saveQuery(query) {
|
||||
const { queryEditor: qe, actions } = this.props;
|
||||
const savedQuery = await actions.saveQuery(query);
|
||||
@@ -673,11 +608,10 @@ class SqlEditor extends React.PureComponent {
|
||||
? this.props.database.allow_run_async
|
||||
: false
|
||||
}
|
||||
queryEditor={qe}
|
||||
queryState={this.props.latestQuery?.state}
|
||||
runQuery={this.runQuery}
|
||||
selectedText={qe.selectedText}
|
||||
stopQuery={this.stopQuery}
|
||||
sql={this.props.queryEditor.sql}
|
||||
overlayCreateAsMenu={showMenu ? runMenuBtn : null}
|
||||
/>
|
||||
</span>
|
||||
@@ -687,27 +621,17 @@ class SqlEditor extends React.PureComponent {
|
||||
<span>
|
||||
<EstimateQueryCostButton
|
||||
getEstimate={this.getQueryCostEstimate}
|
||||
queryCostEstimate={qe.queryCostEstimate}
|
||||
selectedText={qe.selectedText}
|
||||
queryEditor={qe}
|
||||
tooltip={t('Estimate the cost before running a query')}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
<LimitSelectStyled>
|
||||
<AntdDropdown overlay={this.renderQueryLimit()} trigger="click">
|
||||
<a onClick={e => e.preventDefault()}>
|
||||
<span>LIMIT:</span>
|
||||
<span className="limitDropdown">
|
||||
{this.convertToNumWithSpaces(
|
||||
this.props.queryEditor.queryLimit ||
|
||||
this.props.defaultQueryLimit,
|
||||
)}
|
||||
</span>
|
||||
<Icons.TriangleDown iconColor={theme.colors.grayscale.base} />
|
||||
</a>
|
||||
</AntdDropdown>
|
||||
</LimitSelectStyled>
|
||||
<QueryLimitSelect
|
||||
queryEditor={this.props.queryEditor}
|
||||
maxRow={this.props.maxRow}
|
||||
defaultQueryLimit={this.props.defaultQueryLimit}
|
||||
/>
|
||||
</span>
|
||||
{this.props.latestQuery && (
|
||||
<Timer
|
||||
@@ -721,11 +645,8 @@ class SqlEditor extends React.PureComponent {
|
||||
<div className="rightItems">
|
||||
<span>
|
||||
<SaveQuery
|
||||
query={{
|
||||
...qe,
|
||||
columns: this.props.latestQuery?.results?.columns || [],
|
||||
}}
|
||||
defaultLabel={qe.name || qe.description}
|
||||
queryEditor={qe}
|
||||
columns={this.props.latestQuery?.results?.columns || []}
|
||||
onSave={this.saveQuery}
|
||||
onUpdate={this.props.actions.updateSavedQuery}
|
||||
saveQueryWarning={this.props.saveQueryWarning}
|
||||
@@ -832,12 +753,22 @@ class SqlEditor extends React.PureComponent {
|
||||
SqlEditor.defaultProps = defaultProps;
|
||||
SqlEditor.propTypes = propTypes;
|
||||
|
||||
function mapStateToProps({ sqlLab }, props) {
|
||||
const queryEditor = sqlLab.queryEditors.find(
|
||||
editor => editor.id === props.queryEditorId,
|
||||
);
|
||||
function mapStateToProps({ sqlLab }, { queryEditor }) {
|
||||
let { latestQueryId, dbId } = queryEditor;
|
||||
if (sqlLab.unsavedQueryEditor.id === queryEditor.id) {
|
||||
const { latestQueryId: unsavedQID, dbId: unsavedDBID } =
|
||||
sqlLab.unsavedQueryEditor;
|
||||
latestQueryId = unsavedQID || latestQueryId;
|
||||
dbId = unsavedDBID || dbId;
|
||||
}
|
||||
const database = sqlLab.databases[dbId];
|
||||
const latestQuery = sqlLab.queries[latestQueryId];
|
||||
|
||||
return { sqlLab, ...props, queryEditor, queryEditors: sqlLab.queryEditors };
|
||||
return {
|
||||
queryEditors: sqlLab.queryEditors,
|
||||
latestQuery,
|
||||
database,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
@@ -848,10 +779,10 @@ function mapDispatchToProps(dispatch) {
|
||||
persistEditorHeight,
|
||||
postStopQuery,
|
||||
queryEditorSetAutorun,
|
||||
queryEditorSetQueryLimit,
|
||||
queryEditorSetSql,
|
||||
queryEditorSetAndSaveSql,
|
||||
queryEditorSetTemplateParams,
|
||||
runQueryFromSqlEditor,
|
||||
runQuery,
|
||||
saveQuery,
|
||||
addSavedQueryToTabState,
|
||||
|
||||
@@ -32,7 +32,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 } from 'src/SqlLab/types';
|
||||
import { QueryEditor, SchemaOption } from 'src/SqlLab/types';
|
||||
import { DatabaseObject } from 'src/components/DatabaseSelector';
|
||||
import { EmptyStateSmall } from 'src/components/EmptyState';
|
||||
import {
|
||||
@@ -55,7 +55,10 @@ interface actionsTypes {
|
||||
setDatabases: (arg0: any) => {};
|
||||
addDangerToast: (msg: string) => void;
|
||||
queryEditorSetSchema: (queryEditor: QueryEditor, schema?: string) => void;
|
||||
queryEditorSetSchemaOptions: () => void;
|
||||
queryEditorSetSchemaOptions: (
|
||||
queryEditor: QueryEditor,
|
||||
options: SchemaOption[],
|
||||
) => void;
|
||||
queryEditorSetTableOptions: (
|
||||
queryEditor: QueryEditor,
|
||||
options: Array<any>,
|
||||
@@ -70,7 +73,6 @@ interface SqlEditorLeftBarProps {
|
||||
actions: actionsTypes & TableElementProps['actions'];
|
||||
database: DatabaseObject;
|
||||
setEmptyState: Dispatch<SetStateAction<boolean>>;
|
||||
showDisabled: boolean;
|
||||
}
|
||||
|
||||
const StyledScrollbarContainer = styled.div`
|
||||
@@ -239,6 +241,15 @@ export default function SqlEditorLeftBar({
|
||||
[actions],
|
||||
);
|
||||
|
||||
const handleSchemasLoad = React.useCallback(
|
||||
(options: Array<any>) => {
|
||||
if (queryEditorRef.current) {
|
||||
actions.queryEditorSetSchemaOptions(queryEditorRef.current, options);
|
||||
}
|
||||
},
|
||||
[actions],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="SqlEditorLeftBar">
|
||||
<TableSelectorMultiple
|
||||
@@ -249,7 +260,7 @@ export default function SqlEditorLeftBar({
|
||||
handleError={actions.addDangerToast}
|
||||
onDbChange={onDbChange}
|
||||
onSchemaChange={handleSchemaChange}
|
||||
onSchemasLoad={actions.queryEditorSetSchemaOptions}
|
||||
onSchemasLoad={handleSchemasLoad}
|
||||
onTableSelectChange={onTablesChange}
|
||||
onTablesLoad={handleTablesLoad}
|
||||
schema={queryEditor.schema}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import {
|
||||
fireEvent,
|
||||
screen,
|
||||
render,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { QueryEditor } from 'src/SqlLab/types';
|
||||
import {
|
||||
initialState,
|
||||
defaultQueryEditor,
|
||||
extraQueryEditor1,
|
||||
extraQueryEditor2,
|
||||
} from 'src/SqlLab/fixtures';
|
||||
import { Store } from 'redux';
|
||||
import {
|
||||
REMOVE_QUERY_EDITOR,
|
||||
QUERY_EDITOR_SET_TITLE,
|
||||
ADD_QUERY_EDITOR,
|
||||
QUERY_EDITOR_TOGGLE_LEFT_BAR,
|
||||
} from 'src/SqlLab/actions/sqlLab';
|
||||
import SqlEditorTabHeader from 'src/SqlLab/components/SqlEditorTabHeader';
|
||||
|
||||
jest.mock('src/components/Select', () => () => (
|
||||
<div data-test="mock-deprecated-select" />
|
||||
));
|
||||
jest.mock('src/components/Select/Select', () => () => (
|
||||
<div data-test="mock-deprecated-select-select" />
|
||||
));
|
||||
jest.mock('src/components/Select/AsyncSelect', () => () => (
|
||||
<div data-test="mock-async-select" />
|
||||
));
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureStore(middlewares);
|
||||
const setup = (queryEditor: QueryEditor, store?: Store) =>
|
||||
render(<SqlEditorTabHeader queryEditor={queryEditor} />, {
|
||||
useRedux: true,
|
||||
...(store && { store }),
|
||||
});
|
||||
|
||||
describe('SqlEditorTabHeader', () => {
|
||||
it('renders name', () => {
|
||||
const { queryByText } = setup(defaultQueryEditor, mockStore(initialState));
|
||||
expect(queryByText(defaultQueryEditor.name)).toBeTruthy();
|
||||
expect(queryByText(extraQueryEditor1.name)).toBeFalsy();
|
||||
expect(queryByText(extraQueryEditor2.name)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('renders name from unsaved changes', () => {
|
||||
const expectedTitle = 'updated title';
|
||||
const { queryByText } = setup(
|
||||
defaultQueryEditor,
|
||||
mockStore({
|
||||
...initialState,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
unsavedQueryEditor: {
|
||||
id: defaultQueryEditor.id,
|
||||
name: expectedTitle,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(queryByText(expectedTitle)).toBeTruthy();
|
||||
expect(queryByText(defaultQueryEditor.name)).toBeFalsy();
|
||||
expect(queryByText(extraQueryEditor1.name)).toBeFalsy();
|
||||
expect(queryByText(extraQueryEditor2.name)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('renders current name for unrelated unsaved changes', () => {
|
||||
const unrelatedTitle = 'updated title';
|
||||
const { queryByText } = setup(
|
||||
defaultQueryEditor,
|
||||
mockStore({
|
||||
...initialState,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
unsavedQueryEditor: {
|
||||
id: `${defaultQueryEditor.id}-other`,
|
||||
name: unrelatedTitle,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(queryByText(defaultQueryEditor.name)).toBeTruthy();
|
||||
expect(queryByText(unrelatedTitle)).toBeFalsy();
|
||||
expect(queryByText(extraQueryEditor1.name)).toBeFalsy();
|
||||
expect(queryByText(extraQueryEditor2.name)).toBeFalsy();
|
||||
});
|
||||
|
||||
describe('with dropdown menus', () => {
|
||||
let store = mockStore();
|
||||
beforeEach(async () => {
|
||||
store = mockStore(initialState);
|
||||
const { getByTestId } = setup(defaultQueryEditor, store);
|
||||
const dropdown = getByTestId('dropdown-trigger');
|
||||
|
||||
userEvent.click(dropdown);
|
||||
});
|
||||
|
||||
it('should dispatch removeQueryEditor action', async () => {
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId('close-tab-menu-option'));
|
||||
|
||||
const actions = store.getActions();
|
||||
await waitFor(() =>
|
||||
expect(actions[0]).toEqual({
|
||||
type: REMOVE_QUERY_EDITOR,
|
||||
queryEditor: defaultQueryEditor,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should dispatch queryEditorSetTitle action', async () => {
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
|
||||
);
|
||||
const expectedTitle = 'typed text';
|
||||
const mockPrompt = jest
|
||||
.spyOn(window, 'prompt')
|
||||
.mockImplementation(() => expectedTitle);
|
||||
fireEvent.click(screen.getByTestId('rename-tab-menu-option'));
|
||||
|
||||
const actions = store.getActions();
|
||||
await waitFor(() =>
|
||||
expect(actions[0]).toEqual({
|
||||
type: QUERY_EDITOR_SET_TITLE,
|
||||
name: expectedTitle,
|
||||
queryEditor: expect.objectContaining({
|
||||
id: defaultQueryEditor.id,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
mockPrompt.mockClear();
|
||||
});
|
||||
|
||||
it('should dispatch toggleLeftBar action', async () => {
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('toggle-menu-option'));
|
||||
|
||||
const actions = store.getActions();
|
||||
await waitFor(() =>
|
||||
expect(actions[0]).toEqual({
|
||||
type: QUERY_EDITOR_TOGGLE_LEFT_BAR,
|
||||
hideLeftBar: !defaultQueryEditor.hideLeftBar,
|
||||
queryEditor: expect.objectContaining({
|
||||
id: defaultQueryEditor.id,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should dispatch removeAllOtherQueryEditors action', async () => {
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('close-all-other-menu-option'));
|
||||
|
||||
const actions = store.getActions();
|
||||
await waitFor(() =>
|
||||
expect(actions).toEqual([
|
||||
{
|
||||
type: REMOVE_QUERY_EDITOR,
|
||||
queryEditor: initialState.sqlLab.queryEditors[1],
|
||||
},
|
||||
{
|
||||
type: REMOVE_QUERY_EDITOR,
|
||||
queryEditor: initialState.sqlLab.queryEditors[2],
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should dispatch cloneQueryToNewTab action', async () => {
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('clone-tab-menu-option'));
|
||||
|
||||
const actions = store.getActions();
|
||||
await waitFor(() =>
|
||||
expect(actions[0]).toEqual({
|
||||
type: ADD_QUERY_EDITOR,
|
||||
queryEditor: expect.objectContaining({
|
||||
name: `Copy of ${defaultQueryEditor.name}`,
|
||||
sql: defaultQueryEditor.sql,
|
||||
autorun: false,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useMemo } from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import { styled, t, QueryState } from '@superset-ui/core';
|
||||
import {
|
||||
removeQueryEditor,
|
||||
removeAllOtherQueryEditors,
|
||||
queryEditorSetTitle,
|
||||
cloneQueryToNewTab,
|
||||
toggleLeftBar,
|
||||
} from 'src/SqlLab/actions/sqlLab';
|
||||
import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
|
||||
import TabStatusIcon from '../TabStatusIcon';
|
||||
|
||||
const TabTitleWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
const TabTitle = styled.span`
|
||||
margin-right: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
text-transform: none;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
queryEditor: QueryEditor;
|
||||
}
|
||||
|
||||
const SqlEditorTabHeader: React.FC<Props> = ({ queryEditor }) => {
|
||||
const qe = useSelector<SqlLabRootState, QueryEditor>(
|
||||
({ sqlLab: { unsavedQueryEditor } }) => ({
|
||||
...queryEditor,
|
||||
...(queryEditor.id === unsavedQueryEditor.id && unsavedQueryEditor),
|
||||
}),
|
||||
shallowEqual,
|
||||
);
|
||||
const queryStatus = useSelector<SqlLabRootState, QueryState>(
|
||||
({ sqlLab }) => sqlLab.queries[qe.latestQueryId || '']?.state || '',
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
const actions = useMemo(
|
||||
() =>
|
||||
bindActionCreators(
|
||||
{
|
||||
removeQueryEditor,
|
||||
removeAllOtherQueryEditors,
|
||||
queryEditorSetTitle,
|
||||
cloneQueryToNewTab,
|
||||
toggleLeftBar,
|
||||
},
|
||||
dispatch,
|
||||
),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
function renameTab() {
|
||||
const newTitle = prompt(t('Enter a new title for the tab'));
|
||||
if (newTitle) {
|
||||
actions.queryEditorSetTitle(qe, newTitle);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TabTitleWrapper>
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
overlay={
|
||||
<Menu style={{ width: 176 }}>
|
||||
<Menu.Item
|
||||
className="close-btn"
|
||||
key="1"
|
||||
onClick={() => actions.removeQueryEditor(qe)}
|
||||
data-test="close-tab-menu-option"
|
||||
>
|
||||
<div className="icon-container">
|
||||
<i className="fa fa-close" />
|
||||
</div>
|
||||
{t('Close tab')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="2"
|
||||
onClick={renameTab}
|
||||
data-test="rename-tab-menu-option"
|
||||
>
|
||||
<div className="icon-container">
|
||||
<i className="fa fa-i-cursor" />
|
||||
</div>
|
||||
{t('Rename tab')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="3"
|
||||
onClick={() => actions.toggleLeftBar(qe)}
|
||||
data-test="toggle-menu-option"
|
||||
>
|
||||
<div className="icon-container">
|
||||
<i className="fa fa-cogs" />
|
||||
</div>
|
||||
{qe.hideLeftBar ? t('Expand tool bar') : t('Hide tool bar')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="4"
|
||||
onClick={() => actions.removeAllOtherQueryEditors(qe)}
|
||||
data-test="close-all-other-menu-option"
|
||||
>
|
||||
<div className="icon-container">
|
||||
<i className="fa fa-times-circle-o" />
|
||||
</div>
|
||||
{t('Close all other tabs')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="5"
|
||||
onClick={() => actions.cloneQueryToNewTab(qe, false)}
|
||||
data-test="clone-tab-menu-option"
|
||||
>
|
||||
<div className="icon-container">
|
||||
<i className="fa fa-files-o" />
|
||||
</div>
|
||||
{t('Duplicate tab')}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
/>
|
||||
<TabTitle>{qe.name}</TabTitle> <TabStatusIcon tabState={queryStatus} />{' '}
|
||||
</TabTitleWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SqlEditorTabHeader;
|
||||
@@ -30,6 +30,7 @@ import { EditableTabs } from 'src/components/Tabs';
|
||||
import TabbedSqlEditors from 'src/SqlLab/components/TabbedSqlEditors';
|
||||
import SqlEditor from 'src/SqlLab/components/SqlEditor';
|
||||
import { table, initialState } from 'src/SqlLab/fixtures';
|
||||
import { newQueryTabName } from 'src/SqlLab/utils/newQueryTabName';
|
||||
|
||||
fetchMock.get('glob:*/api/v1/database/*', {});
|
||||
fetchMock.get('glob:*/savedqueryviewapi/api/get/*', {});
|
||||
@@ -150,18 +151,6 @@ describe('TabbedSqlEditors', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
it('should rename Tab', () => {
|
||||
global.prompt = () => 'new title';
|
||||
wrapper = getWrapper();
|
||||
sinon.stub(wrapper.instance().props.actions, 'queryEditorSetTitle');
|
||||
|
||||
wrapper.instance().renameTab(queryEditors[0]);
|
||||
expect(
|
||||
wrapper.instance().props.actions.queryEditorSetTitle.getCall(0).args[1],
|
||||
).toBe('new title');
|
||||
|
||||
delete global.prompt;
|
||||
});
|
||||
it('should removeQueryEditor', () => {
|
||||
wrapper = getWrapper();
|
||||
sinon.stub(wrapper.instance().props.actions, 'removeQueryEditor');
|
||||
@@ -183,11 +172,11 @@ describe('TabbedSqlEditors', () => {
|
||||
it('should properly increment query tab name', () => {
|
||||
wrapper = getWrapper();
|
||||
sinon.stub(wrapper.instance().props.actions, 'addQueryEditor');
|
||||
|
||||
const newTitle = newQueryTabName(wrapper.instance().props.queryEditors);
|
||||
wrapper.instance().newQueryEditor();
|
||||
expect(
|
||||
wrapper.instance().props.actions.addQueryEditor.getCall(0).args[0].name,
|
||||
).toContain('Untitled Query 2');
|
||||
).toContain(newTitle);
|
||||
});
|
||||
it('should duplicate query editor', () => {
|
||||
wrapper = getWrapper();
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { EditableTabs } from 'src/components/Tabs';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import URI from 'urijs';
|
||||
@@ -33,7 +31,7 @@ import * as Actions from 'src/SqlLab/actions/sqlLab';
|
||||
import { EmptyStateBig } from 'src/components/EmptyState';
|
||||
import { newQueryTabName } from '../../utils/newQueryTabName';
|
||||
import SqlEditor from '../SqlEditor';
|
||||
import TabStatusIcon from '../TabStatusIcon';
|
||||
import SqlEditorTabHeader from '../SqlEditorTabHeader';
|
||||
|
||||
const propTypes = {
|
||||
actions: PropTypes.object.isRequired,
|
||||
@@ -44,7 +42,6 @@ const propTypes = {
|
||||
databases: PropTypes.object.isRequired,
|
||||
queries: PropTypes.object.isRequired,
|
||||
queryEditors: PropTypes.array,
|
||||
requestedQuery: PropTypes.object,
|
||||
tabHistory: PropTypes.array.isRequired,
|
||||
tables: PropTypes.array.isRequired,
|
||||
offline: PropTypes.bool,
|
||||
@@ -54,16 +51,10 @@ const propTypes = {
|
||||
const defaultProps = {
|
||||
queryEditors: [],
|
||||
offline: false,
|
||||
requestedQuery: null,
|
||||
saveQueryWarning: null,
|
||||
scheduleQueryWarning: null,
|
||||
};
|
||||
|
||||
const TabTitleWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledTab = styled.span`
|
||||
line-height: 24px;
|
||||
`;
|
||||
@@ -86,10 +77,6 @@ class TabbedSqlEditors extends React.PureComponent {
|
||||
dataPreviewQueries: [],
|
||||
};
|
||||
this.removeQueryEditor = this.removeQueryEditor.bind(this);
|
||||
this.renameTab = this.renameTab.bind(this);
|
||||
this.toggleLeftBar = this.toggleLeftBar.bind(this);
|
||||
this.removeAllOtherQueryEditors =
|
||||
this.removeAllOtherQueryEditors.bind(this);
|
||||
this.duplicateQueryEditor = this.duplicateQueryEditor.bind(this);
|
||||
this.handleSelect = this.handleSelect.bind(this);
|
||||
this.handleEdit = this.handleEdit.bind(this);
|
||||
@@ -236,14 +223,6 @@ class TabbedSqlEditors extends React.PureComponent {
|
||||
window.history.replaceState({}, document.title, this.state.sqlLabUrl);
|
||||
}
|
||||
|
||||
renameTab(qe) {
|
||||
/* eslint no-alert: 0 */
|
||||
const newTitle = prompt(t('Enter a new title for the tab'));
|
||||
if (newTitle) {
|
||||
this.props.actions.queryEditorSetTitle(qe, newTitle);
|
||||
}
|
||||
}
|
||||
|
||||
activeQueryEditor() {
|
||||
if (this.props.tabHistory.length === 0) {
|
||||
return this.props.queryEditors[0];
|
||||
@@ -304,106 +283,34 @@ class TabbedSqlEditors extends React.PureComponent {
|
||||
this.props.actions.removeQueryEditor(qe);
|
||||
}
|
||||
|
||||
removeAllOtherQueryEditors(cqe) {
|
||||
this.props.queryEditors.forEach(
|
||||
qe => qe !== cqe && this.removeQueryEditor(qe),
|
||||
);
|
||||
}
|
||||
|
||||
duplicateQueryEditor(qe) {
|
||||
this.props.actions.cloneQueryToNewTab(qe, false);
|
||||
}
|
||||
|
||||
toggleLeftBar(qe) {
|
||||
this.props.actions.toggleLeftBar(qe);
|
||||
}
|
||||
|
||||
render() {
|
||||
const noQueryEditors = this.props.queryEditors?.length === 0;
|
||||
const editors = this.props.queryEditors.map(qe => {
|
||||
let latestQuery;
|
||||
if (qe.latestQueryId) {
|
||||
latestQuery = this.props.queries[qe.latestQueryId];
|
||||
}
|
||||
let database;
|
||||
if (qe.dbId) {
|
||||
database = this.props.databases[qe.dbId];
|
||||
}
|
||||
const state = latestQuery ? latestQuery.state : '';
|
||||
|
||||
const menu = (
|
||||
<Menu style={{ width: 176 }}>
|
||||
<Menu.Item
|
||||
className="close-btn"
|
||||
key="1"
|
||||
onClick={() => this.removeQueryEditor(qe)}
|
||||
data-test="close-tab-menu-option"
|
||||
>
|
||||
<div className="icon-container">
|
||||
<i className="fa fa-close" />
|
||||
</div>
|
||||
{t('Close tab')}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="2" onClick={() => this.renameTab(qe)}>
|
||||
<div className="icon-container">
|
||||
<i className="fa fa-i-cursor" />
|
||||
</div>
|
||||
{t('Rename tab')}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="3" onClick={() => this.toggleLeftBar(qe)}>
|
||||
<div className="icon-container">
|
||||
<i className="fa fa-cogs" />
|
||||
</div>
|
||||
{qe.hideLeftBar ? t('Expand tool bar') : t('Hide tool bar')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="4"
|
||||
onClick={() => this.removeAllOtherQueryEditors(qe)}
|
||||
>
|
||||
<div className="icon-container">
|
||||
<i className="fa fa-times-circle-o" />
|
||||
</div>
|
||||
{t('Close all other tabs')}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="5" onClick={() => this.duplicateQueryEditor(qe)}>
|
||||
<div className="icon-container">
|
||||
<i className="fa fa-files-o" />
|
||||
</div>
|
||||
{t('Duplicate tab')}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
const tabHeader = (
|
||||
<TabTitleWrapper>
|
||||
<Dropdown overlay={menu} trigger={['click']} />
|
||||
<TabTitle>{qe.name}</TabTitle> <TabStatusIcon tabState={state} />{' '}
|
||||
</TabTitleWrapper>
|
||||
);
|
||||
return (
|
||||
<EditableTabs.TabPane
|
||||
key={qe.id}
|
||||
tab={tabHeader}
|
||||
// for tests - key prop isn't handled by enzyme well bcs it's a react keyword
|
||||
data-key={qe.id}
|
||||
>
|
||||
<SqlEditor
|
||||
tables={this.props.tables.filter(xt => xt.queryEditorId === qe.id)}
|
||||
queryEditorId={qe.id}
|
||||
editorQueries={this.state.queriesArray}
|
||||
dataPreviewQueries={this.state.dataPreviewQueries}
|
||||
latestQuery={latestQuery}
|
||||
database={database}
|
||||
actions={this.props.actions}
|
||||
hideLeftBar={qe.hideLeftBar}
|
||||
defaultQueryLimit={this.props.defaultQueryLimit}
|
||||
maxRow={this.props.maxRow}
|
||||
displayLimit={this.props.displayLimit}
|
||||
saveQueryWarning={this.props.saveQueryWarning}
|
||||
scheduleQueryWarning={this.props.scheduleQueryWarning}
|
||||
/>
|
||||
</EditableTabs.TabPane>
|
||||
);
|
||||
});
|
||||
const editors = this.props.queryEditors?.map(qe => (
|
||||
<EditableTabs.TabPane
|
||||
key={qe.id}
|
||||
tab={<SqlEditorTabHeader queryEditor={qe} />}
|
||||
// for tests - key prop isn't handled by enzyme well bcs it's a react keyword
|
||||
data-key={qe.id}
|
||||
>
|
||||
<SqlEditor
|
||||
tables={this.props.tables.filter(xt => xt.queryEditorId === qe.id)}
|
||||
queryEditor={qe}
|
||||
editorQueries={this.state.queriesArray}
|
||||
dataPreviewQueries={this.state.dataPreviewQueries}
|
||||
actions={this.props.actions}
|
||||
hideLeftBar={qe.hideLeftBar}
|
||||
defaultQueryLimit={this.props.defaultQueryLimit}
|
||||
maxRow={this.props.maxRow}
|
||||
displayLimit={this.props.displayLimit}
|
||||
saveQueryWarning={this.props.saveQueryWarning}
|
||||
scheduleQueryWarning={this.props.scheduleQueryWarning}
|
||||
/>
|
||||
</EditableTabs.TabPane>
|
||||
));
|
||||
|
||||
const emptyTab = (
|
||||
<StyledTab>
|
||||
@@ -472,7 +379,7 @@ class TabbedSqlEditors extends React.PureComponent {
|
||||
TabbedSqlEditors.propTypes = propTypes;
|
||||
TabbedSqlEditors.defaultProps = defaultProps;
|
||||
|
||||
function mapStateToProps({ sqlLab, common, requestedQuery }) {
|
||||
function mapStateToProps({ sqlLab, common }) {
|
||||
return {
|
||||
databases: sqlLab.databases,
|
||||
queryEditors: sqlLab.queryEditors,
|
||||
@@ -486,7 +393,6 @@ function mapStateToProps({ sqlLab, common, requestedQuery }) {
|
||||
maxRow: common.conf.SQL_MAX_ROW,
|
||||
saveQueryWarning: common.conf.SQLLAB_SAVE_WARNING_MESSAGE,
|
||||
scheduleQueryWarning: common.conf.SQLLAB_SCHEDULE_WARNING_MESSAGE,
|
||||
requestedQuery,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
|
||||
@@ -17,38 +17,100 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { Store } from 'redux';
|
||||
import {
|
||||
render,
|
||||
fireEvent,
|
||||
getByText,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { ThemeProvider, supersetTheme } from '@superset-ui/core';
|
||||
import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
|
||||
|
||||
import TemplateParamsEditor from 'src/SqlLab/components/TemplateParamsEditor';
|
||||
import TemplateParamsEditor, {
|
||||
Props,
|
||||
} from 'src/SqlLab/components/TemplateParamsEditor';
|
||||
|
||||
const ThemeWrapper = ({ children }: { children: ReactNode }) => (
|
||||
<ThemeProvider theme={supersetTheme}>{children}</ThemeProvider>
|
||||
);
|
||||
jest.mock('src/components/Select', () => () => (
|
||||
<div data-test="mock-deprecated-select" />
|
||||
));
|
||||
jest.mock('src/components/Select/Select', () => () => (
|
||||
<div data-test="mock-deprecated-select-select" />
|
||||
));
|
||||
jest.mock('src/components/Select/AsyncSelect', () => () => (
|
||||
<div data-test="mock-async-select" />
|
||||
));
|
||||
jest.mock('src/components/AsyncAceEditor', () => ({
|
||||
ConfigEditor: ({ value }: { value: string }) => (
|
||||
<div data-test="mock-async-ace-editor">{value}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureStore(middlewares);
|
||||
const setup = (otherProps: Partial<Props> = {}, store?: Store) =>
|
||||
render(
|
||||
<TemplateParamsEditor
|
||||
language="json"
|
||||
onChange={() => {}}
|
||||
queryEditor={defaultQueryEditor}
|
||||
{...otherProps}
|
||||
/>,
|
||||
{
|
||||
useRedux: true,
|
||||
store: mockStore(initialState),
|
||||
...(store && { store }),
|
||||
},
|
||||
);
|
||||
|
||||
describe('TemplateParamsEditor', () => {
|
||||
it('should render with a title', () => {
|
||||
const { container } = render(
|
||||
<TemplateParamsEditor code="FOO" language="json" onChange={() => {}} />,
|
||||
{ wrapper: ThemeWrapper },
|
||||
);
|
||||
const { container } = setup();
|
||||
expect(container.querySelector('div[role="button"]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open a modal with the ace editor', async () => {
|
||||
const { container, baseElement } = render(
|
||||
<TemplateParamsEditor code="FOO" language="json" onChange={() => {}} />,
|
||||
{ wrapper: ThemeWrapper },
|
||||
const { container, getByTestId } = setup();
|
||||
fireEvent.click(getByText(container, 'Parameters'));
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('mock-async-ace-editor')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders templateParams', async () => {
|
||||
const { container, getByTestId } = setup();
|
||||
fireEvent.click(getByText(container, 'Parameters'));
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('mock-async-ace-editor')).toBeInTheDocument();
|
||||
});
|
||||
expect(getByTestId('mock-async-ace-editor')).toHaveTextContent(
|
||||
defaultQueryEditor.templateParams,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders code from unsaved changes', async () => {
|
||||
const expectedCode = 'custom code value';
|
||||
const { container, getByTestId } = setup(
|
||||
{},
|
||||
mockStore({
|
||||
...initialState,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
unsavedQueryEditor: {
|
||||
id: defaultQueryEditor.id,
|
||||
templateParams: expectedCode,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
fireEvent.click(getByText(container, 'Parameters'));
|
||||
await waitFor(() => {
|
||||
expect(baseElement.querySelector('#ace-editor')).toBeInTheDocument();
|
||||
expect(getByTestId('mock-async-ace-editor')).toBeInTheDocument();
|
||||
});
|
||||
expect(getByTestId('mock-async-ace-editor')).toHaveTextContent(
|
||||
expectedCode,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,9 @@ import ModalTrigger from 'src/components/ModalTrigger';
|
||||
import { ConfigEditor } from 'src/components/AsyncAceEditor';
|
||||
import { FAST_DEBOUNCE } from 'src/constants';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
|
||||
import { getUpToDateQuery } from 'src/SqlLab/actions/sqlLab';
|
||||
|
||||
const StyledConfigEditor = styled(ConfigEditor)`
|
||||
&.ace_editor {
|
||||
@@ -33,17 +36,24 @@ const StyledConfigEditor = styled(ConfigEditor)`
|
||||
}
|
||||
`;
|
||||
|
||||
function TemplateParamsEditor({
|
||||
code = '{}',
|
||||
language,
|
||||
onChange = () => {},
|
||||
}: {
|
||||
code: string;
|
||||
export type Props = {
|
||||
queryEditor: QueryEditor;
|
||||
language: 'yaml' | 'json';
|
||||
onChange: () => void;
|
||||
}) {
|
||||
};
|
||||
|
||||
function TemplateParamsEditor({
|
||||
queryEditor,
|
||||
language,
|
||||
onChange = () => {},
|
||||
}: Props) {
|
||||
const [parsedJSON, setParsedJSON] = useState({});
|
||||
const [isValid, setIsValid] = useState(true);
|
||||
const code = useSelector<SqlLabRootState, string>(
|
||||
rootState =>
|
||||
(getUpToDateQuery(rootState, queryEditor) as unknown as QueryEditor)
|
||||
.templateParams || '{}',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user