- {nodes.map((childNode) => {
- const selected = childNode.selectable && scopeNames.includes(childNode.linkId!);
+ const scopeNode = scopeNodes[treeNode.scopeNodeId];
+ if (!scopeNode) {
+ // Should not happen as only way we show a tree is if we also load the nodes.
+ return null;
+ }
+ const parentNode = scopeNode.spec.parentName ? scopeNodes[scopeNode.spec.parentName] : undefined;
+ const disableMultiSelect = parentNode?.spec.disableMultiSelect ?? false;
- if (anyChildExpanded && !childNode.expanded) {
- return null;
- }
+ const isSelectable = isNodeSelectable(scopeNode);
+ const isExpandable = isNodeExpandable(scopeNode);
- const childNodePath = [...nodePath, childNode.name];
+ return (
+
+
+ {isSelectable && !treeNode.expanded ? (
+ disableMultiSelect ? (
+
{
+ selected ? deselectScope(treeNode.scopeNodeId) : selectScope(treeNode.scopeNodeId);
+ }}
+ />
+ ) : (
+ {
+ selected ? deselectScope(treeNode.scopeNodeId) : selectScope(treeNode.scopeNodeId);
+ }}
+ />
+ )
+ ) : null}
- const radioName = childNodePath.join('.');
-
- return (
- {
+ onNodeUpdate(treeNode.scopeNodeId, !treeNode.expanded, treeNode.query);
+ }}
>
-
- {childNode.selectable && !childNode.expanded ? (
- node.disableMultiSelect ? (
- {
- onNodeSelectToggle({ path: childNodePath });
- }}
- />
- ) : (
- {
- onNodeSelectToggle({ path: childNodePath });
- }}
- />
- )
- ) : null}
+
- {childNode.expandable ? (
-
+ ) : (
+ {scopeNode.spec.title}
+ )}
+
- {childNode.title}
-
- ) : (
-
{childNode.title}
- )}
-
-
-
- {childNode.expanded && (
-
- )}
-
-
- );
- })}
+
+ {treeNode.expanded && (
+
+ )}
+
);
-
- if (lastExpandedNode) {
- return (
-
- {children}
-
- );
- }
-
- return children;
}
const getStyles = (theme: GrafanaTheme2) => {
diff --git a/public/app/features/scopes/selector/ScopesTreeItemList.tsx b/public/app/features/scopes/selector/ScopesTreeItemList.tsx
new file mode 100644
index 00000000000..8f663b0db0a
--- /dev/null
+++ b/public/app/features/scopes/selector/ScopesTreeItemList.tsx
@@ -0,0 +1,92 @@
+import { css } from '@emotion/css';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { ScrollContainer, useStyles2 } from '@grafana/ui';
+
+import { ScopesTreeItem } from './ScopesTreeItem';
+import { isNodeSelectable } from './scopesTreeUtils';
+import { NodesMap, SelectedScope, TreeNode } from './types';
+
+type Props = {
+ anyChildExpanded: boolean;
+ lastExpandedNode: boolean;
+ loadingNodeName: string | undefined;
+ items: TreeNode[];
+ maxHeight: string;
+ selectedScopes: SelectedScope[];
+ scopeNodes: NodesMap;
+ onNodeUpdate: (scopeNodeId: string, expanded: boolean, query: string) => void;
+ selectScope: (scopeNodeId: string) => void;
+ deselectScope: (scopeNodeId: string) => void;
+};
+
+export function ScopesTreeItemList({
+ items,
+ anyChildExpanded,
+ lastExpandedNode,
+ maxHeight,
+ selectedScopes,
+ scopeNodes,
+ loadingNodeName,
+ onNodeUpdate,
+ selectScope,
+ deselectScope,
+}: Props) {
+ const styles = useStyles2(getStyles);
+
+ if (items.length === 0) {
+ return null;
+ }
+
+ const children = (
+
+ {items.map((childNode) => {
+ const selected =
+ isNodeSelectable(scopeNodes[childNode.scopeNodeId]) &&
+ selectedScopes.some((s) => {
+ if (s.scopeNodeId) {
+ // If we have scopeNodeId we only match based on that so even if the actual scope is the same we don't
+ // mark different scopeNode as selected.
+ return s.scopeNodeId === childNode.scopeNodeId;
+ } else {
+ return s.scopeId === scopeNodes[childNode.scopeNodeId]?.spec.linkId;
+ }
+ });
+ return (
+
+ );
+ })}
+
+ );
+
+ if (lastExpandedNode) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return children;
+}
+
+const getStyles = (theme: GrafanaTheme2) => {
+ return {
+ expandedContainer: css({
+ display: 'flex',
+ flexDirection: 'column',
+ maxHeight: '100%',
+ }),
+ };
+};
diff --git a/public/app/features/scopes/selector/ScopesTreeLoading.tsx b/public/app/features/scopes/selector/ScopesTreeLoading.tsx
deleted file mode 100644
index 3f24890d4d7..00000000000
--- a/public/app/features/scopes/selector/ScopesTreeLoading.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { css } from '@emotion/css';
-import { ReactNode } from 'react';
-import Skeleton from 'react-loading-skeleton';
-
-import { GrafanaTheme2 } from '@grafana/data';
-import { useStyles2 } from '@grafana/ui';
-
-export interface ScopesTreeLoadingProps {
- children: ReactNode;
- nodeLoading: boolean;
-}
-
-export function ScopesTreeLoading({ children, nodeLoading }: ScopesTreeLoadingProps) {
- const styles = useStyles2(getStyles);
-
- if (nodeLoading) {
- return
;
- }
-
- return children;
-}
-
-const getStyles = (theme: GrafanaTheme2) => {
- return {
- loader: css({
- margin: theme.spacing(0.5, 0),
- }),
- };
-};
diff --git a/public/app/features/scopes/selector/ScopesTreeSearch.tsx b/public/app/features/scopes/selector/ScopesTreeSearch.tsx
index b3aa7c5504f..972770ff910 100644
--- a/public/app/features/scopes/selector/ScopesTreeSearch.tsx
+++ b/public/app/features/scopes/selector/ScopesTreeSearch.tsx
@@ -6,30 +6,32 @@ import { GrafanaTheme2 } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { FilterInput, useStyles2 } from '@grafana/ui';
-import { OnNodeUpdate } from './types';
+import { TreeNode } from './types';
export interface ScopesTreeSearchProps {
anyChildExpanded: boolean;
- nodePath: string[];
- query: string;
- onNodeUpdate: OnNodeUpdate;
+ treeNode: TreeNode;
+ onNodeUpdate: (scopeNodeId: string, expanded: boolean, query: string) => void;
}
-export function ScopesTreeSearch({ anyChildExpanded, nodePath, query, onNodeUpdate }: ScopesTreeSearchProps) {
+export function ScopesTreeSearch({ anyChildExpanded, treeNode, onNodeUpdate }: ScopesTreeSearchProps) {
const styles = useStyles2(getStyles);
- const [inputState, setInputState] = useState<{ value: string; dirty: boolean }>({ value: query, dirty: false });
+ const [inputState, setInputState] = useState<{ value: string; dirty: boolean }>({
+ value: treeNode.query,
+ dirty: false,
+ });
useEffect(() => {
- if (!inputState.dirty && inputState.value !== query) {
- setInputState({ value: query, dirty: false });
+ if (!inputState.dirty && inputState.value !== treeNode.query) {
+ setInputState({ value: treeNode.query, dirty: false });
}
- }, [inputState, query]);
+ }, [inputState, treeNode.query]);
useDebounce(
() => {
if (inputState.dirty) {
- onNodeUpdate(nodePath, true, inputState.value);
+ onNodeUpdate(treeNode.scopeNodeId, true, inputState.value);
}
},
500,
diff --git a/public/app/features/scopes/selector/scopesTreeUtils.test.ts b/public/app/features/scopes/selector/scopesTreeUtils.test.ts
new file mode 100644
index 00000000000..9c88c51b6d5
--- /dev/null
+++ b/public/app/features/scopes/selector/scopesTreeUtils.test.ts
@@ -0,0 +1,206 @@
+import { ScopeNode } from '@grafana/data';
+
+import {
+ closeNodes,
+ expandNodes,
+ isNodeExpandable,
+ isNodeSelectable,
+ getPathOfNode,
+ modifyTreeNodeAtPath,
+ treeNodeAtPath,
+} from './scopesTreeUtils';
+import { TreeNode, NodesMap } from './types';
+
+describe('scopesTreeUtils', () => {
+ describe('closeNodes', () => {
+ it('should create a deep copy with all nodes closed', () => {
+ const tree: TreeNode = {
+ expanded: true,
+ scopeNodeId: 'root',
+ query: '',
+ children: {
+ child1: {
+ expanded: true,
+ scopeNodeId: 'child1',
+ query: '',
+ children: {
+ grandchild1: {
+ expanded: true,
+ scopeNodeId: 'grandchild1',
+ query: '',
+ },
+ },
+ },
+ },
+ };
+
+ const result = closeNodes(tree);
+
+ expect(result.expanded).toBe(false);
+ expect(result.children?.child1.expanded).toBe(false);
+ expect(result.children?.child1.children?.grandchild1.expanded).toBe(false);
+ // Verify it's a deep copy
+ expect(result).not.toBe(tree);
+ expect(result.children).not.toBe(tree.children);
+ });
+ });
+
+ describe('expandNodes', () => {
+ it('should expand nodes along the specified path', () => {
+ const tree: TreeNode = {
+ expanded: false,
+ scopeNodeId: 'root',
+ query: '',
+ children: {
+ child1: {
+ expanded: false,
+ scopeNodeId: 'child1',
+ query: '',
+ children: {
+ grandchild1: {
+ expanded: false,
+ scopeNodeId: 'grandchild1',
+ query: '',
+ },
+
+ grandchild2: {
+ expanded: false,
+ scopeNodeId: 'grandchild2',
+ query: '',
+ },
+ },
+ },
+ },
+ };
+
+ const path = ['', 'child1', 'grandchild1'];
+ const result = expandNodes(tree, path);
+
+ expect(result.expanded).toBe(true);
+ expect(result.children?.child1.expanded).toBe(true);
+ expect(result.children?.child1.children?.grandchild1.expanded).toBe(true);
+ // Other nodes don't get expanded
+ expect(result.children?.child1.children?.grandchild2.expanded).toBe(false);
+ });
+
+ it('should throw error when path contains non-existent node', () => {
+ const tree: TreeNode = {
+ expanded: false,
+ scopeNodeId: 'root',
+ query: '',
+ children: {},
+ };
+
+ expect(() => expandNodes(tree, ['', 'nonexistent'])).toThrow('Node nonexistent not found in tree');
+ });
+ });
+
+ describe('isNodeExpandable', () => {
+ it('should return true for container nodes', () => {
+ const node = { spec: { nodeType: 'container' } } as ScopeNode;
+ expect(isNodeExpandable(node)).toBe(true);
+ });
+
+ it('should return false for non-container nodes', () => {
+ const node = { spec: { nodeType: 'leaf' } } as ScopeNode;
+ expect(isNodeExpandable(node)).toBe(false);
+ });
+ });
+
+ describe('isNodeSelectable', () => {
+ it('should return true for scope nodes', () => {
+ const node = { spec: { linkType: 'scope' } } as ScopeNode;
+ expect(isNodeSelectable(node)).toBe(true);
+ });
+
+ it('should return false for non-scope nodes', () => {
+ const node = { spec: { linkType: undefined } } as ScopeNode;
+ expect(isNodeSelectable(node)).toBe(false);
+ });
+ });
+
+ describe('getPathOfNode', () => {
+ it('should return correct path for nested node', () => {
+ const nodes: NodesMap = {
+ root: { spec: { parentName: '' } } as ScopeNode,
+ child: { spec: { parentName: 'root' } } as ScopeNode,
+ grandchild: { spec: { parentName: 'child' } } as ScopeNode,
+ };
+
+ const path = getPathOfNode('grandchild', nodes);
+ expect(path).toEqual(['', 'root', 'child', 'grandchild']);
+ });
+ });
+
+ describe('modifyTreeNodeAtPath', () => {
+ it('should modify node at specified path', () => {
+ const tree: TreeNode = {
+ expanded: false,
+ scopeNodeId: 'root',
+ query: '',
+ children: {
+ child1: {
+ expanded: false,
+ scopeNodeId: 'child1',
+ query: '',
+ },
+ },
+ };
+
+ const result = modifyTreeNodeAtPath(tree, ['', 'child1'], (node) => {
+ node.expanded = true;
+ node.query = 'test';
+ });
+
+ expect(result.children?.child1.expanded).toBe(true);
+ expect(result.children?.child1.query).toBe('test');
+ });
+
+ it('should return original tree if path is invalid', () => {
+ const tree: TreeNode = {
+ expanded: false,
+ scopeNodeId: 'root',
+ query: '',
+ children: {},
+ };
+
+ const result = modifyTreeNodeAtPath(tree, ['', 'nonexistent'], (node) => {
+ node.expanded = true;
+ });
+
+ expect(result).toEqual(tree);
+ });
+ });
+
+ describe('treeNodeAtPath', () => {
+ it('should return node at specified path', () => {
+ const tree: TreeNode = {
+ expanded: false,
+ scopeNodeId: 'root',
+ query: '',
+ children: {
+ child1: {
+ expanded: false,
+ scopeNodeId: 'child1',
+ query: '',
+ },
+ },
+ };
+
+ const result = treeNodeAtPath(tree, ['', 'child1']);
+ expect(result).toBe(tree.children?.child1);
+ });
+
+ it('should return undefined for invalid path', () => {
+ const tree: TreeNode = {
+ expanded: false,
+ scopeNodeId: 'root',
+ query: '',
+ children: {},
+ };
+
+ const result = treeNodeAtPath(tree, ['', 'nonexistent']);
+ expect(result).toBeUndefined();
+ });
+ });
+});
diff --git a/public/app/features/scopes/selector/scopesTreeUtils.ts b/public/app/features/scopes/selector/scopesTreeUtils.ts
new file mode 100644
index 00000000000..2acb3bc86a6
--- /dev/null
+++ b/public/app/features/scopes/selector/scopesTreeUtils.ts
@@ -0,0 +1,111 @@
+import { ScopeNode } from '@grafana/data';
+
+import { NodesMap, TreeNode } from './types';
+
+/**
+ * Creates a deep copy of the node tree with expanded prop set to false.
+ */
+export function closeNodes(tree: TreeNode): TreeNode {
+ const node = { ...tree };
+ node.expanded = false;
+ if (node.children) {
+ node.children = { ...node.children };
+ for (const key of Object.keys(node.children)) {
+ node.children[key] = closeNodes(node.children[key]);
+ }
+ }
+ return node;
+}
+
+export function expandNodes(tree: TreeNode, path: string[]): TreeNode {
+ let newTree = { ...tree };
+ let currentTree = newTree;
+ currentTree.expanded = true;
+ // Remove the root segment
+ const newPath = path.slice(1);
+
+ for (const segment of newPath) {
+ const node = currentTree.children?.[segment];
+ if (!node) {
+ throw new Error(`Node ${segment} not found in tree`);
+ }
+
+ const newNode = { ...node };
+ currentTree.children = { ...currentTree.children };
+ currentTree.children[segment] = newNode;
+ newNode.expanded = true;
+ currentTree = newNode;
+ }
+
+ return newTree;
+}
+
+export function isNodeExpandable(node: ScopeNode) {
+ return node.spec.nodeType === 'container';
+}
+
+export function isNodeSelectable(node: ScopeNode) {
+ return node.spec.linkType === 'scope';
+}
+
+export function getPathOfNode(scopeNodeId: string, nodes: NodesMap): string[] {
+ if (scopeNodeId === '') {
+ return [''];
+ }
+ const path = [scopeNodeId];
+ let parent = nodes[scopeNodeId]?.spec.parentName;
+ while (parent) {
+ path.unshift(parent);
+ parent = nodes[parent]?.spec.parentName;
+ }
+ path.unshift('');
+ return path;
+}
+
+export function modifyTreeNodeAtPath(tree: TreeNode, path: string[], modifier: (treeNode: TreeNode) => void) {
+ if (path.length < 1) {
+ return tree;
+ }
+
+ const newTree = { ...tree };
+ let currentNode = newTree;
+
+ if (path.length === 1 && path[0] === '') {
+ modifier(currentNode);
+ return newTree;
+ }
+
+ for (const section of path.slice(1)) {
+ if (!currentNode.children?.[section]) {
+ return newTree;
+ }
+
+ currentNode.children = { ...currentNode.children };
+ currentNode.children[section] = { ...currentNode.children[section] };
+ currentNode = currentNode.children[section];
+ }
+
+ modifier(currentNode);
+ return newTree;
+}
+
+export function treeNodeAtPath(tree: TreeNode, path: string[]) {
+ if (path.length < 1) {
+ return undefined;
+ }
+
+ if (path.length === 1 && path[0] === '') {
+ return tree;
+ }
+
+ let treeNode: TreeNode | undefined = tree;
+
+ for (const section of path.slice(1)) {
+ treeNode = treeNode.children?.[section];
+ if (!treeNode) {
+ return undefined;
+ }
+ }
+
+ return treeNode;
+}
diff --git a/public/app/features/scopes/selector/types.ts b/public/app/features/scopes/selector/types.ts
index 11d6e3c7693..d0b109ab08c 100644
--- a/public/app/features/scopes/selector/types.ts
+++ b/public/app/features/scopes/selector/types.ts
@@ -1,35 +1,16 @@
-import { Scope, ScopeNodeSpec } from '@grafana/data';
+import { Scope, ScopeNode } from '@grafana/data';
-export enum NodeReason {
- Persisted,
- Result,
-}
-
-export interface Node extends ScopeNodeSpec {
- name: string;
- reason: NodeReason;
- expandable: boolean;
- selectable: boolean;
- expanded: boolean;
- query: string;
- nodes: NodesMap;
-}
-
-export type NodesMap = Record
;
+export type NodesMap = Record;
+export type ScopesMap = Record;
export interface SelectedScope {
- scope: Scope;
- path: string[];
+ scopeId: string;
+ scopeNodeId?: string;
}
-export interface TreeScope {
- title: string;
- scopeName: string;
- path: string[];
+export interface TreeNode {
+ scopeNodeId: string;
+ expanded: boolean;
+ query: string;
+ children?: Record;
}
-
-// Sort of partial treeScope that is used as a way to say which node should be toggled.
-export type ToggleNode = { scopeName: string; path?: string[] } | { path: string[]; scopeName?: string };
-
-export type OnNodeUpdate = (path: string[], expanded: boolean, query: string) => void;
-export type OnNodeSelectToggle = (node: ToggleNode) => void;
diff --git a/public/app/features/scopes/tests/tree.test.ts b/public/app/features/scopes/tests/tree.test.ts
index c630eb9bd70..30d698d6410 100644
--- a/public/app/features/scopes/tests/tree.test.ts
+++ b/public/app/features/scopes/tests/tree.test.ts
@@ -1,7 +1,6 @@
import { config, locationService } from '@grafana/runtime';
import { ScopesService } from '../ScopesService';
-import { ScopesSelectorService } from '../selector/ScopesSelectorService';
import {
applyScopes,
@@ -22,8 +21,6 @@ import {
updateScopes,
} from './utils/actions';
import {
- expectPersistedApplicationsGrafanaNotPresent,
- expectPersistedApplicationsMimirNotPresent,
expectPersistedApplicationsMimirPresent,
expectResultApplicationsCloudNotPresent,
expectResultApplicationsCloudPresent,
@@ -39,8 +36,6 @@ import {
expectResultCloudOpsSelected,
expectScopesHeadline,
expectScopesSelectorValue,
- expectSelectedScopePath,
- expectTreeScopePath,
} from './utils/assertions';
import { getDatasource, getInstanceSettings, getMock } from './utils/mocks';
import { renderDashboard, resetScenes } from './utils/render';
@@ -58,7 +53,6 @@ describe('Tree', () => {
let fetchNodesSpy: jest.SpyInstance;
let fetchScopeSpy: jest.SpyInstance;
let scopesService: ScopesService;
- let scopesSelectorService: ScopesSelectorService;
beforeAll(() => {
config.featureToggles.scopeFilters = true;
@@ -68,8 +62,7 @@ describe('Tree', () => {
beforeEach(async () => {
const result = await renderDashboard();
scopesService = result.scopesService;
- scopesSelectorService = result.scopesSelectorService;
- fetchNodesSpy = jest.spyOn(result.client, 'fetchNode');
+ fetchNodesSpy = jest.spyOn(result.client, 'fetchNodes');
fetchScopeSpy = jest.spyOn(result.client, 'fetchScope');
});
@@ -171,8 +164,6 @@ describe('Tree', () => {
await searchScopes('grafana');
expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
expectPersistedApplicationsMimirPresent();
- expectPersistedApplicationsGrafanaNotPresent();
- expectResultApplicationsMimirNotPresent();
expectResultApplicationsGrafanaPresent();
});
@@ -182,7 +173,6 @@ describe('Tree', () => {
await selectResultApplicationsMimir();
await searchScopes('mimir');
expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
- expectPersistedApplicationsMimirNotPresent();
expectResultApplicationsMimirPresent();
});
@@ -195,8 +185,6 @@ describe('Tree', () => {
await clearScopesSearch();
expect(fetchNodesSpy).toHaveBeenCalledTimes(4);
- expectPersistedApplicationsMimirNotPresent();
- expectPersistedApplicationsGrafanaNotPresent();
expectResultApplicationsMimirPresent();
expectResultApplicationsGrafanaPresent();
});
@@ -265,28 +253,4 @@ describe('Tree', () => {
await expandResultApplicationsCloud();
expectScopesHeadline('Recommended');
});
-
- it('Updates the paths for scopes without paths on nodes fetching', async () => {
- const selectedScopeName = 'grafana';
- const unselectedScopeName = 'mimir';
- const selectedScopeNameFromOtherGroup = 'dev';
-
- await updateScopes(scopesService, [selectedScopeName, selectedScopeNameFromOtherGroup]);
- expectSelectedScopePath(scopesSelectorService, selectedScopeName, []);
- expectTreeScopePath(scopesSelectorService, selectedScopeName, []);
- expectSelectedScopePath(scopesSelectorService, unselectedScopeName, undefined);
- expectTreeScopePath(scopesSelectorService, unselectedScopeName, undefined);
- expectSelectedScopePath(scopesSelectorService, selectedScopeNameFromOtherGroup, []);
- expectTreeScopePath(scopesSelectorService, selectedScopeNameFromOtherGroup, []);
-
- await openSelector();
- await expandResultApplications();
- const expectedPath = ['', 'applications', 'applications-grafana'];
- expectSelectedScopePath(scopesSelectorService, selectedScopeName, expectedPath);
- expectTreeScopePath(scopesSelectorService, selectedScopeName, expectedPath);
- expectSelectedScopePath(scopesSelectorService, unselectedScopeName, undefined);
- expectTreeScopePath(scopesSelectorService, unselectedScopeName, undefined);
- expectSelectedScopePath(scopesSelectorService, selectedScopeNameFromOtherGroup, []);
- expectTreeScopePath(scopesSelectorService, selectedScopeNameFromOtherGroup, []);
- });
});
diff --git a/public/app/features/scopes/tests/utils/assertions.ts b/public/app/features/scopes/tests/utils/assertions.ts
index f9425179988..cab6c527c06 100644
--- a/public/app/features/scopes/tests/utils/assertions.ts
+++ b/public/app/features/scopes/tests/utils/assertions.ts
@@ -1,5 +1,3 @@
-import { ScopesSelectorService } from '../../selector/ScopesSelectorService';
-
import {
getDashboard,
getDashboardsContainer,
@@ -16,10 +14,8 @@ import {
getResultApplicationsMimirSelect,
getResultCloudDevRadio,
getResultCloudOpsRadio,
- getSelectedScope,
getSelectorInput,
getTreeHeadline,
- getTreeScope,
queryAllDashboard,
queryDashboard,
queryDashboardFolderExpand,
@@ -86,8 +82,3 @@ export const expectDashboardInDocument = (uid: string) => expectInDocument(() =>
export const expectDashboardNotInDocument = (uid: string) => expectNotInDocument(() => queryDashboard(uid));
export const expectDashboardLength = (uid: string, length: number) =>
expect(queryAllDashboard(uid)).toHaveLength(length);
-
-export const expectSelectedScopePath = (service: ScopesSelectorService, name: string, path: string[] | undefined) =>
- expect(getSelectedScope(service, name)?.path).toEqual(path);
-export const expectTreeScopePath = (service: ScopesSelectorService, name: string, path: string[] | undefined) =>
- expect(getTreeScope(service, name)?.path).toEqual(path);
diff --git a/public/app/features/scopes/tests/utils/mocks.ts b/public/app/features/scopes/tests/utils/mocks.ts
index f61e251a8bb..2482e9c2053 100644
--- a/public/app/features/scopes/tests/utils/mocks.ts
+++ b/public/app/features/scopes/tests/utils/mocks.ts
@@ -169,18 +169,17 @@ export const mocksScopeDashboardBindings: ScopeDashboardBinding[] = [
),
] as const;
-export const mocksNodes: Array = [
+export const mocksNodes: ScopeNode[] = [
{
- parent: '',
metadata: { name: 'applications' },
spec: {
nodeType: 'container',
title: 'Applications',
description: 'Application Scopes',
+ parentName: '',
},
},
{
- parent: '',
metadata: { name: 'cloud' },
spec: {
nodeType: 'container',
@@ -189,10 +188,10 @@ export const mocksNodes: Array = [
disableMultiSelect: true,
linkType: 'scope',
linkId: 'cloud',
+ parentName: '',
},
},
{
- parent: 'applications',
metadata: { name: 'applications-grafana' },
spec: {
nodeType: 'leaf',
@@ -200,10 +199,10 @@ export const mocksNodes: Array = [
description: 'Grafana',
linkType: 'scope',
linkId: 'grafana',
+ parentName: 'applications',
},
},
{
- parent: 'applications',
metadata: { name: 'applications-mimir' },
spec: {
nodeType: 'leaf',
@@ -211,10 +210,10 @@ export const mocksNodes: Array = [
description: 'Mimir',
linkType: 'scope',
linkId: 'mimir',
+ parentName: 'applications',
},
},
{
- parent: 'applications',
metadata: { name: 'applications-loki' },
spec: {
nodeType: 'leaf',
@@ -222,10 +221,10 @@ export const mocksNodes: Array = [
description: 'Loki',
linkType: 'scope',
linkId: 'loki',
+ parentName: 'applications',
},
},
{
- parent: 'applications',
metadata: { name: 'applications-tempo' },
spec: {
nodeType: 'leaf',
@@ -233,10 +232,10 @@ export const mocksNodes: Array = [
description: 'Tempo',
linkType: 'scope',
linkId: 'tempo',
+ parentName: 'applications',
},
},
{
- parent: 'applications',
metadata: { name: 'applications-cloud' },
spec: {
nodeType: 'container',
@@ -244,10 +243,10 @@ export const mocksNodes: Array = [
description: 'Application/Cloud Scopes',
linkType: 'scope',
linkId: 'cloud',
+ parentName: 'applications',
},
},
{
- parent: 'applications-cloud',
metadata: { name: 'applications-cloud-dev' },
spec: {
nodeType: 'leaf',
@@ -255,10 +254,10 @@ export const mocksNodes: Array = [
description: 'Dev',
linkType: 'scope',
linkId: 'dev',
+ parentName: 'applications-cloud',
},
},
{
- parent: 'applications-cloud',
metadata: { name: 'applications-cloud-ops' },
spec: {
nodeType: 'leaf',
@@ -266,10 +265,10 @@ export const mocksNodes: Array = [
description: 'Ops',
linkType: 'scope',
linkId: 'ops',
+ parentName: 'applications-cloud',
},
},
{
- parent: 'applications-cloud',
metadata: { name: 'applications-cloud-prod' },
spec: {
nodeType: 'leaf',
@@ -277,10 +276,10 @@ export const mocksNodes: Array = [
description: 'Prod',
linkType: 'scope',
linkId: 'prod',
+ parentName: 'applications-cloud',
},
},
{
- parent: 'cloud',
metadata: { name: 'cloud-dev' },
spec: {
nodeType: 'leaf',
@@ -288,10 +287,10 @@ export const mocksNodes: Array = [
description: 'Dev',
linkType: 'scope',
linkId: 'dev',
+ parentName: 'cloud',
},
},
{
- parent: 'cloud',
metadata: { name: 'cloud-ops' },
spec: {
nodeType: 'leaf',
@@ -299,10 +298,10 @@ export const mocksNodes: Array = [
description: 'Ops',
linkType: 'scope',
linkId: 'ops',
+ parentName: 'cloud',
},
},
{
- parent: 'cloud',
metadata: { name: 'cloud-prod' },
spec: {
nodeType: 'leaf',
@@ -310,19 +309,19 @@ export const mocksNodes: Array = [
description: 'Prod',
linkType: 'scope',
linkId: 'prod',
+ parentName: 'cloud',
},
},
{
- parent: 'cloud',
metadata: { name: 'cloud-applications' },
spec: {
nodeType: 'container',
title: 'Applications',
description: 'Cloud/Application Scopes',
+ parentName: 'cloud',
},
},
{
- parent: 'cloud-applications',
metadata: { name: 'cloud-applications-grafana' },
spec: {
nodeType: 'leaf',
@@ -330,10 +329,10 @@ export const mocksNodes: Array = [
description: 'Grafana',
linkType: 'scope',
linkId: 'grafana',
+ parentName: 'cloud-applications',
},
},
{
- parent: 'cloud-applications',
metadata: { name: 'cloud-applications-mimir' },
spec: {
nodeType: 'leaf',
@@ -341,10 +340,10 @@ export const mocksNodes: Array = [
description: 'Mimir',
linkType: 'scope',
linkId: 'mimir',
+ parentName: 'cloud-applications',
},
},
{
- parent: 'cloud-applications',
metadata: { name: 'cloud-applications-loki' },
spec: {
nodeType: 'leaf',
@@ -352,10 +351,10 @@ export const mocksNodes: Array = [
description: 'Loki',
linkType: 'scope',
linkId: 'loki',
+ parentName: 'cloud-applications',
},
},
{
- parent: 'cloud-applications',
metadata: { name: 'cloud-applications-tempo' },
spec: {
nodeType: 'leaf',
@@ -363,6 +362,7 @@ export const mocksNodes: Array = [
description: 'Tempo',
linkType: 'scope',
linkId: 'tempo',
+ parentName: 'cloud-applications',
},
},
] as const;
@@ -376,8 +376,8 @@ export const getMock = jest
if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/find/scope_node_children')) {
return {
items: mocksNodes.filter(
- ({ parent, spec: { title } }) =>
- parent === params.parent && title.toLowerCase().includes((params.query ?? '').toLowerCase())
+ ({ spec: { title, parentName } }) =>
+ parentName === params.parent && title.toLowerCase().includes((params.query ?? '').toLowerCase())
),
};
}
diff --git a/public/app/features/scopes/tests/utils/selectors.ts b/public/app/features/scopes/tests/utils/selectors.ts
index 79d0b7ca3d2..85d86968ba5 100644
--- a/public/app/features/scopes/tests/utils/selectors.ts
+++ b/public/app/features/scopes/tests/utils/selectors.ts
@@ -1,17 +1,16 @@
import { screen } from '@testing-library/react';
import { ScopesService } from '../../ScopesService';
-import { ScopesSelectorService } from '../../selector/ScopesSelectorService';
const selectors = {
tree: {
recentScopesSection: 'scopes-selector-recent-scopes-section',
search: 'scopes-tree-search',
headline: 'scopes-tree-headline',
- select: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-checkbox`,
- radio: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-radio`,
- expand: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-expand`,
- title: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-title`,
+ select: (nodeId: string) => `scopes-tree-${nodeId}-checkbox`,
+ radio: (nodeId: string) => `scopes-tree-${nodeId}-radio`,
+ expand: (nodeId: string) => `scopes-tree-${nodeId}-expand`,
+ title: (nodeId: string) => `scopes-tree-${nodeId}-title`,
},
selector: {
input: 'scopes-selector-input',
@@ -64,43 +63,33 @@ export const getNotFoundForFilterClear = () => screen.getByTestId(selectors.dash
export const getTreeSearch = () => screen.getByTestId(selectors.tree.search);
export const getTreeHeadline = () => screen.getByTestId(selectors.tree.headline);
-export const getResultApplicationsExpand = () => screen.getByTestId(selectors.tree.expand('applications', 'result'));
+export const getResultApplicationsExpand = () => screen.getByTestId(selectors.tree.expand('applications'));
export const queryResultApplicationsGrafanaSelect = () =>
- screen.queryByTestId(selectors.tree.select('applications-grafana', 'result'));
+ screen.queryByTestId(selectors.tree.select('applications-grafana'));
export const getResultApplicationsGrafanaSelect = () =>
- screen.getByTestId(selectors.tree.select('applications-grafana', 'result'));
+ screen.getByTestId(selectors.tree.select('applications-grafana'));
export const queryPersistedApplicationsGrafanaSelect = () =>
- screen.queryByTestId(selectors.tree.select('applications-grafana', 'persisted'));
+ screen.queryByTestId(selectors.tree.select('applications-grafana'));
export const getPersistedApplicationsGrafanaSelect = () =>
- screen.getByTestId(selectors.tree.select('applications-grafana', 'persisted'));
+ screen.getByTestId(selectors.tree.select('applications-grafana'));
export const queryResultApplicationsMimirSelect = () =>
- screen.queryByTestId(selectors.tree.select('applications-mimir', 'result'));
+ screen.queryByTestId(selectors.tree.select('applications-mimir'));
export const getResultApplicationsMimirSelect = () =>
- screen.getByTestId(selectors.tree.select('applications-mimir', 'result'));
+ screen.getByTestId(selectors.tree.select('applications-mimir'));
export const queryPersistedApplicationsMimirSelect = () =>
- screen.queryByTestId(selectors.tree.select('applications-mimir', 'persisted'));
+ screen.queryByTestId(selectors.tree.select('applications-mimir'));
export const getPersistedApplicationsMimirSelect = () =>
- screen.getByTestId(selectors.tree.select('applications-mimir', 'persisted'));
+ screen.getByTestId(selectors.tree.select('applications-mimir'));
export const queryResultApplicationsCloudSelect = () =>
- screen.queryByTestId(selectors.tree.select('applications-cloud', 'result'));
-export const getResultApplicationsCloudSelect = () =>
- screen.getByTestId(selectors.tree.select('applications-cloud', 'result'));
-export const getResultApplicationsCloudExpand = () =>
- screen.getByTestId(selectors.tree.expand('applications-cloud', 'result'));
+ screen.queryByTestId(selectors.tree.select('applications-cloud'));
+export const getResultApplicationsCloudSelect = () => screen.getByTestId(selectors.tree.select('applications-cloud'));
+export const getResultApplicationsCloudExpand = () => screen.getByTestId(selectors.tree.expand('applications-cloud'));
export const getResultApplicationsCloudDevSelect = () =>
- screen.getByTestId(selectors.tree.select('applications-cloud-dev', 'result'));
+ screen.getByTestId(selectors.tree.select('applications-cloud-dev'));
-export const getResultCloudSelect = () => screen.getByTestId(selectors.tree.select('cloud', 'result'));
-export const getResultCloudExpand = () => screen.getByTestId(selectors.tree.expand('cloud', 'result'));
-export const getResultCloudDevRadio = () =>
- screen.getByTestId(selectors.tree.radio('cloud-dev', 'result'));
-export const getResultCloudOpsRadio = () =>
- screen.getByTestId(selectors.tree.radio('cloud-ops', 'result'));
+export const getResultCloudSelect = () => screen.getByTestId(selectors.tree.select('cloud'));
+export const getResultCloudExpand = () => screen.getByTestId(selectors.tree.expand('cloud'));
+export const getResultCloudDevRadio = () => screen.getByTestId(selectors.tree.radio('cloud-dev'));
+export const getResultCloudOpsRadio = () => screen.getByTestId(selectors.tree.radio('cloud-ops'));
export const getListOfScopes = (service: ScopesService) => service.state.value;
-export const getListOfSelectedScopes = (service: ScopesSelectorService) => service.state.selectedScopes;
-export const getListOfTreeScopes = (service: ScopesSelectorService) => service.state.treeScopes;
-export const getSelectedScope = (service: ScopesSelectorService, name: string) =>
- getListOfSelectedScopes(service)?.find((selectedScope) => selectedScope.scope.metadata.name === name);
-export const getTreeScope = (service: ScopesSelectorService, name: string) =>
- getListOfTreeScopes(service)?.find((treeScope) => treeScope.scopeName === name);