mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 19:49:06 +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 { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
@ -8,9 +8,7 @@ const query = async (
|
|||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
const response = await axios.post(`/variables/query`, props);
|
||||||
`/variables/query?query=${encodeURIComponent(props.query)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
|
@ -25,6 +25,7 @@ import {
|
|||||||
} from 'types/api/dashboard/getAll';
|
} from 'types/api/dashboard/getAll';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
import { variablePropsToPayloadVariables } from '../../../utils';
|
||||||
import { TVariableViewMode } from '../types';
|
import { TVariableViewMode } from '../types';
|
||||||
import { LabelContainer, VariableItemRow } from './styles';
|
import { LabelContainer, VariableItemRow } from './styles';
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ const { Option } = Select;
|
|||||||
|
|
||||||
interface VariableItemProps {
|
interface VariableItemProps {
|
||||||
variableData: IDashboardVariable;
|
variableData: IDashboardVariable;
|
||||||
|
existingVariables: Record<string, IDashboardVariable>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSave: (name: string, arg0: IDashboardVariable, arg1: string) => void;
|
onSave: (name: string, arg0: IDashboardVariable, arg1: string) => void;
|
||||||
validateName: (arg0: string) => boolean;
|
validateName: (arg0: string) => boolean;
|
||||||
@ -39,6 +41,7 @@ interface VariableItemProps {
|
|||||||
}
|
}
|
||||||
function VariableItem({
|
function VariableItem({
|
||||||
variableData,
|
variableData,
|
||||||
|
existingVariables,
|
||||||
onCancel,
|
onCancel,
|
||||||
onSave,
|
onSave,
|
||||||
validateName,
|
validateName,
|
||||||
@ -134,10 +137,16 @@ function VariableItem({
|
|||||||
try {
|
try {
|
||||||
const variableQueryResponse = await query({
|
const variableQueryResponse = await query({
|
||||||
query: variableQueryValue,
|
query: variableQueryValue,
|
||||||
|
variables: variablePropsToPayloadVariables(existingVariables),
|
||||||
});
|
});
|
||||||
setPreviewLoading(false);
|
setPreviewLoading(false);
|
||||||
if (variableQueryResponse.error) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (variableQueryResponse.payload?.variableValues)
|
if (variableQueryResponse.payload?.variableValues)
|
||||||
|
@ -140,6 +140,7 @@ function VariablesSetting({
|
|||||||
{variableViewMode ? (
|
{variableViewMode ? (
|
||||||
<VariableItem
|
<VariableItem
|
||||||
variableData={{ ...variableEditData } as IDashboardVariable}
|
variableData={{ ...variableEditData } as IDashboardVariable}
|
||||||
|
existingVariables={variables}
|
||||||
onSave={onVariableSaveHandler}
|
onSave={onVariableSaveHandler}
|
||||||
onCancel={onDoneVariableViewMode}
|
onCancel={onDoneVariableViewMode}
|
||||||
validateName={validateVariableName}
|
validateName={validateVariableName}
|
||||||
|
@ -8,7 +8,9 @@ import { map } from 'lodash-es';
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
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;
|
const { Option } = Select;
|
||||||
|
|
||||||
@ -16,18 +18,35 @@ const ALL_SELECT_VALUE = '__ALL__';
|
|||||||
|
|
||||||
interface VariableItemProps {
|
interface VariableItemProps {
|
||||||
variableData: IDashboardVariable;
|
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;
|
onAllSelectedUpdate: (name: string | undefined, arg1: boolean) => void;
|
||||||
|
lastUpdatedVar: string;
|
||||||
}
|
}
|
||||||
function VariableItem({
|
function VariableItem({
|
||||||
variableData,
|
variableData,
|
||||||
|
existingVariables,
|
||||||
onValueUpdate,
|
onValueUpdate,
|
||||||
onAllSelectedUpdate,
|
onAllSelectedUpdate,
|
||||||
|
lastUpdatedVar,
|
||||||
}: VariableItemProps): JSX.Element {
|
}: VariableItemProps): JSX.Element {
|
||||||
const [optionsData, setOptionsData] = useState([]);
|
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
const [errorMessage, setErrorMessage] = useState<null | string>(null);
|
const [errorMessage, setErrorMessage] = useState<null | string>(null);
|
||||||
|
|
||||||
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
const getOptions = useCallback(async (): Promise<void> => {
|
const getOptions = useCallback(async (): Promise<void> => {
|
||||||
if (variableData.type === 'QUERY') {
|
if (variableData.type === 'QUERY') {
|
||||||
try {
|
try {
|
||||||
@ -36,17 +55,58 @@ function VariableItem({
|
|||||||
|
|
||||||
const response = await query({
|
const response = await query({
|
||||||
query: variableData.queryValue || '',
|
query: variableData.queryValue || '',
|
||||||
|
variables: variablePropsToPayloadVariables(existingVariables),
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
if (response.error) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (response.payload?.variableValues)
|
if (response.payload?.variableValues) {
|
||||||
setOptionsData(
|
const newOptionsData = sortValues(
|
||||||
sortValues(response.payload?.variableValues, variableData.sort) as never,
|
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) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
@ -59,10 +119,12 @@ function VariableItem({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
variableData.customValue,
|
variableData,
|
||||||
variableData.queryValue,
|
existingVariables,
|
||||||
variableData.sort,
|
onValueUpdate,
|
||||||
variableData.type,
|
onAllSelectedUpdate,
|
||||||
|
optionsData,
|
||||||
|
lastUpdatedVar,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -72,7 +134,8 @@ function VariableItem({
|
|||||||
const handleChange = (value: string | string[]): void => {
|
const handleChange = (value: string | string[]): void => {
|
||||||
if (
|
if (
|
||||||
value === ALL_SELECT_VALUE ||
|
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);
|
onValueUpdate(variableData.name, optionsData);
|
||||||
onAllSelectedUpdate(variableData.name, true);
|
onAllSelectedUpdate(variableData.name, true);
|
||||||
@ -81,6 +144,15 @@ function VariableItem({
|
|||||||
onAllSelectedUpdate(variableData.name, false);
|
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 (
|
return (
|
||||||
<VariableContainer>
|
<VariableContainer>
|
||||||
<VariableName>${variableData.name}</VariableName>
|
<VariableName>${variableData.name}</VariableName>
|
||||||
@ -93,35 +165,29 @@ function VariableItem({
|
|||||||
handleChange(e.target.value || '');
|
handleChange(e.target.value || '');
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
width: 50 + ((variableData.selectedValue?.length || 0) * 7 || 50),
|
width:
|
||||||
|
50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
!errorMessage && (
|
||||||
<Select
|
<Select
|
||||||
value={variableData.allSelected ? 'ALL' : variableData.selectedValue}
|
value={selectValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
bordered={false}
|
bordered={false}
|
||||||
placeholder="Select value"
|
placeholder="Select value"
|
||||||
mode={
|
mode={mode}
|
||||||
(variableData.multiSelect && !variableData.allSelected
|
|
||||||
? 'multiple'
|
|
||||||
: null) as never
|
|
||||||
}
|
|
||||||
dropdownMatchSelectWidth={false}
|
dropdownMatchSelectWidth={false}
|
||||||
style={{
|
style={SelectItemStyle}
|
||||||
minWidth: 120,
|
|
||||||
fontSize: '0.8rem',
|
|
||||||
}}
|
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
showArrow
|
showArrow
|
||||||
>
|
>
|
||||||
{variableData.multiSelect && variableData.showALLOption && (
|
{enableSelectAll && <Option value={ALL_SELECT_VALUE}>ALL</Option>}
|
||||||
<Option value={ALL_SELECT_VALUE}>ALL</Option>
|
|
||||||
)}
|
|
||||||
{map(optionsData, (option) => (
|
{map(optionsData, (option) => (
|
||||||
<Option value={option}>{(option as string).toString()}</Option>
|
<Option value={option}>{option.toString()}</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<span style={{ margin: '0 0.5rem' }}>
|
<span style={{ margin: '0 0.5rem' }}>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Row } from 'antd';
|
import { Row } from 'antd';
|
||||||
import { map, sortBy } from 'lodash-es';
|
import { map, sortBy } from 'lodash-es';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { connect, useSelector } from 'react-redux';
|
import { connect, useSelector } from 'react-redux';
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
import { ThunkDispatch } from 'redux-thunk';
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
@ -23,13 +23,30 @@ function DashboardVariableSelection({
|
|||||||
data: { variables = {} },
|
data: { variables = {} },
|
||||||
} = selectedDashboard;
|
} = selectedDashboard;
|
||||||
|
|
||||||
|
const [update, setUpdate] = useState<boolean>(false);
|
||||||
|
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
|
||||||
|
|
||||||
|
const onVarChanged = (name: string): void => {
|
||||||
|
setLastUpdatedVar(name);
|
||||||
|
setUpdate(!update);
|
||||||
|
};
|
||||||
|
|
||||||
const onValueUpdate = (
|
const onValueUpdate = (
|
||||||
name: string,
|
name: string,
|
||||||
value: IDashboardVariable['selectedValue'],
|
value:
|
||||||
|
| string
|
||||||
|
| string[]
|
||||||
|
| number
|
||||||
|
| number[]
|
||||||
|
| boolean
|
||||||
|
| boolean[]
|
||||||
|
| null
|
||||||
|
| undefined,
|
||||||
): void => {
|
): void => {
|
||||||
const updatedVariablesData = { ...variables };
|
const updatedVariablesData = { ...variables };
|
||||||
updatedVariablesData[name].selectedValue = value;
|
updatedVariablesData[name].selectedValue = value;
|
||||||
updateDashboardVariables(updatedVariablesData);
|
updateDashboardVariables(updatedVariablesData);
|
||||||
|
onVarChanged(name);
|
||||||
};
|
};
|
||||||
const onAllSelectedUpdate = (
|
const onAllSelectedUpdate = (
|
||||||
name: string,
|
name: string,
|
||||||
@ -38,6 +55,7 @@ function DashboardVariableSelection({
|
|||||||
const updatedVariablesData = { ...variables };
|
const updatedVariablesData = { ...variables };
|
||||||
updatedVariablesData[name].allSelected = value;
|
updatedVariablesData[name].allSelected = value;
|
||||||
updateDashboardVariables(updatedVariablesData);
|
updateDashboardVariables(updatedVariablesData);
|
||||||
|
onVarChanged(name);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -45,9 +63,15 @@ function DashboardVariableSelection({
|
|||||||
{map(sortBy(Object.keys(variables)), (variableName) => (
|
{map(sortBy(Object.keys(variables)), (variableName) => (
|
||||||
<VariableItem
|
<VariableItem
|
||||||
key={`${variableName}${variables[variableName].modificationUUID}`}
|
key={`${variableName}${variables[variableName].modificationUUID}`}
|
||||||
variableData={{ name: variableName, ...variables[variableName] }}
|
existingVariables={variables}
|
||||||
|
variableData={{
|
||||||
|
name: variableName,
|
||||||
|
...variables[variableName],
|
||||||
|
change: update,
|
||||||
|
}}
|
||||||
onValueUpdate={onValueUpdate as never}
|
onValueUpdate={onValueUpdate as never}
|
||||||
onAllSelectedUpdate={onAllSelectedUpdate as never}
|
onAllSelectedUpdate={onAllSelectedUpdate as never}
|
||||||
|
lastUpdatedVar={lastUpdatedVar}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -17,3 +17,8 @@ export const VariableName = styled(Typography)`
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: ${grey[0]};
|
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;
|
sort: TSortVariableValuesType;
|
||||||
multiSelect: boolean;
|
multiSelect: boolean;
|
||||||
showALLOption: boolean;
|
showALLOption: boolean;
|
||||||
selectedValue?: null | string | string[];
|
selectedValue?:
|
||||||
|
| null
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| (string | number | boolean)[];
|
||||||
// Internal use
|
// Internal use
|
||||||
modificationUUID?: string;
|
modificationUUID?: string;
|
||||||
allSelected?: boolean;
|
allSelected?: boolean;
|
||||||
|
change?: boolean;
|
||||||
}
|
}
|
||||||
export interface Dashboard {
|
export interface Dashboard {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
|
export type PayloadVariables = Record<
|
||||||
|
string,
|
||||||
|
undefined | null | string | number | boolean | (string | number | boolean)[]
|
||||||
|
>;
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
query: string;
|
query: string;
|
||||||
|
variables: PayloadVariables;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PayloadProps = {
|
export type PayloadProps = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user