|
|
@ -1,9 +1,10 @@ |
|
|
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; |
|
|
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; |
|
|
import { useState } from "react"; |
|
|
import { useState } from "react"; |
|
|
|
|
|
import React from "react"; |
|
|
|
|
|
|
|
|
export interface TableProps { |
|
|
export interface TableProps { |
|
|
headings: Heading[]; |
|
|
headings: Heading[]; |
|
|
rows: [][]; |
|
|
rows: React.ReactNode[][]; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export interface Heading { |
|
|
export interface Heading { |
|
|
@ -12,18 +13,15 @@ export interface Heading { |
|
|
sortable: boolean; |
|
|
sortable: boolean; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
function numericHops(hopsAway: string | unknown): number { |
|
|
* @param hopsAway String describing the number of hops away the node is from the current node |
|
|
if (typeof hopsAway !== "string") { |
|
|
* @returns number of hopsAway or `0` if hopsAway is 'Direct' |
|
|
return Number.MAX_SAFE_INTEGER; |
|
|
*/ |
|
|
} |
|
|
function numericHops(hopsAway: string): number { |
|
|
|
|
|
if (hopsAway.match(/direct/i)) { |
|
|
if (hopsAway.match(/direct/i)) { |
|
|
return 0; |
|
|
return 0; |
|
|
} |
|
|
} |
|
|
if (hopsAway.match(/\d+\s+hop/gi)) { |
|
|
const match = hopsAway.match(/(\d+)\s+hop/i); |
|
|
return Number(hopsAway.match(/(\d+)\s+hop/i)?.[1]); |
|
|
return Number(match?.[1] ?? Number.MAX_SAFE_INTEGER); |
|
|
} |
|
|
|
|
|
return Number.MAX_SAFE_INTEGER; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export const Table = ({ headings, rows }: TableProps) => { |
|
|
export const Table = ({ headings, rows }: TableProps) => { |
|
|
@ -39,49 +37,63 @@ export const Table = ({ headings, rows }: TableProps) => { |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const getElement = (cell: React.ReactNode): React.ReactElement | null => { |
|
|
|
|
|
if (!React.isValidElement(cell)) { |
|
|
|
|
|
return null; |
|
|
|
|
|
} |
|
|
|
|
|
if (cell.type === React.Fragment) { |
|
|
|
|
|
const childrenArray = React.Children.toArray(cell.props.children); |
|
|
|
|
|
const firstElement = childrenArray.find((child) => |
|
|
|
|
|
React.isValidElement(child) |
|
|
|
|
|
); |
|
|
|
|
|
return (firstElement as React.ReactElement) ?? null; |
|
|
|
|
|
} |
|
|
|
|
|
// If not a fragment, return the element itself
|
|
|
|
|
|
return cell; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
const sortedRows = rows.slice().sort((a, b) => { |
|
|
const sortedRows = rows.slice().sort((a, b) => { |
|
|
if (!sortColumn) return 0; |
|
|
if (!sortColumn) return 0; |
|
|
|
|
|
|
|
|
const columnIndex = headings.findIndex((h) => h.title === sortColumn); |
|
|
const columnIndex = headings.findIndex((h) => h.title === sortColumn); |
|
|
const aValue = a[columnIndex].props.children; |
|
|
if (columnIndex === -1) return 0; |
|
|
const bValue = b[columnIndex].props.children; |
|
|
|
|
|
|
|
|
const elementA = getElement(a[columnIndex]); |
|
|
|
|
|
const elementB = getElement(b[columnIndex]); |
|
|
|
|
|
|
|
|
if (sortColumn === "Last Heard") { |
|
|
if (sortColumn === "Last Heard") { |
|
|
const aTimestamp = aValue.props.timestamp ?? 0; |
|
|
const aTimestamp = elementA?.props?.timestamp ?? 0; |
|
|
const bTimestamp = bValue.props.timestamp ?? 0; |
|
|
const bTimestamp = elementB?.props?.timestamp ?? 0; |
|
|
|
|
|
if (aTimestamp < bTimestamp) return sortOrder === "asc" ? -1 : 1; |
|
|
if (aTimestamp < bTimestamp) { |
|
|
if (aTimestamp > bTimestamp) return sortOrder === "asc" ? 1 : -1; |
|
|
return sortOrder === "asc" ? -1 : 1; |
|
|
|
|
|
} |
|
|
|
|
|
if (aTimestamp > bTimestamp) { |
|
|
|
|
|
return sortOrder === "asc" ? 1 : -1; |
|
|
|
|
|
} |
|
|
|
|
|
return 0; |
|
|
return 0; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (sortColumn === "Connection") { |
|
|
if (sortColumn === "Connection") { |
|
|
const aNumHops = numericHops( |
|
|
const aHopsStr = elementA?.props?.children; |
|
|
aValue instanceof Array ? aValue[0] : aValue, |
|
|
const bHopsStr = elementB?.props?.children; |
|
|
); |
|
|
const aNumHops = numericHops(aHopsStr); |
|
|
const bNumHops = numericHops( |
|
|
const bNumHops = numericHops(bHopsStr); |
|
|
bValue instanceof Array ? bValue[0] : bValue, |
|
|
if (aNumHops < bNumHops) return sortOrder === "asc" ? -1 : 1; |
|
|
); |
|
|
if (aNumHops > bNumHops) return sortOrder === "asc" ? 1 : -1; |
|
|
|
|
|
|
|
|
if (aNumHops < bNumHops) { |
|
|
|
|
|
return sortOrder === "asc" ? -1 : 1; |
|
|
|
|
|
} |
|
|
|
|
|
if (aNumHops > bNumHops) { |
|
|
|
|
|
return sortOrder === "asc" ? 1 : -1; |
|
|
|
|
|
} |
|
|
|
|
|
return 0; |
|
|
return 0; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (aValue < bValue) { |
|
|
const aValue = elementA?.props?.children; |
|
|
return sortOrder === "asc" ? -1 : 1; |
|
|
const bValue = elementB?.props?.children; |
|
|
} |
|
|
const valA = aValue ?? ""; |
|
|
if (aValue > bValue) { |
|
|
const valB = bValue ?? ""; |
|
|
return sortOrder === "asc" ? 1 : -1; |
|
|
|
|
|
} |
|
|
// Ensure consistent comparison for potentially different types
|
|
|
|
|
|
const compareA = typeof valA === "string" || typeof valA === "number" |
|
|
|
|
|
? valA |
|
|
|
|
|
: String(valA); |
|
|
|
|
|
const compareB = typeof valB === "string" || typeof valB === "number" |
|
|
|
|
|
? valB |
|
|
|
|
|
: String(valB); |
|
|
|
|
|
|
|
|
|
|
|
if (compareA < compareB) return sortOrder === "asc" ? -1 : 1; |
|
|
|
|
|
if (compareA > compareB) return sortOrder === "asc" ? 1 : -1; |
|
|
return 0; |
|
|
return 0; |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
@ -99,37 +111,52 @@ export const Table = ({ headings, rows }: TableProps) => { |
|
|
: "" |
|
|
: "" |
|
|
}`}
|
|
|
}`}
|
|
|
onClick={() => heading.sortable && headingSort(heading.title)} |
|
|
onClick={() => heading.sortable && headingSort(heading.title)} |
|
|
onKeyUp={() => heading.sortable && headingSort(heading.title)} |
|
|
onKeyUp={(e) => { |
|
|
|
|
|
if (heading.sortable && (e.key === "Enter" || e.key === " ")) { |
|
|
|
|
|
headingSort(heading.title); |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
tabIndex={heading.sortable ? 0 : -1} |
|
|
|
|
|
role="columnheader" |
|
|
|
|
|
aria-sort={sortColumn === heading.title |
|
|
|
|
|
? sortOrder === "asc" ? "ascending" : "descending" |
|
|
|
|
|
: "none"} |
|
|
> |
|
|
> |
|
|
<div className="flex gap-2"> |
|
|
<div className="flex items-center gap-2"> |
|
|
{heading.title} |
|
|
{heading.title} |
|
|
{sortColumn === heading.title && |
|
|
{heading.sortable && sortColumn === heading.title && ( |
|
|
(sortOrder === "asc" |
|
|
sortOrder === "asc" |
|
|
? <ChevronUpIcon size={16} /> |
|
|
? <ChevronUpIcon size={16} aria-hidden="true" /> |
|
|
: <ChevronDownIcon size={16} />)} |
|
|
: <ChevronDownIcon size={16} aria-hidden="true" /> |
|
|
|
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
</th> |
|
|
</th> |
|
|
))} |
|
|
))} |
|
|
</tr> |
|
|
</tr> |
|
|
</thead> |
|
|
</thead> |
|
|
<tbody className="max-w-fit"> |
|
|
<tbody className="max-w-fit"> |
|
|
{sortedRows.map((row, index) => { |
|
|
{sortedRows.map((row) => { |
|
|
// biome-ignore lint/suspicious/noArrayIndexKey: TODO: Once this table is sortable, this should get fixed.
|
|
|
const firstCellKey = |
|
|
|
|
|
(React.isValidElement(row[0]) && row[0].key !== null) |
|
|
|
|
|
? String(row[0].key) |
|
|
|
|
|
: null; |
|
|
|
|
|
const rowKey = firstCellKey ?? Math.random().toString(); // Use random only as last resort
|
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<tr |
|
|
<tr |
|
|
key={index} |
|
|
key={rowKey} |
|
|
className={`${ |
|
|
className={` |
|
|
index % 2 |
|
|
bg-white dark:bg-white/10 |
|
|
? "bg-white dark:bg-white/2" |
|
|
odd:bg-slate-800/70 dark:even:bg-slate-900/70 |
|
|
: "bg-slate-50/50 dark:bg-slate-50/5" |
|
|
`}
|
|
|
} border-b-1 border-slate-200 dark:border-slate-900`}
|
|
|
|
|
|
> |
|
|
> |
|
|
{row.map((item, index) => { |
|
|
{row.map((item, cellIndex) => { |
|
|
return (index === 0 |
|
|
const cellKey = `${rowKey}_${cellIndex}`; |
|
|
|
|
|
return cellIndex === 0 |
|
|
? ( |
|
|
? ( |
|
|
<th |
|
|
<th |
|
|
key={item.key ?? index} |
|
|
key={cellKey} |
|
|
className="whitespace-nowrap py-2 text-sm text-text-secondary first:pl-2" |
|
|
className="whitespace-nowrap px-3 py-2 text-sm text-left text-text-secondary" |
|
|
scope="row" |
|
|
scope="row" |
|
|
> |
|
|
> |
|
|
{item} |
|
|
{item} |
|
|
@ -137,12 +164,12 @@ export const Table = ({ headings, rows }: TableProps) => { |
|
|
) |
|
|
) |
|
|
: ( |
|
|
: ( |
|
|
<td |
|
|
<td |
|
|
key={item.key ?? index} |
|
|
key={cellKey} |
|
|
className="whitespace-nowrap py-2 text-sm text-text-secondary first:pl-2" |
|
|
className="whitespace-nowrap px-3 py-2 text-sm text-text-secondary" |
|
|
> |
|
|
> |
|
|
{item} |
|
|
{item} |
|
|
</td> |
|
|
</td> |
|
|
)); |
|
|
); |
|
|
})} |
|
|
})} |
|
|
</tr> |
|
|
</tr> |
|
|
); |
|
|
); |
|
|
|