diff --git a/CHANGES b/CHANGES index 8651b2200..00b36cbac 100644 --- a/CHANGES +++ b/CHANGES @@ -2777,4 +2777,5 @@ * dashboard: set '/' as keyboard shortcut * app: memory limit is redefined to be just RAM and unlimited swap * dashboard: rework filter UI +* cpu: rework cpu shares into cpu quota diff --git a/dashboard/src/js/client.js b/dashboard/src/js/client.js index 413639d5e..435b35a3f 100644 --- a/dashboard/src/js/client.js +++ b/dashboard/src/js/client.js @@ -3705,6 +3705,8 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout return 'Memory limit ' + appName('of', app) + ' was set to ' + data.memoryLimit; } else if (data.cpuShares) { return 'CPU shares ' + appName('of', app) + ' was set to ' + Math.round((data.cpuShares * 100)/1024) + '%'; + } else if (data.cpuQuota) { + return 'CPU quota ' + appName('of', app) + ' was set to ' + data.cupQuota + '%'; } else if (data.env) { return 'Env vars ' + appName('of', app) + ' was changed'; } else if ('debugMode' in data) { // since it can be null diff --git a/dashboard/src/translation/en.json b/dashboard/src/translation/en.json index 3021242ed..fb0d3fc13 100644 --- a/dashboard/src/translation/en.json +++ b/dashboard/src/translation/en.json @@ -1500,8 +1500,8 @@ }, "cpu": { "setAction": "Set", - "title": "CPU Shares", - "description": "Percent of CPU time when system is under heavy load." + "title": "CPU Limit", + "description": "Maximum percent of CPU app can use" } }, "storage": { diff --git a/dashboard/src/translation/fr.json b/dashboard/src/translation/fr.json index fe5370e55..68291da41 100644 --- a/dashboard/src/translation/fr.json +++ b/dashboard/src/translation/fr.json @@ -156,7 +156,8 @@ "description": "Cloudron va importer les utilisateurs et les groupes depuis un annuaire LDAP externe ou Active Directory. La vérification du mot de passe pour l'authentification de ces utilisateurs se fait via le serveur externe. La synchronisation ne s'exécute pas automatiquement, elle doit être lancée manuellement.", "subscriptionRequiredAction": "Paramétrer mon abonnement maintenant", "providerOther": "Autre", - "providerDisabled": "Désactivé" + "providerDisabled": "Désactivé", + "disableWarning": "La source d'authentification de tous les utilisateurs existants sera réinitialisée pour utiliser la base de données locale." }, "role": { "usermanager": "Gestionnaire", @@ -188,7 +189,9 @@ "errorInvalidEmail": "Cette adresse email est invalide", "usernamePlaceholder": "Optionnel. Si laissé vide, l'utilisateur peut en choisir un lors de la première connexion", "fallbackEmailPlaceholder": "Optionnel. Si laissé vide, ce sera l'adresse email principale qui sera utilisée", - "displayNamePlaceholder": "Optionnel. Si laissé vide, l'utilisateur peut en choisir un lors de la création du compte" + "displayNamePlaceholder": "Optionnel. Si laissé vide, l'utilisateur peut en choisir un lors de la création du compte", + "external2FA": "La configuration multi-facteur est gérée par une source externe", + "ldapGroups": "Groupes LDAP" }, "group": { "errorNameRequired": "Un nom est nécessaire", @@ -271,7 +274,7 @@ }, "exposedLdap": { "secret": { - "label": "Mot de passe de liaison", + "label": "Mot de passe Bind", "description": "Toutes les requêtes LDAP doivent être authentifiées avec ce secret et le DN utilisateur {{ userDN }}", "url": "URL du serveur" }, @@ -282,7 +285,8 @@ "placeholder": "Adresse IP séparée par ligne ou sous-réseau", "label": "Accès restreint" }, - "title": "Serveur d'annuaire" + "title": "Serveur d'annuaire", + "cloudflarePortWarning": "Le proxy Cloudflare doit être désactivé sur le domaine du tableau de bord pour accéder au service LDAP" }, "userImportDialog": { "title": "Importer des utilisateurs", @@ -345,7 +349,10 @@ "changeEmail": { "errorEmailInvalid": "Cette adresse email est invalide", "title": "Modifier l'adresse email principale", - "errorEmailRequired": "Une adresse email valide est nécessaire" + "errorEmailRequired": "Une adresse email valide est nécessaire", + "email": "Nouvelle adresse e-mail", + "password": "Mot de passe pour confirmation", + "errorWrongPassword": "Mauvais mot de passe" }, "createAppPassword": { "copyNow": "Veillez à copier le mot de passe maintenant. Il ne s'affichera plus pour des raisons de sécurité.", @@ -416,7 +423,8 @@ }, "changeBackgroundImage": { "title": "Définir l'image d'arrière-plan" - } + }, + "enable2FANotAvailable": "Non disponible pour les utilisateurs provenant d'une source d'authentification externe" }, "backups": { "title": "Sauvegardes", @@ -500,7 +508,7 @@ "cifsSealSupport": "Utilisez le cryptage du sceau. Nécessite au moins SMB v3", "chown": "Le système de fichiers distant prend en charge chown", "encryptedFilenames": "Crypter les noms de fichiers", - "encryptFilenames": "Fichiers Cryptés" + "encryptFilenames": "Chiffré les nom de fichiers" }, "backupDetails": { "title": "Informations sur la sauvegarde", @@ -543,7 +551,7 @@ "description": "Sauvegarde persistante quelle que soit la politique de rétention", "tooltip": "Cela préservera également le courrier et les sauvegardes d'application {{ appsLength }}." }, - "remotePath": "Répertoire Distant" + "remotePath": "Chemin d'accès à distance" } }, "emails": { @@ -553,7 +561,7 @@ "location": "Emplacement", "title": "Changer l'emplacement du serveur de messagerie", "locationPlaceholder": "Laisser vide pour utiliser le nom de domaine nu", - "description": "Cloudron effectuera les modifications DNS nécessaires pour l'ensemble des domaines et redémarrera le serveur de messagerie. Les clients de messagerie sur ordinateur et sur mobile doivent être reconfigurés pour que ce nouvel emplacement soit utilisé comme serveur IMAP et SMTP." + "description": "Cela déplacera le serveur IMAP et SMTP vers l'emplacement choisi." }, "eventlog": { "details": "Détails", @@ -663,6 +671,10 @@ }, "action": { "queue": "File d'attente" + }, + "changeVirtualAllMailDialog": { + "title": "Dossier \"Tout les Emails\"", + "description": "Le dossier \"Tout les E-mails\" est un dossier contenant tout les e-mails de votre boite de réception. Ce dossier peut être utile pour les clients e-mails ne supportant pas les dossiers imbriqués." } }, "network": { @@ -679,7 +691,8 @@ }, "dyndns": { "title": "DNS dynamique", - "description": "Activez cette option pour conserver tous vos enregistrements DNS synchronisés avec une adresse IP dynamique. Cette option est utile lorsque Cloudron fonctionne avec un réseau dont l'adresse IP publique change fréquemment, comme dans le cas d'une connexion domestique." + "description": "Activez cette option pour conserver tous vos enregistrements DNS synchronisés avec une adresse IP dynamique. Cette option est utile lorsque Cloudron fonctionne avec un réseau dont l'adresse IP publique change fréquemment, comme dans le cas d'une connexion domestique.", + "showLogsAction": "Afficher les journaux" }, "ip": { "configure": "Paramétrer", @@ -705,7 +718,13 @@ "address": "Adresse IPv6", "title": "IPv6", "description": "Cloudron utilise cette adresse IPv6 pour configurer les enregistrements DNS AAAA.\n" - } + }, + "trustedIps": { + "description": "Les en-têtes HTTP provenant d'adresses IP correspondantes seront considérés comme sûrs", + "title": "Configurer les adresses IP de Confiance", + "summary": "{{ trustCount }} adresses IP de confiance" + }, + "trustedIpRanges": "Adresses et plages d'IP de confiance. " }, "settings": { "title": "Paramètres", @@ -809,7 +828,12 @@ "subscriptionRequiredDescription": "Vous devriez trouver votre réponse dans notre documentation, vous pouvez également poser votre question sur le forum.", "title": "Ticket", "emailVerifyAction": "Confirmer maintenant", - "emailNotVerified": "L'adresse email de votre compte Cloudron.io {{ email }} n'a pas encore été confirmée. Veuillez la valider pour ouvrir des tickets d'incident." + "emailNotVerified": "L'adresse email de votre compte Cloudron.io {{ email }} n'a pas encore été confirmée. Veuillez la valider pour ouvrir des tickets d'incident.", + "typeBilling": "Problème de facturation" + }, + "help": { + "description": "Veuillez utiliser les ressources suivantes pour obtenir de l'aide\n* [Forum Cloudron]({{ forumLink }}) - Veuillez utiliser les catégories d'assistance et d'applications spécifiques pour vos questions.\n* [Documentation et base de connaissances de Cloudron]({{ docsLink }})\n* [Packaging d'applications personnalisées et API]({{ packagingLink }})\n", + "title": "Aide" } }, "notifications": { @@ -917,7 +941,8 @@ "packageVersion": "Version du package", "appId": "ID de l'application", "description": "Nom et version de l'application", - "title": "Informations sur l'application" + "title": "Informations sur l'application", + "repository": "Dépot de paquets" }, "auto": { "title": "Mises à jour automatiques", @@ -926,7 +951,8 @@ "disabled": "Les mises à jour automatiques sont actuellement désactivées.", "enabled": "Les mises à jour automatiques sont actuellement activées.", "description": "Cloudron vérifie régulièrement les mises à jour disponibles dans l'App Store. Si vous désactivez les mises à jour automatiques, veillez à les faire manuellement." - } + }, + "noUpdates": "Aucune nouvelle mise à jour disponible" }, "backupsTabTitle": "Sauvegardes", "storage": { @@ -936,13 +962,20 @@ "noMounts": "Aucun volume n'est monté.", "volume": "Volume", "readOnly": "En lecture seule", - "title": "Montages" + "title": "Montages", + "permissions": { + "label": "Permissions", + "readOnly": "Lecture seule", + "readWrite": "Lecture et écriture" + } }, "appdata": { "moveAction": "Déplacer les données", "dataDirPlaceholder": "Laisser vide pour utiliser la plateforme par défaut", "description": "Si le serveur manque d'espace disque, utilisez-le pour déplacer les données de l'application vers un volume. Toutes les données ici font partie de la sauvegarde de l'application.", - "title": "Données de l'application" + "title": "Données de l'application", + "diskUsage": "L'application utilise actuellement {{ size }} de stockage (en date du {{ date }}).", + "mountTypeWarning": "Le système de fichiers de destination doit prendre en charge les autorisations et la propriété des fichiers pour que le transfert fonctionne" } }, "security": { @@ -955,7 +988,8 @@ "disableIndexingAction": "Désactiver l'indexation", "txtPlaceholder": "Laisser vide pour autoriser les robots à indexer cette application", "title": "Robots.txt" - } + }, + "hstsPreload": "Activer HSTS pour ce site et tous les sous-domaines" }, "updateDialog": { "updateAction": "Mettre à jour", @@ -1049,7 +1083,8 @@ "uploadAction": "Charger le fichier de configuration de la sauvegarde", "description": "Toutes les données créées depuis la dernière sauvegarde connue seront définitivement perdues. Il est fortement recommandé de sauvegarder les données actuelles avant de lancer un import.", "title": "Importer la sauvegarde", - "importAction": "Importer" + "importAction": "Importer", + "remotePath": "Chemin de la sauvegarde" }, "repairDialog": { "fromBackup": "Restaurer depuis la sauvegarde :", @@ -1120,7 +1155,8 @@ "time": "Créée le", "packageVersion": "Version du package", "description": "Les sauvegardes sont des instantanés complets de l'application. Vous pouvez utiliser les sauvegardes pour restaurer ou cloner l'application.", - "title": "Sauvegardes" + "title": "Sauvegardes", + "downloadBackupTooltip": "Télécharger la sauvegarde" } }, "graphs": { @@ -1132,7 +1168,9 @@ "12h": "12 heures", "6h": "6 heures" }, - "diskTitle": "Utilisation du disque" + "diskTitle": "Utilisation du disque", + "diskIOTotal": "total: lecture {{ read }} / écriture {{ write }}", + "networkIOTotal": "total : entrant {{ inbound }} / sortant {{ outbound }}" }, "resources": { "memory": { @@ -1221,12 +1259,25 @@ "label": "Étiquette", "clearIconAction": "Effacer Icône", "clearIconDescription": "Cela récupérera le favicon de l'application." + }, + "servicesTabTitle": "Services", + "turn": { + "enable": "Configurer l'application pour utiliser le serveur TURN intégré", + "disable": "Ne pas configurer les paramètres TURN de l'application. Les paramètres TURN de l'application sont laissés à leur valeurs par défaut. Vous pouvez les configurer à l'intérieur de l'application.", + "title": "Configuration de TURN" + }, + "redis": { + "title": "Configuration de Redis", + "enable": "Configurer l'application pour utiliser Redis", + "disable": "Désactiver Redis" } }, "logs": { "title": "Journaux", "download": "Télécharger l'ensemble des journaux", - "clear": "Nettoyer" + "clear": "Nettoyer", + "notFoundError": "Aucune tâche ou application de ce type", + "logsGoneError": "Fichier(s) journal(s) introuvable(s)" }, "volumes": { "name": "Nom", @@ -1263,7 +1314,11 @@ "title": "Mettre à jour le volume {{ volume }}" }, "mountStatus": "Statut du montage", - "type": "Type" + "type": "Type", + "editVolumeDialog": { + "title": "Modifier le volume {{ name }}" + }, + "editActionTooltip": "Modifier le volume" }, "lang": { "en": "Anglais", @@ -1277,7 +1332,8 @@ "zh_Hans": "Chinois (Simplifié)", "es": "Espagnol", "ru": "Russe", - "pt": "Portugais" + "pt": "Portugais", + "da": "Danois" }, "email": { "mailboxboxDialog": { @@ -1522,10 +1578,19 @@ "vultrToken": "Token Vultr", "wellKnownDescription": "Les valeurs seront utilisées par Cloudron pour répondre aux URL /.well-known/. Notez qu'une application doit être disponible sur le domaine nu {{ domaine }} pour que cela fonctionne. Consultez la documentation pour plus d'informations.", "hetznerToken": "Token Hetzner", - "jitsiHostname": "Emplacement de Jitsi" + "jitsiHostname": "Emplacement de Jitsi", + "cloudflareDefaultProxyStatus": "Activer le proxy pour les nouveaux enregistrements DNS", + "porkbunApikey": "Clé API", + "porkbunSecretapikey": "Clé API secrète", + "dnsimpleAccessToken": "Jeton d'accès", + "ovhEndpoint": "Point de terminaison", + "bunnyAccessKey": "Bunny Access Key", + "ovhConsumerKey": "Consumer Key", + "ovhAppKey": "Application Key", + "ovhAppSecret": "Application Secret" }, "changeDashboardDomain": { - "description": "Cette action entraînera le déplacement du tableau de bord et du serveur de messagerie vers le sous-domaine my du domaine sélectionné.", + "description": "Cette action entraînera le déplacement du tableau de bord vers le sous-domaine my du domaine sélectionné.", "showLogsAction": "Afficher les journaux", "cancelAction": "Annuler", "changeAction": "Changer le domaine", @@ -1551,7 +1616,8 @@ "domainWellKnown": { "title": "Emplacements Well-Known de {{ domain }}" }, - "tooltipWellKnown": "Définir des emplacements Well-Known" + "tooltipWellKnown": "Définir des emplacements Well-Known", + "count": "Nombre de domaines: {{ count }}" }, "branding": { "footer": { @@ -1632,7 +1698,8 @@ "download": "Télécharger", "extract": "Extraire ici", "chown": "Modifier la propriété", - "rename": "Renommer" + "rename": "Renommer", + "open": "Ouvrir" }, "symlink": "Symlink vers {{ target }}", "empty": "Aucun fichier", @@ -1678,7 +1745,8 @@ "renameDialog": { "rename": "Renommer", "newName": "Nouveau nom", - "title": "Renommer {{ fileName }}" + "title": "Renommer {{ fileName }}", + "reallyOverwrite": "Un fichier portant ce nom existe déjà. Écraser le fichier existant ?" }, "newFileDialog": { "create": "Créer", @@ -1691,7 +1759,19 @@ "removeDialog": { "reallyDelete": "Voulez-vous vraiment supprimer ces fichiers ?" }, - "title": "Gestionnaire de fichiers" + "title": "Gestionnaire de fichiers", + "uploader": { + "uploading": "Téléversement", + "exitWarning": "Téléversement toujours en cours. Voulez-vous vraiment fermer cette page ?" + }, + "deleteInProgress": "Suppression en cours", + "textEditor": { + "undo": "Annuler", + "redo": "Refaire", + "save": "Enregistrer" + }, + "extractionInProgress": "Décompression en cours", + "pasteInProgress": "Collage en cours" }, "terminal": { "contextmenu": { @@ -1732,7 +1812,8 @@ "selectPeriodLabel": "Période sélectionnée", "cpuUsage": { "graphTitle": "Pourcentage", - "title": "Utilisation du microprocesseur" + "title": "Utilisation du microprocesseur", + "graphSubtext": "Seules les applications utilisant plus de {{ threshold }} de processeur sont affichées" }, "systemMemory": { "graphSubtext": "Seules les applications utilisant plus de 1GB de mémoire sont affichées", @@ -1746,9 +1827,22 @@ "title": "Utilisation du disque", "usedInfo": "{{ used }} utilisé de {{ size }}", "uninstalledApp": "Désinstaller App", - "diskSpeed": "Vitesse : {{ speed }} MB/sec" + "diskSpeed": "Vitesse : {{ speed }} MB/sec", + "volumeContent": "Ce disque est le volume {{ name }}" }, - "title": "Info système" + "title": "Info système", + "info": { + "platformVersion": "Version de la Plate-forme", + "vendor": "Vendeur", + "product": "Produit", + "memory": "Mémoire", + "uptime": "Durée de fonctionnement", + "activationTime": "Heure de création de Cloudron", + "title": "Informations" + }, + "graphs": { + "title": "Graphiques" + } }, "services": { "refresh": "Rafraîchir", @@ -1803,7 +1897,9 @@ "password": "Mot de passe", "username": "Nom d'utilisateur", "errorIncorrectCredentials": "Nom d'utilisateur ou mot de passe incorrect", - "loginTo": "Se connecter à" + "loginTo": "Se connecter à", + "errorIncorrect2FAToken": "Le jeton 2FA n'est pas valide", + "errorInternal": "Erreur interne, réessayer ultérieurement" }, "newLoginEmail": { "salutation": "Bonjour <%= user %>,", @@ -1819,5 +1915,43 @@ "mounts": { "description": "Les applications peuvent accéder aux volumes montés via le répertoire /media/{volume name}. Ces données ne sont pas incluses dans la sauvegarde de l'application." } - } + }, + "oidc": { + "client": { + "signingAlgorithm": "Algorithme de signature", + "name": "Nom", + "id": "ID du client", + "secret": "Secret du client", + "loginRedirectUri": "Url de retour post connexion (séparées par des virgules s'il y en a plus d'une)", + "logoutRedirectUri": "Url de retour après déconnexion (facultatif)" + }, + "description": "Cloudron peut agir en tant que fournisseur OpenID Connect pour les applications internes et les services externes.", + "deleteClientDialog": { + "description": "Cela déconnectera toutes les applications OpenID externes de ce Cloudron utilisant cet identifiant client.", + "title": "Supprimer définitivement le client {{ client }} ?" + }, + "newClientDialog": { + "title": "Ajouter un client", + "description": "Ajouter de nouveaux paramètres pour le client OpenID connect.", + "createAction": "Créer" + }, + "title": "OpenID Connect Provider", + "editClientDialog": { + "title": "Modifier le client {{ client }}" + }, + "env": { + "discoveryUrl": "URL de découverte", + "logoutUrl": "URL de déconnexion", + "profileEndpoint": "Point de terminaison pour le profil", + "keysEndpoint": "Point de terminaison pour les clés", + "tokenEndpoint": "Point de terminaison pour les jetons", + "authEndpoint": "Point de terminaison pour l'authentification" + }, + "clients": { + "title": "Clients", + "newClient": "Nouveau client", + "empty": "Aucun client pour le moment" + } + }, + "automation": "Automatisation" } diff --git a/dashboard/src/translation/nl.json b/dashboard/src/translation/nl.json index a6dbfe9d4..b5e963409 100644 --- a/dashboard/src/translation/nl.json +++ b/dashboard/src/translation/nl.json @@ -274,7 +274,8 @@ "usernamePlaceholder": "Optioneel. Indien niet ingevuld mag de gebruiker bij registratie zelf kiezen", "fallbackEmailPlaceholder": "Optioneel. Indien niet ingevoerd zal de primaire e-mail gebruikt worden", "displayNamePlaceholder": "Optioneel. Indien niet ingevoerd kan de gebruiker het kiezen tijdens eerste aanmelding", - "external2FA": "2FA instellingen worden beheerd door een externe authenticatie bron" + "external2FA": "2FA instellingen worden beheerd door een externe authenticatie bron", + "ldapGroups": "LDAP Groepen" }, "deleteUserDialog": { "deleteAction": "Verwijder", diff --git a/dashboard/src/translation/ru.json b/dashboard/src/translation/ru.json index c00158b89..8a74fe2ed 100644 --- a/dashboard/src/translation/ru.json +++ b/dashboard/src/translation/ru.json @@ -162,7 +162,10 @@ "loginAction": "Логин", "switchToSignUpAction": "Ещё нет учётной записи? Зарегистрироваться", "createAccountAction": "Создать учётную запись", - "switchToLoginAction": "Уже есть учётная запись? Войти" + "switchToLoginAction": "Уже есть учётная запись? Войти", + "setupWithTokenAction": "Настройка", + "setupToken": "Настроить Токен", + "titleToken": "Войти с Настроенным Токеном" }, "title": "Магазин приложений", "noAppsFound": "Приложения не найдены.", @@ -217,7 +220,7 @@ "require2FAWarning": "Сперва настройте 2FA, чтобы иметь доступ к аккаунту в будущем." }, "externalLdap": { - "description": "Cloudron будет синхронизировать пользователей и группы с внешнего сервера LDAP или ActiveDirectory. Проверка пароля для аутентификации таких пользователей выполняется на внешнем сервере. Синхронизация не запускается автоматически, ее нужно активировать вручную.", + "description": "Эта настройка будет сихронизировать и идентифицировать пользователй и группы из внешнего сервера LDAP или AcriveDirectory. Синхронизация запускается с периодичностью, но также может быть запущена вручную.", "bindPassword": "Привязать пароль (необязательно)", "bindUsername": "Привязать Уникальное имя (DN)/Имя пользователя (необязательно)", "title": "Подключиться к удалённому каталогу", @@ -234,13 +237,14 @@ "groupFilter": "Фильтр группы", "groupnameField": "Поле с именем группы", "auth": "Авторизоваться", - "autocreateUsersOnLogin": "Автоматически создавать пользователей после их входа в Cloudron", + "autocreateUsersOnLogin": "Автоматически создавать пользователей после их входа", "showLogsAction": "Показать логи", "syncAction": "Синхронизировать", "configureAction": "Настроить", "errorSelfSignedCert": "Сервер использует недействительный или самоподписанный сертификат.", "providerOther": "Другое", - "providerDisabled": "Отключить" + "providerDisabled": "Отключить", + "disableWarning": "Источник аутентификации будет сброшен до локальных паролей для всех активных пользователей." }, "subscriptionDialog": { "title": "Требуется подписка", @@ -269,7 +273,9 @@ "errorDisplayNameRequired": "Требуется имя", "activeCheckbox": "Пользователь активен", "fallbackEmailPlaceholder": "Необязательно. Если не указано, будет использоваться основной почтовый ящик", - "displayNamePlaceholder": "Необязательно. Если не указано, пользователь может указать во время регистрации" + "displayNamePlaceholder": "Необязательно. Если не указано, пользователь может указать во время регистрации", + "external2FA": "Настройка 2FA осуществляется внешним ресурсом аутентификации", + "ldapGroups": "Группы LDAP" }, "deleteUserDialog": { "title": "Удалить пользователя {{ username }}", @@ -356,7 +362,7 @@ "exposedLdap": { "title": "Сервер LDAP", "ipRestriction": { - "description": "Сервер каталогов может быть ограничен для определённого круга IP адресов.", + "description": "Ограничьте доступ к серверу каталогов только для определённого круга IP-адресов и диапазонов. Строки, начинающиеся с #, будут считаться комментарием.", "placeholder": "IP-адреса или подсети, разделённые строками", "label": "Ограничить доступ" }, @@ -366,7 +372,8 @@ "label": "Привязать пароль", "description": "Все запросы LDAP должны быть идентифицированы при помощи данного секрета и уникального имени пользователя (DN) {{ userDN }}", "url": "URL сервера" - } + }, + "cloudflarePortWarning": "Для доступа к LDAP серверу через домен панели управления проксирование Cloudflare должно быть выключено" }, "userImportDialog": { "title": "Импорт пользователей", @@ -456,7 +463,10 @@ "changeEmail": { "title": "Изменить главный адрес электронной почты", "errorEmailInvalid": "Неверный адрес электронной почты", - "errorEmailRequired": "Требуется действительный адрес электронной почты" + "errorEmailRequired": "Требуется действительный адрес электронной почты", + "email": "Новый адрес электронной почты", + "password": "Пароль для подтверждения", + "errorWrongPassword": "Неверный пароль" }, "changeFallbackEmail": { "title": "Изменить пароль электронной почты восстановления", @@ -1190,7 +1200,7 @@ "cloudronId": "Cloudron ID", "subscriptionEndsAt": "Отменена и завершена", "subscriptionSetupAction": "Обновить до Premium", - "subscriptionChangeAction": "Изменить подписку", + "subscriptionChangeAction": "Управление подпиской", "subscriptionReactivateAction": "Реактивировать подписку", "emailNotVerified": "Электронная почта не подтверждена" }, @@ -1283,6 +1293,10 @@ "enableAction": "Включить SSH доступ", "title": "Удалённая поддержка", "description": "Выберите эту опцию, чтобы позволить сотрудникам поддержки подключиться к Вашему серверу через SSH." + }, + "help": { + "title": "Помощь", + "description": "Для поддержки и помощи, пожалуйста, воспользуйтесь следующими ресурсами:\n* [Форум Cloudron]({{ forumLink }}) - пожалуйста, задавайте вопросы в соответствующих темах Поддержки или конкретных приложений.\n* [Документация Cloudron & База знаний]({{ docsLink }})\n* [Создание сторонних приложений и API]({{ packagingLink }})\n" } }, "system": { @@ -1307,7 +1321,19 @@ "graphTitle": "Процент", "graphSubtext": "Отображаются приложения, использующие более {{ threshold }} CPU" }, - "selectPeriodLabel": "Выберите период" + "selectPeriodLabel": "Выберите период", + "info": { + "platformVersion": "Версия Платформы", + "product": "Продукт", + "vendor": "Поставщик", + "memory": "Память", + "uptime": "Аптайм", + "activationTime": "Время создания Cloudron", + "title": "Информация" + }, + "graphs": { + "title": "Графики" + } }, "eventlog": { "title": "Журнал", @@ -1467,7 +1493,8 @@ "renameDialog": { "newName": "Новое имя", "rename": "Переименовать", - "title": "Переименовать {{ fileName }}" + "title": "Переименовать {{ fileName }}", + "reallyOverwrite": "Файл с таким именем уже существует. Хотите перезаписать его?" }, "chownDialog": { "newOwner": "Новый владелец", diff --git a/dashboard/src/views/app.html b/dashboard/src/views/app.html index cf422bf67..5336ef138 100644 --- a/dashboard/src/views/app.html +++ b/dashboard/src/views/app.html @@ -1014,18 +1014,16 @@
-
+
- +

{{ 'app.resources.cpu.description' | tr }}

- - - - - - - + + + + +
@@ -1034,7 +1032,7 @@
- +
diff --git a/dashboard/src/views/app.js b/dashboard/src/views/app.js index 5fb8cd698..b5321137f 100644 --- a/dashboard/src/views/app.js +++ b/dashboard/src/views/app.js @@ -543,8 +543,8 @@ angular.module('Application').controller('AppController', ['$scope', '$location' memoryLimit: 0, // RAM memoryTicks: [], - currentCpuShares: 0, - cpuShares: 0, + currentCpuQuota: 0, + cpuQuota: 0, show: function () { var app = $scope.app; @@ -569,7 +569,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location' // for firefox widget update $timeout(function() { - $scope.resources.currentCpuShares = $scope.resources.cpuShares = app.cpuShares; + $scope.resources.currentCpuQuota = $scope.resources.cpuQuota = app.cpuQuota; $scope.resources.memoryLimit = $scope.resources.currentMemoryLimit; $scope.resources.busy = false; }, 500); @@ -600,14 +600,14 @@ angular.module('Application').controller('AppController', ['$scope', '$location' }); }, - submitCpuShares: function () { + submitCpuQuota: function () { $scope.resources.busy = true; $scope.resources.error = {}; - Client.configureApp($scope.app.id, 'cpu_shares', { cpuShares: parseInt($scope.resources.cpuShares) }, function (error) { + Client.configureApp($scope.app.id, 'cpu_quota', { cpuQuota: parseInt($scope.resources.cpuQuota) }, function (error) { if (error) return Client.error(error); - $scope.resources.currentCpuShares = $scope.resources.cpuShares; + $scope.resources.currentCpuQuota = $scope.resources.cpuQuota; refreshApp($scope.app.id, function (error) { if (error) return Client.error(error); diff --git a/migrations/20240410125743-apps-rename-cpuShares-to-cpuQuota.js b/migrations/20240410125743-apps-rename-cpuShares-to-cpuQuota.js new file mode 100644 index 000000000..891565db2 --- /dev/null +++ b/migrations/20240410125743-apps-rename-cpuShares-to-cpuQuota.js @@ -0,0 +1,10 @@ +'use strict'; + +exports.up = async function (db) { + await db.runSql('ALTER TABLE apps RENAME COLUMN cpuShares to cpuQuota'); + await db.runSql('ALTER TABLE apps MODIFY COLUMN cpuQuota INTEGER DEFAULT 100'); + await db.runSql('UPDATE apps SET cpuQuota=?', [ 100 ]); +}; + +exports.down = async function () { +}; diff --git a/migrations/schema.sql b/migrations/schema.sql index 7696c2fc5..266284f2b 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -77,7 +77,7 @@ CREATE TABLE IF NOT EXISTS apps( updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, // when this db record was updated (useful for UI caching) memoryLimit BIGINT DEFAULT 0, - cpuShares INTEGER DEFAULT 512, + cpuQuota INTEGER DEFAULT 100, xFrameOptions VARCHAR(512), sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO debugModeJson TEXT, // options for development mode diff --git a/src/apps.js b/src/apps.js index 001d7e190..92b75b527 100644 --- a/src/apps.js +++ b/src/apps.js @@ -32,7 +32,7 @@ exports = module.exports = { setIcon, setTags, setMemoryLimit, - setCpuShares, + setCpuQuota, setMounts, setAutomaticBackup, setAutomaticUpdate, @@ -163,7 +163,6 @@ const appstore = require('./appstore.js'), manifestFormat = require('cloudron-manifestformat'), notifications = require('./notifications.js'), once = require('./once.js'), - os = require('os'), path = require('path'), paths = require('./paths.js'), PassThrough = require('stream').PassThrough, @@ -173,7 +172,6 @@ const appstore = require('./appstore.js'), services = require('./services.js'), shell = require('./shell.js'), storage = require('./storage.js'), - system = require('./system.js'), tasks = require('./tasks.js'), tgz = require('./backupformat/tgz.js'), TransformStream = require('stream').Transform, @@ -185,7 +183,7 @@ const appstore = require('./appstore.js'), _ = require('underscore'); const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState', - 'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares', + 'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuQuota', 'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', 'apps.operatorsJson', 'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp', 'apps.crontab', 'apps.creationTime', 'apps.updateTime', 'apps.enableAutomaticUpdate', 'apps.upstreamUri', @@ -408,10 +406,10 @@ function validateMemoryLimit(manifest, memoryLimit) { return null; } -function validateCpuShares(cpuShares) { - assert.strictEqual(typeof cpuShares, 'number'); +function validateCpuQuota(cpuQuota) { + assert.strictEqual(typeof cpuQuota, 'number'); - if (cpuShares < 2 || cpuShares > 1024) return new BoxError(BoxError.BAD_FIELD, 'cpuShares has to be between 2 and 1024'); + if (cpuQuota < 1 || cpuQuota > 100) return new BoxError(BoxError.BAD_FIELD, 'cpuQuota has to be between 1 and 100'); return null; } @@ -574,7 +572,7 @@ function removeInternalFields(app) { const result = _.pick(app, 'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'subdomain', 'domain', 'fqdn', 'certificate', 'crontab', 'upstreamUri', - 'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares', 'operators', + 'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuQuota', 'operators', 'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags', 'label', 'secondaryDomains', 'redirectDomains', 'aliasDomains', 'env', 'enableAutomaticUpdate', 'storageVolumeId', 'storageVolumePrefix', 'mounts', 'enableTurn', 'enableRedis', @@ -834,7 +832,7 @@ async function add(id, appStoreId, manifest, subdomain, domain, portBindings, da accessRestriction = data.accessRestriction || null, accessRestrictionJson = JSON.stringify(accessRestriction), memoryLimit = data.memoryLimit || 0, - cpuShares = data.cpuShares || 512, + cpuQuota = data.cpuQuota || 100, installationState = data.installationState, runState = data.runState, sso = 'sso' in data ? data.sso : null, @@ -858,11 +856,11 @@ async function add(id, appStoreId, manifest, subdomain, domain, portBindings, da const queries = []; queries.push({ - query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, ' + query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuQuota, ' + 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, servicesConfigJson, icon, ' + 'enableMailbox, mailboxDisplayName, upstreamUri, enableTurn, enableRedis) ' + ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', - args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, + args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuQuota, sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, servicesConfigJson, icon, enableMailbox, mailboxDisplayName, upstreamUri, enableTurn, enableRedis ] }); @@ -1568,25 +1566,25 @@ async function setMemoryLimit(app, memoryLimit, auditSource) { return { taskId }; } -async function setCpuShares(app, cpuShares, auditSource) { +async function setCpuQuota(app, cpuQuota, auditSource) { assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof cpuShares, 'number'); + assert.strictEqual(typeof cpuQuota, 'number'); assert.strictEqual(typeof auditSource, 'object'); const appId = app.id; let error = checkAppState(app, exports.ISTATE_PENDING_RESIZE); if (error) throw error; - error = validateCpuShares(cpuShares); + error = validateCpuQuota(cpuQuota); if (error) throw error; const task = { args: {}, - values: { cpuShares } + values: { cpuQuota } }; const taskId = await safe(addTask(appId, exports.ISTATE_PENDING_RESIZE, task, auditSource)); - await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cpuShares, taskId }); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cpuQuota, taskId }); return { taskId }; } @@ -2338,7 +2336,7 @@ async function clone(app, data, user, auditSource) { const icons = await getIcons(app.id); - const dolly = _.pick(app, 'memoryLimit', 'cpuShares', 'crontab', 'reverseProxyConfig', 'env', 'servicesConfig', 'tags', + const dolly = _.pick(app, 'memoryLimit', 'cpuQuota', 'crontab', 'reverseProxyConfig', 'env', 'servicesConfig', 'tags', 'enableMailbox', 'mailboxDisplayName', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain', 'enableTurn', 'enableRedis', 'mounts', 'enableBackup', 'enableAutomaticUpdate', 'accessRestriction', 'operators', 'sso'); @@ -2902,7 +2900,7 @@ async function loadConfig(app) { const appConfig = safe.JSON.parse(safe.fs.readFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'))); let data = {}; if (appConfig) { - data = _.pick(appConfig, 'memoryLimit', 'cpuShares', 'enableBackup', 'reverseProxyConfig', 'env', 'servicesConfig', 'label', 'tags', 'enableAutomaticUpdate'); + data = _.pick(appConfig, 'memoryLimit', 'cpuQuota', 'enableBackup', 'reverseProxyConfig', 'env', 'servicesConfig', 'label', 'tags', 'enableAutomaticUpdate'); } const icon = safe.fs.readFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/icon.png')); diff --git a/src/docker.js b/src/docker.js index 96a6d7f5a..c6d3151c1 100644 --- a/src/docker.js +++ b/src/docker.js @@ -42,6 +42,7 @@ const apps = require('./apps.js'), debug = require('debug')('box:docker'), Docker = require('dockerode'), fs = require('fs'), + os = require('os'), paths = require('./paths.js'), promiseRetry = require('./promise-retry.js'), services = require('./services.js'), @@ -357,7 +358,9 @@ async function createSubcontainer(app, name, cmd, options) { 'Name': isAppContainer ? 'unless-stopped' : 'no', 'MaximumRetryCount': 0 }, - CpuShares: app.cpuShares, + // CpuPeriod (100000 microseconds) and CpuQuota(app.cpuQuota% of CpuPeriod) + // 1000000000 is one core https://github.com/moby/moby/issues/24713#issuecomment-233167619 and https://stackoverflow.com/questions/52391877/set-the-number-of-cpu-cores-of-a-container-using-docker-engine-api + NanoCPUs: app.cpuQuota === 100 ? 0 : (os.cpus().length * app.cpuQuota/100).toFixed(2) * 1000000000, VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ], SecurityOpt: [ 'apparmor=docker-cloudron-app' ], CapAdd: [], diff --git a/src/routes/apps.js b/src/routes/apps.js index b63712ce9..21fae5417 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -27,7 +27,7 @@ exports = module.exports = { setTurn, setRedis, setMemoryLimit, - setCpuShares, + setCpuQuota, setAutomaticBackup, setAutomaticUpdate, setReverseProxyConfig, @@ -281,13 +281,13 @@ async function setMemoryLimit(req, res, next) { next(new HttpSuccess(202, { taskId: result.taskId })); } -function setCpuShares(req, res, next) { +function setCpuQuota(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); - if (typeof req.body.cpuShares !== 'number') return next(new HttpError(400, 'cpuShares is not a number')); + if (typeof req.body.cpuQuota !== 'number') return next(new HttpError(400, 'cpuQuota is not a number')); - apps.setCpuShares(req.app, req.body.cpuShares, AuditSource.fromRequest(req), function (error, result) { + apps.setCpuQuota(req.app, req.body.cpuQuota, AuditSource.fromRequest(req), function (error, result) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); diff --git a/src/server.js b/src/server.js index f55d18a47..55eeea528 100644 --- a/src/server.js +++ b/src/server.js @@ -244,7 +244,7 @@ async function initializeExpressSync() { router.post('/api/v1/apps/:id/configure/tags', json, token, routes.apps.load, authorizeOperator, routes.apps.setTags); router.post('/api/v1/apps/:id/configure/icon', json, token, routes.apps.load, authorizeOperator, routes.apps.setIcon); router.post('/api/v1/apps/:id/configure/memory_limit', json, token, routes.apps.load, authorizeOperator, routes.apps.setMemoryLimit); - router.post('/api/v1/apps/:id/configure/cpu_shares', json, token, routes.apps.load, authorizeOperator, routes.apps.setCpuShares); + router.post('/api/v1/apps/:id/configure/cpu_quota', json, token, routes.apps.load, authorizeOperator, routes.apps.setCpuQuota); router.post('/api/v1/apps/:id/configure/automatic_backup', json, token, routes.apps.load, authorizeOperator, routes.apps.setAutomaticBackup); router.post('/api/v1/apps/:id/configure/automatic_update', json, token, routes.apps.load, authorizeOperator, routes.apps.setAutomaticUpdate); router.post('/api/v1/apps/:id/configure/reverse_proxy', json, token, routes.apps.load, authorizeOperator, routes.apps.setReverseProxyConfig); diff --git a/src/test/apps-test.js b/src/test/apps-test.js index 9398ca0bc..25d0812dd 100644 --- a/src/test/apps-test.js +++ b/src/test/apps-test.js @@ -302,7 +302,7 @@ describe('Apps', function () { manifest: Object.assign({}, app.manifest, { version: '0.2.0' }), accessRestriction: '', memoryLimit: 1337, - cpuShares: 102, + cpuQuota: 79, operators: { users: [ 'someid' ] } }; @@ -313,7 +313,7 @@ describe('Apps', function () { expect(newApp.manifest.version).to.be('0.2.0'); expect(newApp.accessRestriction).to.be(''); expect(newApp.memoryLimit).to.be(1337); - expect(newApp.cpuShares).to.be(102); + expect(newApp.cpuQuota).to.be(79); }); it('update of nonexisting app fails', async function () {