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 <palashgdev@gmail.com>
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
This commit is contained in:
Srikanth Chekuri 2023-01-25 13:22:57 +05:30 committed by GitHub
parent c46bef321c
commit ca53136cbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 192 additions and 47 deletions

View File

@ -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<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/variables/query?query=${encodeURIComponent(props.query)}`,
);
const response = await axios.post(`/variables/query`, props);
return {
statusCode: 200,

View File

@ -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<string, IDashboardVariable>;
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)

View File

@ -140,6 +140,7 @@ function VariablesSetting({
{variableViewMode ? (
<VariableItem
variableData={{ ...variableEditData } as IDashboardVariable}
existingVariables={variables}
onSave={onVariableSaveHandler}
onCancel={onDoneVariableViewMode}
validateName={validateVariableName}

View File

@ -8,7 +8,9 @@ import { map } from 'lodash-es';
import React, { useCallback, useEffect, useState } from 'react';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { VariableContainer, VariableName } from './styles';
import { variablePropsToPayloadVariables } from '../utils';
import { SelectItemStyle, VariableContainer, VariableName } from './styles';
import { areArraysEqual } from './util';
const { Option } = Select;
@ -16,18 +18,35 @@ const ALL_SELECT_VALUE = '__ALL__';
interface VariableItemProps {
variableData: IDashboardVariable;
onValueUpdate: (name: string | undefined, arg1: string | string[]) => void;
existingVariables: Record<string, IDashboardVariable>;
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<boolean>(false);
const [errorMessage, setErrorMessage] = useState<null | string>(null);
/* eslint-disable sonarjs/cognitive-complexity */
const getOptions = useCallback(async (): Promise<void> => {
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 (
<VariableContainer>
<VariableName>${variableData.name}</VariableName>
@ -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),
}}
/>
) : (
<Select
value={variableData.allSelected ? 'ALL' : variableData.selectedValue}
onChange={handleChange}
bordered={false}
placeholder="Select value"
mode={
(variableData.multiSelect && !variableData.allSelected
? 'multiple'
: null) as never
}
dropdownMatchSelectWidth={false}
style={{
minWidth: 120,
fontSize: '0.8rem',
}}
loading={isLoading}
showArrow
>
{variableData.multiSelect && variableData.showALLOption && (
<Option value={ALL_SELECT_VALUE}>ALL</Option>
)}
{map(optionsData, (option) => (
<Option value={option}>{(option as string).toString()}</Option>
))}
</Select>
!errorMessage && (
<Select
value={selectValue}
onChange={handleChange}
bordered={false}
placeholder="Select value"
mode={mode}
dropdownMatchSelectWidth={false}
style={SelectItemStyle}
loading={isLoading}
showArrow
>
{enableSelectAll && <Option value={ALL_SELECT_VALUE}>ALL</Option>}
{map(optionsData, (option) => (
<Option value={option}>{option.toString()}</Option>
))}
</Select>
)
)}
{errorMessage && (
<span style={{ margin: '0 0.5rem' }}>

View File

@ -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<boolean>(false);
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
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) => (
<VariableItem
key={`${variableName}${variables[variableName].modificationUUID}`}
variableData={{ name: variableName, ...variables[variableName] }}
existingVariables={variables}
variableData={{
name: variableName,
...variables[variableName],
change: update,
}}
onValueUpdate={onValueUpdate as never}
onAllSelectedUpdate={onAllSelectedUpdate as never}
lastUpdatedVar={lastUpdatedVar}
/>
))}
</Row>

View File

@ -17,3 +17,8 @@ export const VariableName = styled(Typography)`
font-style: italic;
color: ${grey[0]};
`;
export const SelectItemStyle = {
minWidth: 120,
fontSize: '0.8rem',
};

View File

@ -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;
}

View File

@ -0,0 +1,14 @@
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { PayloadVariables } from 'types/api/dashboard/variables/query';
export function variablePropsToPayloadVariables(
variables: Record<string, IDashboardVariable>,
): PayloadVariables {
const payloadVariables: PayloadVariables = {};
Object.entries(variables).forEach(([key, value]) => {
payloadVariables[key] = value?.selectedValue;
});
return payloadVariables;
}

View File

@ -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;

View File

@ -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 = {