import { LitElement, html, css, property, state, customElement } from './plugins.js'; import type { CSSResult, TemplateResult } from './plugins.js'; import { sharedStyles, tableStyles } from './sw-dash-styles.js'; export interface IColumnConfig { key: string; label: string; sortable?: boolean; formatter?: (value: any, row: any) => string; className?: string; } /** * Base sortable table component for sw-dash */ @customElement('sw-dash-table') export class SwDashTable extends LitElement { public static styles: CSSResult[] = [ sharedStyles, tableStyles, css` :host { display: block; } ` ]; @property({ type: Array }) accessor columns: IColumnConfig[] = []; @property({ type: Array }) accessor data: any[] = []; @property({ type: String }) accessor filterPlaceholder = 'Filter...'; @property({ type: String }) accessor infoLabel = 'items'; @state() accessor sortColumn = ''; @state() accessor sortDirection: 'asc' | 'desc' = 'desc'; @state() accessor filterText = ''; // Utility formatters static formatNumber(n: number): string { return n.toLocaleString(); } static formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } static formatTimestamp(ts: number): string { if (!ts || ts === 0) return 'never'; const ago = Date.now() - ts; if (ago < 60000) return Math.floor(ago / 1000) + 's ago'; if (ago < 3600000) return Math.floor(ago / 60000) + 'm ago'; if (ago < 86400000) return Math.floor(ago / 3600000) + 'h ago'; return new Date(ts).toLocaleDateString(); } static getGaugeClass(rate: number): string { if (rate >= 80) return 'good'; if (rate >= 50) return 'warning'; return 'bad'; } private handleSort(column: string): void { if (this.sortColumn === column) { this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; } else { this.sortColumn = column; this.sortDirection = 'desc'; } } private handleFilter(e: Event): void { this.filterText = (e.target as HTMLInputElement).value; } private getSortedFilteredData(): any[] { let result = [...this.data]; // Filter if (this.filterText) { const search = this.filterText.toLowerCase(); result = result.filter(row => { return this.columns.some(col => { const val = row[col.key]; if (val == null) return false; return String(val).toLowerCase().includes(search); }); }); } // Sort if (this.sortColumn) { result.sort((a, b) => { let valA = a[this.sortColumn]; let valB = b[this.sortColumn]; if (typeof valA === 'string') valA = valA.toLowerCase(); if (typeof valB === 'string') valB = valB.toLowerCase(); if (valA < valB) return this.sortDirection === 'asc' ? -1 : 1; if (valA > valB) return this.sortDirection === 'asc' ? 1 : -1; return 0; }); } return result; } private renderHitRateBar(rate: number): TemplateResult { const cls = SwDashTable.getGaugeClass(rate); return html` ${rate}% `; } protected renderCellValue(value: any, row: any, column: IColumnConfig): any { if (column.formatter) { return column.formatter(value, row); } // Special handling for hitRate if (column.key === 'hitRate') { return this.renderHitRateBar(value); } return value; } public render(): TemplateResult { const sortedData = this.getSortedFilteredData(); return html`
${sortedData.length} of ${this.data.length} ${this.infoLabel}
${this.columns.map(col => html` `)} ${sortedData.map(row => html` ${this.columns.map(col => html` `)} `)}
${col.label} ${col.sortable !== false ? html` ${this.sortColumn === col.key && this.sortDirection === 'asc' ? '^' : 'v'} ` : ''}
${this.renderCellValue(row[col.key], row, col)}
`; } }