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