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 "./ui/Modal"; import { useLiveQuery } from "../lib/useLiveQuery"; import { api } from "../lib/api"; import { formatBytes } from "../lib/format"; import { Button } from "./ui/Button"; import { StatusPill } from "./downloadRow"; import { jobTitle } from "./downloadTitle"; import { IconButton } from "./ui/IconButton"; import { Pressable } from "./ui/Pressable"; import { Input } from "./ui/Input"; import { CheckRow } from "./ui/NativeControls"; // 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, meta: { failTitle: "errors.writeFailed.downloadQuota" }, }); const reset = useMutation({ mutationFn: () => api.adminResetDownloadQuota(userId), onSuccess: done, meta: { failTitle: "errors.writeFailed.downloadQuotaReset" }, }); if (!state) return null; const upd = (p: Partial) => setForm({ ...state, ...p }); return (
upd({ unlimited: checked })} className="text-sm" > {t("downloads.admin.unlimited")}
); } // 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")} /> {open && filtered.length > 0 && (
{filtered.map((u) => ( { onPick({ id: u.id, email: u.email }); setQ(""); setOpen(false); }} className="w-full px-3 py-2 text-sm flex flex-col" > {u.display_name || u.email} {u.display_name && {u.email}} ))}
)}
); } 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)} setQuotaFor({ id: u.user_id, email: u.email ?? "" })} label={t("downloads.admin.editQuota")} >
))} {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)} /> )}
); }