Build Proxmox visualizer app
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
import { AlertCircle, RefreshCw, Search } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import DashboardTable from "../components/DashboardTable.jsx";
|
||||
import ServerStatus from "../components/ServerStatus.jsx";
|
||||
import { useFetchVms } from "../hooks/useFetchVms.js";
|
||||
|
||||
function buildDuplicateIndex(duplicates) {
|
||||
const index = new Map();
|
||||
for (const group of duplicates) {
|
||||
for (const item of group.items ?? []) {
|
||||
index.set(item.id, { reason: group.reason, count: group.count });
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const { data, loading, error, lastRefresh, refresh } = useFetchVms();
|
||||
const [query, setQuery] = useState("");
|
||||
const [mode, setMode] = useState("all");
|
||||
|
||||
const duplicateIndex = useMemo(
|
||||
() => buildDuplicateIndex(data.duplicates),
|
||||
[data.duplicates],
|
||||
);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
return data.items.filter((item) => {
|
||||
const matchesMode = mode === "all" || duplicateIndex.has(item.id);
|
||||
const matchesQuery =
|
||||
!normalizedQuery ||
|
||||
[item.name, item.server, item.node, item.vmid, item.type, item.status]
|
||||
.filter(Boolean)
|
||||
.some((value) => String(value).toLowerCase().includes(normalizedQuery));
|
||||
return matchesMode && matchesQuery;
|
||||
});
|
||||
}, [data.items, duplicateIndex, mode, query]);
|
||||
|
||||
const onlineServers = data.servers.filter((server) => server.ok).length;
|
||||
const duplicateItems = duplicateIndex.size;
|
||||
|
||||
return (
|
||||
<main className="app-shell">
|
||||
<header className="topbar">
|
||||
<div>
|
||||
<p className="eyebrow">Proxmox</p>
|
||||
<h1>Prox Visualizer</h1>
|
||||
</div>
|
||||
<button className="icon-button primary" onClick={refresh} disabled={loading} title="Rafraichir">
|
||||
<RefreshCw size={18} aria-hidden="true" className={loading ? "spin" : ""} />
|
||||
<span>Rafraichir</span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section className="metrics-grid">
|
||||
<div className="metric-card">
|
||||
<span>Machines</span>
|
||||
<strong>{data.items.length}</strong>
|
||||
</div>
|
||||
<div className="metric-card">
|
||||
<span>Doublons</span>
|
||||
<strong>{data.duplicates.length}</strong>
|
||||
</div>
|
||||
<div className="metric-card">
|
||||
<span>Elements lies</span>
|
||||
<strong>{duplicateItems}</strong>
|
||||
</div>
|
||||
<div className="metric-card">
|
||||
<span>Serveurs OK</span>
|
||||
<strong>{onlineServers}/{data.servers.length}</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="toolbar">
|
||||
<div className="search-box">
|
||||
<Search size={18} aria-hidden="true" />
|
||||
<input
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Rechercher"
|
||||
/>
|
||||
</div>
|
||||
<div className="segmented">
|
||||
<button className={mode === "all" ? "active" : ""} onClick={() => setMode("all")}>
|
||||
Tout
|
||||
</button>
|
||||
<button
|
||||
className={mode === "duplicates" ? "active" : ""}
|
||||
onClick={() => setMode("duplicates")}
|
||||
>
|
||||
Doublons
|
||||
</button>
|
||||
</div>
|
||||
{lastRefresh && (
|
||||
<span className="refresh-time">
|
||||
{lastRefresh.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })}
|
||||
</span>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{error && (
|
||||
<div className="notice error">
|
||||
<AlertCircle size={18} aria-hidden="true" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="content-grid">
|
||||
<ServerStatus servers={data.servers} />
|
||||
<section className="panel">
|
||||
<div className="panel-header">
|
||||
<h2>Groupes</h2>
|
||||
<span>{data.duplicates.length}</span>
|
||||
</div>
|
||||
<div className="duplicate-list">
|
||||
{data.duplicates.length === 0 ? (
|
||||
<p className="empty-state">Aucun doublon</p>
|
||||
) : (
|
||||
data.duplicates.map((group) => (
|
||||
<div className="duplicate-row" key={group.id}>
|
||||
<div>
|
||||
<strong>{group.label}</strong>
|
||||
<span>{group.reason === "metadata" ? "Metadata" : "Nom identique"}</span>
|
||||
</div>
|
||||
<span className="metric-value small">{group.count}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<DashboardTable items={filteredItems} duplicateIndex={duplicateIndex} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user