feat(web): add dashboard SPA routing
This commit is contained in:
@@ -2,6 +2,7 @@ import { commitinfo } from '../00_commitinfo_data.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
import * as appstate from '../appstate.js';
|
||||
import { appRouter } from '../router.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
@@ -36,6 +37,11 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
interface ICloudlyView extends plugins.deesCatalog.IView {
|
||||
slug?: string;
|
||||
subViews?: ICloudlyView[];
|
||||
}
|
||||
|
||||
@customElement('cloudly-dashboard')
|
||||
export class CloudlyDashboard extends DeesElement {
|
||||
@state() private accessor identity: plugins.interfaces.data.IIdentity | null = null;
|
||||
@@ -44,75 +50,108 @@ export class CloudlyDashboard extends DeesElement {
|
||||
secretBundles: [],
|
||||
clusters: [],
|
||||
};
|
||||
@state() private accessor uiState: appstate.IUiState = {
|
||||
activeView: 'overview',
|
||||
activeSubview: null,
|
||||
};
|
||||
|
||||
// Keep view tabs stable across renders to preserve active selection
|
||||
private readonly viewTabs: plugins.deesCatalog.IView[] = [
|
||||
private readonly viewTabs: ICloudlyView[] = [
|
||||
{
|
||||
slug: 'overview',
|
||||
name: 'Overview',
|
||||
iconName: 'lucide:LayoutDashboard',
|
||||
element: CloudlyViewOverview,
|
||||
},
|
||||
{
|
||||
slug: 'platform',
|
||||
name: 'Platform',
|
||||
iconName: 'lucide:Settings',
|
||||
subViews: [
|
||||
{ name: 'Settings', iconName: 'lucide:Settings', element: CloudlyViewSettings },
|
||||
{ name: 'BaseOS', iconName: 'lucide:HardDriveDownload', element: CloudlyViewBaseOs },
|
||||
{ name: 'Fleet', iconName: 'lucide:Truck', element: CloudlyViewBackups },
|
||||
{ slug: 'settings', name: 'Settings', iconName: 'lucide:Settings', element: CloudlyViewSettings },
|
||||
{ slug: 'baseos', name: 'BaseOS', iconName: 'lucide:HardDriveDownload', element: CloudlyViewBaseOs },
|
||||
{ slug: 'fleet', name: 'Fleet', iconName: 'lucide:Truck', element: CloudlyViewBackups },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'runtime',
|
||||
name: 'Runtime',
|
||||
iconName: 'lucide:Network',
|
||||
subViews: [
|
||||
{ name: 'Clusters', iconName: 'lucide:Network', element: CloudlyViewClusters },
|
||||
{ name: 'Services', iconName: 'lucide:Layers', element: CloudlyViewServices },
|
||||
{ name: 'Images', iconName: 'lucide:Image', element: CloudlyViewImages },
|
||||
{ name: 'Deployments', iconName: 'lucide:Rocket', element: CloudlyViewDeployments },
|
||||
{ name: 'Tasks', iconName: 'lucide:ListChecks', element: CloudlyViewTasks },
|
||||
{ slug: 'clusters', name: 'Clusters', iconName: 'lucide:Network', element: CloudlyViewClusters },
|
||||
{ slug: 'services', name: 'Services', iconName: 'lucide:Layers', element: CloudlyViewServices },
|
||||
{ slug: 'images', name: 'Images', iconName: 'lucide:Image', element: CloudlyViewImages },
|
||||
{ slug: 'deployments', name: 'Deployments', iconName: 'lucide:Rocket', element: CloudlyViewDeployments },
|
||||
{ slug: 'tasks', name: 'Tasks', iconName: 'lucide:ListChecks', element: CloudlyViewTasks },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'registry',
|
||||
name: 'Registry & Build',
|
||||
iconName: 'lucide:Package',
|
||||
subViews: [
|
||||
{ name: 'ExternalRegistries', iconName: 'lucide:Package', element: CloudlyViewExternalRegistries },
|
||||
{ name: 'Testing & Building', iconName: 'lucide:HardHat', element: CloudlyViewServices },
|
||||
{ slug: 'externalregistries', name: 'ExternalRegistries', iconName: 'lucide:Package', element: CloudlyViewExternalRegistries },
|
||||
{ slug: 'testing', name: 'Testing & Building', iconName: 'lucide:HardHat', element: CloudlyViewServices },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'secrets',
|
||||
name: 'Secrets',
|
||||
iconName: 'lucide:ShieldCheck',
|
||||
subViews: [
|
||||
{ name: 'SecretGroups', iconName: 'lucide:ShieldCheck', element: CloudlyViewSecretGroups },
|
||||
{ name: 'SecretBundles', iconName: 'lucide:LockKeyhole', element: CloudlyViewSecretBundles },
|
||||
{ slug: 'secretgroups', name: 'SecretGroups', iconName: 'lucide:ShieldCheck', element: CloudlyViewSecretGroups },
|
||||
{ slug: 'secretbundles', name: 'SecretBundles', iconName: 'lucide:LockKeyhole', element: CloudlyViewSecretBundles },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'domains',
|
||||
name: 'Domains & Messaging',
|
||||
iconName: 'lucide:Globe2',
|
||||
subViews: [
|
||||
{ name: 'Domains', iconName: 'lucide:Globe2', element: CloudlyViewDomains },
|
||||
{ name: 'DNS', iconName: 'lucide:Globe', element: CloudlyViewDns },
|
||||
{ name: 'Mails', iconName: 'lucide:Mail', element: CloudlyViewMails },
|
||||
{ slug: 'domains', name: 'Domains', iconName: 'lucide:Globe2', element: CloudlyViewDomains },
|
||||
{ slug: 'dns', name: 'DNS', iconName: 'lucide:Globe', element: CloudlyViewDns },
|
||||
{ slug: 'mails', name: 'Mails', iconName: 'lucide:Mail', element: CloudlyViewMails },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'storage',
|
||||
name: 'Storage',
|
||||
iconName: 'lucide:Database',
|
||||
subViews: [
|
||||
{ name: 's3', iconName: 'lucide:Cloud', element: CloudlyViewS3 },
|
||||
{ name: 'DBs', iconName: 'lucide:Database', element: CloudlyViewDbs },
|
||||
{ name: 'Backups', iconName: 'lucide:Save', element: CloudlyViewBackups },
|
||||
{ slug: 's3', name: 's3', iconName: 'lucide:Cloud', element: CloudlyViewS3 },
|
||||
{ slug: 'dbs', name: 'DBs', iconName: 'lucide:Database', element: CloudlyViewDbs },
|
||||
{ slug: 'backups', name: 'Backups', iconName: 'lucide:Save', element: CloudlyViewBackups },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'logs',
|
||||
name: 'Logs',
|
||||
iconName: 'lucide:FileText',
|
||||
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() {
|
||||
super();
|
||||
document.title = `cloudly v${commitinfo.version}`;
|
||||
@@ -122,6 +161,24 @@ export class CloudlyDashboard extends DeesElement {
|
||||
this.data = dataArg;
|
||||
});
|
||||
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 = [
|
||||
@@ -146,6 +203,7 @@ export class CloudlyDashboard extends DeesElement {
|
||||
<dees-simple-login name="cloudly v${commitinfo.version}">
|
||||
<dees-simple-appdash name="cloudly v${commitinfo.version}"
|
||||
.viewTabs=${this.viewTabs}
|
||||
.selectedView=${this.currentViewTab}
|
||||
></dees-simple-appdash>
|
||||
</dees-simple-login>
|
||||
</div>
|
||||
@@ -158,6 +216,30 @@ export class CloudlyDashboard extends DeesElement {
|
||||
console.log(loginEvent.detail);
|
||||
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) => {
|
||||
plugins.deesCatalog.DeesContextmenu.openContextMenuWithOptions(eventArg, [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user