feat: add agent data detail modal with interface telemetry, conntrack flows, and raw payload viewer
Add ScrollText icon button to nodes table to open agent data modal, implement agentFlows/agentInterfaces/agentConntrack helpers to safely extract telemetry from last_payload with type guards, add Modal with status/flows/conntrack summary cards, display interface list with operstate/vmid/rx/tx stats in scrollable container, show top 50 conntrack flows with source/destination/protocol/bytes/packets/state details
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Cpu, Copy, RadioTower } from "lucide-react";
|
||||
import { Activity, Cpu, Copy, RadioTower, ScrollText } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { AgentInstallInfo, api, Node } from "../api/client";
|
||||
@@ -11,6 +11,7 @@ import { PageHeader } from "../components/PageHeader";
|
||||
export function Nodes() {
|
||||
const nodes = useQuery({ queryKey: ["nodes-agents"], queryFn: () => api<Node[]>("/nodes/agents") });
|
||||
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
|
||||
const [detailNode, setDetailNode] = useState<Node | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const installInfo = useMutation({
|
||||
mutationFn: (node: Node) => api<AgentInstallInfo>(`/nodes/${node.id}/agent/install-info`),
|
||||
@@ -28,6 +29,21 @@ export function Nodes() {
|
||||
setCopied(true);
|
||||
}
|
||||
|
||||
function agentFlows(node: Node) {
|
||||
const flows = node.agent?.last_payload?.flows;
|
||||
return Array.isArray(flows) ? flows : [];
|
||||
}
|
||||
|
||||
function agentInterfaces(node: Node) {
|
||||
const interfaces = node.agent?.last_payload?.interfaces;
|
||||
return Array.isArray(interfaces) ? interfaces : [];
|
||||
}
|
||||
|
||||
function agentConntrack(node: Node) {
|
||||
const conntrack = node.agent?.last_payload?.conntrack;
|
||||
return conntrack && typeof conntrack === "object" ? conntrack as Record<string, unknown> : {};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Nodes" subtitle="Cluster nodes, capacity, health, and NexaFabric agent enrollment." />
|
||||
@@ -56,6 +72,15 @@ export function Nodes() {
|
||||
const node = row as unknown as Node;
|
||||
return (
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
className={iconButtonClass}
|
||||
title="View agent data"
|
||||
aria-label={`View agent data for ${node.name}`}
|
||||
disabled={!node.agent?.last_payload}
|
||||
onClick={() => setDetailNode(node)}
|
||||
>
|
||||
<ScrollText size={16} />
|
||||
</button>
|
||||
<button
|
||||
className={iconButtonClass}
|
||||
title="Install node agent"
|
||||
@@ -93,6 +118,70 @@ export function Nodes() {
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal title="Agent Data" open={Boolean(detailNode)} onClose={() => setDetailNode(null)}>
|
||||
{detailNode ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
<Activity size={18} />
|
||||
{detailNode.name}
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<div className="rounded-md border border-border bg-canvas p-3">
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400">Status</div>
|
||||
<div className="mt-1 text-sm font-medium">{detailNode.agent?.status ?? "not_installed"}</div>
|
||||
</div>
|
||||
<div className="rounded-md border border-border bg-canvas p-3">
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400">Flows</div>
|
||||
<div className="mt-1 text-sm font-medium">{agentFlows(detailNode).length}</div>
|
||||
</div>
|
||||
<div className="rounded-md border border-border bg-canvas p-3">
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400">Conntrack</div>
|
||||
<div className="mt-1 text-sm font-medium">{String(agentConntrack(detailNode).count ?? "unknown")}</div>
|
||||
</div>
|
||||
</div>
|
||||
<section>
|
||||
<div className="mb-2 text-sm font-medium">Interfaces</div>
|
||||
<div className="max-h-48 overflow-auto rounded-md border border-border">
|
||||
{agentInterfaces(detailNode).length ? agentInterfaces(detailNode).map((item, index) => {
|
||||
const iface = item as Record<string, unknown>;
|
||||
return (
|
||||
<div key={`${String(iface.name)}-${index}`} className="grid grid-cols-[1fr_auto] gap-3 border-b border-border px-3 py-2 text-xs last:border-0">
|
||||
<div>
|
||||
<div className="font-medium">{String(iface.name)}</div>
|
||||
<div className="text-slate-500">state {String(iface.operstate ?? "unknown")} {iface.vmid ? `· VMID ${String(iface.vmid)}` : ""}</div>
|
||||
</div>
|
||||
<div className="text-right text-slate-500">
|
||||
<div>rx {String(iface.rx_bytes ?? 0)}</div>
|
||||
<div>tx {String(iface.tx_bytes ?? 0)}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}) : <div className="p-3 text-xs text-slate-500">No interface telemetry in the last heartbeat.</div>}
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div className="mb-2 text-sm font-medium">Flows</div>
|
||||
<div className="max-h-48 overflow-auto rounded-md border border-border">
|
||||
{agentFlows(detailNode).length ? agentFlows(detailNode).slice(0, 50).map((item, index) => {
|
||||
const flow = item as Record<string, unknown>;
|
||||
return (
|
||||
<div key={index} className="border-b border-border px-3 py-2 text-xs last:border-0">
|
||||
<div className="font-medium">{String(flow.source_ip)}:{String(flow.source_port ?? "")} -> {String(flow.destination_ip)}:{String(flow.destination_port ?? "")}</div>
|
||||
<div className="text-slate-500">{String(flow.protocol ?? "unknown")} · {String(flow.bytes ?? 0)} bytes · {String(flow.packets ?? 0)} packets · {String(flow.state ?? "unknown")}</div>
|
||||
</div>
|
||||
);
|
||||
}) : <div className="p-3 text-xs text-slate-500">No conntrack flows were reported in the last heartbeat.</div>}
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div className="mb-2 text-sm font-medium">Raw Payload</div>
|
||||
<pre className="max-h-60 overflow-auto rounded-md border border-border bg-canvas p-3 text-xs">
|
||||
{JSON.stringify(detailNode.agent?.last_payload ?? {}, null, 2)}
|
||||
</pre>
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user