-
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
Conversation
There are currently two places that invoke redash/redash/tasks/queries.py Lines 192 to 197 in 45a3b72
The List logic is in the ParametizedQuery, so I was thinking about turn the above to use it instead of a direct call to As I'm not that familiar with the backend, where would you put this |
Since this component's dimensions could be as high and wide as the content it holds, we should limit it so layout doesn't break. Here are some suggestions: CSS limits {
max-width: 350px;
max-height: 69px;
overflow: auto;
} or Ant options (I think it's better): <Select
maxTagCount="3"
maxTagTextLength="10"
maxTagPlaceholder={num => `+${num.length} more`}
/> |
Sounds like a good idea 👍 But probably worth waiting to hear what @rauchy thinks.
Sounds like the better option. Not sure we should change the default, btw. And we should reuse this for multi-filters as well (but in a separate PR). |
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.
Thanks for the comments!
@rauchy, I'll finish with the backend part + add some tests, ping you back when done :)
80be362
to
293a83c
Compare
Taking another step and considering Serialization Settings I wanted to make it custom (separator, prefix and suffix to each value options at least backend-wise for future support to any UI). While exploring options I realized there is another case that will need to be handled: When running adhoc queries (or running a modified unsaved existing one) there is no information about parameters (only their values), thus no information about the Serialization options. As it's a "trusted" route for queries I think I'll just replicate serialization logic in the frontend and send the value as string for such cases. An alternative is to always serialize this on frontend and have a "reverse operation" to validate the values in backend based on settings. Edit: regex = "(?:{}|^)(?:{})(.*?)(?:{})(?={}|$)".format(re.escape(separator), re.escape(prefix),
re.escape(suffix), re.escape(separator))
if not re.match('^(' + regex +')*$', value):
return False
values = re.findall(regex, value)
return set(map(unicode, values)).issubset(set(dropdown_options)) @arikfr WDYT? (I couldn't think of a simpler way to do that without regex) |
In what cases one would want different prefix and suffix? I'm not sure if at this stage we should let the user control the separator anyway, but we can have a setting for it just in case without visible UI. Also, if the user picks quotes they need to pick the escaping form. Most SQL implementation escape a single quote by double it (
The regex seems to work, but this is tricky and can lead to various issues down the road. Maybe instead we can just skip validation in such case (after confirming the user has access to the data source), like we do with dropdowns in general? |
I have no idea, but could be a use case at some point, my plan was to have it only on backend for now 😅. The same for the separator. For UI I was thinking about showing quote options in a Select.
That was sort of my concern too, a Regex that takes strings on it doesn't seem to be something that reliable.
For such cases that you only have the parameter values the validation is already skipped the problem is that you'd only have the list of values, not information on how to serialize data. As the validation is already skipped I think the best option will be to replicate the serialization logic on frontend and just send it as String. |
- Set parameter value again to pass through checks - Add setValue check for multi values
Very nice @gabrieldutra. I have a few wording suggestions.
The hint at the bottom is essential to understanding the feature and its result. |
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.
Notes
- I wanted to let options available on Backend (prefix, suffix and separator), even though not showed in UI just to handle possible future cases;
- I tried to handle parameter issues when changing their settings, the result is a more complex Parameter Structure (
query.js
file), which will be even more complex after Add Dynamic Values to Date and Date Range Parameters #3904. A refactor for that is on my plans once both are merged; - I want to add Cypress tests here too, but it's probably better to wait Parameter “Apply Changes” button #3907 to avoid rework.
Caveats
- Needed to replicate the logic for the List Join on frontend for cases where the Parameter schema is not present (
joinListValue = true
). In that case value is sent as String as in backend validation is already skipped (it's a "trusted" route).
@@ -38,7 +38,7 @@ function ParametersDirective($location) { | |||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
This makes Parameter settings edits safer. setValue
seems to be the centered place to process parameter values, so when a parameter changes it either adapts the value or sets it to empty.
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.
Cool cool cool
disabled={enumOptionsArray.length === 0} | ||
defaultValue={value} | ||
value={value} |
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.
Otherwise the changes from setValue
wouldn't affect here.
Other inputs remain the same as is, one need is to see the behavior after #3907.
@@ -105,7 +126,7 @@ def apply(self, parameters): | |||
raise InvalidParameterError(invalid_parameter_names) | |||
else: | |||
self.parameters.update(parameters) | |||
self.query = mustache_render(self.template, self.parameters) | |||
self.query = mustache_render(self.template, join_parameter_list_values(parameters, self.schema)) |
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.
Needed to provide the schema
to get the prefix, suffix and separator options.
The easy solution in this case is just to let the user select from a few options. For some it might be a trial and error process to find the right setting, but it's a reasonable thing. |
@@ -129,14 +129,14 @@ describe('Parameter', () => { | |||
.find('.ant-select') | |||
.click(); | |||
|
|||
cy.contains('li.ant-select-dropdown-menu-item', 'value1') | |||
cy.contains('li.ant-select-dropdown-menu-item', 'value2') |
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.
Needed to change to value2
since now I make sure the first dropdown option is selected when value is invalid or empty.
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.
Great job. Works perfect.
Few code questions and suggestions.
prefix: '', | ||
suffix: '', | ||
separator: ',', | ||
} : null })} |
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
const separator = get(multiValuesOptions, 'separator', ','); | |
const prefix = get(multiValuesOptions, 'prefix', ''); | |
const suffix = get(multiValuesOptions, 'suffix', ''); |
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: the query.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
@@ -22,6 +23,7 @@ export class ParameterValueInput extends React.Component { | |||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
@gabrieldutra do you also feel this file (and perhaps query.js:Parameters
) would be better off with some inheritance pattern (e.g. hoc, extends, ..)?
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.
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.
@@ -80,3 +80,7 @@ | |||
} | |||
} | |||
} | |||
|
|||
.parameter-multi-select { | |||
min-width: 195px; |
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.
Can you elaborate on the reasoning for this?
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.
It gets too short when it has no selected values. I let it with the same value as Text Parameters, LMK if you have other suggestions.
(Also should add a comment here in the code 😅) Edit: comment added
if (this.props.mode === 'multiple' && isArray(this.props.value)) { | ||
const optionValues = map(options, option => option.value); | ||
const validValues = intersection(this.props.value, optionValues); | ||
if (validValues.length !== this.props.value.length) { |
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.
Not sure what this means, can you explain or add a comment?
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 this case the if
is equivalent to !isEqual
, I'll just change to it as it's easier to understand 😅. Overall it checks if all values are valid (if there's any that's not on the options), in case the validValues
list is not the same as the value
provided, it forces selection of only the valid ones.
@@ -38,7 +38,7 @@ function ParametersDirective($location) { | |||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Cool cool cool
const isEmptyValue = isNull(value) || isUndefined(value) || (value === ''); | ||
static getValue(param, joinListValue = false) { | ||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is now exactly how _.isEmpty()
works 😆
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.
Almost there 😂, _.isEmpty
considers Numbers as empty. It may be interesting to have the "Empty Parameter" rule explicit, though.
@@ -145,7 +145,7 @@ describe('Parameter', () => { | |||
.find('.ant-select') | |||
.click(); | |||
|
|||
cy.contains('li.ant-select-dropdown-menu-item', 'value1') | |||
cy.contains('li.ant-select-dropdown-menu-item', 'value2') | |||
.click(); | |||
}); | |||
}); |
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.
No new tests?
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.
I was waiting for the Apply All 😝, I will add Cypress tests today.
Co-Authored-By: Ran Byron <[email protected]>
Important note I forgot 😬: I didn't code the "Refresh Schedule" part, because I couldn't get Parameterized Queries to be scheduled (Python debugger + logs to track), I suspect this is a bug, but I'm not 100% aware of how it's supposed to work ^^ (if you schedule a parameterized query on preview it won't update the results). |
How could this work for selecting multiple dates as well? |
Hi @shenoyroopesh 🙂, this will work for the existing Dropdown and Query Based Dropdown parameters. Dates won't be supported directly, but Query Based Dropdown parameters make any type of data possible with this workaround (with actual dates and not intervals). Unfortunately, it would still not be that convenient. |
@shenoyroopesh can you share where you might want to select multiple dates but a date range won't be applicable? (just trying to understand the use case) |
@@ -193,7 +193,7 @@ def refresh_queries(): | |||
if query.options and len(query.options.get('parameters', [])) > 0: | |||
query_params = {p['name']: p.get('value') | |||
for p in query.options['parameters']} | |||
query_text = mustache_render(query.query_text, query_params) | |||
query_text = query.parameterized.apply(query_params).query |
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.
Refresh schedule updated. Tested that by changing the outdated_queries
method locally so all queries were outdated 🙂.
@arikfr this one is ready to me, but it's worth to give a check on the backend part :) |
🎉 |
* Allow multiple values for enum parameter * Allow multi-select for Query dropdown parameters * CR + make sure list values are allowed * Add prefix, suffix and separator * Rename multipleValues and cast options as strings * Replicate serialization logic on frontend * Add Quote Option Select * Make sure it's enum or query before join * Add a couple of tests * Add help to quote option * Add min-width and normalize empty array * Improve behavior when changing parameter settings - Set parameter value again to pass through checks - Add setValue check for multi values * Validate enum values on setValue + CodeClimate * Ran wording suggestions * Updates after Apply Changes * Fix failing Cypress tests * Make sure enumOptions exists before split * Improve propTypes for QueyBasedParameterInput Co-Authored-By: Ran Byron <[email protected]> * CR * Cypress: Test for multi-select Enum * Fix multi-selection Cypress spec * Update Refresh Schedule
What type of PR is this? (check all applicable)
Description
This one is to add an option to allow multiple values in Dropdown parameters (see preview below :)).
maxWidth
, etc)Related Tickets & Documents
Closes #3053
Mobile & Desktop Screenshots/Recordings (if there are UI changes)
Dropdown

Query Based Dropdown
