feat: add initial setup wizard, workload insights, and policy audit mode
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
This commit is contained in:
@@ -5,6 +5,7 @@ import { BriefcaseBusiness, Plus } from "lucide-react";
|
||||
import { api, Project, Tenant } 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 TenantsProjects() {
|
||||
@@ -13,14 +14,22 @@ export function TenantsProjects() {
|
||||
const projects = useQuery({ queryKey: ["projects"], queryFn: () => api<Project[]>("/projects") });
|
||||
const [tenantForm, setTenantForm] = useState({ name: "Operations", description: "Operations tenant" });
|
||||
const [projectForm, setProjectForm] = useState({ tenant_id: "", name: "Monitoring", description: "Monitoring workloads" });
|
||||
const [tenantOpen, setTenantOpen] = useState(false);
|
||||
const [projectOpen, setProjectOpen] = useState(false);
|
||||
|
||||
const createTenant = useMutation({
|
||||
mutationFn: () => api<Tenant>("/tenants", { method: "POST", body: JSON.stringify(tenantForm) }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["tenants"] }),
|
||||
onSuccess: () => {
|
||||
setTenantOpen(false);
|
||||
queryClient.invalidateQueries({ queryKey: ["tenants"] });
|
||||
},
|
||||
});
|
||||
const createProject = useMutation({
|
||||
mutationFn: () => api<Project>("/projects", { method: "POST", body: JSON.stringify({ ...projectForm, tenant_id: projectForm.tenant_id || tenants.data?.[0]?.id }) }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["projects"] }),
|
||||
onSuccess: () => {
|
||||
setProjectOpen(false);
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
},
|
||||
});
|
||||
|
||||
async function submitTenant(event: FormEvent) {
|
||||
@@ -36,16 +45,23 @@ export function TenantsProjects() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Tenants & Projects" subtitle="Define ownership boundaries for visibility, IPAM, networks, and policies." />
|
||||
<div className="grid gap-4 xl:grid-cols-[330px_330px_1fr]">
|
||||
<form onSubmit={submitTenant} className="rounded-md border border-border bg-panel p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button className={buttonClass} onClick={() => setTenantOpen(true)}><Plus size={16} /> Add Tenant</button>
|
||||
<button className={buttonClass} onClick={() => setProjectOpen(true)}><Plus size={16} /> Add Project</button>
|
||||
</div>
|
||||
<Modal title="Add Tenant" open={tenantOpen} onClose={() => setTenantOpen(false)}>
|
||||
<form onSubmit={submitTenant}>
|
||||
<div className="mb-4 flex items-center gap-2 font-medium"><BriefcaseBusiness size={18} /> Add Tenant</div>
|
||||
<div className="grid gap-3">
|
||||
<Field label="Name"><input className={inputClass} value={tenantForm.name} onChange={(event) => setTenantForm({ ...tenantForm, name: event.target.value })} /></Field>
|
||||
<Field label="Description"><input className={inputClass} value={tenantForm.description} onChange={(event) => setTenantForm({ ...tenantForm, description: event.target.value })} /></Field>
|
||||
<button className={buttonClass}><Plus size={16} /> Save Tenant</button>
|
||||
</div>
|
||||
</form>
|
||||
<form onSubmit={submitProject} className="rounded-md border border-border bg-panel p-4">
|
||||
</form>
|
||||
</Modal>
|
||||
<Modal title="Add Project" open={projectOpen} onClose={() => setProjectOpen(false)}>
|
||||
<form onSubmit={submitProject}>
|
||||
<div className="mb-4 font-medium">Add Project</div>
|
||||
<div className="grid gap-3">
|
||||
<Field label="Tenant"><select className={selectClass} value={projectForm.tenant_id} onChange={(event) => setProjectForm({ ...projectForm, tenant_id: event.target.value })}><option value="">Auto select</option>{(tenants.data ?? []).map((tenant) => <option key={tenant.id} value={tenant.id}>{tenant.name}</option>)}</select></Field>
|
||||
@@ -53,7 +69,8 @@ export function TenantsProjects() {
|
||||
<Field label="Description"><input className={inputClass} value={projectForm.description} onChange={(event) => setProjectForm({ ...projectForm, description: event.target.value })} /></Field>
|
||||
<button className={buttonClass}><Plus size={16} /> Save Project</button>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</Modal>
|
||||
<section className="space-y-4">
|
||||
<DataTable rows={(tenants.data ?? []) as unknown as Record<string, unknown>[]} columns={[{ key: "name", label: "Tenant" }, { key: "description", label: "Description" }]} />
|
||||
<DataTable rows={(projects.data ?? []) as unknown as Record<string, unknown>[]} columns={[{ key: "name", label: "Project" }, { key: "description", label: "Description" }]} />
|
||||
@@ -62,4 +79,3 @@ export function TenantsProjects() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user