import { DeesElement, html, customElement, type TemplateResult, css, state, cssManager, } from '@design.estate/dees-element'; import * as appstate from '../appstate.js'; import * as interfaces from '../../dist_ts_interfaces/index.js'; import { viewHostCss } from './shared/css.js'; import { type IStatsTile } from '@design.estate/dees-catalog'; declare global { interface HTMLElementTagNameMap { 'ops-view-remoteingress': OpsViewRemoteIngress; } } @customElement('ops-view-remoteingress') export class OpsViewRemoteIngress extends DeesElement { @state() accessor riState: appstate.IRemoteIngressState = appstate.remoteIngressStatePart.getState(); constructor() { super(); const sub = appstate.remoteIngressStatePart.state.subscribe((newState) => { this.riState = newState; }); this.rxSubscriptions.push(sub); } async connectedCallback() { await super.connectedCallback(); await appstate.remoteIngressStatePart.dispatchAction(appstate.fetchRemoteIngressAction, null); } public static styles = [ cssManager.defaultStyles, viewHostCss, css` .remoteIngressContainer { display: flex; flex-direction: column; gap: 24px; } .statusBadge { display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; letter-spacing: 0.02em; text-transform: uppercase; } .statusBadge.connected { background: ${cssManager.bdTheme('#dcfce7', '#14532d')}; color: ${cssManager.bdTheme('#166534', '#4ade80')}; } .statusBadge.disconnected { background: ${cssManager.bdTheme('#fef2f2', '#450a0a')}; color: ${cssManager.bdTheme('#991b1b', '#f87171')}; } .statusBadge.disabled { background: ${cssManager.bdTheme('#f3f4f6', '#374151')}; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .secretDialog { padding: 16px; background: ${cssManager.bdTheme('#fffbeb', '#1c1917')}; border: 1px solid ${cssManager.bdTheme('#fbbf24', '#92400e')}; border-radius: 8px; margin-bottom: 16px; } .secretDialog code { display: block; padding: 8px 12px; background: ${cssManager.bdTheme('#1f2937', '#111827')}; color: #10b981; border-radius: 4px; font-family: monospace; font-size: 13px; word-break: break-all; margin: 8px 0; user-select: all; } .secretDialog .warning { font-size: 12px; color: ${cssManager.bdTheme('#92400e', '#fbbf24')}; margin-top: 8px; } .portsDisplay { display: flex; gap: 4px; flex-wrap: wrap; } .portBadge { display: inline-flex; padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; background: ${cssManager.bdTheme('#eff6ff', '#172554')}; color: ${cssManager.bdTheme('#1e40af', '#60a5fa')}; } `, ]; render(): TemplateResult { const totalEdges = this.riState.edges.length; const connectedEdges = this.riState.statuses.filter(s => s.connected).length; const disconnectedEdges = totalEdges - connectedEdges; const activeTunnels = this.riState.statuses.reduce((sum, s) => sum + s.activeTunnels, 0); const statsTiles: IStatsTile[] = [ { id: 'totalEdges', title: 'Total Edges', type: 'number', value: totalEdges, icon: 'lucide:server', description: 'Registered edge nodes', color: '#3b82f6', }, { id: 'connectedEdges', title: 'Connected', type: 'number', value: connectedEdges, icon: 'lucide:link', description: 'Currently connected edges', color: '#10b981', }, { id: 'disconnectedEdges', title: 'Disconnected', type: 'number', value: disconnectedEdges, icon: 'lucide:unlink', description: 'Offline edge nodes', color: disconnectedEdges > 0 ? '#ef4444' : '#6b7280', }, { id: 'activeTunnels', title: 'Active Tunnels', type: 'number', value: activeTunnels, icon: 'lucide:cable', description: 'Active client connections', color: '#8b5cf6', }, ]; return html` Remote Ingress ${this.riState.newEdgeSecret ? html`
Edge Secret (copy now - shown only once): ${this.riState.newEdgeSecret}
This secret will not be shown again. Save it securely.
appstate.remoteIngressStatePart.dispatchAction(appstate.clearNewEdgeSecretAction, null)} >Dismiss
` : ''}
({ name: edge.name, status: this.getEdgeStatusHtml(edge), publicIp: this.getEdgePublicIp(edge.id), ports: this.getPortsHtml(edge.listenPorts), tunnels: this.getEdgeTunnelCount(edge.id), lastHeartbeat: this.getLastHeartbeat(edge.id), })} .dataActions=${[ { name: 'Create Edge Node', iconName: 'lucide:plus', type: ['header'], actionFunc: async () => { const { DeesModal } = await import('@design.estate/dees-catalog'); const result = await DeesModal.createAndShow({ heading: 'Create Edge Node', content: html` `, menuOptions: [], }); if (result) { const formData = result as any; const ports = (formData.name ? formData.listenPorts : '443') .split(',') .map((p: string) => parseInt(p.trim(), 10)) .filter((p: number) => !isNaN(p)); const tags = formData.tags ? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean) : undefined; await appstate.remoteIngressStatePart.dispatchAction( appstate.createRemoteIngressAction, { name: formData.name, listenPorts: ports, tags, }, ); } }, }, { name: 'Regenerate Secret', iconName: 'lucide:key', type: ['row'], action: async (edge: interfaces.data.IRemoteIngress) => { await appstate.remoteIngressStatePart.dispatchAction( appstate.regenerateRemoteIngressSecretAction, edge.id, ); }, }, { name: 'Delete', iconName: 'lucide:trash2', type: ['row'], action: async (edge: interfaces.data.IRemoteIngress) => { await appstate.remoteIngressStatePart.dispatchAction( appstate.deleteRemoteIngressAction, edge.id, ); }, }, ]} >
`; } private getEdgeStatus(edgeId: string): interfaces.data.IRemoteIngressStatus | undefined { return this.riState.statuses.find(s => s.edgeId === edgeId); } private getEdgeStatusHtml(edge: interfaces.data.IRemoteIngress): TemplateResult { if (!edge.enabled) { return html`Disabled`; } const status = this.getEdgeStatus(edge.id); if (status?.connected) { return html`Connected`; } return html`Disconnected`; } private getEdgePublicIp(edgeId: string): string { const status = this.getEdgeStatus(edgeId); return status?.publicIp || '-'; } private getPortsHtml(ports: number[]): TemplateResult { return html`
${ports.map(p => html`${p}`)}
`; } private getEdgeTunnelCount(edgeId: string): number { const status = this.getEdgeStatus(edgeId); return status?.activeTunnels || 0; } private getLastHeartbeat(edgeId: string): string { const status = this.getEdgeStatus(edgeId); if (!status?.lastHeartbeat) return '-'; const ago = Date.now() - status.lastHeartbeat; if (ago < 60000) return `${Math.floor(ago / 1000)}s ago`; if (ago < 3600000) return `${Math.floor(ago / 60000)}m ago`; return `${Math.floor(ago / 3600000)}h ago`; } }