From c81f46e6a63dcc84ef7d337093ea80de1a61c79d Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Tue, 6 Aug 2019 22:15:59 -0300 Subject: [PATCH 01/22] Start draft for new Parameter structure --- .../services/parameters/NumberParameter.js | 16 ++++ client/app/services/parameters/Parameter.js | 77 +++++++++++++++++++ .../app/services/parameters/TextParameter.js | 16 ++++ client/app/services/parameters/index.js | 3 + 4 files changed, 112 insertions(+) create mode 100644 client/app/services/parameters/NumberParameter.js create mode 100644 client/app/services/parameters/Parameter.js create mode 100644 client/app/services/parameters/TextParameter.js create mode 100644 client/app/services/parameters/index.js diff --git a/client/app/services/parameters/NumberParameter.js b/client/app/services/parameters/NumberParameter.js new file mode 100644 index 0000000000..755d4f0bec --- /dev/null +++ b/client/app/services/parameters/NumberParameter.js @@ -0,0 +1,16 @@ +import { toNumber } from 'lodash'; +import { Parameter } from '.'; + +class NumberParameter extends Parameter { + constructor(parameter, parentQueryId) { + super(parameter, parentQueryId); + this.setValue(parameter.value); + } + + getValue() { + const value = toNumber(this.value); + return !isNaN(value) ? value : null; + } +} + +export default NumberParameter; diff --git a/client/app/services/parameters/Parameter.js b/client/app/services/parameters/Parameter.js new file mode 100644 index 0000000000..b1c12d1f99 --- /dev/null +++ b/client/app/services/parameters/Parameter.js @@ -0,0 +1,77 @@ +import { isNull, isObject, isFunction, has } from 'lodash'; +import { TextParameter, NumberParameter } from '.'; + +class Parameter { + constructor(parameter, parentQueryId) { + this.title = parameter.title; + this.name = parameter.name; + this.type = parameter.type; + this.global = parameter.global; // backward compatibility in Widget service + this.parentQueryId = parentQueryId; + + // Used for meta-parameters (i.e. dashboard-level params) + this.locals = []; + + // Used for URL serialization + Object.defineProperty(this, 'urlPrefix', { + configurable: true, + enumerable: false, // don't save it + writable: true, + value: 'p_', + }); + } + + static create(param) { + switch (param.type) { + case 'text': + return new TextParameter(param); + case 'number': + return new NumberParameter(param); + default: + return null; + } + } + + static getValue(param, extra = {}) { + if (!isObject(param) || !isFunction(param.getValue)) { + return null; + } + + return param.getValue(extra); + } + + static setValue(param, value) { + if (!isObject(param) || !isFunction(param.setValue)) { + return null; + } + + return param.setValue(value); + } + + get isEmpty() { + return isNull(this.getValue()); + } + + clone() { + return Parameter.create(this); + } + + toUrlParams() { + const prefix = this.urlPrefix; + return { + [`${prefix}${this.name}`]: !this.isEmpty ? this.value : null, + [`${prefix}${this.name}.start`]: null, + [`${prefix}${this.name}.end`]: null, + }; + } + + fromUrlParams(query) { + const prefix = this.urlPrefix; + const key = `${prefix}${this.name}`; + if (has(query, key)) { + this.setValue(query[key]); + } + } +} + +export default Parameter; diff --git a/client/app/services/parameters/TextParameter.js b/client/app/services/parameters/TextParameter.js new file mode 100644 index 0000000000..8f33a0d78d --- /dev/null +++ b/client/app/services/parameters/TextParameter.js @@ -0,0 +1,16 @@ +import { toString, isEmpty } from 'lodash'; +import { Parameter } from '.'; + +class TextParameter extends Parameter { + constructor(parameter, parentQueryId) { + super(parameter, parentQueryId); + this.setValue(parameter.value); + } + + getValue() { + const value = toString(this.value); + return !isEmpty(value) ? value : null; + } +} + +export default TextParameter; diff --git a/client/app/services/parameters/index.js b/client/app/services/parameters/index.js new file mode 100644 index 0000000000..f90b826d40 --- /dev/null +++ b/client/app/services/parameters/index.js @@ -0,0 +1,3 @@ +export { default as Parameter } from './Parameter'; +export { default as TextParameter } from './TextParameter'; +export { default as NumberParameter } from './NumberParameter'; From 20a2f835a2c1056bd4abd31dd330548d143500ab Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Wed, 7 Aug 2019 18:45:18 -0300 Subject: [PATCH 02/22] Add the rest of the methods --- .../services/parameters/NumberParameter.js | 12 ++-- client/app/services/parameters/Parameter.js | 65 +++++++++++++++++-- .../app/services/parameters/TextParameter.js | 9 ++- 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/client/app/services/parameters/NumberParameter.js b/client/app/services/parameters/NumberParameter.js index 755d4f0bec..f29b7dadb0 100644 --- a/client/app/services/parameters/NumberParameter.js +++ b/client/app/services/parameters/NumberParameter.js @@ -1,4 +1,4 @@ -import { toNumber } from 'lodash'; +import { toNumber, isNull, isUndefined } from 'lodash'; import { Parameter } from '.'; class NumberParameter extends Parameter { @@ -7,9 +7,13 @@ class NumberParameter extends Parameter { this.setValue(parameter.value); } - getValue() { - const value = toNumber(this.value); - return !isNaN(value) ? value : null; + static normalizeValue(value) { + if (isNull(value) || isUndefined(value)) { + return null; + } + + const normalizedValue = toNumber(value); + return !isNaN(normalizedValue) ? normalizedValue : 0; } } diff --git a/client/app/services/parameters/Parameter.js b/client/app/services/parameters/Parameter.js index b1c12d1f99..681a5142b6 100644 --- a/client/app/services/parameters/Parameter.js +++ b/client/app/services/parameters/Parameter.js @@ -1,4 +1,4 @@ -import { isNull, isObject, isFunction, has } from 'lodash'; +import { isNull, isObject, isFunction, isUndefined, isEqual, has } from 'lodash'; import { TextParameter, NumberParameter } from '.'; class Parameter { @@ -21,17 +21,22 @@ class Parameter { }); } - static create(param) { + static create(param, parentQueryId) { switch (param.type) { - case 'text': - return new TextParameter(param); case 'number': - return new NumberParameter(param); + return new NumberParameter(param, parentQueryId); default: - return null; + return new TextParameter({ ...param, type: 'text' }, parentQueryId); } } + static normalizeValue(value) { + if (isUndefined(value)) { + return null; + } + return value; + } + static getValue(param, extra = {}) { if (!isObject(param) || !isFunction(param.getValue)) { return null; @@ -52,10 +57,54 @@ class Parameter { return isNull(this.getValue()); } + get hasPendingValue() { + return this.pendingValue !== undefined && !isEqual(this.pendingValue, this.normalizedValue); + } + + 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); + } + clone() { return Parameter.create(this); } + setValue(value) { + const normalizedValue = this.constructor.normalizeValue(value); + this.value = normalizedValue; + this.$$value = normalizedValue; + + this.clearPendingValue(); + return this; + } + + getValue() { + return this.value; + } + + setPendingValue(value) { + this.pendingValue = this.constructor.normalizeValue(value); + } + + applyPendingValue() { + if (this.hasPendingValue) { + this.setValue(this.pendingValue); + } + } + + clearPendingValue() { + this.pendingValue = undefined; + } + toUrlParams() { const prefix = this.urlPrefix; return { @@ -72,6 +121,10 @@ class Parameter { this.setValue(query[key]); } } + + toQueryTextFragment() { + return `{{ ${this.name} }}`; + } } export default Parameter; diff --git a/client/app/services/parameters/TextParameter.js b/client/app/services/parameters/TextParameter.js index 8f33a0d78d..d0bb4bd3c5 100644 --- a/client/app/services/parameters/TextParameter.js +++ b/client/app/services/parameters/TextParameter.js @@ -7,9 +7,12 @@ class TextParameter extends Parameter { this.setValue(parameter.value); } - getValue() { - const value = toString(this.value); - return !isEmpty(value) ? value : null; + static normalizeValue(value) { + const normalizedValue = toString(value); + if (isEmpty(normalizedValue)) { + return null; + } + return normalizedValue; } } From 5c7e74543bfa462daf08e5fa2790b01014c42074 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Wed, 7 Aug 2019 20:11:33 -0300 Subject: [PATCH 03/22] EnumParameter --- .../app/services/parameters/EnumParameter.js | 73 +++++++++++++++++++ .../services/parameters/NumberParameter.js | 9 +-- client/app/services/parameters/Parameter.js | 23 +++--- client/app/services/parameters/index.js | 1 + 4 files changed, 90 insertions(+), 16 deletions(-) create mode 100644 client/app/services/parameters/EnumParameter.js diff --git a/client/app/services/parameters/EnumParameter.js b/client/app/services/parameters/EnumParameter.js new file mode 100644 index 0000000000..ba42a5833c --- /dev/null +++ b/client/app/services/parameters/EnumParameter.js @@ -0,0 +1,73 @@ +import { isArray, isEmpty, includes, intersection, get, map, join, has } from 'lodash'; +import { Parameter } from '.'; + +class EnumParameter extends Parameter { + constructor(parameter, parentQueryId) { + super(parameter, parentQueryId); + this.enumOptions = parameter.enumOptions; + this.multiValuesOptions = parameter.multiValuesOptions; + this.setValue(parameter.value); + } + + normalizeValue(value) { + if (isEmpty(this.enumOptions)) { + return null; + } + + const enumOptionsArray = this.enumOptions.split('\n') || []; + if (this.multiValuesOptions) { + if (!isArray(value)) { + value = [value]; + } + value = intersection(value, enumOptionsArray); + } else if (!value || isArray(value) || !includes(enumOptionsArray, value)) { + value = enumOptionsArray[0]; + } + return value; + } + + getValue(extra = {}) { + const { joinListValues } = extra; + if (joinListValues && isArray(this.value)) { + const separator = get(this.multiValuesOptions, 'separator', ','); + const prefix = get(this.multiValuesOptions, 'prefix', ''); + const suffix = get(this.multiValuesOptions, 'suffix', ''); + const parameterValues = map(this.value, v => `${prefix}${v}${suffix}`); + return join(parameterValues, separator); + } + return this.value; + } + + toUrlParams() { + const prefix = this.urlPrefix; + + let urlParam = this.value; + if (this.multiValuesOptions && isArray(this.value)) { + urlParam = JSON.stringify(this.value); + } + + return { + [`${prefix}${this.name}`]: !this.isEmpty ? urlParam : null, + [`${prefix}${this.name}.start`]: null, + [`${prefix}${this.name}.end`]: null, + }; + } + + fromUrlParams(query) { + const prefix = this.urlPrefix; + const key = `${prefix}${this.name}`; + if (has(query, key)) { + if (this.multiValuesOptions) { + try { + this.setValue(JSON.parse(query[key])); + } catch (e) { + this.setValue(query[key]); + } + } else { + this.setValue(query[key]); + } + } + } +} + +export default EnumParameter; diff --git a/client/app/services/parameters/NumberParameter.js b/client/app/services/parameters/NumberParameter.js index f29b7dadb0..2029235cf8 100644 --- a/client/app/services/parameters/NumberParameter.js +++ b/client/app/services/parameters/NumberParameter.js @@ -1,4 +1,4 @@ -import { toNumber, isNull, isUndefined } from 'lodash'; +import { toNumber } from 'lodash'; import { Parameter } from '.'; class NumberParameter extends Parameter { @@ -7,11 +7,8 @@ class NumberParameter extends Parameter { this.setValue(parameter.value); } - static normalizeValue(value) { - if (isNull(value) || isUndefined(value)) { - return null; - } - + // eslint-disable-next-line class-methods-use-this + normalizeValue(value) { const normalizedValue = toNumber(value); return !isNaN(normalizedValue) ? normalizedValue : 0; } diff --git a/client/app/services/parameters/Parameter.js b/client/app/services/parameters/Parameter.js index 681a5142b6..ad41e5a426 100644 --- a/client/app/services/parameters/Parameter.js +++ b/client/app/services/parameters/Parameter.js @@ -1,5 +1,5 @@ import { isNull, isObject, isFunction, isUndefined, isEqual, has } from 'lodash'; -import { TextParameter, NumberParameter } from '.'; +import { TextParameter, NumberParameter, EnumParameter } from '.'; class Parameter { constructor(parameter, parentQueryId) { @@ -25,18 +25,13 @@ class Parameter { switch (param.type) { case 'number': return new NumberParameter(param, parentQueryId); + case 'enum': + return new EnumParameter(param, parentQueryId); default: return new TextParameter({ ...param, type: 'text' }, parentQueryId); } } - static normalizeValue(value) { - if (isUndefined(value)) { - return null; - } - return value; - } - static getValue(param, extra = {}) { if (!isObject(param) || !isFunction(param.getValue)) { return null; @@ -78,8 +73,16 @@ class Parameter { return Parameter.create(this); } + // eslint-disable-next-line class-methods-use-this + normalizeValue(value) { + if (isUndefined(value)) { + return null; + } + return value; + } + setValue(value) { - const normalizedValue = this.constructor.normalizeValue(value); + const normalizedValue = this.normalizeValue(value); this.value = normalizedValue; this.$$value = normalizedValue; @@ -92,7 +95,7 @@ class Parameter { } setPendingValue(value) { - this.pendingValue = this.constructor.normalizeValue(value); + this.pendingValue = this.normalizeValue(value); } applyPendingValue() { diff --git a/client/app/services/parameters/index.js b/client/app/services/parameters/index.js index f90b826d40..5b75752e6b 100644 --- a/client/app/services/parameters/index.js +++ b/client/app/services/parameters/index.js @@ -1,3 +1,4 @@ export { default as Parameter } from './Parameter'; export { default as TextParameter } from './TextParameter'; export { default as NumberParameter } from './NumberParameter'; +export { default as EnumParameter } from './EnumParameter'; From a05d4b25698feda32b4ceb4d69e08abb73ecf4d7 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Wed, 7 Aug 2019 20:22:30 -0300 Subject: [PATCH 04/22] QueryBasedDropdownParameter --- client/app/services/parameters/Parameter.js | 6 +- .../parameters/QueryBasedDropdownParameter.js | 65 +++++++++++++++++++ client/app/services/parameters/index.js | 1 + 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 client/app/services/parameters/QueryBasedDropdownParameter.js diff --git a/client/app/services/parameters/Parameter.js b/client/app/services/parameters/Parameter.js index ad41e5a426..4065521810 100644 --- a/client/app/services/parameters/Parameter.js +++ b/client/app/services/parameters/Parameter.js @@ -1,5 +1,7 @@ import { isNull, isObject, isFunction, isUndefined, isEqual, has } from 'lodash'; -import { TextParameter, NumberParameter, EnumParameter } from '.'; +import { + TextParameter, NumberParameter, EnumParameter, QueryBasedDropdownParameter, +} from '.'; class Parameter { constructor(parameter, parentQueryId) { @@ -27,6 +29,8 @@ class Parameter { return new NumberParameter(param, parentQueryId); case 'enum': return new EnumParameter(param, parentQueryId); + case 'query': + return new QueryBasedDropdownParameter(param, parentQueryId); default: return new TextParameter({ ...param, type: 'text' }, parentQueryId); } diff --git a/client/app/services/parameters/QueryBasedDropdownParameter.js b/client/app/services/parameters/QueryBasedDropdownParameter.js new file mode 100644 index 0000000000..bc10630a74 --- /dev/null +++ b/client/app/services/parameters/QueryBasedDropdownParameter.js @@ -0,0 +1,65 @@ +import { isArray, get, map, join, has } from 'lodash'; +import { Query } from '@/services/query'; +import { Parameter } from '.'; + +class QueryBasedDropdownParameter extends Parameter { + constructor(parameter, parentQueryId) { + super(parameter, parentQueryId); + this.queryId = parameter.queryId; + this.multiValuesOptions = parameter.multiValuesOptions; + this.setValue(parameter.value); + } + + getValue(extra = {}) { + const { joinListValues } = extra; + if (joinListValues && isArray(this.value)) { + const separator = get(this.multiValuesOptions, 'separator', ','); + const prefix = get(this.multiValuesOptions, 'prefix', ''); + const suffix = get(this.multiValuesOptions, 'suffix', ''); + const parameterValues = map(this.value, v => `${prefix}${v}${suffix}`); + return join(parameterValues, separator); + } + return this.value; + } + + toUrlParams() { + const prefix = this.urlPrefix; + + let urlParam = this.value; + if (this.multiValuesOptions && isArray(this.value)) { + urlParam = JSON.stringify(this.value); + } + + return { + [`${prefix}${this.name}`]: !this.isEmpty ? urlParam : null, + [`${prefix}${this.name}.start`]: null, + [`${prefix}${this.name}.end`]: null, + }; + } + + fromUrlParams(query) { + const prefix = this.urlPrefix; + const key = `${prefix}${this.name}`; + if (has(query, key)) { + if (this.multiValuesOptions) { + try { + this.setValue(JSON.parse(query[key])); + } catch (e) { + this.setValue(query[key]); + } + } else { + this.setValue(query[key]); + } + } + } + + loadDropdownValues() { + if (this.parentQueryId) { + return Query.associatedDropdown({ queryId: this.parentQueryId, dropdownQueryId: this.queryId }).$promise; + } + + return Query.asDropdown({ id: this.queryId }).$promise; + } +} + +export default QueryBasedDropdownParameter; diff --git a/client/app/services/parameters/index.js b/client/app/services/parameters/index.js index 5b75752e6b..a878c9bdf5 100644 --- a/client/app/services/parameters/index.js +++ b/client/app/services/parameters/index.js @@ -2,3 +2,4 @@ export { default as Parameter } from './Parameter'; export { default as TextParameter } from './TextParameter'; export { default as NumberParameter } from './NumberParameter'; export { default as EnumParameter } from './EnumParameter'; +export { default as QueryBasedDropdownParameter } from './QueryBasedDropdownParameter'; From 3c52dcbc33093b4d3b7868e3870de9b1663b5d10 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Wed, 7 Aug 2019 20:56:50 -0300 Subject: [PATCH 05/22] DateParameter --- .../app/services/parameters/DateParameter.js | 85 +++++++++++++++++++ client/app/services/parameters/Parameter.js | 7 +- .../app/services/parameters/TextParameter.js | 3 +- client/app/services/parameters/index.js | 1 + 4 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 client/app/services/parameters/DateParameter.js diff --git a/client/app/services/parameters/DateParameter.js b/client/app/services/parameters/DateParameter.js new file mode 100644 index 0000000000..84adfdadeb --- /dev/null +++ b/client/app/services/parameters/DateParameter.js @@ -0,0 +1,85 @@ +import { startsWith } from 'lodash'; +import moment from 'moment'; +import { Parameter } from '.'; + +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', +}; + +const DYNAMIC_PREFIX = 'd_'; + +const DYNAMIC_DATES = { + now: { + name: 'Today/Now', + value: () => moment(), + }, + yesterday: { + name: 'Yesterday', + value: () => moment().subtract(1, 'day'), + }, +}; + +export function isDynamicDate(value) { + if (!startsWith(value, DYNAMIC_PREFIX)) { + return false; + } + return !!DYNAMIC_DATES[value.substring(DYNAMIC_PREFIX.length)]; +} + +export function getDynamicDate(value) { + if (!isDynamicDate(value)) { + return null; + } + return DYNAMIC_DATES[value.substring(DYNAMIC_PREFIX.length)]; +} + +class DateParameter extends Parameter { + constructor(parameter, parentQueryId) { + super(parameter, parentQueryId); + this.useCurrentDateTime = parameter.useCurrentDateTime; + this.setValue(parameter.value); + } + + get hasDynamicValue() { + return isDynamicDate(this.value); + } + + get dynamicValue() { + return getDynamicDate(this.value); + } + + // eslint-disable-next-line class-methods-use-this + normalizeValue(value) { + if (isDynamicDate(value)) { + return value; + } + + const normalizedValue = moment(value); + return normalizedValue.isValid() ? normalizedValue : null; + } + + setValue(value) { + const normalizedValue = this.normalizeValue(value); + if (moment.isMoment(normalizedValue)) { + this.value = normalizedValue.format(DATETIME_FORMATS[this.type]); + } else { + this.value = normalizedValue; + } + this.$$value = normalizedValue; + return this; + } + + getValue() { + if (this.hasDynamicValue) { + if (this.dynamicValue) { + return this.dynamicValue.value().format(DATETIME_FORMATS[this.type]); + } + } + return this.value; + } +} + +export default DateParameter; diff --git a/client/app/services/parameters/Parameter.js b/client/app/services/parameters/Parameter.js index 4065521810..b20173e028 100644 --- a/client/app/services/parameters/Parameter.js +++ b/client/app/services/parameters/Parameter.js @@ -1,6 +1,7 @@ import { isNull, isObject, isFunction, isUndefined, isEqual, has } from 'lodash'; import { - TextParameter, NumberParameter, EnumParameter, QueryBasedDropdownParameter, + TextParameter, NumberParameter, EnumParameter, + QueryBasedDropdownParameter, DateParameter, } from '.'; class Parameter { @@ -31,6 +32,10 @@ class Parameter { return new EnumParameter(param, parentQueryId); case 'query': return new QueryBasedDropdownParameter(param, parentQueryId); + case 'date': + case 'datetime-local': + case 'datetime-with-seconds': + return new DateParameter(param, parentQueryId); default: return new TextParameter({ ...param, type: 'text' }, parentQueryId); } diff --git a/client/app/services/parameters/TextParameter.js b/client/app/services/parameters/TextParameter.js index d0bb4bd3c5..645adbc8e3 100644 --- a/client/app/services/parameters/TextParameter.js +++ b/client/app/services/parameters/TextParameter.js @@ -7,7 +7,8 @@ class TextParameter extends Parameter { this.setValue(parameter.value); } - static normalizeValue(value) { + // eslint-disable-next-line class-methods-use-this + normalizeValue(value) { const normalizedValue = toString(value); if (isEmpty(normalizedValue)) { return null; diff --git a/client/app/services/parameters/index.js b/client/app/services/parameters/index.js index a878c9bdf5..6f60255aed 100644 --- a/client/app/services/parameters/index.js +++ b/client/app/services/parameters/index.js @@ -3,3 +3,4 @@ export { default as TextParameter } from './TextParameter'; export { default as NumberParameter } from './NumberParameter'; export { default as EnumParameter } from './EnumParameter'; export { default as QueryBasedDropdownParameter } from './QueryBasedDropdownParameter'; +export { default as DateParameter } from './DateParameter'; From db009df64454b7806e0463c3b0c17b862e9000eb Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Wed, 7 Aug 2019 21:55:56 -0300 Subject: [PATCH 06/22] DateRangeParameter --- .../app/services/parameters/DateParameter.js | 6 +- .../services/parameters/DateRangeParameter.js | 157 ++++++++++++++++++ client/app/services/parameters/Parameter.js | 8 +- client/app/services/parameters/index.js | 1 + 4 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 client/app/services/parameters/DateRangeParameter.js diff --git a/client/app/services/parameters/DateParameter.js b/client/app/services/parameters/DateParameter.js index 84adfdadeb..0fdc3cfe77 100644 --- a/client/app/services/parameters/DateParameter.js +++ b/client/app/services/parameters/DateParameter.js @@ -1,4 +1,4 @@ -import { startsWith } from 'lodash'; +import { startsWith, isNull } from 'lodash'; import moment from 'moment'; import { Parameter } from '.'; @@ -69,6 +69,7 @@ class DateParameter extends Parameter { this.value = normalizedValue; } this.$$value = normalizedValue; + this.clearPendingValue(); return this; } @@ -78,6 +79,9 @@ class DateParameter extends Parameter { return this.dynamicValue.value().format(DATETIME_FORMATS[this.type]); } } + if (isNull(this.value) && this.useCurrentDateTime) { + return moment(); + } return this.value; } } diff --git a/client/app/services/parameters/DateRangeParameter.js b/client/app/services/parameters/DateRangeParameter.js new file mode 100644 index 0000000000..c38cb8aaea --- /dev/null +++ b/client/app/services/parameters/DateRangeParameter.js @@ -0,0 +1,157 @@ +import { startsWith, has } from 'lodash'; +import moment from 'moment'; +import { isObject, isArray } from 'util'; +import { Parameter } from '.'; + +const DATETIME_FORMATS = { + 'date-range': 'YYYY-MM-DD', + 'datetime-range': 'YYYY-MM-DD HH:mm', + 'datetime-range-with-seconds': 'YYYY-MM-DD HH:mm:ss', +}; + +const DYNAMIC_PREFIX = 'd_'; + +const DYNAMIC_DATE_RANGES = { + today: { + name: 'Today', + value: () => [moment().startOf('day'), moment().endOf('day')], + }, + yesterday: { + name: 'Yesterday', + value: () => [moment().subtract(1, 'day').startOf('day'), moment().subtract(1, 'day').endOf('day')], + }, + this_week: { + name: 'This week', + value: () => [moment().startOf('week'), moment().endOf('week')], + }, + this_month: { + name: 'This month', + value: () => [moment().startOf('month'), moment().endOf('month')], + }, + this_year: { + name: 'This year', + value: () => [moment().startOf('year'), moment().endOf('year')], + }, + last_week: { + name: 'Last week', + value: () => [moment().subtract(1, 'week').startOf('week'), moment().subtract(1, 'week').endOf('week')], + }, + last_month: { + name: 'Last month', + value: () => [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')], + }, + last_year: { + name: 'Last year', + value: () => [moment().subtract(1, 'year').startOf('year'), moment().subtract(1, 'year').endOf('year')], + }, + last_7_days: { + name: 'Last 7 days', + value: () => [moment().subtract(7, 'days'), moment()], + }, +}; + +export function isDynamicDateRange(value) { + if (!startsWith(value, DYNAMIC_PREFIX)) { + return false; + } + return !!DYNAMIC_DATE_RANGES[value.substring(DYNAMIC_PREFIX.length)]; +} + +export function getDynamicDateRange(value) { + if (!isDynamicDateRange(value)) { + return null; + } + return DYNAMIC_DATE_RANGES[value.substring(DYNAMIC_PREFIX.length)]; +} + +class DateRangeParameter extends Parameter { + constructor(parameter, parentQueryId) { + super(parameter, parentQueryId); + this.setValue(parameter.value); + } + + get hasDynamicValue() { + return isDynamicDateRange(this.value); + } + + get dynamicValue() { + return getDynamicDateRange(this.value); + } + + // eslint-disable-next-line class-methods-use-this + normalizeValue(value) { + if (isDynamicDateRange(value)) { + return value; + } + + if (isObject(value) && !isArray(value)) { + value = [value.start, value.end]; + } + + if (isArray(value) && (value.length === 2)) { + value = [moment(value[0]), moment(value[1])]; + if (value[0].isValid() && value[1].isValid()) { + return value; + } + } + return null; + } + + setValue(value) { + const normalizedValue = this.normalizeValue(value); + if (isArray(normalizedValue)) { + this.value = { + start: normalizedValue[0].format(DATETIME_FORMATS[this.type]), + end: normalizedValue[1].format(DATETIME_FORMATS[this.type]), + }; + } else { + this.value = normalizedValue; + } + this.$$value = normalizedValue; + this.clearPendingValue(); + return this; + } + + getValue() { + if (this.hasDynamicValue) { + if (this.dynamicValue) { + const dateRange = this.dynamicValue.value(); + return { + start: dateRange[0].format(DATETIME_FORMATS[this.type]), + end: dateRange[1].format(DATETIME_FORMATS[this.type]), + }; + } + } + return this.value; + } + + toUrlParams() { + const prefix = this.urlPrefix; + if (isObject(this.value) && this.value.start && this.value.end) { + return { + [`${prefix}${this.name}`]: null, + [`${prefix}${this.name}.start`]: this.value.start, + [`${prefix}${this.name}.end`]: this.value.end, + }; + } + return super.toUrlParams(); + } + + fromUrlParams(query) { + const prefix = this.urlPrefix; + const key = `${prefix}${this.name}`; + const keyStart = `${prefix}${this.name}.start`; + const keyEnd = `${prefix}${this.name}.end`; + if (has(query, key)) { + this.setValue(query[key]); + } else if (has(query, keyStart) && has(query, keyEnd)) { + this.setValue([query[keyStart], query[keyEnd]]); + } + } + + toQueryTextFragment() { + return `{{ ${this.name}.start }} {{ ${this.name}.end }}`; + } +} + +export default DateRangeParameter; diff --git a/client/app/services/parameters/Parameter.js b/client/app/services/parameters/Parameter.js index b20173e028..17578e5f89 100644 --- a/client/app/services/parameters/Parameter.js +++ b/client/app/services/parameters/Parameter.js @@ -1,7 +1,7 @@ import { isNull, isObject, isFunction, isUndefined, isEqual, has } from 'lodash'; import { - TextParameter, NumberParameter, EnumParameter, - QueryBasedDropdownParameter, DateParameter, + TextParameter, NumberParameter, EnumParameter, QueryBasedDropdownParameter, + DateParameter, DateRangeParameter, } from '.'; class Parameter { @@ -36,6 +36,10 @@ class Parameter { case 'datetime-local': case 'datetime-with-seconds': return new DateParameter(param, parentQueryId); + case 'date-range': + case 'datetime-range-local': + case 'datetime-range-with-seconds': + return new DateRangeParameter(param, parentQueryId); default: return new TextParameter({ ...param, type: 'text' }, parentQueryId); } diff --git a/client/app/services/parameters/index.js b/client/app/services/parameters/index.js index 6f60255aed..f9f75cb163 100644 --- a/client/app/services/parameters/index.js +++ b/client/app/services/parameters/index.js @@ -4,3 +4,4 @@ export { default as NumberParameter } from './NumberParameter'; export { default as EnumParameter } from './EnumParameter'; export { default as QueryBasedDropdownParameter } from './QueryBasedDropdownParameter'; export { default as DateParameter } from './DateParameter'; +export { default as DateRangeParameter } from './DateRangeParameter'; From 8cccf8b72e1068072ba6e2fdf8d96c91ef74679c Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Wed, 7 Aug 2019 22:00:33 -0300 Subject: [PATCH 07/22] Update Parameter usage on code --- .../app/components/ParameterMappingInput.jsx | 2 +- .../dynamic-parameters/DateParameter.jsx | 2 +- .../dynamic-parameters/DateRangeParameter.jsx | 2 +- client/app/components/parameters.js | 3 +- client/app/services/query.js | 392 +----------------- 5 files changed, 12 insertions(+), 389 deletions(-) diff --git a/client/app/components/ParameterMappingInput.jsx b/client/app/components/ParameterMappingInput.jsx index fe8e82b901..8d0fd2ed54 100644 --- a/client/app/components/ParameterMappingInput.jsx +++ b/client/app/components/ParameterMappingInput.jsx @@ -16,7 +16,7 @@ import Form from 'antd/lib/form'; import Tooltip from 'antd/lib/tooltip'; import { ParameterValueInput } from '@/components/ParameterValueInput'; import { ParameterMappingType } from '@/services/widget'; -import { Parameter } from '@/services/query'; +import { Parameter } from '@/services/parameters'; import { HelpTrigger } from '@/components/HelpTrigger'; import './ParameterMappingInput.less'; diff --git a/client/app/components/dynamic-parameters/DateParameter.jsx b/client/app/components/dynamic-parameters/DateParameter.jsx index 0776196eca..fdb71b41f0 100644 --- a/client/app/components/dynamic-parameters/DateParameter.jsx +++ b/client/app/components/dynamic-parameters/DateParameter.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import moment from 'moment'; import { includes } from 'lodash'; -import { isDynamicDate, getDynamicDate } from '@/services/query'; +import { isDynamicDate, getDynamicDate } from '@/services/parameters/DateParameter'; import DateInput from '@/components/DateInput'; import DateTimeInput from '@/components/DateTimeInput'; import DynamicButton from '@/components/dynamic-parameters/DynamicButton'; diff --git a/client/app/components/dynamic-parameters/DateRangeParameter.jsx b/client/app/components/dynamic-parameters/DateRangeParameter.jsx index 3e7b75895a..7578a3d4d5 100644 --- a/client/app/components/dynamic-parameters/DateRangeParameter.jsx +++ b/client/app/components/dynamic-parameters/DateRangeParameter.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import moment from 'moment'; import { includes, isArray, isObject } from 'lodash'; -import { isDynamicDateRange, getDynamicDateRange } from '@/services/query'; +import { isDynamicDateRange, getDynamicDateRange } from '@/services/parameters/DateRangeParameter'; import DateRangeInput from '@/components/DateRangeInput'; import DateTimeRangeInput from '@/components/DateTimeRangeInput'; import DynamicButton from '@/components/dynamic-parameters/DynamicButton'; diff --git a/client/app/components/parameters.js b/client/app/components/parameters.js index 515abe5df5..11500b491b 100644 --- a/client/app/components/parameters.js +++ b/client/app/components/parameters.js @@ -1,4 +1,5 @@ import { extend, filter, forEach, size } from 'lodash'; +import { Parameter } from '@/services/parameters'; import template from './parameters.html'; import EditParameterSettingsDialog from './EditParameterSettingsDialog'; @@ -58,7 +59,7 @@ function ParametersDirective($location, KeyboardShortcuts) { EditParameterSettingsDialog .showModal({ parameter }) .result.then((updated) => { - scope.parameters[index] = extend(parameter, updated).setValue(updated.value); + scope.parameters[index] = Parameter.create(extend(parameter, updated)); scope.onUpdated(); }); }; diff --git a/client/app/services/query.js b/client/app/services/query.js index 317da77c11..21ee659497 100644 --- a/client/app/services/query.js +++ b/client/app/services/query.js @@ -2,83 +2,18 @@ import moment from 'moment'; import debug from 'debug'; import Mustache from 'mustache'; import { - zipObject, isEmpty, map, filter, includes, union, uniq, has, get, intersection, - isNull, isUndefined, isArray, isObject, identity, extend, each, join, some, startsWith, + zipObject, isEmpty, map, filter, includes, union, + uniq, has, identity, extend, each, some, } from 'lodash'; +import { Parameter } from './parameters'; + Mustache.escape = identity; // do not html-escape values export let Query = null; // eslint-disable-line import/no-mutable-exports const logger = debug('redash:services:query'); -const DATETIME_FORMATS = { - // eslint-disable-next-line quote-props - 'date': 'YYYY-MM-DD', - 'date-range': 'YYYY-MM-DD', - 'datetime-local': 'YYYY-MM-DD HH:mm', - 'datetime-range': 'YYYY-MM-DD HH:mm', - 'datetime-with-seconds': 'YYYY-MM-DD HH:mm:ss', - 'datetime-range-with-seconds': 'YYYY-MM-DD HH:mm:ss', -}; - -const DYNAMIC_PREFIX = 'd_'; - -const DYNAMIC_DATE_RANGES = { - today: { - name: 'Today', - value: () => [moment().startOf('day'), moment().endOf('day')], - }, - yesterday: { - name: 'Yesterday', - value: () => [moment().subtract(1, 'day').startOf('day'), moment().subtract(1, 'day').endOf('day')], - }, - this_week: { - name: 'This week', - value: () => [moment().startOf('week'), moment().endOf('week')], - }, - this_month: { - name: 'This month', - value: () => [moment().startOf('month'), moment().endOf('month')], - }, - this_year: { - name: 'This year', - value: () => [moment().startOf('year'), moment().endOf('year')], - }, - last_week: { - name: 'Last week', - value: () => [moment().subtract(1, 'week').startOf('week'), moment().subtract(1, 'week').endOf('week')], - }, - last_month: { - name: 'Last month', - value: () => [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')], - }, - last_year: { - name: 'Last year', - value: () => [moment().subtract(1, 'year').startOf('year'), moment().subtract(1, 'year').endOf('year')], - }, - last_7_days: { - name: 'Last 7 days', - value: () => [moment().subtract(7, 'days'), moment()], - }, -}; - -const DYNAMIC_DATES = { - now: { - name: 'Today/Now', - value: () => moment(), - }, - yesterday: { - name: 'Yesterday', - value: () => moment().subtract(1, 'day'), - }, -}; - -function normalizeNumericValue(value, defaultValue = null) { - const result = parseFloat(value); - return isFinite(result) ? result : defaultValue; -} - function collectParams(parts) { let parameters = []; @@ -93,319 +28,6 @@ function collectParams(parts) { return parameters; } -function isDateParameter(paramType) { - return includes(['date', 'datetime-local', 'datetime-with-seconds'], paramType); -} - -function isDateRangeParameter(paramType) { - return includes(['date-range', 'datetime-range', 'datetime-range-with-seconds'], paramType); -} - -export function isDynamicDate(value) { - if (!startsWith(value, DYNAMIC_PREFIX)) { - return false; - } - return !!DYNAMIC_DATES[value.substring(DYNAMIC_PREFIX.length)]; -} - -export function isDynamicDateRange(value) { - if (!startsWith(value, DYNAMIC_PREFIX)) { - return false; - } - return !!DYNAMIC_DATE_RANGES[value.substring(DYNAMIC_PREFIX.length)]; -} - -export function getDynamicDate(value) { - if (!isDynamicDate(value)) { - return null; - } - return DYNAMIC_DATES[value.substring(DYNAMIC_PREFIX.length)]; -} - -export function getDynamicDateRange(value) { - if (!isDynamicDateRange(value)) { - return null; - } - return DYNAMIC_DATE_RANGES[value.substring(DYNAMIC_PREFIX.length)]; -} - -export class Parameter { - constructor(parameter, parentQueryId) { - this.title = parameter.title; - this.name = parameter.name; - this.type = parameter.type; - this.useCurrentDateTime = parameter.useCurrentDateTime; - this.global = parameter.global; // backward compatibility in Widget service - this.enumOptions = parameter.enumOptions; - this.multiValuesOptions = parameter.multiValuesOptions; - this.queryId = parameter.queryId; - this.parentQueryId = parentQueryId; - - // Used for meta-parameters (i.e. dashboard-level params) - this.locals = []; - - // validate value and init internal state - this.setValue(parameter.value); - - // Used for URL serialization - Object.defineProperty(this, 'urlPrefix', { - configurable: true, - enumerable: false, // don't save it - writable: true, - value: 'p_', - }); - } - - clone() { - return new Parameter(this, this.parentQueryId); - } - - get isEmpty() { - return isNull(this.getValue()); - } - - getValue(extra = {}) { - return this.constructor.getValue(this, extra); - } - - get hasDynamicValue() { - if (isDateParameter(this.type)) { - return isDynamicDate(this.value); - } - if (isDateRangeParameter(this.type)) { - return isDynamicDateRange(this.value); - } - return false; - } - - get dynamicValue() { - if (isDateParameter(this.type)) { - return getDynamicDate(this.value); - } - if (isDateRangeParameter(this.type)) { - return getDynamicDateRange(this.value); - } - return false; - } - - static getValue(param, extra = {}) { - const { value, type, useCurrentDateTime, multiValuesOptions } = param; - const isEmptyValue = isNull(value) || isUndefined(value) || (value === '') || (isArray(value) && value.length === 0); - if (isDateRangeParameter(type) && param.hasDynamicValue) { - const { dynamicValue } = param; - if (dynamicValue) { - const dateRange = dynamicValue.value(); - return { - start: dateRange[0].format(DATETIME_FORMATS[type]), - end: dateRange[1].format(DATETIME_FORMATS[type]), - }; - } - return null; - } - - if (isDateParameter(type) && param.hasDynamicValue) { - const { dynamicValue } = param; - if (dynamicValue) { - return dynamicValue.value().format(DATETIME_FORMATS[type]); - } - return null; - } - - if (isEmptyValue) { - // keep support for existing useCurentDateTime (not available in UI) - if ( - includes(['date', 'datetime-local', 'datetime-with-seconds'], type) && - useCurrentDateTime - ) { - return moment().format(DATETIME_FORMATS[type]); - } - return null; // normalize empty value - } - if (type === 'number') { - return normalizeNumericValue(value, null); // normalize empty value - } - - // join array in frontend when query is executed as a text - const { joinListValues } = extra; - if (includes(['enum', 'query'], type) && multiValuesOptions && isArray(value) && joinListValues) { - const separator = get(multiValuesOptions, 'separator', ','); - const prefix = get(multiValuesOptions, 'prefix', ''); - const suffix = get(multiValuesOptions, 'suffix', ''); - const parameterValues = map(value, v => `${prefix}${v}${suffix}`); - return join(parameterValues, separator); - } - return value; - } - - setValue(value) { - if (this.type === 'enum') { - const enumOptionsArray = this.enumOptions && this.enumOptions.split('\n') || []; - if (this.multiValuesOptions) { - if (!isArray(value)) { - value = [value]; - } - value = intersection(value, enumOptionsArray); - } else if (!value || isArray(value) || !includes(enumOptionsArray, value)) { - value = enumOptionsArray[0]; - } - } - - if (isDateRangeParameter(this.type)) { - this.value = null; - this.$$value = null; - - if (isObject(value) && !isArray(value)) { - value = [value.start, value.end]; - } - - if (isArray(value) && (value.length === 2)) { - value = [moment(value[0]), moment(value[1])]; - if (value[0].isValid() && value[1].isValid()) { - this.value = { - start: value[0].format(DATETIME_FORMATS[this.type]), - end: value[1].format(DATETIME_FORMATS[this.type]), - }; - this.$$value = value; - } - } else if (isDynamicDateRange(value)) { - const dynamicDateRange = getDynamicDateRange(value, this.type); - if (dynamicDateRange) { - this.value = value; - this.$$value = value; - } - } - } else if (isDateParameter(this.type)) { - this.value = null; - this.$$value = null; - - if (isDynamicDate(value)) { - const dynamicDate = getDynamicDate(value); - if (dynamicDate) { - this.value = value; - this.$$value = value; - } - } else { - value = moment(value); - if (value.isValid()) { - this.value = value.format(DATETIME_FORMATS[this.type]); - this.$$value = value; - } - } - } else if (this.type === 'number') { - this.value = value; - this.$$value = normalizeNumericValue(value, null); - } else { - this.value = value; - this.$$value = value; - } - - if (isArray(this.locals)) { - each(this.locals, (local) => { - local.setValue(this.value); - }); - } - - this.clearPendingValue(); - - return this; - } - - setPendingValue(value) { - this.pendingValue = value; - } - - applyPendingValue() { - if (this.hasPendingValue) { - this.setValue(this.pendingValue); - } - } - - clearPendingValue() { - this.setPendingValue(undefined); - } - - get hasPendingValue() { - return this.pendingValue !== undefined && this.pendingValue !== this.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); - } - - toUrlParams() { - if (this.isEmpty) { - return {}; - } - const prefix = this.urlPrefix; - if (isDateRangeParameter(this.type) && isObject(this.value)) { - return { - [`${prefix}${this.name}.start`]: this.value.start, - [`${prefix}${this.name}.end`]: this.value.end, - [`${prefix}${this.name}`]: null, - }; - } - if (this.multiValuesOptions && isArray(this.value)) { - return { [`${prefix}${this.name}`]: JSON.stringify(this.value) }; - } - return { - [`${prefix}${this.name}`]: this.value, - [`${prefix}${this.name}.start`]: null, - [`${prefix}${this.name}.end`]: null, - }; - } - - fromUrlParams(query) { - const prefix = this.urlPrefix; - if (isDateRangeParameter(this.type)) { - const key = `${prefix}${this.name}`; - const keyStart = `${prefix}${this.name}.start`; - const keyEnd = `${prefix}${this.name}.end`; - if (has(query, key)) { - this.setValue(query[key]); - } else if (has(query, keyStart) && has(query, keyEnd)) { - this.setValue([query[keyStart], query[keyEnd]]); - } - } else { - const key = `${prefix}${this.name}`; - if (has(query, key)) { - if (this.multiValuesOptions) { - try { - this.setValue(JSON.parse(query[key])); - } catch (e) { - this.setValue(query[key]); - } - } else { - this.setValue(query[key]); - } - } - } - } - - toQueryTextFragment() { - if (isDateRangeParameter(this.type)) { - return `{{ ${this.name}.start }} {{ ${this.name}.end }}`; - } - return `{{ ${this.name} }}`; - } - - loadDropdownValues() { - if (this.parentQueryId) { - return Query.associatedDropdown({ queryId: this.parentQueryId, dropdownQueryId: this.queryId }).$promise; - } - - return Query.asDropdown({ id: this.queryId }).$promise; - } -} - class Parameters { constructor(query, queryString) { this.query = query; @@ -450,7 +72,7 @@ class Parameters { parameterNames.forEach((param) => { if (!has(parametersMap, param)) { - this.query.options.parameters.push(new Parameter({ + this.query.options.parameters.push(Parameter.create({ title: param, name: param, type: 'text', @@ -463,7 +85,7 @@ class Parameters { const parameterExists = p => includes(parameterNames, p.name); const parameters = this.query.options.parameters; this.query.options.parameters = parameters.filter(parameterExists) - .map(p => (p instanceof Parameter ? p : new Parameter(p, this.query.id))); + .map(p => (p instanceof Parameter ? p : Parameter.create(p, this.query.id))); } initFromQueryString(query) { @@ -480,7 +102,7 @@ class Parameters { add(parameterDef) { this.query.options.parameters = this.query.options.parameters .filter(p => p.name !== parameterDef.name); - const param = new Parameter(parameterDef); + const param = Parameter.create(parameterDef); this.query.options.parameters.push(param); return param; } From a1a342155c7a9a1bdfb7cc0406b63913812d917b Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Sun, 11 Aug 2019 14:26:49 -0300 Subject: [PATCH 08/22] Merge dynamicValue into normalizedValue --- .../app/components/ParameterMappingInput.jsx | 2 +- client/app/components/ParameterValueInput.jsx | 4 +- .../dynamic-parameters/DateParameter.jsx | 12 ++--- .../dynamic-parameters/DateRangeParameter.jsx | 44 +++++++++++-------- .../dynamic-parameters/DynamicButton.jsx | 4 +- .../app/services/parameters/DateParameter.js | 38 +++++++++------- .../services/parameters/DateRangeParameter.js | 40 +++++++++-------- client/app/services/parameters/Parameter.js | 4 +- 8 files changed, 83 insertions(+), 65 deletions(-) diff --git a/client/app/components/ParameterMappingInput.jsx b/client/app/components/ParameterMappingInput.jsx index 8d0fd2ed54..acbcd359aa 100644 --- a/client/app/components/ParameterMappingInput.jsx +++ b/client/app/components/ParameterMappingInput.jsx @@ -540,7 +540,7 @@ export class ParameterMappingListInput extends React.Component { // in case of dynamic value display the name instead of value if (param.hasDynamicValue) { - value = param.dynamicValue.name; + value = param.normalizedValue.name; } return this.getStringValue(value); diff --git a/client/app/components/ParameterValueInput.jsx b/client/app/components/ParameterValueInput.jsx index 1cc7ec3ce1..022df6c333 100644 --- a/client/app/components/ParameterValueInput.jsx +++ b/client/app/components/ParameterValueInput.jsx @@ -6,7 +6,7 @@ import Input from 'antd/lib/input'; import InputNumber from 'antd/lib/input-number'; import DateParameter from '@/components/dynamic-parameters/DateParameter'; import DateRangeParameter from '@/components/dynamic-parameters/DateRangeParameter'; -import { toString } from 'lodash'; +import { isEqual } from 'lodash'; import { QueryBasedParameterInput } from './QueryBasedParameterInput'; import './ParameterValueInput.less'; @@ -62,7 +62,7 @@ export class ParameterValueInput extends React.Component { } onSelect = (value) => { - const isDirty = toString(value) !== toString(this.props.value); + const isDirty = !isEqual(value, this.props.value); this.setState({ value, isDirty }); this.props.onSelect(value, isDirty); } diff --git a/client/app/components/dynamic-parameters/DateParameter.jsx b/client/app/components/dynamic-parameters/DateParameter.jsx index fdb71b41f0..9d6db5a585 100644 --- a/client/app/components/dynamic-parameters/DateParameter.jsx +++ b/client/app/components/dynamic-parameters/DateParameter.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import moment from 'moment'; import { includes } from 'lodash'; -import { isDynamicDate, getDynamicDate } from '@/services/parameters/DateParameter'; +import { isDynamicDate, getDynamicDateFromString } from '@/services/parameters/DateParameter'; import DateInput from '@/components/DateInput'; import DateTimeInput from '@/components/DateTimeInput'; import DynamicButton from '@/components/dynamic-parameters/DynamicButton'; @@ -12,11 +12,11 @@ import './DynamicParameters.less'; const DYNAMIC_DATE_OPTIONS = [ { name: 'Today/Now', - value: 'd_now', - label: () => getDynamicDate('d_now').value().format('MMM D') }, + value: getDynamicDateFromString('d_now'), + label: () => getDynamicDateFromString('d_now').value().format('MMM D') }, { name: 'Yesterday', - value: 'd_yesterday', - label: () => getDynamicDate('d_yesterday').value().format('MMM D') }, + value: getDynamicDateFromString('d_yesterday'), + label: () => getDynamicDateFromString('d_yesterday').value().format('MMM D') }, ]; class DateParameter extends React.Component { @@ -77,7 +77,7 @@ class DateParameter extends React.Component { } if (hasDynamicValue) { - const dynamicDate = getDynamicDate(value); + const dynamicDate = value; additionalAttributes.placeholder = dynamicDate && dynamicDate.name; additionalAttributes.value = null; } diff --git a/client/app/components/dynamic-parameters/DateRangeParameter.jsx b/client/app/components/dynamic-parameters/DateRangeParameter.jsx index 7578a3d4d5..41dc433f16 100644 --- a/client/app/components/dynamic-parameters/DateRangeParameter.jsx +++ b/client/app/components/dynamic-parameters/DateRangeParameter.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import moment from 'moment'; import { includes, isArray, isObject } from 'lodash'; -import { isDynamicDateRange, getDynamicDateRange } from '@/services/parameters/DateRangeParameter'; +import { isDynamicDateRange, getDynamicDateRangeFromString } from '@/services/parameters/DateRangeParameter'; import DateRangeInput from '@/components/DateRangeInput'; import DateTimeRangeInput from '@/components/DateTimeRangeInput'; import DynamicButton from '@/components/dynamic-parameters/DynamicButton'; @@ -12,29 +12,37 @@ import './DynamicParameters.less'; const DYNAMIC_DATE_OPTIONS = [ { name: 'This week', - value: 'd_this_week', - label: () => getDynamicDateRange('d_this_week').value()[0].format('MMM D') + ' - ' + - getDynamicDateRange('d_this_week').value()[1].format('MMM D') }, - { name: 'This month', value: 'd_this_month', label: () => getDynamicDateRange('d_this_month').value()[0].format('MMMM') }, - { name: 'This year', value: 'd_this_year', label: () => getDynamicDateRange('d_this_year').value()[0].format('YYYY') }, + value: getDynamicDateRangeFromString('d_this_week'), + label: () => getDynamicDateRangeFromString('d_this_week').value()[0].format('MMM D') + ' - ' + + getDynamicDateRangeFromString('d_this_week').value()[1].format('MMM D') }, + { name: 'This month', + value: getDynamicDateRangeFromString('d_this_month'), + label: () => getDynamicDateRangeFromString('d_this_month').value()[0].format('MMMM') }, + { name: 'This year', + value: getDynamicDateRangeFromString('d_this_year'), + label: () => getDynamicDateRangeFromString('d_this_year').value()[0].format('YYYY') }, { name: 'Last week', - value: 'd_last_week', - label: () => getDynamicDateRange('d_last_week').value()[0].format('MMM D') + ' - ' + - getDynamicDateRange('d_last_week').value()[1].format('MMM D') }, - { name: 'Last month', value: 'd_last_month', label: () => getDynamicDateRange('d_last_month').value()[0].format('MMMM') }, - { name: 'Last year', value: 'd_last_year', label: () => getDynamicDateRange('d_last_year').value()[0].format('YYYY') }, + value: getDynamicDateRangeFromString('d_last_week'), + label: () => getDynamicDateRangeFromString('d_last_week').value()[0].format('MMM D') + ' - ' + + getDynamicDateRangeFromString('d_last_week').value()[1].format('MMM D') }, + { name: 'Last month', + value: getDynamicDateRangeFromString('d_last_month'), + label: () => getDynamicDateRangeFromString('d_last_month').value()[0].format('MMMM') }, + { name: 'Last year', + value: getDynamicDateRangeFromString('d_last_year'), + label: () => getDynamicDateRangeFromString('d_last_year').value()[0].format('YYYY') }, { name: 'Last 7 days', - value: 'd_last_7_days', - label: () => getDynamicDateRange('d_last_7_days').value()[0].format('MMM D') + ' - Today' }, + value: getDynamicDateRangeFromString('d_last_7_days'), + label: () => getDynamicDateRangeFromString('d_last_7_days').value()[0].format('MMM D') + ' - Today' }, ]; const DYNAMIC_DATETIME_OPTIONS = [ { name: 'Today', - value: 'd_today', - label: () => getDynamicDateRange('d_today').value()[0].format('MMM D') }, + value: getDynamicDateRangeFromString('d_today'), + label: () => getDynamicDateRangeFromString('d_today').value()[0].format('MMM D') }, { name: 'Yesterday', - value: 'd_yesterday', - label: () => getDynamicDateRange('d_yesterday').value()[0].format('MMM D') }, + value: getDynamicDateRangeFromString('d_yesterday'), + label: () => getDynamicDateRangeFromString('d_yesterday').value()[0].format('MMM D') }, ...DYNAMIC_DATE_OPTIONS, ]; @@ -107,7 +115,7 @@ class DateRangeParameter extends React.Component { } if (hasDynamicValue) { - const dynamicDateRange = getDynamicDateRange(value); + const dynamicDateRange = value; additionalAttributes.placeholder = [dynamicDateRange && dynamicDateRange.name]; additionalAttributes.value = null; } diff --git a/client/app/components/dynamic-parameters/DynamicButton.jsx b/client/app/components/dynamic-parameters/DynamicButton.jsx index 49dbc0640b..7778b4b166 100644 --- a/client/app/components/dynamic-parameters/DynamicButton.jsx +++ b/client/app/components/dynamic-parameters/DynamicButton.jsx @@ -5,6 +5,8 @@ import Dropdown from 'antd/lib/dropdown'; import Icon from 'antd/lib/icon'; import Menu from 'antd/lib/menu'; import Typography from 'antd/lib/typography'; +import { DynamicDateType } from '@/services/parameters/DateParameter'; +import { DynamicDateRangeType } from '@/services/parameters/DateRangeParameter'; import './DynamicButton.less'; @@ -62,7 +64,7 @@ function DynamicButton({ options, selectedDynamicValue, onSelect, enabled }) { DynamicButton.propTypes = { options: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types - selectedDynamicValue: PropTypes.string, + selectedDynamicValue: PropTypes.oneOfType([DynamicDateType, DynamicDateRangeType]), onSelect: PropTypes.func, enabled: PropTypes.bool, }; diff --git a/client/app/services/parameters/DateParameter.js b/client/app/services/parameters/DateParameter.js index 0fdc3cfe77..7752008915 100644 --- a/client/app/services/parameters/DateParameter.js +++ b/client/app/services/parameters/DateParameter.js @@ -1,5 +1,6 @@ -import { startsWith, isNull } from 'lodash'; +import { findKey, startsWith, has, includes, isNull, values } from 'lodash'; import moment from 'moment'; +import PropTypes from 'prop-types'; import { Parameter } from '.'; const DATETIME_FORMATS = { @@ -22,15 +23,18 @@ const DYNAMIC_DATES = { }, }; +export const DynamicDateType = PropTypes.oneOf(values(DYNAMIC_DATES)); + +function isDynamicDateString(value) { + return startsWith(value, DYNAMIC_PREFIX) && has(DYNAMIC_DATES, value.substring(DYNAMIC_PREFIX.length)); +} + export function isDynamicDate(value) { - if (!startsWith(value, DYNAMIC_PREFIX)) { - return false; - } - return !!DYNAMIC_DATES[value.substring(DYNAMIC_PREFIX.length)]; + return includes(DYNAMIC_DATES, value); } -export function getDynamicDate(value) { - if (!isDynamicDate(value)) { +export function getDynamicDateFromString(value) { + if (!isDynamicDateString(value)) { return null; } return DYNAMIC_DATES[value.substring(DYNAMIC_PREFIX.length)]; @@ -44,15 +48,15 @@ class DateParameter extends Parameter { } get hasDynamicValue() { - return isDynamicDate(this.value); - } - - get dynamicValue() { - return getDynamicDate(this.value); + return isDynamicDate(this.$$value); } // eslint-disable-next-line class-methods-use-this normalizeValue(value) { + if (isDynamicDateString(value)) { + return getDynamicDateFromString(value); + } + if (isDynamicDate(value)) { return value; } @@ -63,7 +67,9 @@ class DateParameter extends Parameter { setValue(value) { const normalizedValue = this.normalizeValue(value); - if (moment.isMoment(normalizedValue)) { + if (isDynamicDate(normalizedValue)) { + this.value = DYNAMIC_PREFIX + findKey(DYNAMIC_DATES, normalizedValue); + } else if (moment.isMoment(normalizedValue)) { this.value = normalizedValue.format(DATETIME_FORMATS[this.type]); } else { this.value = normalizedValue; @@ -75,12 +81,10 @@ class DateParameter extends Parameter { getValue() { if (this.hasDynamicValue) { - if (this.dynamicValue) { - return this.dynamicValue.value().format(DATETIME_FORMATS[this.type]); - } + return this.$$value.value().format(DATETIME_FORMATS[this.type]); } if (isNull(this.value) && this.useCurrentDateTime) { - return moment(); + return moment().format(DATETIME_FORMATS[this.type]); } return this.value; } diff --git a/client/app/services/parameters/DateRangeParameter.js b/client/app/services/parameters/DateRangeParameter.js index c38cb8aaea..a9341b7f1b 100644 --- a/client/app/services/parameters/DateRangeParameter.js +++ b/client/app/services/parameters/DateRangeParameter.js @@ -1,6 +1,6 @@ -import { startsWith, has } from 'lodash'; +import { startsWith, has, includes, findKey, values, isObject, isArray } from 'lodash'; import moment from 'moment'; -import { isObject, isArray } from 'util'; +import PropTypes from 'prop-types'; import { Parameter } from '.'; const DATETIME_FORMATS = { @@ -50,15 +50,21 @@ const DYNAMIC_DATE_RANGES = { }, }; -export function isDynamicDateRange(value) { +export const DynamicDateRangeType = PropTypes.oneOf(values(DYNAMIC_DATE_RANGES)); + +export function isDynamicDateRangeString(value) { if (!startsWith(value, DYNAMIC_PREFIX)) { return false; } return !!DYNAMIC_DATE_RANGES[value.substring(DYNAMIC_PREFIX.length)]; } -export function getDynamicDateRange(value) { - if (!isDynamicDateRange(value)) { +export function isDynamicDateRange(value) { + return includes(DYNAMIC_DATE_RANGES, value); +} + +export function getDynamicDateRangeFromString(value) { + if (!isDynamicDateRangeString(value)) { return null; } return DYNAMIC_DATE_RANGES[value.substring(DYNAMIC_PREFIX.length)]; @@ -71,15 +77,15 @@ class DateRangeParameter extends Parameter { } get hasDynamicValue() { - return isDynamicDateRange(this.value); - } - - get dynamicValue() { - return getDynamicDateRange(this.value); + return isDynamicDateRange(this.$$value); } // eslint-disable-next-line class-methods-use-this normalizeValue(value) { + if (isDynamicDateRangeString(value)) { + return getDynamicDateRangeFromString(value); + } + if (isDynamicDateRange(value)) { return value; } @@ -99,7 +105,9 @@ class DateRangeParameter extends Parameter { setValue(value) { const normalizedValue = this.normalizeValue(value); - if (isArray(normalizedValue)) { + if (isDynamicDateRange(normalizedValue)) { + this.value = DYNAMIC_PREFIX + findKey(DYNAMIC_DATE_RANGES, normalizedValue); + } else if (isArray(normalizedValue)) { this.value = { start: normalizedValue[0].format(DATETIME_FORMATS[this.type]), end: normalizedValue[1].format(DATETIME_FORMATS[this.type]), @@ -114,13 +122,9 @@ class DateRangeParameter extends Parameter { getValue() { if (this.hasDynamicValue) { - if (this.dynamicValue) { - const dateRange = this.dynamicValue.value(); - return { - start: dateRange[0].format(DATETIME_FORMATS[this.type]), - end: dateRange[1].format(DATETIME_FORMATS[this.type]), - }; - } + const format = date => date.format(DATETIME_FORMATS[this.type]); + const [start, end] = this.$$value.value().map(format); + return { start, end }; } return this.value; } diff --git a/client/app/services/parameters/Parameter.js b/client/app/services/parameters/Parameter.js index 17578e5f89..b6d1193300 100644 --- a/client/app/services/parameters/Parameter.js +++ b/client/app/services/parameters/Parameter.js @@ -37,7 +37,7 @@ class Parameter { case 'datetime-with-seconds': return new DateParameter(param, parentQueryId); case 'date-range': - case 'datetime-range-local': + case 'datetime-range': case 'datetime-range-with-seconds': return new DateRangeParameter(param, parentQueryId); default: @@ -83,7 +83,7 @@ class Parameter { } clone() { - return Parameter.create(this); + return Parameter.create(this, this.parentQueryId); } // eslint-disable-next-line class-methods-use-this From 622240dfdeb102f238e48e01a94cea56cf4f4186 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Wed, 4 Sep 2019 21:25:42 -0300 Subject: [PATCH 09/22] Add updateLocals and omit unwanted props --- client/app/pages/queries/view.js | 4 ++-- .../app/services/parameters/DateParameter.js | 2 ++ .../services/parameters/DateRangeParameter.js | 2 ++ client/app/services/parameters/Parameter.js | 22 +++++++++++++------ 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/client/app/pages/queries/view.js b/client/app/pages/queries/view.js index 592066f06a..f5b8099408 100644 --- a/client/app/pages/queries/view.js +++ b/client/app/pages/queries/view.js @@ -1,4 +1,4 @@ -import { pick, some, find, minBy, map, intersection, isArray, omit } from 'lodash'; +import { pick, some, find, minBy, map, intersection, isArray } from 'lodash'; import { SCHEMA_NOT_SUPPORTED, SCHEMA_LOAD_ERROR } from '@/services/data-source'; import getTags from '@/services/getTags'; import { policy } from '@/services/policy'; @@ -261,7 +261,7 @@ function QueryViewCtrl( if (request.options && request.options.parameters) { request.options = { ...request.options, - parameters: map(request.options.parameters, p => omit(p, 'pendingValue')), + parameters: map(request.options.parameters, p => p.toSaveableObject()), }; } diff --git a/client/app/services/parameters/DateParameter.js b/client/app/services/parameters/DateParameter.js index 7752008915..749e6b80d5 100644 --- a/client/app/services/parameters/DateParameter.js +++ b/client/app/services/parameters/DateParameter.js @@ -75,6 +75,8 @@ class DateParameter extends Parameter { this.value = normalizedValue; } this.$$value = normalizedValue; + + this.updateLocals(); this.clearPendingValue(); return this; } diff --git a/client/app/services/parameters/DateRangeParameter.js b/client/app/services/parameters/DateRangeParameter.js index a9341b7f1b..ca174233e0 100644 --- a/client/app/services/parameters/DateRangeParameter.js +++ b/client/app/services/parameters/DateRangeParameter.js @@ -116,6 +116,8 @@ class DateRangeParameter extends Parameter { this.value = normalizedValue; } this.$$value = normalizedValue; + + this.updateLocals(); this.clearPendingValue(); return this; } diff --git a/client/app/services/parameters/Parameter.js b/client/app/services/parameters/Parameter.js index b6d1193300..c21e9aaa26 100644 --- a/client/app/services/parameters/Parameter.js +++ b/client/app/services/parameters/Parameter.js @@ -1,4 +1,4 @@ -import { isNull, isObject, isFunction, isUndefined, isEqual, has } from 'lodash'; +import { isNull, isObject, isFunction, isUndefined, isEqual, has, omit, isArray, each } from 'lodash'; import { TextParameter, NumberParameter, EnumParameter, QueryBasedDropdownParameter, DateParameter, DateRangeParameter, @@ -16,12 +16,7 @@ class Parameter { this.locals = []; // Used for URL serialization - Object.defineProperty(this, 'urlPrefix', { - configurable: true, - enumerable: false, // don't save it - writable: true, - value: 'p_', - }); + this.urlPrefix = 'p_'; } static create(param, parentQueryId) { @@ -94,11 +89,20 @@ class Parameter { return value; } + updateLocals() { + if (isArray(this.locals)) { + each(this.locals, (local) => { + local.setValue(this.value); + }); + } + } + setValue(value) { const normalizedValue = this.normalizeValue(value); this.value = normalizedValue; this.$$value = normalizedValue; + this.updateLocals(); this.clearPendingValue(); return this; } @@ -141,6 +145,10 @@ class Parameter { toQueryTextFragment() { return `{{ ${this.name} }}`; } + + toSaveableObject() { + return omit(this, ['urlPrefix', 'pendingValue']); + } } export default Parameter; From 5dcacbe637b5b4224f9b168c0e95ace2dfdd49d1 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Sat, 7 Sep 2019 11:00:32 -0300 Subject: [PATCH 10/22] Allow null NumberParameter and omit parentQueryId --- client/app/services/parameters/NumberParameter.js | 7 +++++-- client/app/services/parameters/Parameter.js | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/client/app/services/parameters/NumberParameter.js b/client/app/services/parameters/NumberParameter.js index 2029235cf8..997cdb4b29 100644 --- a/client/app/services/parameters/NumberParameter.js +++ b/client/app/services/parameters/NumberParameter.js @@ -1,4 +1,4 @@ -import { toNumber } from 'lodash'; +import { toNumber, isNull } from 'lodash'; import { Parameter } from '.'; class NumberParameter extends Parameter { @@ -9,8 +9,11 @@ class NumberParameter extends Parameter { // eslint-disable-next-line class-methods-use-this normalizeValue(value) { + if (isNull(value)) { + return null; + } const normalizedValue = toNumber(value); - return !isNaN(normalizedValue) ? normalizedValue : 0; + return !isNaN(normalizedValue) ? normalizedValue : null; } } diff --git a/client/app/services/parameters/Parameter.js b/client/app/services/parameters/Parameter.js index c21e9aaa26..48fc386e3d 100644 --- a/client/app/services/parameters/Parameter.js +++ b/client/app/services/parameters/Parameter.js @@ -147,7 +147,7 @@ class Parameter { } toSaveableObject() { - return omit(this, ['urlPrefix', 'pendingValue']); + return omit(this, ['urlPrefix', 'pendingValue', 'parentQueryId']); } } From 3279e881c54426af527f48a7720bce9b19abb1e1 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Wed, 11 Sep 2019 19:40:21 -0300 Subject: [PATCH 11/22] Rename parameter getValue to getExecutionValue --- client/app/components/ParameterMappingInput.jsx | 2 +- .../components/dynamic-parameters/DateParameter.jsx | 2 +- .../dynamic-parameters/DateRangeParameter.jsx | 2 +- client/app/services/parameters/DateParameter.js | 2 +- client/app/services/parameters/DateRangeParameter.js | 2 +- client/app/services/parameters/EnumParameter.js | 2 +- client/app/services/parameters/Parameter.js | 10 +++++----- .../services/parameters/QueryBasedDropdownParameter.js | 2 +- client/app/services/query.js | 8 ++++---- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/client/app/components/ParameterMappingInput.jsx b/client/app/components/ParameterMappingInput.jsx index 9fa635c18d..e968f86eb1 100644 --- a/client/app/components/ParameterMappingInput.jsx +++ b/client/app/components/ParameterMappingInput.jsx @@ -536,7 +536,7 @@ export class ParameterMappingListInput extends React.Component { param = param.clone().setValue(mapping.value); } - let value = Parameter.getValue(param); + let value = Parameter.getExecutionValue(param); // in case of dynamic value display the name instead of value if (param.hasDynamicValue) { diff --git a/client/app/components/dynamic-parameters/DateParameter.jsx b/client/app/components/dynamic-parameters/DateParameter.jsx index 9d6db5a585..64f52c6deb 100644 --- a/client/app/components/dynamic-parameters/DateParameter.jsx +++ b/client/app/components/dynamic-parameters/DateParameter.jsx @@ -44,7 +44,7 @@ class DateParameter extends React.Component { onDynamicValueSelect = (dynamicValue) => { const { onSelect, parameter } = this.props; if (dynamicValue === 'static') { - const parameterValue = parameter.getValue(); + const parameterValue = parameter.getExecutionValue(); if (parameterValue) { onSelect(moment(parameterValue)); } else { diff --git a/client/app/components/dynamic-parameters/DateRangeParameter.jsx b/client/app/components/dynamic-parameters/DateRangeParameter.jsx index 41dc433f16..5fb78b1c4e 100644 --- a/client/app/components/dynamic-parameters/DateRangeParameter.jsx +++ b/client/app/components/dynamic-parameters/DateRangeParameter.jsx @@ -81,7 +81,7 @@ class DateRangeParameter extends React.Component { onDynamicValueSelect = (dynamicValue) => { const { onSelect, parameter } = this.props; if (dynamicValue === 'static') { - const parameterValue = parameter.getValue(); + const parameterValue = parameter.getExecutionValue(); if (isObject(parameterValue) && parameterValue.start && parameterValue.end) { onSelect([moment(parameterValue.start), moment(parameterValue.end)]); } else { diff --git a/client/app/services/parameters/DateParameter.js b/client/app/services/parameters/DateParameter.js index 749e6b80d5..d03e4e82ca 100644 --- a/client/app/services/parameters/DateParameter.js +++ b/client/app/services/parameters/DateParameter.js @@ -81,7 +81,7 @@ class DateParameter extends Parameter { return this; } - getValue() { + getExecutionValue() { if (this.hasDynamicValue) { return this.$$value.value().format(DATETIME_FORMATS[this.type]); } diff --git a/client/app/services/parameters/DateRangeParameter.js b/client/app/services/parameters/DateRangeParameter.js index ca174233e0..c5dbae3d6c 100644 --- a/client/app/services/parameters/DateRangeParameter.js +++ b/client/app/services/parameters/DateRangeParameter.js @@ -122,7 +122,7 @@ class DateRangeParameter extends Parameter { return this; } - getValue() { + getExecutionValue() { if (this.hasDynamicValue) { const format = date => date.format(DATETIME_FORMATS[this.type]); const [start, end] = this.$$value.value().map(format); diff --git a/client/app/services/parameters/EnumParameter.js b/client/app/services/parameters/EnumParameter.js index ba42a5833c..952bc3a126 100644 --- a/client/app/services/parameters/EnumParameter.js +++ b/client/app/services/parameters/EnumParameter.js @@ -26,7 +26,7 @@ class EnumParameter extends Parameter { return value; } - getValue(extra = {}) { + getExecutionValue(extra = {}) { const { joinListValues } = extra; if (joinListValues && isArray(this.value)) { const separator = get(this.multiValuesOptions, 'separator', ','); diff --git a/client/app/services/parameters/Parameter.js b/client/app/services/parameters/Parameter.js index 48fc386e3d..4c2e0e1d57 100644 --- a/client/app/services/parameters/Parameter.js +++ b/client/app/services/parameters/Parameter.js @@ -40,12 +40,12 @@ class Parameter { } } - static getValue(param, extra = {}) { - if (!isObject(param) || !isFunction(param.getValue)) { + static getExecutionValue(param, extra = {}) { + if (!isObject(param) || !isFunction(param.getExecutionValue)) { return null; } - return param.getValue(extra); + return param.getExecutionValue(extra); } static setValue(param, value) { @@ -57,7 +57,7 @@ class Parameter { } get isEmpty() { - return isNull(this.getValue()); + return isNull(this.getExecutionValue()); } get hasPendingValue() { @@ -107,7 +107,7 @@ class Parameter { return this; } - getValue() { + getExecutionValue() { return this.value; } diff --git a/client/app/services/parameters/QueryBasedDropdownParameter.js b/client/app/services/parameters/QueryBasedDropdownParameter.js index bc10630a74..86200513ca 100644 --- a/client/app/services/parameters/QueryBasedDropdownParameter.js +++ b/client/app/services/parameters/QueryBasedDropdownParameter.js @@ -10,7 +10,7 @@ class QueryBasedDropdownParameter extends Parameter { this.setValue(parameter.value); } - getValue(extra = {}) { + getExecutionValue(extra = {}) { const { joinListValues } = extra; if (joinListValues && isArray(this.value)) { const separator = get(this.multiValuesOptions, 'separator', ','); diff --git a/client/app/services/query.js b/client/app/services/query.js index 21ee659497..e930f58c76 100644 --- a/client/app/services/query.js +++ b/client/app/services/query.js @@ -115,9 +115,9 @@ class Parameters { return !isEmpty(this.get()); } - getValues(extra = {}) { + getExecutionValues(extra = {}) { const params = this.get(); - return zipObject(map(params, i => i.name), map(params, i => i.getValue(extra))); + return zipObject(map(params, i => i.name), map(params, i => i.getExecutionValue(extra))); } hasPendingValues() { @@ -355,7 +355,7 @@ function QueryResource( }; QueryService.prototype.getQueryResult = function getQueryResult(maxAge) { - const execute = () => QueryResult.getByQueryId(this.id, this.getParameters().getValues(), maxAge); + const execute = () => QueryResult.getByQueryId(this.id, this.getParameters().getExecutionValues(), maxAge); return this.prepareQueryResultExecution(execute, maxAge); }; @@ -365,7 +365,7 @@ function QueryResource( return new QueryResultError("Can't execute empty query."); } - const parameters = this.getParameters().getValues({ joinListValues: true }); + const parameters = this.getParameters().getExecutionValues({ joinListValues: true }); const execute = () => QueryResult.get(this.data_source_id, queryText, parameters, maxAge, this.id); return this.prepareQueryResultExecution(execute, maxAge); }; From c4c0418aeb28949e32e25756dd6e3161633c1761 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Wed, 11 Sep 2019 19:53:12 -0300 Subject: [PATCH 12/22] Update $$value to normalizedValue + omit on save --- client/app/services/parameters/DateParameter.js | 4 ++-- client/app/services/parameters/DateRangeParameter.js | 4 ++-- client/app/services/parameters/Parameter.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/app/services/parameters/DateParameter.js b/client/app/services/parameters/DateParameter.js index d03e4e82ca..0681d393b4 100644 --- a/client/app/services/parameters/DateParameter.js +++ b/client/app/services/parameters/DateParameter.js @@ -48,7 +48,7 @@ class DateParameter extends Parameter { } get hasDynamicValue() { - return isDynamicDate(this.$$value); + return isDynamicDate(this.normalizedValue); } // eslint-disable-next-line class-methods-use-this @@ -83,7 +83,7 @@ class DateParameter extends Parameter { getExecutionValue() { if (this.hasDynamicValue) { - return this.$$value.value().format(DATETIME_FORMATS[this.type]); + return this.normalizedValue.value().format(DATETIME_FORMATS[this.type]); } if (isNull(this.value) && this.useCurrentDateTime) { return moment().format(DATETIME_FORMATS[this.type]); diff --git a/client/app/services/parameters/DateRangeParameter.js b/client/app/services/parameters/DateRangeParameter.js index c5dbae3d6c..f24e4ee458 100644 --- a/client/app/services/parameters/DateRangeParameter.js +++ b/client/app/services/parameters/DateRangeParameter.js @@ -77,7 +77,7 @@ class DateRangeParameter extends Parameter { } get hasDynamicValue() { - return isDynamicDateRange(this.$$value); + return isDynamicDateRange(this.normalizedValue); } // eslint-disable-next-line class-methods-use-this @@ -125,7 +125,7 @@ class DateRangeParameter extends Parameter { getExecutionValue() { if (this.hasDynamicValue) { const format = date => date.format(DATETIME_FORMATS[this.type]); - const [start, end] = this.$$value.value().map(format); + const [start, end] = this.normalizedValue.value().map(format); return { start, end }; } return this.value; diff --git a/client/app/services/parameters/Parameter.js b/client/app/services/parameters/Parameter.js index 4c2e0e1d57..7879acee39 100644 --- a/client/app/services/parameters/Parameter.js +++ b/client/app/services/parameters/Parameter.js @@ -147,7 +147,7 @@ class Parameter { } toSaveableObject() { - return omit(this, ['urlPrefix', 'pendingValue', 'parentQueryId']); + return omit(this, ['$$value', 'urlPrefix', 'pendingValue', 'parentQueryId']); } } From a6df8b3c7ecabc7d2a25bad8bf40e1aeb4dbb806 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Mon, 16 Sep 2019 20:03:12 -0300 Subject: [PATCH 13/22] Add a few comments --- client/app/services/parameters/Parameter.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/app/services/parameters/Parameter.js b/client/app/services/parameters/Parameter.js index 7879acee39..18d54d1b89 100644 --- a/client/app/services/parameters/Parameter.js +++ b/client/app/services/parameters/Parameter.js @@ -64,6 +64,7 @@ class Parameter { return this.pendingValue !== undefined && !isEqual(this.pendingValue, this.normalizedValue); } + /** Get normalized value to be used in inputs */ get normalizedValue() { return this.$$value; } @@ -107,6 +108,7 @@ class Parameter { return this; } + /** Get execution value for a query */ getExecutionValue() { return this.value; } @@ -125,8 +127,10 @@ class Parameter { this.pendingValue = undefined; } + /** Update URL with Parameter value */ toUrlParams() { const prefix = this.urlPrefix; + // `null` removes the parameter from the URL in case it exists return { [`${prefix}${this.name}`]: !this.isEmpty ? this.value : null, [`${prefix}${this.name}.start`]: null, @@ -134,6 +138,7 @@ class Parameter { }; } + /** Set parameter value from the URL */ fromUrlParams(query) { const prefix = this.urlPrefix; const key = `${prefix}${this.name}`; @@ -146,6 +151,7 @@ class Parameter { return `{{ ${this.name} }}`; } + /** Get a saveable version of the Parameter by omitting unnecessary props */ toSaveableObject() { return omit(this, ['$$value', 'urlPrefix', 'pendingValue', 'parentQueryId']); } From 8e0a6843b89fc652f5147c6b40c9520d3d7c2681 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Tue, 17 Sep 2019 09:27:23 -0300 Subject: [PATCH 14/22] Remove ngModel property from Parameter --- client/app/services/parameters/Parameter.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/client/app/services/parameters/Parameter.js b/client/app/services/parameters/Parameter.js index 18d54d1b89..d08ececa9c 100644 --- a/client/app/services/parameters/Parameter.js +++ b/client/app/services/parameters/Parameter.js @@ -69,15 +69,6 @@ class Parameter { return this.$$value; } - // TODO: Remove this property when finally moved to React - get ngModel() { - return this.normalizedValue; - } - - set ngModel(value) { - this.setValue(value); - } - clone() { return Parameter.create(this, this.parentQueryId); } From 6b09a84e1aca63b74bb187bf4c112ce9741f5bba Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Wed, 18 Sep 2019 19:41:41 -0300 Subject: [PATCH 15/22] Use value directly in DateRangeParameter --- .../app/components/dynamic-parameters/DateRangeParameter.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/app/components/dynamic-parameters/DateRangeParameter.jsx b/client/app/components/dynamic-parameters/DateRangeParameter.jsx index 5fb78b1c4e..8c6a86d865 100644 --- a/client/app/components/dynamic-parameters/DateRangeParameter.jsx +++ b/client/app/components/dynamic-parameters/DateRangeParameter.jsx @@ -115,8 +115,7 @@ class DateRangeParameter extends React.Component { } if (hasDynamicValue) { - const dynamicDateRange = value; - additionalAttributes.placeholder = [dynamicDateRange && dynamicDateRange.name]; + additionalAttributes.placeholder = [value && value.name]; additionalAttributes.value = null; } From 037484134b078eb3bc551ead8166ce49820792fa Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Wed, 18 Sep 2019 19:41:52 -0300 Subject: [PATCH 16/22] Use simpler separator for DateRange url param --- .../app/services/parameters/DateRangeParameter.js | 15 +++++++-------- client/app/services/parameters/EnumParameter.js | 2 -- client/app/services/parameters/Parameter.js | 2 -- .../parameters/QueryBasedDropdownParameter.js | 2 -- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/client/app/services/parameters/DateRangeParameter.js b/client/app/services/parameters/DateRangeParameter.js index f24e4ee458..9a052bd1fb 100644 --- a/client/app/services/parameters/DateRangeParameter.js +++ b/client/app/services/parameters/DateRangeParameter.js @@ -135,9 +135,7 @@ class DateRangeParameter extends Parameter { const prefix = this.urlPrefix; if (isObject(this.value) && this.value.start && this.value.end) { return { - [`${prefix}${this.name}`]: null, - [`${prefix}${this.name}.start`]: this.value.start, - [`${prefix}${this.name}.end`]: this.value.end, + [`${prefix}${this.name}`]: `${this.value.start}--${this.value.end}`, }; } return super.toUrlParams(); @@ -146,12 +144,13 @@ class DateRangeParameter extends Parameter { fromUrlParams(query) { const prefix = this.urlPrefix; const key = `${prefix}${this.name}`; - const keyStart = `${prefix}${this.name}.start`; - const keyEnd = `${prefix}${this.name}.end`; if (has(query, key)) { - this.setValue(query[key]); - } else if (has(query, keyStart) && has(query, keyEnd)) { - this.setValue([query[keyStart], query[keyEnd]]); + const dates = query[key].split('--'); + if (dates.length === 2) { + this.setValue(dates); + } else { + this.setValue(query[key]); + } } } diff --git a/client/app/services/parameters/EnumParameter.js b/client/app/services/parameters/EnumParameter.js index 952bc3a126..6999df730e 100644 --- a/client/app/services/parameters/EnumParameter.js +++ b/client/app/services/parameters/EnumParameter.js @@ -48,8 +48,6 @@ class EnumParameter extends Parameter { return { [`${prefix}${this.name}`]: !this.isEmpty ? urlParam : null, - [`${prefix}${this.name}.start`]: null, - [`${prefix}${this.name}.end`]: null, }; } diff --git a/client/app/services/parameters/Parameter.js b/client/app/services/parameters/Parameter.js index d08ececa9c..05ee72cd46 100644 --- a/client/app/services/parameters/Parameter.js +++ b/client/app/services/parameters/Parameter.js @@ -124,8 +124,6 @@ class Parameter { // `null` removes the parameter from the URL in case it exists return { [`${prefix}${this.name}`]: !this.isEmpty ? this.value : null, - [`${prefix}${this.name}.start`]: null, - [`${prefix}${this.name}.end`]: null, }; } diff --git a/client/app/services/parameters/QueryBasedDropdownParameter.js b/client/app/services/parameters/QueryBasedDropdownParameter.js index 86200513ca..7bad235184 100644 --- a/client/app/services/parameters/QueryBasedDropdownParameter.js +++ b/client/app/services/parameters/QueryBasedDropdownParameter.js @@ -32,8 +32,6 @@ class QueryBasedDropdownParameter extends Parameter { return { [`${prefix}${this.name}`]: !this.isEmpty ? urlParam : null, - [`${prefix}${this.name}.start`]: null, - [`${prefix}${this.name}.end`]: null, }; } From 0e5782ee22a82e9822642060151c70bfcf19a0f3 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Thu, 19 Sep 2019 07:55:15 -0300 Subject: [PATCH 17/22] Add backward compatibility --- client/app/services/parameters/DateRangeParameter.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/app/services/parameters/DateRangeParameter.js b/client/app/services/parameters/DateRangeParameter.js index 9a052bd1fb..dce8900283 100644 --- a/client/app/services/parameters/DateRangeParameter.js +++ b/client/app/services/parameters/DateRangeParameter.js @@ -144,6 +144,11 @@ class DateRangeParameter extends Parameter { fromUrlParams(query) { const prefix = this.urlPrefix; const key = `${prefix}${this.name}`; + + // backward compatibility + const keyStart = `${prefix}${this.name}.start`; + const keyEnd = `${prefix}${this.name}.end`; + if (has(query, key)) { const dates = query[key].split('--'); if (dates.length === 2) { @@ -151,6 +156,8 @@ class DateRangeParameter extends Parameter { } else { this.setValue(query[key]); } + } else if (has(query, keyStart) && has(query, keyEnd)) { + this.setValue([query[keyStart], query[keyEnd]]); } } From da7255ec74812f87e2d4b3a51e241d6b39c41a7b Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Sun, 29 Sep 2019 16:17:53 -0300 Subject: [PATCH 18/22] Use normalizeValue null value for isEmpty --- client/app/services/parameters/EnumParameter.js | 4 ++++ client/app/services/parameters/Parameter.js | 6 +++++- .../parameters/QueryBasedDropdownParameter.js | 15 ++++++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/client/app/services/parameters/EnumParameter.js b/client/app/services/parameters/EnumParameter.js index 6999df730e..6eaf8c3335 100644 --- a/client/app/services/parameters/EnumParameter.js +++ b/client/app/services/parameters/EnumParameter.js @@ -23,6 +23,10 @@ class EnumParameter extends Parameter { } else if (!value || isArray(value) || !includes(enumOptionsArray, value)) { value = enumOptionsArray[0]; } + + if (isArray(value) && isEmpty(value)) { + return null; + } return value; } diff --git a/client/app/services/parameters/Parameter.js b/client/app/services/parameters/Parameter.js index 05ee72cd46..47516f156b 100644 --- a/client/app/services/parameters/Parameter.js +++ b/client/app/services/parameters/Parameter.js @@ -57,7 +57,7 @@ class Parameter { } get isEmpty() { - return isNull(this.getExecutionValue()); + return this.isEmptyValue(this.value); } get hasPendingValue() { @@ -73,6 +73,10 @@ class Parameter { return Parameter.create(this, this.parentQueryId); } + isEmptyValue(value) { + return isNull(this.normalizeValue(value)); + } + // eslint-disable-next-line class-methods-use-this normalizeValue(value) { if (isUndefined(value)) { diff --git a/client/app/services/parameters/QueryBasedDropdownParameter.js b/client/app/services/parameters/QueryBasedDropdownParameter.js index 7bad235184..cdc8d6edcd 100644 --- a/client/app/services/parameters/QueryBasedDropdownParameter.js +++ b/client/app/services/parameters/QueryBasedDropdownParameter.js @@ -1,4 +1,4 @@ -import { isArray, get, map, join, has } from 'lodash'; +import { isNull, isUndefined, isArray, isEmpty, get, map, join, has } from 'lodash'; import { Query } from '@/services/query'; import { Parameter } from '.'; @@ -10,6 +10,19 @@ class QueryBasedDropdownParameter extends Parameter { this.setValue(parameter.value); } + normalizeValue(value) { + if (isUndefined(value) || isNull(value) || (isArray(value) && isEmpty(value))) { + return null; + } + + if (this.multiValuesOptions) { + value = isArray(value) ? value : [value]; + } else { + value = isArray(value) ? value[0] : value; + } + return value; + } + getExecutionValue(extra = {}) { const { joinListValues } = extra; if (joinListValues && isArray(this.value)) { From 927783d84e4a7d0bae953160ce2d7349ba98ef2d Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Sat, 5 Oct 2019 12:28:13 -0300 Subject: [PATCH 19/22] Start creating jest tests --- .../parameters/tests/Parameter.test.js | 25 ++++++++++++++++ .../parameters/tests/TextParameter.test.js | 30 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 client/app/services/parameters/tests/Parameter.test.js create mode 100644 client/app/services/parameters/tests/TextParameter.test.js diff --git a/client/app/services/parameters/tests/Parameter.test.js b/client/app/services/parameters/tests/Parameter.test.js new file mode 100644 index 0000000000..5280f1169d --- /dev/null +++ b/client/app/services/parameters/tests/Parameter.test.js @@ -0,0 +1,25 @@ +import { Parameter, TextParameter, NumberParameter, EnumParameter, + QueryBasedDropdownParameter, DateParameter, DateRangeParameter } from '..'; + +describe('Parameter', () => { + describe('create', () => { + const parameterTypes = [ + ['text', TextParameter], + ['number', NumberParameter], + ['enum', EnumParameter], + ['query', QueryBasedDropdownParameter], + ['date', DateParameter], + ['datetime-local', DateParameter], + ['datetime-with-seconds', DateParameter], + ['date-range', DateRangeParameter], + ['datetime-range', DateRangeParameter], + ['datetime-range-with-seconds', DateRangeParameter], + [null, TextParameter], + ]; + + test.each(parameterTypes)('when type is \'%s\' creates a %p', (type, expectedClass) => { + const parameter = Parameter.create({ name: 'param', title: 'Param', type }); + expect(parameter).toBeInstanceOf(expectedClass); + }); + }); +}); diff --git a/client/app/services/parameters/tests/TextParameter.test.js b/client/app/services/parameters/tests/TextParameter.test.js new file mode 100644 index 0000000000..f6726a8b76 --- /dev/null +++ b/client/app/services/parameters/tests/TextParameter.test.js @@ -0,0 +1,30 @@ +import { Parameter } from '..'; + +describe('TextParameter', () => { + let param; + beforeEach(() => { + param = Parameter.create({ name: 'param', title: 'Param', type: 'text' }); + }); + + describe('normalizeValue', () => { + test('converts Strings', () => { + const normalizedValue = param.normalizeValue('exampleString'); // TODO: use faker?? + expect(normalizedValue).toBe('exampleString'); + }); + + test('converts Numbers', () => { + const normalizedValue = param.normalizeValue(3); // TODO: use faker?? + expect(normalizedValue).toBe('3'); + }); + + describe('Empty values', () => { + const emptyValues = [null, undefined, '']; + + test.each(emptyValues)('normalizes empty value \'%s\' as null', (emptyValue) => { + const normalizedValue = param.normalizeValue(emptyValue); + expect(normalizedValue).toBeNull(); + expect(param.isEmpty).toBeTruthy(); + }); + }); + }); +}); From 3a3c08f6e954f4d4f17f83606342e1adee40f76a Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Sun, 6 Oct 2019 15:33:03 -0300 Subject: [PATCH 20/22] Add more tests --- .../parameters/tests/DateParameter.test.js | 79 +++++++++++++++++++ .../tests/DateRangeParameter.test.js | 76 ++++++++++++++++++ .../parameters/tests/EnumParameter.test.js | 56 +++++++++++++ .../parameters/tests/NumberParameter.test.js | 26 ++++++ .../tests/QueryBasedDropdownParameter.test.js | 54 +++++++++++++ .../parameters/tests/TextParameter.test.js | 6 +- 6 files changed, 294 insertions(+), 3 deletions(-) create mode 100644 client/app/services/parameters/tests/DateParameter.test.js create mode 100644 client/app/services/parameters/tests/DateRangeParameter.test.js create mode 100644 client/app/services/parameters/tests/EnumParameter.test.js create mode 100644 client/app/services/parameters/tests/NumberParameter.test.js create mode 100644 client/app/services/parameters/tests/QueryBasedDropdownParameter.test.js diff --git a/client/app/services/parameters/tests/DateParameter.test.js b/client/app/services/parameters/tests/DateParameter.test.js new file mode 100644 index 0000000000..c0fdaa1800 --- /dev/null +++ b/client/app/services/parameters/tests/DateParameter.test.js @@ -0,0 +1,79 @@ +import { Parameter } from '..'; +import { getDynamicDateFromString } from '../DateParameter'; +import moment from 'moment'; + +describe('DateParameter', () => { + let type = 'date'; + let param; + + beforeEach(() => { + param = Parameter.create({ name: 'param', title: 'Param', type }); + }); + + describe('getExecutionValue', () => { + beforeEach(() => { + param.setValue(moment('2019-10-06 10:00:00')); + }); + + test('formats value as a string date', () => { + const executionValue = param.getExecutionValue(); + expect(executionValue).toBe('2019-10-06'); + }); + + describe('type is datetime-local', () => { + beforeAll(() => { + type = 'datetime-local'; + }); + + test('formats value as a string datetime', () => { + const executionValue = param.getExecutionValue(); + expect(executionValue).toBe('2019-10-06 10:00'); + }); + }); + + describe('type is datetime-with-seconds', () => { + beforeAll(() => { + type = 'datetime-with-seconds'; + }); + + test('formats value as a string datetime with seconds', () => { + const executionValue = param.getExecutionValue(); + expect(executionValue).toBe('2019-10-06 10:00:00'); + }); + }); + }); + + describe('normalizeValue', () => { + test('recognizes dates from strings', () => { + const normalizedValue = param.normalizeValue('2019-10-06'); + expect(moment.isMoment(normalizedValue)).toBeTruthy(); + expect(normalizedValue.format('YYYY-MM-DD')).toBe('2019-10-06'); + }); + + test('recognizes dates from moment values', () => { + const normalizedValue = param.normalizeValue(moment('2019-10-06')); + expect(moment.isMoment(normalizedValue)).toBeTruthy(); + expect(normalizedValue.format('YYYY-MM-DD')).toBe('2019-10-06'); + }); + + test('normalizes unrecognized values as null', () => { + const normalizedValue = param.normalizeValue('value'); + expect(normalizedValue).toBeNull(); + }); + + describe('Dynamic values', () => { + test('recognizes dynamic values from string index', () => { + const normalizedValue = param.normalizeValue('d_now'); + expect(normalizedValue).not.toBeNull(); + expect(normalizedValue).toEqual(getDynamicDateFromString('d_now')); + }); + + test('recognizes dynamic values from a dynamic date', () => { + const dynamicDate = getDynamicDateFromString('d_now'); + const normalizedValue = param.normalizeValue(dynamicDate); + expect(normalizedValue).not.toBeNull(); + expect(normalizedValue).toEqual(dynamicDate); + }); + }); + }); +}); diff --git a/client/app/services/parameters/tests/DateRangeParameter.test.js b/client/app/services/parameters/tests/DateRangeParameter.test.js new file mode 100644 index 0000000000..3d04636dbb --- /dev/null +++ b/client/app/services/parameters/tests/DateRangeParameter.test.js @@ -0,0 +1,76 @@ +import { Parameter } from '..'; +import { getDynamicDateRangeFromString } from '../DateRangeParameter'; +import moment from 'moment'; + +describe('DateRangeParameter', () => { + let type = 'date-range'; + let param; + + beforeEach(() => { + param = Parameter.create({ name: 'param', title: 'Param', type }); + }); + + describe('getExecutionValue', () => { + beforeEach(() => { + param.setValue({ start: '2019-10-05 10:00:00', end: '2019-10-06 09:59:59' }); + }); + + test('formats value as a string date', () => { + const executionValue = param.getExecutionValue(); + expect(executionValue).toEqual({ start: '2019-10-05', end: '2019-10-06' }); + }); + + describe('type is datetime-range', () => { + beforeAll(() => { + type = 'datetime-range'; + }); + + test('formats value as a string datetime', () => { + const executionValue = param.getExecutionValue(); + expect(executionValue).toEqual({ start: '2019-10-05 10:00', end: '2019-10-06 09:59' }); + }); + }); + + describe('type is datetime-range-with-seconds', () => { + beforeAll(() => { + type = 'datetime-range-with-seconds'; + }); + + test('formats value as a string datetime with seconds', () => { + const executionValue = param.getExecutionValue(); + expect(executionValue).toEqual({ start: '2019-10-05 10:00:00', end: '2019-10-06 09:59:59' }); + }); + }); + }); + + describe('normalizeValue', () => { + test('recognizes dates from moment arrays', () => { + const normalizedValue = param.normalizeValue([moment('2019-10-05'), moment('2019-10-06')]); + expect(normalizedValue).toHaveLength(2); + expect(normalizedValue[0].format('YYYY-MM-DD')).toBe('2019-10-05'); + expect(normalizedValue[1].format('YYYY-MM-DD')).toBe('2019-10-06'); + }); + + test('recognizes dates from object', () => { + const normalizedValue = param.normalizeValue({ start: '2019-10-05', end: '2019-10-06' }); + expect(normalizedValue).toHaveLength(2); + expect(normalizedValue[0].format('YYYY-MM-DD')).toBe('2019-10-05'); + expect(normalizedValue[1].format('YYYY-MM-DD')).toBe('2019-10-06'); + }); + + describe('Dynamic values', () => { + test('recognizes dynamic values from string index', () => { + const normalizedValue = param.normalizeValue('d_last_week'); + expect(normalizedValue).not.toBeNull(); + expect(normalizedValue).toEqual(getDynamicDateRangeFromString('d_last_week')); + }); + + test('recognizes dynamic values from a dynamic date range', () => { + const dynamicDateRange = getDynamicDateRangeFromString('d_last_week'); + const normalizedValue = param.normalizeValue(dynamicDateRange); + expect(normalizedValue).not.toBeNull(); + expect(normalizedValue).toEqual(dynamicDateRange); + }); + }); + }); +}); diff --git a/client/app/services/parameters/tests/EnumParameter.test.js b/client/app/services/parameters/tests/EnumParameter.test.js new file mode 100644 index 0000000000..ac64be5e4b --- /dev/null +++ b/client/app/services/parameters/tests/EnumParameter.test.js @@ -0,0 +1,56 @@ +import { Parameter } from '..'; + +describe('EnumParameter', () => { + let param; + let multiValuesOptions = null; + const enumOptions = 'value1\nvalue2\nvalue3\nvalue4'; + + beforeEach(() => { + const paramOptions = { + name: 'param', + title: 'Param', + type: 'enum', + enumOptions, + multiValuesOptions, + }; + param = Parameter.create(paramOptions); + }); + + describe('normalizeValue', () => { + test('returns the value when the input in the enum options', () => { + const normalizedValue = param.normalizeValue('value2'); + expect(normalizedValue).toBe('value2'); + }); + + test('returns the first value when the input is not in the enum options', () => { + const normalizedValue = param.normalizeValue('anything'); + expect(normalizedValue).toBe('value1'); + }); + }); + + describe('Multi-valued', () => { + beforeAll(() => { + multiValuesOptions = { prefix: '"', suffix: '"', separator: ',' }; + }); + + describe('normalizeValue', () => { + test('returns only valid values', () => { + const normalizedValue = param.normalizeValue(['value3', 'anything', null]); + expect(normalizedValue).toEqual(['value3']); + }); + + test('normalizes empty values as null', () => { + const normalizedValue = param.normalizeValue([]); + expect(normalizedValue).toBeNull(); + }); + }); + + describe('getExecutionValue', () => { + test('joins values when joinListValues is truthy', () => { + param.setValue(['value1', 'value3']); + const executionValue = param.getExecutionValue({ joinListValues: true }); + expect(executionValue).toBe('"value1","value3"'); + }); + }); + }); +}); diff --git a/client/app/services/parameters/tests/NumberParameter.test.js b/client/app/services/parameters/tests/NumberParameter.test.js new file mode 100644 index 0000000000..2a292ea880 --- /dev/null +++ b/client/app/services/parameters/tests/NumberParameter.test.js @@ -0,0 +1,26 @@ +import { Parameter } from '..'; + +describe('NumberParameter', () => { + let param; + + beforeEach(() => { + param = Parameter.create({ name: 'param', title: 'Param', type: 'number' }); + }); + + describe('normalizeValue', () => { + test('converts Strings', () => { + const normalizedValue = param.normalizeValue('15'); + expect(normalizedValue).toBe(15); + }); + + test('converts Numbers', () => { + const normalizedValue = param.normalizeValue(42); + expect(normalizedValue).toBe(42); + }); + + test('returns null when not possible to convert to number', () => { + const normalizedValue = param.normalizeValue('notanumber'); + expect(normalizedValue).toBeNull(); + }); + }); +}); diff --git a/client/app/services/parameters/tests/QueryBasedDropdownParameter.test.js b/client/app/services/parameters/tests/QueryBasedDropdownParameter.test.js new file mode 100644 index 0000000000..46c5cbb7c6 --- /dev/null +++ b/client/app/services/parameters/tests/QueryBasedDropdownParameter.test.js @@ -0,0 +1,54 @@ +import { Parameter } from '..'; + +describe('QueryBasedDropdownParameter', () => { + let param; + let multiValuesOptions = null; + + beforeEach(() => { + const paramOptions = { + name: 'param', + title: 'Param', + type: 'query', + queryId: 1, + multiValuesOptions, + }; + param = Parameter.create(paramOptions); + }); + + describe('normalizeValue', () => { + test('returns the value when the input in the enum options', () => { + const normalizedValue = param.normalizeValue('value2'); + expect(normalizedValue).toBe('value2'); + }); + + describe('Empty values', () => { + const emptyValues = [null, undefined, []]; + + test.each(emptyValues)('normalizes empty value \'%s\' as null', (emptyValue) => { + const normalizedValue = param.normalizeValue(emptyValue); + expect(normalizedValue).toBeNull(); + }); + }); + }); + + describe('Multi-valued', () => { + beforeAll(() => { + multiValuesOptions = { prefix: '"', suffix: '"', separator: ',' }; + }); + + describe('normalizeValue', () => { + test('returns an array with the input when input is not an array', () => { + const normalizedValue = param.normalizeValue('value'); + expect(normalizedValue).toEqual(['value']); + }); + }); + + describe('getExecutionValue', () => { + test('joins values when joinListValues is truthy', () => { + param.setValue(['value1', 'value3']); + const executionValue = param.getExecutionValue({ joinListValues: true }); + expect(executionValue).toBe('"value1","value3"'); + }); + }); + }); +}); diff --git a/client/app/services/parameters/tests/TextParameter.test.js b/client/app/services/parameters/tests/TextParameter.test.js index f6726a8b76..a9a648c657 100644 --- a/client/app/services/parameters/tests/TextParameter.test.js +++ b/client/app/services/parameters/tests/TextParameter.test.js @@ -2,18 +2,19 @@ import { Parameter } from '..'; describe('TextParameter', () => { let param; + beforeEach(() => { param = Parameter.create({ name: 'param', title: 'Param', type: 'text' }); }); describe('normalizeValue', () => { test('converts Strings', () => { - const normalizedValue = param.normalizeValue('exampleString'); // TODO: use faker?? + const normalizedValue = param.normalizeValue('exampleString'); expect(normalizedValue).toBe('exampleString'); }); test('converts Numbers', () => { - const normalizedValue = param.normalizeValue(3); // TODO: use faker?? + const normalizedValue = param.normalizeValue(3); expect(normalizedValue).toBe('3'); }); @@ -23,7 +24,6 @@ describe('TextParameter', () => { test.each(emptyValues)('normalizes empty value \'%s\' as null', (emptyValue) => { const normalizedValue = param.normalizeValue(emptyValue); expect(normalizedValue).toBeNull(); - expect(param.isEmpty).toBeTruthy(); }); }); }); From d269a38a4b951f0e72455d5300c6620674e6c8ec Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Thu, 10 Oct 2019 17:14:01 -0300 Subject: [PATCH 21/22] Normalize null value for multi mode in Enum --- client/app/components/ParameterValueInput.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/app/components/ParameterValueInput.jsx b/client/app/components/ParameterValueInput.jsx index 30a8ba28c4..bcf45d6a83 100644 --- a/client/app/components/ParameterValueInput.jsx +++ b/client/app/components/ParameterValueInput.jsx @@ -96,13 +96,15 @@ class ParameterValueInput extends React.Component { const { enumOptions, parameter } = this.props; const { value } = this.state; const enumOptionsArray = enumOptions.split('\n').filter(v => v !== ''); + // Antd Select doesn't handle null in multiple mode + const normalize = val => (parameter.multiValuesOptions && val === null ? [] : val); return (