Files
proxmox_list/prox-visualizer/frontend/src/views/Dashboard.jsx
T
2026-06-07 09:30:11 +02:00

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>
);
}