diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx index 5a3c48ad465..813185e0b6f 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx @@ -1,19 +1,9 @@ import { Row } from 'antd'; -import { isEmpty } from 'lodash-es'; +import { isNull } from 'lodash-es'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { memo, useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { AppState } from 'store/reducers'; import { IDashboardVariable } from 'types/api/dashboard/getAll'; -import { GlobalReducer } from 'types/reducer/globalTime'; - -import { - buildDependencies, - buildDependencyGraph, - buildParentDependencyGraph, - IDependencyData, - onUpdateVariableNode, -} from './util'; + import VariableItem from './VariableItem'; function DashboardVariableSelection(): JSX.Element | null { @@ -31,14 +21,6 @@ function DashboardVariableSelection(): JSX.Element | null { const [variablesTableData, setVariablesTableData] = useState([]); - const [dependencyData, setDependencyData] = useState( - null, - ); - - const { maxTime, minTime } = useSelector( - (state) => state.globalTime, - ); - useEffect(() => { if (variables) { const tableRowData = []; @@ -61,37 +43,35 @@ function DashboardVariableSelection(): JSX.Element | null { } }, [variables]); - useEffect(() => { - if (variablesTableData.length > 0) { - const depGrp = buildDependencies(variablesTableData); - const { order, graph } = buildDependencyGraph(depGrp); - const parentDependencyGraph = buildParentDependencyGraph(graph); - setDependencyData({ - order, - graph, - parentDependencyGraph, - }); - } - }, [setVariablesToGetUpdated, variables, variablesTableData]); - - // this handles the case where the dependency order changes i.e. variable list updated via creation or deletion etc. and we need to refetch the variables - // also trigger when the global time changes - useEffect( - () => { - if (!isEmpty(dependencyData?.order)) { - setVariablesToGetUpdated(dependencyData?.order || []); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [JSON.stringify(dependencyData?.order), minTime, maxTime], - ); + 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 onValueUpdate = ( name: string, id: string, value: IDashboardVariable['selectedValue'], allSelected: boolean, - // isMountedCall?: boolean, // eslint-disable-next-line sonarjs/cognitive-complexity ): void => { if (id) { @@ -131,20 +111,7 @@ function DashboardVariableSelection(): JSX.Element | null { }); } - if (dependencyData) { - const updatedVariables: string[] = []; - onUpdateVariableNode( - name, - dependencyData.graph, - dependencyData.order, - (node) => updatedVariables.push(node), - ); - setVariablesToGetUpdated((prev) => [ - ...new Set([...prev, ...updatedVariables.filter((v) => v !== name)]), - ]); - } else { - setVariablesToGetUpdated((prev) => prev.filter((v) => v !== name)); - } + onVarChanged(name); } }; @@ -172,7 +139,6 @@ function DashboardVariableSelection(): JSX.Element | null { onValueUpdate={onValueUpdate} variablesToGetUpdated={variablesToGetUpdated} setVariablesToGetUpdated={setVariablesToGetUpdated} - dependencyData={dependencyData} /> ))} diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx index 823cf539237..1cb89d6b958 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx @@ -49,11 +49,6 @@ describe('VariableItem', () => { onValueUpdate={mockOnValueUpdate} variablesToGetUpdated={[]} setVariablesToGetUpdated={(): void => {}} - dependencyData={{ - order: [], - graph: {}, - parentDependencyGraph: {}, - }} /> , ); @@ -70,11 +65,6 @@ describe('VariableItem', () => { onValueUpdate={mockOnValueUpdate} variablesToGetUpdated={[]} setVariablesToGetUpdated={(): void => {}} - dependencyData={{ - order: [], - graph: {}, - parentDependencyGraph: {}, - }} /> , ); @@ -90,11 +80,6 @@ describe('VariableItem', () => { onValueUpdate={mockOnValueUpdate} variablesToGetUpdated={[]} setVariablesToGetUpdated={(): void => {}} - dependencyData={{ - order: [], - graph: {}, - parentDependencyGraph: {}, - }} /> , ); @@ -124,11 +109,6 @@ describe('VariableItem', () => { onValueUpdate={mockOnValueUpdate} variablesToGetUpdated={[]} setVariablesToGetUpdated={(): void => {}} - dependencyData={{ - order: [], - graph: {}, - parentDependencyGraph: {}, - }} /> , ); @@ -153,11 +133,6 @@ describe('VariableItem', () => { onValueUpdate={mockOnValueUpdate} variablesToGetUpdated={[]} setVariablesToGetUpdated={(): void => {}} - dependencyData={{ - order: [], - graph: {}, - parentDependencyGraph: {}, - }} /> , ); @@ -174,11 +149,6 @@ describe('VariableItem', () => { onValueUpdate={mockOnValueUpdate} variablesToGetUpdated={[]} setVariablesToGetUpdated={(): void => {}} - dependencyData={{ - order: [], - graph: {}, - parentDependencyGraph: {}, - }} /> , ); diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx index 7e6c0506531..398ade82595 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx @@ -35,10 +35,12 @@ import { popupContainer } from 'utils/selectPopupContainer'; import { variablePropsToPayloadVariables } from '../utils'; import { SelectItemStyle } from './styles'; -import { areArraysEqual, checkAPIInvocation, IDependencyData } from './util'; +import { areArraysEqual } from './util'; const ALL_SELECT_VALUE = '__ALL__'; +const variableRegexPattern = /\{\{\s*?\.([^\s}]+)\s*?\}\}/g; + enum ToggleTagValue { Only = 'Only', All = 'All', @@ -55,7 +57,6 @@ interface VariableItemProps { ) => void; variablesToGetUpdated: string[]; setVariablesToGetUpdated: React.Dispatch>; - dependencyData: IDependencyData | null; } const getSelectValue = ( @@ -78,7 +79,6 @@ function VariableItem({ onValueUpdate, variablesToGetUpdated, setVariablesToGetUpdated, - dependencyData, }: VariableItemProps): JSX.Element { const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>( [], @@ -88,19 +88,59 @@ function VariableItem({ (state) => state.globalTime, ); - const validVariableUpdate = (): boolean => { - if (!variableData.name) { - return false; + 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]); - // variableData.name is present as the top element or next in the queue - variablesToGetUpdated - return Boolean( - variablesToGetUpdated.length && - variablesToGetUpdated[0] === variableData.name, - ); + const [errorMessage, setErrorMessage] = useState(null); + + const getDependentVariables = (queryValue: string): string[] => { + const matches = queryValue.match(variableRegexPattern); + + // Extract variable names from the matches array without {{ . }} + return matches + ? matches.map((match) => match.replace(variableRegexPattern, '$1')) + : []; }; - const [errorMessage, setErrorMessage] = useState(null); + 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}`, + ]; + }; // eslint-disable-next-line sonarjs/cognitive-complexity const getOptions = (variablesRes: VariableResponseProps | null): void => { @@ -144,7 +184,9 @@ function VariableItem({ if ( variableData.type === 'QUERY' && variableData.name && - (validVariableUpdate() || valueNotInList || variableData.allSelected) + (variablesToGetUpdated.includes(variableData.name) || + valueNotInList || + variableData.allSelected) ) { let value = variableData.selectedValue; let allSelected = false; @@ -182,64 +224,36 @@ function VariableItem({ } }; - const { isLoading } = useQuery( - [ - REACT_QUERY_KEY.DASHBOARD_BY_ID, - variableData.name || '', - `${minTime}`, - `${maxTime}`, - JSON.stringify(dependencyData?.order), - ], - { - 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); - setVariablesToGetUpdated((prev) => - prev.filter((v) => v !== variableData.name), - ); - }, - 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 { 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'; } - }, + 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 deleted file mode 100644 index 0add4c5cad0..00000000000 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/__test__/dashboardVariables.test.tsx +++ /dev/null @@ -1,241 +0,0 @@ -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, {}), - ).toBeFalsy(); - }); - }); - - 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 deleted file mode 100644 index c39841fcf49..00000000000 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/__test__/mock.ts +++ /dev/null @@ -1,251 +0,0 @@ -/* 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 03c6f2c5859..a3fe59ccd8f 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/util.ts +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/util.ts @@ -1,4 +1,3 @@ -import { isEmpty } from 'lodash-es'; import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; export function areArraysEqual( @@ -30,178 +29,3 @@ export const convertVariablesToDbFormat = ( result[id] = obj; return result; }, {}); - -const getDependentVariables = (queryValue: string): string[] => { - // Combined pattern for all formats: - // {{.variable_name}} - original format - // $variable_name - dollar prefix format - // [[variable_name]] - square bracket format - // {{variable_name}} - without dot format - const variableRegexPattern = /(?:\{\{\s*\.?([^\s}]+)\s*\}\}|\$([^\s\W]+)|\[\[([^\]]+)\]\])/g; - - const matches = queryValue.match(variableRegexPattern); - - // Extract variable names from the matches array, handling all formats - return matches - ? matches.map((match) => { - if (match.startsWith('$')) { - return match.slice(1); // Remove $ prefix - } - if (match.startsWith('[[')) { - return match.slice(2, -2); // Remove [[ and ]] - } - // Handle both {{.var}} and {{var}} formats - return match.replace(/\{\{\s*\.?([^\s}]+)\s*\}\}/, '$1'); - }) - : []; -}; -export type VariableGraph = Record; - -export const buildDependencies = ( - variables: IDashboardVariable[], -): VariableGraph => { - const graph: VariableGraph = {}; - - // Initialize empty arrays for all variables first - variables.forEach((variable) => { - if (variable.name && variable.type === 'QUERY') { - 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) { - console.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 false; - } - - // 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 - ); -}; - -export interface IDependencyData { - order: string[]; - graph: VariableGraph; - parentDependencyGraph: VariableGraph; -} diff --git a/frontend/src/container/TraceDetail/index.tsx b/frontend/src/container/TraceDetail/index.tsx index a44fd917914..fddfeaa3c59 100644 --- a/frontend/src/container/TraceDetail/index.tsx +++ b/frontend/src/container/TraceDetail/index.tsx @@ -1,7 +1,7 @@ import './TraceDetails.styles.scss'; import { FilterOutlined } from '@ant-design/icons'; -import { Button, Col, Layout, Typography } from 'antd'; +import { Button, Col, Empty, Input, Layout, Typography } from 'antd'; import cx from 'classnames'; import { StyledCol, @@ -12,12 +12,14 @@ import { StyledTypography, } from 'components/Styled'; import { Flex, Spacing } from 'components/Styled/styles'; +import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig'; import GanttChart, { ITraceMetaData } from 'container/GantChart'; import { getNodeById } from 'container/GantChart/utils'; import Timeline from 'container/Timeline'; import TraceFlameGraph from 'container/TraceFlameGraph'; import dayjs from 'dayjs'; import { useIsDarkMode } from 'hooks/useDarkMode'; +import useDebouncedFn from 'hooks/useDebouncedFunction'; import useUrlQuery from 'hooks/useUrlQuery'; import { spanServiceNameToColorMapping } from 'lib/getRandomColor'; import history from 'lib/history'; @@ -26,7 +28,11 @@ import { PanelRight } from 'lucide-react'; import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants'; import { useTimezone } from 'providers/Timezone'; import { useEffect, useMemo, useState } from 'react'; -import { ITraceForest, PayloadProps } from 'types/api/trace/getTraceItem'; +import { + ITraceForest, + ITraceTree, + PayloadProps, +} from 'types/api/trace/getTraceItem'; import { getSpanTreeMetadata } from 'utils/getSpanTreeMetadata'; import { spanToTreeUtil } from 'utils/spanToTree'; @@ -36,7 +42,9 @@ import * as styles from './styles'; import { FlameGraphMissingSpansContainer, GanttChartWrapper } from './styles'; import SubTreeMessage from './SubTree'; import { + DEFAULT_FILTER_KEYS, formUrlParams, + getFilteredData, getSortedData, getTreeLevelsCount, IIntervalUnit, @@ -59,6 +67,7 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element { const urlQuery = useUrlQuery(); const [spanId] = useState(urlQuery.get('spanId')); + const [searchText, setSearchText] = useState(''); const [intervalUnit, setIntervalUnit] = useState( INTERVAL_UNITS[0], @@ -78,10 +87,19 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element { ); const { treesData: tree, ...traceMetaData } = useMemo(() => { + const filteredTreesData: ITraceForest = { + spanTree: map(treesData.spanTree, (tree) => + getFilteredData(tree, searchText, DEFAULT_FILTER_KEYS), + ).filter(Boolean) as ITraceTree[], + missingSpanTree: map(treesData.missingSpanTree, (tree) => + getFilteredData(tree, searchText, DEFAULT_FILTER_KEYS), + ).filter(Boolean) as ITraceTree[], + }; + const sortedTreesData: ITraceForest = { - spanTree: map(treesData.spanTree, (tree) => getSortedData(tree)), + spanTree: map(filteredTreesData.spanTree, (tree) => getSortedData(tree)), missingSpanTree: map( - treesData.missingSpanTree, + filteredTreesData.missingSpanTree, (tree) => getSortedData(tree) || [], ), }; @@ -89,7 +107,7 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element { /*eslint-disable */ return getSpanTreeMetadata(sortedTreesData, spanServiceColors); /* eslint-enable */ - }, [treesData, spanServiceColors]); + }, [treesData, spanServiceColors, searchText]); const firstSpanStartTime = tree.spanTree[0]?.startTime; @@ -127,6 +145,10 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element { setTreesData(spanToTreeUtil(response[0].events)); }; + const setSearchTextDebounced = useDebouncedFn((...args) => { + setSearchText(args[0] as string); + }, DEBOUNCE_DELAY); + const hasMissingSpans = useMemo( (): boolean => tree.missingSpanTree && @@ -225,6 +247,11 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element { + setSearchTextDebounced(e.target.value)} + />