Add setup wizard with status tracking via SystemSetting model, implement /setup/status and /setup/complete endpoints to create initial admin user and optional cluster configuration, add workload insights endpoint with traffic analysis and policy matching including audit mode detection, implement enforcement_mode property on Policy model with audit/enforced states, add Modal component for dialogs, create SetupWizard page with multi
118 lines
6.8 KiB
TypeScript
118 lines
6.8 KiB
TypeScript
import { FormEvent, useMemo, useState } from "react";
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { Plus, Shield } from "lucide-react";
|
|
|
|
import { api, Project, SecurityGroup, SecurityRule } from "../api/client";
|
|
import { DataTable } from "../components/DataTable";
|
|
import { buttonClass, Field, inputClass, selectClass } from "../components/FormControls";
|
|
import { Modal } from "../components/Modal";
|
|
import { PageHeader } from "../components/PageHeader";
|
|
|
|
export function SecurityGroups() {
|
|
const queryClient = useQueryClient();
|
|
const projects = useQuery({ queryKey: ["projects"], queryFn: () => api<Project[]>("/projects") });
|
|
const groups = useQuery({ queryKey: ["security-groups"], queryFn: () => api<SecurityGroup[]>("/security-groups") });
|
|
const [selectedGroupId, setSelectedGroupId] = useState("");
|
|
const selectedGroup = useMemo(() => selectedGroupId || groups.data?.[0]?.id || "", [groups.data, selectedGroupId]);
|
|
const rules = useQuery({
|
|
queryKey: ["security-rules", selectedGroup],
|
|
queryFn: () => api<SecurityRule[]>(`/security-groups/${selectedGroup}/rules`),
|
|
enabled: Boolean(selectedGroup),
|
|
});
|
|
const [groupForm, setGroupForm] = useState({ project_id: "", name: "Web Tier", description: "Application frontend workloads" });
|
|
const [ruleForm, setRuleForm] = useState({
|
|
direction: "ingress",
|
|
action: "allow",
|
|
protocol: "tcp",
|
|
source: "any",
|
|
destination: "sg:Web Tier",
|
|
port: "443",
|
|
priority: 1000,
|
|
logging: true,
|
|
description: "Allow HTTPS",
|
|
});
|
|
const [groupOpen, setGroupOpen] = useState(false);
|
|
const [ruleOpen, setRuleOpen] = useState(false);
|
|
|
|
const createGroup = useMutation({
|
|
mutationFn: () => api<SecurityGroup>("/security-groups", { method: "POST", body: JSON.stringify({ ...groupForm, project_id: groupForm.project_id || null }) }),
|
|
onSuccess: () => {
|
|
setGroupOpen(false);
|
|
queryClient.invalidateQueries({ queryKey: ["security-groups"] });
|
|
},
|
|
});
|
|
const createRule = useMutation({
|
|
mutationFn: () => api<SecurityRule>("/security-rules", { method: "POST", body: JSON.stringify({ ...ruleForm, security_group_id: selectedGroup }) }),
|
|
onSuccess: () => {
|
|
setRuleOpen(false);
|
|
queryClient.invalidateQueries({ queryKey: ["security-rules", selectedGroup] });
|
|
},
|
|
});
|
|
|
|
async function submitGroup(event: FormEvent) {
|
|
event.preventDefault();
|
|
const group = await createGroup.mutateAsync();
|
|
setSelectedGroupId(group.id);
|
|
}
|
|
|
|
async function submitRule(event: FormEvent) {
|
|
event.preventDefault();
|
|
await createRule.mutateAsync();
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<PageHeader title="Security Groups" subtitle="Create logical groups and attach ordered ingress or egress rules." />
|
|
<div className="space-y-4">
|
|
<div className="flex flex-wrap gap-2">
|
|
<button className={buttonClass} onClick={() => setGroupOpen(true)}><Plus size={16} /> Add Group</button>
|
|
<button className={buttonClass} onClick={() => setRuleOpen(true)} disabled={!selectedGroup}><Plus size={16} /> Add Rule</button>
|
|
</div>
|
|
<Modal title="Add Security Group" open={groupOpen} onClose={() => setGroupOpen(false)}>
|
|
<form onSubmit={submitGroup}>
|
|
<div className="mb-4 flex items-center gap-2 font-medium"><Shield size={18} /> Add Group</div>
|
|
<div className="grid gap-3">
|
|
<Field label="Project">
|
|
<select className={selectClass} value={groupForm.project_id} onChange={(event) => setGroupForm({ ...groupForm, project_id: event.target.value })}>
|
|
<option value="">Global</option>
|
|
{(projects.data ?? []).map((project) => <option key={project.id} value={project.id}>{project.name}</option>)}
|
|
</select>
|
|
</Field>
|
|
<Field label="Name"><input className={inputClass} value={groupForm.name} onChange={(event) => setGroupForm({ ...groupForm, name: event.target.value })} /></Field>
|
|
<Field label="Description"><input className={inputClass} value={groupForm.description} onChange={(event) => setGroupForm({ ...groupForm, description: event.target.value })} /></Field>
|
|
<button className={buttonClass}><Plus size={16} /> Save Group</button>
|
|
</div>
|
|
</form>
|
|
</Modal>
|
|
<Modal title="Add Security Rule" open={ruleOpen} onClose={() => setRuleOpen(false)}>
|
|
<form onSubmit={submitRule}>
|
|
<div className="mb-4 font-medium">Add Rule</div>
|
|
<div className="grid gap-3">
|
|
<Field label="Security Group">
|
|
<select className={selectClass} value={selectedGroup} onChange={(event) => setSelectedGroupId(event.target.value)}>
|
|
{(groups.data ?? []).map((group) => <option key={group.id} value={group.id}>{group.name}</option>)}
|
|
</select>
|
|
</Field>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<Field label="Direction"><select className={selectClass} value={ruleForm.direction} onChange={(event) => setRuleForm({ ...ruleForm, direction: event.target.value })}><option>ingress</option><option>egress</option></select></Field>
|
|
<Field label="Action"><select className={selectClass} value={ruleForm.action} onChange={(event) => setRuleForm({ ...ruleForm, action: event.target.value })}><option>allow</option><option>deny</option><option>reject</option></select></Field>
|
|
</div>
|
|
<Field label="Source"><input className={inputClass} value={ruleForm.source} onChange={(event) => setRuleForm({ ...ruleForm, source: event.target.value })} /></Field>
|
|
<Field label="Destination"><input className={inputClass} value={ruleForm.destination} onChange={(event) => setRuleForm({ ...ruleForm, destination: event.target.value })} /></Field>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<Field label="Protocol"><input className={inputClass} value={ruleForm.protocol} onChange={(event) => setRuleForm({ ...ruleForm, protocol: event.target.value })} /></Field>
|
|
<Field label="Port"><input className={inputClass} value={ruleForm.port} onChange={(event) => setRuleForm({ ...ruleForm, port: event.target.value })} /></Field>
|
|
</div>
|
|
<button className={buttonClass} disabled={!selectedGroup}><Plus size={16} /> Save Rule</button>
|
|
</div>
|
|
</form>
|
|
</Modal>
|
|
<section className="space-y-4">
|
|
<DataTable rows={(groups.data ?? []) as unknown as Record<string, unknown>[]} columns={[{ key: "name", label: "Group" }, { key: "description", label: "Description" }]} />
|
|
<DataTable rows={(rules.data ?? []) as unknown as Record<string, unknown>[]} columns={[{ key: "priority", label: "Priority" }, { key: "direction", label: "Direction" }, { key: "action", label: "Action" }, { key: "protocol", label: "Protocol" }, { key: "port", label: "Port" }]} />
|
|
</section>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|