Add dialog Node Details
This commit is contained in:
@@ -6,6 +6,8 @@ import { QRDialog } from "@components/Dialog/QRDialog.tsx";
|
||||
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
|
||||
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import type { JSX } from "react";
|
||||
import { NodeDetailsDialog } from "./NodeDetailsDialog";
|
||||
|
||||
export const DialogManager = (): JSX.Element => {
|
||||
const { channels, config, dialog, setDialogOpen } = useDevice();
|
||||
@@ -56,6 +58,12 @@ export const DialogManager = (): JSX.Element => {
|
||||
setDialogOpen("pkiBackup", open);
|
||||
}}
|
||||
/>
|
||||
<NodeDetailsDialog
|
||||
open={dialog.nodeDetails}
|
||||
onOpenChange={(open) => {
|
||||
setDialogOpen("nodeDetails", open);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -25,7 +25,7 @@ export const NodeOptionsDialog = ({
|
||||
onOpenChange,
|
||||
}: NodeOptionsDialogProps): JSX.Element => {
|
||||
const { setDialogOpen, connection } = useDevice();
|
||||
const { setNodeNumToBeRemoved } = useAppStore();
|
||||
const { setNodeNumToBeRemoved, setNodeNumDetails } = useAppStore();
|
||||
const longName =
|
||||
node?.user?.longName ??
|
||||
(node ? `!${numberToHexUnpadded(node?.num)}` : "Unknown");
|
||||
@@ -85,6 +85,16 @@ export const NodeOptionsDialog = ({
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setNodeNumDetails(node.num);
|
||||
setDialogOpen("nodeDetails", true);
|
||||
}}
|
||||
>
|
||||
More Details
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -44,7 +44,7 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed z-50 grid w-full scale-100 gap-4 bg-white p-6 opacity-100 animate-in fade-in-90 slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0",
|
||||
"fixed z-50 grid w-full max-w-[512px] max-h-[100vh] overflow-y-scroll scale-100 gap-4 bg-white p-6 opacity-100 animate-in fade-in-90 slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0",
|
||||
"dark:bg-slate-900",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -29,6 +29,7 @@ interface AppState {
|
||||
nodeNumToBeRemoved: number;
|
||||
accent: AccentColor;
|
||||
connectDialogOpen: boolean;
|
||||
nodeNumDetails: number;
|
||||
|
||||
setRasterSources: (sources: RasterSource[]) => void;
|
||||
addRasterSource: (source: RasterSource) => void;
|
||||
@@ -42,6 +43,7 @@ interface AppState {
|
||||
setNodeNumToBeRemoved: (nodeNum: number) => void;
|
||||
setAccent: (color: AccentColor) => void;
|
||||
setConnectDialogOpen: (open: boolean) => void;
|
||||
setNodeNumDetails: (nodeNum: number) => void;
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>()((set) => ({
|
||||
@@ -57,6 +59,7 @@ export const useAppStore = create<AppState>()((set) => ({
|
||||
accent: "orange",
|
||||
connectDialogOpen: false,
|
||||
nodeNumToBeRemoved: 0,
|
||||
nodeNumDetails: 0,
|
||||
|
||||
setRasterSources: (sources: RasterSource[]) => {
|
||||
set(
|
||||
@@ -124,4 +127,8 @@ export const useAppStore = create<AppState>()((set) => ({
|
||||
}),
|
||||
);
|
||||
},
|
||||
setNodeNumDetails: (nodeNum) =>
|
||||
set((state) => ({
|
||||
nodeNumDetails: nodeNum,
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -26,7 +26,8 @@ export type DialogVariant =
|
||||
| "reboot"
|
||||
| "deviceName"
|
||||
| "nodeRemoval"
|
||||
| "pkiBackup";
|
||||
| "pkiBackup"
|
||||
| "nodeDetails";
|
||||
|
||||
export interface Device {
|
||||
id: number;
|
||||
@@ -62,6 +63,7 @@ export interface Device {
|
||||
deviceName: boolean;
|
||||
nodeRemoval: boolean;
|
||||
pkiBackup: boolean;
|
||||
nodeDetails: boolean;
|
||||
};
|
||||
|
||||
setStatus: (status: Types.DeviceStatusEnum) => void;
|
||||
@@ -145,6 +147,7 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
||||
deviceName: false,
|
||||
nodeRemoval: false,
|
||||
pkiBackup: false,
|
||||
nodeDetails: false,
|
||||
},
|
||||
pendingSettingsChanges: false,
|
||||
messageDraft: "",
|
||||
|
||||
Reference in New Issue
Block a user