mirror of
https://github.com/meshtastic/web.git
synced 2026-04-21 22:40:01 -04:00
Improve NodeDetailsDialog UI and add security info (#770)
* Improve NodeDetailsDialog UI and add security info Refactored NodeDetailsDialog to use tables for better layout and readability, added a security section displaying public key and verification status, and included messageable status. Updated i18n files with new keys and improved battery level formatting. Fixed logic in Nodes page for handling location packets and improved hardware model sorting. * Update NodeDetailsDialog.tsx
This commit is contained in:
@@ -107,5 +107,7 @@
|
||||
"managed": "At least one admin key is requred if the node is managed.",
|
||||
"key": "Key is required."
|
||||
}
|
||||
}
|
||||
},
|
||||
"yes": "Yes",
|
||||
"no": "No"
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
"deviceMetrics": "Device Metrics:",
|
||||
"hardware": "Hardware: ",
|
||||
"lastHeard": "Last Heard: ",
|
||||
"nodeHexPrefix": "Node Hex: !",
|
||||
"nodeHexPrefix": "Node Hex: ",
|
||||
"nodeNumber": "Node Number: ",
|
||||
"position": "Position:",
|
||||
"role": "Role: ",
|
||||
@@ -102,7 +102,12 @@
|
||||
"title": "Node Details for {{identifier}}",
|
||||
"ignoreNode": "Ignore node",
|
||||
"removeNode": "Remove node",
|
||||
"unignoreNode": "Unignore node"
|
||||
"unignoreNode": "Unignore node",
|
||||
"security": "Security:",
|
||||
"publicKey": "Public Key: ",
|
||||
"messageable": "Messageable: ",
|
||||
"KeyManuallyVerifiedTrue": "Public Key has been manually verified",
|
||||
"KeyManuallyVerifiedFalse": "Public Key is not manually verified"
|
||||
},
|
||||
"pkiBackup": {
|
||||
"loseKeysWarning": "If you lose your keys, you will need to reset your device.",
|
||||
|
||||
@@ -33,6 +33,7 @@ import { cn } from "@core/utils/cn.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { fromByteArray } from "base64-js";
|
||||
import {
|
||||
BellIcon,
|
||||
BellOffIcon,
|
||||
@@ -167,7 +168,8 @@ export const NodeDetailsDialog = ({
|
||||
key: "batteryLevel",
|
||||
label: t("nodeDetails.batteryLevel"),
|
||||
value: node.deviceMetrics?.batteryLevel,
|
||||
format: (val: number) => `${val.toFixed(2)}%`,
|
||||
format: (val: number) =>
|
||||
val === 101 ? t("batteryStatus.pluggedIn") : `${val.toFixed(2)}%`,
|
||||
},
|
||||
{
|
||||
key: "voltage",
|
||||
@@ -177,6 +179,9 @@ export const NodeDetailsDialog = ({
|
||||
},
|
||||
];
|
||||
|
||||
const sectionClassName =
|
||||
"text-slate-900 dark:text-slate-100 bg-slate-100 dark:bg-slate-800 p-4 rounded-lg mt-3";
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
@@ -192,7 +197,7 @@ export const NodeDetailsDialog = ({
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<div className="w-full">
|
||||
<div className="w-full ">
|
||||
<div className="flex flex-row flex-wrap space-y-1">
|
||||
<Button
|
||||
className="mr-1"
|
||||
@@ -270,79 +275,134 @@ export const NodeDetailsDialog = ({
|
||||
<p className="text-lg font-semibold">
|
||||
{t("nodeDetails.details")}
|
||||
</p>
|
||||
<p>
|
||||
{t("nodeDetails.nodeNumber")}
|
||||
{node.num}
|
||||
</p>
|
||||
<p>
|
||||
{t("nodeDetails.nodeHexPrefix")}
|
||||
{numberToHexUnpadded(node.num)}
|
||||
</p>
|
||||
<p>
|
||||
{t("nodeDetails.role")}
|
||||
{Protobuf.Config.Config_DeviceConfig_Role[
|
||||
node.user?.role ?? 0
|
||||
].replace(/_/g, " ")}
|
||||
</p>
|
||||
<p>
|
||||
{t("nodeDetails.lastHeard")}
|
||||
{node.lastHeard === 0 ? (
|
||||
t("nodesTable.lastHeardStatus.never", { ns: "nodes" })
|
||||
) : (
|
||||
<TimeAgo timestamp={node.lastHeard * 1000} />
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t("nodeDetails.hardware")}
|
||||
{(
|
||||
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ??
|
||||
t("unknown.shortName")
|
||||
).replace(/_/g, " ")}
|
||||
</p>
|
||||
<table className="table-fixed w-full">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{t("nodeDetails.nodeNumber")}</td>
|
||||
<td>{node.num}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t("nodeDetails.nodeHexPrefix")}</td>
|
||||
<td>!{numberToHexUnpadded(node.num)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t("nodeDetails.role")}</td>
|
||||
<td>
|
||||
{Protobuf.Config.Config_DeviceConfig_Role[
|
||||
node.user?.role ?? 0
|
||||
]?.replace(/_/g, " ")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t("nodeDetails.lastHeard")}</td>
|
||||
<td>
|
||||
{node.lastHeard === 0 ? (
|
||||
t("nodesTable.lastHeardStatus.never", {
|
||||
ns: "nodes",
|
||||
})
|
||||
) : (
|
||||
<TimeAgo timestamp={node.lastHeard * 1000} />
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t("nodeDetails.hardware")}</td>
|
||||
<td>
|
||||
{(
|
||||
Protobuf.Mesh.HardwareModel[
|
||||
node.user?.hwModel ?? 0
|
||||
] ?? t("unknown.shortName")
|
||||
).replace(/_/g, " ")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t("nodeDetails.messageable")}</td>
|
||||
<td>
|
||||
{node.user?.isUnmessagable ? t("no") : t("yes")}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DeviceImage
|
||||
className="h-45 w-45 p-2 rounded-lg border-4 border-slate-200 dark:border-slate-800"
|
||||
className="w-40 p-2 rounded-lg border-4 border-slate-200 dark:border-slate-800"
|
||||
deviceType={
|
||||
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]
|
||||
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ??
|
||||
"UNKNOWN"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-slate-900 dark:text-slate-100 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||
<div className={sectionClassName}>
|
||||
<p className="text-lg font-semibold">
|
||||
{t("nodeDetails.security")}
|
||||
</p>
|
||||
<table className="table-auto w-full">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="pr-2">{t("nodeDetails.publicKey")}</td>
|
||||
<td>
|
||||
<pre className="text-xs pt-0.5">
|
||||
{node.user?.publicKey &&
|
||||
node.user?.publicKey.length > 0
|
||||
? fromByteArray(node.user.publicKey)
|
||||
: t("unknown.longName")}
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
{node.isKeyManuallyVerified
|
||||
? t("nodeDetails.KeyManuallyVerifiedTrue")
|
||||
: t("nodeDetails.KeyManuallyVerifiedFalse")}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className={sectionClassName}>
|
||||
<p className="text-lg font-semibold">
|
||||
{t("nodeDetails.position")}
|
||||
</p>
|
||||
|
||||
{node.position ? (
|
||||
<>
|
||||
{node.position.latitudeI && node.position.longitudeI && (
|
||||
<p>
|
||||
{t("locationResponse.coordinates")}
|
||||
<a
|
||||
className="text-blue-500 dark:text-blue-400"
|
||||
href={`https://www.openstreetmap.org/?mlat=${
|
||||
node.position.latitudeI / 1e7
|
||||
}&mlon=${node.position.longitudeI / 1e7}&layers=N`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{node.position.latitudeI / 1e7},{" "}
|
||||
{node.position.longitudeI / 1e7}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{node.position.altitude && (
|
||||
<p>
|
||||
{t("locationResponse.altitude")}
|
||||
{node.position.altitude}
|
||||
{t("unit.meter.one")}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
<table className="table-auto w-full">
|
||||
<tbody>
|
||||
{node.position.latitudeI && node.position.longitudeI && (
|
||||
<tr>
|
||||
<td>{t("locationResponse.coordinates")}</td>
|
||||
<td>
|
||||
<a
|
||||
className="text-blue-500 dark:text-blue-400"
|
||||
href={`https://www.openstreetmap.org/?mlat=${
|
||||
node.position.latitudeI / 1e7
|
||||
}&mlon=${node.position.longitudeI / 1e7}&layers=N`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{node.position.latitudeI / 1e7},{" "}
|
||||
{node.position.longitudeI / 1e7}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{node.position.altitude && (
|
||||
<tr>
|
||||
<td>{t("locationResponse.altitude")}</td>
|
||||
<td>
|
||||
{node.position.altitude}
|
||||
{t("unit.meter.suffix")}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<p>{t("unknown.shortName")}</p>
|
||||
<p>{t("unknown.longName")}</p>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleRequestPosition}
|
||||
@@ -355,28 +415,37 @@ export const NodeDetailsDialog = ({
|
||||
</div>
|
||||
|
||||
{node.deviceMetrics && (
|
||||
<div className="text-slate-900 dark:text-slate-100 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||
<div className={sectionClassName}>
|
||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-100">
|
||||
{t("nodeDetails.deviceMetrics")}
|
||||
</p>
|
||||
{deviceMetricsMap
|
||||
.filter((metric) => metric.value !== undefined)
|
||||
.map((metric) => (
|
||||
<p key={metric.key}>
|
||||
{metric.label}: {metric.format(metric?.value ?? 0)}
|
||||
</p>
|
||||
))}
|
||||
{node.deviceMetrics.uptimeSeconds && (
|
||||
<p>
|
||||
{t("nodeDetails.uptime")}
|
||||
<Uptime seconds={node.deviceMetrics.uptimeSeconds} />
|
||||
</p>
|
||||
)}
|
||||
<table className="table-fixed w-full">
|
||||
<tbody>
|
||||
{deviceMetricsMap
|
||||
.filter((metric) => metric.value !== undefined)
|
||||
.map((metric) => (
|
||||
<tr key={metric.key}>
|
||||
<td>{metric.label}: </td>
|
||||
<td>{metric.format(metric?.value ?? 0)}</td>
|
||||
</tr>
|
||||
))}
|
||||
{node.deviceMetrics.uptimeSeconds && (
|
||||
<tr>
|
||||
<td>{t("nodeDetails.uptime")}</td>
|
||||
<td>
|
||||
<Uptime
|
||||
seconds={node.deviceMetrics.uptimeSeconds}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-slate-900 dark:text-slate-100 w-full max-w-[464px] bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||
<div className="text-slate-900 dark:text-slate-100 w-full max-w-[464px] bg-slate-100 dark:bg-slate-800 rounded-lg mt-3">
|
||||
<Accordion className="AccordionRoot" type="single" collapsible>
|
||||
<AccordionItem className="AccordionItem" value="item-1">
|
||||
<AccordionTrigger>
|
||||
|
||||
@@ -72,7 +72,10 @@ const NodesPage = (): JSX.Element => {
|
||||
|
||||
const handleLocation = useCallback(
|
||||
(location: Types.PacketMetadata<Protobuf.Mesh.Position>) => {
|
||||
if (location.to.valueOf() !== hardware.myNodeNum) {
|
||||
if (
|
||||
location.to.valueOf() !== hardware.myNodeNum ||
|
||||
location.from.valueOf() === hardware.myNodeNum
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setSelectedLocation(location);
|
||||
@@ -213,7 +216,8 @@ const NodesPage = (): JSX.Element => {
|
||||
content: (
|
||||
<Mono>{Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]}</Mono>
|
||||
),
|
||||
sortValue: Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0],
|
||||
sortValue:
|
||||
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ?? "UNSET",
|
||||
},
|
||||
{
|
||||
content: <Mono>{macAddress}</Mono>,
|
||||
|
||||
Reference in New Issue
Block a user