6 changed files with 222 additions and 3 deletions
@ -0,0 +1,191 @@ |
|||
import { useAppStore } from "@app/core/stores/appStore"; |
|||
import { useDevice } from "@app/core/stores/deviceStore"; |
|||
import { |
|||
Dialog, |
|||
DialogContent, |
|||
DialogFooter, |
|||
DialogHeader, |
|||
DialogTitle, |
|||
} from "@components/UI/Dialog.tsx"; |
|||
import { Protobuf } from "@meshtastic/js"; |
|||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; |
|||
import { useEffect } from "react"; |
|||
import { useState } from "react"; |
|||
import { DeviceImage } from "../generic/DeviceImage"; |
|||
import { TimeAgo } from "../generic/TimeAgo"; |
|||
import { Uptime } from "../generic/Uptime"; |
|||
|
|||
export interface NodeDetailsDialogProps { |
|||
open: boolean; |
|||
onOpenChange: (open: boolean) => void; |
|||
} |
|||
|
|||
export const NodeDetailsDialog = ({ |
|||
open, |
|||
onOpenChange, |
|||
}: NodeDetailsDialogProps) => { |
|||
const { nodes } = useDevice(); |
|||
const { nodeNumDetails } = useAppStore(); |
|||
const [device, setDevice] = useState<Protobuf.Mesh.NodeInfo | null>(null); |
|||
|
|||
useEffect(() => { |
|||
if (!nodeNumDetails) return; |
|||
setDevice(nodes.get(nodeNumDetails)); |
|||
}, [nodeNumDetails, nodes]); |
|||
|
|||
return device ? ( |
|||
<Dialog open={open} onOpenChange={onOpenChange}> |
|||
<DialogContent> |
|||
<DialogHeader> |
|||
<DialogTitle> |
|||
Node Details for {device.user?.longName ?? "UNKNOWN"} ( |
|||
{device.user?.shortName ?? "UNK"}) |
|||
</DialogTitle> |
|||
</DialogHeader> |
|||
<DialogFooter> |
|||
<div className="w-full"> |
|||
<DeviceImage |
|||
className="w-32 h-32 mx-auto rounded-lg border-4 border-gray-200 dark:border-gray-800" |
|||
deviceType={ |
|||
Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0] |
|||
} |
|||
/> |
|||
<div className="mt-5 bg-gray-100 dark:bg-slate-800 p-3 rounded-lg mt-3"> |
|||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50"> |
|||
Details: |
|||
</p> |
|||
<p> |
|||
Hardware:{" "} |
|||
{Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]} |
|||
</p> |
|||
<p>Node Number: {device.num}</p> |
|||
<p>Node HEX: !{numberToHexUnpadded(device.num)}</p> |
|||
<p> |
|||
Role:{" "} |
|||
{ |
|||
Protobuf.Config.Config_DeviceConfig_Role[ |
|||
device.user?.role ?? 0 |
|||
] |
|||
} |
|||
</p> |
|||
<p> |
|||
Last Heard:{" "} |
|||
{device.lastHeard === 0 ? ( |
|||
"Never" |
|||
) : ( |
|||
<TimeAgo timestamp={device.lastHeard * 1000} /> |
|||
)} |
|||
</p> |
|||
</div> |
|||
|
|||
{device.position ? ( |
|||
<div className="mt-5 bg-gray-100 dark:bg-slate-800 p-3 rounded-lg mt-3"> |
|||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50"> |
|||
Position: |
|||
</p> |
|||
{device.position.latitudeI && device.position.longitudeI ? ( |
|||
<p> |
|||
Coordinates:{" "} |
|||
<a |
|||
className="text-blue-500 dark:text-blue-400" |
|||
href={`https://www.openstreetmap.org/?mlat=${device.position.latitudeI / 1e7}&mlon=${device.position.longitudeI / 1e7}&layers=N`} |
|||
target="_blank" |
|||
rel="noreferrer" |
|||
> |
|||
{device.position.latitudeI / 1e7},{" "} |
|||
{device.position.longitudeI / 1e7} |
|||
</a> |
|||
</p> |
|||
) : null} |
|||
{device.position.altitude ? ( |
|||
<p>Altitude: {device.position.altitude}m</p> |
|||
) : null} |
|||
</div> |
|||
) : null} |
|||
|
|||
{device.deviceMetrics ? ( |
|||
<div className="mt-5 bg-gray-100 dark:bg-slate-800 p-3 rounded-lg mt-3"> |
|||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50"> |
|||
Device Metrics: |
|||
</p> |
|||
{device.deviceMetrics.airUtilTx ? ( |
|||
<p> |
|||
Air TX utilization:{" "} |
|||
{device.deviceMetrics.airUtilTx.toFixed(2)}% |
|||
</p> |
|||
) : null} |
|||
{device.deviceMetrics.channelUtilization ? ( |
|||
<p> |
|||
Channel utilization:{" "} |
|||
{device.deviceMetrics.channelUtilization.toFixed(2)}% |
|||
</p> |
|||
) : null} |
|||
{device.deviceMetrics.batteryLevel ? ( |
|||
<p> |
|||
Battery level:{" "} |
|||
{device.deviceMetrics.batteryLevel.toFixed(2)}% |
|||
</p> |
|||
) : null} |
|||
{device.deviceMetrics.voltage ? ( |
|||
<p>Voltage: {device.deviceMetrics.voltage.toFixed(2)}V</p> |
|||
) : null} |
|||
{device.deviceMetrics.uptimeSeconds ? ( |
|||
<p> |
|||
Uptime:{" "} |
|||
<Uptime seconds={device.deviceMetrics.uptimeSeconds} /> |
|||
</p> |
|||
) : null} |
|||
</div> |
|||
) : null} |
|||
|
|||
{device.environmentMetrics ? ( |
|||
<div className="mt-5 bg-gray-100 dark:bg-slate-800 p-3 rounded-lg mt-3"> |
|||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50"> |
|||
Environment Metrics: |
|||
</p> |
|||
{device.deviceMetrics.airUtilTx ? ( |
|||
<p> |
|||
Air TX utilization:{" "} |
|||
{device.deviceMetrics.airUtilTx.toFixed(2)}% |
|||
</p> |
|||
) : null} |
|||
{device.deviceMetrics.channelUtilization ? ( |
|||
<p> |
|||
Channel utilization:{" "} |
|||
{device.deviceMetrics.channelUtilization.toFixed(2)}% |
|||
</p> |
|||
) : null} |
|||
{device.deviceMetrics.batteryLevel ? ( |
|||
<p> |
|||
Battery level:{" "} |
|||
{device.deviceMetrics.batteryLevel.toFixed(2)}% |
|||
</p> |
|||
) : null} |
|||
{device.deviceMetrics.voltage ? ( |
|||
<p>Voltage: {device.deviceMetrics.voltage.toFixed(2)}V</p> |
|||
) : null} |
|||
{device.deviceMetrics.uptimeSeconds ? ( |
|||
<p> |
|||
Uptime:{" "} |
|||
<Uptime seconds={device.deviceMetrics.uptimeSeconds} /> |
|||
</p> |
|||
) : null} |
|||
</div> |
|||
) : null} |
|||
|
|||
{device ? ( |
|||
<div className="mt-5 w-full max-w-[464px] bg-gray-100 dark:bg-slate-800 p-3 rounded-lg mt-3"> |
|||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50"> |
|||
All Raw Metrics: |
|||
</p> |
|||
<pre className="text-xs w-full"> |
|||
{JSON.stringify(device, null, 2)} |
|||
</pre> |
|||
</div> |
|||
) : null} |
|||
</div> |
|||
</DialogFooter> |
|||
</DialogContent> |
|||
</Dialog> |
|||
) : null; |
|||
}; |
|||
Loading…
Reference in new issue