import { useMemo, useState } from "react"; import { qk } from "../lib/queryKeys"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Settings2 } from "lucide-react"; import Modal from "./Modal"; import { useLiveQuery } from "../lib/useLiveQuery"; import { api } from "../lib/api"; import { formatBytes } from "../lib/format"; import { inputCls } from "./ui/form"; import { Button } from "./ui/Button"; import { StatusPill, jobTitle } from "./downloadRow"; // The admin "System" tab — storage overview, per-user footprint, quota editor, and the global job // list. Split out of DownloadCenter (self-contained, admin-only surface). const GiB = 1024 ** 3; function QuotaModal({ userId, email, onClose, }: { userId: number; email: string; onClose: () => void; }) { const { t } = useTranslation(); const qc = useQueryClient(); const q = useQuery({ queryKey: qk.dlQuota(userId), queryFn: () => api.adminGetDownloadQuota(userId), }); const [form, setForm] = useState<{ gb: number; jobs: number; conc: number; unlimited: boolean; } | null>(null); const cur = q.data; const state = form ?? (cur ? { gb: Math.round((cur.max_bytes / GiB) * 10) / 10, jobs: cur.max_jobs, conc: cur.max_concurrent, unlimited: cur.unlimited, } : null); const done = () => { qc.invalidateQueries({ queryKey: qk.dlQuota(userId) }); qc.invalidateQueries({ queryKey: qk.adminStorage() }); onClose(); }; const save = useMutation({ mutationFn: () => api.adminSetDownloadQuota(userId, { max_bytes: Math.round(state!.gb * GiB), max_jobs: state!.jobs, max_concurrent: state!.conc, unlimited: state!.unlimited, }), onSuccess: done, }); const reset = useMutation({ mutationFn: () => api.adminResetDownloadQuota(userId), onSuccess: done, }); if (!state) return null; const upd = (p: Partial) => setForm({ ...state, ...p }); return (
); } // Pick ANY user to set a download quota for — even one who hasn't downloaded anything yet (the // footprint list below only shows users who already have files). function QuotaUserPicker({ onPick }: { onPick: (u: { id: number; email: string }) => void }) { const { t } = useTranslation(); const [q, setQ] = useState(""); const [open, setOpen] = useState(false); const users = useQuery({ queryKey: qk.adminUsers(), queryFn: api.adminUsers }); const list = (users.data ?? []).filter((u) => !u.is_demo); const filtered = useMemo(() => { const s = q.trim().toLowerCase(); const base = s ? list.filter( (u) => u.email.toLowerCase().includes(s) || (u.display_name ?? "").toLowerCase().includes(s), ) : list; return base.slice(0, 8); }, [q, list]); return (
{ setQ(e.target.value); setOpen(true); }} onFocus={() => setOpen(true)} onBlur={() => setTimeout(() => setOpen(false), 150)} placeholder={t("downloads.admin.quotaPickPlaceholder")} className={inputCls} /> {open && filtered.length > 0 && (
{filtered.map((u) => ( ))}
)}
); } export function AdminSystem() { const { t } = useTranslation(); const storage = useLiveQuery(qk.adminStorage(), api.adminDownloadStorage, { intervalMs: 5000 }); const jobs = useLiveQuery(qk.adminDownloads(), api.adminDownloads, { intervalMs: 5000 }); const [quotaFor, setQuotaFor] = useState<{ id: number; email: string } | null>(null); const s = storage.data; return (
{s && (
{[ [t("downloads.admin.readyFiles"), String(s.ready_files)], [t("downloads.admin.totalSize"), formatBytes(s.total_bytes)], [ t("downloads.admin.cap"), s.total_cap_bytes ? formatBytes(s.total_cap_bytes) : t("downloads.admin.noCap"), ], ].map(([label, val]) => (
{label}
{val}
))}
)} {s && (
{t("downloads.admin.perUser")}
{/* Set a quota for any user, regardless of whether they've downloaded anything. */}
{s.per_user.map((u) => (
{u.email} {formatBytes(u.footprint_bytes)}
))} {s.per_user.length === 0 && (
{t("downloads.admin.noFootprint")}
)}
)}
{t("downloads.tabs.system")}
{(jobs.data ?? []).map((j) => (
{jobTitle(j)} {j.user_email} {j.asset?.size_bytes ? formatBytes(j.asset.size_bytes) : ""}
))} {jobs.data && jobs.data.length === 0 && (
{t("downloads.empty.admin")}
)}
{quotaFor && ( setQuotaFor(null)} /> )}
); }