Files
dcrouter/ts_web/router.ts

171 lines
4.7 KiB
TypeScript
Raw Normal View History

import * as plugins from './plugins.js';
import * as appstate from './appstate.js';
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
// Flat top-level views (no subviews)
const flatViews = ['logs', 'certificates'] as const;
// Tabbed views and their valid subviews
const subviewMap: Record<string, readonly string[]> = {
overview: ['stats', 'configuration'] as const,
network: ['activity', 'routes', 'sourceprofiles', 'networktargets', 'targetprofiles', 'remoteingress', 'vpn'] as const,
email: ['log', 'security'] as const,
access: ['apitokens'] as const,
security: ['overview', 'blocked', 'authentication'] as const,
};
// Default subview when user visits the bare parent URL
const defaultSubview: Record<string, string> = {
overview: 'stats',
network: 'activity',
email: 'log',
access: 'apitokens',
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>;
private initialized = false;
private suppressStateUpdate = false;
constructor() {
this.router = new SmartRouter({ debug: false });
}
public init(): void {
if (this.initialized) return;
this.setupRoutes();
this.setupStateSync();
this.handleInitialRoute();
this.initialized = true;
}
private setupRoutes(): void {
// Flat views
for (const view of flatViews) {
this.router.on(`/${view}`, async () => {
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');
});
}
private setupStateSync(): void {
appstate.uiStatePart.select().subscribe((uiState) => {
if (this.suppressStateUpdate) return;
const currentPath = window.location.pathname;
const expectedPath = uiState.activeSubview
? `/${uiState.activeView}/${uiState.activeSubview}`
: `/${uiState.activeView}`;
if (currentPath !== expectedPath) {
this.suppressStateUpdate = true;
this.router.pushUrl(expectedPath);
this.suppressStateUpdate = false;
}
});
}
private handleInitialRoute(): void {
const path = window.location.pathname;
if (!path || path === '/') {
this.router.pushUrl('/overview');
return;
}
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 {
// Bare parent or invalid sub → default subview
this.router.pushUrl(`/${view}/${defaultSubview[view]}`);
}
} else {
this.updateViewState(view, null);
}
}
private updateViewState(view: string, subview: string | null): void {
this.suppressStateUpdate = true;
const currentState = appstate.uiStatePart.getState()!;
if (currentState.activeView !== view || currentState.activeSubview !== subview) {
appstate.uiStatePart.setState({
...currentState,
activeView: view,
activeSubview: subview,
} as appstate.IUiState);
}
this.suppressStateUpdate = false;
}
public navigateTo(path: string): void {
this.router.pushUrl(path);
}
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}`);
}
}
public getCurrentView(): string {
return appstate.uiStatePart.getState()!.activeView;
}
public destroy(): void {
this.router.destroy();
this.initialized = false;
}
}
export const appRouter = new AppRouter();