2020-11-05 02:53:27 -06:00
|
|
|
import { variableAdapters } from '../adapters';
|
2021-11-09 05:30:04 -06:00
|
|
|
import { DashboardModel, PanelModel } from '../../dashboard/state';
|
2020-11-05 02:53:27 -06:00
|
|
|
import { isAdHoc } from '../guard';
|
|
|
|
import { safeStringifyValue } from '../../../core/utils/explore';
|
|
|
|
import { VariableModel } from '../types';
|
2021-05-12 01:28:24 -05:00
|
|
|
import { containsVariable, variableRegex, variableRegexExec } from '../utils';
|
2021-11-09 05:30:04 -06:00
|
|
|
import { DataLinkBuiltInVars } from '@grafana/data';
|
2020-11-05 02:53:27 -06:00
|
|
|
|
|
|
|
export interface GraphNode {
|
|
|
|
id: string;
|
|
|
|
label: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface GraphEdge {
|
|
|
|
from: string;
|
|
|
|
to: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export const createDependencyNodes = (variables: VariableModel[]): GraphNode[] => {
|
|
|
|
const nodes: GraphNode[] = [];
|
|
|
|
|
|
|
|
for (const variable of variables) {
|
|
|
|
nodes.push({ id: variable.id, label: `${variable.id}` });
|
|
|
|
}
|
|
|
|
|
|
|
|
return nodes;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const filterNodesWithDependencies = (nodes: GraphNode[], edges: GraphEdge[]): GraphNode[] => {
|
2021-01-20 00:59:48 -06:00
|
|
|
return nodes.filter((node) => edges.some((edge) => edge.from === node.id || edge.to === node.id));
|
2020-11-05 02:53:27 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
export const createDependencyEdges = (variables: VariableModel[]): GraphEdge[] => {
|
|
|
|
const edges: GraphEdge[] = [];
|
|
|
|
|
|
|
|
for (const variable of variables) {
|
|
|
|
for (const other of variables) {
|
|
|
|
if (variable === other) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const dependsOn = variableAdapters.get(variable.type).dependsOn(variable, other);
|
|
|
|
|
|
|
|
if (dependsOn) {
|
|
|
|
edges.push({ from: variable.id, to: other.id });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return edges;
|
|
|
|
};
|
|
|
|
|
2021-03-09 05:49:05 -06:00
|
|
|
function getVariableName(expression: string) {
|
2021-05-12 01:28:24 -05:00
|
|
|
const match = variableRegexExec(expression);
|
2021-03-09 05:49:05 -06:00
|
|
|
if (!match) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
const variableName = match.slice(1).find((match) => match !== undefined);
|
|
|
|
return variableName;
|
|
|
|
}
|
|
|
|
|
2020-11-05 02:53:27 -06:00
|
|
|
export const getUnknownVariableStrings = (variables: VariableModel[], model: any) => {
|
2021-05-12 01:28:24 -05:00
|
|
|
variableRegex.lastIndex = 0;
|
2020-11-05 02:53:27 -06:00
|
|
|
const unknownVariableNames: string[] = [];
|
|
|
|
const modelAsString = safeStringifyValue(model, 2);
|
|
|
|
const matches = modelAsString.match(variableRegex);
|
|
|
|
|
|
|
|
if (!matches) {
|
|
|
|
return unknownVariableNames;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const match of matches) {
|
|
|
|
if (!match) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (match.indexOf('$__') !== -1) {
|
|
|
|
// ignore builtin variables
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-05-12 01:28:24 -05:00
|
|
|
if (match.indexOf('${__') !== -1) {
|
|
|
|
// ignore builtin variables
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-11-05 02:53:27 -06:00
|
|
|
if (match.indexOf('$hashKey') !== -1) {
|
|
|
|
// ignore Angular props
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-03-09 05:49:05 -06:00
|
|
|
const variableName = getVariableName(match);
|
2020-11-05 02:53:27 -06:00
|
|
|
|
2021-01-20 00:59:48 -06:00
|
|
|
if (variables.some((variable) => variable.id === variableName)) {
|
2020-11-05 02:53:27 -06:00
|
|
|
// ignore defined variables
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-01-20 00:59:48 -06:00
|
|
|
if (unknownVariableNames.find((name) => name === variableName)) {
|
2020-11-05 02:53:27 -06:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-03-09 05:49:05 -06:00
|
|
|
if (variableName) {
|
|
|
|
unknownVariableNames.push(variableName);
|
|
|
|
}
|
2020-11-05 02:53:27 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
return unknownVariableNames;
|
|
|
|
};
|
|
|
|
|
2021-03-09 05:49:05 -06:00
|
|
|
const validVariableNames: Record<string, RegExp[]> = {
|
2021-11-09 05:30:04 -06:00
|
|
|
alias: [/^m$/, /^measurement$/, /^col$/, /^tag_(\w+|\d+)$/],
|
2021-03-09 05:49:05 -06:00
|
|
|
query: [/^timeFilter$/],
|
|
|
|
};
|
|
|
|
|
2020-11-05 02:53:27 -06:00
|
|
|
export const getPropsWithVariable = (variableId: string, parent: { key: string; value: any }, result: any) => {
|
|
|
|
const stringValues = Object.keys(parent.value).reduce((all, key) => {
|
|
|
|
const value = parent.value[key];
|
2021-03-09 05:49:05 -06:00
|
|
|
if (!value || typeof value !== 'string') {
|
|
|
|
return all;
|
|
|
|
}
|
|
|
|
|
|
|
|
const isValidName = validVariableNames[key]
|
|
|
|
? validVariableNames[key].find((regex: RegExp) => regex.test(variableId))
|
|
|
|
: undefined;
|
2021-11-09 05:30:04 -06:00
|
|
|
|
|
|
|
let hasVariable = containsVariable(value, variableId);
|
|
|
|
if (key === 'repeat' && value === variableId) {
|
|
|
|
// repeat stores value without variable format
|
|
|
|
hasVariable = true;
|
|
|
|
}
|
2021-03-09 05:49:05 -06:00
|
|
|
|
|
|
|
if (!isValidName && hasVariable) {
|
2020-11-05 02:53:27 -06:00
|
|
|
all = {
|
|
|
|
...all,
|
|
|
|
[key]: value,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return all;
|
2021-11-09 05:30:04 -06:00
|
|
|
}, {} as Record<string, any>);
|
2020-11-05 02:53:27 -06:00
|
|
|
|
|
|
|
const objectValues = Object.keys(parent.value).reduce((all, key) => {
|
|
|
|
const value = parent.value[key];
|
|
|
|
if (value && typeof value === 'object' && Object.keys(value).length) {
|
2021-11-09 05:30:04 -06:00
|
|
|
let id = value.title || value.name || value.id || key;
|
|
|
|
if (Array.isArray(parent.value) && parent.key === 'panels') {
|
|
|
|
id = `${id}[${value.id}]`;
|
|
|
|
}
|
|
|
|
|
2020-11-05 02:53:27 -06:00
|
|
|
const newResult = getPropsWithVariable(variableId, { key, value }, {});
|
2021-11-09 05:30:04 -06:00
|
|
|
|
2020-11-05 02:53:27 -06:00
|
|
|
if (Object.keys(newResult).length) {
|
|
|
|
all = {
|
|
|
|
...all,
|
|
|
|
[id]: newResult,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return all;
|
2021-11-09 05:30:04 -06:00
|
|
|
}, {} as Record<string, any>);
|
2020-11-05 02:53:27 -06:00
|
|
|
|
|
|
|
if (Object.keys(stringValues).length || Object.keys(objectValues).length) {
|
|
|
|
result = {
|
|
|
|
...result,
|
|
|
|
...stringValues,
|
|
|
|
...objectValues,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
2021-03-09 05:49:05 -06:00
|
|
|
export interface VariableUsageTree {
|
|
|
|
variable: VariableModel;
|
|
|
|
tree: any;
|
|
|
|
}
|
|
|
|
|
2020-11-05 02:53:27 -06:00
|
|
|
export interface VariableUsages {
|
|
|
|
unUsed: VariableModel[];
|
2021-03-09 05:49:05 -06:00
|
|
|
unknown: VariableUsageTree[];
|
|
|
|
usages: VariableUsageTree[];
|
2020-11-05 02:53:27 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
export const createUsagesNetwork = (variables: VariableModel[], dashboard: DashboardModel | null): VariableUsages => {
|
|
|
|
if (!dashboard) {
|
|
|
|
return { unUsed: [], unknown: [], usages: [] };
|
|
|
|
}
|
|
|
|
|
|
|
|
const unUsed: VariableModel[] = [];
|
2021-03-09 05:49:05 -06:00
|
|
|
let usages: VariableUsageTree[] = [];
|
|
|
|
let unknown: VariableUsageTree[] = [];
|
2020-11-05 02:53:27 -06:00
|
|
|
const model = dashboard.getSaveModelClone();
|
|
|
|
|
|
|
|
const unknownVariables = getUnknownVariableStrings(variables, model);
|
|
|
|
for (const unknownVariable of unknownVariables) {
|
|
|
|
const props = getPropsWithVariable(unknownVariable, { key: 'model', value: model }, {});
|
|
|
|
if (Object.keys(props).length) {
|
|
|
|
const variable = ({ id: unknownVariable, name: unknownVariable } as unknown) as VariableModel;
|
|
|
|
unknown.push({ variable, tree: props });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const variable of variables) {
|
|
|
|
const variableId = variable.id;
|
|
|
|
const props = getPropsWithVariable(variableId, { key: 'model', value: model }, {});
|
|
|
|
if (!Object.keys(props).length && !isAdHoc(variable)) {
|
|
|
|
unUsed.push(variable);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Object.keys(props).length) {
|
|
|
|
usages.push({ variable, tree: props });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return { unUsed, unknown, usages };
|
|
|
|
};
|
|
|
|
|
2021-11-09 05:30:04 -06:00
|
|
|
/*
|
|
|
|
getAllAffectedPanelIdsForVariableChange is a function that extracts all the panel ids that are affected by a single variable
|
|
|
|
change. It will traverse all chained variables to identify all cascading changes too.
|
|
|
|
|
|
|
|
This is done entirely by parsing the current dashboard json and doesn't take under consideration a user cancelling
|
|
|
|
a variable query or any faulty variable queries.
|
|
|
|
|
|
|
|
This doesn't take circular dependencies in consideration.
|
|
|
|
*/
|
|
|
|
export function getAllAffectedPanelIdsForVariableChange(
|
|
|
|
variableId: string,
|
|
|
|
variables: VariableModel[],
|
|
|
|
panels: PanelModel[]
|
|
|
|
): number[] {
|
|
|
|
let affectedPanelIds: number[] = getAffectedPanelIdsForVariable(variableId, panels);
|
|
|
|
const affectedPanelIdsForAllVariables = getAffectedPanelIdsForVariable(DataLinkBuiltInVars.includeVars, panels);
|
|
|
|
affectedPanelIds = [...new Set([...affectedPanelIdsForAllVariables, ...affectedPanelIds])];
|
|
|
|
|
|
|
|
const dependencies = getDependenciesForVariable(variableId, variables, new Set());
|
|
|
|
for (const dependency of dependencies) {
|
|
|
|
const affectedPanelIdsForDependency = getAffectedPanelIdsForVariable(dependency, panels);
|
|
|
|
affectedPanelIds = [...new Set([...affectedPanelIdsForDependency, ...affectedPanelIds])];
|
|
|
|
}
|
|
|
|
|
|
|
|
return affectedPanelIds;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getDependenciesForVariable(
|
|
|
|
variableId: string,
|
|
|
|
variables: VariableModel[],
|
|
|
|
deps: Set<string>
|
|
|
|
): Set<string> {
|
|
|
|
if (!variables.length) {
|
|
|
|
return deps;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const variable of variables) {
|
|
|
|
if (variable.name === variableId) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const depends = variableAdapters.get(variable.type).dependsOn(variable, { name: variableId });
|
|
|
|
if (!depends) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
deps.add(variable.name);
|
|
|
|
deps = getDependenciesForVariable(variable.name, variables, deps);
|
|
|
|
}
|
|
|
|
|
|
|
|
return deps;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getAffectedPanelIdsForVariable(variableId: string, panels: PanelModel[]): number[] {
|
|
|
|
if (!panels.length) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
const affectedPanelIds: number[] = [];
|
|
|
|
const repeatRegex = new RegExp(`"repeat":"${variableId}"`);
|
|
|
|
for (const panel of panels) {
|
|
|
|
const panelAsJson = safeStringifyValue(panel.getSaveModel());
|
|
|
|
|
|
|
|
// check for repeats that don't use variableRegex
|
|
|
|
const repeatMatches = panelAsJson.match(repeatRegex);
|
|
|
|
if (repeatMatches?.length) {
|
|
|
|
affectedPanelIds.push(panel.id);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const matches = panelAsJson.match(variableRegex);
|
|
|
|
if (!matches) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const match of matches) {
|
|
|
|
const variableName = getVariableName(match);
|
|
|
|
if (variableName === variableId) {
|
|
|
|
affectedPanelIds.push(panel.id);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return affectedPanelIds;
|
|
|
|
}
|
|
|
|
|
2020-11-05 02:53:27 -06:00
|
|
|
export interface UsagesToNetwork {
|
|
|
|
variable: VariableModel;
|
|
|
|
nodes: GraphNode[];
|
|
|
|
edges: GraphEdge[];
|
|
|
|
showGraph: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
export const traverseTree = (usage: UsagesToNetwork, parent: { id: string; value: any }): UsagesToNetwork => {
|
|
|
|
const { id, value } = parent;
|
|
|
|
const { nodes, edges } = usage;
|
|
|
|
|
|
|
|
if (value && typeof value === 'string') {
|
|
|
|
const leafId = `${parent.id}-${value}`;
|
|
|
|
nodes.push({ id: leafId, label: value });
|
|
|
|
edges.push({ from: leafId, to: id });
|
|
|
|
|
|
|
|
return usage;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (value && typeof value === 'object') {
|
|
|
|
const keys = Object.keys(value);
|
|
|
|
for (const key of keys) {
|
|
|
|
const leafId = `${parent.id}-${key}`;
|
|
|
|
nodes.push({ id: leafId, label: key });
|
|
|
|
edges.push({ from: leafId, to: id });
|
|
|
|
usage = traverseTree(usage, { id: leafId, value: value[key] });
|
|
|
|
}
|
|
|
|
|
|
|
|
return usage;
|
|
|
|
}
|
|
|
|
|
|
|
|
return usage;
|
|
|
|
};
|
|
|
|
|
2021-03-09 05:49:05 -06:00
|
|
|
export const transformUsagesToNetwork = (usages: VariableUsageTree[]): UsagesToNetwork[] => {
|
2020-11-05 02:53:27 -06:00
|
|
|
const results: UsagesToNetwork[] = [];
|
|
|
|
|
|
|
|
for (const usage of usages) {
|
|
|
|
const { variable, tree } = usage;
|
|
|
|
const result: UsagesToNetwork = {
|
|
|
|
variable,
|
|
|
|
nodes: [{ id: 'dashboard', label: 'dashboard' }],
|
|
|
|
edges: [],
|
|
|
|
showGraph: false,
|
|
|
|
};
|
|
|
|
results.push(traverseTree(result, { id: 'dashboard', value: tree }));
|
|
|
|
}
|
|
|
|
|
|
|
|
return results;
|
|
|
|
};
|
|
|
|
|
|
|
|
const countLeaves = (object: any): number => {
|
|
|
|
const total = Object.values(object).reduce((count: number, value: any) => {
|
|
|
|
if (typeof value === 'object') {
|
|
|
|
return count + countLeaves(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
return count + 1;
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
return (total as unknown) as number;
|
|
|
|
};
|
|
|
|
|
2021-03-09 05:49:05 -06:00
|
|
|
export const getVariableUsages = (variableId: string, usages: VariableUsageTree[]): number => {
|
2021-01-20 00:59:48 -06:00
|
|
|
const usage = usages.find((usage) => usage.variable.id === variableId);
|
2020-11-05 02:53:27 -06:00
|
|
|
if (!usage) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return countLeaves(usage.tree);
|
|
|
|
};
|