From d1c19389d7c5852283e22f7bd4c5feb03cac1c92 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sun, 22 Feb 2026 01:05:56 +0000 Subject: [PATCH] feat(routes): add route UI components and demo view with list/card and app-shell integration --- changelog.md | 9 + ts_web/00_commitinfo_data.ts | 2 +- ts_web/elements/index.ts | 5 + ts_web/elements/sz-demo-view-routes.ts | 362 ++++++++++++++ ts_web/elements/sz-route-card.ts | 667 +++++++++++++++++++++++++ ts_web/elements/sz-route-list-view.ts | 326 ++++++++++++ ts_web/pages/sz-demo-app-shell.ts | 8 +- 7 files changed, 1377 insertions(+), 2 deletions(-) create mode 100644 ts_web/elements/sz-demo-view-routes.ts create mode 100644 ts_web/elements/sz-route-card.ts create mode 100644 ts_web/elements/sz-route-list-view.ts diff --git a/changelog.md b/changelog.md index 691afbe..556c411 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2026-02-22 - 2.3.0 - feat(routes) +add route UI components and demo view with list/card and app-shell integration + +- Add new route UI components: sz-route-card, sz-route-list-view, and sz-demo-view-routes under ts_web/elements +- Export new components from ts_web/elements/index.ts and register demo view in the demo app shell menu (ts_web/pages/sz-demo-app-shell.ts) +- sz-route-card introduces route types/interfaces (IRouteConfig, IRouteAction, IRouteMatch, IRouteTls, IRouteSecurity) and rich rendering (ports/domains formatting, feature icons, security and headers display) +- sz-route-list-view provides demo data, search, filtering (by action and enabled state), results count, grid rendering of sz-route-card, and emits a 'route-click' event +- Demo view integrates with app UI secondary menu (actions and statistics) and wires up route-click handling for interactivity + ## 2026-02-21 - 2.2.0 - feat(demo-mta) add MTA / Email demo views and components and integrate into demo app shell diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index db192b9..44fe2ea 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/catalog', - version: '2.2.0', + version: '2.3.0', description: 'UI component catalog for serve.zone' } diff --git a/ts_web/elements/index.ts b/ts_web/elements/index.ts index 204b8c4..a4bbd85 100644 --- a/ts_web/elements/index.ts +++ b/ts_web/elements/index.ts @@ -49,6 +49,10 @@ export * from './sz-domain-detail-view.js'; export * from './sz-mta-list-view.js'; export * from './sz-mta-detail-view.js'; +// Route Configuration Views +export * from './sz-route-card.js'; +export * from './sz-route-list-view.js'; + // Demo Views export * from './sz-demo-view-dashboard.js'; export * from './sz-demo-view-services.js'; @@ -57,3 +61,4 @@ export * from './sz-demo-view-registries.js'; export * from './sz-demo-view-tokens.js'; export * from './sz-demo-view-settings.js'; export * from './sz-demo-view-mta.js'; +export * from './sz-demo-view-routes.js'; diff --git a/ts_web/elements/sz-demo-view-routes.ts b/ts_web/elements/sz-demo-view-routes.ts new file mode 100644 index 0000000..0621ca1 --- /dev/null +++ b/ts_web/elements/sz-demo-view-routes.ts @@ -0,0 +1,362 @@ +import { + DeesElement, + customElement, + html, + css, + cssManager, + state, + type TemplateResult, +} from '@design.estate/dees-element'; +import type { DeesAppui } from '@design.estate/dees-catalog'; +import type { IRouteConfig } from './sz-route-card.js'; +import './index.js'; + +declare global { + interface HTMLElementTagNameMap { + 'sz-demo-view-routes': SzDemoViewRoutes; + } +} + +@customElement('sz-demo-view-routes') +export class SzDemoViewRoutes extends DeesElement { + private appui: DeesAppui | null = null; + + @state() + private accessor currentTab: 'all' | 'https' | 'email' | 'dns' = 'all'; + + private demoRoutes: IRouteConfig[] = [ + // 1. HTTPS with TLS termination + auto cert + { + id: 'route-1', + name: 'Web Frontend', + description: 'Main website with TLS termination and automatic certificates', + enabled: true, + priority: 10, + tags: ['web', 'https', 'production'], + match: { + ports: 443, + domains: ['serve.zone', 'www.serve.zone'], + protocol: 'http', + }, + action: { + type: 'forward', + targets: [{ host: '10.0.0.10', port: 3000 }], + tls: { mode: 'terminate', certificate: 'auto' }, + }, + }, + // 2. HTTP to HTTPS redirect + { + id: 'route-2', + name: 'HTTP Redirect', + description: 'Redirects all HTTP traffic to HTTPS', + enabled: true, + priority: 100, + tags: ['web', 'http', 'redirect'], + match: { + ports: 80, + domains: ['serve.zone', 'www.serve.zone'], + protocol: 'http', + }, + action: { + type: 'socket-handler', + }, + }, + // 3. Email SMTP route + { + id: 'route-3', + name: 'SMTP Inbound', + description: 'Inbound email relay for serve.zone domain', + enabled: true, + tags: ['email', 'smtp', 'production'], + match: { + ports: 25, + domains: 'mail.serve.zone', + }, + action: { + type: 'forward', + targets: [{ host: '10.0.1.5', port: 25 }], + tls: { mode: 'terminate', certificate: 'auto' }, + }, + }, + // 4. API gateway with path matching, rate limiting, CORS + { + id: 'route-4', + name: 'API Gateway', + description: 'API gateway with rate limiting, CORS headers, and load balancing', + enabled: true, + priority: 20, + tags: ['web', 'api', 'https', 'production'], + match: { + ports: 443, + domains: 'api.serve.zone', + path: '/v2/*', + protocol: 'http', + }, + action: { + type: 'forward', + targets: [ + { host: ['10.0.0.20', '10.0.0.21', '10.0.0.22'], port: 8080 }, + ], + tls: { mode: 'terminate', certificate: 'auto' }, + loadBalancing: { algorithm: 'round-robin' }, + }, + security: { + rateLimit: { enabled: true, maxRequests: 200, window: 60 }, + maxConnections: 5000, + }, + headers: { + response: { + 'Access-Control-Allow-Origin': '*', + 'X-Request-Id': '{{requestId}}', + }, + }, + }, + // 5. WebSocket route + { + id: 'route-5', + name: 'WebSocket Realtime', + description: 'Real-time WebSocket connections for live updates', + enabled: true, + tags: ['web', 'https', 'websocket'], + match: { + ports: 443, + domains: 'ws.serve.zone', + path: '/ws/*', + protocol: 'http', + }, + action: { + type: 'forward', + targets: [{ host: '10.0.0.30', port: 9090 }], + tls: { mode: 'terminate', certificate: 'auto' }, + websocket: { enabled: true }, + }, + }, + // 6. Wildcard domain route + { + id: 'route-6', + name: 'Tenant Wildcard', + description: 'Multi-tenant wildcard routing for customer subdomains', + enabled: true, + priority: 50, + tags: ['web', 'https', 'multi-tenant'], + match: { + ports: 443, + domains: '*.customers.serve.zone', + protocol: 'http', + }, + action: { + type: 'forward', + targets: [{ host: '10.0.0.40', port: 8080 }], + tls: { mode: 'terminate', certificate: 'auto' }, + }, + security: { + ipAllowList: ['10.0.0.0/8', '172.16.0.0/12'], + }, + }, + // 7. Load-balanced route with health check + { + id: 'route-7', + name: 'Microservices LB', + description: 'Load-balanced microservices backend with IP-hash affinity', + enabled: true, + tags: ['web', 'https', 'production'], + match: { + ports: [443, 8443], + domains: 'services.serve.zone', + protocol: 'http', + }, + action: { + type: 'forward', + targets: [ + { host: ['10.0.2.1', '10.0.2.2', '10.0.2.3', '10.0.2.4'], port: 3000 }, + ], + tls: { mode: 'terminate-and-reencrypt', certificate: 'auto' }, + loadBalancing: { algorithm: 'ip-hash' }, + }, + }, + // 8. DNS-over-HTTPS route + { + id: 'route-8', + name: 'DNS over HTTPS', + description: 'DNS-over-HTTPS resolver endpoint', + enabled: true, + tags: ['dns', 'https'], + match: { + ports: 443, + domains: 'dns.serve.zone', + path: '/dns-query', + protocol: 'http', + }, + action: { + type: 'forward', + targets: [{ host: '10.0.3.1', port: 8053 }], + tls: { mode: 'terminate', certificate: 'auto' }, + }, + }, + // 9. NFTables high-performance route + { + id: 'route-9', + name: 'High-Perf TCP Proxy', + description: 'NFTables-accelerated TCP proxy for game servers', + enabled: true, + tags: ['tcp', 'nftables', 'production'], + match: { + ports: [{ from: 27000, to: 27050 }], + protocol: 'tcp', + }, + action: { + type: 'forward', + targets: [{ host: '10.0.4.1', port: 'preserve' }], + forwardingEngine: 'nftables', + }, + }, + // 10. Disabled maintenance route + { + id: 'route-10', + name: 'Legacy Admin Panel', + description: 'Deprecated admin panel — disabled for maintenance', + enabled: false, + tags: ['web', 'https', 'deprecated'], + match: { + ports: 443, + domains: 'admin-old.serve.zone', + protocol: 'http', + }, + action: { + type: 'socket-handler', + }, + security: { + ipBlockList: ['0.0.0.0/0'], + }, + }, + ]; + + private get filteredRoutes(): IRouteConfig[] { + if (this.currentTab === 'all') return this.demoRoutes; + if (this.currentTab === 'https') { + return this.demoRoutes.filter((r) => + r.tags?.some((t) => ['web', 'https', 'http'].includes(t)) + ); + } + if (this.currentTab === 'email') { + return this.demoRoutes.filter((r) => + r.tags?.some((t) => ['email', 'smtp'].includes(t)) + ); + } + if (this.currentTab === 'dns') { + return this.demoRoutes.filter((r) => + r.tags?.some((t) => ['dns'].includes(t)) + ); + } + return this.demoRoutes; + } + + async onActivate(context: { appui: DeesAppui; viewId: string }) { + this.appui = context.appui; + + this.appui.setContentTabs([ + { + key: 'All Routes', + action: () => { + this.currentTab = 'all'; + this.updateSecondaryMenu(); + }, + }, + { + key: 'HTTP/S', + action: () => { + this.currentTab = 'https'; + this.updateSecondaryMenu(); + }, + }, + { + key: 'Email', + action: () => { + this.currentTab = 'email'; + this.updateSecondaryMenu(); + }, + }, + { + key: 'DNS', + action: () => { + this.currentTab = 'dns'; + this.updateSecondaryMenu(); + }, + }, + ]); + + this.updateSecondaryMenu(); + } + + private updateSecondaryMenu() { + if (!this.appui) return; + + const total = this.demoRoutes.length; + const active = this.demoRoutes.filter((r) => r.enabled !== false).length; + const forwardCount = this.demoRoutes.filter((r) => r.action.type === 'forward').length; + + this.appui.setSecondaryMenu({ + heading: 'Routes', + groups: [ + { + name: 'Actions', + items: [ + { + type: 'action', + key: 'Refresh', + iconName: 'lucide:RefreshCw', + action: () => { + console.log('Refresh routes'); + }, + }, + { + type: 'action', + key: 'Add Route', + iconName: 'lucide:Plus', + action: () => { + console.log('Add route'); + }, + }, + ], + }, + { + name: 'Statistics', + items: [ + { type: 'header' as const, label: `${total} Total Routes` }, + { type: 'header' as const, label: `${active} Active` }, + { type: 'header' as const, label: `${forwardCount} Forward` }, + { type: 'header' as const, label: `${total - forwardCount} Socket Handler` }, + ], + }, + ], + }); + } + + onDeactivate() { + // Cleanup if needed + } + + public static styles = [ + cssManager.defaultStyles, + css` + :host { + display: block; + padding: 24px; + height: 100%; + overflow-y: auto; + box-sizing: border-box; + } + `, + ]; + + public render(): TemplateResult { + return html` + ) => { + console.log('Route clicked:', e.detail.name); + }} + > + `; + } +} diff --git a/ts_web/elements/sz-route-card.ts b/ts_web/elements/sz-route-card.ts new file mode 100644 index 0000000..dbcc110 --- /dev/null +++ b/ts_web/elements/sz-route-card.ts @@ -0,0 +1,667 @@ +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}
`; + } +} diff --git a/ts_web/elements/sz-route-list-view.ts b/ts_web/elements/sz-route-list-view.ts new file mode 100644 index 0000000..ae80814 --- /dev/null +++ b/ts_web/elements/sz-route-list-view.ts @@ -0,0 +1,326 @@ +import { + DeesElement, + customElement, + html, + css, + cssManager, + property, + state, + type TemplateResult, +} from '@design.estate/dees-element'; + +import type { IRouteConfig, TRouteActionType } from './sz-route-card.js'; + +declare global { + interface HTMLElementTagNameMap { + 'sz-route-list-view': SzRouteListView; + } +} + +@customElement('sz-route-list-view') +export class SzRouteListView extends DeesElement { + public static demo = () => html` +
+ +
+ `; + + public static demoGroups = ['Routes']; + + @property({ type: Array }) + public accessor routes: IRouteConfig[] = []; + + @state() + private accessor searchQuery: string = ''; + + @state() + private accessor actionFilter: TRouteActionType | 'all' = 'all'; + + @state() + private accessor enabledFilter: 'all' | 'enabled' | 'disabled' = 'all'; + + private get filteredRoutes(): IRouteConfig[] { + return this.routes.filter((route) => { + // Action type filter + if (this.actionFilter !== 'all' && route.action.type !== this.actionFilter) return false; + + // Enabled/disabled filter + if (this.enabledFilter === 'enabled' && route.enabled === false) return false; + if (this.enabledFilter === 'disabled' && route.enabled !== false) return false; + + // Search query + if (this.searchQuery) { + const q = this.searchQuery.toLowerCase(); + return this.routeMatchesSearch(route, q); + } + return true; + }); + } + + private routeMatchesSearch(route: IRouteConfig, q: string): boolean { + // Name and description + if (route.name?.toLowerCase().includes(q)) return true; + if (route.description?.toLowerCase().includes(q)) return true; + + // Domains + if (route.match.domains) { + const domains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains]; + if (domains.some((d) => d.toLowerCase().includes(q))) return true; + } + + // Ports + const portsStr = this.formatPortsForSearch(route.match.ports); + if (portsStr.includes(q)) return true; + + // Path + if (route.match.path?.toLowerCase().includes(q)) return true; + + // Client IPs + if (route.match.clientIp?.some((ip) => ip.includes(q))) return true; + + // Targets + if (route.action.targets) { + for (const t of route.action.targets) { + const hosts = Array.isArray(t.host) ? t.host : [t.host]; + if (hosts.some((h) => h.toLowerCase().includes(q))) return true; + } + } + + // Tags + if (route.tags?.some((t) => t.toLowerCase().includes(q))) return true; + + return false; + } + + private formatPortsForSearch(ports: import('./sz-route-card.js').TPortRange): string { + if (typeof ports === 'number') return String(ports); + if (Array.isArray(ports)) { + return ports + .map((p) => (typeof p === 'number' ? String(p) : `${p.from}-${p.to}`)) + .join(' '); + } + return String(ports); + } + + public static styles = [ + cssManager.defaultStyles, + css` + :host { + display: block; + } + + .filter-bar { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + margin-bottom: 12px; + } + + .search-input { + flex: 1; + min-width: 200px; + padding: 8px 12px; + background: ${cssManager.bdTheme('#ffffff', '#09090b')}; + border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')}; + border-radius: 6px; + font-size: 14px; + color: ${cssManager.bdTheme('#18181b', '#fafafa')}; + outline: none; + transition: border-color 200ms ease; + } + + .search-input::placeholder { + color: ${cssManager.bdTheme('#a1a1aa', '#52525b')}; + } + + .search-input:focus { + border-color: ${cssManager.bdTheme('#2563eb', '#3b82f6')}; + } + + .chip-group { + display: flex; + gap: 4px; + } + + .chip { + padding: 6px 12px; + background: transparent; + border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')}; + border-radius: 9999px; + font-size: 12px; + font-weight: 500; + color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; + cursor: pointer; + transition: all 200ms ease; + white-space: nowrap; + } + + .chip:hover { + background: ${cssManager.bdTheme('#f4f4f5', '#18181b')}; + color: ${cssManager.bdTheme('#18181b', '#fafafa')}; + } + + .chip.active { + background: ${cssManager.bdTheme('#18181b', '#fafafa')}; + color: ${cssManager.bdTheme('#fafafa', '#18181b')}; + border-color: ${cssManager.bdTheme('#18181b', '#fafafa')}; + } + + .results-count { + font-size: 13px; + color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; + margin-bottom: 16px; + } + + .grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(420px, 1fr)); + gap: 16px; + } + + .grid sz-route-card { + cursor: pointer; + } + + .empty-state { + text-align: center; + padding: 48px 24px; + color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; + font-size: 14px; + } + + .empty-state-icon { + font-size: 32px; + margin-bottom: 12px; + opacity: 0.5; + } + `, + ]; + + public render(): TemplateResult { + const filtered = this.filteredRoutes; + + return html` +
+ { + this.searchQuery = (e.target as HTMLInputElement).value; + }} + /> +
+ ${(['all', 'forward', 'socket-handler'] as const).map( + (type) => html` + + ` + )} +
+
+ ${(['all', 'enabled', 'disabled'] as const).map( + (status) => html` + + ` + )} +
+
+ +
+ Showing ${filtered.length} of ${this.routes.length} routes +
+ + ${filtered.length > 0 + ? html` +
+ ${filtered.map( + (route) => html` + this.handleRouteClick(route)} + > + ` + )} +
+ ` + : html` +
+
🔍
+
No routes match your filters
+
+ `} + `; + } + + private handleRouteClick(route: IRouteConfig) { + this.dispatchEvent( + new CustomEvent('route-click', { + detail: route, + bubbles: true, + composed: true, + }) + ); + } +} diff --git a/ts_web/pages/sz-demo-app-shell.ts b/ts_web/pages/sz-demo-app-shell.ts index 53975b2..1905381 100644 --- a/ts_web/pages/sz-demo-app-shell.ts +++ b/ts_web/pages/sz-demo-app-shell.ts @@ -138,6 +138,12 @@ export class SzDemoAppShell extends DeesElement { iconName: 'lucide:Mail', content: 'sz-demo-view-mta', }, + { + id: 'routes', + name: 'Routes', + iconName: 'lucide:Route', + content: 'sz-demo-view-routes', + }, { id: 'settings', name: 'Settings', @@ -153,7 +159,7 @@ export class SzDemoAppShell extends DeesElement { }, { name: 'Infrastructure', - views: ['services', 'network', 'registries', 'mta'], + views: ['services', 'network', 'registries', 'mta', 'routes'], }, { name: 'Administration',