feat(web): add storage dashboard and team retention settings

Add per-team storage breakdown table to the usage section (from
GET /api/v1/admin/usage teamStorage data). Add storage quota (MB)
and retention (hours) inline fields to the teams section, saving
via PUT /api/v1/teams/:id. Add i18n keys to all 21 locale files.
This commit is contained in:
SnapOtter
2026-06-13 21:20:12 +08:00
parent dd2a50799a
commit c7e9b1ddc6
23 changed files with 372 additions and 1 deletions
@@ -338,6 +338,8 @@ interface TeamEntry {
id: string;
name: string;
memberCount: number;
storageQuota: number | null;
retentionHours: number | null;
createdAt: string;
}
@@ -1916,6 +1918,10 @@ function TeamsSection() {
const [editingTeamId, setEditingTeamId] = useState<string | null>(null);
const [editingTeamName, setEditingTeamName] = useState("");
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
const [expandedTeamId, setExpandedTeamId] = useState<string | null>(null);
const [quotaMb, setQuotaMb] = useState("");
const [retention, setRetention] = useState("");
const [savingQuota, setSavingQuota] = useState(false);
const [actionMsg, setActionMsg] = useState<{ type: "success" | "error"; text: string } | null>(
null,
);
@@ -2006,6 +2012,45 @@ function TeamsSection() {
[loadTeams],
);
const handleExpandTeam = useCallback(
(tm: TeamEntry) => {
if (expandedTeamId === tm.id) {
setExpandedTeamId(null);
return;
}
setExpandedTeamId(tm.id);
setQuotaMb(
tm.storageQuota ? String(Math.round(tm.storageQuota / (1024 * 1024))) : "",
);
setRetention(tm.retentionHours ? String(tm.retentionHours) : "");
},
[expandedTeamId],
);
const handleSaveQuota = useCallback(
async (id: string) => {
setSavingQuota(true);
try {
const body: Record<string, number | null> = {};
const mbVal = quotaMb.trim() ? Number(quotaMb) : 0;
body.storageQuota = mbVal > 0 ? mbVal * 1024 * 1024 : null;
const retVal = retention.trim() ? Number(retention) : 0;
body.retentionHours = retVal > 0 ? retVal : null;
await apiPut(`/v1/teams/${id}`, body);
setActionMsg({ type: "success", text: t.settings.teams.quotaSaved });
setExpandedTeamId(null);
await loadTeams();
} catch (err) {
const msg = err instanceof Error ? err.message : "Failed to save";
setActionMsg({ type: "error", text: msg });
} finally {
setSavingQuota(false);
setTimeout(() => setActionMsg(null), 3000);
}
},
[quotaMb, retention, loadTeams],
);
if (loading) {
return (
<div className="flex items-center justify-center py-12">
@@ -2095,8 +2140,8 @@ function TeamsSection() {
</div>
) : (
teams.map((tm) => (
<Fragment key={tm.id}>
<div
key={tm.id}
className={cn(
"items-center px-4 py-3 border-b border-border last:border-0 last:rounded-b-lg hover:bg-muted/20 transition-colors",
isMobile ? "flex gap-3" : "grid grid-cols-[1fr_100px_60px] gap-2",
@@ -2173,6 +2218,17 @@ function TeamsSection() {
<Pencil className="h-3.5 w-3.5" />
{t.settings.teams.renameAction}
</button>
<button
type="button"
onClick={() => {
handleExpandTeam(tm);
setOpenMenuId(null);
}}
className="flex items-center gap-2 w-full px-3 py-2 text-sm text-foreground hover:bg-muted transition-colors"
>
<Settings className="h-3.5 w-3.5" />
{t.settings.heading}
</button>
<div className="border-t border-border my-1" />
<button
type="button"
@@ -2186,6 +2242,73 @@ function TeamsSection() {
)}
</div>
</div>
{expandedTeamId === tm.id && (
<div className="px-4 py-3 border-b border-border last:border-0 bg-muted/10 space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1">
<label
htmlFor={`quota-${tm.id}`}
className="text-xs font-medium text-muted-foreground"
>
{t.settings.teams.teamStorageQuota}
</label>
<input
id={`quota-${tm.id}`}
type="number"
min="0"
value={quotaMb}
onChange={(e) => setQuotaMb(e.target.value)}
placeholder="0"
className="w-full px-3 py-1.5 rounded-lg border border-border bg-background text-sm text-foreground"
/>
<p className="text-[11px] text-muted-foreground">
{t.settings.teams.teamStorageQuotaDesc}
</p>
</div>
<div className="space-y-1">
<label
htmlFor={`retention-${tm.id}`}
className="text-xs font-medium text-muted-foreground"
>
{t.settings.teams.teamRetentionHours}
</label>
<input
id={`retention-${tm.id}`}
type="number"
min="0"
value={retention}
onChange={(e) => setRetention(e.target.value)}
placeholder="0"
className="w-full px-3 py-1.5 rounded-lg border border-border bg-background text-sm text-foreground"
/>
<p className="text-[11px] text-muted-foreground">
{t.settings.teams.teamRetentionHoursDesc}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
disabled={savingQuota}
onClick={() => handleSaveQuota(tm.id)}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-primary text-primary-foreground text-xs font-medium hover:bg-primary/90 transition-colors disabled:opacity-50"
>
{savingQuota && (
<Loader2 className="h-3 w-3 animate-spin" aria-hidden="true" />
)}
{t.common.save}
</button>
<button
type="button"
onClick={() => setExpandedTeamId(null)}
className="px-3 py-1.5 rounded-lg border border-border text-xs text-muted-foreground hover:bg-muted transition-colors"
>
{t.common.cancel}
</button>
</div>
</div>
)}
</Fragment>
))
)}
</div>
@@ -12,6 +12,7 @@ interface UsageData {
perUser: Array<{ username: string | null; runs: number; bytesIn: string }>;
durations: Array<{ pool: string; p50Ms: number | null; p95Ms: number | null }>;
storage: { libraryBytes: string; libraryFiles: number };
teamStorage?: Array<{ teamName: string; totalBytes: string; userCount: number }>;
}
export function UsageSection() {
@@ -260,6 +261,43 @@ export function UsageSection() {
</div>
</div>
</div>
{/* Storage by Team */}
{data.teamStorage && data.teamStorage.length > 0 && (
<div className="rounded-lg border border-border p-4 space-y-3">
<h4 className="text-sm font-semibold text-foreground">
{t.settings.usage.storageByTeam}
</h4>
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-start py-1.5 text-xs font-medium text-muted-foreground">
{t.settings.usage.teamColumn}
</th>
<th className="text-end py-1.5 text-xs font-medium text-muted-foreground">
{t.settings.usage.storageColumn}
</th>
<th className="text-end py-1.5 text-xs font-medium text-muted-foreground">
{t.settings.usage.usersColumn}
</th>
</tr>
</thead>
<tbody>
{data.teamStorage.map((row) => (
<tr key={row.teamName} className="border-b border-border last:border-0">
<td className="py-1.5 text-foreground">{row.teamName}</td>
<td className="py-1.5 text-end text-muted-foreground font-mono">
{formatFileSize(Number(row.totalBytes))}
</td>
<td className="py-1.5 text-end text-muted-foreground font-mono">
{row.userCount}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
) : null}
</div>
+10
View File
@@ -2791,6 +2791,12 @@ export const ar: TranslationKeys = {
nameRequired: "اسم الفريق مطلوب",
nameTooLong: "يجب ألا يتجاوز اسم الفريق 50 حرفًا",
duplicateName: "يوجد فريق بهذا الاسم بالفعل",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "الأدوار",
@@ -2872,6 +2878,10 @@ export const ar: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "مفاتيح API",
+10
View File
@@ -2810,6 +2810,12 @@ export const de: TranslationKeys = {
nameRequired: "Teamname ist erforderlich",
nameTooLong: "Teamname darf maximal 50 Zeichen lang sein",
duplicateName: "Ein Team mit diesem Namen existiert bereits",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "Rollen",
@@ -2892,6 +2898,10 @@ export const de: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "API-Schluessel",
+10
View File
@@ -2756,6 +2756,12 @@ export const en = {
nameRequired: "Team name is required",
nameTooLong: "Team name must be 50 characters or less",
duplicateName: "A team with this name already exists",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "Roles",
@@ -2837,6 +2843,10 @@ export const en = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "API Keys",
+10
View File
@@ -2790,6 +2790,12 @@ export const es: TranslationKeys = {
nameRequired: "El nombre del equipo es obligatorio",
nameTooLong: "El nombre del equipo debe tener 50 caracteres o menos",
duplicateName: "Ya existe un equipo con este nombre",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "Roles",
@@ -2872,6 +2878,10 @@ export const es: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "Claves API",
+10
View File
@@ -2810,6 +2810,12 @@ export const fr: TranslationKeys = {
nameRequired: "Le nom de l'equipe est requis",
nameTooLong: "Le nom de l'equipe doit contenir 50 caracteres ou moins",
duplicateName: "Une equipe avec ce nom existe deja",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "Roles",
@@ -2892,6 +2898,10 @@ export const fr: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "Cles API",
+10
View File
@@ -2787,6 +2787,12 @@ export const hi: TranslationKeys = {
nameRequired: "टीम का नाम आवश्यक है",
nameTooLong: "टीम का नाम 50 अक्षरों से कम होना चाहिए",
duplicateName: "इस नाम की टीम पहले से मौजूद है",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "भूमिकाएं",
@@ -2868,6 +2874,10 @@ export const hi: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "API कुंजियां",
+10
View File
@@ -2800,6 +2800,12 @@ export const id: TranslationKeys = {
nameRequired: "Nama tim wajib diisi",
nameTooLong: "Nama tim maksimal 50 karakter",
duplicateName: "Tim dengan nama ini sudah ada",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "Peran",
@@ -2881,6 +2887,10 @@ export const id: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "Kunci API",
+10
View File
@@ -2803,6 +2803,12 @@ export const it: TranslationKeys = {
nameRequired: "Il nome del team e obbligatorio",
nameTooLong: "Il nome del team deve avere al massimo 50 caratteri",
duplicateName: "Un team con questo nome esiste gia",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "Ruoli",
@@ -2885,6 +2891,10 @@ export const it: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "Chiavi API",
+10
View File
@@ -2758,6 +2758,12 @@ export const ja: TranslationKeys = {
nameRequired: "チーム名は必須です",
nameTooLong: "チーム名は50文字以内にしてください",
duplicateName: "同じ名前のチームが既にあります",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "ロール",
@@ -2839,6 +2845,10 @@ export const ja: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "APIキー",
+10
View File
@@ -2742,6 +2742,12 @@ export const ko: TranslationKeys = {
nameRequired: "팀 이름은 필수입니다",
nameTooLong: "팀 이름은 50자 이내여야 합니다",
duplicateName: "같은 이름의 팀이 이미 존재합니다",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "역할",
@@ -2824,6 +2830,10 @@ export const ko: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "API 키",
+10
View File
@@ -2803,6 +2803,12 @@ export const nl: TranslationKeys = {
nameRequired: "Teamnaam is verplicht",
nameTooLong: "Teamnaam mag maximaal 50 tekens zijn",
duplicateName: "Er bestaat al een team met deze naam",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "Rollen",
@@ -2885,6 +2891,10 @@ export const nl: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "API-sleutels",
+10
View File
@@ -2807,6 +2807,12 @@ export const pl: TranslationKeys = {
nameRequired: "Nazwa zespołu jest wymagana",
nameTooLong: "Nazwa zespołu nie może przekraczać 50 znaków",
duplicateName: "Zespół o takiej nazwie już istnieje",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "Role",
@@ -2889,6 +2895,10 @@ export const pl: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "Klucze API",
+10
View File
@@ -2801,6 +2801,12 @@ export const ptBR: TranslationKeys = {
nameRequired: "O nome da equipe e obrigatorio",
nameTooLong: "O nome da equipe deve ter 50 caracteres ou menos",
duplicateName: "Ja existe uma equipe com este nome",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "Funcoes",
@@ -2883,6 +2889,10 @@ export const ptBR: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "Chaves API",
+10
View File
@@ -2801,6 +2801,12 @@ export const ru: TranslationKeys = {
nameRequired: "Название команды обязательно",
nameTooLong: "Название команды не должно превышать 50 символов",
duplicateName: "Команда с таким именем уже существует",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "Роли",
@@ -2882,6 +2888,10 @@ export const ru: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "API-ключи",
+10
View File
@@ -2799,6 +2799,12 @@ export const sv: TranslationKeys = {
nameRequired: "Teamnamn kravs",
nameTooLong: "Teamnamn far vara hogst 50 tecken",
duplicateName: "Ett team med detta namn finns redan",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "Roller",
@@ -2880,6 +2886,10 @@ export const sv: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "API-nycklar",
+10
View File
@@ -2779,6 +2779,12 @@ export const th: TranslationKeys = {
nameRequired: "ต้องระบุชื่อทีม",
nameTooLong: "ชื่อทีมต้องไม่เกิน 50 ตัวอักษร",
duplicateName: "มีทีมชื่อนี้อยู่แล้ว",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "บทบาท",
@@ -2860,6 +2866,10 @@ export const th: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "คีย์ API",
+10
View File
@@ -2805,6 +2805,12 @@ export const tr: TranslationKeys = {
nameRequired: "Takım adı gereklidir",
nameTooLong: "Takım adı en fazla 50 karakter olmalıdır",
duplicateName: "Bu isimde bir takım zaten var",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "Roller",
@@ -2886,6 +2892,10 @@ export const tr: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "API Anahtarları",
+10
View File
@@ -2801,6 +2801,12 @@ export const uk: TranslationKeys = {
nameRequired: "Назва команди обов'язкова",
nameTooLong: "Назва команди не може перевищувати 50 символів",
duplicateName: "Команда з такою назвою вже існує",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "Ролі",
@@ -2882,6 +2888,10 @@ export const uk: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "API-ключі",
+10
View File
@@ -2801,6 +2801,12 @@ export const vi: TranslationKeys = {
nameRequired: "Tên nhóm là bắt buộc",
nameTooLong: "Tên nhóm phải tối đa 50 ký tự",
duplicateName: "Đã tồn tại nhóm có tên này",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "Vai trò",
@@ -2882,6 +2888,10 @@ export const vi: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "Khóa API",
+10
View File
@@ -2731,6 +2731,12 @@ export const zhCN: TranslationKeys = {
nameRequired: "团队名称为必填项",
nameTooLong: "团队名称不能超过 50 个字符",
duplicateName: "该名称的团队已存在",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "角色",
@@ -2812,6 +2818,10 @@ export const zhCN: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "API 密钥",
+10
View File
@@ -2729,6 +2729,12 @@ export const zhTW: TranslationKeys = {
nameRequired: "需要團隊名稱",
nameTooLong: "團隊名稱不超過50個字元",
duplicateName: "已存在同名團隊",
teamStorageQuota: "Storage Quota (MB)",
teamStorageQuotaDesc: "Maximum storage for this team in MB. Leave empty for no limit.",
teamRetentionHours: "Retention (hours)",
teamRetentionHoursDesc:
"Processing file retention for this team. Leave empty to use global default.",
quotaSaved: "Team settings saved",
},
roles: {
heading: "角色",
@@ -2810,6 +2816,10 @@ export const zhTW: TranslationKeys = {
storageHeading: "Library storage",
storageFiles: "{count} files",
unknownUser: "(unknown)",
storageByTeam: "Storage by Team",
teamColumn: "Team",
storageColumn: "Storage",
usersColumn: "Users",
},
apiKeys: {
heading: "API金鑰",