diff --git a/devenv/scopes/scopes-config.yaml b/devenv/scopes/scopes-config.yaml index 36fd6645fa9..d18679f6dcd 100644 --- a/devenv/scopes/scopes-config.yaml +++ b/devenv/scopes/scopes-config.yaml @@ -210,6 +210,7 @@ navigationTree: url: /d/UTv--wqMk scope: shoe-org subScope: apparel + disableSubScopeSelection: true children: - name: apparel-product-overview title: Product Overview diff --git a/devenv/scopes/scopes.go b/devenv/scopes/scopes.go index 335540cbef5..b252072c398 100644 --- a/devenv/scopes/scopes.go +++ b/devenv/scopes/scopes.go @@ -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 diff --git a/public/app/features/scopes/dashboards/ScopesDashboardsService.test.ts b/public/app/features/scopes/dashboards/ScopesDashboardsService.test.ts index ca621106f81..082c8c790cc 100644 --- a/public/app/features/scopes/dashboards/ScopesDashboardsService.test.ts +++ b/public/app/features/scopes/dashboards/ScopesDashboardsService.test.ts @@ -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(); + }); + }); }); diff --git a/public/app/features/scopes/dashboards/ScopesDashboardsService.ts b/public/app/features/scopes/dashboards/ScopesDashboardsService.ts index 96518cba5f7..4d6c686280d 100644 --- a/public/app/features/scopes/dashboards/ScopesDashboardsService.ts +++ b/public/app/features/scopes/dashboards/ScopesDashboardsService.ts @@ -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 { // 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( + + ); + + 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( + + ); + + 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( + + ); + + const exchangeButton = screen.getByRole('button', { name: /change root scope/i }); + expect(exchangeButton).toBeInTheDocument(); + }); + }); }); diff --git a/public/app/features/scopes/dashboards/ScopesDashboardsTreeFolderItem.tsx b/public/app/features/scopes/dashboards/ScopesDashboardsTreeFolderItem.tsx index d6846f432ac..e4029d82b5b 100644 --- a/public/app/features/scopes/dashboards/ScopesDashboardsTreeFolderItem.tsx +++ b/public/app/features/scopes/dashboards/ScopesDashboardsTreeFolderItem.tsx @@ -48,7 +48,7 @@ export function ScopesDashboardsTreeFolderItem({ {folder.loading && } - {folder.subScopeName && ( + {folder.subScopeName && !folder.disableSubScopeSelection && ( ;