diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx index 813185e0b6..68b5759db3 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx @@ -1,9 +1,15 @@ import { Row } from 'antd'; -import { isNull } from 'lodash-es'; import { useDashboard } from 'providers/Dashboard/Dashboard'; -import { memo, useEffect, useState } from 'react'; +import { memo, useEffect, useRef, useState } from 'react'; import { IDashboardVariable } from 'types/api/dashboard/getAll'; +import { + buildDependencies, + buildDependencyGraph, + buildParentDependencyGraph, + onUpdateVariableNode, + VariableGraph, +} from './util'; import VariableItem from './VariableItem'; function DashboardVariableSelection(): JSX.Element | null { @@ -21,6 +27,12 @@ function DashboardVariableSelection(): JSX.Element | null { const [variablesTableData, setVariablesTableData] = useState([]); + const [dependencyData, setDependencyData] = useState<{ + order: string[]; + graph: VariableGraph; + parentDependencyGraph: VariableGraph; + } | null>(null); + useEffect(() => { if (variables) { const tableRowData = []; @@ -43,35 +55,28 @@ function DashboardVariableSelection(): JSX.Element | null { } }, [variables]); - const onVarChanged = (name: string): void => { - /** - * this function takes care of adding the dependent variables to current update queue and removing - * the updated variable name from the queue - */ - const dependentVariables = variablesTableData - ?.map((variable: any) => { - if (variable.type === 'QUERY') { - const re = new RegExp(`\\{\\{\\s*?\\.${name}\\s*?\\}\\}`); // regex for `{{.var}}` - const queryValue = variable.queryValue || ''; - const dependVarReMatch = queryValue.match(re); - if (dependVarReMatch !== null && dependVarReMatch.length > 0) { - return variable.name; - } - } - return null; - }) - .filter((val: string | null) => !isNull(val)); - setVariablesToGetUpdated((prev) => [ - ...prev.filter((v) => v !== name), - ...dependentVariables, - ]); - }; + const initializationRef = useRef(false); + + useEffect(() => { + if (variablesTableData.length > 0 && !initializationRef.current) { + const depGrp = buildDependencies(variablesTableData); + const { order, graph } = buildDependencyGraph(depGrp); + const parentDependencyGraph = buildParentDependencyGraph(graph); + setDependencyData({ + order, + graph, + parentDependencyGraph, + }); + initializationRef.current = true; + } + }, [variablesTableData]); const onValueUpdate = ( name: string, id: string, value: IDashboardVariable['selectedValue'], allSelected: boolean, + isMountedCall?: boolean, // eslint-disable-next-line sonarjs/cognitive-complexity ): void => { if (id) { @@ -111,7 +116,18 @@ function DashboardVariableSelection(): JSX.Element | null { }); } - onVarChanged(name); + if (dependencyData && !isMountedCall) { + const updatedVariables: string[] = []; + onUpdateVariableNode( + name, + dependencyData.graph, + dependencyData.order, + (node) => updatedVariables.push(node), + ); + setVariablesToGetUpdated(updatedVariables.filter((v) => v !== name)); + } else if (isMountedCall) { + setVariablesToGetUpdated((prev) => prev.filter((v) => v !== name)); + } } }; @@ -139,6 +155,7 @@ function DashboardVariableSelection(): JSX.Element | null { onValueUpdate={onValueUpdate} variablesToGetUpdated={variablesToGetUpdated} setVariablesToGetUpdated={setVariablesToGetUpdated} + dependencyData={dependencyData} /> ))} diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx index 398ade8259..abc3cfc2f0 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx @@ -24,7 +24,7 @@ import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParse import sortValues from 'lib/dashbaordVariables/sortVariableValues'; import { debounce, isArray, isString } from 'lodash-es'; import map from 'lodash-es/map'; -import { ChangeEvent, memo, useEffect, useMemo, useState } from 'react'; +import { ChangeEvent, memo, useEffect, useMemo, useRef, useState } from 'react'; import { useQuery } from 'react-query'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; @@ -35,12 +35,15 @@ import { popupContainer } from 'utils/selectPopupContainer'; import { variablePropsToPayloadVariables } from '../utils'; import { SelectItemStyle } from './styles'; -import { areArraysEqual } from './util'; +import { + areArraysEqual, + checkAPIInvocation, + onUpdateVariableNode, + VariableGraph, +} from './util'; const ALL_SELECT_VALUE = '__ALL__'; -const variableRegexPattern = /\{\{\s*?\.([^\s}]+)\s*?\}\}/g; - enum ToggleTagValue { Only = 'Only', All = 'All', @@ -54,9 +57,15 @@ interface VariableItemProps { id: string, arg1: IDashboardVariable['selectedValue'], allSelected: boolean, + isMountedCall?: boolean, ) => void; variablesToGetUpdated: string[]; setVariablesToGetUpdated: React.Dispatch>; + dependencyData: { + order: string[]; + graph: VariableGraph; + parentDependencyGraph: VariableGraph; + } | null; } const getSelectValue = ( @@ -79,6 +88,7 @@ function VariableItem({ onValueUpdate, variablesToGetUpdated, setVariablesToGetUpdated, + dependencyData, }: VariableItemProps): JSX.Element { const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>( [], @@ -88,59 +98,28 @@ function VariableItem({ (state) => state.globalTime, ); - useEffect(() => { - if (variableData.allSelected && variableData.type === 'QUERY') { - setVariablesToGetUpdated((prev) => { - const variablesQueue = [...prev.filter((v) => v !== variableData.name)]; - if (variableData.name) { - variablesQueue.push(variableData.name); - } - return variablesQueue; - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [minTime, maxTime]); - - const [errorMessage, setErrorMessage] = useState(null); + // logic to detect if its a rerender or a new render/mount + const isMounted = useRef(false); - const getDependentVariables = (queryValue: string): string[] => { - const matches = queryValue.match(variableRegexPattern); + useEffect(() => { + isMounted.current = true; + }, []); - // Extract variable names from the matches array without {{ . }} - return matches - ? matches.map((match) => match.replace(variableRegexPattern, '$1')) - : []; + const validVariableUpdate = (): boolean => { + if (!variableData.name) { + return false; + } + if (!isMounted.current) { + // variableData.name is present as the top element or next in the queue - variablesToGetUpdated + return Boolean( + variablesToGetUpdated.length && + variablesToGetUpdated[0] === variableData.name, + ); + } + return variablesToGetUpdated.includes(variableData.name); }; - const getQueryKey = (variableData: IDashboardVariable): string[] => { - let dependentVariablesStr = ''; - - const dependentVariables = getDependentVariables( - variableData.queryValue || '', - ); - - const variableName = variableData.name || ''; - - dependentVariables?.forEach((element) => { - const [, variable] = - Object.entries(existingVariables).find( - ([, value]) => value.name === element, - ) || []; - - dependentVariablesStr += `${element}${variable?.selectedValue}`; - }); - - const variableKey = dependentVariablesStr.replace(/\s/g, ''); - - // added this time dependency for variables query as API respects the passed time range now - return [ - REACT_QUERY_KEY.DASHBOARD_BY_ID, - variableName, - variableKey, - `${minTime}`, - `${maxTime}`, - ]; - }; + const [errorMessage, setErrorMessage] = useState(null); // eslint-disable-next-line sonarjs/cognitive-complexity const getOptions = (variablesRes: VariableResponseProps | null): void => { @@ -184,9 +163,7 @@ function VariableItem({ if ( variableData.type === 'QUERY' && variableData.name && - (variablesToGetUpdated.includes(variableData.name) || - valueNotInList || - variableData.allSelected) + (validVariableUpdate() || valueNotInList || variableData.allSelected) ) { let value = variableData.selectedValue; let allSelected = false; @@ -200,7 +177,16 @@ function VariableItem({ } if (variableData && variableData?.name && variableData?.id) { - onValueUpdate(variableData.name, variableData.id, value, allSelected); + onValueUpdate( + variableData.name, + variableData.id, + value, + allSelected, + isMounted.current, + ); + setVariablesToGetUpdated((prev) => + prev.filter((name) => name !== variableData.name), + ); } } @@ -224,36 +210,75 @@ function VariableItem({ } }; - const { isLoading } = useQuery(getQueryKey(variableData), { - enabled: variableData && variableData.type === 'QUERY', - queryFn: () => - dashboardVariablesQuery({ - query: variableData.queryValue || '', - variables: variablePropsToPayloadVariables(existingVariables), - }), - refetchOnWindowFocus: false, - onSuccess: (response) => { - getOptions(response.payload); - }, - onError: (error: { - details: { - error: string; - }; - }) => { - const { details } = error; - - if (details.error) { - let message = details.error; - if (details.error.includes('Syntax error:')) { - message = - 'Please make sure query is valid and dependent variables are selected'; + const { isLoading } = useQuery( + [ + REACT_QUERY_KEY.DASHBOARD_BY_ID, + variableData.name || '', + `${minTime}`, + `${maxTime}`, + ], + { + enabled: + variableData && + variableData.type === 'QUERY' && + checkAPIInvocation( + variablesToGetUpdated, + variableData, + dependencyData?.parentDependencyGraph, + ), + queryFn: () => + dashboardVariablesQuery({ + query: variableData.queryValue || '', + variables: variablePropsToPayloadVariables(existingVariables), + }), + refetchOnWindowFocus: false, + onSuccess: (response) => { + getOptions(response.payload); + if ( + dependencyData?.parentDependencyGraph[variableData.name || ''].length === 0 + ) { + const updatedVariables: string[] = []; + onUpdateVariableNode( + variableData.name || '', + dependencyData.graph, + dependencyData.order, + (node) => updatedVariables.push(node), + ); + setVariablesToGetUpdated((prev) => [ + ...prev, + ...updatedVariables.filter((v) => v !== variableData.name), + ]); } - setErrorMessage(message); - } + }, + onError: (error: { + details: { + error: string; + }; + }) => { + const { details } = error; + + if (details.error) { + let message = details.error; + if (details.error.includes('Syntax error:')) { + message = + 'Please make sure query is valid and dependent variables are selected'; + } + setErrorMessage(message); + } + }, }, - }); + ); const handleChange = (value: string | string[]): void => { + // if value is equal to selected value then return + if ( + value === variableData.selectedValue || + (Array.isArray(value) && + Array.isArray(variableData.selectedValue) && + areArraysEqual(value, variableData.selectedValue)) + ) { + return; + } if (variableData.name) { if ( value === ALL_SELECT_VALUE || diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/__test__/dashboardVariables.test.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/__test__/dashboardVariables.test.tsx new file mode 100644 index 0000000000..9a7982b3ce --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/__test__/dashboardVariables.test.tsx @@ -0,0 +1,241 @@ +import { + buildDependencies, + buildDependencyGraph, + buildParentDependencyGraph, + checkAPIInvocation, + onUpdateVariableNode, + VariableGraph, +} from '../util'; +import { + buildDependenciesMock, + buildGraphMock, + checkAPIInvocationMock, + onUpdateVariableNodeMock, +} from './mock'; + +describe('dashboardVariables - utilities and processors', () => { + describe('onUpdateVariableNode', () => { + const { graph, topologicalOrder } = onUpdateVariableNodeMock; + const testCases = [ + { + scenario: 'root element', + nodeToUpdate: 'deployment_environment', + expected: [ + 'deployment_environment', + 'service_name', + 'endpoint', + 'http_status_code', + ], + }, + { + scenario: 'middle child', + nodeToUpdate: 'k8s_node_name', + expected: ['k8s_node_name', 'k8s_namespace_name'], + }, + { + scenario: 'leaf element', + nodeToUpdate: 'http_status_code', + expected: ['http_status_code'], + }, + { + scenario: 'node not in graph', + nodeToUpdate: 'unknown', + expected: [], + }, + { + scenario: 'node not in topological order', + nodeToUpdate: 'unknown', + expected: [], + }, + ]; + + test.each(testCases)( + 'should update variable node when $scenario', + ({ nodeToUpdate, expected }) => { + const updatedVariables: string[] = []; + const callback = (node: string): void => { + updatedVariables.push(node); + }; + + onUpdateVariableNode(nodeToUpdate, graph, topologicalOrder, callback); + + expect(updatedVariables).toEqual(expected); + }, + ); + + it('should return empty array when topological order is empty', () => { + const updatedVariables: string[] = []; + onUpdateVariableNode('http_status_code', graph, [], (node) => + updatedVariables.push(node), + ); + expect(updatedVariables).toEqual([]); + }); + }); + + describe('checkAPIInvocation', () => { + const { + variablesToGetUpdated, + variableData, + parentDependencyGraph, + } = checkAPIInvocationMock; + + const mockRootElement = { + name: 'deployment_environment', + key: '036a47cd-9ffc-47de-9f27-0329198964a8', + id: '036a47cd-9ffc-47de-9f27-0329198964a8', + modificationUUID: '5f71b591-f583-497c-839d-6a1590c3f60f', + selectedValue: 'production', + type: 'QUERY', + // ... other properties omitted for brevity + } as any; + + describe('edge cases', () => { + it('should return false when variableData is empty', () => { + expect( + checkAPIInvocation( + variablesToGetUpdated, + variableData, + parentDependencyGraph, + ), + ).toBeFalsy(); + }); + + it('should return true when parentDependencyGraph is empty', () => { + expect( + checkAPIInvocation(variablesToGetUpdated, variableData, {}), + ).toBeTruthy(); + }); + }); + + describe('variable sequences', () => { + it('should return true for valid sequence', () => { + expect( + checkAPIInvocation( + ['k8s_node_name', 'k8s_namespace_name'], + variableData, + parentDependencyGraph, + ), + ).toBeTruthy(); + }); + + it('should return false for invalid sequence', () => { + expect( + checkAPIInvocation( + ['k8s_cluster_name', 'k8s_node_name', 'k8s_namespace_name'], + variableData, + parentDependencyGraph, + ), + ).toBeFalsy(); + }); + + it('should return false when variableData is not in sequence', () => { + expect( + checkAPIInvocation( + ['deployment_environment', 'service_name', 'endpoint'], + variableData, + parentDependencyGraph, + ), + ).toBeFalsy(); + }); + }); + + describe('root element behavior', () => { + it('should return true for valid root element sequence', () => { + expect( + checkAPIInvocation( + [ + 'deployment_environment', + 'service_name', + 'endpoint', + 'http_status_code', + ], + mockRootElement, + parentDependencyGraph, + ), + ).toBeTruthy(); + }); + + it('should return true for empty variablesToGetUpdated array', () => { + expect( + checkAPIInvocation([], mockRootElement, parentDependencyGraph), + ).toBeTruthy(); + }); + }); + }); + + describe('Graph Building Utilities', () => { + const { graph } = buildGraphMock; + const { variables } = buildDependenciesMock; + + describe('buildParentDependencyGraph', () => { + it('should build parent dependency graph with correct relationships', () => { + const expected = { + deployment_environment: [], + service_name: ['deployment_environment'], + endpoint: ['deployment_environment', 'service_name'], + http_status_code: ['endpoint'], + k8s_cluster_name: [], + k8s_node_name: ['k8s_cluster_name'], + k8s_namespace_name: ['k8s_cluster_name', 'k8s_node_name'], + environment: [], + }; + + expect(buildParentDependencyGraph(graph)).toEqual(expected); + }); + + it('should handle empty graph', () => { + expect(buildParentDependencyGraph({})).toEqual({}); + }); + }); + + describe('buildDependencyGraph', () => { + it('should build complete dependency graph with correct structure and order', () => { + const expected = { + graph: { + deployment_environment: ['service_name', 'endpoint'], + service_name: ['endpoint'], + endpoint: ['http_status_code'], + http_status_code: [], + k8s_cluster_name: ['k8s_node_name', 'k8s_namespace_name'], + k8s_node_name: ['k8s_namespace_name'], + k8s_namespace_name: [], + environment: [], + }, + order: [ + 'deployment_environment', + 'k8s_cluster_name', + 'environment', + 'service_name', + 'k8s_node_name', + 'endpoint', + 'k8s_namespace_name', + 'http_status_code', + ], + }; + + expect(buildDependencyGraph(graph)).toEqual(expected); + }); + }); + + describe('buildDependencies', () => { + it('should build dependency map from variables array', () => { + const expected: VariableGraph = { + deployment_environment: ['service_name', 'endpoint'], + service_name: ['endpoint'], + endpoint: ['http_status_code'], + http_status_code: [], + k8s_cluster_name: ['k8s_node_name', 'k8s_namespace_name'], + k8s_node_name: ['k8s_namespace_name'], + k8s_namespace_name: [], + environment: [], + }; + + expect(buildDependencies(variables)).toEqual(expected); + }); + + it('should handle empty variables array', () => { + expect(buildDependencies([])).toEqual({}); + }); + }); + }); +}); diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/__test__/mock.ts b/frontend/src/container/NewDashboard/DashboardVariablesSelection/__test__/mock.ts new file mode 100644 index 0000000000..c39841fcf4 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/__test__/mock.ts @@ -0,0 +1,251 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +export const checkAPIInvocationMock = { + variablesToGetUpdated: [], + variableData: { + name: 'k8s_node_name', + key: '4d71d385-beaf-4434-8dbf-c62be68049fc', + allSelected: false, + customValue: '', + description: '', + id: '4d71d385-beaf-4434-8dbf-c62be68049fc', + modificationUUID: '77233d3c-96d7-4ccb-aa9d-11b04d563068', + multiSelect: false, + order: 6, + queryValue: + "SELECT JSONExtractString(labels, 'k8s_node_name') AS k8s_node_name\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'k8s_node_cpu_time' AND JSONExtractString(labels, 'k8s_cluster_name') = {{.k8s_cluster_name}}\nGROUP BY k8s_node_name", + selectedValue: 'gke-signoz-saas-si-consumer-bsc-e2sd4-a6d430fa-gvm2', + showALLOption: false, + sort: 'DISABLED', + textboxValue: '', + type: 'QUERY', + }, + parentDependencyGraph: { + deployment_environment: [], + service_name: ['deployment_environment'], + endpoint: ['deployment_environment', 'service_name'], + http_status_code: ['endpoint'], + k8s_cluster_name: [], + environment: [], + k8s_node_name: ['k8s_cluster_name'], + k8s_namespace_name: ['k8s_cluster_name', 'k8s_node_name'], + }, +} as any; + +export const onUpdateVariableNodeMock = { + nodeToUpdate: 'deployment_environment', + graph: { + deployment_environment: ['service_name', 'endpoint'], + service_name: ['endpoint'], + endpoint: ['http_status_code'], + http_status_code: [], + k8s_cluster_name: ['k8s_node_name', 'k8s_namespace_name'], + environment: [], + k8s_node_name: ['k8s_namespace_name'], + k8s_namespace_name: [], + }, + topologicalOrder: [ + 'deployment_environment', + 'k8s_cluster_name', + 'environment', + 'service_name', + 'k8s_node_name', + 'endpoint', + 'k8s_namespace_name', + 'http_status_code', + ], + callback: jest.fn(), +}; + +export const buildGraphMock = { + graph: { + deployment_environment: ['service_name', 'endpoint'], + service_name: ['endpoint'], + endpoint: ['http_status_code'], + http_status_code: [], + k8s_cluster_name: ['k8s_node_name', 'k8s_namespace_name'], + environment: [], + k8s_node_name: ['k8s_namespace_name'], + k8s_namespace_name: [], + }, +}; + +export const buildDependenciesMock = { + variables: [ + { + key: '036a47cd-9ffc-47de-9f27-0329198964a8', + name: 'deployment_environment', + allSelected: false, + customValue: '', + description: '', + id: '036a47cd-9ffc-47de-9f27-0329198964a8', + modificationUUID: '5f71b591-f583-497c-839d-6a1590c3f60f', + multiSelect: false, + order: 0, + queryValue: + "SELECT DISTINCT JSONExtractString(labels, 'deployment_environment') AS deployment_environment\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'signoz_calls_total'", + selectedValue: 'production', + showALLOption: false, + sort: 'DISABLED', + textboxValue: '', + type: 'QUERY', + }, + { + key: 'eed5c917-1860-4c7e-bf6d-a05b97bafbc9', + name: 'service_name', + allSelected: true, + customValue: '', + description: '', + id: 'eed5c917-1860-4c7e-bf6d-a05b97bafbc9', + modificationUUID: '85db928b-ac9b-4e9f-b274-791112102fdf', + multiSelect: true, + order: 1, + queryValue: + "SELECT DISTINCT JSONExtractString(labels, 'service_name') FROM signoz_metrics.distributed_time_series_v4_1day\n WHERE metric_name = 'signoz_calls_total' and JSONExtractString(labels, 'deployment_environment') = {{.deployment_environment}}", + selectedValue: ['otelgateway'], + showALLOption: true, + sort: 'ASC', + textboxValue: '', + type: 'QUERY', + }, + { + key: '4022d3c1-e845-4952-8984-78f25f575c7a', + name: 'endpoint', + allSelected: true, + customValue: '', + description: '', + id: '4022d3c1-e845-4952-8984-78f25f575c7a', + modificationUUID: 'c0107fa1-ebb7-4dd3-aa9d-6ba08ecc594d', + multiSelect: true, + order: 2, + queryValue: + "SELECT DISTINCT JSONExtractString(labels, 'operation') FROM signoz_metrics.distributed_time_series_v4_1day\n WHERE metric_name = 'signoz_calls_total' AND JSONExtractString(labels, 'service_name') IN {{.service_name}} and JSONExtractString(labels, 'deployment_environment') = {{.deployment_environment}}", + selectedValue: [ + '//v1/traces', + '/logs/heroku', + '/logs/json', + '/logs/vector', + '/v1/logs', + '/v1/metrics', + '/v1/traces', + 'SELECT', + 'exporter/signozkafka/logs', + 'exporter/signozkafka/metrics', + 'exporter/signozkafka/traces', + 'extension/signozkeyauth/Authenticate', + 'get', + 'hmget', + 'opentelemetry.proto.collector.logs.v1.LogsService/Export', + 'opentelemetry.proto.collector.metrics.v1.MetricsService/Export', + 'opentelemetry.proto.collector.trace.v1.TraceService/Export', + 'processor/signozlimiter/LogsProcessed', + 'processor/signozlimiter/MetricsProcessed', + 'processor/signozlimiter/TracesProcessed', + 'receiver/otlp/LogsReceived', + 'receiver/otlp/MetricsReceived', + 'receiver/otlp/TraceDataReceived', + 'receiver/signozhttplog/heroku/LogsReceived', + 'receiver/signozhttplog/json/LogsReceived', + 'receiver/signozhttplog/vector/LogsReceived', + 'redis.dial', + 'redis.pipeline eval', + 'sadd', + 'set', + 'sismember', + ], + showALLOption: true, + sort: 'ASC', + textboxValue: '', + type: 'QUERY', + }, + { + key: '5e8a3cd9-3cd9-42df-a76c-79471a0f75bd', + name: 'http_status_code', + customValue: '', + description: '', + id: '5e8a3cd9-3cd9-42df-a76c-79471a0f75bd', + modificationUUID: '9a4021cc-a80a-4f15-8899-78892b763ca7', + multiSelect: true, + order: 3, + queryValue: + "SELECT DISTINCT JSONExtractString(labels, 'http_status_code') FROM signoz_metrics.distributed_time_series_v4_1day\n WHERE metric_name = 'signoz_calls_total' AND JSONExtractString(labels, 'operation') IN {{.endpoint}}", + showALLOption: true, + sort: 'ASC', + textboxValue: '', + type: 'QUERY', + selectedValue: ['', '200', '301', '400', '401', '405', '415', '429'], + allSelected: true, + }, + { + key: '48e9aa64-05ca-41c2-a1bd-6c8aeca659f1', + name: 'k8s_cluster_name', + allSelected: false, + customValue: 'test-1,\ntest-2,\ntest-3', + description: '', + id: '48e9aa64-05ca-41c2-a1bd-6c8aeca659f1', + modificationUUID: '44722322-368c-4613-bb7f-d0b12867d57a', + multiSelect: false, + order: 4, + queryValue: + "SELECT JSONExtractString(labels, 'k8s_cluster_name') AS k8s_cluster_name\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'k8s_node_cpu_time'\nGROUP BY k8s_cluster_name", + selectedValue: 'saasmonitor-cluster', + showALLOption: false, + sort: 'DISABLED', + textboxValue: '', + type: 'QUERY', + }, + { + key: '3ea18ba2-30cf-4220-b03b-720b5eaf35f8', + name: 'environment', + allSelected: false, + customValue: '', + description: '', + id: '3ea18ba2-30cf-4220-b03b-720b5eaf35f8', + modificationUUID: '9f76cb06-1b9f-460f-a174-0b210bb3cf93', + multiSelect: false, + order: 5, + queryValue: + "SELECT DISTINCT JSONExtractString(labels, 'deployment_environment') AS environment\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'signoz_calls_total'", + selectedValue: 'production', + showALLOption: false, + sort: 'DISABLED', + textboxValue: '', + type: 'QUERY', + }, + { + key: '4d71d385-beaf-4434-8dbf-c62be68049fc', + name: 'k8s_node_name', + allSelected: false, + customValue: '', + description: '', + id: '4d71d385-beaf-4434-8dbf-c62be68049fc', + modificationUUID: '77233d3c-96d7-4ccb-aa9d-11b04d563068', + multiSelect: false, + order: 6, + queryValue: + "SELECT JSONExtractString(labels, 'k8s_node_name') AS k8s_node_name\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'k8s_node_cpu_time' AND JSONExtractString(labels, 'k8s_cluster_name') = {{.k8s_cluster_name}}\nGROUP BY k8s_node_name", + selectedValue: 'gke-signoz-saas-si-consumer-bsc-e2sd4-a6d430fa-gvm2', + showALLOption: false, + sort: 'DISABLED', + textboxValue: '', + type: 'QUERY', + }, + { + key: '937ecbae-b24b-4d6d-8cc4-5d5b8d53569b', + name: 'k8s_namespace_name', + customValue: '', + description: '', + id: '937ecbae-b24b-4d6d-8cc4-5d5b8d53569b', + modificationUUID: '8ad2442d-8b4d-4c64-848e-af847d1d0eec', + multiSelect: false, + order: 7, + queryValue: + "SELECT JSONExtractString(labels, 'k8s_namespace_name') AS k8s_namespace_name\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'k8s_pod_cpu_time' AND JSONExtractString(labels, 'k8s_cluster_name') = {{.k8s_cluster_name}} AND JSONExtractString(labels, 'k8s_node_name') IN {{.k8s_node_name}}\nGROUP BY k8s_namespace_name", + showALLOption: false, + sort: 'DISABLED', + textboxValue: '', + type: 'QUERY', + selectedValue: 'saasmonitor', + allSelected: false, + }, + ] as any, +}; diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/util.ts b/frontend/src/container/NewDashboard/DashboardVariablesSelection/util.ts index a3fe59ccd8..af44d339d6 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/util.ts +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/util.ts @@ -1,3 +1,4 @@ +import { isEmpty } from 'lodash-es'; import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; export function areArraysEqual( @@ -29,3 +30,159 @@ export const convertVariablesToDbFormat = ( result[id] = obj; return result; }, {}); + +const getDependentVariables = (queryValue: string): string[] => { + const variableRegexPattern = /\{\{\s*?\.([^\s}]+)\s*?\}\}/g; + + const matches = queryValue.match(variableRegexPattern); + + // Extract variable names from the matches array without {{ . }} + return matches + ? matches.map((match) => match.replace(variableRegexPattern, '$1')) + : []; +}; +export type VariableGraph = Record; + +export const buildDependencies = ( + variables: IDashboardVariable[], +): VariableGraph => { + console.log('buildDependencies', variables); + const graph: VariableGraph = {}; + + // Initialize empty arrays for all variables first + variables.forEach((variable) => { + if (variable.name) { + graph[variable.name] = []; + } + }); + + // For each QUERY variable, add it as a dependent to its referenced variables + variables.forEach((variable) => { + if (variable.type === 'QUERY' && variable.name) { + const dependentVariables = getDependentVariables(variable.queryValue || ''); + + // For each referenced variable, add the current query as a dependent + dependentVariables.forEach((referencedVar) => { + if (graph[referencedVar]) { + graph[referencedVar].push(variable.name as string); + } else { + graph[referencedVar] = [variable.name as string]; + } + }); + } + }); + + return graph; +}; + +// Function to build the dependency graph +export const buildDependencyGraph = ( + dependencies: VariableGraph, +): { order: string[]; graph: VariableGraph } => { + const inDegree: Record = {}; + const adjList: VariableGraph = {}; + + // Initialize in-degree and adjacency list + Object.keys(dependencies).forEach((node) => { + if (!inDegree[node]) inDegree[node] = 0; + if (!adjList[node]) adjList[node] = []; + dependencies[node].forEach((child) => { + if (!inDegree[child]) inDegree[child] = 0; + inDegree[child]++; + adjList[node].push(child); + }); + }); + + // Topological sort using Kahn's Algorithm + const queue: string[] = Object.keys(inDegree).filter( + (node) => inDegree[node] === 0, + ); + const topologicalOrder: string[] = []; + + while (queue.length > 0) { + const current = queue.shift(); + if (current === undefined) { + break; + } + topologicalOrder.push(current); + + adjList[current].forEach((neighbor) => { + inDegree[neighbor]--; + if (inDegree[neighbor] === 0) queue.push(neighbor); + }); + } + + if (topologicalOrder.length !== Object.keys(dependencies).length) { + throw new Error('Cycle detected in the dependency graph!'); + } + + return { order: topologicalOrder, graph: adjList }; +}; + +export const onUpdateVariableNode = ( + nodeToUpdate: string, + graph: VariableGraph, + topologicalOrder: string[], + callback: (node: string) => void, +): void => { + const visited = new Set(); + + // Start processing from the node to update + topologicalOrder.forEach((node) => { + if (node === nodeToUpdate || visited.has(node)) { + visited.add(node); + callback(node); + (graph[node] || []).forEach((child) => { + visited.add(child); + }); + } + }); +}; + +export const buildParentDependencyGraph = ( + graph: VariableGraph, +): VariableGraph => { + const parentGraph: VariableGraph = {}; + + // Initialize empty arrays for all nodes + Object.keys(graph).forEach((node) => { + parentGraph[node] = []; + }); + + // For each node and its children in the original graph + Object.entries(graph).forEach(([node, children]) => { + // For each child, add the current node as its parent + children.forEach((child) => { + parentGraph[child].push(node); + }); + }); + + return parentGraph; +}; + +export const checkAPIInvocation = ( + variablesToGetUpdated: string[], + variableData: IDashboardVariable, + parentDependencyGraph?: VariableGraph, +): boolean => { + if (isEmpty(variableData.name)) { + return false; + } + + if (isEmpty(parentDependencyGraph)) { + return true; + } + + // if no dependency then true + const haveDependency = + parentDependencyGraph?.[variableData.name || '']?.length > 0; + if (!haveDependency) { + return true; + } + + // if variable is in the list and has dependency then check if its the top element in the queue then true else false + return ( + variablesToGetUpdated.length > 0 && + variablesToGetUpdated[0] === variableData.name + ); +};