diff --git a/public/app/features/scopes/dashboards/ScopesDashboardsService.ts b/public/app/features/scopes/dashboards/ScopesDashboardsService.ts index a0def1b4fd5..73b58ca78b2 100644 --- a/public/app/features/scopes/dashboards/ScopesDashboardsService.ts +++ b/public/app/features/scopes/dashboards/ScopesDashboardsService.ts @@ -257,7 +257,7 @@ export class ScopesDashboardsService extends ScopesServiceBase { + private preloadSubScopeChildren = async (foldersToCheck: SuggestedNavigationsFoldersMap, basePath: string[]) => { + const preloadPromises: Array> = []; + for (const [folderKey, folder] of Object.entries(foldersToCheck)) { if (folder.preLoadSubScopeChildren && folder.subScopeName) { const path = [...basePath, folderKey]; - this.fetchSubScopeItems(path, folder.subScopeName); + preloadPromises.push(this.fetchSubScopeItems(path, folder.subScopeName)); } } + + await Promise.all(preloadPromises); }; public groupSuggestedItems = ( @@ -503,6 +507,72 @@ export class ScopesDashboardsService extends ScopesServiceBase this.updateState({ drawerOpened: !this.state.drawerOpened }); + + /** + * Result of finding a navigation in the folder structure. + * Contains the subscope path if the navigation was found within a subscope folder. + */ + public findNavigationInfo = ( + currentPath: string + ): { found: boolean; nearestSubscope?: string; subscopePath?: string[] } => { + // First check the top-level scopeNavigations + const inTopLevel = this.state.scopeNavigations.some((s) => { + if (!('url' in s.spec) || typeof s.spec.url !== 'string') { + return false; + } + return isCurrentPath(currentPath, s.spec.url); + }); + + if (inTopLevel) { + return { found: true }; + } + + // Then check all navigations in the folder structure (including subscope-loaded ones) + return this.findNavigationInFolders(currentPath, this.state.folders, []); + }; + + /** + * Recursively searches for a navigation URL in the folders structure. + * Returns subscope information if the navigation is found within a subscope folder. + */ + private findNavigationInFolders = ( + currentPath: string, + folders: SuggestedNavigationsFoldersMap, + currentSubscopePath: string[] + ): { found: boolean; nearestSubscope?: string; subscopePath?: string[] } => { + for (const folder of Object.values(folders)) { + // Build the subscope path - add this folder's subscope if it has one + const newSubscopePath = folder.subScopeName ? [...currentSubscopePath, folder.subScopeName] : currentSubscopePath; + + // Check navigations in this folder + for (const navigation of Object.values(folder.suggestedNavigations)) { + if (isCurrentPath(currentPath, navigation.url)) { + // Found! Return the nearest subscope (last in the path) and full path + return { + found: true, + nearestSubscope: newSubscopePath.length > 0 ? newSubscopePath[newSubscopePath.length - 1] : undefined, + subscopePath: newSubscopePath.length > 0 ? newSubscopePath : undefined, + }; + } + } + + // Recursively check nested folders + const nestedResult = this.findNavigationInFolders(currentPath, folder.folders, newSubscopePath); + if (nestedResult.found) { + return nestedResult; + } + } + + return { found: false }; + }; + + /** + * Checks if the given path matches any navigation URL in the entire folder structure, + * including navigations loaded by subscopes. + */ + public isPathInNavigations = (currentPath: string): boolean => { + return this.findNavigationInfo(currentPath).found; + }; } /** diff --git a/public/app/features/scopes/selector/ScopesSelectorService.ts b/public/app/features/scopes/selector/ScopesSelectorService.ts index abd837c7fb7..136ba14e86a 100644 --- a/public/app/features/scopes/selector/ScopesSelectorService.ts +++ b/public/app/features/scopes/selector/ScopesSelectorService.ts @@ -371,19 +371,35 @@ export class ScopesSelectorService extends ScopesServiceBase { - // Check if we are currently on an active scope navigation + // Check if we are currently on an active scope navigation (including those loaded by subscopes) const currentPath = locationService.getLocation().pathname; - const activeScopeNavigation = this.dashboardsService.state.scopeNavigations.find((s) => { - if (!('url' in s.spec) || typeof s.spec.url !== 'string') { - return false; + const navigationInfo = this.dashboardsService.findNavigationInfo(currentPath); + + // If we're on a navigation within a subscope: + // - Set the navigation scope to the current applied scope (so drawer keeps showing original items) + // - Apply the nearest subscope as the new scope + // - Set the navScopePath to expand the folders + if (navigationInfo.found && navigationInfo.nearestSubscope && navigationInfo.subscopePath) { + const currentAppliedScopeId = this.state.appliedScopes[0]?.scopeId; + if (currentAppliedScopeId) { + // Set navigation scope to current scope, then apply the subscope + this.dashboardsService.setNavigationScope(currentAppliedScopeId, undefined, navigationInfo.subscopePath); + // Apply the nearest subscope as the new applied scope (redirectOnApply=false to avoid recursion) + this.changeScopes([navigationInfo.nearestSubscope], undefined, undefined, false); } - return isCurrentPath(currentPath, s.spec.url); - }); + return; + } + + // If we're on a top-level navigation, no redirect needed + if (navigationInfo.found) { + return; + } // Only redirect to redirectPath if we are not currently on an active scope navigation if ( - !activeScopeNavigation && + !navigationInfo.found && scopeNode && scopeNode.spec.redirectPath && typeof scopeNode.spec.redirectPath === 'string' && @@ -395,7 +411,7 @@ export class ScopesSelectorService extends ScopesServiceBase 0) { + if (!navigationInfo.found && this.dashboardsService.state.scopeNavigations.length > 0) { // Redirect to the first available scopeNavigation const firstScopeNavigation = this.dashboardsService.state.scopeNavigations[0];