mirror of https://github.com/grafana/grafana.git
[Scopes] Inherit scope paths on node fetching (#93824)
* [Scopes]: Inherit scope paths on node fetching * add function desc --------- Co-authored-by: Bogdan Matei <bogdan.matei@grafana.com>
This commit is contained in:
parent
08d03cc315
commit
4a3ce66193
|
@ -22,7 +22,12 @@ import { ScopesInput } from './ScopesInput';
|
||||||
import { ScopesTree } from './ScopesTree';
|
import { ScopesTree } from './ScopesTree';
|
||||||
import { fetchNodes, fetchScope, fetchSelectedScopes } from './api';
|
import { fetchNodes, fetchScope, fetchSelectedScopes } from './api';
|
||||||
import { NodeReason, NodesMap, SelectedScope, TreeScope } from './types';
|
import { NodeReason, NodesMap, SelectedScope, TreeScope } from './types';
|
||||||
import { getBasicScope, getScopeNamesFromSelectedScopes, getTreeScopesFromSelectedScopes } from './utils';
|
import {
|
||||||
|
getBasicScope,
|
||||||
|
getScopeNamesFromSelectedScopes,
|
||||||
|
getScopesAndTreeScopesWithPaths,
|
||||||
|
getTreeScopesFromSelectedScopes,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
export interface ScopesSelectorSceneState extends SceneObjectState {
|
export interface ScopesSelectorSceneState extends SceneObjectState {
|
||||||
dashboards: SceneObjectRef<ScopesDashboardsScene> | null;
|
dashboards: SceneObjectRef<ScopesDashboardsScene> | null;
|
||||||
|
@ -126,7 +131,14 @@ export class ScopesSelectorScene extends SceneObjectBase<ScopesSelectorSceneStat
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.subscribe((childNodes) => {
|
.subscribe((childNodes) => {
|
||||||
const persistedNodes = this.state.treeScopes
|
const [scopes, treeScopes] = getScopesAndTreeScopesWithPaths(
|
||||||
|
this.state.scopes,
|
||||||
|
this.state.treeScopes,
|
||||||
|
path,
|
||||||
|
childNodes
|
||||||
|
);
|
||||||
|
|
||||||
|
const persistedNodes = treeScopes
|
||||||
.map(({ path }) => path[path.length - 1])
|
.map(({ path }) => path[path.length - 1])
|
||||||
.filter((nodeName) => nodeName in currentNode.nodes && !(nodeName in childNodes))
|
.filter((nodeName) => nodeName in currentNode.nodes && !(nodeName in childNodes))
|
||||||
.reduce<NodesMap>((acc, nodeName) => {
|
.reduce<NodesMap>((acc, nodeName) => {
|
||||||
|
@ -140,7 +152,7 @@ export class ScopesSelectorScene extends SceneObjectBase<ScopesSelectorSceneStat
|
||||||
|
|
||||||
currentNode.nodes = { ...persistedNodes, ...childNodes };
|
currentNode.nodes = { ...persistedNodes, ...childNodes };
|
||||||
|
|
||||||
this.setState({ nodes });
|
this.setState({ nodes, scopes, treeScopes });
|
||||||
|
|
||||||
this.nodesFetchingSub?.unsubscribe();
|
this.nodesFetchingSub?.unsubscribe();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Scope, ScopeDashboardBinding } from '@grafana/data';
|
import { Scope, ScopeDashboardBinding } from '@grafana/data';
|
||||||
|
|
||||||
import { SelectedScope, SuggestedDashboardsFoldersMap, TreeScope } from './types';
|
import { NodesMap, SelectedScope, SuggestedDashboardsFoldersMap, TreeScope } from './types';
|
||||||
|
|
||||||
export function getBasicScope(name: string): Scope {
|
export function getBasicScope(name: string): Scope {
|
||||||
return {
|
return {
|
||||||
|
@ -44,6 +44,59 @@ export function getScopeNamesFromSelectedScopes(scopes: SelectedScope[]): string
|
||||||
return scopes.map(({ scope }) => scope.metadata.name);
|
return scopes.map(({ scope }) => scope.metadata.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// helper func to get the selected/tree scopes together with their paths
|
||||||
|
// needed to maintain selected scopes in tree for example when navigating
|
||||||
|
// between categories or when loading scopes from URL to find the scope's path
|
||||||
|
export function getScopesAndTreeScopesWithPaths(
|
||||||
|
selectedScopes: SelectedScope[],
|
||||||
|
treeScopes: TreeScope[],
|
||||||
|
path: string[],
|
||||||
|
childNodes: NodesMap
|
||||||
|
): [SelectedScope[], TreeScope[]] {
|
||||||
|
const childNodesArr = Object.values(childNodes);
|
||||||
|
|
||||||
|
// Get all scopes without paths
|
||||||
|
// We use tree scopes as the list is always up to date as opposed to selected scopes which can be outdated
|
||||||
|
const scopeNamesWithoutPaths = treeScopes.filter(({ path }) => path.length === 0).map(({ scopeName }) => scopeName);
|
||||||
|
|
||||||
|
// We search for the path of each scope name without a path
|
||||||
|
const scopeNamesWithPaths = scopeNamesWithoutPaths.reduce<Record<string, string[]>>((acc, scopeName) => {
|
||||||
|
const possibleParent = childNodesArr.find((childNode) => childNode.isSelectable && childNode.linkId === scopeName);
|
||||||
|
|
||||||
|
if (possibleParent) {
|
||||||
|
acc[scopeName] = [...path, possibleParent.name];
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// Update the paths of the selected scopes based on what we found
|
||||||
|
const newSelectedScopes = selectedScopes.map((selectedScope) => {
|
||||||
|
if (selectedScope.path.length > 0) {
|
||||||
|
return selectedScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...selectedScope,
|
||||||
|
path: scopeNamesWithPaths[selectedScope.scope.metadata.name] ?? [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the paths of the tree scopes based on what we found
|
||||||
|
const newTreeScopes = treeScopes.map((treeScope) => {
|
||||||
|
if (treeScope.path.length > 0) {
|
||||||
|
return treeScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...treeScope,
|
||||||
|
path: scopeNamesWithPaths[treeScope.scopeName] ?? [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return [newSelectedScopes, newTreeScopes];
|
||||||
|
}
|
||||||
|
|
||||||
export function groupDashboards(dashboards: ScopeDashboardBinding[]): SuggestedDashboardsFoldersMap {
|
export function groupDashboards(dashboards: ScopeDashboardBinding[]): SuggestedDashboardsFoldersMap {
|
||||||
return dashboards.reduce<SuggestedDashboardsFoldersMap>(
|
return dashboards.reduce<SuggestedDashboardsFoldersMap>(
|
||||||
(acc, dashboard) => {
|
(acc, dashboard) => {
|
||||||
|
|
|
@ -36,6 +36,8 @@ import {
|
||||||
expectResultCloudOpsSelected,
|
expectResultCloudOpsSelected,
|
||||||
expectScopesHeadline,
|
expectScopesHeadline,
|
||||||
expectScopesSelectorValue,
|
expectScopesSelectorValue,
|
||||||
|
expectSelectedScopePath,
|
||||||
|
expectTreeScopePath,
|
||||||
} from './utils/assertions';
|
} from './utils/assertions';
|
||||||
import { fetchNodesSpy, fetchScopeSpy, getDatasource, getInstanceSettings, getMock } from './utils/mocks';
|
import { fetchNodesSpy, fetchScopeSpy, getDatasource, getInstanceSettings, getMock } from './utils/mocks';
|
||||||
import { renderDashboard, resetScenes } from './utils/render';
|
import { renderDashboard, resetScenes } from './utils/render';
|
||||||
|
@ -244,4 +246,28 @@ describe('Tree', () => {
|
||||||
expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
|
expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
|
||||||
expectScopesHeadline('No results found for your query');
|
expectScopesHeadline('No results found for your query');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Updates the paths for scopes without paths on nodes fetching', async () => {
|
||||||
|
const selectedScopeName = 'grafana';
|
||||||
|
const unselectedScopeName = 'mimir';
|
||||||
|
const selectedScopeNameFromOtherGroup = 'dev';
|
||||||
|
|
||||||
|
await updateScopes([selectedScopeName, selectedScopeNameFromOtherGroup]);
|
||||||
|
expectSelectedScopePath(selectedScopeName, []);
|
||||||
|
expectTreeScopePath(selectedScopeName, []);
|
||||||
|
expectSelectedScopePath(unselectedScopeName, undefined);
|
||||||
|
expectTreeScopePath(unselectedScopeName, undefined);
|
||||||
|
expectSelectedScopePath(selectedScopeNameFromOtherGroup, []);
|
||||||
|
expectTreeScopePath(selectedScopeNameFromOtherGroup, []);
|
||||||
|
|
||||||
|
await openSelector();
|
||||||
|
await expandResultApplications();
|
||||||
|
const expectedPath = ['', 'applications', 'applications-grafana'];
|
||||||
|
expectSelectedScopePath(selectedScopeName, expectedPath);
|
||||||
|
expectTreeScopePath(selectedScopeName, expectedPath);
|
||||||
|
expectSelectedScopePath(unselectedScopeName, undefined);
|
||||||
|
expectTreeScopePath(unselectedScopeName, undefined);
|
||||||
|
expectSelectedScopePath(selectedScopeNameFromOtherGroup, []);
|
||||||
|
expectTreeScopePath(selectedScopeNameFromOtherGroup, []);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,8 +12,10 @@ import {
|
||||||
getResultApplicationsMimirSelect,
|
getResultApplicationsMimirSelect,
|
||||||
getResultCloudDevRadio,
|
getResultCloudDevRadio,
|
||||||
getResultCloudOpsRadio,
|
getResultCloudOpsRadio,
|
||||||
|
getSelectedScope,
|
||||||
getSelectorInput,
|
getSelectorInput,
|
||||||
getTreeHeadline,
|
getTreeHeadline,
|
||||||
|
getTreeScope,
|
||||||
queryAllDashboard,
|
queryAllDashboard,
|
||||||
queryDashboard,
|
queryDashboard,
|
||||||
queryDashboardFolderExpand,
|
queryDashboardFolderExpand,
|
||||||
|
@ -80,3 +82,8 @@ export const expectOldDashboardDTO = (scopes?: string[]) =>
|
||||||
expect(getMock).toHaveBeenCalledWith('/api/dashboards/uid/1', scopes ? { scopes } : undefined);
|
expect(getMock).toHaveBeenCalledWith('/api/dashboards/uid/1', scopes ? { scopes } : undefined);
|
||||||
export const expectNewDashboardDTO = () =>
|
export const expectNewDashboardDTO = () =>
|
||||||
expect(getMock).toHaveBeenCalledWith('/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/1/dto');
|
expect(getMock).toHaveBeenCalledWith('/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/1/dto');
|
||||||
|
|
||||||
|
export const expectSelectedScopePath = (name: string, path: string[] | undefined) =>
|
||||||
|
expect(getSelectedScope(name)?.path).toEqual(path);
|
||||||
|
export const expectTreeScopePath = (name: string, path: string[] | undefined) =>
|
||||||
|
expect(getTreeScope(name)?.path).toEqual(path);
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { screen } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { scopesSelectorScene } from '../../instance';
|
||||||
|
|
||||||
const selectors = {
|
const selectors = {
|
||||||
tree: {
|
tree: {
|
||||||
search: 'scopes-tree-search',
|
search: 'scopes-tree-search',
|
||||||
|
@ -82,3 +84,9 @@ export const getResultCloudDevRadio = () =>
|
||||||
screen.getByTestId<HTMLInputElement>(selectors.tree.radio('cloud-dev', 'result'));
|
screen.getByTestId<HTMLInputElement>(selectors.tree.radio('cloud-dev', 'result'));
|
||||||
export const getResultCloudOpsRadio = () =>
|
export const getResultCloudOpsRadio = () =>
|
||||||
screen.getByTestId<HTMLInputElement>(selectors.tree.radio('cloud-ops', 'result'));
|
screen.getByTestId<HTMLInputElement>(selectors.tree.radio('cloud-ops', 'result'));
|
||||||
|
|
||||||
|
export const getListOfSelectedScopes = () => scopesSelectorScene?.state.scopes;
|
||||||
|
export const getListOfTreeScopes = () => scopesSelectorScene?.state.treeScopes;
|
||||||
|
export const getSelectedScope = (name: string) =>
|
||||||
|
getListOfSelectedScopes()?.find((selectedScope) => selectedScope.scope.metadata.name === name);
|
||||||
|
export const getTreeScope = (name: string) => getListOfTreeScopes()?.find((treeScope) => treeScope.scopeName === name);
|
||||||
|
|
Loading…
Reference in New Issue