Skip to content

Commit

Permalink
feat: Multi Select Variable (#3372)
Browse files Browse the repository at this point in the history
- #3460 
- #2015 

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
  - Added multi-select functionality for variables in dashboards.
  - Introduced a custom value selector component for dashboard settings.
  - Updated the app to use Quasar's fullscreen module.

- **Enhancements**
  - Improved query verification process in DashboardQueryEditor.
- Enhanced filtering logic for variable placeholders and values in
queries.
  - Added "IN" operator support for dashboard queries.

- **Bug Fixes**
- Fixed issues with handling and displaying selected values in various
components.

- **Performance Improvements**
- Increased size parameter for dashboard panel data loading from 10 to
100.

- **Localization**
- Added new localization key: "multiSelect" for multiple selection
support.

- **UI/UX**
  - Updated styles and key bindings in DrilldownPopUp.
- Enhanced display logic for selected values in
VariableQueryValueSelector.

- **Dependencies**
  - Updated Quasar package from version ^2.7.5 to ^2.15.4.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: ktx-abhay <abhay.padamani@kiara.tech>
  • Loading branch information
2 people authored and taimingl committed May 30, 2024
1 parent 27a305d commit b600966
Show file tree
Hide file tree
Showing 19 changed files with 571 additions and 133 deletions.
2 changes: 2 additions & 0 deletions src/common/meta/dashboards/v3/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,8 @@ pub struct VariableList {
pub query_data: Option<QueryData>,
pub value: Option<String>,
pub options: Option<Vec<CustomFieldsOption>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub multi_select: Option<bool>,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
Expand Down
8 changes: 4 additions & 4 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"moment-timezone": "^0.5.43",
"node-polyfill-webpack-plugin": "^2.0.1",
"node-sql-parser": "^4.6.4",
"quasar": "^2.7.5",
"quasar": "^2.15.4",
"rollup-plugin-node-polyfills": "^0.2.1",
"rollup-plugin-polyfill-node": "^0.10.2",
"rudder-sdk-js": "^2.45.0",
Expand Down
8 changes: 8 additions & 0 deletions web/src/components/dashboards/PanelSchemaRenderer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,14 @@ export default defineComponent({
// drilldown
const replacePlaceholders = (str: any, obj: any) => {
// if the str is same as the key, return it's value(it can be an string or array).
for (const key in obj) {
// ${varName} == str
if (`\$\{${key}\}` == str) {
return obj[key];
}
}
return str.replace(/\$\{([^}]+)\}/g, function (_: any, key: any) {
// Split the key into parts by either a dot or a ["xyz"] pattern and filter out empty strings
let parts = key.split(/\.|\["(.*?)"\]/).filter(Boolean);
Expand Down
154 changes: 93 additions & 61 deletions web/src/components/dashboards/VariablesValueSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,38 +65,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
></q-input>
</div>
<div v-else-if="item.type == 'custom'">
<q-select
style="min-width: 150px"
outlined
dense
<VariableCustomValueSelector
v-model="item.value"
:display-value="
item.value
? item.value
: !item.isLoading
? '(No Options Available)'
: ''
"
:options="item.options"
map-options
stack-label
filled
borderless
:label="item.label || item.name"
option-value="value"
option-label="label"
emit-value
data-test="dashboard-variable-custom-selector"
:variableItem="item"
@update:model-value="onVariablesValueUpdated(index)"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-italic text-grey">
No Options Available
</q-item-section>
</q-item>
</template>
</q-select>
/>
</div>
<div v-else-if="item.type == 'dynamic_filters'">
<VariableAdHocValueSelector v-model="item.value" :variableItem="item" />
Expand All @@ -111,6 +84,7 @@ import { defineComponent, reactive } from "vue";
import streamService from "../../services/stream";
import { useStore } from "vuex";
import VariableQueryValueSelector from "./settings/VariableQueryValueSelector.vue";
import VariableCustomValueSelector from "./settings/VariableCustomValueSelector.vue";
import VariableAdHocValueSelector from "./settings/VariableAdHocValueSelector.vue";
import { isInvalidDate } from "@/utils/date";
import { addLabelsToSQlQuery } from "@/utils/query/sqlUtils";
Expand All @@ -129,6 +103,7 @@ export default defineComponent({
components: {
VariableQueryValueSelector,
VariableAdHocValueSelector,
VariableCustomValueSelector,
},
setup(props: any, { emit }) {
const instance = getCurrentInstance();
Expand All @@ -138,7 +113,6 @@ export default defineComponent({
isVariablesLoading: false,
values: [],
});
// variables dependency graph
let variablesDependencyGraph: any = {};
Expand Down Expand Up @@ -167,7 +141,7 @@ export default defineComponent({
// make list of variables using variables config list
// set initial variables values from props
props?.variablesConfig?.list?.forEach((item: any) => {
const initialValue =
let initialValue =
item.type == "dynamic_filters"
? JSON.parse(
decodeURIComponent(
Expand All @@ -177,6 +151,11 @@ export default defineComponent({
) ?? []
: props.initialVariableValues?.value[item.name] ?? null;
if (item.multiSelect) {
initialValue = Array.isArray(initialValue)
? initialValue
: [initialValue];
}
const variableData = {
...item,
// isLoading is used to check that currently, if the variable is loading(it is used to show the loading icon)
Expand All @@ -185,7 +164,6 @@ export default defineComponent({
// if parent variable is not loaded or it's value is changed, isVariableLoadingPending will be true
isVariableLoadingPending: true,
};
// need to use initial value
// also, constant type variable should not be updated
if (item.type != "constant") {
Expand Down Expand Up @@ -399,10 +377,23 @@ export default defineComponent({
variable.isVariableLoadingPending === false
) {
// replace it's value in the query if it is dependent on query context
queryContext = queryContext.replace(
`$${variable.name}`,
variable.value
);
if (Array.isArray(variable.value)) {
const arrayValues = variable.value
.map((value: any) => {
return `'${value}'`;
})
.join(", ");
queryContext = queryContext.replace(
`'$${variable.name}'`,
`(${arrayValues})`
);
} else {
queryContext = queryContext.replace(
`$${variable.name}`,
variable.value
);
}
}
// above condition not matched, means variable is not loaded
// so, check if it is dependent on query context
Expand Down Expand Up @@ -451,19 +442,41 @@ export default defineComponent({
value: value.zo_sql_key.toString(),
}));
// if the old value exist in dropdown set the old value otherwise set first value of drop down otherwise set blank string value
// Define oldVariableSelectedValues array
let oldVariableSelectedValues: any = [];
if (oldVariablesData[currentVariable.name]) {
oldVariableSelectedValues = Array.isArray(
oldVariablesData[currentVariable.name]
)
? oldVariablesData[currentVariable.name]
: [oldVariablesData[currentVariable.name]];
}
// if the old value exists in the dropdown, set the old value; otherwise, set the first value of the dropdown; otherwise, set a blank string value
if (
oldVariablesData[currentVariable.name] !== undefined ||
oldVariablesData[currentVariable.name] !== null
) {
currentVariable.value = currentVariable.options.some(
(option: any) =>
option.value === oldVariablesData[currentVariable.name]
)
? oldVariablesData[currentVariable.name]
: currentVariable.options.length
? currentVariable.options[0].value
: null;
if (currentVariable.multiSelect) {
const selectedValues = currentVariable.options
.filter((option: any) =>
oldVariableSelectedValues.includes(option.value)
)
.map((option: any) => option.value);
currentVariable.value =
selectedValues.length > 0
? selectedValues
: [currentVariable.options[0].value]; // If no option is available, set as the first value
} else {
currentVariable.value = currentVariable.options.some(
(option: any) =>
option.value === oldVariablesData[currentVariable.name]
)
? oldVariablesData[currentVariable.name]
: currentVariable.options.length
? currentVariable.options[0].value
: null;
}
} else {
currentVariable.value = currentVariable.options.length
? currentVariable.options[0].value
Expand All @@ -475,7 +488,7 @@ export default defineComponent({
} else {
// no response hits found
// set value as empty string
currentVariable.value = null;
currentVariable.value = currentVariable.multiSelect ? [] : null;
// set options as empty array
currentVariable.options = [];
Expand All @@ -502,21 +515,40 @@ export default defineComponent({
break;
}
case "custom": {
// assign options
currentVariable["options"] = currentVariable?.options;
// if the old value exist in dropdown set the old value
// otherwise set first value of drop down
// otherwise set null value
let oldVariableObjectSelectedValue = currentVariable.options.find(
(option: any) =>
option.value === oldVariablesData[currentVariable.name]
);
// if the old value exist in dropdown set the old value otherwise set first value of drop down otherwise set blank string value
if (oldVariableObjectSelectedValue) {
currentVariable.value = oldVariableObjectSelectedValue.value;
currentVariable.options = currentVariable?.options;
// Check if the old value exists and set it
let oldVariableSelectedValues: any = [];
if (oldVariablesData[currentVariable.name]) {
oldVariableSelectedValues = Array.isArray(
oldVariablesData[currentVariable.name]
)
? oldVariablesData[currentVariable.name]
: [oldVariablesData[currentVariable.name]];
}
// If multiSelect is true, set the value as an array containing old value and selected value
if (currentVariable.multiSelect) {
const selectedValues = currentVariable.options
.filter((option: any) =>
oldVariableSelectedValues.includes(option.value)
)
.map((option: any) => option.value);
currentVariable.value =
// If no option is available, set as the first value or if old value exists, set the old value
selectedValues.length > 0
? selectedValues
: [currentVariable.options[0].value] ||
oldVariableSelectedValues;
} else {
currentVariable.value = currentVariable.options[0]?.value ?? null;
// If multiSelect is false, set the value as a single value from options which is selected
currentVariable.value =
currentVariable.options.find(
(option: any) => option.value === oldVariableSelectedValues[0]
)?.value ??
(currentVariable.options.length > 0
? currentVariable.options[0].value
: null);
}
resolve(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -931,7 +931,9 @@ export default defineComponent({
const dashboardVariablesFilterItems = computed(() =>
(props.dashboardData?.variables?.list ?? []).map((it: any) => ({
label: it.name,
value: "'" + "$" + it.name + "'",
value: it.multiSelect
? "(" + "$" + "{" + it.name + "}" + ")"
: "'" + "$" + it.name + "'",
}))
);
Expand Down Expand Up @@ -969,6 +971,7 @@ export default defineComponent({
"<=",
">",
"<",
"IN",
"Contains",
"Not Contains",
"Is Null",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1336,10 +1336,11 @@ export default defineComponent({
const dashboardVariablesFilterItems = computed(() =>
(props.dashboardData?.variables?.list ?? []).map((it: any) => ({
label: it.name,
value: "'" + "$" + it.name + "'",
value: it.multiSelect
? "(" + "$" + "{" + it.name + "}" + ")"
: "'" + "$" + it.name + "'",
}))
);
return {
showXAxis,
t,
Expand All @@ -1364,6 +1365,7 @@ export default defineComponent({
"<=",
">",
"<",
"IN",
"Contains",
"Not Contains",
"Is Null",
Expand Down
23 changes: 20 additions & 3 deletions web/src/components/dashboards/addPanel/DashboardQueryEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -838,11 +838,28 @@ export default defineComponent({
// Get the parsed query
try {
dashboardPanelData.meta.parsedQuery = parser.astify(
dashboardPanelData.data.queries[
let currentQuery = dashboardPanelData.data.queries[
dashboardPanelData.layout.currentQueryIndex
].query
);
// replace variables with dummy values to verify query is correct or not
if(/\${[a-zA-Z0-9_-]+:csv}/.test(currentQuery)){
currentQuery = currentQuery.replaceAll(/\${[a-zA-Z0-9_-]+:csv}/g, "1,2")
}
if(/\${[a-zA-Z0-9_-]+:singlequote}/.test(currentQuery)){
currentQuery = currentQuery.replaceAll(/\${[a-zA-Z0-9_-]+:singlequote}/g, "'1','2'")
}
if(/\${[a-zA-Z0-9_-]+:doublequote}/.test(currentQuery)){
currentQuery = currentQuery.replaceAll(/\${[a-zA-Z0-9_-]+:doublequote}/g, '"1","2"')
}
if(/\${[a-zA-Z0-9_-]+:pipe}/.test(currentQuery)){
currentQuery = currentQuery.replaceAll(/\${[a-zA-Z0-9_-]+:pipe}/g, "1|2")
}
if(/\$(\w+|\{\w+\})/.test(currentQuery)){
currentQuery = currentQuery.replaceAll(/\$(\w+|\{\w+\})/g, "10")
}
dashboardPanelData.meta.parsedQuery = parser.astify(currentQuery);
} catch (e) {
// exit as there is an invalid query
dashboardPanelData.meta.errors.queryErrors.push("Invalid SQL Syntax");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -922,7 +922,9 @@ export default defineComponent({
const dashboardVariablesFilterItems = computed(() =>
(props.dashboardData?.variables?.list ?? []).map((it: any) => ({
label: it.name,
value: "'" + "$" + it.name + "'",
value: it.multiSelect
? "(" + "$" + "{" + it.name + "}" + ")"
: "'" + "$" + it.name + "'",
}))
);
Expand Down Expand Up @@ -959,6 +961,7 @@ export default defineComponent({
"<=",
">",
"<",
"IN",
"Contains",
"Not Contains",
"Is Null",
Expand Down
Loading

0 comments on commit b600966

Please sign in to comment.