Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 20 additions & 14 deletions ui/core/components/detailed_results/metrics_table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import { EventID, TypedEvent } from '../../typed_event.js';
import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js';
import tippy from 'tippy.js'
import { element, fragment, ref } from 'tsx-vanilla'

declare var $: any;
import { TableSorter } from './table_sorter.js';

export enum ColumnSortType {
None,
Expand All @@ -32,7 +31,8 @@ export abstract class MetricsTable<T> extends ResultComponent {
private readonly columnConfigs: Array<MetricsColumnConfig<T>>;

protected readonly tableElem: HTMLElement;
protected readonly bodyElem: HTMLElement;
protected readonly bodyElem: HTMLTableSectionElement;
private readonly sorter: TableSorter;

readonly onUpdate = new TypedEvent<void>('MetricsTableUpdate');

Expand All @@ -50,10 +50,10 @@ export abstract class MetricsTable<T> extends ResultComponent {
</table>
);

this.tableElem = this.rootElem.getElementsByClassName('metrics-table')[0] as HTMLTableSectionElement;
this.bodyElem = this.rootElem.getElementsByClassName('metrics-table-body')[0] as HTMLElement;
this.tableElem = this.rootElem.querySelector<HTMLTableElement>('.metrics-table')!;
this.bodyElem = this.rootElem.querySelector<HTMLTableSectionElement>('.metrics-table-body')!;

const headerRowElem = this.rootElem.getElementsByClassName('metrics-table-header-row')[0] as HTMLElement;
const headerRowElem = this.rootElem.querySelector<HTMLTableRowElement>('.metrics-table-header-row')!;
this.columnConfigs.forEach(columnConfig => {
const headerCell = document.createElement('th');
headerCell.classList.add('metrics-table-header-cell');
Expand All @@ -73,12 +73,15 @@ export abstract class MetricsTable<T> extends ResultComponent {
headerRowElem.appendChild(headerCell);
});

const sortList = this.columnConfigs
.map((config, i) => [i, config.sort == ColumnSortType.Ascending ? 0 : 1])
.filter(sortData => this.columnConfigs[sortData[0]].sort);
$(this.tableElem).tablesorter({
sortList: sortList,
cssChildRow: 'child-metric',
const sortCol = this.columnConfigs.findIndex(v => !!v.sort);

this.sorter = new TableSorter({
tableHead: headerRowElem,
tableBody: this.bodyElem,
dataSetKey: 'text',
childRowClass: 'child-metric',
defaultSortCol: sortCol !== -1 ? sortCol : 0,
defaultSortDesc: sortCol !== -1 && this.columnConfigs[sortCol].sort == ColumnSortType.Descending,
});
}

Expand All @@ -101,6 +104,9 @@ export abstract class MetricsTable<T> extends ResultComponent {

this.columnConfigs.forEach(columnConfig => {
const cellElem = document.createElement('td');
if (columnConfig.getValue) {
cellElem.dataset.text = String(columnConfig.getValue(metric));
}
if (columnConfig.columnClass) {
cellElem.classList.add(columnConfig.columnClass);
}
Expand Down Expand Up @@ -128,7 +134,7 @@ export abstract class MetricsTable<T> extends ResultComponent {
return;
}

// Manually sort because tablesorter doesn't let us apply sorting to child rows.
// Manually sort because the sorter doesn't apply sorting to child rows.
this.sortMetrics(metrics);

const mergedMetrics = this.mergeMetrics(metrics);
Expand Down Expand Up @@ -162,7 +168,7 @@ export abstract class MetricsTable<T> extends ResultComponent {
}

groupedMetrics.forEach(group => this.addGroup(group));
$(this.tableElem).trigger('update');
this.sorter.update();
this.onUpdate.emit(resultData.eventID);
}

Expand Down
107 changes: 107 additions & 0 deletions ui/core/components/detailed_results/table_sorter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
type TableSorterRowData = {
readonly values: ReadonlyArray<string | number>;
readonly rowElement: HTMLTableRowElement;
};

type TableSorterConfig = {
tableHead: HTMLTableRowElement;
tableBody: HTMLTableSectionElement;
dataSetKey: string;
childRowClass: string;
defaultSortCol: number;
defaultSortDesc: boolean;
};

export class TableSorter {
private readonly cfg: Readonly<TableSorterConfig>;
private readonly rowData: Array<TableSorterRowData & { children?: Array<TableSorterRowData> }> = [];
private sortCol = -1;
private sortDesc: Array<boolean>;

constructor(config: TableSorterConfig) {
if (config.tableHead.cells[config.defaultSortCol] === undefined)
throw new Error('Default sort column must be a valid header cell index!');

this.cfg = config;

this.sortCol = this.cfg.defaultSortCol;
this.sortDesc = Array(config.tableHead.cells.length).fill(true);
this.sortDesc[config.defaultSortCol] = config.defaultSortDesc;

Array.from(config.tableHead.cells).forEach((cell, i) => {
cell.addEventListener('click', () => this.setSort(i));
});
}

private sortFunc = (a: TableSorterRowData, b: TableSorterRowData) => {
const aValue = a.values[this.sortCol];
const bValue = b.values[this.sortCol];
const asc = !this.sortDesc[this.sortCol];
if (typeof aValue === 'number' && typeof bValue === 'number') {
return asc ? aValue - bValue : bValue - aValue;
} else {
return asc
? aValue.toString().localeCompare(bValue.toString())
: bValue.toString().localeCompare(aValue.toString());
}
};

private sort() {
if (!this.rowData.length || !(this.sortCol in this.rowData[0].values)) return;

const sortedRowElems: Array<HTMLTableRowElement> = [];

this.rowData.sort(this.sortFunc);
for (const row of this.rowData) {
sortedRowElems.push(row.rowElement);
if (row.children) {
row.children.sort(this.sortFunc);
sortedRowElems.push(...row.children.map(v => v.rowElement));
}
}

this.cfg.tableBody.replaceChildren(...sortedRowElems);
}

/**
* Set column to sort by. If set to the current sort column the order will be reversed.
* @param column If omitted use default column.
*/
setSort(column = -1) {
if (this.sortDesc[column] === undefined) column = this.cfg.defaultSortCol;
this.sortDesc[column] = !this.sortDesc[column];
this.sortCol = column;
this.sort();
}

private parseRowValues(rowElement: HTMLTableRowElement): Array<number | string> {
const values: Array<string | number> = [];
for (const cell of rowElement.cells) {
const val = cell.dataset[this.cfg.dataSetKey] ?? cell.innerText;
const numVal = parseFloat(val);
values.push(!isNaN(numVal) ? numVal : val);
}
return values;
}

/**
* Update internal data structure for changed table data.
*/
update() {
this.rowData.length = 0;

for (const rowElement of this.cfg.tableBody.rows) {
const values = this.parseRowValues(rowElement);
if (!rowElement.classList.contains(this.cfg.childRowClass)) {
this.rowData.push({ values, rowElement });
} else {
const parentData = this.rowData[this.rowData.length - 1];
if (!parentData) throw new Error('Child row has no parent!');
if (!parentData.children) parentData.children = [];
parentData.children.push({ values, rowElement });
}
}

this.sort();
}
}
6 changes: 0 additions & 6 deletions ui/index_template.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@
crossorigin="anonymous"
referrerpolicy="no-referrer" />

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.31.3/js/jquery.tablesorter.min.js"
integrity="sha512-qzgd5cYSZcosqpzpn7zF2ZId8f/8CHmFKZ8j7mU4OUXTNRd5g+ZHBPsgKEwoqxCtdQvExE5LprwwPAgoicguNg=="
crossorigin="anonymous"
referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<script src="../index.ts" type="module"></script>
<script src="./index.ts" type="module"></script>
Expand Down