mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 02:48:59 +08:00
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:
parent
c46bef321c
commit
ca53136cbf
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -140,6 +140,7 @@ function VariablesSetting({
|
||||
{variableViewMode ? (
|
||||
<VariableItem
|
||||
variableData={{ ...variableEditData } as IDashboardVariable}
|
||||
existingVariables={variables}
|
||||
onSave={onVariableSaveHandler}
|
||||
onCancel={onDoneVariableViewMode}
|
||||
validateName={validateVariableName}
|
||||
|
@ -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' }}>
|
||||
|
@ -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>
|
||||
|
@ -17,3 +17,8 @@ export const VariableName = styled(Typography)`
|
||||
font-style: italic;
|
||||
color: ${grey[0]};
|
||||
`;
|
||||
|
||||
export const SelectItemStyle = {
|
||||
minWidth: 120,
|
||||
fontSize: '0.8rem',
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
14
frontend/src/container/NewDashboard/utils.ts
Normal file
14
frontend/src/container/NewDashboard/utils.ts
Normal 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;
|
||||
}
|
@ -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;
|
||||
|
@ -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 = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user