import { useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { Activity, ArrowRight, CircuitBoard, Hash, Network, Shield, ShieldCheck } from "lucide-react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { api, Workload, WorkloadInsight } from "../api/client"; import { DataTable } from "../components/DataTable"; import { secondaryButtonClass } from "../components/FormControls"; import { PageHeader } from "../components/PageHeader"; type TrafficSummary = { key: string; source: string; destination: string; sourceLabel: string; destinationLabel: string; protocol: string; port: string; bytes: number; packets: number; count: number; decision: string; interfaceName: string; note: string; ipAddresses: string[]; matchingFirewallRules: Array>; matchingAuditPolicies: Array>; matchingPolicies: Array>; }; function records(value: unknown) { return Array.isArray(value) ? (value.filter((item) => item && typeof item === "object") as Array>) : []; } function mergeRecords(left: Array>, right: Array>) { const seen = new Set(); const merged: Array> = []; for (const item of [...left, ...right]) { const key = String(item.id ?? item.pos ?? item.comment ?? JSON.stringify(item)); if (seen.has(key)) { continue; } seen.add(key); merged.push(item); } return merged; } function formatBytes(value: unknown) { const bytes = Number(value ?? 0); if (!Number.isFinite(bytes) || bytes <= 0) { return "0 B"; } const units = ["B", "KB", "MB", "GB", "TB"]; const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); return `${(bytes / 1024 ** index).toFixed(index === 0 ? 0 : 1)} ${units[index]}`; } function summarizeTraffic(traffic: Array>) { const summaries = new Map(); for (const flow of traffic) { const source = String(flow.source ?? flow.source_ip ?? "external"); const destination = String(flow.destination ?? flow.destination_ip ?? "external"); const sourceLabel = String(flow.source_label ?? flow.source_ip ?? source); const destinationLabel = String(flow.destination_label ?? flow.destination_ip ?? destination); const protocol = String(flow.protocol ?? "unknown"); const port = String(flow.port ?? flow.destination_port ?? ""); const key = [source, destination, protocol, port, String(flow.interface ?? "")].join("|"); const existing = summaries.get(key); const bytes = Number(flow.bytes ?? 0); const packets = Number(flow.packets ?? 0); const ipAddresses = Array.isArray(flow.ip_addresses) ? flow.ip_addresses.map(String) : []; const matchingFirewallRules = records(flow.matching_firewall_rules); const matchingAuditPolicies = records(flow.matching_audit_policies); const matchingPolicies = records(flow.matching_policies); if (existing) { existing.bytes += Number.isFinite(bytes) ? bytes : 0; existing.packets += Number.isFinite(packets) ? packets : 0; existing.count += 1; existing.ipAddresses = Array.from(new Set([...existing.ipAddresses, ...ipAddresses])); existing.matchingFirewallRules = mergeRecords(existing.matchingFirewallRules, matchingFirewallRules); existing.matchingAuditPolicies = mergeRecords(existing.matchingAuditPolicies, matchingAuditPolicies); existing.matchingPolicies = mergeRecords(existing.matchingPolicies, matchingPolicies); if (existing.decision === "observed" && String(flow.decision ?? "observed") !== "observed") { existing.decision = String(flow.decision ?? "observed"); } continue; } summaries.set(key, { key, source, destination, sourceLabel, destinationLabel, protocol, port, bytes: Number.isFinite(bytes) ? bytes : 0, packets: Number.isFinite(packets) ? packets : 0, count: 1, decision: String(flow.decision ?? "observed"), interfaceName: String(flow.interface ?? ""), note: String(flow.note ?? ""), ipAddresses, matchingFirewallRules, matchingAuditPolicies, matchingPolicies, }); } return Array.from(summaries.values()).sort((left, right) => right.bytes - left.bytes); } function endpointText(flow: TrafficSummary) { return `${flow.sourceLabel} -> ${flow.destinationLabel}`; } function totalBytes(traffic: TrafficSummary[]) { return traffic.reduce((sum, flow) => sum + flow.bytes, 0); } function TrafficBars({ traffic }: { traffic: TrafficSummary[] }) { const top = traffic.slice(0, 5); const max = Math.max(...top.map((flow) => flow.bytes), 1); if (!top.length) { return
No traffic data yet.
; } return (
{top.map((flow) => (
{endpointText(flow)} {formatBytes(flow.bytes)}
))} {Array.from({ length: Math.max(5 - top.length, 0) }).map((_, index) => (
No additional flow 0 B
))}
); } function CompactFlowList({ traffic }: { traffic: TrafficSummary[] }) { const top = traffic.filter((flow) => flow.protocol !== "interface-counter").slice(0, 3); if (!top.length) { const fallback = traffic.find((flow) => flow.protocol === "interface-counter"); return (
{fallback ? `Interface counter fallback: ${formatBytes(fallback.bytes)} observed.` : "No flow telemetry collected yet."}
); } return (
{top.map((flow) => (
{endpointText(flow)}
{flow.protocol}{flow.port ? `:${flow.port}` : ""} · {flow.decision}
{formatBytes(flow.bytes)}
))}
); } function ruleLabel(rule: Record) { if (rule.error) { return String(rule.error); } const type = String(rule.type ?? "rule"); const action = String(rule.action ?? "unknown"); const proto = rule.proto ? String(rule.proto) : "any"; const port = rule.dport || rule.sport ? `:${String(rule.dport ?? rule.sport)}` : ""; return `${type} ${action} ${proto}${port}`; } function policyLabel(policy: Record) { const name = String(policy.name ?? "Policy"); const mode = String(policy.enforcement_mode ?? "enforced"); const decision = String(policy.decision ?? "observed").replace("_", " "); const protocol = String(policy.protocol ?? "any"); const ports = policy.ports ? `:${String(policy.ports)}` : ""; return `${name} · ${mode} · ${decision} · ${protocol}${ports}`; } function decisionClass(decision: string) { if (decision.includes("would")) { return "border-amber-400/40 bg-amber-400/10 text-amber-300"; } if (decision.includes("block")) { return "border-danger/40 bg-danger/10 text-danger"; } if (decision.includes("allow")) { return "border-accent/40 bg-accent/10 text-accent"; } return "border-border bg-canvas text-slate-500"; } function FlowRuleContext({ flow }: { flow: TrafficSummary }) { const activeRules = flow.matchingFirewallRules.slice(0, 2); const auditPolicies = flow.matchingAuditPolicies.slice(0, 2); const hasContext = activeRules.length || auditPolicies.length; if (!hasContext) { return
No matching active or audit rule.
; } return (
{activeRules.map((rule, index) => (
Rule: {ruleLabel(rule)} {String(rule.decision ?? "observed")}
))} {auditPolicies.map((policy, index) => (
Audit: {policyLabel(policy)}
))} {flow.matchingFirewallRules.length + flow.matchingAuditPolicies.length > activeRules.length + auditPolicies.length ? (
{flow.matchingFirewallRules.length + flow.matchingAuditPolicies.length - activeRules.length - auditPolicies.length} more match {flow.matchingFirewallRules.length + flow.matchingAuditPolicies.length - activeRules.length - auditPolicies.length === 1 ? "" : "es"}
) : null}
); } function ActiveRulesList({ rules, compact = false }: { rules: Array>; compact?: boolean }) { const visibleRules = compact ? rules.slice(0, 3) : rules; if (!rules.length) { return
No active firewall rules were read for this workload.
; } return (
{visibleRules.map((rule, index) => (
{ruleLabel(rule)}
{String(rule.comment ?? (rule.managed_by_nexafabric ? "NexaFabric managed" : "manual or provider rule"))}
{rule.enable === 0 ? "off" : "on"}
))} {compact && rules.length > visibleRules.length ? (
{rules.length - visibleRules.length} more rule{rules.length - visibleRules.length === 1 ? "" : "s"} on detail page.
) : null}
); } function ProtocolChart({ traffic }: { traffic: TrafficSummary[] }) { const protocolTotals = Array.from( traffic.reduce((map, flow) => map.set(flow.protocol, (map.get(flow.protocol) ?? 0) + flow.bytes), new Map()), ).sort((left, right) => right[1] - left[1]); const total = protocolTotals.reduce((sum, [, bytes]) => sum + bytes, 0); const palette = ["#2dd4bf", "#60a5fa", "#f59e0b", "#f472b6", "#a78bfa"]; if (!total) { return
No protocol split available.
; } return (
{protocolTotals.map(([protocol, bytes], index) => { const width = (bytes / total) * 100; return
; })}
{protocolTotals.map(([protocol, bytes], index) => ( {protocol} {formatBytes(bytes)} ))}
); } function WorkloadFacts({ insight }: { insight: WorkloadInsight }) { return (
Type
{insight.workload.kind}
Status
{insight.workload.status}
VMID
{insight.workload.external_id}
Decision
{insight.effective_decision}
); } function TrafficTable({ traffic }: { traffic: TrafficSummary[] }) { return (
{traffic.map((flow) => ( ))}
Flow Protocol Decision Traffic Packets Seen
{endpointText(flow)}
{flow.ipAddresses.join(", ") || flow.interfaceName || "no endpoint metadata"}
{flow.protocol}{flow.port ? `:${flow.port}` : ""} {flow.decision.replace("_", " ")} {formatBytes(flow.bytes)} {flow.packets} {flow.count} sample{flow.count === 1 ? "" : "s"}
); } export function Workloads() { const navigate = useNavigate(); const workloads = useQuery({ queryKey: ["workloads"], queryFn: () => api("/vms") }); const [selectedId, setSelectedId] = useState(""); const selected = selectedId || workloads.data?.[0]?.id || ""; const insight = useQuery({ queryKey: ["workload-insight", selected], queryFn: () => api(`/vms/${selected}/insights`), enabled: Boolean(selected), }); const traffic = useMemo(() => summarizeTraffic(insight.data?.traffic ?? []), [insight.data?.traffic]); return ( <>
[]} columns={[{ key: "name", label: "Name" }, { key: "kind", label: "Kind" }, { key: "status", label: "Status" }, { key: "external_id", label: "VMID" }]} selectedId={selected} onRowClick={(row) => setSelectedId(String(row.id))} />
); } export function WorkloadDetail() { const { workloadId } = useParams(); const insight = useQuery({ queryKey: ["workload-insight", workloadId], queryFn: () => api(`/vms/${workloadId}/insights`), enabled: Boolean(workloadId), }); const traffic = useMemo(() => summarizeTraffic(insight.data?.traffic ?? []), [insight.data?.traffic]); if (insight.isLoading) { return
Loading workload...
; } if (!insight.data) { return
Workload details could not be loaded.
; } return ( <>
Back to VMs/LXCs
Traffic Distribution
{traffic.length} aggregated flows · {formatBytes(totalBytes(traffic))}
Flow Table
{traffic.length ? :
No flow telemetry collected yet.
}
); }