feat: add active firewall rules display to workload insights with managed rule detection and enable status

Add active_firewall_rules_for_workload helper to fetch live firewall rules from provider with NexaFabric policy comment detection and enable status, implement list_firewall_rules method in ProxmoxProvider to retrieve rules via firewall API endpoint, extend WorkloadInsight schema with active_firewall_rules field, add ActiveRulesList component showing rule type/action/protocol/port with enable status and
This commit is contained in:
2026-07-09 19:42:51 +02:00
parent 1b81847fc6
commit b12ac38c6c
5 changed files with 89 additions and 2 deletions
+44 -1
View File
@@ -1,6 +1,6 @@
import { useMemo, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Activity, ArrowRight, CircuitBoard, Hash, Network, ShieldCheck } from "lucide-react";
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";
@@ -143,6 +143,41 @@ function CompactFlowList({ traffic }: { traffic: TrafficSummary[] }) {
);
}
function ruleLabel(rule: Record<string, unknown>) {
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 ActiveRulesList({ rules, compact = false }: { rules: Array<Record<string, unknown>>; compact?: boolean }) {
const visibleRules = compact ? rules.slice(0, 3) : rules;
if (!rules.length) {
return <div className="rounded-md border border-border p-2 text-xs text-slate-500">No active firewall rules were read for this workload.</div>;
}
return (
<div className="divide-y divide-border rounded-md border border-border">
{visibleRules.map((rule, index) => (
<div key={`${String(rule.pos ?? index)}-${index}`} className="grid grid-cols-[1fr_auto] gap-3 px-3 py-2 text-xs">
<div className="min-w-0">
<div className="truncate font-medium">{ruleLabel(rule)}</div>
<div className="truncate text-slate-500">{String(rule.comment ?? (rule.managed_by_nexafabric ? "NexaFabric managed" : "manual or provider rule"))}</div>
</div>
<div className={rule.enable === 0 ? "text-slate-500" : "text-accent"}>{rule.enable === 0 ? "off" : "on"}</div>
</div>
))}
{compact && rules.length > visibleRules.length ? (
<div className="px-3 py-2 text-xs text-slate-500">{rules.length - visibleRules.length} more rule{rules.length - visibleRules.length === 1 ? "" : "s"} on detail page.</div>
) : null}
</div>
);
}
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<string, number>()),
@@ -308,6 +343,10 @@ export function Workloads() {
<div className="mb-1.5 text-sm font-medium">Top Flows</div>
<CompactFlowList traffic={traffic} />
</section>
<section>
<div className="mb-1.5 flex items-center gap-2 text-sm font-medium"><Shield size={15} /> Active Rules</div>
<ActiveRulesList rules={insight.data.active_firewall_rules ?? []} compact />
</section>
</div>
) : (
<div className="text-sm text-slate-500">Select a workload.</div>
@@ -374,6 +413,10 @@ export function WorkloadDetail() {
{!insight.data.assigned_ips.length ? <div className="text-xs text-slate-500">No assigned IPs discovered.</div> : null}
</div>
</div>
<div className="rounded-md border border-border bg-panel p-4">
<div className="mb-3 flex items-center gap-2 font-medium"><Shield size={16} /> Active Firewall Rules</div>
<ActiveRulesList rules={insight.data.active_firewall_rules ?? []} />
</div>
<div className="rounded-md border border-border bg-panel p-4">
<div className="mb-3 font-medium">Matching Policies</div>
<div className="space-y-2">