import { FormEvent, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
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";
import { buttonClass, Field, iconButtonClass, inputClass, selectClass } from "../components/FormControls";
import { LoadingOverlay } from "../components/LoadingOverlay";
import { Modal } from "../components/Modal";
import { PageHeader } from "../components/PageHeader";
function policyValue(policy: Policy, key: string) {
return String(policy.definition?.[key] ?? "");
}
function policyService(policy: Policy) {
const service = policy.definition?.service;
if (!service || typeof service !== "object") {
return "";
}
const value = service as { protocol?: unknown; ports?: unknown };
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 ;
}
if (state === "audit") {
return ;
}
if (["stale", "partial", "unresolved"].includes(state)) {
return ;
}
return ;
}
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 (
{status.label}
{detail}
);
}
const defaultPolicyForm = {
project_id: "",
name: "Web to DB",
source: "sg:Web Tier",
destination: "sg:Database",
service_id: "",
protocol: "tcp",
ports: "5432",
action: "allow",
direction: "egress",
enforcement_mode: "enforced",
logging: true,
description: "Allow application database traffic",
};
export function Policies() {
const queryClient = useQueryClient();
const policies = useQuery({ queryKey: ["policies"], queryFn: () => api("/policies") });
const projects = useQuery({ queryKey: ["projects"], queryFn: () => api("/projects") });
const services = useQuery({ queryKey: ["service-catalog"], queryFn: () => api("/service-catalog") });
const [preview, setPreview] = useState("");
const [open, setOpen] = useState(false);
const [editing, setEditing] = useState(null);
const [form, setForm] = useState(defaultPolicyForm);
const [busyMessage, setBusyMessage] = useState("");
const save = useMutation({
mutationFn: () => {
const service = services.data?.find((item) => item.id === form.service_id);
return api(editing ? `/policies/${editing.id}` : "/policies", {
method: editing ? "PATCH" : "POST",
body: JSON.stringify({
project_id: form.project_id || null,
name: form.name,
enabled: true,
definition: {
source: form.source,
destination: form.destination,
service: { protocol: service?.protocol ?? form.protocol, ports: service?.ports ?? form.ports },
action: form.action,
direction: form.direction,
enforcement_mode: form.enforcement_mode,
logging: form.logging,
description: form.description,
},
}),
});
},
onSuccess: () => {
setOpen(false);
setEditing(null);
queryClient.invalidateQueries({ queryKey: ["policies"] });
},
});
const remove = useMutation({
mutationFn: (policy: Policy) => api(`/policies/${policy.id}`, { method: "DELETE" }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["policies"] }),
});
async function submit(event: FormEvent) {
event.preventDefault();
await save.mutateAsync();
}
async function compile(policy: Policy) {
setBusyMessage(`Compiling ${policy.name}...`);
try {
const data = await api(`/policies/${policy.id}/compile`, { method: "POST" });
setPreview(JSON.stringify(data.last_compiled, null, 2));
await queryClient.invalidateQueries({ queryKey: ["policies"] });
} finally {
setBusyMessage("");
}
}
async function firewallPreview(policy: Policy) {
setBusyMessage(`Generating preview for ${policy.name}...`);
try {
const data = await api>(`/firewall/preview/${policy.id}`, { method: "POST" });
setPreview(JSON.stringify(data, null, 2));
} finally {
setBusyMessage("");
}
}
function addPolicy() {
setEditing(null);
setForm(defaultPolicyForm);
setOpen(true);
}
function editPolicy(policy: Policy) {
const service = policy.definition?.service as { protocol?: string; ports?: string } | undefined;
setEditing(policy);
setForm({
project_id: policy.project_id ?? "",
name: policy.name,
source: policyValue(policy, "source") || "any",
destination: policyValue(policy, "destination") || "any",
service_id: "",
protocol: service?.protocol ?? "tcp",
ports: service?.ports ?? "",
action: policyValue(policy, "action") || "allow",
direction: policyValue(policy, "direction") || "ingress",
enforcement_mode: policy.enforcement_mode || "enforced",
logging: Boolean(policy.definition?.logging),
description: policyValue(policy, "description"),
});
setOpen(true);
}
function chooseService(serviceId: string) {
const service = services.data?.find((item) => item.id === serviceId);
setForm({
...form,
service_id: serviceId,
protocol: service?.protocol ?? form.protocol,
ports: service?.ports ?? form.ports,
});
}
function deletePolicy(policy: Policy) {
if (window.confirm(`Delete policy "${policy.name}"?`)) {
remove.mutate(policy);
}
}
return (
<>
setOpen(false)}>
[]}
columns={[
{ key: "name", label: "Policy" },
{ key: "source", label: "Source", render: (row) => policyValue(row as unknown as Policy, "source") },
{ 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) => },
{ key: "version", label: "Version" },
{
key: "actions",
label: "Actions",
render: (row) => {
const policy = row as unknown as Policy;
return (
);
},
},
]}
/>
{preview || "No policy output yet."}
>
);
}