getredash/redash#2642 Date/Time parameters: dynamic default value (current date/time)

This commit is contained in:
Levko Kravets 2018-07-25 17:37:50 +03:00
parent d70bcfd615
commit b0f0b49d1c
8 changed files with 105 additions and 89 deletions

View File

@ -5,10 +5,6 @@
"transform-object-assign",
["babel-plugin-transform-builtin-extend", {
"globals": ["Error"]
}],
["import", {
"libraryName": "antd",
"style": true
}]
]
}

View File

@ -2,7 +2,10 @@ import moment from 'moment';
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import { DatePicker } from 'antd';
import DatePicker from 'antd/lib/date-picker';
import 'antd/lib/style/core/iconfont.less';
import 'antd/lib/input/style/index.less';
import 'antd/lib/date-picker/style/index.less';
function DateInput({
value,
@ -22,12 +25,18 @@ function DateInput({
}
DateInput.propTypes = {
value: PropTypes.instanceOf(Date),
value: (props, propName, componentName) => {
const value = props[propName];
if ((value !== null) && !moment.isMoment(props[propName])) {
return new Error('Prop `' + propName + '` supplied to `' + componentName +
'` should be a Moment.js instance.');
}
},
onSelect: PropTypes.func,
};
DateInput.defaultProps = {
value: Date.now(),
value: null,
onSelect: () => {},
};

View File

@ -2,7 +2,10 @@ import moment from 'moment';
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import { DatePicker } from 'antd';
import DatePicker from 'antd/lib/date-picker';
import 'antd/lib/style/core/iconfont.less';
import 'antd/lib/input/style/index.less';
import 'antd/lib/date-picker/style/index.less';
function DateTimeInput({
value,
@ -25,13 +28,19 @@ function DateTimeInput({
}
DateTimeInput.propTypes = {
value: PropTypes.instanceOf(Date),
value: (props, propName, componentName) => {
const value = props[propName];
if ((value !== null) && !moment.isMoment(props[propName])) {
return new Error('Prop `' + propName + '` supplied to `' + componentName +
'` should be a Moment.js instance.');
}
},
withSeconds: PropTypes.bool,
onSelect: PropTypes.func,
};
DateTimeInput.defaultProps = {
value: Date.now(),
value: null,
withSeconds: false,
onSelect: () => {},
};

View File

@ -26,6 +26,12 @@
Global
</label>
</div>
<div class="form-group" ng-if="['date', 'datetime-local', 'datetime-with-seconds'].indexOf($ctrl.parameter.type) >= 0">
<label>
<input type="checkbox" class="form-inline" ng-model="$ctrl.parameter.useCurrentDateTime">
Use Today/Now as default value if no other value is set
</label>
</div>
<div class="form-group" ng-if="$ctrl.parameter.type === 'enum'">
<label>Dropdown List Values (newline delimited)</label>
<textarea class="form-control" rows="3" ng-model="$ctrl.parameter.enumOptions"></textarea>

View File

@ -11,12 +11,12 @@
<i class="zmdi zmdi-settings"></i>
</button>
<span ng-switch="param.type">
<date-time-input ng-switch-when="datetime-with-seconds" value="param.ngModel" with-seconds="true"
on-select="param.updateValue"></date-time-input>
<date-time-input ng-switch-when="datetime-local" value="param.ngModel"
on-select="param.updateValue"></date-time-input>
<date-input ng-switch-when="date" value="param.ngModel"
on-select="param.updateValue"></date-input>
<date-time-input ng-switch-when="datetime-with-seconds" value="param.normalizedValue"
on-select="param.setValue"></date-time-input>
<date-time-input ng-switch-when="datetime-local" value="param.normalizedValue"
on-select="param.setValue"></date-time-input>
<date-input ng-switch-when="date" value="param.normalizedValue"
on-select="param.setValue"></date-input>
<span ng-switch-when="enum">
<select ng-model="param.value" class="form-control">
<option ng-repeat="option in extractEnumOptions(param.enumOptions)" value="{{option}}">{{option}}</option>

View File

@ -1,10 +1,25 @@
import moment from 'moment';
import debug from 'debug';
import Mustache from 'mustache';
import { each, zipObject, isEmpty, map, filter, includes, union, uniq, has } from 'lodash';
import {
each, zipObject, isEmpty, map, filter, includes, union, uniq, has,
isNull, isUndefined,
} from 'lodash';
const logger = debug('redash:services:query');
const DATETIME_FORMATS = {
// eslint-disable-next-line quote-props
'date': 'YYYY-MM-DD',
'datetime-local': 'YYYY-MM-DD HH:mm',
'datetime-with-seconds': 'YYYY-MM-DD HH:mm:ss',
};
function normalizeNumericValue(value, defaultValue = null) {
const result = parseFloat(value);
return isFinite(result) ? result : defaultValue;
}
function collectParams(parts) {
let parameters = [];
@ -24,43 +39,69 @@ class Parameter {
this.title = parameter.title;
this.name = parameter.name;
this.type = parameter.type;
this.value = parameter.value;
this.useCurrentDateTime = parameter.useCurrentDateTime;
this.global = parameter.global;
this.enumOptions = parameter.enumOptions;
this.queryId = parameter.queryId;
// method to update parameter value from date/time picker component
// (react does not support two-way binding with `ngModel`)
this.updateValue = (value) => {
this.ngModel = value;
};
// validate value and init internal state
this.setValue(parameter.value);
// explicitly bind it to `this` to allow passing it as callback to Ant's
// DatePicker component
this.setValue = this.setValue.bind(this);
}
get ngModel() {
if (this.type === 'date' || this.type === 'datetime-local' || this.type === 'datetime-with-seconds') {
this.$$value = this.$$value || moment(this.value).toDate();
return this.$$value;
} else if (this.type === 'number') {
this.$$value = this.$$value || parseInt(this.value, 10);
return this.$$value;
get isEmpty() {
return isNull(this.getValue());
}
getValue() {
const isEmptyValue = isNull(this.value) || isUndefined(this.value) || (this.value === '');
if (isEmptyValue) {
if (
includes(['date', 'datetime-local', 'datetime-with-seconds'], this.type) &&
this.useCurrentDateTime
) {
return moment().format(DATETIME_FORMATS[this.type]);
}
return null; // normalize empty value
}
if (this.type === 'number') {
return normalizeNumericValue(this.value, null); // normalize empty value
}
return this.value;
}
set ngModel(value) {
if (value && this.type === 'date') {
this.value = moment(value).format('YYYY-MM-DD');
this.$$value = moment(this.value).toDate();
} else if (value && this.type === 'datetime-local') {
this.value = moment(value).format('YYYY-MM-DD HH:mm');
this.$$value = moment(this.value).toDate();
} else if (value && this.type === 'datetime-with-seconds') {
this.value = moment(value).format('YYYY-MM-DD HH:mm:ss');
this.$$value = moment(this.value).toDate();
setValue(value) {
if (includes(['date', 'datetime-local', 'datetime-with-seconds'], this.type)) {
value = moment(value);
if (value.isValid()) {
this.value = value.format(DATETIME_FORMATS[this.type]);
this.$$value = value;
} else {
this.value = this.$$value = value;
this.value = null;
this.$$value = null;
}
} else if (this.type === 'number') {
this.value = value;
this.$$value = normalizeNumericValue(value, null);
} else {
this.value = value;
this.$$value = value;
}
}
get normalizedValue() {
return this.$$value;
}
// TODO: Remove this property when finally moved to React
get ngModel() {
return this.normalizedValue;
}
set ngModel(value) {
this.setValue(value);
}
}
@ -130,7 +171,7 @@ class Parameters {
}
getMissing() {
return map(filter(this.get(), p => p.value === null || p.value === ''), i => i.title);
return map(filter(this.get(), p => p.isEmpty), i => i.title);
}
isRequired() {
@ -139,7 +180,7 @@ class Parameters {
getValues() {
const params = this.get();
return zipObject(map(params, i => i.name), map(params, i => i.value));
return zipObject(map(params, i => i.name), map(params, i => i.getValue()));
}
}

44
package-lock.json generated
View File

@ -82,41 +82,6 @@
"@babel/types": "7.0.0-beta.44"
}
},
"@babel/helper-module-imports": {
"version": "7.0.0-beta.54",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0-beta.54.tgz",
"integrity": "sha1-wtjhT/A0Ilv0MTVtt370Z7jTWqw=",
"dev": true,
"requires": {
"@babel/types": "7.0.0-beta.54",
"lodash": "^4.17.5"
},
"dependencies": {
"@babel/types": {
"version": "7.0.0-beta.54",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.54.tgz",
"integrity": "sha1-AlrWhJL+1ULBPxTFeaRMhI5TEGM=",
"dev": true,
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.5",
"to-fast-properties": "^2.0.0"
}
},
"esutils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
"integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
"dev": true
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
"dev": true
}
}
},
"@babel/helper-split-export-declaration": {
"version": "7.0.0-beta.44",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz",
@ -1219,15 +1184,6 @@
"babel-runtime": "^6.22.0"
}
},
"babel-plugin-import": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/babel-plugin-import/-/babel-plugin-import-1.8.0.tgz",
"integrity": "sha512-5Aw8aZnJPuhJdumK6mS2ZRlfmGaBIKm/h6dw5uS0bkRMTqwHespRG3NeN9x9TB4W38I16ZXGGlHHz+8Gt5/shQ==",
"dev": true,
"requires": {
"@babel/helper-module-imports": "^7.0.0-beta.34"
}
},
"babel-plugin-syntax-async-functions": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz",

View File

@ -80,7 +80,6 @@
"babel-eslint": "^8.2.3",
"babel-loader": "^7.1.2",
"babel-plugin-angularjs-annotate": "^0.8.2",
"babel-plugin-import": "^1.8.0",
"babel-plugin-transform-builtin-extend": "^1.1.2",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-object-assign": "^6.22.0",