Compare commits

..

44 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
29 changed files with 346 additions and 180 deletions
+1 -1
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">
+23 -38
View File
@@ -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));
+26 -17
View File
@@ -281,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 () {
@@ -581,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;
@@ -589,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) {
@@ -613,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;
@@ -626,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) {
@@ -637,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()');
}
@@ -645,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; });
+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">
-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">
+139 -34
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;
}
}
@@ -1155,8 +1168,8 @@ footer {
.settings-avatar {
position: relative;
cursor: pointer;
width: 64px;
height: 64px;
width: 128px;
height: 128px;
background-position: center;
background-size: 100% 100%;
background-repeat: no-repeat;
@@ -1267,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
// ----------------------------
@@ -1327,7 +1352,7 @@ footer {
// Eventlog/Activity
// ----------------------------
.filter {
.eventlog-filter {
display: inline-block;
padding-left: 0;
margin: 20px 0;
@@ -1356,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>
+16 -13
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>
@@ -404,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>
@@ -433,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>
@@ -470,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>
@@ -725,7 +731,7 @@
<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>
<form role="form" name="resourcesDataDirForm" ng-submit="resources.submitDataDir()" autocomplete="off">
<fieldset>
@@ -959,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();
}
+8 -1
View File
@@ -174,7 +174,14 @@
</div>
<div class="card" style="margin-bottom: 15px;">
<p>Cloudron makes a complete backup of your system based on this configuration.</p>
<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">
+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();
});
+2 -2
View File
@@ -5,7 +5,7 @@
<div class="modal-header">
<h4 class="modal-title">Choose Cloudron Avatar</h4>
</div>
<div class="modal-body settings-avatar-selector">
<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"/>
@@ -47,7 +47,7 @@
<div>
<label class="control-label">Logo</label>
</div>
<div class="settings-avatar" ng-click="avatarChange.showChangeAvatar()">
<div class="branding-avatar" ng-click="avatarChange.showChangeAvatar()">
<img ng-src="{{ about.avatarUrl() }}"/>
<div class="overlay"></div>
</div>
+5 -1
View File
@@ -196,7 +196,11 @@ angular.module('Application').controller('BrandingController', ['$scope', '$loca
busy: false,
refresh: function () {
$scope.footer.content = $scope.config.footer;
Client.getFooter(function (error, result) {
if (error) return console.error('Failed to get footer.', error);
$scope.footer.content = result;
});
},
submit: function () {
+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
+17 -10
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>
+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) {
+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;
});
});
},
+8 -6
View File
@@ -221,22 +221,24 @@
<div class="row">
<br/>
<div class="col-md-12" style="margin-bottom: 10px;">
<div ng-show="update.busy" class="progress progress-striped active animateMe">
<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="row">
<div class="col-md-6">
<div class="col-md-7">
<p ng-show="update.busy">{{ update.message }}</p>
<p ng-hide="update.busy">
<div class="has-error" ng-show="update.errorMessage">{{ update.errorMessage }}</div>
<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-6 text-right">
<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>
+1 -1
View File
@@ -230,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;
+8 -1
View File
@@ -64,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>
@@ -71,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>
+22 -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() {
@@ -153,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 = [];
@@ -170,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) {
@@ -216,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
});
});
});
+1 -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>