Files
onebox/ts_web/elements/ob-app-shell.ts
T

337 lines
10 KiB
TypeScript
Raw Normal View History

import * as plugins from '../plugins.js';
import * as appstate from '../appstate.js';
import * as interfaces from '../../ts_interfaces/index.js';
import { appRouter } from '../router.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
type TemplateResult,
} from '@design.estate/dees-element';
2026-05-21 18:38:44 +00:00
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 {
@state()
accessor loginState: appstate.ILoginState = { identity: null, isLoggedIn: false };
@state()
accessor uiState: appstate.IUiState = {
activeView: 'dashboard',
2026-05-21 18:38:44 +00:00
activeSubview: null,
autoRefresh: true,
refreshInterval: 30000,
};
@state()
accessor loginLoading: boolean = false;
@state()
accessor loginError: string = '';
2026-05-21 18:38:44 +00:00
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)(),
},
];
2026-05-21 18:38:44 +00:00
private resolvedViewTabs: IResolvedView[] = [];
constructor() {
super();
document.title = 'Onebox';
const loginSubscription = appstate.loginStatePart
2026-05-21 18:38:44 +00:00
.select((stateArg: appstate.ILoginState) => stateArg)
.subscribe((loginState: appstate.ILoginState) => {
this.loginState = loginState;
if (loginState.isLoggedIn) {
appstate.systemStatePart.dispatchAction(appstate.fetchSystemStatusAction, null);
}
});
this.rxSubscriptions.push(loginSubscription);
const uiSubscription = appstate.uiStatePart
2026-05-21 18:38:44 +00:00
.select((stateArg: appstate.IUiState) => stateArg)
.subscribe((uiState: appstate.IUiState) => {
this.uiState = uiState;
2026-05-21 18:38:44 +00:00
this.syncAppdashView(uiState.activeView, uiState.activeSubview);
});
this.rxSubscriptions.push(uiSubscription);
}
2026-05-21 18:38:44 +00:00
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 {
display: block;
width: 100%;
height: 100%;
}
.maincontainer {
width: 100%;
height: 100vh;
}
`,
];
2026-05-21 18:38:44 +00:00
public override render(): TemplateResult {
return html`
<div class="maincontainer">
<dees-simple-login name="Onebox">
<dees-simple-appdash
name="Onebox"
.viewTabs=${this.resolvedViewTabs}
2026-05-21 18:38:44 +00:00
.selectedView=${this.currentViewTab}
>
</dees-simple-appdash>
</dees-simple-login>
</div>
`;
}
2026-05-21 18:38:44 +00:00
public override async firstUpdated() {
this.resolvedViewTabs = await this.resolveViewTabs(this.viewTabs);
this.requestUpdate();
await this.updateComplete;
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
if (simpleLogin) {
simpleLogin.addEventListener('login', (e: CustomEvent) => {
this.login(e.detail.data.username, e.detail.data.password);
});
}
const appDash = this.shadowRoot!.querySelector('dees-simple-appdash') as any;
if (appDash) {
appDash.addEventListener('view-select', (e: CustomEvent) => {
2026-05-21 18:38:44 +00:00
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);
});
}
if (appDash && this.resolvedViewTabs.length > 0) {
2026-05-21 18:38:44 +00:00
const currentUiState = appstate.uiStatePart.getState();
const initialView =
this.findViewBySlug(currentUiState.activeView, currentUiState.activeSubview) ||
this.resolvedViewTabs[0];
await appDash.loadView(initialView);
}
const loginState = appstate.loginStatePart.getState();
if (loginState.identity?.jwt) {
if (loginState.identity.expiresAt > Date.now()) {
this.loginState = loginState;
if (simpleLogin) {
await simpleLogin.switchToSlottedContent();
}
try {
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetSystemStatus
>('/typedrequest', 'getSystemStatus');
const response = await typedRequest.fire({ identity: loginState.identity });
appstate.systemStatePart.setState({ status: response.status });
} catch (err) {
console.warn('Stored session invalid, returning to login:', err);
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
if (simpleLogin) {
window.location.reload();
}
}
} else {
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
}
}
}
private async login(username: string, password: string) {
const domtools = await this.domtoolsPromise;
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
const form = simpleLogin?.shadowRoot?.querySelector('dees-form') as any;
if (form) {
form.setStatus('pending', 'Logging in...');
}
const newState = await appstate.loginStatePart.dispatchAction(appstate.loginAction, {
username,
password,
});
if (newState.identity) {
if (form) {
form.setStatus('success', 'Logged in!');
}
if (simpleLogin) {
await simpleLogin.switchToSlottedContent();
}
await appstate.systemStatePart.dispatchAction(appstate.fetchSystemStatusAction, null);
} else {
if (form) {
form.setStatus('error', 'Login failed!');
await domtools.convenience.smartdelay.delayFor(2000);
form.reset();
}
}
}
2026-05-21 18:38:44 +00:00
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;
2026-05-21 18:38:44 +00:00
const targetTab = this.findViewBySlug(viewName, subviewName);
if (!targetTab || appDash.selectedView === targetTab) return;
appDash.loadView(targetTab);
}
}