feat(SqlLab): Change Save Dataset Button to Split Save Query Button IV (#20852)

* Moving entire split save btn PR

* Addressed review comments

* Remove arbitrary div from ErrorBoundary in Chart

* Added accidentally removed comment

* Fix act errors in SaveQuery tests

* Fix SaveDatasetActionButton test

* SaveDatasetModal test almost working

* SaveDatasetModal tests all passing

* Clean SaveDatasetModal test

* Fix create chart button and SaveDatasetModal text in SQL Lab

* Fix untitled dataset name on SaveDatasetModal in explore

* Fix styling on split save button
This commit is contained in:
Lyndsi Kay Williams
2022-08-01 14:36:34 -05:00
committed by GitHub
parent 3a11856ecb
commit 8a04536f9d
25 changed files with 515 additions and 196 deletions

View File

@@ -188,7 +188,7 @@ export default class ResultSet extends React.PureComponent<
popSelectStar(tempSchema: string | null, tempTable: string) {
const qe = {
id: shortid.generate(),
title: tempTable,
name: tempTable,
autorun: false,
dbId: this.props.query.dbId,
sql: `SELECT * FROM ${tempSchema ? `${tempSchema}.` : ''}${tempTable}`,
@@ -281,11 +281,8 @@ export default class ResultSet extends React.PureComponent<
this.props.database?.allows_virtual_table_explore && (
<ExploreResultsButton
database={this.props.database}
onClick={() => this.setState({ showSaveDatasetModal: true })}
onClick={this.createExploreResultsOnClick}
/>
// In order to use the new workflow for a query powered chart, replace the
// above function with:
// onClick={this.createExploreResultsOnClick}
)}
{this.props.csv && (
<Button buttonSize="small" href={`/superset/csv/${query.id}`}>

View File

@@ -20,13 +20,11 @@ import React, { useMemo } from 'react';
import { t, styled, useTheme } from '@superset-ui/core';
import { Menu } from 'src/components/Menu';
import Button, { ButtonProps } from 'src/components/Button';
import Button from 'src/components/Button';
import Icons from 'src/components/Icons';
import {
DropdownButton,
DropdownButtonProps,
} from 'src/components/DropdownButton';
import { DropdownButton } from 'src/components/DropdownButton';
import { detectOS } from 'src/utils/common';
import { QueryButtonProps } from 'src/SqlLab/types';
interface Props {
allowAsync: boolean;
@@ -38,8 +36,6 @@ interface Props {
overlayCreateAsMenu: typeof Menu | null;
}
type QueryButtonProps = DropdownButtonProps | ButtonProps;
const buildText = (
shouldShowStopButton: boolean,
selectedText: string | undefined,
@@ -80,7 +76,7 @@ const StyledButton = styled.span`
}
span[name='caret-down'] {
display: flex;
margin-right: ${({ theme }) => theme.gridUnit * -2}px;
margin-left: ${({ theme }) => theme.gridUnit * 1}px;
}
}
`;

View File

@@ -0,0 +1,62 @@
/**
* 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 { render, screen } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { Menu } from 'src/components/Menu';
import SaveDatasetActionButton from 'src/SqlLab/components/SaveDatasetActionButton';
const overlayMenu = (
<Menu>
<Menu.Item>Save dataset</Menu.Item>
</Menu>
);
describe('SaveDatasetActionButton', () => {
it('renders a split save button', () => {
render(
<SaveDatasetActionButton
setShowSave={() => true}
overlayMenu={overlayMenu}
/>,
);
const saveBtn = screen.getByRole('button', { name: /save/i });
const caretBtn = screen.getByRole('button', { name: /caret-down/i });
expect(saveBtn).toBeVisible();
expect(caretBtn).toBeVisible();
});
it('renders a "save dataset" dropdown menu item when user clicks caret button', () => {
render(
<SaveDatasetActionButton
setShowSave={() => true}
overlayMenu={overlayMenu}
/>,
);
const caretBtn = screen.getByRole('button', { name: /caret-down/i });
userEvent.click(caretBtn);
const saveDatasetMenuItem = screen.getByText(/save dataset/i);
expect(saveDatasetMenuItem).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,83 @@
/**
* 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 { t, useTheme, styled } from '@superset-ui/core';
import Icons from 'src/components/Icons';
import { DropdownButton } from 'src/components/DropdownButton';
import Button from 'src/components/Button';
import { DropdownButtonProps } from 'antd/lib/dropdown';
interface Props {
setShowSave: (arg0: boolean) => void;
overlayMenu: JSX.Element | null;
}
export default function SaveDatasetActionButton({
setShowSave,
overlayMenu,
}: Props) {
const theme = useTheme();
const StyledDropdownButton = styled(
DropdownButton as React.FC<DropdownButtonProps>,
)`
&.ant-dropdown-button button.ant-btn.ant-btn-default {
&:first-of-type {
width: ${theme.gridUnit * 16}px;
}
font-weight: ${theme.gridUnit * 150};
background-color: ${theme.colors.primary.light4};
color: ${theme.colors.primary.dark1};
&:nth-child(2) {
&:before,
&:hover:before {
border-left: 2px solid ${theme.colors.primary.dark2};
}
}
}
span[name='caret-down'] {
margin-left: ${theme.gridUnit * 1}px;
color: ${theme.colors.primary.dark2};
}
`;
return !overlayMenu ? (
<Button
onClick={() => setShowSave(true)}
buttonStyle="primary"
css={{ width: theme.gridUnit * 25 }}
>
{t('Save')}
</Button>
) : (
<StyledDropdownButton
onClick={() => setShowSave(true)}
overlay={overlayMenu}
icon={
<Icons.CaretDown
iconColor={theme.colors.grayscale.light5}
name="caret-down"
/>
}
trigger={['click']}
>
{t('Save')}
</StyledDropdownButton>
);
}

View File

@@ -17,35 +17,12 @@
* under the License.
*/
import React from 'react';
import {
ISaveableDatasource,
SaveDatasetModal,
} from 'src/SqlLab/components/SaveDatasetModal';
import { render, screen } from 'spec/helpers/testing-library';
import { DatasourceType } from '@superset-ui/core';
const testQuery: ISaveableDatasource = {
name: 'unimportant',
dbId: 1,
sql: 'SELECT *',
columns: [
{
name: 'Column 1',
type: DatasourceType.Query,
is_dttm: false,
},
{
name: 'Column 3',
type: DatasourceType.Query,
is_dttm: false,
},
{
name: 'Column 2',
type: DatasourceType.Query,
is_dttm: true,
},
],
};
import * as reactRedux from 'react-redux';
import { render, screen, waitFor, within } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock';
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
import { user, testQuery, mockdatasets } from 'src/SqlLab/fixtures';
const mockedProps = {
visible: true,
@@ -55,8 +32,17 @@ const mockedProps = {
datasource: testQuery,
};
describe('SaveDatasetModal RTL', () => {
it('renders a "Save as new" field', () => {
fetchMock.get('glob:*/api/v1/dataset?*', {
result: mockdatasets,
dataset_count: 3,
});
// Mock the user
const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
beforeEach(() => useSelectorMock.mockClear());
describe('SaveDatasetModal', () => {
it('renders a "Save as new" field', async () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
const saveRadioBtn = screen.getByRole('radio', {
@@ -73,7 +59,7 @@ describe('SaveDatasetModal RTL', () => {
expect(inputFieldText).toBeVisible();
});
it('renders an "Overwrite existing" field', () => {
it('renders an "Overwrite existing" field', async () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
const overwriteRadioBtn = screen.getByRole('radio', {
@@ -89,15 +75,61 @@ describe('SaveDatasetModal RTL', () => {
expect(placeholderText).toBeVisible();
});
it('renders a save button', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
expect(screen.getByRole('button', { name: /save/i })).toBeVisible();
});
it('renders a close button', () => {
it('renders a close button', async () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
expect(screen.getByRole('button', { name: /close/i })).toBeVisible();
});
it('renders a save button when "Save as new" is selected', async () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
// "Save as new" is selected when the modal opens by default
expect(screen.getByRole('button', { name: /save/i })).toBeVisible();
});
it('renders a back and overwrite button when "Overwrite existing" is selected', async () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
// Click the overwrite radio button to reveal the overwrite confirmation and back buttons
const overwriteRadioBtn = screen.getByRole('radio', {
name: /overwrite existing/i,
});
userEvent.click(overwriteRadioBtn);
expect(screen.getByRole('button', { name: /back/i })).toBeVisible();
expect(screen.getByRole('button', { name: /overwrite/i })).toBeVisible();
});
it('renders the overwrite button as disabled until an existing dataset is selected', async () => {
useSelectorMock.mockReturnValue({ ...user });
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
// Click the overwrite radio button
const overwriteRadioBtn = screen.getByRole('radio', {
name: /overwrite existing/i,
});
await waitFor(async () => {
userEvent.click(overwriteRadioBtn);
});
// Overwrite confirmation button should be disabled at this point
const overwriteConfirmationBtn = screen.getByRole('button', {
name: /overwrite/i,
});
expect(overwriteConfirmationBtn).toBeDisabled();
// Click the select component
const select = screen.getByRole('combobox', { name: /existing dataset/i })!;
await waitFor(async () => userEvent.click(select));
// Select the first "existing dataset" from the listbox
const option = within(
document.querySelector('.rc-virtual-list')!,
).getByText('coolest table 0')!;
userEvent.click(option);
// Overwrite button should now be enabled
expect(overwriteConfirmationBtn).toBeEnabled();
});
});

View File

@@ -186,6 +186,11 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
...(formData || {}),
};
const handleOverwriteDataset = async () => {
// if user wants to overwrite a dataset we need to prompt them
if (!shouldOverwriteDataset) {
setShouldOverwriteDataset(true);
return;
}
const [, key] = await Promise.all([
updateDataset(
datasource?.dbId,
@@ -258,12 +263,6 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
);
const handleSaveInDataset = () => {
// if user wants to overwrite a dataset we need to prompt them
if (newOrOverwrite === DatasetRadioState.OVERWRITE_DATASET) {
setShouldOverwriteDataset(true);
return;
}
const selectedColumns = datasource?.columns ?? [];
// The filters param is only used to test jinja templates.
@@ -347,7 +346,7 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
onHide={onHide}
footer={
<>
{!shouldOverwriteDataset && (
{newOrOverwrite === DatasetRadioState.SAVE_NEW && (
<Button
disabled={disableSaveAndExploreBtn}
buttonStyle="primary"
@@ -356,7 +355,7 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
{buttonTextOnSave}
</Button>
)}
{shouldOverwriteDataset && (
{newOrOverwrite === DatasetRadioState.OVERWRITE_DATASET && (
<>
<Button onClick={handleOverwriteCancel}>Back</Button>
<Button

View File

@@ -17,60 +17,87 @@
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import * as sinon from 'sinon';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import SaveQuery from 'src/SqlLab/components/SaveQuery';
import Modal from 'src/components/Modal';
import Button from 'src/components/Button';
import { FormItem } from 'src/components/Form';
import { databases } from 'src/SqlLab/fixtures';
const mockedProps = {
query: {
dbId: 1,
schema: 'main',
sql: 'SELECT * FROM t',
},
defaultLabel: 'untitled',
animation: false,
database: databases.result[0],
onUpdate: () => {},
onSave: () => {},
};
const splitSaveBtnProps = {
...mockedProps,
database: {
...mockedProps.database,
allows_virtual_table_explore: true,
},
};
describe('SavedQuery', () => {
const mockedProps = {
query: {
dbId: 1,
schema: 'main',
sql: 'SELECT * FROM t',
},
defaultLabel: 'untitled',
animation: false,
};
it('is valid', () => {
expect(React.isValidElement(<SaveQuery />)).toBe(true);
});
it('is valid with props', () => {
expect(React.isValidElement(<SaveQuery {...mockedProps} />)).toBe(true);
});
it('has a Modal', () => {
const wrapper = shallow(<SaveQuery {...mockedProps} />);
expect(wrapper.find(Modal)).toExist();
});
// TODO: eschutho convert test to RTL
// eslint-disable-next-line jest/no-disabled-tests
it.skip('has a cancel button', () => {
const wrapper = shallow(<SaveQuery {...mockedProps} />);
const modal = wrapper.find(Modal);
it('renders a non-split save button when allows_virtual_table_explore is not enabled', () => {
render(<SaveQuery {...mockedProps} />, { useRedux: true });
expect(modal.find('[data-test="cancel-query"]')).toHaveLength(1);
});
it('has 2 FormItem', () => {
const wrapper = shallow(<SaveQuery {...mockedProps} />);
const modal = wrapper.find(Modal);
const saveBtn = screen.getByRole('button', { name: /save/i });
expect(modal.find(FormItem)).toHaveLength(2);
expect(saveBtn).toBeVisible();
});
// eslint-disable-next-line jest/no-disabled-tests
it.skip('has a save button if this is a new query', () => {
const saveSpy = sinon.spy();
const wrapper = shallow(<SaveQuery {...mockedProps} onSave={saveSpy} />);
const modal = wrapper.find(Modal);
expect(modal.find(Button)).toHaveLength(2);
modal.find(Button).at(0).simulate('click');
expect(saveSpy.calledOnce).toBe(true);
it('renders a save query modal when user clicks save button', () => {
render(<SaveQuery {...mockedProps} />, { useRedux: true });
const saveBtn = screen.getByRole('button', { name: /save/i });
userEvent.click(saveBtn);
const saveQueryModalHeader = screen.getByRole('heading', {
name: /save query/i,
});
expect(saveQueryModalHeader).toBeVisible();
});
// eslint-disable-next-line jest/no-disabled-tests
it.skip('has an update button if this is an existing query', () => {
const updateSpy = sinon.spy();
it('renders the save query modal UI', () => {
render(<SaveQuery {...mockedProps} />, { useRedux: true });
const saveBtn = screen.getByRole('button', { name: /save/i });
userEvent.click(saveBtn);
const closeBtn = screen.getByRole('button', { name: /close/i });
const saveQueryModalHeader = screen.getByRole('heading', {
name: /save query/i,
});
const nameLabel = screen.getByText(/name/i);
const descriptionLabel = screen.getByText(/description/i);
const textBoxes = screen.getAllByRole('textbox');
const nameTextbox = textBoxes[0];
const descriptionTextbox = textBoxes[1];
// There are now two save buttons, the initial save button and the modal save button
const saveBtns = screen.getAllByRole('button', { name: /save/i });
const cancelBtn = screen.getByRole('button', { name: /cancel/i });
expect(closeBtn).toBeVisible();
expect(saveQueryModalHeader).toBeVisible();
expect(nameLabel).toBeVisible();
expect(descriptionLabel).toBeVisible();
expect(textBoxes.length).toBe(2);
expect(nameTextbox).toBeVisible();
expect(descriptionTextbox).toBeVisible();
expect(saveBtns.length).toBe(2);
expect(saveBtns[0]).toBeVisible();
expect(saveBtns[1]).toBeVisible();
expect(cancelBtn).toBeVisible();
});
it('renders a "save as new" and "update" button if query already exists', () => {
const props = {
...mockedProps,
query: {
@@ -78,11 +105,81 @@ describe('SavedQuery', () => {
remoteId: '42',
},
};
const wrapper = shallow(<SaveQuery {...props} onUpdate={updateSpy} />);
const modal = wrapper.find(Modal);
render(<SaveQuery {...props} />, { useRedux: true });
expect(modal.find(Button)).toHaveLength(3);
modal.find(Button).at(0).simulate('click');
expect(updateSpy.calledOnce).toBe(true);
const saveBtn = screen.getByRole('button', { name: /save/i });
userEvent.click(saveBtn);
const saveAsNewBtn = screen.getByRole('button', { name: /save as new/i });
const updateBtn = screen.getByRole('button', { name: /update/i });
expect(saveAsNewBtn).toBeVisible();
expect(updateBtn).toBeVisible();
});
it('renders a split save button when allows_virtual_table_explore is enabled', async () => {
render(<SaveQuery {...splitSaveBtnProps} />, { useRedux: true });
await waitFor(() => {
const saveBtn = screen.getByRole('button', { name: /save/i });
const caretBtn = screen.getByRole('button', { name: /caret-down/i });
expect(saveBtn).toBeVisible();
expect(caretBtn).toBeVisible();
});
});
it('renders a save dataset modal when user clicks "save dataset" menu item', async () => {
render(<SaveQuery {...splitSaveBtnProps} />, { useRedux: true });
await waitFor(() => {
const caretBtn = screen.getByRole('button', { name: /caret-down/i });
userEvent.click(caretBtn);
const saveDatasetMenuItem = screen.getByText(/save dataset/i);
userEvent.click(saveDatasetMenuItem);
});
const saveDatasetHeader = screen.getByText(/save or overwrite dataset/i);
expect(saveDatasetHeader).toBeVisible();
});
it('renders the save dataset modal UI', async () => {
render(<SaveQuery {...splitSaveBtnProps} />, { useRedux: true });
await waitFor(() => {
const caretBtn = screen.getByRole('button', { name: /caret-down/i });
userEvent.click(caretBtn);
const saveDatasetMenuItem = screen.getByText(/save dataset/i);
userEvent.click(saveDatasetMenuItem);
});
const closeBtn = screen.getByRole('button', { name: /close/i });
const saveDatasetHeader = screen.getByText(/save or overwrite dataset/i);
const saveRadio = screen.getByRole('radio', {
name: /save as new untitled dataset/i,
});
const saveLabel = screen.getByText(/save as new/i);
const saveTextbox = screen.getByRole('textbox');
const overwriteRadio = screen.getByRole('radio', {
name: /overwrite existing/i,
});
const overwriteLabel = screen.getByText(/overwrite existing/i);
const overwriteCombobox = screen.getByRole('combobox');
const overwritePlaceholderText = screen.getByText(
/select or type dataset name/i,
);
expect(saveDatasetHeader).toBeVisible();
expect(closeBtn).toBeVisible();
expect(saveRadio).toBeVisible();
expect(saveLabel).toBeVisible();
expect(saveTextbox).toBeVisible();
expect(overwriteRadio).toBeVisible();
expect(overwriteLabel).toBeVisible();
expect(overwriteCombobox).toBeVisible();
expect(overwritePlaceholderText).toBeVisible();
});
});

View File

@@ -21,21 +21,11 @@ import { Row, Col } from 'src/components';
import { Input, TextArea } from 'src/components/Input';
import { t, styled } from '@superset-ui/core';
import Button from 'src/components/Button';
import { Menu } from 'src/components/Menu';
import { Form, FormItem } from 'src/components/Form';
import Modal from 'src/components/Modal';
import Icons from 'src/components/Icons';
const Styles = styled.span`
span[role='img'] {
display: flex;
margin: 0;
color: ${({ theme }) => theme.colors.grayscale.base};
svg {
vertical-align: -${({ theme }) => theme.gridUnit * 1.25}px;
margin: 0;
}
}
`;
import SaveDatasetActionButton from 'src/SqlLab/components/SaveDatasetActionButton';
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
interface SaveQueryProps {
query: any;
@@ -43,6 +33,7 @@ interface SaveQueryProps {
onSave: (arg0: QueryPayload) => void;
onUpdate: (arg0: QueryPayload) => void;
saveQueryWarning: string | null;
database: Record<string, any>;
}
type QueryPayload = {
@@ -68,37 +59,57 @@ type QueryPayload = {
type: string;
value: string;
}>;
title: string;
name: string;
};
const Styles = styled.span`
span[role='img'] {
display: flex;
margin: 0;
color: ${({ theme }) => theme.colors.grayscale.base};
svg {
vertical-align: -${({ theme }) => theme.gridUnit * 1.25}px;
margin: 0;
}
}
`;
export default function SaveQuery({
query,
defaultLabel = t('Undefined'),
onSave = () => {},
onUpdate,
saveQueryWarning = null,
database,
}: SaveQueryProps) {
const [description, setDescription] = useState<string>(
query.description || '',
);
const [label, setLabel] = useState<string>(defaultLabel);
const [showSave, setShowSave] = useState<boolean>(false);
const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false);
const isSaved = !!query.remoteId;
const canExploreDatabase = !!database?.allows_virtual_table_explore;
const overlayMenu = (
<Menu>
<Menu.Item onClick={() => setShowSaveDatasetModal(true)}>
{t('Save dataset')}
</Menu.Item>
</Menu>
);
const queryPayload = () => ({
...query,
title: label,
name: label,
description,
});
useEffect(() => {
if (!isSaved) {
setLabel(defaultLabel);
}
if (!isSaved) setLabel(defaultLabel);
}, [defaultLabel]);
const close = () => {
setShowSave(false);
};
const close = () => setShowSave(false);
const onSaveWrapper = () => {
onSave(queryPayload());
@@ -118,10 +129,6 @@ export default function SaveQuery({
setDescription(e.target.value);
};
const toggleSave = () => {
setShowSave(!showSave);
};
const renderModalBody = () => (
<Form layout="vertical">
<Row>
@@ -161,10 +168,17 @@ export default function SaveQuery({
return (
<Styles className="SaveQuery">
<Button buttonSize="small" onClick={toggleSave}>
<Icons.Save iconSize="xl" />
{isSaved ? t('Save') : t('Save as')}
</Button>
<SaveDatasetActionButton
setShowSave={setShowSave}
overlayMenu={canExploreDatabase ? overlayMenu : null}
/>
<SaveDatasetModal
visible={showSaveDatasetModal}
onHide={() => setShowSaveDatasetModal(false)}
buttonTextOnSave={t('Save & Explore')}
buttonTextOnOverwrite={t('Overwrite & Explore')}
datasource={query}
/>
<Modal
className="save-query-modal"
onHandledPrimaryAction={onSaveWrapper}

View File

@@ -42,7 +42,7 @@ const standardProvider = ({ children }) => (
const defaultProps = {
queryEditor: {
dbId: 0,
title: 'query title',
name: 'query title',
schema: 'query_schema',
autorun: false,
sql: 'SELECT * FROM ...',

View File

@@ -49,8 +49,8 @@ function ShareSqlLabQuery({
const theme = useTheme();
const getCopyUrlForKvStore = (callback: Function) => {
const { dbId, title, schema, autorun, sql } = queryEditor;
const sharedQuery = { dbId, title, schema, autorun, sql };
const { dbId, name, schema, autorun, sql } = queryEditor;
const sharedQuery = { dbId, name, schema, autorun, sql };
return storeQuery(sharedQuery)
.then(shortUrl => {

View File

@@ -345,10 +345,10 @@ class SqlEditor extends React.PureComponent {
key: userOS === 'Windows' ? 'ctrl+q' : 'ctrl+t',
descr: t('New tab'),
func: () => {
const title = newQueryTabName(this.props.queryEditors || []);
const name = newQueryTabName(this.props.queryEditors || []);
this.props.addQueryEditor({
...this.props.queryEditor,
title,
name,
});
},
},
@@ -463,7 +463,7 @@ class SqlEditor extends React.PureComponent {
dbId: qe.dbId,
sql: qe.selectedText ? qe.selectedText : qe.sql,
sqlEditorId: qe.id,
tab: qe.title,
tab: qe.name,
schema: qe.schema,
tempTable: ctas ? this.state.ctas : '',
templateParams: qe.templateParams,
@@ -584,7 +584,7 @@ class SqlEditor extends React.PureComponent {
{scheduledQueriesConf && (
<Menu.Item>
<ScheduleQueryButton
defaultLabel={qe.title}
defaultLabel={qe.name}
sql={qe.sql}
onSchedule={this.props.actions.scheduleQuery}
schema={qe.schema}
@@ -722,10 +722,11 @@ class SqlEditor extends React.PureComponent {
<span>
<SaveQuery
query={qe}
defaultLabel={qe.title || qe.description}
defaultLabel={qe.name || qe.description}
onSave={this.saveQuery}
onUpdate={this.props.actions.updateSavedQuery}
saveQueryWarning={this.props.saveQueryWarning}
database={this.props.database}
/>
</span>
<span>

View File

@@ -55,7 +55,7 @@ describe('TabbedSqlEditors', () => {
schema: null,
selectedText: null,
sql: 'SELECT ds...',
title: 'Untitled Query',
name: 'Untitled Query',
},
];
const queries = {
@@ -177,7 +177,7 @@ describe('TabbedSqlEditors', () => {
wrapper.instance().newQueryEditor();
expect(
wrapper.instance().props.actions.addQueryEditor.getCall(0).args[0].title,
wrapper.instance().props.actions.addQueryEditor.getCall(0).args[0].name,
).toContain('Untitled Query');
});
it('should properly increment query tab name', () => {
@@ -186,7 +186,7 @@ describe('TabbedSqlEditors', () => {
wrapper.instance().newQueryEditor();
expect(
wrapper.instance().props.actions.addQueryEditor.getCall(0).args[0].title,
wrapper.instance().props.actions.addQueryEditor.getCall(0).args[0].name,
).toContain('Untitled Query 2');
});
it('should duplicate query editor', () => {

View File

@@ -167,7 +167,7 @@ class TabbedSqlEditors extends React.PureComponent {
}
}
const newQueryEditor = {
title: query.title,
name: query.name,
dbId,
schema: query.schema,
autorun: query.autorun,
@@ -266,7 +266,7 @@ class TabbedSqlEditors extends React.PureComponent {
const newTitle = newQueryTabName(this.props.queryEditors || []);
const qe = {
title: newTitle,
name: newTitle,
dbId:
activeQueryEditor && activeQueryEditor.dbId
? activeQueryEditor.dbId
@@ -376,7 +376,7 @@ class TabbedSqlEditors extends React.PureComponent {
const tabHeader = (
<TabTitleWrapper>
<Dropdown overlay={menu} trigger={['click']} />
<TabTitle>{qe.title}</TabTitle> <TabStatusIcon tabState={state} />{' '}
<TabTitle>{qe.name}</TabTitle> <TabStatusIcon tabState={state} />{' '}
</TabTitleWrapper>
);
return (