Files
proxui/frontend/src/main.tsx
T

302 lines
8.3 KiB
TypeScript

import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom/client";
import { createClient, type Session } from "@supabase/supabase-js";
import type { VM } from "./api";
import { fetchMe } from "./api";
import "./styles.css";
import { AuthScreen } from "./components/AuthScreen";
import { VMList } from "./components/VMList";
import { VMCreate } from "./components/VMCreate";
import { SSHKeys } from "./components/SSHKeys";
import { AuditLog } from "./components/AuditLog";
import { ConsoleView } from "./components/ConsoleView";
type ViewKey = "overview" | "vms" | "ssh" | "audit" | "console";
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL ?? "";
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY ?? "";
const supabase =
supabaseUrl && supabaseAnonKey
? createClient(supabaseUrl, supabaseAnonKey)
: undefined;
const navItems: Array<{ key: ViewKey; label: string }> = [
{ key: "overview", label: "Uebersicht" },
{ key: "vms", label: "VMs" },
{ key: "ssh", label: "SSH-Keys" },
{ key: "audit", label: "Audit" },
{ key: "console", label: "Konsole" },
];
function App() {
const [session, setSession] = useState<Session | null>(null);
const [loadingSession, setLoadingSession] = useState(true);
const [activeView, setActiveView] = useState<ViewKey>("overview");
const [backendPrincipal, setBackendPrincipal] = useState<{
Subject: string;
Email: string;
Role: string;
} | null>(null);
const [backendStatus, setBackendStatus] = useState<
"idle" | "loading" | "ok" | "error"
>("idle");
const [backendError, setBackendError] = useState("");
useEffect(() => {
if (!supabase) {
setLoadingSession(false);
return;
}
supabase.auth.getSession().then(({ data }) => {
setSession(data.session);
setLoadingSession(false);
});
const { data } = supabase.auth.onAuthStateChange((_event, nextSession) => {
setSession(nextSession);
setBackendPrincipal(null);
setBackendStatus("idle");
setBackendError("");
});
return () => data.subscription.unsubscribe();
}, []);
useEffect(() => {
if (session) {
refreshBackendPrincipal();
}
}, [session]);
async function refreshBackendPrincipal() {
if (!session) return;
setBackendStatus("loading");
setBackendError("");
try {
const principal = await fetchMe(session.access_token);
setBackendPrincipal(principal);
setBackendStatus("ok");
} catch (error) {
setBackendStatus("error");
setBackendError(
error instanceof Error ? error.message : "Unbekannter Fehler"
);
}
}
const connected = Boolean(session && backendPrincipal);
if (loadingSession) {
return (
<main className="startup-screen">
<div className="loader" />
</main>
);
}
if (!supabase) {
return (
<main className="startup-screen">
<section className="auth-panel">
<p className="eyebrow">Konfiguration</p>
<h1>Frontend Env fehlt</h1>
<p>
`VITE_SUPABASE_URL` und `VITE_SUPABASE_ANON_KEY` mussen gesetzt
sein.
</p>
</section>
</main>
);
}
if (!session) {
return <AuthScreen supabase={supabase} />;
}
return (
<main className="app-frame">
<aside className="sidebar">
<div className="brand-mark">P</div>
<nav className="main-nav" aria-label="Hauptnavigation">
{navItems.map((item) => (
<button
className={
item.key === activeView ? "nav-item active" : "nav-item"
}
key={item.key}
onClick={() => setActiveView(item.key)}
type="button"
>
{item.label}
</button>
))}
</nav>
</aside>
<section className="workspace">
<header className="topbar">
<div>
<p className="eyebrow">ProxUI</p>
<h1>Proxmox Multi-Tenant Console</h1>
</div>
<div className="session-box">
<span className={connected ? "status-dot ok" : "status-dot"} />
<div>
<strong>{session.user.email}</strong>
<span>
{connected ? "Backend verbunden" : "Session aktiv"}
</span>
</div>
<button
className="ghost-button"
onClick={() => supabase.auth.signOut()}
>
Abmelden
</button>
</div>
</header>
<section className="content-grid">
<section className="panel backend-panel">
<div className="panel-header">
<div>
<p className="eyebrow">Backend</p>
<h2>Status</h2>
</div>
<button
className="primary-button"
disabled={backendStatus === "loading"}
onClick={refreshBackendPrincipal}
type="button"
>
Pruefen
</button>
</div>
<div className="metric-list">
<Metric label="Status" value={statusLabel(backendStatus)} />
<Metric
label="User-ID"
value={backendPrincipal?.Subject ?? "-"}
/>
<Metric
label="Rolle"
value={backendPrincipal?.Role ?? "-"}
/>
</div>
{backendError ? (
<p className="form-error">{backendError}</p>
) : null}
</section>
<ViewPanel
activeView={activeView}
connected={connected}
token={session.access_token}
/>
</section>
</section>
</main>
);
}
function ViewPanel({
activeView,
connected,
token,
}: {
activeView: ViewKey;
connected: boolean;
token: string;
}) {
const projectID = "00000000-0000-0000-0000-000000000000";
return (
<section className="panel view-panel">
{activeView === "overview" && <Overview connected={connected} />}
{activeView === "vms" && <VMView token={token} projectID={projectID} />}
{activeView === "ssh" && <SSHKeys token={token} tenantID={projectID} />}
{activeView === "audit" && <AuditLog token={token} tenantID={projectID} />}
{activeView === "console" && <ConsoleView token={token} />}
</section>
);
}
function Overview({ connected }: { connected: boolean }) {
return (
<>
<div className="panel-header">
<div>
<p className="eyebrow">Status</p>
<h2>Arbeitsbereich</h2>
</div>
<span className={connected ? "pill ok" : "pill"}>
{connected ? "Verbunden" : "Warte"}
</span>
</div>
<div className="placeholder-table">
<div className="placeholder-row">
<span>Backend</span>
<strong>{connected ? "Online" : "Offline"}</strong>
</div>
<div className="placeholder-row">
<span>RBAC</span>
<strong>Aktiv</strong>
</div>
<div className="placeholder-row">
<span>Worker</span>
<strong>Aktiv</strong>
</div>
<div className="placeholder-row">
<span>Audit</span>
<strong>Aktiv</strong>
</div>
</div>
</>
);
}
function VMView({ token, projectID }: { token: string; projectID: string }) {
const [refreshKey, setRefreshKey] = useState(0);
return (
<>
<div className="panel-header">
<div>
<p className="eyebrow">VMs</p>
<h2>Virtuelle Maschinen</h2>
</div>
</div>
<VMCreate
token={token}
projectID={projectID}
onCreated={() => setRefreshKey((k) => k + 1)}
/>
<VMList key={refreshKey} token={token} projectID={projectID} />
</>
);
}
function Metric({ label, value }: { label: string; value: string }) {
return (
<div className="metric">
<span>{label}</span>
<strong>{value}</strong>
</div>
);
}
function statusLabel(status: "idle" | "loading" | "ok" | "error") {
if (status === "loading") return "Pruefung laeuft";
if (status === "ok") return "Verbunden";
if (status === "error") return "Fehler";
return "Nicht geprueft";
}
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);