feat(web): group onebox sidebar navigation
This commit is contained in:
+170
-57
@@ -12,14 +12,21 @@ import {
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import type { ObViewDashboard } from './ob-view-dashboard.js';
|
||||
import type { ObViewServices } from './ob-view-services.js';
|
||||
import type { ObViewDomains } from './ob-view-domains.js';
|
||||
import type { ObViewDnsRecords } from './ob-view-dns-records.js';
|
||||
import type { ObViewNetwork } from './ob-view-network.js';
|
||||
import type { ObViewRegistries } from './ob-view-registries.js';
|
||||
import type { ObViewTokens } from './ob-view-tokens.js';
|
||||
import type { ObViewSettings } from './ob-view-settings.js';
|
||||
interface IUnresolvedView {
|
||||
slug?: string;
|
||||
name: string;
|
||||
iconName?: string;
|
||||
element?: Promise<any>;
|
||||
subViews?: IUnresolvedView[];
|
||||
}
|
||||
|
||||
interface IResolvedView {
|
||||
slug?: string;
|
||||
name: string;
|
||||
iconName?: string;
|
||||
element?: any;
|
||||
subViews?: IResolvedView[];
|
||||
}
|
||||
|
||||
@customElement('ob-app-shell')
|
||||
export class ObAppShell extends DeesElement {
|
||||
@@ -29,6 +36,7 @@ export class ObAppShell extends DeesElement {
|
||||
@state()
|
||||
accessor uiState: appstate.IUiState = {
|
||||
activeView: 'dashboard',
|
||||
activeSubview: null,
|
||||
autoRefresh: true,
|
||||
refreshInterval: 30000,
|
||||
};
|
||||
@@ -39,27 +47,93 @@ export class ObAppShell extends DeesElement {
|
||||
@state()
|
||||
accessor loginError: string = '';
|
||||
|
||||
private viewTabs = [
|
||||
{ name: 'Dashboard', iconName: 'lucide:layoutDashboard', element: (async () => (await import('./ob-view-dashboard.js')).ObViewDashboard)() },
|
||||
{ name: 'App Store', iconName: 'lucide:store', element: (async () => (await import('./ob-view-appstore.js')).ObViewAppStore)() },
|
||||
{ name: 'Services', iconName: 'lucide:boxes', element: (async () => (await import('./ob-view-services.js')).ObViewServices)() },
|
||||
{ name: 'Domains', iconName: 'lucide:globe', element: (async () => (await import('./ob-view-domains.js')).ObViewDomains)() },
|
||||
{ name: 'DNS Records', iconName: 'lucide:listTree', element: (async () => (await import('./ob-view-dns-records.js')).ObViewDnsRecords)() },
|
||||
{ name: 'Network', iconName: 'lucide:network', element: (async () => (await import('./ob-view-network.js')).ObViewNetwork)() },
|
||||
{ name: 'Registries', iconName: 'lucide:package', element: (async () => (await import('./ob-view-registries.js')).ObViewRegistries)() },
|
||||
{ name: 'Tokens', iconName: 'lucide:key', element: (async () => (await import('./ob-view-tokens.js')).ObViewTokens)() },
|
||||
{ name: 'Settings', iconName: 'lucide:settings', element: (async () => (await import('./ob-view-settings.js')).ObViewSettings)() },
|
||||
private viewTabs: IUnresolvedView[] = [
|
||||
{
|
||||
slug: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
iconName: 'lucide:layoutDashboard',
|
||||
element: (async () => (await import('./ob-view-dashboard.js')).ObViewDashboard)(),
|
||||
},
|
||||
{
|
||||
slug: 'apps',
|
||||
name: 'Apps',
|
||||
iconName: 'lucide:store',
|
||||
subViews: [
|
||||
{
|
||||
slug: 'app-store',
|
||||
name: 'App Store',
|
||||
iconName: 'lucide:store',
|
||||
element: (async () => (await import('./ob-view-appstore.js')).ObViewAppStore)(),
|
||||
},
|
||||
{
|
||||
slug: 'services',
|
||||
name: 'Services',
|
||||
iconName: 'lucide:boxes',
|
||||
element: (async () => (await import('./ob-view-services.js')).ObViewServices)(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'network',
|
||||
name: 'Network',
|
||||
iconName: 'lucide:network',
|
||||
subViews: [
|
||||
{
|
||||
slug: 'proxy',
|
||||
name: 'Proxy',
|
||||
iconName: 'lucide:route',
|
||||
element: (async () => (await import('./ob-view-network.js')).ObViewNetwork)(),
|
||||
},
|
||||
{
|
||||
slug: 'domains',
|
||||
name: 'Domains',
|
||||
iconName: 'lucide:globe',
|
||||
element: (async () => (await import('./ob-view-domains.js')).ObViewDomains)(),
|
||||
},
|
||||
{
|
||||
slug: 'dns-records',
|
||||
name: 'DNS Records',
|
||||
iconName: 'lucide:listTree',
|
||||
element: (async () => (await import('./ob-view-dns-records.js')).ObViewDnsRecords)(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'registry',
|
||||
name: 'Registry',
|
||||
iconName: 'lucide:package',
|
||||
subViews: [
|
||||
{
|
||||
slug: 'registries',
|
||||
name: 'Registries',
|
||||
iconName: 'lucide:package',
|
||||
element: (async () => (await import('./ob-view-registries.js')).ObViewRegistries)(),
|
||||
},
|
||||
{
|
||||
slug: 'tokens',
|
||||
name: 'Tokens',
|
||||
iconName: 'lucide:key',
|
||||
element: (async () => (await import('./ob-view-tokens.js')).ObViewTokens)(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'settings',
|
||||
name: 'Settings',
|
||||
iconName: 'lucide:settings',
|
||||
element: (async () => (await import('./ob-view-settings.js')).ObViewSettings)(),
|
||||
},
|
||||
];
|
||||
|
||||
private resolvedViewTabs: Array<{ name: string; iconName?: string; element: any }> = [];
|
||||
private resolvedViewTabs: IResolvedView[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
document.title = 'Onebox';
|
||||
|
||||
const loginSubscription = appstate.loginStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((loginState) => {
|
||||
.select((stateArg: appstate.ILoginState) => stateArg)
|
||||
.subscribe((loginState: appstate.ILoginState) => {
|
||||
this.loginState = loginState;
|
||||
if (loginState.isLoggedIn) {
|
||||
appstate.systemStatePart.dispatchAction(appstate.fetchSystemStatusAction, null);
|
||||
@@ -68,15 +142,56 @@ export class ObAppShell extends DeesElement {
|
||||
this.rxSubscriptions.push(loginSubscription);
|
||||
|
||||
const uiSubscription = appstate.uiStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((uiState) => {
|
||||
.select((stateArg: appstate.IUiState) => stateArg)
|
||||
.subscribe((uiState: appstate.IUiState) => {
|
||||
this.uiState = uiState;
|
||||
this.syncAppdashView(uiState.activeView);
|
||||
this.syncAppdashView(uiState.activeView, uiState.activeSubview);
|
||||
});
|
||||
this.rxSubscriptions.push(uiSubscription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
private async resolveViewTabs(tabs: IUnresolvedView[]): Promise<IResolvedView[]> {
|
||||
return Promise.all(
|
||||
tabs.map(async (tab) => {
|
||||
const resolvedTab: IResolvedView = {
|
||||
slug: tab.slug,
|
||||
name: tab.name,
|
||||
iconName: tab.iconName,
|
||||
};
|
||||
if (tab.element) {
|
||||
resolvedTab.element = await tab.element;
|
||||
}
|
||||
if (tab.subViews) {
|
||||
resolvedTab.subViews = await this.resolveViewTabs(tab.subViews);
|
||||
}
|
||||
return resolvedTab;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private slugFor(view: IResolvedView): string {
|
||||
return view.slug ?? view.name.toLowerCase().replace(/\s+/g, '-');
|
||||
}
|
||||
|
||||
private findParent(view: IResolvedView): IResolvedView | undefined {
|
||||
return this.resolvedViewTabs.find((viewTab) => viewTab.subViews?.includes(view));
|
||||
}
|
||||
|
||||
private findViewBySlug(viewSlug: string, subviewSlug: string | null): IResolvedView | undefined {
|
||||
const topLevelView = this.resolvedViewTabs.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(): IResolvedView | undefined {
|
||||
if (this.resolvedViewTabs.length === 0) return undefined;
|
||||
return this.findViewBySlug(this.uiState.activeView, this.uiState.activeSubview) ?? this.resolvedViewTabs[0];
|
||||
}
|
||||
|
||||
public static override styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
@@ -91,16 +206,14 @@ export class ObAppShell extends DeesElement {
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
public override render(): TemplateResult {
|
||||
return html`
|
||||
<div class="maincontainer">
|
||||
<dees-simple-login name="Onebox">
|
||||
<dees-simple-appdash
|
||||
name="Onebox"
|
||||
.viewTabs=${this.resolvedViewTabs}
|
||||
.selectedView=${this.resolvedViewTabs.find(
|
||||
(t) => t.name.toLowerCase().replace(/\s+/g, '-') === this.uiState.activeView
|
||||
) || this.resolvedViewTabs[0]}
|
||||
.selectedView=${this.currentViewTab}
|
||||
>
|
||||
</dees-simple-appdash>
|
||||
</dees-simple-login>
|
||||
@@ -108,15 +221,8 @@ export class ObAppShell extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
// Resolve async view tab imports
|
||||
this.resolvedViewTabs = await Promise.all(
|
||||
this.viewTabs.map(async (tab) => ({
|
||||
name: tab.name,
|
||||
iconName: tab.iconName,
|
||||
element: await tab.element,
|
||||
})),
|
||||
);
|
||||
public override async firstUpdated() {
|
||||
this.resolvedViewTabs = await this.resolveViewTabs(this.viewTabs);
|
||||
this.requestUpdate();
|
||||
await this.updateComplete;
|
||||
|
||||
@@ -130,34 +236,44 @@ export class ObAppShell extends DeesElement {
|
||||
const appDash = this.shadowRoot!.querySelector('dees-simple-appdash') as any;
|
||||
if (appDash) {
|
||||
appDash.addEventListener('view-select', (e: CustomEvent) => {
|
||||
const viewName = e.detail.view.name.toLowerCase().replace(/\s+/g, '-');
|
||||
appRouter.navigateToView(viewName);
|
||||
const view = e.detail.view as IResolvedView;
|
||||
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);
|
||||
}
|
||||
});
|
||||
appDash.addEventListener('logout', async () => {
|
||||
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
||||
});
|
||||
}
|
||||
|
||||
// Load the initial view on the appdash now that tabs are resolved
|
||||
// Read activeView directly from state (not this.uiState which may be stale)
|
||||
if (appDash && this.resolvedViewTabs.length > 0) {
|
||||
const currentActiveView = appstate.uiStatePart.getState().activeView;
|
||||
const initialView = this.resolvedViewTabs.find(
|
||||
(t) => t.name.toLowerCase().replace(/\s+/g, '-') === currentActiveView,
|
||||
) || this.resolvedViewTabs[0];
|
||||
const currentUiState = appstate.uiStatePart.getState();
|
||||
const initialView =
|
||||
this.findViewBySlug(currentUiState.activeView, currentUiState.activeSubview) ||
|
||||
this.resolvedViewTabs[0];
|
||||
await appDash.loadView(initialView);
|
||||
}
|
||||
|
||||
// Check for stored session (persistent login state)
|
||||
const loginState = appstate.loginStatePart.getState();
|
||||
if (loginState.identity?.jwt) {
|
||||
if (loginState.identity.expiresAt > Date.now()) {
|
||||
// Switch to dashboard immediately (no flash of login form)
|
||||
this.loginState = loginState;
|
||||
if (simpleLogin) {
|
||||
await simpleLogin.switchToSlottedContent();
|
||||
}
|
||||
// Validate token with server in the background
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetSystemStatus
|
||||
@@ -165,11 +281,9 @@ export class ObAppShell extends DeesElement {
|
||||
const response = await typedRequest.fire({ identity: loginState.identity });
|
||||
appstate.systemStatePart.setState({ status: response.status });
|
||||
} catch (err) {
|
||||
// Token rejected by server - switch back to login
|
||||
console.warn('Stored session invalid, returning to login:', err);
|
||||
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
||||
if (simpleLogin) {
|
||||
// Force page reload to show login properly
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
@@ -210,14 +324,13 @@ export class ObAppShell extends DeesElement {
|
||||
}
|
||||
}
|
||||
|
||||
private syncAppdashView(viewName: string): void {
|
||||
private syncAppdashView(viewName: string, subviewName: string | null): void {
|
||||
const appDash = this.shadowRoot?.querySelector('dees-simple-appdash') as any;
|
||||
if (!appDash || this.resolvedViewTabs.length === 0) return;
|
||||
// Match kebab-case view name (e.g., 'app-store') to tab name (e.g., 'App Store')
|
||||
const targetTab = this.resolvedViewTabs.find(
|
||||
(t) => t.name.toLowerCase().replace(/\s+/g, '-') === viewName
|
||||
);
|
||||
if (!targetTab) return;
|
||||
|
||||
const targetTab = this.findViewBySlug(viewName, subviewName);
|
||||
if (!targetTab || appDash.selectedView === targetTab) return;
|
||||
|
||||
appDash.loadView(targetTab);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user