import * as plugins from '../plugins.js'; import * as appstate from '../appstate.js'; import * as interfaces from '../../dist_ts_interfaces/index.js'; import { appRouter } from '../router.js'; import { DeesElement, css, cssManager, customElement, html, state, type TemplateResult } from '@design.estate/dees-element'; import type { IView } from '@design.estate/dees-catalog'; // Top-level / flat views import { OpsViewLogs } from './ops-view-logs.js'; import { OpsViewCertificates } from './ops-view-certificates.js'; // Overview group import { OpsViewOverview } from './overview/ops-view-overview.js'; import { OpsViewConfig } from './overview/ops-view-config.js'; // Network group import { OpsViewNetworkActivity } from './network/ops-view-network-activity.js'; import { OpsViewRoutes } from './network/ops-view-routes.js'; import { OpsViewSourceProfiles } from './network/ops-view-sourceprofiles.js'; import { OpsViewNetworkTargets } from './network/ops-view-networktargets.js'; import { OpsViewTargetProfiles } from './network/ops-view-targetprofiles.js'; import { OpsViewRemoteIngress } from './network/ops-view-remoteingress.js'; import { OpsViewVpn } from './network/ops-view-vpn.js'; // Email group import { OpsViewEmails } from './email/ops-view-emails.js'; import { OpsViewEmailSecurity } from './email/ops-view-email-security.js'; // Access group import { OpsViewApiTokens } from './access/ops-view-apitokens.js'; // Security group import { OpsViewSecurityOverview } from './security/ops-view-security-overview.js'; import { OpsViewSecurityBlocked } from './security/ops-view-security-blocked.js'; import { OpsViewSecurityAuthentication } from './security/ops-view-security-authentication.js'; /** * Extended IView with explicit URL slug. Without an explicit `slug`, the URL * slug is derived from `name.toLowerCase().replace(/\s+/g, '')`. */ interface ITabbedView extends IView { slug?: string; subViews?: ITabbedView[]; } @customElement('ops-dashboard') export class OpsDashboard extends DeesElement { @state() accessor loginState: appstate.ILoginState = { identity: null, isLoggedIn: false, }; @state() accessor uiState: appstate.IUiState = { activeView: 'overview', activeSubview: null, sidebarCollapsed: false, autoRefresh: true, refreshInterval: 1000, theme: 'light', }; @state() accessor configState: appstate.IConfigState = { config: null, isLoading: false, error: null, }; // Store viewTabs as a property to maintain object references (used for === selectedView identity) private viewTabs: ITabbedView[] = [ { name: 'Overview', iconName: 'lucide:layoutDashboard', subViews: [ { slug: 'stats', name: 'Stats', iconName: 'lucide:activity', element: OpsViewOverview }, { slug: 'configuration', name: 'Configuration', iconName: 'lucide:settings', element: OpsViewConfig }, ], }, { name: 'Network', iconName: 'lucide:network', subViews: [ { slug: 'activity', name: 'Network Activity', iconName: 'lucide:activity', element: OpsViewNetworkActivity }, { slug: 'routes', name: 'Routes', iconName: 'lucide:route', element: OpsViewRoutes }, { slug: 'sourceprofiles', name: 'Source Profiles', iconName: 'lucide:shieldCheck', element: OpsViewSourceProfiles }, { slug: 'networktargets', name: 'Network Targets', iconName: 'lucide:server', element: OpsViewNetworkTargets }, { slug: 'targetprofiles', name: 'Target Profiles', iconName: 'lucide:target', element: OpsViewTargetProfiles }, { slug: 'remoteingress', name: 'Remote Ingress', iconName: 'lucide:globe', element: OpsViewRemoteIngress }, { slug: 'vpn', name: 'VPN', iconName: 'lucide:shield', element: OpsViewVpn }, ], }, { name: 'Email', iconName: 'lucide:mail', subViews: [ { slug: 'log', name: 'Email Log', iconName: 'lucide:scrollText', element: OpsViewEmails }, { slug: 'security', name: 'Email Security', iconName: 'lucide:shieldCheck', element: OpsViewEmailSecurity }, ], }, { name: 'Logs', iconName: 'lucide:scrollText', element: OpsViewLogs, }, { name: 'Access', iconName: 'lucide:keyRound', subViews: [ { slug: 'apitokens', name: 'API Tokens', iconName: 'lucide:key', element: OpsViewApiTokens }, ], }, { name: 'Security', iconName: 'lucide:shield', subViews: [ { slug: 'overview', name: 'Overview', iconName: 'lucide:eye', element: OpsViewSecurityOverview }, { slug: 'blocked', name: 'Blocked IPs', iconName: 'lucide:shieldBan', element: OpsViewSecurityBlocked }, { slug: 'authentication', name: 'Authentication', iconName: 'lucide:lock', element: OpsViewSecurityAuthentication }, ], }, { name: 'Certificates', iconName: 'lucide:badgeCheck', element: OpsViewCertificates, }, ]; /** URL slug for a view (explicit `slug` field, or lowercased name with spaces stripped). */ private slugFor(view: ITabbedView): string { return view.slug ?? view.name.toLowerCase().replace(/\s+/g, ''); } /** Find the parent group of a subview, or undefined for top-level views. */ private findParent(view: ITabbedView): ITabbedView | undefined { return this.viewTabs.find((v) => v.subViews?.includes(view)); } /** Look up a view (or subview) by its URL slug pair. */ private findViewBySlug(viewSlug: string, subSlug: string | null): ITabbedView | undefined { const top = this.viewTabs.find((v) => this.slugFor(v) === viewSlug); if (!top) return undefined; if (subSlug && top.subViews) { return top.subViews.find((sv) => this.slugFor(sv) === subSlug) ?? top; } return top; } private get globalMessages() { const messages: Array<{ id: string; type: string; message: string; dismissible?: boolean }> = []; const config = this.configState.config; if (config && !config.cache.enabled) { messages.push({ id: 'db-disabled', type: 'warning', message: 'Database is disabled. Creating and editing routes, profiles, targets, and API tokens is not available.', dismissible: false, }); } return messages; } /** * Get the current view tab based on the UI state's activeView/activeSubview. * Used to pass the correct selectedView to dees-simple-appdash on initial render. */ private get currentViewTab(): ITabbedView { return ( this.findViewBySlug(this.uiState.activeView, this.uiState.activeSubview) ?? this.viewTabs[0] ); } constructor() { super(); document.title = 'DCRouter OpsServer'; // Subscribe to login state const loginSubscription = appstate.loginStatePart .select((stateArg) => stateArg) .subscribe((loginState) => { this.loginState = loginState; // Trigger data fetch when logged in if (loginState.isLoggedIn) { appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null); appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null); } }); this.rxSubscriptions.push(loginSubscription); // Subscribe to config state (for global warnings) const configSubscription = appstate.configStatePart .select((stateArg) => stateArg) .subscribe((configState) => { this.configState = configState; }); this.rxSubscriptions.push(configSubscription); // Subscribe to UI state const uiSubscription = appstate.uiStatePart .select((stateArg) => stateArg) .subscribe((uiState) => { this.uiState = uiState; // Sync appdash view when state changes (e.g., from URL navigation) this.syncAppdashView(uiState.activeView, uiState.activeSubview); }); this.rxSubscriptions.push(uiSubscription); } /** * Sync the dees-simple-appdash view selection with the current state. * This is needed when the URL changes externally (back/forward, deep link). */ 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) return; if (appDash.selectedView === targetView) return; // Use loadView to update both selectedView and the mounted element. // It will dispatch view-select; our handler skips when state already matches. appDash.loadView(targetView); } public static styles = [ cssManager.defaultStyles, css` :host { display: block; width: 100%; height: 100vh; overflow: hidden; } .maincontainer { position: relative; width: 100%; height: 100%; } `, ]; public render(): TemplateResult { return html`