-
Notifications
You must be signed in to change notification settings - Fork 4.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support multi-select in parameters #3952
Changes from all commits
113fa1a
8c627f0
25fed53
a98cb5a
293a83c
de31c67
e4270cd
a703ec0
138ebab
286e9ce
403dd7d
a8a6413
f5fc796
c25db79
c0d5297
00ede72
6234f86
0be0693
3b3d410
a71bde9
6b89b4c
58c25aa
6ae403a
e2f3756
96319d2
5487731
6cf7e5c
26e8274
8d127e4
2b85c79
1d209e2
52523c0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,13 +13,20 @@ import './ParameterValueInput.less'; | |
|
||
const { Option } = Select; | ||
|
||
const multipleValuesProps = { | ||
maxTagCount: 3, | ||
maxTagTextLength: 10, | ||
maxTagPlaceholder: num => `+${num.length} more`, | ||
}; | ||
|
||
export class ParameterValueInput extends React.Component { | ||
static propTypes = { | ||
type: PropTypes.string, | ||
value: PropTypes.any, // eslint-disable-line react/forbid-prop-types | ||
enumOptions: PropTypes.string, | ||
queryId: PropTypes.number, | ||
parameter: PropTypes.any, // eslint-disable-line react/forbid-prop-types | ||
allowMultipleValues: PropTypes.bool, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gabrieldutra do you also feel this file (and perhaps There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like everything related to Parameters should be on an inheritance pattern. This included. This is all on the "Parameters Design" thought. Once both this and the Dynamic Values are merged I have a refactoring in mind. |
||
onSelect: PropTypes.func, | ||
className: PropTypes.string, | ||
}; | ||
|
@@ -30,6 +37,7 @@ export class ParameterValueInput extends React.Component { | |
enumOptions: '', | ||
queryId: null, | ||
parameter: null, | ||
allowMultipleValues: false, | ||
onSelect: () => {}, | ||
className: '', | ||
}; | ||
|
@@ -88,36 +96,43 @@ export class ParameterValueInput extends React.Component { | |
} | ||
|
||
renderEnumInput() { | ||
const { value, enumOptions } = this.props; | ||
const { enumOptions, allowMultipleValues } = this.props; | ||
const { value } = this.state; | ||
const enumOptionsArray = enumOptions.split('\n').filter(v => v !== ''); | ||
return ( | ||
<Select | ||
className={this.props.className} | ||
mode={allowMultipleValues ? 'multiple' : 'default'} | ||
gabrieldutra marked this conversation as resolved.
Show resolved
Hide resolved
|
||
optionFilterProp="children" | ||
disabled={enumOptionsArray.length === 0} | ||
defaultValue={value} | ||
value={value} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Otherwise the changes from Other inputs remain the same as is, one need is to see the behavior after #3907. |
||
onChange={this.onSelect} | ||
dropdownMatchSelectWidth={false} | ||
dropdownClassName="ant-dropdown-in-bootstrap-modal" | ||
showSearch | ||
style={{ minWidth: 60 }} | ||
optionFilterProp="children" | ||
style={{ minWidth: allowMultipleValues ? 195 : 60 }} | ||
notFoundContent={null} | ||
{...multipleValuesProps} | ||
> | ||
{enumOptionsArray.map(option => (<Option key={option} value={option}>{ option }</Option>))} | ||
</Select> | ||
); | ||
} | ||
|
||
renderQueryBasedInput() { | ||
const { queryId, parameter } = this.props; | ||
const { queryId, parameter, allowMultipleValues } = this.props; | ||
const { value } = this.state; | ||
return ( | ||
<QueryBasedParameterInput | ||
className={this.props.className} | ||
mode={allowMultipleValues ? 'multiple' : 'default'} | ||
optionFilterProp="children" | ||
parameter={parameter} | ||
value={value} | ||
queryId={queryId} | ||
onSelect={this.onSelect} | ||
style={{ minWidth: allowMultipleValues ? 195 : 60 }} | ||
{...multipleValuesProps} | ||
/> | ||
); | ||
} | ||
|
@@ -187,6 +202,7 @@ export default function init(ngModule) { | |
parameter="$ctrl.param" | ||
enum-options="$ctrl.param.enumOptions" | ||
query-id="$ctrl.param.queryId" | ||
allow-multiple-values="!!$ctrl.param.multiValuesOptions" | ||
on-select="$ctrl.setValue" | ||
></parameter-value-input-impl> | ||
`, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -58,7 +58,7 @@ function ParametersDirective($location, KeyboardShortcuts) { | |
EditParameterSettingsDialog | ||
.showModal({ parameter }) | ||
.result.then((updated) => { | ||
scope.parameters[index] = extend(parameter, updated); | ||
scope.parameters[index] = extend(parameter, updated).setValue(updated.value); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes Parameter settings edits safer. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool cool cool |
||
scope.onUpdated(); | ||
}); | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,9 +2,8 @@ import moment from 'moment'; | |
import debug from 'debug'; | ||
import Mustache from 'mustache'; | ||
import { | ||
zipObject, isEmpty, map, filter, includes, union, uniq, has, | ||
isNull, isUndefined, isArray, isObject, identity, extend, each, | ||
startsWith, some, | ||
zipObject, isEmpty, map, filter, includes, union, uniq, has, get, intersection, | ||
isNull, isUndefined, isArray, isObject, identity, extend, each, join, some, startsWith, | ||
} from 'lodash'; | ||
|
||
Mustache.escape = identity; // do not html-escape values | ||
|
@@ -138,6 +137,7 @@ export class Parameter { | |
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; | ||
|
||
|
@@ -164,6 +164,10 @@ export class Parameter { | |
return isNull(this.getValue()); | ||
} | ||
|
||
getValue(extra = {}) { | ||
return this.constructor.getValue(this, extra); | ||
} | ||
|
||
get hasDynamicValue() { | ||
if (isDateParameter(this.type)) { | ||
return isDynamicDate(this.value); | ||
|
@@ -184,13 +188,9 @@ export class Parameter { | |
return false; | ||
} | ||
|
||
getValue() { | ||
return this.constructor.getValue(this); | ||
} | ||
|
||
static getValue(param) { | ||
const { value, type, useCurrentDateTime } = param; | ||
const isEmptyValue = isNull(value) || isUndefined(value) || (value === ''); | ||
static getValue(param, extra = {}) { | ||
const { value, type, useCurrentDateTime, multiValuesOptions } = param; | ||
const isEmptyValue = isNull(value) || isUndefined(value) || (value === '') || (isArray(value) && value.length === 0); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is now exactly how There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Almost there 😂, |
||
if (isDateRangeParameter(type) && param.hasDynamicValue) { | ||
const { dynamicValue } = param; | ||
if (dynamicValue) { | ||
|
@@ -224,10 +224,32 @@ export class Parameter { | |
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; | ||
|
@@ -331,6 +353,9 @@ export class Parameter { | |
[`${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, | ||
|
@@ -352,7 +377,15 @@ export class Parameter { | |
} else { | ||
const key = `${prefix}${this.name}`; | ||
if (has(query, key)) { | ||
this.setValue(query[key]); | ||
if (this.multiValuesOptions) { | ||
try { | ||
this.setValue(JSON.parse(query[key])); | ||
} catch (e) { | ||
this.setValue(query[key]); | ||
} | ||
} else { | ||
this.setValue(query[key]); | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -460,9 +493,9 @@ class Parameters { | |
return !isEmpty(this.get()); | ||
} | ||
|
||
getValues() { | ||
getValues(extra = {}) { | ||
const params = this.get(); | ||
return zipObject(map(params, i => i.name), map(params, i => i.getValue())); | ||
return zipObject(map(params, i => i.name), map(params, i => i.getValue(extra))); | ||
} | ||
|
||
hasPendingValues() { | ||
|
@@ -710,7 +743,7 @@ function QueryResource( | |
return new QueryResultError("Can't execute empty query."); | ||
} | ||
|
||
const parameters = this.getParameters().getValues(); | ||
const parameters = this.getParameters().getValues({ joinListValues: true }); | ||
const execute = () => QueryResult.get(this.data_source_id, queryText, parameters, maxAge, this.id); | ||
return this.prepareQueryResultExecution(execute, maxAge); | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since these are already set as the defaults in
query.js:Parameters
, perhaps no need for it here?redash/client/app/services/query.js
Lines 107 to 109 in a71bde9
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In
EditParameterSettingsDialog
it's used to manage the Quotation Select value. I was actually a bit "paranoid" with this, there's one in the backend as well 😆. In any case: thequery.js
one is only triggered for "text query" cases, where the backend is not aware of these settings and serialization logic is made by the frontend (yep, the same logic is duplicated to workaround this scenario).Should probably add a comment in there to mention that. Edit: comment added