diff --git a/ui/core/components/detailed_results.tsx b/ui/core/components/detailed_results.tsx index c43435bfb7..3b711dd8b0 100644 --- a/ui/core/components/detailed_results.tsx +++ b/ui/core/components/detailed_results.tsx @@ -185,7 +185,7 @@ export abstract class DetailedResults extends Component { this.simUI?.sim.settingsChangeEmitter.on(async () => await this.updateSettings()); // Allow styling the sticky toolbar - const toolbar = document.querySelector('.dr-toolbar') as HTMLElement; + const toolbar = document.querySelector('.dr-toolbar')!; new IntersectionObserver( ([e]) => { e.target.classList.toggle('stuck', e.intersectionRatio < 1); diff --git a/ui/core/components/detailed_results/metrics_table/metrics_table.tsx b/ui/core/components/detailed_results/metrics_table/metrics_table.tsx index 83d4232c21..bb3b3921a5 100644 --- a/ui/core/components/detailed_results/metrics_table/metrics_table.tsx +++ b/ui/core/components/detailed_results/metrics_table/metrics_table.tsx @@ -7,8 +7,7 @@ import { ActionId } from '../../../proto_utils/action_id'; import { ActionMetrics, AuraMetrics, ResourceMetrics, UnitMetrics } from '../../../proto_utils/sim_result'; import { TypedEvent } from '../../../typed_event'; import { ResultComponent, ResultComponentConfig, SimResultData } from '../result_component'; - -declare let $: any; +import { TableSorter } from './table_sorter'; export enum ColumnSortType { None, @@ -36,7 +35,8 @@ export abstract class MetricsTable>; protected readonly tableElem: HTMLElement; - protected readonly bodyElem: HTMLElement; + protected readonly bodyElem: HTMLTableSectionElement; + private readonly sorter: TableSorter; readonly onUpdate = new TypedEvent('MetricsTableUpdate'); @@ -53,7 +53,7 @@ export abstract class MetricsTable, ); - this.tableElem = this.rootElem.querySelector('.metrics-table')!; + this.tableElem = this.rootElem.querySelector('.metrics-table')!; this.bodyElem = this.rootElem.querySelector('.metrics-table-body')!; const headerRowElem = this.rootElem.querySelector('.metrics-table-header-row')!; @@ -78,13 +78,15 @@ export abstract class MetricsTable [i, config.sort == ColumnSortType.Ascending ? 0 : 1]) - .filter(sortData => this.columnConfigs[sortData[0]].sort); + const sortCol = this.columnConfigs.findIndex(v => !!v.sort); - $(this.tableElem).tablesorter({ - sortList: sortList, - cssChildRow: 'child-metric', + 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, }); } @@ -173,7 +175,7 @@ export abstract class MetricsTable this.addGroup(group)); - $(this.tableElem).trigger('update'); + this.sorter.update(); this.onUpdate.emit(resultData.eventID); } diff --git a/ui/core/components/detailed_results/metrics_table/table_sorter.tsx b/ui/core/components/detailed_results/metrics_table/table_sorter.tsx new file mode 100644 index 0000000000..f050ea235b --- /dev/null +++ b/ui/core/components/detailed_results/metrics_table/table_sorter.tsx @@ -0,0 +1,104 @@ +type TableSorterRowData = { + readonly values: ReadonlyArray; + readonly rowElement: HTMLTableRowElement; +}; + +type TableSorterConfig = { + tableHead: HTMLTableRowElement; + tableBody: HTMLTableSectionElement; + dataSetKey: string; + childRowClass: string; + defaultSortCol: number; + defaultSortDesc: boolean; +}; + +export class TableSorter { + private readonly cfg: Readonly; + private readonly rowData: Array }> = []; + private sortCol = -1; + private sortDesc: Array; + + 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 = []; + + 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 { + const values: Array = []; + 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(); + } +} diff --git a/ui/index_template.html b/ui/index_template.html index 3454574577..66f2eca5a5 100644 --- a/ui/index_template.html +++ b/ui/index_template.html @@ -19,13 +19,6 @@ crossorigin="anonymous" referrerpolicy="no-referrer" /> - - -