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
+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金鑰",