Scenes: Add support for viewing variable dependency graph in settings (#87577)

* Scenes: Add support for viewing variable dependency graph in settings

* Fix failing e2e test

* Add test for createDependencyNodes/Edges

* Refactor for better separation between old architecture and scenes

* Add internationalization

* run maker

---------

Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>
This commit is contained in:
kay delaney 2024-06-12 09:40:52 +01:00 committed by GitHub
parent 34c40f959f
commit 195c17da60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 141 additions and 19 deletions

View File

@ -8,6 +8,8 @@ import { SceneVariable, SceneVariableState } from '@grafana/scenes';
import { useStyles2, Stack, Button, EmptyState, TextLink } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { VariablesDependenciesButton } from '../../variables/VariablesDependenciesButton';
import { VariableEditorListRow } from './VariableEditorListRow';
export interface Props {
@ -81,6 +83,7 @@ export function VariableEditorList({
</table>
</div>
<Stack>
<VariablesDependenciesButton variables={variables} />
<Button
data-testid={selectors.pages.Dashboard.Settings.Variables.List.newButton}
onClick={onAdd}

View File

@ -0,0 +1,46 @@
import React, { useMemo } from 'react';
import { reportInteraction } from '@grafana/runtime';
import { SceneVariable, SceneVariableState } from '@grafana/scenes';
import { Button } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { NetworkGraphModal } from 'app/features/variables/inspect/NetworkGraphModal';
import { createDependencyEdges, createDependencyNodes, filterNodesWithDependencies } from './utils';
interface Props {
variables: Array<SceneVariable<SceneVariableState>>;
}
export const VariablesDependenciesButton = ({ variables }: Props) => {
const nodes = useMemo(() => createDependencyNodes(variables), [variables]);
const edges = useMemo(() => createDependencyEdges(variables), [variables]);
if (!edges.length) {
return null;
}
return (
<NetworkGraphModal
show={false}
title={t('dashboards.settings.variables.dependencies.title', 'Dependencies')}
nodes={filterNodesWithDependencies(nodes, edges)}
edges={edges}
>
{({ showModal }) => {
return (
<Button
onClick={() => {
reportInteraction('Show variable dependencies');
showModal();
}}
icon="channel-add"
variant="secondary"
>
<Trans i18nKey={'dashboards.settings.variables.dependencies.button'}>Show dependencies</Trans>
</Button>
);
}}
</NetworkGraphModal>
);
};

View File

@ -0,0 +1,40 @@
import { TestVariable } from '@grafana/scenes';
import { variableAdapters } from 'app/features/variables/adapters';
import { createCustomVariableAdapter } from 'app/features/variables/custom/adapter';
import { createDataSourceVariableAdapter } from 'app/features/variables/datasource/adapter';
import { createQueryVariableAdapter } from 'app/features/variables/query/adapter';
import { createDependencyEdges, createDependencyNodes } from './utils';
variableAdapters.setInit(() => [
createDataSourceVariableAdapter(),
createCustomVariableAdapter(),
createQueryVariableAdapter(),
]);
describe('createDependencyNodes', () => {
it('should create node for each variable', () => {
const variables = [
new TestVariable({ name: 'A', query: 'A.*', value: '', text: '', options: [] }),
new TestVariable({ name: 'B', query: 'B.*', value: '', text: '', options: [] }),
new TestVariable({ name: 'C', query: 'C.*', value: '', text: '', options: [] }),
];
const graphNodes = createDependencyNodes(variables);
expect(graphNodes[0].id).toBe('A');
expect(graphNodes[1].id).toBe('B');
expect(graphNodes[2].id).toBe('C');
});
});
describe('createDependencyEdges', () => {
it('should create edges for variable dependencies', () => {
const variables = [
new TestVariable({ name: 'A', query: 'A.*', value: '', text: '', options: [] }),
new TestVariable({ name: 'B', query: '${A}.*', value: '', text: '', options: [] }),
new TestVariable({ name: 'C', query: '${B}.*', value: '', text: '', options: [] }),
];
const graphEdges = createDependencyEdges(variables);
expect(graphEdges).toContainEqual({ from: 'B', to: 'A' });
expect(graphEdges).toContainEqual({ from: 'C', to: 'B' });
});
});

View File

@ -0,0 +1,28 @@
import { SceneVariable, SceneVariableState } from '@grafana/scenes';
import { GraphEdge, GraphNode } from 'app/features/variables/inspect/utils';
export function createDependencyNodes(variables: Array<SceneVariable<SceneVariableState>>): GraphNode[] {
return variables.map((variable) => ({ id: variable.state.name, label: `${variable.state.name}` }));
}
export function filterNodesWithDependencies(nodes: GraphNode[], edges: GraphEdge[]): GraphNode[] {
return nodes.filter((node) => edges.some((edge) => edge.from === node.id || edge.to === node.id));
}
export const createDependencyEdges = (variables: Array<SceneVariable<SceneVariableState>>): GraphEdge[] => {
const edges: GraphEdge[] = [];
for (const variable of variables) {
for (const other of variables) {
if (variable === other) {
continue;
}
const dependsOn = variable.variableDependency?.hasDependencyOn(other.state.name);
if (dependsOn) {
edges.push({ from: variable.state.name, to: other.state.name });
}
}
}
return edges;
};

View File

@ -1,26 +1,17 @@
import React, { useMemo } from 'react';
import { Provider } from 'react-redux';
import { TypedVariableModel } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { Button } from '@grafana/ui';
import { store } from '../../../store/store';
import { VariableModel } from '../types';
import { NetworkGraphModal } from './NetworkGraphModal';
import { createDependencyEdges, createDependencyNodes, filterNodesWithDependencies } from './utils';
interface OwnProps {
variables: VariableModel[];
interface Props {
variables: TypedVariableModel[];
}
interface ConnectedProps {}
interface DispatchProps {}
type Props = OwnProps & ConnectedProps & DispatchProps;
export const UnProvidedVariablesDependenciesButton = ({ variables }: Props) => {
export const VariablesDependenciesButton = ({ variables }: Props) => {
const nodes = useMemo(() => createDependencyNodes(variables), [variables]);
const edges = useMemo(() => createDependencyEdges(variables), [variables]);
@ -52,9 +43,3 @@ export const UnProvidedVariablesDependenciesButton = ({ variables }: Props) => {
</NetworkGraphModal>
);
};
export const VariablesDependenciesButton = (props: Props) => (
<Provider store={store}>
<UnProvidedVariablesDependenciesButton {...props} />
</Provider>
);

View File

@ -453,6 +453,16 @@
"title": "Versions"
}
},
"dashboards": {
"settings": {
"variables": {
"dependencies": {
"button": "Show dependencies",
"title": "Dependencies"
}
}
}
},
"data-source-list": {
"empty-state": {
"button-title": "Add data source",

View File

@ -453,6 +453,16 @@
"title": "Vęřşįőʼnş"
}
},
"dashboards": {
"settings": {
"variables": {
"dependencies": {
"button": "Ŝĥőŵ đępęʼnđęʼnčįęş",
"title": "Đępęʼnđęʼnčįęş"
}
}
}
},
"data-source-list": {
"empty-state": {
"button-title": "Åđđ đäŧä şőūřčę",