Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
28 changes: 15 additions & 13 deletions ui/core/components/detailed_results/metrics_table/metrics_table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,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,
Expand All @@ -33,7 +32,8 @@ export abstract class MetricsTable<T extends ActionMetrics | AuraMetrics | UnitM
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 ActionMetrics | AuraMetrics | UnitM
</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('.metrics-table') as HTMLTableElement;
Copy link
Contributor

@1337LutZ 1337LutZ Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can pass the HTMLTableElement as a generic: .querySelector<HTMLTableElement>()! and then use ! to tell TS you are sure this is never undefined. Cleaner than typecasting (i tried to fix this in a lot of legacy components)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol used typecasting because it's done everywhere else like that. Should I just regex replace all of them then? :D

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to get rid of it where I did major refactors 😅😂 feel free to run a find replace regex tbh 👌🫠

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

this.bodyElem = this.rootElem.querySelector('.metrics-table-body') as HTMLTableSectionElement;

const headerRowElem = this.rootElem.getElementsByClassName('metrics-table-header-row')[0] as HTMLElement;
const headerRowElem = this.rootElem.querySelector('.metrics-table-header-row') as HTMLTableRowElement;
this.columnConfigs.forEach(columnConfig => {
const headerCell = document.createElement('th');
const tooltip = columnConfig.tooltip || TOOLTIP_METRIC_LABELS[columnConfig.name as keyof typeof TOOLTIP_METRIC_LABELS];
Expand All @@ -74,13 +74,15 @@ export abstract class MetricsTable<T extends ActionMetrics | AuraMetrics | UnitM
headerRowElem.appendChild(headerCell);
});

const sortList = this.columnConfigs
.map((config, i) => [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,
});
}

Expand Down Expand Up @@ -168,7 +170,7 @@ export abstract class MetricsTable<T extends ActionMetrics | AuraMetrics | UnitM
this.rootElem.classList.remove('hide');
}
groupedMetrics.forEach(group => this.addGroup(group));
$(this.tableElem).trigger('update');
this.sorter.update();
this.onUpdate.emit(resultData.eventID);
}

Expand Down
104 changes: 104 additions & 0 deletions ui/core/components/detailed_results/metrics_table/table_sorter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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();
}
}
7 changes: 0 additions & 7 deletions ui/index_template.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +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>

<!-- Load the top-level ui/index.ts -->
<script src="../index.ts" type="module"></script>
<script src="./index.ts" type="module"></script>
Expand Down
Loading