mirror of
https://github.com/apache/superset.git
synced 2024-09-16 10:39:55 -04:00
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 fetchMock from 'fetch-mock';
|
||||||
import shortid from 'shortid';
|
import shortid from 'shortid';
|
||||||
import sqlLabReducer from 'src/SqlLab/reducers/index';
|
import sqlLabReducer from 'src/SqlLab/reducers/index';
|
||||||
import * as actions from 'src/SqlLab/actions/sqlLab';
|
|
||||||
import ExploreResultsButton from 'src/SqlLab/components/ExploreResultsButton';
|
import ExploreResultsButton from 'src/SqlLab/components/ExploreResultsButton';
|
||||||
import * as exploreUtils from 'src/explore/exploreUtils';
|
import * as exploreUtils from 'src/explore/exploreUtils';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
@ -184,77 +183,5 @@ describe('ExploreResultsButton', () => {
|
|||||||
wrapper.instance().buildVizOptions.restore();
|
wrapper.instance().buildVizOptions.restore();
|
||||||
fetchMock.reset();
|
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: [],
|
userDatasetsOwned: [],
|
||||||
handleSaveDatasetRadioBtnState: () => {},
|
handleSaveDatasetRadioBtnState: () => {},
|
||||||
saveDatasetRadioBtnState: 1,
|
saveDatasetRadioBtnState: 1,
|
||||||
shouldOverwriteDataset: false,
|
|
||||||
handleOverwriteCancel: () => {},
|
handleOverwriteCancel: () => {},
|
||||||
handleOverwriteDataset: () => {},
|
handleOverwriteDataset: () => {},
|
||||||
handleOverwriteDatasetOption: () => {},
|
handleOverwriteDatasetOption: () => {},
|
||||||
defaultCreateDatasetValue: 'someDatasets',
|
defaultCreateDatasetValue: 'someDatasets',
|
||||||
|
shouldOverwriteDataset: false,
|
||||||
|
userDatasetOptions: [],
|
||||||
|
disableSaveAndExploreBtn: false,
|
||||||
|
handleSaveDatasetModalSearch: () => {},
|
||||||
|
filterAutocompleteOption: () => false,
|
||||||
|
onChangeAutoComplete: () => {},
|
||||||
};
|
};
|
||||||
it('renders a radio group btn', () => {
|
it('renders a radio group btn', () => {
|
||||||
const wrapper = shallow(<SaveDatasetModal {...mockedProps} />);
|
const wrapper = shallow(<SaveDatasetModal {...mockedProps} />);
|
||||||
|
@ -26,9 +26,7 @@ import { t } from '@superset-ui/core';
|
|||||||
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
||||||
import shortid from 'shortid';
|
import shortid from 'shortid';
|
||||||
|
|
||||||
import Modal from 'src/common/components/Modal';
|
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
import { exploreChart } from '../../explore/exploreUtils';
|
|
||||||
import * as actions from '../actions/sqlLab';
|
import * as actions from '../actions/sqlLab';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
@ -37,6 +35,7 @@ const propTypes = {
|
|||||||
errorMessage: PropTypes.string,
|
errorMessage: PropTypes.string,
|
||||||
timeout: PropTypes.number,
|
timeout: PropTypes.number,
|
||||||
database: PropTypes.object.isRequired,
|
database: PropTypes.object.isRequired,
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
query: {},
|
query: {},
|
||||||
@ -45,34 +44,12 @@ const defaultProps = {
|
|||||||
class ExploreResultsButton extends React.PureComponent {
|
class ExploreResultsButton extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.visualize = this.visualize.bind(this);
|
|
||||||
this.onClick = this.onClick.bind(this);
|
|
||||||
this.getInvalidColumns = this.getInvalidColumns.bind(this);
|
this.getInvalidColumns = this.getInvalidColumns.bind(this);
|
||||||
this.renderInvalidColumnMessage = this.renderInvalidColumnMessage.bind(
|
this.renderInvalidColumnMessage = this.renderInvalidColumnMessage.bind(
|
||||||
this,
|
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() {
|
getColumns() {
|
||||||
const { props } = this;
|
const { props } = this;
|
||||||
if (
|
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() {
|
renderTimeoutWarning() {
|
||||||
return (
|
return (
|
||||||
<Alert bsStyle="warning">
|
<Alert bsStyle="warning">
|
||||||
@ -203,7 +151,7 @@ class ExploreResultsButton extends React.PureComponent {
|
|||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
buttonSize="small"
|
buttonSize="small"
|
||||||
onClick={this.onClick}
|
onClick={this.props.onClick}
|
||||||
disabled={!allowsSubquery}
|
disabled={!allowsSubquery}
|
||||||
tooltip={t('Explore the result set in the data exploration view')}
|
tooltip={t('Explore the result set in the data exploration view')}
|
||||||
>
|
>
|
||||||
|
@ -19,11 +19,15 @@
|
|||||||
import React, { CSSProperties } from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
import { Alert, ButtonGroup } from 'react-bootstrap';
|
import { Alert, ButtonGroup } from 'react-bootstrap';
|
||||||
import ProgressBar from 'src/common/components/ProgressBar';
|
import ProgressBar from 'src/common/components/ProgressBar';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { RadioChangeEvent } from 'antd/lib/radio';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
import shortid from 'shortid';
|
import shortid from 'shortid';
|
||||||
import { styled, t } from '@superset-ui/core';
|
import { styled, t } from '@superset-ui/core';
|
||||||
|
|
||||||
import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace';
|
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 Loading from '../../components/Loading';
|
||||||
import ExploreCtasResultsButton from './ExploreCtasResultsButton';
|
import ExploreCtasResultsButton from './ExploreCtasResultsButton';
|
||||||
import ExploreResultsButton from './ExploreResultsButton';
|
import ExploreResultsButton from './ExploreResultsButton';
|
||||||
@ -32,13 +36,36 @@ import FilterableTable from '../../components/FilterableTable/FilterableTable';
|
|||||||
import QueryStateLabel from './QueryStateLabel';
|
import QueryStateLabel from './QueryStateLabel';
|
||||||
import CopyToClipboard from '../../components/CopyToClipboard';
|
import CopyToClipboard from '../../components/CopyToClipboard';
|
||||||
import { prepareCopyToClipboardTabularData } from '../../utils/common';
|
import { prepareCopyToClipboardTabularData } from '../../utils/common';
|
||||||
|
import { exploreChart } from '../../explore/exploreUtils';
|
||||||
import { CtasEnum } from '../actions/sqlLab';
|
import { CtasEnum } from '../actions/sqlLab';
|
||||||
import { Query } from '../types';
|
import { Query } from '../types';
|
||||||
|
|
||||||
const SEARCH_HEIGHT = 46;
|
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 };
|
const LOADING_STYLES: CSSProperties = { position: 'relative', minHeight: 100 };
|
||||||
|
|
||||||
|
interface DatasetOption {
|
||||||
|
datasetId: number;
|
||||||
|
datasetName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DatasetOptionAutocomplete {
|
||||||
|
value: string;
|
||||||
|
datasetId: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface ResultSetProps {
|
interface ResultSetProps {
|
||||||
actions: Record<string, any>;
|
actions: Record<string, any>;
|
||||||
cache?: boolean;
|
cache?: boolean;
|
||||||
@ -56,6 +83,14 @@ interface ResultSetState {
|
|||||||
searchText: string;
|
searchText: string;
|
||||||
showExploreResultsButton: boolean;
|
showExploreResultsButton: boolean;
|
||||||
data: Record<string, any>[];
|
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,
|
// Making text render line breaks/tabs as is as monospace,
|
||||||
@ -87,6 +122,14 @@ export default class ResultSet extends React.PureComponent<
|
|||||||
searchText: '',
|
searchText: '',
|
||||||
showExploreResultsButton: false,
|
showExploreResultsButton: false,
|
||||||
data: [],
|
data: [],
|
||||||
|
showSaveDatasetModal: false,
|
||||||
|
newSaveDatasetName: this.getDefaultDatasetName(),
|
||||||
|
userDatasetsOwned: [],
|
||||||
|
saveDatasetRadioBtnState: DatasetRadioState.SAVE_NEW,
|
||||||
|
shouldOverwriteDataSet: false,
|
||||||
|
datasetToOverwrite: {},
|
||||||
|
saveModalAutocompleteValue: '',
|
||||||
|
userDatasetOptions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
this.changeSearch = this.changeSearch.bind(this);
|
this.changeSearch = this.changeSearch.bind(this);
|
||||||
@ -96,11 +139,47 @@ export default class ResultSet extends React.PureComponent<
|
|||||||
this.toggleExploreResultsButton = this.toggleExploreResultsButton.bind(
|
this.toggleExploreResultsButton = this.toggleExploreResultsButton.bind(
|
||||||
this,
|
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
|
// only do this the first time the component is rendered/mounted
|
||||||
this.reRunQueryIfSessionTimeoutErrorOnMount();
|
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) {
|
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) {
|
clearQueryResults(query: Query) {
|
||||||
this.props.actions.clearQueryResults(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) {
|
if (this.props.cache && this.props.query.cached) {
|
||||||
({ data } = this.state);
|
({ 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 (
|
return (
|
||||||
<div className="ResultSetControls">
|
<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">
|
<div className="ResultSetButtons">
|
||||||
{this.props.visualize &&
|
{this.props.visualize &&
|
||||||
this.props.database &&
|
this.props.database &&
|
||||||
@ -184,6 +432,7 @@ export default class ResultSet extends React.PureComponent<
|
|||||||
query={this.props.query}
|
query={this.props.query}
|
||||||
database={this.props.database}
|
database={this.props.database}
|
||||||
actions={this.props.actions}
|
actions={this.props.actions}
|
||||||
|
onClick={this.handleExploreBtnClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{this.props.csv && (
|
{this.props.csv && (
|
||||||
|
@ -17,11 +17,12 @@
|
|||||||
* under the License.
|
* 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 { Radio, AutoComplete, Input } from 'src/common/components';
|
||||||
import StyledModal from 'src/common/components/Modal';
|
import StyledModal from 'src/common/components/Modal';
|
||||||
import Button from 'src/components/Button';
|
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';
|
import { RadioChangeEvent } from 'antd/lib/radio';
|
||||||
|
|
||||||
interface SaveDatasetModalProps {
|
interface SaveDatasetModalProps {
|
||||||
@ -29,27 +30,37 @@ interface SaveDatasetModalProps {
|
|||||||
onOk: () => void;
|
onOk: () => void;
|
||||||
onHide: () => void;
|
onHide: () => void;
|
||||||
handleDatasetNameChange: (e: React.FormEvent<HTMLInputElement>) => 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;
|
handleSaveDatasetRadioBtnState: (e: RadioChangeEvent) => void;
|
||||||
saveDatasetRadioBtnState: number;
|
|
||||||
shouldOverwriteDataset: boolean;
|
|
||||||
handleOverwriteCancel: () => void;
|
handleOverwriteCancel: () => void;
|
||||||
handleOverwriteDataset: () => void;
|
handleOverwriteDataset: () => void;
|
||||||
handleOverwriteDatasetOption: (
|
handleOverwriteDatasetOption: (
|
||||||
data: string,
|
data: string,
|
||||||
option: Record<string, any>,
|
option: Record<string, any>,
|
||||||
) => void;
|
) => void;
|
||||||
|
onChangeAutoComplete: () => void;
|
||||||
defaultCreateDatasetValue: string;
|
defaultCreateDatasetValue: string;
|
||||||
|
disableSaveAndExploreBtn: boolean;
|
||||||
|
saveDatasetRadioBtnState: number;
|
||||||
|
shouldOverwriteDataset: boolean;
|
||||||
|
userDatasetOptions: AutoCompleteProps['options'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Styles = styled.div`
|
const Styles = styled.div`
|
||||||
|
.smd-body {
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
.smd-input {
|
.smd-input {
|
||||||
margin-left: 45px;
|
margin-left: 45px;
|
||||||
width: 290px;
|
width: 401px;
|
||||||
}
|
}
|
||||||
.smd-autocomplete {
|
.smd-autocomplete {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
width: 290px;
|
width: 401px;
|
||||||
}
|
}
|
||||||
.smd-radio {
|
.smd-radio {
|
||||||
display: block;
|
display: block;
|
||||||
@ -57,6 +68,9 @@ const Styles = styled.div`
|
|||||||
margin: 10px 0px;
|
margin: 10px 0px;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
}
|
}
|
||||||
|
.smd-overwrite-msg {
|
||||||
|
margin: 7px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// eslint-disable-next-line no-empty-pattern
|
// eslint-disable-next-line no-empty-pattern
|
||||||
@ -65,7 +79,6 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
|
|||||||
onOk,
|
onOk,
|
||||||
onHide,
|
onHide,
|
||||||
handleDatasetNameChange,
|
handleDatasetNameChange,
|
||||||
userDatasetsOwned,
|
|
||||||
handleSaveDatasetRadioBtnState,
|
handleSaveDatasetRadioBtnState,
|
||||||
saveDatasetRadioBtnState,
|
saveDatasetRadioBtnState,
|
||||||
shouldOverwriteDataset,
|
shouldOverwriteDataset,
|
||||||
@ -73,32 +86,12 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
|
|||||||
handleOverwriteDataset,
|
handleOverwriteDataset,
|
||||||
handleOverwriteDatasetOption,
|
handleOverwriteDatasetOption,
|
||||||
defaultCreateDatasetValue,
|
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 (
|
return (
|
||||||
<StyledModal
|
<StyledModal
|
||||||
show={visible}
|
show={visible}
|
||||||
@ -108,31 +101,27 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
|
|||||||
<>
|
<>
|
||||||
{!shouldOverwriteDataset && (
|
{!shouldOverwriteDataset && (
|
||||||
<Button
|
<Button
|
||||||
buttonSize="sm"
|
disabled={disableSaveAndExploreBtn}
|
||||||
|
buttonSize="medium"
|
||||||
buttonStyle="primary"
|
buttonStyle="primary"
|
||||||
className="m-r-5"
|
|
||||||
onClick={onOk}
|
onClick={onOk}
|
||||||
>
|
>
|
||||||
Save & Explore
|
{t('Save & Explore')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{shouldOverwriteDataset && (
|
{shouldOverwriteDataset && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button buttonSize="medium" onClick={handleOverwriteCancel}>
|
||||||
buttonSize="sm"
|
Back
|
||||||
buttonStyle="danger"
|
|
||||||
className="m-r-5"
|
|
||||||
onClick={handleOverwriteCancel}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
buttonSize="sm"
|
className="md"
|
||||||
|
buttonSize="medium"
|
||||||
buttonStyle="primary"
|
buttonStyle="primary"
|
||||||
className="m-r-5"
|
|
||||||
onClick={handleOverwriteDataset}
|
onClick={handleOverwriteDataset}
|
||||||
|
disabled={disableSaveAndExploreBtn}
|
||||||
>
|
>
|
||||||
Ok
|
{t('Overwrite & Explore')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -155,24 +144,29 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
|
|||||||
className="smd-input"
|
className="smd-input"
|
||||||
defaultValue={defaultCreateDatasetValue}
|
defaultValue={defaultCreateDatasetValue}
|
||||||
onChange={handleDatasetNameChange}
|
onChange={handleDatasetNameChange}
|
||||||
|
disabled={saveDatasetRadioBtnState !== 1}
|
||||||
/>
|
/>
|
||||||
</Radio>
|
</Radio>
|
||||||
<Radio className="smd-radio" value={2}>
|
<Radio className="smd-radio" value={2}>
|
||||||
Overwrite existing
|
Overwrite existing
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
className="smd-autocomplete"
|
className="smd-autocomplete"
|
||||||
options={options}
|
options={userDatasetOptions}
|
||||||
onSelect={handleOverwriteDatasetOption}
|
onSelect={handleOverwriteDatasetOption}
|
||||||
onSearch={onSearch}
|
onSearch={handleSaveDatasetModalSearch}
|
||||||
|
onChange={onChangeAutoComplete}
|
||||||
placeholder="Select or type dataset name"
|
placeholder="Select or type dataset name"
|
||||||
filterOption={filterAutocompleteOption}
|
filterOption={filterAutocompleteOption}
|
||||||
|
disabled={saveDatasetRadioBtnState !== 2}
|
||||||
/>
|
/>
|
||||||
</Radio>
|
</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{shouldOverwriteDataset && (
|
{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>
|
</Styles>
|
||||||
</StyledModal>
|
</StyledModal>
|
||||||
|
@ -41,6 +41,7 @@ export const getByUser = async (userId: number) => {
|
|||||||
|
|
||||||
export const put = async (
|
export const put = async (
|
||||||
datasetId: number,
|
datasetId: number,
|
||||||
|
dbId: number,
|
||||||
sql: string,
|
sql: string,
|
||||||
columns: Array<Record<string, any>>,
|
columns: Array<Record<string, any>>,
|
||||||
overrideColumns: boolean,
|
overrideColumns: boolean,
|
||||||
@ -50,6 +51,7 @@ export const put = async (
|
|||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
sql,
|
sql,
|
||||||
columns,
|
columns,
|
||||||
|
database_id: dbId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const data: JsonResponse = await SupersetClient.put({
|
const data: JsonResponse = await SupersetClient.put({
|
||||||
|
@ -75,6 +75,7 @@ class DatasetPostSchema(Schema):
|
|||||||
|
|
||||||
class DatasetPutSchema(Schema):
|
class DatasetPutSchema(Schema):
|
||||||
table_name = fields.String(allow_none=True, validate=Length(1, 250))
|
table_name = fields.String(allow_none=True, validate=Length(1, 250))
|
||||||
|
database_id = fields.Integer()
|
||||||
sql = fields.String(allow_none=True)
|
sql = fields.String(allow_none=True)
|
||||||
filter_select_enabled = fields.Boolean(allow_none=True)
|
filter_select_enabled = fields.Boolean(allow_none=True)
|
||||||
fetch_values_predicate = fields.String(allow_none=True, validate=Length(0, 1000))
|
fetch_values_predicate = fields.String(allow_none=True, validate=Length(0, 1000))
|
||||||
|
Loading…
Reference in New Issue
Block a user