import { DeesElement, customElement, html, css, cssManager, property, type TemplateResult, } from '@design.estate/dees-element'; declare global { interface HTMLElementTagNameMap { 'sz-route-card': SzRouteCard; } } // Simplified route types for display purposes export type TRouteActionType = 'forward' | 'socket-handler'; export type TTlsMode = 'passthrough' | 'terminate' | 'terminate-and-reencrypt'; export type TPortRange = number | number[] | Array<{ from: number; to: number }>; export interface IRouteMatch { ports: TPortRange; domains?: string | string[]; path?: string; clientIp?: string[]; tlsVersion?: string[]; headers?: Record; protocol?: 'http' | 'tcp'; } export interface IRouteTarget { host: string | string[]; port: number | 'preserve'; } export interface IRouteTls { mode: TTlsMode; certificate?: 'auto' | { key: string; cert: string }; } export interface IRouteAction { type: TRouteActionType; targets?: IRouteTarget[]; tls?: IRouteTls; websocket?: { enabled: boolean }; loadBalancing?: { algorithm: 'round-robin' | 'least-connections' | 'ip-hash' }; forwardingEngine?: 'node' | 'nftables'; } export interface IRouteSecurity { ipAllowList?: string[]; ipBlockList?: string[]; maxConnections?: number; rateLimit?: { enabled: boolean; maxRequests: number; window: number }; } export interface IRouteConfig { id?: string; match: IRouteMatch; action: IRouteAction; security?: IRouteSecurity; headers?: { request?: Record; response?: Record }; name?: string; description?: string; priority?: number; tags?: string[]; enabled?: boolean; } function formatPorts(ports: TPortRange): string { if (typeof ports === 'number') return String(ports); if (Array.isArray(ports)) { return ports .map((p) => { if (typeof p === 'number') return String(p); return `${p.from}\u2013${p.to}`; }) .join(', '); } return String(ports); } function formatTargets(targets: IRouteTarget[]): string[] { const result: string[] = []; for (const t of targets) { const hosts = Array.isArray(t.host) ? t.host : [t.host]; const portStr = t.port === 'preserve' ? '(preserve)' : String(t.port); for (const h of hosts) { result.push(`${h}:${portStr}`); } } return result; } @customElement('sz-route-card') export class SzRouteCard extends DeesElement { public static demo = () => html`
`; public static demoGroups = ['Routes']; @property({ type: Object }) public accessor route: IRouteConfig | null = null; public static styles = [ cssManager.defaultStyles, css` :host { display: block; } .card { background: ${cssManager.bdTheme('#ffffff', '#09090b')}; border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')}; border-radius: 8px; padding: 20px; transition: border-color 200ms ease, box-shadow 200ms ease; } .card:hover { border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')}; box-shadow: 0 2px 8px ${cssManager.bdTheme('rgba(0,0,0,0.06)', 'rgba(0,0,0,0.2)')}; } /* Header */ .header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 4px; } .header-left { display: flex; align-items: center; gap: 8px; min-width: 0; } .status-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } .status-dot.enabled { background: ${cssManager.bdTheme('#22c55e', '#22c55e')}; box-shadow: 0 0 6px ${cssManager.bdTheme('rgba(34,197,94,0.4)', 'rgba(34,197,94,0.3)')}; } .status-dot.disabled { background: ${cssManager.bdTheme('#a1a1aa', '#52525b')}; } .route-name { font-size: 15px; font-weight: 600; color: ${cssManager.bdTheme('#18181b', '#fafafa')}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .header-badges { display: flex; gap: 6px; flex-shrink: 0; } .badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 9999px; font-size: 11px; font-weight: 500; white-space: nowrap; } .badge.forward { background: ${cssManager.bdTheme('#dbeafe', 'rgba(59, 130, 246, 0.2)')}; color: ${cssManager.bdTheme('#2563eb', '#60a5fa')}; } .badge.socket-handler { background: ${cssManager.bdTheme('#ede9fe', 'rgba(139, 92, 246, 0.2)')}; color: ${cssManager.bdTheme('#7c3aed', '#a78bfa')}; } .badge.enabled { background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')}; color: ${cssManager.bdTheme('#16a34a', '#22c55e')}; } .badge.disabled { background: ${cssManager.bdTheme('#f4f4f5', 'rgba(113, 113, 122, 0.2)')}; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; } .description { font-size: 13px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; margin-bottom: 8px; line-height: 1.4; } .meta-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 6px; margin-bottom: 16px; } .tags { display: flex; flex-wrap: wrap; gap: 4px; } .tag { padding: 2px 8px; background: ${cssManager.bdTheme('#f4f4f5', '#27272a')}; border-radius: 4px; font-size: 11px; font-weight: 500; color: ${cssManager.bdTheme('#52525b', '#a1a1aa')}; } .priority { font-size: 11px; color: ${cssManager.bdTheme('#a1a1aa', '#71717a')}; font-weight: 500; } /* Sections */ .section { border-left: 3px solid; padding: 10px 14px; margin-bottom: 12px; border-radius: 0 6px 6px 0; background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')}; } .section:last-of-type { margin-bottom: 0; } .section.match { border-left-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')}; } .section.action { border-left-color: ${cssManager.bdTheme('#22c55e', '#22c55e')}; } .section.security { border-left-color: ${cssManager.bdTheme('#f59e0b', '#f59e0b')}; } .section-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: ${cssManager.bdTheme('#a1a1aa', '#71717a')}; margin-bottom: 8px; } .field-row { display: flex; gap: 8px; margin-bottom: 5px; font-size: 13px; line-height: 1.5; } .field-row:last-child { margin-bottom: 0; } .field-key { color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; min-width: 64px; flex-shrink: 0; font-weight: 500; } .field-value { color: ${cssManager.bdTheme('#18181b', '#e4e4e7')}; word-break: break-all; } .domain-chip { display: inline-flex; padding: 1px 6px; background: ${cssManager.bdTheme('#eff6ff', 'rgba(59, 130, 246, 0.1)')}; border-radius: 3px; font-size: 12px; margin-right: 4px; margin-bottom: 2px; font-family: monospace; } .domain-chip.glob { background: ${cssManager.bdTheme('#fef3c7', 'rgba(245, 158, 11, 0.15)')}; color: ${cssManager.bdTheme('#92400e', '#fbbf24')}; } .mono { font-family: monospace; font-size: 12px; } .protocol-badge { display: inline-flex; padding: 1px 6px; border-radius: 3px; font-size: 11px; font-weight: 500; } .protocol-badge.http { background: ${cssManager.bdTheme('#dbeafe', 'rgba(59, 130, 246, 0.2)')}; color: ${cssManager.bdTheme('#2563eb', '#60a5fa')}; } .protocol-badge.tcp { background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')}; color: ${cssManager.bdTheme('#16a34a', '#22c55e')}; } .tls-badge { display: inline-flex; padding: 1px 6px; border-radius: 3px; font-size: 11px; font-weight: 500; } .tls-badge.auto { background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')}; color: ${cssManager.bdTheme('#16a34a', '#22c55e')}; } .tls-badge.custom { background: ${cssManager.bdTheme('#ffedd5', 'rgba(249, 115, 22, 0.2)')}; color: ${cssManager.bdTheme('#c2410c', '#fb923c')}; } .engine-badge { display: inline-flex; padding: 1px 6px; border-radius: 3px; font-size: 11px; font-weight: 500; background: ${cssManager.bdTheme('#fae8ff', 'rgba(168, 85, 247, 0.2)')}; color: ${cssManager.bdTheme('#7e22ce', '#c084fc')}; } .header-pair { display: inline; font-family: monospace; font-size: 12px; } /* Feature icons */ .features-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; padding-top: 12px; border-top: 1px solid ${cssManager.bdTheme('#f4f4f5', '#1a1a1a')}; } .feature { display: flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 500; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; } .feature-icon { font-size: 13px; } .no-route { text-align: center; padding: 24px; color: ${cssManager.bdTheme('#a1a1aa', '#52525b')}; font-size: 13px; } `, ]; public render(): TemplateResult { if (!this.route) { return html`
No route data
`; } const r = this.route; const isEnabled = r.enabled !== false; const match = r.match; const action = r.action; const security = r.security; return html`
${r.name || r.id || 'Unnamed Route'}
${action.type} ${isEnabled ? 'enabled' : 'disabled'}
${r.description ? html`
${r.description}
` : ''}
${r.tags && r.tags.length > 0 ? html`
${r.tags.map((t) => html`${t}`)}
` : html`
`} ${r.priority != null ? html`Priority: ${r.priority}` : ''}
Ports ${formatPorts(match.ports)}
${match.domains ? html`
Domains ${this.renderDomains(match.domains)}
` : ''} ${match.path ? html`
Path ${match.path}
` : ''} ${match.protocol ? html`
Protocol ${match.protocol}
` : ''} ${match.clientIp && match.clientIp.length > 0 ? html`
Client ${match.clientIp.join(', ')}
` : ''} ${match.tlsVersion && match.tlsVersion.length > 0 ? html`
TLS Ver ${match.tlsVersion.join(', ')}
` : ''} ${match.headers ? html`
Headers ${Object.entries(match.headers).map( ([k, v]) => html`${k}=${v} ` )}
` : ''}
${action.targets && action.targets.length > 0 ? html`
Targets ${formatTargets(action.targets).join(', ')}
` : ''} ${action.tls ? html`
TLS ${action.tls.mode} ${action.tls.certificate ? action.tls.certificate === 'auto' ? html` auto cert` : html` custom cert` : ''}
` : ''} ${action.forwardingEngine ? html`
Engine ${action.forwardingEngine}
` : ''} ${action.loadBalancing ? html`
LB ${action.loadBalancing.algorithm}
` : ''} ${action.websocket?.enabled ? html`
WS enabled
` : ''}
${security ? html`
${security.ipAllowList && security.ipAllowList.length > 0 ? html`
Allow ${security.ipAllowList.join(', ')}
` : ''} ${security.ipBlockList && security.ipBlockList.length > 0 ? html`
Block ${security.ipBlockList.join(', ')}
` : ''} ${security.rateLimit?.enabled ? html`
Rate ${security.rateLimit.maxRequests} req / ${security.rateLimit.window}s
` : ''} ${security.maxConnections ? html`
Max Conn ${security.maxConnections}
` : ''}
` : ''} ${this.renderFeatures()}
`; } private renderDomains(domains: string | string[]): TemplateResult { const list = Array.isArray(domains) ? domains : [domains]; return html`${list.map( (d) => html`${d}` )}`; } private renderFeatures(): TemplateResult { if (!this.route) return html``; const features: TemplateResult[] = []; const action = this.route.action; const security = this.route.security; const headers = this.route.headers; if (action.tls) { features.push(html`🔒TLS`); } if (action.websocket?.enabled) { features.push(html`WS`); } if (action.loadBalancing) { features.push(html`LB`); } if (security) { features.push(html`🛡Security`); } if (headers) { features.push(html`Headers`); } if (features.length === 0) return html``; return html`
${features}
`; } }