Compare commits

...

2 Commits

Author SHA1 Message Date
Tobias Skarhed 1a6112c71f Make use of a Map 2025-12-17 12:22:32 +01:00
Tobias Skarhed c65a738812 Don't redirect on nested navigation match 2025-12-17 11:39:19 +01:00
2 changed files with 123 additions and 13 deletions
@@ -34,8 +34,16 @@ interface ScopesDashboardsServiceState {
navScopePath?: string[];
}
export interface NavigationUrlInfo {
nearestSubscope?: string;
subscopePath?: string[];
}
export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsServiceState> {
private locationSubscription: Subscription | undefined;
// Index mapping navigation URLs to their subscope info for O(1) lookup
private navigationUrlIndex: Map<string, NavigationUrlInfo> = new Map();
constructor(private apiClient: ScopesApiClient) {
super({
drawerOpened: false,
@@ -199,6 +207,7 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
}
let subScopeFolders: SuggestedNavigationsFoldersMap | undefined;
let filteredItems: Array<ScopeDashboardBinding | ScopeNavigation> = [];
try {
// Fetch navigations for this subScope
@@ -210,7 +219,7 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
// Filter out items that have a subScope matching any subScope already in the path
// This prevents infinite loops when a subScope returns items with the same subScope
const filteredItems = filterItemsWithSubScopesInPath(subScopeItems, path, subScopeName, this.state.folders);
filteredItems = filterItemsWithSubScopesInPath(subScopeItems, path, subScopeName, this.state.folders);
// Group the items and add them to the subScope folder
subScopeFolders = this.groupSuggestedItems(filteredItems);
@@ -254,10 +263,14 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
...rootSubScopeFolder.suggestedNavigations,
};
// Build the subscope path for these items and add to the index
const subscopePath = this.getSubscopePathFromFolderPath(path, folders);
this.addNavigationsToIndex(filteredItems, subscopePath);
this.updateState({ folders, filteredFolders });
// Preload children for any newly added folders with preLoadSubScopeChildren
this.preloadSubScopeChildren(rootSubScopeFolder.folders, path);
await this.preloadSubScopeChildren(rootSubScopeFolder.folders, path);
} else {
this.updateState({ folders, filteredFolders });
}
@@ -290,6 +303,7 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
}
if (forScopeNames.length === 0) {
this.navigationUrlIndex.clear();
this.updateState({
dashboards: [],
filteredFolders: {},
@@ -314,6 +328,10 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
const folders = this.groupSuggestedItems(res);
const filteredFolders = this.filterFolders(folders, this.state.searchQuery);
// Build navigation URL index directly from the response (no subscope info for top-level)
this.navigationUrlIndex.clear();
this.addNavigationsToIndex(res, []);
this.updateState({
scopeNavigations: res,
filteredFolders,
@@ -323,7 +341,7 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
});
// Preload children for folders with preLoadSubScopeChildren set
this.preloadSubScopeChildren(folders[''].folders, ['']);
await this.preloadSubScopeChildren(folders[''].folders, ['']);
}
};
@@ -334,13 +352,17 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
* @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[]) => {
private preloadSubScopeChildren = async (foldersToCheck: SuggestedNavigationsFoldersMap, basePath: string[]) => {
const preloadPromises: Array<Promise<void>> = [];
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 +525,78 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
};
public toggleDrawer = () => this.updateState({ drawerOpened: !this.state.drawerOpened });
/**
* Adds navigation URLs to the index with their subscope info.
* Called when processing navigation items from API responses.
*/
private addNavigationsToIndex = (items: Array<ScopeDashboardBinding | ScopeNavigation>, subscopePath: string[]) => {
const info: NavigationUrlInfo =
subscopePath.length > 0
? {
nearestSubscope: subscopePath[subscopePath.length - 1],
subscopePath: [...subscopePath],
}
: {};
for (const item of items) {
if ('url' in item.spec && typeof item.spec.url === 'string') {
this.navigationUrlIndex.set(item.spec.url, info);
} else if ('dashboard' in item.spec) {
// Dashboard items have URL format /d/{dashboardId}
this.navigationUrlIndex.set('/d/' + item.spec.dashboard, info);
}
}
};
/**
* Gets the subscope path by traversing the folder path and collecting subScopeName values.
*/
private getSubscopePathFromFolderPath = (folderPath: string[], folders: SuggestedNavigationsFoldersMap): string[] => {
const subscopePath: string[] = [];
let currentLevel = folders;
for (const key of folderPath) {
const folder = currentLevel[key];
if (folder?.subScopeName) {
subscopePath.push(folder.subScopeName);
}
if (folder?.folders) {
currentLevel = folder.folders;
}
}
return subscopePath;
};
/**
* Finds navigation info for the given path using the URL index for O(1) lookup.
* Returns subscope information if the navigation was found within a subscope folder.
*/
public findNavigationInfo = (
currentPath: string
): { found: boolean; nearestSubscope?: string; subscopePath?: string[] } => {
// Check each indexed URL using isCurrentPath for proper path matching
for (const [url, info] of this.navigationUrlIndex) {
if (isCurrentPath(currentPath, url)) {
return {
found: true,
nearestSubscope: info.nearestSubscope,
subscopePath: info.subscopePath,
};
}
}
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;
};
}
/**
@@ -371,19 +371,35 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
};
// Redirect to the scope node's redirect URL if it exists, otherwise redirect to the first scope navigation.
// If the current path is within a subscope's navigations, apply the subscope and set navigation scope.
private redirectAfterApply = (scopeNode: ScopeNode | undefined) => {
// 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<ScopesSelectorServi
}
// Redirect to first scopeNavigation if current URL isn't a scopeNavigation
if (!activeScopeNavigation && this.dashboardsService.state.scopeNavigations.length > 0) {
if (!navigationInfo.found && this.dashboardsService.state.scopeNavigations.length > 0) {
// Redirect to the first available scopeNavigation
const firstScopeNavigation = this.dashboardsService.state.scopeNavigations[0];