174 lines
5.1 KiB
TypeScript
174 lines
5.1 KiB
TypeScript
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`
|
|
<span class="hit-rate-bar">
|
|
<span class="hit-rate-fill ${cls}" style="width: ${rate}%"></span>
|
|
</span>${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`
|
|
<div class="table-controls">
|
|
<input
|
|
type="text"
|
|
class="search-input"
|
|
placeholder="${this.filterPlaceholder}"
|
|
.value="${this.filterText}"
|
|
@input="${this.handleFilter}"
|
|
>
|
|
<span class="table-info">${sortedData.length} of ${this.data.length} ${this.infoLabel}</span>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
${this.columns.map(col => html`
|
|
<th
|
|
class="${this.sortColumn === col.key ? 'sorted' : ''}"
|
|
@click="${() => col.sortable !== false && this.handleSort(col.key)}"
|
|
>
|
|
${col.label}
|
|
${col.sortable !== false ? html`
|
|
<span class="sort-icon">${this.sortColumn === col.key && this.sortDirection === 'asc' ? '^' : 'v'}</span>
|
|
` : ''}
|
|
</th>
|
|
`)}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${sortedData.map(row => html`
|
|
<tr>
|
|
${this.columns.map(col => html`
|
|
<td class="${col.className || ''}">${this.renderCellValue(row[col.key], row, col)}</td>
|
|
`)}
|
|
</tr>
|
|
`)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|