feat(downloads): let admins set a quota for any user, not only downloaders
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.
This commit is contained in:
parent
1e557db12e
commit
b7f365dd5a
4 changed files with 69 additions and 6 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
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 (
|
||||||
|
<div className="relative w-full sm:w-72">
|
||||||
|
<input
|
||||||
|
value={q}
|
||||||
|
onChange={(e) => {
|
||||||
|
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 && (
|
||||||
|
<div className="absolute z-10 mt-1 w-full rounded-lg border border-border bg-card shadow-xl overflow-hidden">
|
||||||
|
{filtered.map((u) => (
|
||||||
|
<button
|
||||||
|
key={u.id}
|
||||||
|
onClick={() => {
|
||||||
|
onPick({ id: u.id, email: u.email });
|
||||||
|
setQ("");
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
className="w-full text-left px-3 py-2 text-sm hover:bg-surface transition flex flex-col"
|
||||||
|
>
|
||||||
|
<span className="truncate">{u.display_name || u.email}</span>
|
||||||
|
{u.display_name && <span className="text-xs text-muted truncate">{u.email}</span>}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function AdminSystem() {
|
function AdminSystem() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const storage = useLiveQuery(["admin-storage"], api.adminDownloadStorage, { intervalMs: 5000 });
|
const storage = useLiveQuery(["admin-storage"], api.adminDownloadStorage, { intervalMs: 5000 });
|
||||||
|
|
@ -298,9 +348,13 @@ function AdminSystem() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{s && s.per_user.length > 0 && (
|
{s && (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="text-sm font-medium mb-2">{t("downloads.admin.perUser")}</div>
|
<div className="flex items-center justify-between gap-3 mb-2 flex-wrap">
|
||||||
|
<div className="text-sm font-medium">{t("downloads.admin.perUser")}</div>
|
||||||
|
{/* Set a quota for any user, regardless of whether they've downloaded anything. */}
|
||||||
|
<QuotaUserPicker onPick={setQuotaFor} />
|
||||||
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{s.per_user.map((u) => (
|
{s.per_user.map((u) => (
|
||||||
<div key={u.user_id} className="flex items-center gap-2 text-sm px-3 py-1.5 rounded-lg bg-card/40">
|
<div key={u.user_id} className="flex items-center gap-2 text-sm px-3 py-1.5 rounded-lg bg-card/40">
|
||||||
|
|
@ -315,6 +369,9 @@ function AdminSystem() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{s.per_user.length === 0 && (
|
||||||
|
<div className="text-xs text-muted px-3 py-2">{t("downloads.admin.noFootprint")}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,9 @@
|
||||||
"reset": "Auf Standard zurücksetzen",
|
"reset": "Auf Standard zurücksetzen",
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"custom": "individuell",
|
"custom": "individuell",
|
||||||
"default": "Standard"
|
"default": "Standard",
|
||||||
|
"quotaPickPlaceholder": "Kontingent für einen Nutzer festlegen…",
|
||||||
|
"noFootprint": "Noch keine Downloads."
|
||||||
},
|
},
|
||||||
"cols": {
|
"cols": {
|
||||||
"title": "Titel",
|
"title": "Titel",
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,9 @@
|
||||||
"reset": "Reset to default",
|
"reset": "Reset to default",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"custom": "custom",
|
"custom": "custom",
|
||||||
"default": "default"
|
"default": "default",
|
||||||
|
"quotaPickPlaceholder": "Set a user's quota…",
|
||||||
|
"noFootprint": "No downloads yet."
|
||||||
},
|
},
|
||||||
"cols": {
|
"cols": {
|
||||||
"title": "Title",
|
"title": "Title",
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,9 @@
|
||||||
"reset": "Visszaállítás alapértékre",
|
"reset": "Visszaállítás alapértékre",
|
||||||
"save": "Mentés",
|
"save": "Mentés",
|
||||||
"custom": "egyedi",
|
"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": {
|
"cols": {
|
||||||
"title": "Cím",
|
"title": "Cím",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue