import { useMemo, useState } from "react"; import { ShieldCheck, Plus, Trash2, Save } from "lucide-react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { Button } from "@/components/button"; import { managerPost, type FirewallChain } from "@/main-axios"; import { useManagerData, extractError } from "./useManagerData"; import { ManagerCardShell } from "./ManagerCardShell"; import { ManagerSearch } from "./ManagerToolbar"; import { Select2 } from "@/components/select2"; interface FirewallData { type: "iptables" | "nftables" | "none"; status: string; chains: FirewallChain[]; } export function FirewallManagerCard({ hostId }: { hostId: number | null }) { const { t } = useTranslation(); const { data, loading, error, refresh } = useManagerData( hostId, "firewall", ); const [proto, setProto] = useState<"tcp" | "udp">("tcp"); const [port, setPort] = useState(""); const [target, setTarget] = useState<"ACCEPT" | "DROP" | "REJECT">("ACCEPT"); const [busy, setBusy] = useState(false); const [query, setQuery] = useState(""); const rule = async (op: "add" | "delete") => { if (hostId == null) return; const portNum = Number(port); if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) { toast.error(t("hostMetrics.managers.invalidPort")); return; } setBusy(true); try { const res = await managerPost<{ success: boolean; output: string }>( hostId, "firewall", { op, protocol: proto, port: portNum, target }, "rule", ); if (res.success) { toast.success(t("hostMetrics.managers.ruleApplied")); refresh(); } else { toast.error(res.output || t("hostMetrics.managers.actionFailed")); } } catch (e) { toast.error(extractError(e).message); } finally { setBusy(false); } }; const persist = async () => { if (hostId == null) return; setBusy(true); toast.loading(t("hostMetrics.managers.working"), { id: "fw-persist" }); try { const res = await managerPost<{ success: boolean; output: string }>( hostId, "firewall", {}, "persist", ); toast[res.success ? "success" : "error"]( res.success ? t("hostMetrics.managers.firewallPersisted") : t("hostMetrics.managers.actionFailed"), { id: "fw-persist", description: res.output?.slice(-200) }, ); } catch (e) { toast.error(extractError(e).message, { id: "fw-persist" }); } finally { setBusy(false); } }; const chains = useMemo(() => { const q = query.toLowerCase(); return (data?.chains ?? []) .map((c) => ({ ...c, rules: c.rules.filter( (r) => !q || r.target.toLowerCase().includes(q) || (r.dport ?? "").includes(q) || r.protocol.toLowerCase().includes(q) || r.source.toLowerCase().includes(q), ), })) .filter((c) => c.rules.length > 0 || !q); }, [data?.chains, query]); return ( } loading={loading} error={error} onRefresh={refresh} headerExtra={ <> {data?.type ?? ""} } >
{t("hostMetrics.managers.addInputRule")}
setProto(e.target.value as "tcp" | "udp")} className="h-7 border border-border bg-background px-1 text-xs" > setPort(e.target.value)} placeholder={t("hostMetrics.ports.port")} className="h-7 w-16 border border-border bg-background px-1.5 text-xs outline-none focus:ring-1 focus:ring-ring" /> setTarget(e.target.value as "ACCEPT" | "DROP" | "REJECT") } className="h-7 border border-border bg-background px-1 text-xs" >
{t("hostMetrics.managers.firewallWarning")}
{chains.length === 0 ? ( {t("hostMetrics.firewall.noData")} ) : (
{chains.map((chain) => (
{chain.name} {chain.policy && ( {t("hostMetrics.firewall.policy")}: {chain.policy} )}
{chain.rules.length === 0 ? ( {t("hostMetrics.firewall.noRules")} ) : ( <>
{t("hostMetrics.firewall.action")} {t("hostMetrics.firewall.protocol")} {t("hostMetrics.firewall.port")} {t("hostMetrics.firewall.source")}
{chain.rules.map((r, i) => (
{r.target} {r.protocol.toUpperCase()} {r.dport ?? "—"} {r.source}
))} )}
))}
)}
); }