feat(web-ui): reorganize network and security views into tabbed subviews with route-aware navigation
This commit is contained in:
@@ -3,9 +3,31 @@ import * as appstate from './appstate.js';
|
||||
|
||||
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
|
||||
|
||||
export const validViews = ['overview', 'network', 'emails', 'logs', 'routes', 'apitokens', 'configuration', 'security', 'certificates', 'remoteingress', 'vpn', 'sourceprofiles', 'networktargets', 'targetprofiles'] as const;
|
||||
// Flat top-level views (no subviews)
|
||||
const flatViews = ['overview', 'configuration', 'emails', 'logs', 'apitokens', 'certificates', 'remoteingress', 'vpn'] as const;
|
||||
|
||||
export type TValidView = typeof validViews[number];
|
||||
// Tabbed views and their valid subviews
|
||||
const subviewMap: Record<string, readonly string[]> = {
|
||||
network: ['activity', 'routes', 'sourceprofiles', 'networktargets', 'targetprofiles'] as const,
|
||||
security: ['overview', 'blocked', 'authentication', 'emailsecurity'] as const,
|
||||
};
|
||||
|
||||
// Default subview when user visits the bare parent URL
|
||||
const defaultSubview: Record<string, string> = {
|
||||
network: 'activity',
|
||||
security: 'overview',
|
||||
};
|
||||
|
||||
export const validTopLevelViews = [...flatViews, ...Object.keys(subviewMap)] as const;
|
||||
export type TValidView = typeof validTopLevelViews[number];
|
||||
|
||||
export function isValidView(view: string): boolean {
|
||||
return (validTopLevelViews as readonly string[]).includes(view);
|
||||
}
|
||||
|
||||
export function isValidSubview(view: string, subview: string): boolean {
|
||||
return subviewMap[view]?.includes(subview) ?? false;
|
||||
}
|
||||
|
||||
class AppRouter {
|
||||
private router: InstanceType<typeof SmartRouter>;
|
||||
@@ -25,12 +47,27 @@ class AppRouter {
|
||||
}
|
||||
|
||||
private setupRoutes(): void {
|
||||
for (const view of validViews) {
|
||||
// Flat views
|
||||
for (const view of flatViews) {
|
||||
this.router.on(`/${view}`, async () => {
|
||||
this.updateViewState(view);
|
||||
this.updateViewState(view, null);
|
||||
});
|
||||
}
|
||||
|
||||
// Tabbed views
|
||||
for (const view of Object.keys(subviewMap)) {
|
||||
// Bare parent → redirect to default subview
|
||||
this.router.on(`/${view}`, async () => {
|
||||
this.navigateTo(`/${view}/${defaultSubview[view]}`);
|
||||
});
|
||||
// Each valid subview
|
||||
for (const sub of subviewMap[view]) {
|
||||
this.router.on(`/${view}/${sub}`, async () => {
|
||||
this.updateViewState(view, sub);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Root redirect
|
||||
this.router.on('/', async () => {
|
||||
this.navigateTo('/overview');
|
||||
@@ -42,7 +79,9 @@ class AppRouter {
|
||||
if (this.suppressStateUpdate) return;
|
||||
|
||||
const currentPath = window.location.pathname;
|
||||
const expectedPath = `/${uiState.activeView}`;
|
||||
const expectedPath = uiState.activeSubview
|
||||
? `/${uiState.activeView}/${uiState.activeSubview}`
|
||||
: `/${uiState.activeView}`;
|
||||
|
||||
if (currentPath !== expectedPath) {
|
||||
this.suppressStateUpdate = true;
|
||||
@@ -57,25 +96,38 @@ class AppRouter {
|
||||
|
||||
if (!path || path === '/') {
|
||||
this.router.pushUrl('/overview');
|
||||
} else {
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
const view = segments[0];
|
||||
return;
|
||||
}
|
||||
|
||||
if (validViews.includes(view as TValidView)) {
|
||||
this.updateViewState(view as TValidView);
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
const view = segments[0];
|
||||
const sub = segments[1];
|
||||
|
||||
if (!isValidView(view)) {
|
||||
this.router.pushUrl('/overview');
|
||||
return;
|
||||
}
|
||||
|
||||
if (subviewMap[view]) {
|
||||
if (sub && isValidSubview(view, sub)) {
|
||||
this.updateViewState(view, sub);
|
||||
} else {
|
||||
this.router.pushUrl('/overview');
|
||||
// Bare parent or invalid sub → default subview
|
||||
this.router.pushUrl(`/${view}/${defaultSubview[view]}`);
|
||||
}
|
||||
} else {
|
||||
this.updateViewState(view, null);
|
||||
}
|
||||
}
|
||||
|
||||
private updateViewState(view: string): void {
|
||||
private updateViewState(view: string, subview: string | null): void {
|
||||
this.suppressStateUpdate = true;
|
||||
const currentState = appstate.uiStatePart.getState()!;
|
||||
if (currentState.activeView !== view) {
|
||||
if (currentState.activeView !== view || currentState.activeSubview !== subview) {
|
||||
appstate.uiStatePart.setState({
|
||||
...currentState,
|
||||
activeView: view,
|
||||
activeSubview: subview,
|
||||
} as appstate.IUiState);
|
||||
}
|
||||
this.suppressStateUpdate = false;
|
||||
@@ -85,11 +137,17 @@ class AppRouter {
|
||||
this.router.pushUrl(path);
|
||||
}
|
||||
|
||||
public navigateToView(view: string): void {
|
||||
if (validViews.includes(view as TValidView)) {
|
||||
this.navigateTo(`/${view}`);
|
||||
} else {
|
||||
public navigateToView(view: string, subview?: string): void {
|
||||
if (!isValidView(view)) {
|
||||
this.navigateTo('/overview');
|
||||
return;
|
||||
}
|
||||
if (subview && isValidSubview(view, subview)) {
|
||||
this.navigateTo(`/${view}/${subview}`);
|
||||
} else if (subviewMap[view]) {
|
||||
this.navigateTo(`/${view}/${defaultSubview[view]}`);
|
||||
} else {
|
||||
this.navigateTo(`/${view}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user