Scopes: Add frontend support for disableSubScopeSelection (#115323)

* Add devenv scopes and don't display the switch button

* Add tests

* Remove redundant test
This commit is contained in:
Tobias Skarhed
2025-12-15 13:14:44 +00:00
committed by GitHub
parent 00a6e1781f
commit 75caaccad4
7 changed files with 249 additions and 18 deletions
+1
View File
@@ -210,6 +210,7 @@ navigationTree:
url: /d/UTv--wqMk
scope: shoe-org
subScope: apparel
disableSubScopeSelection: true
children:
- name: apparel-product-overview
title: Product Overview
+21 -17
View File
@@ -77,22 +77,24 @@ type TreeNode struct {
}
type NavigationConfig struct {
URL string `yaml:"url"` // URL path (e.g., /d/abc123 or /explore)
Scope string `yaml:"scope"` // Required scope
SubScope string `yaml:"subScope"` // Optional subScope for hierarchical navigation
Title string `yaml:"title"` // Display title
Groups []string `yaml:"groups"` // Optional groups for categorization
URL string `yaml:"url"` // URL path (e.g., /d/abc123 or /explore)
Scope string `yaml:"scope"` // Required scope
SubScope string `yaml:"subScope"` // Optional subScope for hierarchical navigation
Title string `yaml:"title"` // Display title
Groups []string `yaml:"groups"` // Optional groups for categorization
DisableSubScopeSelection bool `yaml:"disableSubScopeSelection"` // Makes the subscope not selectable
}
// NavigationTreeNode represents a node in the navigation tree structure
type NavigationTreeNode struct {
Name string `yaml:"name"`
Title string `yaml:"title"`
URL string `yaml:"url"`
Scope string `yaml:"scope"`
SubScope string `yaml:"subScope,omitempty"`
Groups []string `yaml:"groups,omitempty"`
Children []NavigationTreeNode `yaml:"children,omitempty"`
Name string `yaml:"name"`
Title string `yaml:"title"`
URL string `yaml:"url"`
Scope string `yaml:"scope"`
SubScope string `yaml:"subScope,omitempty"`
Groups []string `yaml:"groups,omitempty"`
DisableSubScopeSelection bool `yaml:"disableSubScopeSelection,omitempty"`
Children []NavigationTreeNode `yaml:"children,omitempty"`
}
// Helper function to convert ScopeFilterConfig to v0alpha1.ScopeFilter
@@ -313,8 +315,9 @@ func (c *Client) createScopeNavigation(name string, nav NavigationConfig) error
prefixedScope := prefix + "-" + nav.Scope
spec := v0alpha1.ScopeNavigationSpec{
URL: nav.URL,
Scope: prefixedScope,
URL: nav.URL,
Scope: prefixedScope,
DisableSubScopeSelection: nav.DisableSubScopeSelection,
}
if nav.SubScope != "" {
@@ -404,9 +407,10 @@ func treeToNavigations(node NavigationTreeNode, parentPath []string, dashboardCo
// Create navigation for this node
nav := NavigationConfig{
URL: url,
Scope: node.Scope,
Title: node.Title,
URL: url,
Scope: node.Scope,
Title: node.Title,
DisableSubScopeSelection: node.DisableSubScopeSelection,
}
if node.SubScope != "" {
nav.SubScope = node.SubScope
@@ -887,4 +887,161 @@ describe('ScopesDashboardsService', () => {
expect(service.state.navScopePath).toEqual(['mimir']);
});
});
describe('disableSubScopeSelection', () => {
it('should set disableSubScopeSelection on folder when navigation has it set to true', async () => {
const mockNavigations: ScopeNavigation[] = [
{
spec: {
url: '/d/dashboard1',
scope: 'scope1',
subScope: 'subScope1',
disableSubScopeSelection: true,
},
status: {
title: 'Test Navigation',
},
metadata: {
name: 'nav1',
},
},
];
mockApiClient.fetchScopeNavigations.mockResolvedValue(mockNavigations);
await service.fetchDashboards(['scope1']);
// Find the folder created for this subScope
const folderKey = Object.keys(service.state.folders[''].folders).find((key) => key.includes('subScope1'));
expect(folderKey).toBeDefined();
if (folderKey) {
const folder = service.state.folders[''].folders[folderKey];
expect(folder.disableSubScopeSelection).toBe(true);
expect(folder.subScopeName).toBe('subScope1');
}
});
it('should set disableSubScopeSelection to false when navigation has it set to false', async () => {
const mockNavigations: ScopeNavigation[] = [
{
spec: {
url: '/d/dashboard1',
scope: 'scope1',
subScope: 'subScope1',
disableSubScopeSelection: false,
},
status: {
title: 'Test Navigation',
},
metadata: {
name: 'nav1',
},
},
];
mockApiClient.fetchScopeNavigations.mockResolvedValue(mockNavigations);
await service.fetchDashboards(['scope1']);
const folderKey = Object.keys(service.state.folders[''].folders).find((key) => key.includes('subScope1'));
expect(folderKey).toBeDefined();
if (folderKey) {
const folder = service.state.folders[''].folders[folderKey];
expect(folder.disableSubScopeSelection).toBe(false);
expect(folder.subScopeName).toBe('subScope1');
}
});
it('should set disableSubScopeSelection to undefined when navigation does not have it', async () => {
const mockNavigations: ScopeNavigation[] = [
{
spec: {
url: '/d/dashboard1',
scope: 'scope1',
subScope: 'subScope1',
},
status: {
title: 'Test Navigation',
},
metadata: {
name: 'nav1',
},
},
];
mockApiClient.fetchScopeNavigations.mockResolvedValue(mockNavigations);
await service.fetchDashboards(['scope1']);
const folderKey = Object.keys(service.state.folders[''].folders).find((key) => key.includes('subScope1'));
expect(folderKey).toBeDefined();
if (folderKey) {
const folder = service.state.folders[''].folders[folderKey];
expect(folder.disableSubScopeSelection).toBeUndefined();
expect(folder.subScopeName).toBe('subScope1');
}
});
it('should handle multiple navigations with different disableSubScopeSelection values', async () => {
const mockNavigations: ScopeNavigation[] = [
{
spec: {
url: '/d/dashboard1',
scope: 'scope1',
subScope: 'subScope1',
disableSubScopeSelection: true,
},
status: {
title: 'Disabled Navigation',
},
metadata: {
name: 'nav1',
},
},
{
spec: {
url: '/d/dashboard2',
scope: 'scope1',
subScope: 'subScope2',
disableSubScopeSelection: false,
},
status: {
title: 'Enabled Navigation',
},
metadata: {
name: 'nav2',
},
},
{
spec: {
url: '/d/dashboard3',
scope: 'scope1',
subScope: 'subScope3',
},
status: {
title: 'Default Navigation',
},
metadata: {
name: 'nav3',
},
},
];
mockApiClient.fetchScopeNavigations.mockResolvedValue(mockNavigations);
await service.fetchDashboards(['scope1']);
const folders = service.state.folders[''].folders;
const folder1Key = Object.keys(folders).find((key) => key.includes('subScope1'));
const folder2Key = Object.keys(folders).find((key) => key.includes('subScope2'));
const folder3Key = Object.keys(folders).find((key) => key.includes('subScope3'));
expect(folder1Key).toBeDefined();
expect(folder2Key).toBeDefined();
expect(folder3Key).toBeDefined();
expect(folders[folder1Key!].disableSubScopeSelection).toBe(true);
expect(folders[folder2Key!].disableSubScopeSelection).toBe(false);
expect(folders[folder3Key!].disableSubScopeSelection).toBeUndefined();
});
});
});
@@ -10,6 +10,7 @@ import { ScopesServiceBase } from '../ScopesServiceBase';
import { buildSubScopePath, isCurrentPath } from './scopeNavgiationUtils';
import {
ScopeNavigation,
ScopeNavigationSpec,
SuggestedNavigationsFolder,
SuggestedNavigationsFoldersMap,
SuggestedNavigationsMap,
@@ -386,12 +387,17 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
// All folders with the same subScope will load the same content when expanded
const folderKey = `${subScope}-${navigation.metadata.name}`;
if (!rootNode.folders[folderKey]) {
let disableSubScopeSelection: ScopeNavigationSpec['disableSubScopeSelection'] = undefined;
if ('disableSubScopeSelection' in navigation.spec) {
disableSubScopeSelection = navigation.spec.disableSubScopeSelection;
}
rootNode.folders[folderKey] = {
title: navigationTitle,
expanded,
folders: {},
suggestedNavigations: {},
subScopeName: subScope,
disableSubScopeSelection,
};
}
if (expanded && !rootNode.folders[folderKey].expanded) {
@@ -287,4 +287,63 @@ describe('ScopesDashboardsTreeFolderItem', () => {
// The component checks for scopesSelectorService existence before calling setNavigationScope
expect(mockScopesDashboardsService.setNavigationScope).not.toHaveBeenCalled();
});
describe('disableSubScopeSelection', () => {
it('does not show exchange icon when disableSubScopeSelection is true', () => {
const folder = createMockFolder({
subScopeName: 'subScope1',
disableSubScopeSelection: true,
});
render(
<ScopesDashboardsTreeFolderItem
folder={folder}
folderPath={['']}
folders={createMockFolders}
onFolderUpdate={mockOnFolderUpdate}
/>
);
const exchangeButtons = screen.queryAllByRole('button', { name: /change root scope/i });
expect(exchangeButtons).toHaveLength(0);
});
it('shows exchange icon when disableSubScopeSelection is false', () => {
const folder = createMockFolder({
subScopeName: 'subScope1',
disableSubScopeSelection: false,
});
render(
<ScopesDashboardsTreeFolderItem
folder={folder}
folderPath={['']}
folders={createMockFolders}
onFolderUpdate={mockOnFolderUpdate}
/>
);
const exchangeButton = screen.getByRole('button', { name: /change root scope/i });
expect(exchangeButton).toBeInTheDocument();
});
it('shows exchange icon when disableSubScopeSelection is undefined', () => {
const folder = createMockFolder({
subScopeName: 'subScope1',
disableSubScopeSelection: undefined,
});
render(
<ScopesDashboardsTreeFolderItem
folder={folder}
folderPath={['']}
folders={createMockFolders}
onFolderUpdate={mockOnFolderUpdate}
/>
);
const exchangeButton = screen.getByRole('button', { name: /change root scope/i });
expect(exchangeButton).toBeInTheDocument();
});
});
});
@@ -48,7 +48,7 @@ export function ScopesDashboardsTreeFolderItem({
{folder.loading && <Spinner inline size="sm" className={styles.loadingIcon} />}
</button>
{folder.subScopeName && (
{folder.subScopeName && !folder.disableSubScopeSelection && (
<IconButton
className={styles.exchangeIcon}
tooltip={t('scopes.dashboards.exchange', 'Change root scope to {{scope}}', {
@@ -5,6 +5,9 @@ export interface ScopeNavigationSpec {
url: string;
scope: string;
subScope?: string;
preLoadSubScopeChildren?: boolean;
expandOnLoad?: boolean;
disableSubScopeSelection?: boolean;
}
export interface ScopeNavigationStatus {
@@ -40,6 +43,7 @@ export interface SuggestedNavigationsFolder {
suggestedNavigations: SuggestedNavigationsMap;
subScopeName?: string;
loading?: boolean;
disableSubScopeSelection?: boolean;
}
export type SuggestedNavigationsFoldersMap = Record<string, SuggestedNavigationsFolder>;