Files
PacketStation/frontend/src/screens/admin/StorageTab.tsx
T

145 lines
5.5 KiB
TypeScript
Raw Normal View History

// Admin: the assets volume, the virus scanner, the archive folder (phase 10) and the open alerts (phase 4).
import { useState } from "react";
import { storageApi } from "../../api/storage";
import type { AlertRow } from "../../api/types";
import { errorText, fmtBytes, fmtTime } from "../../lib/format";
import { useLoad } from "../../lib/useLoad";
import { FileLocations } from "./FileLocations";
const whoName = (a: AlertRow["acknowledged_by"]) => (a && typeof a === "object" ? a.display_name : (a ?? ""));
function alertText(a: AlertRow): string {
if (a.kind === "assets_quota") {
const pct = Number((a.detail as { pct?: unknown }).pct);
return Number.isFinite(pct) ? `The assets volume is ${Math.round(pct)}% full.` : "The assets volume is nearly full.";
}
return a.subject ? `${a.kind.replace(/_/g, " ")}: ${a.subject}` : a.kind.replace(/_/g, " ");
}
export function StorageTab() {
const [load, reload] = useLoad((s) => storageApi.storage(s));
const [alerts, reloadAlerts] = useLoad((s) => storageApi.alerts(s));
const [busy, setBusy] = useState(false);
const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null);
const ack = async (id: number) => {
setBusy(true);
setMsg(null);
try {
await storageApi.ack(id);
setMsg({ ok: true, text: "Alert acknowledged." });
reload();
reloadAlerts();
} catch (err) {
setMsg({ ok: false, text: errorText(err) });
} finally {
setBusy(false);
}
};
if (load.state === "loading") return <div className="empty slim">Loading</div>;
if (load.state === "error") return <div className="callout warn">{load.message}</div>;
const s = load.data;
const u = s.usage;
const pct = Math.max(0, Math.min(100, Number(u.pct) || 0));
const scan = s.clamav;
return (
<div className="stack">
{msg ? (
<div className={msg.ok ? "callout flush" : "callout warn flush"} role={msg.ok ? "status" : "alert"}>
{msg.text}
</div>
) : null}
<section className="panel stack" aria-labelledby="stor-h">
<h3 id="stor-h">Assets volume</h3>
<div>
Used <b>{fmtBytes(u.used)}</b> of <b>{fmtBytes(u.total)}</b> ({Math.round(pct)}%). <span className="muted">{fmtBytes(u.free)} free.</span>
</div>
<div
className="bar usage"
role="meter"
aria-label="Assets volume used"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(pct)}
>
<i className={u.warn ? "over" : undefined} style={{ width: `${pct}%` }} />
<span className="mark" style={{ left: `${s.quota_warn_pct}%` }} aria-hidden="true" />
</div>
<div className="small muted">Warning at {s.quota_warn_pct}%.</div>
{s.alert ? (
<div className="callout warn flush" role="status">
<b>{alertText(s.alert)}</b> Raised {fmtTime(s.alert.created_at)}.
{s.alert.acknowledged_at ? (
<> Acknowledged {fmtTime(s.alert.acknowledged_at)}{whoName(s.alert.acknowledged_by) ? ` by ${whoName(s.alert.acknowledged_by)}` : ""}.</>
) : (
<>
{" "}
<button className="btn sm" type="button" disabled={busy} onClick={() => void ack(s.alert?.id ?? 0)}>
Acknowledge
</button>
</>
)}
</div>
) : null}
</section>
<section className="panel" aria-label="Virus scanner">
<b>Virus scanner (ClamAV): </b>
{!scan.configured ? (
<span className="muted">Not configured</span>
) : (
<span className={scan.reachable ? "st-ok" : "st-bad"}>
{scan.host ?? "clamav"}: {scan.reachable ? "reachable" : "not reachable"}
</span>
)}
</section>
<FileLocations />
<section className="sec" aria-labelledby="alerts-h">
<div className="sec-h">
<h3 id="alerts-h">Open alerts</h3>
</div>
{alerts.state === "loading" ? <div className="empty slim">Loading</div> : null}
{alerts.state === "error" ? <div className="callout warn">{alerts.message}</div> : null}
{alerts.state === "ok" && !alerts.data.length ? <div className="empty slim">No open alerts.</div> : null}
{alerts.state === "ok" && alerts.data.length ? (
<div className="tw">
<table aria-label="Open alerts">
<thead>
<tr>
<th>Raised</th>
<th>Severity</th>
<th>What</th>
<th>Acknowledged</th>
<th />
</tr>
</thead>
<tbody>
{alerts.data.map((a) => (
<tr key={a.id}>
<td className="small mono nowrap">{fmtTime(a.created_at)}</td>
<td className="small">{a.severity}</td>
<td>{alertText(a)}</td>
<td className="small">{a.acknowledged_at ? `${fmtTime(a.acknowledged_at)} ${whoName(a.acknowledged_by)}` : "-"}</td>
<td>
{a.acknowledged_at ? null : (
<button className="btn sm" type="button" disabled={busy} onClick={() => void ack(a.id)}>
Acknowledge
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
) : null}
</section>
</div>
);
}