Skip to content

Commit

Permalink
[explorev2] adding support for client side validators on controls (#1920
Browse files Browse the repository at this point in the history
)

* Adding support for client side validators on controls

* Applying validators to more fields

* Addressing comments
  • Loading branch information
mistercrunch committed Jan 12, 2017
1 parent fc74fbe commit 470a6e9
Show file tree
Hide file tree
Showing 20 changed files with 237 additions and 105 deletions.
64 changes: 41 additions & 23 deletions superset/assets/javascripts/explore/components/QueryAndSaveBtns.jsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,64 @@
import React, { PropTypes } from 'react';
import { Button, ButtonGroup } from 'react-bootstrap';
import { ButtonGroup, OverlayTrigger, Tooltip } from 'react-bootstrap';
import Button from '../../components/Button';
import classnames from 'classnames';

const propTypes = {
canAdd: PropTypes.string.isRequired,
onQuery: PropTypes.func.isRequired,
onSave: PropTypes.func,
disabled: PropTypes.bool,
errorMessage: PropTypes.string,
};

const defaultProps = {
onSave: () => {},
disabled: false,
};

export default function QueryAndSaveBtns({ canAdd, onQuery, onSave, disabled }) {
export default function QueryAndSaveBtns({ canAdd, onQuery, onSave, disabled, errorMessage }) {
const saveClasses = classnames({
'disabled disabledButton': canAdd !== 'True',
});
const qryButtonStyle = errorMessage ? 'danger' : 'primary';
const qryButtonDisabled = errorMessage ? true : disabled;

return (
<ButtonGroup className="query-and-save">
<Button
id="query_button"
onClick={onQuery}
bsSize="small"
disabled={disabled}
bsStyle="primary"
>
<i className="fa fa-bolt" /> Query
</Button>
<Button
className={saveClasses}
bsSize="small"
data-target="#save_modal"
data-toggle="modal"
disabled={disabled}
onClick={onSave}
>
<i className="fa fa-plus-circle"></i> Save as
</Button>
</ButtonGroup>
<div>
<ButtonGroup className="query-and-save">
<Button
id="query_button"
onClick={onQuery}
disabled={qryButtonDisabled}
bsStyle={qryButtonStyle}
>
<i className="fa fa-bolt" /> Query
</Button>
<Button
className={saveClasses}
data-target="#save_modal"
data-toggle="modal"
disabled={qryButtonDisabled}
onClick={onSave}
>
<i className="fa fa-plus-circle"></i> Save as
</Button>
</ButtonGroup>
{errorMessage &&
<span>
{' '}
<OverlayTrigger
placement="right"
overlay={
<Tooltip id={'query-error-tooltip'}>
{errorMessage}
</Tooltip>}
>
<i className="fa fa-exclamation-circle text-danger fa-lg" />
</OverlayTrigger>
</span>
}
</div>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ export function fetchFilterValues(datasource_type, datasource_id, filter, col) {
}

export const SET_FIELD_VALUE = 'SET_FIELD_VALUE';
export function setFieldValue(datasource_type, key, value, label) {
return { type: SET_FIELD_VALUE, datasource_type, key, value, label };
export function setFieldValue(fieldName, value, validationErrors) {
return { type: SET_FIELD_VALUE, fieldName, value, validationErrors };
}

export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@ const propTypes = {

const defaultProps = {
value: false,
label: null,
description: null,
onChange: () => {},
};

export default class CheckboxField extends React.Component {
onToggle() {
this.props.onChange(this.props.name);
this.props.onChange(!this.props.value);
}
render() {
return (
Expand Down
48 changes: 48 additions & 0 deletions superset/assets/javascripts/explorev2/components/ControlHeader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { PropTypes } from 'react';
import { ControlLabel, OverlayTrigger, Tooltip } from 'react-bootstrap';
import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';

const propTypes = {
label: PropTypes.string.isRequired,
description: PropTypes.string,
validationErrors: PropTypes.array,
};

const defaultProps = {
description: null,
validationErrors: [],
};

export default function ControlHeader({ label, description, validationErrors }) {
const hasError = (validationErrors.length > 0);
return (
<ControlLabel>
{hasError ?
<strong className="text-danger">{label}</strong> :
<span>{label}</span>
}
{' '}
{(validationErrors.length > 0) &&
<span>
<OverlayTrigger
placement="right"
overlay={
<Tooltip id={'error-tooltip'}>
{validationErrors.join(' ')}
</Tooltip>
}
>
<i className="fa fa-exclamation-circle text-danger" />
</OverlayTrigger>
{' '}
</span>
}
{description &&
<InfoTooltipWithTrigger label={label} tooltip={description} />
}
</ControlLabel>
);
}

ControlHeader.propTypes = propTypes;
ControlHeader.defaultProps = defaultProps;

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ class ControlPanelsContainer extends React.Component {
this.fieldOverrides = this.fieldOverrides.bind(this);
this.getFieldData = this.getFieldData.bind(this);
this.removeAlert = this.removeAlert.bind(this);
this.onChange = this.onChange.bind(this);
}
componentWillMount() {
const datasource_id = this.props.form_data.datasource;
Expand All @@ -44,14 +43,8 @@ class ControlPanelsContainer extends React.Component {
}
}
}
onChange(name, value) {
this.props.actions.setFieldValue(this.props.datasource_type, name, value);
}
getFieldData(fs) {
const fieldOverrides = this.fieldOverrides();
if (!this.props.fields) {
return null;
}
let fieldData = this.props.fields[fs] || {};
if (fieldOverrides.hasOwnProperty(fs)) {
const overrideData = fieldOverrides[fs];
Expand Down Expand Up @@ -100,13 +93,14 @@ class ControlPanelsContainer extends React.Component {
{section.fieldSetRows.map((fieldSets, i) => (
<FieldSetRow
key={`fieldsetrow-${i}`}
fields={fieldSets.map(field => (
fields={fieldSets.map(fieldName => (
<FieldSet
name={field}
key={`field-${field}`}
onChange={this.onChange}
value={this.props.form_data[field]}
{...this.getFieldData(field)}
name={fieldName}
key={`field-${fieldName}`}
value={this.props.form_data[fieldName]}
validationErrors={this.props.fields[fieldName].validationErrors}
actions={this.props.actions}
{...this.getFieldData(fieldName)}
/>
))}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const propTypes = {
actions: React.PropTypes.object.isRequired,
datasource_type: React.PropTypes.string.isRequired,
chartStatus: React.PropTypes.string.isRequired,
fields: React.PropTypes.object.isRequired,
};


Expand Down Expand Up @@ -72,6 +73,28 @@ class ExploreViewContainer extends React.Component {
toggleModal() {
this.setState({ showModal: !this.state.showModal });
}
renderErrorMessage() {
// Returns an error message as a node if any errors are in the store
const errors = [];
for (const fieldName in this.props.fields) {
const field = this.props.fields[fieldName];
if (field.validationErrors && field.validationErrors.length > 0) {
errors.push(
<div key={fieldName}>
<strong>{`[ ${field.label} ] `}</strong>
{field.validationErrors.join('. ')}
</div>
);
}
}
let errorMessage;
if (errors.length > 0) {
errorMessage = (
<div style={{ textAlign: 'left' }}>{errors}</div>
);
}
return errorMessage;
}

render() {
return (
Expand All @@ -98,8 +121,9 @@ class ExploreViewContainer extends React.Component {
onQuery={this.onQuery.bind(this, this.props.form_data)}
onSave={this.toggleModal.bind(this)}
disabled={this.props.chartStatus === 'loading'}
errorMessage={this.renderErrorMessage()}
/>
<br /><br />
<br />
<ControlPanelsContainer
actions={this.props.actions}
form_data={this.props.form_data}
Expand All @@ -126,6 +150,7 @@ function mapStateToProps(state) {
datasource_type: state.datasource_type,
form_data: state.viz.form_data,
chartStatus: state.chartStatus,
fields: state.fields,
};
}

Expand Down
43 changes: 37 additions & 6 deletions superset/assets/javascripts/explorev2/components/FieldSet.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import CheckboxField from './CheckboxField';
import TextAreaField from './TextAreaField';
import SelectField from './SelectField';

import ControlLabelWithTooltip from './ControlLabelWithTooltip';
import ControlHeader from './ControlHeader';

const fieldMap = {
TextField,
Expand All @@ -15,14 +15,15 @@ const fieldMap = {
const fieldTypes = Object.keys(fieldMap);

const propTypes = {
actions: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
type: PropTypes.oneOf(fieldTypes).isRequired,
label: PropTypes.string.isRequired,
choices: PropTypes.arrayOf(PropTypes.array),
description: PropTypes.string,
places: PropTypes.number,
validators: PropTypes.any,
onChange: React.PropTypes.func,
validators: PropTypes.array,
validationErrors: PropTypes.array,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
Expand All @@ -31,16 +32,46 @@ const propTypes = {
};

const defaultProps = {
onChange: () => {},
validators: [],
validationErrors: [],
};

export default class FieldSet extends React.PureComponent {
constructor(props) {
super(props);
this.validate = this.validate.bind(this);
this.onChange = this.onChange.bind(this);
}
onChange(value) {
const validationErrors = this.validate(value);
this.props.actions.setFieldValue(this.props.name, value, validationErrors);
}
validate(value) {
const validators = this.props.validators;
const validationErrors = [];
if (validators && validators.length > 0) {
validators.forEach(f => {
const v = f(value);
if (v) {
validationErrors.push(v);
}
});
}
return validationErrors;
}
render() {
const FieldType = fieldMap[this.props.type];
return (
<div>
<ControlLabelWithTooltip label={this.props.label} description={this.props.description} />
<FieldType {...this.props} />
<ControlHeader
label={this.props.label}
description={this.props.description}
validationErrors={this.props.validationErrors}
/>
<FieldType
onChange={this.onChange}
{...this.props}
/>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,7 @@ export default class SelectField extends React.Component {
if (this.props.multi) {
optionValue = opt ? opt.map((o) => o.value) : null;
}
if (this.props.name === 'datasource' && optionValue !== null) {
this.props.onChange(this.props.name, optionValue, opt.label);
} else {
this.props.onChange(this.props.name, optionValue);
}
this.props.onChange(optionValue);
}
renderOption(opt) {
if (this.props.name === 'viz_type') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const defaultProps = {

export default class TextAreaField extends React.Component {
onChange(event) {
this.props.onChange(this.props.name, event.target.value);
this.props.onChange(event.target.value);
}
render() {
return (
Expand Down
Loading

0 comments on commit 470a6e9

Please sign in to comment.