From 32906bca1e905675b4430e89a88f0ec25a0eec74 Mon Sep 17 00:00:00 2001 From: nessi Date: Thu, 9 Jul 2026 15:42:56 +0200 Subject: [PATCH] feat: add IP-based flow labels with internal/external classification and redesign workload summary sidebar with compact flow visualization Add flow_ip_label helper to format flow endpoints with IP addresses and internal/external classification based on subnet membership, extend workload_insights response with source_label/destination_label fields showing IP addresses with context, implement endpointText helper to display formatted flow labels in UI, add CompactFlowList component showing top 3 flows with protocol --- backend/app/api/v1/router.py | 10 ++++ frontend/src/pages/Workloads.tsx | 89 ++++++++++++++++++++++---------- 2 files changed, 73 insertions(+), 26 deletions(-) diff --git a/backend/app/api/v1/router.py b/backend/app/api/v1/router.py index 58979d7..1bf31f2 100644 --- a/backend/app/api/v1/router.py +++ b/backend/app/api/v1/router.py @@ -286,6 +286,12 @@ def flow_endpoint_label(owner: Workload | None, subnets: list[Subnet], value: st return subnet_label_for_ip(subnets, value) or "external" +def flow_ip_label(owner: Workload | None, subnets: list[Subnet], value: str) -> str: + if owner: + return f"{value} (internal)" + return f"{value} ({'internal' if subnet_label_for_ip(subnets, value) else 'external'})" + + def dashboard_top_talkers(db: Session) -> list[dict[str, int | str]]: totals: dict[str, int] = {} workloads = db.scalars(select(Workload)).all() @@ -966,6 +972,8 @@ def workload_insights(workload_id: str, _: CurrentUser, db: Session = Depends(ge { "source": flow_endpoint_label(source_owner, known_subnets, flow.source_ip), "destination": flow_endpoint_label(destination_owner, known_subnets, flow.destination_ip), + "source_label": flow_ip_label(source_owner, known_subnets, flow.source_ip), + "destination_label": flow_ip_label(destination_owner, known_subnets, flow.destination_ip), "source_ip": flow.source_ip, "destination_ip": flow.destination_ip, "protocol": flow.protocol, @@ -989,6 +997,8 @@ def workload_insights(workload_id: str, _: CurrentUser, db: Session = Depends(ge { "source": workload.name, "destination": "network", + "source_label": f"{workload_ips[0]} (internal)" if workload_ips else workload.name, + "destination_label": "network", "interface": item.get("interface"), "protocol": item.get("protocol") or "interface-counter", "port": None, diff --git a/frontend/src/pages/Workloads.tsx b/frontend/src/pages/Workloads.tsx index d114311..d644dd2 100644 --- a/frontend/src/pages/Workloads.tsx +++ b/frontend/src/pages/Workloads.tsx @@ -12,6 +12,8 @@ type TrafficSummary = { key: string; source: string; destination: string; + sourceLabel: string; + destinationLabel: string; protocol: string; port: string; bytes: number; @@ -38,6 +40,8 @@ function summarizeTraffic(traffic: Array>) { 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("|"); @@ -56,6 +60,8 @@ function summarizeTraffic(traffic: Array>) { key, source, destination, + sourceLabel, + destinationLabel, protocol, port, bytes: Number.isFinite(bytes) ? bytes : 0, @@ -70,6 +76,10 @@ function summarizeTraffic(traffic: Array>) { 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); } @@ -82,11 +92,11 @@ function TrafficBars({ traffic }: { traffic: TrafficSummary[] }) { } return ( -
+
{top.map((flow) => (
- {flow.source} {"->"} {flow.destination} + {endpointText(flow)} {formatBytes(flow.bytes)}
@@ -94,6 +104,41 @@ function TrafficBars({ traffic }: { traffic: TrafficSummary[] }) {
))} + {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)}
+
+ ))}
); } @@ -170,7 +215,7 @@ function TrafficTable({ traffic }: { traffic: TrafficSummary[] }) { {traffic.map((flow) => ( -
{flow.source} {"->"} {flow.destination}
+
{endpointText(flow)}
{flow.ipAddresses.join(", ") || flow.interfaceName || "no endpoint metadata"}
{flow.protocol}{flow.port ? `:${flow.port}` : ""} @@ -210,66 +255,58 @@ export function Workloads() { onRowClick={(row) => setSelectedId(String(row.id))} /> -