Compare commits

..

63 Commits

Author SHA1 Message Date
Johannes Zellner eef360673b Also hide the app header bits to avoid empty ui fragments while loading 2020-04-12 13:20:01 +02:00
Girish Ramakrishnan 36e298c758 check for updates wants more space 2020-04-11 17:46:19 -07:00
Girish Ramakrishnan 275157f27b Show logs link when updater has error 2020-04-11 17:44:04 -07:00
Girish Ramakrishnan e776deaa3f Add note on Ext4/NFS mounts only 2020-04-09 15:49:47 -07:00
Johannes Zellner 4fc8e9b45e Ensure disable state for all form elements in backup import 2020-04-09 13:15:26 +02:00
Johannes Zellner fe41eec7c5 Fix spacing on import button in app view 2020-04-09 13:13:14 +02:00
Johannes Zellner d1d1d22734 Ensure we only show the tabs and content when app is loaded 2020-04-08 12:56:57 +02:00
Girish Ramakrishnan da8b76957a sort disk contents by usage 2020-04-03 10:41:04 -07:00
Girish Ramakrishnan 305f9fd1cf show apps with automatic backups disabled 2020-04-03 10:36:51 -07:00
Girish Ramakrishnan cd2a94ddb8 typo in variable name 2020-04-03 09:56:38 -07:00
Johannes Zellner a2df4db504 Parse task creationTime also as utc 2020-04-02 12:19:42 +02:00
Girish Ramakrishnan b7740a4758 do not count reserved as used 2020-04-01 22:15:03 -07:00
Girish Ramakrishnan 62c24de5c4 don't say ubuntu
https://forum.cloudron.io/topic/2228/what-type-area-of-data-makes-up-other-in-disk-usage/4
2020-04-01 18:39:05 -07:00
Girish Ramakrishnan 5ed3e67b76 graphs: ubuntu is only on the root mount point 2020-04-01 16:56:56 -07:00
Girish Ramakrishnan c7f2314a15 add note that memory is 1024 based 2020-04-01 16:42:20 -07:00
Girish Ramakrishnan 420c7ebd67 Fixup mail sizes to be 1000 and not 1024 2020-04-01 16:29:10 -07:00
Girish Ramakrishnan b93b1a6eec Fix prettyDiskSize to use 1000 instead of 1024 2020-04-01 16:26:47 -07:00
Girish Ramakrishnan 7d52be6e99 system: setError is not defined 2020-03-31 18:47:19 -07:00
Girish Ramakrishnan 9b1f0e394a set busy to false on error 2020-03-31 17:45:34 -07:00
Girish Ramakrishnan 1b0cb5d455 remove API calls to add/remove mail domain separately
part of cloudron/box#669
2020-03-31 10:59:01 -07:00
Girish Ramakrishnan 9b79d59d93 Add API token note 2020-03-30 22:37:25 -07:00
Girish Ramakrishnan 3e12316ea1 better wording from rob 2020-03-30 22:34:47 -07:00
Johannes Zellner 1b38c0111f Add turn to logviewer 2020-03-30 18:43:43 +02:00
Girish Ramakrishnan 5542393eb5 branding: fix login page title 2020-03-28 22:59:07 -07:00
Girish Ramakrishnan ad48bc0ee8 mail: refresh in the background 2020-03-28 17:48:11 -07:00
Girish Ramakrishnan ba0e5d0b59 query 1000 aliases and mailboxes
we don't handle pagination yet. it's not needed
2020-03-28 17:35:53 -07:00
Girish Ramakrishnan 1c5ff88e3c Use space instead of command for tag-input
this makes sure that email aliases wrap. if we used comma, it does not wrap
2020-03-28 16:46:19 -07:00
Girish Ramakrishnan bf7d4a550e ftp apps can be set a per-app password
this is useful for use in ftp clients
2020-03-26 21:50:44 -07:00
Girish Ramakrishnan 324bc763fc mail eventlog is owner only 2020-03-26 18:56:32 -07:00
Girish Ramakrishnan f9fb2ca3a1 Fixup users filter 2020-03-26 18:32:49 -07:00
Girish Ramakrishnan b5eac7c91b email: add type filter to eventlog 2020-03-25 22:07:01 -07:00
Johannes Zellner 3c858ca0fd Only show the progress bar when task is actually active 2020-03-26 00:22:46 +01:00
Johannes Zellner da9d634b83 Remove already hidden task stop button 2020-03-26 00:19:50 +01:00
Johannes Zellner 128704400f Hook up task cancel action 2020-03-26 00:19:06 +01:00
Johannes Zellner a3594322bd Show task cancel button after 5min 2020-03-26 00:16:23 +01:00
Girish Ramakrishnan fe4b3d5f1d branding: use separate css 2020-03-25 08:56:56 -07:00
Johannes Zellner da08da2b54 Use footer info from settings to show empty on default 2020-03-25 07:00:53 +01:00
Johannes Zellner 5deb5f79bd Ensure textareas don't overflow horizontally on resize 2020-03-25 06:58:38 +01:00
Johannes Zellner 9f0d694f0a Prevent angular crash when adding already existing tag 2020-03-25 06:51:42 +01:00
Johannes Zellner 4153fb7d1e Use theme for tag-input tags 2020-03-25 06:51:42 +01:00
Johannes Zellner 6994ec0f03 Allow to click anywhere in tag-input for focus 2020-03-25 06:51:42 +01:00
Johannes Zellner e1af60cfa9 Fix tag-input with flex layout to better overflow 2020-03-25 06:51:42 +01:00
Johannes Zellner 7bcec61e6d Make tag-input support dirty handling on tag deletion 2020-03-25 06:51:42 +01:00
Girish Ramakrishnan dde287f05d avatar size is 128px 2020-03-24 13:04:12 -07:00
Girish Ramakrishnan 27fc37e55c descriptive mail eventlog 2020-03-20 13:05:58 -07:00
Girish Ramakrishnan ad901760f6 move footer to separate section 2020-03-19 23:28:22 -07:00
Girish Ramakrishnan 973029865e Branding UI changes 2020-03-19 22:59:30 -07:00
Girish Ramakrishnan 52e4fedd16 fieldset must be inside form 2020-03-19 19:26:19 -07:00
Girish Ramakrishnan b81ba49370 CPU shares is a percent 2020-03-19 17:15:08 -07:00
Girish Ramakrishnan 39a0f93f69 add cpuShares 2020-03-19 17:11:51 -07:00
Girish Ramakrishnan 53cb83eacc eventlog: add start/stop/restart logs 2020-03-19 17:05:50 -07:00
Girish Ramakrishnan b307d278b0 mailboxName should have lower priority than location change 2020-03-19 16:48:46 -07:00
Girish Ramakrishnan 14348eba38 Move name and logo into branding page 2020-03-18 22:11:33 -07:00
Girish Ramakrishnan cead5b74ae if ldap is noop, show a message 2020-03-18 21:44:55 -07:00
Girish Ramakrishnan 2e2a945f7c add custom apps link 2020-03-18 21:25:19 -07:00
Girish Ramakrishnan 0e3ae2b450 add new branding view 2020-03-18 17:53:50 -07:00
Girish Ramakrishnan 19e2df65ca backups: hide configure button for non-owners 2020-03-18 17:24:20 -07:00
Girish Ramakrishnan 565d715a66 remove extra break 2020-03-18 13:43:15 -07:00
Girish Ramakrishnan abe6f55aa6 gcdns: fix add/save 2020-03-17 22:51:47 -07:00
Girish Ramakrishnan c278d0c5d4 bring back reboot button 2020-03-17 22:26:01 -07:00
Girish Ramakrishnan a7e2c74158 more linode warnings 2020-03-13 12:05:47 -07:00
Girish Ramakrishnan d84900d601 linode: dns frontend 2020-03-13 11:32:30 -07:00
Girish Ramakrishnan fdda28d67f lint 2020-03-12 17:07:17 -07:00
36 changed files with 984 additions and 655 deletions
+3 -2
View File
@@ -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>&lrm;</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
View File
@@ -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
View File
@@ -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)">&times;</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)">&times;</button>' +
'</div>' +
'<input type="text" data-ng-model="tagText" ng-blur="addTag()" placeholder="{{placeholder}}"/>' +
'</div>'
+2 -1
View File
@@ -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;
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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>&lrm;</title>
<link id="favicon" href="<%= apiOrigin %>/api/v1/cloudron/avatar" rel="icon" type="image/png">
-1
View File
@@ -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">
+5
View File
@@ -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 &amp; 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>
-1
View File
@@ -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
View File
@@ -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;
}
}
}
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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();
}
+6
View File
@@ -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
View File
@@ -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>
+4
View File
@@ -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();
});
+97
View File
@@ -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>
+226
View File
@@ -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
View File
@@ -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 &amp; 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
View File
@@ -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') {
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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();
});
});
+1 -1
View File
@@ -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
View File
@@ -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) {
+1 -1
View File
@@ -111,5 +111,5 @@ angular.module('Application').controller('NotificationsController', ['$scope', '
Client.onReconnect(function () {
$scope.notifications.refresh();
})
});
}]);
+2 -1
View File
@@ -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 &amp; 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
View File
@@ -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
View File
@@ -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">&nbsp;</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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
});
}]);
+8 -1
View File
@@ -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>