[explorev2] adding support for client side validators on controls (#1920)

* Adding support for client side validators on controls

* Applying validators to more fields

* Addressing comments
This commit is contained in:
Maxime Beauchemin
2017-01-12 09:21:17 -08:00
committed by GitHub
parent fc74fbeeaa
commit 470a6e9d76
20 changed files with 237 additions and 105 deletions

View File

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

View File

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

View File

@@ -1,26 +0,0 @@
import React, { PropTypes } from 'react';
import { ControlLabel } from 'react-bootstrap';
import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
const propTypes = {
label: PropTypes.string.isRequired,
description: PropTypes.string,
};
const defaultProps = {
description: null,
};
export default function ControlLabelWithTooltip({ label, description }) {
return (
<ControlLabel>
{label} &nbsp;
{description &&
<InfoTooltipWithTrigger label={label} tooltip={description} />
}
</ControlLabel>
);
}
ControlLabelWithTooltip.propTypes = propTypes;
ControlLabelWithTooltip.defaultProps = defaultProps;

View File

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

View File

@@ -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,
};
@@ -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 (
@@ -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}
@@ -126,6 +150,7 @@ function mapStateToProps(state) {
datasource_type: state.datasource_type,
form_data: state.viz.form_data,
chartStatus: state.chartStatus,
fields: state.fields,
};
}

View File

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

View File

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

View File

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

View File

@@ -18,16 +18,17 @@ const defaultProps = {
export default class TextField extends React.Component {
onChange(event) {
this.props.onChange(this.props.name, event.target.value);
this.props.onChange(event.target.value);
}
render() {
const value = this.props.value || '';
return (
<FormGroup controlId="formInlineName" bsSize="small">
<FormControl
type="text"
placeholder=""
onChange={this.onChange.bind(this)}
value={this.props.value}
value={value}
/>
</FormGroup>
);