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:
@@ -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
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user