Skip to content

Commit c590ab2

Browse files
committed
Fix: issue 459 / sort direct nodes
1 parent 09bb0bc commit c590ab2

File tree

2 files changed

+140
-1
lines changed

2 files changed

+140
-1
lines changed
+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { describe, it, expect } from "vitest";
2+
import { render, screen, fireEvent } from "@testing-library/react";
3+
import { Table } from "@components/generic/Table/index.tsx";
4+
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
5+
import { Mono } from "@components/generic/Mono.tsx";
6+
7+
8+
describe("Generic Table", () => {
9+
it("Can render an empty table.", () => {
10+
render(
11+
<Table
12+
headings={[]}
13+
rows={[]}
14+
/>
15+
);
16+
expect(screen.getByRole("table")).toBeInTheDocument();
17+
});
18+
19+
it("Can render a table with headers and no rows.", async () => {
20+
render(
21+
<Table
22+
headings={[
23+
{ title: "", type: "blank", sortable: false },
24+
{ title: "Short Name", type: "normal", sortable: true },
25+
{ title: "Long Name", type: "normal", sortable: true },
26+
{ title: "Model", type: "normal", sortable: true },
27+
{ title: "MAC Address", type: "normal", sortable: true },
28+
{ title: "Last Heard", type: "normal", sortable: true },
29+
{ title: "SNR", type: "normal", sortable: true },
30+
{ title: "Encryption", type: "normal", sortable: false },
31+
{ title: "Connection", type: "normal", sortable: true },
32+
]}
33+
rows={[]}
34+
/>
35+
);
36+
await screen.findByRole('table');
37+
expect(screen.getAllByRole("columnheader")).toHaveLength(9);
38+
});
39+
40+
// A simplified version of the rows in pages/Nodes.tsx for testing purposes
41+
const mockDevicesWithShortNameAndConnection = [
42+
{user: {shortName: "TST1"}, hopsAway: 1, lastHeard: Date.now() + 1000 },
43+
{user: {shortName: "TST2"}, hopsAway: 0, lastHeard: Date.now() + 4000 },
44+
{user: {shortName: "TST3"}, hopsAway: 4, lastHeard: Date.now() },
45+
{user: {shortName: "TST4"}, hopsAway: 3, lastHeard: Date.now() + 2000 }
46+
];
47+
48+
const mockRows = mockDevicesWithShortNameAndConnection.map(node => [
49+
<h1 data-testshortname> { node.user.shortName } </h1>,
50+
<><TimeAgo timestamp={node.lastHeard * 1000} /></>,
51+
<Mono key="hops" data-testhops>
52+
{node.lastHeard !== 0
53+
? node.hopsAway === 0
54+
? "Direct"
55+
: `${node.hopsAway?.toString()} ${
56+
node.hopsAway > 1 ? "hops" : "hop"
57+
} away`
58+
: "-"}
59+
</Mono>
60+
])
61+
62+
it("Can sort rows appropriately.", async () => {
63+
render(
64+
<Table
65+
headings={[
66+
{ title: "Short Name", type: "normal", sortable: true },
67+
{ title: "Last Heard", type: "normal", sortable: true },
68+
{ title: "Connection", type: "normal", sortable: true },
69+
]}
70+
rows={mockRows}
71+
/>
72+
);
73+
const renderedTable = await screen.findByRole('table');
74+
const columnHeaders = screen.getAllByRole("columnheader");
75+
expect(columnHeaders).toHaveLength(3);
76+
77+
// Will be sorted "Last heard" "asc" by default
78+
expect( [...renderedTable.querySelectorAll('[data-testshortname]')]
79+
.map(el=>el.textContent)
80+
.map(v=>v?.trim())
81+
.join(','))
82+
.toMatch('TST2,TST4,TST1,TST3');
83+
84+
fireEvent.click(columnHeaders[0]);
85+
86+
// Re-sort by Short Name asc
87+
expect( [...renderedTable.querySelectorAll('[data-testshortname]')]
88+
.map(el=>el.textContent)
89+
.map(v=>v?.trim())
90+
.join(','))
91+
.toMatch('TST1,TST2,TST3,TST4');
92+
93+
fireEvent.click(columnHeaders[0]);
94+
95+
// Re-sort by Short Name desc
96+
expect( [...renderedTable.querySelectorAll('[data-testshortname]')]
97+
.map(el=>el.textContent)
98+
.map(v=>v?.trim())
99+
.join(','))
100+
.toMatch('TST4,TST3,TST2,TST1');
101+
102+
fireEvent.click(columnHeaders[2]);
103+
104+
// Re-sort by Hops Away
105+
expect( [...renderedTable.querySelectorAll('[data-testshortname]')]
106+
.map(el=>el.textContent)
107+
.map(v=>v?.trim())
108+
.join(','))
109+
.toMatch('TST2,TST1,TST4,TST3');
110+
});
111+
})

src/components/generic/Table/index.tsx

+29-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ export interface Heading {
1212
sortable: boolean;
1313
}
1414

15+
/**
16+
* @param hopsAway String describing the number of hops away the node is from the current node
17+
* @returns number of hopsAway or `0` if hopsAway is 'Direct'
18+
*/
19+
function numericHops(hopsAway: string): number {
20+
if(hopsAway.match(/direct/i)){
21+
return 0;
22+
}
23+
if ( hopsAway.match(/\d+\s+hop/gi) ) {
24+
return Number( hopsAway.match(/(\d+)\s+hop/i)?.[1] );
25+
}
26+
return Number.MAX_SAFE_INTEGER;
27+
}
28+
1529
export const Table = ({ headings, rows }: TableProps) => {
1630
const [sortColumn, setSortColumn] = useState<string | null>("Last Heard");
1731
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
@@ -46,6 +60,20 @@ export const Table = ({ headings, rows }: TableProps) => {
4660
return 0;
4761
}
4862

63+
// Custom comparison for 'Connection' column
64+
if (sortColumn === "Connection") {
65+
const aNumHops = numericHops(aValue instanceof Array ? aValue[0] : aValue);
66+
const bNumHops = numericHops(bValue instanceof Array ? bValue[0] : bValue);
67+
68+
if (aNumHops < bNumHops) {
69+
return sortOrder === "asc" ? -1 : 1;
70+
}
71+
if (aNumHops > bNumHops) {
72+
return sortOrder === "asc" ? 1 : -1;
73+
}
74+
return 0;
75+
}
76+
4977
// Default comparison for other columns
5078
if (aValue < bValue) {
5179
return sortOrder === "asc" ? -1 : 1;
@@ -100,4 +128,4 @@ export const Table = ({ headings, rows }: TableProps) => {
100128
</tbody>
101129
</table>
102130
);
103-
};
131+
};

0 commit comments

Comments
 (0)