139 lines
4.5 KiB
React
139 lines
4.5 KiB
React
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>
|
|
);
|
|
}
|