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:
Hugh A. Miles II 2020-12-08 17:29:41 -08:00 committed by GitHub
parent 8164aeafb1
commit cc44a2c01b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 304 additions and 178 deletions

View File

@ -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();
});
});
});
});
});

View File

@ -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} />);

View File

@ -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')}
>

View File

@ -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 && (

View File

@ -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 &amp; 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>

View File

@ -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({

View File

@ -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))