Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a723e3a4dd | |||
| 4dbd794b41 | |||
| 1a406c4d7d | |||
| baf543ba00 | |||
| e06400bb71 | |||
| 1ee6560f30 | |||
| 5e5948ecd4 | |||
| 7b768d6149 | |||
| e9029eb1f9 | |||
| a7783fdb0d | |||
| 7bc76f2f34 | |||
| 7472e78755 | |||
| 0e5f8e75f9 | |||
| 0fdb7f0a93 | |||
| cc0705183a | |||
| 2aee2c9e27 | |||
| ccc45a41e6 | |||
| b27b4a38eb | |||
| 90112de6e4 | |||
| cad08380ea | |||
| 2aecf0c96a | |||
| e614703305 | |||
| a2f1a1feb3 | |||
| b0965b3ec7 | |||
| 9f9f745f47 | |||
| 52ab35d8c6 | |||
| 5f78722c8f | |||
| 3da97fb7cb | |||
| 9c191c6c11 | |||
| 095d9fd7fa | |||
| 2df8769fcf | |||
| 043d6692f5 | |||
| 10a377e083 | |||
| 042d6099c4 | |||
| e001a21e4b | |||
| 5aa6e18ea7 | |||
| 841c9bc261 | |||
| 218450880e | |||
| fcee182ca3 | |||
| 8590148803 | |||
| d93c9b3c59 | |||
| 4a238256e8 | |||
| 5718775bf7 | |||
| 89754a62fe | |||
| ecb93cb115 | |||
| e79c90f330 | |||
| 691013d7e0 | |||
| 5b3e800567 | |||
| a3c2fcf1b6 | |||
| 5d6a794d52 | |||
| a54a404dac | |||
| a3ea2a32f1 | |||
| 0148a46244 | |||
| c680428b3c | |||
| 247dcbfe11 | |||
| 95e2b726c1 | |||
| fd3fb23955 | |||
| c56c43c464 | |||
| 445325453b | |||
| d072682e82 | |||
| a3245278f0 | |||
| 80d00577e5 | |||
| 2dd46b31a2 | |||
| 84bc28b371 | |||
| 8b0fbd8e77 | |||
| 375978b526 | |||
| 1487823641 | |||
| a68a4ce36b | |||
| 81c393153b | |||
| e4076d7a75 | |||
| d5201a29da | |||
| f06b4e5b1d | |||
| 11cc074a09 | |||
| 737b9fb73e | |||
| d0f0dc7339 | |||
| 09368dd267 | |||
| f964178682 | |||
| ba7ef8e7f0 | |||
| 4e2a5e6f15 | |||
| f54ffa796f | |||
| 3e00e924f8 | |||
| 592c50ba75 | |||
| 5c79ac8893 | |||
| 01dddade5a | |||
| b83f263919 | |||
| 82e8a893fd | |||
| 27236a5692 | |||
| 6e4b9d8196 | |||
| 36df6b9e1e | |||
| ab1d3f41fa | |||
| dbeb523882 | |||
| aae2a36d1e | |||
| 2d4323a72c | |||
| da008874dc | |||
| ae36ce07d1 | |||
| b9ef941b80 | |||
| 465fc427d6 | |||
| 850ff87849 | |||
| bc45423eca | |||
| 92cb5f3583 | |||
| 9a5bd8a846 | |||
| 4393143ee8 | |||
| ddb29fd85b | |||
| c04951c45e | |||
| ec50163b66 | |||
| 9c7241e9ac | |||
| 3b5c0c2e63 | |||
| c69c8b57c4 | |||
| c9628970d9 | |||
| ee0c50bea2 | |||
| 3c527b7064 | |||
| 0bd250a34b | |||
| ff5ad8b062 | |||
| 3b38889f32 | |||
| a6f202be04 | |||
| 9161e5f7e8 | |||
| 12e32cc8ff | |||
| 394b784106 | |||
| a3a928367b | |||
| bacdf2c87c | |||
| 53e50912e6 | |||
| 5d5c712f1c | |||
| 050ea48e3e | |||
| 09e07868bb | |||
| 613ac16601 | |||
| 84cf5809a0 | |||
| 20b42042da | |||
| 10fee49e9a | |||
| b18f42b372 | |||
| 515d93f5ef | |||
| 10d1bb861a | |||
| 0abf1e76d4 | |||
| 168636e493 |
+1
-1
@@ -265,7 +265,7 @@ gulp.task('watch', function (done) {
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('serve', serve({ root: 'dist', port: 4000 }));
|
||||
gulp.task('serve', serve({ root: 'dist', port: 4000, hostname: '0.0.0.0' }));
|
||||
|
||||
gulp.task('develop', gulp.series(['default', 'watch', 'serve']));
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ function getAccessToken(callback) {
|
||||
let username = readlineSync.question('Username: ', {});
|
||||
let password = readlineSync.question('Password: ', { noEchoBack: true });
|
||||
|
||||
superagent.post(`https://${cloudronDomain}/api/v1/developer/login`, { username: username, password: password }).end(function (error, result) {
|
||||
superagent.post(`https://${cloudronDomain}/api/v1/cloudron/login`, { username: username, password: password }).end(function (error, result) {
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.log('Login failed');
|
||||
return getAccessToken(callback);
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ function getAccessToken(callback) {
|
||||
let username = readlineSync.question('Username: ', {});
|
||||
let password = readlineSync.question('Password: ', { noEchoBack: true });
|
||||
|
||||
superagent.post(`https://${cloudronDomain}/api/v1/developer/login`, { username: username, password: password }).end(function (error, result) {
|
||||
superagent.post(`https://${cloudronDomain}/api/v1/cloudron/login`, { username: username, password: password }).end(function (error, result) {
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.log('Login failed');
|
||||
return getAccessToken(callback);
|
||||
|
||||
+11
-4
@@ -1,5 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
// -------------------------------
|
||||
// WARNING
|
||||
// -------------------------------
|
||||
// This file is taken from https://github.com/sebastianha/angular-bootstrap-multiselect
|
||||
// There are local modifications like support for translation
|
||||
// -------------------------------
|
||||
|
||||
angular.module("ui.multiselect", ["multiselect.tpl.html"])
|
||||
//from bootstrap-ui typeahead parser
|
||||
.factory("optionParser", ["$parse", function($parse) {
|
||||
@@ -23,7 +30,7 @@ angular.module("ui.multiselect", ["multiselect.tpl.html"])
|
||||
}
|
||||
};
|
||||
}])
|
||||
.directive("multiselect", ["$parse", "$document", "$compile", "$interpolate", "optionParser", function($parse, $document, $compile, $interpolate, optionParser) {
|
||||
.directive("multiselect", ["$parse", "$document", "$compile", "$interpolate", "$translate", "optionParser", function($parse, $document, $compile, $interpolate, $translate, optionParser) {
|
||||
return {
|
||||
restrict: "E",
|
||||
require : "ngModel",
|
||||
@@ -154,7 +161,7 @@ angular.module("ui.multiselect", ["multiselect.tpl.html"])
|
||||
|
||||
function getHeaderText() {
|
||||
if(isEmpty(modelCtrl.$modelValue)) {
|
||||
scope.header = attrs.msHeader || "Select";
|
||||
scope.header = attrs.msHeader || $translate.instant('main.multiselect.select');
|
||||
return scope.header;
|
||||
}
|
||||
|
||||
@@ -162,7 +169,7 @@ angular.module("ui.multiselect", ["multiselect.tpl.html"])
|
||||
if(attrs.msSelected) {
|
||||
scope.header = $interpolate(attrs.msSelected)(scope);
|
||||
} else {
|
||||
scope.header = modelCtrl.$modelValue.length + " " + "selected";
|
||||
scope.header = $translate.instant('main.multiselect.selected', { n: modelCtrl.$modelValue.length });
|
||||
}
|
||||
} else {
|
||||
var local = {};
|
||||
@@ -342,7 +349,7 @@ angular.module("multiselect.tpl.html", []).run(["$templateCache", function($temp
|
||||
" <div ng-style=\"maxWidth\" style=\"padding-right: 13px; overflow: hidden; text-overflow: ellipsis;\">{{header}}</div><span class=\"caret\" style=\"position:absolute;right:10px;top:14px;\"></span>\n" +
|
||||
" </button>\n" +
|
||||
" <ul class=\"dropdown-menu\" style=\"margin-bottom:30px;padding-left:5px;padding-right:5px;\" ng-style=\"ulStyle\">\n" +
|
||||
" <input ng-show=\"items.length > filterAfterRows\" ng-model=\"filter\" style=\"padding: 0px 3px;margin-right: 15px; margin-bottom: 4px;\" placeholder=\"Type to filter options\">" +
|
||||
" <input ng-show=\"items.length > filterAfterRows\" ng-model=\"filter\" style=\"padding: 0px 3px;margin-right: 15px; margin-bottom: 4px;\" placeholder=\"{{ 'main.multiselect.filterPlaceholder' | tr }}\">" +
|
||||
" <li data-stopPropagation=\"true\" ng-repeat=\"i in items | filter:filter\" ng-class=\"{'dropdown-header': i.header, 'divider': i.divider}\">\n" +
|
||||
" <a ng-if=\"!i.header && !i.divider\" ng-click=\"select($event, i)\" style=\"padding:3px 10px;cursor:pointer;\">\n" +
|
||||
" <i class=\"fa\" ng-class=\"{'fa-check': i.checked, 'empty': !i.checked}\"></i> {{i.label}}" +
|
||||
|
||||
+2
-4
@@ -23,7 +23,6 @@
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-family: "Roboto","Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.846;
|
||||
}
|
||||
|
||||
@@ -71,9 +70,8 @@
|
||||
<body>
|
||||
|
||||
<div class="content">
|
||||
<h1>🙁</h1>
|
||||
<h2>Something has gone wrong</h2>
|
||||
<p>This app is currently not responding. Try refreshing the page.</p>
|
||||
<h1>⌛</h1>
|
||||
<p>This app is currently not responding. Please try refreshing the page in a few minutes.</p>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
|
||||
@@ -298,10 +298,10 @@
|
||||
<div class="toolbar">
|
||||
<div class="btn-group" role="group" style="display: block;">
|
||||
<!-- TODO figure out why a line break in code between the two buttons results in a gap visually without any margin/padding set -->
|
||||
<button class="btn btn-primary" ng-click="goDirectoryUp()" ng-disabled="cwd === '/'"><i class="fas fa-arrow-left"></i></button><button class="btn btn-primary" ng-click="refresh()"><i class="fas fa-sync-alt"></i></button>
|
||||
<button class="btn btn-primary" ng-click="goDirectoryUp()" ng-disabled="busy || cwd === '/'"><i class="fas fa-arrow-left"></i></button><button class="btn btn-primary" ng-disabled="busy" ng-click="refresh()"><i class="fas fa-sync-alt"></i></button>
|
||||
</div>
|
||||
<div class="btn-group path-parts" role="group">
|
||||
<button class="btn btn-default" ng-disabled="cwd === '/'" ng-click="changeDirectory('/')" ng-drop="drop($event, '/')" ng-dragleave="dragExit($event, '/')" ng-dragover="dragEnter($event, '/')"><i class="fas fa-home"></i> {{ rootDirLabel }} </button><button class="btn btn-default" ng-disabled="part.path === cwd" ng-click="changeDirectory(part.path)" ng-drop="drop($event, part.path)" ng-dragleave="dragExit($event, part.path)" ng-dragover="dragEnter($event, part.path)" ng-repeat="part in cwdParts">{{ part.name }}</button>
|
||||
<button class="btn btn-default" ng-disabled="busy || cwd === '/'" ng-click="changeDirectory('/')" ng-drop="drop($event, '/')" ng-dragleave="dragExit($event, '/')" ng-dragover="dragEnter($event, '/')"><i class="fas fa-home"></i> {{ rootDirLabel }} </button><button class="btn btn-default" ng-disabled="busy || part.path === cwd" ng-click="changeDirectory(part.path)" ng-drop="drop($event, part.path)" ng-dragleave="dragExit($event, part.path)" ng-dragover="dragEnter($event, part.path)" ng-repeat="part in cwdParts">{{ part.name }}</button>
|
||||
</div>
|
||||
<div style="display: block;">
|
||||
<div class="btn-group">
|
||||
@@ -350,10 +350,13 @@
|
||||
<div class="file-list" ng-class="{ 'entry-hovered': dropToBody, 'busy': busy }" context-menu="menuOptionsBlank" ng-mousedown="select($event, null)">
|
||||
<table class="table table-hover" style="margin: 0;">
|
||||
<tbody>
|
||||
<tr ng-show="entries.length === 0">
|
||||
<td colspan="5" class="text-center">{{ 'filemanager.list.empty' | tr }}</td>
|
||||
<tr ng-show="busy">
|
||||
<td colspan="6"><center><h2><i class="fa fa-circle-notch fa-spin"></i></h2></center></td>
|
||||
</tr>
|
||||
<tr ng-repeat="entry in entries | orderBy:sortProperty:sortAsc | orderBy:'isDirectory':true" draggable="true" ng-dragstart="dragStart($event, entry)" ng-drop="drop($event, entry)" context-menu="menuOptions" model="entry" ng-dragleave="dragExit($event, entry)" ng-dragover="dragEnter($event, entry)" ng-class="{ 'entry-hovered': entry.hovered, 'entry-selected': isSelected(entry) }">
|
||||
<tr ng-show="!busy && entries.length === 0">
|
||||
<td colspan="" class="text-center">{{ 'filemanager.list.empty' | tr }}</td>
|
||||
</tr>
|
||||
<tr ng-hide="busy" ng-repeat="entry in entries | orderBy:sortProperty:sortAsc | orderBy:'isDirectory':true" draggable="true" ng-dragstart="dragStart($event, entry)" ng-drop="drop($event, entry)" context-menu="menuOptions" model="entry" ng-dragleave="dragExit($event, entry)" ng-dragover="dragEnter($event, entry)" ng-class="{ 'entry-hovered': entry.hovered, 'entry-selected': isSelected(entry) }">
|
||||
<td style="width: 40px" ng-mousedown="select($event, entry)" ng-dblclick="open(entry)" class="text-center">
|
||||
<i class="fas fa-lg {{ entry.icon }}" ng-class="{ 'text-primary': entry.isDirectory && !isSelected(entry) }"></i>
|
||||
</td>
|
||||
|
||||
+5
-5
@@ -147,7 +147,7 @@
|
||||
<ul class="nav navbar-nav navbar-right" ng-hide="hideNavBarActions">
|
||||
<li ng-show="user.isAtLeastOwner && (subscription.plan.id === 'free' || subscription.plan.id === 'expired')">
|
||||
<a ng-click="openSubscriptionSetup()" style="cursor: pointer">
|
||||
<span class="badge badge-success">{{ subscription.plan.id === 'free' ? 'Setup' : 'Reactivate' }} Subscription</span>
|
||||
<span class="badge badge-success">{{ subscription.plan.id === 'free' ? 'Set up' : 'Reactivate' }} Subscription</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -161,9 +161,9 @@
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/notifications">
|
||||
<i class="fas fa-bell" ng-show="notifications.length"></i>
|
||||
<i class="far fa-bell" ng-hide="notifications.length"></i>
|
||||
<span class="badge badge-danger" ng-show="notifications.length">{{ notifications.length }}</span>
|
||||
<i class="fas fa-bell" ng-show="notificationCount"></i>
|
||||
<i class="far fa-bell" ng-hide="notificationCount"></i>
|
||||
<span class="badge badge-danger" ng-show="notificationCount">{{ notificationCount }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
@@ -175,7 +175,7 @@
|
||||
<li ng-show="user.role === 'owner'"><a href="#/branding"><i class="fa fa-passport fa-fw"></i> {{ 'branding.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/domains"><i class="fa fa-globe fa-fw"></i> {{ 'domains.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/email"><i class="fa fa-envelope fa-fw"></i> {{ 'emails.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/activity"><i class="fa fa-list-alt fa-fw"></i> {{ 'eventlog.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/eventlog"><i class="fa fa-list-alt fa-fw"></i> {{ 'eventlog.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/network"><i class="fas fa-network-wired fa-fw"></i> {{ 'network.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/services"><i class="fa fa-cogs fa-fw"></i> {{ 'services.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> {{ 'settings.title' | tr }}</a></li>
|
||||
|
||||
+103
-43
@@ -12,6 +12,7 @@ var ISTATES = {
|
||||
PENDING_CONFIGURE: 'pending_configure',
|
||||
PENDING_UNINSTALL: 'pending_uninstall',
|
||||
PENDING_RESTORE: 'pending_restore',
|
||||
PENDING_IMPORT: 'pending_import',
|
||||
PENDING_UPDATE: 'pending_update',
|
||||
PENDING_BACKUP: 'pending_backup',
|
||||
PENDING_RECREATE_CONTAINER: 'pending_recreate_container', // env change or addon change
|
||||
@@ -80,6 +81,14 @@ var TASK_TYPES = {
|
||||
|
||||
var SECRET_PLACEHOLDER = String.fromCharCode(0x25CF).repeat(8);
|
||||
|
||||
// ----------------------------------------------
|
||||
// Helper to ensure loading a fallback app icon on first load failure
|
||||
// ----------------------------------------------
|
||||
function imageErrorHandler(elem) {
|
||||
elem.src = elem.getAttribute('fallback-icon');
|
||||
elem.onerror = null; // avoid retry after default icon cannot be loaded
|
||||
}
|
||||
|
||||
// ----------------------------------------------
|
||||
// Shared Angular Filters
|
||||
// ----------------------------------------------
|
||||
@@ -100,27 +109,39 @@ angular.module('Application').filter('prettyDiskSize', function () {
|
||||
return function (size, fallback) { return prettyByteSize(size, fallback) || 'Not available yet'; };
|
||||
});
|
||||
|
||||
angular.module('Application').filter('prettyDate', function () {
|
||||
angular.module('Application').filter('trKeyFromPeriod', function () {
|
||||
return function (period) {
|
||||
if (period === 6) return 'app.graphs.period.6h';
|
||||
if (period === 12) return 'app.graphs.period.12h';
|
||||
if (period === 24) return 'app.graphs.period.24h';
|
||||
if (period === 24*7) return 'app.graphs.period.7d';
|
||||
if (period === 24*30) return 'app.graphs.period.30d';
|
||||
|
||||
return '';
|
||||
};
|
||||
});
|
||||
|
||||
angular.module('Application').filter('prettyDate', function ($translate) {
|
||||
// http://ejohn.org/files/pretty.js
|
||||
return function prettyDate(utc) {
|
||||
var date = new Date(utc), // this converts utc into browser timezone and not cloudron timezone!
|
||||
diff = (((new Date()).getTime() - date.getTime()) / 1000) + 30, // add 30seconds for clock skew
|
||||
day_diff = Math.floor(diff / 86400);
|
||||
|
||||
if (isNaN(day_diff) || day_diff < 0)
|
||||
return 'just now';
|
||||
if (isNaN(day_diff) || day_diff < 0) return $translate.instant('main.prettyDate.justNow', {});
|
||||
|
||||
return day_diff === 0 && (
|
||||
diff < 60 && 'just now' ||
|
||||
diff < 120 && '1 minute ago' ||
|
||||
diff < 3600 && Math.floor( diff / 60 ) + ' minutes ago' ||
|
||||
diff < 7200 && '1 hour ago' ||
|
||||
diff < 86400 && Math.floor( diff / 3600 ) + ' hours ago') ||
|
||||
day_diff === 1 && 'Yesterday' ||
|
||||
day_diff < 7 && day_diff + ' days ago' ||
|
||||
day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago' ||
|
||||
day_diff < 365 && Math.round( day_diff / 30 ) + ' months ago' ||
|
||||
Math.round( day_diff / 365 ) + ' years ago';
|
||||
diff < 60 && $translate.instant('main.prettyDate.justNow', {}) ||
|
||||
diff < 120 && $translate.instant('main.prettyDate.minutesAgo', { m: 1 }) ||
|
||||
diff < 3600 && $translate.instant('main.prettyDate.minutesAgo', { m: Math.floor( diff / 60 ) }) ||
|
||||
diff < 7200 && $translate.instant('main.prettyDate.hoursAgo', { h: 1 }) ||
|
||||
diff < 86400 && $translate.instant('main.prettyDate.hoursAgo', { h: Math.floor( diff / 3600 ) })
|
||||
) ||
|
||||
day_diff === 1 && $translate.instant('main.prettyDate.yeserday', {}) ||
|
||||
day_diff < 7 && $translate.instant('main.prettyDate.daysAgo', { d: day_diff }) ||
|
||||
day_diff < 31 && $translate.instant('main.prettyDate.weeksAgo', { w: Math.ceil( day_diff / 7 ) }) ||
|
||||
day_diff < 365 && $translate.instant('main.prettyDate.monthsAgo', { m: Math.round( day_diff / 30 ) }) ||
|
||||
$translate.instant('main.prettyDate.yearsAgo', { m: Math.round( day_diff / 365 ) });
|
||||
};
|
||||
});
|
||||
|
||||
@@ -238,12 +259,16 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
if (status === 401) return client.login();
|
||||
|
||||
if (status === 500 || status === 501) {
|
||||
if (!client.offline) client.error(data);
|
||||
// actual internal server error, most likely a bug or timeout log to console only to not alert the user
|
||||
if (!client.offline) {
|
||||
console.error(status, data);
|
||||
console.log('------\nCloudron Internal Error\n\nIf you see this, please send a mail with above log to support@cloudron.io\n------\n');
|
||||
}
|
||||
} else if (status === 502 || status === 503 || status === 504) {
|
||||
// This means the box service is not reachable. We just show offline banner for now
|
||||
}
|
||||
|
||||
if (status >= 500) {
|
||||
if (status >= 502) {
|
||||
handleServerOffline();
|
||||
return callback(new ClientError(status, data));
|
||||
}
|
||||
@@ -739,7 +764,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
Client.prototype.debugApp = function (id, state, callback) {
|
||||
var data = {
|
||||
debugMode: state ? {
|
||||
readonlyRootfs: true,
|
||||
readonlyRootfs: false,
|
||||
cmd: [ '/bin/bash', '-c', 'echo "Repair mode. Use the webterminal or cloudron exec to repair. Sleeping" && sleep infinity' ]
|
||||
} : null
|
||||
};
|
||||
@@ -786,15 +811,6 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.checkBackupConfig = function (callback) {
|
||||
get('/api/v1/backups/check', null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null, data);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.getSupportConfig = function (callback) {
|
||||
get('/api/v1/settings/support_config', null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
@@ -1147,7 +1163,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.getNotifications = function (acknowledged, page, perPage, callback) {
|
||||
Client.prototype.getNotifications = function (options, page, perPage, callback) {
|
||||
var config = {
|
||||
params: {
|
||||
page: page,
|
||||
@@ -1155,7 +1171,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof acknowledged === 'boolean') config.params.acknowledged = acknowledged;
|
||||
if (typeof options.acknowledged === 'boolean') config.params.acknowledged = options.acknowledged;
|
||||
|
||||
get('/api/v1/notifications', config, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
@@ -1165,8 +1181,8 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.ackNotification = function (notificationId, callback) {
|
||||
post('/api/v1/notifications/' + notificationId, {}, null, function (error, data, status) {
|
||||
Client.prototype.ackNotification = function (notificationId, acknowledged, callback) {
|
||||
post('/api/v1/notifications/' + notificationId, { acknowledged: acknowledged }, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 204) return callback(new ClientError(status));
|
||||
|
||||
@@ -1501,6 +1517,15 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.disableTwoFactorAuthenticationByUserId = function (userId, callback) {
|
||||
post('/api/v1/users/' + userId + '/twofactorauthentication_disable', {}, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null, data);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.setup = function (data, callback) {
|
||||
post('/api/v1/cloudron/setup', data, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
@@ -1542,7 +1567,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
if (error) return callback(error);
|
||||
if (status !== 201) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null, data.token);
|
||||
callback(null, data);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1821,7 +1846,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
Client.prototype.setTwoFactorAuthenticationSecret = function (callback) {
|
||||
var data = {};
|
||||
|
||||
post('/api/v1/profile/twofactorauthentication', data, null, function (error, data, status) {
|
||||
post('/api/v1/profile/twofactorauthentication_secret', data, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 201) return callback(new ClientError(status, data));
|
||||
|
||||
@@ -1834,7 +1859,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
totpToken: totpToken
|
||||
};
|
||||
|
||||
post('/api/v1/profile/twofactorauthentication/enable', data, null, function (error, data, status) {
|
||||
post('/api/v1/profile/twofactorauthentication_enable', data, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 202) return callback(new ClientError(status, data));
|
||||
|
||||
@@ -1847,7 +1872,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
password: password
|
||||
};
|
||||
|
||||
post('/api/v1/profile/twofactorauthentication/disable', data, null, function (error, data, status) {
|
||||
post('/api/v1/profile/twofactorauthentication_disable', data, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 202) return callback(new ClientError(status, data));
|
||||
|
||||
@@ -1922,7 +1947,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
|
||||
Client.prototype._appPostProcess = function (app) {
|
||||
// calculate the icon paths
|
||||
app.iconUrl = app.iconUrl ? (this.apiOrigin + app.iconUrl + '?access_token=' + token + '&' + app.manifest.version) : null;
|
||||
app.iconUrl = app.iconUrl ? (this.apiOrigin + app.iconUrl + '?access_token=' + token + '&ts=' + app.ts) : null;
|
||||
|
||||
// amend the post install confirm state
|
||||
app.pendingPostInstallConfirmation = !!localStorage['confirmPostInstall_' + app.id];
|
||||
@@ -1939,7 +1964,9 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
if (app.manifest.postInstallMessage) {
|
||||
var text= app.manifest.postInstallMessage;
|
||||
// we chose - because underscore has special meaning in markdown
|
||||
text = text.replace(/\$CLOUDRON-APP-DOMAIN/g, app.fqdn);
|
||||
text = text.replace(/\$CLOUDRON-APP-LOCATION/g, app.location);
|
||||
text = text.replace(/\$CLOUDRON-APP-DOMAIN/g, app.domain);
|
||||
text = text.replace(/\$CLOUDRON-APP-FQDN/g, app.fqdn);
|
||||
text = text.replace(/\$CLOUDRON-APP-ORIGIN/g, 'https://' + app.fqdn);
|
||||
text = text.replace(/\$CLOUDRON-API-DOMAIN/g, this._config.adminFqdn);
|
||||
text = text.replace(/\$CLOUDRON-API-ORIGIN/g, 'https://' + this._config.adminFqdn);
|
||||
@@ -2005,6 +2032,8 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachLimit(apps, 20, function (app, iteratorCallback) {
|
||||
app.ssoAuth = (app.manifest.addons['ldap'] || app.manifest.addons['proxyAuth']) && app.sso;
|
||||
|
||||
// only fetch if we have permissions
|
||||
if (!that._userInfo.isAtLeastAdmin) {
|
||||
app.progress = 0;
|
||||
@@ -2471,7 +2500,8 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
var data = {
|
||||
name: name,
|
||||
ownerId: ownerId,
|
||||
ownerType: ownerType
|
||||
ownerType: ownerType,
|
||||
active: true
|
||||
};
|
||||
|
||||
post('/api/v1/mail/' + domain + '/mailboxes', data, null, function (error, data, status) {
|
||||
@@ -2482,10 +2512,11 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.updateMailbox = function (domain, name, ownerId, ownerType, callback) {
|
||||
Client.prototype.updateMailbox = function (domain, name, ownerId, ownerType, active, callback) {
|
||||
var data = {
|
||||
ownerId: ownerId,
|
||||
ownerType: ownerType
|
||||
ownerType: ownerType,
|
||||
active: active
|
||||
};
|
||||
|
||||
post('/api/v1/mail/' + domain + '/mailboxes/' + name, data, null, function (error, data, status) {
|
||||
@@ -2573,7 +2604,8 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
var data = {
|
||||
name: name,
|
||||
members: members,
|
||||
membersOnly: membersOnly
|
||||
membersOnly: membersOnly,
|
||||
active: true
|
||||
};
|
||||
|
||||
post('/api/v1/mail/' + domain + '/lists', data, null, function (error, data, status) {
|
||||
@@ -2584,10 +2616,11 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.updateMailingList = function (domain, name, members, membersOnly, callback) {
|
||||
Client.prototype.updateMailingList = function (domain, name, members, membersOnly, active, callback) {
|
||||
var data = {
|
||||
members: members,
|
||||
membersOnly: membersOnly
|
||||
membersOnly: membersOnly,
|
||||
active: active
|
||||
};
|
||||
|
||||
post('/api/v1/mail/' + domain + '/lists/' + name, data, null, function (error, data, status) {
|
||||
@@ -2626,10 +2659,21 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.addVolume = function (name, hostPath, callback) {
|
||||
Client.prototype.getVolumeStatus = function (volume, callback) {
|
||||
get('/api/v1/volumes/' + volume + '/status', null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null, data);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.addVolume = function (name, hostPath, mountType, mountOptions, callback) {
|
||||
var data = {
|
||||
name: name,
|
||||
hostPath: hostPath
|
||||
hostPath: hostPath,
|
||||
mountType: mountType,
|
||||
mountOptions: mountOptions
|
||||
};
|
||||
var that = this;
|
||||
|
||||
@@ -2637,6 +2681,22 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
if (error) return callback(error);
|
||||
if (status !== 201) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null, data.id);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.updateVolume = function (volumeId, mountType, mountOptions, callback) {
|
||||
var data = {
|
||||
mountType: mountType,
|
||||
mountOptions: mountOptions
|
||||
};
|
||||
|
||||
var that = this;
|
||||
|
||||
post('/api/v1/volumes/' + volumeId, data, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
+30
-13
@@ -127,8 +127,11 @@ app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Cl
|
||||
function openPath(path) {
|
||||
path = sanitize(path);
|
||||
|
||||
// we always show the parent path, even if overlayed by a mediaviewer
|
||||
var parentPath = sanitize(path + '/..');
|
||||
|
||||
$scope.busy = true;
|
||||
|
||||
// nothing changes here, mostly triggered when editor is closed
|
||||
if ($scope.cwd === path) {
|
||||
$scope.busy = false;
|
||||
@@ -140,7 +143,10 @@ app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Cl
|
||||
if (!entry) return Client.error('No such file or folder: ' + path);
|
||||
|
||||
if (entry.isDirectory) {
|
||||
$scope.changeDirectory(path);
|
||||
$scope.cwd = path;
|
||||
|
||||
// refresh will set busy to false once done
|
||||
$scope.refresh();
|
||||
} else if (entry.isFile) {
|
||||
var mimeType = Mimer().get(entry.fileName);
|
||||
var mimeGroup = mimeType.split('/')[0];
|
||||
@@ -154,15 +160,15 @@ app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Cl
|
||||
} else {
|
||||
Client.filesGet($scope.id, $scope.type, path, 'open', function (error) { if (error) return Client.error(error); });
|
||||
}
|
||||
}
|
||||
|
||||
$scope.busy = false;
|
||||
$scope.busy = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Client.filesGet($scope.id, $scope.type, parentPath, 'data', function (error, result) {
|
||||
if (error) return Client.error(error);
|
||||
if (error) return setTimeout(function () { openPath(path); }, 2000); // try again in some time
|
||||
|
||||
// amend icons
|
||||
result.entries.forEach(function (e) {
|
||||
@@ -183,10 +189,9 @@ app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Cl
|
||||
});
|
||||
|
||||
$scope.entries = result.entries;
|
||||
$scope.cwd = parentPath;
|
||||
$scope.cwdParts = parentPath.split('/').filter(function (p) { return !!p; }).map(function (p, i) { return { name: decodeURIComponent(p), path: path.split('/').slice(0, i+1).join('/') }; });
|
||||
|
||||
// call itself now that we know
|
||||
$scope.cwd = parentPath;
|
||||
openPath(path);
|
||||
});
|
||||
}
|
||||
@@ -363,7 +368,6 @@ app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Cl
|
||||
$scope.busy = true;
|
||||
|
||||
Client.filesGet($scope.id, $scope.type, $scope.cwd, 'data', function (error, result) {
|
||||
$scope.busy = false;
|
||||
if (error) return Client.error(error);
|
||||
|
||||
// amend icons
|
||||
@@ -385,6 +389,9 @@ app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Cl
|
||||
});
|
||||
|
||||
$scope.entries = result.entries;
|
||||
$scope.cwdParts = $scope.cwd.split('/').filter(function (p) { return !!p; }).map(function (p, i) { return { name: decodeURIComponent(p), path: $scope.cwd.split('/').slice(0, i+1).join('/') }; });
|
||||
|
||||
$scope.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -536,7 +543,7 @@ app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Cl
|
||||
};
|
||||
|
||||
$scope.goDirectoryUp = function () {
|
||||
$scope.changeDirectory($scope.cwd + '/..');
|
||||
openPath($scope.cwd + '/..');
|
||||
};
|
||||
|
||||
$scope.changeDirectory = function (path) {
|
||||
@@ -545,11 +552,6 @@ app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Cl
|
||||
if ($scope.cwd === path) return;
|
||||
|
||||
location.hash = path;
|
||||
|
||||
$scope.cwd = path;
|
||||
$scope.cwdParts = path.split('/').filter(function (p) { return !!p; }).map(function (p, i) { return { name: decodeURIComponent(p), path: path.split('/').slice(0, i+1).join('/') }; });
|
||||
|
||||
$scope.refresh();
|
||||
};
|
||||
|
||||
$scope.uploadStatus = {
|
||||
@@ -794,6 +796,9 @@ app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Cl
|
||||
},
|
||||
|
||||
close: function () {
|
||||
// set an empty pixel image to bust the cached img to avoid flickering on slow load
|
||||
$scope.mediaViewer.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==';
|
||||
|
||||
$('#mediaViewerModal').modal('hide');
|
||||
}
|
||||
};
|
||||
@@ -1019,7 +1024,19 @@ app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Cl
|
||||
|
||||
init();
|
||||
|
||||
var busyHash = window.location.hash.slice(1);
|
||||
$scope.$watch('busy', function (prev, next) {
|
||||
if (!prev && next) {
|
||||
window.location.hash = busyHash;
|
||||
busyHash = window.location.hash.slice(1);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('hashchange', function () {
|
||||
if ($scope.busy) return false;
|
||||
|
||||
busyHash = window.location.hash.slice(1);
|
||||
|
||||
$scope.$apply(function () {
|
||||
// first close all dialogs
|
||||
$scope.mediaViewer.close();
|
||||
|
||||
+5
-3
@@ -90,9 +90,9 @@ app.config(['$routeProvider', function ($routeProvider) {
|
||||
}).when('/settings', {
|
||||
controller: 'SettingsController',
|
||||
templateUrl: 'views/settings.html?<%= revision %>'
|
||||
}).when('/activity', {
|
||||
controller: 'ActivityController',
|
||||
templateUrl: 'views/activity.html?<%= revision %>'
|
||||
}).when('/eventlog', {
|
||||
controller: 'EventLogController',
|
||||
templateUrl: 'views/eventlog.html?<%= revision %>'
|
||||
}).when('/support', {
|
||||
controller: 'SupportController',
|
||||
templateUrl: 'views/support.html?<%= revision %>'
|
||||
@@ -291,6 +291,7 @@ app.filter('installationStateLabel', function () {
|
||||
return 'Migrating data' + waiting;
|
||||
case ISTATES.PENDING_UNINSTALL: return 'Uninstalling' + waiting;
|
||||
case ISTATES.PENDING_RESTORE: return 'Restoring' + waiting;
|
||||
case ISTATES.PENDING_IMPORT: return 'Importing' + waiting;
|
||||
case ISTATES.PENDING_UPDATE: return 'Updating' + waiting;
|
||||
case ISTATES.PENDING_BACKUP: return 'Backing up' + waiting;
|
||||
case ISTATES.PENDING_START: return 'Starting' + waiting;
|
||||
@@ -328,6 +329,7 @@ app.filter('taskName', function () {
|
||||
case ISTATES.PENDING_DATA_DIR_MIGRATION: return 'data migration';
|
||||
case ISTATES.PENDING_UNINSTALL: return 'uninstall';
|
||||
case ISTATES.PENDING_RESTORE: return 'restore';
|
||||
case ISTATES.PENDING_IMPORT: return 'import';
|
||||
case ISTATES.PENDING_UPDATE: return 'update';
|
||||
case ISTATES.PENDING_BACKUP: return 'backup';
|
||||
case ISTATES.PENDING_START: return 'start app';
|
||||
|
||||
+6
-1
@@ -101,7 +101,12 @@ app.controller('LoginController', ['$scope', '$translate', '$http', function ($s
|
||||
if (status !== 200) return error();
|
||||
|
||||
localStorage.token = data.accessToken;
|
||||
window.location.href = search.returnTo || '/';
|
||||
|
||||
// prevent redirecting to random domains
|
||||
var returnTo = search.returnTo || '/';
|
||||
if (returnTo.indexOf('/') !== 0) returnTo = '/';
|
||||
|
||||
window.location.href = returnTo;
|
||||
}).error(error);
|
||||
};
|
||||
|
||||
|
||||
+10
-16
@@ -3,14 +3,14 @@
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
|
||||
angular.module('Application').controller('MainController', ['$scope', '$route', '$timeout', '$location', 'Client', function ($scope, $route, $timeout, $location, Client) {
|
||||
angular.module('Application').controller('MainController', ['$scope', '$route', '$timeout', '$location', '$interval', 'Client', function ($scope, $route, $timeout, $location, $interval, Client) {
|
||||
$scope.initialized = false; // used to animate the UI
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
$scope.config = {};
|
||||
$scope.client = Client;
|
||||
$scope.subscription = {};
|
||||
$scope.notifications = [];
|
||||
$scope.notificationCount = 0;
|
||||
$scope.hideNavBarActions = $location.path() === '/logs';
|
||||
|
||||
$scope.reboot = {
|
||||
@@ -63,19 +63,16 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
});
|
||||
};
|
||||
|
||||
function refreshNotifications(poll) {
|
||||
Client.getNotifications(false, 1, 100, function (error, results) {
|
||||
function refreshNotifications() {
|
||||
Client.getNotifications({ acknowledged: false }, 1, 100, function (error, results) { // counter maxes out at 100
|
||||
if (error) console.error(error);
|
||||
else $scope.notifications = results;
|
||||
|
||||
if (poll) $timeout(refreshNotifications, 60 * 1000);
|
||||
else $scope.notificationCount = results.length;
|
||||
});
|
||||
}
|
||||
|
||||
// update state of acknowledged notification
|
||||
$scope.notificationAcknowledged = function (notificationId) {
|
||||
// remove notification from list
|
||||
$scope.notifications = $scope.notifications.filter(function (n) { return n.id !== notificationId; });
|
||||
$scope.notificationAcknowledged = function (ack) {
|
||||
$scope.notificationCount += (ack ? -1 : +1);
|
||||
};
|
||||
|
||||
function init() {
|
||||
@@ -99,7 +96,7 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
}
|
||||
|
||||
// support local development with localhost check
|
||||
if (window.location.hostname !== status.adminFqdn && window.location.hostname !== 'localhost') {
|
||||
if (window.location.hostname !== status.adminFqdn && window.location.hostname !== 'localhost' && !window.location.hostname.startsWith('192.')) {
|
||||
// user is accessing by IP or by the old admin location (pre-migration)
|
||||
window.location.href = '/setupdns.html' + window.location.search;
|
||||
return;
|
||||
@@ -140,7 +137,8 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
return;
|
||||
}
|
||||
|
||||
refreshNotifications(true);
|
||||
$interval(refreshNotifications, 60 * 1000);
|
||||
refreshNotifications();
|
||||
|
||||
$scope.updateSubscriptionStatus();
|
||||
});
|
||||
@@ -156,10 +154,6 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
}
|
||||
});
|
||||
|
||||
Client.onReconnect(function () {
|
||||
refreshNotifications(false);
|
||||
});
|
||||
|
||||
init();
|
||||
|
||||
// setup all the dialog focus handling
|
||||
|
||||
+45
-5
@@ -42,6 +42,17 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
$scope.setupToken = '';
|
||||
$scope.skipDnsSetup = false;
|
||||
|
||||
$scope.mountOptions = {
|
||||
host: '',
|
||||
remoteDir: '',
|
||||
username: '',
|
||||
password: '',
|
||||
diskPath: '',
|
||||
user: '',
|
||||
port: 22,
|
||||
privateKey: ''
|
||||
};
|
||||
|
||||
$scope.sysinfo = {
|
||||
provider: 'generic',
|
||||
ip: '',
|
||||
@@ -130,6 +141,10 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
{ name: 'DE', value: 'https://s3-de-central.profitbricks.com', region: 's3-de-central' }, // default
|
||||
];
|
||||
|
||||
$scope.vultrRegions = [
|
||||
{ name: 'New Jersey', value: 'https://ewr1.vultrobjects.com', region: 'us-east-1' }, // default
|
||||
];
|
||||
|
||||
$scope.wasabiRegions = [
|
||||
{ name: 'EU Central 1', value: 'https://s3.eu-central-1.wasabisys.com' },
|
||||
{ name: 'US East 1', value: 'https://s3.us-east-1.wasabisys.com' },
|
||||
@@ -144,6 +159,7 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
{ name: 'DigitalOcean Spaces', value: 'digitalocean-spaces' },
|
||||
{ name: 'Exoscale SOS', value: 'exoscale-sos' },
|
||||
{ name: 'Filesystem', value: 'filesystem' },
|
||||
{ name: 'Filesystem (Mountpoint)', value: 'mountpoint' }, // legacy
|
||||
{ name: 'Google Cloud Storage', value: 'gcs' },
|
||||
{ name: 'IONOS (Profitbricks)', value: 'ionos-objectstorage' },
|
||||
{ name: 'Linode Object Storage', value: 'linode-objectstorage' },
|
||||
@@ -153,6 +169,7 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
{ name: 'S3 API Compatible (v4)', value: 's3-v4-compat' },
|
||||
{ name: 'Scaleway Object Storage', value: 'scaleway-objectstorage' },
|
||||
{ name: 'SSHFS Mount', value: 'sshfs' },
|
||||
{ name: 'Vultr Object Storage', value: 'vultr-objectstorage' },
|
||||
{ name: 'Wasabi', value: 'wasabi' }
|
||||
];
|
||||
|
||||
@@ -165,11 +182,11 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' || provider === 'exoscale-sos'
|
||||
|| provider === 'digitalocean-spaces' || provider === 'wasabi' || provider === 'scaleway-objectstorage'
|
||||
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'backblaze-b2'
|
||||
|| provider === 'ionos-objectstorage';
|
||||
|| provider === 'ionos-objectstorage' || provider === 'vultr-objectstorage';
|
||||
};
|
||||
|
||||
$scope.mountlike = function (provider) {
|
||||
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs';
|
||||
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4';
|
||||
};
|
||||
|
||||
$scope.restore = function () {
|
||||
@@ -217,6 +234,9 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
} else if (backupConfig.provider === 'ionos-objectstorage') {
|
||||
backupConfig.region = $scope.ionosRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'vultr-objectstorage') {
|
||||
backupConfig.region = $scope.vultrRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'digitalocean-spaces') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
}
|
||||
@@ -240,11 +260,31 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
$scope.busy = false;
|
||||
return;
|
||||
}
|
||||
} else if (backupConfig.provider === 'sshfs' || backupConfig.provider === 'cifs' || backupConfig.provider === 'nfs') {
|
||||
backupConfig.mountPoint = $scope.mountPoint;
|
||||
} else if ($scope.mountlike(backupConfig.provider)) {
|
||||
backupConfig.prefix = $scope.prefix;
|
||||
backupConfig.mountOptions = {};
|
||||
|
||||
if (backupConfig.provider === 'cifs' || backupConfig.provider === 'sshfs' || backupConfig.provider === 'nfs') {
|
||||
backupConfig.mountPoint = '/mnt/cloudronbackup'; // harcoded for ease of use
|
||||
backupConfig.mountOptions.host = $scope.mountOptions.host;
|
||||
backupConfig.mountOptions.remoteDir = $scope.mountOptions.remoteDir;
|
||||
|
||||
if (backupConfig.provider === 'cifs') {
|
||||
backupConfig.mountOptions.username = $scope.mountOptions.username;
|
||||
backupConfig.mountOptions.password = $scope.mountOptions.password;
|
||||
} else if (backupConfig.provider === 'sshfs') {
|
||||
backupConfig.mountOptions.user = $scope.mountOptions.user;
|
||||
backupConfig.mountOptions.port = $scope.mountOptions.port;
|
||||
backupConfig.mountOptions.privateKey = $scope.mountOptions.privateKey;
|
||||
}
|
||||
} else if (backupConfig.provider === 'ext4') {
|
||||
backupConfig.mountPoint = '/mnt/cloudronbackup'; // harcoded for ease of use
|
||||
backupConfig.mountOptions.diskPath = $scope.diskPath;
|
||||
} else if (backupConfig.provider === 'mountpoint') {
|
||||
backupConfig.mountPoint = $scope.mountPoint;
|
||||
}
|
||||
} else if (backupConfig.provider === 'filesystem') {
|
||||
backupConfig.backupFolder = $scope.backupFolder;
|
||||
backupConfig.backupFolder = $scope.configureBackup.backupFolder;
|
||||
}
|
||||
|
||||
if ($scope.backupId.indexOf('/') === -1) {
|
||||
|
||||
+6
-1
@@ -89,6 +89,7 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
{ name: 'Name.com', value: 'namecom' },
|
||||
{ name: 'Namecheap', value: 'namecheap' },
|
||||
{ name: 'Netcup', value: 'netcup' },
|
||||
{ name: 'Vultr', value: 'vultr' },
|
||||
{ name: 'Wildcard', value: 'wildcard' },
|
||||
{ name: 'Manual (not recommended)', value: 'manual' },
|
||||
{ name: 'No-op (only for development)', value: 'noop' }
|
||||
@@ -107,6 +108,7 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
godaddyApiKey: '',
|
||||
godaddyApiSecret: '',
|
||||
linodeToken: '',
|
||||
vultrToken: '',
|
||||
nameComUsername: '',
|
||||
nameComToken: '',
|
||||
namecheapUsername: '',
|
||||
@@ -191,6 +193,8 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
config.tokenType = $scope.dnsCredentials.cloudflareTokenType;
|
||||
} else if (provider === 'linode') {
|
||||
config.token = $scope.dnsCredentials.linodeToken;
|
||||
} else if (provider === 'vultr') {
|
||||
config.token = $scope.dnsCredentials.vultrToken;
|
||||
} else if (provider === 'namecom') {
|
||||
config.username = $scope.dnsCredentials.nameComUsername;
|
||||
config.token = $scope.dnsCredentials.nameComToken;
|
||||
@@ -288,9 +292,10 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
|
||||
if (status.provider === 'digitalocean' || status.provider === 'digitalocean-mp') {
|
||||
$scope.dnsCredentials.provider = 'digitalocean';
|
||||
// don't suggest linode by default since it takes a while for DNS to propagate
|
||||
} else if (status.provider === 'linode' || status.provider === 'linode-oneclick' || status.provider === 'linode-stackscript') {
|
||||
$scope.dnsCredentials.provider = 'linode';
|
||||
} else if (status.provider === 'vultr' || status.provider === 'vultr-mp') {
|
||||
$scope.dnsCredentials.provider = 'vultr';
|
||||
} else if (status.provider === 'gce') {
|
||||
$scope.dnsCredentials.provider = 'gcdns';
|
||||
} else if (status.provider === 'ami') {
|
||||
|
||||
+2
-1
@@ -149,7 +149,8 @@ angular.module('Application').controller('TerminalController', ['$scope', '$tran
|
||||
function createTerminalSocket() {
|
||||
try {
|
||||
// websocket cannot use relative urls
|
||||
var url = Client.apiOrigin.replace('https', 'wss') + '/api/v1/apps/' + $scope.selected.value + '/execws?tty=true&rows=' + $scope.terminal.rows + '&columns=' + $scope.terminal.cols + '&access_token=' + Client.getToken();
|
||||
var cmd = JSON.stringify([ '/bin/bash', '--rcfile', '/app/data/.bashrc' ]);
|
||||
var url = Client.apiOrigin.replace('https', 'wss') + '/api/v1/apps/' + $scope.selected.value + '/execws?tty=true&rows=' + $scope.terminal.rows + '&columns=' + $scope.terminal.cols + '&access_token=' + Client.getToken() + '&cmd=' + cmd;
|
||||
$scope.terminalSocket = new WebSocket(url);
|
||||
$scope.terminal.loadAddon(new AttachAddon.AttachAddon($scope.terminalSocket));
|
||||
|
||||
|
||||
+58
-5
@@ -93,10 +93,58 @@
|
||||
<select class="form-control" id="storageProviderProvider" ng-model="provider" ng-options="a.value as a.name for a in storageProvider" ng-change=clearForm()></select>
|
||||
</div>
|
||||
|
||||
<!-- SSHFS/CIFS/NFS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.mountPoint }" ng-show="mountlike(provider)">
|
||||
<label class="control-label" for="inputConfigureMountPoint">Mountpoint</label>
|
||||
<input type="text" class="form-control" ng-model="mountPoint" id="inputConfigureMountPoint" name="mountPoint" ng-disabled="busy" placeholder="Folder where filesystem is mounted" ng-required="mountlike(provider)">
|
||||
<!-- mountpoint -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.mountPoint }" ng-show="provider === 'mountpoint'">
|
||||
<label class="control-label" for="inputConfigureMountPoint">Mountpoint</label>
|
||||
<input type="text" class="form-control" ng-model="mountPoint" id="inputConfigureMountPoint" name="mountPoint" ng-disabled="busy" placeholder="Folder where filesystem is mounted" ng-required="provider === 'mountpoint'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs' || provider === 'nfs' || provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupHost">Server IP or Hostname</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.host" id="configureBackupHost" name="host" ng-disabled="busy" placeholder="Server IP or hostname" ng-required="provider === 'cifs' || provider === 'nfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs' || provider === 'nfs' || provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupRemoteDir">Remote Directory</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.remoteDir" id="configureBackupRemoteDir" name="remoteDir" ng-disabled="busy" placeholder="/share" ng-required="provider === 'cifs' || provider === 'nfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupUsername">Username ({{ provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.username" id="configureBackupUsername" name="username" ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupPassword">Password ({{ provider }})</label>
|
||||
<input type="password" class="form-control" ng-model="mountOptions.password" id="configureBackupPassword" name="password" ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<!-- EXT4 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.diskPath }" ng-show="provider === 'ext4'">
|
||||
<label class="control-label" for="inputConfigureDiskPath">Disk Path</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.diskPath" id="inputConfigureDiskPath" name="diskPath" ng-disabled="busy" placeholder="Directory for backups" ng-required="provider === 'ext4'">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPort">SSH Port</label>
|
||||
<input type="number" class="form-control" ng-model="mountOptions.port" id="configureBackupPort" name="port" ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupUser">SSH User</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.user" id="configureBackupUser" name="user" ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPrivateKey">SSH Private Key</label>
|
||||
<textarea class="form-control" ng-model="mountOptions.privateKey" id="configureBackupPrivateKey" name="privateKey" ng-disabled="busy"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Filesystem -->
|
||||
@@ -124,7 +172,7 @@
|
||||
<input type="text" class="form-control" ng-model="bucket" id="inputConfigureBackupBucket" name="bucket" ng-disabled="busy" ng-required="s3like(provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.prefix }" ng-show="s3like(provider) || provider === 'gcs'">
|
||||
<div class="form-group" ng-class="{ 'has-error': error.prefix }" ng-show="provider !== 'filesystem' && provider !== 'noop'">
|
||||
<label class="control-label" for="inputConfigureBackupPrefix">Prefix</label>
|
||||
<input type="text" class="form-control" ng-model="prefix" id="inputConfigureBackupPrefix" name="prefix" ng-disabled="busy" placeholder="Prefix for backup file names">
|
||||
</div>
|
||||
@@ -174,6 +222,11 @@
|
||||
<select class="form-control" name="region" id="inputConfigureBackupIonosRegion" ng-model="endpoint" ng-options="a.value as a.name for a in ionosRegions" ng-disabled="busy" ng-required="provider === 'ionos-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'vultr-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupVultrRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupVultrRegion" ng-model="endpoint" ng-options="a.value as a.name for a in vultrRegions" ng-disabled="busy" ng-required="provider === 'vultr-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.accessKeyId }" ng-show="s3like(provider)">
|
||||
<label class="control-label" for="inputConfigureBackupAccessKeyId">Access key id</label>
|
||||
<input type="text" class="form-control" ng-model="accessKeyId" id="inputConfigureBackupAccessKeyId" name="accessKeyId" ng-disabled="busy" ng-required="s3like(provider)">
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<h1>Welcome to Cloudron</h1>
|
||||
<h3>Setup Admin Account</h3>
|
||||
<h3>Set up Admin Account</h3>
|
||||
<p class="has-error text-center" ng-show="owner.error.generic">{{ owner.error.generic }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+8
-2
@@ -211,14 +211,20 @@
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.linodeToken" name="linodeToken" ng-required="dnsCredentials.provider === 'linode'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- Vultr -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'vultr'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.vultrToken" name="vultrToken" ng-required="dnsCredentials.provider === 'vultr'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- Wildcard -->
|
||||
<p class="small text-info" ng-show="dnsCredentials.provider === 'wildcard'">
|
||||
<span>Setup A records for <b>*.{{ dnsCredentials.domain || 'example.com' }}</b> and <b>{{ dnsCredentials.domain || 'example.com' }}</b> to this server's IP.</span>
|
||||
<span>Set up A records for <b>*.{{ dnsCredentials.domain || 'example.com' }}.</b> and <b>{{ dnsCredentials.domain || 'example.com' }}.</b> to this server's IP.</span>
|
||||
</p>
|
||||
|
||||
<!-- Manual -->
|
||||
<p class="small text-info" ng-show="dnsCredentials.provider === 'manual'">
|
||||
<span>Setup an A record for <b>my.{{ dnsCredentials.domain || 'example.com' }}</b> to this server's IP.<br/></span>
|
||||
<span>Set up an A record for <b>my.{{ dnsCredentials.domain || 'example.com' }}.</b> to this server's IP.<br/></span>
|
||||
</p>
|
||||
|
||||
<p class="small text-info" ng-show="needsPort80(dnsCredentials.provider, dnsCredentials.tlsConfig.provider)">Let's Encrypt requires your server to be reachable on port 80</p>
|
||||
|
||||
+80
-86
@@ -132,27 +132,13 @@ input[type="checkbox"], input[type="radio"] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.tooltip.long {
|
||||
.tooltip-inner {
|
||||
max-width: 400px;
|
||||
white-space: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip.long.nowrap {
|
||||
.tooltip-inner {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-inner {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
pointer-events: none;
|
||||
|
||||
.tooltip-inner {
|
||||
max-width: 800px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.btn.pull-right {
|
||||
@@ -310,6 +296,10 @@ textarea {
|
||||
|
||||
@media(max-width:767px) {
|
||||
width: 100%;
|
||||
|
||||
&.admin-action {
|
||||
height: 270px;
|
||||
}
|
||||
}
|
||||
|
||||
.col-xs-12 {
|
||||
@@ -321,68 +311,67 @@ textarea {
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grid-item-content {
|
||||
position: relative; // required to make action buttons positioned absolute within the element
|
||||
background-color: white;
|
||||
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.grid-item-top {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.grid-item-top-title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-size: 24px;
|
||||
font-family: $font-family-heading;
|
||||
}
|
||||
|
||||
.grid-item-actions {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
background-color: transparent;
|
||||
transition: opacity 250ms, right 250ms;
|
||||
|
||||
@media(max-width:767px) {
|
||||
opacity: 1;
|
||||
top: 0;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
.grid-item-content {
|
||||
display: block;
|
||||
text-align: center;
|
||||
transition: transform 150ms ease-in;
|
||||
margin: 10px 0;
|
||||
background-color: white;
|
||||
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
height: 100%;
|
||||
|
||||
@media(max-width:767px) {
|
||||
margin: 40px 0;
|
||||
transform: scale(1.8);
|
||||
&:focus-within {
|
||||
box-shadow: 0px 2px 5px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
@media(min-width:768px) {
|
||||
&:hover {
|
||||
transform: scale(2);
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1) !important;
|
||||
|
||||
& > .grid-item-action {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grid-items-action-tooltip {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.grid-item-action {
|
||||
border: none;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
float: right;
|
||||
margin-bottom: -14px;
|
||||
background-color: unset;
|
||||
opacity: 0;
|
||||
transition: opacity 200ms;
|
||||
color: $text-dark;
|
||||
|
||||
@media(min-width:768px) {
|
||||
.grid-item:hover .grid-item-actions {
|
||||
opacity: 1;
|
||||
right: 10px;
|
||||
&:hover {
|
||||
color: $brand-primary;
|
||||
background-color: rgba(256, 256, 256, 0.5);
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-item-top {
|
||||
position: relative;
|
||||
padding: 10px 15px;
|
||||
height: 100%;
|
||||
padding-top: 30px; // offset for non-admins
|
||||
|
||||
.grid-item-top-title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-size: 24px;
|
||||
font-family: $font-family-heading;
|
||||
}
|
||||
|
||||
.usermanagement-indicator {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
color: #03a9f49e;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,7 +394,6 @@ textarea {
|
||||
&:hover {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.app-postinstall-message {
|
||||
@@ -433,10 +421,6 @@ textarea {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.app-tooltip {
|
||||
left: 32px !important;
|
||||
}
|
||||
|
||||
multiselect {
|
||||
&.stretch {
|
||||
button {
|
||||
@@ -656,10 +640,11 @@ multiselect {
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
&>div {
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
padding-top: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,6 +678,9 @@ multiselect {
|
||||
}
|
||||
|
||||
.app-configure-links {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
div {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
@@ -713,13 +701,13 @@ multiselect {
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
background-color: #f3f3f3;
|
||||
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: -4px 3px 5px -2px rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: white;
|
||||
color: $navbar-default-link-color;
|
||||
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: -4px 3px 5px -2px rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
@@ -1810,10 +1798,6 @@ tag-input {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
&.busy {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.top-scroll-indicator {
|
||||
transition: box-shadow 200ms;
|
||||
box-shadow: inset 0 10px 10px -10px rgba(0,0,0,0.5);
|
||||
@@ -1897,8 +1881,18 @@ tag-input {
|
||||
background-color: $backgroundDark;
|
||||
}
|
||||
|
||||
.grid-item-content {
|
||||
background-color: $backgroundDark;
|
||||
.grid-item {
|
||||
.grid-item-content {
|
||||
background-color: $backgroundDark !important;
|
||||
|
||||
.grid-item-action {
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer, .card, .app-configure-links div.active {
|
||||
|
||||
+98
-19
@@ -17,7 +17,8 @@
|
||||
"tagsFilterHeaderAll": "Alle Schlagworte",
|
||||
"tagsFilterHeader": "Schlagworte: {{ tags }}",
|
||||
"stateFilterHeader": "Jeder Status",
|
||||
"searchPlaceholder": "Suche Apps"
|
||||
"searchPlaceholder": "Suche Apps",
|
||||
"groupsFilterHeader": "Wähle Gruppe"
|
||||
},
|
||||
"main": {
|
||||
"offline": "Cloudron ist nicht erreichbar. Verbindungsaufbau…",
|
||||
@@ -54,6 +55,22 @@
|
||||
"description": "Einen Neustart verwenden, um Sicherheitsupdates anzuwenden oder wenn ein unerwartetes Verhalten festgestellt wurde. Alle Anwendungen und Dienste, die derzeit auf dieser Cloudron-Instanz laufen, werden automatisch gestartet, wenn der Neustart abgeschlossen ist.",
|
||||
"warning": "Ein Neustart des Servers führt zu temporären Ausfallzeiten für alle Anwendungen, die auf dieser Cloudron-Instanz installiert sind!",
|
||||
"title": "Den Server wirklich neustarten?"
|
||||
},
|
||||
"searchPlaceholder": "Suche",
|
||||
"multiselect": {
|
||||
"selected": "{{ n }} ausgewählt",
|
||||
"select": "Auswählen",
|
||||
"filterPlaceholder": "Tippen um zu filtern"
|
||||
},
|
||||
"prettyDate": {
|
||||
"justNow": "gerade eben",
|
||||
"yeserday": "Gestern",
|
||||
"yearsAgo": "vor {{ y }} Jahren",
|
||||
"minutesAgo": "vor {{ m }} Minuten",
|
||||
"hoursAgo": "vor {{ h }} Stunden",
|
||||
"daysAgo": "vor {{ d }} Tagen",
|
||||
"weeksAgo": "vor {{ w }} Wochen",
|
||||
"monthsAgo": "vor {{ m }} Monaten"
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
@@ -103,7 +120,8 @@
|
||||
"description": "Cloudron kann <a href=\"{{ customAppsLink }}\" target=\"_blank\">benutzerdefinierte Anwendungen</a> aus einem privaten Docker-Register laden und installieren.",
|
||||
"subscriptionRequired": "Diese Funktion ist nur im Abo enthalten.",
|
||||
"setupSubscriptionAction": "Abonnenement jetzt abschließen",
|
||||
"usernameNotSet": "Nicht gesetzt"
|
||||
"usernameNotSet": "Nicht gesetzt",
|
||||
"serverNotSet": "Nicht gesetzt"
|
||||
},
|
||||
"updates": {
|
||||
"checkForUpdatesAction": "Auf Aktualisierungen überprüfen",
|
||||
@@ -156,7 +174,9 @@
|
||||
"updateAction": "Aktualisierung"
|
||||
},
|
||||
"registryConfig": {
|
||||
"provider": "Docker Registry Anbieter"
|
||||
"provider": "Docker Registry Anbieter",
|
||||
"providerOther": "Sonstige",
|
||||
"providerDisabled": "Deaktiviert"
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
@@ -184,7 +204,9 @@
|
||||
"subscriptionRequiredAction": "Abonnenement jetzt abschließen",
|
||||
"subscriptionRequired": "Diese Funktion ist nur im Abo enthalten.",
|
||||
"description": "Cloudron synchronisiert User und Gruppen aus dem externen LDAP- oder Active-Directory-Server. Passwörter beim Anmelden werden immer durch den externen Server validiert. Die Synchronisierung läuft nicht automatisch, sondern muss manuell gestartet werden.",
|
||||
"title": "LDAP"
|
||||
"title": "LDAP",
|
||||
"providerOther": "Sonstige",
|
||||
"providerDisabled": "Deaktiviert"
|
||||
},
|
||||
"settings": {
|
||||
"saveAction": "Speichern",
|
||||
@@ -204,7 +226,7 @@
|
||||
"users": {
|
||||
"removeUserTooltip": "User löschen",
|
||||
"editUserTooltip": "User bearbeiten",
|
||||
"resetPasswordTooltip": "Passwort zurücksetzen Link",
|
||||
"resetPasswordTooltip": "Passwort oder 2FA zurücksetzen",
|
||||
"notActivatedYetTooltip": "Dieser User ist noch nicht aktiviert",
|
||||
"externalLdapTooltip": "Aus externem LDAP Verzeichnis",
|
||||
"inactiveTooltip": "Dieser User ist inaktiv",
|
||||
@@ -229,7 +251,13 @@
|
||||
"passwordResetDialog": {
|
||||
"sendEmailLinkAction": "Link per E-Mail an User senden",
|
||||
"description": "Link für Passwort wiederherstellen oder {{ username }} erneut einladen:",
|
||||
"title": "Passwort zurücksetzen Link für {{ username }}"
|
||||
"title": "Passwort oder 2FA zurücksetzen Link für {{ username }}",
|
||||
"reset2FAAction": "2FA zurücksetzen",
|
||||
"emailSent": "Gesendet",
|
||||
"no2FASetup": "User hat 2FA nicht aktiviert.",
|
||||
"2FAIsSetup": "Hier kann das 2FA Setup des User's deaktiviert werden. Es kann anschließend im Profil vom User wieder eingerichtet werden.",
|
||||
"newLinkAction": "Neuen Link erstellen",
|
||||
"resetLinkExplanation": "Hier kann ein neuer Link für die initiale User Aktivierung oder zum Passwort zurücksetzen erstellt werden. Dies macht den vorherigen Link ungültig."
|
||||
},
|
||||
"deleteGroupDialog": {
|
||||
"deleteAction": "Löschen",
|
||||
@@ -287,7 +315,6 @@
|
||||
"setupAction": "Abonnement einrichten",
|
||||
"title": "Abonnement notwendig"
|
||||
},
|
||||
"searchPlaceholder": "Suche",
|
||||
"transferOwnershipDialog": {
|
||||
"transferAction": "Besitzer*in Wechseln",
|
||||
"title": "Wirklich Besitzer*in wechseln?",
|
||||
@@ -382,7 +409,9 @@
|
||||
"expiresAt": "Verfällt am",
|
||||
"name": "Name",
|
||||
"newApiToken": "Neuer API-Token",
|
||||
"title": "API-Tokens"
|
||||
"title": "API-Tokens",
|
||||
"lastUsed": "Zuletzt Verwendet",
|
||||
"neverUsed": "nie"
|
||||
},
|
||||
"passwordRecoveryEmail": "Alternative E-Mail-Adresse"
|
||||
},
|
||||
@@ -554,7 +583,8 @@
|
||||
"matrixHostname": "Matrix Server Domäne",
|
||||
"netcupApiPassword": "API Passwort",
|
||||
"netcupApiKey": "API Key",
|
||||
"netcupCustomerNumber": "Kundennummer"
|
||||
"netcupCustomerNumber": "Kundennummer",
|
||||
"vultrToken": "Vultr Token"
|
||||
},
|
||||
"changeDashboardDomain": {
|
||||
"title": "Die Dashboard-Domäne ändern",
|
||||
@@ -577,13 +607,20 @@
|
||||
"title": "Abonnement erforderlich",
|
||||
"description": "Weitere Domänen hinzufügen ist nur im Abo verfügbar.",
|
||||
"setupAction": "Abonnement erstellen"
|
||||
},
|
||||
"syncDns": {
|
||||
"showLogsAction": "Zeige Logs",
|
||||
"syncAction": "Synchronisiere DNS",
|
||||
"title": "Synchronisiere DNS",
|
||||
"description": "Hiermit werden all App und Email DNS Einträge über alle Domains neu erstellt."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Benachrichtigungen",
|
||||
"nonePending": "Alles erledigt!",
|
||||
"dismissTooltip": "Verwerfen",
|
||||
"clearAll": "Alles löschen"
|
||||
"clearAll": "Alles löschen",
|
||||
"markAllAsRead": "Alle als gelesen markieren"
|
||||
},
|
||||
"system": {
|
||||
"title": "Systeminformationen",
|
||||
@@ -641,7 +678,16 @@
|
||||
"downloadConcurrencyDescription": "Anzahl der Dateien, die beim Wiederherstellen parallel heruntergeladen werden",
|
||||
"downloadConcurrency": "Gleichzeitiges Herunterladen",
|
||||
"uploadPartSizeDescription": "Paketgröße beim Hochladen. Bis zu 3 Pakete werden gleichzeitig hochgeladen. Dementsprechend wird auch Arbeitsspeicher benötigt.",
|
||||
"memoryLimitDescription": "Arbeitsspeicherlimit für die Datensicherung. Das Limit erhöhen, wenn die Datensicherung-Concurrency erhöht wird."
|
||||
"memoryLimitDescription": "Arbeitsspeicherlimit für die Datensicherung. Das Limit erhöhen, wenn die Datensicherung-Concurrency erhöht wird.",
|
||||
"server": "Server IP oder Hostname",
|
||||
"remoteDirectory": "Remote-Verzeichnis",
|
||||
"username": "Username",
|
||||
"password": "Passwort",
|
||||
"configureMount": "Konfiguration des Einhängepunkts",
|
||||
"setupMountDescription": "Wenn aktiv, konfiguriert Cloudron den Einhängepunkts auf dem Server",
|
||||
"port": "Port",
|
||||
"user": "User",
|
||||
"privateKey": "Privater Schlüssel"
|
||||
},
|
||||
"configureBackupSchedule": {
|
||||
"retentionPolicy": "Aufbewahrungsrichtlinie",
|
||||
@@ -774,7 +820,8 @@
|
||||
"document": "Dokumente",
|
||||
"blog": "Blog",
|
||||
"chat": "Chat",
|
||||
"analytics": "Analytics"
|
||||
"analytics": "Analytics",
|
||||
"federated": "Föderiert"
|
||||
},
|
||||
"categoryLabel": "Kategorie"
|
||||
},
|
||||
@@ -942,7 +989,8 @@
|
||||
"title": "Mail-Liste hinzufügen",
|
||||
"members": "Listen-Mitglieder",
|
||||
"membersInfo": "Mehrere E-Mail-Adressen jeweils in eine neue Zeile",
|
||||
"membersOnlyCheckbox": "Den Mailversand an diese Liste so einschränken, dass nur Mitglieder senden dürfen."
|
||||
"membersOnlyCheckbox": "Den Mailversand an diese Liste so einschränken, dass nur Mitglieder senden dürfen.",
|
||||
"name": "Name"
|
||||
},
|
||||
"mailboxboxDialog": {
|
||||
"groupsHeader": "Gruppen",
|
||||
@@ -992,6 +1040,12 @@
|
||||
},
|
||||
"editMailinglistDialog": {
|
||||
"title": "Die Mail-Liste {{ name }}@{{ domain }} bearbeiten"
|
||||
},
|
||||
"updateMailboxDialog": {
|
||||
"activeCheckbox": "Postfach ist aktiv"
|
||||
},
|
||||
"updateMailinglistDialog": {
|
||||
"activeCheckbox": "Mailing-Liste ist aktiv"
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
@@ -1202,7 +1256,12 @@
|
||||
"title": "E-Mail FROM Adresse",
|
||||
"description": "Hier wird die Adresse festgelegt, von der diese Anwendung E-Mails sendet. Diese App ist bereits so konfiguriert, dass sie E-Mails unter Verwendung der Einstellungen {{ domain }} <a href=\"{{ domainConfigLink }}\">ausgehende E-Mail</a> sendet.",
|
||||
"mailboxPlaceholder": "Leer lassen, um Systemvorgabe zu verwenden",
|
||||
"saveAction": "Speichern"
|
||||
"saveAction": "Speichern",
|
||||
"disableDescription": "Die E-Mail Einstellungen werden nicht automatisch vorgenommen, dies muss in der App selbst gemacht werden.",
|
||||
"enable": "Verwende Cloudron um E-Mails zu versenden",
|
||||
"enableDescription": "Diese App ist verwendet die <a href=\"{{ domainConfigLink }}\">ausgehende E-Mail Konfiguration</a> der Domäne {{ domain }}.",
|
||||
"disable": "E-Mail Konfiguration nicht automatisch vornehmen",
|
||||
"description2": "Wenn dies aktiviert ist, wird der interne E-Mail Server verwendet. Dieser verwendet die <a href=\"{{ domainConfigLink }}\">ausgehende E-Mail Konfiguration</a> der Domäne {{ domain }}. Wenn dies deaktiviert ist, muss die E-Mail Konfiguration in der App selber vorgenommen werden."
|
||||
},
|
||||
"csp": {
|
||||
"title": "Content-Security-Policy"
|
||||
@@ -1379,9 +1438,9 @@
|
||||
"24h": "24 Stunden",
|
||||
"7d": "7 Tage",
|
||||
"30d": "30 Tage",
|
||||
"12h": "12 Stunden"
|
||||
"12h": "12 Stunden",
|
||||
"6h": "6 Stunden"
|
||||
},
|
||||
"selectPeriod": "Periode {{ period }} auswählen",
|
||||
"memoryTitle": "Speicher (RAM + Swap) in MB"
|
||||
},
|
||||
"uninstallTabTitle": "Deinstallieren",
|
||||
@@ -1411,6 +1470,9 @@
|
||||
"notResponding": "Nicht Ansprechbar",
|
||||
"stopped": "Angehalten",
|
||||
"running": "Laufend"
|
||||
},
|
||||
"stopDialog": {
|
||||
"title": "App {{ app }} wirklich stoppen?"
|
||||
}
|
||||
},
|
||||
"logs": {
|
||||
@@ -1426,7 +1488,8 @@
|
||||
"nl": "Niederländisch",
|
||||
"zh_Hans": "Chinesisch (vereinfacht)",
|
||||
"vi": "Vietnamesisch",
|
||||
"pl": "Polnisch"
|
||||
"pl": "Polnisch",
|
||||
"es": "Spanisch"
|
||||
},
|
||||
"storage": {
|
||||
"mounts": {
|
||||
@@ -1444,14 +1507,30 @@
|
||||
"addVolumeDialog": {
|
||||
"addAction": "Hinzufügen",
|
||||
"nameWarning": "Cloudron wird den Host-Pfad in den Container der Anwendung mit diesem Namen unter <code>/media</code> einhängen.",
|
||||
"title": "Datenträger hinzufügen"
|
||||
"title": "Datenträger hinzufügen",
|
||||
"server": "Server IP oder Hostname",
|
||||
"remoteDirectory": "Remote-Verzeichnis",
|
||||
"username": "Username",
|
||||
"password": "Passwort",
|
||||
"diskPath": "Festplattenpfad",
|
||||
"noopWarning": "Cloudron konfiguriert den Server nicht um diesen Datenträger einzubinden",
|
||||
"mountTypeInfo": "Cloudron konfiguriert den Server um diesen Datenträger einzubinden",
|
||||
"port": "Port",
|
||||
"user": "User",
|
||||
"privateKey": "Privater SSH Schlüssel"
|
||||
},
|
||||
"removeVolumeActionTooltip": "Datenträger entfernen",
|
||||
"openFileManagerActionTooltip": "File-Manager öffnen",
|
||||
"name": "Name",
|
||||
"hostPath": "Host-Pfad",
|
||||
"addVolumeAction": "Datenträger hinzufügen",
|
||||
"title": "Datenträger"
|
||||
"title": "Datenträger",
|
||||
"mountType": "Einhängepunkttyp",
|
||||
"updateVolumeDialog": {
|
||||
"title": "Konfiguriere Datenträger {{ volume }}"
|
||||
},
|
||||
"tooltipEdit": "Konfiguriere Datenträger",
|
||||
"mountStatus": "Einhängestatus"
|
||||
},
|
||||
"lang.ja": "Japanisch"
|
||||
}
|
||||
|
||||
+126
-49
@@ -55,6 +55,22 @@
|
||||
"warning": "Rebooting the server will cause temporary downtime for all apps installed on this Cloudron!",
|
||||
"description": "Use this to apply security updates or if you experience unexpected behavior. All apps and services currently running on this Cloudron will automatically start when the reboot is complete.",
|
||||
"rebootAction": "Reboot now"
|
||||
},
|
||||
"searchPlaceholder": "Search",
|
||||
"multiselect": {
|
||||
"selected": "{{ n }} selected",
|
||||
"select": "Select",
|
||||
"filterPlaceholder": "Type to filter options"
|
||||
},
|
||||
"prettyDate": {
|
||||
"justNow": "just now",
|
||||
"yeserday": "Yesterday",
|
||||
"minutesAgo": "{{ m }} minutes ago",
|
||||
"hoursAgo": "{{ h }} hours ago",
|
||||
"daysAgo": "{{ d }} days ago",
|
||||
"weeksAgo": "{{ w }} weeks ago",
|
||||
"monthsAgo": "{{ m }} months ago",
|
||||
"yearsAgo": "{{ y }} years ago"
|
||||
}
|
||||
},
|
||||
"appstore": {
|
||||
@@ -81,7 +97,8 @@
|
||||
"sync": "File Sync",
|
||||
"project": "Project Management",
|
||||
"wiki": "Wiki",
|
||||
"vpn": "VPN"
|
||||
"vpn": "VPN",
|
||||
"federated": "Federated"
|
||||
},
|
||||
"searchPlaceholder": "Search for alternatives like Github, Dropbox, Slack, Trello, …",
|
||||
"noAppsFound": "No apps found.",
|
||||
@@ -106,7 +123,7 @@
|
||||
"lowOnResources": "This Cloudron is running low on resources.",
|
||||
"pleaseUpgradeServer": "Please upgrade to a server instance with more memory. Alternately, free up resources by uninstalling unused applications.",
|
||||
"subscriptionRequired": "To install more apps, a paid subscription is required.",
|
||||
"setupSubscriptionAction": "Setup Subscription",
|
||||
"setupSubscriptionAction": "Set up Subscription",
|
||||
"installAnywayAction": "Install anyway",
|
||||
"installAction": "Install",
|
||||
"doInstallAction": "Install {{ dnsOverwrite ? 'and overwrite DNS' : '' }}"
|
||||
@@ -146,7 +163,7 @@
|
||||
"inactiveTooltip": "User is inactive",
|
||||
"externalLdapTooltip": "From external LDAP directory",
|
||||
"notActivatedYetTooltip": "User is not activated yet",
|
||||
"resetPasswordTooltip": "Reset password or invite link",
|
||||
"resetPasswordTooltip": "Reset password, disable 2FA or send invite link",
|
||||
"editUserTooltip": "Edit User",
|
||||
"removeUserTooltip": "Remove User",
|
||||
"transferOwnershipTooltip": "Transfer Ownership"
|
||||
@@ -161,16 +178,16 @@
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"allowProfileEditCheckbox": "Allow users to edit their name and email",
|
||||
"require2FACheckbox": "Require users to setup 2FA",
|
||||
"require2FACheckbox": "Require users to set up 2FA",
|
||||
"subscriptionRequired": "These features are only available in the paid plans.",
|
||||
"subscriptionRequiredAction": "Setup Subscription Now",
|
||||
"subscriptionRequiredAction": "Set up Subscription Now",
|
||||
"saveAction": "Save"
|
||||
},
|
||||
"externalLdap": {
|
||||
"title": "LDAP",
|
||||
"description": "Cloudron will synchronize users and groups from an external LDAP or ActiveDirectory server. Password verification for authenticating those users is done against the external server. The synchronization is not run automatically but needs to be triggered manually.",
|
||||
"subscriptionRequired": "This feature is only available in the paid plans.",
|
||||
"subscriptionRequiredAction": "Setup Subscription Now",
|
||||
"subscriptionRequiredAction": "Set up Subscription Now",
|
||||
"noopInfo": "LDAP authentication is not configured.",
|
||||
"provider": "Provider",
|
||||
"server": "Server URL",
|
||||
@@ -189,11 +206,13 @@
|
||||
"configureAction": "Configure",
|
||||
"bindUsername": "Bind DN/Username (optional)",
|
||||
"bindPassword": "Bind Password (optional)",
|
||||
"errorSelfSignedCert": "Server is using an invalid or self-signed certificate."
|
||||
"errorSelfSignedCert": "Server is using an invalid or self-signed certificate.",
|
||||
"providerOther": "Other",
|
||||
"providerDisabled": "Disabled"
|
||||
},
|
||||
"subscriptionDialog": {
|
||||
"title": "Subscription required",
|
||||
"setupAction": "Setup Subscription"
|
||||
"setupAction": "Set up Subscription"
|
||||
},
|
||||
"addUserDialog": {
|
||||
"title": "Add User",
|
||||
@@ -248,9 +267,15 @@
|
||||
"deleteAction": "Delete"
|
||||
},
|
||||
"passwordResetDialog": {
|
||||
"title": "Reset password or invite link for {{ username }}",
|
||||
"title": "Password/2FA reset for {{ username }}",
|
||||
"description": "Use the link below to reset {{ username }}'s password or re-invite:",
|
||||
"sendEmailLinkAction": "Email link to user"
|
||||
"sendEmailLinkAction": "Email link to user",
|
||||
"emailSent": "Sent",
|
||||
"no2FASetup": "This user has not set up 2FA.",
|
||||
"2FAIsSetup": "Use this to disable user's 2FA. The user can set it up again from the Profile view.",
|
||||
"newLinkAction": "Generate new link",
|
||||
"resetLinkExplanation": "Use this to generate a password reset or invitation link. The new link will invalidate any old link immediately.",
|
||||
"reset2FAAction": "Reset 2FA"
|
||||
},
|
||||
"externalLdapDialog": {
|
||||
"title": "Configure LDAP"
|
||||
@@ -261,7 +286,6 @@
|
||||
"admin": "Administrator",
|
||||
"owner": "Superadmin"
|
||||
},
|
||||
"searchPlaceholder": "Search",
|
||||
"transferOwnershipDialog": {
|
||||
"title": "Really transfer ownership?",
|
||||
"description": "This will make the selected user the owner and admin of this Cloudron and remove admin rights to the current owner.",
|
||||
@@ -299,7 +323,7 @@
|
||||
"authenticatorAppDescription": "Use Google Authenticator (<a href=\"{{ googleAuthenticatorPlayStoreLink }}\" target=\"_blank\">Android</a>, <a href=\"{{ googleAuthenticatorITunesLink }}\" target=\"_blank\">iOS</a>), FreeOTP authenticator (<a href=\"{{ freeOTPPlayStoreLink }}\" target=\"_blank\">Android</a>, <a href=\"{{ freeOTPITunesLink }}\" target=\"_blank\">iOS</a>) or a similar TOTP app to scan the secret.",
|
||||
"token": "Token",
|
||||
"enable": "Enable",
|
||||
"setup2FA": "Setup Two-Factor"
|
||||
"setup2FA": "Set up Two-Factor"
|
||||
},
|
||||
"appPasswords": {
|
||||
"title": "App Passwords",
|
||||
@@ -317,7 +341,9 @@
|
||||
"expiresAt": "Expires At",
|
||||
"description": "Use these personal access tokens to authenticate to the <a target=\"_blank\" href=\"{{ apiDocsLink }}\">Cloudron API</a>",
|
||||
"noTokensPlaceholder": "No API Tokens created",
|
||||
"revokeTokenTooltip": "Revoke Token"
|
||||
"revokeTokenTooltip": "Revoke Token",
|
||||
"lastUsed": "Last Used",
|
||||
"neverUsed": "never"
|
||||
},
|
||||
"loginTokens": {
|
||||
"title": "Login Tokens",
|
||||
@@ -425,7 +451,7 @@
|
||||
"provider": "Storage provider",
|
||||
"noopNote": "This option breaks the backup and restore functionality of Cloudron and should only be used for testing. Please make sure the server is completely backed up using alternate means.",
|
||||
"mountPoint": "Mount point",
|
||||
"mountPointDescription": "The mount point has to be setup manually. See <a href=\"{{ providerDocsLink }}\" target=\"_blank\">docs</a>.",
|
||||
"mountPointDescription": "The mount point has to be set up manually. See <a href=\"{{ providerDocsLink }}\" target=\"_blank\">docs</a>.",
|
||||
"localDirectory": "Local backup directory",
|
||||
"ext4Label": "Backup directory is an external EXT4 Disk",
|
||||
"hardlinksLabel": "Use hardlinks",
|
||||
@@ -455,7 +481,21 @@
|
||||
"copyConcurrencyDescription": "Number of remote file copies in parallel when backing up.",
|
||||
"copyConcurrencyDigitalOceanNote": "DigitalOcean Spaces rate limits at 20.",
|
||||
"encryptionPasswordPlaceholder": "Passphrase used to encrypt the backups",
|
||||
"encryptionPasswordRepeat": "Repeat Password"
|
||||
"encryptionPasswordRepeat": "Repeat Password",
|
||||
"server": "Server IP or Hostname",
|
||||
"remoteDirectory": "Remote Directory",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"configureMount": "Specify mount point configuration",
|
||||
"setupMountDescription": "When checked, Cloudron will configure the mount point on the server",
|
||||
"port": "Port",
|
||||
"user": "User",
|
||||
"privateKey": "Private Key",
|
||||
"diskPath": "Disk Path"
|
||||
},
|
||||
"check": {
|
||||
"noop": "Cloudron backups are disabled. Please ensure this server is backed up using alternate means. See https://docs.cloudron.io/backups/#storage-providers for more information.",
|
||||
"sameDisk": "Cloudron backups are currently on the same disk as the Cloudron server instance. This is dangerous and can lead to complete data loss if the disk fails. See https://docs.cloudron.io/backups/#storage-providers for storing backups in an external location."
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
@@ -466,7 +506,7 @@
|
||||
"title": "Footer",
|
||||
"description": "Use markdown to style the footer.",
|
||||
"subscriptionRequired": "Customizing the footer is only available in the paid plans.",
|
||||
"setupSubscriptionNow": "Setup Subscription Now"
|
||||
"setupSubscriptionNow": "Set up Subscription Now"
|
||||
},
|
||||
"changeLogo": {
|
||||
"title": "Choose Cloudron Avatar"
|
||||
@@ -612,12 +652,12 @@
|
||||
"appstoreAccount": {
|
||||
"title": "Cloudron.io Account",
|
||||
"description": "A Cloudron.io account is used to access the App Store and manage your subscription.",
|
||||
"setupAction": "Setup Account",
|
||||
"setupAction": "Set up Account",
|
||||
"email": "Account Email",
|
||||
"subscription": "Subscription",
|
||||
"cloudronId": "Cloudron ID",
|
||||
"subscriptionEndsAt": "Canceled and ends on",
|
||||
"subscriptionSetupAction": "Setup Subscription",
|
||||
"subscriptionSetupAction": "Set up Subscription",
|
||||
"subscriptionChangeAction": "Change Subscription",
|
||||
"subscriptionReactivateAction": "Reactivate Subscription"
|
||||
},
|
||||
@@ -640,11 +680,12 @@
|
||||
"title": "Private Docker Registry",
|
||||
"description": "Cloudron can pull and install <a href=\"{{ customAppsLink }}\" target=\"_blank\">custom apps</a> from a private docker registry.",
|
||||
"subscriptionRequired": "This feature is only available in the paid plans.",
|
||||
"setupSubscriptionAction": "Setup Subscription Now",
|
||||
"setupSubscriptionAction": "Set up Subscription Now",
|
||||
"server": "Server address",
|
||||
"username": "Username",
|
||||
"usernameNotSet": "Not set",
|
||||
"configureAction": "Configure Registry"
|
||||
"configureAction": "Configure Registry",
|
||||
"serverNotSet": "Not set"
|
||||
},
|
||||
"privateDockerRegistryDialog": {
|
||||
"title": "Private Registry Configuration",
|
||||
@@ -674,7 +715,9 @@
|
||||
"description": "The default language of this Cloudron can be set here. This will be used also for transactional emails like user invitation and password reset. Each user can still change the preferred language for the dashboard individually in the profile."
|
||||
},
|
||||
"registryConfig": {
|
||||
"provider": "Docker Registry Provider"
|
||||
"provider": "Docker Registry Provider",
|
||||
"providerOther": "Other",
|
||||
"providerDisabled": "Disabled"
|
||||
}
|
||||
},
|
||||
"support": {
|
||||
@@ -755,8 +798,8 @@
|
||||
},
|
||||
"subscriptionRequired": {
|
||||
"title": "Subscription required",
|
||||
"description": "To add more domains, please setup a paid plan.",
|
||||
"setupAction": "Setup Subscription"
|
||||
"description": "To add more domains, please set up a paid plan.",
|
||||
"setupAction": "Set up Subscription"
|
||||
},
|
||||
"domainDialog": {
|
||||
"addTitle": "Add Domain",
|
||||
@@ -781,8 +824,8 @@
|
||||
"namecheapUsername": "Namecheap Username",
|
||||
"namecheapApiKey": "API Key",
|
||||
"namecheapInfo": "The server IP needs to be allowlisted for this API Key.",
|
||||
"manualInfo": "All DNS records have to be setup manually before each app installation.",
|
||||
"wildcardInfo": "Setup <i>A</i> records for <b>*.{{ domain }}</b> and <b>{{ domain }}</b> to this server's IP.",
|
||||
"manualInfo": "All DNS records have to be set up manually before each app installation.",
|
||||
"wildcardInfo": "Set up <i>A</i> records for <b>*.{{ domain }}.</b> and <b>{{ domain }}.</b> to this server's IP.",
|
||||
"letsEncryptInfo": "Let's Encrypt requires your server to be reachable on port 80",
|
||||
"advancedAction": "Advanced settings…",
|
||||
"zoneName": "Zone Name (Optional)",
|
||||
@@ -797,7 +840,8 @@
|
||||
"mastodonHostname": "Mastodon server location",
|
||||
"netcupCustomerNumber": "Customer Number",
|
||||
"netcupApiKey": "API Key",
|
||||
"netcupApiPassword": "API Password"
|
||||
"netcupApiPassword": "API Password",
|
||||
"vultrToken": "Vultr Token"
|
||||
},
|
||||
"removeDialog": {
|
||||
"title": "Really remove {{ domain }}?",
|
||||
@@ -815,7 +859,8 @@
|
||||
"title": "Notifications",
|
||||
"nonePending": "All Caught Up!",
|
||||
"dismissTooltip": "Dismiss",
|
||||
"clearAll": "Clear All"
|
||||
"clearAll": "Clear All",
|
||||
"markAllAsRead": "Mark All as Read"
|
||||
},
|
||||
"logs": {
|
||||
"title": "Logs",
|
||||
@@ -972,7 +1017,7 @@
|
||||
"catchall": {
|
||||
"title": "Catch-all",
|
||||
"description": "Emails sent to non existing addresses will be forwarded to the following mailboxes.",
|
||||
"subscriptionRequired": "This feature is only available in the paid plans. <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Setup Subscription Now</a>",
|
||||
"subscriptionRequired": "This feature is only available in the paid plans. <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Set up Subscription Now</a>",
|
||||
"saveAction": "Save"
|
||||
},
|
||||
"incomingServerInfo": "Incoming Mail (IMAP)"
|
||||
@@ -992,7 +1037,7 @@
|
||||
"password": "Password",
|
||||
"saveSuccess": "Saved",
|
||||
"saveAction": "Save",
|
||||
"spfDocInfo": "Cloudron does not automatically setup SPF record. Set it up manually by following the <a href=\"{{ spfDocsLink }}\" target=\"_blank\">{{ name }} docs</a>."
|
||||
"spfDocInfo": "Cloudron does not automatically set up SPF record. Set it up manually by following the <a href=\"{{ spfDocsLink }}\" target=\"_blank\">{{ name }} docs</a>."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -1007,7 +1052,7 @@
|
||||
"signature": {
|
||||
"title": "Signature",
|
||||
"description": "The text here will be attached to all emails going out from this domain.",
|
||||
"subscriptionRequired": "This feature is only available in the paid plans. <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Setup Subscription Now</a>",
|
||||
"subscriptionRequired": "This feature is only available in the paid plans. <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Set up Subscription Now</a>",
|
||||
"plainTextFormat": "Text format",
|
||||
"htmlFormat": "HTML format (Optional)",
|
||||
"saveAction": "Save"
|
||||
@@ -1017,7 +1062,7 @@
|
||||
},
|
||||
"dnsStatus": {
|
||||
"title": "DNS Status",
|
||||
"reSetupAction": "Re-setup DNS",
|
||||
"reSetupAction": "Redo DNS setup",
|
||||
"description": "Status of DNS Records may show an error while DNS is propagating (~5 minutes). See the <a href=\"{{ emailDnsDocsLink }}\" target=\"_blank\">troubleshooting</a> docs for help.",
|
||||
"namecheapInfo": "Namecheap requires manual steps for MX records",
|
||||
"ptrInfo": "The PTR record is set by your VPS provider and not by your DNS provider.",
|
||||
@@ -1038,16 +1083,16 @@
|
||||
},
|
||||
"subscriptionDialog": {
|
||||
"title": "Subscription required",
|
||||
"description": "To add more mailboxes, please setup a paid plain.",
|
||||
"setupAction": "Setup Subscription"
|
||||
"description": "To add more mailboxes, please set up a paid plain.",
|
||||
"setupAction": "Set up Subscription"
|
||||
},
|
||||
"enableEmailDialog": {
|
||||
"title": "Enable Email for {{ domain }}?",
|
||||
"description": "This will configure Cloudron to receive emails for <b>{{ domain }}</b>. See the documentation for opening up the <a href=\"{{ requiredPortsDocsLink }}\" target=\"_blank\">required ports</a> for Cloudron Email.",
|
||||
"noProviderInfo": "No DNS provider is setup. The DNS records listed in the Status tab have to be setup manually.",
|
||||
"noProviderInfo": "No DNS provider is set up. The DNS records listed in the Status tab have to be set up manually.",
|
||||
"cloudflareInfo": "The domain <code>{{ adminDomain }}</code> is managed by Cloudflare. Please verify that Cloudflare proxying is disabled for <code>{{ mailFqdn }}</code> and set to <code>DNS only</code>. This is required because Cloudflare does not proxy email.",
|
||||
"setupDnsCheckbox": "Setup Mail DNS records now",
|
||||
"setupDnsInfo": "Use this option to automatically setup Email related DNS records. Leaving this option unchecked is useful for creating mail boxes and <a href=\"{{ importEmailDocsLink }}\">importing email</a> before going live.",
|
||||
"setupDnsCheckbox": "Set up Mail DNS records now",
|
||||
"setupDnsInfo": "Use this option to automatically set up Email related DNS records. Leaving this option unchecked is useful for creating mail boxes and <a href=\"{{ importEmailDocsLink }}\">importing email</a> before going live.",
|
||||
"enableAction": "Enable"
|
||||
},
|
||||
"disableEmailDialog": {
|
||||
@@ -1078,7 +1123,8 @@
|
||||
"title": "Add Mailing list",
|
||||
"members": "List Members",
|
||||
"membersInfo": "Separate email addresses with a newline",
|
||||
"membersOnlyCheckbox": "Restrict posting to members only"
|
||||
"membersOnlyCheckbox": "Restrict posting to members only",
|
||||
"name": "Name"
|
||||
},
|
||||
"editMailinglistDialog": {
|
||||
"title": "Edit Mailing list {{ name }}@{{ domain }}"
|
||||
@@ -1091,6 +1137,12 @@
|
||||
"mailboxboxDialog": {
|
||||
"usersHeader": "Users",
|
||||
"groupsHeader": "Groups"
|
||||
},
|
||||
"updateMailinglistDialog": {
|
||||
"activeCheckbox": "Mailing list is active"
|
||||
},
|
||||
"updateMailboxDialog": {
|
||||
"activeCheckbox": "Mailbox is active"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
@@ -1184,21 +1236,26 @@
|
||||
}
|
||||
},
|
||||
"graphs": {
|
||||
"selectPeriod": "Select Period {{ period }}",
|
||||
"period": {
|
||||
"12h": "12 hours",
|
||||
"24h": "24 hours",
|
||||
"7d": "7 days",
|
||||
"30d": "30 days"
|
||||
"30d": "30 days",
|
||||
"6h": "6 hours"
|
||||
},
|
||||
"memoryTitle": "Memory (RAM + Swap) in MB"
|
||||
},
|
||||
"email": {
|
||||
"from": {
|
||||
"title": "Mail FROM Address",
|
||||
"description": "This sets the address from which this app sends email. This app is already configured to send mail using {{ domain }}'s <a href=\"{{ domainConfigLink }}\">Outbound Email</a> settings.",
|
||||
"description": "This sets the address from which this app sends email. This app is already configured to send mail using {{ domain }}'s <a href=\\\"{{ domainConfigLink }}\\\">Outbound Email</a> settings.",
|
||||
"mailboxPlaceholder": "Leave empty to use platform default",
|
||||
"saveAction": "Save"
|
||||
"saveAction": "Save",
|
||||
"enable": "Use Cloudron Mail to send emails",
|
||||
"description2": "When enabled, the app is configured to send emails via the internal mail server using this address. The internal mail server will use the {{ domain }}'s <a href=\"{{ domainConfigLink }}\">Outbound Email</a> settings to send mail. When disabled, you can configure the email settings within the app.",
|
||||
"disable": "Do not configure mail settings",
|
||||
"enableDescription": "The app is configured to send mails using the address below and the {{ domain }}'s <a href=\\\"{{ domainConfigLink }}\\\">Outbound Email</a> settings.",
|
||||
"disableDescription": "The app's mail delivery settings is left alone. You can configure it inside the app."
|
||||
},
|
||||
"csp": {
|
||||
"title": "Content Security Policy"
|
||||
@@ -1331,9 +1388,9 @@
|
||||
"title": "Update {{ app }}",
|
||||
"unstableWarning": "This update is a pre-release and not considered stable yet. Please update at your own risk.",
|
||||
"changelogHeader": "Changes for new version {{ version}}:",
|
||||
"subscriptionExpired": "Your Cloudron subscription has expired. Please setup a subscription to update the app.",
|
||||
"subscriptionExpired": "Your Cloudron subscription has expired. Please set up a subscription to update the app.",
|
||||
"skipBackupCheckbox": "Skip backup",
|
||||
"setupSubscriptionAction": "Setup Subscription",
|
||||
"setupSubscriptionAction": "Set up Subscription",
|
||||
"updateAction": "Update"
|
||||
},
|
||||
"restoreDialog": {
|
||||
@@ -1352,6 +1409,9 @@
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"notResponding": "Not Responding"
|
||||
},
|
||||
"stopDialog": {
|
||||
"title": "Really stop app {{ app }}?"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
@@ -1388,7 +1448,7 @@
|
||||
},
|
||||
"setupAccount": {
|
||||
"welcomeTo": "Welcome to",
|
||||
"description": "Please setup your account",
|
||||
"description": "Please set up your account",
|
||||
"username": "Username",
|
||||
"errorUsernameTooShort": "The username is too short",
|
||||
"errorUsernameTooLong": "The username is too long",
|
||||
@@ -1398,7 +1458,7 @@
|
||||
"passwordRepeat": "Repeat Password",
|
||||
"errorPassword": "Password must be at least 8 characters",
|
||||
"errorPasswordNoMatch": "Passwords don't match",
|
||||
"setupAction": "Setup",
|
||||
"setupAction": "Set up",
|
||||
"invalidToken": {
|
||||
"title": "Invalid or Expired Invite Link",
|
||||
"description": "Contact your server admin to get a new invite link."
|
||||
@@ -1440,22 +1500,39 @@
|
||||
"volumes": {
|
||||
"title": "Volumes",
|
||||
"addVolumeAction": "Add Volume",
|
||||
"hostPath": "Host Path",
|
||||
"hostPath": "Mount Point",
|
||||
"name": "Name",
|
||||
"openFileManagerActionTooltip": "Open FileManager",
|
||||
"removeVolumeActionTooltip": "Remove Volume",
|
||||
"addVolumeDialog": {
|
||||
"title": "Add Volume",
|
||||
"nameWarning": "Cloudron will mount the host path into the app's container with this name under <code>/media</code>.",
|
||||
"addAction": "Add"
|
||||
"addAction": "Add",
|
||||
"server": "Server IP or Hostname",
|
||||
"remoteDirectory": "Remote Directory",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"diskPath": "Disk Path",
|
||||
"noopWarning": "Cloudron will not configure the server to mount this volume",
|
||||
"mountTypeInfo": "Cloudron will configure the server to automatically mount this volume",
|
||||
"port": "Port",
|
||||
"user": "User",
|
||||
"privateKey": "Private SSH Key"
|
||||
},
|
||||
"removeVolumeDialog": {
|
||||
"title": "Really remove {{ volume }} ?",
|
||||
"description": "This will delete the volume <code>{{ volume }}</code>. Data inside the host path will not be removed.",
|
||||
"removeAction": "Remove"
|
||||
},
|
||||
"description": "Volumes are directories on the server that can be shared between apps. These may be NFS/SSHFS mounts or external storage disks attached to the server.",
|
||||
"backupWarning": "Volumes are <i>not</i> backed up. Restoring an app will not restore the volume's content. Please make sure to have a suitable backup plan for each volume."
|
||||
"description": "Volumes are directories on the server that can be shared between apps. These may be NFS/SSHFS/CIFS mounts or external storage disks attached to the server. Volumes are attached to the app's container under <code>/media</code>.",
|
||||
"backupWarning": "Volumes are <i>not</i> backed up. Restoring an app will not restore the volume's content. Please make sure to have a suitable backup plan for each volume.",
|
||||
"mountType": "Mount Type",
|
||||
"updateVolumeDialog": {
|
||||
"title": "Update Volume {{ volume }}"
|
||||
},
|
||||
"tooltipEdit": "Edit Volume",
|
||||
"mountStatus": "Mount Status",
|
||||
"type": "Type"
|
||||
},
|
||||
"storage": {
|
||||
"mounts": {
|
||||
|
||||
+610
-41
@@ -50,7 +50,8 @@
|
||||
"analytics": "Analítica",
|
||||
"newApps": "Nuevas Aplicaciones",
|
||||
"popular": "Popular",
|
||||
"all": "Todas"
|
||||
"all": "Todas",
|
||||
"federated": "Federada"
|
||||
},
|
||||
"title": "Tienda de Aplicaciones",
|
||||
"categoryLabel": "Categoría",
|
||||
@@ -71,7 +72,7 @@
|
||||
"titleSignUp": "Regístrate en Cloudron.io"
|
||||
},
|
||||
"appNotFoundDialog": {
|
||||
"description": "No hay aplicación <b>{{ appId }}</b> con versión <b>{{ version }}</b>.",
|
||||
"description": "No hay aplicación <b>{{ appId }}</b> con versión <b>{{ version }}</b>.",
|
||||
"title": "Aplicación no encontrada"
|
||||
}
|
||||
},
|
||||
@@ -110,7 +111,23 @@
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"logout": "Salir",
|
||||
"offline": "Cloudron está desconectado. Reconectando…"
|
||||
"offline": "Cloudron está desconectado. Reconectando…",
|
||||
"searchPlaceholder": "Buscar",
|
||||
"prettyDate": {
|
||||
"justNow": "Ahora mismo",
|
||||
"yeserday": "Ayer",
|
||||
"yearsAgo": "hace {{ y }} años",
|
||||
"minutesAgo": "Hace {{ m }} minutos",
|
||||
"hoursAgo": "Hace {{ h }} horas",
|
||||
"daysAgo": "Hace {{ d }} días",
|
||||
"weeksAgo": "Hace {{ w }} semanas",
|
||||
"monthsAgo": "Hace {{ m }} meses"
|
||||
},
|
||||
"multiselect": {
|
||||
"selected": "{{ n }} seleccionado",
|
||||
"select": "Seleccionar",
|
||||
"filterPlaceholder": "Escribe para filtrar opciones"
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
"domainsFilterHeader": "Todos los Dominios",
|
||||
@@ -130,7 +147,8 @@
|
||||
"description": "¿Qué te parece si instalas algunas? Echa un vistazo a la <a href=\"{{ appStoreLink }}\"> Tienda de Aplicaciones</a>",
|
||||
"title": "¡No hay aplicaciones instaladas todavía!"
|
||||
},
|
||||
"title": "Mis aplicaciones"
|
||||
"title": "Mis aplicaciones",
|
||||
"groupsFilterHeader": "Selecciona Grupo"
|
||||
},
|
||||
"users": {
|
||||
"addUserDialog": {
|
||||
@@ -147,7 +165,7 @@
|
||||
"bindUsername": "Enlazar DN/Nombre de usuario (opcional)",
|
||||
"bindPassword": "Enlazar Contraseña (opcional)",
|
||||
"groupBaseDn": "Grupo Base DN",
|
||||
"baseDn": "Base DN",
|
||||
"baseDn": "DN Base",
|
||||
"configureAction": "Configurar",
|
||||
"syncAction": "Sincronizar",
|
||||
"showLogsAction": "Mostrar Registros",
|
||||
@@ -165,7 +183,9 @@
|
||||
"subscriptionRequired": "Esta característica solo está habilitada en planes de pago.",
|
||||
"description": "Cloudron sincronizará usuarios y grupos desde un servidor LDAP o ActiveDirectory externo. La verificación de la contraseña para autentificar a esos usuarios se realiza en el servidor externo. La sincronización no se ejecuta automáticamente, sino que debe activarse manualmente.",
|
||||
"title": "LDAP",
|
||||
"auth": "Auth"
|
||||
"auth": "Auth",
|
||||
"providerOther": "Otra",
|
||||
"providerDisabled": "Deshabilitada"
|
||||
},
|
||||
"settings": {
|
||||
"saveAction": "Guardar",
|
||||
@@ -186,7 +206,7 @@
|
||||
"transferOwnershipTooltip": "Transferir Propiedad",
|
||||
"removeUserTooltip": "Borrar Usuario",
|
||||
"editUserTooltip": "Editar Usuario",
|
||||
"resetPasswordTooltip": "Restablece la contraseña o enlace de invitación",
|
||||
"resetPasswordTooltip": "Restablece la contraseña, deshabilita 2FA o envía enlace de invitación",
|
||||
"notActivatedYetTooltip": "Usuario todavía no activado",
|
||||
"externalLdapTooltip": "Desde un directorio LDAP externo",
|
||||
"inactiveTooltip": "Usuario está inactivo",
|
||||
@@ -205,7 +225,6 @@
|
||||
"newOwner": "Nuevo Propietario",
|
||||
"transferAction": "Transferir Propiedad"
|
||||
},
|
||||
"searchPlaceholder": "Buscar",
|
||||
"role": {
|
||||
"owner": "Super-administrador",
|
||||
"admin": "Administrador",
|
||||
@@ -218,7 +237,13 @@
|
||||
"passwordResetDialog": {
|
||||
"sendEmailLinkAction": "Enviar enlace al usuario",
|
||||
"description": "Usa el enlace de abajo para restablecer la contraseña o re-invitar a {{ username }}:",
|
||||
"title": "Restablecer la contraseña o enviar enlace de invitación a {{ username }}"
|
||||
"title": "Restablecer contraseña/2FA para {{ username }}",
|
||||
"emailSent": "Enviados",
|
||||
"newLinkAction": "Generar nuevo enlace",
|
||||
"resetLinkExplanation": "Usa esto para generar un enlace de invitación o restablecimiento de contraseña. El nuevo enlace invalidará cualquier enlace antiguo inmediatamente.",
|
||||
"2FAIsSetup": "Usa esto para deshabilitar 2FA del usuario. El usuario puede configurarlo nuevamente desde la vista Perfil.",
|
||||
"no2FASetup": "Este usuario no ha configurado 2FA.",
|
||||
"reset2FAAction": "Restablecer 2FA"
|
||||
},
|
||||
"deleteGroupDialog": {
|
||||
"deleteAction": "Borrar",
|
||||
@@ -303,7 +328,7 @@
|
||||
"title": "Ubicación",
|
||||
"description": "Cloudron realiza una copia de seguridad completa de su sistema en la ubicación configurada."
|
||||
},
|
||||
"title": "Copias de Seguridad",
|
||||
"title": "Backups",
|
||||
"configureBackupStorage": {
|
||||
"encryptionPasswordRepeat": "Repetir Contraseña",
|
||||
"encryptionPasswordPlaceholder": "Frase de contraseña utilizada para cifrar las copias de seguridad",
|
||||
@@ -335,7 +360,7 @@
|
||||
"hardlinksLabel": "Usar enlaces duros",
|
||||
"ext4Label": "El directorio es un disco EXT4 externo",
|
||||
"localDirectory": "Directorio local para copias de seguridad",
|
||||
"mountPointDescription": "El punto de montaje debe configurarse manualmente. Consulte esta <a href=\"{{ providerDocsLink }}\" target=\"_blank\"> documentación </a>.",
|
||||
"mountPointDescription": "El punto de montaje debe configurarse manualmente. Consulta esta <a href=\"{{ providerDocsLink }}\" target=\"_blank\"> documentación </a>.",
|
||||
"mountPoint": "Punto de montaje",
|
||||
"noopNote": "Esta opción rompe la funcionalidad de copia de seguridad y restauración de Cloudron y solo debe usarse para realizar pruebas. Asegúrese de que se haya realizado una copia de seguridad completa del servidor utilizando medios alternativos.",
|
||||
"provider": "Proveedor de almacenamiento",
|
||||
@@ -364,6 +389,10 @@
|
||||
"date": "Fecha",
|
||||
"id": "ID",
|
||||
"title": "Detalles de la Copia de Seguridad"
|
||||
},
|
||||
"check": {
|
||||
"sameDisk": "Las copias de seguridad de Cloudron se encuentran actualmente en el mismo disco que la instancia del servidor Cloudron. Esto es peligroso y puede provocar la pérdida total de datos si falla el disco. Consulta https://docs.cloudron.io/backups/#storage-providers para almacenar copias de seguridad en una ubicación externa.",
|
||||
"noop": "Las copias de seguridad de Cloudron están deshabilitadas. Asegúrate de que se haya realizado una copia de seguridad de este servidor utilizando medios alternativos. Consulta https://docs.cloudron.io/backups/#storage-providers para más información."
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@@ -414,7 +443,9 @@
|
||||
"expiresAt": "Expira el",
|
||||
"name": "Nombre",
|
||||
"title": "Tokens API",
|
||||
"newApiToken": "Nuevo Token API"
|
||||
"newApiToken": "Nuevo Token API",
|
||||
"neverUsed": "nunca",
|
||||
"lastUsed": "Última utilizada"
|
||||
},
|
||||
"appPasswords": {
|
||||
"deletePasswordTooltip": "Borrar Contraseña",
|
||||
@@ -608,7 +639,16 @@
|
||||
},
|
||||
"settings": {
|
||||
"appstoreAccount": {
|
||||
"title": "Cuenta Cloudron.io"
|
||||
"title": "Cuenta Cloudron.io",
|
||||
"email": "Email de la Cuenta",
|
||||
"subscriptionEndsAt": "Cancelado y finaliza el",
|
||||
"subscriptionSetupAction": "Configurar Suscripción",
|
||||
"subscriptionReactivateAction": "Reactivar Suscripción",
|
||||
"setupAction": "Configurar Cuenta",
|
||||
"subscription": "Suscripción",
|
||||
"cloudronId": "ID de Cloudron",
|
||||
"subscriptionChangeAction": "Cambiar Suscripción",
|
||||
"description": "Se utiliza una cuenta de Cloudron.io para acceder a la App Store y administrar su suscripción."
|
||||
},
|
||||
"title": "Ajustes",
|
||||
"updateScheduleDialog": {
|
||||
@@ -636,10 +676,38 @@
|
||||
"title": "Idioma"
|
||||
},
|
||||
"timezone": {
|
||||
"description": "La configuración de zona horaria actual es <b> {{timeZone}} </b>.\nEsta configuración se utiliza para programar tareas de respaldo y actualización."
|
||||
"description": "La configuración de zona horaria actual es <b> {{timeZone}} </b>.\nEsta configuración se utiliza para programar tareas de respaldo y actualización.",
|
||||
"title": "Zona horaria"
|
||||
},
|
||||
"privateDockerRegistry": {
|
||||
"subscriptionRequired": "Esta funcionalidad solo está disponible en planes de pago."
|
||||
"subscriptionRequired": "Esta funcionalidad solo está disponible en planes de pago.",
|
||||
"server": "Dirección del Servidor",
|
||||
"username": "Nombre de Usuario",
|
||||
"configureAction": "Configurar Registro",
|
||||
"setupSubscriptionAction": "Configura tu Suscripción Ahora",
|
||||
"usernameNotSet": "No configurado",
|
||||
"title": "Registro Privado de Docker",
|
||||
"description": "Cloudron puede extraer e instalar <a href=\"{{ customAppsLink }}\" target=\"_blank\"> aplicaciones personalizadas </a> desde un registro de Docker privado.",
|
||||
"serverNotSet": "Sin configurar"
|
||||
},
|
||||
"privateDockerRegistryDialog": {
|
||||
"title": "Configuración del registro privado",
|
||||
"email": "Email (Opcional)",
|
||||
"passwordToken": "Contraseña / Token"
|
||||
},
|
||||
"registryConfig": {
|
||||
"provider": "Proveedor de registro de Docker",
|
||||
"providerOther": "Otra",
|
||||
"providerDisabled": "Deshabilitada"
|
||||
},
|
||||
"updateDialog": {
|
||||
"changes": "Cambios",
|
||||
"updateAction": "Actualizar",
|
||||
"title": "Actualizar Cloudron a",
|
||||
"skipBackupCheckbox": "Omitir Copia de Seguridad",
|
||||
"blockingAppsInfo": "Por favor, espere a que terminen las operaciones anteriores.",
|
||||
"blockingApps": "Las aplicaciones siguientes están bloqueando la actualización porque tienen acciones pendientes:",
|
||||
"unstableWarning": "Esta actualización es una versión previa y no se considera estable aún. Por favor, actualiza bajo tu responsabilidad."
|
||||
}
|
||||
},
|
||||
"domains": {
|
||||
@@ -662,7 +730,35 @@
|
||||
"provider": "Proveedor DNS",
|
||||
"domain": "Dominio",
|
||||
"editTitle": "Configurar {{ domain }}",
|
||||
"addTitle": "Añadir Dominio"
|
||||
"addTitle": "Añadir Dominio",
|
||||
"gandiApiKey": "Clave API de Gandi",
|
||||
"cloudflareTokenTypeGlobalApiKey": "Clave API Global",
|
||||
"cloudflareTokenTypeApiToken": "Token API",
|
||||
"cloudflareEmail": "Email de Cloudflare",
|
||||
"linodeToken": "Token de Linode",
|
||||
"nameComUsername": "Usuario de Name.com",
|
||||
"nameComApiToken": "Token API",
|
||||
"namecheapApiKey": "Clave API",
|
||||
"manualInfo": "Todos los registros DNS deben configurarse manualmente antes de la instalación de cada aplicación.",
|
||||
"letsEncryptInfo": "Let's Encrypt requiere que tu servidor sea accesible en el puerto 80",
|
||||
"advancedAction": "Configuración Avanzada…",
|
||||
"zoneName": "Nombre de Zona (Opcional)",
|
||||
"certProvider": "Proveedor del certificado",
|
||||
"fallbackCert": "Certificado alternativo (opcional)",
|
||||
"fallbackCertCustomCert": "Certificado personalizado",
|
||||
"fallbackCertKeyPlaceholder": "Clave",
|
||||
"fallbackCertCertificatePlaceholder": "Certificado",
|
||||
"mastodonHostname": "Ubicación del servidor Mastodon",
|
||||
"netcupCustomerNumber": "Número de cliente",
|
||||
"netcupApiKey": "Clave API",
|
||||
"netcupApiPassword": "Contraseña API",
|
||||
"addDescription": "Agregar un dominio le permite instalar aplicaciones en subdominios de este dominio. La configuración de correo electrónico para el dominio se puede configurar en la vista de correo electrónico.",
|
||||
"namecheapUsername": "Usuario de Namecheap",
|
||||
"namecheapInfo": "La IP del servidor debe estar incluida en la lista de permisos para esta clave de API.",
|
||||
"wildcardInfo": "Configura <i>los registros A</i> para <b>*.{{ domain }}</b> y <b>{{ domain }}</b> con la IP de este servidor.",
|
||||
"matrixHostname": "Ubicación del servidor Matrix",
|
||||
"fallbackCertInfo": "Los certificados se obtienen y renuevan automáticamente desde <a href=\"https://letsencrypt.org/\" target=\"_blank\"> Let's Encrypt </a>. Consulta el límite de frecuencia actual <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\"> aquí </a>.\nEste certificado se utilizará en caso de que falle el certificado de Let's Encrypt. Si no se proporciona, se utilizará como respaldo un certificado autofirmado generado automáticamente.",
|
||||
"fallbackCertCustomCertInfo": "Este <a href=\"{{ customCertLink }}\" target=\"_blank\"> certificado wildcard </a> se utilizará para todas las aplicaciones de este dominio. Si no se proporciona, se generará automáticamente un certificado autofirmado."
|
||||
},
|
||||
"subscriptionRequired": {
|
||||
"setupAction": "Configura tu suscripción",
|
||||
@@ -681,12 +777,28 @@
|
||||
"domain": "Dominio",
|
||||
"addDomain": "Añadir Dominio",
|
||||
"syncDns": {
|
||||
"showLogsAction": "Mostrar Registros"
|
||||
"showLogsAction": "Mostrar Registros",
|
||||
"title": "Sincronizar DNS",
|
||||
"description": "Esto reaprovisionará los registros DNS de la aplicación y del correo electrónico en todos los dominios.",
|
||||
"syncAction": "Sincronizar DNS"
|
||||
},
|
||||
"removeDialog": {
|
||||
"title": "Realmente quieres borrar {{ domain }}?",
|
||||
"removeAction": "Borrar",
|
||||
"description": "Esto borrará el dominio <code>{{ domain }}</code>."
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"appInfo": {
|
||||
"customAppUpdateWarning": "Esta es una aplicación personalizada y no se instala desde la App Store y no recibirá actualizaciones. Consulte la <a target=\"_blank\" href=\"{{ docsLink }}\"> documentación </a> sobre cómo actualizar una aplicación personalizada."
|
||||
"customAppUpdateWarning": "Esta es una aplicación personalizada y no se instala desde la App Store y no recibirá actualizaciones. Consulte la <a target=\"_blank\" href=\"{{ docsLink }}\"> documentación </a> sobre cómo actualizar una aplicación personalizada.",
|
||||
"ssoEmail": "Esta aplicación está configurada para permitir a todos los usuarios con un buzón de correo en este Cloudron. Inicia sesión con el correo electrónico y la contraseña de Cloudron para acceder al buzón.",
|
||||
"sso": "Esta aplicación está configurada para autentificarse con el directorio de usuarios de Cloudron. Los usuarios de Cloudron pueden iniciar sesión y usarlo de inmediato.",
|
||||
"postInstallConfirmCheckbox": "Reconocer las instrucciones",
|
||||
"openAction": "Abrir {{ app }}",
|
||||
"firstTimeTitle": "Uso de primera vez",
|
||||
"firstTimeCollapseHeader": "Instrucciones de ajustes de primera vez",
|
||||
"appDocsUrl": "Consulta la <a target=\"_blank\" href=\"{{ docsUrl }}\"> {{title}} documentación </a> para obtener información útil y temas comunes sobre esta aplicación. Si necesita más ayuda, consulta la <a target=\"_blank\" href=\"{{ forumUrl }}\"> {{title}} sección del foro </a> de Cloudron.",
|
||||
"package": "Paquete"
|
||||
},
|
||||
"updates": {
|
||||
"auto": {
|
||||
@@ -714,10 +826,15 @@
|
||||
"addMountAction": "Añade un volumen a montar",
|
||||
"noMounts": "No se ha montado ningún volumen.",
|
||||
"volume": "Volumen",
|
||||
"saveAction": "Guardar"
|
||||
"saveAction": "Guardar",
|
||||
"title": "Montajes",
|
||||
"readOnly": "Solo lectura"
|
||||
},
|
||||
"appdata": {
|
||||
"title": "Datos de la Aplicación"
|
||||
"title": "Datos de la Aplicación",
|
||||
"dataDirPlaceholder": "Dejar vacío para usar la plataforma predeterminada",
|
||||
"description": "De forma predeterminada, los datos de esta aplicación se encuentran en <code> {{storagePath}} </code>. Si el servidor se está quedando sin espacio en disco, puede montar un disco EXT4 externo y mover allí los datos de esta aplicación.",
|
||||
"moveAction": "Mover datos"
|
||||
}
|
||||
},
|
||||
"logsActionTooltip": "Registros",
|
||||
@@ -726,9 +843,9 @@
|
||||
"24h": "24 horas",
|
||||
"12h": "12 horas",
|
||||
"30d": "30 días",
|
||||
"7d": "7 días"
|
||||
"7d": "7 días",
|
||||
"6h": "6 horas"
|
||||
},
|
||||
"selectPeriod": "Seleccionar Periodo {{ period }}",
|
||||
"memoryTitle": "Memoria (RAM + Swap) en Mb"
|
||||
},
|
||||
"states": {
|
||||
@@ -746,25 +863,48 @@
|
||||
"downloadConfigTooltip": "Descarga Configuración de la Copia de Seguridad",
|
||||
"time": "Creado en",
|
||||
"packageVersion": "Versión del Paquete",
|
||||
"title": "Copias de Seguridad"
|
||||
"title": "Backups",
|
||||
"description": "Las copias de seguridad son instantáneas completas de la aplicación. Puede utilizar copias de seguridad de la aplicación para restaurar o clonar esta aplicación."
|
||||
},
|
||||
"import": {
|
||||
"title": "Importar desde una Copia de Seguridad Externa"
|
||||
"title": "Importar desde una Copia de Seguridad Externa",
|
||||
"description": "Usar para migrar una aplicación desde otro Cloudron. La otra aplicación debe tener la misma versión de paquete y configuración de control de acceso que esta."
|
||||
},
|
||||
"auto": {
|
||||
"title": "Backups automáticos",
|
||||
"enabled": "Los Backups automáticos están actualmente habilitados.",
|
||||
"disabled": "Los Backups automáticos están actualmente deshabilitados.",
|
||||
"disableAction": "Deshabilitar Backups automáticos",
|
||||
"enableAction": "Habilitar Backups automáticos",
|
||||
"description": "Cloudron crea periódicamente una copia de seguridad basada en la configuración de <a href=\"{{ backupLink }}\"> copia de seguridad </a>."
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"robots": {
|
||||
"disableIndexingAction": "Desactivar indexado",
|
||||
"title": "Robots.txt"
|
||||
"title": "Robots.txt",
|
||||
"txtPlaceholder": "Dejar en blanco para permitir que todos los bots indexen esta aplicación"
|
||||
},
|
||||
"csp": {
|
||||
"saveAction": "Guardar"
|
||||
"saveAction": "Guardar",
|
||||
"description": "La configuración de esta opción anulará cualquier encabezado CSP enviado por la propia aplicación",
|
||||
"title": "Política de seguridad de contenido"
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"from": {
|
||||
"saveAction": "Guardar",
|
||||
"title": "Correo DESDE la dirección"
|
||||
"title": "Correo DESDE la dirección",
|
||||
"disableDescription": "La configuración de entrega de correo de la aplicación es independiente. Puedes configurarla dentro de la aplicación.",
|
||||
"description2": "Cuando está habilitada, la aplicación está configurada para enviar correos electrónicos a través del servidor de correo interno usando esta dirección. El servidor de correo interno utilizará la configuración de {{domain}} <a href=\"{{ domainConfigLink }}\"> correo electrónico saliente </a> para enviar correo. Cuando está deshabilitado, puede configurar los ajustes de correo electrónico dentro de la aplicación.",
|
||||
"mailboxPlaceholder": "Dejar vacío para usar la plataforma predeterminada",
|
||||
"disable": "No configurar los ajustes de correo",
|
||||
"enableDescription": "La aplicación está configurada para enviar correos electrónicos utilizando la dirección que aparece a continuación y la configuración de <a href=\\\"{{ domainConfigLink }}\\\"> correo electrónico saliente </a> de {{domain}}.",
|
||||
"description": "Esto establece la dirección desde la que esta aplicación envía el correo electrónico. Esta aplicación ya está configurada para enviar correo mediante la configuración de {{domain}} <a href=\\\"{{ domainConfigLink }}\\\"> correo electrónico saliente </a>.",
|
||||
"enable": "Utilizar Cloudron Mail para enviar correos electrónicos"
|
||||
},
|
||||
"csp": {
|
||||
"title": "Política de seguridad de contenido"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
@@ -821,14 +961,101 @@
|
||||
},
|
||||
"uninstallTabTitle": "Desinstalar",
|
||||
"repairTabTitle": "Raparar",
|
||||
"backupsTabTitle": "Copias de Seguridad",
|
||||
"backupsTabTitle": "Backups",
|
||||
"emailTabTitle": "Correo",
|
||||
"securityTabTitle": "Seguridad",
|
||||
"graphsTabTitle": "Gráficos",
|
||||
"storageTabTitle": "Almacenamiento",
|
||||
"resourcesTabTitle": "Recursos",
|
||||
"accessControlTabTitle": "Control de Accesos",
|
||||
"locationTabTitle": "Ubicación"
|
||||
"accessControlTabTitle": "Accesos",
|
||||
"locationTabTitle": "Ubicación",
|
||||
"backAction": "Volver a Mis Aplicaciones",
|
||||
"adminPageAction": "Página de administrador",
|
||||
"uninstallDialog": {
|
||||
"description": "Esto desinstalará inmediatamente <b>{{ app }}</b> y borrará todos sus datos.",
|
||||
"title": "Desinstalar {{ app }}",
|
||||
"uninstallAction": "Desinstalar"
|
||||
},
|
||||
"domainCollisionDialog": {
|
||||
"description": "Como medida de precaución, Cloudron no sobrescribe los registros DNS existentes. Confirme que los dominios anteriores no están en uso para servicios externos a Cloudron.",
|
||||
"title": "Colisión de dominio",
|
||||
"collisionListTitle": "Los siguientes dominios ya existen en su DNS:",
|
||||
"overwriteAction": "Sobrescribir registros DNS existentes"
|
||||
},
|
||||
"importBackupDialog": {
|
||||
"description": "Todos los datos generados entre ahora y la última copia de seguridad conocida se perderán de forma irrevocable. Se recomienda crear una copia de seguridad de los datos actuales antes de intentar una importación.",
|
||||
"title": "Importar Backup",
|
||||
"uploadAction": "Subir Configuración de Backup",
|
||||
"importAction": "Importar"
|
||||
},
|
||||
"restoreDialog": {
|
||||
"warning": "Todos los datos generados entre ahora y la última copia de seguridad conocida se perderán de forma irrevocable. Se recomienda crear una copia de seguridad de los datos actuales antes de intentar una restauración.",
|
||||
"title": "Restaurar {{ app }}",
|
||||
"description": "Esto restaurará esta aplicación con los datos de {{creationTime}}.",
|
||||
"restoreAction": "Restaurar"
|
||||
},
|
||||
"uninstall": {
|
||||
"uninstall": {
|
||||
"backupWarning": "Las copias de seguridad de las aplicaciones no se eliminan y se borrarán según la política de copias de seguridad. Puede restaurar esta aplicación a partir de una copia de seguridad de la aplicación existente mediante las siguientes <a target=\"_blank\" href=\"{{ importBackupDocsLink }}\"> instrucciones </a>.",
|
||||
"uninstallAction": "Desinstalar",
|
||||
"title": "Desinstalar",
|
||||
"description": "Esto desinstalará la aplicación inmediatamente y eliminará todos sus datos. El sitio será inaccesible."
|
||||
},
|
||||
"startStop": {
|
||||
"title": "Arrancar / Parar",
|
||||
"startAction": "Arrancar Aplicación",
|
||||
"stopAction": "Parar Aplicación",
|
||||
"description": "Las aplicaciones se pueden detener para conservar los recursos del servidor en lugar de desinstalarlas. Las futuras copias de seguridad de la aplicación no incluirán ningún cambio en la aplicación entre ahora y la copia de seguridad de la aplicación más reciente. Por este motivo, se recomienda activar una copia de seguridad antes de detener la aplicación."
|
||||
}
|
||||
},
|
||||
"cloneDialog": {
|
||||
"description": "Usando la copia de seguridad de <b> {{creationTime}} </b> y la versión <b> v {{packageVersion}} </b>",
|
||||
"title": "Clonar {{ app }}",
|
||||
"location": "Ubicación",
|
||||
"cloneAction": "Clonar"
|
||||
},
|
||||
"updateDialog": {
|
||||
"unstableWarning": "Esta actualización es una versión preliminar y aún no se considera estable. Actualiza bajo tu propio riesgo.",
|
||||
"subscriptionExpired": "Tu suscripción a Cloudron ha expirado. Configure una suscripción para actualizar la aplicación.",
|
||||
"title": "Actualizar {{ app }}",
|
||||
"changelogHeader": "Cambios para la nueva versión {{ version}}:",
|
||||
"skipBackupCheckbox": "Saltar backup",
|
||||
"setupSubscriptionAction": "Configura suscripción",
|
||||
"updateAction": "Actualizar"
|
||||
},
|
||||
"terminalActionTooltip": "Terminal",
|
||||
"filemanagerActionTooltip": "Gestor de Archivos",
|
||||
"docsActionTooltip": "Documentación",
|
||||
"firstTimeSetupAction": "Configuración de primera vez",
|
||||
"docsAction": "Documentación",
|
||||
"projectWebsiteAction": "Sitio Web del proyecto",
|
||||
"repairDialog": {
|
||||
"title": "Raparar {{ app }}",
|
||||
"description": "Cloudron reinstalará la aplicación en el mismo sitio con la configuración existente. Se conservarán los datos existentes.",
|
||||
"domainDescription": "Cloudron reparará la aplicación para usarla en los dominios siguientes:",
|
||||
"location": "Ubicación",
|
||||
"fromBackup": "Restaurar desde Backup:",
|
||||
"retryAction": "Reintentar {{ task }}",
|
||||
"taskError": "La operación <b> {{task}} </b> falló con el siguiente error:"
|
||||
},
|
||||
"stopDialog": {
|
||||
"title": "¿De verdad quieres detener la aplicación {{app}}?"
|
||||
},
|
||||
"repair": {
|
||||
"recovery": {
|
||||
"title": "Recuperación en caso de accidente",
|
||||
"restartAction": "Reiniciar Aplicación",
|
||||
"enableRecoveryModeAction": "Habilitar Modo de Recuperación",
|
||||
"disableRecoveryModeAction": "Deshabilitar Modo de Recuperación",
|
||||
"description": "Si la aplicación no responde, intenta reiniciarla. Si la aplicación se reinicia constantemente debido a un complemento roto o una configuración incorrecta, coloca la aplicación en modo de recuperación para acceder a la consola.\nUtiliza las siguientes <a href=\"{{ docsLink }}\" target=\"_blank\"> instrucciones </a> para volver a ejecutar la aplicación."
|
||||
},
|
||||
"taskError": {
|
||||
"title": "Error de tarea",
|
||||
"description": "Si una acción de configuración, actualización, restauración o copia de seguridad resultó en un error, se puede volver a intentar la tarea.",
|
||||
"retryAction": "Reintentar {{ task }} tarea"
|
||||
},
|
||||
"appIsBusyTooltip": "La aplicación está ocupada"
|
||||
}
|
||||
},
|
||||
"lang": {
|
||||
"zh_Hans": "Chino (simple)",
|
||||
@@ -866,7 +1093,9 @@
|
||||
"enableAction": "Habilitar acceso a soporte por SSH",
|
||||
"disableAction": "Deshabilitar acceso a soporte por SSH",
|
||||
"subscriptionRequired": "El Soporte Remoto solo está disponible en planes de pago.",
|
||||
"title": "Soporte Remoto"
|
||||
"title": "Soporte Remoto",
|
||||
"description": "Habilite esta opción para permitir que los ingenieros de soporte se conecten a este servidor a través de SSH.",
|
||||
"warning": "No habilites esta opción a menos que te lo solicite el equipo de soporte de Cloudron."
|
||||
},
|
||||
"ticket": {
|
||||
"reportPlaceholder": "Describe tu problema",
|
||||
@@ -880,18 +1109,24 @@
|
||||
"typeApp": "Error en Aplicación",
|
||||
"type": "Tipo",
|
||||
"subscriptionRequired": "Los tickets de soporte solo están disponibles en planes de pago.",
|
||||
"title": "Ticket"
|
||||
"title": "Tiquet",
|
||||
"subscriptionRequiredDescription": "Puedes encontrar respuestas en nuestra <a href=\"{{ supportViewLink }}\" target=\"_blank\">documentación</a> o pregunta en el <a href=\"{{ forumLink }}\" target=\"_blank\">Foro</a>.",
|
||||
"emailInfo": "(El email de suscripción es {{ email }})",
|
||||
"sshCheckbox": "Permitir que los ingenieros de soporte se conecten a este servidor a través de SSH",
|
||||
"emailPlaceholder": "Si es necesario, proporciona una dirección de correo electrónico diferente de la anterior para contactarte"
|
||||
},
|
||||
"title": "Soporte"
|
||||
},
|
||||
"volumes": {
|
||||
"removeVolumeDialog": {
|
||||
"removeAction": "Borrar",
|
||||
"title": "Realmente borramos {{ volume }} ?"
|
||||
"title": "Realmente borramos {{ volume }} ?",
|
||||
"description": "Esto eliminará el volumen <code> {{volume}} </code>. Los datos dentro de la ruta del host no se eliminarán."
|
||||
},
|
||||
"addVolumeDialog": {
|
||||
"addAction": "Añadir",
|
||||
"title": "Añadir Volumen"
|
||||
"title": "Añadir Volumen",
|
||||
"nameWarning": "Cloudron montará la ruta del host en el contenedor de la aplicación con este nombre en <code> /media </code>."
|
||||
},
|
||||
"removeVolumeActionTooltip": "Borrar Volumen",
|
||||
"openFileManagerActionTooltip": "Abrir Gestor de Archivos",
|
||||
@@ -899,7 +1134,8 @@
|
||||
"hostPath": "Directorio del servidor",
|
||||
"addVolumeAction": "Añade un Volumen",
|
||||
"title": "Volúmenes",
|
||||
"description": "Los volúmenes son directorios en el servidor que se pueden compartir entre aplicaciones. Estos pueden ser montajes NFS / SSHFS o discos de almacenamiento externos conectados al servidor."
|
||||
"description": "Los volúmenes son directorios en el servidor que se pueden compartir entre aplicaciones. Estos pueden ser montajes NFS / SSHFS o discos de almacenamiento externos conectados al servidor.",
|
||||
"backupWarning": "Los volúmenes <i> no </i> están respaldados. Restaurar una aplicación no restaurará el contenido del volumen. Asegúrate de tener un plan de copias de seguridad adecuado para cada volumen."
|
||||
},
|
||||
"eventlog": {
|
||||
"filterAllEvents": "Todos los Eventos",
|
||||
@@ -911,7 +1147,88 @@
|
||||
},
|
||||
"filemanager": {
|
||||
"toolbar": {
|
||||
"openLogs": "Abrir Registros"
|
||||
"openLogs": "Abrir Registros",
|
||||
"new": "Nuevo",
|
||||
"upload": "Cargar",
|
||||
"newFile": "Nuevo archivo",
|
||||
"newFolder": "Nueva carpeta",
|
||||
"uploadFolder": "Subir carpeta",
|
||||
"uploadFile": "Subir archivo",
|
||||
"openTerminal": "Abrir Terminal",
|
||||
"restartApp": "Reiniciar aplicación"
|
||||
},
|
||||
"title": "Gestor de Archivos",
|
||||
"newDirectoryDialog": {
|
||||
"title": "Nueva Carpeta",
|
||||
"create": "Crear"
|
||||
},
|
||||
"newFileDialog": {
|
||||
"title": "Nuevo Archivo",
|
||||
"create": "Crear"
|
||||
},
|
||||
"renameDialog": {
|
||||
"title": "Renombrar {{ fileName }}",
|
||||
"newName": "Nuevo Nombre",
|
||||
"rename": "Renombrar"
|
||||
},
|
||||
"chownDialog": {
|
||||
"newOwner": "Nuevo propietario",
|
||||
"change": "Cambiar propietario",
|
||||
"recursiveCheckbox": "Cambiar propiedad recursivamente",
|
||||
"title": "Cambiar propiedad"
|
||||
},
|
||||
"uploadingDialog": {
|
||||
"errorAlreadyExists": "Uno o más archivos ya existen.",
|
||||
"errorFailed": "Error al cargar uno o más archivos. Inténtalo de nuevo.",
|
||||
"title": "Subiendo archivos ({{ countDone }}/{{ count }})",
|
||||
"retry": "Reintentar",
|
||||
"overwrite": "Sobrescribir",
|
||||
"closeWarning": "No refresques la página hasta que la subida haya terminado."
|
||||
},
|
||||
"removeDialog": {
|
||||
"reallyDelete": "¿Realmente quieres eliminar lo siguiente?"
|
||||
},
|
||||
"extractDialog": {
|
||||
"title": "Extrayendo {{ fileName }}",
|
||||
"closeWarning": "No refresques la página hasta que la extracción haya finalizado."
|
||||
},
|
||||
"textEditorCloseDialog": {
|
||||
"title": "El archivo tiene cambios sin guardar",
|
||||
"details": "Tus cambios se perderán si no los guardas",
|
||||
"dontSave": "No guardar"
|
||||
},
|
||||
"notFound": "No encontrado",
|
||||
"list": {
|
||||
"name": "Nombre",
|
||||
"size": "Tamaño",
|
||||
"owner": "Propietario",
|
||||
"empty": "Sin archivos",
|
||||
"symlink": "enlace simbólico a {{target}}",
|
||||
"menu": {
|
||||
"rename": "Renombrar",
|
||||
"chown": "Cambiar propiedad",
|
||||
"extract": "Extraer aquí",
|
||||
"delete": "Borrar",
|
||||
"edit": "Editar",
|
||||
"cut": "Cortar",
|
||||
"copy": "Copiar",
|
||||
"paste": "Pegar",
|
||||
"selectAll": "Seleccionar todo",
|
||||
"download": "Descargar"
|
||||
},
|
||||
"mtime": "Modificado"
|
||||
},
|
||||
"newDirectory": {
|
||||
"errorAlreadyExists": "Ya existe"
|
||||
},
|
||||
"newFile": {
|
||||
"errorAlreadyExists": "Ya existe"
|
||||
},
|
||||
"status": {
|
||||
"restartingApp": "Reiniciando aplicación"
|
||||
},
|
||||
"extract": {
|
||||
"error": "La extracción falló: {{ message }}"
|
||||
}
|
||||
},
|
||||
"logs": {
|
||||
@@ -921,24 +1238,276 @@
|
||||
},
|
||||
"email": {
|
||||
"signature": {
|
||||
"subscriptionRequired": "Esta funcionalidad solo está disponible en planes de pago. <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Suscríbete Ahora</a>"
|
||||
"subscriptionRequired": "Esta funcionalidad solo está disponible en planes de pago. <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Configura tu Suscripción Ahora</a>",
|
||||
"plainTextFormat": "Formato del texto",
|
||||
"htmlFormat": "Formato HTML (Opcional)",
|
||||
"saveAction": "Guardar",
|
||||
"title": "Firma",
|
||||
"description": "El texto aquí se adjuntará a todos los correos electrónicos que se envíen desde este dominio."
|
||||
},
|
||||
"incoming": {
|
||||
"catchall": {
|
||||
"subscriptionRequired": "Esta funcionalidad solo está disponible en planes de pago. <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Suscríbete Ahora</a>"
|
||||
}
|
||||
"subscriptionRequired": "Esta funcionalidad solo está disponible en planes de pago. <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Configura tu Suscripción Ahora</a>",
|
||||
"description": "Los correos electrónicos enviados a direcciones no existentes se reenviarán a los siguientes buzones de correo.",
|
||||
"title": "Atrapa todo",
|
||||
"saveAction": "Guardar"
|
||||
},
|
||||
"description": "<a href=\"{{ emailDocsLink }}\" target=\"_blank\">El Servidor de Correo</a> de Cloudron permite a los usuarios recibir emails para este dominio. <a href=\"{{ rainloopLink }}\">Rainloop</a>, <a href=\"{{ sogoLink }}\">SOGo</a>, <a href=\"{{ roundcubeLink }}\">Roundcube</a> están pre-configurados para acceder al Correo de Cloudron.",
|
||||
"mailboxes": {
|
||||
"aliases": "Alias",
|
||||
"title": "Buzones de correo",
|
||||
"addAction": "Añadir",
|
||||
"disabledTooltip": "Los correos están deshabilitados para este dominio",
|
||||
"name": "Nombre",
|
||||
"owner": "Propietario",
|
||||
"usage": "Uso"
|
||||
},
|
||||
"mailinglists": {
|
||||
"description": "Una lista de correo reenvía todos los correos electrónicos a sus miembros.",
|
||||
"title": "Listas de correo",
|
||||
"name": "Nombre",
|
||||
"members": "Lista de miembros",
|
||||
"everyoneTooltip": "Publicación permitida por los no miembros",
|
||||
"membersOnlyTooltip": "Publicación restringida solo para miembros"
|
||||
},
|
||||
"outgointServerInfo": "Correo Saliente (SMTP)",
|
||||
"sieveServerInfo": "ManageSieve",
|
||||
"loginHelp": "Utiliza <i> nombre del buzón </i> @ {{domain}} y la contraseña del propietario del buzón para acceder a los buzones de este dominio",
|
||||
"title": "Correo electrónico entrante",
|
||||
"disableAction": "Deshabilitar",
|
||||
"enableAction": "Habilitar",
|
||||
"server": "Servidor",
|
||||
"port": "Puerto",
|
||||
"tabTitle": "Buzones de correo",
|
||||
"incomingServerInfo": "Correo entrante (IMAP)"
|
||||
},
|
||||
"outbound": {
|
||||
"noopAdminDomainWarning": "Cloudron no puede enviar invitaciones de usuario, restablecimiento de contraseña y otras notificaciones cuando el correo electrónico está deshabilitado en el dominio principal"
|
||||
"noopAdminDomainWarning": "Cloudron no puede enviar invitaciones de usuario, restablecimiento de contraseña y otras notificaciones cuando el correo electrónico está deshabilitado en el dominio principal",
|
||||
"noopNonAdminDomainWarning": "Cloudron no puede proporcionar el envío de correo electrónico para aplicaciones alojadas en este dominio cuando el correo electrónico está deshabilitado.",
|
||||
"mailRelay": {
|
||||
"spfDocInfo": "Cloudron no configura automáticamente el registro SPF. Configúralo manualmente siguiendo la <a href=\"{{ spfDocsLink }}\" target=\"_blank\"> {{name}} documentación </a>.",
|
||||
"host": "Host SMTP",
|
||||
"port": "Puerto SMTP (STARTTLS)",
|
||||
"selfsignedCheckbox": "Aceptar certificado autofirmado",
|
||||
"apiTokenOrKey": "Token/Key API",
|
||||
"username": "Nombre de usuario",
|
||||
"saveSuccess": "Guardado",
|
||||
"saveAction": "Guardar",
|
||||
"password": "Contraseña"
|
||||
},
|
||||
"tabTitle": "Saliente",
|
||||
"title": "Retransmisión de correo electrónico",
|
||||
"description": "Cloudron utilizará este servidor de correo (host inteligente) para enviar los correos salientes de las aplicaciones instaladas en este dominio."
|
||||
},
|
||||
"backAction": "Volver a Correo Electrónico",
|
||||
"config": {
|
||||
"title": "Configuración de Correo electrónico {{ domain }}",
|
||||
"connectionDetails": "Detalles de conexión para otros clientes de correo electrónico"
|
||||
},
|
||||
"updateMailboxDialog": {
|
||||
"activeCheckbox": "El buzón de correo está activo"
|
||||
},
|
||||
"dnsStatus": {
|
||||
"ptrInfo": "El registro PTR lo establece tu proveedor de VPS y no tu proveedor de DNS.",
|
||||
"description": "El estado de los registros DNS puede mostrar un error mientras se propaga el DNS (~ 5 minutos). Consulta los documentos de <a href=\"{{ emailDnsDocsLink }}\" target=\"_blank\"> solución de problemas </a> para obtener ayuda.",
|
||||
"title": "Estado DNS",
|
||||
"reSetupAction": "Recargar la configuración de DNS",
|
||||
"namecheapInfo": "Namecheap requiere pasos manuales para los registros MX",
|
||||
"hostname": "Nombre del host",
|
||||
"domain": "Dominio",
|
||||
"expected": "Valor esperado",
|
||||
"current": "Valor actual",
|
||||
"type": "Tipo de registro",
|
||||
"recordNotSet": "no establecido"
|
||||
},
|
||||
"smtpStatus": {
|
||||
"outboudRelay": "Saliente SMTP (Retransmitido)",
|
||||
"notBlacklisted": "La IP de este servidor {{ ip }} <b> no </b> está en una lista de bloqueo.",
|
||||
"title": "Estado SMTP",
|
||||
"outboudDirect": "Saliente SMTP (Directo)",
|
||||
"blacklistCheck": "Comprobación de la lista negra de direcciones IP",
|
||||
"blacklisted": "La IP de este servidor {{ip}} está en una lista de bloqueo."
|
||||
},
|
||||
"enableEmailDialog": {
|
||||
"noProviderInfo": "No se ha configurado ningún proveedor de DNS. Los registros DNS enumerados en la pestaña Estado deben configurarse manualmente.",
|
||||
"description": "Esto configurará Cloudron para recibir correos electrónicos de <b> {{dominio}} </b>. Consulta la documentación para abrir los <a href=\"{{ requiredPortsDocsLink }}\" target=\"_blank\"> puertos obligatorios </a> para Cloudron Email.",
|
||||
"setupDnsInfo": "Utiliza esta opción para configurar automáticamente los registros DNS relacionados con el correo electrónico. Dejar esta opción sin marcar es útil para crear buzones de correo e <a href=\"{{ importEmailDocsLink }}\"> importar correo electrónico </a> antes de publicarlo.",
|
||||
"title": "¿Habilitar el correo electrónico para {{ domain }}?",
|
||||
"setupDnsCheckbox": "Configura los registros DNS de correo ahora",
|
||||
"enableAction": "Habilitar",
|
||||
"cloudflareInfo": "Cloudflare administra el dominio <code> {{adminDomain}} </code>. Verifica que el proxy de Cloudflare esté inhabilitado para <code> {{mailFqdn}} </code> y configurado en <code> solo DNS </code>. Esto es necesario porque Cloudflare no es un proxy de correo electrónico."
|
||||
},
|
||||
"disableEmailDialog": {
|
||||
"description": "Esto configurará Cloudron para que deje de recibir correos electrónicos para <b> {{dominio}} </b>. Los buzones de correo y las listas asociadas con este dominio no se eliminarán.",
|
||||
"title": "¿Deshabilitar Servidor de Correo para {{ domain }}?",
|
||||
"disableAction": "Deshabilitar"
|
||||
},
|
||||
"addMailinglistDialog": {
|
||||
"membersOnlyCheckbox": "Restringir la publicación solo a miembros",
|
||||
"title": "Añadir Lista de correo",
|
||||
"members": "Lista de miembros",
|
||||
"membersInfo": "Separar las direcciones de correo electrónico con una nueva línea",
|
||||
"name": "Nombre"
|
||||
},
|
||||
"deleteMailinglistDialog": {
|
||||
"description": "¿Realmente quieres borrar la lista de correo <b>{{ name }}@{{ domain }}</b>?",
|
||||
"title": "Borrar lista de correo {{ name }}@{{ domain }}",
|
||||
"deleteAction": "Borrar"
|
||||
},
|
||||
"deleteMailboxDialog": {
|
||||
"description": "Después de la eliminación, los correos electrónicos enviados a este buzón rebotarán. Puedes optar por no eliminar los correos electrónicos de este buzón con fines de archivo. Los correos electrónicos archivados se encuentran en <code>/home/yellowtent/boxdata/mail/vmail</code> en el servidor.",
|
||||
"title": "Borrar Buzón de correo {{ name }}@{{ domain }}",
|
||||
"purgeMailboxCheckbox": "Borrar todos los correos y filtros dentro de este buzón de correo",
|
||||
"deleteAction": "Borrar"
|
||||
},
|
||||
"settings": {
|
||||
"tabTitle": "Ajustes"
|
||||
},
|
||||
"masquerading": {
|
||||
"title": "Enmascarado",
|
||||
"enableAction": "Habilitar",
|
||||
"disableAction": "Deshabilitar",
|
||||
"description": "El enmascaramiento permite a los usuarios y aplicaciones enviar correos electrónicos con un nombre de usuario arbitrario en la dirección DE."
|
||||
},
|
||||
"subscriptionDialog": {
|
||||
"title": "Se requiere suscripción",
|
||||
"description": "Para agregar más buzones de correo, configure un plan de pago.",
|
||||
"setupAction": "Configura tu suscripción"
|
||||
},
|
||||
"addMailboxDialog": {
|
||||
"title": "Añadir Buzón de correo",
|
||||
"name": "Nombre",
|
||||
"owner": "Propietario del Buzón de correo"
|
||||
},
|
||||
"editMailboxDialog": {
|
||||
"title": "Editar Buzón de correo {{ name }}@{{ domain }}",
|
||||
"owner": "Propietario del Buzón de correo",
|
||||
"aliases": "Alias",
|
||||
"noAliases": "No hay alias configuradas.",
|
||||
"addAliasAction": "Añadir un alias",
|
||||
"addAnotherAliasAction": "Añadir otro alias"
|
||||
},
|
||||
"editMailinglistDialog": {
|
||||
"title": "Editar Lista de correo {{ name }}@{{ domain }}"
|
||||
},
|
||||
"mailboxboxDialog": {
|
||||
"usersHeader": "Usuarios",
|
||||
"groupsHeader": "Grupos"
|
||||
},
|
||||
"updateMailinglistDialog": {
|
||||
"activeCheckbox": "La lista de correo está activa"
|
||||
},
|
||||
"status": {
|
||||
"tabTitle": "Estado"
|
||||
}
|
||||
},
|
||||
"passwordResetEmail": {
|
||||
"expireNote": "Tenga en cuenta que el enlace para restablecer la contraseña caducará en 24 horas."
|
||||
"expireNote": "Tenga en cuenta que el enlace para restablecer la contraseña caducará en 24 horas.",
|
||||
"description": "Alguien, con suerte, ha solicitado que se restablezca la contraseña de tu cuenta. Si no solicitaste este restablecimiento, ignora este mensaje.",
|
||||
"salutation": "Hola <%= user %>,",
|
||||
"resetAction": "Clic para resetear tu contraseña",
|
||||
"resetActionText": "Para restablecer su contraseña, visita la siguiente página: <% - resetLink%>",
|
||||
"subject": "[<%= cloudron %>] Restablecimiento de contraseña"
|
||||
},
|
||||
"notifications": {
|
||||
"clearAll": "Borrar todo",
|
||||
"dismissTooltip": "Descartar",
|
||||
"title": "Notificaciones",
|
||||
"nonePending": "No hay notificaciones!"
|
||||
},
|
||||
"terminal": {
|
||||
"title": "Terminal",
|
||||
"download": {
|
||||
"filePath": "Ruta al archivo o directorio",
|
||||
"download": "Descargar",
|
||||
"title": "Descargar desde {{ name }}"
|
||||
},
|
||||
"upload": {
|
||||
"title": "Subir archivo a {{ name }}"
|
||||
},
|
||||
"scheduler": "Programador / Cron",
|
||||
"restart": "Reiniciar",
|
||||
"uploadToTmp": "Subir a /tmp",
|
||||
"downloadAction": "Descargar",
|
||||
"busy": {
|
||||
"restarting": "Reiniciando aplicación…",
|
||||
"restartingInPausedMode": "Reiniciando la aplicación en modo pausado…",
|
||||
"resuming": "La aplicación está siendo restablecida…",
|
||||
"installing": "La aplicación está siendo instalada…"
|
||||
},
|
||||
"contextmenu": {
|
||||
"copy": "Copiar",
|
||||
"clear": "Borrar",
|
||||
"pasteInfo": "Para Pegar usa Ctrl+v"
|
||||
},
|
||||
"uploading": "Subiendo…"
|
||||
},
|
||||
"passwordReset": {
|
||||
"newPassword": {
|
||||
"errorLength": "La contraseña debe tener al menos 8 y un máximo de 265 caracteres",
|
||||
"title": "Establecer nueva contraseña",
|
||||
"password": "Nueva contraseña",
|
||||
"passwordRepeat": "Repetir Contraseña",
|
||||
"errorMismatch": "Las contraseñas no coinciden"
|
||||
},
|
||||
"title": "Restablecimiento de contraseña",
|
||||
"usernameOrEmail": "Nombre de usuario o email",
|
||||
"resetAction": "Reiniciar",
|
||||
"backToLoginAction": "Volver a iniciar sesión",
|
||||
"emailSent": {
|
||||
"title": "Se envió un correo electrónico para restablecer la contraseña"
|
||||
},
|
||||
"passwordChanged": {
|
||||
"submitAction": "Enviar"
|
||||
},
|
||||
"success": {
|
||||
"title": "Contraseña cambiada",
|
||||
"openDashboardAction": "Abrir Panel"
|
||||
}
|
||||
},
|
||||
"setupAccount": {
|
||||
"errorUsernameTooLong": "El nombre de usuario es demasiado largo",
|
||||
"fullName": "Nombre completo",
|
||||
"errorPasswordNoMatch": "Las contraseñas no coinciden",
|
||||
"password": "Nueva contraseña",
|
||||
"setupAction": "Configurar",
|
||||
"welcomeTo": "Bienvenido a",
|
||||
"description": "Por favor, configura tu cuenta",
|
||||
"username": "Nombre de usuario",
|
||||
"errorUsernameTooShort": "El nombre de usuario es demasiado corto",
|
||||
"errorUsernameInvalid": "El nombre de usuario no es válido",
|
||||
"passwordRepeat": "Repetir Contraseña",
|
||||
"errorPassword": "La contraseña debe ser de al menos 8 caracteres",
|
||||
"invalidToken": {
|
||||
"title": "Enlace de invitación no válido o caducado",
|
||||
"description": "Póngase en contacto con el administrador de su servidor para obtener un nuevo enlace de invitación."
|
||||
},
|
||||
"success": {
|
||||
"title": "Tu cuenta está lista",
|
||||
"openDashboardAction": "Abrir Panel"
|
||||
}
|
||||
},
|
||||
"welcomeEmail": {
|
||||
"welcomeTo": "Bienvenid@ a <%= cloudronName %>!",
|
||||
"expireNote": "Tenga en cuenta que el enlace de invitación caducará en 7 días.",
|
||||
"salutation": "Hola <%= user %>,",
|
||||
"inviteLinkAction": "Empezar",
|
||||
"invitor": "Recibió este correo electrónico porque fue invitado por <% = invitor%>.",
|
||||
"inviteLinkActionText": "Siga el enlace para comenzar: <% - invite Link%>",
|
||||
"subject": "Bienvenid@ a <%= cloudron %>"
|
||||
},
|
||||
"login": {
|
||||
"loginTo": "Iniciar sesión como",
|
||||
"errorIncorrectCredentials": "Nombre de usuario o contraseña incorrectos",
|
||||
"username": "Nombre de usuario",
|
||||
"password": "Contraseña",
|
||||
"2faToken": "Token 2FA (si está habilitado)",
|
||||
"signInAction": "Iniciar sesión",
|
||||
"resetPasswordAction": "Resetear contraseña"
|
||||
},
|
||||
"storage": {
|
||||
"mounts": {
|
||||
"volumeLocation": "Los volúmenes se montan por nombre de volumen en el directorio <code> / media </code> de esta aplicación."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-23
@@ -17,7 +17,8 @@
|
||||
"title": "Vous n'avez accès à aucune application pour le moment.",
|
||||
"description": "Lorsque ce sera le cas, elles apparaîtront ici."
|
||||
},
|
||||
"tagsFilterHeader": "Tags : {{ tags }}"
|
||||
"tagsFilterHeader": "Tags : {{ tags }}",
|
||||
"groupsFilterHeader": "Sélectioner groupe"
|
||||
},
|
||||
"main": {
|
||||
"offline": "Cloudron est hors ligne. Reconnexion…",
|
||||
@@ -54,7 +55,8 @@
|
||||
"clickToCopy": "Cliquez pour copier",
|
||||
"clickToCopyBackupId": "Cliquez pour sauvegarder l'identifiant de sauvegarde",
|
||||
"copied": "Copier dans le presse-papiers"
|
||||
}
|
||||
},
|
||||
"searchPlaceholder": "Rechercher"
|
||||
},
|
||||
"users": {
|
||||
"title": "Utilisateurs",
|
||||
@@ -189,8 +191,7 @@
|
||||
"subscriptionDialog": {
|
||||
"title": "Abonnement nécessaire",
|
||||
"setupAction": "Paramétrer mon abonnement"
|
||||
},
|
||||
"searchPlaceholder": "Rechercher"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"title": "Profil",
|
||||
@@ -201,7 +202,7 @@
|
||||
},
|
||||
"passwordRecoveryEmail": "Adresse email de récupération de mot de passe",
|
||||
"language": "Langue",
|
||||
"primaryEmail": "Adresse email principale",
|
||||
"primaryEmail": "E-mail principal",
|
||||
"disable2FA": {
|
||||
"disable": "Désactiver",
|
||||
"password": "Mot de passe",
|
||||
@@ -218,12 +219,12 @@
|
||||
},
|
||||
"appPasswords": {
|
||||
"app": "Application",
|
||||
"deletePasswordTooltip": "Supprimer le mot de passe",
|
||||
"deletePasswordTooltip": "Supprimer mot de passe",
|
||||
"newPassword": "Nouveau mot de passe",
|
||||
"name": "Nom",
|
||||
"noPasswordsPlaceholder": "Aucun mot de passe d'application créé",
|
||||
"title": "Mots de passe des applications",
|
||||
"description": "Les mots de passe d'application permettent de garantir la sécurité de votre profil utilisateur Cloudron. Si vous avez besoin d'accéder à une application Cloudron depuis une application mobile ou un appareil non fiable, vous pouvez vous connecter avec votre nom d'utilisateur et un mot de passe alternatif généré spécialement ici."
|
||||
"title": "Mots de passe d'application",
|
||||
"description": "Les mots de passe d'application sont une mesure de sécurité pour protéger votre compte utilisateur Cloudron. Si vous avez besoin d'accéder à une application Cloudron depuis une application mobile ou un client auquel vous ne faites pas confiance, vous pouvez vous connecter avec votre nom d'utilisateur et le mot de passe alternatif généré ici."
|
||||
},
|
||||
"changeEmail": {
|
||||
"errorEmailInvalid": "Cette adresse email est invalide",
|
||||
@@ -270,15 +271,15 @@
|
||||
"expiresAt": "Expire le",
|
||||
"name": "Nom",
|
||||
"noTokensPlaceholder": "Aucun jeton API créé",
|
||||
"revokeTokenTooltip": "Révoquer le jeton",
|
||||
"revokeTokenTooltip": "Révoquer jeton",
|
||||
"newApiToken": "Nouveau jeton API",
|
||||
"title": "Jetons API",
|
||||
"description": "Utilisez ces jetons d'accès personnels pour vous authentifier auprès de l'<a target=\"_blank\" href=\"{{ apiDocsLink }}\">API Cloudron</a>"
|
||||
"description": "Utilisez ces jetons d'accès personnels pour vous authentifier avec <a target=\"_blank\" href=\"{{ apiDocsLink }}\">l'API Cloudron</a>"
|
||||
},
|
||||
"loginTokens": {
|
||||
"logoutAll": "Se déconnecter de tout",
|
||||
"title": "Jetons d'accès",
|
||||
"description": "Vous avez {{ webadminTokenCount}} jeton(s) web et {{ cliTokenCount }} jeton(s) pour l'interface de ligne de commande (CLI) actif(s)."
|
||||
"logoutAll": "Déconnecter de tous",
|
||||
"title": "Jetons de connexion",
|
||||
"description": "Vous avez {{ webadminTokens.length }} jeton(s) web actif(s) et {{ cliTokens.length }} jeton(s) CLI."
|
||||
},
|
||||
"disable2FAAction": "Désactiver l'authentification à deux facteurs (2FA)",
|
||||
"enable2FAAction": "Activer l'authentification à deux facteurs (2FA)"
|
||||
@@ -418,7 +419,7 @@
|
||||
"solrDisabled": "Désactivé",
|
||||
"maxMailSize": "Taille maximale des messages",
|
||||
"location": "Emplacement du serveur de messagerie",
|
||||
"info": "Ces paramètres sont globaux et s'appliquent à tous les domaines.",
|
||||
"info": "Ces paramètres généraux s'appliquent à tous les domaines.",
|
||||
"solrNotRunning": "Inactif",
|
||||
"title": "Paramètres",
|
||||
"spamFilter": "Filtre anti-spam",
|
||||
@@ -625,7 +626,7 @@
|
||||
"git": "Hébergement de codes",
|
||||
"project": "Gestion de projet",
|
||||
"media": "Médias",
|
||||
"analytics": "Analyse des données",
|
||||
"analytics": "Analyse de données",
|
||||
"notes": "Notes"
|
||||
},
|
||||
"accountDialog": {
|
||||
@@ -848,8 +849,8 @@
|
||||
},
|
||||
"startStop": {
|
||||
"description": "Pour économiser les ressources du serveur, vous pouvez mettre en pause les applications au lieu de les désinstaller. Les futures sauvegardes d'applications ne comprendront pas les modifications apportées aux applications entre aujourd'hui et la dernière sauvegarde. Pour cette raison, il est recommandé de lancer une sauvegarde avant de mettre une application en pause.",
|
||||
"stopAction": "Mettre l'application en pause",
|
||||
"title": "Démarrer / Mettre en pause",
|
||||
"stopAction": "Arrêter l'application",
|
||||
"title": "Démarrer / Arrêter",
|
||||
"startAction": "Démarrer l'application"
|
||||
}
|
||||
},
|
||||
@@ -885,8 +886,7 @@
|
||||
"7d": "7 jours",
|
||||
"24h": "24 heures",
|
||||
"12h": "12 heures"
|
||||
},
|
||||
"selectPeriod": "Période sélectionnée {{ period }}"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"memory": {
|
||||
@@ -907,7 +907,7 @@
|
||||
"addAliasAction": "Ajouter un alias",
|
||||
"aliases": "Alias",
|
||||
"saveAction": "Sauvegarder",
|
||||
"addRedirectionAction": "Ajouter un redirection",
|
||||
"addRedirectionAction": "Ajouter une redirection",
|
||||
"noRedirections": "Aucune redirection n'est paramétrée.",
|
||||
"redirectionsPlaceholder": "Laisser vide pour utiliser le nom de domaine nu",
|
||||
"redirections": "Redirections",
|
||||
@@ -974,7 +974,8 @@
|
||||
"ja": "Japonais",
|
||||
"pl": "Polonais",
|
||||
"vi": "Vietnamien",
|
||||
"zh_Hans": "Chinois (Simplifié)"
|
||||
"zh_Hans": "Chinois (Simplifié)",
|
||||
"es": "Espagnol"
|
||||
},
|
||||
"email": {
|
||||
"mailboxboxDialog": {
|
||||
@@ -1390,7 +1391,7 @@
|
||||
"details": "Détails",
|
||||
"source": "Source",
|
||||
"time": "Date",
|
||||
"title": "Journal des événements"
|
||||
"title": "Journal d'évènements"
|
||||
},
|
||||
"system": {
|
||||
"selectPeriodLabel": "Période sélectionnée",
|
||||
@@ -1409,7 +1410,7 @@
|
||||
"mountedAt": "{{ filesystem }} <small>monté sur</small> {{ mountpoint }}",
|
||||
"title": "Utilisation du disque"
|
||||
},
|
||||
"title": "Informations système"
|
||||
"title": "Info système"
|
||||
},
|
||||
"services": {
|
||||
"refresh": "Rafraîchir",
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
"clickToCopyBackupId": "Clicca per copiare l'id del backup",
|
||||
"clickToCopy": "Clicca per copiare",
|
||||
"copied": "Copiato negli appunti"
|
||||
}
|
||||
},
|
||||
"searchPlaceholder": "Cerca"
|
||||
},
|
||||
"apps": {
|
||||
"domainsFilterHeader": "Tutti i domini",
|
||||
@@ -54,7 +55,8 @@
|
||||
"title": "Nessuna App è ancora installata!",
|
||||
"description": "Perché non installare qualche app? Visita l'<a href=\"{{ appStoreLink }}\">App Store</a>"
|
||||
},
|
||||
"title": "Le mie applicazioni"
|
||||
"title": "Le mie applicazioni",
|
||||
"groupsFilterHeader": "Seleziona gruppo"
|
||||
},
|
||||
"volumes": {
|
||||
"backupWarning": "I volumi <i>non</i> sono inclusi nel backup. Il ripristino di un'app non ripristinerà il contenuto del volume. Assicurati di avere un piano di backup adatto per ogni volume.",
|
||||
@@ -327,8 +329,7 @@
|
||||
"7d": "7 giorni",
|
||||
"24h": "24 ore",
|
||||
"12h": "12 ore"
|
||||
},
|
||||
"selectPeriod": "Seleziona Periodo {{ period }}"
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"mounts": {
|
||||
@@ -853,7 +854,6 @@
|
||||
"title": "Profilo"
|
||||
},
|
||||
"users": {
|
||||
"searchPlaceholder": "Cerca",
|
||||
"role": {
|
||||
"owner": "Superadmin",
|
||||
"admin": "Amministartore",
|
||||
|
||||
+89
-18
@@ -17,7 +17,8 @@
|
||||
"tagsFilterHeaderAll": "Alle Tags",
|
||||
"tagsFilterHeader": "Tags: {{ tags }}",
|
||||
"stateFilterHeader": "Alle statussen",
|
||||
"searchPlaceholder": "Zoek Apps"
|
||||
"searchPlaceholder": "Zoek Apps",
|
||||
"groupsFilterHeader": "Selecteer groep"
|
||||
},
|
||||
"main": {
|
||||
"logout": "Uitloggen",
|
||||
@@ -54,7 +55,23 @@
|
||||
"warning": "Het herstarten van de server zorgt voor tijdelijke onbereikbaarheid van alle apps geïnstalleerd op deze Cloudron!",
|
||||
"description": "Gebruik dit om veiligheidsupdates te installeren of indien je onverwachte problemen ervaart. Alle apps en services die momenteel werken op deze Cloudron zullen automatisch opstarten zodra de herstart is voltooid."
|
||||
},
|
||||
"offline": "Cloudron is offline. Opnieuw verbinden…"
|
||||
"offline": "Cloudron is offline. Opnieuw verbinden…",
|
||||
"searchPlaceholder": "Zoeken",
|
||||
"multiselect": {
|
||||
"selected": "{{ n }} geselecteerd",
|
||||
"select": "Selecteer",
|
||||
"filterPlaceholder": "Type om te filteren"
|
||||
},
|
||||
"prettyDate": {
|
||||
"justNow": "zojuist",
|
||||
"yeserday": "Gisteren",
|
||||
"minutesAgo": "{{ m }} minuten geleden",
|
||||
"hoursAgo": "{{ h }} uur geleden",
|
||||
"daysAgo": "{{ d }} dagen geleden",
|
||||
"weeksAgo": "{{ w }} weken geleden",
|
||||
"monthsAgo": "{{ m }} maanden geleden",
|
||||
"yearsAgo": "{{ y }} jaren geleden"
|
||||
}
|
||||
},
|
||||
"appstore": {
|
||||
"title": "App Store",
|
||||
@@ -80,7 +97,8 @@
|
||||
"project": "Project Management",
|
||||
"wiki": "Wiki",
|
||||
"vpn": "VPN",
|
||||
"learning": "Onderwijs"
|
||||
"learning": "Onderwijs",
|
||||
"federated": "Gefedereerd"
|
||||
},
|
||||
"noAppsFound": "Geen apps gevonden.",
|
||||
"appMissing": "Mis je een app? Laat het ons weten.",
|
||||
@@ -143,7 +161,7 @@
|
||||
"usermanagerTooltip": "Deze gebruiker kan groepen en andere gebruikers beheren",
|
||||
"inactiveTooltip": "Gebruiker is inactief",
|
||||
"externalLdapTooltip": "Van externe LDAP adresboek",
|
||||
"resetPasswordTooltip": "Wachtwoord herstel link",
|
||||
"resetPasswordTooltip": "Wachtwoord of 2FA opnieuw instellen",
|
||||
"editUserTooltip": "Wijzig gebruiker",
|
||||
"removeUserTooltip": "Verwijder gebruiker",
|
||||
"superadminTooltip": "Deze gebruiker is superadmin",
|
||||
@@ -188,7 +206,9 @@
|
||||
"errorSelfSignedCert": "Server gebruikt een ongeldig of zelf-ondertekend certificaat.",
|
||||
"description": "Cloudron synchroniseert gebruikers en groepen van een extern LDAP of ActiveDirectory server. Wachtwoordverificatie vindt plaats door de externe server. De synchronisatie is niet automatisch en dient handmatig gestart te worden.",
|
||||
"auth": "Authenticatie",
|
||||
"autocreateUsersOnLogin": "Maak automatisch gebruikers aan na inloggen bij deze Cloudron"
|
||||
"autocreateUsersOnLogin": "Maak automatisch gebruikers aan na inloggen bij deze Cloudron",
|
||||
"providerOther": "Anders",
|
||||
"providerDisabled": "Uitgeschakeld"
|
||||
},
|
||||
"subscriptionDialog": {
|
||||
"title": "Abonnement benodigd",
|
||||
@@ -247,9 +267,15 @@
|
||||
"deleteAction": "Verwijder"
|
||||
},
|
||||
"passwordResetDialog": {
|
||||
"title": "Wachtwoordherstel-link voor {{ username }}",
|
||||
"title": "Wachtwoord of 2FA opnieuw instellen voor {{ username }}",
|
||||
"sendEmailLinkAction": "E-mail link naar gebruiker",
|
||||
"description": "Gebruik onderstaande link om {{ username }}'s wachtwoord te herstellen of opnieuw uit te nodigen:"
|
||||
"description": "Gebruik onderstaande link om {{ username }}'s wachtwoord te herstellen of opnieuw uit te nodigen:",
|
||||
"emailSent": "Verstuurd",
|
||||
"no2FASetup": "Deze gebruiker heeft geen 2FA ingesteld.",
|
||||
"2FAIsSetup": "2FA van de gebruiker uit schakelen. De gebruiker kan het aanzetten via Profiel.",
|
||||
"newLinkAction": "Genereer nieuwe link",
|
||||
"resetLinkExplanation": "Genereer een wachtwoordherstel- of uitnodiging link. De nieuwe link maakt de vorige link ongeldig.",
|
||||
"reset2FAAction": "2FA opnieuw instellen"
|
||||
},
|
||||
"externalLdapDialog": {
|
||||
"title": "Configureer LDAP"
|
||||
@@ -260,7 +286,6 @@
|
||||
"admin": "Administrator",
|
||||
"owner": "Superadmin"
|
||||
},
|
||||
"searchPlaceholder": "Zoeken",
|
||||
"transferOwnershipDialog": {
|
||||
"transferAction": "Eigenaarschap overdragen",
|
||||
"description": "Hiermee wordt de geselecteerde gebruiker de Eigenaar en Admin van deze Cloudron, de huidige Admin verliest diens rechten.",
|
||||
@@ -316,7 +341,9 @@
|
||||
"noTokensPlaceholder": "Er zijn geen API Tokens aangemaakt",
|
||||
"revokeTokenTooltip": "Token intrekken",
|
||||
"expiresAt": "Vervalt op",
|
||||
"description": "Gebruik deze persoonlijke toegangstokens voor authenticatie met de <a target=\"_blank\" href=\"{{ apiDocsLink }}\">Cloudron API</a>"
|
||||
"description": "Gebruik deze persoonlijke toegangstokens voor authenticatie met de <a target=\"_blank\" href=\"{{ apiDocsLink }}\">Cloudron API</a>",
|
||||
"neverUsed": "nooit",
|
||||
"lastUsed": "Laatst gebruikt"
|
||||
},
|
||||
"loginTokens": {
|
||||
"title": "Inlog Tokens",
|
||||
@@ -451,10 +478,20 @@
|
||||
"encryptionDescription": "Let op: bewaar dit wachtwoord op een veilige plaats. Cloudron bewaart dit wachtwoord niet, zonder dit wachtwoord kunnen backups niet ontsleutelt worden",
|
||||
"uploadPartSizeDescription": "Meerdelige uploaddeelgrootte. Er worden maximaal 3 delen parallel geüpload en vereist evenveel geheugen.",
|
||||
"uploadConcurrencyDescription": "Aantal bestanden dat parallel moet worden geüpload tijdens het maken van een backup",
|
||||
"encryptionPasswordRepeat": "Herhaal wachtwoord"
|
||||
"encryptionPasswordRepeat": "Herhaal wachtwoord",
|
||||
"remoteDirectory": "Externe map",
|
||||
"username": "Gebruikersnaam",
|
||||
"setupMountDescription": "Indien aangevinkt zal Cloudron het mount point configureren op de server",
|
||||
"server": "Server IP of Hostnaam",
|
||||
"password": "Wachtwoord",
|
||||
"configureMount": "Specificeer de mount point configuratie"
|
||||
},
|
||||
"backupFailed": {
|
||||
"title": "Backup maken niet mogelijk"
|
||||
},
|
||||
"check": {
|
||||
"noop": "Cloudron backups zijn uitgeschakeld. Zorg ervoor dat deze server op een andere manier wordt geback-upt. Kijk op https://docs.cloudron.io/backups/#storage-providers voor meer informatie.",
|
||||
"sameDisk": "Cloudron backups staan momenteel op dezelfde schijf als deze Cloudron server. Dit is gevaarlijk en kan leiden tot gegevensverlies als de schijf defect raakt. Kijk op https://docs.cloudron.io/backups/#storage-providers hoe je backups op een externe locatie kan zetten."
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
@@ -642,7 +679,12 @@
|
||||
"title": "E-mail VAN adres",
|
||||
"mailboxPlaceholder": "Laat leeg om platformstandaard te gebruiken",
|
||||
"saveAction": "Opslaan",
|
||||
"description": "Dit stelt het adres in waarvandaan deze app e-mail verzendt. Deze app is al geconfigureerd om e-mail te verzenden met deze {{ domain }}'s <a href=\"{{ domainConfigLink }}\">uitgaande e-mail</a> instellingen."
|
||||
"description": "Dit stelt het adres in waarvandaan deze app e-mail verzendt. Deze app is al geconfigureerd om e-mail te verzenden met deze {{ domain }}'s <a href=\\\"{{ domainConfigLink }}\\\">uitgaande e-mail</a> instellingen.",
|
||||
"description2": "Indien ingeschakeld, verstuurt de app e-mails via de interne mailserver met dit adres. De interne mailserver gebruikt de {{ domain }}'s <a href=\"{{ domainConfigLink }}\">Uitgaande e-mail</a> instellingen om e-mail te versturen. Indien uitgeschakeld, kun je de e-mailinstellingen bewerken in de app.",
|
||||
"enable": "Verstuur e-mails via Cloudron Mail",
|
||||
"disable": "Configureer geen e-mailinstellingen",
|
||||
"enableDescription": "De app is geconfigureerd om e-mails te verzenden met het onderstaande adres en het {{ domain }}'s <a href=\\\"{{ domainConfigLink }}\\\">Uitgaande e-mail</a> instellingen.",
|
||||
"disableDescription": "De instellingen voor e-mailaflevering zijn niet geconfigureerd. Je kunt dit nu configureren in de app zelf."
|
||||
}
|
||||
},
|
||||
"backAction": "Terug naar Mijn Apps",
|
||||
@@ -733,12 +775,12 @@
|
||||
}
|
||||
},
|
||||
"graphs": {
|
||||
"selectPeriod": "Selecteer periode {{ period }}",
|
||||
"period": {
|
||||
"24h": "24 uur",
|
||||
"12h": "12 uur",
|
||||
"7d": "7 dagen",
|
||||
"30d": "30 dagen"
|
||||
"30d": "30 dagen",
|
||||
"6h": "6 uur"
|
||||
},
|
||||
"memoryTitle": "Geheugen (RAM + Swap) in MB"
|
||||
},
|
||||
@@ -892,6 +934,9 @@
|
||||
"notResponding": "Reageert niet",
|
||||
"stopped": "Gestopt",
|
||||
"running": "Lopend"
|
||||
},
|
||||
"stopDialog": {
|
||||
"title": "Weet je zeker dat je {{ app }} wilt stoppen?"
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
@@ -981,7 +1026,8 @@
|
||||
"configureAction": "Configureer register",
|
||||
"title": "Privaat Docker Register",
|
||||
"description": "Cloudron kan <a href=\"{{ customAppsLink }}\" target=\"_blank\">aangepaste apps</a> binnenhalen en installeren van een privaat docker register.",
|
||||
"usernameNotSet": "Niet ingesteld"
|
||||
"usernameNotSet": "Niet ingesteld",
|
||||
"serverNotSet": "Niet ingesteld"
|
||||
},
|
||||
"privateDockerRegistryDialog": {
|
||||
"title": "Privaat Register configuratie",
|
||||
@@ -1012,7 +1058,9 @@
|
||||
},
|
||||
"title": "Instellingen",
|
||||
"registryConfig": {
|
||||
"provider": "Docker Register Aanbieder"
|
||||
"provider": "Docker Register Aanbieder",
|
||||
"providerOther": "Anders",
|
||||
"providerDisabled": "Uitgeschakeld"
|
||||
}
|
||||
},
|
||||
"support": {
|
||||
@@ -1335,7 +1383,8 @@
|
||||
"title": "Maillijst toevoegen",
|
||||
"members": "Lijst leden",
|
||||
"membersInfo": "Plaats meerdere e-mailadressen elk op een nieuwe regel",
|
||||
"membersOnlyCheckbox": "Het versturen van e-mail aan deze lijst beperken tot de leden"
|
||||
"membersOnlyCheckbox": "Het versturen van e-mail aan deze lijst beperken tot de leden",
|
||||
"name": "Naam"
|
||||
},
|
||||
"editMailinglistDialog": {
|
||||
"title": "Bewerk Maillijst {{ name }}@{{ domain }}"
|
||||
@@ -1351,6 +1400,12 @@
|
||||
},
|
||||
"settings": {
|
||||
"tabTitle": "Instellingen"
|
||||
},
|
||||
"updateMailboxDialog": {
|
||||
"activeCheckbox": "Mailbox is actief"
|
||||
},
|
||||
"updateMailinglistDialog": {
|
||||
"activeCheckbox": "Mailing-lijst is actief"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
@@ -1401,14 +1456,30 @@
|
||||
"addVolumeDialog": {
|
||||
"addAction": "Toevoegen",
|
||||
"nameWarning": "Cloudron mount dit host-pad in de app's container met deze naam onder <code>/media</code>.",
|
||||
"title": "Volume toevoegen"
|
||||
"title": "Volume toevoegen",
|
||||
"server": "Server IP of Hostnaam",
|
||||
"remoteDirectory": "Externe map",
|
||||
"username": "Gebruikersnaam",
|
||||
"password": "Wachtwoord",
|
||||
"diskPath": "Schijf pad",
|
||||
"noopWarning": "Cloudron zal de server niet configureren om dit Volume te mounten",
|
||||
"port": "Poort",
|
||||
"user": "Gebruiker",
|
||||
"privateKey": "Private SSH sleutel",
|
||||
"mountTypeInfo": "Cloudron zal de server configureren om dit Volume te mounten"
|
||||
},
|
||||
"hostPath": "Host-pad",
|
||||
"removeVolumeActionTooltip": "Verwijder Volume",
|
||||
"openFileManagerActionTooltip": "Open bestandsbeheer",
|
||||
"name": "Naam",
|
||||
"addVolumeAction": "Volume toevoegen",
|
||||
"title": "Volumes"
|
||||
"title": "Volumes",
|
||||
"mountType": "Mount type",
|
||||
"updateVolumeDialog": {
|
||||
"title": "Update Volume {{ volume }}"
|
||||
},
|
||||
"tooltipEdit": "Bewerk Volume",
|
||||
"mountStatus": "Mount status"
|
||||
},
|
||||
"lang": {
|
||||
"it": "Italiaans",
|
||||
|
||||
+98
-42
@@ -17,7 +17,8 @@
|
||||
"noApps": {
|
||||
"title": "Chưa có app cài đặt!",
|
||||
"description": "Cài đặt một vài app nhé? Hãy xem trong <a href=\"{{ appStoreLink }}\">Cửa hàng App</a>"
|
||||
}
|
||||
},
|
||||
"groupsFilterHeader": "Chọn nhóm"
|
||||
},
|
||||
"main": {
|
||||
"logout": "Thoát",
|
||||
@@ -54,7 +55,23 @@
|
||||
"description": "Sử dụng chức năng này cho bản cập nhật an ninh hay khi hệ thống gặp trục trặc ngoài ý muốn. Tất cả app và dịch vụ đang chạy trên Cloudron sẽ tự động chạy lại sau khi khởi động lại hoàn thành."
|
||||
},
|
||||
"actions": "Thao tác",
|
||||
"offline": "Cloudron đang offline. Đang kết nối lại…"
|
||||
"offline": "Cloudron đang offline. Đang kết nối lại…",
|
||||
"searchPlaceholder": "Tìm kiếm",
|
||||
"multiselect": {
|
||||
"selected": "{{ n }} đã chọn",
|
||||
"select": "Chọn",
|
||||
"filterPlaceholder": "Gõ để lọc các lựa chọn"
|
||||
},
|
||||
"prettyDate": {
|
||||
"justNow": "mới đây",
|
||||
"yeserday": "Hôm qua",
|
||||
"minutesAgo": "{{ m }} phút trước",
|
||||
"hoursAgo": "{{ h }} tiếng trước",
|
||||
"yearsAgo": "{{ y }} năm trước",
|
||||
"daysAgo": "{{ d }} ngày trước",
|
||||
"weeksAgo": "{{ w }} tuần trước",
|
||||
"monthsAgo": "{{ m }} tháng trước"
|
||||
}
|
||||
},
|
||||
"appstore": {
|
||||
"title": "Cửa hàng App",
|
||||
@@ -80,7 +97,8 @@
|
||||
"vpn": "VPN",
|
||||
"blog": "Blog",
|
||||
"document": "Tài liệu",
|
||||
"project": "Quản lý dự án"
|
||||
"project": "Quản lý dự án",
|
||||
"federated": "Phần mềm Liên hiệp hoá"
|
||||
},
|
||||
"noAppsFound": "Không tìm thấy app.",
|
||||
"unstable": "Chưa ổn định",
|
||||
@@ -192,11 +210,13 @@
|
||||
"subscriptionRequiredAction": "Cài đặt gói đăng ký ngay",
|
||||
"description": "Cloudron sẽ đồng bộ người dùng và nhóm từ server LDAP hay ActiveDirectory bên ngoài. Xác minh mật khẩu cho người dùng được dựa trên server ngoài. Việc đồng bộ hoá không được chạy tự động mà cần được khởi động bằng tay.",
|
||||
"title": "LDAP",
|
||||
"subscriptionRequired": "Tính năng này chỉ có trong gói trả phí."
|
||||
"subscriptionRequired": "Tính năng này chỉ có trong gói trả phí.",
|
||||
"providerOther": "Khác",
|
||||
"providerDisabled": "Đã tắt"
|
||||
},
|
||||
"users": {
|
||||
"inactiveTooltip": "Người dùng không hoạt động",
|
||||
"resetPasswordTooltip": "Cài lại mật khẩu hay link mời",
|
||||
"resetPasswordTooltip": "Cài lại mật khẩu, tắc xác minh 2 Bước hay gửi link mời",
|
||||
"removeUserTooltip": "Xóa người dùng",
|
||||
"editUserTooltip": "Chỉnh sửa người dùng",
|
||||
"notActivatedYetTooltip": "Người dùng chưa được kích hoạt",
|
||||
@@ -247,9 +267,15 @@
|
||||
"title": "Xoá nhóm {{ name }}"
|
||||
},
|
||||
"passwordResetDialog": {
|
||||
"title": "Đặt lại mật khẩu hoặc link mời cho {{ username }}",
|
||||
"title": "Đặt lại mật khẩu/xác minh 2 Bước cho {{ username }}",
|
||||
"description": "Dùng link dưới đây để đặt lại mật khẩu hoặc mời lại {{ username }}:",
|
||||
"sendEmailLinkAction": "Gửi link qua email cho người dùng"
|
||||
"sendEmailLinkAction": "Gửi link qua email cho người dùng",
|
||||
"2FAIsSetup": "Dùng tính năng này để tắt mã xác minh 2 Bước cho người dùng. Người dùng có thể tự cài lại trong mục Hồ sơ.",
|
||||
"newLinkAction": "Tạo link mới",
|
||||
"resetLinkExplanation": "Dùng tính năng này để tạo link cài lại mật khẩu hay link mời tham gia. Link mới sẽ vô hiệu hoá link cũ ngay tức thì.",
|
||||
"no2FASetup": "Người dùng chưa cài đặt mã xác minh 2 Bước.",
|
||||
"reset2FAAction": "Cài lại mã xác minh 2 Bước",
|
||||
"emailSent": "Đã gửi"
|
||||
},
|
||||
"transferOwnershipDialog": {
|
||||
"newOwner": "Chủ sở hữu mới",
|
||||
@@ -265,8 +291,7 @@
|
||||
"usermanager": "Quản lý người dùng",
|
||||
"admin": "Admin",
|
||||
"owner": "Siêu Admin"
|
||||
},
|
||||
"searchPlaceholder": "Tìm kiếm"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"changeAvatar": {
|
||||
@@ -338,7 +363,9 @@
|
||||
"revokeTokenTooltip": "Rút lại mã",
|
||||
"newApiToken": "Mã API mới",
|
||||
"name": "Tên",
|
||||
"expiresAt": "Hết hiệu lực vào"
|
||||
"expiresAt": "Hết hiệu lực vào",
|
||||
"lastUsed": "Lần dùng cuối",
|
||||
"neverUsed": "chưa từng dùng"
|
||||
},
|
||||
"loginTokens": {
|
||||
"title": "Mã đăng nhập",
|
||||
@@ -372,7 +399,7 @@
|
||||
},
|
||||
"title": "Bản sao lưu",
|
||||
"configureBackupStorage": {
|
||||
"copyConcurrencyDescription": "Số bản sao tập tin từ xa song song khi đang sao lưu Cloudron.",
|
||||
"copyConcurrencyDescription": "Số bản sao tập tin từ xa cùng lúc khi đang sao lưu Cloudron.",
|
||||
"memoryLimitDescription": "Giới hạn bộ nhớ cho thao tác sao lưu. Điều chỉnh nếu bạn cần tăng giới hạn hiện tại so với giá trị mặc định.",
|
||||
"encryptionPasswordRepeat": "Nhập lại mật khẩu",
|
||||
"encryptionPasswordPlaceholder": "Mật khẩu để mã hoá các bản sao lưu",
|
||||
@@ -428,7 +455,7 @@
|
||||
"list": "Tham chiếu sao lưu của {{ appCount }} app",
|
||||
"format": "Định dạng",
|
||||
"version": "Phiên bản",
|
||||
"date": "Ngày",
|
||||
"date": "Thời gian",
|
||||
"id": "ID",
|
||||
"title": "Chi tiết sao lưu"
|
||||
},
|
||||
@@ -455,6 +482,10 @@
|
||||
"schedule": "Lịch sao lưu",
|
||||
"description": "Cloudron sao lưu toàn bộ hệ thống của bạn dựa vào định kỳ sao lưu đã lên lịch và giữ các bản sao lưu theo thời gian lưu giữ đã định.",
|
||||
"title": "Lịch sao lưu và thời gian lưu giữ"
|
||||
},
|
||||
"check": {
|
||||
"noop": "Tính năng sao lưu Cloudron đã tắt. Hãy chắc rằng server được sao lưu bằng một biện pháp khác. Xem thông tin thêm tại https://docs.cloudron.io/backups/#storage-providers.",
|
||||
"sameDisk": "Các bản sao lưu Cloudron đang ở trên cùng ổ đĩa với server chạy Cloudron. Việc này sẽ nguy hiểm và có thể dẫn đến mất dữ liệu nếu ổ đĩa bị trục trặc. Xem cách sao lưu tại ổ đĩa ngoài tại https://docs.cloudron.io/backups/#storage-providers."
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
@@ -500,7 +531,7 @@
|
||||
},
|
||||
"incoming": {
|
||||
"mailinglists": {
|
||||
"members": "Thành viên trong danh sách",
|
||||
"members": "Thành viên",
|
||||
"description": "Danh sách này chuyển tiếp mail cho những thành viên trong danh sách.",
|
||||
"title": "Danh sách chuyển tiếp mail",
|
||||
"membersOnlyTooltip": "Chỉ cho phép chuyển tiếp mail đến thành viên trong danh sách",
|
||||
@@ -554,7 +585,7 @@
|
||||
},
|
||||
"noopNonAdminDomainWarning": "Cloudron không thể cung cấp dịch vụ gửi mail cho các app trên tên miền này khi chế độ email chưa được bật.",
|
||||
"noopAdminDomainWarning": "Cloudron không thể gửi link mời người dùng, đặt lại mật khẩu hay gửi các thông báo khác khi chế độ email chưa được bật trên tên miền chính",
|
||||
"description": "Cloudron sẽ dùng mail server này (Smart host) để gửi mail đi cho các app được cài trên tên miền này.",
|
||||
"description": "Cloudron sẽ dùng mail server này (Smart host) để gửi mail ra cho các app cài trên tên miền.",
|
||||
"title": "Hệ thống relay chuyển mail ra ngoài"
|
||||
},
|
||||
"mailboxboxDialog": {
|
||||
@@ -572,8 +603,9 @@
|
||||
"addMailinglistDialog": {
|
||||
"membersOnlyCheckbox": "Chỉ cho phép chuyển tiếp mail cho thành viên trong danh sách",
|
||||
"membersInfo": "Cách mỗi email bằng một dòng mới",
|
||||
"members": "Thành viên trong danh sách",
|
||||
"title": "Thêm danh sách chuyển tiếp mail"
|
||||
"members": "Thành viên",
|
||||
"title": "Thêm danh sách chuyển tiếp mail",
|
||||
"name": "Tên"
|
||||
},
|
||||
"deleteMailboxDialog": {
|
||||
"deleteAction": "Xoá",
|
||||
@@ -644,6 +676,12 @@
|
||||
},
|
||||
"settings": {
|
||||
"tabTitle": "Cài đặt"
|
||||
},
|
||||
"updateMailboxDialog": {
|
||||
"activeCheckbox": "Hộp thư đang hoạt động"
|
||||
},
|
||||
"updateMailinglistDialog": {
|
||||
"activeCheckbox": "Danh sách chuyển tiếp đang hoạt động"
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
@@ -659,8 +697,8 @@
|
||||
},
|
||||
"firewall": {
|
||||
"configure": {
|
||||
"blocklistPlaceholder": "Dòng ngăn cách địa chỉ IP hay Subnet",
|
||||
"description": "Những địa chỉ IP trùng khớp sẽ không kết nối vào server này được bao gồm mail server, dashboard và tất cả các app. Cẩn thận đừng tự khoá bản thân mình ra khỏi server.",
|
||||
"blocklistPlaceholder": "Địa chỉ IP hay Subnet (ghi cách dòng)",
|
||||
"description": "Những địa chỉ IP trong đây sẽ không kết nối vào server này được bao gồm mail server, dashboard và tất cả các app. Cẩn thận đừng tự khoá mình ra khỏi server.",
|
||||
"title": "Cấu hình tường lửa"
|
||||
},
|
||||
"blocklist": "{{ blockCount }} địa chỉ IP đã được chặn",
|
||||
@@ -668,7 +706,7 @@
|
||||
"title": "Tường lửa"
|
||||
},
|
||||
"ip": {
|
||||
"detected": "được dò ra",
|
||||
"detected": "đã dò tìm ra",
|
||||
"interfaceDescription": "Liệt kê những thiết bị hiện hữu trên server với:",
|
||||
"configure": "Cấu hình",
|
||||
"interface": "Tên giao diện mạng",
|
||||
@@ -696,7 +734,7 @@
|
||||
},
|
||||
"spamFilterDialog": {
|
||||
"customRulesPlaceholder": "Quy định Spamassassin tuỳ chỉnh",
|
||||
"blacklisteAddressesPlaceholder": "Dòng để cácch những kiểu mẫu địa chỉ mail",
|
||||
"blacklisteAddressesPlaceholder": "Mẫu địa chỉ mail (ghi cách dòng)",
|
||||
"customRules": "Quy định Spamassassin tuỳ chỉnh",
|
||||
"blacklisteAddressesInfo": "Địa chỉ mail trùng khớp trong danh sách đen sẽ bị cho vào mục Spam. Kiểu ghi ‘*’ và ‘?’ cũng được hỗ trợ.",
|
||||
"blacklisteAddresses": "Địa chỉ mail trong danh sách đen",
|
||||
@@ -721,14 +759,14 @@
|
||||
"deniedInfo": "Kết nối từ IP {{ remote.ip }} bị từ chối. Lý do: {{ details.message || details.reason }}",
|
||||
"deliveredInfo": "Đã gửi mail cho {{ rcptTo | prettyEmailAddresses }} từ {{ mailFrom | prettyEmailAddresses }}",
|
||||
"receivedInfo": "Đã lưu mail từ {{ mailFrom | prettyEmailAddresses }} vào hộp thư {{ rcptTo | prettyEmailAddresses }}",
|
||||
"outboundInfo": "Mail đã được xếp vào hàng để gửi đến {{ rcptTo | prettyEmailAddresses }} từ {{ mailFrom | prettyEmailAddresses }}",
|
||||
"outboundInfo": "Mail đã xếp vào hàng để gửi đến {{ rcptTo | prettyEmailAddresses }} từ {{ mailFrom | prettyEmailAddresses }}",
|
||||
"inboundInfo": "Nhận mail từ {{ mailFrom | prettyEmailAddresses }} đến {{ rcptTo | prettyEmailAddresses }}. Có phải spam không: {{ details.spamStatus.indexOf('Yes,') === 0 ? 'Yes' : 'No' }}",
|
||||
"deferredInfo": "Không gửi được mail cho {{ rcptTo | prettyEmailAddresses }}. {{ details.message || details.reason }}. Sẽ thử lại tự động trong vòng {{ details.delay }} giây nữa.",
|
||||
"bounceInfo": "Bị trả về {{ mailFrom | prettyEmailAddresses }} cho email gửi cho {{ rcptTo | prettyEmailAddresses }}. Lý do: {{ details.message || details.reason }}",
|
||||
"deferredInfo": "Không gửi được mail cho {{ rcptTo | prettyEmailAddresses }}. {{ details.message || details.reason }}. Sẽ tự động thử lại sau {{ details.delay }} giây nữa.",
|
||||
"bounceInfo": "Gửi trả về {{ mailFrom | prettyEmailAddresses }} cho email gửi đến {{ rcptTo | prettyEmailAddresses }}. Lý do: {{ details.message || details.reason }}",
|
||||
"spamFilterTrained": "Bộ lọc spam đã được rèn giũa thêm",
|
||||
"bounce": "Bị trả về",
|
||||
"denied": "Bị từ chối",
|
||||
"queued": "Xếp vào hàng",
|
||||
"queued": "Xếp hàng",
|
||||
"outgoing": "Gửi mail ra",
|
||||
"incoming": "Nhận mail vào",
|
||||
"deferred": "Trì hoãn lại"
|
||||
@@ -835,10 +873,12 @@
|
||||
},
|
||||
"settings": {
|
||||
"registryConfig": {
|
||||
"provider": "Nhà cung cấp docker registry"
|
||||
"provider": "Nhà cung cấp docker registry",
|
||||
"providerOther": "Khác",
|
||||
"providerDisabled": "Đã tắt"
|
||||
},
|
||||
"language": {
|
||||
"description": "Ngôn ngữ mặc định cho Cloudron được cài đặt ở đây. Ngôn ngữ này sẽ được dùng trong các email trao đổi qua lại như email mời người dùng hay cài đặt lại mật khẩu. Mỗi người dùng vẫn có thể chỉnh ngôn ngữ thuận tiện hơn cho mình trong dashboard cá nhân của từng người.",
|
||||
"description": "Ngôn ngữ mặc định cho Cloudron được cài đặt ở đây. Ngôn ngữ này sẽ được dùng trong các email trao đổi như mời người dùng hay đặt lại mật khẩu. Mỗi người dùng có thể tuỳ chỉnh ngôn ngữ mình thích trong dashboard.",
|
||||
"title": "Ngôn ngữ"
|
||||
},
|
||||
"updateDialog": {
|
||||
@@ -867,7 +907,8 @@
|
||||
"setupSubscriptionAction": "Cài đặt gói đăng ký ngay",
|
||||
"subscriptionRequired": "Tính năng này chỉ có trong gói trả phí.",
|
||||
"description": "Cloudron có thể tải hình ảnh về và cài đặt <a href=\"{{ customAppsLink }}\" target=\"_blank\">những app tuỳ chỉnh </a> từ nơi lưu trữ docker registry cá nhân.",
|
||||
"title": "Docker registry cá nhân"
|
||||
"title": "Docker registry cá nhân",
|
||||
"serverNotSet": "Chưa cài đặt"
|
||||
},
|
||||
"privateDockerRegistryDialog": {
|
||||
"passwordToken": "Mật khẩu/Mật mã",
|
||||
@@ -875,7 +916,7 @@
|
||||
"title": "Cấu hình registry cá nhân"
|
||||
},
|
||||
"updates": {
|
||||
"checkForUpdatesAction": "Kiểm tra phiên bản cập nhật mới",
|
||||
"checkForUpdatesAction": "Kiểm tra cập nhật",
|
||||
"stopUpdateAction": "Dừng cập nhật",
|
||||
"updateAvailableAction": "Có phiên bản cập nhật mới",
|
||||
"changeScheduleAction": "Thay đổi lịch cập nhật",
|
||||
@@ -1055,8 +1096,8 @@
|
||||
"netcupApiKey": "Key API",
|
||||
"netcupApiPassword": "Mật khẩu API",
|
||||
"netcupCustomerNumber": "Số khách hàng",
|
||||
"mastodonHostname": "Vị trí server của Mastodon",
|
||||
"matrixHostname": "Vị trí server của Matrix",
|
||||
"mastodonHostname": "Vị trí server Mastodon",
|
||||
"matrixHostname": "Vị trí server Matrix",
|
||||
"fallbackCertCertificatePlaceholder": "Chứng chỉ số",
|
||||
"fallbackCertKeyPlaceholder": "Mã chứng chỉ số",
|
||||
"fallbackCertCustomCertInfo": "<a href=\"{{ customCertLink }}\" target=\"_blank\">Chứng chỉ số wildcard tuỳ chỉnh này</a> sẽ được dùng cho tất cả app trên tên miền này. Nếu CCS không được nhập vào, một CCS tự ký sẽ được tự động tạo ra.",
|
||||
@@ -1106,22 +1147,28 @@
|
||||
},
|
||||
"renewCerts": {
|
||||
"showLogsAction": "Hiển thị log",
|
||||
"renewAllAction": "Gia hạn tất cả chứng chỉ số",
|
||||
"renewAllAction": "Gia hạn tất cả CCS",
|
||||
"description": "Cloudron gia hạn tự động chứng chỉ số của Let’s Encrypt. Sử dụng lựa chọn này để kích hoạt lệnh gia hạn ngay lập tức.",
|
||||
"title": "Gia hạn chứng chỉ số"
|
||||
},
|
||||
"title": "Tên miền & Chứng chỉ số",
|
||||
"title": "Tên miền & CCS",
|
||||
"tooltipRemove": "Xoá tên miền",
|
||||
"tooltipEdit": "Chỉnh tên miền",
|
||||
"provider": "Nhà cung cấp",
|
||||
"domain": "Tên miền",
|
||||
"addDomain": "Thêm tên miền"
|
||||
"addDomain": "Thêm tên miền",
|
||||
"syncDns": {
|
||||
"title": "Đồng bộ DNS",
|
||||
"description": "Lựa chọn này sẽ cấp lại các bản ghi DNS cho app và email cho tất cả tên miền.",
|
||||
"syncAction": "Đồng bộ DNS",
|
||||
"showLogsAction": "Hiển thị log"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"appInfo": {
|
||||
"sso": "App này được cài đặt để xác minh người dùng bằng Thư mục ngừoi dùng Cloudron. Người dùng Cloudron có thể đăng nhập và sử dụng được ngay.",
|
||||
"ssoEmail": "App này được cài đặt cho phép tất cả người dùng với một hộp thư trên Cloudron này. Hãy đăng nhập với email và mật khẩu trên Cloudron để truy cập vào hộp thư.",
|
||||
"package": "Gói đóng gói",
|
||||
"package": "Bản đóng gói",
|
||||
"customAppUpdateWarning": "Đây là một app tuỳ chỉnh không có trên Cửa hàng app và sẽ không nhận được các bản cập nhật mới. Xem phần <a target=\"_blank\" href=\"{{ docsLink }}\">Hướng dẫn</a> để biết cách cập nhật app tuỳ chỉnh.",
|
||||
"firstTimeTitle": "Lần sử dụng đầu tiên",
|
||||
"firstTimeCollapseHeader": "Hướng dẫn cho lần cài đặt đầu tiên",
|
||||
@@ -1195,7 +1242,7 @@
|
||||
"info": {
|
||||
"updateAvailableAction": "Có phiên bản cập nhật mới",
|
||||
"customAppUpdateInfo": "Phiên bản mới không có sẵn cho các app tuỳ chỉnh",
|
||||
"checkForUpdatesAction": "Kiểm tra phiên bản mới",
|
||||
"checkForUpdatesAction": "Kiểm tra cập nhật",
|
||||
"lastUpdated": "Lần cuối cập nhật",
|
||||
"packageVersion": "Phiên bản đóng gói",
|
||||
"appId": "ID của app",
|
||||
@@ -1222,8 +1269,13 @@
|
||||
"from": {
|
||||
"saveAction": "Lưu",
|
||||
"mailboxPlaceholder": "Để trống để dùng giá trị mặc định của hệ thống",
|
||||
"description": "Lựa chọn này cài đặt tên địa chỉ mà app sẽ gửi mail ra. App này đã được cài đặt để gửi mail trong phần cài đặt {{ domain }}'s <a href=\"{{ domainConfigLink }}\">Mail gửi ra</a>.",
|
||||
"title": "Địa chỉ mail GỬI TỪ (FROM)"
|
||||
"description": "Lựa chọn này cài đặt tên địa chỉ mà app sẽ gửi mail ra. App này đã được cài đặt để gửi mail trong phần cài đặt {{ domain }}'s <a href=\"\\{{ domainConfigLink }}\\\">Mail gửi ra</a>.",
|
||||
"title": "Địa chỉ mail GỬI TỪ (FROM)",
|
||||
"enable": "Dùng Mail Cloudron để gửi mail",
|
||||
"description2": "Khi bật, app sẽ được cấu hình để gửi mail qua mail server nội bộ bằng địa chỉ email này. Mail server nội bộ sẽ dùng phần cài đặt <a href=\"{{ domainConfigLink }}\">Mail gửi ra</a> của {{ domain }} để gửi mail. Khi tắt, bạn có thể tuỳ chỉnh cài đặt mail trong app.",
|
||||
"disable": "Không cài đặt mail",
|
||||
"enableDescription": "App được cấu hình để gửi mail bằng địa chỉ email sau và theo cài đặt phần <a href=\\\"{{ domainConfigLink }}\\\">Mail gửi ra</a> của {{ domain }}.",
|
||||
"disableDescription": "Các cài đặt email của app chưa được chỉnh. Bạn có thể tuỳ chỉnh trong app."
|
||||
}
|
||||
},
|
||||
"graphs": {
|
||||
@@ -1232,9 +1284,9 @@
|
||||
"30d": "30 ngày trước",
|
||||
"7d": "7 ngày trước",
|
||||
"24h": "24 tiếng trước",
|
||||
"12h": "12 tiếng trước"
|
||||
},
|
||||
"selectPeriod": "Chọn giai đoạn {{ period }}"
|
||||
"12h": "12 tiếng trước",
|
||||
"6h": "6 tiếng"
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"mounts": {
|
||||
@@ -1376,6 +1428,9 @@
|
||||
"uninstallAction": "Gỡ cài đặt",
|
||||
"description": "Lựa chọn này sẽ ngay lập tức gỡ app <b>{{ app }}</b> và xoá toàn bộ dữ liệu trong đó.",
|
||||
"title": "Gỡ cài đặt {{ app }}"
|
||||
},
|
||||
"stopDialog": {
|
||||
"title": "Chắc chắn dừng app {{ app }}?"
|
||||
}
|
||||
},
|
||||
"volumes": {
|
||||
@@ -1386,7 +1441,7 @@
|
||||
"removeVolumeDialog": {
|
||||
"removeAction": "Xoá",
|
||||
"description": "Lựa chọn này sẽ xoá volume <code>{{ volume }}</code>. Dữ liệu trong đường dẫn host sẽ không được xoá.",
|
||||
"title": "Chắc chắn xoá volume {{ volume }} ?"
|
||||
"title": "Chắc chắn xoá volume này {{ volume }} ?"
|
||||
},
|
||||
"addVolumeDialog": {
|
||||
"addAction": "Thêm volume",
|
||||
@@ -1421,7 +1476,8 @@
|
||||
"it": "Tiếng Ý",
|
||||
"fr": "Tiếng Pháp",
|
||||
"de": "Tiếng Đức",
|
||||
"en": "Tiếng Anh"
|
||||
"en": "Tiếng Anh",
|
||||
"es": "Tiếng Tây Ban Nha"
|
||||
},
|
||||
"passwordResetEmail": {
|
||||
"subject": "[<%= cloudron %>] Đặt lại mật khẩu",
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"stateFilterHeader": "所有状态",
|
||||
"tagsFilterHeader": "标签:{{ tags }}",
|
||||
"tagsFilterHeaderAll": "所有标签",
|
||||
"domainsFilterHeader": "所有域名"
|
||||
"domainsFilterHeader": "所有域名",
|
||||
"groupsFilterHeader": "选择组"
|
||||
},
|
||||
"profile": {
|
||||
"changeEmail": {
|
||||
@@ -102,7 +103,9 @@
|
||||
"expiresAt": "过期时间",
|
||||
"description": "使用这些个人 access tokens 来进行 <a target=\"_blank\" href=\"{{ apiDocsLink }}\">Cloudron API</a> 认证",
|
||||
"noTokensPlaceholder": "没有创建 API Token",
|
||||
"revokeTokenTooltip": "吊销 Token"
|
||||
"revokeTokenTooltip": "吊销 Token",
|
||||
"neverUsed": "从不",
|
||||
"lastUsed": "最后使用"
|
||||
},
|
||||
"loginTokens": {
|
||||
"title": "登录 Token",
|
||||
@@ -206,6 +209,10 @@
|
||||
"copyConcurrencyDescription": "当备份时同时复制几个文件。",
|
||||
"copyConcurrencyDigitalOceanNote": "DigitalOcean Spaces 的上限为 20。",
|
||||
"s3LikeNote": "请不要在 S3 存储桶上设置 lifecycle 规则,因为这会导致 rsync 备份损坏。"
|
||||
},
|
||||
"check": {
|
||||
"noop": "Cloudron 备份已停用。请确保这台服务器已经使用其它方法备份。更多关于备份的信息请参考 https://docs.cloudron.io/backups/#storage-providers .",
|
||||
"sameDisk": "Cloudron 备份现在和 Cloudron 服务器在同一个硬盘上。若这块硬盘损坏,将会导致所有数据丢失。关于备份到外部存储,请见:https://docs.cloudron.io/backups/#storage-providers ."
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
@@ -243,6 +250,22 @@
|
||||
"rebootAction": "现在重启",
|
||||
"title": "确定要重启服务器?",
|
||||
"warning": "重启期间,这台服务器上的所有应用都不可用!"
|
||||
},
|
||||
"searchPlaceholder": "搜索",
|
||||
"multiselect": {
|
||||
"selected": "{{ n }} 已选中",
|
||||
"select": "选择",
|
||||
"filterPlaceholder": "过滤选项"
|
||||
},
|
||||
"prettyDate": {
|
||||
"justNow": "现在",
|
||||
"yeserday": "昨天",
|
||||
"hoursAgo": "{{ h }} 小时前",
|
||||
"daysAgo": "{{ d }} 天前",
|
||||
"weeksAgo": "{{ w }} 周前",
|
||||
"yearsAgo": "{{ y }} 年前",
|
||||
"minutesAgo": "{{ m }} 分钟前",
|
||||
"monthsAgo": "{{ m }} 个月前"
|
||||
}
|
||||
},
|
||||
"appstore": {
|
||||
@@ -269,7 +292,8 @@
|
||||
"project": "项目管理",
|
||||
"wiki": "百科",
|
||||
"vpn": "VPN",
|
||||
"sync": "文件同步"
|
||||
"sync": "文件同步",
|
||||
"federated": "联盟式"
|
||||
},
|
||||
"noAppsFound": "没有应用。",
|
||||
"appMissing": "找不到想要的应用?告诉我们。",
|
||||
@@ -336,7 +360,7 @@
|
||||
"editUserTooltip": "编辑用户",
|
||||
"removeUserTooltip": "删除用户",
|
||||
"empty": "没有用户",
|
||||
"resetPasswordTooltip": "重设密码或发送邀请链接",
|
||||
"resetPasswordTooltip": "重设密码,关闭双因素验证或发送邀请链接",
|
||||
"transferOwnershipTooltip": "转让所有权"
|
||||
},
|
||||
"groups": {
|
||||
@@ -377,7 +401,9 @@
|
||||
"bindUsername": "绑定 DN/用户名(可选)",
|
||||
"bindPassword": "绑定密码(可选)",
|
||||
"description": "Cloudron 会从外部 LDAP 或者 ActiveDirectory 服务同步用户和用户组。当验证用户时会在外部服务进行密码验证。同步需要被手动触发,无法自动进行。",
|
||||
"errorSelfSignedCert": "服务器使用的是无效的或自签名的证书。"
|
||||
"errorSelfSignedCert": "服务器使用的是无效的或自签名的证书。",
|
||||
"providerOther": "其它",
|
||||
"providerDisabled": "禁用"
|
||||
},
|
||||
"subscriptionDialog": {
|
||||
"title": "需要订阅",
|
||||
@@ -436,9 +462,15 @@
|
||||
"description": "这个用户组里有 {{ memberCount }} 名用户。确定要删除吗?"
|
||||
},
|
||||
"passwordResetDialog": {
|
||||
"title": "重设密码或给 {{ username }} 发送邀请链接",
|
||||
"title": "为 {{ username }} 重设密码或双因素验证",
|
||||
"description": "使用下面的链接来重设 {{ username }} 的密码,或者重新邀请:",
|
||||
"sendEmailLinkAction": "将链接用 Email 发送给用户"
|
||||
"sendEmailLinkAction": "将链接用 Email 发送给用户",
|
||||
"2FAIsSetup": "在此关闭用户的双因素验证。用户可以在资料页面重新设置。",
|
||||
"newLinkAction": "生成新链接",
|
||||
"resetLinkExplanation": "在此生成新的密码重设或者邀请链接。所有未使用的旧链接将会失效。",
|
||||
"no2FASetup": "这位用户未设置双因素验证。",
|
||||
"reset2FAAction": "重设双因素验证",
|
||||
"emailSent": "已发送"
|
||||
},
|
||||
"externalLdapDialog": {
|
||||
"title": "配置 LDAP"
|
||||
@@ -449,7 +481,6 @@
|
||||
"admin": "管理员",
|
||||
"owner": "超级管理员"
|
||||
},
|
||||
"searchPlaceholder": "搜索",
|
||||
"transferOwnershipDialog": {
|
||||
"transferAction": "转移所有权",
|
||||
"description": "这个操作会让选定的用户成为这个 Cloudron 的所有者和管理员,而当前用户的管理权限将会被移除。",
|
||||
@@ -491,7 +522,7 @@
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
"title": "品牌",
|
||||
"title": "个性化",
|
||||
"cloudronName": "Cloudron 名称",
|
||||
"logo": "Logo",
|
||||
"footer": {
|
||||
@@ -603,7 +634,8 @@
|
||||
"requireAdminRoleLabel": "只有管理员用户才能使用 SFTP",
|
||||
"resetToDefaults": "重置为默认选项",
|
||||
"accessControlDescription": "允许非管理员用户使用 SFTP 会允许他们接触到配置文件和密钥。对于有些应用(如 WordPress),他们也可以记录到密码。"
|
||||
}
|
||||
},
|
||||
"refresh": "刷新"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
@@ -642,7 +674,8 @@
|
||||
"usernameNotSet": "未设置",
|
||||
"configureAction": "配置仓库",
|
||||
"description": "Cloudron 可以安装从私有 Docker 仓库安装 <a href=\"{{ customAppsLink }}\" target=\"_blank\">自定义的应用</a>。",
|
||||
"setupSubscriptionAction": "现在设置订阅"
|
||||
"setupSubscriptionAction": "现在设置订阅",
|
||||
"serverNotSet": "未设置"
|
||||
},
|
||||
"privateDockerRegistryDialog": {
|
||||
"title": "私有仓库设置",
|
||||
@@ -670,6 +703,11 @@
|
||||
"language": {
|
||||
"title": "语言",
|
||||
"description": "在这里可以设置 Cloudron 的默认语言。这个设置同样会被应用于通知性的邮件,如用户邀请和密码重置。每个用户都可以单独设置自己的语言偏好,应用于自己的控制面板。"
|
||||
},
|
||||
"registryConfig": {
|
||||
"provider": "Docker 仓库服务商",
|
||||
"providerOther": "其他",
|
||||
"providerDisabled": "已停用"
|
||||
}
|
||||
},
|
||||
"support": {
|
||||
@@ -798,6 +836,12 @@
|
||||
"title": "确定要删除 {{ domain }}?",
|
||||
"description": "将会删除域名 <code>{{ domain }}</code>。",
|
||||
"removeAction": "删除"
|
||||
},
|
||||
"syncDns": {
|
||||
"syncAction": "同步 DNS",
|
||||
"showLogsAction": "显示日志",
|
||||
"title": "同步 DNS",
|
||||
"description": "此操作将会重建所有域名下应用和 Email 的 DNS 记录。"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
@@ -1067,7 +1111,8 @@
|
||||
"title": "添加邮件列表",
|
||||
"members": "列出成员",
|
||||
"membersInfo": "每个 Email 地址一行",
|
||||
"membersOnlyCheckbox": "只允许列表成员使用列表发送邮件"
|
||||
"membersOnlyCheckbox": "只允许列表成员使用列表发送邮件",
|
||||
"name": "名称"
|
||||
},
|
||||
"editMailinglistDialog": {
|
||||
"title": "编辑邮件列表 {{ name }}@{{ domain }}"
|
||||
@@ -1080,6 +1125,12 @@
|
||||
"mailboxboxDialog": {
|
||||
"groupsHeader": "用户组",
|
||||
"usersHeader": "用户"
|
||||
},
|
||||
"updateMailinglistDialog": {
|
||||
"activeCheckbox": "邮件列表已启用"
|
||||
},
|
||||
"updateMailboxDialog": {
|
||||
"activeCheckbox": "邮箱已启用"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
@@ -1196,9 +1247,14 @@
|
||||
"email": {
|
||||
"from": {
|
||||
"saveAction": "保存",
|
||||
"description": "这是此应用发送邮件时使用的地址。这个应用被配置为使用 {{ domain }} 的 <a href=\"{{ domainConfigLink }}\">出站邮件</a> 邮件设置发送邮件。",
|
||||
"description": "这是此应用发送邮件时使用的地址。这个应用被配置为使用 {{ domain }} 的 <a href=\\\"{{ domainConfigLink }}\\\"> 出站邮件</a> 邮件设置发送邮件。",
|
||||
"title": "邮件的 FROM 地址",
|
||||
"mailboxPlaceholder": "留空以使用默认值"
|
||||
"mailboxPlaceholder": "留空以使用默认值",
|
||||
"disable": "不配置邮件选项",
|
||||
"enable": "使用 Cloudron Mail 发送邮件",
|
||||
"enableDescription": "这个应用被设置为使用下列地址和 {{ domain }} 的 <a href=\\\"{{ domainConfigLink }}\\\">出站邮件</a> 设置。",
|
||||
"disableDescription": "没有为此应用配置邮件。你可以在应用内部的设置里设置邮件选项。",
|
||||
"description2": "启用后,这个应用会使用这个地址,从内置的邮件服务器发送邮件。内置的邮件服务器会使用 {{ domain }} 的 <a href=\"{{ domainConfigLink }}\">出站邮件</a>设置来发送邮件。如果禁用此选项,你可以在应用里配置邮件选项。"
|
||||
},
|
||||
"csp": {
|
||||
"title": "内容安全策略(CSP)"
|
||||
@@ -1253,7 +1309,7 @@
|
||||
},
|
||||
"appInfo": {
|
||||
"appDocsUrl": "请从<a target=\"_blank\" href=\"{{ docsUrl }}\"> {{ title }} 文档</a>中查找应用相关的信息。如果你需要更多帮助,请参考 Cloudron 的<a target=\"_blank\" href=\"{{ forumUrl }}\"> {{ title }} 论坛</a>。",
|
||||
"postInstallConfirmCheckbox": "我已知晓",
|
||||
"postInstallConfirmCheckbox": "我知道了",
|
||||
"firstTimeTitle": "首次使用",
|
||||
"firstTimeCollapseHeader": "首次使用设置指南",
|
||||
"customAppUpdateWarning": "这是一个自定义应用,并非从 App Store 安装,所以不会自动更新。关于如果更新一个自定义应用,请参考 <a target=\"_blank\" href=\"{{ docsLink }}\">文档</a>。",
|
||||
@@ -1333,14 +1389,17 @@
|
||||
"title": "域名冲突"
|
||||
},
|
||||
"graphs": {
|
||||
"selectPeriod": "选择时间范围 {{ period }}",
|
||||
"period": {
|
||||
"12h": "12 小时",
|
||||
"24h": "24 小时",
|
||||
"7d": "7 天",
|
||||
"30d": "30 天"
|
||||
"30d": "30 天",
|
||||
"6h": "6 小时"
|
||||
},
|
||||
"memoryTitle": "内存 MB (RAM + Swap)"
|
||||
},
|
||||
"stopDialog": {
|
||||
"title": "确实要停止 {{ app }}?"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
@@ -1401,7 +1460,8 @@
|
||||
"ja": "Japanese",
|
||||
"nl": "Dutch",
|
||||
"zh_Hans": "简体中文",
|
||||
"vi": "Vietnamese"
|
||||
"vi": "Vietnamese",
|
||||
"es": "西班牙语"
|
||||
},
|
||||
"volumes": {
|
||||
"title": "磁盘卷",
|
||||
|
||||
+86
-48
@@ -1,12 +1,3 @@
|
||||
<script>
|
||||
function imageErrorHandler(elem) {
|
||||
'use strict';
|
||||
|
||||
elem.src = elem.getAttribute('fallback-icon');
|
||||
elem.onerror = null; // avoid retry after default icon cannot be loaded
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Modal postinstall -->
|
||||
<div class="modal fade" id="postInstallModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
@@ -36,11 +27,11 @@
|
||||
<div class="modal fade" id="uninstallModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'app.uninstallDialog.title' | tr:{ app: (app.label || app.fqdn) } }}</h4>
|
||||
</div>
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'app.uninstallDialog.title' | tr:{ app: (app.label || app.fqdn) } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'app.uninstallDialog.description' | tr:{ app: (app.label || app.fqdn) }"></p></p>
|
||||
<p ng-bind-html="'app.uninstallDialog.description' | tr:{ app: (app.label || app.fqdn) }"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
@@ -50,13 +41,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal stop app -->
|
||||
<div class="modal fade" id="stopModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'app.stopDialog.title' | tr:{ app: (app.label || app.fqdn) } }}</h4>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="uninstall.toggleRunState()" ng-disabled="uninstall.busyRunState"><i class="fa fa-circle-notch fa-spin" ng-show="uninstall.busyRunState"></i> {{ 'app.uninstall.startStop.stopAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal domain collision -->
|
||||
<div class="modal fade" id="domainCollisionsModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'app.domainCollisionDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'app.domainCollisionDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ 'app.domainCollisionDialog.collisionListTitle' | tr }}</p>
|
||||
<ul>
|
||||
@@ -236,6 +242,11 @@
|
||||
<select class="form-control" name="region" id="inputimportBackupIonosRegion" ng-model="importBackup.endpoint" ng-options="a.value as a.name for a in ionosRegions" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'ionos-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.region }" ng-show="importBackup.provider === 'vultr-objectstorage'">
|
||||
<label class="control-label" for="inputimportBackupVultrRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputimportBackupVultrRegion" ng-model="importBackup.endpoint" ng-options="a.value as a.name for a in vultrRegions" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'vultr-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.accessKeyId }" ng-show="s3like(importBackup.provider)">
|
||||
<label class="control-label" for="inputImportBackupAccessKeyId">{{ 'backups.configureBackupStorage.s3AccessKeyId' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.accessKeyId" id="inputImportBackupAccessKeyId" name="accessKeyId" ng-disabled="importBackup.busy" ng-required="s3like(importBackup.provider)">
|
||||
@@ -406,9 +417,15 @@
|
||||
<a ng-href="{{ app | applicationLink }}" target="_blank" ng-class="{ 'hand': (app | appIsInstalledAndHealthy) }" ng-click="(app | appIsInstalledAndHealthy) && app.pendingPostInstallConfirmation && postInstallMessage.show(true)">{{ app.label || app.fqdn }} <sup ng-show="app | appIsInstalledAndHealthy"><i class="fas fa-external-link-alt" style="font-size: 12px;"></i></sup></a>
|
||||
</h1>
|
||||
<div>
|
||||
<a class="btn btn-sm btn-default" ng-href="{{ '/logs.html?appId=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.logsActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-align-left"></i></a>
|
||||
<a class="btn btn-sm btn-default" ng-href="{{ '/terminal.html?id=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.terminalActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fa fa-terminal"></i></a>
|
||||
<a class="btn btn-sm btn-default" ng-href="{{ '/filemanager.html?appId=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.filemanagerActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-folder"></i></a>
|
||||
<button class="btn btn-sm btn-default" ng-class="{ 'btn-primary': uninstall.startButton }" ng-click="uninstall.toggleRunState(true)" ng-disabled="app.taskId || app.error || app.installationState === 'pending_start' || app.installationState === 'pending_stop'" uib-tooltip="{{ uninstall.startButton ? ('app.uninstall.startStop.startAction' | tr) : ('app.uninstall.startStop.stopAction' | tr) }}" tooltip-append-to-body="true" tooltip-placement="bottom">
|
||||
<i ng-show="app.installationState === 'pending_start' || app.installationState === 'pending_stop'" class="fa fa-circle-notch fa-spin"></i>
|
||||
<i ng-hide="app.installationState === 'pending_start' || app.installationState === 'pending_stop'" class="fas" ng-class="{ 'fa-power-off': !uninstall.startButton, 'fa-play': uninstall.startButton }"></i>
|
||||
</button>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a class="btn btn-sm btn-default" ng-href="{{ '/logs.html?appId=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.logsActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-align-left"></i></a>
|
||||
<a class="btn btn-sm btn-default" ng-href="{{ '/terminal.html?id=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.terminalActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fa fa-terminal"></i></a>
|
||||
<a class="btn btn-sm btn-default" ng-href="{{ '/filemanager.html?appId=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.filemanagerActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-folder"></i></a>
|
||||
</div>
|
||||
<div class="dropdown" style="display: inline-block">
|
||||
<button class="btn btn-sm btn-default dropdown-toggle" type="button" data-toggle="dropdown" uib-tooltip="{{ 'app.docsActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom">
|
||||
<i class="fas fa-book"></i>
|
||||
@@ -839,14 +856,14 @@
|
||||
<div class="col-md-12">
|
||||
<div class="dropdown pull-right">
|
||||
<button class="btn btn-sm btn-primary dropdown-toggle" type="button" data-toggle="dropdown">
|
||||
{{ 'app.graphs.selectPeriod' | tr:{ period: graphs.periodLabel } }}
|
||||
{{ graphs.period | trKeyFromPeriod | tr }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="" ng-click="graphs.setPeriod(12, '12 hours')">{{ 'app.graphs.period.12h' | tr }}</a></li>
|
||||
<li><a href="" ng-click="graphs.setPeriod(24, '24 hours')">{{ 'app.graphs.period.24h' | tr }}</a></li>
|
||||
<li><a href="" ng-click="graphs.setPeriod(24*7, '7 days')">{{ 'app.graphs.period.7d' | tr }}</a></li>
|
||||
<li><a href="" ng-click="graphs.setPeriod(24*30, '30 days')">{{ 'app.graphs.period.30d' | tr }}</a></li>
|
||||
<li><a href="" ng-click="graphs.setPeriod(12)">{{ 12 | trKeyFromPeriod | tr }}</a></li>
|
||||
<li><a href="" ng-click="graphs.setPeriod(24)">{{ 24 | trKeyFromPeriod | tr }}</a></li>
|
||||
<li><a href="" ng-click="graphs.setPeriod(24*7)">{{ 24*7 | trKeyFromPeriod | tr }}</a></li>
|
||||
<li><a href="" ng-click="graphs.setPeriod(24*30)">{{ 24*30 | trKeyFromPeriod | tr }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<label style="margin-top: 10px;">{{ 'app.graphs.memoryTitle' | tr }}</label>
|
||||
@@ -858,38 +875,59 @@
|
||||
<div class="card" ng-show="view === 'email'">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<label class="control-label">{{ 'app.email.from.title' | tr }}</label>
|
||||
<p ng-bind-html="'app.email.from.description' | tr:{ domain: app.domain, domainConfigLink: ('/#/email/' + app.domain) }"></p>
|
||||
<label class="control-label">{{ 'app.email.from.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#mail-from-address" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
|
||||
<form role="form" name="emailForm" ng-submit="email.submit()" autocomplete="off">
|
||||
<!-- recvmail currently only works with cloudron email -->
|
||||
<div class="form-group" ng-class="{ 'has-error': emailForm.$dirty && email.error.mailboxName }">
|
||||
<div ng-show="email.error.mailboxName">{{ email.error.mailboxName }}</div>
|
||||
<div class="radio" ng-show="app.manifest.addons.sendmail.optional">
|
||||
<label>
|
||||
<input type="radio" ng-model="email.enableMailbox" value="1"> {{ 'app.email.from.enable' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="input-group form-inline" ng-class="{ 'has-error': !emailForm.mailboxName.$dirty && email.error.mailboxName }">
|
||||
<input type="text" class="form-control" name="mailboxName" placeholder="{{ 'app.email.from.mailboxPlaceholder' | tr }}" ng-model="email.mailboxName">
|
||||
<div ng-style="{ 'padding-left': app.manifest.addons.sendmail.optional ? '20px' : '0' }">
|
||||
<p ng-bind-html="'app.email.from.enableDescription' | tr:{ domain: app.domain, domainConfigLink: ('/#/email/' + app.domain) }"></p>
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<span>{{ '@' + email.mailboxDomain.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="domain in domains">
|
||||
<a href="" ng-click="email.mailboxDomain = domain">{{ domain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<form role="form" name="emailForm" ng-submit="email.submit()" autocomplete="off">
|
||||
<fieldset ng-disabled="email.enableMailbox === '0'">
|
||||
<div class="form-group" ng-class="{ 'has-error': emailForm.$dirty && email.error.mailboxName }">
|
||||
<div ng-show="email.error.mailboxName">{{ email.error.mailboxName }}</div>
|
||||
|
||||
<div class="input-group form-inline" ng-class="{ 'has-error': !emailForm.mailboxName.$dirty && email.error.mailboxName }">
|
||||
<input type="text" class="form-control" name="mailboxName" placeholder="{{ 'app.email.from.mailboxPlaceholder' | tr }}" ng-model="email.mailboxName">
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<span>{{ '@' + email.mailboxDomain.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="domain in domains">
|
||||
<a href="" ng-click="email.mailboxDomain = domain">{{ domain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="(email.currentMailboxDomainName === email.mailboxDomain.domain && email.currentMailboxName === email.mailboxName) || email.busy || app.error || app.taskId"/>
|
||||
</form>
|
||||
</fieldset>
|
||||
<input class="ng-hide" type="submit" ng-disabled="(email.currentMailboxDomainName === email.mailboxDomain.domain && email.currentMailboxName === email.mailboxName) || email.busy || app.error || app.taskId"/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="radio" ng-show="app.manifest.addons.sendmail.optional">
|
||||
<label>
|
||||
<input type="radio" ng-model="email.enableMailbox" value="0"> {{ 'app.email.from.disable' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="padding-left: 20px;">
|
||||
<p ng-show="app.manifest.addons.sendmail.optional">{{ 'app.email.from.disableDescription' | tr }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="email.submit()" ng-disabled="(email.currentMailboxDomainName === email.mailboxDomain.domain && email.currentMailboxName === email.mailboxName) || email.busy || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">
|
||||
<br/>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="email.submit()" ng-disabled="(app.enableMailbox === email.enableMailbox && email.currentMailboxDomainName === email.mailboxDomain.domain && email.currentMailboxName === email.mailboxName) || email.busy || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="email.busy"></i> {{ 'app.email.from.saveAction' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
+34
-11
@@ -87,22 +87,29 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
{ name: 'DE', value: 'https://s3-de-central.profitbricks.com', region: 's3-de-central' }, // default
|
||||
];
|
||||
|
||||
$scope.vultrRegions = [
|
||||
{ name: 'New Jersey', value: 'https://ewr1.vultrobjects.com', region: 'us-east-1' }, // default
|
||||
];
|
||||
|
||||
$scope.storageProvider = [
|
||||
{ name: 'Amazon S3', value: 's3' },
|
||||
{ name: 'Backblaze B2 (S3 API)', value: 'backblaze-b2' },
|
||||
{ name: 'CIFS Mount', value: 'cifs' },
|
||||
// { name: 'CIFS Mount', value: 'cifs' },
|
||||
{ name: 'DigitalOcean Spaces', value: 'digitalocean-spaces' },
|
||||
{ name: 'Exoscale SOS', value: 'exoscale-sos' },
|
||||
// { name: 'EXT4', value: 'ext4' },
|
||||
{ name: 'Filesystem', value: 'filesystem' },
|
||||
// { name: 'Filesystem (Mountpoint)', value: 'mountpoint' },
|
||||
{ name: 'Google Cloud Storage', value: 'gcs' },
|
||||
{ name: 'IONOS (Profitbricks)', value: 'ionos-objectstorage' },
|
||||
{ name: 'Linode Object Storage', value: 'linode-objectstorage' },
|
||||
{ name: 'Minio', value: 'minio' },
|
||||
{ name: 'NFS Mount', value: 'nfs' },
|
||||
// { name: 'NFS Mount', value: 'nfs' },
|
||||
{ name: 'OVH Object Storage', value: 'ovh-objectstorage' },
|
||||
{ name: 'S3 API Compatible (v4)', value: 's3-v4-compat' },
|
||||
{ name: 'Scaleway Object Storage', value: 'scaleway-objectstorage' },
|
||||
{ name: 'SSHFS Mount', value: 'sshfs' },
|
||||
// { name: 'SSHFS Mount', value: 'sshfs' },
|
||||
{ name: 'Vultr Object Storage', value: 'vultr-objectstorage' },
|
||||
// { name: 'No-op (Only for testing)', value: 'noop' },
|
||||
{ name: 'Wasabi', value: 'wasabi' }
|
||||
];
|
||||
@@ -650,13 +657,11 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
error: {},
|
||||
|
||||
period: 12, // set as 12 because disk graphs is only collected twice a day
|
||||
periodLabel: '12 hours',
|
||||
memoryChart: null,
|
||||
diskChart: null,
|
||||
|
||||
setPeriod: function (hours, label) {
|
||||
setPeriod: function (hours) {
|
||||
$scope.graphs.period = hours;
|
||||
$scope.graphs.periodLabel = label;
|
||||
$scope.graphs.show();
|
||||
},
|
||||
|
||||
@@ -749,6 +754,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
busy: false,
|
||||
error: {},
|
||||
|
||||
enableMailbox: true,
|
||||
mailboxName: '',
|
||||
mailboxDomain: '',
|
||||
currentMailboxName: '',
|
||||
@@ -759,6 +765,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
|
||||
$scope.emailForm.$setPristine();
|
||||
$scope.email.error = {};
|
||||
$scope.email.enableMailbox = app.enableMailbox ? '1' : '0';
|
||||
$scope.email.mailboxName = app.mailboxName || '';
|
||||
$scope.email.mailboxDomain = $scope.domains.filter(function (d) { return d.domain === app.mailboxDomain; })[0];
|
||||
$scope.email.currentMailboxName = app.mailboxName || '';
|
||||
@@ -769,7 +776,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
$scope.email.error = {};
|
||||
$scope.email.busy = true;
|
||||
|
||||
Client.configureApp($scope.app.id, 'mailbox', { mailboxName: $scope.email.mailboxName || null, mailboxDomain: $scope.email.mailboxDomain.domain }, function (error) {
|
||||
Client.configureApp($scope.app.id, 'mailbox', { enable: $scope.email.enableMailbox === '1', mailboxName: $scope.email.mailboxName || null, mailboxDomain: $scope.email.mailboxDomain.domain }, function (error) {
|
||||
if (error && error.statusCode === 400) {
|
||||
$scope.email.busy = false;
|
||||
$scope.email.error.mailboxName = error.message;
|
||||
@@ -784,6 +791,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
if (error) return Client.error(error);
|
||||
|
||||
// when the mailboxName is 'reset', this will fill it up with the default again
|
||||
$scope.email.enableMailbox = $scope.app.enableMailbox ? '1' : '0';
|
||||
$scope.email.mailboxName = $scope.app.mailboxName || '';
|
||||
$scope.email.mailboxDomain = $scope.domains.filter(function (d) { return d.domain === $scope.app.mailboxDomain; })[0];
|
||||
$scope.email.currentMailboxName = $scope.app.mailboxName || '';
|
||||
@@ -945,7 +953,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' ||
|
||||
provider === 'exoscale-sos' || provider === 'digitalocean-spaces' ||
|
||||
provider === 'scaleway-objectstorage' || provider === 'wasabi' || provider === 'backblaze-b2' ||
|
||||
provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'ionos-objectstorage';
|
||||
provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'ionos-objectstorage' || provider === 'vultr-objectstorage';
|
||||
};
|
||||
|
||||
$scope.mountlike = function (provider) {
|
||||
@@ -1033,6 +1041,9 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
} else if (backupConfig.provider === 'ionos-objectstorage') {
|
||||
backupConfig.region = $scope.ionosRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'vultr-objectstorage') {
|
||||
backupConfig.region = $scope.vultrRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'digitalocean-spaces') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
}
|
||||
@@ -1142,7 +1153,14 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
busyRunState: false,
|
||||
startButton: false,
|
||||
|
||||
toggleRunState: function () {
|
||||
toggleRunState: function (confirmStop) {
|
||||
if (confirmStop && $scope.app.runState !== RSTATES.STOPPED) {
|
||||
$('#stopModal').modal('show');
|
||||
return;
|
||||
}
|
||||
|
||||
$('#stopModal').modal('hide');
|
||||
|
||||
var func = $scope.app.runState === RSTATES.STOPPED ? Client.startApp : Client.stopApp;
|
||||
$scope.uninstall.busyRunState = true;
|
||||
|
||||
@@ -1364,7 +1382,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
});
|
||||
}
|
||||
|
||||
if (errorState === ISTATES.PENDING_RESTORE) {
|
||||
if (errorState === ISTATES.PENDING_RESTORE || errorState === ISTATES.PENDING_IMPORT) {
|
||||
Client.getAppBackups($scope.app.id, function (error, backups) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
@@ -1410,7 +1428,12 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
|
||||
// this also happens for import faliures. this UI can only show backup listing. use CLI for arbit id/config
|
||||
case ISTATES.PENDING_RESTORE:
|
||||
repairFunc = Client.restoreApp.bind(null, $scope.app.id, $scope.repair.backupId);
|
||||
case ISTATES.PENDING_IMPORT:
|
||||
if ($scope.repair.backups.length === 0) { // this can happen when you give some invalid backup via CLI and restore via UI
|
||||
repairFunc = Client.repairApp.bind(null, $scope.app.id, {}); // this will trigger a re-install
|
||||
} else {
|
||||
repairFunc = Client.restoreApp.bind(null, $scope.app.id, $scope.repair.backupId);
|
||||
}
|
||||
break;
|
||||
|
||||
case ISTATES.PENDING_UNINSTALL:
|
||||
|
||||
+12
-60
@@ -31,53 +31,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal app info -->
|
||||
<div class="modal fade" id="appInfoModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<img ng-src="{{appInfo.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-info-icon" style="padding-right: 10px;"/>
|
||||
<h4 style="margin-top: 0;">
|
||||
{{ appInfo.app.manifest.title }}
|
||||
<br/>
|
||||
<span class="text-small text-muted">{{ 'app.appInfo.package' | tr }} <a ng-href="/#/appstore/{{appInfo.app.manifest.id}}?version={{appInfo.app.manifest.version}}">v{{ appInfo.app.manifest.version }}</a></span>
|
||||
<br/>
|
||||
<span class="text-small text-muted" ng-show="appInfo.app.upstreamVersion">App v{{ appInfo.app.upstreamVersion }}</a></span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-hide="appInfo.app.appStoreId" ng-bind-html="'app.appInfo.customAppUpdateWarning' | tr:{ docsLink: 'https://docs.cloudron.io/custom-apps/tutorial/' }"></p>
|
||||
<p ng-show="appInfo.app.manifest.documentationUrl" ng-bind-html="'app.appInfo.appDocsUrl' | tr:{ docsUrl: appInfo.app.manifest.documentationUrl, title: appInfo.app.manifest.title, forumUrl: (appInfo.app.manifest.forumUrl || 'https://forum.cloudron.io') }"></p>
|
||||
|
||||
<p ng-show="appInfo.app.manifest.addons.email">{{ 'app.appInfo.ssoEmail' | tr }}</p>
|
||||
<p ng-show="appInfo.app.sso && !appInfo.app.manifest.addons.email">{{ 'app.appInfo.sso' | tr }}</p>
|
||||
|
||||
<a ng-show="appInfo.app.manifest.postInstallMessage" href="" data-toggle="collapse" data-parent="#accordion" data-target="#appinfoPostinstallMessage">
|
||||
<i class="fa fa-angle-right"></i> {{ 'app.appInfo.firstTimeCollapseHeader' | tr }}
|
||||
</a>
|
||||
|
||||
<div id="appinfoPostinstallMessage" class="panel-collapse collapse">
|
||||
<br/>
|
||||
<div ng-bind-html="appInfo.app.manifest.postInstallMessage | markdown2html"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function imageErrorHandler(elem) {
|
||||
'use strict';
|
||||
|
||||
elem.src = elem.getAttribute('fallback-icon');
|
||||
elem.onerror = null; // avoid retry after default icon cannot be loaded
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="content content-large">
|
||||
|
||||
<!-- Workaround for select-all issue, see commit message -->
|
||||
@@ -118,13 +71,13 @@
|
||||
|
||||
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
|
||||
<div class="app-grid">
|
||||
<div class="grid-item" ng-repeat="app in installedApps | selectedGroupAccessFilter:selectedGroup | selectedStateFilter:selectedState | selectedTagFilter:selectedTags | selectedDomainFilter:selectedDomain | appSearchFilter:appSearch | orderBy:'fqdn'">
|
||||
<a ng-href="{{ app | applicationLink }}" ng-click="user.isAtLeastAdmin && (((app | installError) === true || (app | installationActive) === true) && showAppConfigure(app, 'repair')) || ((app | appIsInstalledAndHealthy) && app.pendingPostInstallConfirmation && appPostInstallConfirm.show(app))" target="_blank" ng-class="{ 'hand': (app | appIsInstalledAndHealthy) || (app | installError) || (app | installationActive)}">
|
||||
<div class="highlight grid-item-content" uib-tooltip="{{ app.fqdn }}" tooltip-class="long nowrap">
|
||||
<div class="grid-item" ng-repeat="app in installedApps | selectedGroupAccessFilter:selectedGroup | selectedStateFilter:selectedState | selectedTagFilter:selectedTags | selectedDomainFilter:selectedDomain | appSearchFilter:appSearch | orderBy:'fqdn'" ng-class="{ 'admin-action': app.manifest.configurePath && (app | applicationLink) }">
|
||||
<div class="grid-item-content" uib-tooltip="{{ app.fqdn }}">
|
||||
<a ng-show="user.isAtLeastAdmin" ng-href="#/app/{{ app.id}}/display" class="btn btn-lg btn-default grid-item-action"><i class="fas fa-cog"></i></a>
|
||||
<a ng-href="{{ app | applicationLink }}" ng-click="user.isAtLeastAdmin && (((app | installError) === true || (app | installationActive) === true) && showAppConfigure(app, 'repair')) || ((app | appIsInstalledAndHealthy) && app.pendingPostInstallConfirmation && appPostInstallConfirm.show(app))" target="_blank" ng-class="{ 'hand': (app | appIsInstalledAndHealthy) || (app | installError) || (app | installationActive)}">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-center" style="padding-left: 5px; padding-right: 5px;">
|
||||
<br/>
|
||||
<img ng-src="{{ app.iconUrl || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)" class="app-icon"/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,7 +85,7 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-center">
|
||||
<div class="grid-item-top-title" data-fittext>{{ app.label || app.location || app.fqdn }}</div>
|
||||
<div class="text-muted status" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden" uib-tooltip="{{ app | appProgressMessage }}" tooltip-class="long nowrap">
|
||||
<div class="text-muted status" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden" uib-tooltip="{{ app | appProgressMessage }}">
|
||||
{{ app | installationStateLabel }}
|
||||
</div>
|
||||
<div class="status" ng-style="{ 'visibility': user.isAtLeastAdmin && (app | installationActive) ? 'visible' : 'hidden' }">
|
||||
@@ -142,21 +95,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-item-actions" ng-show="user.isAtLeastAdmin">
|
||||
<a ng-href="#/app/{{ app.id}}/display" uib-tooltip="{{ 'apps.configActionTooltip' | tr }}" tooltip-placement="right" tooltip-class="grid-items-action-tooltip"><i class="fas fa-cogs"></i></a>
|
||||
<a ng-href="{{ '/logs.html?appId=' + app.id }}" target="_blank" uib-tooltip="{{ 'apps.logsActionTooltip' | tr }}" tooltip-placement="right" tooltip-class="grid-items-action-tooltip"><i class="fas fa-align-left"></i></a>
|
||||
<a ng-href="" class="hand" ng-click="appInfo.show(app)" uib-tooltip="{{ 'apps.infoActionTooltip' | tr }}" tooltip-placement="right" tooltip-class="grid-items-action-tooltip"><i class="fas fa-info-circle"></i></a>
|
||||
<a ng-href="{{ app | applicationLink }}{{ app.manifest.configurePath }}" target="_blank" ng-show="app.manifest.configurePath && (app | applicationLink)" uib-tooltip="{{ 'apps.adminPageActionTooltip' | tr }}" tooltip-placement="right" tooltip-class="grid-items-action-tooltip"><i class="fas fa-external-link-alt"></i></a>
|
||||
<div class="usermanagement-indicator" ng-hide="user.isAtLeastAdmin">
|
||||
<i class="fas fa-user" ng-show="app.ssoAuth && !app.manifest.addons.email" uib-tooltip="Log in with Cloudron username and password" tooltip-placement="right"></i>
|
||||
<i class="far fa-user" ng-show="!app.ssoAuth && !app.manifest.addons.email" uib-tooltip="Not using Cloudron's usermanagement" tooltip-placement="right"></i>
|
||||
<i class="fas fa-envelope" ng-show="app.manifest.addons.email" uib-tooltip="Log in with Cloudron mailbox email and password" tooltip-placement="right"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- we check the version here because the box updater does not know when an app gets updated -->
|
||||
<div class="app-update-badge" ng-click="showAppConfigure(app, 'updates')" ng-show="config.update[app.id].manifest.version && config.update[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
|
||||
<i class="fa fa-arrow-up fa-inverse"></i>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -73,21 +73,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$translat
|
||||
}
|
||||
};
|
||||
|
||||
$scope.appInfo = {
|
||||
app: {},
|
||||
message: '',
|
||||
|
||||
show: function (app) {
|
||||
$scope.appInfo.app = app;
|
||||
$scope.appInfo.message = app.manifest.postInstallMessage;
|
||||
|
||||
$('#appinfoPostinstallMessage').collapse('hide');
|
||||
$('#appInfoModal').modal('show');
|
||||
|
||||
return false; // prevent propagation and default
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showAppConfigure = function (app, view) {
|
||||
$location.path('/app/' + app.id + '/' + view);
|
||||
};
|
||||
|
||||
@@ -272,7 +272,7 @@
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="category in categories"><a href="" ng-click="showCategory(category.id);"><i class="{{ category.icon }} fa-fw"></i> {{ category.label }}</a></li>
|
||||
<li ng-repeat="category in categories | orderBy:'label'"><a href="" ng-click="showCategory(category.id);"><i class="{{ category.icon }} fa-fw"></i> {{ category.label }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<input type="text" id="appstoreSearch" class="form-control" placeholder="{{ 'appstore.searchPlaceholder' | tr }}" ng-model="searchString" ng-change="search()" autofocus>
|
||||
|
||||
@@ -46,22 +46,22 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
{ id: 'analytics', icon: 'fa fa-chart-line', label: 'Analytics'},
|
||||
{ id: 'blog', icon: 'fa fa-font', label: 'Blog'},
|
||||
{ id: 'chat', icon: 'fa fa-comments', label: 'Chat'},
|
||||
{ id: 'git', icon: 'fa fa-code-branch', label: 'Code Hosting'},
|
||||
{ id: 'crm', icon: 'fab fa-connectdevelop', label: 'CRM'},
|
||||
{ id: 'document', icon: 'fa fa-file-word', label: 'Documents'},
|
||||
{ id: 'email', icon: 'fa fa-envelope', label: 'Email'},
|
||||
{ id: 'federated', icon: 'fa fa-project-diagram', label: 'Federated'},
|
||||
{ id: 'sync', icon: 'fa fa-sync-alt', label: 'File Sync'},
|
||||
{ id: 'finance', icon: 'fa fa-hand-holding-usd', label: 'Finance'},
|
||||
{ id: 'forum', icon: 'fa fa-users', label: 'Forum'},
|
||||
{ id: 'gallery', icon: 'fa fa-images', label: 'Gallery'},
|
||||
{ id: 'game', icon: 'fa fa-gamepad', label: 'Games'},
|
||||
{ id: 'git', icon: 'fa fa-code-branch', label: 'Code Hosting'},
|
||||
{ id: 'hosting', icon: 'fa fa-server', label: 'Web Hosting'},
|
||||
{ id: 'learning', icon: 'fas fa-graduation-cap', label: 'Learning'},
|
||||
{ id: 'media', icon: 'fas fa-photo-video', label: 'Media'},
|
||||
{ id: 'notes', icon: 'fa fa-sticky-note', label: 'Notes'},
|
||||
{ id: 'project', icon: 'fas fa-project-diagram', label: 'Project Management'},
|
||||
{ id: 'sync', icon: 'fa fa-sync-alt', label: 'File Sync'},
|
||||
{ id: 'vpn', icon: 'fa fa-user-secret', label: 'VPN'},
|
||||
{ id: 'hosting', icon: 'fa fa-server', label: 'Web Hosting'},
|
||||
{ id: 'wiki', icon: 'fab fa-wikipedia-w', label: 'Wiki'},
|
||||
];
|
||||
|
||||
@@ -76,7 +76,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
});
|
||||
|
||||
$scope.categoryButtonLabel = function (category) {
|
||||
var categoryLabel = $translate.instant('appstore.categoryLabel')
|
||||
var categoryLabel = $translate.instant('appstore.categoryLabel');
|
||||
|
||||
if (category === 'new') return categoryLabel;
|
||||
if (category === 'recent') return categoryLabel;
|
||||
|
||||
+66
-13
@@ -130,11 +130,59 @@
|
||||
<p class="has-error">{{ 'backups.configureBackupStorage.noopNote' | tr }}</p>
|
||||
</div>
|
||||
|
||||
<!-- SSHFS/CIFS/NFS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.mountPoint || (configureBackupForm.mountPoint.$dirty && !configureBackup.mountPoint) }" ng-show="mountlike(configureBackup.provider)">
|
||||
<!-- mountpoint -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.mountPoint || (configureBackupForm.mountPoint.$dirty && !configureBackup.mountPoint) }" ng-show="configureBackup.provider === 'mountpoint'">
|
||||
<label class="control-label" for="inputConfigureMountPoint">{{ 'backups.configureBackupStorage.mountPoint' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountPoint" id="inputConfigureMountPoint" name="mountPoint" ng-disabled="configureBackup.busy" placeholder="Folder where filesystem is mounted" ng-required="mountlike(configureBackup.provider)">
|
||||
<p ng-bind-html="'backups.configureBackupStorage.mountPointDescription' | tr:{ providerDocsLink: 'https://docs.cloudron.io/backups/#'+configureBackup.provider }"></p>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountPoint" id="inputConfigureMountPoint" name="mountPoint" ng-disabled="configureBackup.busy" placeholder="/mnt/backups" ng-required="configureBackup.provider === 'mountpoint'">
|
||||
<p ng-show="configureBackup.provider === 'mointpoint'" ng-bind-html="'backups.configureBackupStorage.mountPointDescription' | tr:{ providerDocsLink: 'https://docs.cloudron.io/backups/#'+configureBackup.provider }"></p>
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs' || configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupHost">{{ 'backups.configureBackupStorage.server' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.host" id="configureBackupHost" name="host" ng-disabled="configureBackup.busy" placeholder="Server IP or hostname" ng-required="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs' || configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupRemoteDir">{{ 'backups.configureBackupStorage.remoteDirectory' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.remoteDir" id="configureBackupRemoteDir" name="remoteDir" ng-disabled="configureBackup.busy" placeholder="/share" ng-required="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupUsername">{{ 'backups.configureBackupStorage.username' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.username" id="configureBackupUsername" name="username" ng-disabled="configureBackup.busy">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupPassword">{{ 'backups.configureBackupStorage.password' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="password" class="form-control" ng-model="configureBackup.mountOptions.password" id="configureBackupPassword" name="password" ng-disabled="configureBackup.busy">
|
||||
</div>
|
||||
|
||||
<!-- EXT4 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.diskPath || !configureBackup.mountOptions.diskPath }" ng-show="configureBackup.provider === 'ext4'">
|
||||
<label class="control-label" for="inputConfigureDiskPath">{{ 'backups.configureBackupStorage.diskPath' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.diskPath" id="inputConfigureDiskPath" name="diskPath" ng-disabled="configureBackup.busy" placeholder="/dev/disk/by-uuid/uuid" ng-required="configureBackup.provider === 'ext4'">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPort">{{ 'backups.configureBackupStorage.port' | tr }}</label>
|
||||
<input type="number" class="form-control" ng-model="configureBackup.mountOptions.port" id="configureBackupPort" name="port" ng-disabled="configureBackup.busy">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupUser">{{ 'backups.configureBackupStorage.user' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.user" id="configureBackupUser" name="user" ng-disabled="configureBackup.busy">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPrivateKey">{{ 'backups.configureBackupStorage.privateKey' | tr }}</label>
|
||||
<textarea class="form-control" ng-model="configureBackup.mountOptions.privateKey" id="configureBackupPrivateKey" name="privateKey" ng-disabled="configureBackup.busy"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Filesystem -->
|
||||
@@ -143,13 +191,7 @@
|
||||
<input type="text" class="form-control" ng-model="configureBackup.backupFolder" id="inputConfigureBackupFolder" name="backupFolder" ng-disabled="configureBackup.busy" placeholder="Directory for backups" ng-required="configureBackup.provider === 'filesystem'">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'filesystem'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.externalDisk">{{ 'backups.configureBackupStorage.ext4Label' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Filesystem/SSHFS/CIFS/NFS -->
|
||||
<!-- Filesystem/SSHFS/CIFS/NFS/EXT4/mountpoint -->
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'filesystem' || mountlike(configureBackup.provider)">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.useHardlinks">{{ 'backups.configureBackupStorage.hardlinksLabel' | tr }}</input>
|
||||
@@ -225,6 +267,11 @@
|
||||
<select class="form-control" name="region" id="inputConfigureBackupIonosRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in ionosRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'ionos-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'vultr-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupVultrRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupVultrRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in vultrRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'vultr-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.accessKeyId }" ng-show="s3like(configureBackup.provider)">
|
||||
<label class="control-label" for="inputConfigureBackupAccessKeyId">{{ 'backups.configureBackupStorage.s3AccessKeyId' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.accessKeyId" id="inputConfigureBackupAccessKeyId" name="accessKeyId" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
|
||||
@@ -343,7 +390,8 @@
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p ng-hide="backupCheck.ok" class="text-danger" ng-bind-html="backupCheck.message | markdown2html"></p>
|
||||
<p ng-show="backupConfig.provider === 'noop'" class="text-danger" ng-bind-html="'backups.check.noop' | tr | markdown2html"></p>
|
||||
<p ng-show="backupConfig.provider === 'filesystem'" class="text-danger" ng-bind-html="'backups.check.sameDisk' | tr | markdown2html"></p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
@@ -359,7 +407,12 @@
|
||||
</div>
|
||||
<div class="col-xs-6 text-right no-wrap">
|
||||
<span ng-show="backupConfig.provider === 'filesystem'">{{ backupConfig.backupFolder }}</span>
|
||||
<span ng-show="mountlike(backupConfig.provider)">{{ backupConfig.mountPoint + (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
<span ng-show="mountlike(backupConfig.provider)">
|
||||
<i class="fa fa-circle" ng-style="{ color: backupConfig.mountStatus.state === 'active' ? '#27CE65' : '#d9534f' }" ng-show="backupConfig.mountStatus" uib-tooltip="{{ backupConfig.mountStatus.message }}"></i>
|
||||
<span ng-show="backupConfig.provider === 'filesystem' || backupConfig.provider === 'ext4' || backupConfig.provider === 'mountpoint'">{{ backupConfig.mountOptions.diskPath || backupConfig.mountPoint }}{{ (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
<span ng-show="backupConfig.provider === 'cifs' || backupConfig.provider === 'nfs' || backupConfig.provider === 'sshfs'">{{ backupConfig.mountOptions.host }}:{{ backupConfig.mountOptions.remoteDir }}{{ (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
</span>
|
||||
|
||||
<span ng-show="backupConfig.provider !== 's3' && backupConfig.provider !== 'minio' && (s3like(backupConfig.provider) || backupConfig.provider === 'gcs')">{{ backupConfig.bucket + (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
<span ng-show="backupConfig.provider === 's3'">{{ backupConfig.region + ' ' + backupConfig.bucket + (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
<span ng-show="backupConfig.provider === 'minio'">{{ backupConfig.endpoint + ' ' + backupConfig.bucket + (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
|
||||
+73
-31
@@ -13,7 +13,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
|
||||
$scope.manualBackupApps = [];
|
||||
|
||||
$scope.backupCheck = { ok: true, message: '' };
|
||||
$scope.backupConfig = {};
|
||||
$scope.backups = [];
|
||||
|
||||
@@ -91,13 +90,19 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
{ name: 'DE', value: 'https://s3-de-central.profitbricks.com', region: 's3-de-central' }, // default
|
||||
];
|
||||
|
||||
$scope.vultrRegions = [
|
||||
{ name: 'New Jersey', value: 'https://ewr1.vultrobjects.com', region: 'us-east-1' }, // default
|
||||
];
|
||||
|
||||
$scope.storageProvider = [
|
||||
{ name: 'Amazon S3', value: 's3' },
|
||||
{ name: 'Backblaze B2 (S3 API)', value: 'backblaze-b2' },
|
||||
{ name: 'CIFS Mount', value: 'cifs' },
|
||||
{ name: 'DigitalOcean Spaces', value: 'digitalocean-spaces' },
|
||||
{ name: 'EXT4 Disk', value: 'ext4' },
|
||||
{ name: 'Exoscale SOS', value: 'exoscale-sos' },
|
||||
{ name: 'Filesystem', value: 'filesystem' },
|
||||
{ name: 'Filesystem (Mountpoint)', value: 'mountpoint' }, // legacy
|
||||
{ name: 'Google Cloud Storage', value: 'gcs' },
|
||||
{ name: 'IONOS (Profitbricks)', value: 'ionos-objectstorage' },
|
||||
{ name: 'Linode Object Storage', value: 'linode-objectstorage' },
|
||||
@@ -107,6 +112,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
{ name: 'S3 API Compatible (v4)', value: 's3-v4-compat' },
|
||||
{ name: 'Scaleway Object Storage', value: 'scaleway-objectstorage' },
|
||||
{ name: 'SSHFS Mount', value: 'sshfs' },
|
||||
{ name: 'Vultr Object Storage', value: 'vultr-objectstorage' },
|
||||
{ name: 'Wasabi', value: 'wasabi' },
|
||||
{ name: 'No-op (Only for testing)', value: 'noop' }
|
||||
];
|
||||
@@ -132,14 +138,8 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
{ name: 'Saturday', value: 6 },
|
||||
];
|
||||
|
||||
$scope.cronHours = [
|
||||
{ name: 'Midnight', value: 0 }, { name: '1 AM', value: 1 }, { name: '2 AM', value: 2 }, { name: '3 AM', value: 3 },
|
||||
{ name: '4 AM', value: 4 }, { name: '5 AM', value: 5 }, { name: '6 AM', value: 6 }, { name: '7 AM', value: 7 },
|
||||
{ name: '8 AM', value: 8 }, { name: '9 AM', value: 9 }, { name: '10 AM', value: 10 }, { name: '11 AM', value: 11 },
|
||||
{ name: 'Noon', value: 12 }, { name: '1 PM', value: 13 }, { name: '2 PM', value: 14 }, { name: '3 PM', value: 15 },
|
||||
{ name: '4 PM', value: 16 }, { name: '5 PM', value: 17 }, { name: '6 PM', value: 18 }, { name: '7 PM', value: 19 },
|
||||
{ name: '8 PM', value: 20 }, { name: '9 PM', value: 21 }, { name: '10 PM', value: 22 }, { name: '11 PM', value: 23 }
|
||||
];
|
||||
// generates 24h time sets (instead of american 12h) to avoid having to translate everything to locales eg. 12:00
|
||||
$scope.cronHours = Array.from({ length: 24 }).map(function (v, i) { return { name: (i < 10 ? '0' : '') + i + ':00', value: i }; });
|
||||
|
||||
$scope.formats = [
|
||||
{ name: 'Tarball (zipped)', value: 'tgz' },
|
||||
@@ -289,11 +289,11 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat'
|
||||
|| provider === 'exoscale-sos' || provider === 'digitalocean-spaces'
|
||||
|| provider === 'scaleway-objectstorage' || provider === 'wasabi' || provider === 'backblaze-b2'
|
||||
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'ionos-objectstorage';
|
||||
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'ionos-objectstorage' || provider === 'vultr-objectstorage';
|
||||
};
|
||||
|
||||
$scope.mountlike = function (provider) {
|
||||
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs';
|
||||
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4';
|
||||
};
|
||||
|
||||
// https://stackoverflow.com/questions/3665115/how-to-create-a-file-in-memory-for-user-to-download-but-not-through-server#18197341
|
||||
@@ -419,7 +419,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
mountPoint: '',
|
||||
acceptSelfSignedCerts: false,
|
||||
useHardlinks: true,
|
||||
externalDisk: false,
|
||||
format: 'tgz',
|
||||
password: '',
|
||||
passwordRepeat: '',
|
||||
@@ -433,6 +432,17 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
downloadConcurrency: '',
|
||||
syncConcurrency: '', // sort of similar to upload
|
||||
|
||||
mountOptions: {
|
||||
host: '',
|
||||
remoteDir: '',
|
||||
username: '',
|
||||
password: '',
|
||||
diskPath: '',
|
||||
user: '',
|
||||
port: 22,
|
||||
privateKey: ''
|
||||
},
|
||||
|
||||
clearProviderFields: function () {
|
||||
$scope.configureBackup.bucket = '';
|
||||
$scope.configureBackup.prefix = '';
|
||||
@@ -446,7 +456,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.configureBackup.mountPoint = '';
|
||||
$scope.configureBackup.acceptSelfSignedCerts = false;
|
||||
$scope.configureBackup.useHardlinks = true;
|
||||
$scope.configureBackup.externalDisk = false;
|
||||
$scope.configureBackup.memoryLimit = 400 * 1024 * 1024;
|
||||
|
||||
// scaleway only supports 1000 parts per object (https://www.scaleway.com/en/docs/s3-multipart-upload/)
|
||||
@@ -454,6 +463,8 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.configureBackup.downloadConcurrency = $scope.configureBackup.provider === 's3' ? 30 : 10;
|
||||
$scope.configureBackup.syncConcurrency = $scope.configureBackup.provider === 's3' ? 20 : 10;
|
||||
$scope.configureBackup.copyConcurrency = $scope.configureBackup.provider === 's3' ? 500 : 10;
|
||||
|
||||
$scope.configureBackup.mountOptions = { host: '', remoteDir: '', username: '', password: '', diskPath: '', user: '', port: 22, privateKey: '' };
|
||||
},
|
||||
|
||||
show: function () {
|
||||
@@ -484,7 +495,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.configureBackup.format = $scope.backupConfig.format;
|
||||
$scope.configureBackup.acceptSelfSignedCerts = !!$scope.backupConfig.acceptSelfSignedCerts;
|
||||
$scope.configureBackup.useHardlinks = !$scope.backupConfig.noHardlinks;
|
||||
$scope.configureBackup.externalDisk = !!$scope.backupConfig.externalDisk;
|
||||
|
||||
$scope.configureBackup.memoryLimit = $scope.backupConfig.memoryLimit;
|
||||
|
||||
@@ -504,6 +514,17 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.configureBackup.uploadPartSizeTicks.push(j * 1024 * 1024);
|
||||
}
|
||||
|
||||
var mountOptions = $scope.backupConfig.mountOptions || {};
|
||||
$scope.configureBackup.mountOptions = {
|
||||
host: mountOptions.host || '',
|
||||
remoteDir: mountOptions.remoteDir || '',
|
||||
username: mountOptions.username || '',
|
||||
password: mountOptions.password || '',
|
||||
diskPath: mountOptions.diskPath || '',
|
||||
user: mountOptions.user || '',
|
||||
port: mountOptions.port || 22,
|
||||
privateKey: mountOptions.privateKey || ''
|
||||
};
|
||||
|
||||
$('#configureBackupModal').modal('show');
|
||||
},
|
||||
@@ -556,6 +577,9 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
} else if (backupConfig.provider === 'ionos-objectstorage') {
|
||||
backupConfig.region = $scope.ionosRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'vultr-objectstorage') {
|
||||
backupConfig.region = $scope.vultrRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'digitalocean-spaces') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
}
|
||||
@@ -579,14 +603,35 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.configureBackup.busy = false;
|
||||
return;
|
||||
}
|
||||
} else if (backupConfig.provider === 'sshfs' || backupConfig.provider === 'cifs' || backupConfig.provider === 'nfs') {
|
||||
backupConfig.mountPoint = $scope.configureBackup.mountPoint;
|
||||
} else if ($scope.mountlike(backupConfig.provider)) {
|
||||
backupConfig.prefix = $scope.configureBackup.prefix;
|
||||
backupConfig.noHardlinks = !$scope.configureBackup.useHardlinks;
|
||||
backupConfig.mountOptions = {};
|
||||
|
||||
if (backupConfig.provider === 'cifs' || backupConfig.provider === 'sshfs' || backupConfig.provider === 'nfs') {
|
||||
backupConfig.mountPoint = '/mnt/cloudronbackup'; // harcoded for ease of use
|
||||
backupConfig.mountOptions.host = $scope.configureBackup.mountOptions.host;
|
||||
backupConfig.mountOptions.remoteDir = $scope.configureBackup.mountOptions.remoteDir;
|
||||
|
||||
if (backupConfig.provider === 'cifs') {
|
||||
backupConfig.mountOptions.username = $scope.configureBackup.mountOptions.username;
|
||||
backupConfig.mountOptions.password = $scope.configureBackup.mountOptions.password;
|
||||
} else if (backupConfig.provider === 'sshfs') {
|
||||
backupConfig.mountOptions.user = $scope.configureBackup.mountOptions.user;
|
||||
backupConfig.mountOptions.port = $scope.configureBackup.mountOptions.port;
|
||||
backupConfig.mountOptions.privateKey = $scope.configureBackup.mountOptions.privateKey;
|
||||
}
|
||||
} else if (backupConfig.provider === 'ext4') {
|
||||
backupConfig.mountPoint = '/mnt/cloudronbackup'; // harcoded for ease of use
|
||||
backupConfig.mountOptions.diskPath = $scope.configureBackup.mountOptions.diskPath;
|
||||
} else if (backupConfig.provider === 'mountpoint') {
|
||||
backupConfig.mountPoint = $scope.configureBackup.mountPoint;
|
||||
backupConfig.chown = true;
|
||||
backupConfig.preserveAttributes = true;
|
||||
}
|
||||
} else if (backupConfig.provider === 'filesystem') {
|
||||
backupConfig.backupFolder = $scope.configureBackup.backupFolder;
|
||||
backupConfig.noHardlinks = !$scope.configureBackup.useHardlinks;
|
||||
backupConfig.externalDisk = $scope.configureBackup.externalDisk;
|
||||
}
|
||||
|
||||
backupConfig.uploadPartSize = $scope.configureBackup.uploadPartSize;
|
||||
@@ -652,7 +697,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$('#configureBackupModal').modal('hide');
|
||||
|
||||
getBackupConfig();
|
||||
checkBackupConfig();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -665,15 +709,22 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.backups = $scope.backups.slice(0, 20); // only show 20 since we don't have pagination
|
||||
|
||||
// add contents property
|
||||
var appsById = {};
|
||||
Client.getInstalledApps().forEach(function (app) { appsById[app.id] = app; });
|
||||
var appsById = {}, appsByFqdn = {};
|
||||
Client.getInstalledApps().forEach(function (app) {
|
||||
appsById[app.id] = app;
|
||||
appsByFqdn[app.fqdn] = app;
|
||||
});
|
||||
|
||||
$scope.backups.forEach(function (backup) {
|
||||
backup.contents = [];
|
||||
backup.dependsOn.forEach(function (appBackupId) {
|
||||
let match = appBackupId.match(/app_(.*?)_.*/); // *? means non-greedy
|
||||
if (!match || !appsById[match[1]]) return;
|
||||
backup.contents.push(appsById[match[1]]);
|
||||
if (!match) return;
|
||||
if (match[1].indexOf('.') !== -1) { // newer backups have fqdn in them
|
||||
if (appsByFqdn[match[1]]) backup.contents.push(appsByFqdn[match[1]]);
|
||||
} else {
|
||||
if (appsById[match[1]]) backup.contents.push(appsById[match[1]]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -687,14 +738,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
});
|
||||
}
|
||||
|
||||
function checkBackupConfig() {
|
||||
Client.checkBackupConfig(function (error, check) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.backupCheck = check;
|
||||
});
|
||||
}
|
||||
|
||||
Client.onReady(function () {
|
||||
Client.memory(function (error, memory) {
|
||||
if (error) console.error(error);
|
||||
@@ -703,7 +746,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
|
||||
fetchBackups();
|
||||
getBackupConfig();
|
||||
checkBackupConfig();
|
||||
|
||||
$scope.manualBackupApps = Client.getInstalledApps().filter(function (app) { return !app.enableBackup; });
|
||||
|
||||
|
||||
@@ -123,6 +123,12 @@
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.linodeToken" name="linodeToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'linode'">
|
||||
</div>
|
||||
|
||||
<!-- Vultr -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'vultr'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.vultrToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.vultrToken" name="vultrToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'vultr'">
|
||||
</div>
|
||||
|
||||
<!-- Name.com -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecom'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.nameComUsername' | tr }}</label>
|
||||
|
||||
@@ -41,6 +41,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
{ name: 'Name.com', value: 'namecom' },
|
||||
{ name: 'Namecheap', value: 'namecheap' },
|
||||
{ name: 'Netcup', value: 'netcup' },
|
||||
{ name: 'Vultr', value: 'vultr' },
|
||||
{ name: 'Wildcard', value: 'wildcard' },
|
||||
{ name: 'Manual (not recommended)', value: 'manual' },
|
||||
{ name: 'No-op (only for development)', value: 'noop' }
|
||||
@@ -58,6 +59,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
case 'netcup': return 'Netcup';
|
||||
case 'gcdns': return 'Google Cloud';
|
||||
case 'godaddy': return 'GoDaddy';
|
||||
case 'vultr': return 'Vultr';
|
||||
case 'manual': return 'Manual';
|
||||
case 'wildcard': return 'Wildcard';
|
||||
case 'noop': return 'No-op';
|
||||
@@ -143,6 +145,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
cloudflareEmail: '',
|
||||
cloudflareTokenType: 'GlobalApiKey',
|
||||
linodeToken: '',
|
||||
vultrToken: '',
|
||||
nameComToken: '',
|
||||
nameComUsername: '',
|
||||
namecheapUsername: '',
|
||||
@@ -198,6 +201,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
}
|
||||
$scope.domainConfigure.digitalOceanToken = domain.provider === 'digitalocean' ? domain.config.token : '';
|
||||
$scope.domainConfigure.linodeToken = domain.provider === 'linode' ? domain.config.token : '';
|
||||
$scope.domainConfigure.vultrToken = domain.provider === 'vultr' ? domain.config.token : '';
|
||||
$scope.domainConfigure.gandiApiKey = domain.provider === 'gandi' ? domain.config.token : '';
|
||||
$scope.domainConfigure.cloudflareToken = domain.provider === 'cloudflare' ? domain.config.token : '';
|
||||
$scope.domainConfigure.cloudflareEmail = domain.provider === 'cloudflare' ? domain.config.email : '';
|
||||
@@ -277,6 +281,8 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
data.token = $scope.domainConfigure.digitalOceanToken;
|
||||
} else if (provider === 'linode') {
|
||||
data.token = $scope.domainConfigure.linodeToken;
|
||||
} else if (provider === 'vultr') {
|
||||
data.token = $scope.domainConfigure.vultrToken;
|
||||
} else if (provider === 'gandi') {
|
||||
data.token = $scope.domainConfigure.gandiApiKey;
|
||||
} else if (provider === 'godaddy') {
|
||||
@@ -372,6 +378,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.domainConfigure.netcupCustomerNumber = '';
|
||||
$scope.domainConfigure.netcupApiKey = '';
|
||||
$scope.domainConfigure.netcupApiPassword = '';
|
||||
$scope.domainConfigure.vultrToken = '';
|
||||
|
||||
$scope.domainConfigure.tlsConfig.provider = 'letsencrypt-prod';
|
||||
$scope.domainConfigure.zoneName = '';
|
||||
|
||||
+18
-7
@@ -152,6 +152,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailboxes.edit.active"> {{ 'email.updateMailboxDialog.activeCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<input class="hide" type="submit" ng-disabled="mailboxedit_form.$invalid || mailboxes.edit.busy || !mailboxes.edit.owner"/>
|
||||
</form>
|
||||
</div>
|
||||
@@ -198,7 +204,7 @@
|
||||
<form name="mailinglistadd_form" role="form" ng-submit="mailinglists.add.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': mailinglists.add.error.name }">
|
||||
<label class="control-label">Name</label>
|
||||
<label class="control-label">{{ 'email.addMailinglistDialog.name' | tr }}</label>
|
||||
<div class="control-label" ng-show="mailinglists.add.error.name"><small>{{ mailinglists.add.error.name }}</small></div>
|
||||
<div class="input-group form-inline" style="margin-top: 10px;">
|
||||
<input type="text" class="form-control" ng-model="mailinglists.add.name" required autofocus autocomplete="off"/>
|
||||
@@ -248,6 +254,11 @@
|
||||
<input type="checkbox" ng-model="mailinglists.edit.membersOnly">{{ 'email.addMailinglistDialog.membersOnlyCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailinglists.edit.active"> {{ 'email.updateMailinglistDialog.activeCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="mailinglistedit_form.$invalid || mailinglists.edit.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
@@ -337,7 +348,7 @@
|
||||
<button class="btn btn-default btn-outline" ng-click="mailboxes.showNextPage()" ng-disabled="mailboxes.busy || mailboxes.perPage > mailboxes.mailboxes.length">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
|
||||
<input class="form-control pull-right" style="width: 200px;" placeholder="Search" type="text" ng-model="mailboxes.search" ng-model-options="{ debounce: 1000 }" ng-change="mailboxes.updateFilter()" />
|
||||
<input class="form-control pull-right" style="width: 200px;" placeholder="{{ 'main.searchPlaceholder' | tr }}" type="text" ng-model="mailboxes.search" ng-model-options="{ debounce: 1000 }" ng-change="mailboxes.updateFilter()" />
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -356,7 +367,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="mailbox in mailboxes.mailboxes | filter:mailboxes.search">
|
||||
<tr ng-repeat="mailbox in mailboxes.mailboxes | filter:mailboxes.search" ng-class="{'text-muted': !mailbox.active}">
|
||||
<td class="hand" ng-click="mailboxes.edit.show(mailbox)">
|
||||
{{ mailbox.name }}
|
||||
</td>
|
||||
@@ -390,7 +401,7 @@
|
||||
<button class="btn btn-default btn-outline" ng-click="mailinglists.showNextPage()" ng-disabled="mailinglists.busy || mailinglists.perPage > mailinglists.mailinglists.length">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
|
||||
<input class="form-control pull-right" style="width: 200px;" placeholder="Search" type="text" ng-model="mailinglists.search" ng-model-options="{ debounce: 1000 }" ng-change="mailinglists.updateFilter()" />
|
||||
<input class="form-control pull-right" style="width: 200px;" placeholder="{{ 'main.searchPlaceholder' | tr }}" type="text" ng-model="mailinglists.search" ng-model-options="{ debounce: 1000 }" ng-change="mailinglists.updateFilter()" />
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -410,10 +421,10 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="list in mailinglists.mailinglists | filter:mailinglists.search | orderBy:'name'">
|
||||
<tr ng-repeat="list in mailinglists.mailinglists | filter:mailinglists.search | orderBy:'name'" ng-class="{'text-muted': !list.active}">
|
||||
<td>
|
||||
<i class="fas fa-door-closed" ng-show="list.membersOnly" uib-tooltip="{{ 'email.incoming.mailinglists.membersOnlyTooltip' | tr }}" tooltip-class="long nowrap"></i>
|
||||
<i class="fas fa-door-open" ng-show="!list.membersOnly" uib-tooltip="{{ 'email.incoming.mailinglists.everyoneTooltip' | tr }}" tooltip-class="long nowrap"></i>
|
||||
<i class="fas fa-door-closed" ng-show="list.membersOnly" uib-tooltip="{{ 'email.incoming.mailinglists.membersOnlyTooltip' | tr }}"></i>
|
||||
<i class="fas fa-door-open" ng-show="!list.membersOnly" uib-tooltip="{{ 'email.incoming.mailinglists.everyoneTooltip' | tr }}"></i>
|
||||
</td>
|
||||
<td class="hand" ng-click="mailinglists.edit.show(list)">
|
||||
{{ list.name }}
|
||||
|
||||
+6
-2
@@ -133,11 +133,13 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
name: '',
|
||||
membersTxt: '',
|
||||
membersOnly: false,
|
||||
active: true,
|
||||
|
||||
show: function (list) {
|
||||
$scope.mailinglists.edit.name = list.name;
|
||||
$scope.mailinglists.edit.membersTxt = list.members.sort().join('\n');
|
||||
$scope.mailinglists.edit.membersOnly = list.membersOnly;
|
||||
$scope.mailinglists.edit.active = list.active;
|
||||
|
||||
$('#mailinglistEditModal').modal('show');
|
||||
},
|
||||
@@ -149,7 +151,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
.map(function (m) { return m.trim(); })
|
||||
.filter(function (m) { return m.length !== 0; });
|
||||
|
||||
Client.updateMailingList($scope.domain.domain, $scope.mailinglists.edit.name, members, $scope.mailinglists.edit.membersOnly, function (error) {
|
||||
Client.updateMailingList($scope.domain.domain, $scope.mailinglists.edit.name, members, $scope.mailinglists.edit.membersOnly, $scope.mailinglists.edit.active, function (error) {
|
||||
$scope.mailinglists.edit.busy = false;
|
||||
$scope.mailinglists.edit.error = {};
|
||||
|
||||
@@ -384,6 +386,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
owner: null,
|
||||
incomingDomains: [],
|
||||
aliases: [],
|
||||
active: true,
|
||||
|
||||
addAlias: function (event) {
|
||||
event.preventDefault();
|
||||
@@ -403,6 +406,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
$scope.mailboxes.edit.name = mailbox.name;
|
||||
$scope.mailboxes.edit.owner = mailbox.owner; // this can be null if mailbox had no owner
|
||||
$scope.mailboxes.edit.aliases = angular.copy(mailbox.aliases, []);
|
||||
$scope.mailboxes.edit.active = mailbox.active;
|
||||
|
||||
$('#mailboxEditModal').modal('show');
|
||||
},
|
||||
@@ -411,7 +415,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
$scope.mailboxes.edit.busy = true;
|
||||
|
||||
// $scope.mailboxes.edit.owner is expected to be validated by the UI
|
||||
Client.updateMailbox($scope.domain.domain, $scope.mailboxes.edit.name, $scope.mailboxes.edit.owner.id, $scope.mailboxes.edit.owner.type, function (error) {
|
||||
Client.updateMailbox($scope.domain.domain, $scope.mailboxes.edit.name, $scope.mailboxes.edit.owner.id, $scope.mailboxes.edit.owner.type, $scope.mailboxes.edit.active, function (error) {
|
||||
if (error) {
|
||||
$scope.mailboxes.edit.error = error;
|
||||
$scope.mailboxes.edit.busy = false;
|
||||
|
||||
@@ -280,7 +280,7 @@
|
||||
<div class="row" ng-show="user.role === 'owner'">
|
||||
<div class="col-md-12">
|
||||
<div class="maillog-filter">
|
||||
<input class="form-control" style="width: 200px;" placeholder="{{ 'emails.eventlog.searchPlaceholder' | tr }}" type="text" ng-model="activity.search" ng-model-options="{ debounce: 1000 }" ng-change="activity.updateFilter()" />
|
||||
<input class="form-control" style="width: 200px;" placeholder="{{ 'main.searchPlaceholder' | tr }}" type="text" ng-model="activity.search" ng-model-options="{ debounce: 1000 }" ng-change="activity.updateFilter()" />
|
||||
<multiselect ng-model="activity.selectedTypes" ms-header="{{ 'emails.typeFilterHeader' | tr }}" options="a.name for a in activityTypes" data-multiple="true" ng-change="activity.updateFilter(true)" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<select class="form-control" ng-model="activity.pageItems" ng-options="a.name for a in pageItemCount" ng-change="activity.updateFilter(true)"></select>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="eventlog-filter">
|
||||
<input type="text" class="form-control" style="min-width: 350px;" ng-model="search" ng-model-options="{ debounce: 1000 }" ng-change="updateFilter()" placeholder="{{ 'eventlog.searchPlaceholder' | tr }}"/>
|
||||
<input type="text" class="form-control" style="min-width: 350px;" ng-model="search" ng-model-options="{ debounce: 1000 }" ng-change="updateFilter()" placeholder="{{ 'main.searchPlaceholder' | tr }}"/>
|
||||
<multiselect ng-model="selectedActions" ms-header="{{ 'eventlog.filterAllEvents' | tr }}" options="a.name for a in actions" data-multiple="true" ng-change="updateFilter(true)" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<select class="form-control" ng-model="pageItems" ng-options="a.name for a in pageItemCount" ng-change="updateFilter(true)"></select>
|
||||
<!-- <select class="form-control" ng-model="action" ng-options="a.name for a in actions" ng-change="updateFilter()">
|
||||
@@ -3,7 +3,7 @@
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
|
||||
angular.module('Application').controller('ActivityController', ['$scope', '$location', '$translate', 'Client', function ($scope, $location, $translate, Client) {
|
||||
angular.module('Application').controller('EventLogController', ['$scope', '$location', '$translate', 'Client', function ($scope, $location, $translate, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
@@ -92,6 +92,7 @@ angular.module('Application').controller('ActivityController', ['$scope', '$loca
|
||||
var ACTION_APP_CONFIGURE = 'app.configure';
|
||||
var ACTION_APP_INSTALL = 'app.install';
|
||||
var ACTION_APP_RESTORE = 'app.restore';
|
||||
var ACTION_APP_IMPORT = 'app.import';
|
||||
var ACTION_APP_UNINSTALL = 'app.uninstall';
|
||||
var ACTION_APP_UPDATE = 'app.update';
|
||||
var ACTION_APP_UPDATE_FINISH = 'app.update.finish';
|
||||
@@ -253,6 +254,13 @@ angular.module('Application').controller('ActivityController', ['$scope', '$loca
|
||||
if (data.backupId) details += ' using backup ' + data.backupId;
|
||||
return details;
|
||||
|
||||
case ACTION_APP_IMPORT:
|
||||
if (!data.app) return '';
|
||||
details = data.app.manifest.title + ' was imported at ' + (data.app.fqdn || data.app.location);
|
||||
if (data.toManifest) details += ' to version ' + data.toManifest.version;
|
||||
if (data.backupId) details += ' using backup ' + data.backupId;
|
||||
return details;
|
||||
|
||||
case ACTION_APP_UNINSTALL:
|
||||
if (!data.app) return '';
|
||||
return data.app.manifest.title + ' (package v' + data.app.manifest.version + ') was uninstalled at ' + (data.app.fqdn || data.app.location);
|
||||
@@ -1,14 +1,19 @@
|
||||
<div class="content">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>{{ 'notifications.title' | tr }} <button class="btn btn-primary btn-outline pull-right" ng-click="notifications.clearAll()" ng-disabled="clearAllBusy"><i class="fa fa-circle-notch fa-spin" ng-show="clearAllBusy"></i><i class="fa fa-check" ng-hide="clearAllBusy"></i> {{ 'notifications.clearAll' | tr }}</button></h1>
|
||||
<h1>{{ 'notifications.title' | tr }}
|
||||
|
||||
<button class="btn btn-primary btn-outline pull-right" ng-click="clearAll()" ng-disabled="!hasUnread || clearAllBusy"><i class="fa fa-circle-notch fa-spin" ng-show="clearAllBusy"></i><i class="fa fa-check" ng-hide="clearAllBusy"></i> {{ 'notifications.markAllAsRead' | tr }}</button>
|
||||
<button class="btn btn-default btn-outline pull-right" ng-click="showNextPage()" ng-disabled="busy || perPage > notifications.length">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
<button class="btn btn-default btn-outline pull-right" ng-click="showPrevPage()" ng-disabled="busy || currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-12 text-center" ng-show="notifications.busy">
|
||||
<div class="col-lg-12 text-center" ng-show="busy">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-hide="notifications.busy || notifications.notifications.length">
|
||||
<div class="card" ng-hide="busy || notifications.length">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h3 class="text-center" style="margin: 20px;">{{ 'notifications.nonePending' | tr }}</h3>
|
||||
@@ -16,13 +21,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card notification-item" ng-repeat="notification in notifications.notifications">
|
||||
<div class="card notification-item" ng-repeat="notification in notifications">
|
||||
<div class="row">
|
||||
<div class="col-xs-12" ng-click="notification.isCollapsed = !notification.isCollapsed" ng-class="{ 'notification-details': notification.detailsShown }">
|
||||
{{ notification.title }} <small class="text-muted" uib-tooltip="{{ notification.creationTime | prettyLongDate }}">{{ notification.creationTime | prettyDate }}</small>
|
||||
<button class="btn btn-xs btn-default pull-right" ng-hide="notification.acknowledged" ng-click="notifications.ack(notification, $event)" uib-tooltip="{{ 'notifications.dismissTooltip' | tr }}"><i class="fa fa-times"></i></button>
|
||||
<span ng-class="{'text-bold': !notification.acknowledged }">{{ notification.title }}</span> <small class="text-muted" uib-tooltip="{{ notification.creationTime | prettyLongDate }}">{{ notification.creationTime | prettyDate }}</small>
|
||||
<!-- hidden for now since it seems overkill to have "unread" -->
|
||||
<!-- <button class="btn btn-xs btn-default pull-right" ng-show="notification.acknowledged" ng-click="ack(notification, false, $event)" uib-tooltip="{{ 'notifications.dismissTooltip' | tr }}"><i class="fa fa-asterisk"></i></button> -->
|
||||
|
||||
<div uib-collapse="notification.isCollapsed" expanding="notificationExpanding(notification)">
|
||||
<div uib-collapse="notification.isCollapsed" expanding="ack(notification, true)">
|
||||
<br/>
|
||||
<p ng-hide="notification.messageJson" ng-click="$event.stopPropagation();" style="cursor: auto; overflow: auto;" ng-bind-html="notification.message | markdown2html"></p>
|
||||
<pre ng-show="notification.messageJson" ng-click="$event.stopPropagation();" style="cursor: auto">{{ notification.messageJson | json }}</pre>
|
||||
|
||||
+69
-75
@@ -3,97 +3,91 @@
|
||||
/* global async */
|
||||
/* global angular */
|
||||
|
||||
angular.module('Application').controller('NotificationsController', ['$scope', '$timeout', 'Client', function ($scope, $timeout, Client) {
|
||||
angular.module('Application').controller('NotificationsController', ['$scope', '$timeout', '$translate', '$interval', 'Client', function ($scope, $timeout, $translate, $interval, Client) {
|
||||
$scope.clearAllBusy = false;
|
||||
|
||||
$scope.notifications = {
|
||||
notifications: [],
|
||||
activeNotification: null,
|
||||
busy: true,
|
||||
$scope.notifications = [];
|
||||
$scope.activeNotification = null;
|
||||
$scope.busy = true;
|
||||
$scope.hasUnread = false;
|
||||
$scope.currentPage = 1;
|
||||
$scope.perPage = 20;
|
||||
|
||||
refresh: function () {
|
||||
Client.getNotifications(false, 1, 100, function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
$scope.refresh = function () {
|
||||
Client.getNotifications({}, $scope.currentPage, $scope.perPage, function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
// collapse by default
|
||||
result.forEach(function (r) { r.isCollapsed = true; });
|
||||
// collapse by default
|
||||
result.forEach(function (r) { r.isCollapsed = true; });
|
||||
|
||||
// attempt to parse the message as json
|
||||
result.forEach(function (r) {
|
||||
try {
|
||||
r.messageJson = JSON.parse(r.message);
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
$scope.notifications.notifications = result;
|
||||
|
||||
$scope.notifications.busy = false;
|
||||
// attempt to translate or parse the message as json
|
||||
result.forEach(function (r) {
|
||||
try {
|
||||
r.messageJson = JSON.parse(r.message);
|
||||
} catch (e) {}
|
||||
});
|
||||
},
|
||||
|
||||
clicked: function (notification) {
|
||||
if ($scope.notifications.activeNotification === notification) return $scope.notifications.activeNotification = null;
|
||||
$scope.notifications.activeNotification = notification;
|
||||
},
|
||||
$scope.notifications = result;
|
||||
$scope.hasUnread = !!result.find(function (n) { return !n.acknowledged; });
|
||||
|
||||
ackOne: function (id, callback) {
|
||||
Client.ackNotification(id, function (error) {
|
||||
if (error) return callback(error);
|
||||
$scope.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$parent.notificationAcknowledged(id);
|
||||
$scope.showNextPage = function () {
|
||||
$scope.currentPage++;
|
||||
$scope.refresh();
|
||||
};
|
||||
|
||||
$scope.showPrevPage = function () {
|
||||
if ($scope.currentPage > 1) $scope.currentPage--;
|
||||
else $scope.currentPage = 1;
|
||||
|
||||
$scope.refresh();
|
||||
};
|
||||
|
||||
$scope.ack = function (notification, acked, event) {
|
||||
if (event) event.stopPropagation();
|
||||
|
||||
if (notification.acknowledged === acked) return;
|
||||
|
||||
Client.ackNotification(notification.id, acked, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
notification.acknowledged = acked;
|
||||
$scope.$parent.notificationAcknowledged(acked);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.clearAll = function () {
|
||||
$scope.clearAllBusy = true;
|
||||
|
||||
async.eachLimit($scope.notifications, 20, function (notification, callback) {
|
||||
if (notification.acknowledged) return callback();
|
||||
|
||||
Client.ackNotification(notification.id, true, function (error) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
} else {
|
||||
notification.acknowledged = true;
|
||||
$scope.$parent.notificationAcknowledged(true);
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
ack: function (notification, event) {
|
||||
event.stopPropagation();
|
||||
|
||||
$scope.notifications.ackOne(notification.id, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.notifications.refresh();
|
||||
});
|
||||
},
|
||||
|
||||
action: function (notification) {
|
||||
if (notification.action) window.location = notification.action;
|
||||
},
|
||||
|
||||
clearAll: function () {
|
||||
$scope.clearAllBusy = true;
|
||||
|
||||
async.eachLimit($scope.notifications.notifications, 20, function (notification, callback) {
|
||||
if (notification.acknowledged) return callback();
|
||||
$scope.notifications.ackOne(notification.id, callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.notifications.refresh();
|
||||
$scope.clearAllBusy = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.notificationExpanding = function (notification) {
|
||||
if (!notification.eventId) return;
|
||||
|
||||
notification.busyLoadEvent = true;
|
||||
|
||||
Client.getEvent(notification.eventId, function (error, result) {
|
||||
notification.busyLoadEvent = false;
|
||||
|
||||
if (error) return console.error(error);
|
||||
|
||||
notification.event = result;
|
||||
$scope.hasUnread = false;
|
||||
$scope.clearAllBusy = false;
|
||||
});
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.notifications.refresh();
|
||||
});
|
||||
|
||||
Client.onReconnect(function () {
|
||||
$scope.notifications.refresh();
|
||||
var refreshTimer = $interval($scope.refresh, 60 * 1000); // keep this interval in sync with the notification count indicator in main.js
|
||||
$scope.$on('$destroy', function () {
|
||||
$interval.cancel(refreshTimer);
|
||||
});
|
||||
$scope.refresh();
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -364,7 +364,7 @@
|
||||
<tr>
|
||||
<td class="text-right" colspan="2" style="vertical-align: top;">
|
||||
<br/>
|
||||
<button class="btn btn-primary" ng-click="twoFactorAuthentication.show()">{{ user.twoFactorAuthenticationEnabled ? 'profile.disable2FAAction' : 'profile.enable2FAAction' | tr }}</button>
|
||||
<button class="btn" ng-class="user.twoFactorAuthenticationEnabled ? 'btn-danger' : 'btn-success'" ng-click="twoFactorAuthentication.show()">{{ user.twoFactorAuthenticationEnabled ? 'profile.disable2FAAction' : 'profile.enable2FAAction' | tr }}</button>
|
||||
<button class="btn btn-primary" ng-click="passwordchange.show()" ng-hide="user.source">{{ 'profile.changePasswordAction' | tr }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -430,7 +430,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 45%">{{ 'profile.apiTokens.name' | tr }}</th>
|
||||
<th style="width: 45%">{{ 'profile.apiTokens.expiresAt' | tr }}</th>
|
||||
<th style="width: 45%">{{ 'profile.apiTokens.lastUsed' | tr }}</th>
|
||||
<th style="width: 10%" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -443,7 +443,8 @@
|
||||
{{ token.name || 'unnamed' }}
|
||||
</td>
|
||||
<td class="elide-table-cell" style="text-overflow: ellipsis; white-space: nowrap;">
|
||||
{{ token.expires | prettyShortDate }}
|
||||
<span ng-show="token.lastUsedTime">{{ token.lastUsedTime | prettyLongDate }}</span>
|
||||
<span ng-show="!token.lastUsedTime">{{ 'profile.apiTokens.neverUsed' | tr }}</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-danger" ng-click="tokens.revokeToken(token)" uib-tooltip="{{ 'profile.apiTokens.revokeTokenTooltip' | tr }}"><i class="far fa-trash-alt"></i></button>
|
||||
|
||||
@@ -313,7 +313,6 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans
|
||||
return;
|
||||
}
|
||||
|
||||
// update user info in the background
|
||||
Client.refreshUserInfo();
|
||||
|
||||
$scope.emailchange.reset();
|
||||
@@ -603,6 +602,7 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans
|
||||
Client.onReady(function () {
|
||||
$scope.appPassword.refresh();
|
||||
$scope.tokens.refresh();
|
||||
Client.refreshUserInfo(); // 2fa status might have changed by admin
|
||||
|
||||
$translate.onReady(function () {
|
||||
var usedLang = $translate.use() || $translate.fallbackLanguage();
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<th style="width: 5%;"></th>
|
||||
<th style="width: 20%">{{ 'services.service' | tr }}</th>
|
||||
<th style="width: 50%">{{ 'services.memoryUsage' | tr }}</th>
|
||||
<th style="width: 20%" class="text-center">{{ 'services.memoryLimit' | tr }}</th>
|
||||
<th style="width: 20%" class="text-center no-wrap">{{ 'services.memoryLimit' | tr }}</th>
|
||||
<th style="width: 5%" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -102,7 +102,7 @@
|
||||
<span ng-show="service.config.memoryLimit">{{ service.config.memoryLimit | prettyByteSize }}</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="serviceConfigure.show(service)" uib-tooltip="{{ 'services.configureActionTooltip' | tr }}" ng-show="service.config.memoryLimit"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="serviceConfigure.show(service)" uib-tooltip="{{ 'services.configureActionTooltip' | tr }}" ng-disabled="!service.config.memoryLimit"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="restartService(service.name)" uib-tooltip="{{ 'services.restartActionTooltip' | tr }}"><i class="fa fa-sync-alt" ng-class="{ 'fa-spin': service.status === 'starting' }"></i></button>
|
||||
<a class="btn btn-xs btn-default" ng-href="{{ '/logs.html?id=' + service.name }}" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}"><i class="fa fa-file-alt"></i></a>
|
||||
</td>
|
||||
|
||||
@@ -270,7 +270,7 @@
|
||||
<span class="text-muted">{{ 'settings.updates.version' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
v{{ config.version }}
|
||||
v{{ config.version }} ({{ config.ubuntuVersion }})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -324,7 +324,7 @@
|
||||
<span class="text-muted">{{ 'settings.privateDockerRegistry.server' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ registryConfig.currentConfig.serverAddress || 'Not set' }}</span>
|
||||
<span>{{ registryConfig.currentConfig.serverAddress || ('settings.privateDockerRegistry.serverNotSet' | tr) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -25,14 +25,8 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
{ name: 'Saturday', value: 6 },
|
||||
];
|
||||
|
||||
$scope.cronHours = [
|
||||
{ name: 'Midnight', value: 0 }, { name: '1 AM', value: 1 }, { name: '2 AM', value: 2 }, { name: '3 AM', value: 3 },
|
||||
{ name: '4 AM', value: 4 }, { name: '5 AM', value: 5 }, { name: '6 AM', value: 6 }, { name: '7 AM', value: 7 },
|
||||
{ name: '8 AM', value: 8 }, { name: '9 AM', value: 9 }, { name: '10 AM', value: 10 }, { name: '11 AM', value: 11 },
|
||||
{ name: 'Noon', value: 12 }, { name: '1 PM', value: 13 }, { name: '2 PM', value: 14 }, { name: '3 PM', value: 15 },
|
||||
{ name: '4 PM', value: 16 }, { name: '5 PM', value: 17 }, { name: '6 PM', value: 18 }, { name: '7 PM', value: 19 },
|
||||
{ name: '8 PM', value: 20 }, { name: '9 PM', value: 21 }, { name: '10 PM', value: 22 }, { name: '11 PM', value: 23 }
|
||||
];
|
||||
// generates 24h time sets (instead of american 12h) to avoid having to translate everything to locales eg. 12:00
|
||||
$scope.cronHours = Array.from({ length: 24 }).map(function (v, i) { return { name: (i < 10 ? '0' : '') + i + ':00', value: i }; });
|
||||
|
||||
$scope.registryConfigProviders = [
|
||||
{ name: 'AWS', value: 'aws' },
|
||||
@@ -40,12 +34,17 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
{ name: 'DockerHub', value: 'dockerhub' },
|
||||
{ name: 'Google Cloud', value: 'google-cloud' },
|
||||
{ name: 'Linode', value: 'linode' },
|
||||
{ name: 'Other', value: 'other' },
|
||||
{ name: 'Quay', value: 'quay' },
|
||||
{ name: 'Treescale', value: 'treescale' },
|
||||
{ name: 'Other', value: 'other' },
|
||||
{ name: 'Disabled', value: 'noop' }
|
||||
];
|
||||
|
||||
$translate(['settings.registryConfig.providerOther', 'settings.registryConfig.providerDisabled']).then(function (tr) {
|
||||
if (tr['settings.registryConfig.providerOther']) $scope.registryConfigProviders.find(function (p) { return p.value === 'other'; }).name = tr['settings.registryConfig.providerOther'];
|
||||
if (tr['settings.registryConfig.providerDisabled']) $scope.registryConfigProviders.find(function (p) { return p.value === 'noop'; }).name = tr['settings.registryConfig.providerDisabled'];
|
||||
});
|
||||
|
||||
$scope.openSubscriptionSetup = function () {
|
||||
Client.openSubscriptionSetup($scope.subscription || {});
|
||||
};
|
||||
|
||||
@@ -66,14 +66,14 @@
|
||||
|
||||
<div class="dropdown pull-right" ng-hide="activeTab === 0">
|
||||
<button class="btn btn-sm btn-primary dropdown-toggle" type="button" data-toggle="dropdown">
|
||||
{{ 'system.selectPeriodLabel' | tr }} {{ graphs.periodLabel }}
|
||||
{{ graphs.period | trKeyFromPeriod | tr }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="" ng-click="graphs.setPeriod(6, '6 hours')">6 hours</a></li>
|
||||
<li><a href="" ng-click="graphs.setPeriod(24, '24 hours')">24 hours</a></li>
|
||||
<li><a href="" ng-click="graphs.setPeriod(24*7, '7 days')">7 days</a></li>
|
||||
<li><a href="" ng-click="graphs.setPeriod(24*30, '30 days')">30 days</a></li>
|
||||
<li><a href="" ng-click="graphs.setPeriod(6)">{{ 6 | trKeyFromPeriod | tr }}</a></li>
|
||||
<li><a href="" ng-click="graphs.setPeriod(24)">{{ 24 | trKeyFromPeriod | tr }}</a></li>
|
||||
<li><a href="" ng-click="graphs.setPeriod(24*7)">{{ 24*7 | trKeyFromPeriod | tr }}</a></li>
|
||||
<li><a href="" ng-click="graphs.setPeriod(24*30)">{{ 24*30 | trKeyFromPeriod | tr }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
+1
-3
@@ -154,13 +154,11 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati
|
||||
error: {},
|
||||
|
||||
period: 6,
|
||||
periodLabel: '6 hours',
|
||||
memoryChart: null,
|
||||
diskChart: null,
|
||||
|
||||
setPeriod: function (hours, label) {
|
||||
setPeriod: function (hours) {
|
||||
$scope.graphs.period = hours;
|
||||
$scope.graphs.periodLabel = label;
|
||||
$scope.graphs.show();
|
||||
},
|
||||
|
||||
|
||||
+27
-15
@@ -317,7 +317,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal invite -->
|
||||
<!-- Modal invite/reset -->
|
||||
<div class="modal fade" id="invitationModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
@@ -325,18 +325,30 @@
|
||||
<h4 class="modal-title">{{ 'users.passwordResetDialog.title' | tr:{ username: (invitation.user.username || invitation.user.email) } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ 'users.passwordResetDialog.description' | tr:{ username: (invitation.user.username || invitation.user.email) } }}</p>
|
||||
<div class="input-group">
|
||||
<input type="text" id="setupLinkInput" class="form-control" ng-value="invitation.setupLink" readonly/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" id="setupLinkButton" type="button" data-clipboard-target="#setupLinkInput"><i class="fa fa-clipboard"></i></button>
|
||||
</span>
|
||||
<div ng-hide="invitation.setupLink">
|
||||
<p>{{ 'users.passwordResetDialog.resetLinkExplanation' | tr }}</p>
|
||||
<button type="button" class="btn btn-primary" ng-click="invitation.generateNewLink()" ng-disabled="invitation.busyNew"><i class="fa fa-circle-notch fa-spin" ng-show="invitation.busyNew"></i> {{ 'users.passwordResetDialog.newLinkAction' | tr }}</button>
|
||||
</div>
|
||||
<div ng-show="invitation.setupLink">
|
||||
<p>{{ 'users.passwordResetDialog.description' | tr:{ username: (invitation.user.username || invitation.user.email) } }}</p>
|
||||
<div class="input-group" style="margin-bottom: 10px">
|
||||
<input type="text" id="setupLinkInput" class="form-control" ng-value="invitation.setupLink" readonly/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" id="setupLinkButton" type="button" data-clipboard-target="#setupLinkInput"><i class="fa fa-clipboard"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success" ng-click="invitation.email()" ng-hide="invitation.successSend" ng-disabled="invitation.busySend"><i class="fa fa-circle-notch fa-spin" ng-show="invitation.busySend"></i> {{ 'users.passwordResetDialog.sendEmailLinkAction' | tr }}</button>
|
||||
<b class="text-success" ng-show="invitation.successSend">{{ 'users.passwordResetDialog.emailSent' | tr }}</b>
|
||||
</div>
|
||||
<hr/>
|
||||
<div>
|
||||
<p ng-hide="invitation.user.twoFactorAuthenticationEnabled">{{ 'users.passwordResetDialog.no2FASetup' | tr }}</p>
|
||||
<p ng-show="invitation.user.twoFactorAuthenticationEnabled">{{ 'users.passwordResetDialog.2FAIsSetup' | tr }}</p>
|
||||
<button type="button" class="btn btn-danger" ng-click="invitation.reset2FA()" ng-disabled="!invitation.user.twoFactorAuthenticationEnabled || invitation.reset2FABusy"><i class="fa fa-circle-notch fa-spin" ng-show="invitation.reset2FABusy"></i> {{ 'users.passwordResetDialog.reset2FAAction' | tr }}</button>
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="invitation.email()" ng-disabled="invitation.busy"><i class="fa fa-circle-notch fa-spin" ng-show="invitation.busy"></i> {{ 'users.passwordResetDialog.sendEmailLinkAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -374,7 +386,7 @@
|
||||
<input type="checkbox" ng-model="externalLdap.acceptSelfSignedCerts"> {{ 'users.externalLdap.acceptSelfSignedCert' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-error" ng-show="externalLdap.error.acceptSelfSignedCerts">{{ 'users.externalLdap.errorSelfSignedCert' | tr }}</p>
|
||||
<p class="has-error" ng-show="externalLdap.error.acceptSelfSignedCerts">{{ 'users.externalLdap.errorSelfSignedCert' | tr }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.baseDn }">
|
||||
@@ -460,7 +472,7 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="users-filter">
|
||||
<input type="text" class="form-control" style="min-width: 350px;" ng-model="userSearchString" ng-model-options="{ debounce: 1000 }" ng-change="updateFilter()" placeholder="{{ 'users.searchPlaceholder' | tr }}"/>
|
||||
<input type="text" class="form-control" style="min-width: 350px;" ng-model="userSearchString" ng-model-options="{ debounce: 1000 }" ng-change="updateFilter()" placeholder="{{ 'main.searchPlaceholder' | tr }}"/>
|
||||
<select class="form-control" ng-model="pageItems" ng-options="a.name for a in pageItemCount" ng-change="updateFilter(true)"></select>
|
||||
</div>
|
||||
<div class="pagination pull-right">
|
||||
@@ -495,9 +507,9 @@
|
||||
</tr>
|
||||
<tr ng-repeat="user in users" ng-class="{'text-muted': !user.active}">
|
||||
<td>
|
||||
<i class="fas fa-crown arrow" ng-show="user.active && user.role === 'owner'" uib-tooltip="{{ 'users.users.superadminTooltip' | tr }}" tooltip-class="long nowrap"></i>
|
||||
<i class="fa fa-user-tie arrow" ng-show="user.active && user.role === 'admin'" uib-tooltip="{{ 'users.users.adminTooltip' | tr }}" tooltip-class="long nowrap"></i>
|
||||
<i class="fas fa-users-cog arrow" ng-show="user.active && user.role === 'usermanager'" uib-tooltip="{{ 'users.users.usermanagerTooltip' | tr }}" tooltip-class="long nowrap"></i>
|
||||
<i class="fas fa-crown arrow" ng-show="user.active && user.role === 'owner'" uib-tooltip="{{ 'users.users.superadminTooltip' | tr }}"></i>
|
||||
<i class="fa fa-user-tie arrow" ng-show="user.active && user.role === 'admin'" uib-tooltip="{{ 'users.users.adminTooltip' | tr }}"></i>
|
||||
<i class="fas fa-users-cog arrow" ng-show="user.active && user.role === 'usermanager'" uib-tooltip="{{ 'users.users.usermanagerTooltip' | tr }}"></i>
|
||||
<i class="fa fa-ban" ng-show="!user.active" uib-tooltip="{{ 'users.users.inactiveTooltip' | tr }}"></i>
|
||||
</td>
|
||||
<td class="hand elide-table-cell" ng-click="canEdit(user) && useredit.show(user)" ng-show="user.username">
|
||||
|
||||
+38
-7
@@ -17,6 +17,11 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
{ name: 'Disabled', value: 'noop' }
|
||||
];
|
||||
|
||||
$translate(['users.externalLdap.providerOther', 'users.externalLdap.providerDisabled']).then(function (tr) {
|
||||
if (tr['users.externalLdap.providerOther']) $scope.ldapProvider.find(function (p) { return p.value === 'other'; }).name = tr['users.externalLdap.providerOther'];
|
||||
if (tr['users.externalLdap.providerDisabled']) $scope.ldapProvider.find(function (p) { return p.value === 'noop'; }).name = tr['users.externalLdap.providerDisabled'];
|
||||
});
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.users = []; // users of current page
|
||||
$scope.allUsersById = [];
|
||||
@@ -520,30 +525,56 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
|
||||
$scope.invitation = {
|
||||
busy: false,
|
||||
reset2FABusy: false,
|
||||
setupLink: '',
|
||||
user: null,
|
||||
successSend: false,
|
||||
|
||||
show: function (user) {
|
||||
$scope.invitation.user = user;
|
||||
$scope.invitation.setupLink = '';
|
||||
$scope.invitation.busy = false;
|
||||
$scope.invitation.reset2FABusy = false;
|
||||
$scope.invitation.successSend = false;
|
||||
|
||||
Client.createInvite(user.id, function (error, result) {
|
||||
$('#invitationModal').modal('show');
|
||||
},
|
||||
|
||||
generateNewLink: function () {
|
||||
$scope.invitation.busyNew = true;
|
||||
|
||||
Client.createInvite($scope.invitation.user.id, function (error, result) {
|
||||
$scope.invitation.busyNew = false;
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.invitation.setupLink = result.inviteLink;
|
||||
|
||||
$('#invitationModal').modal('show');
|
||||
});
|
||||
},
|
||||
|
||||
email: function () {
|
||||
$scope.invitation.busy = true;
|
||||
$scope.invitation.busySend = true;
|
||||
|
||||
Client.sendInvite($scope.invitation.user.id, function (error) {
|
||||
$scope.invitation.busy = false;
|
||||
$scope.invitation.busySend = false;
|
||||
if (error) return console.error(error);
|
||||
$('#invitationModal').modal('hide');
|
||||
|
||||
$scope.invitation.successSend = true;
|
||||
|
||||
$timeout(function () { $scope.invitation.successSend = false; }, 3000);
|
||||
});
|
||||
},
|
||||
|
||||
reset2FA: function () {
|
||||
$scope.invitation.reset2FABusy = true;
|
||||
|
||||
Client.disableTwoFactorAuthenticationByUserId($scope.invitation.user.id, function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$timeout(function () {
|
||||
$scope.invitation.reset2FABusy = false;
|
||||
$scope.invitation.user.twoFactorAuthenticationEnabled = false;
|
||||
}, 3000);
|
||||
|
||||
refreshUsers();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
+67
-8
@@ -17,8 +17,57 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="mountType">{{ 'volumes.mountType' | tr }}</label>
|
||||
<select class="form-control" id="mountType" ng-model="volumeAdd.mountType" ng-options="a.value as a.name for a in mountTypes"></select>
|
||||
<p class="small text-info" ng-show="volumeAdd.mountType === 'noop'" ng-bind-html="'volumes.addVolumeDialog.noopWarning' | tr"></p>
|
||||
<p class="small text-info" ng-hide="volumeAdd.mountType === 'noop'" ng-bind-html="'volumes.addVolumeDialog.mountTypeInfo' | tr"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'noop'">
|
||||
<label class="control-label">{{ 'volumes.hostPath' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="volumeAdd.hostPath" name="hostPath" ng-disabled="volumeAdd.busy" autofocus>
|
||||
<input type="text" class="form-control" ng-model="volumeAdd.hostPath" name="hostPath" ng-disabled="volumeAdd.busy" placeholder="/mnt/data" autofocus>
|
||||
</div>
|
||||
|
||||
<div uib-collapse="volumeAdd.mountType === 'noop'">
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'ext4'">
|
||||
<label class="control-label" for="volumeAddHost">{{ 'volumes.addVolumeDialog.diskPath' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="volumeAdd.diskPath" id="volumeAddDiskPath" name="diskPath" ng-disabled="volumeAdd.busy" placeholder="/dev/disk/by-uuid/uuid">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'cifs' || volumeAdd.mountType === 'nfs' || volumeAdd.mountType === 'sshfs'">
|
||||
<label class="control-label" for="volumeAddHost">{{ 'volumes.addVolumeDialog.server' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="volumeAdd.host" id="volumeAddHost" name="host" ng-disabled="volumeAdd.busy" placeholder="Server IP or hostname">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'cifs' || volumeAdd.mountType === 'nfs' || volumeAdd.mountType === 'sshfs'">
|
||||
<label class="control-label" for="volumeAddRemoteDir">{{ 'volumes.addVolumeDialog.remoteDirectory' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="volumeAdd.remoteDir" id="volumeAddRemoteDir" name="remoteDir" ng-disabled="volumeAdd.busy" placeholder="/share">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'cifs'">
|
||||
<label class="control-label" for="volumeAddUsername">{{ 'volumes.addVolumeDialog.username' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="volumeAdd.username" id="volumeAddUsername" name="username" ng-disabled="volumeAdd.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'cifs'">
|
||||
<label class="control-label" for="volumeAddPassword">{{ 'volumes.addVolumeDialog.password' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="volumeAdd.password" id="volumeAddPassword" name="password" ng-disabled="volumeAdd.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'sshfs'">
|
||||
<label class="control-label" for="volumeAddPort">{{ 'volumes.addVolumeDialog.port' | tr }}</label>
|
||||
<input type="number" class="form-control" ng-model="volumeAdd.port" id="volumeAddPort" name="port" ng-disabled="volumeAdd.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'sshfs'">
|
||||
<label class="control-label" for="volumeAddUser">{{ 'volumes.addVolumeDialog.user' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="volumeAdd.user" id="volumeAddUser" name="user" ng-disabled="volumeAdd.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'sshfs'">
|
||||
<label class="control-label" for="volumeAddPrivateKey">{{ 'volumes.addVolumeDialog.privateKey' | tr }}</label>
|
||||
<textarea class="form-control" ng-model="volumeAdd.privateKey" id="volumeAddPrivateKey" name="privateKey" ng-disabled="volumeAdd.busy"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="volumeAddForm.$invalid || volumeAdd.busy"/>
|
||||
@@ -27,7 +76,7 @@
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="volumeAdd.submit()" ng-disabled="volumeAddForm.$invalid || volumeAdd.busy"><i class="fa fa-circle-notch fa-spin" ng-show="volumeAdd.busy"></i> {{ 'volumes.addVolumeDialog.addAction' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="volumeAdd.submit()" ng-disabled="volumeAddForm.$invalid || volumeAdd.busy"><i class="fa fa-circle-notch fa-spin" ng-show="volumeAdd.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,18 +120,28 @@
|
||||
<table class="table table-hover" style="margin-top: 10px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">{{ 'volumes.name' | tr }}</th>
|
||||
<th class="text-left">{{ 'volumes.hostPath' | tr }}</th>
|
||||
<th style="width: 100px" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
<th style="width: 5%"></th>
|
||||
<th style="width: 25%" class="text-left">{{ 'volumes.name' | tr }}</th>
|
||||
<th style="width: 10%" class="text-left">{{ 'volumes.type' | tr }}</th>
|
||||
<th style="width: 55%" class="text-left">{{ 'volumes.hostPath' | tr }}</th>
|
||||
<th style="width: 10%" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="volume in volumes">
|
||||
<td class="elide-table-cell hand">
|
||||
<td>
|
||||
<i class="fa fa-circle" ng-style="{ color: volume.status.state === 'active' ? '#27CE65' : '#d9534f' }" ng-show="volume.status" uib-tooltip="{{ volume.status.message }}"></i>
|
||||
<i class="fa fa-circle-notch fa-spin" ng-hide="volume.status"></i>
|
||||
</td>
|
||||
<td class="elide-table-cell">
|
||||
{{ volume.name }}
|
||||
</td>
|
||||
<td class="text-left elide-table-cell hidden-xs hidden-sm hand">
|
||||
{{ volume.hostPath }}
|
||||
<td class="elide-table-cell">
|
||||
{{ volume.mountType }}
|
||||
</td>
|
||||
<td class="text-left elide-table-cell hidden-xs hidden-sm">
|
||||
<span ng-show="volume.mountType !== 'noop'">{{ volume.mountOptions.host || volume.mountOptions.diskPath || volume.hostPath }}{{ volume.mountOptions.remoteDir }}</span>
|
||||
<span ng-show="volume.mountType === 'noop'">{{ volume.hostPath }}</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<a class="btn btn-xs btn-default" ng-href="{{ '/filemanager.html?volumeId=' + volume.id }}" target="_blank" uib-tooltip="{{ 'volumes.openFileManagerActionTooltip' | tr }}"><i class="fas fa-folder"></i></a>
|
||||
|
||||
+91
-3
@@ -2,20 +2,55 @@
|
||||
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
/* global async */
|
||||
|
||||
angular.module('Application').controller('VolumesController', ['$scope', '$location', '$timeout', 'Client', function ($scope, $location, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastUserManager) $location.path('/'); });
|
||||
|
||||
var refreshVolumesTimerId = null;
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.volumes = [];
|
||||
$scope.ready = false;
|
||||
|
||||
$scope.mountTypes = [
|
||||
{ name: 'CIFS', value: 'cifs' },
|
||||
{ name: 'EXT4', value: 'ext4' },
|
||||
{ name: 'NFS', value: 'nfs' },
|
||||
{ name: 'SSHFS', value: 'sshfs' },
|
||||
{ name: 'No-op', value: 'noop' }
|
||||
];
|
||||
|
||||
function refreshVolumes(callback) {
|
||||
let refreshAgain = false;
|
||||
|
||||
Client.getVolumes(function (error, results) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.volumes = results;
|
||||
if (callback) callback();
|
||||
|
||||
async.eachSeries($scope.volumes, function (volume, iteratorDone) {
|
||||
Client.getVolumeStatus(volume.id, function (error, result) {
|
||||
if (error) {
|
||||
console.error('Failed to fetch volume status', volume.name, error);
|
||||
iteratorDone();
|
||||
}
|
||||
|
||||
volume.status = result;
|
||||
if (volume.status.state === 'activating') refreshAgain = true;
|
||||
|
||||
iteratorDone();
|
||||
});
|
||||
}, function () {
|
||||
if (!refreshAgain) {
|
||||
clearTimeout(refreshVolumesTimerId);
|
||||
refreshVolumesTimerId = null;
|
||||
} else if (!refreshVolumesTimerId) {
|
||||
refreshVolumesTimerId = setTimeout(refreshVolumes, 5000);
|
||||
}
|
||||
|
||||
if (callback) callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,12 +61,30 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
|
||||
name: '',
|
||||
hostPath: '',
|
||||
|
||||
mountType: 'noop',
|
||||
host: '',
|
||||
remoteDir: '',
|
||||
username: '',
|
||||
password: '',
|
||||
diskPath: '',
|
||||
user: '',
|
||||
port: 22,
|
||||
privateKey: '',
|
||||
|
||||
reset: function () {
|
||||
$scope.volumeAdd.busy = false;
|
||||
$scope.volumeAdd.error = null;
|
||||
$scope.volumeAdd.busy = false;
|
||||
$scope.volumeAdd.name = '';
|
||||
$scope.volumeAdd.hostPath = '';
|
||||
$scope.volumeAdd.mountType = 'noop';
|
||||
$scope.volumeAdd.host = '';
|
||||
$scope.volumeAdd.remoteDir = '';
|
||||
$scope.volumeAdd.username = '';
|
||||
$scope.volumeAdd.password = '';
|
||||
$scope.volumeAdd.diskPath = '';
|
||||
$scope.volumeAdd.user = '';
|
||||
$scope.volumeAdd.port = 22;
|
||||
$scope.volumeAdd.privateKey = '';
|
||||
|
||||
$scope.volumeAddForm.$setPristine();
|
||||
$scope.volumeAddForm.$setUntouched();
|
||||
@@ -47,7 +100,42 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
|
||||
$scope.volumeAdd.busy = true;
|
||||
$scope.volumeAdd.error = null;
|
||||
|
||||
Client.addVolume($scope.volumeAdd.name, $scope.volumeAdd.hostPath, function (error) {
|
||||
var mountOptions = null;
|
||||
|
||||
if ($scope.volumeAdd.mountType === 'cifs') {
|
||||
mountOptions = {
|
||||
host: $scope.volumeAdd.host,
|
||||
remoteDir: $scope.volumeAdd.remoteDir,
|
||||
username: $scope.volumeAdd.username,
|
||||
password: $scope.volumeAdd.password
|
||||
};
|
||||
} else if ($scope.volumeAdd.mountType === 'nfs') {
|
||||
mountOptions = {
|
||||
host: $scope.volumeAdd.host,
|
||||
remoteDir: $scope.volumeAdd.remoteDir,
|
||||
};
|
||||
} else if ($scope.volumeAdd.mountType === 'sshfs') {
|
||||
mountOptions = {
|
||||
host: $scope.volumeAdd.host,
|
||||
port: $scope.volumeAdd.port,
|
||||
remoteDir: $scope.volumeAdd.remoteDir,
|
||||
user: $scope.volumeAdd.user,
|
||||
privateKey: $scope.volumeAdd.privateKey,
|
||||
};
|
||||
} else if ($scope.volumeAdd.mountType === 'ext4') {
|
||||
mountOptions = {
|
||||
diskPath: $scope.volumeAdd.diskPath
|
||||
};
|
||||
}
|
||||
|
||||
var hostPath;
|
||||
if ($scope.volumeAdd.mountType === 'noop') {
|
||||
hostPath = $scope.volumeAdd.hostPath; // settable by user
|
||||
} else {
|
||||
hostPath = '/mnt/volumes/' + $scope.volumeAdd.name; // hardcoded in UI for ease of use
|
||||
}
|
||||
|
||||
Client.addVolume($scope.volumeAdd.name, hostPath, $scope.volumeAdd.mountType, mountOptions, function (error) {
|
||||
$scope.volumeAdd.busy = false;
|
||||
if (error) {
|
||||
$scope.volumeAdd.error = error.message;
|
||||
|
||||
Reference in New Issue
Block a user