From ca53136cbf2f595a34ca8a94f4f0a8a9b27fd642 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 25 Jan 2023 13:22:57 +0530 Subject: [PATCH] feat(ui): dashboard variable chaining (#2037) * feat: dashboard variable chaining * feat(ui): dashboard variable chaining * chore: update vars loading * chore: fix lint * chore: better dependent vars * chore: multi dependent variables * chore: add more user friendly error * chore: review comments * chore: address review comments * chore: remove string assertion * chore: fix build by updating types * chore: fix the variable data auto loading Co-authored-by: Palash Gupta Co-authored-by: Vishal Sharma --- frontend/src/api/dashboard/variables/query.ts | 6 +- .../Variables/VariableItem/VariableItem.tsx | 11 +- .../DashboardSettings/Variables/index.tsx | 1 + .../VariableItem.tsx | 142 +++++++++++++----- .../DashboardVariablesSelection/index.tsx | 30 +++- .../DashboardVariablesSelection/styles.ts | 5 + .../DashboardVariablesSelection/util.ts | 16 ++ frontend/src/container/NewDashboard/utils.ts | 14 ++ frontend/src/types/api/dashboard/getAll.ts | 8 +- .../types/api/dashboard/variables/query.ts | 6 + 10 files changed, 192 insertions(+), 47 deletions(-) create mode 100644 frontend/src/container/NewDashboard/DashboardVariablesSelection/util.ts create mode 100644 frontend/src/container/NewDashboard/utils.ts diff --git a/frontend/src/api/dashboard/variables/query.ts b/frontend/src/api/dashboard/variables/query.ts index 61693ba4c0..958fdb7e3a 100644 --- a/frontend/src/api/dashboard/variables/query.ts +++ b/frontend/src/api/dashboard/variables/query.ts @@ -1,4 +1,4 @@ -import axios from 'api'; +import { ApiV2Instance as axios } from 'api'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { AxiosError } from 'axios'; import { ErrorResponse, SuccessResponse } from 'types/api'; @@ -8,9 +8,7 @@ const query = async ( props: Props, ): Promise | ErrorResponse> => { try { - const response = await axios.get( - `/variables/query?query=${encodeURIComponent(props.query)}`, - ); + const response = await axios.post(`/variables/query`, props); return { statusCode: 200, diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx index f8a08f2677..80b77283aa 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx @@ -25,6 +25,7 @@ import { } from 'types/api/dashboard/getAll'; import { v4 } from 'uuid'; +import { variablePropsToPayloadVariables } from '../../../utils'; import { TVariableViewMode } from '../types'; import { LabelContainer, VariableItemRow } from './styles'; @@ -32,6 +33,7 @@ const { Option } = Select; interface VariableItemProps { variableData: IDashboardVariable; + existingVariables: Record; onCancel: () => void; onSave: (name: string, arg0: IDashboardVariable, arg1: string) => void; validateName: (arg0: string) => boolean; @@ -39,6 +41,7 @@ interface VariableItemProps { } function VariableItem({ variableData, + existingVariables, onCancel, onSave, validateName, @@ -134,10 +137,16 @@ function VariableItem({ try { const variableQueryResponse = await query({ query: variableQueryValue, + variables: variablePropsToPayloadVariables(existingVariables), }); setPreviewLoading(false); if (variableQueryResponse.error) { - setErrorPreview(variableQueryResponse.error); + let message = variableQueryResponse.error; + if (variableQueryResponse.error.includes('Syntax error:')) { + message = + 'Please make sure query is valid and dependent variables are selected'; + } + setErrorPreview(message); return; } if (variableQueryResponse.payload?.variableValues) diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx index b419085c6a..8dfc3a73e0 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx @@ -140,6 +140,7 @@ function VariablesSetting({ {variableViewMode ? ( void; + existingVariables: Record; + onValueUpdate: ( + name: string | undefined, + arg1: + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined, + ) => void; onAllSelectedUpdate: (name: string | undefined, arg1: boolean) => void; + lastUpdatedVar: string; } function VariableItem({ variableData, + existingVariables, onValueUpdate, onAllSelectedUpdate, + lastUpdatedVar, }: VariableItemProps): JSX.Element { - const [optionsData, setOptionsData] = useState([]); + const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>( + [], + ); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(null); + + /* eslint-disable sonarjs/cognitive-complexity */ const getOptions = useCallback(async (): Promise => { if (variableData.type === 'QUERY') { try { @@ -36,17 +55,58 @@ function VariableItem({ const response = await query({ query: variableData.queryValue || '', + variables: variablePropsToPayloadVariables(existingVariables), }); setIsLoading(false); if (response.error) { - setErrorMessage(response.error); + let message = response.error; + if (response.error.includes('Syntax error:')) { + message = + 'Please make sure query is valid and dependent variables are selected'; + } + setErrorMessage(message); return; } - if (response.payload?.variableValues) - setOptionsData( - sortValues(response.payload?.variableValues, variableData.sort) as never, + if (response.payload?.variableValues) { + const newOptionsData = sortValues( + response.payload?.variableValues, + variableData.sort, ); + // Since there is a chance of a variable being dependent on other + // variables, we need to check if the optionsData has changed + // If it has changed, we need to update the dependent variable + // So we compare the new optionsData with the old optionsData + const oldOptionsData = sortValues(optionsData, variableData.sort) as never; + if (!areArraysEqual(newOptionsData, oldOptionsData)) { + /* eslint-disable no-useless-escape */ + const re = new RegExp(`\\{\\{\\s*?\\.${lastUpdatedVar}\\s*?\\}\\}`); // regex for `{{.var}}` + // If the variable is dependent on the last updated variable + // and contains the last updated variable in its query (of the form `{{.var}}`) + // then we need to update the value of the variable + const queryValue = variableData.queryValue || ''; + const dependVarReMatch = queryValue.match(re); + if ( + variableData.type === 'QUERY' && + dependVarReMatch !== null && + dependVarReMatch.length > 0 + ) { + let value = variableData.selectedValue; + let allSelected = false; + // The default value for multi-select is ALL and first value for + // single select + if (variableData.multiSelect) { + value = newOptionsData; + allSelected = true; + } else { + [value] = newOptionsData; + } + onValueUpdate(variableData.name, value); + onAllSelectedUpdate(variableData.name, allSelected); + } + setOptionsData(newOptionsData); + } + } } catch (e) { console.error(e); } @@ -59,10 +119,12 @@ function VariableItem({ ); } }, [ - variableData.customValue, - variableData.queryValue, - variableData.sort, - variableData.type, + variableData, + existingVariables, + onValueUpdate, + onAllSelectedUpdate, + optionsData, + lastUpdatedVar, ]); useEffect(() => { @@ -72,7 +134,8 @@ function VariableItem({ const handleChange = (value: string | string[]): void => { if ( value === ALL_SELECT_VALUE || - (Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) + (Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) || + (Array.isArray(value) && value.length === 0) ) { onValueUpdate(variableData.name, optionsData); onAllSelectedUpdate(variableData.name, true); @@ -81,6 +144,15 @@ function VariableItem({ onAllSelectedUpdate(variableData.name, false); } }; + + const selectValue = variableData.allSelected + ? 'ALL' + : variableData.selectedValue?.toString() || ''; + const mode = + variableData.multiSelect && !variableData.allSelected + ? 'multiple' + : undefined; + const enableSelectAll = variableData.multiSelect && variableData.showALLOption; return ( ${variableData.name} @@ -93,35 +165,29 @@ function VariableItem({ handleChange(e.target.value || ''); }} style={{ - width: 50 + ((variableData.selectedValue?.length || 0) * 7 || 50), + width: + 50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50), }} /> ) : ( - + !errorMessage && ( + + ) )} {errorMessage && ( diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx index c3aad577bf..18f909d3ac 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx @@ -1,6 +1,6 @@ import { Row } from 'antd'; import { map, sortBy } from 'lodash-es'; -import React from 'react'; +import React, { useState } from 'react'; import { connect, useSelector } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; @@ -23,13 +23,30 @@ function DashboardVariableSelection({ data: { variables = {} }, } = selectedDashboard; + const [update, setUpdate] = useState(false); + const [lastUpdatedVar, setLastUpdatedVar] = useState(''); + + const onVarChanged = (name: string): void => { + setLastUpdatedVar(name); + setUpdate(!update); + }; + const onValueUpdate = ( name: string, - value: IDashboardVariable['selectedValue'], + value: + | string + | string[] + | number + | number[] + | boolean + | boolean[] + | null + | undefined, ): void => { const updatedVariablesData = { ...variables }; updatedVariablesData[name].selectedValue = value; updateDashboardVariables(updatedVariablesData); + onVarChanged(name); }; const onAllSelectedUpdate = ( name: string, @@ -38,6 +55,7 @@ function DashboardVariableSelection({ const updatedVariablesData = { ...variables }; updatedVariablesData[name].allSelected = value; updateDashboardVariables(updatedVariablesData); + onVarChanged(name); }; return ( @@ -45,9 +63,15 @@ function DashboardVariableSelection({ {map(sortBy(Object.keys(variables)), (variableName) => ( ))} diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts b/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts index 9b4b1d3322..76ba50c38c 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts @@ -17,3 +17,8 @@ export const VariableName = styled(Typography)` font-style: italic; color: ${grey[0]}; `; + +export const SelectItemStyle = { + minWidth: 120, + fontSize: '0.8rem', +}; diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/util.ts b/frontend/src/container/NewDashboard/DashboardVariablesSelection/util.ts new file mode 100644 index 0000000000..1b1f6ff29e --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/util.ts @@ -0,0 +1,16 @@ +export function areArraysEqual( + a: (string | number | boolean)[], + b: (string | number | boolean)[], +): boolean { + if (a.length !== b.length) { + return false; + } + + for (let i = 0; i < a.length; i += 1) { + if (a[i] !== b[i]) { + return false; + } + } + + return true; +} diff --git a/frontend/src/container/NewDashboard/utils.ts b/frontend/src/container/NewDashboard/utils.ts new file mode 100644 index 0000000000..bf96e429c5 --- /dev/null +++ b/frontend/src/container/NewDashboard/utils.ts @@ -0,0 +1,14 @@ +import { IDashboardVariable } from 'types/api/dashboard/getAll'; +import { PayloadVariables } from 'types/api/dashboard/variables/query'; + +export function variablePropsToPayloadVariables( + variables: Record, +): PayloadVariables { + const payloadVariables: PayloadVariables = {}; + + Object.entries(variables).forEach(([key, value]) => { + payloadVariables[key] = value?.selectedValue; + }); + + return payloadVariables; +} diff --git a/frontend/src/types/api/dashboard/getAll.ts b/frontend/src/types/api/dashboard/getAll.ts index 8de0978fb0..680cbb5377 100644 --- a/frontend/src/types/api/dashboard/getAll.ts +++ b/frontend/src/types/api/dashboard/getAll.ts @@ -31,10 +31,16 @@ export interface IDashboardVariable { sort: TSortVariableValuesType; multiSelect: boolean; showALLOption: boolean; - selectedValue?: null | string | string[]; + selectedValue?: + | null + | string + | number + | boolean + | (string | number | boolean)[]; // Internal use modificationUUID?: string; allSelected?: boolean; + change?: boolean; } export interface Dashboard { id: number; diff --git a/frontend/src/types/api/dashboard/variables/query.ts b/frontend/src/types/api/dashboard/variables/query.ts index e715378c66..4fe305600e 100644 --- a/frontend/src/types/api/dashboard/variables/query.ts +++ b/frontend/src/types/api/dashboard/variables/query.ts @@ -1,5 +1,11 @@ +export type PayloadVariables = Record< + string, + undefined | null | string | number | boolean | (string | number | boolean)[] +>; + export type Props = { query: string; + variables: PayloadVariables; }; export type PayloadProps = {