mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-08-26 02:46:28 +00:00
more progress
This commit is contained in:
@@ -222,7 +222,6 @@
|
||||
"add_repo_modal_field_cpu_priority_low": "CPU_LOW - أولوية أقل من الأولوية الافتراضية",
|
||||
"add_repo_modal_field_cpu_priority_placeholder": "حدد أولوية وحدة المعالجة المركزية",
|
||||
"add_repo_modal_field_auto_unlock": "فتح تلقائي",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "ستؤدي خاصية الفتح التلقائي إلى إزالة ملفات القفل عند بدء عمليات النسيان والحذف. قد يكون هذا غير آمن إذا تمت مشاركة المستودع من قبل عدة أجهزة عميلة. هذه الخاصية معطلة افتراضيًا.",
|
||||
"add_repo_modal_preview_json": "ملف إعدادات المستودع بصيغة JSON",
|
||||
"add_repo_modal_error_missing_password": "كلمة مرور المستودع مفقودة. يرجى إدخال كلمة مرور أو تعيين أحد متغيرات البيئة التالية: RESTIC_PASSWORD، RESTIC_PASSWORD_COMMAND، RESTIC_PASSWORD_FILE.",
|
||||
"dashboard_error_fetch": "فشل في جلب بيانات الملخص: ",
|
||||
@@ -294,10 +293,6 @@
|
||||
"op_row_cancel_error": "فشل إلغاء العملية: ",
|
||||
"op_row_logs_title": "سجلات العملية {name} في {time}",
|
||||
"op_row_view_logs": "[عرض السجلات]",
|
||||
"op_row_confirm_cancel": "[تأكيد إلغاء؟]",
|
||||
"op_row_cancel_op": "[إلغاء العملية]",
|
||||
"op_row_confirm_delete": "[تأكيد الحذف؟]",
|
||||
"op_row_delete": "[يمسح]",
|
||||
"op_row_backup_details": "تفاصيل النسخ الاحتياطي",
|
||||
"op_row_item_errors": "أخطاء في العنصر",
|
||||
"op_row_error_on_item": "خطأ في العنصر: {item}",
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
"add_repo_modal_field_cpu_priority_low": "CPU_LOW - ডিফল্ট অগ্রাধিকারের চেয়ে কম",
|
||||
"add_repo_modal_field_cpu_priority_placeholder": "একটি CPU অগ্রাধিকার নির্বাচন করুন",
|
||||
"add_repo_modal_field_auto_unlock": "অটো আনলক",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "ফরগেট এবং প্রুন অপারেশনের শুরুতে অটো-আনলক লকফাইলগুলি সরিয়ে ফেলবে। একাধিক ক্লায়েন্ট ডিভাইস দ্বারা রেপো শেয়ার করা থাকলে এটি সম্ভাব্যভাবে অনিরাপদ। ডিফল্টরূপে অক্ষম।",
|
||||
"add_repo_modal_preview_json": "JSON হিসেবে রেপো কনফিগারেশন",
|
||||
"add_repo_modal_error_missing_password": "রেপো পাসওয়ার্ড নেই। হয় একটি পাসওয়ার্ড দিন অথবা env ভেরিয়েবলগুলির মধ্যে একটি সেট করুন: RESTIC_PASSWORD, RESTIC_PASSWORD_COMMAND, RESTIC_PASSWORD_FILE।",
|
||||
"dashboard_error_fetch": "সারাংশ তথ্য আনতে ব্যর্থ: ",
|
||||
@@ -294,10 +293,6 @@
|
||||
"op_row_cancel_error": "অপারেশন বাতিল করতে ব্যর্থ: ",
|
||||
"op_row_logs_title": "{name} এ {time}",
|
||||
"op_row_view_logs": "[লগ দেখুন]",
|
||||
"op_row_confirm_cancel": "[বাতিল নিশ্চিত করবেন?]",
|
||||
"op_row_cancel_op": "[কার্যক্রম বাতিল করুন]",
|
||||
"op_row_confirm_delete": "[মুছে ফেলা নিশ্চিত করবেন?]",
|
||||
"op_row_delete": "[মুছে ফেলুন]",
|
||||
"op_row_backup_details": "ব্যাকআপের বিবরণ",
|
||||
"op_row_item_errors": "আইটেম ত্রুটি",
|
||||
"op_row_error_on_item": "আইটেমটিতে ত্রুটি: {item}",
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
"add_repo_modal_field_cpu_priority_low": "CPU_LOW – niedrigere Priorität als die Standardpriorität",
|
||||
"add_repo_modal_field_cpu_priority_placeholder": "Wählen Sie eine CPU-Priorität aus.",
|
||||
"add_repo_modal_field_auto_unlock": "Automatisches Entsperren",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "Die automatische Entsperrung entfernt Sperrdateien zu Beginn von Lösch- und Bereinigungsvorgängen. Dies kann ein Sicherheitsrisiko darstellen, wenn das Repository von mehreren Clientgeräten gemeinsam genutzt wird. Standardmäßig deaktiviert.",
|
||||
"add_repo_modal_preview_json": "Repository-Konfiguration als JSON",
|
||||
"add_repo_modal_error_missing_password": "Fehlendes Repository-Passwort. Geben Sie entweder ein Passwort an oder setzen Sie eine der Umgebungsvariablen RESTIC_PASSWORD, RESTIC_PASSWORD_COMMAND oder RESTIC_PASSWORD_FILE.",
|
||||
"dashboard_error_fetch": "Zusammenfassende Daten konnten nicht abgerufen werden: ",
|
||||
@@ -294,10 +293,6 @@
|
||||
"op_row_cancel_error": "Vorgang konnte nicht abgebrochen werden: ",
|
||||
"op_row_logs_title": "Protokolle für die Operation {name} um {time}",
|
||||
"op_row_view_logs": "[Protokolle anzeigen]",
|
||||
"op_row_confirm_cancel": "[Bestätigen Abbrechen?]",
|
||||
"op_row_cancel_op": "[Vorgang abbrechen]",
|
||||
"op_row_confirm_delete": "[Löschen bestätigen?]",
|
||||
"op_row_delete": "[Löschen]",
|
||||
"op_row_backup_details": "Sicherungsdetails",
|
||||
"op_row_item_errors": "Artikelfehler",
|
||||
"op_row_error_on_item": "Fehler beim Element: {item}",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"add_plan_modal_error_template_not_found": "template not found",
|
||||
"add_plan_modal_error_plan_delete_not_found": "failed to update config, plan to delete not found",
|
||||
"add_plan_modal_success_plan_deleted": "Plan deleted from config, but not from restic repo. Snapshots will remain in storage and operations will be tracked until manually deleted. Reusing a deleted plan ID is not recommended if backups have already been performed.",
|
||||
"add_plan_modal_error_destroy_prefix": "Destroy error:",
|
||||
"add_plan_modal_error_destroy_prefix": "Destroy error: ",
|
||||
"add_plan_modal_error_operation_prefix": "Operation error: ",
|
||||
"add_plan_modal_title_update": "Update Plan",
|
||||
"add_plan_modal_title_add": "Add Plan",
|
||||
@@ -197,10 +197,6 @@
|
||||
"op_row_cancel_error": "Failed to cancel operation: ",
|
||||
"op_row_logs_title": "Logs for operation {name} at {time}",
|
||||
"op_row_view_logs": "[View Logs]",
|
||||
"op_row_confirm_cancel": "[Confirm Cancel?]",
|
||||
"op_row_cancel_op": "[Cancel Operation]",
|
||||
"op_row_confirm_delete": "[Confirm Delete?]",
|
||||
"op_row_delete": "[Delete]",
|
||||
"op_row_backup_details": "Backup Details",
|
||||
"op_row_item_errors": "Item Errors",
|
||||
"op_row_error_on_item": "Error on item: {item}",
|
||||
@@ -293,13 +289,12 @@
|
||||
"add_repo_modal_field_io_priority_idle": "IO_IDLE - only runs when disk bandwidth is idle (e.g. no other operations are queued)",
|
||||
"add_repo_modal_field_io_priority_placeholder": "Select an IO priority",
|
||||
"add_repo_modal_field_cpu_priority": "CPU Priority:",
|
||||
"add_repo_modal_field_cpu_priority_tooltip_intro": "Available CPU priority modes:",
|
||||
"add_repo_modal_field_cpu_priority_tooltip_intro": "Available CPU priority modes",
|
||||
"add_repo_modal_field_cpu_priority_default": "CPU_DEFAULT - no change in priority",
|
||||
"add_repo_modal_field_cpu_priority_high": "CPU_HIGH - higher than default priority (backrest must be running as root)",
|
||||
"add_repo_modal_field_cpu_priority_low": "CPU_LOW - lower than default priority",
|
||||
"add_repo_modal_field_cpu_priority_placeholder": "Select a CPU priority",
|
||||
"add_repo_modal_field_auto_unlock": "Auto Unlock",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "Auto-unlock will remove lockfiles at the start of forget and prune operations. This is potentially unsafe if the repo is shared by multiple client devices. Disabled by default.",
|
||||
"add_repo_modal_preview_json": "Repo Config as JSON",
|
||||
"add_repo_modal_error_missing_password": "Missing repo password. Either provide a password or set one of the env variables RESTIC_PASSWORD, RESTIC_PASSWORD_COMMAND, RESTIC_PASSWORD_FILE.",
|
||||
"dashboard_error_fetch": "Failed to fetch summary data: ",
|
||||
@@ -353,5 +348,9 @@
|
||||
"add_plan_modal_retention_policy_keep_regardless_label": "Latest snapshots to keep regardless of age",
|
||||
"add_plan_modal_retention_policy_subhourly_helper": "Schedule runs frequently; keep some recent snapshots.",
|
||||
"op_row_error_path": "Path",
|
||||
"op_row_error_message": "Message"
|
||||
"op_row_error_message": "Message",
|
||||
"add_repo_modal_env_vars_placeholder": "KEY=VALUE (One per line)",
|
||||
"app_menu_edit_plan": "app_menu_edit_plan",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "Auto-unlock will remove lockfiles at the start of most operations. This is potentially unsafe if the repo is shared by multiple client devices, otherwise is recommended for single-client setups.",
|
||||
"op_row_cancel_op": "Cancel Operation"
|
||||
}
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
"add_repo_modal_field_cpu_priority_low": "CPU_LOW - prioridad inferior a la predeterminada",
|
||||
"add_repo_modal_field_cpu_priority_placeholder": "Seleccionar una prioridad de CPU",
|
||||
"add_repo_modal_field_auto_unlock": "Desbloqueo automático",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "El desbloqueo automático eliminará los archivos de bloqueo al iniciar las operaciones de olvido y poda. Esto puede ser peligroso si el repositorio se comparte entre varios dispositivos cliente. Está deshabilitado por defecto.",
|
||||
"add_repo_modal_preview_json": "Configuración del repositorio como JSON",
|
||||
"add_repo_modal_error_missing_password": "Falta la contraseña del repositorio. Proporcione una contraseña o configure una de las variables de entorno RESTIC_PASSWORD, RESTIC_PASSWORD_COMMAND o RESTIC_PASSWORD_FILE.",
|
||||
"dashboard_error_fetch": "No se pudieron obtener los datos de resumen: ",
|
||||
@@ -294,10 +293,6 @@
|
||||
"op_row_cancel_error": "No se pudo cancelar la operación: ",
|
||||
"op_row_logs_title": "Registros de la operación {name} a las {time}",
|
||||
"op_row_view_logs": "[Ver registros]",
|
||||
"op_row_confirm_cancel": "[¿Confirmar Cancelar?]",
|
||||
"op_row_cancel_op": "[Cancelar operación]",
|
||||
"op_row_confirm_delete": "[¿Confirmar eliminación?]",
|
||||
"op_row_delete": "[Borrar]",
|
||||
"op_row_backup_details": "Detalles de la copia de seguridad",
|
||||
"op_row_item_errors": "Errores de artículos",
|
||||
"op_row_error_on_item": "Error en el artículo: {item}",
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
"add_repo_modal_field_cpu_priority_low": "CPU_LOW - priorité inférieure à la priorité par défaut",
|
||||
"add_repo_modal_field_cpu_priority_placeholder": "Sélectionnez une priorité du processeur",
|
||||
"add_repo_modal_field_auto_unlock": "Déverrouillage automatique",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "Le déverrouillage automatique supprime les fichiers de verrouillage au début des opérations d'oubli et d'élagage. Cette opération peut s'avérer dangereuse si le dépôt est partagé par plusieurs appareils clients. Elle est désactivée par défaut.",
|
||||
"add_repo_modal_preview_json": "Configuration du dépôt au format JSON",
|
||||
"add_repo_modal_error_missing_password": "Mot de passe du dépôt manquant. Veuillez fournir un mot de passe ou définir l'une des variables d'environnement RESTIC_PASSWORD, RESTIC_PASSWORD_COMMAND ou RESTIC_PASSWORD_FILE.",
|
||||
"dashboard_error_fetch": "Impossible de récupérer les données récapitulatives : ",
|
||||
@@ -294,10 +293,6 @@
|
||||
"op_row_cancel_error": "L'annulation de l'opération a échoué : ",
|
||||
"op_row_logs_title": "Journaux pour l'opération {name} à {time}",
|
||||
"op_row_view_logs": "[Afficher les journaux]",
|
||||
"op_row_confirm_cancel": "[Confirmer Annuler ?]",
|
||||
"op_row_cancel_op": "[Annuler l'opération]",
|
||||
"op_row_confirm_delete": "[Confirmer la suppression ?]",
|
||||
"op_row_delete": "[Supprimer]",
|
||||
"op_row_backup_details": "Détails de la sauvegarde",
|
||||
"op_row_item_errors": "Erreurs liées aux articles",
|
||||
"op_row_error_on_item": "Erreur sur l'élément : {item}",
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
"add_repo_modal_field_cpu_priority_low": "CPU_LOW - डिफ़ॉल्ट प्राथमिकता से कम",
|
||||
"add_repo_modal_field_cpu_priority_placeholder": "सीपीयू प्राथमिकता का चयन करें",
|
||||
"add_repo_modal_field_auto_unlock": "ऑटो अनलॉक",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "ऑटो-अनलॉक, फॉरगेट और प्रून ऑपरेशन शुरू होने पर लॉकफाइल हटा देगा। यदि रिपॉजिटरी कई क्लाइंट डिवाइसों द्वारा साझा की जाती है, तो यह संभावित रूप से असुरक्षित हो सकता है। डिफ़ॉल्ट रूप से अक्षम।",
|
||||
"add_repo_modal_preview_json": "JSON फॉर्मेट में रेपो कॉन्फ़िगरेशन",
|
||||
"add_repo_modal_error_missing_password": "रिपॉजिटरी पासवर्ड गायब है। कृपया पासवर्ड प्रदान करें या RESTIC_PASSWORD, RESTIC_PASSWORD_COMMAND या RESTIC_PASSWORD_FILE में से किसी एक एनवायरनमेंट वेरिएबल को सेट करें।",
|
||||
"dashboard_error_fetch": "सारांश डेटा प्राप्त करने में विफल: ",
|
||||
@@ -294,10 +293,6 @@
|
||||
"op_row_cancel_error": "ऑपरेशन रद्द करने में विफल: ",
|
||||
"op_row_logs_title": "ऑपरेशन {name} के लॉग {time}",
|
||||
"op_row_view_logs": "[लॉग देखें]",
|
||||
"op_row_confirm_cancel": "[रद्द करने की पुष्टि करें?]",
|
||||
"op_row_cancel_op": "[संचालन रद्द करें]",
|
||||
"op_row_confirm_delete": "[डिलीट की पुष्टि करें?]",
|
||||
"op_row_delete": "[मिटाना]",
|
||||
"op_row_backup_details": "बैकअप विवरण",
|
||||
"op_row_item_errors": "वस्तु त्रुटियाँ",
|
||||
"op_row_error_on_item": "आइटम पर त्रुटि: {item}",
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
"add_repo_modal_field_cpu_priority_low": "CPU_LOW - prioritas lebih rendah dari prioritas default",
|
||||
"add_repo_modal_field_cpu_priority_placeholder": "Pilih prioritas CPU",
|
||||
"add_repo_modal_field_auto_unlock": "Buka Kunci Otomatis",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "Fitur buka kunci otomatis akan menghapus file kunci di awal operasi lupakan dan pangkas. Ini berpotensi tidak aman jika repositori digunakan bersama oleh beberapa perangkat klien. Dinonaktifkan secara default.",
|
||||
"add_repo_modal_preview_json": "Konfigurasi Repositori sebagai JSON",
|
||||
"add_repo_modal_error_missing_password": "Kata sandi repositori hilang. Berikan kata sandi atau atur salah satu variabel lingkungan RESTIC_PASSWORD, RESTIC_PASSWORD_COMMAND, RESTIC_PASSWORD_FILE.",
|
||||
"dashboard_error_fetch": "Gagal mengambil data ringkasan: ",
|
||||
@@ -294,10 +293,6 @@
|
||||
"op_row_cancel_error": "Pembatalan operasi gagal: ",
|
||||
"op_row_logs_title": "Log untuk operasi {name} pada {time}",
|
||||
"op_row_view_logs": "[Lihat Log]",
|
||||
"op_row_confirm_cancel": "[Konfirmasi Batal?]",
|
||||
"op_row_cancel_op": "[Batalkan Operasi]",
|
||||
"op_row_confirm_delete": "[Konfirmasi Penghapusan?]",
|
||||
"op_row_delete": "[Menghapus]",
|
||||
"op_row_backup_details": "Detail Cadangan",
|
||||
"op_row_item_errors": "Kesalahan Item",
|
||||
"op_row_error_on_item": "Kesalahan pada item: {item}",
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
"add_repo_modal_field_cpu_priority_low": "CPU_LOW - priorità inferiore a quella predefinita",
|
||||
"add_repo_modal_field_cpu_priority_placeholder": "Seleziona una priorità della CPU",
|
||||
"add_repo_modal_field_auto_unlock": "Sblocco automatico",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "Lo sblocco automatico rimuoverà i file di blocco all'inizio delle operazioni di forget e prune. Questa opzione è potenzialmente pericolosa se il repository è condiviso da più dispositivi client. Disabilitata per impostazione predefinita.",
|
||||
"add_repo_modal_preview_json": "Configurazione del repository come JSON",
|
||||
"add_repo_modal_error_missing_password": "Password del repository mancante. Fornire una password o impostare una delle variabili di ambiente RESTIC_PASSWORD, RESTIC_PASSWORD_COMMAND, RESTIC_PASSWORD_FILE.",
|
||||
"dashboard_error_fetch": "Impossibile recuperare i dati di riepilogo: ",
|
||||
@@ -294,10 +293,6 @@
|
||||
"op_row_cancel_error": "Impossibile annullare l'operazione: ",
|
||||
"op_row_logs_title": "Registri per l'operazione {name} a {time}",
|
||||
"op_row_view_logs": "[Visualizza registri]",
|
||||
"op_row_confirm_cancel": "[Confermare l'annullamento?]",
|
||||
"op_row_cancel_op": "[Annulla operazione]",
|
||||
"op_row_confirm_delete": "[Confermare l'eliminazione?]",
|
||||
"op_row_delete": "[Eliminare]",
|
||||
"op_row_backup_details": "Dettagli del backup",
|
||||
"op_row_item_errors": "Errori dell'articolo",
|
||||
"op_row_error_on_item": "Errore sull'elemento: {item}",
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
"add_repo_modal_field_cpu_priority_low": "CPU_LOW - prioridade inferior à padrão",
|
||||
"add_repo_modal_field_cpu_priority_placeholder": "Selecione uma prioridade de CPU",
|
||||
"add_repo_modal_field_auto_unlock": "Desbloqueio automático",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "O desbloqueio automático removerá os arquivos de bloqueio no início das operações de esquecimento e limpeza. Isso pode ser inseguro se o repositório for compartilhado por vários dispositivos clientes. Desativado por padrão.",
|
||||
"add_repo_modal_preview_json": "Configuração do repositório em formato JSON",
|
||||
"add_repo_modal_error_missing_password": "Senha do repositório ausente. Forneça uma senha ou defina uma das variáveis de ambiente RESTIC_PASSWORD, RESTIC_PASSWORD_COMMAND ou RESTIC_PASSWORD_FILE.",
|
||||
"dashboard_error_fetch": "Falha ao obter os dados resumidos: ",
|
||||
@@ -294,10 +293,6 @@
|
||||
"op_row_cancel_error": "Falha ao cancelar a operação: ",
|
||||
"op_row_logs_title": "Registros da operação {name} em {time}",
|
||||
"op_row_view_logs": "[Ver registos]",
|
||||
"op_row_confirm_cancel": "[Confirmar Cancelar?]",
|
||||
"op_row_cancel_op": "[Cancelar operação]",
|
||||
"op_row_confirm_delete": "[Confirmar exclusão?]",
|
||||
"op_row_delete": "[Excluir]",
|
||||
"op_row_backup_details": "Detalhes do backup",
|
||||
"op_row_item_errors": "Erros no item",
|
||||
"op_row_error_on_item": "Erro no item: {item}",
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
"add_repo_modal_field_cpu_priority_low": "CPU_LOW - более низкий приоритет, чем по умолчанию",
|
||||
"add_repo_modal_field_cpu_priority_placeholder": "Выберите приоритет ЦП",
|
||||
"add_repo_modal_field_auto_unlock": "Автоматическая разблокировка",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "Функция автоматической разблокировки удалит файлы блокировки в начале операций удаления и очистки. Это потенциально небезопасно, если репозиторий используется несколькими клиентскими устройствами. По умолчанию отключено.",
|
||||
"add_repo_modal_preview_json": "Конфигурация репозитория в формате JSON",
|
||||
"add_repo_modal_error_missing_password": "Отсутствует пароль репозитория. Укажите пароль или задайте одну из переменных окружения RESTIC_PASSWORD, RESTIC_PASSWORD_COMMAND, RESTIC_PASSWORD_FILE.",
|
||||
"dashboard_error_fetch": "Не удалось получить сводные данные: ",
|
||||
@@ -294,10 +293,6 @@
|
||||
"op_row_cancel_error": "Операция не была отменена: ",
|
||||
"op_row_logs_title": "Журналы операции {name} в {time}",
|
||||
"op_row_view_logs": "[Просмотреть журналы]",
|
||||
"op_row_confirm_cancel": "[Подтвердить отмену?]",
|
||||
"op_row_cancel_op": "[Отменить операцию]",
|
||||
"op_row_confirm_delete": "[Подтвердить удаление?]",
|
||||
"op_row_delete": "[Удалить]",
|
||||
"op_row_backup_details": "Сведения о резервном копировании",
|
||||
"op_row_item_errors": "Ошибки в элементах",
|
||||
"op_row_error_on_item": "Ошибка в элементе: {item}",
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
"add_repo_modal_field_cpu_priority_low": "CPU_LOW - 优先级低于默认优先级",
|
||||
"add_repo_modal_field_cpu_priority_placeholder": "选择 CPU 优先级",
|
||||
"add_repo_modal_field_auto_unlock": "自动解锁",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "自动解锁功能会在执行遗忘和清理操作时移除锁定文件。如果多个客户端设备共享同一个仓库,这可能存在安全隐患。此功能默认禁用。",
|
||||
"add_repo_modal_preview_json": "仓库配置(JSON 格式)",
|
||||
"add_repo_modal_error_missing_password": "缺少仓库密码。请提供密码或设置环境变量 RESTIC_PASSWORD、RESTIC_PASSWORD_COMMAND 或 RESTIC_PASSWORD_FILE 之一。",
|
||||
"dashboard_error_fetch": "获取摘要数据失败: ",
|
||||
@@ -294,10 +293,6 @@
|
||||
"op_row_cancel_error": "取消操作失败: ",
|
||||
"op_row_logs_title": "操作{name}在{time}",
|
||||
"op_row_view_logs": "[查看日志]",
|
||||
"op_row_confirm_cancel": "[确认取消?]",
|
||||
"op_row_cancel_op": "【取消操作】",
|
||||
"op_row_confirm_delete": "[确认删除?]",
|
||||
"op_row_delete": "[删除]",
|
||||
"op_row_backup_details": "备份详情",
|
||||
"op_row_item_errors": "项目错误",
|
||||
"op_row_error_on_item": "错误项: {item}",
|
||||
@@ -363,5 +358,12 @@
|
||||
"add_plan_modal_retention_policy_daily_label": "日常的",
|
||||
"add_plan_modal_retention_policy_hourly_label": "按小时",
|
||||
"add_plan_modal_retention_policy_keep_regardless_label": "无论年龄大小,都要保留最新的快照。",
|
||||
"add_plan_modal_retention_policy_subhourly_helper": "计划程序会频繁运行;请保留一些最近的快照。"
|
||||
"add_plan_modal_retention_policy_subhourly_helper": "计划程序会频繁运行;请保留一些最近的快照。",
|
||||
"add_repo_modal_env_vars_placeholder": "键=值(每行一个)",
|
||||
"app_menu_edit_plan": "app_menu_edit_plan",
|
||||
"add_repo_modal_field_auto_unlock_tooltip": "自动解锁功能会在大多数操作开始时移除锁定文件。如果多个客户端设备共享同一个仓库,这可能存在安全隐患;否则,建议在单客户端环境下启用此功能。",
|
||||
"op_row_cancel_op": "取消操作",
|
||||
"op_row_confirm_cancel": "确认取消?",
|
||||
"op_row_delete": "删除操作",
|
||||
"op_row_confirm_delete": "确认删除?"
|
||||
}
|
||||
|
||||
Generated
+93
-6
@@ -7,9 +7,6 @@ settings:
|
||||
importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@ant-design/icons':
|
||||
specifier: ^5.6.1
|
||||
version: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@bufbuild/protobuf':
|
||||
specifier: ^2.5.2
|
||||
version: 2.5.2
|
||||
@@ -22,15 +19,24 @@ importers:
|
||||
'@connectrpc/connect-web':
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.2(@bufbuild/protobuf@2.5.2)(@connectrpc/connect@2.0.2(@bufbuild/protobuf@2.5.2))
|
||||
'@dnd-kit/core':
|
||||
specifier: ^6.3.1
|
||||
version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@dnd-kit/sortable':
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
'@dnd-kit/utilities':
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2(react@18.3.1)
|
||||
'@emotion/react':
|
||||
specifier: ^11.14.0
|
||||
version: 11.14.0(@types/react@18.3.23)(react@18.3.1)
|
||||
antd:
|
||||
specifier: ^5.26.1
|
||||
version: 5.26.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
buffer:
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3
|
||||
cronstrue:
|
||||
specifier: ^3.9.0
|
||||
version: 3.9.0
|
||||
cross-env:
|
||||
specifier: ^7.0.3
|
||||
version: 7.0.3
|
||||
@@ -95,6 +101,9 @@ importers:
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^5.1.2
|
||||
version: 5.1.2(vite@7.3.0(@types/node@20.19.1)(lightningcss@1.30.1)(sass@1.97.1))
|
||||
prettier:
|
||||
specifier: ^3.7.4
|
||||
version: 3.7.4
|
||||
rc-collapse:
|
||||
specifier: ^3.9.0
|
||||
version: 3.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -365,6 +374,40 @@ packages:
|
||||
peerDependencies:
|
||||
'@bufbuild/protobuf': ^2.2.0
|
||||
|
||||
'@dnd-kit/accessibility@3.1.1':
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==,
|
||||
}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
|
||||
'@dnd-kit/core@6.3.1':
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==,
|
||||
}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@dnd-kit/sortable@10.0.0':
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==,
|
||||
}
|
||||
peerDependencies:
|
||||
'@dnd-kit/core': ^6.3.0
|
||||
react: '>=16.8.0'
|
||||
|
||||
'@dnd-kit/utilities@3.2.2':
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==,
|
||||
}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
|
||||
'@emotion/babel-plugin@11.13.5':
|
||||
resolution:
|
||||
{
|
||||
@@ -2031,6 +2074,13 @@ packages:
|
||||
}
|
||||
engines: { node: '>=10' }
|
||||
|
||||
cronstrue@3.9.0:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-T3S35zmD0Ai2B4ko6+mEM+k9C6tipe2nB9RLiGT6QL2Wn0Vsn2cCZAC8Oeuf4CaE00GZWVdpYitbpWCNlIWqdA==,
|
||||
}
|
||||
hasBin: true
|
||||
|
||||
cross-env@7.0.3:
|
||||
resolution:
|
||||
{
|
||||
@@ -2853,6 +2903,14 @@ packages:
|
||||
}
|
||||
engines: { node: ^10 || ^12 || >=14 }
|
||||
|
||||
prettier@3.7.4:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==,
|
||||
}
|
||||
engines: { node: '>=14' }
|
||||
hasBin: true
|
||||
|
||||
process@0.11.10:
|
||||
resolution:
|
||||
{
|
||||
@@ -4026,6 +4084,31 @@ snapshots:
|
||||
dependencies:
|
||||
'@bufbuild/protobuf': 2.5.2
|
||||
|
||||
'@dnd-kit/accessibility@3.1.1(react@18.3.1)':
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@dnd-kit/accessibility': 3.1.1(react@18.3.1)
|
||||
'@dnd-kit/utilities': 3.2.2(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@dnd-kit/utilities': 3.2.2(react@18.3.1)
|
||||
react: 18.3.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/utilities@3.2.2(react@18.3.1)':
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@emotion/babel-plugin@11.13.5':
|
||||
dependencies:
|
||||
'@babel/helper-module-imports': 7.27.1
|
||||
@@ -5270,6 +5353,8 @@ snapshots:
|
||||
path-type: 4.0.0
|
||||
yaml: 1.10.2
|
||||
|
||||
cronstrue@3.9.0: {}
|
||||
|
||||
cross-env@7.0.3:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
@@ -5673,6 +5758,8 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
prettier@3.7.4: {}
|
||||
|
||||
process@0.11.10: {}
|
||||
|
||||
prop-types@15.8.1:
|
||||
|
||||
+13
-10
@@ -10,6 +10,7 @@ import {
|
||||
FiRadio,
|
||||
FiActivity, // Added as a placeholder/guess for ActivityBar if needed, or stick to component
|
||||
FiServer,
|
||||
FiEdit2,
|
||||
} from "react-icons/fi";
|
||||
|
||||
import {
|
||||
@@ -20,7 +21,9 @@ import {
|
||||
Text,
|
||||
Spinner,
|
||||
Separator,
|
||||
IconButton,
|
||||
} from "@chakra-ui/react";
|
||||
import { Tooltip } from "../components/ui/tooltip";
|
||||
import { keyframes } from "@emotion/react";
|
||||
|
||||
import {
|
||||
@@ -256,7 +259,7 @@ const Sidebar = () => {
|
||||
pr={2}
|
||||
py={1}
|
||||
_hover={{ bg: "bg.subtle" }}
|
||||
role="group"
|
||||
className="group"
|
||||
borderRadius="md"
|
||||
mx={2}
|
||||
>
|
||||
@@ -276,18 +279,18 @@ const Sidebar = () => {
|
||||
_groupHover={{ opacity: 1 }}
|
||||
transition="opacity 0.2s"
|
||||
>
|
||||
<Button
|
||||
<IconButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
onClick={async (e) => {
|
||||
onClick={async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const { AddPlanModal } =
|
||||
await import("../features/plans/AddPlanModal");
|
||||
showModal(<AddPlanModal template={plan} />);
|
||||
}}
|
||||
>
|
||||
<FiSettings />
|
||||
</Button>
|
||||
<FiEdit2 />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
@@ -328,7 +331,7 @@ const Sidebar = () => {
|
||||
pr={2}
|
||||
py={1}
|
||||
_hover={{ bg: "bg.subtle" }}
|
||||
role="group"
|
||||
className="group"
|
||||
borderRadius="md"
|
||||
mx={2}
|
||||
>
|
||||
@@ -353,18 +356,18 @@ const Sidebar = () => {
|
||||
_groupHover={{ opacity: 1 }}
|
||||
transition="opacity 0.2s"
|
||||
>
|
||||
<Button
|
||||
<IconButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
onClick={async (e) => {
|
||||
onClick={async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const { AddRepoModal } =
|
||||
await import("../features/repositories/AddRepoModal");
|
||||
showModal(<AddRepoModal template={repo} />);
|
||||
}}
|
||||
>
|
||||
<FiSettings />
|
||||
</Button>
|
||||
<FiEdit2 />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -56,5 +56,8 @@ export const formatErrorAlert = (error: any, prefix?: string) => {
|
||||
</>
|
||||
);
|
||||
}
|
||||
return `${prefix}: ${contents}`;
|
||||
if (prefix.indexOf(":") === -1) {
|
||||
prefix += ":";
|
||||
}
|
||||
return `${prefix} ${contents}`;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
import {
|
||||
SelectRoot,
|
||||
SelectTrigger,
|
||||
SelectValueText,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectHiddenSelect,
|
||||
} from "../ui/select";
|
||||
import { Box, Text, createListCollection } from "@chakra-ui/react";
|
||||
|
||||
export interface EnumOption<T extends string> {
|
||||
value: T;
|
||||
label?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface EnumSelectorProps<T extends string> {
|
||||
value: T | T[];
|
||||
onChange: (value: T | T[]) => void;
|
||||
options: EnumOption<T>[];
|
||||
multiSelect?: boolean;
|
||||
placeholder?: string;
|
||||
size?: "xs" | "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
export const EnumSelector = <T extends string>({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
multiSelect = false,
|
||||
placeholder = "Select...",
|
||||
size = "sm",
|
||||
}: EnumSelectorProps<T>) => {
|
||||
const collection = createListCollection({
|
||||
items: options.map((opt) => ({
|
||||
label: opt.label || opt.value,
|
||||
value: opt.value,
|
||||
description: opt.description,
|
||||
})),
|
||||
});
|
||||
|
||||
const handleValueChange = (e: { value: string[] }) => {
|
||||
if (multiSelect) {
|
||||
onChange(e.value as T[]);
|
||||
} else {
|
||||
onChange(e.value[0] as T);
|
||||
}
|
||||
};
|
||||
|
||||
const currentValue = Array.isArray(value) ? value : value ? [value] : [];
|
||||
|
||||
return (
|
||||
<SelectRoot
|
||||
multiple={multiSelect}
|
||||
collection={collection}
|
||||
value={currentValue}
|
||||
onValueChange={handleValueChange}
|
||||
size={size}
|
||||
>
|
||||
<SelectHiddenSelect />
|
||||
<SelectTrigger>
|
||||
<SelectValueText placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent zIndex={2000}>
|
||||
{collection.items.map((item: any) => (
|
||||
<SelectItem item={item} key={item.value}>
|
||||
<Box>
|
||||
<Text as="span">{item.label}</Text>
|
||||
{item.description && (
|
||||
<Text as="span" color="fg.muted" ml={2} fontSize="xs">
|
||||
- {item.description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</SelectRoot>
|
||||
);
|
||||
};
|
||||
@@ -19,23 +19,15 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import { FiPlus, FiTrash2, FiInfo } from "react-icons/fi";
|
||||
import {
|
||||
SelectRoot,
|
||||
SelectTrigger,
|
||||
SelectValueText,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectHiddenSelect,
|
||||
} from "../ui/select";
|
||||
import {
|
||||
MenuTrigger,
|
||||
MenuContent,
|
||||
MenuItem,
|
||||
MenuItemText,
|
||||
MenuRoot,
|
||||
MenuTrigger,
|
||||
} from "../ui/menu";
|
||||
import { Tooltip } from "../ui/tooltip";
|
||||
import { createListCollection } from "@chakra-ui/react";
|
||||
import { Link } from "../ui/link";
|
||||
import { EnumSelector, EnumOption } from "./EnumSelector";
|
||||
|
||||
export interface HookFields {
|
||||
conditions: string[];
|
||||
@@ -98,20 +90,20 @@ const hookConditionDescriptions: Record<string, string> = {
|
||||
CONDITION_ANY_ERROR: "Triggered when any operation fails",
|
||||
};
|
||||
|
||||
const conditionCollection = createListCollection({
|
||||
items: Hook_ConditionSchema.values.map((v) => ({
|
||||
const conditionOptions: EnumOption<string>[] = Hook_ConditionSchema.values.map(
|
||||
(v) => ({
|
||||
label: v.name,
|
||||
value: v.name,
|
||||
description: hookConditionDescriptions[v.name],
|
||||
})),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const onErrorCollection = createListCollection({
|
||||
items: Hook_OnErrorSchema.values.map((v) => ({
|
||||
const onErrorOptions: EnumOption<string>[] = Hook_OnErrorSchema.values.map(
|
||||
(v) => ({
|
||||
label: v.name,
|
||||
value: v.name,
|
||||
})),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
interface HooksFormListProps {
|
||||
value?: HookFields[];
|
||||
@@ -205,8 +197,11 @@ const HookItem = ({
|
||||
const typeName = findHookTypeName(hook);
|
||||
|
||||
// @ts-ignore
|
||||
const handleConditionChange = (details: { value: string[] }) => {
|
||||
onChange({ ...hook, conditions: details.value });
|
||||
const handleConditionChange = (value: string | string[]) => {
|
||||
onChange({
|
||||
...hook,
|
||||
conditions: Array.isArray(value) ? value : [value],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -229,41 +224,16 @@ const HookItem = ({
|
||||
</Card.Header>
|
||||
<Card.Body gap={3}>
|
||||
<HookConditionsTooltip>
|
||||
<SelectRoot
|
||||
multiple
|
||||
collection={conditionCollection}
|
||||
value={hook.conditions}
|
||||
// @ts-ignore
|
||||
onValueChange={handleConditionChange}
|
||||
size="sm"
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
<SelectHiddenSelect />
|
||||
<SelectTrigger>
|
||||
<Box flex="1" textAlign="left">
|
||||
{hook.conditions && hook.conditions.length > 0 ? (
|
||||
hook.conditions.join(", ")
|
||||
) : (
|
||||
<Text color="fg.muted">Runs when...</Text>
|
||||
)}
|
||||
</Box>
|
||||
</SelectTrigger>
|
||||
<SelectContent zIndex={2000}>
|
||||
{conditionCollection.items.map((item: any) => (
|
||||
// @ts-ignore
|
||||
<SelectItem item={item} key={item.value}>
|
||||
<span>
|
||||
{item.label}
|
||||
{item.description && (
|
||||
<Text as="span" color="fg.muted" ml={2}>
|
||||
- {item.description}
|
||||
</Text>
|
||||
)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</SelectRoot>
|
||||
<Box width="full">
|
||||
<EnumSelector
|
||||
multiSelect
|
||||
options={conditionOptions}
|
||||
value={hook.conditions}
|
||||
onChange={handleConditionChange}
|
||||
placeholder="Runs when..."
|
||||
size="sm"
|
||||
/>
|
||||
</Box>
|
||||
</HookConditionsTooltip>
|
||||
|
||||
<HookBuilder hook={hook} onChange={onChange} />
|
||||
@@ -441,48 +411,23 @@ const hookTypes: {
|
||||
onChange={(e) => updateGotify("template", e.target.value)}
|
||||
size="sm"
|
||||
/>
|
||||
<SelectRoot
|
||||
collection={createListCollection({
|
||||
items: [
|
||||
{ label: "0 - No notification", value: "0" },
|
||||
{ label: "1 - Icon in notification bar", value: "1" },
|
||||
{ label: "4 - Icon in notification bar + Sound", value: "4" },
|
||||
{
|
||||
label: "8 - Icon in notification bar + Sound + Vibration",
|
||||
value: "8",
|
||||
},
|
||||
],
|
||||
})}
|
||||
value={[String(hook.actionGotify?.priority ?? 5)]}
|
||||
// @ts-ignore
|
||||
onValueChange={(e) =>
|
||||
updateGotify("priority", parseInt(e.value[0]))
|
||||
<EnumSelector
|
||||
options={[
|
||||
{ label: "0 - No notification", value: "0" },
|
||||
{ label: "1 - Icon in notification bar", value: "1" },
|
||||
{ label: "4 - Icon in notification bar + Sound", value: "4" },
|
||||
{
|
||||
label: "8 - Icon in notification bar + Sound + Vibration",
|
||||
value: "8",
|
||||
},
|
||||
]}
|
||||
value={String(hook.actionGotify?.priority ?? 5)}
|
||||
onChange={(val) =>
|
||||
updateGotify("priority", parseInt(val as string))
|
||||
}
|
||||
placeholder="Priority"
|
||||
size="sm"
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
<SelectHiddenSelect />
|
||||
<SelectTrigger>
|
||||
{/* @ts-ignore */}
|
||||
<SelectValueText placeholder="Priority" />
|
||||
</SelectTrigger>
|
||||
<SelectContent zIndex={2000}>
|
||||
{[
|
||||
{ label: "0 - No notification", value: "0" },
|
||||
{ label: "1 - Icon in notification bar", value: "1" },
|
||||
{ label: "4 - Icon in notification bar + Sound", value: "4" },
|
||||
{
|
||||
label: "8 - Icon in notification bar + Sound + Vibration",
|
||||
value: "8",
|
||||
},
|
||||
].map((item) => (
|
||||
// @ts-ignore
|
||||
<SelectItem item={item} key={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</SelectRoot>
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
@@ -675,28 +620,13 @@ const ItemOnErrorSelector = ({
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<SelectRoot
|
||||
collection={onErrorCollection}
|
||||
value={[hook.onError || ""]}
|
||||
// @ts-ignore
|
||||
onValueChange={(e) => onChange({ ...hook, onError: e.value[0] })}
|
||||
<EnumSelector
|
||||
options={onErrorOptions}
|
||||
value={hook.onError || ""}
|
||||
onChange={(val) => onChange({ ...hook, onError: val as string })}
|
||||
placeholder="Error behavior..."
|
||||
size="sm"
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
<SelectHiddenSelect />
|
||||
<SelectTrigger>
|
||||
{/* @ts-ignore */}
|
||||
<SelectValueText placeholder="Error behavior..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent zIndex={2000}>
|
||||
{onErrorCollection.items.map((item: any) => (
|
||||
// @ts-ignore
|
||||
<SelectItem item={item} key={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</SelectRoot>
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
Flex,
|
||||
Stack,
|
||||
Input,
|
||||
Text,
|
||||
Card,
|
||||
Button,
|
||||
} from "@chakra-ui/react";
|
||||
import { Flex, Stack, Input, Text, Card, Button } from "@chakra-ui/react";
|
||||
import { Checkbox } from "../ui/checkbox";
|
||||
import { Radio, RadioGroup } from "../ui/radio";
|
||||
import { NumberInputField } from "./NumberInput"; // Assuming I have this wrapper or standard NumberInput
|
||||
@@ -110,7 +103,6 @@ export const ScheduleFormItem = ({
|
||||
<Card.Root variant="subtle" width="fit-content">
|
||||
<Card.Header pb={0}>
|
||||
<Field label="Schedule Type">
|
||||
|
||||
<Flex gap={2} wrap="wrap">
|
||||
{[
|
||||
{ value: "disabled", label: "Disabled" },
|
||||
@@ -206,13 +198,13 @@ export const ScheduleFormItem = ({
|
||||
)}
|
||||
{mode === "disabled" && (
|
||||
<Text color="fg.muted" fontSize="sm">
|
||||
Automatic snapshots are disabled for this plan. You can still
|
||||
run backups manually.
|
||||
Automatic snapshots are disabled for this plan. You can still run
|
||||
backups manually.
|
||||
</Text>
|
||||
)}
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
</Stack >
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -36,12 +36,12 @@ export const LoginModal = () => {
|
||||
try {
|
||||
const loginResponse = await authenticationService.login(loginReq);
|
||||
setAuthToken(loginResponse.token);
|
||||
alerts.success(m.login_success(), 5);
|
||||
alerts.success(m.login_success());
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
} catch (e: any) {
|
||||
alerts.error(m.login_error() + (e.message ? e.message : "" + e), 10);
|
||||
alerts.error(m.login_error() + (e.message ? e.message : "" + e));
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -216,7 +216,7 @@ export const OperationRow = ({
|
||||
confirmText={m.op_row_confirm_cancel()}
|
||||
color="fg.error"
|
||||
>
|
||||
<FiX /> {m.op_row_cancel_op()}
|
||||
{m.op_row_cancel_op()}
|
||||
</ConfirmMenuItem>,
|
||||
);
|
||||
} else if (showDelete) {
|
||||
@@ -271,7 +271,7 @@ export const OperationRow = ({
|
||||
{e.item}
|
||||
</Table.Cell>
|
||||
<Table.Cell verticalAlign="top">
|
||||
{e.message || "Unknown error"}
|
||||
{e.message || e.item}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
|
||||
@@ -36,11 +36,7 @@ import {
|
||||
type RetentionPolicy,
|
||||
type Schedule,
|
||||
} from "../../../gen/ts/v1/config_pb";
|
||||
import {
|
||||
FiPlus as Plus,
|
||||
FiMinus as Minus,
|
||||
FiMenu,
|
||||
} from "react-icons/fi";
|
||||
import { FiPlus as Plus, FiMinus as Minus, FiMenu } from "react-icons/fi";
|
||||
import { BsCalculator as Calculator } from "react-icons/bs";
|
||||
import { alerts, formatErrorAlert } from "../../components/common/Alerts";
|
||||
import { namePattern } from "../../lib/util";
|
||||
@@ -172,11 +168,10 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
|
||||
configCopy.plans.splice(idx, 1);
|
||||
setConfig(await backrestService.setConfig(configCopy));
|
||||
showModal(null);
|
||||
alerts.success(m.add_plan_modal_success_plan_deleted(), 30);
|
||||
alerts.success(m.add_plan_modal_success_plan_deleted());
|
||||
} catch (e: any) {
|
||||
alerts.error(
|
||||
formatErrorAlert(e, m.add_plan_modal_error_destroy_prefix()),
|
||||
15,
|
||||
);
|
||||
} finally {
|
||||
setConfirmLoading(false);
|
||||
@@ -186,7 +181,45 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
|
||||
const handleOk = async () => {
|
||||
setConfirmLoading(true);
|
||||
try {
|
||||
// TODO: Validation (manual or schema based)
|
||||
// Validation
|
||||
if (!formData.id?.trim()) {
|
||||
throw new Error(m.add_plan_modal_validation_plan_name_required());
|
||||
}
|
||||
if (!namePattern.test(formData.id)) {
|
||||
throw new Error(m.add_plan_modal_validation_plan_name_pattern());
|
||||
}
|
||||
if (!template && config.plans.find((p) => p.id === formData.id)) {
|
||||
throw new Error(m.add_plan_modal_validation_plan_exists());
|
||||
}
|
||||
if (!formData.repo) {
|
||||
throw new Error(m.add_plan_modal_validation_repository_required());
|
||||
}
|
||||
if (!formData.paths || formData.paths.length === 0) {
|
||||
throw new Error(m.add_plan_modal_validation_paths_required());
|
||||
}
|
||||
if (
|
||||
formData.backup_flags &&
|
||||
formData.backup_flags.some((f: string) => !/^\-\-?.*$/.test(f))
|
||||
) {
|
||||
throw new Error(m.add_plan_modal_validation_flag_pattern());
|
||||
}
|
||||
|
||||
// Check retention for sub-hourly schedules
|
||||
const scheduleValue = formData.schedule?.schedule?.value;
|
||||
const isCron = formData.schedule?.schedule?.case === "cron";
|
||||
const isSubHourly =
|
||||
isCron && scheduleValue && !/^\d+ /.test(scheduleValue);
|
||||
|
||||
if (
|
||||
isSubHourly &&
|
||||
formData.retention?.policyTimeBucketed &&
|
||||
!(formData.retention.policyTimeBucketed.keepLastN > 1)
|
||||
) {
|
||||
throw new Error(
|
||||
"Your schedule runs more than once per hour; please specify a 'Latest (Count)' greater than 1 in Retention Policy.",
|
||||
);
|
||||
}
|
||||
|
||||
const plan = fromJson(PlanSchema, formData, {
|
||||
ignoreUnknownFields: true,
|
||||
});
|
||||
@@ -218,7 +251,6 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
|
||||
} catch (e: any) {
|
||||
alerts.error(
|
||||
formatErrorAlert(e, m.add_plan_modal_error_operation_prefix()),
|
||||
15,
|
||||
);
|
||||
} finally {
|
||||
setConfirmLoading(false);
|
||||
@@ -287,6 +319,17 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
|
||||
label={m.add_plan_modal_field_plan_name()}
|
||||
helperText={m.add_plan_modal_field_plan_name_tooltip()}
|
||||
required
|
||||
invalid={
|
||||
!!formData.id &&
|
||||
(!namePattern.test(formData.id) ||
|
||||
(!template &&
|
||||
!!config.plans.find((p) => p.id === formData.id)))
|
||||
}
|
||||
errorText={
|
||||
!!formData.id && !namePattern.test(formData.id)
|
||||
? m.add_plan_modal_validation_plan_name_pattern()
|
||||
: m.add_plan_modal_validation_plan_exists()
|
||||
}
|
||||
>
|
||||
<Input
|
||||
value={getField(["id"])}
|
||||
@@ -300,6 +343,7 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
|
||||
label={m.add_plan_modal_field_repository()}
|
||||
helperText={m.add_plan_modal_field_repository_tooltip()}
|
||||
required
|
||||
invalid={!getField(["repo"]) && confirmLoading}
|
||||
>
|
||||
<SelectRoot
|
||||
collection={repoOptions}
|
||||
@@ -404,11 +448,11 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
|
||||
|
||||
{/* Retention Policy */}
|
||||
<Section title="Retention Policy">
|
||||
<RetentionPolicyView
|
||||
schedule={getField(["schedule"])}
|
||||
retention={getField(["retention"])}
|
||||
onChange={(v: any) => updateField(["retention"], v)}
|
||||
/>
|
||||
<RetentionPolicyView
|
||||
schedule={getField(["schedule"])}
|
||||
retention={getField(["retention"])}
|
||||
onChange={(v: any) => updateField(["retention"], v)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* Advanced */}
|
||||
@@ -416,23 +460,25 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
|
||||
<Card.Root variant="subtle">
|
||||
<Card.Body>
|
||||
<Stack gap={4}>
|
||||
<DynamicList
|
||||
label={m.add_plan_modal_field_backup_flags()}
|
||||
items={getField(["backup_flags"]) || []}
|
||||
onUpdate={(items: string[]) => updateField(["backup_flags"], items)}
|
||||
tooltip={m.add_plan_modal_field_backup_flags_tooltip()}
|
||||
placeholder="--flag"
|
||||
/>
|
||||
<DynamicList
|
||||
label={m.add_plan_modal_field_backup_flags()}
|
||||
items={getField(["backup_flags"]) || []}
|
||||
onUpdate={(items: string[]) =>
|
||||
updateField(["backup_flags"], items)
|
||||
}
|
||||
tooltip={m.add_plan_modal_field_backup_flags_tooltip()}
|
||||
placeholder="--flag"
|
||||
/>
|
||||
|
||||
<Field
|
||||
label={m.add_plan_modal_field_hooks()}
|
||||
helperText={hooksListTooltipText}
|
||||
>
|
||||
<HooksFormList
|
||||
value={getField(["hooks"])}
|
||||
onChange={(v: any) => updateField(["hooks"], v)}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={m.add_plan_modal_field_hooks()}
|
||||
helperText={hooksListTooltipText}
|
||||
>
|
||||
<HooksFormList
|
||||
value={getField(["hooks"])}
|
||||
onChange={(v: any) => updateField(["hooks"], v)}
|
||||
/>
|
||||
</Field>
|
||||
</Stack>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
@@ -460,15 +506,22 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
|
||||
</AccordionItemContent>
|
||||
</AccordionItem>
|
||||
</AccordionRoot>
|
||||
|
||||
</Stack>
|
||||
</FormModal>
|
||||
);
|
||||
};
|
||||
|
||||
const Section = ({ title, children }: { title: string; children: React.ReactNode }) => (
|
||||
const Section = ({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<Stack gap={2}>
|
||||
<CText fontWeight="semibold" fontSize="sm">{title}</CText>
|
||||
<CText fontWeight="semibold" fontSize="sm">
|
||||
{title}
|
||||
</CText>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
@@ -658,4 +711,3 @@ const RetentionPolicyView = ({ schedule, retention, onChange }: any) => {
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Plan } from "../../../gen/ts/v1/config_pb";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Flex, Heading, Text, Box } from "@chakra-ui/react";
|
||||
import { Flex, Heading, Text, Box, Group, IconButton } from "@chakra-ui/react";
|
||||
import { FiChevronDown } from "react-icons/fi";
|
||||
import {
|
||||
Tabs,
|
||||
TabsList,
|
||||
@@ -9,6 +10,12 @@ import {
|
||||
TabsContent,
|
||||
TabsRoot,
|
||||
} from "../../components/ui/tabs";
|
||||
import {
|
||||
MenuContent,
|
||||
MenuItem,
|
||||
MenuRoot,
|
||||
MenuTrigger,
|
||||
} from "../../components/ui/menu";
|
||||
import { Tooltip } from "../../components/ui/tooltip";
|
||||
import { alerts } from "../../components/common/Alerts";
|
||||
import { MAX_OPERATION_HISTORY } from "../../constants";
|
||||
@@ -87,35 +94,42 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
|
||||
<Box>
|
||||
<Flex gap={4} align="center" wrap="wrap" mb={4}>
|
||||
<Heading size="xl">{plan.id}</Heading>
|
||||
<Box flex="1" />
|
||||
|
||||
<SpinButton type="primary" onClickAsync={handleBackupNow}>
|
||||
{m.plan_button_backup()}
|
||||
</SpinButton>
|
||||
|
||||
<Tooltip content={m.repo_tooltip_run_command()}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
const { RunCommandModal } =
|
||||
await import("../operations/RunCommandModal");
|
||||
showModal(<RunCommandModal repo={repo} />);
|
||||
}}
|
||||
>
|
||||
{m.repo_button_run_command()}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={m.repo_tooltip_unlock()}>
|
||||
<SpinButton type="default" onClickAsync={handleUnlockNow}>
|
||||
{m.repo_button_unlock()}
|
||||
<Group attached>
|
||||
<SpinButton type="primary" onClickAsync={handleBackupNow}>
|
||||
{m.plan_button_backup()}
|
||||
</SpinButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={m.plan_tooltip_clear_history()}>
|
||||
<SpinButton type="default" onClickAsync={handleClearErrorHistory}>
|
||||
{m.plan_button_clear_history()}
|
||||
</SpinButton>
|
||||
</Tooltip>
|
||||
<MenuRoot>
|
||||
<MenuTrigger asChild>
|
||||
<IconButton
|
||||
variant="subtle"
|
||||
colorPalette="blue"
|
||||
aria-label="More actions"
|
||||
>
|
||||
<FiChevronDown />
|
||||
</IconButton>
|
||||
</MenuTrigger>
|
||||
<MenuContent>
|
||||
<MenuItem
|
||||
value="run-command"
|
||||
onClick={async () => {
|
||||
const { RunCommandModal } =
|
||||
await import("../operations/RunCommandModal");
|
||||
showModal(<RunCommandModal repo={repo} />);
|
||||
}}
|
||||
>
|
||||
{m.repo_button_run_command()}
|
||||
</MenuItem>
|
||||
<MenuItem value="unlock" onClick={handleUnlockNow}>
|
||||
{m.repo_button_unlock()}
|
||||
</MenuItem>
|
||||
<MenuItem value="clear-history" onClick={handleClearErrorHistory}>
|
||||
{m.plan_button_clear_history()}
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuRoot>
|
||||
</Group>
|
||||
</Flex>
|
||||
|
||||
<TabsRoot defaultValue="tree" lazyMount>
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import {
|
||||
Flex,
|
||||
Stack,
|
||||
Flex,
|
||||
Input,
|
||||
Textarea,
|
||||
createListCollection,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectRoot,
|
||||
SelectTrigger,
|
||||
SelectValueText,
|
||||
Card,
|
||||
Text as CText,
|
||||
Grid,
|
||||
Code,
|
||||
Box,
|
||||
} from "@chakra-ui/react";
|
||||
import { EnumSelector, EnumOption } from "../../components/common/EnumSelector";
|
||||
import { Checkbox } from "../../components/ui/checkbox";
|
||||
import {
|
||||
AccordionItem,
|
||||
AccordionItemContent,
|
||||
AccordionItemTrigger,
|
||||
AccordionRoot,
|
||||
} from "../../components/ui/accordion";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useShowModal } from "../../components/common/ModalManager";
|
||||
// removed ../state/repo_state import
|
||||
// valid imports
|
||||
import {
|
||||
CommandPrefix_CPUNiceLevel,
|
||||
CommandPrefix_CPUNiceLevelSchema,
|
||||
@@ -30,12 +32,11 @@ import { URIAutocomplete } from "../../components/common/URIAutocomplete";
|
||||
import { alerts, formatErrorAlert } from "../../components/common/Alerts";
|
||||
import { namePattern } from "../../lib/util";
|
||||
import { backrestService } from "../../api/client";
|
||||
// import { HooksFormList } from "../components/HooksFormList"; // TODO: Migrate
|
||||
import { ConfirmButton } from "../../components/common/SpinButton";
|
||||
import { ConfirmButton, SpinButton } from "../../components/common/SpinButton";
|
||||
import { useConfig } from "../../app/provider";
|
||||
import {
|
||||
ScheduleFormItem,
|
||||
ScheduleDefaultsDaily,
|
||||
ScheduleDefaultsInfrequent,
|
||||
} from "../../components/common/ScheduleFormItem";
|
||||
import { isWindows } from "../../state/buildcfg";
|
||||
import { create, fromJson, toJson, JsonValue } from "@bufbuild/protobuf";
|
||||
@@ -45,13 +46,12 @@ import { Button } from "../../components/ui/button";
|
||||
import { Field } from "../../components/ui/field";
|
||||
import { PasswordInput } from "../../components/ui/password-input";
|
||||
import { Tooltip } from "../../components/ui/tooltip";
|
||||
import { toaster } from "../../components/ui/toaster";
|
||||
import { NumberInputField } from "../../components/common/NumberInput";
|
||||
|
||||
import {
|
||||
HooksFormList,
|
||||
hooksListTooltipText,
|
||||
} from "../../components/common/HooksFormList";
|
||||
import { DynamicList } from "../../components/common/DynamicList";
|
||||
|
||||
const repoDefaults = create(RepoSchema, {
|
||||
prunePolicy: {
|
||||
@@ -59,7 +59,7 @@ const repoDefaults = create(RepoSchema, {
|
||||
schedule: {
|
||||
schedule: {
|
||||
case: "cron",
|
||||
value: "0 0 1 * *",
|
||||
value: "0 0 1 * *", // 1st of the month
|
||||
},
|
||||
clock: Schedule_Clock.LAST_RUN_TIME,
|
||||
},
|
||||
@@ -68,7 +68,7 @@ const repoDefaults = create(RepoSchema, {
|
||||
schedule: {
|
||||
schedule: {
|
||||
case: "cron",
|
||||
value: "0 0 1 * *",
|
||||
value: "0 0 1 * *", // 1st of the month
|
||||
},
|
||||
clock: Schedule_Clock.LAST_RUN_TIME,
|
||||
},
|
||||
@@ -85,19 +85,26 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
|
||||
const [config, setConfig] = useConfig();
|
||||
|
||||
// Local state for form fields
|
||||
// Using a simple object state to mimic the form values
|
||||
const [formData, setFormData] = useState<any>(
|
||||
template
|
||||
? toJson(RepoSchema, template, { alwaysEmitImplicit: true })
|
||||
: toJson(RepoSchema, repoDefaults, { alwaysEmitImplicit: true }),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFormData(
|
||||
template
|
||||
? toJson(RepoSchema, template, { alwaysEmitImplicit: true })
|
||||
: toJson(RepoSchema, repoDefaults, { alwaysEmitImplicit: true }),
|
||||
);
|
||||
}, [template]);
|
||||
|
||||
const updateField = (path: string[], value: any) => {
|
||||
setFormData((prev: any) => {
|
||||
const next = { ...prev };
|
||||
let curr = next;
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
if (!curr[path[i]]) curr[path[i]] = {};
|
||||
curr[path[i]] = curr[path[i]] ? { ...curr[path[i]] } : {};
|
||||
curr = curr[path[i]];
|
||||
}
|
||||
curr[path[path.length - 1]] = value;
|
||||
@@ -116,6 +123,33 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
const validateLocal = async () => {
|
||||
const id = getField(["id"]);
|
||||
if (!id?.trim()) {
|
||||
throw new Error(m.add_repo_modal_error_repo_name_required());
|
||||
}
|
||||
if (!namePattern.test(id)) {
|
||||
throw new Error(m.add_plan_modal_validation_plan_name_pattern());
|
||||
}
|
||||
if (!template && config.repos.find((r) => r.id === id)) {
|
||||
throw new Error(m.add_repo_modal_error_repo_exists());
|
||||
}
|
||||
|
||||
const uri = getField(["uri"]);
|
||||
if (!uri?.trim()) {
|
||||
throw new Error(m.add_repo_modal_error_uri_required());
|
||||
}
|
||||
|
||||
// Env and Password validation
|
||||
await envVarSetValidator(formData);
|
||||
|
||||
// Flags validation
|
||||
const flags = getField(["flags"]);
|
||||
if (flags && flags.some((f: string) => !/^\-\-?.*$/.test(f))) {
|
||||
throw new Error(m.add_repo_modal_error_flag_format());
|
||||
}
|
||||
};
|
||||
|
||||
const handleDestroy = async () => {
|
||||
setConfirmLoading(true);
|
||||
try {
|
||||
@@ -134,7 +168,6 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
|
||||
} catch (e: any) {
|
||||
alerts.error(
|
||||
formatErrorAlert(e, m.add_plan_modal_error_destroy_prefix()),
|
||||
15,
|
||||
);
|
||||
} finally {
|
||||
setConfirmLoading(false);
|
||||
@@ -144,7 +177,8 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
|
||||
const handleOk = async () => {
|
||||
setConfirmLoading(true);
|
||||
try {
|
||||
// Validation logic would go here
|
||||
await validateLocal();
|
||||
|
||||
const repo = fromJson(RepoSchema, formData, {
|
||||
ignoreUnknownFields: true,
|
||||
});
|
||||
@@ -159,38 +193,81 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
|
||||
alerts.success(m.add_repo_modal_success_added({ uri: repo.uri }));
|
||||
}
|
||||
|
||||
// Trigger generic "check" or "list snapshots" to verify
|
||||
try {
|
||||
await backrestService.listSnapshots({ repoId: repo.id });
|
||||
} catch (e: any) {
|
||||
alerts.error(
|
||||
formatErrorAlert(e, m.add_repo_modal_error_list_snapshots()),
|
||||
10,
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
alerts.error(
|
||||
formatErrorAlert(e, m.add_plan_modal_error_operation_prefix()),
|
||||
10,
|
||||
);
|
||||
} finally {
|
||||
setConfirmLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const ioNiceOptions = createListCollection({
|
||||
items: CommandPrefix_IONiceLevelSchema.values.map((v) => ({
|
||||
label: v.name,
|
||||
value: v.name,
|
||||
})),
|
||||
});
|
||||
const handleTest = async () => {
|
||||
try {
|
||||
await validateLocal();
|
||||
const repo = fromJson(RepoSchema, formData, {
|
||||
ignoreUnknownFields: true,
|
||||
});
|
||||
const exists = await backrestService.checkRepoExists(repo);
|
||||
if (exists.value) {
|
||||
alerts.success(
|
||||
m.add_repo_modal_test_success_existing({ uri: repo.uri }),
|
||||
);
|
||||
} else {
|
||||
alerts.success(m.add_repo_modal_test_success_new({ uri: repo.uri }));
|
||||
}
|
||||
} catch (e: any) {
|
||||
alerts.error(formatErrorAlert(e, m.add_repo_modal_test_error()));
|
||||
}
|
||||
};
|
||||
|
||||
const cpuNiceOptions = createListCollection({
|
||||
items: CommandPrefix_CPUNiceLevelSchema.values.map((v) => ({
|
||||
label: v.name,
|
||||
value: v.name,
|
||||
})),
|
||||
});
|
||||
const ioNiceOptions: EnumOption<string>[] = [
|
||||
{
|
||||
label: "IO_BEST_EFFORT_LOW",
|
||||
value: "IO_BEST_EFFORT_LOW",
|
||||
description: m.add_repo_modal_field_io_priority_low(),
|
||||
},
|
||||
{
|
||||
label: "IO_BEST_EFFORT_HIGH",
|
||||
value: "IO_BEST_EFFORT_HIGH",
|
||||
description: m.add_repo_modal_field_io_priority_high(),
|
||||
},
|
||||
{
|
||||
label: "IO_IDLE",
|
||||
value: "IO_IDLE",
|
||||
description: m.add_repo_modal_field_io_priority_idle(),
|
||||
},
|
||||
{
|
||||
label: "IO_DEFAULT",
|
||||
value: "IO_DEFAULT",
|
||||
description: "Default system priority",
|
||||
},
|
||||
];
|
||||
|
||||
const cpuNiceOptions: EnumOption<string>[] = [
|
||||
{
|
||||
label: "CPU_DEFAULT",
|
||||
value: "CPU_DEFAULT",
|
||||
description: m.add_repo_modal_field_cpu_priority_default(),
|
||||
},
|
||||
{
|
||||
label: "CPU_HIGH",
|
||||
value: "CPU_HIGH",
|
||||
description: m.add_repo_modal_field_cpu_priority_high(),
|
||||
},
|
||||
{
|
||||
label: "CPU_LOW",
|
||||
value: "CPU_LOW",
|
||||
description: m.add_repo_modal_field_cpu_priority_low(),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<FormModal
|
||||
@@ -211,13 +288,16 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
|
||||
</Button>
|
||||
{template && (
|
||||
<ConfirmButton
|
||||
colorPalette="red"
|
||||
danger
|
||||
onClickAsync={handleDestroy}
|
||||
confirmTitle={m.add_plan_modal_button_confirm_delete()}
|
||||
>
|
||||
{m.add_plan_modal_button_delete()}
|
||||
</ConfirmButton>
|
||||
)}
|
||||
<SpinButton onClickAsync={handleTest}>
|
||||
{m.add_repo_modal_test_config()}
|
||||
</SpinButton>
|
||||
<Button loading={confirmLoading} onClick={handleOk}>
|
||||
{m.add_plan_modal_button_submit()}
|
||||
</Button>
|
||||
@@ -225,187 +305,467 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
|
||||
}
|
||||
>
|
||||
<Stack gap={6}>
|
||||
<Field
|
||||
label={m.add_repo_modal_field_repo_name()}
|
||||
helperText={m.add_repo_modal_field_repo_name_tooltip()}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
value={getField(["id"])}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
updateField(["id"], e.target.value)
|
||||
}
|
||||
disabled={!!template}
|
||||
placeholder={"repo" + ((config?.repos?.length || 0) + 1)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={m.add_repo_modal_field_uri()}
|
||||
helperText="Supports local paths, sftp, s3, etc."
|
||||
required
|
||||
>
|
||||
<URIAutocomplete
|
||||
disabled={!!template}
|
||||
value={getField(["uri"])}
|
||||
onChange={(val: string) => updateField(["uri"], val)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={m.add_repo_modal_field_password()}
|
||||
helperText={m.add_repo_modal_field_password_tooltip_intro()}
|
||||
>
|
||||
<Flex gap={2}>
|
||||
<PasswordInput
|
||||
value={getField(["password"])}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
updateField(["password"], e.target.value)
|
||||
}
|
||||
disabled={!!template}
|
||||
flex={1}
|
||||
/>
|
||||
{!template && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
updateField(["password"], cryptoRandomPassword())
|
||||
}
|
||||
>
|
||||
{m.add_repo_modal_button_generate()}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Field>
|
||||
|
||||
{/* Env Vars Placeholder */}
|
||||
<Field label={m.add_repo_modal_field_env_vars()}>
|
||||
<Textarea
|
||||
placeholder="KEY=VALUE (One per line)"
|
||||
value={getField(["env"]) || ""}
|
||||
onChange={(e) => updateField(["env"], e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label={m.add_repo_modal_field_auto_unlock()}>
|
||||
<Checkbox
|
||||
checked={getField(["autoUnlock"])}
|
||||
onCheckedChange={(e: { checked: boolean | "indeterminate" }) =>
|
||||
updateField(["autoUnlock"], !!e.checked)
|
||||
}
|
||||
<p>
|
||||
{m.add_repo_modal_guide_text_p1()}{" "}
|
||||
<a
|
||||
href="https://garethgeorge.github.io/backrest/introduction/getting-started"
|
||||
target="_blank"
|
||||
style={{ textDecoration: "underline" }}
|
||||
>
|
||||
{m.add_repo_modal_field_auto_unlock()}
|
||||
</Checkbox>
|
||||
</Field>
|
||||
{m.add_repo_modal_guide_link_text()}
|
||||
</a>{" "}
|
||||
{m.add_repo_modal_guide_text_p2()}{" "}
|
||||
<a
|
||||
href="https://restic.readthedocs.io/"
|
||||
target="_blank"
|
||||
style={{ textDecoration: "underline" }}
|
||||
>
|
||||
{m.add_repo_modal_guide_restic_link_text()}
|
||||
</a>{" "}
|
||||
{m.add_repo_modal_guide_text_p3()}
|
||||
</p>
|
||||
|
||||
<Field label={m.add_repo_modal_field_prune_policy()}>
|
||||
<Stack gap={4} p={4} borderWidth="1px" borderRadius="md">
|
||||
<NumberInputField
|
||||
label={m.add_repo_modal_field_max_unused()}
|
||||
value={getField(["prunePolicy", "maxUnusedPercent"])}
|
||||
onValueChange={(e: { value: string; valueAsNumber: number }) =>
|
||||
updateField(
|
||||
["prunePolicy", "maxUnusedPercent"],
|
||||
e.valueAsNumber,
|
||||
)
|
||||
<Section title="Repo Details">
|
||||
<Card.Root variant="subtle">
|
||||
<Card.Body>
|
||||
<Stack gap={4}>
|
||||
<Field
|
||||
label={m.add_repo_modal_field_repo_name()}
|
||||
helperText={
|
||||
!template
|
||||
? m.add_repo_modal_field_repo_name_tooltip()
|
||||
: undefined
|
||||
}
|
||||
required
|
||||
invalid={
|
||||
!!getField(["id"]) &&
|
||||
(!namePattern.test(getField(["id"])) ||
|
||||
(!template &&
|
||||
!!config.repos.find((r) => r.id === getField(["id"]))))
|
||||
}
|
||||
errorText={
|
||||
!!getField(["id"]) && !namePattern.test(getField(["id"]))
|
||||
? m.add_plan_modal_validation_plan_name_pattern()
|
||||
: m.add_repo_modal_error_repo_exists()
|
||||
}
|
||||
>
|
||||
<Input
|
||||
value={getField(["id"])}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
updateField(["id"], e.target.value)
|
||||
}
|
||||
disabled={!!template}
|
||||
placeholder={"repo" + ((config?.repos?.length || 0) + 1)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={m.add_repo_modal_field_uri()}
|
||||
helperText={
|
||||
<>
|
||||
{m.add_repo_modal_field_uri_tooltip_title()}
|
||||
<Box as="ul" ml={4} mt={1}>
|
||||
<li>{m.add_repo_modal_field_uri_tooltip_local()}</li>
|
||||
<li>{m.add_repo_modal_field_uri_tooltip_s3()}</li>
|
||||
<li>{m.add_repo_modal_field_uri_tooltip_sftp()}</li>
|
||||
<li>
|
||||
{m.add_repo_modal_field_uri_tooltip_see()}{" "}
|
||||
<a
|
||||
href="https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#preparing-a-new-repository"
|
||||
target="_blank"
|
||||
style={{ textDecoration: "underline" }}
|
||||
>
|
||||
{m.add_repo_modal_field_uri_tooltip_restic_docs()}
|
||||
</a>{" "}
|
||||
{m.add_repo_modal_field_uri_tooltip_info()}
|
||||
</li>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
required
|
||||
>
|
||||
<URIAutocomplete
|
||||
disabled={!!template}
|
||||
value={getField(["uri"])}
|
||||
onChange={(val: string) => updateField(["uri"], val)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={m.add_repo_modal_field_password()}
|
||||
helperText={
|
||||
!template ? (
|
||||
<>
|
||||
{m.add_repo_modal_field_password_tooltip_intro()}
|
||||
<Box as="ul" ml={4} mt={1}>
|
||||
<li>
|
||||
{m.add_repo_modal_field_password_tooltip_entropy()}
|
||||
</li>
|
||||
<li>
|
||||
{m.add_repo_modal_field_password_tooltip_env()}
|
||||
</li>
|
||||
<li>
|
||||
{m.add_repo_modal_field_password_tooltip_generate()}
|
||||
</li>
|
||||
</Box>
|
||||
</>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<Flex gap={2} width="full">
|
||||
<Box flex={1}>
|
||||
<PasswordInput
|
||||
value={getField(["password"])}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
updateField(["password"], e.target.value)
|
||||
}
|
||||
disabled={!!template}
|
||||
/>
|
||||
</Box>
|
||||
{!template && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
updateField(["password"], cryptoRandomPassword())
|
||||
}
|
||||
>
|
||||
{m.add_repo_modal_button_generate()}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Field>
|
||||
|
||||
<Field label={m.add_repo_modal_field_auto_unlock()}>
|
||||
<Checkbox
|
||||
checked={getField(["autoUnlock"])}
|
||||
onCheckedChange={(e: {
|
||||
checked: boolean | "indeterminate";
|
||||
}) => updateField(["autoUnlock"], !!e.checked)}
|
||||
>
|
||||
{m.add_repo_modal_field_auto_unlock()}
|
||||
</Checkbox>
|
||||
<CText fontSize="xs" color="fg.muted" ml={6}>
|
||||
{m.add_repo_modal_field_auto_unlock_tooltip()}
|
||||
</CText>
|
||||
</Field>
|
||||
</Stack>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
</Section>
|
||||
|
||||
<Section title="Environment & Flags">
|
||||
<Card.Root variant="subtle">
|
||||
<Card.Body>
|
||||
<Stack gap={4}>
|
||||
<DynamicList
|
||||
label={m.add_repo_modal_field_env_vars()}
|
||||
items={getField(["env"]) || []}
|
||||
onUpdate={(items: string[]) => updateField(["env"], items)}
|
||||
tooltip={m.add_repo_modal_field_env_vars_tooltip({
|
||||
MY_FOO_VAR: "$MY_FOO_VAR",
|
||||
})}
|
||||
placeholder="KEY=VALUE"
|
||||
/>
|
||||
|
||||
<DynamicList
|
||||
label={m.add_repo_modal_field_flags()}
|
||||
items={getField(["flags"]) || []}
|
||||
onUpdate={(items: string[]) => updateField(["flags"], items)}
|
||||
placeholder="--flag"
|
||||
/>
|
||||
</Stack>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title={
|
||||
<Tooltip
|
||||
content={
|
||||
<span>
|
||||
{m.add_repo_modal_field_prune_policy_tooltip_p1()}{" "}
|
||||
<a
|
||||
href="https://restic.readthedocs.io/en/stable/060_forget.html#customize-pruning"
|
||||
target="_blank"
|
||||
style={{ textDecoration: "underline" }}
|
||||
>
|
||||
{m.add_repo_modal_field_prune_policy_tooltip_link()}
|
||||
</a>{" "}
|
||||
{m.add_repo_modal_field_prune_policy_tooltip_p2()}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<Field label="Prune Schedule">
|
||||
<ScheduleFormItem
|
||||
value={getField(["prunePolicy", "schedule"])}
|
||||
onChange={(val: any) =>
|
||||
updateField(["prunePolicy", "schedule"], val)
|
||||
}
|
||||
defaults={ScheduleDefaultsDaily}
|
||||
/>
|
||||
</Field>
|
||||
</Stack>
|
||||
</Field>
|
||||
|
||||
<Field label="Backend Check Policy">
|
||||
<Stack gap={4} p={4} borderWidth="1px" borderRadius="md">
|
||||
<Field label="Check Schedule">
|
||||
<ScheduleFormItem
|
||||
value={getField(["checkPolicy", "schedule"])}
|
||||
onChange={(val: any) =>
|
||||
updateField(["checkPolicy", "schedule"], val)
|
||||
}
|
||||
defaults={ScheduleDefaultsDaily}
|
||||
/>
|
||||
</Field>
|
||||
</Stack>
|
||||
</Field>
|
||||
|
||||
{!isWindows && (
|
||||
<Flex gap={4}>
|
||||
<Field label={m.add_repo_modal_field_io_priority()} flex={1}>
|
||||
<SelectRoot
|
||||
collection={ioNiceOptions}
|
||||
size="sm"
|
||||
value={[getField(["commandPrefix", "ioNice"])]}
|
||||
onValueChange={(e: any) =>
|
||||
updateField(["commandPrefix", "ioNice"], e.value[0])
|
||||
}
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
<SelectTrigger>
|
||||
{/* @ts-ignore */}
|
||||
<SelectValueText placeholder="Select priority" />
|
||||
</SelectTrigger>
|
||||
{/* @ts-ignore */}
|
||||
<SelectContent>
|
||||
{ioNiceOptions.items.map((item: any) => (
|
||||
// @ts-ignore
|
||||
<SelectItem item={item} key={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</SelectRoot>
|
||||
</Field>
|
||||
<Field label={m.add_repo_modal_field_cpu_priority()} flex={1}>
|
||||
<SelectRoot
|
||||
collection={cpuNiceOptions}
|
||||
size="sm"
|
||||
value={[getField(["commandPrefix", "cpuNice"])]}
|
||||
onValueChange={(e: any) =>
|
||||
updateField(["commandPrefix", "cpuNice"], e.value[0])
|
||||
}
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
<SelectTrigger>
|
||||
{/* @ts-ignore */}
|
||||
<SelectValueText placeholder="Select priority" />
|
||||
</SelectTrigger>
|
||||
{/* @ts-ignore */}
|
||||
<SelectContent>
|
||||
{cpuNiceOptions.items.map((item: any) => (
|
||||
// @ts-ignore
|
||||
<SelectItem item={item} key={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</SelectRoot>
|
||||
</Field>
|
||||
</Flex>
|
||||
)}
|
||||
<Field
|
||||
label={m.add_plan_modal_field_hooks()}
|
||||
helperText={hooksListTooltipText}
|
||||
>
|
||||
{m.add_repo_modal_field_prune_policy()}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<HooksFormList
|
||||
value={getField(["hooks"])}
|
||||
onChange={(v: any) => updateField(["hooks"], v)}
|
||||
/>
|
||||
</Field>
|
||||
<Card.Root variant="subtle" size="sm">
|
||||
<Card.Body>
|
||||
<Stack gap={4}>
|
||||
<NumberInputField
|
||||
label={m.add_repo_modal_field_max_unused()}
|
||||
helperText={m.add_repo_modal_field_max_unused_tooltip()}
|
||||
value={getField(["prunePolicy", "maxUnusedPercent"])}
|
||||
onValueChange={(e: {
|
||||
value: string;
|
||||
valueAsNumber: number;
|
||||
}) =>
|
||||
updateField(
|
||||
["prunePolicy", "maxUnusedPercent"],
|
||||
e.valueAsNumber,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ScheduleFormItem
|
||||
value={getField(["prunePolicy", "schedule"])}
|
||||
onChange={(val: any) =>
|
||||
updateField(["prunePolicy", "schedule"], val)
|
||||
}
|
||||
defaults={ScheduleDefaultsInfrequent}
|
||||
/>
|
||||
</Stack>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title={
|
||||
<Tooltip content={m.add_repo_modal_field_check_policy_tooltip()}>
|
||||
{m.add_repo_modal_field_check_policy()}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Card.Root variant="subtle" size="sm">
|
||||
<Card.Body>
|
||||
<Stack gap={4}>
|
||||
<NumberInputField
|
||||
label={m.add_repo_modal_field_read_data()}
|
||||
helperText={m.add_repo_modal_field_read_data_tooltip()}
|
||||
value={getField(["checkPolicy", "readDataSubsetPercent"])}
|
||||
onValueChange={(e: {
|
||||
value: string;
|
||||
valueAsNumber: number;
|
||||
}) =>
|
||||
updateField(
|
||||
["checkPolicy", "readDataSubsetPercent"],
|
||||
e.valueAsNumber,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ScheduleFormItem
|
||||
value={getField(["checkPolicy", "schedule"])}
|
||||
onChange={(val: any) =>
|
||||
updateField(["checkPolicy", "schedule"], val)
|
||||
}
|
||||
defaults={ScheduleDefaultsInfrequent}
|
||||
/>
|
||||
</Stack>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
</Section>
|
||||
|
||||
<Section title="Advanced">
|
||||
<Card.Root variant="subtle">
|
||||
<Card.Body>
|
||||
<Stack gap={4} width="full">
|
||||
{!isWindows && (
|
||||
<Field label={m.add_repo_modal_field_command_modifiers()}>
|
||||
<Grid templateColumns="1fr 1fr" gap={4} width="full">
|
||||
<Field
|
||||
label={m.add_repo_modal_field_io_priority()}
|
||||
helperText={m.add_repo_modal_field_io_priority_tooltip_intro()}
|
||||
>
|
||||
<EnumSelector
|
||||
options={ioNiceOptions}
|
||||
size="sm"
|
||||
value={getField(["commandPrefix", "ioNice"])}
|
||||
onChange={(val) =>
|
||||
updateField(
|
||||
["commandPrefix", "ioNice"],
|
||||
val as string,
|
||||
)
|
||||
}
|
||||
placeholder={m.add_repo_modal_field_io_priority_placeholder()}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={m.add_repo_modal_field_cpu_priority()}
|
||||
helperText={m.add_repo_modal_field_cpu_priority_tooltip_intro()}
|
||||
>
|
||||
<EnumSelector
|
||||
options={cpuNiceOptions}
|
||||
size="sm"
|
||||
value={getField(["commandPrefix", "cpuNice"])}
|
||||
onChange={(val) =>
|
||||
updateField(
|
||||
["commandPrefix", "cpuNice"],
|
||||
val as string,
|
||||
)
|
||||
}
|
||||
placeholder={m.add_repo_modal_field_cpu_priority_placeholder()}
|
||||
/>
|
||||
</Field>
|
||||
</Grid>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field
|
||||
label={m.add_plan_modal_field_hooks()}
|
||||
helperText={hooksListTooltipText}
|
||||
>
|
||||
<HooksFormList
|
||||
value={getField(["hooks"])}
|
||||
onChange={(v: any) => updateField(["hooks"], v)}
|
||||
/>
|
||||
</Field>
|
||||
</Stack>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
</Section>
|
||||
|
||||
{/* JSON Preview */}
|
||||
<AccordionRoot collapsible variant="plain">
|
||||
<AccordionItem value="json-preview">
|
||||
<AccordionItemTrigger>
|
||||
<CText fontSize="sm" color="fg.muted">
|
||||
{m.add_repo_modal_preview_json()}
|
||||
</CText>
|
||||
</AccordionItemTrigger>
|
||||
<AccordionItemContent>
|
||||
<Code
|
||||
display="block"
|
||||
whiteSpace="pre"
|
||||
overflowX="auto"
|
||||
p={2}
|
||||
borderRadius="md"
|
||||
fontSize="xs"
|
||||
>
|
||||
{JSON.stringify(formData, null, 2)}
|
||||
</Code>
|
||||
</AccordionItemContent>
|
||||
</AccordionItem>
|
||||
</AccordionRoot>
|
||||
</Stack>
|
||||
</FormModal>
|
||||
);
|
||||
};
|
||||
|
||||
const Section = ({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<Stack gap={2}>
|
||||
<CText fontWeight="semibold" fontSize="sm">
|
||||
{title}
|
||||
</CText>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
// Utils
|
||||
const cryptoRandomPassword = (): string => {
|
||||
let vals = crypto.getRandomValues(new Uint8Array(64));
|
||||
return btoa(String.fromCharCode(...vals)).slice(0, 48);
|
||||
};
|
||||
|
||||
// Validation Logic
|
||||
const expectedEnvVars: { [scheme: string]: string[][] } = {
|
||||
s3: [
|
||||
["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
|
||||
["AWS_SHARED_CREDENTIALS_FILE"],
|
||||
],
|
||||
b2: [["B2_ACCOUNT_ID", "B2_ACCOUNT_KEY"]],
|
||||
azure: [
|
||||
["AZURE_ACCOUNT_NAME", "AZURE_ACCOUNT_KEY"],
|
||||
["AZURE_ACCOUNT_NAME", "AZURE_ACCOUNT_SAS"],
|
||||
],
|
||||
gs: [
|
||||
["GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_PROJECT_ID"],
|
||||
["GOOGLE_ACCESS_TOKEN"],
|
||||
],
|
||||
};
|
||||
|
||||
const envVarSetValidator = async (formData: any) => {
|
||||
const envVars = formData.env || [];
|
||||
const uri = formData.uri;
|
||||
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
const envVarNames = envVars.map((e: string) => {
|
||||
if (!e) return "";
|
||||
let idx = e.indexOf("=");
|
||||
if (idx === -1) return "";
|
||||
return e.substring(0, idx);
|
||||
});
|
||||
|
||||
const password = formData.password;
|
||||
if (
|
||||
(!password || password.length === 0) &&
|
||||
!envVarNames.includes("RESTIC_PASSWORD") &&
|
||||
!envVarNames.includes("RESTIC_PASSWORD_COMMAND") &&
|
||||
!envVarNames.includes("RESTIC_PASSWORD_FILE")
|
||||
) {
|
||||
throw new Error(m.add_repo_modal_error_missing_password());
|
||||
}
|
||||
|
||||
let schemeIdx = uri.indexOf(":");
|
||||
if (schemeIdx === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let scheme = uri.substring(0, schemeIdx);
|
||||
await checkSchemeEnvVars(scheme, envVarNames);
|
||||
};
|
||||
|
||||
const checkSchemeEnvVars = async (
|
||||
scheme: string,
|
||||
envVarNames: string[],
|
||||
): Promise<void> => {
|
||||
let expected = expectedEnvVars[scheme];
|
||||
if (!expected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const missingVarsCollection: string[][] = [];
|
||||
|
||||
for (let possibility of expected) {
|
||||
const missingVars = possibility.filter(
|
||||
(envVar) => !envVarNames.includes(envVar),
|
||||
);
|
||||
|
||||
if (missingVars.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (missingVars.length < possibility.length) {
|
||||
missingVarsCollection.push(missingVars);
|
||||
}
|
||||
}
|
||||
|
||||
if (!missingVarsCollection.length) {
|
||||
missingVarsCollection.push(...expected);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Missing env vars " +
|
||||
formatMissingEnvVars(missingVarsCollection) +
|
||||
" for scheme " +
|
||||
scheme,
|
||||
);
|
||||
};
|
||||
|
||||
const formatMissingEnvVars = (partialMatches: string[][]): string => {
|
||||
return partialMatches
|
||||
.map((x) => {
|
||||
if (x.length > 1) {
|
||||
return `[ ${x.join(", ")} ]`;
|
||||
}
|
||||
return x[0];
|
||||
})
|
||||
.join(" or ");
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { Suspense, useContext, useEffect, useState } from "react";
|
||||
import { Repo } from "../../../gen/ts/v1/config_pb";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Flex, Heading, Box } from "@chakra-ui/react";
|
||||
import { Flex, Heading, Box, Group, IconButton } from "@chakra-ui/react";
|
||||
import { FiChevronDown } from "react-icons/fi";
|
||||
import {
|
||||
Tabs,
|
||||
TabsRoot,
|
||||
@@ -9,6 +10,12 @@ import {
|
||||
TabsTrigger,
|
||||
TabsContent,
|
||||
} from "../../components/ui/tabs";
|
||||
import {
|
||||
MenuContent,
|
||||
MenuItem,
|
||||
MenuRoot,
|
||||
MenuTrigger,
|
||||
} from "../../components/ui/menu";
|
||||
import { Tooltip } from "../../components/ui/tooltip";
|
||||
import { OperationListView } from "../operations/OperationListView";
|
||||
import { OperationTreeView } from "../operations/OperationTreeView";
|
||||
@@ -125,49 +132,48 @@ export const RepoView = ({
|
||||
<Box>
|
||||
<Flex gap={4} align="center" wrap="wrap" mb={4}>
|
||||
<Heading size="xl">{repo.id}</Heading>
|
||||
<Box flex="1" />
|
||||
|
||||
<Tooltip content={m.repo_tooltip_run_command()}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
const { RunCommandModal } =
|
||||
await import("../operations/RunCommandModal");
|
||||
showModal(<RunCommandModal repo={repo} />);
|
||||
}}
|
||||
>
|
||||
{m.repo_button_run_command()}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={m.repo_tooltip_index()}>
|
||||
<SpinButton type="default" onClickAsync={handleIndexNow}>
|
||||
<Group attached>
|
||||
<SpinButton type="primary" onClickAsync={handleIndexNow}>
|
||||
{m.repo_button_index()}
|
||||
</SpinButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={m.repo_tooltip_unlock()}>
|
||||
<SpinButton type="default" onClickAsync={handleUnlockNow}>
|
||||
{m.repo_button_unlock()}
|
||||
</SpinButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={m.repo_tooltip_prune()}>
|
||||
<SpinButton type="default" onClickAsync={handlePruneNow}>
|
||||
{m.repo_button_prune()}
|
||||
</SpinButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={m.repo_tooltip_check()}>
|
||||
<SpinButton type="default" onClickAsync={handleCheckNow}>
|
||||
{m.repo_button_check()}
|
||||
</SpinButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={m.repo_tooltip_stats()}>
|
||||
<SpinButton type="default" onClickAsync={handleStatsNow}>
|
||||
{m.repo_button_stats()}
|
||||
</SpinButton>
|
||||
</Tooltip>
|
||||
<MenuRoot>
|
||||
<MenuTrigger asChild>
|
||||
<IconButton
|
||||
variant="subtle"
|
||||
colorPalette="blue"
|
||||
aria-label="More actions"
|
||||
>
|
||||
<FiChevronDown />
|
||||
</IconButton>
|
||||
</MenuTrigger>
|
||||
<MenuContent>
|
||||
<MenuItem
|
||||
value="run-command"
|
||||
onClick={async () => {
|
||||
const { RunCommandModal } =
|
||||
await import("../operations/RunCommandModal");
|
||||
showModal(<RunCommandModal repo={repo} />);
|
||||
}}
|
||||
>
|
||||
{m.repo_button_run_command()}
|
||||
</MenuItem>
|
||||
<MenuItem value="unlock" onClick={handleUnlockNow}>
|
||||
{m.repo_button_unlock()}
|
||||
</MenuItem>
|
||||
<MenuItem value="prune" onClick={handlePruneNow}>
|
||||
{m.repo_button_prune()}
|
||||
</MenuItem>
|
||||
<MenuItem value="check" onClick={handleCheckNow}>
|
||||
{m.repo_button_check()}
|
||||
</MenuItem>
|
||||
<MenuItem value="stats" onClick={handleStatsNow}>
|
||||
{m.repo_button_stats()}
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuRoot>
|
||||
</Group>
|
||||
</Flex>
|
||||
|
||||
<TabsRoot defaultValue="tree" lazyMount>
|
||||
|
||||
@@ -172,9 +172,9 @@ export const SettingsModal = () => {
|
||||
|
||||
setConfig(await backrestService.setConfig(newConfig));
|
||||
setReloadOnCancel(true);
|
||||
alerts.success(m.settings_success_updated(), 5);
|
||||
alerts.success(m.settings_success_updated());
|
||||
} catch (e: any) {
|
||||
alerts.error(formatErrorAlert(e, m.settings_error_operation()), 15);
|
||||
alerts.error(formatErrorAlert(e, m.settings_error_operation()));
|
||||
} finally {
|
||||
setConfirmLoading(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user