Scopes: ScopesNavigation preload functionality (#115354)
* Add devenv configs * Initial preload functionality * Remove support for expandOnLoad * Add tests * Remove unnecessary go code
This commit is contained in:
@@ -125,6 +125,7 @@ navigationTree:
|
||||
url: /d/_5rDmaQiz
|
||||
scope: shoe-org
|
||||
subScope: shoes
|
||||
preLoadSubScopeChildren: true
|
||||
children:
|
||||
- name: shoes-overview
|
||||
title: Overview
|
||||
@@ -141,6 +142,7 @@ navigationTree:
|
||||
url: /d/edediimbjhdz4b
|
||||
scope: shoes
|
||||
subScope: frontend
|
||||
preLoadSubScopeChildren: true
|
||||
children:
|
||||
- name: frontend-api
|
||||
title: API Metrics
|
||||
|
||||
+10
-6
@@ -83,6 +83,7 @@ type NavigationConfig struct {
|
||||
Title string `yaml:"title"` // Display title
|
||||
Groups []string `yaml:"groups"` // Optional groups for categorization
|
||||
DisableSubScopeSelection bool `yaml:"disableSubScopeSelection"` // Makes the subscope not selectable
|
||||
PreLoadSubScopeChildren bool `yaml:"preLoadSubScopeChildren"` // Preload children of subScope without updating UI
|
||||
}
|
||||
|
||||
// NavigationTreeNode represents a node in the navigation tree structure
|
||||
@@ -94,6 +95,7 @@ type NavigationTreeNode struct {
|
||||
SubScope string `yaml:"subScope,omitempty"`
|
||||
Groups []string `yaml:"groups,omitempty"`
|
||||
DisableSubScopeSelection bool `yaml:"disableSubScopeSelection,omitempty"`
|
||||
PreLoadSubScopeChildren bool `yaml:"preLoadSubScopeChildren,omitempty"` // Preload children of subScope without updating UI
|
||||
Children []NavigationTreeNode `yaml:"children,omitempty"`
|
||||
}
|
||||
|
||||
@@ -318,6 +320,7 @@ func (c *Client) createScopeNavigation(name string, nav NavigationConfig) error
|
||||
URL: nav.URL,
|
||||
Scope: prefixedScope,
|
||||
DisableSubScopeSelection: nav.DisableSubScopeSelection,
|
||||
PreLoadSubScopeChildren: nav.PreLoadSubScopeChildren,
|
||||
}
|
||||
|
||||
if nav.SubScope != "" {
|
||||
@@ -353,14 +356,14 @@ func (c *Client) createScopeNavigation(name string, nav NavigationConfig) error
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the created resource to retrieve its resourceVersion for status update
|
||||
createdNav, err := c.getScopeNavigation(prefixedName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get created navigation: %w", err)
|
||||
}
|
||||
|
||||
// Update status in a second request (status is a subresource)
|
||||
if nav.Title != "" || len(nav.Groups) > 0 {
|
||||
// Get the created resource to retrieve its resourceVersion and existing spec
|
||||
createdNav, err := c.getScopeNavigation(prefixedName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get created navigation: %w", err)
|
||||
}
|
||||
|
||||
statusResource := v0alpha1.ScopeNavigation{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: apiVersion,
|
||||
@@ -411,6 +414,7 @@ func treeToNavigations(node NavigationTreeNode, parentPath []string, dashboardCo
|
||||
Scope: node.Scope,
|
||||
Title: node.Title,
|
||||
DisableSubScopeSelection: node.DisableSubScopeSelection,
|
||||
PreLoadSubScopeChildren: node.PreLoadSubScopeChildren,
|
||||
}
|
||||
if node.SubScope != "" {
|
||||
nav.SubScope = node.SubScope
|
||||
|
||||
@@ -888,6 +888,454 @@ describe('ScopesDashboardsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('preLoadSubScopeChildren', () => {
|
||||
beforeEach(() => {
|
||||
config.featureToggles.useScopesNavigationEndpoint = true;
|
||||
(locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/' } as Location);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.featureToggles.useScopesNavigationEndpoint = false;
|
||||
});
|
||||
|
||||
it('should set preLoadSubScopeChildren on folder when navigation has it set to true', async () => {
|
||||
const mockNavigations: ScopeNavigation[] = [
|
||||
{
|
||||
spec: {
|
||||
url: '/d/dashboard1',
|
||||
scope: 'scope1',
|
||||
subScope: 'subScope1',
|
||||
preLoadSubScopeChildren: true,
|
||||
},
|
||||
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.preLoadSubScopeChildren).toBe(true);
|
||||
expect(folder.subScopeName).toBe('subScope1');
|
||||
}
|
||||
});
|
||||
|
||||
it('should set preLoadSubScopeChildren to false when navigation has it set to false', async () => {
|
||||
const mockNavigations: ScopeNavigation[] = [
|
||||
{
|
||||
spec: {
|
||||
url: '/d/dashboard1',
|
||||
scope: 'scope1',
|
||||
subScope: 'subScope1',
|
||||
preLoadSubScopeChildren: 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.preLoadSubScopeChildren).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should set preLoadSubScopeChildren 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.preLoadSubScopeChildren).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should automatically fetch subScope items for folders with preLoadSubScopeChildren set to true', async () => {
|
||||
const mockNavigations: ScopeNavigation[] = [
|
||||
{
|
||||
spec: {
|
||||
url: '/d/dashboard1',
|
||||
scope: 'scope1',
|
||||
subScope: 'mimir',
|
||||
preLoadSubScopeChildren: true,
|
||||
},
|
||||
status: {
|
||||
title: 'Mimir Dashboards',
|
||||
},
|
||||
metadata: {
|
||||
name: 'nav1',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Mock items returned when fetching 'mimir' subScope
|
||||
const mimirItems: ScopeNavigation[] = [
|
||||
{
|
||||
metadata: { name: 'mimir-item-1' },
|
||||
spec: {
|
||||
scope: 'mimir',
|
||||
url: '/d/mimir-dashboard-1',
|
||||
},
|
||||
status: {
|
||||
title: 'Mimir Dashboard 1',
|
||||
groups: ['General'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockApiClient.fetchScopeNavigations.mockImplementation((scopeNames: string[]) => {
|
||||
if (scopeNames.includes('scope1')) {
|
||||
return Promise.resolve(mockNavigations);
|
||||
}
|
||||
if (scopeNames.includes('mimir')) {
|
||||
return Promise.resolve(mimirItems);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
|
||||
await service.fetchDashboards(['scope1']);
|
||||
|
||||
// Wait for the preload to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
// Verify that fetchScopeNavigations was called for the subScope
|
||||
expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['mimir']);
|
||||
|
||||
// Verify the folder now has content from the preloaded items
|
||||
const folderKey = Object.keys(service.state.folders[''].folders).find((key) => key.includes('mimir'));
|
||||
expect(folderKey).toBeDefined();
|
||||
|
||||
if (folderKey) {
|
||||
const folder = service.state.folders[''].folders[folderKey];
|
||||
// The preloaded items should be in the folder
|
||||
expect(folder.folders['General']).toBeDefined();
|
||||
expect(folder.folders['General'].suggestedNavigations['/d/mimir-dashboard-1']).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should not fetch subScope items for folders without preLoadSubScopeChildren', async () => {
|
||||
const mockNavigations: ScopeNavigation[] = [
|
||||
{
|
||||
spec: {
|
||||
url: '/d/dashboard1',
|
||||
scope: 'scope1',
|
||||
subScope: 'mimir',
|
||||
// preLoadSubScopeChildren is not set
|
||||
},
|
||||
status: {
|
||||
title: 'Mimir Dashboards',
|
||||
},
|
||||
metadata: {
|
||||
name: 'nav1',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockApiClient.fetchScopeNavigations.mockResolvedValue(mockNavigations);
|
||||
await service.fetchDashboards(['scope1']);
|
||||
|
||||
// Wait to ensure no additional fetch happens
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
// Verify that fetchScopeNavigations was only called once (for the initial fetch)
|
||||
expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledTimes(1);
|
||||
expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['scope1']);
|
||||
});
|
||||
|
||||
it('should recursively preload nested folders with preLoadSubScopeChildren', async () => {
|
||||
const mockNavigations: ScopeNavigation[] = [
|
||||
{
|
||||
spec: {
|
||||
url: '/d/dashboard1',
|
||||
scope: 'scope1',
|
||||
subScope: 'level1',
|
||||
preLoadSubScopeChildren: true,
|
||||
},
|
||||
status: {
|
||||
title: 'Level 1 Folder',
|
||||
},
|
||||
metadata: {
|
||||
name: 'nav1',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Level 1 items include a folder with preLoadSubScopeChildren
|
||||
const level1Items: ScopeNavigation[] = [
|
||||
{
|
||||
metadata: { name: 'level2-nav' },
|
||||
spec: {
|
||||
scope: 'level1',
|
||||
subScope: 'level2',
|
||||
url: '/d/level2-dashboard',
|
||||
preLoadSubScopeChildren: true,
|
||||
},
|
||||
status: {
|
||||
title: 'Level 2 Folder',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Level 2 items
|
||||
const level2Items: ScopeNavigation[] = [
|
||||
{
|
||||
metadata: { name: 'level2-item-1' },
|
||||
spec: {
|
||||
scope: 'level2',
|
||||
url: '/d/level2-dashboard-1',
|
||||
},
|
||||
status: {
|
||||
title: 'Level 2 Dashboard 1',
|
||||
groups: ['Deep'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockApiClient.fetchScopeNavigations.mockImplementation((scopeNames: string[]) => {
|
||||
if (scopeNames.includes('scope1')) {
|
||||
return Promise.resolve(mockNavigations);
|
||||
}
|
||||
if (scopeNames.includes('level1')) {
|
||||
return Promise.resolve(level1Items);
|
||||
}
|
||||
if (scopeNames.includes('level2')) {
|
||||
return Promise.resolve(level2Items);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
|
||||
await service.fetchDashboards(['scope1']);
|
||||
|
||||
// Wait for all preloads to complete (need to wait for both levels)
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
// Verify all levels were fetched
|
||||
expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['level1']);
|
||||
expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['level2']);
|
||||
});
|
||||
|
||||
it('should handle multiple folders with preLoadSubScopeChildren', async () => {
|
||||
const mockNavigations: ScopeNavigation[] = [
|
||||
{
|
||||
spec: {
|
||||
url: '/d/dashboard1',
|
||||
scope: 'scope1',
|
||||
subScope: 'mimir',
|
||||
preLoadSubScopeChildren: true,
|
||||
},
|
||||
status: {
|
||||
title: 'Mimir Dashboards',
|
||||
},
|
||||
metadata: {
|
||||
name: 'nav1',
|
||||
},
|
||||
},
|
||||
{
|
||||
spec: {
|
||||
url: '/d/dashboard2',
|
||||
scope: 'scope1',
|
||||
subScope: 'loki',
|
||||
preLoadSubScopeChildren: true,
|
||||
},
|
||||
status: {
|
||||
title: 'Loki Dashboards',
|
||||
},
|
||||
metadata: {
|
||||
name: 'nav2',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mimirItems: ScopeNavigation[] = [
|
||||
{
|
||||
metadata: { name: 'mimir-item-1' },
|
||||
spec: { scope: 'mimir', url: '/d/mimir-dashboard-1' },
|
||||
status: { title: 'Mimir Dashboard 1', groups: ['General'] },
|
||||
},
|
||||
];
|
||||
|
||||
const lokiItems: ScopeNavigation[] = [
|
||||
{
|
||||
metadata: { name: 'loki-item-1' },
|
||||
spec: { scope: 'loki', url: '/d/loki-dashboard-1' },
|
||||
status: { title: 'Loki Dashboard 1', groups: ['General'] },
|
||||
},
|
||||
];
|
||||
|
||||
mockApiClient.fetchScopeNavigations.mockImplementation((scopeNames: string[]) => {
|
||||
if (scopeNames.includes('scope1')) {
|
||||
return Promise.resolve(mockNavigations);
|
||||
}
|
||||
if (scopeNames.includes('mimir')) {
|
||||
return Promise.resolve(mimirItems);
|
||||
}
|
||||
if (scopeNames.includes('loki')) {
|
||||
return Promise.resolve(lokiItems);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
|
||||
await service.fetchDashboards(['scope1']);
|
||||
|
||||
// Wait for preloads to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
// Verify both subScopes were fetched
|
||||
expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['mimir']);
|
||||
expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['loki']);
|
||||
});
|
||||
|
||||
it('should handle preload errors gracefully', async () => {
|
||||
const mockNavigations: ScopeNavigation[] = [
|
||||
{
|
||||
spec: {
|
||||
url: '/d/dashboard1',
|
||||
scope: 'scope1',
|
||||
subScope: 'failing-scope',
|
||||
preLoadSubScopeChildren: true,
|
||||
},
|
||||
status: {
|
||||
title: 'Failing Folder',
|
||||
},
|
||||
metadata: {
|
||||
name: 'nav1',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockApiClient.fetchScopeNavigations.mockImplementation((scopeNames: string[]) => {
|
||||
if (scopeNames.includes('scope1')) {
|
||||
return Promise.resolve(mockNavigations);
|
||||
}
|
||||
if (scopeNames.includes('failing-scope')) {
|
||||
return Promise.reject(new Error('Network error'));
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
|
||||
// Should not throw
|
||||
await service.fetchDashboards(['scope1']);
|
||||
|
||||
// Wait for preload to attempt
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
// Verify the folder was still created (even though preload failed)
|
||||
const folderKey = Object.keys(service.state.folders[''].folders).find((key) => key.includes('failing-scope'));
|
||||
expect(folderKey).toBeDefined();
|
||||
});
|
||||
|
||||
it('should preload children after fetching subScope items when parent folder has items with preLoadSubScopeChildren', async () => {
|
||||
const mockNavigations: ScopeNavigation[] = [
|
||||
{
|
||||
spec: {
|
||||
url: '/d/dashboard1',
|
||||
scope: 'scope1',
|
||||
subScope: 'parent',
|
||||
preLoadSubScopeChildren: true,
|
||||
},
|
||||
status: {
|
||||
title: 'Parent Folder',
|
||||
},
|
||||
metadata: {
|
||||
name: 'nav1',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Parent items include a child with preLoadSubScopeChildren
|
||||
const parentItems: ScopeNavigation[] = [
|
||||
{
|
||||
metadata: { name: 'child-nav' },
|
||||
spec: {
|
||||
scope: 'parent',
|
||||
subScope: 'child',
|
||||
url: '/d/child-dashboard',
|
||||
preLoadSubScopeChildren: true,
|
||||
},
|
||||
status: {
|
||||
title: 'Child Folder',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const childItems: ScopeNavigation[] = [
|
||||
{
|
||||
metadata: { name: 'child-item-1' },
|
||||
spec: {
|
||||
scope: 'child',
|
||||
url: '/d/child-dashboard-1',
|
||||
},
|
||||
status: {
|
||||
title: 'Child Dashboard 1',
|
||||
groups: ['Nested'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockApiClient.fetchScopeNavigations.mockImplementation((scopeNames: string[]) => {
|
||||
if (scopeNames.includes('scope1')) {
|
||||
return Promise.resolve(mockNavigations);
|
||||
}
|
||||
if (scopeNames.includes('parent')) {
|
||||
return Promise.resolve(parentItems);
|
||||
}
|
||||
if (scopeNames.includes('child')) {
|
||||
return Promise.resolve(childItems);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
|
||||
await service.fetchDashboards(['scope1']);
|
||||
|
||||
// Wait for cascading preloads
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
|
||||
// Verify the chain of preloads occurred
|
||||
expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['scope1']);
|
||||
expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['parent']);
|
||||
expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['child']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disableSubScopeSelection', () => {
|
||||
it('should set disableSubScopeSelection on folder when navigation has it set to true', async () => {
|
||||
const mockNavigations: ScopeNavigation[] = [
|
||||
|
||||
@@ -253,9 +253,14 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
|
||||
...currentFilteredFolder.suggestedNavigations,
|
||||
...rootSubScopeFolder.suggestedNavigations,
|
||||
};
|
||||
}
|
||||
|
||||
this.updateState({ folders, filteredFolders });
|
||||
this.updateState({ folders, filteredFolders });
|
||||
|
||||
// Preload children for any newly added folders with preLoadSubScopeChildren
|
||||
this.preloadSubScopeChildren(rootSubScopeFolder.folders, path);
|
||||
} else {
|
||||
this.updateState({ folders, filteredFolders });
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to get a folder at a given path
|
||||
@@ -316,6 +321,25 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
|
||||
loading: false,
|
||||
drawerOpened: res.length > 0,
|
||||
});
|
||||
|
||||
// Preload children for folders with preLoadSubScopeChildren set
|
||||
this.preloadSubScopeChildren(folders[''].folders, ['']);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Preloads children for folders that have preLoadSubScopeChildren set to true.
|
||||
* This fetches the subScope items immediately when the navigation is first loaded,
|
||||
* or when a parent subScope folder is fetched.
|
||||
* @param foldersToCheck - The folders to check for preLoadSubScopeChildren
|
||||
* @param basePath - The path to prepend when building the full path for each folder
|
||||
*/
|
||||
private preloadSubScopeChildren = (foldersToCheck: SuggestedNavigationsFoldersMap, basePath: string[]) => {
|
||||
for (const [folderKey, folder] of Object.entries(foldersToCheck)) {
|
||||
if (folder.preLoadSubScopeChildren && folder.subScopeName) {
|
||||
const path = [...basePath, folderKey];
|
||||
this.fetchSubScopeItems(path, folder.subScopeName);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -391,6 +415,10 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
|
||||
if ('disableSubScopeSelection' in navigation.spec) {
|
||||
disableSubScopeSelection = navigation.spec.disableSubScopeSelection;
|
||||
}
|
||||
let preLoadSubScopeChildren: ScopeNavigationSpec['preLoadSubScopeChildren'] = undefined;
|
||||
if ('preLoadSubScopeChildren' in navigation.spec) {
|
||||
preLoadSubScopeChildren = navigation.spec.preLoadSubScopeChildren;
|
||||
}
|
||||
rootNode.folders[folderKey] = {
|
||||
title: navigationTitle,
|
||||
expanded,
|
||||
@@ -398,6 +426,7 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
|
||||
suggestedNavigations: {},
|
||||
subScopeName: subScope,
|
||||
disableSubScopeSelection,
|
||||
preLoadSubScopeChildren,
|
||||
};
|
||||
}
|
||||
if (expanded && !rootNode.folders[folderKey].expanded) {
|
||||
|
||||
@@ -44,6 +44,7 @@ export interface SuggestedNavigationsFolder {
|
||||
subScopeName?: string;
|
||||
loading?: boolean;
|
||||
disableSubScopeSelection?: boolean;
|
||||
preLoadSubScopeChildren?: boolean;
|
||||
}
|
||||
|
||||
export type SuggestedNavigationsFoldersMap = Record<string, SuggestedNavigationsFolder>;
|
||||
|
||||
Reference in New Issue
Block a user