4 changed files with 105 additions and 107 deletions
@ -0,0 +1,53 @@ |
|||||
|
import { ChevronUpIcon } from "@heroicons/react/24/outline"; |
||||
|
|
||||
|
export interface TableProps { |
||||
|
headings: Heading[]; |
||||
|
rows: JSX.Element[][]; |
||||
|
} |
||||
|
|
||||
|
export interface Heading { |
||||
|
title: string; |
||||
|
type: "blank" | "normal"; |
||||
|
sortable: boolean; |
||||
|
} |
||||
|
|
||||
|
export const Table = ({ headings, rows }: TableProps): JSX.Element => { |
||||
|
return ( |
||||
|
<table className="min-w-full"> |
||||
|
<thead className="bg-backgroundPrimary text-sm font-semibold text-textPrimary"> |
||||
|
<tr> |
||||
|
{headings.map((heading, index) => ( |
||||
|
<th |
||||
|
key={index} |
||||
|
scope="col" |
||||
|
className={`py-2 pr-3 pl-6 text-left ${ |
||||
|
heading.sortable |
||||
|
? "cursor-pointer hover:brightness-hover active:brightness-press" |
||||
|
: "" |
||||
|
}`}
|
||||
|
> |
||||
|
<div className="flex gap-2"> |
||||
|
{heading.title} |
||||
|
{heading.sortable && <ChevronUpIcon className="my-auto h-3" />} |
||||
|
</div> |
||||
|
</th> |
||||
|
))} |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
{rows.map((row, index) => ( |
||||
|
<tr key={index}> |
||||
|
{row.map((item, index) => ( |
||||
|
<td |
||||
|
key={index} |
||||
|
className="whitespace-nowrap py-2 text-sm text-textSecondary first:pl-2" |
||||
|
> |
||||
|
{item} |
||||
|
</td> |
||||
|
))} |
||||
|
</tr> |
||||
|
))} |
||||
|
</tbody> |
||||
|
</table> |
||||
|
); |
||||
|
}; |
||||
@ -0,0 +1,9 @@ |
|||||
|
import TimeAgoReact from "timeago-react"; |
||||
|
|
||||
|
export interface TimeAgoProps { |
||||
|
timestamp: number; |
||||
|
} |
||||
|
|
||||
|
export const TimeAgo = ({ timestamp }: TimeAgoProps): JSX.Element => { |
||||
|
return <TimeAgoReact datetime={timestamp} opts={{ minInterval: 10 }} />; |
||||
|
}; |
||||
@ -1,122 +1,53 @@ |
|||||
import type React from "react"; |
import type React from "react"; |
||||
|
|
||||
import toast from "react-hot-toast"; |
|
||||
import { base16 } from "rfc4648"; |
import { base16 } from "rfc4648"; |
||||
|
|
||||
import { IconButton } from "@app/components/form/IconButton.js"; |
import { Mono } from "@components/generic/Mono.js"; |
||||
import { Mono } from "@app/components/generic/Mono.js"; |
import { Table } from "@components/generic/Table"; |
||||
|
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.js"; |
||||
import { useDevice } from "@core/providers/useDevice.js"; |
import { useDevice } from "@core/providers/useDevice.js"; |
||||
import { Hashicon } from "@emeraldpay/hashicon-react"; |
import { Hashicon } from "@emeraldpay/hashicon-react"; |
||||
import { |
|
||||
ArrowPathRoundedSquareIcon, |
|
||||
EllipsisHorizontalIcon |
|
||||
} from "@heroicons/react/24/outline"; |
|
||||
import { Protobuf } from "@meshtastic/meshtasticjs"; |
import { Protobuf } from "@meshtastic/meshtasticjs"; |
||||
import TimeAgo from "timeago-react"; |
|
||||
|
|
||||
export const PeersPage = (): JSX.Element => { |
export const PeersPage = (): JSX.Element => { |
||||
const { connection, nodes } = useDevice(); |
const { connection, nodes } = useDevice(); |
||||
|
|
||||
return ( |
return ( |
||||
<div className="w-full overflow-y-auto"> |
<div className="w-full overflow-y-auto"> |
||||
<table className="min-w-full"> |
<Table |
||||
<thead className="bg-backgroundPrimary text-sm font-semibold text-textPrimary"> |
headings={[ |
||||
<tr> |
{ title: "", type: "blank", sortable: false }, |
||||
<th scope="col" className="py-2 pr-3 pl-6 text-left"> |
{ title: "Name", type: "normal", sortable: true }, |
||||
Name |
{ title: "Model", type: "normal", sortable: true }, |
||||
</th> |
{ title: "MAC Address", type: "normal", sortable: true }, |
||||
<th scope="col" className="py-2 text-left"> |
{ title: "Last Heard", type: "normal", sortable: true }, |
||||
Model |
{ title: "SNR", type: "normal", sortable: true } |
||||
</th> |
]} |
||||
<th scope="col" className="py-2 text-left"> |
rows={nodes.map((node) => [ |
||||
MAC Address |
<Hashicon size={24} value={node.data.num.toString()} />, |
||||
</th> |
<h1> |
||||
<th scope="col" className="py-2 text-left"> |
{node.data.user?.longName ?? node.data.user?.macaddr |
||||
Versions |
? `Meshtastic_${base16 |
||||
</th> |
.stringify(node.data.user?.macaddr.subarray(4, 6) ?? []) |
||||
<th scope="col" className="py-2 text-left"> |
.toLowerCase()}` |
||||
Last Heard |
: `UNK: ${node.data.num}`} |
||||
</th> |
</h1>, |
||||
<th scope="col" className="py-2 text-left"> |
|
||||
SNR |
<Mono>{Protobuf.HardwareModel[node.data.user?.hwModel ?? 0]}</Mono>, |
||||
</th> |
<Mono> |
||||
<th scope="col" className="relative py-2 pl-3 pr-4 sm:pr-6"> |
{base16 |
||||
<span className="sr-only">Edit</span> |
.stringify(node.data.user?.macaddr ?? []) |
||||
</th> |
.match(/.{1,2}/g) |
||||
</tr> |
?.join(":") ?? "UNK"} |
||||
</thead> |
</Mono>, |
||||
<tbody> |
<TimeAgo timestamp={node.data.lastHeard * 1000} />, |
||||
{nodes.map((node, index) => ( |
<Mono> |
||||
<tr key={index}> |
{node.data.snr}db/ |
||||
<td className="flex gap-2 whitespace-nowrap py-2 pr-3 pl-6 text-sm font-medium text-textPrimary"> |
{Math.min(Math.max((node.data.snr + 10) * 5, 0), 100)}%/ |
||||
<Hashicon size={24} value={node.data.num.toString()} /> |
{(node.data.snr + 10) * 5}raw |
||||
<span className="my-auto"> |
</Mono> |
||||
{node.data.user?.longName ?? |
])} |
||||
`Meshtastic_${base16 |
/> |
||||
.stringify(node.data.user?.macaddr.subarray(4, 6) ?? []) |
|
||||
.toLowerCase()}`}
|
|
||||
</span> |
|
||||
</td> |
|
||||
<td className="whitespace-nowrap py-2 text-sm text-textSecondary"> |
|
||||
<span className="bg-slate-200 rounded-md p-1"> |
|
||||
<Mono> |
|
||||
{Protobuf.HardwareModel[node.data.user?.hwModel ?? 0]} |
|
||||
</Mono> |
|
||||
</span> |
|
||||
</td> |
|
||||
<td className="whitespace-nowrap py-2 text-sm text-textSecondary"> |
|
||||
<Mono> |
|
||||
{base16 |
|
||||
.stringify(node.data.user?.macaddr ?? []) |
|
||||
.match(/.{1,2}/g) |
|
||||
?.join(":") ?? ""} |
|
||||
</Mono> |
|
||||
</td> |
|
||||
<td className="whitespace-nowrap py-2 text-sm text-textSecondary"> |
|
||||
{node.metadata ? ( |
|
||||
<> |
|
||||
<Mono>{node.metadata.firmwareVersion}</Mono> |
|
||||
<span className="text-black">/</span> |
|
||||
<Mono>{node.metadata.deviceStateVersion}</Mono> |
|
||||
</> |
|
||||
) : ( |
|
||||
<IconButton |
|
||||
size="sm" |
|
||||
onClick={() => { |
|
||||
if (connection) { |
|
||||
void toast.promise( |
|
||||
connection.getMetadata({ nodeNum: node.data.num }), |
|
||||
{ |
|
||||
loading: "Requesting Metadata...", |
|
||||
success: "Recieved Metadata", |
|
||||
error: "No response received" |
|
||||
} |
|
||||
); |
|
||||
} |
|
||||
}} |
|
||||
icon={<ArrowPathRoundedSquareIcon className="h-4" />} |
|
||||
/> |
|
||||
)} |
|
||||
</td> |
|
||||
<td className="whitespace-nowrap py-2 text-sm text-textSecondary"> |
|
||||
<TimeAgo |
|
||||
datetime={node.data.lastHeard * 1000} |
|
||||
opts={{ minInterval: 10 }} |
|
||||
/> |
|
||||
</td> |
|
||||
<td className="whitespace-nowrap py-2 text-sm text-textSecondary"> |
|
||||
<Mono>{node.data.snr}db</Mono> |
|
||||
</td> |
|
||||
<td className="relative whitespace-nowrap pl-3 pr-4 text-right text-sm font-medium"> |
|
||||
<IconButton |
|
||||
size="sm" |
|
||||
icon={<EllipsisHorizontalIcon className="h-4" />} |
|
||||
/> |
|
||||
</td> |
|
||||
</tr> |
|
||||
))} |
|
||||
</tbody> |
|
||||
</table> |
|
||||
</div> |
</div> |
||||
); |
); |
||||
}; |
}; |
||||
|
|||||
Loading…
Reference in new issue