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, }) ); } }