From b7f365dd5a8583f41cbdf2f2e4536b5d846a13a4 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 4 Jul 2026 05:07:23 +0200 Subject: [PATCH] feat(downloads): let admins set a quota for any user, not only downloaders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The admin System tab only listed users with an existing footprint, so a quota could be set only after someone downloaded something. Add a 'Set a user's quota…' picker (over /api/admin/users, excl. demo) beside the Per-user footprint heading that opens the quota editor for ANY user — the quota GET/PUT endpoints already resolve defaults + create a row on demand, so no backend change. Also show an empty-state line when no one has a footprint yet. i18n en/hu/de. --- frontend/src/components/DownloadCenter.tsx | 63 ++++++++++++++++++++- frontend/src/i18n/locales/de/downloads.json | 4 +- frontend/src/i18n/locales/en/downloads.json | 4 +- frontend/src/i18n/locales/hu/downloads.json | 4 +- 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/DownloadCenter.tsx b/frontend/src/components/DownloadCenter.tsx index d191c5d..074fff0 100644 --- a/frontend/src/components/DownloadCenter.tsx +++ b/frontend/src/components/DownloadCenter.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { @@ -275,6 +275,56 @@ function QuotaModal({ userId, email, onClose }: { userId: number; email: string; ); } +// 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: ["admin-users"], 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) => ( + + ))} +
+ )} +
+ ); +} + function AdminSystem() { const { t } = useTranslation(); const storage = useLiveQuery(["admin-storage"], api.adminDownloadStorage, { intervalMs: 5000 }); @@ -298,9 +348,13 @@ function AdminSystem() { )} - {s && s.per_user.length > 0 && ( + {s && (
-
{t("downloads.admin.perUser")}
+
+
{t("downloads.admin.perUser")}
+ {/* Set a quota for any user, regardless of whether they've downloaded anything. */} + +
{s.per_user.map((u) => (
@@ -315,6 +369,9 @@ function AdminSystem() {
))} + {s.per_user.length === 0 && ( +
{t("downloads.admin.noFootprint")}
+ )}
)} diff --git a/frontend/src/i18n/locales/de/downloads.json b/frontend/src/i18n/locales/de/downloads.json index 6cb36a9..13cc467 100644 --- a/frontend/src/i18n/locales/de/downloads.json +++ b/frontend/src/i18n/locales/de/downloads.json @@ -157,7 +157,9 @@ "reset": "Auf Standard zurücksetzen", "save": "Speichern", "custom": "individuell", - "default": "Standard" + "default": "Standard", + "quotaPickPlaceholder": "Kontingent für einen Nutzer festlegen…", + "noFootprint": "Noch keine Downloads." }, "cols": { "title": "Titel", diff --git a/frontend/src/i18n/locales/en/downloads.json b/frontend/src/i18n/locales/en/downloads.json index c46fd2d..f96cd82 100644 --- a/frontend/src/i18n/locales/en/downloads.json +++ b/frontend/src/i18n/locales/en/downloads.json @@ -157,7 +157,9 @@ "reset": "Reset to default", "save": "Save", "custom": "custom", - "default": "default" + "default": "default", + "quotaPickPlaceholder": "Set a user's quota…", + "noFootprint": "No downloads yet." }, "cols": { "title": "Title", diff --git a/frontend/src/i18n/locales/hu/downloads.json b/frontend/src/i18n/locales/hu/downloads.json index 7362d83..1a097ea 100644 --- a/frontend/src/i18n/locales/hu/downloads.json +++ b/frontend/src/i18n/locales/hu/downloads.json @@ -157,7 +157,9 @@ "reset": "Visszaállítás alapértékre", "save": "Mentés", "custom": "egyedi", - "default": "alapértelmezett" + "default": "alapértelmezett", + "quotaPickPlaceholder": "Kvóta beállítása egy felhasználónak…", + "noFootprint": "Még nincs letöltés." }, "cols": { "title": "Cím",