diff --git a/client/app/components/dynamic-form/DynamicForm.jsx b/client/app/components/dynamic-form/DynamicForm.jsx
index b0ab61b1b0..b41b5630f9 100644
--- a/client/app/components/dynamic-form/DynamicForm.jsx
+++ b/client/app/components/dynamic-form/DynamicForm.jsx
@@ -7,21 +7,37 @@ import Checkbox from 'antd/lib/checkbox';
import Button from 'antd/lib/button';
import Upload from 'antd/lib/upload';
import Icon from 'antd/lib/icon';
-import { includes, isFunction } from 'lodash';
+import { includes, isFunction, isPlainObject, isArray } from 'lodash';
import Select from 'antd/lib/select';
import notification from '@/services/notification';
import { Field, Action, AntdForm } from '../proptypes';
import helper from './dynamicFormHelper';
+const { TextArea } = Input;
+
const fieldRules = ({ type, required, minLength }) => {
const requiredRule = required;
const minLengthRule = minLength && includes(['text', 'email', 'password'], type);
const emailTypeRule = type === 'email';
+ const jsonRule = type === 'json';
return [
requiredRule && { required, message: 'This field is required.' },
minLengthRule && { min: minLength, message: 'This field is too short.' },
emailTypeRule && { type: 'email', message: 'This field must be a valid email.' },
+ jsonRule && {
+ type: 'object',
+ transform(x) {
+ if (x.trim()) {
+ try {
+ JSON.parse(x);
+ } catch {
+ return '';
+ }
+ }
+ },
+ message: 'This field must be a JSON string.',
+ },
].filter(rule => rule);
};
@@ -74,13 +90,22 @@ class DynamicForm extends React.Component {
}));
};
+ parseValues = (values) => {
+ this.props.fields.forEach((field) => {
+ if (field.type === 'json' && field.name in values) {
+ values[field.name] = JSON.parse(values[field.name]);
+ }
+ });
+ return values;
+ };
+
handleSubmit = (e) => {
this.setState({ isSubmitting: true });
e.preventDefault();
this.props.form.validateFieldsAndScroll((err, values) => {
if (!err) {
this.props.onSubmit(
- values,
+ this.parseValues(values),
(msg) => {
const { setFieldsValue, getFieldsValue } = this.props.form;
this.setState({ isSubmitting: false });
@@ -155,8 +180,13 @@ class DynamicForm extends React.Component {
renderField(field, props) {
const { getFieldDecorator } = this.props.form;
- const { name, type, initialValue } = field;
+ const { name, type } = field;
const fieldLabel = field.title || helper.toHuman(name);
+ let initialValue = field.initialValue;
+
+ if (isPlainObject(initialValue) || isArray(initialValue)) {
+ initialValue = JSON.stringify(field.initialValue);
+ }
const options = {
rules: fieldRules(field),
@@ -174,6 +204,8 @@ class DynamicForm extends React.Component {
return field.content;
} else if (type === 'number') {
return getFieldDecorator(name, options)();
+ } else if (type === 'json') {
+ return getFieldDecorator(name, options)();
}
return getFieldDecorator(name, options)();
}
@@ -185,12 +217,6 @@ class DynamicForm extends React.Component {
const fieldLabel = title || helper.toHuman(name);
const { feedbackIcons, form } = this.props;
- const formItemProps = {
- className: 'm-b-10',
- hasFeedback: type !== 'checkbox' && type !== 'file' && feedbackIcons,
- label: type === 'checkbox' ? '' : fieldLabel,
- };
-
const fieldProps = {
...field.props,
className: 'w-100',
@@ -201,6 +227,12 @@ class DynamicForm extends React.Component {
placeholder: field.placeholder,
'data-test': fieldLabel,
};
+ const formItemProps = {
+ className: 'm-b-10',
+ hasFeedback: type !== 'checkbox' && type !== 'file' && feedbackIcons,
+ label: type === 'checkbox' ? '' : fieldLabel,
+ extra: fieldProps.extra,
+ };
return (
diff --git a/client/app/components/dynamic-form/dynamicFormHelper.js b/client/app/components/dynamic-form/dynamicFormHelper.js
index c1d17997d3..f917e3ba6c 100644
--- a/client/app/components/dynamic-form/dynamicFormHelper.js
+++ b/client/app/components/dynamic-form/dynamicFormHelper.js
@@ -5,13 +5,15 @@ function orderedInputs(properties, order, targetOptions) {
const inputs = new Array(order.length);
Object.keys(properties).forEach((key) => {
const position = order.indexOf(key);
+ const field = properties[key];
const input = {
name: key,
- title: properties[key].title,
- type: properties[key].type,
- placeholder: properties[key].default && properties[key].default.toString(),
- required: properties[key].required,
+ title: field.title,
+ type: field.type,
+ placeholder: field.default && field.default.toString(),
+ required: field.required,
initialValue: targetOptions[key],
+ props: field.props,
};
if (position > -1) {
@@ -41,6 +43,10 @@ function normalizeSchema(configurationSchema) {
prop.type = 'text';
}
+ if (prop.type === 'object') {
+ prop.type = 'json';
+ }
+
prop.required = includes(configurationSchema.required, name);
});
diff --git a/client/app/components/proptypes.js b/client/app/components/proptypes.js
index 05b585904d..3af06b5c69 100644
--- a/client/app/components/proptypes.js
+++ b/client/app/components/proptypes.js
@@ -43,11 +43,13 @@ export const Field = PropTypes.shape({
'file',
'select',
'content',
+ 'json',
]).isRequired,
initialValue: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
+ PropTypes.object,
PropTypes.arrayOf(PropTypes.string),
PropTypes.arrayOf(PropTypes.number),
]),
diff --git a/redash/handlers/data_sources.py b/redash/handlers/data_sources.py
index 0d814a0209..13302f7055 100644
--- a/redash/handlers/data_sources.py
+++ b/redash/handlers/data_sources.py
@@ -45,7 +45,7 @@ def post(self, data_source_id):
try:
data_source.options.set_schema(schema)
data_source.options.update(filter_none(req['options']))
- except ValidationError:
+ except ValidationError, e:
abort(400)
data_source.type = req['type']
diff --git a/redash/query_runner/presto.py b/redash/query_runner/presto.py
index 2966d1ccf9..82ad821436 100644
--- a/redash/query_runner/presto.py
+++ b/redash/query_runner/presto.py
@@ -59,8 +59,17 @@ def configuration_schema(cls):
'password': {
'type': 'string'
},
+ 'extras': {
+ 'type': 'object',
+ 'default': '{ "requests_kwargs": null }',
+ 'props': {
+ 'rows': 2,
+ 'extra': 'Extra kwargs passed to presto.connect(...)',
+ }
+ }
},
- 'order': ['host', 'protocol', 'port', 'username', 'password', 'schema', 'catalog'],
+ 'order': ['host', 'protocol', 'port', 'username', 'password',
+ 'schema', 'catalog', 'extras'],
'required': ['host']
}
@@ -105,7 +114,8 @@ def run_query(self, query, user):
username=self.configuration.get('username', 'redash'),
password=(self.configuration.get('password') or None),
catalog=self.configuration.get('catalog', 'hive'),
- schema=self.configuration.get('schema', 'default'))
+ schema=self.configuration.get('schema', 'default'),
+ **self.configuration.get('extras', {}))
cursor = connection.cursor()