feat(web): add dashboard SPA routing
This commit is contained in:
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
## Pending
|
## Pending
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- add SPA dashboard path navigation (web)
|
||||||
|
- support direct links to dashboard views and subviews via URL paths
|
||||||
|
- sync appdash selection with browser history and enable server SPA fallback
|
||||||
|
|
||||||
## 2026-05-21 - 5.6.0
|
## 2026-05-21 - 5.6.0
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export class CloudlyServer {
|
|||||||
: {}),
|
: {}),
|
||||||
injectReload: true,
|
injectReload: true,
|
||||||
serveDir: paths.distServeDir,
|
serveDir: paths.distServeDir,
|
||||||
|
spaFallback: true,
|
||||||
watch: true,
|
watch: true,
|
||||||
compression: {
|
compression: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -11,6 +11,33 @@ export const loginStatePart: plugins.smartstate.StatePart<unknown, ILoginState>
|
|||||||
'persistent'
|
'persistent'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export interface IUiState {
|
||||||
|
activeView: string;
|
||||||
|
activeSubview: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInitialView = (): string => {
|
||||||
|
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
|
||||||
|
const validViews = ['overview', 'platform', 'runtime', 'registry', 'secrets', 'domains', 'storage', 'logs'];
|
||||||
|
const segments = path.split('/').filter(Boolean);
|
||||||
|
const view = segments[0];
|
||||||
|
return validViews.includes(view) ? view : 'overview';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInitialSubview = (): string | null => {
|
||||||
|
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
|
||||||
|
const segments = path.split('/').filter(Boolean);
|
||||||
|
return segments[1] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uiStatePart = await appstate.getStatePart<IUiState>(
|
||||||
|
'ui',
|
||||||
|
{
|
||||||
|
activeView: getInitialView(),
|
||||||
|
activeSubview: getInitialSubview(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const loginAction = loginStatePart.createAction<{ username: string; password: string }>(
|
export const loginAction = loginStatePart.createAction<{ username: string; password: string }>(
|
||||||
async (statePartArg, payloadArg) => {
|
async (statePartArg, payloadArg) => {
|
||||||
const currentState = statePartArg.getState() || { identity: null };
|
const currentState = statePartArg.getState() || { identity: null };
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { commitinfo } from '../00_commitinfo_data.js';
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
import * as appstate from '../appstate.js';
|
import * as appstate from '../appstate.js';
|
||||||
|
import { appRouter } from '../router.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeesElement,
|
DeesElement,
|
||||||
@@ -36,6 +37,11 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ICloudlyView extends plugins.deesCatalog.IView {
|
||||||
|
slug?: string;
|
||||||
|
subViews?: ICloudlyView[];
|
||||||
|
}
|
||||||
|
|
||||||
@customElement('cloudly-dashboard')
|
@customElement('cloudly-dashboard')
|
||||||
export class CloudlyDashboard extends DeesElement {
|
export class CloudlyDashboard extends DeesElement {
|
||||||
@state() private accessor identity: plugins.interfaces.data.IIdentity | null = null;
|
@state() private accessor identity: plugins.interfaces.data.IIdentity | null = null;
|
||||||
@@ -44,75 +50,108 @@ export class CloudlyDashboard extends DeesElement {
|
|||||||
secretBundles: [],
|
secretBundles: [],
|
||||||
clusters: [],
|
clusters: [],
|
||||||
};
|
};
|
||||||
|
@state() private accessor uiState: appstate.IUiState = {
|
||||||
|
activeView: 'overview',
|
||||||
|
activeSubview: null,
|
||||||
|
};
|
||||||
|
|
||||||
// Keep view tabs stable across renders to preserve active selection
|
// Keep view tabs stable across renders to preserve active selection
|
||||||
private readonly viewTabs: plugins.deesCatalog.IView[] = [
|
private readonly viewTabs: ICloudlyView[] = [
|
||||||
{
|
{
|
||||||
|
slug: 'overview',
|
||||||
name: 'Overview',
|
name: 'Overview',
|
||||||
iconName: 'lucide:LayoutDashboard',
|
iconName: 'lucide:LayoutDashboard',
|
||||||
element: CloudlyViewOverview,
|
element: CloudlyViewOverview,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
slug: 'platform',
|
||||||
name: 'Platform',
|
name: 'Platform',
|
||||||
iconName: 'lucide:Settings',
|
iconName: 'lucide:Settings',
|
||||||
subViews: [
|
subViews: [
|
||||||
{ name: 'Settings', iconName: 'lucide:Settings', element: CloudlyViewSettings },
|
{ slug: 'settings', name: 'Settings', iconName: 'lucide:Settings', element: CloudlyViewSettings },
|
||||||
{ name: 'BaseOS', iconName: 'lucide:HardDriveDownload', element: CloudlyViewBaseOs },
|
{ slug: 'baseos', name: 'BaseOS', iconName: 'lucide:HardDriveDownload', element: CloudlyViewBaseOs },
|
||||||
{ name: 'Fleet', iconName: 'lucide:Truck', element: CloudlyViewBackups },
|
{ slug: 'fleet', name: 'Fleet', iconName: 'lucide:Truck', element: CloudlyViewBackups },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
slug: 'runtime',
|
||||||
name: 'Runtime',
|
name: 'Runtime',
|
||||||
iconName: 'lucide:Network',
|
iconName: 'lucide:Network',
|
||||||
subViews: [
|
subViews: [
|
||||||
{ name: 'Clusters', iconName: 'lucide:Network', element: CloudlyViewClusters },
|
{ slug: 'clusters', name: 'Clusters', iconName: 'lucide:Network', element: CloudlyViewClusters },
|
||||||
{ name: 'Services', iconName: 'lucide:Layers', element: CloudlyViewServices },
|
{ slug: 'services', name: 'Services', iconName: 'lucide:Layers', element: CloudlyViewServices },
|
||||||
{ name: 'Images', iconName: 'lucide:Image', element: CloudlyViewImages },
|
{ slug: 'images', name: 'Images', iconName: 'lucide:Image', element: CloudlyViewImages },
|
||||||
{ name: 'Deployments', iconName: 'lucide:Rocket', element: CloudlyViewDeployments },
|
{ slug: 'deployments', name: 'Deployments', iconName: 'lucide:Rocket', element: CloudlyViewDeployments },
|
||||||
{ name: 'Tasks', iconName: 'lucide:ListChecks', element: CloudlyViewTasks },
|
{ slug: 'tasks', name: 'Tasks', iconName: 'lucide:ListChecks', element: CloudlyViewTasks },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
slug: 'registry',
|
||||||
name: 'Registry & Build',
|
name: 'Registry & Build',
|
||||||
iconName: 'lucide:Package',
|
iconName: 'lucide:Package',
|
||||||
subViews: [
|
subViews: [
|
||||||
{ name: 'ExternalRegistries', iconName: 'lucide:Package', element: CloudlyViewExternalRegistries },
|
{ slug: 'externalregistries', name: 'ExternalRegistries', iconName: 'lucide:Package', element: CloudlyViewExternalRegistries },
|
||||||
{ name: 'Testing & Building', iconName: 'lucide:HardHat', element: CloudlyViewServices },
|
{ slug: 'testing', name: 'Testing & Building', iconName: 'lucide:HardHat', element: CloudlyViewServices },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
slug: 'secrets',
|
||||||
name: 'Secrets',
|
name: 'Secrets',
|
||||||
iconName: 'lucide:ShieldCheck',
|
iconName: 'lucide:ShieldCheck',
|
||||||
subViews: [
|
subViews: [
|
||||||
{ name: 'SecretGroups', iconName: 'lucide:ShieldCheck', element: CloudlyViewSecretGroups },
|
{ slug: 'secretgroups', name: 'SecretGroups', iconName: 'lucide:ShieldCheck', element: CloudlyViewSecretGroups },
|
||||||
{ name: 'SecretBundles', iconName: 'lucide:LockKeyhole', element: CloudlyViewSecretBundles },
|
{ slug: 'secretbundles', name: 'SecretBundles', iconName: 'lucide:LockKeyhole', element: CloudlyViewSecretBundles },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
slug: 'domains',
|
||||||
name: 'Domains & Messaging',
|
name: 'Domains & Messaging',
|
||||||
iconName: 'lucide:Globe2',
|
iconName: 'lucide:Globe2',
|
||||||
subViews: [
|
subViews: [
|
||||||
{ name: 'Domains', iconName: 'lucide:Globe2', element: CloudlyViewDomains },
|
{ slug: 'domains', name: 'Domains', iconName: 'lucide:Globe2', element: CloudlyViewDomains },
|
||||||
{ name: 'DNS', iconName: 'lucide:Globe', element: CloudlyViewDns },
|
{ slug: 'dns', name: 'DNS', iconName: 'lucide:Globe', element: CloudlyViewDns },
|
||||||
{ name: 'Mails', iconName: 'lucide:Mail', element: CloudlyViewMails },
|
{ slug: 'mails', name: 'Mails', iconName: 'lucide:Mail', element: CloudlyViewMails },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
slug: 'storage',
|
||||||
name: 'Storage',
|
name: 'Storage',
|
||||||
iconName: 'lucide:Database',
|
iconName: 'lucide:Database',
|
||||||
subViews: [
|
subViews: [
|
||||||
{ name: 's3', iconName: 'lucide:Cloud', element: CloudlyViewS3 },
|
{ slug: 's3', name: 's3', iconName: 'lucide:Cloud', element: CloudlyViewS3 },
|
||||||
{ name: 'DBs', iconName: 'lucide:Database', element: CloudlyViewDbs },
|
{ slug: 'dbs', name: 'DBs', iconName: 'lucide:Database', element: CloudlyViewDbs },
|
||||||
{ name: 'Backups', iconName: 'lucide:Save', element: CloudlyViewBackups },
|
{ slug: 'backups', name: 'Backups', iconName: 'lucide:Save', element: CloudlyViewBackups },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
slug: 'logs',
|
||||||
name: 'Logs',
|
name: 'Logs',
|
||||||
iconName: 'lucide:FileText',
|
iconName: 'lucide:FileText',
|
||||||
element: CloudlyViewLogs,
|
element: CloudlyViewLogs,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private slugFor(view: ICloudlyView): string {
|
||||||
|
return view.slug ?? view.name.toLowerCase().replace(/\s+/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
private findParent(view: ICloudlyView): ICloudlyView | undefined {
|
||||||
|
return this.viewTabs.find((viewTab) => viewTab.subViews?.includes(view));
|
||||||
|
}
|
||||||
|
|
||||||
|
private findViewBySlug(viewSlug: string, subviewSlug: string | null): ICloudlyView | undefined {
|
||||||
|
const topLevelView = this.viewTabs.find((view) => this.slugFor(view) === viewSlug);
|
||||||
|
if (!topLevelView) return undefined;
|
||||||
|
if (subviewSlug && topLevelView.subViews) {
|
||||||
|
return topLevelView.subViews.find((subview) => this.slugFor(subview) === subviewSlug) ?? topLevelView;
|
||||||
|
}
|
||||||
|
return topLevelView;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get currentViewTab(): ICloudlyView {
|
||||||
|
return this.findViewBySlug(this.uiState.activeView, this.uiState.activeSubview) ?? this.viewTabs[0];
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
document.title = `cloudly v${commitinfo.version}`;
|
document.title = `cloudly v${commitinfo.version}`;
|
||||||
@@ -122,6 +161,24 @@ export class CloudlyDashboard extends DeesElement {
|
|||||||
this.data = dataArg;
|
this.data = dataArg;
|
||||||
});
|
});
|
||||||
this.rxSubscriptions.push(subcription);
|
this.rxSubscriptions.push(subcription);
|
||||||
|
|
||||||
|
const uiSubscription = appstate.uiStatePart
|
||||||
|
.select((stateArg) => stateArg)
|
||||||
|
.subscribe((uiState) => {
|
||||||
|
this.uiState = uiState;
|
||||||
|
this.syncAppdashView(uiState.activeView, uiState.activeSubview);
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(uiSubscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncAppdashView(viewSlug: string, subviewSlug: string | null): void {
|
||||||
|
const appDash = this.shadowRoot?.querySelector('dees-simple-appdash') as any;
|
||||||
|
if (!appDash) return;
|
||||||
|
|
||||||
|
const targetView = this.findViewBySlug(viewSlug, subviewSlug);
|
||||||
|
if (!targetView || appDash.selectedView === targetView) return;
|
||||||
|
|
||||||
|
appDash.loadView(targetView);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
@@ -146,6 +203,7 @@ export class CloudlyDashboard extends DeesElement {
|
|||||||
<dees-simple-login name="cloudly v${commitinfo.version}">
|
<dees-simple-login name="cloudly v${commitinfo.version}">
|
||||||
<dees-simple-appdash name="cloudly v${commitinfo.version}"
|
<dees-simple-appdash name="cloudly v${commitinfo.version}"
|
||||||
.viewTabs=${this.viewTabs}
|
.viewTabs=${this.viewTabs}
|
||||||
|
.selectedView=${this.currentViewTab}
|
||||||
></dees-simple-appdash>
|
></dees-simple-appdash>
|
||||||
</dees-simple-login>
|
</dees-simple-login>
|
||||||
</div>
|
</div>
|
||||||
@@ -158,6 +216,30 @@ export class CloudlyDashboard extends DeesElement {
|
|||||||
console.log(loginEvent.detail);
|
console.log(loginEvent.detail);
|
||||||
this.login(loginEvent.detail.data.username, loginEvent.detail.data.password);
|
this.login(loginEvent.detail.data.username, loginEvent.detail.data.password);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const appDash = this.shadowRoot!.querySelector('dees-simple-appdash');
|
||||||
|
if (appDash) {
|
||||||
|
appDash.addEventListener('view-select', (eventArg: Event) => {
|
||||||
|
const view = (eventArg as CustomEvent).detail.view as ICloudlyView;
|
||||||
|
const parent = this.findParent(view);
|
||||||
|
const currentState = appstate.uiStatePart.getState();
|
||||||
|
if (parent) {
|
||||||
|
const parentSlug = this.slugFor(parent);
|
||||||
|
const subviewSlug = this.slugFor(view);
|
||||||
|
if (currentState?.activeView === parentSlug && currentState?.activeSubview === subviewSlug) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appRouter.navigateToView(parentSlug, subviewSlug);
|
||||||
|
} else {
|
||||||
|
const slug = this.slugFor(view);
|
||||||
|
if (currentState?.activeView === slug && !currentState?.activeSubview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appRouter.navigateToView(slug);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.addEventListener('contextmenu', (eventArg) => {
|
this.addEventListener('contextmenu', (eventArg) => {
|
||||||
plugins.deesCatalog.DeesContextmenu.openContextMenuWithOptions(eventArg, [
|
plugins.deesCatalog.DeesContextmenu.openContextMenuWithOptions(eventArg, [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import * as plugins from './plugins.js';
|
|||||||
import { html } from '@design.estate/dees-element';
|
import { html } from '@design.estate/dees-element';
|
||||||
|
|
||||||
import './elements/index.js';
|
import './elements/index.js';
|
||||||
|
import { appRouter } from './router.js';
|
||||||
|
|
||||||
|
appRouter.init();
|
||||||
|
|
||||||
plugins.deesElement.render(html`
|
plugins.deesElement.render(html`
|
||||||
<cloudly-dashboard></cloudly-dashboard>
|
<cloudly-dashboard></cloudly-dashboard>
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import * as appstate from './appstate.js';
|
||||||
|
|
||||||
|
const SmartRouter = plugins.deesDomtools.plugins.smartrouter.SmartRouter;
|
||||||
|
|
||||||
|
const flatViews = ['overview', 'logs'] as const;
|
||||||
|
|
||||||
|
const subviewMap: Record<string, readonly string[]> = {
|
||||||
|
platform: ['settings', 'baseos', 'fleet'] as const,
|
||||||
|
runtime: ['clusters', 'services', 'images', 'deployments', 'tasks'] as const,
|
||||||
|
registry: ['externalregistries', 'testing'] as const,
|
||||||
|
secrets: ['secretgroups', 'secretbundles'] as const,
|
||||||
|
domains: ['domains', 'dns', 'mails'] as const,
|
||||||
|
storage: ['s3', 'dbs', 'backups'] as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultSubview: Record<string, string> = {
|
||||||
|
platform: 'settings',
|
||||||
|
runtime: 'clusters',
|
||||||
|
registry: 'externalregistries',
|
||||||
|
secrets: 'secretgroups',
|
||||||
|
domains: 'domains',
|
||||||
|
storage: 's3',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validTopLevelViews = [...flatViews, ...Object.keys(subviewMap)] as const;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
for (const view of flatViews) {
|
||||||
|
this.router.on(`/${view}`, async () => {
|
||||||
|
this.updateViewState(view, null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const view of Object.keys(subviewMap)) {
|
||||||
|
this.router.on(`/${view}`, async () => {
|
||||||
|
this.navigateTo(`/${view}/${defaultSubview[view]}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const subview of subviewMap[view]) {
|
||||||
|
this.router.on(`/${view}/${subview}`, async () => {
|
||||||
|
this.updateViewState(view, subview);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 subview = segments[1];
|
||||||
|
|
||||||
|
if (!isValidView(view)) {
|
||||||
|
this.router.pushUrl('/overview');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subviewMap[view]) {
|
||||||
|
if (subview && isValidSubview(view, subview)) {
|
||||||
|
this.updateViewState(view, subview);
|
||||||
|
} else {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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 destroy(): void {
|
||||||
|
this.router.destroy();
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const appRouter = new AppRouter();
|
||||||
Reference in New Issue
Block a user