diff --git a/.gitignore b/.gitignore index 350e1f610d..9ab883cbd8 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ npm-debug.log client/cypress/screenshots client/cypress/videos + +client/app/assets/less/**/*.css +client/app/visualizations/vega/vega.css diff --git a/client/app/assets/less/inc/ant-variables.less b/client/app/assets/less/inc/ant-variables.less index 99e379a841..7bf7e7f724 100644 --- a/client/app/assets/less/inc/ant-variables.less +++ b/client/app/assets/less/inc/ant-variables.less @@ -82,5 +82,6 @@ /* -------------------------------------------------------- Notification -----------------------------------------------------------*/ +@notification-padding-vertical: 16px; @notification-padding: @notification-padding-vertical 48px @notification-padding-vertical 17px; @notification-width: auto; diff --git a/client/app/assets/less/redash/query.less b/client/app/assets/less/redash/query.less index 4c73715ee9..27d44cd5f5 100644 --- a/client/app/assets/less/redash/query.less +++ b/client/app/assets/less/redash/query.less @@ -194,6 +194,8 @@ edit-in-place p.editable:hover { } .query__vis { + height: 100%; + table { border: 1px solid #f0f0f0; } diff --git a/client/app/components/QueryEditor.jsx b/client/app/components/QueryEditor.jsx index b2df146704..6cb82fdf44 100644 --- a/client/app/components/QueryEditor.jsx +++ b/client/app/components/QueryEditor.jsx @@ -4,17 +4,9 @@ import Tooltip from 'antd/lib/tooltip'; import { react2angular } from 'react2angular'; import AceEditor from 'react-ace'; -import ace from 'brace'; +import { snippetManager, langTools } from '@/components/editor'; import notification from '@/services/notification'; -import 'brace/ext/language_tools'; -import 'brace/mode/json'; -import 'brace/mode/python'; -import 'brace/mode/sql'; -import 'brace/mode/yaml'; -import 'brace/theme/textmate'; -import 'brace/ext/searchbox'; - import { Query } from '@/services/query'; import { QuerySnippet } from '@/services/query-snippet'; import { KeyboardShortcuts } from '@/services/keyboard-shortcuts'; @@ -26,23 +18,6 @@ import { DataSource, Schema } from './proptypes'; import './QueryEditor.css'; -const langTools = ace.acequire('ace/ext/language_tools'); -const snippetsModule = ace.acequire('ace/snippets'); - -// By default Ace will try to load snippet files for the different modes and fail. -// We don't need them, so we use these placeholders until we define our own. -function defineDummySnippets(mode) { - ace.define(`ace/snippets/${mode}`, ['require', 'exports', 'module'], (require, exports) => { - exports.snippetText = ''; - exports.scope = mode; - }); -} - -defineDummySnippets('python'); -defineDummySnippets('sql'); -defineDummySnippets('json'); -defineDummySnippets('yaml'); - class QueryEditor extends React.Component { static propTypes = { queryText: PropTypes.string.isRequired, @@ -160,7 +135,6 @@ class QueryEditor extends React.Component { }); QuerySnippet.query((snippets) => { - const snippetManager = snippetsModule.snippetManager; const m = { snippetText: '', }; diff --git a/client/app/components/dynamic-form/DynamicForm.jsx b/client/app/components/dynamic-form/DynamicForm.jsx index 673b7ecb3d..3be3714e6d 100644 --- a/client/app/components/dynamic-form/DynamicForm.jsx +++ b/client/app/components/dynamic-form/DynamicForm.jsx @@ -7,22 +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 AceEditorInput from '@/components/AceEditorInput'; 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.' }, + 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 && x.trim()) { + try { + JSON.parse(x); + } catch { + return ''; + } + } + }, + message: 'This field must be a JSON string.', + }, ].filter(rule => rule); }; @@ -75,13 +90,27 @@ class DynamicForm extends React.Component { })); }; + parseValues = (values) => { + this.props.fields.forEach((field) => { + if (field.type === 'json' && field.name in values) { + try { + values[field.name] = JSON.parse(values[field.name]); + } catch { + // Invalid JSON must be discarded + values[field.name] = null; + } + } + }); + 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 }); @@ -156,8 +185,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), @@ -176,9 +210,11 @@ class DynamicForm extends React.Component { } else if (type === 'number') { return getFieldDecorator(name, options)(); } else if (type === 'textarea') { - return getFieldDecorator(name, options)(); + return getFieldDecorator(name, options)(