chore: initial project setup with backend, frontend, CI/CD, and documentation
Add complete NexaFabric project structure including: - FastAPI backend with SQLAlchemy models, JWT auth, RBAC, audit logging, and provider interfaces - React + TypeScript frontend with Vite, Tailwind CSS, TanStack Query, and Zustand - Docker Compose configuration for PostgreSQL, Redis, API, worker, frontend, and nginx - GitHub Actions and GitLab CI workflows for testing, linting, building, and security scanning - Environment
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { AlertTriangle, Boxes, Network, Server, ShieldAlert } from "lucide-react";
|
||||
|
||||
import { api, Dashboard as DashboardData } from "../api/client";
|
||||
import { PageHeader } from "../components/PageHeader";
|
||||
|
||||
const cards = [
|
||||
["clusters", "Clusters", Server],
|
||||
["nodes", "Nodes", Boxes],
|
||||
["workloads", "VMs/LXCs", Network],
|
||||
["networks", "Networks", Network],
|
||||
["open_policy_violations", "Policy Violations", ShieldAlert],
|
||||
] as const;
|
||||
|
||||
export function Dashboard() {
|
||||
const { data } = useQuery({ queryKey: ["dashboard"], queryFn: () => api<DashboardData>("/dashboard") });
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Dashboard" subtitle="Operational overview across clusters, networks, policies, and sync health." />
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-5">
|
||||
{cards.map(([key, label, Icon]) => (
|
||||
<div key={key} className="rounded-md border border-border bg-panel p-4">
|
||||
<div className="mb-3 flex items-center justify-between text-slate-500">
|
||||
<span className="text-sm">{label}</span>
|
||||
<Icon size={18} />
|
||||
</div>
|
||||
<div className="text-3xl font-semibold">{data?.[key] ?? 0}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 grid gap-4 lg:grid-cols-2">
|
||||
<section className="rounded-md border border-border bg-panel p-4">
|
||||
<div className="mb-3 flex items-center gap-2 font-medium">
|
||||
<AlertTriangle size={18} />
|
||||
Faulty Nodes
|
||||
</div>
|
||||
{(data?.faulty_nodes ?? []).map((node) => (
|
||||
<div key={node.id} className="flex justify-between border-t border-border py-3 text-sm">
|
||||
<span>{node.name}</span>
|
||||
<span className="text-danger">{node.status}</span>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
<section className="rounded-md border border-border bg-panel p-4">
|
||||
<div className="mb-3 font-medium">Top Talkers</div>
|
||||
{(data?.top_talkers ?? []).map((item) => (
|
||||
<div key={item.name} className="flex justify-between border-t border-border py-3 text-sm">
|
||||
<span>{item.name}</span>
|
||||
<span>{Math.round(item.bytes / 1_000_000)} MB</span>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Play } from "lucide-react";
|
||||
|
||||
import { api, Policy } from "../api/client";
|
||||
import { PageHeader } from "../components/PageHeader";
|
||||
|
||||
export function FirewallPreview() {
|
||||
const policies = useQuery({ queryKey: ["policies"], queryFn: () => api<Policy[]>("/policies") });
|
||||
const preview = useMutation({
|
||||
mutationFn: (policyId: string) => api<Record<string, unknown>>(`/firewall/preview/${policyId}`, { method: "POST" }),
|
||||
});
|
||||
const firstPolicy = policies.data?.[0];
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Firewall Preview" subtitle="Compile policies into provider-specific rules before any apply operation." />
|
||||
<div className="rounded-md border border-border bg-panel p-4">
|
||||
<button
|
||||
className="inline-flex h-10 items-center gap-2 rounded-md bg-accent px-4 text-sm text-white disabled:opacity-50"
|
||||
disabled={!firstPolicy}
|
||||
onClick={() => firstPolicy && preview.mutate(firstPolicy.id)}
|
||||
>
|
||||
<Play size={18} />
|
||||
Generate Preview
|
||||
</button>
|
||||
<pre className="mt-4 max-h-[520px] overflow-auto rounded-md border border-border bg-canvas p-4 text-xs">
|
||||
{preview.data ? JSON.stringify(preview.data, null, 2) : "No preview generated yet."}
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { api } from "../api/client";
|
||||
import { DataTable } from "../components/DataTable";
|
||||
import { PageHeader } from "../components/PageHeader";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
path: string;
|
||||
columns: Array<{ key: string; label: string }>;
|
||||
};
|
||||
|
||||
export function ListPage({ title, subtitle, path, columns }: Props) {
|
||||
const { data, isLoading, error } = useQuery({ queryKey: [path], queryFn: () => api<Record<string, unknown>[] | Record<string, unknown>>(path) });
|
||||
const rows = Array.isArray(data) ? data : data ? [data] : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={title} subtitle={subtitle} />
|
||||
{isLoading ? <div className="rounded-md border border-border bg-panel p-8 text-sm">Loading...</div> : null}
|
||||
{error ? <div className="rounded-md border border-danger p-4 text-sm text-danger">Failed to load data.</div> : null}
|
||||
{!isLoading && !error ? <DataTable columns={columns as never} rows={rows} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { FormEvent, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ShieldCheck } from "lucide-react";
|
||||
|
||||
import { login } from "../api/client";
|
||||
|
||||
export function Login() {
|
||||
const navigate = useNavigate();
|
||||
const [email, setEmail] = useState("admin@nexafabric.local");
|
||||
const [password, setPassword] = useState("ChangeMe_UseEnvInstead");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
async function submit(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
setError("");
|
||||
try {
|
||||
await login(email, password);
|
||||
navigate("/");
|
||||
} catch {
|
||||
setError("Login failed.");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid min-h-screen place-items-center bg-canvas px-4">
|
||||
<form onSubmit={submit} className="w-full max-w-sm rounded-md border border-border bg-panel p-6 shadow-sm">
|
||||
<div className="mb-6 flex items-center gap-3">
|
||||
<div className="rounded-md bg-accent p-2 text-white">
|
||||
<ShieldCheck size={22} />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">NexaFabric</h1>
|
||||
<p className="text-sm text-slate-500">Sign in to the control plane</p>
|
||||
</div>
|
||||
</div>
|
||||
<label className="mb-4 block text-sm">
|
||||
Email
|
||||
<input className="mt-1 w-full rounded-md border border-border bg-transparent px-3 py-2" value={email} onChange={(event) => setEmail(event.target.value)} />
|
||||
</label>
|
||||
<label className="mb-4 block text-sm">
|
||||
Password
|
||||
<input className="mt-1 w-full rounded-md border border-border bg-transparent px-3 py-2" type="password" value={password} onChange={(event) => setPassword(event.target.value)} />
|
||||
</label>
|
||||
{error ? <div className="mb-4 rounded-md border border-danger px-3 py-2 text-sm text-danger">{error}</div> : null}
|
||||
<button className="h-10 w-full rounded-md bg-accent text-sm font-medium text-white">Sign In</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Save, Wand2 } from "lucide-react";
|
||||
|
||||
import { PageHeader } from "../components/PageHeader";
|
||||
|
||||
export function PolicyDesigner() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Policy Designer" subtitle="Build tenant-aware microsegmentation policies and inspect their intended impact." />
|
||||
<div className="grid gap-4 lg:grid-cols-[1fr_360px]">
|
||||
<section className="rounded-md border border-border bg-panel p-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{["Source", "Destination", "Service", "Action", "Direction", "Logging"].map((label) => (
|
||||
<label key={label} className="text-sm">
|
||||
{label}
|
||||
<select className="mt-1 h-10 w-full rounded-md border border-border bg-transparent px-3">
|
||||
<option>{label === "Action" ? "allow" : label === "Direction" ? "ingress" : "Any"}</option>
|
||||
</select>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<label className="mt-4 block text-sm">
|
||||
Description
|
||||
<input className="mt-1 h-10 w-full rounded-md border border-border bg-transparent px-3" placeholder="Policy intent" />
|
||||
</label>
|
||||
<div className="mt-5 flex gap-3">
|
||||
<button className="inline-flex h-10 items-center gap-2 rounded-md border border-border px-4 text-sm">
|
||||
<Wand2 size={18} />
|
||||
Dry Run
|
||||
</button>
|
||||
<button className="inline-flex h-10 items-center gap-2 rounded-md bg-accent px-4 text-sm text-white">
|
||||
<Save size={18} />
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<aside className="rounded-md border border-border bg-panel p-4">
|
||||
<div className="mb-3 font-medium">Impact Preview</div>
|
||||
<div className="space-y-3 text-sm text-slate-600 dark:text-slate-300">
|
||||
<div className="rounded-md border border-border p-3">Affected VMs: calculated after dry run</div>
|
||||
<div className="rounded-md border border-border p-3">Conflicts: none detected in draft</div>
|
||||
<div className="rounded-md border border-border p-3">Generated rules: preview required before apply</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user