mirror of https://github.com/apache/superset.git
feat: Sqllab to Explore UX improvements (#11755)
* create boiler modal component
* hello world modal
* setup modal flow
* setup savemodal for components
* flake8
* fix onclick reference
* working create datasource boiler
* saving spot for callback on text input
* working dataset with input box
* working redirect on completion
* get data for owners dropdown
* fix build with pull from master
* fix the filteroption
* 💯
* move state to upper component
* add overwrite state
* hacked overwrite process
* linting
* fix filter
* cleaning up the coe
* Delete preset.code-workspace
* remove unused code
* remove visualize
* update default value
* remove unneeded vars
* checkout package-lock.json
* linting
* get user id
* remove page filter
* setup proper call for updating columns in dataset
* add move to explore flow
* linting
* add param for overriding columns
* linting
* change title
* fix some tings
* cleanup
* linting
* add types in some places
* save toast
* use moment
* add error toast
* create enum for radio states
* initial state for saving query
* add tpying
* addressing concerns
* update propTypes
* add functionality for CTAS explor btn
* handle CTAS state
* fix onclick to reference upper level component
* formatting
* reset state after closing
* add error message when user doesn't pick an already selected dataset
* remove unneeded todo
* remove styling
* move async calls to api directory
* remove console.log
* add user id param
* typing
* littty
* move put to seperate file
* save
* dsf
* fix typing errors
* adding more types
* fix typing erros
* linting
* add basic spec test
* create dataset modal
* add components reference
* Rename SaveDatasetModal_spec.jsx to SaveDatasetModal_spec.tsx
* remove sinon for now
* fix typing errors on modal files
* fix linting
* address comments
* attempt to fix linting
* add props
* fix test
* fix the linting
* yerp
* fix this references
* spaces
* handleOverwriteCancel reference cleanup
* rename bool value for shouldOverwriteDataset
* fix typing for onChange
* you still the best in the world
* fix spec
* align branches
* push
* fix key names
* fix dataset reference
* lowercase
* fix save bug with tiem
* fixed styling
* fix date state after push to explore
* add disabling states
* plz refactor this
* this is working fully now
* do some renaming
* renaming
* remove console.logs
* still refactoring
* remove unneeded code
* remove unneeded variables
* still cleaning
* added interface
* fix typing issues
* cleanup unused code
* fix npm lnit
* fix initial problems
* add props to test
* remove unneeded files
* skip linting
* saving
* this works
* remove old test
* remove old test
* fix linting
* fix broken test
* remove jsx file
* refactoring
* cleanup
* remove comments
* reset user object
* fix functions
* fix this
* reverting CTAS btn flow
* remove onclick
* save frontend work
* allow for database id to be passed as param in body
* use enum
* fix linting
* style alignment
* get rid of .then
* add function to compute default value with tiem
* lit
* remove ts-error
* fix typing
This commit is contained in:
parent
8164aeafb1
commit
cc44a2c01b
|
@ -24,7 +24,6 @@ import sinon from 'sinon';
|
|||
import fetchMock from 'fetch-mock';
|
||||
import shortid from 'shortid';
|
||||
import sqlLabReducer from 'src/SqlLab/reducers/index';
|
||||
import * as actions from 'src/SqlLab/actions/sqlLab';
|
||||
import ExploreResultsButton from 'src/SqlLab/components/ExploreResultsButton';
|
||||
import * as exploreUtils from 'src/explore/exploreUtils';
|
||||
import Button from 'src/components/Button';
|
||||
|
@ -184,77 +183,5 @@ describe('ExploreResultsButton', () => {
|
|||
wrapper.instance().buildVizOptions.restore();
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
it('should build request with correct args', () => {
|
||||
return new Promise(done => {
|
||||
wrapper.instance().visualize();
|
||||
|
||||
setTimeout(() => {
|
||||
const calls = fetchMock.calls(visualizeEndpoint);
|
||||
expect(calls).toHaveLength(1);
|
||||
const formData = calls[0][1].body;
|
||||
Object.keys(mockOptions).forEach(key => {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
expect(formData.get(key)).toBeDefined();
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should export chart and add an info toast', () => {
|
||||
return new Promise(done => {
|
||||
const infoToastSpy = sinon.spy();
|
||||
const datasourceSpy = sinon.stub();
|
||||
|
||||
datasourceSpy.callsFake(() => Promise.resolve(visualizationPayload));
|
||||
|
||||
wrapper.setProps({
|
||||
actions: {
|
||||
addInfoToast: infoToastSpy,
|
||||
createDatasource: datasourceSpy,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.instance().visualize();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(datasourceSpy.callCount).toBe(1);
|
||||
expect(exploreUtils.exploreChart.callCount).toBe(1);
|
||||
expect(exploreUtils.exploreChart.getCall(0).args[0].datasource).toBe(
|
||||
'107__table',
|
||||
);
|
||||
expect(infoToastSpy.callCount).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should add error toast', () => {
|
||||
return new Promise(done => {
|
||||
const dangerToastSpy = sinon.stub(actions, 'addDangerToast');
|
||||
const datasourceSpy = sinon.stub();
|
||||
|
||||
datasourceSpy.callsFake(() => Promise.reject({ error: 'error' }));
|
||||
|
||||
wrapper.setProps({
|
||||
actions: {
|
||||
createDatasource: datasourceSpy,
|
||||
addDangerToast: dangerToastSpy,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.instance().visualize();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(datasourceSpy.callCount).toBe(1);
|
||||
expect(exploreUtils.exportChart.callCount).toBe(0);
|
||||
expect(dangerToastSpy.callCount).toBe(1);
|
||||
dangerToastSpy.restore();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -30,11 +30,16 @@ describe('SaveDatasetModal', () => {
|
|||
userDatasetsOwned: [],
|
||||
handleSaveDatasetRadioBtnState: () => {},
|
||||
saveDatasetRadioBtnState: 1,
|
||||
shouldOverwriteDataset: false,
|
||||
handleOverwriteCancel: () => {},
|
||||
handleOverwriteDataset: () => {},
|
||||
handleOverwriteDatasetOption: () => {},
|
||||
defaultCreateDatasetValue: 'someDatasets',
|
||||
shouldOverwriteDataset: false,
|
||||
userDatasetOptions: [],
|
||||
disableSaveAndExploreBtn: false,
|
||||
handleSaveDatasetModalSearch: () => {},
|
||||
filterAutocompleteOption: () => false,
|
||||
onChangeAutoComplete: () => {},
|
||||
};
|
||||
it('renders a radio group btn', () => {
|
||||
const wrapper = shallow(<SaveDatasetModal {...mockedProps} />);
|
||||
|
|
|
@ -26,9 +26,7 @@ import { t } from '@superset-ui/core';
|
|||
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
||||
import shortid from 'shortid';
|
||||
|
||||
import Modal from 'src/common/components/Modal';
|
||||
import Button from 'src/components/Button';
|
||||
import { exploreChart } from '../../explore/exploreUtils';
|
||||
import * as actions from '../actions/sqlLab';
|
||||
|
||||
const propTypes = {
|
||||
|
@ -37,6 +35,7 @@ const propTypes = {
|
|||
errorMessage: PropTypes.string,
|
||||
timeout: PropTypes.number,
|
||||
database: PropTypes.object.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
const defaultProps = {
|
||||
query: {},
|
||||
|
@ -45,34 +44,12 @@ const defaultProps = {
|
|||
class ExploreResultsButton extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.visualize = this.visualize.bind(this);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.getInvalidColumns = this.getInvalidColumns.bind(this);
|
||||
this.renderInvalidColumnMessage = this.renderInvalidColumnMessage.bind(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
onClick() {
|
||||
const { timeout } = this.props;
|
||||
const msg = this.renderInvalidColumnMessage();
|
||||
if (Math.round(this.getQueryDuration()) > timeout) {
|
||||
Modal.confirm({
|
||||
title: t('Explore'),
|
||||
content: this.renderTimeoutWarning(),
|
||||
onOk: this.visualize,
|
||||
icon: null,
|
||||
});
|
||||
} else if (msg) {
|
||||
Modal.warning({
|
||||
title: t('Explore'),
|
||||
content: msg,
|
||||
});
|
||||
} else {
|
||||
this.visualize();
|
||||
}
|
||||
}
|
||||
|
||||
getColumns() {
|
||||
const { props } = this;
|
||||
if (
|
||||
|
@ -123,35 +100,6 @@ class ExploreResultsButton extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
visualize() {
|
||||
this.props.actions
|
||||
.createDatasource(this.buildVizOptions())
|
||||
.then(data => {
|
||||
const columns = this.getColumns();
|
||||
const formData = {
|
||||
datasource: `${data.table_id}__table`,
|
||||
metrics: [],
|
||||
groupby: [],
|
||||
time_range: 'No filter',
|
||||
viz_type: 'table',
|
||||
all_columns: columns.map(c => c.name),
|
||||
row_limit: 1000,
|
||||
};
|
||||
|
||||
this.props.actions.addInfoToast(
|
||||
t('Creating a data source and creating a new tab'),
|
||||
);
|
||||
|
||||
// open new window for data visualization
|
||||
exploreChart(formData);
|
||||
})
|
||||
.catch(() => {
|
||||
this.props.actions.addDangerToast(
|
||||
this.props.errorMessage || t('An error occurred'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderTimeoutWarning() {
|
||||
return (
|
||||
<Alert bsStyle="warning">
|
||||
|
@ -203,7 +151,7 @@ class ExploreResultsButton extends React.PureComponent {
|
|||
<>
|
||||
<Button
|
||||
buttonSize="small"
|
||||
onClick={this.onClick}
|
||||
onClick={this.props.onClick}
|
||||
disabled={!allowsSubquery}
|
||||
tooltip={t('Explore the result set in the data exploration view')}
|
||||
>
|
||||
|
|
|
@ -19,11 +19,15 @@
|
|||
import React, { CSSProperties } from 'react';
|
||||
import { Alert, ButtonGroup } from 'react-bootstrap';
|
||||
import ProgressBar from 'src/common/components/ProgressBar';
|
||||
import moment from 'moment';
|
||||
import { RadioChangeEvent } from 'antd/lib/radio';
|
||||
import Button from 'src/components/Button';
|
||||
import shortid from 'shortid';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
|
||||
import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace';
|
||||
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
|
||||
import { getByUser, put as updateDatset } from 'src/api/dataset';
|
||||
import Loading from '../../components/Loading';
|
||||
import ExploreCtasResultsButton from './ExploreCtasResultsButton';
|
||||
import ExploreResultsButton from './ExploreResultsButton';
|
||||
|
@ -32,13 +36,36 @@ import FilterableTable from '../../components/FilterableTable/FilterableTable';
|
|||
import QueryStateLabel from './QueryStateLabel';
|
||||
import CopyToClipboard from '../../components/CopyToClipboard';
|
||||
import { prepareCopyToClipboardTabularData } from '../../utils/common';
|
||||
import { exploreChart } from '../../explore/exploreUtils';
|
||||
import { CtasEnum } from '../actions/sqlLab';
|
||||
import { Query } from '../types';
|
||||
|
||||
const SEARCH_HEIGHT = 46;
|
||||
|
||||
enum DatasetRadioState {
|
||||
SAVE_NEW = 1,
|
||||
OVERWRITE_DATASET = 2,
|
||||
}
|
||||
|
||||
const EXPLORE_CHART_DEFAULT = {
|
||||
metrics: [],
|
||||
groupby: [],
|
||||
time_range: 'No filter',
|
||||
viz_type: 'table',
|
||||
};
|
||||
|
||||
const LOADING_STYLES: CSSProperties = { position: 'relative', minHeight: 100 };
|
||||
|
||||
interface DatasetOption {
|
||||
datasetId: number;
|
||||
datasetName: string;
|
||||
}
|
||||
|
||||
interface DatasetOptionAutocomplete {
|
||||
value: string;
|
||||
datasetId: number;
|
||||
}
|
||||
|
||||
interface ResultSetProps {
|
||||
actions: Record<string, any>;
|
||||
cache?: boolean;
|
||||
|
@ -56,6 +83,14 @@ interface ResultSetState {
|
|||
searchText: string;
|
||||
showExploreResultsButton: boolean;
|
||||
data: Record<string, any>[];
|
||||
showSaveDatasetModal: boolean;
|
||||
newSaveDatasetName: string;
|
||||
userDatasetsOwned: DatasetOption[];
|
||||
saveDatasetRadioBtnState: number;
|
||||
shouldOverwriteDataSet: boolean;
|
||||
datasetToOverwrite: Record<string, any>;
|
||||
saveModalAutocompleteValue: string;
|
||||
userDatasetOptions: DatasetOptionAutocomplete[];
|
||||
}
|
||||
|
||||
// Making text render line breaks/tabs as is as monospace,
|
||||
|
@ -87,6 +122,14 @@ export default class ResultSet extends React.PureComponent<
|
|||
searchText: '',
|
||||
showExploreResultsButton: false,
|
||||
data: [],
|
||||
showSaveDatasetModal: false,
|
||||
newSaveDatasetName: this.getDefaultDatasetName(),
|
||||
userDatasetsOwned: [],
|
||||
saveDatasetRadioBtnState: DatasetRadioState.SAVE_NEW,
|
||||
shouldOverwriteDataSet: false,
|
||||
datasetToOverwrite: {},
|
||||
saveModalAutocompleteValue: '',
|
||||
userDatasetOptions: [],
|
||||
};
|
||||
|
||||
this.changeSearch = this.changeSearch.bind(this);
|
||||
|
@ -96,11 +139,47 @@ export default class ResultSet extends React.PureComponent<
|
|||
this.toggleExploreResultsButton = this.toggleExploreResultsButton.bind(
|
||||
this,
|
||||
);
|
||||
this.handleSaveInDataset = this.handleSaveInDataset.bind(this);
|
||||
this.handleHideSaveModal = this.handleHideSaveModal.bind(this);
|
||||
this.handleDatasetNameChange = this.handleDatasetNameChange.bind(this);
|
||||
this.handleSaveDatasetRadioBtnState = this.handleSaveDatasetRadioBtnState.bind(
|
||||
this,
|
||||
);
|
||||
this.handleOverwriteCancel = this.handleOverwriteCancel.bind(this);
|
||||
this.handleOverwriteDataset = this.handleOverwriteDataset.bind(this);
|
||||
this.handleOverwriteDatasetOption = this.handleOverwriteDatasetOption.bind(
|
||||
this,
|
||||
);
|
||||
this.handleSaveDatasetModalSearch = this.handleSaveDatasetModalSearch.bind(
|
||||
this,
|
||||
);
|
||||
this.handleFilterAutocompleteOption = this.handleFilterAutocompleteOption.bind(
|
||||
this,
|
||||
);
|
||||
this.handleOnChangeAutoComplete = this.handleOnChangeAutoComplete.bind(
|
||||
this,
|
||||
);
|
||||
this.handleExploreBtnClick = this.handleExploreBtnClick.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
async componentDidMount() {
|
||||
// only do this the first time the component is rendered/mounted
|
||||
this.reRunQueryIfSessionTimeoutErrorOnMount();
|
||||
|
||||
const appContainer = document.getElementById('app');
|
||||
const bootstrapData = JSON.parse(
|
||||
appContainer?.getAttribute('data-bootstrap') || '{}',
|
||||
);
|
||||
|
||||
const datasets = await getByUser(bootstrapData.user.userId);
|
||||
const userDatasetsOwned = datasets.map(
|
||||
(r: { table_name: string; id: number }) => ({
|
||||
datasetName: r.table_name,
|
||||
datasetId: r.id,
|
||||
}),
|
||||
);
|
||||
|
||||
this.setState({ userDatasetsOwned });
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps: ResultSetProps) {
|
||||
|
@ -124,6 +203,139 @@ export default class ResultSet extends React.PureComponent<
|
|||
}
|
||||
}
|
||||
|
||||
getDefaultDatasetName = () => {
|
||||
return `${this.props.query.tab} ${moment().format('MM/DD/YYYY HH:mm:ss')}`;
|
||||
};
|
||||
|
||||
handleOnChangeAutoComplete = () => {
|
||||
this.setState({ datasetToOverwrite: {} });
|
||||
};
|
||||
|
||||
handleOverwriteDataset = async () => {
|
||||
const { sql, results, dbId } = this.props.query;
|
||||
const { datasetToOverwrite } = this.state;
|
||||
|
||||
await updateDatset(
|
||||
datasetToOverwrite.datasetId,
|
||||
dbId,
|
||||
sql,
|
||||
results.selected_columns.map(d => ({ column_name: d.name })),
|
||||
true,
|
||||
);
|
||||
|
||||
this.setState({
|
||||
showSaveDatasetModal: false,
|
||||
shouldOverwriteDataSet: false,
|
||||
datasetToOverwrite: {},
|
||||
newSaveDatasetName: this.getDefaultDatasetName(),
|
||||
});
|
||||
|
||||
exploreChart({
|
||||
...EXPLORE_CHART_DEFAULT,
|
||||
datasource: `${datasetToOverwrite.datasetId}__table`,
|
||||
all_columns: results.selected_columns.map(d => d.name),
|
||||
});
|
||||
};
|
||||
|
||||
handleSaveInDataset = () => {
|
||||
// if user wants to overwrite a dataset we need to prompt them
|
||||
if (
|
||||
this.state.saveDatasetRadioBtnState ===
|
||||
DatasetRadioState.OVERWRITE_DATASET
|
||||
) {
|
||||
this.setState({ shouldOverwriteDataSet: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const { schema, sql, dbId, templateParams } = this.props.query;
|
||||
const selectedColumns = this.props.query?.results?.selected_columns || [];
|
||||
|
||||
this.props.actions
|
||||
.createDatasource({
|
||||
schema,
|
||||
sql,
|
||||
dbId,
|
||||
templateParams,
|
||||
datasourceName: this.state.newSaveDatasetName,
|
||||
columns: selectedColumns,
|
||||
})
|
||||
.then((data: { table_id: number }) => {
|
||||
exploreChart({
|
||||
datasource: `${data.table_id}__table`,
|
||||
metrics: [],
|
||||
groupby: [],
|
||||
time_range: 'No filter',
|
||||
viz_type: 'table',
|
||||
all_columns: selectedColumns.map(c => c.name),
|
||||
row_limit: 1000,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.props.actions.addDangerToast(
|
||||
t('An error occurred saving dataset'),
|
||||
);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
showSaveDatasetModal: false,
|
||||
newSaveDatasetName: this.getDefaultDatasetName(),
|
||||
});
|
||||
};
|
||||
|
||||
handleOverwriteDatasetOption = (
|
||||
_data: string,
|
||||
option: Record<string, any>,
|
||||
) => {
|
||||
this.setState({ datasetToOverwrite: option });
|
||||
};
|
||||
|
||||
handleDatasetNameChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
// @ts-expect-error
|
||||
this.setState({ newSaveDatasetName: e.target.value });
|
||||
};
|
||||
|
||||
handleHideSaveModal = () => {
|
||||
this.setState({
|
||||
showSaveDatasetModal: false,
|
||||
shouldOverwriteDataSet: false,
|
||||
});
|
||||
};
|
||||
|
||||
handleSaveDatasetRadioBtnState = (e: RadioChangeEvent) => {
|
||||
this.setState({ saveDatasetRadioBtnState: Number(e.target.value) });
|
||||
};
|
||||
|
||||
handleOverwriteCancel = () => {
|
||||
this.setState({ shouldOverwriteDataSet: false });
|
||||
};
|
||||
|
||||
handleExploreBtnClick = () => {
|
||||
this.setState({
|
||||
showSaveDatasetModal: true,
|
||||
});
|
||||
};
|
||||
|
||||
handleSaveDatasetModalSearch = (searchText: string) => {
|
||||
// Making sure that autocomplete input has a value before rendering the dropdown
|
||||
// Transforming the userDatasetsOwned data for SaveModalComponent)
|
||||
const { userDatasetsOwned } = this.state;
|
||||
const userDatasets = !searchText
|
||||
? []
|
||||
: userDatasetsOwned.map(d => ({
|
||||
value: d.datasetName,
|
||||
datasetId: d.datasetId,
|
||||
}));
|
||||
|
||||
this.setState({ userDatasetOptions: userDatasets });
|
||||
};
|
||||
|
||||
handleFilterAutocompleteOption = (
|
||||
inputValue: string,
|
||||
option: { value: string; datasetId: number },
|
||||
) => {
|
||||
return option.value.toLowerCase().includes(inputValue.toLowerCase());
|
||||
};
|
||||
|
||||
clearQueryResults(query: Query) {
|
||||
this.props.actions.clearQueryResults(query);
|
||||
}
|
||||
|
@ -173,8 +385,44 @@ export default class ResultSet extends React.PureComponent<
|
|||
if (this.props.cache && this.props.query.cached) {
|
||||
({ data } = this.state);
|
||||
}
|
||||
|
||||
// Added compute logic to stop user from being able to Save & Explore
|
||||
const {
|
||||
saveDatasetRadioBtnState,
|
||||
newSaveDatasetName,
|
||||
datasetToOverwrite,
|
||||
saveModalAutocompleteValue,
|
||||
shouldOverwriteDataSet,
|
||||
userDatasetOptions,
|
||||
showSaveDatasetModal,
|
||||
} = this.state;
|
||||
const disableSaveAndExploreBtn =
|
||||
(saveDatasetRadioBtnState === DatasetRadioState.SAVE_NEW &&
|
||||
newSaveDatasetName.length === 0) ||
|
||||
(saveDatasetRadioBtnState === DatasetRadioState.OVERWRITE_DATASET &&
|
||||
Object.keys(datasetToOverwrite).length === 0 &&
|
||||
saveModalAutocompleteValue.length === 0);
|
||||
|
||||
return (
|
||||
<div className="ResultSetControls">
|
||||
<SaveDatasetModal
|
||||
visible={showSaveDatasetModal}
|
||||
onOk={this.handleSaveInDataset}
|
||||
saveDatasetRadioBtnState={saveDatasetRadioBtnState}
|
||||
shouldOverwriteDataset={shouldOverwriteDataSet}
|
||||
defaultCreateDatasetValue={newSaveDatasetName}
|
||||
userDatasetOptions={userDatasetOptions}
|
||||
disableSaveAndExploreBtn={disableSaveAndExploreBtn}
|
||||
onHide={this.handleHideSaveModal}
|
||||
handleDatasetNameChange={this.handleDatasetNameChange}
|
||||
handleSaveDatasetRadioBtnState={this.handleSaveDatasetRadioBtnState}
|
||||
handleOverwriteCancel={this.handleOverwriteCancel}
|
||||
handleOverwriteDataset={this.handleOverwriteDataset}
|
||||
handleOverwriteDatasetOption={this.handleOverwriteDatasetOption}
|
||||
handleSaveDatasetModalSearch={this.handleSaveDatasetModalSearch}
|
||||
filterAutocompleteOption={this.handleFilterAutocompleteOption}
|
||||
onChangeAutoComplete={this.handleOnChangeAutoComplete}
|
||||
/>
|
||||
<div className="ResultSetButtons">
|
||||
{this.props.visualize &&
|
||||
this.props.database &&
|
||||
|
@ -184,6 +432,7 @@ export default class ResultSet extends React.PureComponent<
|
|||
query={this.props.query}
|
||||
database={this.props.database}
|
||||
actions={this.props.actions}
|
||||
onClick={this.handleExploreBtnClick}
|
||||
/>
|
||||
)}
|
||||
{this.props.csv && (
|
||||
|
|
|
@ -17,11 +17,12 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, FunctionComponent } from 'react';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { AutoCompleteProps } from 'antd/lib/auto-complete';
|
||||
import { Radio, AutoComplete, Input } from 'src/common/components';
|
||||
import StyledModal from 'src/common/components/Modal';
|
||||
import Button from 'src/components/Button';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import { RadioChangeEvent } from 'antd/lib/radio';
|
||||
|
||||
interface SaveDatasetModalProps {
|
||||
|
@ -29,27 +30,37 @@ interface SaveDatasetModalProps {
|
|||
onOk: () => void;
|
||||
onHide: () => void;
|
||||
handleDatasetNameChange: (e: React.FormEvent<HTMLInputElement>) => void;
|
||||
userDatasetsOwned: Array<Record<string, any>>;
|
||||
handleSaveDatasetModalSearch: (searchText: string) => void;
|
||||
filterAutocompleteOption: (
|
||||
inputValue: string,
|
||||
option: { value: string; datasetId: number },
|
||||
) => boolean;
|
||||
handleSaveDatasetRadioBtnState: (e: RadioChangeEvent) => void;
|
||||
saveDatasetRadioBtnState: number;
|
||||
shouldOverwriteDataset: boolean;
|
||||
handleOverwriteCancel: () => void;
|
||||
handleOverwriteDataset: () => void;
|
||||
handleOverwriteDatasetOption: (
|
||||
data: string,
|
||||
option: Record<string, any>,
|
||||
) => void;
|
||||
onChangeAutoComplete: () => void;
|
||||
defaultCreateDatasetValue: string;
|
||||
disableSaveAndExploreBtn: boolean;
|
||||
saveDatasetRadioBtnState: number;
|
||||
shouldOverwriteDataset: boolean;
|
||||
userDatasetOptions: AutoCompleteProps['options'];
|
||||
}
|
||||
|
||||
const Styles = styled.div`
|
||||
.smd-body {
|
||||
margin: 0 8px;
|
||||
}
|
||||
.smd-input {
|
||||
margin-left: 45px;
|
||||
width: 290px;
|
||||
width: 401px;
|
||||
}
|
||||
.smd-autocomplete {
|
||||
margin-left: 8px;
|
||||
width: 290px;
|
||||
width: 401px;
|
||||
}
|
||||
.smd-radio {
|
||||
display: block;
|
||||
|
@ -57,6 +68,9 @@ const Styles = styled.div`
|
|||
margin: 10px 0px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.smd-overwrite-msg {
|
||||
margin: 7px;
|
||||
}
|
||||
`;
|
||||
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
|
@ -65,7 +79,6 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
|
|||
onOk,
|
||||
onHide,
|
||||
handleDatasetNameChange,
|
||||
userDatasetsOwned,
|
||||
handleSaveDatasetRadioBtnState,
|
||||
saveDatasetRadioBtnState,
|
||||
shouldOverwriteDataset,
|
||||
|
@ -73,32 +86,12 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
|
|||
handleOverwriteDataset,
|
||||
handleOverwriteDatasetOption,
|
||||
defaultCreateDatasetValue,
|
||||
disableSaveAndExploreBtn,
|
||||
handleSaveDatasetModalSearch,
|
||||
filterAutocompleteOption,
|
||||
userDatasetOptions,
|
||||
onChangeAutoComplete,
|
||||
}) => {
|
||||
const [options, setOptions] = useState<
|
||||
{
|
||||
value: string;
|
||||
datasetId: number;
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
const onSearch = (searchText: string) => {
|
||||
setOptions(
|
||||
!searchText
|
||||
? []
|
||||
: userDatasetsOwned.map(d => ({
|
||||
value: d.datasetName,
|
||||
datasetId: d.datasetId,
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
const filterAutocompleteOption = (
|
||||
inputValue: string,
|
||||
option: { value: string; datasetId: number },
|
||||
) => {
|
||||
return option.value.includes(inputValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
show={visible}
|
||||
|
@ -108,31 +101,27 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
|
|||
<>
|
||||
{!shouldOverwriteDataset && (
|
||||
<Button
|
||||
buttonSize="sm"
|
||||
disabled={disableSaveAndExploreBtn}
|
||||
buttonSize="medium"
|
||||
buttonStyle="primary"
|
||||
className="m-r-5"
|
||||
onClick={onOk}
|
||||
>
|
||||
Save & Explore
|
||||
{t('Save & Explore')}
|
||||
</Button>
|
||||
)}
|
||||
{shouldOverwriteDataset && (
|
||||
<>
|
||||
<Button
|
||||
buttonSize="sm"
|
||||
buttonStyle="danger"
|
||||
className="m-r-5"
|
||||
onClick={handleOverwriteCancel}
|
||||
>
|
||||
Cancel
|
||||
<Button buttonSize="medium" onClick={handleOverwriteCancel}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
buttonSize="sm"
|
||||
className="md"
|
||||
buttonSize="medium"
|
||||
buttonStyle="primary"
|
||||
className="m-r-5"
|
||||
onClick={handleOverwriteDataset}
|
||||
disabled={disableSaveAndExploreBtn}
|
||||
>
|
||||
Ok
|
||||
{t('Overwrite & Explore')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
@ -155,24 +144,29 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
|
|||
className="smd-input"
|
||||
defaultValue={defaultCreateDatasetValue}
|
||||
onChange={handleDatasetNameChange}
|
||||
disabled={saveDatasetRadioBtnState !== 1}
|
||||
/>
|
||||
</Radio>
|
||||
<Radio className="smd-radio" value={2}>
|
||||
Overwrite existing
|
||||
<AutoComplete
|
||||
className="smd-autocomplete"
|
||||
options={options}
|
||||
options={userDatasetOptions}
|
||||
onSelect={handleOverwriteDatasetOption}
|
||||
onSearch={onSearch}
|
||||
onSearch={handleSaveDatasetModalSearch}
|
||||
onChange={onChangeAutoComplete}
|
||||
placeholder="Select or type dataset name"
|
||||
filterOption={filterAutocompleteOption}
|
||||
disabled={saveDatasetRadioBtnState !== 2}
|
||||
/>
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
)}
|
||||
{shouldOverwriteDataset && (
|
||||
<div>Are you sure you want to overwrite this dataset?</div>
|
||||
<div className="smd-overwrite-msg">
|
||||
Are you sure you want to overwrite this dataset?
|
||||
</div>
|
||||
)}
|
||||
</Styles>
|
||||
</StyledModal>
|
||||
|
|
|
@ -41,6 +41,7 @@ export const getByUser = async (userId: number) => {
|
|||
|
||||
export const put = async (
|
||||
datasetId: number,
|
||||
dbId: number,
|
||||
sql: string,
|
||||
columns: Array<Record<string, any>>,
|
||||
overrideColumns: boolean,
|
||||
|
@ -50,6 +51,7 @@ export const put = async (
|
|||
const body = JSON.stringify({
|
||||
sql,
|
||||
columns,
|
||||
database_id: dbId,
|
||||
});
|
||||
|
||||
const data: JsonResponse = await SupersetClient.put({
|
||||
|
|
|
@ -75,6 +75,7 @@ class DatasetPostSchema(Schema):
|
|||
|
||||
class DatasetPutSchema(Schema):
|
||||
table_name = fields.String(allow_none=True, validate=Length(1, 250))
|
||||
database_id = fields.Integer()
|
||||
sql = fields.String(allow_none=True)
|
||||
filter_select_enabled = fields.Boolean(allow_none=True)
|
||||
fetch_values_predicate = fields.String(allow_none=True, validate=Length(0, 1000))
|
||||
|
|
Loading…
Reference in New Issue