feat: add policy deployment status tracking with cluster-level rule state monitoring and live firewall rule version comparison

Add policy_read_payload helper to build policy response with deployment status, implement nexafabric_rule_version to parse policy ID and version from rule comments, add policy_deployment_status to check active/stale/partial/unresolved states by comparing expected rules from preview against live firewall rules per cluster with version matching, extend PolicyRead schema with
This commit is contained in:
2026-07-09 19:52:10 +02:00
parent 6d5dc310df
commit 1802c2cbee
4 changed files with 216 additions and 3 deletions
+56 -1
View File
@@ -1,6 +1,6 @@
import { FormEvent, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Eye, GitBranch, Pencil, Play, Plus, Trash2 } from "lucide-react";
import { AlertTriangle, CheckCircle2, Clock3, Eye, GitBranch, Pencil, Play, Plus, Shield, Trash2 } from "lucide-react";
import { api, Policy, Project, ServiceCatalogItem } from "../api/client";
import { DataTable } from "../components/DataTable";
@@ -22,6 +22,60 @@ function policyService(policy: Policy) {
return `${String(value.protocol ?? "")}/${String(value.ports ?? "")}`;
}
function policyStatus(policy: Policy) {
const status = policy.deployment_status ?? {};
return {
state: String(status.state ?? "unknown"),
label: String(status.label ?? "Unknown"),
expectedRules: Number(status.expected_rules ?? 0),
activeRules: Number(status.active_rules ?? 0),
staleRules: Number(status.stale_rules ?? 0),
};
}
function statusClass(state: string) {
if (state === "active") {
return "border-accent/40 bg-accent/10 text-accent";
}
if (state === "audit") {
return "border-amber-400/40 bg-amber-400/10 text-amber-300";
}
if (["stale", "partial", "unresolved"].includes(state)) {
return "border-danger/40 bg-danger/10 text-danger";
}
return "border-border bg-canvas text-slate-500";
}
function StatusIcon({ state }: { state: string }) {
if (state === "active") {
return <CheckCircle2 size={15} />;
}
if (state === "audit") {
return <Shield size={15} />;
}
if (["stale", "partial", "unresolved"].includes(state)) {
return <AlertTriangle size={15} />;
}
return <Clock3 size={15} />;
}
function PolicyDeploymentStatus({ policy }: { policy: Policy }) {
const status = policyStatus(policy);
const detail =
status.state === "audit"
? "No live firewall write"
: `${status.activeRules}/${status.expectedRules} active${status.staleRules ? ` · ${status.staleRules} stale` : ""}`;
return (
<div className="flex flex-col gap-1">
<span className={`inline-flex w-fit items-center gap-1.5 rounded-md border px-2 py-1 text-xs ${statusClass(status.state)}`}>
<StatusIcon state={status.state} />
{status.label}
</span>
<span className="text-xs text-slate-500">{detail}</span>
</div>
);
}
const defaultPolicyForm = {
project_id: "",
name: "Web to DB",
@@ -200,6 +254,7 @@ export function Policies() {
{ key: "destination", label: "Destination", render: (row) => policyValue(row as unknown as Policy, "destination") },
{ key: "service", label: "Service", render: (row) => policyService(row as unknown as Policy) },
{ key: "enforcement_mode", label: "Mode" },
{ key: "deployment_status", label: "Status", render: (row) => <PolicyDeploymentStatus policy={row as unknown as Policy} /> },
{ key: "version", label: "Version" },
{
key: "actions",