Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eef360673b | |||
| 36e298c758 | |||
| 275157f27b | |||
| e776deaa3f | |||
| 4fc8e9b45e | |||
| fe41eec7c5 | |||
| d1d1d22734 | |||
| da8b76957a | |||
| 305f9fd1cf | |||
| cd2a94ddb8 | |||
| a2df4db504 | |||
| b7740a4758 | |||
| 62c24de5c4 | |||
| 5ed3e67b76 | |||
| c7f2314a15 | |||
| 420c7ebd67 | |||
| b93b1a6eec | |||
| 7d52be6e99 | |||
| 9b1f0e394a | |||
| 1b0cb5d455 | |||
| 9b79d59d93 | |||
| 3e12316ea1 | |||
| 1b38c0111f | |||
| 5542393eb5 | |||
| ad48bc0ee8 | |||
| ba0e5d0b59 | |||
| 1c5ff88e3c | |||
| bf7d4a550e | |||
| 324bc763fc | |||
| f9fb2ca3a1 | |||
| b5eac7c91b | |||
| 3c858ca0fd | |||
| da9d634b83 | |||
| 128704400f | |||
| a3594322bd | |||
| fe4b3d5f1d | |||
| da08da2b54 | |||
| 5deb5f79bd | |||
| 9f0d694f0a | |||
| 4153fb7d1e | |||
| 6994ec0f03 | |||
| e1af60cfa9 | |||
| 7bcec61e6d | |||
| dde287f05d | |||
| 27fc37e55c | |||
| ad901760f6 | |||
| 973029865e | |||
| 52e4fedd16 | |||
| b81ba49370 | |||
| 39a0f93f69 | |||
| 53cb83eacc | |||
| b307d278b0 | |||
| 14348eba38 | |||
| cead5b74ae | |||
| 2e2a945f7c | |||
| 0e3ae2b450 | |||
| 19e2df65ca | |||
| 565d715a66 | |||
| abe6f55aa6 | |||
| c278d0c5d4 | |||
| a7e2c74158 | |||
| d84900d601 | |||
| fdda28d67f |
+3
-2
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<!-- this gets changed once we get the config (because angular has not loaded yet, we see template string for a flash) -->
|
||||
<title> Cloudron </title>
|
||||
<title>‎</title>
|
||||
|
||||
<link id="favicon" type="image/png" rel="icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
@@ -134,6 +134,7 @@
|
||||
<li><a href="#/profile"><i class="fa fa-user fa-fw"></i> Profile</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin" class="divider"></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/backups"><i class="fa fa-archive fa-fw"></i> Backups</a></li>
|
||||
<li ng-show="user.role === 'owner'"><a href="#/branding"><i class="fa fa-passport fa-fw"></i> Branding</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/domains"><i class="fa fa-globe fa-fw"></i> Domains & Certs</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/email"><i class="fa fa-envelope fa-fw"></i> Email</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/activity"><i class="fa fa-list-alt fa-fw"></i> Event Log</a></li>
|
||||
@@ -143,7 +144,7 @@
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin" class="divider"></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/support"><i class="fa fa-comment fa-fw"></i> Support</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/system"><i class="fa fa-cogs fa-fw"></i> System Info</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/system"><i class="fa fa-cogs fa-fw"></i> System</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out-alt fa-fw"></i> Logout</a></li>
|
||||
</ul>
|
||||
|
||||
+27
-42
@@ -450,7 +450,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
transformRequest: angular.identity
|
||||
};
|
||||
|
||||
post('/api/v1/settings/cloudron_avatar', fd, config, function (error, data, status) {
|
||||
post('/api/v1/branding/cloudron_avatar', fd, config, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 202) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
@@ -462,7 +462,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
name: name
|
||||
};
|
||||
|
||||
post('/api/v1/settings/cloudron_name', data, null, function (error, data, status) {
|
||||
post('/api/v1/branding/cloudron_name', data, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 202) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
@@ -721,7 +721,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
};
|
||||
|
||||
Client.prototype.setFooter = function (footer, callback) {
|
||||
post('/api/v1/settings/footer', { footer: footer }, null, function (error, data, status) {
|
||||
post('/api/v1/branding/footer', { footer: footer }, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
@@ -730,7 +730,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
};
|
||||
|
||||
Client.prototype.getFooter = function (callback) {
|
||||
get('/api/v1/settings/footer', null, function (error, data, status) {
|
||||
get('/api/v1/branding/footer', null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
@@ -1879,15 +1879,6 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.addMailDomain = function (domain, callback) {
|
||||
post('/api/v1/mail', { domain: domain }, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 201) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.addDomain = function (domain, zoneName, provider, config, fallbackCertificate, tlsConfig, callback) {
|
||||
var data = {
|
||||
domain: domain,
|
||||
@@ -1905,7 +1896,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
if (error) return callback(error);
|
||||
if (status !== 201) return callback(new ClientError(status, data));
|
||||
|
||||
that.addMailDomain(domain, callback);
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1928,24 +1919,6 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.removeMailDomain = function (domain, callback) {
|
||||
var config = {
|
||||
data: {
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
// FIXME
|
||||
del('/api/v1/mail/' + domain, config, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.renewCerts = function (domain, callback) {
|
||||
post('/api/v1/cloudron/renew_certs', { domain: domain || null }, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
@@ -1964,14 +1937,11 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
}
|
||||
};
|
||||
|
||||
this.removeMailDomain(domain, function () {
|
||||
// hack: ignore errors until we fix the domains.js
|
||||
del('/api/v1/domains/' + domain, config, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
del('/api/v1/domains/' + domain, config, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2002,10 +1972,11 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
};
|
||||
|
||||
// Email
|
||||
Client.prototype.getMailEventLogs = function (search, page, perPage, callback) {
|
||||
Client.prototype.getMailEventLogs = function (search, types, page, perPage, callback) {
|
||||
var config = {
|
||||
params: {
|
||||
page: page,
|
||||
types: types,
|
||||
per_page: perPage,
|
||||
search: search
|
||||
}
|
||||
@@ -2097,7 +2068,14 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
|
||||
// Mailboxes
|
||||
Client.prototype.getMailboxes = function (domain, callback) {
|
||||
get('/api/v1/mail/' + domain + '/mailboxes', null, function (error, data, status) {
|
||||
var config = {
|
||||
params: {
|
||||
page: 1,
|
||||
per_page: 1000
|
||||
}
|
||||
};
|
||||
|
||||
get('/api/v1/mail/' + domain + '/mailboxes', config, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
@@ -2160,7 +2138,14 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
};
|
||||
|
||||
Client.prototype.getAliases = function (domain, name, callback) {
|
||||
get('/api/v1/mail/' + domain + '/aliases/' + name, null, function (error, data, status) {
|
||||
var config = {
|
||||
params: {
|
||||
page: 1,
|
||||
per_page: 1000
|
||||
}
|
||||
};
|
||||
|
||||
get('/api/v1/mail/' + domain + '/aliases/' + name, config, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
|
||||
+29
-17
@@ -129,6 +129,9 @@ app.config(['$routeProvider', function ($routeProvider) {
|
||||
}).when('/backups', {
|
||||
controller: 'BackupsController',
|
||||
templateUrl: 'views/backups.html?<%= revision %>'
|
||||
}).when('/branding', {
|
||||
controller: 'BrandingController',
|
||||
templateUrl: 'views/branding.html?<%= revision %>'
|
||||
}).when('/graphs', {
|
||||
controller: 'GraphsController',
|
||||
templateUrl: 'views/graphs.html?<%= revision %>'
|
||||
@@ -278,25 +281,27 @@ app.filter('prettyDomains', function () {
|
||||
};
|
||||
});
|
||||
|
||||
// we use 1024 unit in memory limit in manifest
|
||||
app.filter('prettyMemory', function () {
|
||||
return function (memory) {
|
||||
// Adjust the default memory limit if it changes
|
||||
return memory ? Math.floor(memory / 1024 / 1024) : 256;
|
||||
};
|
||||
});
|
||||
|
||||
// df -H style (si) output
|
||||
app.filter('prettyMailSize', function () {
|
||||
return function (size) {
|
||||
if (!size) return '0 kB';
|
||||
var i = Math.floor(Math.log(size) / Math.log(1024));
|
||||
return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; };
|
||||
var i = Math.floor(Math.log(size) / Math.log(1000));
|
||||
return (size / Math.pow(1000, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; };
|
||||
});
|
||||
|
||||
// df -H style (si) output
|
||||
app.filter('prettyDiskSize', function () {
|
||||
return function (size) {
|
||||
if (!size) return 'Not available yet';
|
||||
var i = Math.floor(Math.log(size) / Math.log(1024));
|
||||
return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; };
|
||||
var i = Math.floor(Math.log(size) / Math.log(1000));
|
||||
return (size / Math.pow(1000, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; };
|
||||
});
|
||||
|
||||
app.filter('installationActive', function () {
|
||||
@@ -578,7 +583,8 @@ app.directive('tagInput', function () {
|
||||
scope: {
|
||||
inputTags: '=taglist'
|
||||
},
|
||||
link: function ($scope, element, attrs) {
|
||||
require: '^form',
|
||||
link: function ($scope, element, attrs, formCtrl) {
|
||||
$scope.defaultWidth = 200;
|
||||
$scope.tagText = ''; // current tag being edited
|
||||
$scope.placeholder = attrs.placeholder;
|
||||
@@ -586,18 +592,20 @@ app.directive('tagInput', function () {
|
||||
if ($scope.inputTags === undefined) {
|
||||
return [];
|
||||
}
|
||||
return $scope.inputTags.split(',').filter(function (tag) {
|
||||
return $scope.inputTags.split(' ').filter(function (tag) {
|
||||
return tag !== '';
|
||||
});
|
||||
};
|
||||
$scope.addTag = function () {
|
||||
var tagArray;
|
||||
if ($scope.tagText.length === 0) {
|
||||
return;
|
||||
var tagArray = $scope.tagArray();
|
||||
|
||||
// prevent adding empty or existing items
|
||||
if ($scope.tagText.length === 0 || tagArray.indexOf($scope.tagText) !== -1) {
|
||||
return $scope.tagText = '';
|
||||
}
|
||||
tagArray = $scope.tagArray();
|
||||
|
||||
tagArray.push($scope.tagText);
|
||||
$scope.inputTags = tagArray.join(',');
|
||||
$scope.inputTags = tagArray.join(' ');
|
||||
return $scope.tagText = '';
|
||||
};
|
||||
$scope.deleteTag = function (key) {
|
||||
@@ -610,7 +618,8 @@ app.directive('tagInput', function () {
|
||||
tagArray.splice(key, 1);
|
||||
}
|
||||
}
|
||||
return $scope.inputTags = tagArray.join(',');
|
||||
formCtrl.$setDirty();
|
||||
return $scope.inputTags = tagArray.join(' ');
|
||||
};
|
||||
$scope.$watch('tagText', function (newVal, oldVal) {
|
||||
var tempEl;
|
||||
@@ -623,6 +632,9 @@ app.directive('tagInput', function () {
|
||||
return tempEl.remove();
|
||||
}
|
||||
});
|
||||
element.bind('click', function () {
|
||||
element[0].firstChild.lastChild.focus();
|
||||
});
|
||||
element.bind('keydown', function (e) {
|
||||
var key = e.which;
|
||||
if (key === 9 || key === 13) {
|
||||
@@ -634,7 +646,7 @@ app.directive('tagInput', function () {
|
||||
});
|
||||
element.bind('keyup', function (e) {
|
||||
var key = e.which;
|
||||
if (key === 9 || key === 13 || key === 32 || key === 188) {
|
||||
if (key === 9 || key === 13 || key === 32) {
|
||||
e.preventDefault();
|
||||
return $scope.$apply('addTag()');
|
||||
}
|
||||
@@ -642,9 +654,9 @@ app.directive('tagInput', function () {
|
||||
},
|
||||
template:
|
||||
'<div class="tag-input-container">' +
|
||||
'<div class="input-tag" data-ng-repeat="tag in tagArray()">' +
|
||||
'{{tag}}' +
|
||||
'<div class="delete-tag" data-ng-click="deleteTag($index)">×</div>' +
|
||||
'<div class="btn-group input-tag" data-ng-repeat="tag in tagArray()">' +
|
||||
'<button type="button" class="btn btn-xs btn-primary" disabled>{{ tag }}</button>' +
|
||||
'<button type="button" class="btn btn-xs btn-primary" data-ng-click="deleteTag($index)">×</button>' +
|
||||
'</div>' +
|
||||
'<input type="text" data-ng-model="tagText" ng-blur="addTag()" placeholder="{{placeholder}}"/>' +
|
||||
'</div>'
|
||||
|
||||
+2
-1
@@ -120,7 +120,7 @@ app.controller('LoginController', ['$scope', '$http', function ($scope, $http) {
|
||||
};
|
||||
|
||||
$scope.showLogin = function () {
|
||||
window.document.title = 'Cloudron Login';
|
||||
if ($scope.status) window.document.title = $scope.status.cloudronName + ' Login';
|
||||
$scope.mode = 'login';
|
||||
$scope.error = false;
|
||||
setTimeout(function () { $('#inputUsername').focus(); }, 200);
|
||||
@@ -137,6 +137,7 @@ app.controller('LoginController', ['$scope', '$http', function ($scope, $http) {
|
||||
|
||||
if (status !== 200) return;
|
||||
|
||||
if ($scope.mode === 'login') window.document.title = data.cloudronName + ' Login';
|
||||
$scope.status = data;
|
||||
}).error(function () {
|
||||
$scope.initialized = false;
|
||||
|
||||
@@ -94,6 +94,7 @@ app.controller('LogsController', ['$scope', 'Client', function ($scope, Client)
|
||||
{ name: 'Docker', type: 'service', value: 'docker', url: Client.makeURL('/api/v1/services/docker/logs') },
|
||||
{ name: 'Unbound', type: 'service', value: 'unbound', url: Client.makeURL('/api/v1/services/unbound/logs') },
|
||||
{ name: 'SFTP', type: 'service', value: 'sftp', url: Client.makeURL('/api/v1/services/sftp/logs') },
|
||||
{ name: 'TURN/STUN', type: 'service', value: 'turn', url: Client.makeURL('/api/v1/services/turn/logs') },
|
||||
];
|
||||
|
||||
$scope.selected = BUILT_IN_LOGS.find(function (e) { return e.value === ids.id; });
|
||||
|
||||
+13
-3
@@ -102,6 +102,7 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
cloudflareTokenType: 'GlobalApiKey',
|
||||
godaddyApiKey: '',
|
||||
godaddyApiSecret: '',
|
||||
linodeToken: '',
|
||||
nameComUsername: '',
|
||||
nameComToken: '',
|
||||
namecheapUsername: '',
|
||||
@@ -184,6 +185,8 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
config.email = $scope.dnsCredentials.cloudflareEmail;
|
||||
config.token = $scope.dnsCredentials.cloudflareToken;
|
||||
config.tokenType = $scope.dnsCredentials.cloudflareTokenType;
|
||||
} else if (provider === 'linode') {
|
||||
config.token = $scope.dnsCredentials.linodeToken;
|
||||
} else if (provider === 'namecom') {
|
||||
config.username = $scope.dnsCredentials.nameComUsername;
|
||||
config.token = $scope.dnsCredentials.nameComToken;
|
||||
@@ -270,9 +273,16 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
// domain is currently like a lock flag
|
||||
if (status.adminFqdn) return waitForDnsSetup();
|
||||
|
||||
if (status.provider === 'digitalocean' || status.provider === 'digitalocean-mp') $scope.dnsCredentials.provider = 'digitalocean';
|
||||
if (status.provider === 'gce') $scope.dnsCredentials.provider = 'gcdns';
|
||||
if (status.provider === 'ami') $scope.dnsCredentials.provider = 'route53';
|
||||
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 === 'gce') {
|
||||
$scope.dnsCredentials.provider = 'gcdns';
|
||||
} else if (status.provider === 'ami') {
|
||||
$scope.dnsCredentials.provider = 'route53';
|
||||
}
|
||||
|
||||
$scope.instanceId = search.instanceId;
|
||||
$scope.provider = status.provider;
|
||||
|
||||
+2
-1
@@ -5,7 +5,8 @@
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src <%= apiOrigin %> 'unsafe-inline' 'unsafe-eval' 'self'; img-src <%= apiOrigin %> 'self'" />
|
||||
|
||||
<title>Cloudron Login</title>
|
||||
<!-- this gets changed once we get the status (because angular has not loaded yet, we see template string for a flash) -->
|
||||
<title>‎</title>
|
||||
|
||||
<link id="favicon" href="<%= apiOrigin %>/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<!-- this gets changed once we get the config (because angular has not loaded yet, we see template string for a flash) -->
|
||||
<title> Logs </title>
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
@@ -163,6 +163,11 @@
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.namecheapApiKey" name="namecheapApiKey" placeholder="Namecheap API Key" ng-required="dnsCredentials.provider === 'namecheap'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Linode -->
|
||||
<p class="small text-warning" ng-show="dnsCredentials.provider === 'linode'">
|
||||
<b>Linode DNS average <a target="_blank" ng-href="{{ config.webServerOrigin }}/documentation/domains/#linode-dns">propagation time</a> is 30 minutes. Cloudron setup & installing apps will take a while.</b>
|
||||
</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>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<!-- this gets changed once we get the config (because angular has not loaded yet, we see template string for a flash) -->
|
||||
<title> Terminal </title>
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
+149
-33
@@ -269,6 +269,10 @@ h1, h2, h3 {
|
||||
left: -999em;
|
||||
}
|
||||
|
||||
textarea {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Apps view
|
||||
// ----------------------------
|
||||
@@ -480,6 +484,15 @@ multiselect {
|
||||
// Mail view
|
||||
// ----------------------------
|
||||
|
||||
.maillog-filter {
|
||||
display: inline-block;
|
||||
|
||||
.form-control {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.email-domain-list-item {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
@@ -1032,7 +1045,7 @@ select.purpose:invalid {
|
||||
color: $brand-danger;
|
||||
|
||||
a {
|
||||
color: $brand-danger;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1153,14 +1166,25 @@ footer {
|
||||
// ----------------------------
|
||||
|
||||
.settings-avatar {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
border: 1px solid gray;
|
||||
border-radius: 3px;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(127, 127, 127 ,0.3);
|
||||
@@ -1256,6 +1280,18 @@ footer {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.users-filter {
|
||||
display: inline-block;
|
||||
padding-left: 0;
|
||||
margin: 20px 0;
|
||||
border-radius: 2px;
|
||||
|
||||
.form-control {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Upgrade
|
||||
// ----------------------------
|
||||
@@ -1316,7 +1352,7 @@ footer {
|
||||
// Eventlog/Activity
|
||||
// ----------------------------
|
||||
|
||||
.filter {
|
||||
.eventlog-filter {
|
||||
display: inline-block;
|
||||
padding-left: 0;
|
||||
margin: 20px 0;
|
||||
@@ -1345,42 +1381,122 @@ footer {
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Branding
|
||||
// ----------------------------
|
||||
|
||||
.branding-avatar {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-position: center;
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
border: 1px solid gray;
|
||||
border-radius: 3px;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(127, 127, 127 ,0.3);
|
||||
background-image: url('/img/plus.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
transition: all 150ms;
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.branding-avatar-selector {
|
||||
text-align: center;
|
||||
|
||||
.grid {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.preview-avatar {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: inline-block;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
transform: scale(1.0);
|
||||
transition: all 150ms;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&.add {
|
||||
border-radius: 2px;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
background-color: $brand-primary;
|
||||
background-image: url('/img/plus.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Tag Input
|
||||
// ----------------------------
|
||||
// https://codepen.io/webmatze/pen/isuHh
|
||||
|
||||
.tag-input-container {
|
||||
input {
|
||||
float: left;
|
||||
height: 18px;
|
||||
padding: 0px;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
color: black;
|
||||
border: 0px;
|
||||
margin: 1px;
|
||||
&:focus {
|
||||
outline: 0;
|
||||
box-shadow: 0px;
|
||||
tag-input {
|
||||
height: auto !important;
|
||||
cursor: text;
|
||||
|
||||
.tag-input-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
input {
|
||||
flex-grow: 1;
|
||||
padding: 0px;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
color: black;
|
||||
border: 0px;
|
||||
margin: 4px 0;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
box-shadow: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.input-tag {
|
||||
padding: 2px 4px;
|
||||
line-height: 12px;
|
||||
font-size: 11px;
|
||||
background-color: #e3eaf6;
|
||||
float: left;
|
||||
border-radius: 2px;
|
||||
margin: 2px 5px 2px 0px;
|
||||
border: 1px solid #a9b6d2;
|
||||
.delete-tag {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
padding: 0px 2px;
|
||||
&:hover {
|
||||
background-color: #96b4d2;
|
||||
|
||||
.input-tag {
|
||||
margin: 2px 0;
|
||||
margin-right: 4px;
|
||||
|
||||
:first-child {
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="filter">
|
||||
<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="Search"/>
|
||||
<multiselect ng-model="selectedActions" ms-header="All Events" 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>
|
||||
|
||||
+62
-36
@@ -26,6 +26,9 @@ angular.module('Application').controller('ActivityController', ['$scope', '$loca
|
||||
{ name: 'app.oom', value: 'app.oom' },
|
||||
{ name: 'app.down', value: 'app.down' },
|
||||
{ name: 'app.up', value: 'app.up' },
|
||||
{ name: 'app.start', value: 'app.start' },
|
||||
{ name: 'app.stop', value: 'app.stop' },
|
||||
{ name: 'app.restart', value: 'app.restart' },
|
||||
{ name: 'Apptask Crash', value: 'app.task.crash' },
|
||||
{ name: 'backup.cleanup', value: 'backup.cleanup.start' },
|
||||
{ name: 'backup.cleanup.finish', value: 'backup.cleanup.finish' },
|
||||
@@ -90,6 +93,9 @@ angular.module('Application').controller('ActivityController', ['$scope', '$loca
|
||||
var ACTION_APP_OOM = 'app.oom';
|
||||
var ACTION_APP_UP = 'app.up';
|
||||
var ACTION_APP_DOWN = 'app.down';
|
||||
var ACTION_APP_START = 'app.start';
|
||||
var ACTION_APP_STOP = 'app.stop';
|
||||
var ACTION_APP_RESTART = 'app.restart';
|
||||
|
||||
var ACTION_BACKUP_FINISH = 'backup.finish';
|
||||
var ACTION_BACKUP_START = 'backup.start';
|
||||
@@ -131,7 +137,11 @@ angular.module('Application').controller('ActivityController', ['$scope', '$loca
|
||||
|
||||
var data = eventLog.data;
|
||||
var errorMessage = data.errorMessage;
|
||||
var details;
|
||||
var details, app;
|
||||
|
||||
function appName(app) {
|
||||
return (app.label || app.fqdn || app.location) + ' (' + app.manifest.title + ')';
|
||||
}
|
||||
|
||||
switch (eventLog.action) {
|
||||
case ACTION_ACTIVATE:
|
||||
@@ -143,74 +153,77 @@ angular.module('Application').controller('ActivityController', ['$scope', '$loca
|
||||
case ACTION_RESTORE:
|
||||
return 'Cloudron was restored using backup ' + data.backupId;
|
||||
|
||||
case ACTION_APP_CONFIGURE:
|
||||
case ACTION_APP_CONFIGURE: {
|
||||
if (!data.app) return '';
|
||||
app = data.app;
|
||||
|
||||
var q = function (x) {
|
||||
return '"' + x + '"';
|
||||
};
|
||||
var name = (data.app.label || data.app.fqdn || data.app.location) + ' (' + data.app.manifest.title + ')';
|
||||
|
||||
if ('accessRestriction' in data) { // since it can be null
|
||||
return 'Access restriction of ' + name + ' was changed';
|
||||
return 'Access restriction of ' + appName(app) + ' was changed';
|
||||
} else if (data.label) {
|
||||
return 'Label of ' + name + ' was set to ' + q(data.label);
|
||||
return 'Label of ' + appName(app) + ' was set to ' + q(data.label);
|
||||
} else if (data.tags) {
|
||||
return 'Tags of ' + name + ' was set to ' + q(data.tags.join(','));
|
||||
return 'Tags of ' + appName(app) + ' was set to ' + q(data.tags.join(','));
|
||||
} else if (data.icon) {
|
||||
return 'Icon of ' + name + ' was changed';
|
||||
return 'Icon of ' + appName(app) + ' was changed';
|
||||
} else if (data.memoryLimit) {
|
||||
return 'Memory limit of ' + name + ' was set to ' + data.memoryLimit;
|
||||
return 'Memory limit of ' + appName(app) + ' was set to ' + data.memoryLimit;
|
||||
} else if (data.cpuShares) {
|
||||
return 'CPU shares of ' + appName(app) + ' was set to ' + Math.round((data.cpuShares * 100)/1024) + '%';
|
||||
} else if (data.env) {
|
||||
return 'Env vars of ' + name + ' was changed';
|
||||
return 'Env vars of ' + appName(app) + ' was changed';
|
||||
} else if ('debugMode' in data) { // since it can be null
|
||||
if (data.debugMode) {
|
||||
return name + ' was placed in repair mode';
|
||||
return appName(app) + ' was placed in repair mode';
|
||||
} else {
|
||||
return name + ' was taken out of repair mode';
|
||||
}
|
||||
} else if (('mailboxName' in data) && data.mailboxName !== data.app.mailboxName) {
|
||||
if (data.mailboxName) {
|
||||
return 'Mailbox of ' + name + ' was set to ' + q(data.mailboxName);
|
||||
} else {
|
||||
return 'Mailbox of ' + name + ' was reset';
|
||||
return appName(app) + ' was taken out of repair mode';
|
||||
}
|
||||
} else if ('enableBackup' in data) {
|
||||
return 'Automatic backups of ' + name + ' was ' + (data.enableBackup ? 'enabled' : 'disabled');
|
||||
return 'Automatic backups of ' + appName(app) + ' was ' + (data.enableBackup ? 'enabled' : 'disabled');
|
||||
} else if ('enableAutomaticUpdate' in data) {
|
||||
return 'Automatic updates of ' + name + ' was ' + (data.enableAutomaticUpdate ? 'enabled' : 'disabled');
|
||||
return 'Automatic updates of ' + appName(app) + ' was ' + (data.enableAutomaticUpdate ? 'enabled' : 'disabled');
|
||||
} else if ('reverseProxyConfig' in data) {
|
||||
return 'Reverse proxy configuration of ' + name + ' was updated';
|
||||
return 'Reverse proxy configuration of ' + appName(app) + ' was updated';
|
||||
} else if ('cert' in data) {
|
||||
if (data.cert) {
|
||||
return 'Custom certificate was set for ' + name;
|
||||
return 'Custom certificate was set for ' + appName(app);
|
||||
} else {
|
||||
return 'Certificate of ' + name + ' was reset';
|
||||
return 'Certificate of ' + appName(app) + ' was reset';
|
||||
}
|
||||
} else if (data.location) {
|
||||
if (data.fqdn !== data.app.fqdn) {
|
||||
return 'Location of ' + name + ' was changed to ' + data.fqdn;
|
||||
return 'Location of ' + appName(app) + ' was changed to ' + data.fqdn;
|
||||
} else if (!angular.equals(data.alternateDomains, data.app.alternateDomains)) {
|
||||
var altFqdns = data.alternateDomains.map(function (a) { return a.fqdn; });
|
||||
return 'Alternate domains of ' + name + ' was ' + (altFqdns.length ? 'set to ' + altFqdns.join(', ') : 'reset');
|
||||
return 'Alternate domains of ' + appName(app) + ' was ' + (altFqdns.length ? 'set to ' + altFqdns.join(', ') : 'reset');
|
||||
} else if (!angular.equals(data.portBindings, data.app.portBindings)) {
|
||||
return 'Port bindings of ' + name + ' was changed';
|
||||
return 'Port bindings of ' + appName(app) + ' was changed';
|
||||
}
|
||||
} else if ('dataDir' in data) {
|
||||
if (data.dataDir) {
|
||||
return 'Data directory of ' + name + ' was set ' + data.dataDir;
|
||||
return 'Data directory of ' + appName(app) + ' was set ' + data.dataDir;
|
||||
} else {
|
||||
return 'Data directory of ' + name + ' was reset';
|
||||
return 'Data directory of ' + appName(app) + ' was reset';
|
||||
}
|
||||
} else if ('icon' in data) {
|
||||
if (data.icon) {
|
||||
return 'Icon of ' + name + ' was set';
|
||||
return 'Icon of ' + appName(app) + ' was set';
|
||||
} else {
|
||||
return 'Icon of ' + name + ' was reset';
|
||||
return 'Icon of ' + appName(app) + ' was reset';
|
||||
}
|
||||
} else if (('mailboxName' in data) && data.mailboxName !== data.app.mailboxName) {
|
||||
if (data.mailboxName) {
|
||||
return 'Mailbox of ' + appName(app) + ' was set to ' + q(data.mailboxName);
|
||||
} else {
|
||||
return 'Mailbox of ' + appName(app) + ' was reset';
|
||||
}
|
||||
}
|
||||
|
||||
return data.app.manifest.title + ' was re-configured at ' + (data.app.fqdn || data.app.location);
|
||||
return appName(app) + ' was re-configured';
|
||||
}
|
||||
|
||||
case ACTION_APP_INSTALL:
|
||||
if (!data.app) return '';
|
||||
@@ -241,24 +254,37 @@ angular.module('Application').controller('ActivityController', ['$scope', '$loca
|
||||
return data.newApp.manifest.title + ' at ' + (data.newApp.fqdn || data.newApp.location) + ' was cloned from ' + (data.oldApp.fqdn || data.oldApp.location) + ' using backup ' + data.backupId + ' with v' + data.oldApp.manifest.version;
|
||||
|
||||
case ACTION_APP_REPAIR:
|
||||
return 'App at ' + data.app.fqdn + ' was repaired';
|
||||
return 'App ' + appName(data.app) + ' was repaired';
|
||||
|
||||
case ACTION_APP_LOGIN:
|
||||
var app = Client.getCachedAppSync(data.appId);
|
||||
case ACTION_APP_LOGIN: {
|
||||
app = Client.getCachedAppSync(data.appId);
|
||||
if (!app) return '';
|
||||
return 'App ' + app.fqdn + ' logged in';
|
||||
}
|
||||
|
||||
case ACTION_APP_OOM:
|
||||
if (!data.app) return '';
|
||||
return data.app.manifest.title + ' ran out of memory';
|
||||
return appName(data.app) + ' ran out of memory';
|
||||
|
||||
case ACTION_APP_DOWN:
|
||||
if (!data.app) return '';
|
||||
return data.app.manifest.title + ' is down';
|
||||
return appName(data.app) + ' is down';
|
||||
|
||||
case ACTION_APP_UP:
|
||||
if (!data.app) return '';
|
||||
return data.app.manifest.title + ' is back online';
|
||||
return appName(data.app) + ' is back online';
|
||||
|
||||
case ACTION_APP_START:
|
||||
if (!data.app) return '';
|
||||
return appName(data.app) + ' was started';
|
||||
|
||||
case ACTION_APP_STOP:
|
||||
if (!data.app) return '';
|
||||
return appName(data.app) + ' was stopped';
|
||||
|
||||
case ACTION_APP_RESTART:
|
||||
if (!data.app) return '';
|
||||
return appName(data.app) + ' was restarted';
|
||||
|
||||
case ACTION_BACKUP_START:
|
||||
return 'Backup started';
|
||||
|
||||
+64
-49
@@ -155,7 +155,7 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageProvider">Storage provider <sup><a ng-href="{{ config.webServerOrigin }}/documentation/backups/#storage-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" id="storageProvider" ng-model="importBackup.provider" ng-options="a.value as a.name for a in storageProvider" ng-change=importBackup.clearForm()></select>
|
||||
<select class="form-control" id="storageProvider" ng-model="importBackup.provider" ng-options="a.value as a.name for a in storageProvider" ng-change="importBackup.clearForm()" ng-disabled="importBackup.busy"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.key }">
|
||||
@@ -240,7 +240,7 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageFormat">Storage Format <sup><a ng-href="{{ config.webServerOrigin }}/documentation/backups/#backup-formats" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" id="storageFormat" ng-change="importBackup.key = ''" ng-model="importBackup.format" ng-options="a.value as a.name for a in formats"></select>
|
||||
<select class="form-control" id="storageFormat" ng-change="importBackup.key = ''" ng-model="importBackup.format" ng-options="a.value as a.name for a in formats" ng-disabled="importBackup.busy"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.key }">
|
||||
@@ -254,7 +254,7 @@
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="importBackup.submit()" ng-disabled="importBackupForm.$invalid || importBackup.busy"><i class="fa fa-circle-notch fa-spin" ng-show="importBackup.busy"></i><span>Import</span></button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="importBackup.submit()" ng-disabled="importBackupForm.$invalid || importBackup.busy"><i class="fa fa-circle-notch fa-spin" ng-show="importBackup.busy"></i> Import</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -340,8 +340,8 @@
|
||||
</div>
|
||||
<div class="modal-body" style="padding: 0 15px">
|
||||
<p>Using backup from <b>{{ clone.backup.creationTime | prettyDate }}</b> and version <b>v{{ clone.backup.version }}</b></p>
|
||||
<fieldset>
|
||||
<form role="form" ng-submit="clone.submit()" autocomplete="off">
|
||||
<form role="form" ng-submit="clone.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': clone.error.location }">
|
||||
<label class="control-label" for="cloneLocationInput">Location</label>
|
||||
<div ng-show="clone.error.location"><small>{{ clone.error.location }}</small></div>
|
||||
@@ -361,6 +361,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="small text-center text-warning" ng-show="clone.domain.provider === 'linode'">
|
||||
<b>Linode DNS average <a target="_blank" ng-href="{{ config.webServerOrigin }}/documentation/domains/#linode-dns">propagation time</a> is 30 minutes.
|
||||
Cloning the app will take a while.</b>
|
||||
<br>
|
||||
</p>
|
||||
|
||||
<p class="text-center" ng-show="clone.location && clone.domain.provider === 'manual'">
|
||||
<b>Add an A record manually for {{ clone.location }} to this Cloudron's public IP</b>
|
||||
<br>
|
||||
@@ -380,8 +386,8 @@
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
@@ -398,7 +404,7 @@
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="row" ng-show="view">
|
||||
<div class="col-sm-2 text-center">
|
||||
<img ng-src="{{ app.iconUrl || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)" class="app-icon"/>
|
||||
</div>
|
||||
@@ -427,15 +433,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-sm-offset-2" style="height: 10px;">
|
||||
<div class="progress progress-striped active animateMeOpacity" ng-show="app.taskId" style="height: 10px;">
|
||||
<div class="row" ng-show="app.taskId">
|
||||
<div class="col-sm-8 col-sm-offset-2" style="height: 10px; display: flex; align-items: center;">
|
||||
<div class="progress progress-striped active animateMeOpacity" style="height: 10px; flex-grow: 1;">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.taskProgress }}%"></div>
|
||||
</div>
|
||||
<div ng-show="app.taskMinutesActive >= 5" class="text-danger hand" style="margin: 0 4px;" ng-click="stopAppTask(app.taskId)" uib-tooltip="Cancel Task"><i class="fas fa-times"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row app-configure-links-container">
|
||||
<div class="row" ng-hide="view">
|
||||
<div class="col-md-12 text-center">
|
||||
<br/><br/><h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row app-configure-links-container" ng-show="view">
|
||||
<div class="col-sm-2">
|
||||
<div class="app-configure-links">
|
||||
<div ng-click="setView('display')" ng-class="{ 'active': view === 'display' }">Display</div>
|
||||
@@ -455,8 +467,8 @@
|
||||
<div class="card" ng-show="view === 'display'">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<fieldset>
|
||||
<form role="form" name="displayForm" ng-submit="display.submit()" autocomplete="off">
|
||||
<form role="form" name="displayForm" ng-submit="display.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': !displayForm.label.$dirty && display.error.label }">
|
||||
<label class="control-label">Label</label>
|
||||
<div class="control-label" ng-show="display.error.label">{{display.error.label}}</div>
|
||||
@@ -464,7 +476,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Tags</label>
|
||||
<tag-input class="form-control" placeholder="Use comma to separate tags" taglist="display.tags" name="tags" uib-tooltip="For grouping in the dashboard"></tag-input>
|
||||
<tag-input class="form-control" placeholder="Use space to separate tags" taglist="display.tags" name="tags" uib-tooltip="For grouping in the dashboard"></tag-input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div>
|
||||
@@ -479,8 +491,8 @@
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="(!display.icon.data && !displayForm.$dirty) || displayForm.$invalid || display.busy"/>
|
||||
</form>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -493,8 +505,8 @@
|
||||
<div class="card" ng-show="view === 'location'">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<fieldset>
|
||||
<form role="form" name="locationForm" ng-submit="location.submit()" autocomplete="off">
|
||||
<form role="form" name="locationForm" ng-submit="location.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': (locationForm.location.$dirty && locationForm.location.$invalid) || (!locationForm.location.$dirty && location.error.location) }">
|
||||
<label class="control-label">Location</label>
|
||||
<div class="has-error" ng-show="location.error.location">{{ location.error.location }}</div>
|
||||
@@ -516,6 +528,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="small text-center text-warning" ng-show="location.domain.provider === 'linode'">
|
||||
<b>Linode DNS average <a target="_blank" ng-href="{{ config.webServerOrigin }}/documentation/domains/#linode-dns">propagation time</a> is 30 minutes.
|
||||
Changing the location will take a while.</b>
|
||||
<br>
|
||||
</p>
|
||||
|
||||
<p class="text-center" ng-show="location.location && location.domain.provider === 'manual'">
|
||||
<b>Add an A record manually for {{ location.location }} to this Cloudron's public IP</b>
|
||||
<br>
|
||||
@@ -572,8 +590,8 @@
|
||||
<a href="" ng-click="location.addAlternateDomain($event)">Add another domain</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -588,8 +606,8 @@
|
||||
<div class="card" ng-show="view === 'access'">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<fieldset>
|
||||
<form role="form" name="accessForm" ng-submit="access.submit()" autocomplete="off">
|
||||
<form role="form" name="accessForm" ng-submit="access.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<div ng-show="access.ssoAuth">
|
||||
<label class="control-label">User management</label>
|
||||
@@ -637,8 +655,8 @@
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="(access.accessRestrictionOption === 'groups' && !access.isAccessRestrictionValid()) || accessForm.$invalid || access.busy"/>
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -660,8 +678,8 @@
|
||||
<div class="card" ng-show="view === 'resources'">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<fieldset>
|
||||
<form role="form" name="resourcesForm" ng-submit="resources.submitMemoryLimit()" autocomplete="off">
|
||||
<form role="form" name="resourcesForm" ng-submit="resources.submitMemoryLimit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="memoryLimit">Memory Limit <sup><a ng-href="{{ config.webServerOrigin }}/documentation/apps/#memory-limit" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup> : <b>{{ resources.memoryLimit ? resources.memoryLimit / 1024 / 1024 + 'MB' : 'Default (256 MB)' }}</b></label>
|
||||
<p>Cloudron allocates 50% of this value as RAM and 50% as swap.</p>
|
||||
@@ -671,8 +689,8 @@
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="resources.memoryLimit === resources.currentMemoryLimit || resourcesForm.$invalid || resources.busy"/>
|
||||
</form>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -685,8 +703,8 @@
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<fieldset>
|
||||
<form role="form" name="resourcesForm" ng-submit="resources.submitCpuShares()" autocomplete="off">
|
||||
<form role="form" name="resourcesForm" ng-submit="resources.submitCpuShares()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="cpuShares">CPU Shares <sup><a ng-href="{{ config.webServerOrigin }}/documentation/apps/#cpu-shares" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup> : <b>{{ (resources.cpuShares * 100 / 1024 | number:0) + ' %' }}</b></label>
|
||||
<p>Percent of CPU time when system is under heavy load.</p>
|
||||
@@ -696,8 +714,8 @@
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="resources.cpuShares === resources.currentCpuShares || resourcesForm.$invalid || resources.busyCpuShares"/>
|
||||
</form>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -713,18 +731,18 @@
|
||||
<label class="control-label" for="resourcesEnableDataDir">Storage <sup><a ng-href="{{ config.webServerOrigin }}/documentation/storage/#app-data-directory" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<p>
|
||||
By default, this app's data is located at <code>/home/yellowtent/appsdata/{{ app.id }}</code>. If the server is running out of disk space,
|
||||
you can mount an external disk and move this app's data there.
|
||||
you can mount an external disk and move this app's data there. Only Ext4 and NFS mounts are supported.
|
||||
</p>
|
||||
<fieldset>
|
||||
<form role="form" name="resourcesDataDirForm" ng-submit="resources.submitDataDir()" autocomplete="off">
|
||||
<form role="form" name="resourcesDataDirForm" ng-submit="resources.submitDataDir()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': resourcesDataDirForm.$dirty && resources.error.dataDir }">
|
||||
<div ng-show="resources.error.dataDir">{{ resources.error.dataDir }}</div>
|
||||
<input type="text" class="form-control" name="dataDir" placeholder="Leave empty to use platform default" ng-model="resources.dataDir">
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="!resourcesDataDirForm.$dirty || resourcesDataDirForm.$invalid || resources.busyDataDir"/>
|
||||
</form>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -742,8 +760,8 @@
|
||||
<label class="control-label" for="emailMailboxNameEnabled">Mail FROM Address</label>
|
||||
<p>This sets the address from which this app sends email. This app is already configured to send mail using {{app.domain}}'s <a ng-href="/#/email/{{ app.domain }}">Outbound Email</a> settings.</p>
|
||||
|
||||
<fieldset>
|
||||
<form role="form" name="emailForm" ng-submit="email.submit()" autocomplete="off">
|
||||
<form role="form" name="emailForm" ng-submit="email.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<!-- 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>
|
||||
@@ -766,8 +784,8 @@
|
||||
<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>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -782,8 +800,8 @@
|
||||
<div class="card" ng-show="view === 'security'">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<fieldset>
|
||||
<form role="form" name="securityForm" ng-submit="security.submit()" autocomplete="off">
|
||||
<form role="form" name="securityForm" ng-submit="security.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="control-label" style="width: 100%">Robots.txt <sup><a ng-href="{{ config.webServerOrigin }}/documentation/apps/#robotstxt" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup> <a href="" class="pull-right" style="font-weight: normal;" ng-click="security.robotsTxt = ROBOTS_DISABLE_INDEXING_TEMPLATE">Disable indexing</a></label>
|
||||
<textarea ng-model="security.robotsTxt" placeholder="Leave empty to allow all bots to index this app" class="form-control" rows="4"></textarea>
|
||||
@@ -796,8 +814,8 @@
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="securityForm.$invalid || security.busy"/>
|
||||
</form>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
@@ -947,9 +965,6 @@
|
||||
<p>If a configuration, update, restore or backup action resulted in an error, you can retry the task.</p>
|
||||
<p ng-show="app.error">An error occurred during the <b>{{ app.error.installationState | taskName }}</b> operation: <span class="text-danger"><b>{{ app.error.reason + ': ' + app.error.message }}</b></span></p>
|
||||
<button class="btn btn-primary pull-right" ng-click="repair.confirm()" ng-disabled="app.taskId || !app.error" tooltip-enable="app.taskId" uib-tooltip="App is busy">Retry {{ app.error.installationState | taskName }}</button>
|
||||
|
||||
<!-- this is hidden for now, use the CLI instead -->
|
||||
<button class="btn btn-danger pull-right" ng-click="repair.stopAppTask(app.taskId)" ng-show="false && app.taskId">Cancel Current Task</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+13
-9
@@ -7,6 +7,7 @@
|
||||
/* global RSTATES */
|
||||
/* global ISTATES */
|
||||
/* global ERROR */
|
||||
/* global moment */
|
||||
|
||||
angular.module('Application').controller('AppController', ['$scope', '$location', '$timeout', '$interval', '$route', '$routeParams', 'Client', function ($scope, $location, $timeout, $interval, $route, $routeParams, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
@@ -116,6 +117,13 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
$scope.view = view;
|
||||
};
|
||||
|
||||
$scope.stopAppTask = function (taskId) {
|
||||
Client.stopTask(taskId, function (error) {
|
||||
// we can ignore a call trying to cancel an already done task
|
||||
if (error && error.statusCode !== 409) Client.error(error);
|
||||
});
|
||||
},
|
||||
|
||||
$scope.postInstallMessage = {
|
||||
confirmed: false,
|
||||
openApp: false,
|
||||
@@ -173,7 +181,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
$scope.display.error = {};
|
||||
|
||||
// translate for tag-input
|
||||
$scope.display.tags = app.tags ? app.tags.join(',') : '';
|
||||
$scope.display.tags = app.tags ? app.tags.join(' ') : '';
|
||||
|
||||
$scope.display.label = $scope.app.label || '';
|
||||
$scope.display.icon = { data: null };
|
||||
@@ -204,7 +212,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
configureLabel(function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
var tags = $scope.display.tags.split(',').map(function (t) { return t.trim(); }).filter(function (t) { return !!t; });
|
||||
var tags = $scope.display.tags.split(' ').map(function (t) { return t.trim(); }).filter(function (t) { return !!t; });
|
||||
|
||||
var configureTags = angular.equals(tags, $scope.app.tags) ? NOOP : Client.configureApp.bind(null, $scope.app.id, 'tags', { tags: tags });
|
||||
|
||||
@@ -1194,6 +1202,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
}
|
||||
|
||||
repairFunc(function (error) {
|
||||
$scope.repair.retryBusy = false;
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$scope.repair.retryBusy = false;
|
||||
@@ -1201,13 +1210,6 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
});
|
||||
},
|
||||
|
||||
stopAppTask: function (taskId) {
|
||||
Client.stopTask(taskId, function (error) {
|
||||
// we can ignore a call trying to cancel an already done task
|
||||
if (error && error.statusCode !== 409) Client.error(error);
|
||||
});
|
||||
},
|
||||
|
||||
restartBusy: false,
|
||||
restartApp: function () {
|
||||
$scope.repair.restartBusy = true;
|
||||
@@ -1349,12 +1351,14 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
|
||||
$scope.app.taskProgress = task && task.percent ? task.percent : 5; // start with 5 to avoid empty progress bar
|
||||
$scope.app.taskProgressMessage = task ? task.message : '';
|
||||
$scope.app.taskMinutesActive = task ? moment.duration(moment.utc().diff(moment.utc(task.creationTime))).asMinutes() : 0;
|
||||
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
$scope.app.taskProgress = 0;
|
||||
$scope.app.taskProgressMessage = '';
|
||||
$scope.app.taskMinutesActive = 0;
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
@@ -36,6 +36,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="small text-center text-warning" ng-show="appInstall.domain.provider === 'linode'">
|
||||
<b>Linode DNS average <a target="_blank" ng-href="{{ config.webServerOrigin }}/documentation/domains/#linode-dns">propagation time</a> is 30 minutes.
|
||||
Installing the app will take a while.</b>
|
||||
<br>
|
||||
</p>
|
||||
|
||||
<p class="text-center" ng-show="appInstall.location && appInstall.domain.provider === 'manual'">
|
||||
<b>Add an A record manually for {{ appInstall.location }} to this Cloudron's public IP</b>
|
||||
<br>
|
||||
|
||||
+10
-3
@@ -23,8 +23,6 @@
|
||||
<h4 class="modal-title">Configure Backup Storage</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Cloudron makes a complete backup of your system based on this configuration.</p>
|
||||
|
||||
<form name="configureBackupForm" role="form" novalidate ng-submit="configureBackup.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="configureBackup.error">{{ configureBackup.error.generic }}</p>
|
||||
@@ -176,6 +174,15 @@
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<p>Cloudron makes a complete backup of your system based on this configuration.
|
||||
<span ng-show="manualBackupApps.length">
|
||||
The following apps have automatic backups disabled:
|
||||
<span ng-repeat="app in manualBackupApps">
|
||||
<a ng-href="/#/app/{{app.id}}/backups">{{app.label || app.fqdn}}</a><span ng-hide="$last">,</span>
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Provider</span>
|
||||
@@ -244,7 +251,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="configureBackup.show()" ng-disabled="createBackup.busy">Configure</button>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-show="user.role === 'owner'" ng-click="configureBackup.show()" ng-disabled="createBackup.busy">Configure</button>
|
||||
<button class="btn btn-outline btn-primary" ng-click="createBackup.startBackup()" ng-show="!createBackup.busy" style="margin-right: 10px">Backup now</button>
|
||||
<button class="btn btn-outline btn-danger" ng-click="createBackup.stopBackup()" ng-show="createBackup.busy" style="margin-right: 10px">Stop Backup</button>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,8 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
|
||||
$scope.manualBackupApps = [];
|
||||
|
||||
$scope.backupConfig = {};
|
||||
$scope.lastBackup = null;
|
||||
$scope.backups = [];
|
||||
@@ -419,6 +421,8 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
fetchBackups();
|
||||
getBackupConfig();
|
||||
|
||||
$scope.manualBackupApps = Client.getInstalledApps().filter(function (app) { return !app.enableBackup; });
|
||||
|
||||
// show backup status
|
||||
$scope.createBackup.checkStatus();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<!-- Modal change avatar -->
|
||||
<div class="modal fade" id="avatarChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Choose Cloudron Avatar</h4>
|
||||
</div>
|
||||
<div class="modal-body branding-avatar-selector">
|
||||
<img id="previewAvatar" width="128" height="128" ng-src="{{ avatarChange.avatarUrl() }}"/>
|
||||
<input type="file" id="avatarFileInput" style="display: none" accept="image/png"/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="grid">
|
||||
<div class="item" ng-repeat="avatar in avatarChange.availableAvatars" style="background-image: url('{{avatar.data || avatar.url}}');" ng-click="avatarChange.setPreviewAvatar(avatar)"></div>
|
||||
<div class="item add" ng-click="avatarChange.showCustomAvatarSelector()"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="avatarChange.setAvatar()"> Select</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>Branding</h1>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form role="form" name="aboutForm" ng-submit="about.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': (aboutForm.name.$dirty && aboutForm.name.$invalid) }">
|
||||
<label class="control-label">Cloudron Name</label>
|
||||
<div class="control-label" ng-show="about.error.cloudronName">{{about.error.cloudronName}}</div>
|
||||
<input type="text" class="form-control" id="inputCloudronName" name="name" ng-model="about.cloudronName">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<label class="control-label">Logo</label>
|
||||
</div>
|
||||
<div class="branding-avatar" ng-click="avatarChange.showChangeAvatar()">
|
||||
<img ng-src="{{ about.avatarUrl() }}"/>
|
||||
<div class="overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="(!about.avatar && !aboutForm.$dirty) || aboutForm.$invalid || about.busy"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="about.submit()" ng-disabled="(!about.avatar && !aboutForm.$dirty) || aboutForm.$invalid || about.busy"><i class="fa fa-circle-notch fa-spin" ng-show="about.busy"></i> Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>Footer</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-hide="config.features.branding">Customizing the footer is only available in the business plan.</p>
|
||||
|
||||
<div ng-show="config.features.branding">
|
||||
<form role="form" name="footerForm" autocomplete="off">
|
||||
<p">Use <a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank">markdown</a> to style the footer.</p>
|
||||
<textarea name="footer" class="form-control" ng-model="footer.content" ng-disabled="footer.busy"></textarea>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="footer.submit()" ng-disabled="!footerForm.$dirty || footerForm.$invalid || footer.busy"><i class="fa fa-circle-notch fa-spin" ng-show="footer.busy"></i> Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,226 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
|
||||
angular.module('Application').controller('BrandingController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (Client.getUserInfo().role !== 'owner') $location.path('/'); });
|
||||
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
$scope.avatarChange = {
|
||||
avatar: null, // { file, data, url }
|
||||
|
||||
availableAvatars: [{
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo.png',
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-green.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-orange.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-darkblue.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-red.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-yellow.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-black.png'
|
||||
}],
|
||||
|
||||
avatarUrl: function () {
|
||||
if ($scope.avatarChange.avatar) {
|
||||
return $scope.avatarChange.avatar.data || $scope.avatarChange.avatar.url;
|
||||
} else {
|
||||
return Client.avatar;
|
||||
}
|
||||
},
|
||||
|
||||
getBlobFromImg: function (img, callback) {
|
||||
var size = 256;
|
||||
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
|
||||
var imageDimensionRatio = img.width / img.height;
|
||||
var canvasDimensionRatio = canvas.width / canvas.height;
|
||||
var renderableHeight, renderableWidth, xStart, yStart;
|
||||
|
||||
if (imageDimensionRatio > canvasDimensionRatio) {
|
||||
renderableHeight = canvas.height;
|
||||
renderableWidth = img.width * (renderableHeight / img.height);
|
||||
xStart = (canvas.width - renderableWidth) / 2;
|
||||
yStart = 0;
|
||||
} else if (imageDimensionRatio < canvasDimensionRatio) {
|
||||
renderableWidth = canvas.width;
|
||||
renderableHeight = img.height * (renderableWidth / img.width);
|
||||
xStart = 0;
|
||||
yStart = (canvas.height - renderableHeight) / 2;
|
||||
} else {
|
||||
renderableHeight = canvas.height;
|
||||
renderableWidth = canvas.width;
|
||||
xStart = 0;
|
||||
yStart = 0;
|
||||
}
|
||||
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(img, xStart, yStart, renderableWidth, renderableHeight);
|
||||
|
||||
canvas.toBlob(callback);
|
||||
},
|
||||
|
||||
setPreviewAvatar: function (avatar) {
|
||||
$scope.avatarChange.avatar = avatar;
|
||||
},
|
||||
|
||||
showChangeAvatar: function () {
|
||||
$scope.avatarChange.avatar = $scope.about.avatar;
|
||||
$('#avatarChangeModal').modal('show');
|
||||
},
|
||||
|
||||
showCustomAvatarSelector: function () {
|
||||
$('#avatarFileInput').click();
|
||||
},
|
||||
|
||||
setAvatar: function () {
|
||||
if (angular.equals($scope.about.avatar, $scope.avatarChange.avatar)) return $('#avatarChangeModal').modal('hide'); // nothing changed
|
||||
|
||||
$scope.about.avatar = $scope.avatarChange.avatar;
|
||||
|
||||
// get the blob now, we cannot get it if dialog is hidden
|
||||
var img = document.getElementById('previewAvatar');
|
||||
$scope.avatarChange.getBlobFromImg(img, function (blob) {
|
||||
$scope.about.avatarBlob = blob;
|
||||
|
||||
$('#avatarChangeModal').modal('hide');
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
$('#avatarFileInput').get(0).onchange = function (event) {
|
||||
var fr = new FileReader();
|
||||
fr.onload = function () {
|
||||
$scope.$apply(function () {
|
||||
var tmp = {
|
||||
file: event.target.files[0],
|
||||
data: fr.result,
|
||||
url: null
|
||||
};
|
||||
|
||||
$scope.avatarChange.availableAvatars.push(tmp);
|
||||
$scope.avatarChange.setPreviewAvatar(tmp);
|
||||
});
|
||||
};
|
||||
fr.readAsDataURL(event.target.files[0]);
|
||||
};
|
||||
|
||||
$scope.about = {
|
||||
busy: false,
|
||||
error: {},
|
||||
cloudronName: '',
|
||||
avatar: null,
|
||||
avatarBlob: null,
|
||||
|
||||
avatarUrl: function () {
|
||||
if ($scope.about.avatar) {
|
||||
return $scope.about.avatar.data || $scope.about.avatar.url;
|
||||
} else {
|
||||
return Client.avatar;
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
$scope.about.cloudronName = $scope.config.cloudronName;
|
||||
$scope.about.avatar = null;
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.about.error.name = null;
|
||||
$scope.about.busy = true;
|
||||
|
||||
var NOOP = function (next) { return next(); };
|
||||
var changeCloudronName = $scope.about.cloudronName !== $scope.config.cloudronName ? Client.changeCloudronName.bind(null, $scope.about.cloudronName) : NOOP;
|
||||
|
||||
changeCloudronName(function (error) {
|
||||
if (error) {
|
||||
$scope.about.busy = false;
|
||||
if (error.statusCode === 400) {
|
||||
$scope.about.error.cloudronName = 'Invalid name';
|
||||
$scope.about.cloudronName = '';
|
||||
$('#inputCloudronName').focus();
|
||||
} else {
|
||||
console.error('Unable to change name.', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var changeAvatar = $scope.about.avatar ? Client.changeCloudronAvatar.bind(null, $scope.about.avatarBlob) : NOOP;
|
||||
|
||||
changeAvatar(function (error) {
|
||||
if (error) {
|
||||
$scope.about.busy = false;
|
||||
console.error('Unable to change avatar.', error);
|
||||
return;
|
||||
}
|
||||
|
||||
Client.refreshConfig(function () {
|
||||
if ($scope.about.avatar) Client.resetAvatar();
|
||||
|
||||
$scope.aboutForm.$setPristine();
|
||||
$scope.about.avatar = null;
|
||||
$scope.about.busy = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.footer = {
|
||||
content: '',
|
||||
busy: false,
|
||||
|
||||
refresh: function () {
|
||||
Client.getFooter(function (error, result) {
|
||||
if (error) return console.error('Failed to get footer.', error);
|
||||
|
||||
$scope.footer.content = result;
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.footer.busy = true;
|
||||
|
||||
Client.setFooter($scope.footer.content.trim(), function (error) {
|
||||
if (error) return console.error('Failed to set footer.', error);
|
||||
|
||||
Client.refreshConfig(function () {
|
||||
$scope.footer.busy = false;
|
||||
$scope.footerForm.$setPristine();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.about.refresh();
|
||||
$scope.footer.refresh();
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
+11
-1
@@ -84,6 +84,12 @@
|
||||
<input type="email" class="form-control" ng-model="domainConfigure.cloudflareEmail" name="cloudflareEmail" placeholder="Cloudflare Account Email" ng-required="domainConfigure.provider === 'cloudflare' && domainConfigure.cloudflareTokenType === 'GlobalApiKey'" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
|
||||
<!-- Linode -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'linode'">
|
||||
<label class="control-label">Linode Token</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.linodeToken" name="linodeToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'linode'">
|
||||
</div>
|
||||
|
||||
<!-- Name.com -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecom'">
|
||||
<label class="control-label">Name.com Username</label>
|
||||
@@ -101,12 +107,16 @@
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecheap'">
|
||||
<label class="control-label">API Key</label>
|
||||
<p class="small text-info" ng-show="domainConfigure.provider === 'namecheap'">
|
||||
<p class="small text-info">
|
||||
<b>The server IP needs to be whitelisted for this API Key.</b>
|
||||
</p>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.namecheapApiKey" name="namecheapApiKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'namecheap'">
|
||||
</div>
|
||||
|
||||
<p class="small text-warning" ng-show="domainConfigure.provider === 'linode'">
|
||||
<b>Linode DNS average <a target="_blank" ng-href="{{ config.webServerOrigin }}/documentation/domains/#linode-dns">propagation time</a> is 30 minutes. Installing apps & and getting a Let's Encrypt certificate will take a while.</b>
|
||||
</p>
|
||||
|
||||
<p class="small text-info" ng-show="domainConfigure.provider === 'wildcard'">
|
||||
Setup <i>A</i> records for <b>*.{{ domainConfigure.newDomain || domainConfigure.domain.domain }}</b> and <b>{{ domainConfigure.newDomain || domainConfigure.domain.domain }}</b> to this server's IP.
|
||||
</p>
|
||||
|
||||
+24
-16
@@ -28,6 +28,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
{ name: 'Gandi LiveDNS', value: 'gandi' },
|
||||
{ name: 'GoDaddy', value: 'godaddy' },
|
||||
{ name: 'Google Cloud DNS', value: 'gcdns' },
|
||||
{ name: 'Linode', value: 'linode' },
|
||||
{ name: 'Name.com', value: 'namecom' },
|
||||
{ name: 'Namecheap', value: 'namecheap' },
|
||||
{ name: 'Wildcard', value: 'wildcard' },
|
||||
@@ -37,19 +38,20 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
|
||||
$scope.prettyProviderName = function (domain) {
|
||||
switch (domain.provider) {
|
||||
case 'caas': return 'Managed Cloudron';
|
||||
case 'route53': return 'AWS Route53';
|
||||
case 'cloudflare': return 'Cloudflare';
|
||||
case 'digitalocean': return 'DigitalOcean';
|
||||
case 'gandi': return 'Gandi LiveDNS';
|
||||
case 'namecom': return 'Name.com';
|
||||
case 'namecheap': return 'Namecheap';
|
||||
case 'gcdns': return 'Google Cloud';
|
||||
case 'godaddy': return 'GoDaddy';
|
||||
case 'manual': return 'Manual';
|
||||
case 'wildcard': return 'Wildcard';
|
||||
case 'noop': return 'No-op';
|
||||
default: return 'Unknown';
|
||||
case 'caas': return 'Managed Cloudron';
|
||||
case 'route53': return 'AWS Route53';
|
||||
case 'cloudflare': return 'Cloudflare';
|
||||
case 'digitalocean': return 'DigitalOcean';
|
||||
case 'gandi': return 'Gandi LiveDNS';
|
||||
case 'linode': return 'Linode';
|
||||
case 'namecom': return 'Name.com';
|
||||
case 'namecheap': return 'Namecheap';
|
||||
case 'gcdns': return 'Google Cloud';
|
||||
case 'godaddy': return 'GoDaddy';
|
||||
case 'manual': return 'Manual';
|
||||
case 'wildcard': return 'Wildcard';
|
||||
case 'noop': return 'No-op';
|
||||
default: return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -119,6 +121,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
cloudflareToken: '',
|
||||
cloudflareEmail: '',
|
||||
cloudflareTokenType: 'GlobalApiKey',
|
||||
linodeToken: '',
|
||||
nameComToken: '',
|
||||
nameComUsername: '',
|
||||
namecheapUsername: '',
|
||||
@@ -160,12 +163,15 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.domainConfigure.gcdnsKey.content = '';
|
||||
if (domain.provider === 'gcdns') {
|
||||
$scope.domainConfigure.gcdnsKey.keyFileName = domain.config.credentials && domain.config.credentials.client_email;
|
||||
|
||||
$scope.domainConfigure.gcdnsKey.content = JSON.stringify({
|
||||
project_id: domain.config.projectId,
|
||||
credentials: domain.config.credentials
|
||||
client_email: domain.config.credentials.client_email,
|
||||
private_key: domain.config.credentials.private_key
|
||||
});
|
||||
}
|
||||
$scope.domainConfigure.digitalOceanToken = domain.provider === 'digitalocean' ? domain.config.token : '';
|
||||
$scope.domainConfigure.linodeToken = domain.provider === 'linode' ? 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 : '';
|
||||
@@ -212,8 +218,8 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
var serviceAccountKey = JSON.parse($scope.domainConfigure.gcdnsKey.content);
|
||||
data.projectId = serviceAccountKey.project_id;
|
||||
data.credentials = {
|
||||
client_email: serviceAccountKey.credentials.client_email,
|
||||
private_key: serviceAccountKey.credentials.private_key
|
||||
client_email: serviceAccountKey.client_email,
|
||||
private_key: serviceAccountKey.private_key
|
||||
};
|
||||
|
||||
if (!data.projectId || !data.credentials || !data.credentials.client_email || !data.credentials.private_key) {
|
||||
@@ -226,6 +232,8 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
}
|
||||
} else if (provider === 'digitalocean') {
|
||||
data.token = $scope.domainConfigure.digitalOceanToken;
|
||||
} else if (provider === 'linode') {
|
||||
data.token = $scope.domainConfigure.linodeToken;
|
||||
} else if (provider === 'gandi') {
|
||||
data.token = $scope.domainConfigure.gandiApiKey;
|
||||
} else if (provider === 'godaddy') {
|
||||
|
||||
@@ -318,6 +318,7 @@
|
||||
{{ mailbox.ownerDisplayName }}
|
||||
</td>
|
||||
<td class="hand" ng-click="mailboxes.edit.show(mailbox)">
|
||||
<!-- aliases is spaces separated, so it will wrap as needed -->
|
||||
{{ mailbox.aliases }}
|
||||
</td>
|
||||
<td class="hand" ng-click="mailboxes.edit.show(mailbox)">
|
||||
|
||||
+5
-9
@@ -299,14 +299,10 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.mailboxes.add.reset();
|
||||
$scope.mailboxes.refresh(function (error) {
|
||||
if (error) return console.error(error);
|
||||
$scope.mailboxes.refresh();
|
||||
$scope.catchall.refresh();
|
||||
|
||||
$scope.catchall.refresh();
|
||||
|
||||
$('#mailboxAddModal').modal('hide');
|
||||
});
|
||||
$('#mailboxAddModal').modal('hide');
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -337,7 +333,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
return;
|
||||
}
|
||||
|
||||
var aliases = $scope.mailboxes.edit.aliases.split(',').map(function (a) { return a.trim(); }).filter(function (a) { return !!a; });
|
||||
var aliases = $scope.mailboxes.edit.aliases.split(' ').map(function (a) { return a.trim(); }).filter(function (a) { return !!a; });
|
||||
|
||||
Client.setAliases($scope.domain.domain, $scope.mailboxes.edit.name, aliases, function (error) {
|
||||
if (error) {
|
||||
@@ -398,7 +394,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
if (error) return callback(error);
|
||||
|
||||
$scope.mailboxes.mailboxes = mailboxes.map(function (m) {
|
||||
m.aliases = aliases.filter(function (a) { return a.aliasTarget === m.name; }).map(function (a) { return a.name; }).join(',');
|
||||
m.aliases = aliases.filter(function (a) { return a.aliasTarget === m.name; }).map(function (a) { return a.name; }).join(' ');
|
||||
m.owner = $scope.users.find(function (u) { return u.id === m.ownerId; }); // owner may not exist
|
||||
m.ownerDisplayName = m.owner ? m.owner.display : ''; // this meta property is set when we get the user list
|
||||
|
||||
|
||||
+32
-14
@@ -91,18 +91,25 @@
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>Event Log
|
||||
|
||||
<button class="btn btn-sm btn-default btn-outline pull-right" ng-click="activity.showNextPage()" ng-disabled="activity.busy || activity.perPage > activity.eventLogs.length">next <i class="fa fa-angle-double-right"></i></button>
|
||||
<button class="btn btn-sm btn-default btn-outline pull-right" ng-click="activity.showPrevPage()" ng-disabled="activity.busy || activity.currentPage <= 1"><i class="fa fa-angle-double-left"></i> prev</button>
|
||||
<button class="btn btn-sm btn-primary btn-outline pull-right" ng-click="activity.refresh()" ng-disabled="activity.busy"><i class="fa fa-sync"></i></button>
|
||||
|
||||
<input class="form-control pull-right" style="width: 200px;" placeholder="Search" type="text" ng-model="activity.search" ng-model-options="{ debounce: 1000 }" ng-change="activity.updateFilter()" />
|
||||
</h3>
|
||||
<div class="text-left" ng-show="user.role === 'owner'">
|
||||
<h3>Event Log</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<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="Search" type="text" ng-model="activity.search" ng-model-options="{ debounce: 1000 }" ng-change="activity.updateFilter()" />
|
||||
<multiselect ng-model="activity.selectedTypes" ms-header="All Events" 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>
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-outline" ng-click="activity.showPrevPage()" ng-disabled="activity.busy || activity.currentPage <= 1"><i class="fa fa-angle-double-left"></i> prev</button>
|
||||
<button class="btn btn-default btn-outline" ng-click="activity.showNextPage()" ng-disabled="activity.busy || activity.perPage > activity.eventLogs.length">next <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-top: 10px; margin-bottom: 15px;" ng-show="user.role === 'owner'">
|
||||
<div class="row ng-hide" ng-hide="ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
@@ -143,15 +150,26 @@
|
||||
<i class="fas fa-history" ng-show="eventlog.type === 'deferred'" uib-tooltip="Deferred"></i>
|
||||
<i class="fas fa-arrow-circle-right" ng-show="eventlog.type === 'received'" uib-tooltip="Incoming"></i>
|
||||
<i class="fas fa-align-justify" ng-show="eventlog.type === 'queued'" uib-tooltip="Queued"></i>
|
||||
<!-- <i class="fas fa-ban" ng-show="eventlog.details.spamStatus.indexOf('Yes,') === 0" uib-tooltip="Spam"></i> -->
|
||||
<i class="fas fa-minus-circle" ng-show="eventlog.type === 'denied'" uib-tooltip="Denied"></i>
|
||||
<i class="fas fa-hand-paper" ng-show="eventlog.type === 'bounce'" uib-tooltip="Bounce"></i>
|
||||
<i class="fas fa-filter" ng-show="eventlog.type === 'spam-learn'" uib-tooltip="Spam filter trained"></i>
|
||||
</td>
|
||||
<td class="no-wrap"><span uib-tooltip="{{ eventlog.ts | prettyLongDate }}" class="arrow">{{ eventlog.ts | prettyDate }}</span></td>
|
||||
<td class="elide-table-cell">
|
||||
<span ng-show="eventlog.type === 'delivered' || eventlog.type === 'queued' || eventlog.type === 'received' || eventlog.type === 'bounce' || eventlog.type === 'deferred'">{{ eventlog.mailFrom | prettyEmailAddresses }} <i class="fas fa-long-arrow-alt-right"></i> {{ eventlog.rcptTo | prettyEmailAddresses }}</span>
|
||||
<span ng-show="eventlog.type === 'denied'">Incoming connection from {{ eventlog.remote.ip }} denied</span>
|
||||
<td>
|
||||
<span ng-show="eventlog.type === 'bounce'">Sent bounce to {{ eventlog.mailFrom | prettyEmailAddresses }} for mail sent to {{ eventlog.rcptTo | prettyEmailAddresses }}. {{ eventlog.details.message || eventlog.details.reason }}</span>
|
||||
<span ng-show="eventlog.type === 'deferred'">Failed to deliver mail to {{ eventlog.rcptTo | prettyEmailAddresses }}. {{ eventlog.details.message || eventlog.details.reason }}. Will retry in {{ eventlog.details.delay }}s.</span>
|
||||
<span ng-show="eventlog.type === 'queued'">
|
||||
<span ng-show="eventlog.direction === 'inbound'">
|
||||
Incoming mail from {{ eventlog.mailFrom | prettyEmailAddresses }} to {{ eventlog.rcptTo | prettyEmailAddresses }}. Spam: {{ eventlog.details.spamStatus.indexOf('Yes,') === 0 ? 'Yes' : 'No' }}
|
||||
</span>
|
||||
<span ng-show="eventlog.direction === 'outbound'">
|
||||
Queued mail for delivery to {{ eventlog.rcptTo | prettyEmailAddresses }} from {{ eventlog.mailFrom | prettyEmailAddresses }}
|
||||
</span>
|
||||
</span>
|
||||
<span ng-show="eventlog.type === 'received'">Saved mail from {{ eventlog.mailFrom | prettyEmailAddresses }} in mailbox {{ eventlog.rcptTo | prettyEmailAddresses }}</span>
|
||||
<span ng-show="eventlog.type === 'delivered'">Delivered mail to {{ eventlog.rcptTo | prettyEmailAddresses }} from {{ eventlog.mailFrom | prettyEmailAddresses }}</span>
|
||||
|
||||
<span ng-show="eventlog.type === 'denied'">Connection from {{ eventlog.remote.ip }} denied. {{ eventlog.details.message || eventlog.details.reason }}</span>
|
||||
<span ng-show="eventlog.type === 'spam-learn'">Spam filter trained using mailbox content</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
+24
-4
@@ -11,18 +11,37 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.domains = [];
|
||||
|
||||
$scope.pageItemCount = [
|
||||
{ name: 'Show 20 per page', value: 20 },
|
||||
{ name: 'Show 50 per page', value: 50 },
|
||||
{ name: 'Show 100 per page', value: 100 }
|
||||
];
|
||||
|
||||
$scope.activityTypes = [
|
||||
{ name: 'Bounce', value: 'bounce' },
|
||||
{ name: 'Deferred', value: 'deferred' },
|
||||
{ name: 'Delivered', value: 'delivered' },
|
||||
{ name: 'Denied', value: 'denied' },
|
||||
{ name: 'Queued', value: 'queued' },
|
||||
{ name: 'Received', value: 'received' },
|
||||
];
|
||||
|
||||
$scope.activity = {
|
||||
busy: true,
|
||||
eventLogs: [],
|
||||
activeEventLog: null,
|
||||
currentPage: 1,
|
||||
perPage: 20,
|
||||
pageItems: $scope.pageItemCount[0],
|
||||
selectedTypes: [],
|
||||
search: '',
|
||||
|
||||
refresh: function () {
|
||||
$scope.activity.busy = true;
|
||||
|
||||
Client.getMailEventLogs($scope.activity.search, $scope.activity.currentPage, $scope.activity.perPage, function (error, result) {
|
||||
var types = $scope.activity.selectedTypes.map(function (a) { return a.value; }).join(',');
|
||||
|
||||
Client.getMailEventLogs($scope.activity.search, types, $scope.activity.currentPage, $scope.activity.pageItems.value, function (error, result) {
|
||||
if (error) return console.error('Failed to fetch mail eventlogs.', error);
|
||||
|
||||
$scope.activity.busy = false;
|
||||
@@ -47,8 +66,8 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
else $scope.activity.activeEventLog = eventLog;
|
||||
},
|
||||
|
||||
updateFilter: function () {
|
||||
$scope.activity.currentPage = 1;
|
||||
updateFilter: function (fresh) {
|
||||
if (fresh) $scope.activity.currentPage = 1;
|
||||
$scope.activity.refresh();
|
||||
}
|
||||
};
|
||||
@@ -141,7 +160,8 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
$scope.domains = domains;
|
||||
$scope.ready = true;
|
||||
|
||||
$scope.activity.refresh();
|
||||
if ($scope.user.role === 'owner') $scope.activity.refresh();
|
||||
|
||||
refreshDomainStatuses();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<div class="row" ng-if="errorMessage">
|
||||
<br>
|
||||
<div class="alert alert-danger text-center">
|
||||
<div class="alert alert-warning text-center">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+2
-1
@@ -17,6 +17,7 @@ angular.module('Application').controller('GraphsController', ['$scope', '$locati
|
||||
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
|
||||
// we use 1024 to match free -m output (which is not si units)
|
||||
function bytesToMegaBytes(value) {
|
||||
return (value/1024/1024).toFixed(2);
|
||||
}
|
||||
@@ -39,7 +40,7 @@ angular.module('Application').controller('GraphsController', ['$scope', '$locati
|
||||
}
|
||||
|
||||
$scope.setError = function (context, error) {
|
||||
$scope.errorMessage = 'Error loading ' + context + ' stats : ' + error.message + '. Try restarting the graphite service.';
|
||||
$scope.errorMessage = 'Error loading ' + context + ' : ' + error.message;
|
||||
};
|
||||
|
||||
$scope.setMemoryApp = function (app, color) {
|
||||
|
||||
@@ -111,5 +111,5 @@ angular.module('Application').controller('NotificationsController', ['$scope', '
|
||||
|
||||
Client.onReconnect(function () {
|
||||
$scope.notifications.refresh();
|
||||
})
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -379,7 +379,7 @@
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p>These passwords can be used as a security measure in desktop, email & mobile clients.</p>
|
||||
<p>App passwords are a security measure to protect your Cloudron user account. If you need to access a Cloudron app from an untrusted mobile app or client, you can log in with your username and the alternate password generated here.</p>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -417,6 +417,7 @@
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p>Use these personal access tokens to authenticate to the <a target="_blank" href="https://cloudron.io/documentation/api/">Cloudron API</a></p>
|
||||
<table class="table table-hover" style="margin: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
+11
-12
@@ -399,15 +399,16 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
$scope.appPassword.identifiers = [];
|
||||
var appsById = {};
|
||||
$scope.apps.forEach(function (app) {
|
||||
// ignore apps without ldap or with email
|
||||
if (!app.manifest.addons || !app.manifest.addons.ldap || app.manifest.addons.email || !app.sso) return;
|
||||
if (!app.manifest.addons) return;
|
||||
var ftp = app.manifest.addons.localstorage && app.manifest.addons.localstorage.ftp;
|
||||
|
||||
// ignore apps without ftp and ldap or email
|
||||
if (!ftp && (!app.manifest.addons.ldap || app.manifest.addons.email || !app.sso)) return;
|
||||
|
||||
appsById[app.id] = app;
|
||||
if (app.label) {
|
||||
$scope.appPassword.identifiers.push({ id: app.id, label: app.label + ' (' + app.fqdn + ')' });
|
||||
} else {
|
||||
$scope.appPassword.identifiers.push({ id: app.id, label: app.fqdn });
|
||||
}
|
||||
var labelSuffix = ftp ? ' - SFTP' : '';
|
||||
var label = app.label ? app.label + ' (' + app.fqdn + ')' + labelSuffix : app.fqdn + labelSuffix;
|
||||
$scope.appPassword.identifiers.push({ id: app.id, label: label });
|
||||
});
|
||||
$scope.appPassword.identifiers.push({ id: 'mail', label: 'Mail client' });
|
||||
|
||||
@@ -417,11 +418,9 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
var app = appsById[password.identifier];
|
||||
if (!app) return password.label = password.identifier + ' (App not found)';
|
||||
|
||||
if (app.label) {
|
||||
password.label = app.label + ' (' + app.fqdn + ')';
|
||||
} else {
|
||||
password.label = app.fqdn;
|
||||
}
|
||||
var ftp = app.manifest.addons && app.manifest.addons.localstorage && app.manifest.addons.localstorage.ftp;
|
||||
var labelSuffix = ftp ? ' - SFTP' : '';
|
||||
password.label = app.label ? app.label + ' (' + app.fqdn + ')' + labelSuffix : app.fqdn + labelSuffix;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
+34
-150
@@ -38,62 +38,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change avatar -->
|
||||
<div class="modal fade" id="avatarChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Change your Cloudron Avatar</h4>
|
||||
</div>
|
||||
<div class="modal-body settings-avatar-selector">
|
||||
<img id="previewAvatar" width="128" height="128" ng-src="{{avatarChange.avatar.data || avatarChange.avatar.url || client.avatar}}"/>
|
||||
<input type="file" id="avatarFileInput" style="display: none" accept="image/png"/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="grid">
|
||||
<div class="item" ng-repeat="avatar in avatarChange.availableAvatars" style="background-image: url('{{avatar.data || avatar.url}}');" ng-click="avatarChange.setPreviewAvatar(avatar)"></div>
|
||||
<div class="item add" ng-click="avatarChange.showCustomAvatarSelector()"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="avatarChange.doChangeAvatar()" ng-disabled="avatarChange.busy"><i class="fa fa-circle-notch fa-spin" ng-show="avatarChange.busy"></i> Change</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change cloudron name -->
|
||||
<div class="modal fade" id="cloudronNameChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Change Cloudron Name</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="cloudronNameChangeForm" role="form" novalidate ng-submit="cloudronNameChange.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (cloudronNameChangeForm.name.$dirty && cloudronNameChangeForm.name.$invalid) }">
|
||||
<label class="control-label">Name</label>
|
||||
<div class="control-label" ng-show="(!cloudronNameChangeForm.name.$dirty && cloudronNameChange.error.name) || (cloudronNameChangeForm.name.$dirty && cloudronNameChangeForm.name.$invalid)">
|
||||
<small ng-show="cloudronNameChangeForm.name.$error.required">A name is required</small>
|
||||
<small ng-show="cloudronNameChangeForm.name.$error.maxlength">The name is too long</small>
|
||||
<small ng-show="!cloudronNameChangeForm.name.$dirty && cloudronNameChange.error.name">{{ cloudronNameChange.error.name }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="cloudronNameChange.name" name="name" id="inputCloudronName" ng-maxlength="30" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="cloudronNameChangeForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="cloudronNameChange.submit()" ng-disabled="cloudronNameChangeForm.$invalid || cloudronNameChange.busy"><i class="fa fa-circle-notch fa-spin" ng-show="cloudronNameChange.busy"></i> Change</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal registry config -->
|
||||
<div class="modal fade" id="registryConfigModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
@@ -142,58 +86,6 @@
|
||||
<h1>Settings</h1>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>About</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-xs-4" style="min-width: 150px;">
|
||||
<div class="settings-avatar" ng-click="avatarChange.showChangeAvatar()" style="background-image: url('{{ client.avatar }}');">
|
||||
<div class="overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-8">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Name</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.cloudronName }} <a href="" ng-click="cloudronNameChange.show()"><i class="fa fa-edit text-small"></i></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Version</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.version }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Provider</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ prettyProviderName(config.provider) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"> </td>
|
||||
</tr>
|
||||
<tr ng-show="!update.busy && update.errorMessage">
|
||||
<td class="text-muted" style="vertical-align: top; white-space: nowrap;">Update Error:</td>
|
||||
<td class="text-right has-error" style="vertical-align: top;">{{ update.errorMessage }}</td>
|
||||
</tr>
|
||||
<tr ng-show="update.busy">
|
||||
<td colspan="2">
|
||||
<div class="progress progress-striped active animateMe" style="margin-bottom: 10px;">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{update.percent}}%"></div>
|
||||
</div>
|
||||
<div>{{ update.message }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" style="padding-top: 10px;">
|
||||
<button class="btn btn-primary pull-right" ng-show="!config.update.box && !update.busy" ng-disabled="autoUpdate.busy" ng-click="autoUpdate.checkNow()"><i class="fa fa-circle-notch fa-spin" ng-show="autoUpdate.busy"></i> Check for Updates</button>
|
||||
<button class="btn btn-success pull-right" ng-show="config.update.box && !update.busy" ng-click="update.show()">Update Available</button>
|
||||
<button class="btn btn-danger pull-right" ng-show="config.update.box && update.busy" ng-click="update.stopUpdate()">Stop Update</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left" ng-show="user.isAtLeastOwner && !config.isDemo">
|
||||
<h3>Cloudron.io Account</h3>
|
||||
</div>
|
||||
@@ -288,14 +180,13 @@
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>App Updates</h3>
|
||||
<h3>Updates</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>Configure the update schedule for the apps</p>
|
||||
<p class="text-danger" ng-show="autoUpdate.error"><br/>{{ autoUpdate.error }}</p>
|
||||
<p>Configure the update schedule for apps</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -303,25 +194,25 @@
|
||||
<div class="col-md-12">
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="scheduleRadio" ng-change="autoUpdate.success = false" ng-model="autoUpdate.pattern" value="00 30 1,3,5,23 * * *">
|
||||
<input type="radio" name="scheduleRadio" ng-model="autoUpdate.pattern" value="00 30 1,3,5,23 * * *">
|
||||
Every night
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="scheduleRadio" ng-change="autoUpdate.success = false" ng-model="autoUpdate.pattern" value="00 00 3,5 * * 3">
|
||||
<input type="radio" name="scheduleRadio" ng-model="autoUpdate.pattern" value="00 00 3,5 * * 3">
|
||||
Wednesday night
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="scheduleRadio" ng-change="autoUpdate.success = false" ng-model="autoUpdate.pattern" value="00 00 1,3,5,23 * * 6">
|
||||
<input type="radio" name="scheduleRadio" ng-model="autoUpdate.pattern" value="00 00 1,3,5,23 * * 6">
|
||||
Saturday night
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="scheduleRadio" ng-change="autoUpdate.success = false" ng-model="autoUpdate.pattern" value="never">
|
||||
<input type="radio" name="scheduleRadio" ng-model="autoUpdate.pattern" value="never">
|
||||
No automatic updates
|
||||
</label>
|
||||
</div>
|
||||
@@ -329,13 +220,34 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<span class="text-success text-bold" ng-show="autoUpdate.success && autoUpdate.pattern === autoUpdate.currentPattern">Saved</span>
|
||||
<br/>
|
||||
<div ng-if="update.busy" class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ update.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="autoUpdate.submit()" ng-disabled="autoUpdate.pattern === autoUpdate.currentPattern"> Save</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<p ng-show="update.busy">{{ update.message }}</p>
|
||||
<p ng-if="update.busy">
|
||||
<div class="has-error" ng-show="update.errorMessage">
|
||||
{{ update.errorMessage }}. <a ng-class="warning" ng-href="/logs.html?taskId={{update.taskId}}" target="_blank">Show Logs</a>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="autoUpdate.submit()" ng-disabled="autoUpdate.pattern === autoUpdate.currentPattern">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="autoUpdate.busy"></i> Save
|
||||
</button>
|
||||
<button class="btn btn-default pull-right" ng-show="!config.update.box && !update.busy" ng-disabled="update.checking" ng-click="update.checkNow()">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="update.checking"></i> Check for Updates
|
||||
</button>
|
||||
<button class="btn btn-success pull-right" ng-show="config.update.box && !update.busy" ng-click="update.show()">Update Available</button>
|
||||
<button class="btn btn-danger pull-right" ng-show="config.update.box && update.busy" ng-click="update.stopUpdate()">Stop Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -346,7 +258,8 @@
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
Cloudron can pull and install apps from private docker registry.
|
||||
Cloudron can pull and install <a href="https://cloudron.io/documentation/custom-apps/tutorial/" target="_blank">custom apps</a> from
|
||||
a private docker registry.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -384,33 +297,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>Branding</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row" ng-hide="config.features.branding">
|
||||
<div class="col-xs-12">
|
||||
<b>Customizing the footer is only available in the business plan.</b>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="config.features.branding">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label class="control-label" style="width: 100%">Footer</label>
|
||||
<p class="text-small">Use <a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank">markdown</a> to style the footer.</p>
|
||||
|
||||
<textarea class="form-control" ng-model="branding.footer.content" ng-disabled="branding.footer.busy"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="branding.footer.save()"><i class="fa fa-circle-notch fa-spin" ng-show="branding.footer.busy"></i> Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+18
-230
@@ -21,48 +21,26 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
}
|
||||
};
|
||||
|
||||
$scope.branding = {
|
||||
footer: {
|
||||
busy: false,
|
||||
content: '',
|
||||
|
||||
save: function () {
|
||||
$scope.branding.footer.busy = true;
|
||||
|
||||
Client.setFooter($scope.branding.footer.content.trim(), function (error) {
|
||||
if (error) return console.error('Failed to set footer.', error);
|
||||
|
||||
Client.refreshConfig(function () {
|
||||
$scope.branding.footer.busy = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
$scope.branding.footer.busy = true;
|
||||
Client.getFooter(function (error, result) {
|
||||
if (error) return console.error('Unable to fetch footer content.', error);
|
||||
|
||||
$scope.branding.footer.content = result;
|
||||
$scope.branding.footer.busy = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
$scope.branding.footer.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.update = {
|
||||
error: {}, // this is for the dialog
|
||||
busy: false,
|
||||
checking: false,
|
||||
percent: 0,
|
||||
message: 'Downloading',
|
||||
errorMessage: '', // this shows inline
|
||||
taskId: '',
|
||||
skipBackup: false,
|
||||
|
||||
checkNow: function () {
|
||||
$scope.update.checking = true;
|
||||
|
||||
Client.checkForUpdates(function (error) {
|
||||
if (error) Client.error(error);
|
||||
|
||||
$scope.update.checking = false;
|
||||
});
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.update.error.generic = null;
|
||||
$scope.update.busy = false;
|
||||
@@ -150,118 +128,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
}
|
||||
};
|
||||
|
||||
$scope.avatarChange = {
|
||||
busy: false,
|
||||
error: {},
|
||||
avatar: null,
|
||||
availableAvatars: [{
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo.png',
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-green.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-orange.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-darkblue.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-red.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-yellow.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-black.png'
|
||||
}],
|
||||
|
||||
getBlobFromImg: function (img, callback) {
|
||||
var size = 256;
|
||||
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
|
||||
var imageDimensionRatio = img.width / img.height;
|
||||
var canvasDimensionRatio = canvas.width / canvas.height;
|
||||
var renderableHeight, renderableWidth, xStart, yStart;
|
||||
|
||||
if (imageDimensionRatio > canvasDimensionRatio) {
|
||||
renderableHeight = canvas.height;
|
||||
renderableWidth = img.width * (renderableHeight / img.height);
|
||||
xStart = (canvas.width - renderableWidth) / 2;
|
||||
yStart = 0;
|
||||
} else if (imageDimensionRatio < canvasDimensionRatio) {
|
||||
renderableWidth = canvas.width;
|
||||
renderableHeight = img.height * (renderableWidth / img.width);
|
||||
xStart = 0;
|
||||
yStart = (canvas.height - renderableHeight) / 2;
|
||||
} else {
|
||||
renderableHeight = canvas.height;
|
||||
renderableWidth = canvas.width;
|
||||
xStart = 0;
|
||||
yStart = 0;
|
||||
}
|
||||
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(img, xStart, yStart, renderableWidth, renderableHeight);
|
||||
|
||||
canvas.toBlob(callback);
|
||||
},
|
||||
|
||||
doChangeAvatar: function () {
|
||||
$scope.avatarChange.error.avatar = null;
|
||||
$scope.avatarChange.busy = true;
|
||||
|
||||
var img = document.getElementById('previewAvatar');
|
||||
$scope.avatarChange.avatar.file = $scope.avatarChange.getBlobFromImg(img, function (blob) {
|
||||
Client.changeCloudronAvatar(blob, function (error) {
|
||||
if (error) {
|
||||
console.error('Unable to change cloudron avatar.', error);
|
||||
} else {
|
||||
Client.resetAvatar();
|
||||
}
|
||||
|
||||
$('#avatarChangeModal').modal('hide');
|
||||
$scope.avatarChange.avatarChangeReset();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setPreviewAvatar: function (avatar) {
|
||||
$scope.avatarChange.avatar = avatar;
|
||||
},
|
||||
|
||||
avatarChangeReset: function () {
|
||||
$scope.avatarChange.error.avatar = null;
|
||||
$scope.avatarChange.avatar = null;
|
||||
$scope.avatarChange.busy = false;
|
||||
},
|
||||
|
||||
showChangeAvatar: function () {
|
||||
$scope.avatarChange.avatarChangeReset();
|
||||
$('#avatarChangeModal').modal('show');
|
||||
},
|
||||
|
||||
showCustomAvatarSelector: function () {
|
||||
$('#avatarFileInput').click();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.s3like = function (provider) {
|
||||
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' || provider === 'exoscale-sos' || provider === 'digitalocean-spaces';
|
||||
};
|
||||
|
||||
$scope.timeZone = {
|
||||
busy: false,
|
||||
success: false,
|
||||
@@ -285,38 +151,25 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
$scope.timeZone.success = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.autoUpdate = {
|
||||
busy: false,
|
||||
success: false,
|
||||
error: '',
|
||||
pattern: '',
|
||||
currentPattern: '',
|
||||
|
||||
checkNow: function () {
|
||||
$scope.autoUpdate.busy = true;
|
||||
|
||||
Client.checkForUpdates(function (error) {
|
||||
if (error) $scope.autoUpdate.error = error.message;
|
||||
|
||||
$scope.autoUpdate.busy = false;
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
if ($scope.autoUpdate.pattern === $scope.autoUpdate.currentPattern) return;
|
||||
|
||||
$scope.autoUpdate.error = '';
|
||||
$scope.autoUpdate.busy = true;
|
||||
$scope.autoUpdate.success = false;
|
||||
|
||||
Client.setAppAutoupdatePattern($scope.autoUpdate.pattern, function (error) {
|
||||
if (error) $scope.autoUpdate.error = error.message;
|
||||
if (error) Client.error(error);
|
||||
else $scope.autoUpdate.currentPattern = $scope.autoUpdate.pattern;
|
||||
|
||||
$scope.autoUpdate.busy = false;
|
||||
$scope.autoUpdate.success = true;
|
||||
$timeout(function () {
|
||||
$scope.autoUpdate.busy = false;
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -367,23 +220,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
Client.openSubscriptionSetup($scope.subscription);
|
||||
};
|
||||
|
||||
$('#avatarFileInput').get(0).onchange = function (event) {
|
||||
var fr = new FileReader();
|
||||
fr.onload = function () {
|
||||
$scope.$apply(function () {
|
||||
var tmp = {
|
||||
file: event.target.files[0],
|
||||
data: fr.result,
|
||||
url: null
|
||||
};
|
||||
|
||||
$scope.avatarChange.availableAvatars.push(tmp);
|
||||
$scope.avatarChange.setPreviewAvatar(tmp);
|
||||
});
|
||||
};
|
||||
fr.readAsDataURL(event.target.files[0]);
|
||||
};
|
||||
|
||||
$scope.registryConfig = {
|
||||
busy: false,
|
||||
error: null,
|
||||
@@ -394,7 +230,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
currentConfig: {},
|
||||
|
||||
reset: function () {
|
||||
$scope.cloudronNameChange.busy = false;
|
||||
$scope.registryConfig.busy = false;
|
||||
$scope.registryConfig.error = null;
|
||||
|
||||
$scope.registryConfig.serverAddress = $scope.registryConfig.currentConfig.serverAddress;
|
||||
@@ -436,66 +272,18 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
}
|
||||
};
|
||||
|
||||
$scope.cloudronNameChange = {
|
||||
busy: false,
|
||||
error: {},
|
||||
name: '',
|
||||
|
||||
reset: function () {
|
||||
$scope.cloudronNameChange.busy = false;
|
||||
$scope.cloudronNameChange.error.name = null;
|
||||
$scope.cloudronNameChange.name = '';
|
||||
|
||||
$scope.cloudronNameChangeForm.$setUntouched();
|
||||
$scope.cloudronNameChangeForm.$setPristine();
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.cloudronNameChange.reset();
|
||||
$scope.cloudronNameChange.name = $scope.config.cloudronName;
|
||||
$('#cloudronNameChangeModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.cloudronNameChange.error.name = null;
|
||||
$scope.cloudronNameChange.busy = true;
|
||||
|
||||
Client.changeCloudronName($scope.cloudronNameChange.name, function (error) {
|
||||
$scope.cloudronNameChange.busy = false;
|
||||
|
||||
if (error) {
|
||||
if (error.statusCode === 400) {
|
||||
$scope.cloudronNameChange.error.name = 'Invalid name';
|
||||
$scope.cloudronNameChange.name = '';
|
||||
$('#inputCloudronName').focus();
|
||||
$scope.cloudronNameChangeForm.password.$setPristine();
|
||||
} else {
|
||||
console.error('Unable to change name.', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.cloudronNameChange.reset();
|
||||
$('#cloudronNameChangeModal').modal('hide');
|
||||
|
||||
Client.refreshConfig();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
getAutoupdatePattern();
|
||||
getRegistryConfig();
|
||||
getTimeZone();
|
||||
|
||||
$scope.update.checkStatus();
|
||||
$scope.branding.refresh();
|
||||
|
||||
if ($scope.user.isAtLeastOwner) getSubscription();
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['planChangeModal', 'appstoreLoginModal', 'cloudronNameChangeModal'].forEach(function (id) {
|
||||
['planChangeModal', 'appstoreLoginModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
|
||||
+50
-3
@@ -35,6 +35,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal reboot server -->
|
||||
<div class="modal fade" id="rebootModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Really reboot server?</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-warning">Rebooting the server will cause temporary downtime for all apps installed on this Cloudron!</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="reboot.submit()" ng-disabled="reboot.busy"><i class="fa fa-circle-notch fa-spin" ng-show="reboot.busy"></i> Reboot now</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<div class="text-left">
|
||||
@@ -46,6 +64,13 @@
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
<div class="row" ng-if="errorMessage">
|
||||
<br>
|
||||
<div class="alert alert-warning text-center">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row ng-hide" ng-show="disks.length === 0">
|
||||
<div class="col-md-12 text-center">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
@@ -53,7 +78,7 @@
|
||||
</div>
|
||||
<div class="row" ng-repeat="disk in disks" style="margin-bottom: 20px;">
|
||||
<div class="col-md-12">
|
||||
<h3>{{ disk.filesystem }} <small>mounted at</small> {{ disk.mountpoint }} <span class="pull-right small"><b>{{ disk.available | prettyDiskSize }}</b> of <b>{{ disk.size | prettyDiskSize }}</b> still available</span></h3>
|
||||
<h3>{{ disk.filesystem }} <small>mounted at</small> {{ disk.mountpoint }} <span class="pull-right small"><b>{{ disk.available | prettyDiskSize }}</b> of <b>{{ disk.size | prettyDiskSize }}</b> available</span></h3>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" ng-repeat="content in disk.contains" style="width: {{ content.usage / disk.size * 100 }}%; background-color: {{ content.color }};" uib-tooltip="{{ content.label + ' ' + (content.usage | prettyDiskSize) }}"></div>
|
||||
</div>
|
||||
@@ -69,8 +94,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>Services</h3>
|
||||
</div>
|
||||
@@ -141,4 +164,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>Server</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p class="text-danger text-bold" ng-show="isRebootRequired">
|
||||
This server requires a reboot to finalize Ubuntu security updates.
|
||||
</p>
|
||||
<p ng-hide="isRebootRequired">
|
||||
Use this only when you experience unexpected behavior. All services and apps will be automatically started.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<a class="btn btn-primary" href="/logs.html?id=box" target="_blank">Show Logs</a>
|
||||
<button class="btn btn-danger" ng-click="reboot.show()" ng-disabled="reboot.busy"><i class="fa fa-circle-notch fa-spin" ng-show="reboot.busy"></i> Reboot</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
+46
-12
@@ -11,6 +11,11 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati
|
||||
$scope.services = [];
|
||||
$scope.disks = [];
|
||||
$scope.memory = null;
|
||||
$scope.errorMessage = '';
|
||||
|
||||
$scope.setError = function (context, error) {
|
||||
$scope.errorMessage = 'Error loading ' + context + ' : ' + error.message;
|
||||
};
|
||||
|
||||
// http://stackoverflow.com/questions/1484506/random-color-generator-in-javascript
|
||||
function getRandomColor() {
|
||||
@@ -126,6 +131,25 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati
|
||||
}
|
||||
};
|
||||
|
||||
$scope.reboot = {
|
||||
busy: false,
|
||||
show: function () {
|
||||
$scope.reboot.busy = false;
|
||||
$('#rebootModal').modal('show');
|
||||
},
|
||||
submit: function () {
|
||||
$scope.reboot.busy = true;
|
||||
|
||||
Client.reboot(function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$('#rebootModal').modal('hide');
|
||||
|
||||
// trigger refetch to show offline banner
|
||||
$timeout(function () { Client.getStatus(function () {}); }, 8000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function updateDiskGraphs() {
|
||||
// https://graphite.readthedocs.io/en/latest/render_api.html#paths-and-wildcards
|
||||
@@ -134,6 +158,7 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati
|
||||
Client.disks(function (error, result) {
|
||||
if (error) return $scope.setError('disk', error);
|
||||
|
||||
// segregate locations into the correct disks based on 'filesystem'
|
||||
result.disks.forEach(function (disk, index) {
|
||||
disk.id = index;
|
||||
disk.contains = [];
|
||||
@@ -151,25 +176,26 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati
|
||||
});
|
||||
});
|
||||
|
||||
$scope.disks = result.disks;
|
||||
$scope.disks = result.disks; // [ { filesystem, type, size, used, available, capacity, mountpoint }]
|
||||
|
||||
// lazy fetch graphite data
|
||||
$scope.disks.forEach(function (disk) {
|
||||
// render data of each disk
|
||||
$scope.disks.forEach(function (disk, index) {
|
||||
// /dev/sda1 -> sda1
|
||||
// /dev/mapper/foo -> mapper_foo (see #348)
|
||||
var diskName = disk.filesystem.slice(disk.filesystem.indexOf('/', 1) + 1);
|
||||
diskName = diskName.replace(/\//g, '_');
|
||||
|
||||
// use collectd instead of df data so the timeframe matches with the du data
|
||||
Client.graphs([
|
||||
'absolute(collectd.localhost.df-' + diskName + '.df_complex-free)',
|
||||
'absolute(collectd.localhost.df-' + diskName + '.df_complex-reserved)',
|
||||
'absolute(collectd.localhost.df-' + diskName + '.df_complex-reserved)', // reserved for root (default: 5%) tune2fs -l/m
|
||||
'absolute(collectd.localhost.df-' + diskName + '.df_complex-used)'
|
||||
], '-1min', {}, function (error, data) {
|
||||
if (error) return $scope.setError('disk', error);
|
||||
|
||||
disk.size = data[2].datapoints[0][0] + data[1].datapoints[0][0] + data[0].datapoints[0][0];
|
||||
disk.free = data[0].datapoints[0][0];
|
||||
disk.occupied = data[2].datapoints[0][0] + data[1].datapoints[0][0];
|
||||
disk.occupied = data[2].datapoints[0][0];
|
||||
|
||||
colorIndex = 0;
|
||||
disk.contains.forEach(function (content) {
|
||||
@@ -197,13 +223,16 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati
|
||||
usageOther -= tmp;
|
||||
});
|
||||
|
||||
// add content container for other non tracked data
|
||||
disk.contains.push({
|
||||
label: 'Ubuntu',
|
||||
id: 'other',
|
||||
color: '#27CE65',
|
||||
usage: usageOther
|
||||
});
|
||||
if (index === 0) { // the root mount point is the first disk
|
||||
disk.contains.push({
|
||||
label: 'Everything else (Ubuntu, Swap, etc)',
|
||||
id: 'other',
|
||||
color: '#27CE65',
|
||||
usage: usageOther
|
||||
});
|
||||
}
|
||||
|
||||
disk.contains.sort(function (x, y) { return x.usage > y.usage; }); // sort by usage
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -232,4 +261,9 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Client.onReconnect(function () {
|
||||
$scope.reboot.busy = false;
|
||||
});
|
||||
|
||||
}]);
|
||||
|
||||
@@ -356,7 +356,7 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="filter">
|
||||
<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="Search"/>
|
||||
<select class="form-control" ng-model="pageItems" ng-options="a.name for a in pageItemCount" ng-change="updateFilter(true)"></select>
|
||||
</div>
|
||||
@@ -489,6 +489,13 @@
|
||||
</div>
|
||||
|
||||
<div ng-show="config.features.externalLdap">
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider === 'noop'">
|
||||
<div class="col-xs-12">
|
||||
<span class="text-muted">LDAP authentication is not configured.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Provider</span>
|
||||
|
||||
Reference in New Issue
Block a user