Compare commits

...

9 Commits

Author SHA1 Message Date
SagarRajput-7
a5095bb96c Merge branch 'variable-update-queue' into limiting-api-via-keys 2024-12-17 17:52:45 +05:30
SagarRajput-7
cb91fee7c3 Merge branch 'develop' into variable-update-queue 2024-12-17 17:52:11 +05:30
SagarRajput-7
4b6e934510 Merge branch 'variable-update-queue' into limiting-api-via-keys 2024-12-16 12:03:14 +05:30
SagarRajput-7
99fb8c2a64 Merge branch 'develop' into variable-update-queue 2024-12-16 12:03:05 +05:30
SagarRajput-7
421d355e29 feat: fixed dropdown open triggering the api calls for single-select and misc 2024-12-11 13:18:56 +05:30
SagarRajput-7
eb75e636e8 feat: added API limiting to reduce unneccesary api call for dashboard variables 2024-12-10 11:20:34 +05:30
SagarRajput-7
f121240c82 Merge branch 'develop' into variable-update-queue 2024-12-10 11:18:28 +05:30
SagarRajput-7
a60dbf7f89 Merge branch 'develop' into variable-update-queue 2024-12-04 10:46:17 +05:30
SagarRajput-7
fa4aeae508 feat: updated the logic for variable update queue 2024-12-04 10:19:33 +05:30
3 changed files with 307 additions and 109 deletions

View File

@@ -1,9 +1,15 @@
import { Row } from 'antd'; import { Row } from 'antd';
import { isNull } from 'lodash-es';
import { useDashboard } from 'providers/Dashboard/Dashboard'; 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 { IDashboardVariable } from 'types/api/dashboard/getAll';
import {
buildDependencies,
buildDependencyGraph,
buildParentDependencyGraph,
onUpdateVariableNode,
VariableGraph,
} from './util';
import VariableItem from './VariableItem'; import VariableItem from './VariableItem';
function DashboardVariableSelection(): JSX.Element | null { function DashboardVariableSelection(): JSX.Element | null {
@@ -21,6 +27,12 @@ function DashboardVariableSelection(): JSX.Element | null {
const [variablesTableData, setVariablesTableData] = useState<any>([]); const [variablesTableData, setVariablesTableData] = useState<any>([]);
const [dependencyData, setDependencyData] = useState<{
order: string[];
graph: VariableGraph;
parentDependencyGraph: VariableGraph;
} | null>(null);
useEffect(() => { useEffect(() => {
if (variables) { if (variables) {
const tableRowData = []; const tableRowData = [];
@@ -43,35 +55,28 @@ function DashboardVariableSelection(): JSX.Element | null {
} }
}, [variables]); }, [variables]);
const onVarChanged = (name: string): void => { const initializationRef = useRef(false);
/**
* this function takes care of adding the dependent variables to current update queue and removing useEffect(() => {
* the updated variable name from the queue if (variablesTableData.length > 0 && !initializationRef.current) {
*/ const depGrp = buildDependencies(variablesTableData);
const dependentVariables = variablesTableData const { order, graph } = buildDependencyGraph(depGrp);
?.map((variable: any) => { const parentDependencyGraph = buildParentDependencyGraph(graph);
if (variable.type === 'QUERY') { setDependencyData({
const re = new RegExp(`\\{\\{\\s*?\\.${name}\\s*?\\}\\}`); // regex for `{{.var}}` order,
const queryValue = variable.queryValue || ''; graph,
const dependVarReMatch = queryValue.match(re); parentDependencyGraph,
if (dependVarReMatch !== null && dependVarReMatch.length > 0) { });
return variable.name; initializationRef.current = true;
} }
} }, [variablesTableData]);
return null;
})
.filter((val: string | null) => !isNull(val));
setVariablesToGetUpdated((prev) => [
...prev.filter((v) => v !== name),
...dependentVariables,
]);
};
const onValueUpdate = ( const onValueUpdate = (
name: string, name: string,
id: string, id: string,
value: IDashboardVariable['selectedValue'], value: IDashboardVariable['selectedValue'],
allSelected: boolean, allSelected: boolean,
isMountedCall?: boolean,
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
): void => { ): void => {
if (id) { 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} onValueUpdate={onValueUpdate}
variablesToGetUpdated={variablesToGetUpdated} variablesToGetUpdated={variablesToGetUpdated}
setVariablesToGetUpdated={setVariablesToGetUpdated} setVariablesToGetUpdated={setVariablesToGetUpdated}
dependencyData={dependencyData}
/> />
))} ))}
</Row> </Row>

View File

@@ -24,7 +24,7 @@ import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParse
import sortValues from 'lib/dashbaordVariables/sortVariableValues'; import sortValues from 'lib/dashbaordVariables/sortVariableValues';
import { debounce, isArray, isString } from 'lodash-es'; import { debounce, isArray, isString } from 'lodash-es';
import map from 'lodash-es/map'; 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 { useQuery } from 'react-query';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@@ -35,12 +35,15 @@ import { popupContainer } from 'utils/selectPopupContainer';
import { variablePropsToPayloadVariables } from '../utils'; import { variablePropsToPayloadVariables } from '../utils';
import { SelectItemStyle } from './styles'; import { SelectItemStyle } from './styles';
import { areArraysEqual } from './util'; import {
areArraysEqual,
checkAPIInvocation,
onUpdateVariableNode,
VariableGraph,
} from './util';
const ALL_SELECT_VALUE = '__ALL__'; const ALL_SELECT_VALUE = '__ALL__';
const variableRegexPattern = /\{\{\s*?\.([^\s}]+)\s*?\}\}/g;
enum ToggleTagValue { enum ToggleTagValue {
Only = 'Only', Only = 'Only',
All = 'All', All = 'All',
@@ -54,9 +57,15 @@ interface VariableItemProps {
id: string, id: string,
arg1: IDashboardVariable['selectedValue'], arg1: IDashboardVariable['selectedValue'],
allSelected: boolean, allSelected: boolean,
isMountedCall?: boolean,
) => void; ) => void;
variablesToGetUpdated: string[]; variablesToGetUpdated: string[];
setVariablesToGetUpdated: React.Dispatch<React.SetStateAction<string[]>>; setVariablesToGetUpdated: React.Dispatch<React.SetStateAction<string[]>>;
dependencyData: {
order: string[];
graph: VariableGraph;
parentDependencyGraph: VariableGraph;
} | null;
} }
const getSelectValue = ( const getSelectValue = (
@@ -79,6 +88,7 @@ function VariableItem({
onValueUpdate, onValueUpdate,
variablesToGetUpdated, variablesToGetUpdated,
setVariablesToGetUpdated, setVariablesToGetUpdated,
dependencyData,
}: VariableItemProps): JSX.Element { }: VariableItemProps): JSX.Element {
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>( const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
[], [],
@@ -88,60 +98,29 @@ function VariableItem({
(state) => state.globalTime, (state) => state.globalTime,
); );
// logic to detect if its a rerender or a new render/mount
const isMounted = useRef(false);
useEffect(() => { useEffect(() => {
if (variableData.allSelected && variableData.type === 'QUERY') { isMounted.current = true;
setVariablesToGetUpdated((prev) => { }, []);
const variablesQueue = [...prev.filter((v) => v !== variableData.name)];
if (variableData.name) { const validVariableUpdate = (): boolean => {
variablesQueue.push(variableData.name); if (!variableData.name) {
return false;
} }
return variablesQueue; 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,
);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps return variablesToGetUpdated.includes(variableData.name);
}, [minTime, maxTime]); };
const [errorMessage, setErrorMessage] = useState<null | string>(null); const [errorMessage, setErrorMessage] = useState<null | string>(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 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 // eslint-disable-next-line sonarjs/cognitive-complexity
const getOptions = (variablesRes: VariableResponseProps | null): void => { const getOptions = (variablesRes: VariableResponseProps | null): void => {
if (variablesRes && variableData.type === 'QUERY') { if (variablesRes && variableData.type === 'QUERY') {
@@ -184,9 +163,7 @@ function VariableItem({
if ( if (
variableData.type === 'QUERY' && variableData.type === 'QUERY' &&
variableData.name && variableData.name &&
(variablesToGetUpdated.includes(variableData.name) || (validVariableUpdate() || valueNotInList || variableData.allSelected)
valueNotInList ||
variableData.allSelected)
) { ) {
let value = variableData.selectedValue; let value = variableData.selectedValue;
let allSelected = false; let allSelected = false;
@@ -200,7 +177,16 @@ function VariableItem({
} }
if (variableData && variableData?.name && variableData?.id) { 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,8 +210,22 @@ function VariableItem({
} }
}; };
const { isLoading } = useQuery(getQueryKey(variableData), { const { isLoading } = useQuery(
enabled: variableData && variableData.type === 'QUERY', [
REACT_QUERY_KEY.DASHBOARD_BY_ID,
variableData.name || '',
`${minTime}`,
`${maxTime}`,
],
{
enabled:
variableData &&
variableData.type === 'QUERY' &&
checkAPIInvocation(
variablesToGetUpdated,
variableData,
dependencyData?.parentDependencyGraph,
),
queryFn: () => queryFn: () =>
dashboardVariablesQuery({ dashboardVariablesQuery({
query: variableData.queryValue || '', query: variableData.queryValue || '',
@@ -234,6 +234,21 @@ function VariableItem({
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
onSuccess: (response) => { onSuccess: (response) => {
getOptions(response.payload); 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),
]);
}
}, },
onError: (error: { onError: (error: {
details: { details: {
@@ -251,9 +266,19 @@ function VariableItem({
setErrorMessage(message); setErrorMessage(message);
} }
}, },
}); },
);
const handleChange = (value: string | string[]): void => { 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 (variableData.name) {
if ( if (
value === ALL_SELECT_VALUE || value === ALL_SELECT_VALUE ||

View File

@@ -1,3 +1,4 @@
import { isEmpty } from 'lodash-es';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
export function areArraysEqual( export function areArraysEqual(
@@ -29,3 +30,158 @@ export const convertVariablesToDbFormat = (
result[id] = obj; result[id] = obj;
return result; 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<string, string[]>;
export const buildDependencies = (
variables: IDashboardVariable[],
): VariableGraph => {
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<string, number> = {};
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<string>();
// 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
);
};