Compare commits

..

1 Commits

Author SHA1 Message Date
Girish Ramakrishnan eb1326ac72 typo 2018-09-12 20:27:44 -07:00
66 changed files with 9047 additions and 6915 deletions
+8
View File
@@ -0,0 +1,8 @@
{
"node": true,
"browser": true,
"unused": true,
"globalstrict": true,
"predef": [ "angular", "$" ],
"esnext": true
}
+1 -1
View File
@@ -1,5 +1,5 @@
The Cloudron Subscription license
Copyright (c) 2019 Cloudron UG
Copyright (c) 2018 Cloudron UG
With regard to the Cloudron Software:
+1 -1
View File
@@ -37,7 +37,7 @@ anyone to effortlessly host web applications on their server on their own terms.
## Demo
Try our demo at https://my.demo.cloudron.io (username: cloudron password: cloudron).
Try our demo at https://my-demo.cloudron.me (username: cloudron password: cloudron).
## Installing
+115 -73
View File
@@ -11,7 +11,32 @@ var argv = require('yargs').argv,
rimraf = require('rimraf'),
sass = require('gulp-sass'),
serve = require('gulp-serve'),
sourcemaps = require('gulp-sourcemaps');
sourcemaps = require('gulp-sourcemaps'),
uglify = require('gulp-uglify');
gulp.task('3rdparty', function () {
gulp.src([
'src/3rdparty/**/*.js',
'src/3rdparty/**/*.map',
'src/3rdparty/**/*.css',
'src/3rdparty/**/*.otf',
'src/3rdparty/**/*.eot',
'src/3rdparty/**/*.svg',
'src/3rdparty/**/*.gif',
'src/3rdparty/**/*.ttf',
'src/3rdparty/**/*.woff',
'src/3rdparty/**/*.woff2'
])
.pipe(gulp.dest('dist/3rdparty/'));
gulp.src('node_modules/bootstrap-sass/assets/javascripts/bootstrap.min.js')
.pipe(gulp.dest('dist/3rdparty/js'));
});
// --------------
// JavaScript
// --------------
if (argv.help || argv.h) {
console.log('Supported arguments for "gulp develop":');
@@ -23,6 +48,8 @@ if (argv.help || argv.h) {
process.exit(1);
}
gulp.task('js', ['js-index', 'js-logs', 'js-terminal', 'js-setup', 'js-setupdns', 'js-restore', 'js-update'], function () {});
var oauth = {
clientId: argv.clientId || 'cid-webadmin',
clientSecret: argv.clientSecret || 'unused',
@@ -40,107 +67,133 @@ console.log();
console.log('Building for revision: %s', revision);
console.log();
gulp.task('fontawesome', function () {
return gulp.src([
'node_modules/@fortawesome/fontawesome-free/*css*/all.min.css',
'node_modules/@fortawesome/fontawesome-free/*webfonts*/*.eot',
'node_modules/@fortawesome/fontawesome-free/*webfonts*/*.svg',
'node_modules/@fortawesome/fontawesome-free/*webfonts*/*.ttf',
'node_modules/@fortawesome/fontawesome-free/*webfonts*/*.woff',
'node_modules/@fortawesome/fontawesome-free/*webfonts*/*.woff2'
]).pipe(gulp.dest('dist/3rdparty/fontawesome/'));
});
gulp.task('bootstrap', function () {
return gulp.src('node_modules/bootstrap-sass/assets/javascripts/bootstrap.min.js')
.pipe(gulp.dest('dist/3rdparty/js'));
});
gulp.task('3rdparty-copy', function () {
return gulp.src([
'src/3rdparty/**/*.js',
'src/3rdparty/**/*.map',
'src/3rdparty/**/*.css',
'src/3rdparty/**/*.otf',
'src/3rdparty/**/*.eot',
'src/3rdparty/**/*.svg',
'src/3rdparty/**/*.gif',
'src/3rdparty/**/*.ttf'
])
.pipe(gulp.dest('dist/3rdparty/'));
});
gulp.task('3rdparty', gulp.series(['3rdparty-copy', 'bootstrap', 'fontawesome']));
// --------------
// JavaScript
// --------------
gulp.task('js-index', function () {
return gulp.src([
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src([
'src/js/index.js',
'src/js/client.js',
'src/js/appstore.js',
'src/js/main.js',
'src/views/*.js'
])
.pipe(ejs({ oauth: oauth, revision: revision }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('index.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js-logs', function () {
return gulp.src(['src/js/logs.js', 'src/js/client.js'])
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['src/js/logs.js', 'src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('logs.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js-terminal', function () {
return gulp.src(['src/js/terminal.js', 'src/js/client.js'])
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['src/js/terminal.js', 'src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('terminal.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js-setup', function () {
return gulp.src(['src/js/setup.js', 'src/js/client.js'])
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['src/js/setup.js', 'src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('setup.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js-setupdns', function () {
return gulp.src(['src/js/setupdns.js', 'src/js/client.js'])
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['src/js/setupdns.js', 'src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('setupdns.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js-restore', function () {
return gulp.src(['src/js/restore.js', 'src/js/client.js'])
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['src/js/restore.js', 'src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('restore.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js-update', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['src/js/update.js'])
.pipe(sourcemaps.init())
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js', gulp.series([ 'js-index', 'js-logs', 'js-terminal', 'js-setup', 'js-setupdns', 'js-restore' ]));
// --------------
// HTML
// --------------
gulp.task('html', ['html-views', 'html-templates'], function () {
return gulp.src('src/*.html').pipe(ejs({ revision: revision }, {}, { ext: '.html' })).pipe(gulp.dest('dist'));
});
gulp.task('html-views', function () {
return gulp.src('src/views/**/*.html').pipe(gulp.dest('dist/views'));
});
@@ -149,12 +202,6 @@ gulp.task('html-templates', function () {
return gulp.src('src/templates/**/*.html').pipe(gulp.dest('dist/templates'));
});
gulp.task('html-raw', function () {
return gulp.src('src/*.html').pipe(ejs({ revision: revision }, {}, { ext: '.html' })).pipe(gulp.dest('dist'));
});
gulp.task('html', gulp.series(['html-views', 'html-templates', 'html-raw']));
// --------------
// CSS
// --------------
@@ -174,35 +221,30 @@ gulp.task('images', function () {
.pipe(gulp.dest('dist/img'));
});
// --------------
// Utilities
// --------------
gulp.task('clean', function (done) {
gulp.task('watch', ['default'], function () {
gulp.watch(['src/*.scss'], ['css']);
gulp.watch(['src/img/*'], ['images']);
gulp.watch(['src/**/*.html'], ['html']);
gulp.watch(['src/views/*.html'], ['html-views']);
gulp.watch(['src/templates/*.html'], ['html-templates']);
gulp.watch(['src/js/update.js'], ['js-update']);
gulp.watch(['src/js/setup.js', 'src/js/client.js'], ['js-setup']);
gulp.watch(['src/js/setupdns.js', 'src/js/client.js'], ['js-setupdns']);
gulp.watch(['src/js/restore.js', 'src/js/client.js'], ['js-restore']);
gulp.watch(['src/js/logs.js', 'src/js/client.js'], ['js-logs']);
gulp.watch(['src/js/terminal.js', 'src/js/client.js'], ['js-terminal']);
gulp.watch(['src/js/index.js', 'src/js/client.js', 'src/js/appstore.js', 'src/js/main.js', 'src/views/*.js'], ['js-index']);
gulp.watch(['src/3rdparty/**/*'], ['3rdparty']);
});
gulp.task('clean', function () {
rimraf.sync('dist');
done();
});
gulp.task('default', gulp.series(['clean', 'html', 'js', '3rdparty', 'images', 'css']));
gulp.task('watch', function (done) {
gulp.watch(['src/*.scss'], gulp.series(['css']));
gulp.watch(['src/img/*'], gulp.series(['images']));
gulp.watch(['src/**/*.html'], gulp.series(['html']));
gulp.watch(['src/views/*.html'], gulp.series(['html-views']));
gulp.watch(['src/templates/*.html'], gulp.series(['html-templates']));
gulp.watch(['src/js/setup.js', 'src/js/client.js'], gulp.series(['js-setup']));
gulp.watch(['src/js/setupdns.js', 'src/js/client.js'], gulp.series(['js-setupdns']));
gulp.watch(['src/js/restore.js', 'src/js/client.js'], gulp.series(['js-restore']));
gulp.watch(['src/js/logs.js', 'src/js/client.js'], gulp.series(['js-logs']));
gulp.watch(['src/js/terminal.js', 'src/js/client.js'], gulp.series(['js-terminal']));
gulp.watch(['src/js/index.js', 'src/js/client.js', 'src/js/main.js', 'src/views/*.js'], gulp.series(['js-index']));
gulp.watch(['src/3rdparty/**/*'], gulp.series(['3rdparty']));
done();
});
gulp.task('serve', serve({ root: 'dist', port: 4000 }));
gulp.task('develop', gulp.series(['default', 'watch', 'serve']));
gulp.task('default', ['clean', 'html', 'js', '3rdparty', 'images', 'css'], function () {});
gulp.task('develop', ['watch'], serve({ root: 'dist', port: 4000 }));
+1764 -2942
View File
File diff suppressed because it is too large Load Diff
+6 -11
View File
@@ -12,22 +12,17 @@
"author": "",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.5.0",
"bootstrap-sass": "^3.4.1",
"gulp": "^4.0.0",
"bootstrap-sass": "^3.3.7",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^5.0.0",
"gulp-concat": "^2.6.1",
"gulp-cssnano": "^2.1.3",
"gulp-ejs": "^3.3.0",
"gulp-sass": "^4.0.2",
"gulp-ejs": "^3.1.2",
"gulp-sass": "^4.0.1",
"gulp-serve": "^1.4.0",
"gulp-sourcemaps": "^2.6.5",
"gulp-sourcemaps": "^2.6.4",
"gulp-uglify": "^3.0.0",
"rimraf": "^2.6.2",
"yargs": "^11.0.0"
},
"eslintConfig": {
"env": {
"browser": true
}
}
}
-85
View File
@@ -1,85 +0,0 @@
/**
* angular-ui-notification - Angular.js service providing simple notifications using Bootstrap 3 styles with css transitions for animating
* @author Alex_Crack
* @version v0.3.6
* @link https://github.com/alexcrack/angular-ui-notification
* @license MIT
*/
.ui-notification
{
position: fixed;
z-index: 9999;
width: 300px;
-webkit-transition: all ease .5s;
-o-transition: all ease .5s;
transition: all ease .5s;
color: #fff;
border-radius: 0;
background: #337ab7;
box-shadow: 5px 5px 10px rgba(0, 0, 0, .3);
}
.ui-notification.clickable
{
cursor: pointer;
}
.ui-notification.clickable:hover
{
opacity: .7;
}
.ui-notification.killed
{
-webkit-transition: opacity ease 1s;
-o-transition: opacity ease 1s;
transition: opacity ease 1s;
opacity: 0;
}
.ui-notification > h3
{
font-size: 14px;
font-weight: bold;
display: block;
margin: 10px 10px 0 10px;
padding: 0 0 5px 0;
text-align: left;
border-bottom: 1px solid rgba(255, 255, 255, .3);
}
.ui-notification a
{
color: #fff;
}
.ui-notification a:hover
{
text-decoration: underline;
}
.ui-notification > .message
{
margin: 10px 10px 10px 10px;
}
.ui-notification.warning
{
color: #fff;
background: #f0ad4e;
}
.ui-notification.error
{
color: #fff;
background: #d9534f;
}
.ui-notification.success
{
color: #fff;
background: #5cb85c;
}
.ui-notification.info
{
color: #fff;
background: #5bc0de;
}
+8
View File
@@ -0,0 +1,8 @@
/**
* angular-ui-notification - Angular.js service providing simple notifications using Bootstrap 3 styles with css transitions for animating
* @author Alex_Crack
* @version v0.3.5
* @link https://github.com/alexcrack/angular-ui-notification
* @license MIT
*/
.ui-notification{position:fixed;z-index:9999;width:300px;-webkit-transition:all ease .5s;-o-transition:all ease .5s;transition:all ease .5s;color:#fff;border-radius:0;background:#337ab7;box-shadow:5px 5px 10px rgba(0,0,0,.3)}.ui-notification.clickable{cursor:pointer}.ui-notification.clickable:hover{opacity:.7}.ui-notification.killed{-webkit-transition:opacity ease 1s;-o-transition:opacity ease 1s;transition:opacity ease 1s;opacity:0}.ui-notification>h3{font-size:14px;font-weight:700;display:block;margin:10px 10px 0;padding:0 0 5px;text-align:left;border-bottom:1px solid rgba(255,255,255,.3)}.ui-notification a{color:#fff}.ui-notification a:hover{text-decoration:underline}.ui-notification>.message{margin:10px}.ui-notification.warning{color:#fff;background:#f0ad4e}.ui-notification.error{color:#fff;background:#d9534f}.ui-notification.success{color:#fff;background:#5cb85c}.ui-notification.info{color:#fff;background:#5bc0de}
+2337
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
-271
View File
@@ -1,271 +0,0 @@
/**
* angular-ui-notification - Angular.js service providing simple notifications using Bootstrap 3 styles with css transitions for animating
* @author Alex_Crack
* @version v0.3.6
* @link https://github.com/alexcrack/angular-ui-notification
* @license MIT
*/
angular.module('ui-notification', []);
angular.module('ui-notification').provider('Notification', function () {
this.options = {
delay: 5000,
startTop: 10,
startRight: 10,
verticalSpacing: 10,
horizontalSpacing: 10,
positionX: 'right',
positionY: 'top',
replaceMessage: false,
templateUrl: 'angular-ui-notification.html',
onClose: undefined,
onClick: undefined,
closeOnClick: true,
maxCount: 0, // 0 - Infinite
container: 'body',
priority: 10
};
this.setOptions = function (options) {
if (!angular.isObject(options)) throw new Error("Options should be an object!");
this.options = angular.extend({}, this.options, options);
};
this.$get = ["$timeout", "$http", "$compile", "$templateCache", "$rootScope", "$injector", "$sce", "$q", "$window", function ($timeout, $http, $compile, $templateCache, $rootScope, $injector, $sce, $q, $window) {
var options = this.options;
var startTop = options.startTop;
var startRight = options.startRight;
var verticalSpacing = options.verticalSpacing;
var horizontalSpacing = options.horizontalSpacing;
var delay = options.delay;
var messageElements = [];
var isResizeBound = false;
var notify = function (args, t) {
var deferred = $q.defer();
if (typeof args !== 'object' || args === null) {
args = {message: args};
}
args.scope = args.scope ? args.scope : $rootScope;
args.template = args.templateUrl ? args.templateUrl : options.templateUrl;
args.delay = !angular.isUndefined(args.delay) ? args.delay : delay;
args.type = t || args.type || options.type || '';
args.positionY = args.positionY ? args.positionY : options.positionY;
args.positionX = args.positionX ? args.positionX : options.positionX;
args.replaceMessage = args.replaceMessage ? args.replaceMessage : options.replaceMessage;
args.onClose = args.onClose ? args.onClose : options.onClose;
args.onClick = args.onClick ? args.onClick : options.onClick;
args.closeOnClick = (args.closeOnClick !== null && args.closeOnClick !== undefined) ? args.closeOnClick : options.closeOnClick;
args.container = args.container ? args.container : options.container;
args.priority = args.priority ? args.priority : options.priority;
var template = $templateCache.get(args.template);
if (template) {
processNotificationTemplate(template);
} else {
// load it via $http only if it isn't default template and template isn't exist in template cache
// cache:true means cache it for later access.
$http.get(args.template, {cache: true})
.then(function (response) {
processNotificationTemplate(response.data);
})
.catch(function (data) {
throw new Error('Template (' + args.template + ') could not be loaded. ' + data);
});
}
function processNotificationTemplate(template) {
var scope = args.scope.$new();
scope.message = $sce.trustAsHtml(args.message);
scope.title = $sce.trustAsHtml(args.title);
scope.t = args.type.substr(0, 1);
scope.delay = args.delay;
scope.onClose = args.onClose;
scope.onClick = args.onClick;
var priorityCompareTop = function (a, b) {
return a._priority - b._priority;
};
var priorityCompareBtm = function (a, b) {
return b._priority - a._priority;
};
var reposite = function () {
var j = 0;
var k = 0;
var lastTop = startTop;
var lastRight = startRight;
var lastPosition = [];
if (args.positionY === 'top') {
messageElements.sort(priorityCompareTop);
} else if (args.positionY === 'bottom') {
messageElements.sort(priorityCompareBtm);
}
for (var i = messageElements.length - 1; i >= 0; i--) {
var element = messageElements[i];
if (args.replaceMessage && i < messageElements.length - 1) {
element.addClass('killed');
continue;
}
var elHeight = parseInt(element[0].offsetHeight);
var elWidth = parseInt(element[0].offsetWidth);
var position = lastPosition[element._positionY + element._positionX];
if ((top + elHeight) > window.innerHeight) {
position = startTop;
k++;
j = 0;
}
var top = (lastTop = position ? (j === 0 ? position : position + verticalSpacing) : startTop);
var right = lastRight + (k * (horizontalSpacing + elWidth));
element.css(element._positionY, top + 'px');
if (element._positionX === 'center') {
element.css('left', parseInt(window.innerWidth / 2 - elWidth / 2) + 'px');
} else {
element.css(element._positionX, right + 'px');
}
lastPosition[element._positionY + element._positionX] = top + elHeight;
if (options.maxCount > 0 && messageElements.length > options.maxCount && i === 0) {
element.scope().kill(true);
}
j++;
}
};
var templateElement = $compile(template)(scope);
templateElement._positionY = args.positionY;
templateElement._positionX = args.positionX;
templateElement._priority = args.priority;
templateElement.addClass(args.type);
var closeEvent = function (e) {
e = e.originalEvent || e;
if (e.type === 'click' || e.propertyName === 'opacity' && e.elapsedTime >= 1) {
if (scope.onClose) {
scope.$apply(scope.onClose(templateElement));
}
if (e.type === 'click')
if (scope.onClick) {
scope.$apply(scope.onClick(templateElement));
}
templateElement.remove();
messageElements.splice(messageElements.indexOf(templateElement), 1);
scope.$destroy();
reposite();
}
};
if (args.closeOnClick) {
templateElement.addClass('clickable');
templateElement.bind('click', closeEvent);
}
templateElement.bind('webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd', closeEvent);
if (angular.isNumber(args.delay)) {
$timeout(function () {
templateElement.addClass('killed');
}, args.delay);
}
setCssTransitions('none');
angular.element(document.querySelector(args.container)).append(templateElement);
var offset = -(parseInt(templateElement[0].offsetHeight) + 50);
templateElement.css(templateElement._positionY, offset + "px");
messageElements.push(templateElement);
if (args.positionX == 'center') {
var elWidth = parseInt(templateElement[0].offsetWidth);
templateElement.css('left', parseInt(window.innerWidth / 2 - elWidth / 2) + 'px');
}
$timeout(function () {
setCssTransitions('');
});
function setCssTransitions(value) {
['-webkit-transition', '-o-transition', 'transition'].forEach(function (prefix) {
templateElement.css(prefix, value);
});
}
scope._templateElement = templateElement;
scope.kill = function (isHard) {
if (isHard) {
if (scope.onClose) {
scope.$apply(scope.onClose(scope._templateElement));
}
messageElements.splice(messageElements.indexOf(scope._templateElement), 1);
scope._templateElement.remove();
scope.$destroy();
$timeout(reposite);
} else {
scope._templateElement.addClass('killed');
}
};
$timeout(reposite);
if (!isResizeBound) {
angular.element($window).bind('resize', function (e) {
$timeout(reposite);
});
isResizeBound = true;
}
deferred.resolve(scope);
}
return deferred.promise;
};
notify.primary = function (args) {
return this(args, 'primary');
};
notify.error = function (args) {
return this(args, 'error');
};
notify.success = function (args) {
return this(args, 'success');
};
notify.info = function (args) {
return this(args, 'info');
};
notify.warning = function (args) {
return this(args, 'warning');
};
notify.clearAll = function () {
angular.forEach(messageElements, function (element) {
element.addClass('killed');
});
};
return notify;
}];
});
angular.module("ui-notification").run(["$templateCache", function($templateCache) {$templateCache.put("angular-ui-notification.html","<div class=\"ui-notification\"><h3 ng-show=\"title\" ng-bind-html=\"title\"></h3><div class=\"message\" ng-bind-html=\"message\"></div></div>");}]);
+8
View File
@@ -0,0 +1,8 @@
/**
* angular-ui-notification - Angular.js service providing simple notifications using Bootstrap 3 styles with css transitions for animating
* @author Alex_Crack
* @version v0.3.5
* @link https://github.com/alexcrack/angular-ui-notification
* @license MIT
*/
angular.module("ui-notification",[]),angular.module("ui-notification").provider("Notification",function(){this.options={delay:5e3,startTop:10,startRight:10,verticalSpacing:10,horizontalSpacing:10,positionX:"right",positionY:"top",replaceMessage:!1,templateUrl:"angular-ui-notification.html",onClose:void 0,closeOnClick:!0,maxCount:0,container:"body"},this.setOptions=function(e){if(!angular.isObject(e))throw new Error("Options should be an object!");this.options=angular.extend({},this.options,e)},this.$get=["$timeout","$http","$compile","$templateCache","$rootScope","$injector","$sce","$q","$window",function(e,t,n,i,o,s,a,l,r){var c=this.options,p=c.startTop,d=c.startRight,u=c.verticalSpacing,f=c.horizontalSpacing,m=c.delay,g=[],h=!1,C=function(s,C){function y(t){function i(e){["-webkit-transition","-o-transition","transition"].forEach(function(t){m.css(t,e)})}var o=s.scope.$new();o.message=a.trustAsHtml(s.message),o.title=a.trustAsHtml(s.title),o.t=s.type.substr(0,1),o.delay=s.delay,o.onClose=s.onClose;var l=function(){for(var e=0,t=0,n=p,i=d,o=[],a=g.length-1;a>=0;a--){var l=g[a];if(s.replaceMessage&&a<g.length-1)l.addClass("killed");else{var r=parseInt(l[0].offsetHeight),m=parseInt(l[0].offsetWidth),h=o[l._positionY+l._positionX];C+r>window.innerHeight&&(h=p,t++,e=0);var C=n=h?0===e?h:h+u:p,y=i+t*(f+m);l.css(l._positionY,C+"px"),"center"==l._positionX?l.css("left",parseInt(window.innerWidth/2-m/2)+"px"):l.css(l._positionX,y+"px"),o[l._positionY+l._positionX]=C+r,c.maxCount>0&&g.length>c.maxCount&&0===a&&l.scope().kill(!0),e++}}},m=n(t)(o);m._positionY=s.positionY,m._positionX=s.positionX,m.addClass(s.type);var C=function(e){e=e.originalEvent||e,("click"===e.type||"opacity"===e.propertyName&&e.elapsedTime>=1)&&(o.onClose&&o.$apply(o.onClose(m)),m.remove(),g.splice(g.indexOf(m),1),o.$destroy(),l())};s.closeOnClick&&(m.addClass("clickable"),m.bind("click",C)),m.bind("webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd",C),angular.isNumber(s.delay)&&e(function(){m.addClass("killed")},s.delay),i("none"),angular.element(document.querySelector(s.container)).append(m);var y=-(parseInt(m[0].offsetHeight)+50);if(m.css(m._positionY,y+"px"),g.push(m),"center"==s.positionX){var k=parseInt(m[0].offsetWidth);m.css("left",parseInt(window.innerWidth/2-k/2)+"px")}e(function(){i("")}),o._templateElement=m,o.kill=function(t){t?(o.onClose&&o.$apply(o.onClose(o._templateElement)),g.splice(g.indexOf(o._templateElement),1),o._templateElement.remove(),o.$destroy(),e(l)):o._templateElement.addClass("killed")},e(l),h||(angular.element(r).bind("resize",function(t){e(l)}),h=!0),v.resolve(o)}var v=l.defer();"object"!=typeof s&&(s={message:s}),s.scope=s.scope?s.scope:o,s.template=s.templateUrl?s.templateUrl:c.templateUrl,s.delay=angular.isUndefined(s.delay)?m:s.delay,s.type=C||s.type||c.type||"",s.positionY=s.positionY?s.positionY:c.positionY,s.positionX=s.positionX?s.positionX:c.positionX,s.replaceMessage=s.replaceMessage?s.replaceMessage:c.replaceMessage,s.onClose=s.onClose?s.onClose:c.onClose,s.closeOnClick=null!==s.closeOnClick&&void 0!==s.closeOnClick?s.closeOnClick:c.closeOnClick,s.container=s.container?s.container:c.container;var k=i.get(s.template);return k?y(k):t.get(s.template,{cache:!0}).then(y)["catch"](function(e){throw new Error("Template ("+s.template+") could not be loaded. "+e)}),v.promise};return C.primary=function(e){return this(e,"primary")},C.error=function(e){return this(e,"error")},C.success=function(e){return this(e,"success")},C.info=function(e){return this(e,"info")},C.warning=function(e){return this(e,"warning")},C.clearAll=function(){angular.forEach(g,function(e){e.addClass("killed")})},C}]}),angular.module("ui-notification").run(["$templateCache",function(e){e.put("angular-ui-notification.html",'<div class="ui-notification"><h3 ng-show="title" ng-bind-html="title"></h3><div class="message" ng-bind-html="message"></div></div>')}]);
+44 -38
View File
@@ -1,30 +1,30 @@
<!DOCTYPE html>
<html ng-app="Application" ng-controller="Controller">
<head>
<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" />
<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" />
<title> Cloudron Error </title>
<title> Cloudron Error </title>
<link id="favicon" type="image/png" rel="icon" href="/api/v1/cloudron/avatar">
<link id="favicon" type="image/png" rel="icon" href="/api/v1/cloudron/avatar">
<!-- Theme CSS -->
<link type="text/css" rel="stylesheet" href="/theme.css">
<!-- Theme CSS -->
<link type="text/css" rel="stylesheet" href="/theme.css">
<!-- external fonts and CSS -->
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.css">
<!-- external fonts and CSS -->
<link type="text/css" rel="stylesheet" href="/3rdparty/css/font-awesome.min.css">
<!-- jQuery-->
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
<!-- jQuery-->
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
<!-- Bootstrap Core JavaScript -->
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
<!-- Bootstrap Core JavaScript -->
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
<!-- Angularjs scripts -->
<script type="text/javascript" src="/3rdparty/js/angular.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js"></script>
<!-- Angularjs scripts -->
<script type="text/javascript" src="/3rdparty/js/angular.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js"></script>
<script>
<script>
'use strict';
@@ -50,42 +50,48 @@
var search = window.location.search.slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.errorCode = search.errorCode || 0;
$scope.errorContext = search.errorContext || '';
}]);
</script>
<style>
h3 {
padding-bottom: 15px;
}
</style>
</script>
</head>
<body class="status-page">
<div class="wrapper">
<div class="content">
<img ng-src="{{avatarUrl}}" width="128" height="128" onerror="this.src = '/img/logo.png'"/>
<h1> Cloudron </h1>
<div>
<h3> <i class="far fa-frown fa-fw text-danger"></i> Something has gone wrong </h3>
<a class="btn btn-primary" href="/" ng-show="statusOk">Back to the dashboard</a>
<br/>
<br/>
<p>If you are the server administrator, follow the <a href="https://cloudron.io/documentation/troubleshooting/" target="_blank">troubleshooting guide</a>.</p>
<div class="content">
<img ng-src="avatarUrl" width="128" height="128" onerror="this.src = '/img/logo.png'"/>
<h1> Cloudron </h1>
<div ng-show="errorCode == 0">
<h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Something has gone wrong </h3>
<span ng-show="statusOk">Please try again reloading the page <a href="/">here</a>.</span>
</div>
<div ng-show="errorCode == 1">
<h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Cloudron is not setup </h3>
Please use the setup link you received via mail.
</div>
<div ng-show="errorCode == 2">
<h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Setup requires a setupToken in the query </h3>
Please use the setup link you received via mail.
</div>
<div ng-show="errorCode == 3">
<h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Setup requires an email in the query </h3>
Please use the setup link you received via mail.
</div>
</div>
</div>
</div>
<footer class="text-center">
<span class="text-muted">&copy;2019 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fab fa-twitter"></i></a></span>
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
<span class="text-muted">&copy;2018 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
</footer>
</body>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

-102
View File
@@ -1,102 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="546.13336"
height="546.13336"
viewBox="0 0 512.00001 512.00001"
id="svg4519"
version="1.1"
inkscape:version="0.92.2 2405546, 2018-03-11"
sodipodi:docname="appicon_fallback.svg"
inkscape:export-filename="/home/nebulon/projects/yellowtent/dashboard/src/img/appicon_fallback.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4521" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.7"
inkscape:cx="89.894291"
inkscape:cy="162.5294"
inkscape:document-units="px"
inkscape:current-layer="g4496"
showgrid="false"
units="px"
inkscape:window-width="2880"
inkscape:window-height="1565"
inkscape:window-x="0"
inkscape:window-y="55"
inkscape:window-maximized="1" />
<metadata
id="metadata4524">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-540.36216)">
<g
id="g4467"
transform="matrix(20.50952,0,0,20.859456,-526.58031,-94.042799)">
<g
inkscape:export-ydpi="67.349998"
inkscape:export-xdpi="67.349998"
transform="matrix(0.59473169,0,0,0.59473169,31.04719,102.48374)"
id="g4382">
<g
id="g4496">
<path
sodipodi:type="star"
style="opacity:1;fill:#03a9f4;fill-opacity:1;stroke:none;stroke-width:1.10000002;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path4162"
sodipodi:sides="6"
sodipodi:cx="12.46875"
sodipodi:cy="-99.893143"
sodipodi:r1="19.266006"
sodipodi:r2="16.307295"
sodipodi:arg1="-0.52224059"
sodipodi:arg2="0.0013581913"
inkscape:flatsided="true"
inkscape:rounded="0.12490573"
inkscape:randomized="0"
d="m 29.166669,-109.50348 c 1.200386,2.08567 1.17988,17.183595 -0.02617,19.265993 -1.206046,2.082397 -14.291486,9.613601 -16.697919,9.610333 -2.406432,-0.0033 -15.4713664,-7.56999 -16.671752,-9.655655 -1.2003857,-2.085666 -1.1798799,-17.183591 0.026167,-19.265991 1.2060467,-2.0824 14.2914862,-9.6136 16.6979192,-9.61033 2.406432,0.003 15.471366,7.56999 16.671752,9.65565 z"
transform="rotate(-30,10.993604,-99.259973)"
inkscape:export-xdpi="67.349998"
inkscape:export-ydpi="67.349998" />
<rect
inkscape:transform-center-x="0.66390665"
ry="3.9522502"
y="-107.69034"
x="4.8100815"
height="14.288903"
width="14.288903"
id="rect4168-1-1"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.75875854;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:transform-center-y="3.7035412e-06" />
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

+19 -23
View File
@@ -13,12 +13,12 @@
<!-- CSS -->
<link type="text/css" rel="stylesheet" href="/3rdparty/slick.css?<%= revision %>"/>
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.css?<%= revision %>"/>
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.min.css?<%= revision %>"/>
<link type="text/css" rel="stylesheet" href="/3rdparty/bootstrap-slider/bootstrap-slider.min.css?<%= revision %>"/>
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
<!-- Custom Fonts -->
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.css?<%= revision %>">
<link type="text/css" rel="stylesheet" href="/3rdparty/css/font-awesome.min.css?<%= revision %>">
<!-- jQuery-->
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
@@ -41,7 +41,7 @@
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-sanitize.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-slick.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-fittext.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/autofill-event.js?<%= revision %>"></script>
@@ -90,8 +90,6 @@
</div>
</script>
<div class="offline-banner animateMe" ng-show="client.offline"><i class="fa fa-circle-notch fa-spin"></i> Cloudron is offline. Reconnecting...</div>
<!-- Modal setup subscription -->
<div class="modal fade" id="setupSubscriptionModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
@@ -102,13 +100,13 @@
</div>
<div class="modal-body">
<p ng-show="config.update.box">
You can update to the next version once you have <a ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + subscription.emailEncoded + '&cloudronId=' + subscription.cloudronId }}" target="_blank">setup billing</a>.
You can update to the next version once you have <a ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.emailEncoded + '&cloudronId=' + appstoreConfig.cloudronId }}" target="_blank">setup billing</a>.
</p>
<p>
Our paid plans allow you to install unlimited standard and premium apps.
Our paid plans allow you to install more apps and create more users.
</div>
<div class="modal-footer">
<a class="btn btn-success" ng-click="waitForPlanSelection()" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + subscription.emailEncoded + '&cloudronId=' + subscription.cloudronId }}" target="_blank" ng-disabled="waitingForPlanSelection"><i class="fa fa-circle-notch fa-spin" ng-show="waitingForPlanSelection"></i> Setup Subscription</a>
<a class="btn btn-success" ng-click="waitForPlanSelection()" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.emailEncoded + '&cloudronId=' + appstoreConfig.cloudronId }}" target="_blank" ng-disabled="waitingForPlanSelection"><i class="fa fa-circle-o-notch fa-spin" ng-show="waitingForPlanSelection"></i> Setup Subscription</a>
</div>
</div>
</div>
@@ -133,37 +131,35 @@
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav navbar-right" ng-hide="hideNavBarActions">
<li ng-show="subscription.plan && subscription.plan.id === 'free'">
<li ng-show="ready && subscription.plan.id === 'free'">
<a ng-href="" ng-click="showSubscriptionModal()" style="cursor: pointer">
<span class="badge badge-success">Setup Subscription</span>
</a>
</li>
<li>
<a ng-class="{ active: isActive('/apps')}" href="#/apps"><i class="fa fa-cloud-download-alt fa-fw"></i> My Apps</a>
<a ng-class="{ active: isActive('/apps')}" href="#/apps"><i class="fa fa-cloud-download fa-fw"></i> My Apps</a>
</li>
<li ng-show="user.admin">
<a ng-class="{ active: isActive('/appstore')}" href="#/appstore"><i class="fa fa-th fa-fw"></i> App Store</a>
<li ng-show="user.admin || config.features.spaces">
<a ng-class="{ active: isActive('/appstore')}" href="#/appstore"><i class="fa fa-th-large fa-fw"></i> App Store</a>
</li>
<li ng-show="user.admin">
<a ng-class="{ active: isActive('/users')}" href="#/users"><i class="fa fa-users fa-fw"></i> Users</a>
</li>
<li class="dropdown">
<a href="" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><img ng-src="{{user.gravatar}}" style="margin-top: -4px;"/> {{user.username}} <span class="badge badge-danger" ng-show="notifications.length">{{ notifications.length }}</span> <span class="caret"></span></a>
<a href="" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><img ng-src="{{user.gravatar}}" style="margin-top: -4px;"/> {{user.username}} <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="#/account"><i class="fa fa-user fa-fw"></i> Account</a></li>
<li ng-show="user.admin"><a href="#/activity"><i class="fa fa-list-alt fa-fw"></i> Activity</a></li>
<!-- <li ng-show="user.admin"><a href="#/tokens"><i class="fa fa-key fa-fw"></i> API Access</a></li> -->
<li ng-show="user.admin"><a href="#/backups"><i class="fa fa-archive fa-fw"></i> Backups</a></li>
<li ng-show="user.admin"><a href="#/domains"><i class="fa fa-globe fa-fw"></i> Domains & Certs</a></li>
<li ng-show="user.admin"><a href="#/domains"><i class="fa fa-globe fa-fw"></i> Domains</a></li>
<li ng-show="user.admin"><a href="#/email"><i class="fa fa-envelope fa-fw"></i> Email</a></li>
<li ng-show="user.admin"><a href="#/activity"><i class="fa fa-list-alt fa-fw"></i> Event Log</a></li>
<li ng-show="user.admin"><a href="#/graphs"><i class="fa fa-chart-bar fa-fw"></i> Graphs</a></li>
<li><a href="#/notifications"><i class="fa fa-bell fa-fw"></i> Notifications <span class="badge badge-danger pull-right" ng-show="notifications.length">{{ notifications.length }}</span></a></li>
<li ng-show="user.admin"><a href="#/graphs"><i class="fa fa-bar-chart fa-fw"></i> Graphs</a></li>
<li ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
<li ng-show="user.admin" class="divider"></li>
<li ng-show="user.admin"><a href="#/support"><i class="fa fa-comment fa-fw"></i> Support</a></li>
<li ng-show="user.admin"><a href="#/system"><i class="fa fa-cogs fa-fw"></i> System</a></li>
<li ng-show="user.admin && config.features.operatorActions" class="divider"></li>
<li ng-show="user.admin && config.features.operatorActions"><a href="#/support"><i class="fa fa-comment fa-fw"></i> Support</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>
<li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out fa-fw"></i> Logout</a></li>
</ul>
</li>
</ul>
@@ -175,9 +171,9 @@
<div ng-view id="ng-view" class="layout-content"></div>
<footer class="text-center ng-cloak">
<span class="text-muted">&copy; 2019 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted">&copy; 2018 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"> v{{config.version}}</span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fab fa-twitter"></i></a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
</footer>
</div>
+213
View File
@@ -0,0 +1,213 @@
'use strict';
/* global angular:false */
angular.module('Application').service('AppStore', ['$http', '$base64', 'Client', function ($http, $base64, Client) {
function AppStoreError(statusCode, message) {
Error.call(this);
this.name = this.constructor.name;
this.statusCode = statusCode;
if (typeof message == 'string') {
this.message = message;
} else {
this.message = JSON.stringify(message);
}
}
function AppStore() {
this._appsCache = [];
}
AppStore.prototype.getApps = function (callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
var that = this;
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/apps', { params: { boxVersion: Client.getConfig().version } }).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
angular.copy(data.apps, that._appsCache);
return callback(null, that._appsCache);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getAppsFast = function (callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
if (this._appsCache.length !== 0) return callback(null, this._appsCache);
this.getApps(callback);
};
AppStore.prototype.getAppById = function (appId, callback) {
var that = this;
// check cache
for (var app in this._appsCache) {
if (this._appsCache[app].id === appId) return callback(null, this._appsCache[app]);
}
this.getApps(function (error) {
if (error) return callback(error);
// recheck cache
for (var app in that._appsCache) {
if (that._appsCache[app].id === appId) return callback(null, that._appsCache[app]);
}
callback(new AppStoreError(404, 'Not found'));
});
};
AppStore.prototype.getAppByIdAndVersion = function (appId, version, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
// check cache
for (var app in this._appsCache) {
if (this._appsCache[app].id === appId && this._appsCache[app].manifest.version === version) return callback(null, this._appsCache[app]);
}
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/apps/' + appId + '/versions/' + version).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getAppById = function (appId, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
// do not check cache, always get the latest
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/apps/' + appId).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getManifest = function (appId, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
var manifestUrl = Client.getConfig().apiServerOrigin + '/api/v1/apps/' + appId;
console.log('Getting the manifest of ', appId, manifestUrl);
$http.get(manifestUrl).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data.manifest);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getSizes = function (callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/sizes').success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data.sizes);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getRegions = function (callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/regions').success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data.regions);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.register = function (email, password, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
var data = {
email: email,
password: password
};
$http.post(Client.getConfig().apiServerOrigin + '/api/v1/users', data).success(function (data, status) {
if (status !== 201) return callback(new AppStoreError(status, data));
return callback(null, data);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.login = function (email, password, totpToken, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
var data = {
email: email,
password: password,
persistent: true,
totpToken: totpToken
};
$http.post(Client.getConfig().apiServerOrigin + '/api/v1/login', data).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.logout = function (email, password, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.post(Client.getConfig().apiServerOrigin + '/api/v1/logout').success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getProfile = function (token, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/profile', { params: { accessToken: token }}).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
// just some helper property, since angular bindings cannot dot his easily
data.profile.emailEncoded = encodeURIComponent(data.profile.email);
return callback(null, data.profile);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getCloudronDetails = function (appstoreConfig, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId, { params: { accessToken: appstoreConfig.token }}).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data.cloudron);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getSubscription = function (appstoreConfig, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/subscription', { params: { accessToken: appstoreConfig.token }}).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data.subscription);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
return new AppStore();
}]);
+448 -918
View File
File diff suppressed because it is too large Load Diff
+30 -210
View File
@@ -3,7 +3,6 @@
/* global angular:false */
/* global showdown:false */
/* global moment:false */
/* global $:false */
// deal with accessToken in the query, this is passed for example on password reset and account setup upon invite
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
@@ -19,32 +18,6 @@ if (search.accessToken) {
}
// poor man's async in the global namespace
function asyncForEachParallel(items, handler, callback) {
var alreadyDone = 0;
var errored = false;
if (items.length === 0) return callback();
function done(error) {
// do nothing if already called back due to error
if (errored) return;
if (error) {
errored = true;
return callback(error);
}
++alreadyDone;
// we are done
if (alreadyDone === items.length) callback();
}
for (var i = 0; i < items.length; ++i) {
handler(items[i], done);
}
}
function asyncForEach(items, handler, callback) {
var cur = 0;
@@ -85,6 +58,7 @@ app.config(['NotificationProvider', function (NotificationProvider) {
delay: 5000,
startTop: 60,
positionX: 'left',
maxCount: 3,
templateUrl: 'notification.html'
});
}]);
@@ -123,9 +97,6 @@ app.config(['$routeProvider', function ($routeProvider) {
}).when('/email/:domain', {
controller: 'EmailController',
templateUrl: 'views/email.html?<%= revision %>'
}).when('/notifications', {
controller: 'NotificationsController',
templateUrl: 'views/notifications.html?<%= revision %>'
}).when('/settings', {
controller: 'SettingsController',
templateUrl: 'views/settings.html?<%= revision %>'
@@ -135,9 +106,6 @@ app.config(['$routeProvider', function ($routeProvider) {
}).when('/support', {
controller: 'SupportController',
templateUrl: 'views/support.html?<%= revision %>'
}).when('/system', {
controller: 'SystemController',
templateUrl: 'views/system.html?<%= revision %>'
}).when('/tokens', {
controller: 'TokensController',
templateUrl: 'views/tokens.html?<%= revision %>'
@@ -197,7 +165,7 @@ app.filter('activeOAuthClients', function () {
app.filter('prettyAppMessage', function () {
return function (message) {
if (message === 'ETRYAGAIN') return 'The DNS record for this location is not setup correctly. Please verify your DNS settings and repair this app.';
if (message === 'DNS Record already exists') return 'The DNS record for this location already exists. Cloudron does not remove existing DNS records. Manually remove the DNS record and then click on repair.';
if (message === 'DNS Record already exists') return 'The DNS record for this location already exists. Manually remove the DNS record and then click on repair.';
return message;
};
});
@@ -209,36 +177,6 @@ app.filter('shortAppMessage', function () {
};
});
app.filter('selectedTagFilter', function () {
return function selectedTagFilter(apps, selectedTags) {
return apps.filter(function (app) {
if (selectedTags.length === 0) return true;
return !!selectedTags.find(function (tag) {
return !app.tags ? false : (app.tags.indexOf(tag) !== -1);
});
});
};
});
app.filter('selectedDomainFilter', function () {
return function selectedDomainFilter(apps, selectedDomains) {
return apps.filter(function (app) {
if (selectedDomains.length === 0) return true;
return !!selectedDomains.find(function (domain) {
return app.domain === domain.domain;
});
});
};
});
app.filter('prettyDomains', function () {
return function prettyDomains(domains) {
return domains.map(function (d) { return d.domain; }).join(', ');
};
});
app.filter('prettyMemory', function () {
return function (memory) {
// Adjust the default memory limit if it changes
@@ -286,6 +224,7 @@ app.filter('installationStateLabel', function() {
else if (app.runState === 'pending_stop') return 'Stopping...';
else if (app.runState === 'stopped') return 'Stopped';
else return app.runState;
break;
}
default: return app.installationState;
}
@@ -357,12 +296,6 @@ app.filter('prettyLongDate', function () {
};
});
app.filter('prettyShortDate', function () {
return function prettyShortDate(time) {
return moment(time).format('MMMM Do YYYY');
};
});
app.filter('markdown2html', function () {
var converter = new showdown.Converter({
extensions: ['targetblank'],
@@ -398,33 +331,16 @@ app.filter('postInstallMessage', function () {
// keep this in sync with eventlog.js and CLI tool
var ACTION_ACTIVATE = 'cloudron.activate';
var ACTION_PROVISION = 'cloudron.provision';
var ACTION_RESTORE = 'cloudron.restore';
var ACTION_APP_CLONE = 'app.clone';
var ACTION_APP_CONFIGURE = 'app.configure';
var ACTION_APP_INSTALL = 'app.install';
var ACTION_APP_RESTORE = 'app.restore';
var ACTION_APP_UNINSTALL = 'app.uninstall';
var ACTION_APP_UPDATE = 'app.update';
var ACTION_APP_LOGIN = 'app.login';
var ACTION_APP_OOM = 'app.oom';
var ACTION_APP_UP = 'app.up';
var ACTION_APP_DOWN = 'app.down';
var ACTION_APP_TASK_CRASH = 'app.task.crash';
var ACTION_BACKUP_FINISH = 'backup.finish';
var ACTION_BACKUP_START = 'backup.start';
var ACTION_BACKUP_CLEANUP_START = 'backup.cleanup.start';
var ACTION_BACKUP_CLEANUP_FINISH = 'backup.cleanup.finish';
var ACTION_CERTIFICATE_NEW = 'certificate.new';
var ACTION_BACKUP_CLEANUP = 'backup.cleanup';
var ACTION_CERTIFICATE_RENEWAL = 'certificate.renew';
var ACTION_DASHBOARD_DOMAIN_UPDATE = 'dashboard.domain.update';
var ACTION_DOMAIN_ADD = 'domain.add';
var ACTION_DOMAIN_UPDATE = 'domain.update';
var ACTION_DOMAIN_REMOVE = 'domain.remove';
var ACTION_START = 'cloudron.start';
var ACTION_UPDATE = 'cloudron.update';
var ACTION_USER_ADD = 'user.add';
@@ -433,142 +349,64 @@ var ACTION_USER_REMOVE = 'user.remove';
var ACTION_USER_UPDATE = 'user.update';
var ACTION_USER_TRANSFER = 'user.transfer';
var ACTION_MAIL_ENABLED = 'mail.enabled';
var ACTION_MAIL_DISABLED = 'mail.disabled';
var ACTION_MAIL_MAILBOX_ADD = 'mail.box.add';
var ACTION_MAIL_MAILBOX_REMOVE = 'mail.box.remove';
var ACTION_MAIL_LIST_ADD = 'mail.list.add';
var ACTION_MAIL_LIST_REMOVE = 'mail.list.remove';
var ACTION_DYNDNS_UPDATE = 'dyndns.update';
var ACTION_SYSTEM_CRASH = 'system.crash';
app.filter('eventLogSource', ['Client', function (Client) {
app.filter('eventLogSource', function() {
return function(eventLog) {
var source = eventLog.source;
var line = '';
var data = eventLog.data;
var errorMessage = data.errorMessage;
line = source.username || source.userId || source.authType || 'system';
if (source.appId) {
var app = Client.getCachedAppSync(source.appId);
line += ' - ' + (app ? app.fqdn : source.appId);
} else if (source.ip) {
line += ' - ' + source.ip;
}
// <span ng-show="eventLog.source.ip || eventLog.source.appId"> ({{ eventLog.source.ip || eventLog.source.appId }}) </span>
var line = source.username || source.userId || source.authType || 'system';
if (source.app) line += ' - ' + source.app.fqdn;
else if (source.ip) line += ' - ' + source.ip;
else if (source.appId) line += ' - ' + source.appId;
return line;
};
}]);
});
app.filter('eventLogDetails', function() {
// NOTE: if you change this, the CLI tool (cloudron machine eventlog) probably needs fixing as well
return function(eventLog) {
var source = eventLog.source;
var data = eventLog.data;
var errorMessage = data.errorMessage;
var details;
switch (eventLog.action) {
case ACTION_ACTIVATE:
return 'Cloudron was activated';
case ACTION_PROVISION:
return 'Cloudron was setup';
case ACTION_RESTORE:
return 'Cloudron was restored from backup ' + data.backupId;
case ACTION_APP_CONFIGURE:
if (!data.app) return '';
return data.app.manifest.title + ' was re-configured at ' + (data.app.fqdn || data.app.location);
return (data.app ? (data.app.manifest.title + ' was re-configured at ' + (data.app.fqdn || data.app.location)) : '');
case ACTION_APP_INSTALL:
if (!data.app) return '';
return data.app.manifest.title + ' (package v' + data.app.manifest.version + ') was installed at ' + (data.app.fqdn || data.app.location);
return (data.app ? (data.app.manifest.title + ' was installed at ' + (data.app.fqdn || data.app.location)) : '');
case ACTION_APP_RESTORE:
if (!data.app) return '';
details = data.app.manifest.title + ' was restored at ' + (data.app.fqdn || data.app.location);
// older versions (<3.5) did not have these fields
if (data.fromManifest) details += ' from version ' + data.fromManifest.version;
if (data.toManifest) details += ' to version ' + data.toManifest.version;
if (data.backupId) details += ' using backup ' + data.backupId;
return details;
return (data.app ? (data.app.manifest.title + ' was restored at ' + (data.app.fqdn || data.app.location)) : '');
case ACTION_APP_UNINSTALL:
if (!data.app) return '';
return data.app.manifest.title + ' (package v' + data.app.manifest.version + ') was uninstalled at ' + (data.app.fqdn || data.app.location);
return (data.app ? (data.app.manifest.title + ' was uninstalled at ' + (data.app.fqdn || data.app.location)) : '');
case ACTION_APP_UPDATE:
if (!data.app) return '';
return data.app.manifest.title + ' at ' + (data.app.fqdn || data.app.location) + ' was updated from v' + data.fromManifest.version + ' to v' + data.toManifest.version;
case ACTION_APP_CLONE:
return data.newApp.manifest.title + ' at ' + (data.newApp.fqdn || data.newApp.location) + ' was cloned from ' + (data.oldApp.fqdn || data.oldApp.location) + ' from backup ' + data.backupId + ' with v' + data.oldApp.manifest.version;
return (data.app ? (data.app.manifest.title + ' at ' + (data.app.fqdn || data.app.location)) : '') + ' was updated to version ' + data.toManifest.id + '@' + data.toManifest.version;
case ACTION_APP_LOGIN:
return 'App ' + data.appId + ' logged in';
case ACTION_APP_OOM:
return data.app.manifest.title + ' ran out of memory';
case ACTION_APP_DOWN:
return data.app.manifest.title + ' is down';
case ACTION_APP_UP:
return data.app.manifest.title + ' is back online';
case ACTION_APP_TASK_CRASH:
return 'Apptask for app with id ' + data.appId + ' crashed';
case ACTION_BACKUP_START:
return 'Backup started';
case ACTION_BACKUP_FINISH:
return 'Backup finished' + (errorMessage ? (' error: ' + errorMessage) : '');
case ACTION_BACKUP_CLEANUP_START:
return 'Backup cleaner started';
case ACTION_BACKUP_CLEANUP_FINISH:
return data.errorMessage ? 'Backup cleaner errored: ' + data.errorMessage : 'Backup cleaner removed ' + data.removedBoxBackups.length + ' backups';
case ACTION_CERTIFICATE_NEW:
return 'Certificate install for ' + data.domain + (errorMessage ? ' failed' : ' succeeded');
case ACTION_BACKUP_CLEANUP:
return 'Backup ' + data.backup.id + ' removed';
case ACTION_CERTIFICATE_RENEWAL:
return 'Certificate renewal for ' + data.domain + (errorMessage ? ' failed' : ' succeeded');
case ACTION_DASHBOARD_DOMAIN_UPDATE:
return 'Dashboard domain set to ' + data.fqdn;
case ACTION_DOMAIN_ADD:
return 'Domain ' + data.domain + ' with ' + data.provider + ' provider was added';
case ACTION_DOMAIN_UPDATE:
return 'Domain ' + data.domain + ' with ' + data.provider + ' provider was updated';
case ACTION_DOMAIN_REMOVE:
return 'Domain ' + data.domain + ' was removed';
case ACTION_MAIL_ENABLED:
return 'Cloudron Mail was enabled for domain ' + data.domain;
case ACTION_MAIL_DISABLED:
return 'Cloudron Mail was disabled for domain ' + data.domain;
case ACTION_MAIL_MAILBOX_ADD:
return 'Mailbox with name ' + data.name + ' was added in domain ' + data.domain;
case ACTION_MAIL_MAILBOX_REMOVE:
return 'Mailbox with name ' + data.name + ' was removed in domain ' + data.domain;
case ACTION_MAIL_LIST_ADD:
return 'Mail list with name ' + data.name + ' was added in domain ' + data.domain;
case ACTION_MAIL_LIST_REMOVE:
return 'Mail list with name ' + data.name + ' was added in domain ' + data.domain;
case ACTION_START:
return 'Cloudron started with version ' + data.version;
@@ -590,54 +428,36 @@ app.filter('eventLogDetails', function() {
case ACTION_USER_LOGIN:
return (data.user ? (data.user.email + (data.user.username ? ' (' + data.user.username + ')' : '')) : data.userId) + ' logged in';
case ACTION_DYNDNS_UPDATE:
return 'DNS was updated from ' + data.fromIp + ' to ' + data.toIp;
case ACTION_SYSTEM_CRASH:
return 'A system process crashed';
default: return eventLog.action;
}
};
});
app.filter('eventLogAction', function() {
// NOTE: if you change this, the CLI tool (cloudron machine eventlog) probably needs fixing as well
return function(eventLog) {
var source = eventLog.source;
var data = eventLog.data;
var errorMessage = data.errorMessage;
switch (eventLog.action) {
case ACTION_ACTIVATE: return 'Cloudron activated';
case ACTION_RESTORE: return 'Cloudron restored';
case ACTION_PROVISION: return 'Cloudron provisioned';
case ACTION_APP_CONFIGURE: return 'App configured';
case ACTION_APP_INSTALL: return 'App installed';
case ACTION_APP_RESTORE: return 'App restored';
case ACTION_APP_UNINSTALL: return 'App uninstalled';
case ACTION_APP_UPDATE: return 'App updated';
case ACTION_APP_CLONE: return 'App cloned';
case ACTION_APP_LOGIN: return 'App login';
case ACTION_BACKUP_START: return 'Backup started';
case ACTION_BACKUP_FINISH: return 'Backup finished';
case ACTION_BACKUP_CLEANUP_START: return 'Backup cleaner started';
case ACTION_BACKUP_CLEANUP_FINISH: return 'Backup cleaner finished';
case ACTION_CERTIFICATE_NEW: return 'Certificated installed';
case ACTION_BACKUP_CLEANUP: return 'Backup removed';
case ACTION_CERTIFICATE_RENEWAL: return 'Certificate renewal';
case ACTION_DASHBOARD_DOMAIN_UPDATE: return 'Dashboard domain updated';
case ACTION_DOMAIN_ADD: return 'Domain added';
case ACTION_DOMAIN_UPDATE: return 'Domain updated';
case ACTION_DOMAIN_REMOVE: return 'Domain removed';
case ACTION_MAIL_ENABLED: return 'Mail enabled';
case ACTION_MAIL_DISABLED: return 'Mail disabled';
case ACTION_MAIL_MAILBOX_ADD: return 'Mailbox added';
case ACTION_MAIL_MAILBOX_REMOVE: return 'Mailbox removed';
case ACTION_MAIL_LIST_ADD: return 'Mail list added';
case ACTION_MAIL_LIST_REMOVE: return 'Mail list removed';
case ACTION_START: return 'Cloudron started';
case ACTION_UPDATE: return 'Cloudron updated';
case ACTION_UPDATE: return 'Platform updated';
case ACTION_USER_ADD: return 'User added';
case ACTION_USER_LOGIN: return 'User login';
case ACTION_USER_REMOVE: return 'User removed';
case ACTION_USER_UPDATE: return 'User updated';
case ACTION_DYNDNS_UPDATE: return 'DNS Updated';
default: return eventLog.action;
}
};
@@ -682,7 +502,7 @@ app.run(['$route', '$rootScope', '$location', function ($route, $rootScope, $loc
app.directive('ngClickSelect', function () {
return {
restrict: 'AC',
link: function (scope, element/*, attrs */) {
link: function (scope, element, attrs) {
element.bind('click', function () {
var selection = window.getSelection();
var range = document.createRange();
+25 -79
View File
@@ -14,7 +14,6 @@ app.controller('LogsController', ['$scope', '$timeout', '$location', 'Client', f
$scope.activeEventSource = null;
$scope.lines = 100;
$scope.selectedAppInfo = null;
$scope.selectedTaskInfo = null;
$scope.error = function (error) {
console.error(error);
@@ -30,34 +29,11 @@ app.controller('LogsController', ['$scope', '$timeout', '$location', 'Client', f
logViewer.empty();
};
// https://github.com/janl/mustache.js/blob/master/mustache.js#L60
var entityMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;'
};
function escapeHtml(string) {
return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) {
return entityMap[s];
});
}
function showLogs() {
if (!$scope.selected) return;
var func;
if ($scope.selected.type === 'platform') func = Client.getPlatformLogs;
else if ($scope.selected.type === 'service') func = Client.getServiceLogs;
else if ($scope.selected.type === 'task') func = Client.getTaskLogs;
else if ($scope.selected.type === 'app') func = Client.getAppLogs;
func($scope.selected.value, true /* follow */, $scope.lines, function handleLogs(error, result) {
var func = $scope.selected.type === 'platform' ? Client.getPlatformLogs : Client.getAppLogs;
func($scope.selected.value, true, $scope.lines, function handleLogs(error, result) {
if (error) return console.error(error);
$scope.activeEventSource = result;
@@ -76,7 +52,7 @@ app.controller('LogsController', ['$scope', '$timeout', '$location', 'Client', f
var logLine = $('<div class="log-line">');
var timeString = moment.utc(data.realtimeTimestamp/1000).format('MMM DD HH:mm:ss');
logLine.html('<span class="time">' + timeString + ' </span>' + window.ansiToHTML(escapeHtml(typeof data.message === 'string' ? data.message : ab2str(data.message))));
logLine.html('<span class="time">' + timeString + ' </span>' + window.ansiToHTML(typeof data.message === 'string' ? data.message : ab2str(data.message)));
tmp.append(logLine);
if (autoScroll) tmp[0].lastChild.scrollIntoView({ behavior: 'instant', block: 'end' });
@@ -84,62 +60,32 @@ app.controller('LogsController', ['$scope', '$timeout', '$location', 'Client', f
});
}
function select(ids, callback) {
if (ids.id) {
var BUILT_IN_LOGS = [
{ name: 'Box', type: 'platform', value: 'box', url: Client.makeURL('/api/v1/cloudron/logs/box') },
{ name: 'MongoDB', type: 'service', value: 'mongodb', url: Client.makeURL('/api/v1/services/mongodb/logs') },
{ name: 'MySQL', type: 'service', value: 'mysql', url: Client.makeURL('/api/v1/services/mysql/logs') },
{ name: 'PostgreSQL', type: 'service', value: 'postgresql', url: Client.makeURL('/api/v1/services/postgresql/logs') },
{ name: 'Mail', type: 'service', value: 'mail', url: Client.makeURL('/api/v1/services/mail/logs') },
{ 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') },
];
function loadId(id, callback) {
// Add built-in log types for now
var BUILT_IN_LOGS = [
{ name: 'Box', type: 'platform', value: 'box', url: Client.makeURL('/api/v1/cloudron/logs/box') },
{ name: 'Mail', type: 'platform', value: 'mail', url: Client.makeURL('/api/v1/cloudron/logs/mail') },
{ name: 'Backup', type: 'platform', value: 'backup', url: Client.makeURL('/api/v1/cloudron/logs/backup') }
];
$scope.selected = BUILT_IN_LOGS.find(function (e) { return e.value === id; });
if ($scope.selected) return callback();
Client.getApp(id, function (error, app) {
if (error) return callback(error);
$scope.selectedAppInfo = app;
$scope.selected = BUILT_IN_LOGS.find(function (e) { return e.value === ids.id; });
callback();
} else if (ids.crashId) {
$scope.selected = {
type: 'platform',
value: 'crash-' + ids.crashId,
name: 'Crash',
url: Client.makeURL('/api/v1/cloudron/logs/crash-' + ids.crashId)
type: 'app',
value: app.id,
name: app.fqdn + ' (' + app.manifest.title + ')',
url: Client.makeURL('/api/v1/apps/' + app.id + '/logs'),
addons: app.manifest.addons
};
callback();
} else if (ids.appId) {
Client.getApp(ids.appId, function (error, app) {
if (error) return callback(error);
$scope.selectedAppInfo = app;
$scope.selected = {
type: 'app',
value: app.id,
name: app.fqdn + ' (' + app.manifest.title + ')',
url: Client.makeURL('/api/v1/apps/' + app.id + '/logs'),
addons: app.manifest.addons
};
callback();
});
} else if (ids.taskId) {
Client.getTask(ids.taskId, function (error, task) {
if (error) return callback(error);
$scope.selectedTaskInfo = task;
$scope.selected = {
type: 'task',
value: task.id,
name: task.type,
url: Client.makeURL('/api/v1/tasks/' + task.id + '/logs')
};
callback();
});
}
});
}
Client.getStatus(function (error, status) {
@@ -168,7 +114,7 @@ app.controller('LogsController', ['$scope', '$timeout', '$location', 'Client', f
Client.refreshConfig(function (error) {
if (error) return $scope.error(error);
select({ id: search.id, taskId: search.taskId, appId: search.appId, crashId: search.crashId }, function (error) {
loadId(search.id, function (error) {
if (error) return $scope.error(error);
// now mark the Client to be ready
+70 -29
View File
@@ -1,17 +1,15 @@
'use strict';
/* global angular:false */
/* global $:false */
angular.module('Application').controller('MainController', ['$scope', '$route', '$timeout', '$location', 'Client', function ($scope, $route, $timeout, $location, Client) {
angular.module('Application').controller('MainController', ['$scope', '$route', '$timeout', '$location', 'Client', 'AppStore', function ($scope, $route, $timeout, $location, Client, AppStore) {
$scope.initialized = false; // used to animate the UI
$scope.user = Client.getUserInfo();
$scope.installedApps = Client.getInstalledApps();
$scope.config = {};
$scope.status = {};
$scope.client = Client;
$scope.appstoreConfig = {};
$scope.subscription = {};
$scope.notifications = [];
$scope.ready = false;
$scope.hideNavBarActions = $location.path() === '/logs';
@@ -51,16 +49,16 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
function checkPlan() {
if (!$scope.waitingForPlanSelection) return;
Client.getSubscription(function (error, subscription) {
AppStore.getSubscription($scope.appstoreConfig, function (error, result) {
if (error) return console.error(error);
// check again to give more immediate feedback once a subscription was setup
if (subscription.plan.id === 'free') {
if (result.plan.id === 'free') {
$timeout(checkPlan, 5000);
} else {
$scope.waitingForPlanSelection = false;
$('#setupSubscriptionModal').modal('hide');
$scope.subscription = subscription;
if ($scope.config.update && $scope.config.update.box) $('#updateModal').modal('show');
}
});
}
@@ -72,43 +70,75 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
$('#setupSubscriptionModal').modal('show');
};
function refreshNotifications() {
Client.getNotifications(false, 1, 20, function (error, results) {
if (error) console.error(error);
else $scope.notifications = results;
function runConfigurationChecks() {
// warn user if dns config is not working (the 'configuring' flag detects if configureWebadmin is 'active')
if (!$scope.status.webadminStatus.configuring && !$scope.status.webadminStatus.dns) {
var dnsActionScope = $scope.$new(true);
dnsActionScope.action = '/#/domains';
Client.notify('Invalid Domain Config', 'Unable to update DNS. Click here to update it.', true, 'error', dnsActionScope);
}
$timeout(refreshNotifications, 10000);
if ($scope.config.update && $scope.config.update.box) {
var updateActionScope = $scope.$new(true);
updateActionScope.action = '/#/settings';
Client.notify('Update Available', 'Update now to version ' + $scope.config.update.box.version + '.', true, 'success', updateActionScope);
}
Client.getBackupConfig(function (error, backupConfig) {
if (error) return console.error(error);
if (backupConfig.provider === 'noop') {
var actionScope = $scope.$new(true);
actionScope.action = '/#/backups';
Client.notify('Backup Configuration', 'Cloudron backups are disabled. Please ensure this server is backed up using alternate means.', false, 'info', actionScope);
} else if (backupConfig.provider === 'filesystem' && !backupConfig.externalDisk) {
var actionScope = $scope.$new(true);
actionScope.action = '/#/backups';
Client.notify('Backup Configuration',
'Cloudron backups are currently on the same disk as the Cloudron server instance. This is dangerous and can lead to complete data loss if the disk fails.',
false /* persistent */, 'info', actionScope);
}
});
}
// NOTE: this function is exported and called from the settings.js
$scope.updateSubscriptionStatus = function () {
Client.getSubscription(function (error, subscription) {
if (error && error.statusCode === 412) return; // ignore if not yet registered
if (error) console.error(error);
$scope.fetchAppstoreProfileAndSubscription = function (callback) {
Client.getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
if (!appstoreConfig.token) return callback();
$scope.subscription = subscription;
AppStore.getProfile(appstoreConfig.token, function (error, result) {
if (error) return callback(error);
// assign late to avoid UI flicketing on update
appstoreConfig.profile = result;
$scope.appstoreConfig = appstoreConfig;
AppStore.getSubscription($scope.appstoreConfig, function (error, result) {
if (error) return callback(error);
$scope.subscription = result;
callback();
});
});
});
};
// update state of acknowledged notification
$scope.notificationAcknowledged = function (notificationId) {
// remove notification from list
$scope.notifications = $scope.notifications.filter(function (n) { return n.id !== notificationId; });
};
Client.getStatus(function (error, status) {
if (error) return $scope.error(error);
// WARNING if anything about the routing is changed here test these use-cases:
//
// 1. Caas
// 2. selfhosted with --domain argument
// 3. selfhosted restore
// 4. local development with gulp develop
if (!status.activated) {
console.log('Not activated yet, redirecting', status);
if (status.restore.active || status.restore.errorMessage) { // show the error message in restore page
if (status.webadminStatus.restore.active || status.webadminStatus.restore.error) {
window.location.href = '/restore.html';
} else {
window.location.href = status.adminFqdn ? '/setup.html' : '/setupdns.html';
@@ -152,15 +182,26 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
$scope.initialized = true;
refreshNotifications();
if ($scope.user.admin && $scope.config.features.operatorActions) {
runConfigurationChecks();
$scope.updateSubscriptionStatus();
$scope.fetchAppstoreProfileAndSubscription(function (error) {
if (error) console.error(error);
$scope.ready = true;
});
}
});
});
});
});
Client.onConfig(function (config) {
// check if we are actually updating
if (config.progress.update && config.progress.update.percent !== -1) {
window.location.href = '/update.html';
}
if (config.cloudronName) {
document.title = config.cloudronName;
}
@@ -169,7 +210,7 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
// setup all the dialog focus handling
['updateModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find('[autofocus]:first').focus();
$(this).find("[autofocus]:first").focus();
});
});
}]);
+6 -19
View File
@@ -16,7 +16,6 @@ app.controller('RestoreController', ['$scope', '$http', 'Client', function ($sco
$scope.busy = false;
$scope.error = {};
$scope.message = ''; // progress
$scope.provider = '';
$scope.bucket = '';
$scope.prefix = '';
@@ -34,7 +33,6 @@ app.controller('RestoreController', ['$scope', '$http', 'Client', function ($sco
// List is from http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
$scope.s3Regions = [
{ name: 'Asia Pacific (Mumbai)', value: 'ap-south-1' },
{ name: 'Asia Pacific (Osaka-Local)', value: 'ap-northeast-3' },
{ name: 'Asia Pacific (Seoul)', value: 'ap-northeast-2' },
{ name: 'Asia Pacific (Singapore)', value: 'ap-southeast-1' },
{ name: 'Asia Pacific (Sydney)', value: 'ap-southeast-2' },
@@ -43,8 +41,6 @@ app.controller('RestoreController', ['$scope', '$http', 'Client', function ($sco
{ name: 'EU (Frankfurt)', value: 'eu-central-1' },
{ name: 'EU (Ireland)', value: 'eu-west-1' },
{ name: 'EU (London)', value: 'eu-west-2' },
{ name: 'EU (Paris)', value: 'eu-west-3' },
{ name: 'EU (Stockholm)', value: 'eu-north-1' },
{ name: 'South America (São Paulo)', value: 'sa-east-1' },
{ name: 'US East (N. Virginia)', value: 'us-east-1' },
{ name: 'US East (Ohio)', value: 'us-east-2' },
@@ -55,16 +51,9 @@ app.controller('RestoreController', ['$scope', '$http', 'Client', function ($sco
$scope.doSpacesRegions = [
{ name: 'AMS3', value: 'https://ams3.digitaloceanspaces.com' },
{ name: 'NYC3', value: 'https://nyc3.digitaloceanspaces.com' },
{ name: 'SFO2', value: 'https://sfo2.digitaloceanspaces.com' },
{ name: 'SGP1', value: 'https://sgp1.digitaloceanspaces.com' }
];
$scope.exoscaleSosRegions = [
{ name: 'CH-DK-2', value: 'https://sos-ch-dk-2.exo.io' }, // default
{ name: 'DE-FRA-1', value: 'https://sos-de-fra-1.exo.io' },
{ name: 'AT-VIE-1', value: 'https://sos-at-vie-1.exo.io' }
];
$scope.storageProvider = [
{ name: 'Amazon S3', value: 's3' },
{ name: 'DigitalOcean Spaces', value: 'digitalocean-spaces' },
@@ -159,7 +148,7 @@ app.controller('RestoreController', ['$scope', '$http', 'Client', function ($sco
$scope.busy = false;
if (error) {
if (error.statusCode === 424) {
if (error.statusCode === 402) {
$scope.error.generic = error.message;
if (error.message.indexOf('AWS Access Key Id') !== -1) {
@@ -205,18 +194,16 @@ app.controller('RestoreController', ['$scope', '$http', 'Client', function ($sco
$scope.busy = true;
Client.getStatus(function (error, status) {
if (!error && !status.restore.active) { // restore finished
if (status.restore.errorMessage) {
if (!error && !status.webadminStatus.restore.active) { // restore finished
if (status.webadminStatus.restore.error) {
$scope.busy = false;
$scope.error.generic = status.restore.errorMessage;
$scope.error.generic = status.webadminStatus.restore.error;
} else { // restore worked, redirect to admin page
window.location.href = '/';
}
return;
}
$scope.message = status.restore.message;
setTimeout(waitForRestore, 5000);
});
}
@@ -245,9 +232,9 @@ app.controller('RestoreController', ['$scope', '$http', 'Client', function ($sco
return;
}
if (status.restore.active) return waitForRestore();
if (status.webadminStatus.restore.active) return waitForRestore();
if (status.restore.errorMessage) $scope.error.generic = status.restore.errorMessage;
if (status.webadminStatus.restore.error) $scope.error.generic = status.webadminStatus.restore.error;
if (status.activated) {
window.location.href = '/';
+10
View File
@@ -68,6 +68,16 @@ app.controller('SetupController', ['$scope', '$http', 'Client', function ($scope
}
if (status.provider === 'caas') {
if (!search.setupToken) {
window.location.href = '/error.html?errorCode=2';
return;
}
if (!search.email) {
window.location.href = '/error.html?errorCode=3';
return;
}
$scope.setupToken = search.setupToken;
}
+17 -50
View File
@@ -1,7 +1,6 @@
'use strict';
/* global tld:false */
/* global angular:false */
/* global tld */
// create main application module
var app = angular.module('Application', ['angular-md5', 'ui-notification', 'ui.bootstrap']);
@@ -25,17 +24,10 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
$scope.hyphenatedSubdomains = false;
$scope.tlsProvider = [
{ name: 'Let\'s Encrypt Prod', value: 'letsencrypt-prod' },
{ name: 'Let\'s Encrypt Prod - Wildcard', value: 'letsencrypt-prod-wildcard' },
{ name: 'Let\'s Encrypt Staging', value: 'letsencrypt-staging' },
{ name: 'Let\'s Encrypt Staging - Wildcard', value: 'letsencrypt-staging-wildcard' },
{ name: 'Self-Signed', value: 'fallback' }, // this is not 'Custom' because we don't allow user to upload certs during setup phase
];
$scope.needsPort80 = function (dnsProvider, tlsProvider) {
return ((dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') &&
(tlsProvider === 'letsencrypt-prod' || tlsProvider === 'letsencrypt-staging'));
};
// If we migrate the api origin we have to poll the new location
if (search.admin_fqdn) Client.apiOrigin = 'https://' + search.admin_fqdn;
@@ -55,13 +47,12 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
// keep in sync with domains.js
$scope.dnsProvider = [
{ name: 'AWS Route53', value: 'route53' },
{ name: 'Cloudflare', value: 'cloudflare' },
{ name: 'DigitalOcean', value: 'digitalocean' },
{ name: 'Cloudflare (DNS only)', value: 'cloudflare' },
{ name: 'Digital Ocean', value: 'digitalocean' },
{ name: 'Gandi LiveDNS', value: 'gandi' },
{ name: 'GoDaddy', value: 'godaddy' },
{ name: 'Google Cloud DNS', value: 'gcdns' },
{ name: 'Name.com', value: 'namecom' },
{ name: 'Namecheap', value: 'namecheap' },
{ name: 'Wildcard', value: 'wildcard' },
{ name: 'Manual (not recommended)', value: 'manual' },
{ name: 'No-op (only for development)', value: 'noop' }
@@ -82,27 +73,14 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
godaddyApiSecret: '',
nameComUsername: '',
nameComToken: '',
namecheapUsername: '',
namecheapApiKey: '',
provider: 'route53',
zoneName: '',
tlsConfig: {
provider: 'letsencrypt-prod-wildcard'
provider: 'letsencrypt-prod'
},
hyphenatedSubdomains: false
};
$scope.setDefaultTlsProvider = function () {
var dnsProvider = $scope.dnsCredentials.provider;
// wildcard LE won't work without automated DNS
if (dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') {
$scope.dnsCredentials.tlsConfig.provider = 'letsencrypt-prod';
} else {
$scope.dnsCredentials.tlsConfig.provider = 'letsencrypt-prod-wildcard';
}
};
function readFileLocally(obj, file, fileName) {
return function (event) {
$scope.$apply(function () {
@@ -133,6 +111,12 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
hyphenatedSubdomains: $scope.dnsCredentials.hyphenatedSubdomains
};
// special case the wildcard provider
if (provider === 'wildcard') {
provider = 'manual';
data.wildcard = true;
}
if (provider === 'route53') {
data.accessKeyId = $scope.dnsCredentials.accessKeyId;
data.secretAccessKey = $scope.dnsCredentials.secretAccessKey;
@@ -166,21 +150,9 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
} else if (provider === 'namecom') {
data.username = $scope.dnsCredentials.nameComUsername;
data.token = $scope.dnsCredentials.nameComToken;
} else if (provider === 'namecheap') {
data.token = $scope.dnsCredentials.namecheapApiKey;
data.username = $scope.dnsCredentials.namecheapUsername;
}
var tlsConfig = {
provider: $scope.dnsCredentials.tlsConfig.provider,
wildcard: false
};
if ($scope.dnsCredentials.tlsConfig.provider.indexOf('-wildcard') !== -1) {
tlsConfig.provider = tlsConfig.provider.replace('-wildcard', '');
tlsConfig.wildcard = true;
}
Client.setup($scope.dnsCredentials.domain, $scope.dnsCredentials.zoneName, provider, data, tlsConfig, function (error) {
Client.setupDnsConfig($scope.dnsCredentials.domain, $scope.dnsCredentials.zoneName, provider, data, $scope.dnsCredentials.tlsConfig, function (error) {
if (error && error.statusCode === 401) {
$scope.dnsCredentials.busy = false;
$scope.error = 'Wrong instance id provided.';
@@ -199,18 +171,12 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
$scope.state = 'waitingForDnsSetup';
Client.getStatus(function (error, status) {
if (!error && !status.setup.active) {
if (!status.adminFqdn || status.setup.errorMessage) { // setup reset or errored. start over
$scope.error = status.setup.errorMessage;
$scope.status = 'initialized';
} else { // proceed to activation
window.location.href = 'https://' + status.adminFqdn + '/setup.html';
}
return;
// webadminStatus.dns is intentionally not tested. it can be false if dns creds are invalid
// runConfigurationChecks() in main.js will pick the .dns and show a notification
if (!error && status.adminFqdn && status.webadminStatus.tls) {
window.location.href = 'https://' + status.adminFqdn + '/setup.html';
}
$scope.message = status.setup.message;
setTimeout(waitForDnsSetup, 5000);
});
}
@@ -227,7 +193,7 @@ 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 === 'digitalocean') $scope.dnsCredentials.provider = 'digitalocean';
if (status.provider === 'gce') $scope.dnsCredentials.provider = 'gcdns';
if (status.provider === 'ami') {
// remove route53 on ami
@@ -237,6 +203,7 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
$scope.instanceId = search.instanceId;
$scope.provider = status.provider;
$scope.hyphenatedSubdomains = status.edition === 'hostingprovider';
$scope.state = 'initialized';
});
}
+1 -1
View File
@@ -28,7 +28,7 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
downloadUrl: function () {
if (!$scope.downloadFile.filePath) return '';
var filePath = encodeURIComponent($scope.downloadFile.filePath);
var filePath = $scope.downloadFile.filePath.replace(/\/*\//g, '/');
return Client.apiOrigin + '/api/v1/apps/' + $scope.selected.value + '/download?file=' + filePath + '&access_token=' + Client.getToken();
},
+69
View File
@@ -0,0 +1,69 @@
'use strict';
// create main application module
var app = angular.module('Application', []);
app.controller('Controller', ['$scope', '$http', '$interval', function ($scope, $http, $interval) {
$scope.title = '';
$scope.percent = 0;
$scope.message = '';
$scope.error = false;
$scope.loadWebadmin = function () {
window.location.href = '/';
};
function fetchProgress() {
$http.get('/api/v1/cloudron/progress').success(function(data, status) {
if (status === 404) return; // just wait until we create the progress.json on the server side
if (status !== 200 || typeof data !== 'object') return console.error('Invalid response for progress', status, data);
if (!data.update && !data.migrate) return $scope.loadWebadmin();
if (data.update) {
if (data.update.percent >= 100) {
return $scope.loadWebadmin();
} else if (data.update.percent === -1) {
$scope.title = 'Update Error';
$scope.error = true;
$scope.message = data.update.message;
} else {
if (data.backup && data.backup.percent < 100) {
$scope.title = 'Backup in progress...';
$scope.percent = data.backup.percent < 0 ? 5 : (data.backup.percent / 100) * 50; // never show 0 as it looks like nothing happens
$scope.message = data.backup.message;
} else {
$scope.title = 'Update in progress...';
$scope.percent = 50 + ((data.update.percent / 100) * 50); // first half is backup
$scope.message = data.update.message;
}
}
} else { // migrating
if (data.migrate.percent === -1) {
$scope.title = 'Migration Error';
$scope.error = true;
$scope.message = data.migrate.message;
} else {
$scope.title = 'Migration in progress...';
$scope.percent = data.migrate.percent;
$scope.message = data.migrate.message;
if (!data.migrate.info) return;
// check if the new domain is available via the appstore (cannot use cloudron
// directly as we might hit NXDOMAIN)
$http.get(data.apiServerOrigin + '/api/v1/boxes/' + data.migrate.info.domain + '/status').success(function(data2, status) {
if (status === 200 && data2.status === 'ready') {
window.location = 'https://my.' + data.migrate.info.domain;
}
});
}
}
}).error(function (data, status) {
console.error('Error getting progress', status, data);
});
}
$interval(fetchProgress, 2000);
fetchProgress();
}]);
+4 -4
View File
@@ -12,11 +12,11 @@
<link rel="icon" href="/api/v1/cloudron/avatar">
<!-- CSS -->
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.css"/>
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.min.css"/>
<link type="text/css" rel="stylesheet" href="/theme.css">
<!-- Custom Fonts -->
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.css">
<link type="text/css" rel="stylesheet" href="/3rdparty/css/font-awesome.min.css">
<!-- jQuery-->
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
@@ -31,7 +31,7 @@
<script type="text/javascript" src="/3rdparty/js/angular-base64.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-sanitize.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.min.js"></script>
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
@@ -58,7 +58,7 @@
<div class="pull-right">
<a class="btn btn-primary" ng-href="{{ '/terminal.html?id=' + selected.value }}" target="_blank" ng-show="selected.type === 'app'"><i class="fa fa-terminal"></i> Terminal</a>
<a class="btn btn-primary" ng-click="clear()"><i class="fa fa-trash"></i> Clear View</a>
<a class="btn btn-primary" ng-href="{{ selected.url }}&format=short&lines=-1"><i class="fa fa-download"></i> Download Full Logs</a>
<a class="btn btn-primary" ng-href="{{ selected.url }}&format=short&lines=800"><i class="fa fa-download"></i> Download Full Logs</a>
</div>
</div>
+7 -12
View File
@@ -12,7 +12,7 @@
<link type="text/css" rel="stylesheet" href="/theme.css">
<!-- Custom Fonts -->
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.css">
<link type="text/css" rel="stylesheet" href="/3rdparty/css/font-awesome.min.css">
<!-- jQuery-->
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
@@ -24,7 +24,7 @@
<script type="text/javascript" src="/3rdparty/js/angular.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/autofill-event.js"></script>
<!-- Angular directives for tldjs -->
@@ -40,8 +40,8 @@
<div class="main-container ng-cloak text-center" ng-show="busy">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<i class="fa fa-circle-notch fa-spin fa-5x"></i><br/>
<h3>{{ message }} ...</h3>
<i class="fa fa-circle-o-notch fa-spin fa-5x"></i><br/>
<h3>Downloading backup</h3>
</div>
</div>
</div>
@@ -108,11 +108,6 @@
<select class="form-control" name="region" id="inputConfigureBackupDORegion" ng-model="endpoint" ng-options="a.value as a.name for a in doSpacesRegions" ng-disabled="busy" ng-required="provider === 'digitalocean-spaces'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'exoscale-sos'">
<label class="control-label" for="inputConfigureBackupExoscaleRegion">Region</label>
<select class="form-control" name="region" id="inputConfigureBackupExoscaleRegion" ng-model="endpoint" ng-options="a.value as a.name for a in exoscaleSosRegions" ng-disabled="busy" ng-required="provider === 'exoscale-sos'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': error.accessKeyId }" ng-show="s3like(provider)">
<label class="control-label" for="inputConfigureBackupAccessKeyId">Access key id</label>
<input type="text" class="form-control" ng-model="accessKeyId" id="inputConfigureBackupAccessKeyId" name="accessKeyId" ng-disabled="busy" ng-required="s3like(provider)">
@@ -158,7 +153,7 @@
<br/>
<div class="row">
<div class="col-md-12 text-center">
<button type="submit" class="btn btn-primary" ng-disabled="configureBackupForm.$invalid"/><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> Restore</button>
<button type="submit" class="btn btn-primary" ng-disabled="configureBackupForm.$invalid"/><i class="fa fa-circle-o-notch fa-spin" ng-show="busy"></i> Restore</button>
</div>
</div>
@@ -169,8 +164,8 @@
</div>
<footer class="text-center">
<span class="text-muted">&copy;2019 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fab fa-twitter"></i></a></span>
<span class="text-muted">&copy;2018 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
</footer>
+8 -8
View File
@@ -12,7 +12,7 @@
<link type="text/css" rel="stylesheet" href="/theme.css">
<!-- Custom Fonts -->
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.css">
<link type="text/css" rel="stylesheet" href="/3rdparty/css/font-awesome.min.css">
<!-- jQuery-->
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
@@ -24,7 +24,7 @@
<script type="text/javascript" src="/3rdparty/js/angular.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/autofill-event.js"></script>
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
@@ -49,7 +49,7 @@
<div class="main-container ng-cloak text-center" ng-show="busy">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<i class="fa fa-circle-notch fa-spin fa-5x"></i>
<i class="fa fa-circle-o-notch fa-spin fa-5x"></i>
</div>
</div>
</div>
@@ -74,8 +74,8 @@
<input type="text" class="form-control" ng-model="account.displayName" id="inputDisplayName" name="displayName" placeholder="Full Name" required autocomplete="off" autofocus>
</div>
<div ng-show="account.requireEmail" class="form-group" ng-class="{ 'has-error': setupForm.email.$dirty && setupForm.email.$invalid }">
<label class="control-label">Email <sup><a href="https://cloudron.io/documentation/installation/#admin-account" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<input type="email" class="form-control" ng-model="account.email" id="inputEmail" name="email" placeholder="Email" required autocomplete="off" tooltip-class="long" tooltip-trigger="focus" uib-tooltip="This email address is local to your Cloudron and used for notifications and password reset. A valid email is also required for Let's Encrypt certificates.">
<label class="control-label">Email <sup><a href="https://cloudron.io/documentation/installation/#administrator-setup" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<input type="email" class="form-control" ng-model="account.email" id="inputEmail" name="email" placeholder="Email" required autocomplete="off" tooltip-trigger="focus" uib-tooltip="This email address is local to your Cloudron and used for notifications and password reset. A valid email is also required for Let's Encrypt certificates.">
</div>
<div class="form-group" ng-class="{ 'has-error': (setupForm.username.$dirty && setupForm.username.$invalid) || (!setupForm.username.$dirty && error.username) }">
<label class="control-label">Username</label>
@@ -84,7 +84,7 @@
</div>
<div class="form-group" ng-class="{ 'has-error': setupForm.password.$dirty && setupForm.password.$invalid }">
<label class="control-label">Password</label>
<input type="password" class="form-control" ng-model="account.password" id="inputPassword" name="password" placeholder="Password" ng-pattern="/^.{8,}$/" required autocomplete="off">
<input type="password" class="form-control" ng-model="account.password" id="inputPassword" name="password" placeholder="Password" ng-pattern="/^.{8,30}$/" required autocomplete="off">
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be atleast 8 characters</small>
</div>
@@ -109,8 +109,8 @@
</div>
<footer class="text-center">
<span class="text-muted">&copy;2019 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fab fa-twitter"></i></a></span>
<span class="text-muted">&copy;2018 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
</footer>
+22 -33
View File
@@ -12,7 +12,7 @@
<link type="text/css" rel="stylesheet" href="/theme.css">
<!-- Custom Fonts -->
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.css">
<link type="text/css" rel="stylesheet" href="/3rdparty/css/font-awesome.min.css">
<!-- jQuery-->
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
@@ -24,7 +24,7 @@
<script type="text/javascript" src="/3rdparty/js/angular.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/autofill-event.js"></script>
<!-- Angular directives for tldjs -->
@@ -43,8 +43,8 @@
<div class="main-container ng-cloak text-center" ng-show="state === 'waitingForDnsSetup' || state === 'waitingForBox'">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<i class="fa fa-circle-notch fa-spin fa-5x"></i><br/>
<h3>{{ message }} ...</h3>
<i class="fa fa-circle-o-notch fa-spin fa-5x"></i><br/>
<h3>Waiting for domain and certificate setup</h3>
</div>
</div>
</div>
@@ -66,7 +66,7 @@
<br/>
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': dnsCredentialsForm.domain.$dirty && dnsCredentialsForm.domain.$invalid }">
<label class="control-label">Primary Domain <sup><a href="https://cloudron.io/documentation/installation/#domain-setup" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<label class="control-label">Primary Domain</label>
<input type="text" class="form-control" ng-model="dnsCredentials.domain" name="domain" placeholder="example.com" required autofocus ng-disabled="dnsCredentials.busy">
</div>
</div>
@@ -79,8 +79,8 @@
<br/>
<div class="form-group">
<label class="control-label">DNS Provider <sup><a href="https://cloudron.io/documentation/domains/#dns-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<select class="form-control" ng-model="dnsCredentials.provider" ng-options="a.value as a.name for a in dnsProvider" ng-disabled="dnsCredentials.busy" ng-change="setDefaultTlsProvider()"></select>
<label class="control-label">Domain Provider</label>
<select class="form-control" ng-model="dnsCredentials.provider" ng-options="a.value as a.name for a in dnsProvider" ng-disabled="dnsCredentials.busy"></select>
</div>
<!-- Route53 -->
@@ -107,19 +107,19 @@
<!-- DigitalOcean -->
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.digitalOceanToken.$dirty && dnsCredentialsForm.digitalOceanToken.$invalid }" ng-show="dnsCredentials.provider === 'digitalocean'">
<label class="control-label">DigitalOcean Token</label>
<label class="control-label">DigitalOcean Token <sup><a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean#step-two%E2%80%94change-your-domain-server" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<input type="text" class="form-control" ng-model="dnsCredentials.digitalOceanToken" name="digitalOceanToken" placeholder="API Token" ng-required="dnsCredentials.provider === 'digitalocean'" ng-disabled="dnsCredentials.busy">
</div>
<!-- Gandi -->
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.gandiApiKey.$dirty && dnsCredentialsForm.gandiApiKey.$invalid }" ng-show="dnsCredentials.provider === 'gandi'">
<label class="control-label">Gandi API Key</label>
<label class="control-label">Gandi API Key <sup><a href="http://doc.livedns.gandi.net/" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<input type="text" class="form-control" ng-model="dnsCredentials.gandiApiKey" name="gandiApiKey" placeholder="API Key" ng-required="dnsCredentials.provider === 'gandi'" ng-disabled="dnsCredentials.busy">
</div>
<!-- GoDaddy -->
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.godaddyApiKey.$dirty && dnsCredentialsForm.godaddyApiKey.$invalid }" ng-show="dnsCredentials.provider === 'godaddy'">
<label class="control-label">API Key</label>
<label class="control-label">API Key <sup><a href="https://developer.godaddy.com/keys" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<input type="text" class="form-control" ng-model="dnsCredentials.godaddyApiKey" name="godaddyApiKey" placeholder="API Key" ng-minlength="1" ng-required="dnsCredentials.provider === 'godaddy'" ng-disabled="dnsCredentials.busy">
</div>
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.godaddyApiSecret.$dirty && dnsCredentialsForm.godaddyApiSecret.$invalid }" ng-show="dnsCredentials.provider === 'godaddy'">
@@ -143,29 +143,20 @@
<input type="text" class="form-control" ng-model="dnsCredentials.nameComUsername" name="nameComUsername" placeholder="Name.com Username" ng-required="dnsCredentials.provider === 'namecom'" ng-disabled="dnsCredentials.busy">
</div>
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.nameComToken.$dirty && dnsCredentialsForm.nameComToken.$invalid }" ng-show="dnsCredentials.provider === 'namecom'">
<label class="control-label">API Token</label>
<label class="control-label">API Token <sup><a href="https://www.name.com/account/settings/api" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<input type="text" class="form-control" ng-model="dnsCredentials.nameComToken" name="nameComToken" placeholder="Name.com API Token" ng-required="dnsCredentials.provider === 'namecom'" ng-disabled="dnsCredentials.busy">
</div>
<!-- Namecheap -->
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.namecheapUsername.$dirty && dnsCredentialsForm.namecheapUsername.$invalid }" ng-show="dnsCredentials.provider === 'namecheap'">
<label class="control-label">Name.com Username</label>
<input type="text" class="form-control" ng-model="dnsCredentials.namecheapUsername" name="namecheapUsername" placeholder="Namecheap Username" ng-required="dnsCredentials.provider === 'namecheap'" ng-disabled="dnsCredentials.busy">
</div>
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.namecheapApiKey.$dirty && dnsCredentialsForm.namecheapApiKey.$invalid }" ng-show="dnsCredentials.provider === 'namecheap'">
<label class="control-label">API Key</label>
<p class="small text-info" ng-show="dnsCredentials.provider === 'namecheap'"><b>The server IP needs to be whitelisted for this API Key.</b></p>
<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>
<!-- Wildcard -->
<p class="small text-info" ng-show="dnsCredentials.provider === 'wildcard'">
<p 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>
</p>
<!-- Manual -->
<p class="small text-info" ng-show="dnsCredentials.provider === 'manual'">
<span>Setup an A record for <b>my.{{ dnsCredentials.domain || 'example.com' }}</b> to this server's IP.<br/></span>
<p ng-show="dnsCredentials.provider === 'manual'">
<span>
Setup an A record for <b>my.{{ dnsCredentials.domain || 'example.com' }}</b> to this server's IP.<br/>
</span>
</p>
<div ng-show="provider === 'ami'">
@@ -178,12 +169,10 @@
<p>&nbsp;<span ng-show="error" class="text-danger">{{ error }}</span></p>
</div>
<p class="small text-info" ng-show="needsPort80(dnsCredentials.provider, dnsCredentials.tlsConfig.provider)">Let's Encrypt requires your server to be reachable on port 80</p>
<br/>
<div uib-collapse="!dnsCredentials.advancedVisible">
<div ng-show="false">
<div ng-show="hyphenatedSubdomains">
<label>
<input type="checkbox" ng-model="dnsCredentials.hyphenatedSubdomains" name="hyphenatedSubdomains" ng-disabled="dnsCredentials.busy"/>&nbsp; Hyphenate Subdomains
</label>
@@ -192,12 +181,12 @@
</div>
<div class="form-group">
<label class="control-label">Zone Name (Optional) <sup><a href="https://cloudron.io/documentation/domains/#zone-name" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<label class="control-label">Zone Name (Optional)</label>
<input type="text" class="form-control" ng-model="dnsCredentials.zoneName" name="zoneName" placeholder="{{dnsCredentials.domain | zoneName}}" ng-disabled="dnsCredentials.busy">
</div>
<div class="form-group">
<label class="control-label">Certificate Provider <sup><a href="https://cloudron.io/documentation/certificates/#certificate-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<label class="control-label">Certificate Provider</label>
<select class="form-control" ng-model="dnsCredentials.tlsConfig.provider" ng-options="a.value as a.name for a in tlsProvider" ng-disabled="dnsCredentials.busy"></select>
</div>
</div>
@@ -213,7 +202,7 @@
<div class="row">
<div class="col-md-12 text-center">
<button type="submit" class="btn btn-primary" ng-disabled="dnsCredentialsForm.$invalid"/><i class="fa fa-circle-notch fa-spin" ng-show="dnsCredentials.busy"></i> Next</button>
<button type="submit" class="btn btn-primary" ng-disabled="dnsCredentialsForm.$invalid"/><i class="fa fa-circle-o-notch fa-spin" ng-show="dnsCredentials.busy"></i> Next</button>
</div>
</div>
<br/>
@@ -229,8 +218,8 @@
</div>
<footer class="text-center">
<span class="text-muted">&copy;2019 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fab fa-twitter"></i></a></span>
<span class="text-muted">&copy;2018 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
</footer>
+6 -6
View File
@@ -12,12 +12,12 @@
<link rel="icon" href="/api/v1/cloudron/avatar">
<!-- CSS -->
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.css"/>
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.min.css"/>
<link type="text/css" rel="stylesheet" href="/3rdparty/xterm/xterm.css">
<link type="text/css" rel="stylesheet" href="/theme.css">
<!-- Custom Fonts -->
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.css">
<link type="text/css" rel="stylesheet" href="/3rdparty/css/font-awesome.min.css">
<!-- jQuery-->
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
@@ -32,7 +32,7 @@
<script type="text/javascript" src="/3rdparty/js/angular-base64.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-sanitize.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.min.js"></script>
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
@@ -76,7 +76,7 @@
<a id="fileDownloadLink" class="" ng-href="{{ downloadFile.downloadUrl() }}" target="_blank"></a>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" ng-click="downloadFile.submit()" ng-disabled="!downloadFile.filePath"><i class="fa fa-circle-notch fa-spin" ng-show="downloadFile.busy"></i> Download</button>
<button type="button" class="btn btn-success" ng-click="downloadFile.submit()" ng-disabled="!downloadFile.filePath"><i class="fa fa-circle-o-notch fa-spin" ng-show="downloadFile.busy"></i> Download</button>
</div>
</div>
</div>
@@ -144,9 +144,9 @@
<a class="btn btn-success" ng-click="terminalInject('redis')" ng-show="usesAddon('redis')">Redis</a>
<!-- terminal actions -->
<a class="btn btn-primary" ng-click="restartApp()" ng-show="selected.type === 'app'" ng-disabled="restartAppBusy"><i class="fa fa-sync-alt" ng-class="{ 'fa-spin': restartAppBusy }"></i> Restart</a>
<a class="btn btn-primary" ng-click="restartApp()" ng-show="selected.type === 'app'" ng-disabled="restartAppBusy"><i class="fa fa-refresh" ng-class="{ 'fa-spin': restartAppBusy }"></i> Restart</a>
<a class="btn btn-primary" ng-click="uploadFile()" ng-show="selected.type === 'app' && !uploadProgress.busy"><i class="fa fa-upload"></i> Upload to /tmp</a>
<a class="btn btn-primary" ng-click="uploadProgress.show()" ng-show="uploadProgress.busy"><i class="fa fa-circle-notch fa-spin"></i> Uploading...</a>
<a class="btn btn-primary" ng-click="uploadProgress.show()" ng-show="uploadProgress.busy"><i class="fa fa-circle-o-notch fa-spin"></i> Uploading...</a>
<a class="btn btn-primary" ng-click="downloadFile.show()" ng-show="selected.type === 'app'"><i class="fa fa-download"></i> Download</a>
<a class="btn btn-primary" ng-click="repairApp()" ng-show="selected.type === 'app' && !selectedAppInfo.debugMode && !appBusy"><i class="fa fa-wrench"></i> Repair</a>
<a class="btn btn-danger" ng-click="repairAppDone()" ng-show="selectedAppInfo.debugMode && !appBusy"><i class="fa fa-wrench"></i> Repair Done</a>
+41 -95
View File
@@ -1,9 +1,9 @@
$brand-primary: #2196F3 !default; // #62bdfc
$brand-success: #27CE65 !default;
$brand-info: #3995b1 !default;
$brand-info: #5bc0de !default;
$brand-warning: #f0ad4e !default;
$brand-danger: #ff4c4c !default;
$brand-danger: #d9534f !default;
$body-bg: #E5E5E5;
$font-family-sans-serif: Roboto, Helvetica, Arial, sans-serif;
@@ -88,6 +88,12 @@ $table-border-color: transparent !default;
clear: both;
}
.btn-admin {
color: white !important;
background-color: $brand-danger !important;
border-color: $brand-danger !important;
}
.elide-table-cell {
text-overflow: ellipsis;
white-space: nowrap;
@@ -95,10 +101,6 @@ $table-border-color: transparent !default;
max-width: 300px;
}
.dropdown-menu {
min-width: 190px;
}
.dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover {
background-color: $brand-primary;
color: white;
@@ -113,23 +115,6 @@ input[type="checkbox"] {
outline: none;
}
.fa-fw {
margin-right: 5px;
}
.tooltip.long {
.tooltip-inner {
max-width: 400px;
white-space: unset;
}
}
.tooltip-inner {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// ----------------------------
// Main classes
// ----------------------------
@@ -140,16 +125,6 @@ html, body {
padding: 0;
}
.offline-banner {
position: fixed;
z-index: 30000;
background-color: $brand-danger;
width: 100%;
padding: 2px;
text-align: center;
color: white;
}
.layout-root {
display: flex;
flex-direction: column;
@@ -236,26 +211,10 @@ h1, h2, h3 {
font-family: $font-family-heading;
}
.view-header {
padding-left: 15px;
padding-right: 15px;
}
.offscreen {
position: absolute;
left: -999em;
}
// ----------------------------
// Apps view
// ----------------------------
.app-grid {
display: flex;
flex-wrap: wrap;
width: 100%;
}
.grid-item {
padding: 10px;
min-width: 225px;
@@ -271,8 +230,14 @@ h1, h2, h3 {
}
}
.grid-item:hover .grid-item-bottom {
@media(min-width:768px) {
opacity: 1;
right: 10px;
}
}
.grid-item-content {
position: relative; // required to make action buttons positioned absolute within the element
background-color: white;
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
border-radius: 2px;
@@ -289,51 +254,36 @@ h1, h2, h3 {
font-family: $font-family-heading;
}
.grid-item-actions {
display: block;
.grid-item-bottom-mobile {
padding: 10px 15px;
border-top: 1px solid #ddd;
background-color: white;
@media(min-width:768px) {
display: none;
}
}
.grid-item-bottom {
display: none;
position: absolute;
top: 26px;
top: 20px;
padding: 10px 15px;
right: -10px;
opacity: 0;
background-color: transparent;
transition: all 250ms;
@media(max-width:768px) {
opacity: 1;
right: 10px;
}
a {
@media(min-width:768px) {
display: block;
text-align: center;
}
}
.grid-item:hover .grid-item-actions {
opacity: 1;
right: 10px;
}
.app-update-badge {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
right: -12px;
top: -12px;
font-size: 24px;
height: 36px;
width: 36px;
color: white;
cursor: pointer;
background-color: $brand-success;
border-radius: 34px;
transition: all 100ms ease-out;
&:hover {
transform: scale(1.4);
}
right: 0;
top: 0;
}
.app-postinstall-message {
@@ -541,6 +491,14 @@ multiselect {
}
}
.scale-small {
transition: transform 100ms ease-out;
&:hover {
transform: scale(1.5)
}
}
.loading-banner {
padding-top: 150px;
text-align: center;
@@ -1055,18 +1013,6 @@ footer {
background-color: white;
}
// ----------------------------
// Account/Notifications
// ----------------------------
.notification-item {
cursor: pointer;
&:hover {
box-shadow: 0 2px 27px rgba(0,0,0,.1);
}
}
// ----------------------------
// Tag Input
// ----------------------------
+69
View File
@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html>
<head>
<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"/>
<title> Cloudron Update </title>
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
<!-- Theme CSS -->
<link type="text/css" rel="stylesheet" href="/theme.css">
<!-- Custom Fonts -->
<link type="text/css" rel="stylesheet" href="/3rdparty/css/font-awesome.min.css">
<!-- jQuery-->
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
<!-- Bootstrap Core JavaScript -->
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
<!-- Angularjs scripts -->
<script type="text/javascript" src="/3rdparty/js/angular.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js"></script>
<!-- Update Application -->
<script type="text/javascript" src="/js/update.js"></script>
</head>
<body ng-app="Application" ng-controller="Controller" style="background-color: #7F7F7F">
<div class="modal show" id="updateProgressModal" tabindex="-1" role="dialog" aria-labelledby="updateProgressModalLabel" aria-hidden="true" data-keyboard="false" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" ng-show="!error">{{title}}</h4>
<h4 class="modal-title text-danger" ng-show="error">{{title}}</h4>
</div>
<div class="modal-body" ng-show="!error">
<div class="progress progress-striped active">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{percent}}%"></div>
</div>
<span>{{message}}</span>
</div>
<div class="modal-body" ng-show="error">
<span>{{message}}</span>
</div>
<div class="modal-footer" ng-show="error">
<button type="button" class="btn btn-primary" ng-click="loadWebadmin()">OK</button>
</div>
</div>
</div>
</div>
<div class="layout-root">
<div class="layout-content"></div>
<footer class="text-center">
<span class="text-muted">&copy;2018 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
</footer>
</div>
</body>
</html>
+19 -19
View File
@@ -22,7 +22,7 @@
<small ng-show="!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword">{{ passwordchange.error.newPassword }}<br/><br/></small>
<small ng-show=" passwordChangeForm.newPassword.$dirty && passwordChangeForm.newPassword.$invalid">Password must be atleast 8 characters</small>
</div>
<input type="password" class="form-control" ng-model="passwordchange.newPassword" id="inputPasswordChangeNewPassword" name="newPassword" ng-pattern="/^.{8,}$/" required autofocus>
<input type="password" class="form-control" ng-model="passwordchange.newPassword" id="inputPasswordChangeNewPassword" name="newPassword" ng-pattern="/^.{8,30}$/" required autofocus>
</div>
<div class="form-group" ng-class="{ 'has-error': (!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword) || (passwordChangeForm.newPasswordRepeat.$dirty && passwordChangeForm.newPasswordRepeat.$error.required) || (passwordChangeForm.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat) }">
<label class="control-label" for="inputPasswordChangeNewPasswordRepeat">Repeat new password</label>
@@ -37,7 +37,7 @@
</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="passwordchange.submit()" ng-disabled="passwordChangeForm.$invalid || passwordchange.busy"><i class="fa fa-circle-notch fa-spin" ng-show="passwordchange.busy"></i> Change</button>
<button type="button" class="btn btn-danger" ng-click="passwordchange.submit()" ng-disabled="passwordChangeForm.$invalid || passwordchange.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="passwordchange.busy"></i> Change</button>
</div>
</div>
</div>
@@ -65,7 +65,7 @@
</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="emailchange.submit()" ng-disabled="emailChangeForm.$invalid || emailchange.busy"><i class="fa fa-circle-notch fa-spin" ng-show="emailchange.busy"></i> Change</button>
<button type="button" class="btn btn-success" ng-click="emailchange.submit()" ng-disabled="emailChangeForm.$invalid || emailchange.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="emailchange.busy"></i> Change</button>
</div>
</div>
</div>
@@ -93,7 +93,7 @@
</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="fallbackEmailChange.submit()" ng-disabled="fallbackEmailChangeForm.$invalid || fallbackEmailChange.busy"><i class="fa fa-circle-notch fa-spin" ng-show="fallbackEmailChange.busy"></i> Change</button>
<button type="button" class="btn btn-success" ng-click="fallbackEmailChange.submit()" ng-disabled="fallbackEmailChangeForm.$invalid || fallbackEmailChange.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="fallbackEmailChange.busy"></i> Change</button>
</div>
</div>
</div>
@@ -122,7 +122,7 @@
</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="displayNameChange.submit()" ng-disabled="displayNameChangeForm.$invalid || displayNameChange.busy"><i class="fa fa-circle-notch fa-spin" ng-show="displayNameChange.busy"></i> Change</button>
<button type="button" class="btn btn-success" ng-click="displayNameChange.submit()" ng-disabled="displayNameChangeForm.$invalid || displayNameChange.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="displayNameChange.busy"></i> Change</button>
</div>
</div>
</div>
@@ -136,7 +136,7 @@
<h4 class="modal-title">Enable Two-Factor Authentication</h4>
</div>
<div class="modal-body text-center" ng-hide="twoFactorAuthentication.secret">
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
<h2><i class="fa fa-circle-o-notch fa-spin"></i></h2>
</div>
<div class="modal-body" ng-show="twoFactorAuthentication.secret">
<p>
@@ -160,7 +160,7 @@
</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="twoFactorAuthentication.enable()" ng-disabled="twoFactorAuthenticationEnableForm.$invalid || twoFactorAuthentication.busy"><i class="fa fa-circle-notch fa-spin" ng-show="twoFactorAuthentication.busy"></i> Enable</button>
<button type="button" class="btn btn-success" ng-click="twoFactorAuthentication.enable()" ng-disabled="twoFactorAuthenticationEnableForm.$invalid || twoFactorAuthentication.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="twoFactorAuthentication.busy"></i> Enable</button>
</div>
</div>
</div>
@@ -187,7 +187,7 @@
</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="twoFactorAuthentication.disable()" ng-disabled="twoFactorAuthenticationDisableForm.$invalid || twoFactorAuthentication.busy"><i class="fa fa-circle-notch fa-spin" ng-show="twoFactorAuthentication.busy"></i> Disable</button>
<button type="button" class="btn btn-success" ng-click="twoFactorAuthentication.disable()" ng-disabled="twoFactorAuthenticationDisableForm.$invalid || twoFactorAuthentication.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="twoFactorAuthentication.busy"></i> Disable</button>
</div>
</div>
</div>
@@ -217,19 +217,15 @@
</div>
<div ng-show="tokenAdd.token.accessToken">
Use the following token to authenticate against the <a href="https://cloudron.io/developer/api/" target="_blank">Cloudron API</a>:
Use the following token to authenticate against the Cloudron API:
<br/>
<b ng-click-select>{{ tokenAdd.token.accessToken }}</b>
<br/>
<br/>
<p>Please copy the token now. It won't be shown again for security purposes.</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-success" ng-click="tokenAdd.submit(apiClient)" ng-hide="tokenAdd.token.accessToken" ng-disabled="tokenAddForm.$invalid || tokenAdd.busy">
<i class="fa fa-circle-notch fa-spin" ng-show="tokenAdd.busy"></i> Generate Token
<i class="fa fa-circle-o-notch fa-spin" ng-show="tokenAdd.busy"></i> Generate Token
</button>
</div>
</div>
@@ -259,19 +255,19 @@
<tr>
<td class="text-muted" style="vertical-align: top;">Display name</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">
{{ user.displayName }} <a href="" ng-click="displayNameChange.show()"><i class="fa fa-edit text-small"></i></a>
{{ user.displayName }} <a href="" ng-click="displayNameChange.show()"><i class="fa fa-pencil text-small"></i></a>
</td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">Primary email</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">
{{ user.email }} <a href="" ng-click="emailchange.show()"><i class="fa fa-edit text-small"></i></a>
{{ user.email }} <a href="" ng-click="emailchange.show()"><i class="fa fa-pencil text-small"></i></a>
</td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">Password recovery email</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">
{{ user.fallbackEmail }} <a href="" ng-click="fallbackEmailChange.show()"><i class="fa fa-edit text-small"></i></a>
{{ user.fallbackEmail }} <a href="" ng-click="fallbackEmailChange.show()"><i class="fa fa-pencil text-small"></i></a>
</td>
</tr>
<tr>
@@ -302,7 +298,8 @@
<table class="table table-hover">
<thead>
<tr>
<th style="width:70%">Name</th>
<th style="width:40%">Name</th>
<th style="width:55%" class="hidden-xs hidden-sm">Token</th>
<th style="width: 5%" class="text-right">Actions</th>
</tr>
</thead>
@@ -311,8 +308,11 @@
<td class="text-left elide-table-cell">
{{ token.name || '-' }}
</td>
<td class="text-left hand elide-table-cell hidden-xs hidden-sm" ng-click="useredit.show(user)">
<span ng-click-select>{{ token.accessToken }}</span>
</td>
<td class="text-right no-wrap" style="vertical-align: bottom">
<button class="btn btn-xs btn-danger pull-right" ng-click="removeToken(apiClient, token)" title="Revoke Token"><i class="far fa-trash-alt"></i></button>
<button class="btn btn-xs btn-danger pull-right" ng-click="removeToken(apiClient, token)" title="Revoke Token"><i class="fa fa-trash-o"></i></button>
</td>
</tr>
</tbody>
+2 -4
View File
@@ -1,8 +1,6 @@
'use strict';
/* global asyncForEach:false */
/* global angular:false */
/* global $:false */
angular.module('Application').controller('AccountController', ['$scope', 'Client', function ($scope, Client) {
$scope.user = Client.getUserInfo();
@@ -313,7 +311,7 @@ angular.module('Application').controller('AccountController', ['$scope', 'Client
$scope.tokenAddForm.$setPristine();
},
show: function () {
show: function (client) {
$scope.tokenAdd.reset();
$('#tokenAddModal').modal('show');
},
@@ -345,7 +343,7 @@ angular.module('Application').controller('AccountController', ['$scope', 'Client
};
$scope.removeToken = function (client, token) {
Client.delToken(client.id, token.id, function (error) {
Client.delToken(client.id, token.accessToken, function (error) {
if (error) console.error(error);
refreshClientTokens(client);
+2 -2
View File
@@ -1,7 +1,7 @@
<div>
<div class="col-md-10 col-md-offset-1">
<h1>Eventlog</h1>
<h1>Activity Log</h1>
</div>
</div>
@@ -25,7 +25,7 @@
<div>
<div class="col-md-10 col-md-offset-1">
<div class="card card-block" style="max-width: 100%">
<center ng-show="busy"><h2><i class="fa fa-circle-notch fa-spin"></i></h2></center>
<center ng-show="busy"><h2><i class="fa fa-circle-o-notch fa-spin"></i></h2></center>
<table ng-hide="busy" class="table table-condensed table-hover">
<thead>
<tr>
+3 -24
View File
@@ -1,8 +1,5 @@
'use strict';
/* global angular:false */
/* global $:false */
angular.module('Application').controller('ActivityController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
@@ -22,37 +19,19 @@ angular.module('Application').controller('ActivityController', ['$scope', '$loca
{ name: 'app.uninstall', value: 'app.uninstall' },
{ name: 'app.update', value: 'app.update' },
{ name: 'app.login', value: 'app.login' },
{ name: 'app.oom', value: 'app.oom' },
{ name: 'app.down', value: 'app.down' },
{ name: 'app.up', value: 'app.up' },
{ name: 'Apptask Crash', value: 'app.task.crash' },
{ name: 'backup.cleanup', value: 'backup.cleanup.start' },
{ name: 'backup.cleanup.finish', value: 'backup.cleanup.finish' },
{ name: 'backup.cleanup', value: 'backup.cleanup' },
{ name: 'backup.finish', value: 'backup.finish' },
{ name: 'backup.start', value: 'backup.start' },
{ name: 'certificate.new', value: 'certificate.new' },
{ name: 'certificate.renew', value: 'certificate.renew' },
{ name: 'settings.climode', value: 'settings.climode' },
{ name: 'cloudron.activate', value: 'cloudron.activate' },
{ name: 'cloudron.provision', value: 'cloudron.provision' },
{ name: 'cloudron.restore', value: 'cloudron.restore' },
{ name: 'cloudron.start', value: 'cloudron.start' },
{ name: 'cloudron.update', value: 'cloudron.update' },
{ name: 'dashboard.domain.update', value: 'dashboard.domain.update' },
{ name: 'dyndns.update', value: 'dyndns.update' },
{ name: 'domain.add', value: 'domain.add' },
{ name: 'domain.update', value: 'domain.update' },
{ name: 'domain.remove', value: 'domain.remove' },
{ name: 'mail.enabled', value: 'mail.enabled' },
{ name: 'mail.box.add', value: 'mail.box.add' },
{ name: 'mail.box.remove', value: 'mail.box.remove' },
{ name: 'mail.list.add', value: 'mail.list.add' },
{ name: 'mail.list.remove', value: 'mail.list.remove' },
{ name: 'user.add', value: 'user.add' },
{ name: 'user.login', value: 'user.login' },
{ name: 'user.remove', value: 'user.remove' },
{ name: 'user.transfer', value: 'user.transfer' },
{ name: 'user.update', value: 'user.update' },
{ name: 'System Crash', value: 'system.crash' }
{ name: 'user.update', value: 'user.update' }
];
$scope.pageItemCount = [
+133 -126
View File
@@ -9,8 +9,8 @@
<div class="modal-body" style="padding: 0 15px">
<fieldset>
<form role="form" name="appConfigureForm" ng-submit="appConfigure.submit()" autocomplete="off">
<uib-tabset active="appConfigure.action">
<uib-tab index="'general'" heading="General">
<uib-tabset>
<uib-tab index="0" heading="General">
<br/>
<div class="has-error text-center" ng-show="appConfigure.error.other">{{ appConfigure.error.other }}</div>
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.location.$dirty && appConfigureForm.location.$invalid) || (!appConfigureForm.location.$dirty && appConfigure.error.location) }">
@@ -20,7 +20,9 @@
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span>{{ (!appConfigure.location ? '' : (appConfigure.domain.config.hyphenatedSubdomains ? '-' : '.')) + appConfigure.domain.domain }}</span>
<!-- the admin check is to check for spaces user -->
<span ng-if="user.admin">{{ (!appConfigure.location ? '' : (appConfigure.domain.config.hyphenatedSubdomains ? '-' : '.')) + appConfigure.domain.domain }}</span>
<span ng-if="!user.admin">{{ (!appConfigure.location ? '' : '-') + spacesSuffix + (appConfigure.domain.config.hyphenatedSubdomains ? '-' : '.') + appConfigure.domain.domain }}</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
@@ -32,7 +34,7 @@
</div>
</div>
<p class="text-center" ng-show="appConfigure.location && appConfigure.domain.provider === 'manual'">
<p class="text-center" ng-show="appConfigure.location && appConfigure.domain.provider === 'manual' && !appConfigure.domain.config.wildcard">
<b>Add an A record manually for {{ appConfigure.location }} to this Cloudron's public IP</b>
<br>
</p>
@@ -41,12 +43,7 @@
<div ng-repeat="(env, info) in appConfigure.portBindingsInfo">
<ng-form name="portInfo_form">
<div class="form-group" ng-class="{ 'has-error': (!appConfigureForm.itemName{{$index}}.$dirty && appConfigure.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
<label class="control-label" for="appConfigurePortInput{{env}}"><input type="checkbox" ng-model="appConfigure.portBindingsEnabled[env]">
{{ info.title }}
<sup>
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})"><i class="fa fa-question-circle"></i></a>
</sup>
</label>
<label class="control-label" for="appConfigurePortInput{{env}}"><input type="checkbox" ng-model="appConfigure.portBindingsEnabled[env]"> {{ info.description }} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})</label>
<input type="number" class="form-control" ng-model="appConfigure.portBindings[env]" ng-disabled="!appConfigure.portBindingsEnabled[env]" id="appConfigurePortInput{{env}}" later-name="itemName{{$index}}" min="{{HOST_PORT_MIN}}" max="{{HOST_PORT_MAX}}" required>
</div>
</ng-form>
@@ -96,22 +93,10 @@
<br/>
<br/>
</div>
</uib-tab>
<uib-tab index="'display'" heading="Display">
<br/>
<div class="form-group" ng-class="{ 'has-error': !appConfigureForm.label.$dirty && appConfigure.error.label }">
<label class="control-label">Display Label</label>
<div class="control-label" ng-show="appConfigure.error.label">{{appConfigure.error.label}}</div>
<input type="text" class="form-control" id="appConfigureLabelInput" name="label" ng-model="appConfigure.label">
</div>
<div class="form-group">
<label class="control-label">Tags</label>
<tag-input class="form-control" placeholder="Use comma to separate tags" taglist="appConfigure.tags" name="tags" uib-tooltip="For grouping in the dashboard"></tag-input>
</div>
</uib-tab>
<uib-tab index="'advanced'" heading="Advanced">
<uib-tab index="1" heading="Advanced">
<br/>
<div class="form-group">
<label class="control-label" for="memoryLimit">Memory Limit <sup><a ng-href="{{ config.webServerOrigin }}/documentation/apps/#increasing-the-memory-limit-of-an-app" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup> : <b>{{ appConfigure.memoryLimit ? appConfigure.memoryLimit / 1024 / 1024 + 'MB' : 'Default (256 MB)' }}</b></label>
@@ -121,34 +106,33 @@
</div>
</div>
<!-- recvmail currently only works with cloudron email -->
<div class="form-group" ng-show="appConfigure.app.manifest.addons.sendmail || appConfigure.app.manifest.addons.recvmail" ng-class="{ 'has-error': !appConfigureForm.mailboxName.$dirty && appConfigure.error.mailboxName }">
<input type="checkbox" id="appConfigureMailboxNameEnabled" ng-model="appConfigure.mailboxNameEnabled">
<label class="control-label" for="appConfigureMailboxNameEnabled">Custom Mailbox Name</label>
<div class="has-error" ng-show="appConfigure.error.mailboxName">{{ appConfigure.error.mailboxName }}</div>
<div class="form-group" ng-show="appConfigure.app.manifest.addons.sendmail" ng-class="{ 'has-error': !appConfigureForm.mailboxName.$dirty && appConfigure.error.mailboxName }">
<label class="control-label">Mailbox Name</label>
<div class="control-label" ng-show="appConfigure.error.mailboxName">{{appConfigure.error.mailboxName}}</div>
<div class="input-group form-inline">
<input type="text" class="form-control" id="appConfigureMailboxNameInput" ng-required="appConfigure.mailboxNameEnabled" name="mailboxName" ng-model="appConfigure.mailboxName" uib-tooltip="App FROM email address. Addresses ending with '.app' are reserved." ng-disabled="!appConfigure.mailboxNameEnabled">
<input type="text" class="form-control" id="appConfigureMailboxNameInput" name="mailboxName" ng-model="appConfigure.mailboxName" uib-tooltip="App FROM email address">
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" ng-disabled="!appConfigure.mailboxNameEnabled">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
@{{ appConfigure.domain.domain }}
</button>
</div>
</div>
</div>
<div class="has-error" ng-show="appConfigure.error.alternateDomains">{{ appConfigure.error.alternateDomains }}</div>
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.alternateSubdomain.$dirty && appConfigureForm.alternateSubdomain.$invalid) || (!appConfigureForm.alternateSubdomain.$dirty && appConfigure.error.alternateDomains) }">
<input type="checkbox" id="appConfigureAlternateDomainEnabled" ng-model="appConfigure.alternateDomainEnabled">
<label class="control-label" for="appConfigureAlternateDomainEnabled">Redirect the following domain to this app</label>
<div class="has-error" ng-show="appConfigure.error.alternateDomains">{{ appConfigure.error.alternateDomains }}</div>
<div class="input-group form-inline">
<input type="text" class="form-control" ng-model="appConfigure.alternateSubdomain" id="appConfigureAlternateSubdomainInput" name="alternateSubdomain" placeholder="Leave empty to use bare domain" ng-disabled="!appConfigure.alternateDomainEnabled">
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" ng-disabled="!appConfigure.alternateDomainEnabled">
<span>{{ (!appConfigure.alternateSubdomain ? '' : (appConfigure.alternateDomain.config.hyphenatedSubdomains ? '-' : '.')) + appConfigure.alternateDomain.domain }}</span>
<!-- the admin check is to check for spaces user -->
<span ng-if="user.admin">{{ (!appConfigure.alternateSubdomain ? '' : (appConfigure.alternateDomain.config.hyphenatedSubdomains ? '-' : '.')) + appConfigure.alternateDomain.domain }}</span>
<span ng-if="!user.admin">{{ (!appConfigure.alternateSubdomain ? '' : '-') + spacesSuffix + (appConfigure.alternateDomain.config.hyphenatedSubdomains ? '-' : '.') + appConfigure.alternateDomain.domain }}</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
@@ -166,16 +150,9 @@
<input type="text" class="form-control" id="appConfigureXFrameOptionsInput" name="xFrameOptions" placeholder="https://example.com" ng-model="appConfigure.xFrameOptions" uib-tooltip="Leave blank to not allow embedding">
</div>
<div ng-hide="true" class="form-group" ng-class="{ 'has-error': !appConfigureForm.dataDir.$dirty && appConfigure.error.dataDir }">
<input type="checkbox" id="appConfigureEnableDataDir" ng-model="appConfigure.dataDirEnabled">
<label class="control-label" for="appConfigureEnableDataDir">Custom Data Directory</label>
<div class="control-label" ng-show="appConfigure.error.dataDir">{{appConfigure.error.dataDir}}</div>
<input type="text" class="form-control" id="appConfigureDataDirInput" name="dataDir" ng-disabled="!appConfigure.dataDirEnabled" placeholder="/mnt/appdata" ng-model="appConfigure.dataDir">
</div>
<div class="form-group">
<label class="control-label" style="width: 100%">Specify robots.txt file content <sup><a ng-href="{{ config.webServerOrigin }}/documentation/apps/#indexing-by-search-engines-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="appConfigure.robotsTxt = disableIndexingTemplate">Disable indexing</a></label>
<textarea ng-model="appConfigure.robotsTxt" placeholder="Leave empty to allow all bots to index this app." class="form-control" rows="4"></textarea>
<label class="control-label">Specify robots.txt file content</label>
<textarea ng-model="appConfigure.robotsTxt" placeholder="Leave empty to allow all bots to index this app." class="form-control" rows="3"></textarea>
</div>
<div class="form-group">
@@ -183,11 +160,6 @@
<label class="control-label" for="appConfigureEnableBackup">Enable automatic daily backups</label>
</div>
<div class="form-group">
<input type="checkbox" id="appConfigureEnableAutomaticUpdate" ng-model="appConfigure.enableAutomaticUpdate">
<label class="control-label" for="appConfigureEnableAutomaticUpdate">Enable automatic updates</label>
</div>
<div class="hide">
<label class="control-label" for="appConfigureCertificateInput" ng-show="appConfigure.domain.provider !== 'caas'">Certificate (optional)</label>
<div class="has-error text-center" ng-show="appConfigure.error.cert && appConfigure.domain.provider !== 'caas'">{{ appConfigure.error.cert }}</div>
@@ -219,7 +191,7 @@
</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="appConfigure.submit()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy || (appConfigure.accessRestrictionOption === 'groups' && !appConfigure.isAccessRestrictionValid())"><i class="fa fa-circle-notch fa-spin" ng-show="appConfigure.busy"></i> Configure</button>
<button type="button" class="btn btn-success" ng-click="appConfigure.submit()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy || (appConfigure.accessRestrictionOption === 'groups' && !appConfigure.isAccessRestrictionValid())"><i class="fa fa-circle-o-notch fa-spin" ng-show="appConfigure.busy"></i> Configure</button>
</div>
</div>
</div>
@@ -233,9 +205,9 @@
<h4 class="modal-title">Backups - {{ appRestore.app.fqdn }}</h4>
</div>
<div class="modal-body" style="padding: 0 15px">
<p class="text-center" ng-show="appRestore.busyFetching"><i class="fa fa-circle-notch fa-spin"></i> Fetching backups</p>
<p class="text-center" ng-show="appRestore.busyFetching"><i class="fa fa-circle-o-notch fa-spin"></i> Fetching backups</p>
<button type="button" class="btn btn-primary pull-right" ng-click="appRestore.createBackup()" ng-hide="appRestore.busyFetching" ng-disabled="appRestore.app.installationState === 'pending_backup'"><i class="fa fa-circle-notch fa-spin" ng-show="appRestore.app.installationState === 'pending_backup'"></i> Create Backup</button>
<button type="button" class="btn btn-primary pull-right" ng-click="appRestore.createBackup()" ng-hide="appRestore.busyFetching" ng-disabled="appRestore.app.installationState === 'pending_backup'"><i class="fa fa-circle-o-notch fa-spin" ng-show="appRestore.app.installationState === 'pending_backup'"></i> Create Backup</button>
<uib-tabset active="appRestore.action" ng-show="!appRestore.busyFetching">
<!-- restore -->
@@ -243,7 +215,7 @@
<br/>
<p class="text-danger" ng-hide="appRestore.backups.length">This app has no backups to restore or clone from yet.</p>
<div ng-show="appRestore.backups.length">
<p>Restoring the app will lose all it's data since the backup.</p>
<p>Restoring the app will lose all content generated since the backup.</p>
<label class="control-label">Select Backup</label>
<div class="dropdown">
<button type="button" class="btn btn-default" data-toggle="dropdown">{{ appRestore.selectedBackup.creationTime | prettyDate }} - v{{appRestore.selectedBackup.version}} ({{ appRestore.selectedBackup.creationTime | prettyLongDate }}) <span class="caret"></span></button>
@@ -252,8 +224,6 @@
<a href="" ng-click="appRestore.selectBackup(backup)">{{ backup.creationTime | prettyDate }} - v{{backup.version}} ({{ backup.creationTime | prettyLongDate }})</a>
</li>
</ul>
<input type="text" class="offscreen" aria-hidden="true" id="appRestoreSelectedBackupId" value="{{appRestore.selectedBackup.id}}">
<i style="margin-left: 10px;" class="fa fa-copy hand" uib-tooltip="{{ appRestore.copyBackupIdDone ? 'Copied to clipboard' : 'Click to copy backup id' }}" tooltip-placement="right" ng-click="appRestore.copyBackupId()"></i>
</div>
<br/>
<fieldset>
@@ -293,7 +263,9 @@
<input type="text" class="form-control" ng-model="appRestore.location" id="appRestoreLocationInput" name="location" placeholder="Leave empty to use bare domain" autofocus>
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span>{{ (!appRestore.location ? '' : (appRestore.domain.config.hyphenatedSubdomains ? '-' : '.')) + appRestore.domain.domain }}</span>
<!-- the admin check is to check for spaces user -->
<span ng-if="user.admin">{{ (!appRestore.location ? '' : (appRestore.domain.config.hyphenatedSubdomains ? '-' : '.')) + appRestore.domain.domain }}</span>
<span ng-if="!user.admin">{{ (!appRestore.location ? '' : '-') + spacesSuffix + (appRestore.domain.config.hyphenatedSubdomains ? '-' : '.') + appRestore.domain.domain }}</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
@@ -305,7 +277,7 @@
</div>
</div>
<p class="text-center" ng-show="appRestore.location && appRestore.domain.provider === 'manual'">
<p class="text-center" ng-show="appRestore.location && appRestore.domain.provider === 'manual' && !appRestore.domain.config.wildcard">
<b>Add an A record manually for {{ appRestore.location }} to this Cloudron's public IP</b>
<br>
</p>
@@ -314,12 +286,7 @@
<div ng-repeat="(env, info) in appRestore.portBindingsInfo">
<ng-form name="portInfo_form">
<div class="form-group" ng-class="{ 'has-error': (!appRestore.itemName{{$index}}.$dirty && appRestore.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="appRestore.portBindingsEnabled[env]">
{{ info.title }}
<sup>
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})"><i class="fa fa-question-circle"></i></a>
</sup>
</label>
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="appRestore.portBindingsEnabled[env]"> {{ info.description }} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})</label>
<input type="number" class="form-control" ng-model="appRestore.portBindings[env]" ng-disabled="!appRestore.portBindingsEnabled[env]" id="inputPortInfo{{env}}" later-name="itemName{{$index}}" min="{{hostPortMin}}" max="{{hostPortMax}}" required>
</div>
</ng-form>
@@ -333,8 +300,8 @@
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-success" ng-click="appRestore.clone()" ng-show="appRestore.action === 'clone' && appRestore.backups.length !== 0" ng-disabled="appRestore.busy || !appRestore.selectedBackup"><i class="fa fa-circle-notch fa-spin" ng-show="appRestore.busy"></i> Clone</button>
<button type="button" class="btn btn-danger" ng-click="appRestore.restore()" ng-show="appRestore.action === 'restore' && appRestore.backups.length !== 0" ng-disabled="!appRestore.password || appRestore.busy || !appRestore.selectedBackup"><i class="fa fa-circle-notch fa-spin" ng-show="appRestore.busy"></i> Restore</button>
<button type="button" class="btn btn-success" ng-click="appRestore.clone()" ng-show="appRestore.action === 'clone' && appRestore.backups.length !== 0" ng-disabled="appRestore.busy || !appRestore.selectedBackup"><i class="fa fa-circle-o-notch fa-spin" ng-show="appRestore.busy"></i> Clone</button>
<button type="button" class="btn btn-danger" ng-click="appRestore.restore()" ng-show="appRestore.action === 'restore' && appRestore.backups.length !== 0" ng-disabled="!appRestore.password || appRestore.busy || !appRestore.selectedBackup"><i class="fa fa-circle-o-notch fa-spin" ng-show="appRestore.busy"></i> Restore</button>
</div>
</div>
</div>
@@ -348,7 +315,7 @@
<img ng-src="{{appInfo.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-info-icon"/>
<h5 class="app-info-title">
{{ appInfo.app.manifest.title }}
<span class="app-info-meta text-small">{{ appInfo.app.upstreamVersion }} (Package <a ng-href="/#/appstore/{{appInfo.app.manifest.id}}?version={{appInfo.app.manifest.version}}">v{{ appInfo.app.manifest.version }}</a>) </span>
<span class="app-info-meta text-small">Package <a ng-href="/#/appstore/{{appInfo.app.manifest.id}}?version={{appInfo.app.manifest.version}}">v{{ appInfo.app.manifest.version }}</a> </span>
<br/>
App ID <span class="app-info-meta text-small">{{ appInfo.app.id }}</a> </span>
<br/>
@@ -362,17 +329,13 @@
<div class="app-postinstall-message" ng-show="appInfo.app.manifest && appInfo.app.manifest.postInstallMessage">
<div ng-bind-html="appInfo.message | postInstallMessage:appInfo.app | markdown2html"></div>
</div>
<div ng-show="appInfo.app.manifest.addons.localstorage.ftp">
<div ng-show="appInfo.app.manifest.documentationUrl">
<br/>
<b>SFTP</b> <sup><a ng-href="{{ config.webServerOrigin }}/documentation/apps/#ftp-access" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup><br/>
Server: {{ config.adminFqdn }}<br/>
Port: 222<br/>
Username: {{ user.username }}@{{ appInfo.app.fqdn }}<br/>
Please see the <a target="_blank" ng-href="{{appInfo.app.manifest.documentationUrl}}">documentation</a> for more information.
</div>
</div>
<div class="modal-footer">
<a ng-show="appInfo.app.manifest.documentationUrl" target="_blank" ng-href="{{appInfo.app.manifest.documentationUrl}}" class="btn btn-info pull-left">Documentation</a>
<button type="button" class="btn btn-default" data-dismiss="modal" autofocus>Close</button>
<button type="button" class="btn btn-default" data-dismiss="modal" autofocus>Got it</button>
</div>
</div>
</div>
@@ -421,9 +384,8 @@
<p>{{ appError.app.message | prettyAppMessage }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary pull-left" ng-click="appConfigure.show(appError.app)" autofocus>Repair</button>
<a type="button" class="btn btn-default pull-left" ng-href="{{ '/logs.html?appId=' + appError.app.id }}" target="_blank">Logs</a>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-default pull-left" ng-click="appConfigure.show(appError.app)" autofocus>Repair</button>
<button type="button" class="btn btn-default" data-dismiss="modal">OK</button>
</div>
</div>
</div>
@@ -437,7 +399,7 @@
<h4 class="modal-title">Really uninstall {{ appUninstall.app.fqdn }} ?</h4>
</div>
<div class="modal-body">
<p>Deleting the app will also remove all it's data!</p>
<p>Deleting the app will also remove all content generated within this app!</p>
<fieldset>
<form role="form" name="appUninstallForm" ng-submit="doUninstall()" autocomplete="off">
<div class="form-group" ng-class="{ 'has-error': (appUninstallForm.password.$dirty && appUninstallForm.password.$invalid) || (!appUninstallForm.password.$dirty && appUninstall.error.password) }">
@@ -455,7 +417,7 @@
</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="doUninstall()" ng-disabled="appUninstallForm.$invalid || appUninstall.busy"><i class="fa fa-circle-notch fa-spin" ng-show="appUninstall.busy"></i> Uninstall</button>
<button type="button" class="btn btn-danger" ng-click="doUninstall()" ng-disabled="appUninstallForm.$invalid || appUninstall.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="appUninstall.busy"></i> Uninstall</button>
</div>
</div>
</div>
@@ -474,7 +436,7 @@
</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="doUpdate()" ng-disabled="appUpdate.busy"><i class="fa fa-circle-notch fa-spin" ng-show="appUpdate.busy"></i> Update</button>
<button type="button" class="btn btn-danger" ng-click="doUpdate()" ng-disabled="appUpdate.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="appUpdate.busy"></i> Update</button>
</div>
</div>
</div>
@@ -484,8 +446,15 @@
function imageErrorHandler(elem) {
'use strict';
elem.src = elem.getAttribute('fallback-icon');
elem.onerror = null; // avoid retry after default icon cannot be loaded
var appstoreIconUrl = elem.getAttribute('appstore-icon');
var fallbackIconUrl = elem.getAttribute('fallback-icon');
if (elem.src === appstoreIconUrl) {
elem.src = fallbackIconUrl;
elem.onerror = null; // avoid retry after default icon cannot be loaded
} else {
elem.src = appstoreIconUrl;
}
}
</script>
@@ -494,7 +463,7 @@
<!-- Workaround for select-all issue, see commit message -->
<div style="font-size: 1px;">&nbsp;</div>
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && user.admin">
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && (user.admin || config.features.spaces)">
<div class="col-md-12" style="text-align: center;">
<br/><br/><br/><br/>
<h1><i class="fa fa-cloud-download fa-fw"></i> No apps installed yet!</h1>
@@ -503,7 +472,7 @@
</div>
</div>
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && !user.admin">
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && !(user.admin || config.features.spaces)">
<div class="col-md-12" style="text-align: center;">
<br/><br/><br/><br/>
<h1>You don't have access to any apps on this Cloudron yet!</h1>
@@ -513,60 +482,98 @@
</div>
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
<h1 class="view-header">
Your Apps
<div class="pull-right">
<multiselect ng-model="selectedTags" ng-show="tags.length > 0" ms-header="All Tags" ms-selected="Tags: {{ selectedTags.join(', ') }}" options="tag for tag in tags" data-multiple="true"></multiselect>
<multiselect ng-model="selectedDomains" ng-show="domains.length > 1" ms-header="All Domains" ms-selected="{{ selectedDomains | prettyDomains }}" options="domain.domain for domain in domains" data-multiple="true"></multiselect>
<div class="col-md-12">
<h1>Your Apps</h1>
</div>
</h1>
</div>
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
<div class="app-grid">
<div class="grid-item" ng-repeat="app in installedApps | selectedTagFilter:selectedTags | selectedDomainFilter:selectedDomains | orderBy:'location'">
<a ng-href="{{ app | applicationLink }}" ng-click="((app | installError) === true && showError(app)) || ((app | appIsInstalledAndHealthy) && app.pendingPostInstallConfirmation && appPostInstallConfirm.show(app))" target="_blank" ng-class="{ 'hand': (app | appIsInstalledAndHealthy) }">
<div class="col-sm-1 grid-item" ng-repeat="app in installedApps | orderBy:'location'">
<div style="background-color: white;" class="highlight grid-item-content" uib-tooltip="{{ app.fqdn }}">
<div class="grid-item-top">
<div class="row">
<div class="col-xs-12 text-center" style="padding-left: 5px; padding-right: 5px;">
<br/>
<img ng-src="{{ app.iconUrl || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)" class="app-icon"/>
</div>
</div>
<br/>
<div class="row">
<div class="col-xs-12 text-center">
<div class="grid-item-top-title" data-fittext>{{ app.label || app.location || app.fqdn }}</div>
<div class="text-muted status" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden" uib-tooltip="{{ app.message | shortAppMessage }}">
{{ app | installationStateLabel }}
<a ng-href="{{ app | applicationLink }}" ng-click="((app | installError) === true && showError(app)) || ((app | appIsInstalledAndHealthy) && app.pendingPostInstallConfirmation && appPostInstallConfirm.show(app))" target="_blank" ng-class="{ 'hand': (app | appIsInstalledAndHealthy) }">
<div class="grid-item-top">
<div class="row">
<div class="col-xs-12 text-center" style="padding-left: 5px; padding-right: 5px;">
<br/>
<img ng-src="{{app.iconUrl || 'img/appicon_fallback.png'}}" fallback-icon="img/appicon_fallback.png" appstore-icon="{{ app.iconUrlStore }}" onerror="imageErrorHandler(this)" class="app-icon"/>
</div>
</div>
<br/>
<div class="row">
<div class="col-xs-12 text-center">
<div class="grid-item-top-title" data-fittext>{{ app.location || app.fqdn }}</div>
<div class="text-muted status" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden" uib-tooltip="{{ app.message | shortAppMessage }}">
{{ app | installationStateLabel }}
</div>
<div class="status" ng-style="{ 'visibility': (app | installationActive) ? 'visible' : 'hidden' }">
<div class="progress progress-striped active">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
</div>
</div>
</div>
</div>
</div>
<div class="status" ng-style="{ 'visibility': (app | installationActive) ? 'visible' : 'hidden' }">
<div class="progress progress-striped active">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
</div>
<div class="grid-item-bottom-mobile" ng-show="user.admin || (config.features.spaces && app.ownerId === user.id)">
<div class="row">
<div class="col-xs-4 text-left">
<a href="" ng-click="appRestore.show(app)" ng-show="backupConfig.provider !== 'noop'">
<i class="fa fa-undo scale"></i>
</a>
<a href="" ng-click="appConfigure.show(app)" ng-show="app.installationState === 'installed' || app.installationState === 'pending_configure' || (app | installError)">
<i ng-hide="(app | installError)" class="fa fa-pencil scale"></i>
<i ng-show="(app | installError)" class="fa fa-wrench scale"></i>
</a>
</div>
<div class="col-xs-4 text-center"></div>
<div class="col-xs-4 text-right">
<a href="" ng-click="showUninstall(app)">
<i class="fa fa-remove scale"></i>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="grid-item-bottom" ng-show="user.admin || (config.features.spaces && app.ownerId === user.id)">
<div>
<a href="" ng-click="showUninstall(app)" uib-tooltip="Uninstall" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-remove scale"></i></a>
</div>
<div class="grid-item-actions" ng-show="user.admin">
<a href="" ng-click="showUninstall(app)" uib-tooltip="Uninstall" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-times scale"></i></a>
<a href="" ng-click="appRestore.show(app)" ng-show="backupsEnabled" uib-tooltip="Backups" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-archive scale"></i></a>
<a href="" ng-click="appConfigure.show(app)" ng-show="(app.installationState === 'installed' || app.installationState === 'pending_configure') && !(app | installError)" uib-tooltip="Configure" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-pencil-alt scale"></i></a>
<a href="" ng-click="appConfigure.show(app)" ng-show="app | installError" uib-tooltip="Repair" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-wrench scale"></i></a>
<a ng-href="{{ '/terminal.html?id=' + app.id }}" target="_blank" uib-tooltip="Terminal" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-terminal scale"></i></a>
<a ng-href="{{ '/logs.html?appId=' + app.id }}" target="_blank" uib-tooltip="Logs" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-file-alt scale"></i></a>
<a href="" ng-click="showInformation(app)" uib-tooltip="Information" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-info-circle scale"></i></a>
</div>
<div>
<a href="" ng-click="appRestore.show(app)" ng-show="backupConfig.provider !== 'noop'" uib-tooltip="Backups" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-archive scale"></i></a>
</div>
<!-- we check the version here because the box updater does not know when an app gets updated -->
<div class="app-update-badge" ng-click="showUpdate(app, config.update.apps[app.id].manifest)" ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
<i class="fa fa-arrow-up fa-inverse"></i>
</div>
<div ng-show="(app.installationState === 'installed' || app.installationState === 'pending_configure') && !(app | installError)">
<a href="" ng-click="appConfigure.show(app)" uib-tooltip="Configure" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-pencil scale"></i></a>
</div>
<div ng-show="app | installError">
<a href="" ng-click="appConfigure.show(app)" uib-tooltip="Repair" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-wrench scale"></i></a>
</div>
<div>
<a ng-href="{{ '/terminal.html?id=' + app.id }}" target="_blank" uib-tooltip="Terminal" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-terminal scale"></i></a>
</div>
<div>
<a ng-href="{{ '/logs.html?id=' + app.id }}" target="_blank" uib-tooltip="Logs" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-file-text scale"></i></a>
</div>
<div>
<a href="" ng-click="showInformation(app)" uib-tooltip="Information" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-info-circle scale"></i></a>
</div>
</div>
<!-- we check the version here because the box updater does not know when an app gets updated -->
<div class="app-update-badge" ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
<a href="" ng-click="showUpdate(app, config.update.apps[app.id].manifest)" title="Update Available">
<span class="fa-stack fa-lg scale-small">
<i class="fa fa-circle fa-stack-2x text-success"></i>
<i class="fa fa-refresh fa-stack-1x fa-inverse"></i>
</span>
</a>
</div>
</a>
</div>
</a>
</div>
</div>
</div>
</div>
+22 -97
View File
@@ -1,23 +1,17 @@
'use strict';
/* global angular:false */
/* global $:false */
angular.module('Application').controller('AppsController', ['$scope', '$location', '$timeout', '$interval', 'Client', function ($scope, $location, $timeout, $interval, Client) {
angular.module('Application').controller('AppsController', ['$scope', '$location', '$timeout', '$interval', 'Client', 'ngTld', 'AppStore', function ($scope, $location, $timeout, $interval, Client, ngTld, AppStore) {
$scope.HOST_PORT_MIN = 1024;
$scope.HOST_PORT_MAX = 65535;
$scope.installedApps = Client.getInstalledApps();
$scope.tags = Client.getAppTags();
$scope.selectedTags = [];
$scope.selectedDomains = [];
$scope.config = Client.getConfig();
$scope.user = Client.getUserInfo();
$scope.domains = [];
$scope.usedDomains = [];
$scope.groups = [];
$scope.users = [];
$scope.backupsEnabled = true;
$scope.disableIndexingTemplate = '# Disable search engine indexing\n\nUser-agent: *\nDisallow: /';
$scope.backupConfig = {};
$scope.spacesSuffix = '';
$scope.appConfigure = {
busy: false,
@@ -37,20 +31,14 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
memoryLimit: 0,
memoryTicks: [],
mailboxName: '',
accessRestrictionOption: 'any',
accessRestriction: { users: [], groups: [] },
xFrameOptions: '',
dataDir: null,
alternateDomainEnabled: false,
mailboxNameEnabled: false,
dataDirEnabled: false,
alternateSubdomain: '',
alternateDomain: null,
ssoAuth: false,
tags: '',
label: '',
action: 'general',
isAccessRestrictionValid: function () {
var tmp = $scope.appConfigure.accessRestriction;
@@ -62,22 +50,21 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
// fill relevant info from the app
$scope.appConfigure.app = app;
$scope.appConfigure.location = app.location;
if ($scope.user.admin) {
$scope.appConfigure.location = app.location;
} else { // strip the trailing username in spaces mode
$scope.appConfigure.location = app.location === $scope.spacesSuffix ? '' : app.location.replace(new RegExp('-' + $scope.spacesSuffix + '$'),'');
}
$scope.appConfigure.domain = $scope.domains.filter(function (d) { return d.domain === app.domain; })[0];
$scope.appConfigure.portBindingsInfo = angular.extend({}, app.manifest.tcpPorts, app.manifest.udpPorts); // Portbinding map only for information
$scope.appConfigure.memoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024);
$scope.appConfigure.xFrameOptions = app.xFrameOptions.indexOf('ALLOW-FROM') === 0 ? app.xFrameOptions.split(' ')[1] : '';
$scope.appConfigure.dataDirEnabled = !!app.dataDir;
$scope.appConfigure.dataDir = app.dataDir;
$scope.appConfigure.alternateDomainEnabled = !!app.alternateDomains[0];
$scope.appConfigure.alternateSubdomain = app.alternateDomains[0] ? app.alternateDomains[0].subdomain : '';
$scope.appConfigure.alternateDomain = app.alternateDomains[0] ? $scope.domains.filter(function (d) { return d.domain === app.alternateDomains[0].domain; })[0] : $scope.appConfigure.domain;
$scope.appConfigure.robotsTxt = app.robotsTxt;
$scope.appConfigure.enableBackup = app.enableBackup;
$scope.appConfigure.enableAutomaticUpdate = app.enableAutomaticUpdate;
$scope.appConfigure.mailboxNameEnabled = app.mailboxName && (app.mailboxName.match(/\.app$/) === null);
$scope.appConfigure.mailboxName = app.mailboxName || '';
$scope.appConfigure.label = app.label || '';
$scope.appConfigure.ssoAuth = (app.manifest.addons['ldap'] || app.manifest.addons['oauth']) && app.sso;
@@ -116,9 +103,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
}
}
// translate for tag-input
$scope.appConfigure.tags = app.tags ? app.tags.join(',') : '';
$('#appConfigureModal').modal('show');
},
@@ -127,8 +111,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appConfigure.error.other = null;
$scope.appConfigure.error.location = null;
$scope.appConfigure.error.xFrameOptions = null;
$scope.appConfigure.error.label = null;
$scope.appConfigure.error.dataDir = null;
$scope.appConfigure.error.alternateDomains = null;
$scope.appConfigure.error.mailboxName = null;
@@ -158,44 +140,25 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
memoryLimit: $scope.appConfigure.memoryLimit === $scope.appConfigure.memoryTicks[0] ? 0 : $scope.appConfigure.memoryLimit,
robotsTxt: $scope.appConfigure.robotsTxt,
enableBackup: $scope.appConfigure.enableBackup,
enableAutomaticUpdate: $scope.appConfigure.enableAutomaticUpdate,
alternateDomains: [],
label: $scope.appConfigure.label,
tags: $scope.appConfigure.tags.split(',').map(function (t) { return t.trim(); }).filter(function (t) { return !!t; }),
dataDir: $scope.appConfigure.dataDirEnabled ? $scope.appConfigure.dataDir : ''
alternateDomains: []
};
// The backend supports multiple alternateDomains, however we only have ui for one
if ($scope.appConfigure.alternateDomainEnabled) data.alternateDomains = [{ domain: $scope.appConfigure.alternateDomain.domain, subdomain: $scope.appConfigure.alternateSubdomain }];
if ($scope.appConfigure.mailboxNameEnabled) {
data.mailboxName = $scope.appConfigure.mailboxName;
// add mailbox automatically for convenience
if ($scope.appConfigure.app.manifest.addons.recvmail) {
Client.addMailbox(data.domain, data.mailboxName, $scope.user.id, function (error) {
if (error && error.statusCode !== 409) console.error(error); // it's fine if it already exists
});
}
} else { // setting to empty will reset to .app name
data.mailboxName = '';
}
if ($scope.appConfigure.mailboxName !== $scope.appConfigure.app.mailboxName) data.mailboxName = $scope.appConfigure.mailboxName;
Client.configureApp($scope.appConfigure.app.id, data, function (error) {
let tab = 'advanced'; // the tab to switch to
if (error) {
if (error.statusCode === 409 && (error.message.indexOf('Port') !== -1)) {
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
$scope.appConfigure.error.port = error.message;
tab = 'general';
} else if (error.statusCode === 400 && error.message.indexOf('mailbox') !== -1 ) {
} else if (error.statusCode === 409 && error.message.indexOf('mailbox') !== -1 ) {
$scope.appConfigure.error.mailboxName = error.message;
$scope.appConfigureForm.mailboxName.$setPristine();
$('#appConfigureMailboxNameInput').focus();
} else if (error.statusCode === 409 && error.message.indexOf('subdomain') !== -1) {
} else if (error.statusCode === 409) {
$scope.appConfigure.error.location = error.message;
$scope.appConfigureForm.location.$setPristine();
tab = 'general';
$('#appConfigureLocationInput').focus();
} else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) {
$scope.appConfigure.error.cert = error.message;
@@ -211,16 +174,10 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appConfigure.error.alternateDomains = error.message;
$scope.appConfigureForm.alternateDomains.$setPristine();
$('#appConfigureAlternateSubdomainInput').focus();
} else if (error.message.indexOf('dataDir') !== -1 ) { // can be 400 or 409
$scope.appConfigure.error.dataDir = error.message;
$scope.appConfigureForm.dataDir.$setPristine();
$('#appConfigureDataDirInput').focus();
} else {
$scope.appConfigure.error.other = error.message;
tab = 'general';
}
$scope.appConfigure.action = tab;
$scope.appConfigure.busy = false;
return;
}
@@ -261,19 +218,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
action: 'restore',
copyBackupIdDone: false,
copyBackupId: function () {
var copyText = document.getElementById('appRestoreSelectedBackupId');
copyText.select();
document.execCommand('copy');
$scope.appRestore.copyBackupIdDone = true;
// reset after 2.5sec
$timeout(function () { $scope.appRestore.copyBackupIdDone = false; }, 2500);
},
selectBackup: function (backup) {
$scope.appRestore.selectedBackup = backup;
},
@@ -302,18 +246,10 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
clone: function () {
$scope.appRestore.busy = true;
// only use enabled ports from portBindings
var finalPortBindings = {};
for (var env in $scope.appRestore.portBindings) {
if ($scope.appRestore.portBindingsEnabled[env]) {
finalPortBindings[env] = $scope.appRestore.portBindings[env];
}
}
var data = {
location: $scope.appRestore.location,
domain: $scope.appRestore.domain.domain,
portBindings: finalPortBindings,
portBindings: $scope.appRestore.portBindings,
backupId: $scope.appRestore.selectedBackup.id
};
@@ -463,7 +399,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appConfigure.ssoAuth = false;
$scope.appConfigure.robotsTxt = '';
$scope.appConfigure.enableBackup = true;
$scope.appConfigure.enableAutomaticUpdate = true;
$scope.appConfigureForm.$setPristine();
$scope.appConfigureForm.$setUntouched();
@@ -660,33 +595,23 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
Client.getBackupConfig(function (error, backupConfig) {
if (error) return console.error(error);
$scope.backupEnabled = backupConfig.provider !== 'noop';
$scope.backupConfig = backupConfig;
});
}
function refreshInstalledApps() {
Client.refreshInstalledApps();
var tmp = [];
$scope.installedApps.forEach(function (app) {
if (!tmp.find(function (d) { return d.domain === app.domain; })) tmp.push({ domain: app.domain, apps: [] });
tmp.find(function (d) { return d.domain === app.domain; }).apps.push(app);
});
$scope.usedDomains = tmp;
}
Client.onReady(function () {
refreshInstalledApps(); // refresh the new list immediately when switching from another view (appstore)
Client.refreshInstalledApps(); // refresh the new list immediately when switching from another view (appstore)
if ($scope.user.admin) {
$scope.spacesSuffix = $scope.user.username.replace(/\./g, '-');
if ($scope.user.admin || $scope.config.features.spaces) {
fetchUsers();
fetchGroups();
getDomains();
getBackupConfig();
if ($scope.user.admin && $scope.config.features.operatorActions) getBackupConfig(); // FIXME: detect disabled backups some other way
}
var refreshAppsTimer = $interval(refreshInstalledApps, 5000);
var refreshAppsTimer = $interval(Client.refreshInstalledApps.bind(Client), 5000);
$scope.$on('$destroy', function () {
$interval.cancel(refreshAppsTimer);
+32 -33
View File
@@ -23,7 +23,9 @@
<input type="text" class="form-control" ng-model="appInstall.location" id="appInstallLocationInput" name="location" placeholder="Leave empty to use bare domain" autofocus>
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span>{{ (appInstall.location ? (appInstall.domain.config.hyphenatedSubdomains ? '-' : '.') : '') + appInstall.domain.domain }}</span>
<!-- the admin check is to check for spaces user -->
<span ng-if="user.admin">{{ (appInstall.location ? (appInstall.domain.config.hyphenatedSubdomains ? '-' : '.') : '') + appInstall.domain.domain }}</span>
<span ng-if="!user.admin">{{ (appInstall.location ? '-' : '') + spacesSuffix + (appInstall.domain.config.hyphenatedSubdomains ? '-' : '.') + appInstall.domain.domain }}</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
@@ -35,7 +37,7 @@
</div>
</div>
<p class="text-center" ng-show="appInstall.location && appInstall.domain.provider === 'manual'">
<p class="text-center" ng-show="appInstall.location && appInstall.domain.provider === 'manual' && !appInstall.domain.config.wildcard">
<b>Add an A record manually for {{ appInstall.location }} to this Cloudron's public IP</b>
<br>
</p>
@@ -44,12 +46,7 @@
<div ng-repeat="(env, info) in appInstall.portBindingsInfo">
<ng-form name="portInfo_form">
<div class="form-group" ng-class="{ 'has-error': (!appInstallForm.itemName{{$index}}.$dirty && appInstall.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="appInstall.portBindingsEnabled[env]">
{{ info.title }}
<sup>
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})"><i class="fa fa-question-circle"></i></a>
</sup>
</label>
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="appInstall.portBindingsEnabled[env]"> {{ info.description }} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})</label>
<input type="number" class="form-control" ng-model="appInstall.portBindings[env]" ng-disabled="!appInstall.portBindingsEnabled[env]" id="inputPortInfo{{env}}" later-name="itemName{{$index}}" min="{{hostPortMin}}" max="{{hostPortMax}}" required>
</div>
</ng-form>
@@ -149,7 +146,8 @@
</div>
<div class="collapse" id="collapseResourceConstraint" data-toggle="false">
<h4 class="text-danger">This Cloudron is running low on resources.</h4>
<p>Please upgrade to a server instance with more memory. Alternately, free up resources by uninstalling unused applications.</p>
<p ng-show="config.provider === 'caas'">Please upgrade to a bigger plan. Alternately, free up resources by uninstalling unused applications.</p>
<p ng-hide="config.provider === 'caas'">Please upgrade to a server instance with more memory. Alternately, free up resources by uninstalling unused applications.</p>
</div>
<div class="collapse" id="collapseAppLimitReached" data-toggle="false">
<h4 class="text-danger">Subscription required</h4>
@@ -158,10 +156,11 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default"data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" ng-show="user.admin && appInstall.state === 'resourceConstraint'" ng-click="appInstall.showForm(true)">Install anyway</button>
<button type="button" class="btn btn-success" ng-show="config.provider === 'caas' && user.admin && appInstall.state === 'resourceConstraint'" ng-click="showView('/settings')">Upgrade Cloudron</button>
<button type="button" class="btn btn-danger" ng-show="config.provider !== 'caas' && user.admin && appInstall.state === 'resourceConstraint'" ng-click="appInstall.showForm(true)">Install anyway</button>
<button type="button" class="btn btn-success" ng-show="appInstall.state === 'appInfo'" ng-click="appInstall.showForm()">Install</button>
<button type="button" class="btn btn-success" ng-show="appInstall.state === 'installForm'" ng-click="appInstall.submit()" ng-disabled="appInstallForm.$invalid || appInstall.busy"><i class="fa fa-circle-notch fa-spin" ng-show="appInstall.busy"></i> Install</button>
<a class="btn btn-success" ng-show="appInstall.state === 'appLimitReached'" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + subscription.emailEncoded + '&cloudronId=' + subscription.cloudronId }}" target="_blank">Setup Subscription</a>
<button type="button" class="btn btn-success" ng-show="appInstall.state === 'installForm'" ng-click="appInstall.submit()" ng-disabled="appInstallForm.$invalid || appInstall.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="appInstall.busy"></i> Install</button>
<a class="btn btn-success" ng-show="appInstall.state === 'appLimitReached'" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.emailEncoded + '&cloudronId=' + appstoreConfig.cloudronId }}" target="_blank">Setup Subscription</a>
</div>
</div>
</div>
@@ -185,11 +184,11 @@
</div>
<div ng-show="!ready" class="loading-banner">
<h1><i class="fa fa-circle-notch fa-spin"></i></h1>
<h1><i class="fa fa-circle-o-notch fa-spin"></i></h1>
</div>
<!-- appstore login -->
<div ng-show="ready && !validSubscription" class="container card card-small appstore-login ng-cloak">
<div ng-show="ready && !validAppstoreAccount" class="container card card-small appstore-login ng-cloak">
<div class="col-md-12 text-center">
<h1 ng-show="appstoreLogin.register">Sign up with Cloudron.io</h1>
<h1 ng-hide="appstoreLogin.register">Login to Cloudron.io</h1>
@@ -240,7 +239,7 @@
<center>
<button type="submit" class="btn btn-lg btn-success" ng-disabled="appstoreLoginForm.$invalid || appstoreLogin.busy || !appstoreLogin.termsAccepted">
<i class="fa fa-circle-notch fa-spin" ng-show="appstoreLogin.busy"></i> <span ng-hide="appstoreLogin.register">Login</span><span ng-show="appstoreLogin.register">Create Account</span>
<i class="fa fa-circle-o-notch fa-spin" ng-show="appstoreLogin.busy"></i> <span ng-hide="appstoreLogin.register">Login</span><span ng-show="appstoreLogin.register">Create Account</span>
</button>
<br/>
@@ -255,12 +254,12 @@
</div>
<!-- give more vertical spacing so the login form does not appear clipped -->
<div ng-show="ready && !validSubscription">
<div ng-show="ready && !validAppstoreAccount">
<br/>
<br/>
</div>
<div ng-show="ready && validSubscription" class="ng-cloak" id="appstoreGrid">
<div ng-show="ready && validAppstoreAccount" class="ng-cloak" id="appstoreGrid">
<div class="col-md-2">
<br/>
<div>
@@ -274,23 +273,22 @@
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'featured' }" category="featured">Popular</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === '' }" category="">All</a>
<br/>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'analytics' }" category="analytics"><i class="fa fa-chart-line"></i> Analytics</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'analytics' }" category="analytics"><i class="fa fa-bar-chart"></i> Analytics</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'blog' }" category="blog"><i class="fa fa-font"></i> Blog</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'chat' }" category="chat"><i class="fa fa-comments"></i> Chat</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'git' }" category="git"><i class="fa fa-code-branch"></i> Code Hosting</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'CRM' }" category="crm"><i class="fab fa-connectdevelop"></i> CRM</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'document' }" category="document"><i class="fa fa-file-word"></i> Documents</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'email' }" category="email"><i class="fa fa-envelope"></i> Email</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'sync' }" category="sync"><i class="fa fa-sync-alt"></i> File Sync</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'finance' }" category="finance"><i class="fa fa-hand-holding-usd"></i> Finance</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'chat' }" category="chat"><i class="fa fa-comments-o"></i> Chat</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'git' }" category="git"><i class="fa fa-code-fork"></i> Code Hosting</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'CRM' }" category="crm"><i class="fa fa-connectdevelop"></i> CRM</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'document' }" category="document"><i class="fa fa-file-word-o"></i> Documents</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'email' }" category="email"><i class="fa fa-envelope-o"></i> Email</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'sync' }" category="sync"><i class="fa fa-refresh"></i> File Sync</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'finance' }" category="finance"><i class="fa fa-dollar"></i> Finance</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'forum' }" category="forum"><i class="fa fa-users"></i> Forum</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'gallery' }" category="gallery"><i class="fa fa-images"></i> Gallery</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'game' }" category="game"><i class="fa fa-gamepad"></i> Games</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'notes' }" category="notes"><i class="fa fa-sticky-note"></i> Notes</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'project' }" category="project"><i class="fas fa-project-diagram"></i> Project Management</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'gallery' }" category="gallery"><i class="fa fa-picture-o"></i> Gallery</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'notes' }" category="notes"><i class="fa fa-sticky-note-o"></i> Notes</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'project' }" category="project"><i class="fa fa-line-chart"></i> Project Management</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'vpn' }" category="vpn"><i class="fa fa-user-secret"></i> VPN</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'hosting' }" category="hosting"><i class="fa fa-server"></i> Web Hosting</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'wiki' }" category="wiki"><i class="fab fa-wikipedia-w"></i> Wiki</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'hosting' }" category="hosting"><i class="fa fa-bars"></i> Web Hosting</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'wiki' }" category="wiki"><i class="fa fa-wikipedia-w"></i> Wiki</a>
<br/>
<br/>
<a href="https://forum.cloudron.io/category/5/app-requests" target="_blank">Missing an app? Let us know.</a>
@@ -298,8 +296,9 @@
<div class="col-md-10" ng-show="apps.length">
<div class="row-no-margin">
<div class="col-sm-1 appstore-item" ng-repeat="app in apps | orderBy:'installCount':true">
<div class="appstore-item-content highlight" ng-click="gotoApp(app)" ng-class="{ 'appstore-item-content-testing': app.releaseState === 'unstable' }">
<span class="badge badge-danger appstore-item-badge-testing" ng-show="app.releaseState === 'unstable'">Unstable</span>
<div class="appstore-item-content highlight" ng-click="gotoApp(app)" ng-class="{ 'appstore-item-content-testing': (app.publishState === 'testing' || app.publishState === 'pending_approval') }">
<span class="badge badge-danger appstore-item-badge-testing" ng-show="app.publishState === 'testing'">Testing</span>
<span class="badge badge-warning appstore-item-badge-testing" ng-show="app.publishState === 'pending_approval'">Pending Approval</span>
<div class="appstore-item-content-icon col-same-height">
<img ng-src="{{app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
</div>
+180 -104
View File
@@ -1,10 +1,7 @@
'use strict';
/* global angular:false */
/* global $:false */
angular.module('Application').controller('AppStoreController', ['$scope', '$location', '$timeout', '$routeParams', 'Client', function ($scope, $location, $timeout, $routeParams, Client) {
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
angular.module('Application').controller('AppStoreController', ['$scope', '$location', '$timeout', '$routeParams', 'Client', 'AppStore', function ($scope, $location, $timeout, $routeParams, Client, AppStore) {
Client.onReady(function () { if (!Client.getUserInfo().admin && !Client.getConfig().features.spaces) $location.path('/'); });
$scope.HOST_PORT_MIN = 1024;
$scope.HOST_PORT_MAX = 65535;
@@ -19,9 +16,9 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$scope.category = '';
$scope.cachedCategory = ''; // used to cache the selected category while searching
$scope.searchString = '';
$scope.validSubscription = false;
$scope.unstableApps = false;
$scope.subscription = {};
$scope.validAppstoreAccount = false;
$scope.appstoreConfig = null;
$scope.spacesSuffix = '';
$scope.showView = function (view) {
// wait for dialog to be fully closed to avoid modal behavior breakage when moving to a different view already
@@ -180,10 +177,10 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$('#collapseResourceConstraint').collapse('hide');
$('#collapseInstallForm').collapse('hide');
$('#collapseAppLimitReached').collapse('show');
} else if (error.statusCode === 409 && (error.message.indexOf('Port') !== -1)) {
} else if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
$scope.appInstall.error.port = error.message;
} else if (error.statusCode === 409 && error.message.indexOf('subdomain') !== -1) {
$scope.appInstall.error.location = error.message;
} else if (error.statusCode === 409) {
$scope.appInstall.error.location = 'This name is already taken.';
$scope.appInstallForm.location.$setPristine();
$('#appInstallLocationInput').focus();
} else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) {
@@ -241,7 +238,70 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$scope.appstoreLogin.error = {};
$scope.appstoreLogin.busy = true;
Client.registerCloudron($scope.appstoreLogin.email, $scope.appstoreLogin.password, $scope.appstoreLogin.totpToken, $scope.appstoreLogin.register, function (error) {
function login() {
AppStore.login($scope.appstoreLogin.email, $scope.appstoreLogin.password, $scope.appstoreLogin.totpToken, function (error, result) {
if (error) {
$scope.appstoreLogin.busy = false;
if (error.statusCode === 401) {
if (error.message.indexOf('TOTP token missing') !== -1) {
$scope.appstoreLogin.error.totpToken = 'A 2FA token is required';
setTimeout(function () { $('#inputAppstoreLoginTotpToken').focus(); }, 0);
} else if (error.message.indexOf('TOTP token invalid') !== -1) {
$scope.appstoreLogin.error.totpToken = 'Wrong 2FA token';
$scope.appstoreLogin.totpToken = '';
setTimeout(function () { $('#inputAppstoreLoginTotpToken').focus(); }, 0);
} else {
$scope.appstoreLogin.error.password = 'Wrong email or password';
$scope.appstoreLogin.password = '';
$('#inputAppstoreLoginPassword').focus();
$scope.appstoreLoginForm.password.$setPristine();
}
} else {
console.error(error);
}
return;
}
var config = {
userId: result.userId,
token: result.accessToken
};
Client.setAppstoreConfig(config, function (error) {
if (error) {
$scope.appstoreLogin.busy = false;
if (error.statusCode === 406) {
if (error.message === 'wrong user') {
$scope.appstoreLogin.error.generic = 'Wrong cloudron.io account';
$scope.appstoreLogin.email = '';
$scope.appstoreLogin.password = '';
$scope.appstoreLoginForm.email.$setPristine();
$scope.appstoreLoginForm.password.$setPristine();
$('#inputAppstoreLoginEmail').focus();
} else {
console.error(error);
$scope.appstoreLogin.error.generic = 'Please retry later';
}
} else {
console.error(error);
}
return;
}
fetchAppstoreConfig(function (error) {
if (error) return console.error('Unable to fetch appstore config.', error);
});
});
});
}
if (!$scope.appstoreLogin.register) return login();
AppStore.register($scope.appstoreLogin.email, $scope.appstoreLogin.password, function (error) {
if (error) {
$scope.appstoreLogin.busy = false;
@@ -251,74 +311,29 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$scope.appstoreLoginForm.email.$setPristine();
$scope.appstoreLoginForm.password.$setPristine();
$('#inputAppstoreLoginEmail').focus();
} else if (error.statusCode === 412) {
if (error.message.indexOf('TOTP token missing') !== -1) {
$scope.appstoreLogin.error.totpToken = 'A 2FA token is required';
setTimeout(function () { $('#inputAppstoreLoginTotpToken').focus(); }, 0);
} else if (error.message.indexOf('TOTP token invalid') !== -1) {
$scope.appstoreLogin.error.totpToken = 'Wrong 2FA token';
$scope.appstoreLogin.totpToken = '';
setTimeout(function () { $('#inputAppstoreLoginTotpToken').focus(); }, 0);
} else {
$scope.appstoreLogin.error.password = 'Wrong email or password';
$scope.appstoreLogin.password = '';
$('#inputAppstoreLoginPassword').focus();
$scope.appstoreLoginForm.password.$setPristine();
}
} else if (error.statusCode === 424) {
if (error.message === 'wrong user') {
$scope.appstoreLogin.error.generic = 'Wrong cloudron.io account';
$scope.appstoreLogin.email = '';
$scope.appstoreLogin.password = '';
$scope.appstoreLoginForm.email.$setPristine();
$scope.appstoreLoginForm.password.$setPristine();
$('#inputAppstoreLoginEmail').focus();
} else {
console.error(error);
$scope.appstoreLogin.error.generic = error.message;
}
} else {
console.error(error);
$scope.appstoreLogin.error.generic = error.message || 'Please retry later';
$scope.appstoreLogin.error.generic = 'Please retry later';
}
return;
}
getSubscription(function (error) {
if (error) return console.error(error);
onSubscribed(function (error) { if (error) console.error(error); });
});
login();
});
}
};
function onSubscribed(callback) {
Client.getAppstoreApps(function (error) {
function getAppList(callback) {
AppStore.getApps(function (error, apps) {
if (error) return callback(error);
// start with featured apps listing. this also sets $scope.apps accordingly
$scope.showCategory(null, 'featured');
// do this in background
fetchUsers();
fetchGroups();
// domains is required since we populate the dropdown with domains[0]
Client.getDomains(function (error, result) {
if (error) return callback(error);
$scope.domains = result;
// show install app dialog immediately if an app id was passed in the query
// hashChangeListener calls $apply, so make sure we don't double digest here
setTimeout(hashChangeListener, 1);
setTimeout(function () { $('#appstoreSearch').focus(); }, 1000);
callback();
// ensure we have a tags property for further use
apps.forEach(function (app) {
if (!app.manifest.tags) app.manifest.tags = [];
});
return callback(null, apps);
});
}
@@ -328,7 +343,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$scope.category = '';
Client.getAppstoreAppsFast(function (error, apps) {
AppStore.getAppsFast(function (error, apps) {
if (error) return $timeout($scope.search, 1000);
var token = $scope.searchString.toUpperCase();
@@ -350,7 +365,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$scope.cachedCategory = $scope.category;
Client.getAppstoreAppsFast(function (error, apps) {
AppStore.getAppsFast(function (error, apps) {
if (error) return $timeout($scope.showCategory.bind(null, event), 1000);
if (!$scope.category) {
@@ -363,7 +378,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
});
}
if (document.getElementById('appstoreGrid')) document.getElementById('appstoreGrid').scrollIntoView();
document.getElementById('appstoreGrid').scrollIntoView();
});
};
@@ -413,15 +428,27 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
var version = $location.search().version;
if (appId) {
Client.getAppstoreAppByIdAndVersion(appId, version || 'latest', function (error, result) {
if (error) {
$scope.showAppNotFound(appId, version);
console.error(error);
return;
}
if (version) {
AppStore.getAppByIdAndVersion(appId, version, function (error, result) {
if (error) {
$scope.showAppNotFound(appId, version);
console.error(error);
return;
}
$scope.appInstall.show(result);
});
$scope.appInstall.show(result);
});
} else {
AppStore.getAppById(appId, function (error, result) {
if (error) {
$scope.showAppNotFound(appId, null);
console.error(error);
return;
}
$scope.appInstall.show(result);
});
}
} else {
$scope.appInstall.reset();
}
@@ -443,54 +470,103 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
Client.getGroups(function (error, groups) {
if (error) {
console.error(error);
return $timeout(fetchGroups, 5000);
return $timeout(fetchUsers, 5000);
}
$scope.groups = groups;
});
}
function getSubscription(callback) {
Client.getSubscription(function (error, subscription) {
if (error) {
if (error.statusCode === 412) { // not registered yet
$scope.validSubscription = false;
} else {
return callback(error);
}
} else {
$scope.validSubscription = true;
$scope.subscription = subscription;
}
function fetchAppstoreConfig(callback) {
callback = callback || function (error) { if (error) console.error(error); };
// clear busy state when a login/signup was performed
$scope.appstoreLogin.busy = false;
// caas always has a valid appstore account
if ($scope.config.provider === 'caas') {
$scope.validAppstoreAccount = true;
return callback();
}
// also update the root controller status
if ($scope.$parent) $scope.$parent.updateSubscriptionStatus();
// non admins cannot read appstore settings in spaces mode
if (!$scope.user.admin && $scope.config.features.spaces) {
$scope.validAppstoreAccount = true;
return callback();
}
callback();
if ($scope.user.admin && !$scope.config.features.operatorActions) {
$scope.validAppstoreAccount = true;
return callback();
}
Client.getAppstoreConfig(function (error, result) {
if (error) return callback(error);
if (!result.token || !result.cloudronId) return callback();
var appstoreConfig = result;
AppStore.getCloudronDetails(appstoreConfig, function (error) {
if (error) return callback(error);
AppStore.getProfile(appstoreConfig.token, function (error, result) {
if (error) return console.error(error);
// assign late to avoid UI flicketing on update
appstoreConfig.profile = result;
$scope.appstoreConfig = appstoreConfig;
$scope.validAppstoreAccount = true;
// clear busy state when a login/signup was performed
$scope.appstoreLogin.busy = false;
// also update the root controller status
if ($scope.$parent) {
$scope.$parent.fetchAppstoreProfileAndSubscription(function (error) {
if (error) console.error(error);
});
}
callback();
});
});
});
}
function init() {
$scope.ready = false;
getSubscription(function (error) {
$scope.spacesSuffix = $scope.user.username.replace(/\./g, '-');
getAppList(function (error, apps) {
if (error) {
console.error(error);
return $timeout(init, 1000);
}
if (!$scope.validSubscription) { // show the login form
$scope.ready = true;
return;
}
$scope.apps = apps;
onSubscribed(function (error) {
fetchUsers();
fetchGroups();
// start with featured apps listing
$scope.showCategory(null, 'featured');
// domains is required since we populate the dropdown with domains[0]
Client.getDomains(function (error, result) {
if (error) console.error(error);
$scope.ready = true;
$scope.domains = result;
// show install app dialog immediately if an app id was passed in the query
// hashChangeListener calls $apply, so make sure we don't double digest here
setTimeout(hashChangeListener, 1);
fetchAppstoreConfig(function (error) {
if (error) console.error(error);
$scope.ready = true;
setTimeout(function () { $('#appstoreSearch').focus(); }, 1000);
});
});
});
}
@@ -514,12 +590,12 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
// setup all the dialog focus handling
['appInstallModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find('[autofocus]:first').focus();
$(this).find("[autofocus]:first").focus();
});
});
// autofocus if appstore login is shown
$scope.$watch('validSubscription', function (newValue/*, oldValue */) {
$scope.$watch('validAppstoreAccount', function (newValue, oldValue) {
if (!newValue) setTimeout(function () { $('[name=appstoreLoginForm]').find('[autofocus]:first').focus(); }, 1000);
});
+25 -29
View File
@@ -30,7 +30,7 @@
<p class="has-error text-center" ng-show="configureBackup.error">{{ configureBackup.error.generic }}</p>
<div class="form-group">
<label class="control-label" for="storageProviderProvider">Storage provider <sup><a ng-href="https://cloudron.io/documentation/backups/#storage-provider" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<label class="control-label" for="storageProviderProvider">Storage provider <sup><a ng-href="{{ config.webServerOrigin }}/documentation/backups/" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<select class="form-control" id="storageProviderProvider" ng-model="configureBackup.provider" ng-options="a.value as a.name for a in storageProvider" ng-change=configureBackup.clearForm()></select>
</div>
@@ -91,16 +91,6 @@
<select class="form-control" name="region" id="inputConfigureBackupDORegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in doSpacesRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'digitalocean-spaces'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'exoscale-sos'">
<label class="control-label" for="inputConfigureBackupExoscaleRegion">Region</label>
<select class="form-control" name="region" id="inputConfigureBackupExoscaleRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in exoscaleSosRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'exoscale-sos'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'scaleway-objectstorage'">
<label class="control-label" for="inputConfigureBackupScalewayRegion">Region</label>
<select class="form-control" name="region" id="inputConfigureBackupScalewayRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in scalewayRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'scaleway-objectstorage'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.accessKeyId }" ng-show="s3like(configureBackup.provider)">
<label class="control-label" for="inputConfigureBackupAccessKeyId">Access key id</label>
<input type="text" class="form-control" ng-model="configureBackup.accessKeyId" id="inputConfigureBackupAccessKeyId" name="accessKeyId" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
@@ -124,12 +114,12 @@
</div>
<div class="form-group" ng-show="configureBackup.provider !== 'noop'">
<label class="control-label" for="storageFormat">Storage Format <sup><a ng-href="https://cloudron.io/documentation/backups/#backup-formats" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<label class="control-label" for="storageFormat">Storage Format</label>
<select class="form-control" id="storageFormat" ng-change="configureBackup.key = ''" ng-model="configureBackup.format" ng-options="a.value as a.name for a in formats"></select>
</div>
<div class="form-group" ng-show="configureBackup.provider !== 'noop'">
<label class="control-label" for="storageInterval">Backup Interval <sup><a ng-href="https://cloudron.io/documentation/backups/#backup-interval" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<label class="control-label" for="storageInterval">Backup Interval</label>
<select class="form-control" id="storageInterval" ng-model="configureBackup.intervalSecs" ng-options="a.value as a.name for a in intervalTimes"></select>
</div>
@@ -139,8 +129,7 @@
</div>
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.key }" ng-show="configureBackup.provider !== 'noop'">
<label class="control-label" for="inputConfigureBackupKey">Encryption key (optional) <sup><a ng-href="https://cloudron.io/documentation/backups/#encrypting-backups" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<p class="small text-info">Save this passphrase in a safe place. Backups cannot be decrypted without the passphrase</p>
<label class="control-label" for="inputConfigureBackupKey">Encryption key (optional)</label>
<input type="text" class="form-control" ng-model="configureBackup.key" id="inputConfigureBackupKey" name="prefix" ng-disabled="configureBackup.busy" placeholder="Passphrase used to encrypt the backups">
</div>
@@ -150,7 +139,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="configureBackup.submit()" ng-disabled="configureBackupForm.$invalid || configureBackup.busy"><i class="fa fa-circle-notch fa-spin" ng-show="configureBackup.busy"></i><span>Save</span></button>
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="configureBackup.submit()" ng-disabled="configureBackupForm.$invalid || configureBackup.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="configureBackup.busy"></i><span>Save</span></button>
</div>
</div>
</div>
@@ -171,18 +160,18 @@
<span>{{ prettyProviderName(backupConfig.provider) }}</span>
</div>
</div>
<div class="row" ng-show="config.features.configureBackup">
<div class="row" ng-show="backupConfig.provider !== 'caas' && config.features.operatorActions">
<div class="col-xs-6">
<span class="text-muted">Location</span>
</div>
<div class="col-xs-6 text-right">
<span ng-show="backupConfig.provider === 'filesystem'">{{ backupConfig.backupFolder }}</span>
<span ng-show="backupConfig.provider === 'scaleway-objectstorage' || backupConfig.provider === 'minio' || backupConfig.provider === 'exoscale-sos' || backupConfig.provider === 's3-v4-compat' || backupConfig.provider === 'digitalocean-spaces' || backupConfig.provider === 'gcs'">{{ backupConfig.bucket + '/' + backupConfig.prefix }}</span>
<span ng-show="backupConfig.provider === 'minio' || backupConfig.provider === 'exoscale-sos' || backupConfig.provider === 's3-v4-compat' || backupConfig.provider === 'digitalocean-spaces' || backupConfig.provider === 'gcs'">{{ backupConfig.bucket + '/' + backupConfig.prefix }}</span>
<span ng-show="backupConfig.provider === 's3'">{{ backupConfig.region + ' ' + backupConfig.bucket + '/' + backupConfig.prefix }}</span>
</div>
</div>
<div class="row" ng-show="config.features.configureBackup">
<div class="row" ng-show="backupConfig.provider !== 'caas' && config.features.operatorActions">
<div class="col-xs-6">
<span class="text-muted">Storage Format</span>
</div>
@@ -215,7 +204,7 @@
<div class="row">
<br/>
<div class="col-md-12" style="margin-bottom: 10px;">
<div class="col-md-12">
<div ng-show="createBackup.busy" class="progress progress-striped active animateMe">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ createBackup.percent }}%"></div>
</div>
@@ -223,16 +212,23 @@
</div>
<div class="row">
<div class="col-md-8">
<p ng-show="createBackup.busy">{{ createBackup.message }}</p>
<p ng-hide="createBackup.busy">
<div class="has-error" ng-show="!createBackup.active">{{ createBackup.errorMessage }}</div>
<div class="col-md-11" ng-show="createBackup.busy">
<p class="text-muted status" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
{{ createBackup.detail || 'Syncing ...' }}
</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" ng-show="config.features.configureBackup">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>
<div class="row">
<div class="col-md-6">
<p ng-show="createBackup.busy">{{ createBackup.message }}</p>
<p ng-hide="createBackup.busy">
<div class="has-error" ng-show="createBackup.percent === 100 && createBackup.result">{{ createBackup.result }}</div>
</p>
</div>
<div class="col-md-6 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="configureBackup.show()" ng-disabled="createBackup.busy" ng-show="backupConfig.provider !== 'caas' && user.admin && config.features.operatorActions">Configure</button>
<button class="btn btn-outline btn-primary" ng-click="createBackup.doCreateBackup()" ng-disabled="createBackup.busy" style="margin-right: 10px">Backup now</button>
</div>
</div>
</div>
@@ -247,7 +243,7 @@
<p>
Please be careful when uploading these logs to a public server since they may contain sensitive information.
</p>
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{createBackup.taskId}}" ng-enabled="createBackup.taskId" target="_blank">Show Logs</a>
<a class="btn btn-primary pull-right" href="/logs.html?id=backup" target="_blank">Show Logs</a>
</div>
</div>
</div>
+26 -70
View File
@@ -1,9 +1,6 @@
'use strict';
/* global angular:false */
/* global $:false */
angular.module('Application').controller('BackupsController', ['$scope', '$location', '$rootScope', '$timeout', 'Client', function ($scope, $location, $rootScope, $timeout, Client) {
angular.module('Application').controller('BackupsController', ['$scope', '$location', '$rootScope', '$timeout', 'Client', 'AppStore', function ($scope, $location, $rootScope, $timeout, Client, AppStore) {
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
$scope.config = Client.getConfig();
@@ -16,7 +13,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
// List is from http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
$scope.s3Regions = [
{ name: 'Asia Pacific (Mumbai)', value: 'ap-south-1' },
{ name: 'Asia Pacific (Osaka-Local)', value: 'ap-northeast-3' },
{ name: 'Asia Pacific (Seoul)', value: 'ap-northeast-2' },
{ name: 'Asia Pacific (Singapore)', value: 'ap-southeast-1' },
{ name: 'Asia Pacific (Sydney)', value: 'ap-southeast-2' },
@@ -25,8 +21,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
{ name: 'EU (Frankfurt)', value: 'eu-central-1' },
{ name: 'EU (Ireland)', value: 'eu-west-1' },
{ name: 'EU (London)', value: 'eu-west-2' },
{ name: 'EU (Paris)', value: 'eu-west-3' },
{ name: 'EU (Stockholm)', value: 'eu-north-1' },
{ name: 'South America (São Paulo)', value: 'sa-east-1' },
{ name: 'US East (N. Virginia)', value: 'us-east-1' },
{ name: 'US East (Ohio)', value: 'us-east-2' },
@@ -37,22 +31,9 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
$scope.doSpacesRegions = [
{ name: 'AMS3', value: 'https://ams3.digitaloceanspaces.com' },
{ name: 'NYC3', value: 'https://nyc3.digitaloceanspaces.com' },
{ name: 'SFO2', value: 'https://sfo2.digitaloceanspaces.com' },
{ name: 'SGP1', value: 'https://sgp1.digitaloceanspaces.com' }
];
$scope.exoscaleSosRegions = [
{ name: 'CH-DK-2', value: 'https://sos-ch-dk-2.exo.io' }, // default
{ name: 'DE-FRA-1', value: 'https://sos-de-fra-1.exo.io' },
{ name: 'AT-VIE-1', value: 'https://sos-at-vie-1.exo.io' }
];
// https://www.scaleway.com/docs/object-storage-feature/
$scope.scalewayRegions = [
{ name: 'FR-PAR', value: 'https://s3.fr-par.scw.cloud', region: 'fr-par' }, // default
{ name: 'NL-AMS', value: 'https://s3.nl-ams.scw.cloud', region: 'nl-ams' }
];
$scope.storageProvider = [
{ name: 'Amazon S3', value: 's3' },
{ name: 'DigitalOcean Spaces', value: 'digitalocean-spaces' },
@@ -60,9 +41,8 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
{ name: 'Filesystem', value: 'filesystem' },
{ name: 'Google Cloud Storage', value: 'gcs' },
{ name: 'Minio', value: 'minio' },
{ name: 'Scaleway Object Storage', value: 'scaleway-objectstorage' },
{ name: 'No-op (Only for testing)', value: 'noop' },
{ name: 'S3 API Compatible (v4)', value: 's3-v4-compat' }
{ name: 'S3 API Compatible (v4)', value: 's3-v4-compat' },
];
$scope.retentionTimes = [
@@ -86,6 +66,8 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
];
$scope.prettyProviderName = function (provider) {
if (!$scope.config.features.operatorActions) return $scope.config.provider;
switch (provider) {
case 'caas': return 'Managed Cloudron';
default: return provider;
@@ -97,46 +79,42 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
percent: 0,
message: '',
errorMessage: '',
taskId: '',
checkStatus: function () {
Client.getLatestTaskByType('backup', function (error, task) {
if (error) return console.error(error);
if (!task) return;
$scope.createBackup.taskId = task.id;
$scope.createBackup.updateStatus();
});
},
result: '',
updateStatus: function () {
Client.getTask($scope.createBackup.taskId, function (error, data) {
if (error) return window.setTimeout($scope.createBackup.updateStatus, 5000);
Client.progress(function (error, data) {
if (error) return window.setTimeout($scope.createBackup.updateStatus, 250);
// check if we are done
if (!data.backup || data.backup.percent >= 100) {
if (data.backup && data.backup.message) console.error('Backup message: ' + data.backup.message); // backup error message
if (!data.active) {
$scope.createBackup.busy = false;
$scope.createBackup.message = '';
$scope.createBackup.detail = '';
$scope.createBackup.percent = 100; // indicates that 'result' is valid
$scope.createBackup.errorMessage = data.errorMessage;
$scope.createBackup.result = data.backup ? data.backup.message : null;
return fetchBackups();
}
$scope.createBackup.busy = true;
$scope.createBackup.percent = data.percent;
$scope.createBackup.message = data.message;
window.setTimeout($scope.createBackup.updateStatus, 3000);
$scope.createBackup.percent = data.backup.percent;
$scope.createBackup.message = data.backup.message;
$scope.createBackup.detail = data.backup.detail;
window.setTimeout($scope.createBackup.updateStatus, 500);
});
},
startBackup: function () {
doCreateBackup: function () {
$scope.createBackup.busy = true;
$scope.createBackup.percent = 0;
$scope.createBackup.message = '';
$scope.createBackup.detail = '';
$scope.createBackup.result = '';
$scope.createBackup.errorMessage = '';
Client.startBackup(function (error, taskId) {
Client.backup(function (error) {
if (error) {
if (error.statusCode === 409 && error.message.indexOf('full_backup') !== -1) {
$scope.createBackup.errorMessage = 'Backup already in progress. Please retry later.';
@@ -153,33 +131,13 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
return;
}
$scope.createBackup.taskId = taskId;
$scope.createBackup.updateStatus();
});
},
stopBackup: function () {
Client.stopTask($scope.createBackup.taskId, function (error) {
if (error) {
if (error.statusCode === 409) {
$scope.createBackup.errorMessage = 'No backup is currently in progress';
} else {
console.error(error);
$scope.createBackup.errorMessage = error.message;
}
$scope.createBackup.busy = false;
return;
}
});
}
};
$scope.s3like = function (provider) {
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat'
|| provider === 'exoscale-sos' || provider === 'digitalocean-spaces'
|| provider === 'scaleway-objectstorage';
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' || provider === 'exoscale-sos' || provider === 'digitalocean-spaces';
};
$scope.configureBackup = {
@@ -279,11 +237,9 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
backupConfig.region = 'us-east-1';
backupConfig.acceptSelfSignedCerts = $scope.configureBackup.acceptSelfSignedCerts;
} else if (backupConfig.provider === 'exoscale-sos') {
backupConfig.endpoint = 'https://sos-ch-dk-2.exo.io';
backupConfig.region = 'us-east-1';
backupConfig.signatureVersion = 'v4';
} else if (backupConfig.provider === 'scaleway-objectstorage') {
backupConfig.region = $scope.scalewayRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
} else if (backupConfig.provider === 'digitalocean-spaces') {
backupConfig.region = 'us-east-1';
}
@@ -317,7 +273,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
$scope.configureBackup.busy = false;
if (error) {
if (error.statusCode === 424) {
if (error.statusCode === 402) {
$scope.configureBackup.error.generic = error.message;
if (error.message.indexOf('AWS Access Key Id') !== -1) {
@@ -393,10 +349,10 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
Client.onReady(function () {
fetchBackups();
getBackupConfig();
if ($scope.config.features.operatorActions) getBackupConfig();
// show backup status
$scope.createBackup.checkStatus();
$scope.createBackup.updateStatus();
});
function readFileLocally(obj, file, fileName) {
+66 -190
View File
@@ -1,4 +1,3 @@
<!-- modal domain add/configure -->
<div class="modal fade" id="domainConfigureModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
@@ -17,8 +16,8 @@
</div>
<div class="form-group">
<label class="control-label">DNS Provider <sup><a href="https://cloudron.io/documentation/domains/#dns-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<select class="form-control" ng-model="domainConfigure.provider" ng-options="a.value as a.name for a in dnsProvider" ng-change="domainConfigure.setDefaultTlsProvider()"></select>
<label class="control-label">DNS API provider</label>
<select class="form-control" ng-model="domainConfigure.provider" ng-options="a.value as a.name for a in dnsProvider"></select>
</div>
<!-- Route53 -->
@@ -57,7 +56,7 @@
<!-- GoDaddy -->
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'godaddy'">
<label class="control-label">API Key</label>
<label class="control-label">API Key <sup><a href="https://developer.godaddy.com/keys" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<input type="text" class="form-control" ng-model="domainConfigure.godaddyApiKey" name="apiKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'godaddy'">
</div>
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'godaddy'">
@@ -81,37 +80,22 @@
<input type="text" class="form-control" ng-model="domainConfigure.nameComUsername" name="nameComUsername" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'namecom'">
</div>
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecom'">
<label class="control-label">API Token</label>
<label class="control-label">API Token <sup><a href="https://www.name.com/account/settings/api" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<input type="text" class="form-control" ng-model="domainConfigure.nameComToken" name="nameComToken" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'namecom'">
</div>
<!-- Namecheap -->
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecheap'">
<label class="control-label">Namecheap Username</label>
<input type="text" class="form-control" ng-model="domainConfigure.namecheapUsername" name="namecheapUsername" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'namecheap'">
</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'">
<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-info" ng-show="domainConfigure.provider === 'wildcard'">
<p 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>
<p class="small text-info" ng-show="domainConfigure.provider === 'manual'">
<p ng-show="domainConfigure.provider === 'manual'">
<b>All DNS records have to be setup manually before each app installation.</b>
</p>
<p class="small text-info" ng-show="needsPort80(domainConfigure.provider, domainConfigure.tlsConfig.provider)">Let's Encrypt requires your server to be reachable on port 80</p>
<a href="" ng-click="domainConfigure.advancedVisible = true" ng-hide="domainConfigure.advancedVisible">Advanced settings...</a>
<div uib-collapse="!domainConfigure.advancedVisible">
<div ng-show="false">
<div ng-show="config.features.hyphenatedSubdomains">
<label class="control-label">
<input type="checkbox" ng-model="domainConfigure.hyphenatedSubdomains" name="hyphenatedSubdomains" ng-disabled="domainConfigure.busy"/>&nbsp; Hyphenate Subdomains
</label>
@@ -119,12 +103,12 @@
</div>
<div class="form-group">
<label class="control-label">Zone Name (Optional) <sup><a href="https://cloudron.io/documentation/domains/#zone-name" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<label class="control-label">Zone Name (Optional)</label>
<input type="text" class="form-control" ng-model="domainConfigure.zoneName" name="zoneName" ng-disabled="domainConfigure.busy">
</div>
<div class="form-group">
<label class="control-label">Certificate Provider <sup><a href="https://cloudron.io/documentation/certificates/#certificate-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<label class="control-label">Certificate Provider</label>
<select class="form-control" ng-model="domainConfigure.tlsConfig.provider" ng-options="a.value as a.name for a in tlsProvider"></select>
</div>
@@ -172,7 +156,7 @@
<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="domainConfigure.submit()" ng-disabled="domainConfigureForm.$invalid || domainConfigure.busy">
<i class="fa fa-circle-notch fa-spin" ng-show="domainConfigure.busy"></i> Save
<i class="fa fa-circle-o-notch fa-spin" ng-show="domainConfigure.busy"></i> Save
</button>
</div>
</div>
@@ -181,36 +165,36 @@
<!-- Modal domain migrate -->
<div class="modal fade" id="domainMigrateModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Migrate to {{ domainMigrate.domain.domain }} ?</h4>
</div>
<div class="modal-body">
<p>Moving to a custom domain will retain all your apps and data and will take around 15 minutes.</p>
<br/>
<fieldset>
<form role="form" name="domainMigrateForm" ng-submit="domainMigrate.submit()" autocomplete="off">
<div class="form-group" ng-class="{ 'has-error': (domainMigrateForm.password.$dirty && domainMigrateForm.password.$invalid) || (!domainMigrateForm.password.$dirty && domainMigrate.error) }">
<label class="control-label">Provide your password to confirm this action</label>
<div class="control-label" ng-show="(domainMigrateForm.password.$dirty && domainMigrateForm.password.$invalid) || (!domainMigrateForm.password.$dirty && domainMigrate.error)">
<small ng-show=" domainMigrateForm.password.$dirty && domainMigrateForm.password.$invalid">Password required</small>
<small ng-show="!domainMigrateForm.password.$dirty && domainMigrate.error">{{ domainMigrate.error }}</small>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Migrate to {{ domainMigrate.domain.domain }} ?</h4>
</div>
<div class="modal-body">
<p>Moving to a custom domain will retain all your apps and data and will take around 15 minutes.</p>
<br/>
<fieldset>
<form role="form" name="domainMigrateForm" ng-submit="domainMigrate.submit()" autocomplete="off">
<div class="form-group" ng-class="{ 'has-error': (domainMigrateForm.password.$dirty && domainMigrateForm.password.$invalid) || (!domainMigrateForm.password.$dirty && domainMigrate.error) }">
<label class="control-label">Provide your password to confirm this action</label>
<div class="control-label" ng-show="(domainMigrateForm.password.$dirty && domainMigrateForm.password.$invalid) || (!domainMigrateForm.password.$dirty && domainMigrate.error)">
<small ng-show=" domainMigrateForm.password.$dirty && domainMigrateForm.password.$invalid">Password required</small>
<small ng-show="!domainMigrateForm.password.$dirty && domainMigrate.error">{{ domainMigrate.error }}</small>
</div>
<input type="password" class="form-control" ng-model="domainMigrate.password" id="domainMigratePasswordInput" name="password" required autofocus>
</div>
<input type="password" class="form-control" ng-model="domainMigrate.password" id="domainMigratePasswordInput" name="password" required autofocus>
</div>
<input class="ng-hide" type="submit" ng-disabled="domainMigrateForm.$invalid || busy"/>
</form>
</fieldset>
</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="domainMigrate.submit()" ng-disabled="domainMigrateForm.$invalid || domainMigrate.busy"><i class="fa fa-circle-notch fa-spin" ng-show="domainMigrate.busy"></i> Migrate</button>
<input class="ng-hide" type="submit" ng-disabled="domainMigrateForm.$invalid || busy"/>
</form>
</fieldset>
</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="domainMigrate.submit()" ng-disabled="domainMigrateForm.$invalid || domainMigrate.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="domainMigrate.busy"></i> Migrate</button>
</div>
</div>
</div>
</div>
</div>
<!-- Modal domain remove -->
<div class="modal fade" id="domainRemoveModal" tabindex="-1" role="dialog">
@@ -237,7 +221,7 @@
</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="domainRemove.submit()" ng-disabled="domainRemoveForm.$invalid || domainRemove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="domainRemove.busy"></i> Remove</button>
<button type="button" class="btn btn-danger" ng-click="domainRemove.submit()" ng-disabled="domainRemoveForm.$invalid || domainRemove.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="domainRemove.busy"></i> Remove</button>
</div>
</div>
</div>
@@ -249,148 +233,40 @@
</div>
<div class="card card-large">
<div class="row ng-hide" ng-show="!ready">
<div class="col-lg-12 text-center">
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
</div>
</div>
<div class="row animateMeOpacity ng-hide" ng-show="ready">
<div class="col-lg-12">
<table class="table table-hover" style="margin-top: 10px;">
<thead>
<tr>
<th style="width: 10px"></th>
<th>Domain</th>
<th class="text-left hidden-xs hidden-sm">Provider</th>
<th style="width: 100px" class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="domain in domains">
<td>
<i class="fa fa-lock" ng-show="domain.locked" uib-tooltip="This domain is locked and cannot be edited"></i>
</td>
<td class="elide-table-cell hand" ng-click="domain.provider !== 'caas' && !domain.locked && domainConfigure.show(domain)">
{{ domain.domain }}
</td>
<td class="text-left elide-table-cell hidden-xs hidden-sm hand" ng-click="domain.provider !== 'caas' && !domain.locked && domainConfigure.show(domain)">
{{ prettyProviderName(domain) }}
</td>
<td class="text-right no-wrap" style="vertical-align: bottom">
<button class="btn btn-xs btn-default" ng-click="domainMigrate.show(domain)" ng-show="domain.domain !== config.adminDomain && domain.provider !== 'caas' && provider === 'caas'" title="Migrate Domain"><i class="fa fa-exchange"></i></button>
<button class="btn btn-xs btn-default" ng-click="domainConfigure.show(domain)" ng-show="domain.provider !== 'caas' && !domain.locked" title="Edit Domain"><i class="fa fa-pencil-alt"></i></button>
<button class="btn btn-xs btn-danger" ng-click="domainRemove.show(domain)" ng-show="domain.provider !== 'caas' && !domain.locked" title="Remove Domain"><i class="far fa-trash-alt"></i></button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="text-left">
<h3>Renew certificates</h3>
</div>
<div class="card">
<div class="row">
<div class="col-md-12">
<p>
Cloudron renews Let's Encrypt certificates automatically. Use this option to trigger a renewal immediately.
</p>
</div>
</div>
<div class="row">
<div class="col-md-12" style="margin-bottom: 10px;">
<div ng-show="renewCerts.busy" class="progress progress-striped active animateMe">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ renewCerts.percent }}%"></div>
<div class="grid-item-top">
<div class="row ng-hide" ng-show="!ready">
<div class="col-lg-12 text-center">
<h2><i class="fa fa-circle-o-notch fa-spin"></i></h2>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<p ng-show="renewCerts.busy">{{ renewCerts.message }}</p>
<p ng-hide="renewCerts.busy">
<div class="has-error" ng-show="!renewCerts.active">{{ renewCerts.errorMessage }}</div>
</p>
</div>
<div class="col-md-6 text-right">
<button class="btn btn-outline btn-primary" ng-click="renewCerts.renew()" ng-disabled="renewCerts.busy" style="margin-right: 10px">Renew All Certs</button>
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{renewCerts.taskId}}" ng-enabled="renewCerts.taskId" target="_blank">Show Logs</a>
</div>
</div>
</div>
<div class="text-left" ng-show="domains.length > 1">
<h3>Change Dashboard Domain</h3>
</div>
<div class="card" ng-show="domains.length > 1">
<div class="row">
<div class="col-md-8">
<p>
This will move the dashboard to the <code>my</code>subdomain of the selected domain. Email server will be reconfigured to
send notifications from this domain.
</p>
</div>
<div class="col-md-4">
<select class="form-control pull-right" style="display: inline-block; width: 200px;" ng-model="changeDashboard.selectedDomain" ng-options="a.domain for a in domains"></select>
</div>
</div>
<div class="row">
<div class="col-md-12" style="margin-bottom: 10px;">
<div ng-show="changeDashboard.busy" class="progress progress-striped active animateMe">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ changeDashboard.percent }}%"></div>
<div class="row animateMeOpacity ng-hide" ng-show="ready">
<div class="col-lg-12">
<table class="table table-hover">
<thead>
<tr>
<th>Domain</th>
<th class="text-left hidden-xs hidden-sm">Provider</th>
<th style="width: 100px" class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="domain in domains">
<td class="elide-table-cell hand" ng-click="domain.provider !== 'caas' && !domain.locked && domainConfigure.show(domain)">
{{ domain.domain }}
</td>
<td class="text-left elide-table-cell hidden-xs hidden-sm hand" ng-click="domain.provider !== 'caas' && !domain.locked && domainConfigure.show(domain)">
{{ prettyProviderName(domain) }}
</td>
<td class="text-right no-wrap" style="vertical-align: bottom">
<button class="btn btn-xs btn-default" ng-click="domainMigrate.show(domain)" ng-show="domain.domain !== config.adminDomain && domain.provider !== 'caas' && provider === 'caas'" title="Migrate Domain"><i class="fa fa-exchange"></i></button>
<button class="btn btn-xs btn-default" ng-click="domainConfigure.show(domain)" ng-show="domain.provider !== 'caas' && !domain.locked" title="Edit Domain"><i class="fa fa-pencil"></i></button>
<button class="btn btn-xs btn-danger" ng-click="domainRemove.show(domain)" ng-show="domain.provider !== 'caas' && !domain.locked" title="Remove Domain"><i class="fa fa-trash-o"></i></button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<p ng-show="changeDashboard.busy">{{ changeDashboard.message }}</p>
<p ng-hide="changeDashboard.busy">
<div class="has-error" ng-show="!changeDashboard.active">{{ changeDashboard.errorMessage }}</div>
</p>
</div>
<div class="col-md-6 text-right">
<button class="btn btn-outline btn-primary" ng-click="changeDashboard.change()" ng-hide="changeDashboard.busy" ng-disabled="changeDashboard.selectedDomain.domain === changeDashboard.adminDomain.domain">Change Domain</button>
<button class="btn btn-outline btn-danger" ng-click="changeDashboard.stop()" ng-show="changeDashboard.busy" style="margin-right: 10px">Cancel</button>
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{changeDashboard.taskId}}" ng-show="changeDashboard.busy" target="_blank">Show Logs</a>
</div>
</div>
</div>
<div class="text-left" ng-show="config.features.dynamicDns">
<h3>Dynamic DNS</h3>
</div>
<div class="card" ng-show="config.features.dynamicDns">
<div class="row">
<div class="col-md-12">
<p>
Enable this option to keep all your DNS records in sync with a changing IP address.
This is useful when Cloudron runs in a network with a frequently changing public IP address like a home connection.
</p>
<p class="text-danger" ng-show="dyndnsConfigure.error"><br/>{{ dyndnsConfigure.error }}</p>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="dyndnsConfigure.enabled" name="dynamicDns" ng-disabled="dyndnsConfigure.busy"/>&nbsp; Use Dynamic DNS
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<span class="text-success text-bold" ng-show="dyndnsConfigure.success">Saved</span>
</div>
<div class="col-md-6 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="dyndnsConfigure.submit()" ng-disabled="dyndnsConfigure.currentState === dyndnsConfigure.enabled"><i class="fa fa-circle-notch fa-spin" ng-show="dyndnsConfigure.busy"></i> Save</button>
</div>
</div>
</div>
</div>
+32 -245
View File
@@ -1,34 +1,28 @@
'use strict';
/* global asyncForEach:false */
/* global angular:false */
/* global $:false */
angular.module('Application').controller('DomainsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
angular.module('Application').controller('DomainsController', ['$scope', '$location', 'Client', 'ngTld', function ($scope, $location, Client, ngTld) {
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
$scope.config = Client.getConfig();
$scope.domains = [];
$scope.ready = false;
// currently, validation of wildcard with various provider is done server side
$scope.tlsProvider = [
{ name: 'Let\'s Encrypt Prod', value: 'letsencrypt-prod' },
{ name: 'Let\'s Encrypt Prod - Wildcard', value: 'letsencrypt-prod-wildcard' },
{ name: 'Let\'s Encrypt Staging', value: 'letsencrypt-staging' },
{ name: 'Let\'s Encrypt Staging - Wildcard', value: 'letsencrypt-staging-wildcard' },
{ name: 'Custom Wildcard Certificate', value: 'fallback' },
];
// keep in sync with setupdns.js
$scope.dnsProvider = [
{ name: 'AWS Route53', value: 'route53' },
{ name: 'Cloudflare', value: 'cloudflare' },
{ name: 'DigitalOcean', value: 'digitalocean' },
{ name: 'Cloudflare (DNS only)', value: 'cloudflare' },
{ name: 'Digital Ocean', value: 'digitalocean' },
{ name: 'Gandi LiveDNS', value: 'gandi' },
{ name: 'GoDaddy', value: 'godaddy' },
{ name: 'Google Cloud DNS', value: 'gcdns' },
{ name: 'Name.com', value: 'namecom' },
{ name: 'Namecheap', value: 'namecheap' },
{ name: 'Wildcard', value: 'wildcard' },
{ name: 'Manual (not recommended)', value: 'manual' },
{ name: 'No-op (only for development)', value: 'noop' }
@@ -38,25 +32,18 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
switch (domain.provider) {
case 'caas': return 'Managed Cloudron';
case 'route53': return 'AWS Route53';
case 'cloudflare': return 'Cloudflare';
case 'digitalocean': return 'DigitalOcean';
case 'cloudflare': return 'Cloudflare (DNS only)';
case 'digitalocean': return 'Digital Ocean';
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 'manual': return domain.config.wildcard ? 'Wildcard' : 'Manual';
case 'noop': return 'No-op';
default: return 'Unknown';
}
};
$scope.needsPort80 = function (dnsProvider, tlsProvider) {
return ((dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') &&
(tlsProvider === 'letsencrypt-prod' || tlsProvider === 'letsencrypt-staging'));
};
function readFileLocally(obj, file, fileName) {
return function (event) {
$scope.$apply(function () {
@@ -73,7 +60,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
};
}
function refreshDomains(callback) {
function getDomains(callback) {
var domains = [ ];
Client.getDomains(function (error, results) {
@@ -93,12 +80,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
iteratorDone();
});
}, function (error) {
angular.copy(domains, $scope.domains);
$scope.changeDashboard.selectedDomain = $scope.changeDashboard.adminDomain = $scope.domains.find(function (d) { return d.domain === $scope.config.adminDomain; });
if (error) console.error(error);
if (callback) callback(error);
callback(error, domains);
});
});
}
@@ -124,14 +106,12 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
cloudflareEmail: '',
nameComToken: '',
nameComUsername: '',
namecheapUsername: '',
namecheapApiKey: '',
provider: 'route53',
zoneName: '',
hyphenatedSubdomains: false,
tlsConfig: {
provider: 'letsencrypt-prod-wildcard'
provider: 'letsencrypt-prod'
},
fallbackCert: {
@@ -141,16 +121,6 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
keyFileName: ''
},
setDefaultTlsProvider: function () {
var dnsProvider = $scope.domainConfigure.provider;
// wildcard LE won't work without automated DNS
if (dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') {
$scope.domainConfigure.tlsConfig.provider = 'letsencrypt-prod';
} else {
$scope.domainConfigure.tlsConfig.provider = 'letsencrypt-prod-wildcard';
}
},
show: function (domain) {
$scope.domainConfigure.reset();
@@ -179,15 +149,10 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
$scope.domainConfigure.nameComToken = domain.provider === 'namecom' ? domain.config.token : '';
$scope.domainConfigure.nameComUsername = domain.provider === 'namecom' ? domain.config.username : '';
$scope.domainConfigure.namecheapApiKey = domain.provider === 'namecheap' ? domain.config.token : '';
$scope.domainConfigure.namecheapUsername = domain.provider === 'namecheap' ? domain.config.username : '';
$scope.domainConfigure.provider = domain.provider;
$scope.domainConfigure.provider = ($scope.domainConfigure.provider === 'manual' && domain.config.wildcard) ? 'wildcard' : domain.provider;
$scope.domainConfigure.tlsConfig.provider = domain.tlsConfig.provider;
if (domain.tlsConfig.provider.indexOf('letsencrypt') === 0) {
if (domain.tlsConfig.wildcard) $scope.domainConfigure.tlsConfig.provider += '-wildcard';
}
$scope.domainConfigure.zoneName = domain.zoneName;
$scope.domainConfigure.hyphenatedSubdomains = !!domain.config.hyphenatedSubdomains;
@@ -206,6 +171,12 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
var data = {};
// special case the wildcard provider
if (provider === 'wildcard') {
provider = 'manual';
data.wildcard = true;
}
if (provider === 'route53') {
data.accessKeyId = $scope.domainConfigure.accessKeyId;
data.secretAccessKey = $scope.domainConfigure.secretAccessKey;
@@ -239,9 +210,6 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
} else if (provider === 'namecom') {
data.token = $scope.domainConfigure.nameComToken;
data.username = $scope.domainConfigure.nameComUsername;
} else if (provider === 'namecheap') {
data.token = $scope.domainConfigure.namecheapApiKey;
data.username = $scope.domainConfigure.namecheapUsername;
}
data.hyphenatedSubdomains = $scope.domainConfigure.hyphenatedSubdomains;
@@ -254,19 +222,10 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
};
}
var tlsConfig = {
provider: $scope.domainConfigure.tlsConfig.provider,
wildcard: false
};
if ($scope.domainConfigure.tlsConfig.provider.indexOf('-wildcard') !== -1) {
tlsConfig.provider = tlsConfig.provider.replace('-wildcard', '');
tlsConfig.wildcard = true;
}
// choose the right api, since we reuse this for adding and configuring domains
var func;
if ($scope.domainConfigure.adding) func = Client.addDomain.bind(Client, $scope.domainConfigure.newDomain, $scope.domainConfigure.zoneName, provider, data, fallbackCertificate, tlsConfig);
else func = Client.updateDomain.bind(Client, $scope.domainConfigure.domain.domain, $scope.domainConfigure.zoneName, provider, data, fallbackCertificate, tlsConfig);
if ($scope.domainConfigure.adding) func = Client.addDomain.bind(Client, $scope.domainConfigure.newDomain, $scope.domainConfigure.zoneName, provider, data, fallbackCertificate, $scope.domainConfigure.tlsConfig);
else func = Client.updateDomain.bind(Client, $scope.domainConfigure.domain.domain, $scope.domainConfigure.zoneName, provider, data, fallbackCertificate, $scope.domainConfigure.tlsConfig);
func(function (error) {
$scope.domainConfigure.busy = false;
@@ -278,7 +237,12 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
$('#domainConfigureModal').modal('hide');
$scope.domainConfigure.reset();
refreshDomains();
// reload the domains
getDomains(function (error, result) {
if (error) return console.error(error);
$scope.domains = result;
});
});
},
@@ -303,8 +267,6 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
$scope.domainConfigure.cloudflareEmail = '';
$scope.domainConfigure.nameComToken = '';
$scope.domainConfigure.nameComUsername = '';
$scope.domainConfigure.namecheapApiKey = '';
$scope.domainConfigure.namecheapUsername = '';
$scope.domainConfigure.tlsConfig.provider = 'letsencrypt-prod';
$scope.domainConfigure.zoneName = '';
@@ -366,64 +328,6 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
}
};
$scope.renewCerts = {
busy: false,
percent: 0,
message: '',
errorMessage: '',
taskId: '',
checkStatus: function () {
Client.getLatestTaskByType('renewcerts', function (error, task) {
if (error) return console.error(error);
if (!task) return;
$scope.renewCerts.taskId = task.id;
$scope.renewCerts.updateStatus();
});
},
updateStatus: function () {
Client.getTask($scope.renewCerts.taskId, function (error, data) {
if (error) return window.setTimeout($scope.renewCerts.updateStatus, 5000);
if (!data.active) {
$scope.renewCerts.busy = false;
$scope.renewCerts.message = '';
$scope.renewCerts.percent = 100; // indicates that 'result' is valid
$scope.renewCerts.errorMessage = data.errorMessage;
return;
}
$scope.renewCerts.busy = true;
$scope.renewCerts.percent = data.percent;
$scope.renewCerts.message = data.message;
window.setTimeout($scope.renewCerts.updateStatus, 500);
});
},
renew: function () {
$scope.renewCerts.busy = true;
$scope.renewCerts.percent = 0;
$scope.renewCerts.message = '';
$scope.renewCerts.errorMessage = '';
Client.renewCerts(null /* all domains */, function (error, taskId) {
if (error) {
console.error(error);
$scope.renewCerts.errorMessage = error.message;
$scope.renewCerts.busy = false;
} else {
$scope.renewCerts.taskId = taskId;
$scope.renewCerts.updateStatus();
}
});
}
};
$scope.domainRemove = {
busy: false,
error: null,
@@ -454,7 +358,12 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
$('#domainRemoveModal').modal('hide');
$scope.domainRemove.reset();
refreshDomains();
// reload the domains
getDomains(function (error, result) {
if (error) return console.error(error);
$scope.domains = result;
});
}
$scope.domainRemove.busy = false;
@@ -472,135 +381,13 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
}
};
$scope.changeDashboard = {
busy: false,
percent: 0,
message: '',
errorMessage: '',
taskId: '',
selectedDomain: null,
adminDomain: null,
stop: function () {
Client.stopTask($scope.changeDashboard.taskId, function (error) {
if (error) console.error(error);
$scope.changeDashboard.busy = false;
});
},
// this function is not called intentionally. currently, we do switching in two steps - prepare and set
// if the user refreshed the UI in the middle of prepare, then it would be awkward to resume/call 'set' when the
// user visits the UI the next time around.
checkStatus: function () {
Client.getLatestTaskByType('prepareDashboardDomain', function (error, task) {
if (error) return console.error(error);
if (!task) return;
$scope.changeDashboard.taskId = task.id;
$scope.changeDashboard.updateStatus();
});
},
updateStatus: function () {
if (!$scope.changeDashboard.busy) return; // task got stopped
Client.getTask($scope.changeDashboard.taskId, function (error, data) {
if (error) return window.setTimeout($scope.changeDashboard.updateStatus, 5000);
if (!data.active) {
$scope.changeDashboard.busy = false;
$scope.changeDashboard.message = '';
$scope.changeDashboard.percent = 100; // indicates that 'result' is valid
$scope.changeDashboard.errorMessage = data.errorMessage;
if (!$scope.changeDashboard.errorMessage) $scope.changeDashboard.setDashboardDomain();
return;
}
$scope.changeDashboard.busy = true;
$scope.changeDashboard.percent = data.percent;
$scope.changeDashboard.message = data.message;
window.setTimeout($scope.changeDashboard.updateStatus, 500);
});
},
setDashboardDomain: function () {
Client.setDashboardDomain($scope.changeDashboard.selectedDomain.domain, function (error) {
if (error) {
console.error(error);
$scope.changeDashboard.errorMessage = error.message;
$scope.changeDashboard.busy = false;
} else {
window.location.href = 'https://my.' + $scope.changeDashboard.selectedDomain.domain;
}
});
},
change: function () {
$scope.changeDashboard.busy = true;
$scope.changeDashboard.message = 'Preparing dashboard domain';
$scope.changeDashboard.percent = 0;
$scope.changeDashboard.errorMessage = '';
Client.prepareDashboardDomain($scope.changeDashboard.selectedDomain.domain, function (error, taskId) {
if (error) {
console.error(error);
$scope.changeDashboard.errorMessage = error.message;
$scope.changeDashboard.busy = false;
} else {
$scope.changeDashboard.taskId = taskId;
$scope.changeDashboard.updateStatus();
}
});
}
};
$scope.dyndnsConfigure = {
busy: false,
success: false,
error: '',
currentState: false,
enabled: false,
refresh: function () {
Client.getDynamicDnsConfig(function (error, enabled) {
if (error) return console.error(error);
$scope.dyndnsConfigure.currentState = enabled;
$scope.dyndnsConfigure.enabled = enabled;
});
},
submit: function () {
$scope.dyndnsConfigure.busy = true;
$scope.dyndnsConfigure.success = false;
$scope.dyndnsConfigure.error = '';
Client.setDynamicDnsConfig($scope.dyndnsConfigure.enabled, function (error) {
if (error) $scope.dyndnsConfigure.error = error.message;
else $scope.dyndnsConfigure.currentState = $scope.dyndnsConfigure.enabled;
$scope.dyndnsConfigure.busy = false;
$scope.dyndnsConfigure.success = true;
});
}
};
Client.onReady(function () {
refreshDomains(function (error) {
getDomains(function (error, result) {
if (error) return console.error(error);
$scope.domains = result;
$scope.ready = true;
if ($scope.config.features.dynamicDns) {
$scope.dyndnsConfigure.refresh();
}
});
$scope.renewCerts.checkStatus();
});
document.getElementById('gcdnsKeyFileInput').onchange = readFileLocally($scope.domainConfigure.gcdnsKey, 'content', 'keyFileName');
+59 -88
View File
@@ -4,33 +4,24 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Enable Email for {{selectedDomain.domain}}?</h4>
<h4 class="modal-title">Cloudron Email Server</h4>
</div>
<div class="modal-body">
<div>This will configure Cloudron to receive emails for <b>{{selectedDomain.domain}}</b>. See the
documentation for opening up the <a href="https://cloudron.io/documentation/email/#required-ports-for-cloudron-email" target="_blank">required ports</a>
for Cloudron Email.
</div>
<br/>
<div ng-show="selectedDomain.provider === 'noop' || selectedDomain.provider === 'manual'">
No DNS provider is setup. The DNS records listed in the Status tab have to be setup manually.<br/>
No DNS provider is setup. The DNS records listed below have to be setup manually.<br/>
</div>
<div ng-hide="selectedDomain.provider === 'noop' || selectedDomain.provider === 'manual'">
<p>
<label class="control-label">
<input type="checkbox" ng-model="incomingEmail.setupDns"> Setup Mail DNS records now
</label>
</p>
Use this option to automatically setup Email related DNS records. Leaving this option unchecked
is useful for creating mail boxes and <a href="https://cloudron.io/documentation/email/#import-email">importing email</a>
before going live.
Cloudron will setup Email related DNS records automatically for {{selectedDomain.domain}}. Status of DNS Records below
may show an error while DNS is propagating (~5 minutes).
<br/><br/>
If this domain is already configured to handle email with some other provider, it will overwrite those records.
</div>
<br/>
<div>Any installed webmail clients will be automatically re-configured to reflect this change.</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="incomingEmail.enable()">Enable</button>
<button type="button" class="btn btn-success" ng-click="incomingEmail.enable()">I understand, enable</button>
</div>
</div>
</div>
@@ -41,12 +32,12 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Disable Email Server for {{selectedDomain.domain}}?</h4>
<h4 class="modal-title">Cloudron Email Server</h4>
</div>
<div class="modal-body">
<div>This will configure Cloudron to stop receiving emails for <b>{{selectedDomain.domain}}</b>. Mailboxes and lists associated with this
domain will not be deleted.
</div>
<div>This will disable the Cloudron Email Server for {{selectedDomain.domain}}.</div>
<br/>
<div>Any installed webmail clients will be automatically re-configured to reflect this change.</div>
<br/>
</div>
<div class="modal-footer">
@@ -86,7 +77,7 @@
</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="mailboxes.add.submit()" ng-disabled="mailboxadd_form.$invalid || mailboxes.add.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxes.add.busy"></i> Save</button>
<button type="button" class="btn btn-success" ng-click="mailboxes.add.submit()" ng-disabled="mailboxadd_form.$invalid || mailboxes.add.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="mailboxes.add.busy"></i> Save</button>
</div>
</div>
</div>
@@ -116,12 +107,12 @@
<div class="input-group-addon">@{{ selectedDomain.domain }}</div>
</div>
</div>
<input class="hide" type="submit" ng-disabled="mailboxedit_form.$invalid || mailboxes.edit.busy || !mailboxes.edit.owner"/>
<input class="hide" type="submit" ng-disabled="mailboxedit_form.$invalid || mailboxes.edit.busy"/>
</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="mailboxes.edit.submit()" ng-disabled="mailboxedit_form.$invalid || mailboxes.edit.busy || !mailboxes.edit.owner"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxes.edit.busy"></i> Save</button>
<button type="button" class="btn btn-success" ng-click="mailboxes.edit.submit()" ng-disabled="mailboxedit_form.$invalid || mailboxes.edit.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="mailboxes.edit.busy"></i> Save</button>
</div>
</div>
</div>
@@ -139,7 +130,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" ng-click="mailboxes.remove.submit()" ng-disabled="mailboxes.remove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxes.remove.busy"></i> Delete</button>
<button type="button" class="btn btn-danger" ng-click="mailboxes.remove.submit()" ng-disabled="mailboxes.remove.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="mailboxes.remove.busy"></i> Delete</button>
</div>
</div>
</div>
@@ -174,7 +165,7 @@
</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="mailinglists.add.submit()" ng-disabled="mailinglistadd_form.$invalid || mailinglists.add.members.length === 0 || mailinglists.add.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailinglists.add.busy"></i> Save</button>
<button type="button" class="btn btn-success" ng-click="mailinglists.add.submit()" ng-disabled="mailinglistadd_form.$invalid || mailinglists.add.members.length === 0 || mailinglists.add.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="mailinglists.add.busy"></i> Save</button>
</div>
</div>
</div>
@@ -202,7 +193,7 @@
</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="mailinglists.edit.submit()" ng-disabled="mailinglistedit_form.$invalid || mailinglists.edit.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailinglists.edit.busy"></i> Save</button>
<button type="button" class="btn btn-success" ng-click="mailinglists.edit.submit()" ng-disabled="mailinglistedit_form.$invalid || mailinglists.edit.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="mailinglists.edit.busy"></i> Save</button>
</div>
</div>
</div>
@@ -220,7 +211,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" ng-click="mailinglists.remove.submit()" ng-disabled="mailinglists.remove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailinglist.remove.busy"></i> Delete</button>
<button type="button" class="btn btn-danger" ng-click="mailinglists.remove.submit()" ng-disabled="mailinglists.remove.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="mailinglist.remove.busy"></i> Delete</button>
</div>
</div>
</div>
@@ -252,7 +243,7 @@
<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="testEmail.submit()" ng-disabled="testEmail.$invalid || testEmail.busy">
<i class="fa fa-circle-notch fa-spin" ng-show="testEmail.busy"></i><span>Send</span>
<i class="fa fa-circle-o-notch fa-spin" ng-show="testEmail.busy"></i><span>Send</span>
</button>
</div>
</div>
@@ -260,7 +251,7 @@
</div>
<div ng-show="!ready" class="loading-banner">
<h1><i class="fa fa-circle-notch fa-spin"></i></h1>
<h1><i class="fa fa-circle-o-notch fa-spin"></i></h1>
</div>
<div class="content" ng-show="ready">
@@ -285,7 +276,7 @@
</div>
<div class="col-md-2">
<button ng-class="selectedDomain.mailConfig.enabled ? 'btn btn-danger' : 'btn btn-primary'" ng-click="selectedDomain.provider !== 'caas' && incomingEmail.toggleEmailEnabled()" ng-disabled="selectedDomain.provider === 'caas' || incomingEmail.busy">
<i class="fa fa-circle-notch fa-spin" ng-show="incomingEmail.busy"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-show="incomingEmail.busy"></i>
{{ selectedDomain.mailConfig.enabled ? "Disable" : "Enable" }}
</button>
</div>
@@ -308,16 +299,15 @@
<br/>
</div>
<div class="text-left">
<div class="text-left" ng-show="selectedDomain.mailConfig.enabled">
<h3 style="margin-bottom: 15px;">Mailboxes
<button class="btn btn-primary btn-outline pull-right" ng-click="mailboxes.add.show()" ng-disabled="!selectedDomain.mailConfig.enabled"
tooltip-enable="!selectedDomain.mailConfig.enabled" uib-tooltip="Email is disabled for this domain">
<button class="btn btn-primary btn-outline pull-right" ng-click="mailboxes.add.show()">
<i class="fa fa-inbox"></i> New Mailbox
</button>
</h3>
</div>
<div class="card card-large" style="margin-bottom: 15px;">
<div class="card card-large" style="margin-bottom: 15px;" ng-show="selectedDomain.mailConfig.enabled">
<div class="row">
<div class="col-md-12">
<table class="table table-hover">
@@ -335,14 +325,14 @@
{{ mailbox.name }}
</td>
<td class="hand" ng-click="mailboxes.edit.show(mailbox)">
{{ mailbox.ownerDisplayName }}
{{ mailbox.owner.display }}
</td>
<td class="hand" ng-click="mailboxes.edit.show(mailbox)">
{{ mailbox.aliases }}
</td>
<td class="text-right no-wrap">
<button class="btn btn-xs btn-default" ng-click="mailboxes.edit.show(mailbox)"><i class="fa fa-pencil-alt"></i></button>
<button class="btn btn-xs btn-danger" ng-click="mailboxes.remove.show(mailbox)"><i class="far fa-trash-alt"></i></button>
<button class="btn btn-xs btn-default" ng-click="mailboxes.edit.show(mailbox)"><i class="fa fa-pencil"></i></button>
<button class="btn btn-xs btn-danger" ng-click="mailboxes.remove.show(mailbox)"><i class="fa fa-trash"></i></button>
</td>
</tr>
</tbody>
@@ -351,16 +341,15 @@
</div>
</div>
<div class="text-left">
<div class="text-left" ng-show="selectedDomain.mailConfig.enabled">
<h3 style="margin-bottom: 15px;">Mailing Lists
<button class="btn btn-primary btn-outline pull-right" ng-click="mailinglists.add.show()" ng-disabled="!selectedDomain.mailConfig.enabled"
tooltip-enable="!selectedDomain.mailConfig.enabled" uib-tooltip="Email is disabled for this domain">
<button class="btn btn-primary btn-outline pull-right" ng-click="mailinglists.add.show()">
<i class="fa fa-list"></i> New Mailing list
</button>
</h3>
</div>
<div class="card card-large" style="margin-bottom: 15px;">
<div class="card card-large" style="margin-bottom: 15px;" ng-show="selectedDomain.mailConfig.enabled">
<div class="row">
<div class="col-md-12">
A Mailing list forwards all emails to it's members.
@@ -385,8 +374,8 @@
{{ list.members.join(', ') }}
</td>
<td class="text-right no-wrap">
<button class="btn btn-xs btn-default" ng-click="mailinglists.edit.show(list)"><i class="fa fa-pencil-alt"></i></button>
<button class="btn btn-xs btn-danger" ng-click="mailinglists.remove.show(list)"><i class="far fa-trash-alt"></i></button>
<button class="btn btn-xs btn-default" ng-click="mailinglists.edit.show(list)"><i class="fa fa-pencil"></i></button>
<button class="btn btn-xs btn-danger" ng-click="mailinglists.remove.show(list)"><i class="fa fa-trash"></i></button>
</td>
</tr>
</tbody>
@@ -395,11 +384,11 @@
</div>
</div>
<div class="text-left">
<div class="text-left" ng-show="selectedDomain.mailConfig.enabled">
<h3>Catch-all</h3>
</div>
<div class="card card-large" style="margin-bottom: 15px;">
<div class="card card-large" style="margin-bottom: 15px;" ng-show="selectedDomain.mailConfig.enabled">
<div class="row">
<div class="col-md-12">
Emails sent to non existing addresses will be forwarded to the following mailboxes:
@@ -409,22 +398,17 @@
<div class="row">
<div class="col-md-6">
<multiselect ng-model="catchall.mailboxes" options="mailbox.name for mailbox in mailboxes.mailboxes" data-compare-by="name" data-multiple="true"></multiselect>
<button class="btn btn-outline btn-primary" ng-click="catchall.submit()" ng-disabled="catchall.busy || !selectedDomain.mailConfig.enabled"
tooltip-enable="!selectedDomain.mailConfig.enabled" uib-tooltip="Email is disabled for this domain">
<i class="fa fa-circle-notch fa-spin" ng-show="catchall.busy"></i> Save
</button>
<button class="btn btn-outline btn-primary" ng-disabled="catchall.busy" ng-click="catchall.submit()"><i class="fa fa-circle-o-notch fa-spin" ng-show="catchall.busy"></i> Save</button>
</div>
</div>
</div>
</uib-tab>
<uib-tab index="1" heading="Outbound">
<uib-tab index="1" heading="Outbound Relay">
<div class="card card-large" style="margin-bottom: 15px;">
<h4>Email Relay</h4>
<div class="row">
<div class="col-md-12">
Select the mail server (Smart host) through which Cloudron will send outbound mails:
Select the mail server through which Cloudron will send outbound mails:
</div>
</div>
@@ -435,15 +419,10 @@
<div class="form-group">
<select class="form-control" style="width: 50%;" ng-model="mailRelay.preset" ng-options="a.name for a in mailRelayPresets track by a.provider" ng-change="mailRelay.presetChanged()"></select>
</div>
<p class="small text-danger" ng-show="mailRelay.preset.provider === 'noop' && selectedDomain.domain === config.adminDomain">
Cloudron cannot send user invites, password reset and other notifications when email is disabled on the primary domain
</p>
</div>
</div>
<div class="row" ng-show="usesExternalServer(mailRelay.preset.provider)">
<div class="row" ng-show="mailRelay.preset.provider !== 'cloudron-smtp'">
<div class="col-md-6">
<div>
<form name="mailRelayForm" role="form" ng-submit="mailRelay.submit()" autocomplete="off" novalidate>
@@ -462,36 +441,30 @@
<input type="number" class="form-control" ng-model="mailRelay.relay.port" name="port" required>
</div>
<div class="checkbox" ng-show="mailRelay.relay.provider === 'external-smtp' || mailRelay.relay.provider === 'external-smtp-noauth'" >
<label>
<input type="checkbox" ng-model="mailRelay.relay.acceptSelfSignedCerts">Accept Self-signed certificate</input>
</label>
</div>
<!-- Postmark and Sendgrid -->
<div ng-show="usesTokenAuth(mailRelay.relay.provider)" class="form-group" ng-class="{ 'has-error': (mailRelayForm.serverApiToken.$dirty && mailRelayForm.serverApiToken.$invalid) }">
<div ng-show="isProvider('postmark-smtp') || isProvider('sendgrid-smtp')" class="form-group" ng-class="{ 'has-error': (mailRelayForm.serverApiToken.$dirty && mailRelayForm.serverApiToken.$invalid) }">
<label class="control-label">API Token/Key</label>
<div class="control-label" ng-show="(!mailRelayForm.serverApiToken.$dirty && mailRelay.error.serverApiToken) || (mailRelayForm.serverApiToken.$dirty && mailRelayForm.serverApiToken.$invalid)">
<small ng-show="!mailRelayForm.serverApiToken.$dirty && mailRelay.error.serverApiToken">{{ mailRelay.error.serverApiToken }}</small>
</div>
<input type="text" class="form-control" ng-model="mailRelay.relay.serverApiToken" name="serverApiToken" ng-required="usesTokenAuth(mailRelay.relay.provider)">
<input type="text" class="form-control" ng-model="mailRelay.relay.serverApiToken" name="serverApiToken" ng-required="isProvider('postmark-smtp') || isProvider('sendgrid-smtp')">
</div>
<!-- Other -->
<div ng-show="usesPasswordAuth(mailRelay.relay.provider)" class="form-group" ng-class="{ 'has-error': (mailRelayForm.username.$dirty && mailRelayForm.username.$invalid) }">
<div ng-show="!isProvider('postmark-smtp') && !isProvider('sendgrid-smtp')" class="form-group" ng-class="{ 'has-error': (mailRelayForm.username.$dirty && mailRelayForm.username.$invalid) }">
<label class="control-label">Username</label>
<div class="control-label" ng-show="(!mailRelayForm.username.$dirty && mailRelay.error.username) || (mailRelayForm.username.$dirty && mailRelayForm.username.$invalid)">
<small ng-show="!mailRelayForm.username.$dirty && mailRelay.error.username">{{ mailRelay.error.username }}</small>
</div>
<input type="text" class="form-control" ng-model="mailRelay.relay.username" name="username" ng-required="usesPasswordAuth(mailRelay.relay.provider)">
<input type="text" class="form-control" ng-model="mailRelay.relay.username" name="username" ng-required="!isProvider('postmark-smtp') && !isProvider('sendgrid-smtp')">
</div>
<div ng-show="usesPasswordAuth(mailRelay.relay.provider)" class="form-group" ng-class="{ 'has-error': (mailRelayForm.password.$dirty && mailRelayForm.password.$invalid) }">
<div ng-show="!isProvider('postmark-smtp') && !isProvider('sendgrid-smtp')" class="form-group" ng-class="{ 'has-error': (mailRelayForm.password.$dirty && mailRelayForm.password.$invalid) }">
<label class="control-label">Password</label>
<div class="control-label" ng-show="(!mailRelayForm.password.$dirty && mailRelay.error.password) || (mailRelayForm.password.$dirty && mailRelayForm.password.$invalid)">
<small ng-show="!mailRelayForm.password.$dirty && mailRelay.error.password">{{ mailRelay.error.password }}</small>
</div>
<input type="password" class="form-control" ng-model="mailRelay.relay.password" name="password" ng-required="usesPasswordAuth(mailRelay.relay.provider)">
<input type="password" class="form-control" ng-model="mailRelay.relay.password" name="password" ng-required="!isProvider('postmark-smtp') && !isProvider('sendgrid-smtp')">
</div>
<input class="ng-hide" type="submit" ng-disabled="mailRelayForm.$invalid"/>
@@ -502,7 +475,7 @@
<div class="row">
<div class="col-md-12">
<button class="btn btn-primary" ng-click="mailRelay.submit()" ng-disabled="(usesExternalServer(mailRelay.preset.provider) && (!mailRelayForm.$dirty || mailRelayForm.$invalid)) || mailRelay.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailRelay.busy"></i> Save</button>
<button class="btn btn-primary" ng-click="mailRelay.submit()" ng-disabled="(mailRelay.preset.provider !== 'cloudron-smtp' && (!mailRelayForm.$dirty || mailRelayForm.$invalid)) || mailRelay.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="mailRelay.busy"></i> Save</button>
<span class="has-error text-center" ng-show="mailRelay.error">{{ mailRelay.error }}</span>
<span class="text-success text-center text-bold" ng-show="mailRelay.success">Saved</span>
@@ -526,18 +499,13 @@
</div>
</uib-tab>
<uib-tab index="3" heading="Status">
<uib-tab index="3" heading="Status" ng-if="selectedDomain.provider !== 'caas'">
<div class="card card-large" style="margin-bottom: 15px;" ng-show="selectedDomain.mailConfig.relay.provider === 'cloudron-smtp'">
<div class="row">
<div class="col-md-12">
<h4>DNS Status
<button class="btn btn-xs btn-primary btn-outline pull-right" ng-click="incomingEmail.setDnsRecords()">
<i class="fa fa-circle-notch fa-spin" ng-show="incomingEmail.setupDnsBusy"></i>&nbsp;Re-setup DNS
</button>
</h4>
<h4>DNS Status</h4>
Status of DNS Records may show an error while DNS is propagating (~5 minutes). See the
<a href="https://cloudron.io/documentation/troubleshooting/#mail-dns" target="_blank">troubleshooting</a> docs for help.
Set the following DNS records for <b><tt>{{ selectedDomain.domain }}</tt></b> to guarantee email delivery:
<br/><br/>
@@ -547,11 +515,10 @@
<p class="text-muted">
<i ng-hide="refreshBusy" ng-class="expectedDnsRecords[record.value].status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i> &nbsp;
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_{{ record.value }}">{{ record.name }} record</a>
<button class="btn btn-xs btn-default" ng-click="refreshStatus()" ng-disabled="refreshBusy" ng-show="!expectedDnsRecords[record.value].status"><i class="fa fa-sync-alt" ng-class="{ 'fa-pulse': refreshBusy }"></i></button>
<button class="btn btn-xs btn-default" ng-click="refreshStatus()" ng-disabled="refreshBusy" ng-show="!expectedDnsRecords[record.value].status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': refreshBusy }"></i></button>
</p>
<div id="collapse_dns_{{ record.value }}" class="panel-collapse collapse">
<div class="panel-body">
<p ng-show="record.name === 'MX' && selectedDomain.provider === 'namecheap'"><a href="https://cloudron.io/documentation/domains/#namecheap-dns" target="_blank" class="btn btn-xs btn-danger">Namecheap requires manual steps for MX records</a></p>
<p ng-show="expectedDnsRecords[record.value].name">Hostname: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].name }}</tt></b></p>
<p ng-hide="expectedDnsRecords[record.value].name">Domain: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].domain }}</tt></b></p>
<p>Record type: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].type }}</tt></b></p>
@@ -566,10 +533,12 @@
</div>
</div>
<div class="card card-large" style="margin-bottom: 15px;" ng-if="selectedDomain.mailConfig.relay.provider !== 'noop'">
<br/>
<div class="card card-large" style="margin-bottom: 15px;">
<div class="row">
<div class="col-md-12">
<h4>SMTP Status <sup><a href="https://cloudron.io/documentation/troubleshooting/#mail-smtp" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></h4>
<h4>SMTP Status</h4>
<div class="row">
<div class="col-xs-12">
@@ -578,7 +547,7 @@
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_outbound_smtp">
{{ selectedDomain.mailConfig.relay.provider === 'cloudron-smtp' ? 'Outbound SMTP (Direct)' : 'Outbound SMTP (Relay)' }}
</a>
<button class="btn btn-xs btn-default" ng-click="refreshStatus()" ng-disabled="refreshBusy" ng-show="!selectedDomain.mailStatus.relay.status"><i class="fa fa-sync-alt" ng-class="{ 'fa-pulse': refreshBusy }"></i></button>
<button class="btn btn-xs btn-default" ng-click="refreshStatus()" ng-disabled="refreshBusy" ng-show="!selectedDomain.mailStatus.relay.status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': refreshBusy }"></i></button>
</p>
<div id="collapse_outbound_smtp" class="panel-collapse collapse">
<div class="panel-body">
@@ -595,7 +564,7 @@
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_rbl">
IP Address Blacklist Check
</a>
<button class="btn btn-xs btn-default" ng-click="refreshStatus()" ng-disabled="refreshBusy" ng-show="!selectedDomain.mailStatus.rbl.status"><i class="fa fa-sync-alt" ng-class="{ 'fa-pulse': refreshBusy }"></i></button>
<button class="btn btn-xs btn-default" ng-click="refreshStatus()" ng-disabled="refreshBusy" ng-show="!selectedDomain.mailStatus.rbl.status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': refreshBusy }"></i></button>
</p>
<div id="collapse_rbl" class="panel-collapse collapse">
<div class="panel-body">
@@ -617,6 +586,8 @@
</div>
</div>
<br/>
<div class="card card-large" style="margin-bottom: 15px;">
<div class="row">
<div class="col-md-12">
+10 -53
View File
@@ -1,8 +1,5 @@
'use strict';
/* global angular:false */
/* global $:false */
angular.module('Application').controller('EmailController', ['$scope', '$location', '$timeout', '$rootScope', 'Client', function ($scope, $location, $timeout, $rootScope, Client) {
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
@@ -11,7 +8,6 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
$scope.client = Client;
$scope.user = Client.getUserInfo();
$scope.config = Client.getConfig();
$scope.apps = Client.getInstalledApps();
$scope.domains = [];
$scope.users = [];
$scope.selectedDomain = null;
@@ -41,6 +37,10 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
$('.modal').modal('hide');
};
$scope.isProvider = function (provider) {
return $scope.mailRelay.relay.provider === provider;
};
$scope.catchall = {
mailboxes: [],
busy: false,
@@ -53,7 +53,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
Client.setCatchallAddresses($scope.selectedDomain.domain, addresses, function (error) {
if (error) console.error('Unable to add catchall address.', error);
$timeout(function () { $scope.catchall.busy = false; }, 2000); // otherwise, it's too fast
$scope.catchall.busy = false;
});
},
@@ -189,8 +189,6 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
$scope.incomingEmail = {
busy: false,
setupDns: true,
setupDnsBusy: false,
toggleEmailEnabled: function () {
if ($scope.selectedDomain.mailConfig.enabled) {
@@ -200,18 +198,6 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
}
},
setDnsRecords: function (callback) {
$scope.incomingEmail.setupDnsBusy = true;
Client.setDnsRecords($scope.selectedDomain.domain, function (error) {
if (error) console.error(error);
$timeout(function () { $scope.incomingEmail.setupDnsBusy = false; }, 2000); // otherwise, it's too fast
if (callback) callback();
});
},
enable: function () {
$('#enableEmailModal').modal('hide');
@@ -222,9 +208,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
$scope.reconfigureEmailApps();
let maybeSetupDns = $scope.incomingEmail.setupDns ? $scope.incomingEmail.setDnsRecords : function (next) { next(); };
maybeSetupDns(function (error) {
Client.setDnsRecords($scope.selectedDomain.domain, function (error) {
if (error) return console.error(error);
$scope.refreshDomain();
@@ -301,7 +285,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
show: function (mailbox) {
$scope.mailboxes.edit.name = mailbox.name;
$scope.mailboxes.edit.owner = mailbox.owner; // this can be null if mailbox had no owner
$scope.mailboxes.edit.owner = mailbox.owner;
$scope.mailboxes.edit.aliases = mailbox.aliases;
$('#mailboxEditModal').modal('show');
@@ -310,7 +294,6 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
submit: function () {
$scope.mailboxes.edit.busy = true;
// $scope.mailboxes.edit.owner is expected to be validated by the UI
Client.updateMailbox($scope.selectedDomain.domain, $scope.mailboxes.edit.name, $scope.mailboxes.edit.owner.id, function (error) {
if (error) {
$scope.mailboxes.edit.error = error;
@@ -380,9 +363,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
$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.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
m.owner = $scope.users.find(function (u) { return u.id === m.ownerId; });
return m;
});
@@ -395,32 +376,14 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
$scope.mailRelayPresets = [
{ provider: 'cloudron-smtp', name: 'Built-in SMTP server' },
{ provider: 'external-smtp', name: 'External SMTP server', host: '', port: 587 },
{ provider: 'external-smtp-noauth', name: 'External SMTP server (No Authentication)', host: '', port: 587 },
{ provider: 'ses-smtp', name: 'Amazon SES', host: 'email-smtp.us-east-1.amazonaws.com', port: 587 },
{ provider: 'google-smtp', name: 'Google', host: 'smtp.gmail.com', port: 587 },
{ provider: 'mailgun-smtp', name: 'Mailgun', host: 'smtp.mailgun.org', port: 587 },
{ provider: 'mailjet-smtp', name: 'Mailjet', host: '', port: 587 },
{ provider: 'postmark-smtp', name: 'Postmark', host: 'smtp.postmarkapp.com', port: 587 },
{ provider: 'sendgrid-smtp', name: 'SendGrid', host: 'smtp.sendgrid.net', port: 587, username: 'apikey' },
{ provider: 'noop', name: 'Disable' },
];
$scope.usesTokenAuth = function (provider) {
return provider === 'postmark-smtp' || provider === 'sendgrid-smtp';
};
$scope.usesExternalServer = function (provider) {
return provider !== 'cloudron-smtp' && provider !== 'noop';
};
$scope.usesPasswordAuth = function (provider) {
return provider === 'external-smtp'
|| provider === 'ses-smtp'
|| provider === 'google-smtp'
|| provider === 'mailgun-smtp'
|| provider === 'mailjet-smtp';
};
$scope.mailRelay = {
error: null,
success: false,
@@ -436,7 +399,6 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
$scope.mailRelay.relay.username = '';
$scope.mailRelay.relay.password = '';
$scope.mailRelay.relay.serverApiToken = '';
$scope.mailRelay.relay.acceptSelfSignedCerts = false;
},
// form data to be set on load
@@ -446,8 +408,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
port: 25,
username: '',
password: '',
serverApiToken: '',
acceptSelfSignedCerts: false
serverApiToken: ''
},
submit: function () {
@@ -458,8 +419,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
var data = {
provider: $scope.mailRelay.relay.provider,
host: $scope.mailRelay.relay.host,
port: $scope.mailRelay.relay.port,
acceptSelfSignedCerts: $scope.mailRelay.relay.acceptSelfSignedCerts
port: $scope.mailRelay.relay.port
};
// fill in provider specific username/password usage
@@ -596,7 +556,6 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
$scope.mailRelay.relay.provider = mailConfig.relay.provider;
$scope.mailRelay.relay.host = mailConfig.relay.host;
$scope.mailRelay.relay.port = mailConfig.relay.port;
$scope.mailRelay.relay.acceptSelfSignedCerts = !!mailConfig.relay.acceptSelfSignedCerts;
$scope.mailRelay.relay.username = '';
$scope.mailRelay.relay.password = '';
$scope.mailRelay.relay.serverApiToken = '';
@@ -671,8 +630,6 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
return u;
});
$scope.users = users;
Client.getDomains(function (error, domains) {
if (error) return console.error('Unable to get domain listing.', error);
+2 -4
View File
@@ -1,9 +1,7 @@
'use strict';
/* global Chart:false */
/* global asyncForEach:false */
/* global angular:false */
/* global $:false */
'use strict';
angular.module('Application').controller('GraphsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
-41
View File
@@ -1,41 +0,0 @@
<div class="content">
<div class="text-left">
<h1>Notifications <button class="btn btn-primary btn-outline pull-right" ng-click="notifications.clearAll()" ng-disabled="clearAllBusy"><i class="fa fa-circle-notch fa-spin" ng-show="clearAllBusy"></i><i class="fa fa-check" ng-hide="clearAllBusy"></i> Clear All</button></h1>
</div>
<div class="col-lg-12 text-center" ng-show="notifications.busy">
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
</div>
<div class="card" ng-hide="notifications.busy || notifications.notifications.length">
<div class="row">
<div class="col-xs-12">
<h3 class="text-center" style="margin: 20px;">All Caught Up!</h3>
</div>
</div>
</div>
<div class="card notification-item" ng-repeat="notification in notifications.notifications">
<div class="row">
<div class="col-xs-12" ng-click="notification.isCollapsed = !notification.isCollapsed" ng-class="{ 'notification-details': notification.detailsShown }">
{{ notification.title }} <small class="text-muted" uib-tooltip="{{ notification.creationTime | prettyLongDate }}">{{ notification.creationTime | prettyDate }}</small>
<button class="btn btn-xs btn-success pull-right" ng-hide="notification.acknowledged" ng-click="notifications.ack(notification, $event)" uib-tooltip="Clear"><i class="fa fa-check"></i></button>
<div uib-collapse="notification.isCollapsed" expanding="notificationExpanding(notification)">
<br/>
<p ng-hide="notification.messageJson" ng-bind-html="notification.message | markdown2html"></p>
<pre ng-show="notification.messageJson" ng-click="$event.stopPropagation();" style="cursor: auto">{{ notification.messageJson | json }}</pre>
<br/>
<h2 ng-show="notification.eventId && notification.busyLoadEvent" class="text-center"><i class="fa fa-circle-notch fa-spin"></i></h2>
<pre ng-hide="!notification.eventId || notification.busyLoadEvent" ng-click="$event.stopPropagation();" style="cursor: auto">{{ notification.event.data | json }}</pre>
</div>
</div>
</div>
</div>
<div class="col-md-12 text-center" ng-hide="notifications.all">
<button class="btn btn-outline btn-default" ng-click="notifications.showOld()">Show older Notifications</button>
</div>
</div>
-97
View File
@@ -1,97 +0,0 @@
'use strict';
/* global asyncForEach:false */
/* global angular:false */
angular.module('Application').controller('NotificationsController', ['$scope', 'Client', function ($scope, Client) {
$scope.clearAllBusy = false;
$scope.notifications = {
notifications: [],
activeNotification: null,
busy: true,
all: false,
refresh: function () {
Client.getNotifications($scope.notifications.all ? null : false, 1, 20, function (error, result) {
if (error) return console.error(error);
// collapse by default
result.forEach(function (r) { r.isCollapsed = true; });
// attempt to parse the message as json
result.forEach(function (r) {
try {
r.messageJson = JSON.parse(r.message);
} catch (e) {}
});
$scope.notifications.notifications = result;
$scope.notifications.busy = false;
});
},
showOld: function () {
$scope.notifications.busy = true;
$scope.notifications.all = true;
$scope.notifications.refresh();
},
clicked: function (notification) {
if ($scope.notifications.activeNotification === notification) return $scope.notifications.activeNotification = null;
$scope.notifications.activeNotification = notification;
},
ack: function (notification, event, callback) {
callback = callback || function (error) { if (error) console.error(error); };
if (event) event.stopPropagation();
Client.ackNotification(notification.id, function (error) {
if (error) return callback(error);
$scope.$parent.notificationAcknowledged(notification.id);
$scope.notifications.refresh();
callback();
});
},
action: function (notification) {
if (notification.action) window.location = notification.action;
},
clearAll: function () {
$scope.clearAllBusy = true;
asyncForEach($scope.notifications.notifications, function (notification, callback) {
if (notification.acknowledged) return callback();
$scope.notifications.ack(notification, null /* no click event */, callback);
}, function (error) {
if (error) console.error(error);
$scope.clearAllBusy = false;
});
}
};
$scope.notificationExpanding = function (notification) {
if (!notification.eventId) return;
notification.busyLoadEvent = true;
Client.getEvent(notification.eventId, function (error, result) {
notification.busyLoadEvent = false;
if (error) return console.error(error);
notification.event = result;
});
};
Client.onReady(function () {
$scope.notifications.refresh();
});
}]);
+130 -109
View File
@@ -19,21 +19,30 @@
</div>
<div ng-show="installedApps | readyToUpdate">
<b ng-show="config.update.box.upgrade" class="text-danger">
This update upgrades the base system and will cause some application downtime.<br/>
</b>
<p>Changes:</p>
<ul>
<li ng-repeat="change in config.update.box.changelog" ng-bind-html="change | markdown2html"></li>
</ul>
<br/>
<p ng-show="update.error.generic" class="text-danger">{{ update.error.generic }}</p>
<div ng-hide="config.provider !== 'caas' && config.update.box.upgrade">
<fieldset>
<form name="update_form" role="form" ng-submit="update.submit()" autocomplete="off">
<input class="ng-hide" type="submit" ng-disabled="update_form.$invalid || update.busy"/>
</form>
</fieldset>
</div>
<div ng-show="config.provider !== 'caas' && config.update.box.upgrade">
<b>Please use the CLI tool to upgrade by following the instructions <a ng-href="{{ config.webServerOrigin + '/documentation/updates/' }}" target="_blank" >here</a>.</b>
</div>
</div>
</div>
<div class="modal-footer">
<label class="checkbox-inline pull-left">
<input type="checkbox" ng-model="update.skipBackup"><b>Skip backup</b>
</label>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-success" ng-click="update.startUpdate()" ng-disabled="update.busy" ng-show="(installedApps | readyToUpdate)"><i class="fa fa-circle-notch fa-spin" ng-show="update.busy"></i> Update</button>
<button type="button" class="btn btn-success" ng-click="update.submit()" ng-disabled="update_form.$invalid || update.busy" ng-show="(installedApps | readyToUpdate) && !(config.provider !== 'caas' && config.update.box.upgrade)"><i class="fa fa-circle-o-notch fa-spin" ng-show="update.busy"></i> Update</button>
</div>
</div>
</div>
@@ -60,7 +69,7 @@
</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>
<button type="button" class="btn btn-success" ng-click="avatarChange.doChangeAvatar()" ng-disabled="avatarChange.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="avatarChange.busy"></i> Change</button>
</div>
</div>
</div>
@@ -89,7 +98,46 @@
</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>
<button type="button" class="btn btn-success" ng-click="cloudronNameChange.submit()" ng-disabled="cloudronNameChangeForm.$invalid || cloudronNameChange.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="cloudronNameChange.busy"></i> Change</button>
</div>
</div>
</div>
</div>
<!-- Modal plan change -->
<div class="modal fade" id="planChangeModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Cloudron Change Plan</h4>
</div>
<div class="modal-body">
This will change your plan from <b>{{ currentPlan.name }}</b> to <b>{{ planChange.requestedPlan.name }}</b>.
<br/>
<br/>
Your apps and data will be migrated to the new Cloudron and will take around 15 minutes.
<br/>
<br/>
<br/>
<form name="planChangeForm" role="form" novalidate ng-submit="planChange.doChangePlan(planChangeForm)" autocomplete="off">
<fieldset>
<input type="password" style="display: none;">
<div class="form-group" ng-class="{ 'has-error': (!planChangeForm.password.$dirty && planChange.error.password) || (planChangeForm.password.$dirty && planChangeForm.password.$invalid) }">
<label class="control-label">Give your password to verify that you are performing that action</label>
<div class="control-label" ng-show="(!planChangeForm.password.$dirty && planChange.error.password) || (planChangeForm.password.$dirty && planChangeForm.password.$invalid)">
<small ng-show=" planChangeForm.password.$dirty && planChangeForm.password.$invalid">A password is required</small>
<small ng-show="!planChangeForm.password.$dirty && planChange.error.password">Wrong password</small>
</div>
<input type="password" class="form-control" ng-model="planChange.password" id="inputPlanChangePassword" name="password" required autofocus>
</div>
<input class="ng-hide" type="submit" ng-disabled="planChangeForm.$invalid"/>
</fieldset>
</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="planChange.doChangePlan()" ng-disabled="planChange.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="planChange.busy"></i> Confirm</button>
</div>
</div>
</div>
@@ -116,7 +164,11 @@
<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>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.cloudronName }} <a href="" ng-click="cloudronNameChange.show()"><i class="fa fa-pencil text-small"></i></a></td>
</tr>
<tr ng-show="config.provider === 'caas'">
<td class="text-muted" style="vertical-align: top;">Model</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ caasConfig.size }} - {{ caasConfig.region }}</td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">Version</td>
@@ -129,23 +181,14 @@
<tr>
<td colspan="2">&nbsp;</td>
</tr>
<tr ng-show="!update.busy && update.errorMessage">
<td class="text-muted" style="vertical-align: top;">Update Error:</td>
<td class="text-right has-error" style="vertical-align: top; white-space: nowrap;">{{ 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>
<td colspan="2">&nbsp;</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 class="text-muted" style="vertical-align: top;"></td>
<td class="text-right" style="vertical-align: bottom;">
<button class="btn btn-primary pull-right" ng-hide="config.update.box" ng-disabled="autoUpdate.busy" ng-click="autoUpdate.checkNow()"><i class="fa fa-circle-o-notch fa-spin" ng-show="autoUpdate.busy"></i> Check for Updates</button>
<button class="btn btn-success pull-right" ng-show="config.update.box" ng-click="update.show(update_form)">Update Available</button>
</td>
</tr>
</table>
@@ -153,68 +196,80 @@
</div>
</div>
<div class="text-left" ng-show="config.features.subscription">
<div class="text-left" ng-show="config.provider === 'caas'">
<h3>Plans</h3>
</div>
<div class="card" style="margin-bottom: 15px;" ng-show="config.provider === 'caas'">
<div class="row">
<div class="col-xs-12 text-right">
<a href="{{ config.webServerOrigin }}/console.html#/userprofile?view=credit_card" target="_blank">Change payment method</a>
or
<a href="{{ config.webServerOrigin }}/console.html" target="_blank">Cancel this Cloudron</a>
</div>
</div>
<div class="row">
<div class="col-xs-10 plans" style="margin-left: 20px">
<div ng-repeat="plan in availablePlans">
<label>
<input type="radio" ng-model="planChange.requestedPlan" ng-value="plan">
{{ plan.name }} ({{ plan.slug | uppercase }}) - {{ plan.price/100 }}{{ currency }}/month
<span ng-show="currentPlan.name === plan.name" style="font-weight: bold"> (current plan)
</span>
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<button class="btn btn-primary pull-right" ng-disabled="planChange.requestedPlan.name === currentPlan.name" ng-click="planChange.showChangePlan()">Change Plan</button>
</div>
</div>
</div>
<div class="text-left" ng-show="config.provider !== 'caas' && user.admin && config.features.operatorActions">
<h3>Cloudron.io Account</h3>
</div>
<div class="card" style="margin-bottom: 15px;" ng-show="config.features.subscription">
<div ng-show="subscriptionBusy">
<div class="card" style="margin-bottom: 15px;" ng-show="config.provider !== 'caas' && user.admin && config.features.operatorActions">
<div class="row">
<div class="col-xs-12 text-center">
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
</div>
</div>
</div>
<div ng-show="!subscriptionBusy">
<div class="row">
<div class="col-xs-12">
A Cloudron account provides access to the Cloudron App Store. This ensures you are running the latest versions to keep your apps and server secure.
</div>
<div class="col-xs-12">
A Cloudron subscription provides access to the Cloudron App Store. This ensures you are running the latest version and keeps your apps and server secure.
</div>
</div>
<br/>
<div class="row" ng-show="!subscription">
<div class="col-xs-12 text-center">
<a class="btn btn-success" ng-href="/#/appstore">Setup Account</a>
</div>
<div class="row">
<div class="col-xs-6">
<span class="text-muted">Account Email</span>
</div>
<div class="col-xs-6 text-right">
<a ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?email=' + appstoreConfig.profile.emailEncoded }}" target="_blank">{{ appstoreConfig.profile.email }}</a>
</div>
</div>
<div class="row" ng-show="subscription">
<div class="col-xs-6">
<span class="text-muted">Account Email</span>
</div>
<div class="col-xs-6 text-right">
<a ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?email=' + subscription.emailEncoded }}" target="_blank">{{ subscription.email }}</a>
</div>
<div class="row">
<div class="col-xs-6">
<span class="text-muted">Cloudron ID</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ appstoreConfig.cloudronId }}</span>
</div>
</div>
<div class="row" ng-show="subscription">
<div class="col-xs-6">
<span class="text-muted">Cloudron ID</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ subscription.cloudronId }}</span>
</div>
</div>
<div class="row" ng-show="subscription">
<div class="col-xs-6">
<span class="text-muted">Subscription</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ subscription.plan.name }}</span>
</div>
</div>
<div class="row" ng-show="subscription">
<div class="col-xs-12 text-right">
<b class="text-danger" ng-show="subscription.cancel_at">Canceled and ends on {{ (subscription.cancel_at*1000) | prettyShortDate }}</b>
</div>
<div class="row">
<div class="col-xs-6">
<span class="text-muted">Subscription</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ subscription.plan.name }}</span>
</div>
</div>
<br/>
<div class="row" ng-show="subscription">
<div class="col-xs-12">
<a class="btn btn-primary pull-right" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + subscription.emailEncoded + '&cloudronId=' + subscription.cloudronId }}" target="_blank" ng-show="subscription.plan.id !== 'free' && !subscription.cancel_at">Change Subscription</a>
<a class="btn btn-success pull-right" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + subscription.emailEncoded }}" target="_blank" ng-show="subscription.plan.id !== 'free' && subscription.cancel_at">Reactivate Subscription</a>
<a class="btn btn-success pull-right" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + subscription.emailEncoded + '&cloudronId=' + subscription.cloudronId }}" target="_blank" ng-show="subscription.plan.id === 'free'">Setup Subscription</a>
</div>
<div class="row">
<div class="col-xs-12" ng-show="subscription">
<a class="btn btn-primary pull-right" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.emailEncoded + '&cloudronId=' + appstoreConfig.cloudronId }}" target="_blank" ng-hide="subscription.plan.id === 'free'">Change Plan</a>
<a class="btn btn-success pull-right" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.emailEncoded + '&cloudronId=' + appstoreConfig.cloudronId }}" target="_blank" ng-show="subscription.plan.id === 'free'">Setup Subscription</a>
</div>
</div>
</div>
</div>
<div class="text-left">
@@ -252,7 +307,7 @@
<div class="radio">
<label>
<input type="radio" name="scheduleRadio" ng-change="autoUpdate.success = false" ng-model="autoUpdate.pattern" value="never">
No automatic updates
Update manually (Not recommended)
</label>
</div>
</div>
@@ -269,38 +324,4 @@
</div>
</div>
<div class="text-left">
<h3>Unstable App Listing</h3>
</div>
<div class="card" style="margin-bottom: 15px;">
<div class="row">
<div class="col-md-12">
<p>
Besides the officially supported and tested apps, Cloudron can also install apps, which are currently in testing phase or not officially supported.
There is no guarantee that those apps will be updated in the future.
If enabled, those apps will be listed in the <a href="#/appstore">App Store</a> and marked accordingly.
</p>
<b class="text-danger">
Do not use those apps in any production environment.
</b>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="unstableApps.enabled">Enable unstable app listing</input>
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<span class="text-success text-bold" ng-show="unstableApps.success">Saved</span>
</div>
<div class="col-md-6 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="unstableApps.submit()" ng-disabled="unstableApps.busy"><i class="fa fa-circle-notch fa-spin" ng-show="unstableApps.busy"></i> Save</button>
</div>
</div>
</div>
</div>
+132 -123
View File
@@ -1,18 +1,22 @@
'use strict';
/* global angular:false */
/* global $:false */
angular.module('Application').controller('SettingsController', ['$scope', '$location', '$rootScope', '$timeout', 'Client', function ($scope, $location, $rootScope, $timeout, Client) {
angular.module('Application').controller('SettingsController', ['$scope', '$location', '$rootScope', '$timeout', 'Client', 'AppStore', function ($scope, $location, $rootScope, $timeout, Client, AppStore) {
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
$scope.client = Client;
$scope.user = Client.getUserInfo();
$scope.config = Client.getConfig();
$scope.caasConfig = {};
$scope.appstoreConfig = {};
$scope.installedApps = Client.getInstalledApps();
$scope.currency = null;
$scope.availableRegions = [];
$scope.currentRegionSlug = null;
$scope.availablePlans = [];
$scope.currentPlan = null;
$scope.subscription = null;
$scope.subscriptionBusy = true;
$scope.prettyProviderName = function (provider) {
switch (provider) {
@@ -22,18 +26,16 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
};
$scope.update = {
error: {}, // this is for the dialog
error: {},
busy: false,
percent: 0,
message: 'Downloading',
errorMessage: '', // this shows inline
taskId: '',
skipBackup: false,
show: function () {
show: function (form) {
$scope.update.error.generic = null;
$scope.update.busy = false;
form.$setPristine();
form.$setUntouched();
if (!$scope.config.update.box.sourceTarballUrl) {
$('#setupSubscriptionModal').modal('show');
} else {
@@ -41,82 +43,76 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
}
},
stopUpdate: function () {
Client.stopTask($scope.update.taskId, function (error) {
if (error) {
if (error.statusCode === 409) {
$scope.update.errorMessage = 'No update is currently in progress';
} else {
console.error(error);
$scope.update.errorMessage = error.message;
}
$scope.update.busy = false;
return;
}
});
},
checkStatus: function () {
Client.getLatestTaskByType('update', function (error, task) {
if (error) return console.error(error);
if (!task) return;
$scope.update.taskId = task.id;
$scope.update.updateStatus();
});
},
reloadIfNeeded: function () {
Client.getStatus(function (error, status) {
if (error) return $scope.error(error);
if (window.localStorage.version !== status.version) window.location.reload(true);
});
},
updateStatus: function () {
Client.getTask($scope.update.taskId, function (error, data) {
if (error) return window.setTimeout($scope.update.updateStatus, 5000);
if (!data.active) {
$scope.update.busy = false;
$scope.update.message = '';
$scope.update.percent = 100; // indicates that 'result' is valid
$scope.update.errorMessage = data.errorMessage;
if (!data.errorMessage) $scope.update.reloadIfNeeded(); // assume success
return;
}
$scope.update.busy = true;
$scope.update.percent = data.percent;
$scope.update.message = data.message;
window.setTimeout($scope.update.updateStatus, 500);
});
},
startUpdate: function () {
submit: function () {
$scope.update.error.generic = null;
$scope.update.busy = true;
$scope.update.percent = 0;
$scope.update.message = '';
$scope.update.errorMessage = '';
Client.update({ skipBackup: $scope.update.skipBackup }, function (error, taskId) {
Client.update(function (error) {
if (error) {
$scope.update.error.generic = error.message;
if (error.statusCode === 409) {
$scope.update.error.generic = 'Please try again later. The Cloudron is creating a backup at the moment.';
} else {
$scope.update.error.generic = error.message;
console.error('Unable to update.', error);
}
$scope.update.busy = false;
return;
}
$('#updateModal').modal('hide');
window.location.href = '/update.html';
});
}
};
$scope.update.taskId = taskId;
$scope.update.updateStatus();
$scope.planChange = {
busy: false,
error: {},
password: '',
requestedPlan: null,
showChangePlan: function () {
$('#planChangeModal').modal('show');
},
planChangeReset: function () {
$scope.planChange.error.password = null;
$scope.planChange.password = '';
$scope.planChangeForm.$setPristine();
$scope.planChangeForm.$setUntouched();
},
doChangePlan: function () {
$scope.planChange.busy = true;
var options = {
size: $scope.planChange.requestedPlan.slug,
name: $scope.planChange.requestedPlan.name,
price: $scope.planChange.requestedPlan.price,
region: $scope.currentRegionSlug
};
Client.changePlan(options, $scope.planChange.password, function (error) {
$scope.planChange.busy = false;
if (error) {
if (error.statusCode === 403) {
$scope.planChange.error.password = true;
$scope.planChange.password = '';
$scope.planChangeForm.password.$setPristine();
$('#inputPlanChangePassword').focus();
} else {
console.error('Unable to change plan.', error);
}
} else {
$scope.planChange.planChangeReset();
$('#planChangeModal').modal('hide');
window.location.href = '/update.html';
}
$scope.planChange.busy = false;
});
}
};
@@ -300,34 +296,49 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
});
}
function getUnstableAppsConfig() {
Client.getUnstableAppsConfig(function (error, result) {
if (error) return console.error(error);
$scope.unstableApps.enabled = result;
});
}
function getSubscription() {
$scope.subscriptionBusy = true;
Client.getSubscription(function (error, result) {
$scope.subscriptionBusy = false;
if (error && error.statusCode === 412) return; // not yet registered
AppStore.getSubscription($scope.appstoreConfig, function (error, result) {
if (error) return console.error(error);
if (!$scope.$parent) return; // user changed view. otherwise we get an error that $scope.$parent is null
$scope.subscription = result;
// also reload the subscription on the main controller
$scope.$parent.updateSubscriptionStatus();
$scope.$parent.fetchAppstoreProfileAndSubscription(function () {});
// check again to give more immediate feedback once a subscription was setup
if (result.plan.id === 'free') $timeout(getSubscription, 10000);
});
}
function getPlans() {
AppStore.getSizes(function (error, result) {
if (error) return console.error(error);
var found = false;
var SIZE_SLUGS = [ '512mb', '1gb', '2gb', '4gb', '8gb', '16gb', '32gb', '48gb', '64gb' ];
result = result.filter(function (size) {
// only show plans bigger than the current size
if (found) return true;
found = SIZE_SLUGS.indexOf(size.slug) > SIZE_SLUGS.indexOf($scope.caasConfig.plan.slug);
return found;
});
angular.copy(result, $scope.availablePlans);
// prepend the current plan
$scope.availablePlans.unshift($scope.caasConfig.plan);
$scope.planChange.requestedPlan = $scope.availablePlans[0]; // need the reference to preselect
AppStore.getRegions(function (error, result) {
if (error) return console.error(error);
angular.copy(result, $scope.availableRegions);
$scope.currentRegionSlug = $scope.caasConfig.region;
});
});
}
$('#avatarFileInput').get(0).onchange = function (event) {
var fr = new FileReader();
fr.onload = function () {
@@ -345,31 +356,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
fr.readAsDataURL(event.target.files[0]);
};
$scope.unstableApps = {
busy: false,
success: false,
enabled: false,
submit: function () {
$scope.unstableApps.busy = true;
Client.setUnstableAppsConfig($scope.unstableApps.enabled, function (error) {
$scope.unstableApps.busy = false;
if (error) {
console.error('Unable to change unstable app listing.', error);
return;
}
$scope.unstableApps.success = true;
$timeout(function () {
$scope.unstableApps.success = false;
}, 5000);
});
}
};
$scope.cloudronNameChange = {
busy: false,
error: {},
@@ -419,11 +405,34 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
Client.onReady(function () {
getAutoupdatePattern();
getUnstableAppsConfig();
$scope.update.checkStatus();
if ($scope.config.provider === 'caas') {
Client.getCaasConfig(function (error, caasConfig) {
if (error) return console.error(error);
getSubscription();
$scope.caasConfig = caasConfig;
getPlans();
$scope.currentPlan = caasConfig.plan;
$scope.currency = caasConfig.currency === 'eur' ? '€' : '$';
});
} else if ($scope.config.features.operatorActions) {
Client.getAppstoreConfig(function (error, appstoreConfig) {
if (error) return console.error(error);
if (!appstoreConfig.token) return;
AppStore.getProfile(appstoreConfig.token, function (error, result) {
if (error) return console.error(error);
// assign late to avoid UI flicketing on update
appstoreConfig.profile = result;
$scope.appstoreConfig = appstoreConfig;
getSubscription();
});
});
}
});
// setup all the dialog focus handling
+20 -3
View File
@@ -40,7 +40,7 @@
<div class="form-group" ng-class="{ 'has-error': (feedbackForm.description.$dirty && feedbackForm.description.$invalid) }">
<textarea class="form-control" name="description" rows="3" placeholder="Describe your issue" ng-model="feedback.description" ng-minlength="1" required></textarea>
</div>
<button type="submit" class="btn btn-primary" ng-disabled="feedbackForm.$invalid || feedback.busy"><i class="fa fa-circle-notch fa-spin" ng-show="feedback.busy"></i> Submit</button>
<button type="submit" class="btn btn-primary" ng-disabled="feedbackForm.$invalid || feedback.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="feedback.busy"></i> Submit</button>
<span ng-show="feedback.error" class="text-danger text-bold">{{feedback.error}}</span>
<span ng-show="feedback.success" class="text-success text-bold"> An email for sent to support@cloudron.io. We will get back shortly!</span>
</form>
@@ -49,11 +49,28 @@
</div>
</div>
<div class="text-left" ng-show="config.features.remoteSupport">
<div class="text-left" ng-show="config.provider !== 'caas'">
<h3>Logs</h3>
</div>
<div class="card" ng-show="config.provider !== 'caas'">
<div class="grid-item-top">
<div class="row animateMeOpacity">
<div class="col-lg-12">
<p>
Please be careful when uploading these logs to a public server since they may contain sensitive information.
</p>
<a class="btn btn-primary" href="/logs.html?id=box" target="_blank">Show Logs</a>
</div>
</div>
</div>
</div>
<div class="text-left" ng-show="config.provider !== 'caas'">
<h3>Remote Support</h3>
</div>
<div class="card" ng-show="config.features.remoteSupport">
<div class="card" ng-show="config.provider !== 'caas'">
<div class="grid-item-top">
<div class="row animateMeOpacity">
<div class="col-lg-12">
+18 -12
View File
@@ -1,10 +1,7 @@
'use strict';
/* global angular:false */
/* global $:false */
angular.module('Application').controller('SupportController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
Client.onReady(function () { if (!Client.getConfig().features.operatorActions || !Client.getUserInfo().admin) $location.path('/'); });
$scope.config = Client.getConfig();
$scope.user = Client.getUserInfo();
@@ -37,7 +34,7 @@ angular.module('Application').controller('SupportController', ['$scope', '$locat
$scope.feedback.success = false;
$scope.feedback.error = null;
Client.createTicket($scope.feedback.type, $scope.feedback.subject, $scope.feedback.description, $scope.feedback.appId, function (error) {
Client.feedback($scope.feedback.type, $scope.feedback.subject, $scope.feedback.description, $scope.feedback.appId, function (error) {
if (error) {
$scope.feedback.error = error.message;
} else {
@@ -49,19 +46,28 @@ angular.module('Application').controller('SupportController', ['$scope', '$locat
});
};
$scope.toggleSshSupport = function () {
Client.enableRemoteSupport(!$scope.sshSupportEnabled, function (error) {
if (error) return $scope.error(error);
var CLOUDRON_SUPPORT_PUBLIC_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io';
var CLOUDRON_SUPPORT_PUBLIC_KEY_IDENTIFIER = 'support@cloudron.io';
$scope.sshSupportEnabled = !$scope.sshSupportEnabled;
});
$scope.toggleSshSupport = function () {
if ($scope.sshSupportEnabled) {
Client.delAuthorizedKey(CLOUDRON_SUPPORT_PUBLIC_KEY_IDENTIFIER, function (error) {
if (error) return console.error(error);
$scope.sshSupportEnabled = false;
});
} else {
Client.addAuthorizedKey(CLOUDRON_SUPPORT_PUBLIC_KEY, function (error) {
if (error) return console.error(error);
$scope.sshSupportEnabled = true;
});
}
};
Client.onReady(function () {
Client.getRemoteSupport(function (error, enabled) {
Client.getAuthorizedKeys(function (error, keys) {
if (error) return console.error(error);
$scope.sshSupportEnabled = enabled;
$scope.sshSupportEnabled = keys.some(function (k) { return k.key === CLOUDRON_SUPPORT_PUBLIC_KEY; });
});
});
-147
View File
@@ -1,147 +0,0 @@
<!-- 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-bold">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>
<!-- Modal service configure -->
<div class="modal fade" id="serviceConfigureModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Configure {{ serviceConfigure.service.name }}</h4>
</div>
<div class="modal-body">
<form name="serviceConfigureForm" role="form" novalidate ng-submit="serviceConfigure.submit()" autocomplete="off">
<fieldset>
<p class="has-error text-center" ng-show="serviceConfigure.error">{{ serviceConfigure.error }}</p>
<div class="form-group">
<label class="control-label" for="memoryLimit">Memory Limit <sup><a ng-href="/" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup> : <b>{{ serviceConfigure.memoryLimit / 1024 / 1024 + 'MB' }}</b></label>
<br/>
<div style="padding: 0 10px;">
<slider id="memoryLimit" ng-model="serviceConfigure.memoryLimit" step="134217728" tooltip="hide" ticks="serviceConfigure.memoryTicks" ticks-snap-bounds="67108864"></slider>
</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="serviceConfigureForm.$invalid || serviceConfigure.busy"/>
</fieldset>
</form>
</div>
<div class="modal-footer ">
<button type="button" class="btn btn-default pull-left" ng-click="serviceConfigure.submit(0)" ng-disabled="serviceConfigureForm.$invalid || serviceConfigure.busy">
<i class="fa fa-circle-notch fa-spin" ng-show="serviceConfigure.busy"></i> Reset to defaults
</button>
<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="serviceConfigure.submit(serviceConfigure.memoryLimit)" ng-disabled="serviceConfigureForm.$invalid || serviceConfigure.busy">
<i class="fa fa-circle-notch fa-spin" ng-show="serviceConfigure.busy"></i> Save
</button>
</div>
</div>
</div>
</div>
<div class="content">
<div class="text-left">
<h1>System</h1>
</div>
<div class="text-left">
<h3>Services</h3>
</div>
<div class="card" style="margin-bottom: 15px;">
<div class="row">
<div class="col-md-12">
<p>
Cloudron services implement functionality such as databases, email and authentication.<br/>
</p>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="row ng-hide" ng-show="!ready">
<div class="col-md-12 text-center">
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
</div>
</div>
<div class="row animateMeOpacity ng-hide" ng-show="ready">
<div class="col-md-12">
<table class="table table-hover">
<thead>
<tr>
<th style="width: 5%;"></th>
<th style="width: 20%">Service</th>
<th style="width: 50%">Memory Usage</th>
<th style="width: 20%" class="text-center">Memory Limit</th>
<th style="width: 5%" class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="service in services | orderBy:'name'">
<td>
<i class="fa fa-circle" uib-tooltip="{{ service.status }}" ng-style="{ color: service.status === 'active' ? '#27CE65' : (service.status === 'starting' ? '#f0ad4e' : '#d9534f') }" ng-show="service.status"></i>
<i class="fa fa-circle-notch fa-spin" ng-hide="service.status"></i>
</td>
<td class="elide-table-cell">
{{ service.name }}
</td>
<td class="elide-table-cell">
<div class="progress progress-striped" ng-show="service.config.memory">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ service.memoryPercent }}%"></div>
</div>
</td>
<td class="elide-table-cell text-center">
<span ng-show="service.config.memory">{{ service.config.memory / 1024 / 1024 + ' MB' }}</span>
</td>
<td class="text-right no-wrap" style="vertical-align: bottom">
<button class="btn btn-xs btn-default" ng-click="serviceConfigure.show(service)" uib-tooltip="Configure Memory Limit" ng-show="service.config.memory"><i class="fa fa-pencil-alt"></i></button>
<a class="btn btn-xs btn-default" ng-href="{{ '/logs.html?id=' + service.name }}" target="_blank" uib-tooltip="Logs"><i class="fa fa-file-alt"></i></a>
<button class="btn btn-xs btn-default" ng-click="restartService(service.name)" uib-tooltip="Restart"><i class="fa fa-sync-alt" ng-class="{ 'fa-spin': service.status === 'starting' }"></i></button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</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 Cloudron requires a reboot, to finalize 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()">Reboot</button>
</div>
</div>
</div>
</div>
-156
View File
@@ -1,156 +0,0 @@
'use strict';
/* global angular:false */
/* global $:false */
angular.module('Application').controller('SystemController', ['$scope', '$location', '$interval', 'Client', function ($scope, $location, $interval, Client) {
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
$scope.config = Client.getConfig();
$scope.ready = false;
$scope.services = [];
$scope.isRebootRequired = false;
function refresh(serviceName, callback) {
callback = callback || function () {};
Client.getService(serviceName, function (error, result) {
if (error) Client.error(error);
var service = $scope.services.find(function (s) { return s.name === serviceName; });
if (!service) $scope.services.push(result);
service.status = result.status;
service.config = result.config;
service.memoryUsed = result.memoryUsed;
service.memoryPercent = result.memoryPercent;
callback(null, service);
});
}
function refreshAll() {
$scope.services.forEach(function (s) {
refresh(s.name);
});
}
$scope.reboot = {
busy: false,
show: function () {
$scope.reboot.busy = false;
$('#rebootModal').modal('show');
},
submit: function () {
$scope.reboot.busy = true;
Client.reboot(function (error) {
$scope.reboot.busy = false;
if (error) return Client.error(error);
$('#rebootModal').modal('hide');
});
}
};
$scope.restartService = function (serviceName) {
function waitForActive(serviceName) {
refresh(serviceName, function (error, result) {
if (result.status === 'active') return;
setTimeout(function () { waitForActive(serviceName); }, 3000);
});
}
$scope.services.find(function (s) { return s.name === serviceName; }).status = 'starting';
Client.restartService(serviceName, function (error) {
if (error) return Client.error(error);
// show "busy" indicator for 3 seconds to show some ui activity
setTimeout(function () { waitForActive(serviceName); }, 3000);
});
};
$scope.serviceConfigure = {
error: null,
busy: false,
service: null,
// form model
memoryLimit: 0,
memoryTicks: [],
show: function (service) {
$scope.serviceConfigure.reset();
$scope.serviceConfigure.service = service;
$scope.serviceConfigure.memoryLimit = service.config.memory;
// TODO improve those
$scope.serviceConfigure.memoryTicks = [];
// create ticks starting from manifest memory limit. the memory limit here is currently split into ram+swap (and thus *2 below)
// TODO: the *2 will overallocate since 4GB is max swap that cloudron itself allocates
$scope.serviceConfigure.memoryTicks = [];
var npow2 = Math.pow(2, Math.ceil(Math.log($scope.config.memory)/Math.log(2)));
for (var i = 256; i <= (npow2*2/1024/1024); i *= 2) {
$scope.serviceConfigure.memoryTicks.push(i * 1024 * 1024);
}
$('#serviceConfigureModal').modal('show');
},
submit: function (memoryLimit) {
$scope.serviceConfigure.busy = true;
$scope.serviceConfigure.error = null;
Client.configureService($scope.serviceConfigure.service.name, memoryLimit, function (error) {
$scope.serviceConfigure.busy = false;
if (error) {
$scope.serviceConfigure.error = error.message;
return;
}
refresh($scope.serviceConfigure.service.name);
$('#serviceConfigureModal').modal('hide');
$scope.serviceConfigure.reset();
});
},
reset: function () {
$scope.serviceConfigure.busy = false;
$scope.serviceConfigure.error = null;
$scope.serviceConfigure.service = null;
$scope.serviceConfigure.memoryLimit = 0;
$scope.serviceConfigure.memoryTicks = [];
$scope.serviceConfigureForm.$setPristine();
$scope.serviceConfigureForm.$setUntouched();
}
};
Client.onReady(function () {
Client.isRebootRequired(function (error, result) {
if (error) console.error(error);
$scope.isRebootRequired = !!result;
Client.getServices(function (error, result) {
if (error) return Client.error(error);
$scope.services = result.map(function (serviceName) { return { name: serviceName }; });
// just kick off the status fetching
refreshAll();
$scope.ready = true;
});
});
});
}]);
+5 -5
View File
@@ -36,7 +36,7 @@
</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="clientAdd.submit()" ng-disabled="clientAddForm.$invalid || clientAdd.busy"><i class="fa fa-circle-notch fa-spin" ng-show="clientAdd.busy"></i> Add OAuth Client</button>
<button type="button" class="btn btn-success" ng-click="clientAdd.submit()" ng-disabled="clientAddForm.$invalid || clientAdd.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="clientAdd.busy"></i> Add OAuth Client</button>
</div>
</div>
</div>
@@ -57,7 +57,7 @@
</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="clientRemove.submit()" ng-disabled="clientRemove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="clientRemove.busy"></i> Remove OAuth Client</button>
<button type="button" class="btn btn-danger" ng-click="clientRemove.submit()" ng-disabled="clientRemove.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="clientRemove.busy"></i> Remove OAuth Client</button>
</div>
</div>
</div>
@@ -93,7 +93,7 @@
<h4 class="text-muted">Active Tokens</h4>
<hr/>
<p ng-repeat="token in apiClient.activeTokens">
<span ng-click-select>{{ token.accessToken }}</span> <button class="btn btn-xs btn-danger pull-right" ng-click="removeToken(apiClient, token)" title="Revoke Token"><i class="far fa-trash-alt"></i></button>
<span ng-click-select>{{ token.accessToken }}</span> <button class="btn btn-xs btn-danger pull-right" ng-click="removeToken(apiClient, token)" title="Revoke Token"><i class="fa fa-trash-o"></i></button>
</p>
</div>
</div>
@@ -136,7 +136,7 @@
<h4 class="text-muted">Tokens
<div class="pull-right">
<button class="btn btn-xs btn-default" ng-click="removeAccessTokens(client)" ng-disabled="!client.activeTokens.length || client.busy"><i class="fa fa-circle-notch fa-spin" ng-show="client.busy"></i> Revoke All</button>
<button class="btn btn-xs btn-default" ng-click="removeAccessTokens(client)" ng-disabled="!client.activeTokens.length || client.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="client.busy"></i> Revoke All</button>
<button class="btn btn-xs btn-primary btn-outline" ng-click="tokenAdd.show(client)"><i class="fa fa-plus"></i> New Token</button>
</div>
</h4>
@@ -144,7 +144,7 @@
<hr/>
<p ng-repeat="token in client.activeTokens">
<b ng-click-select>{{ token.accessToken }}</b> <button class="btn btn-xs btn-danger pull-right" ng-click="removeToken(client, token)" title="Revoke Token"><i class="far fa-trash-alt"></i></button>
<b ng-click-select>{{ token.accessToken }}</b> <button class="btn btn-xs btn-danger pull-right" ng-click="removeToken(client, token)" title="Revoke Token"><i class="fa fa-trash-o"></i></button>
</p>
</div>
</div>
+29 -6
View File
@@ -1,9 +1,6 @@
'use strict';
/* global angular:false */
/* global $:false */
angular.module('Application').controller('TokensController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
angular.module('Application').controller('TokensController', ['$scope', 'Client', function ($scope, Client) {
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
$scope.user = Client.getUserInfo();
@@ -58,6 +55,13 @@ angular.module('Application').controller('TokensController', ['$scope', '$locati
} else {
console.error(error);
}
return;
} else if (error && error.statusCode === 412) {
var actionScope = $scope.$new(true);
actionScope.action = '/#/settings';
Client.notify('Not allowed', 'You have to enable the external API in the settings.', false, 'error', actionScope);
return;
} else if (error) return console.error('Unable to create API client.', error.statusCode, error.message);
@@ -83,7 +87,17 @@ angular.module('Application').controller('TokensController', ['$scope', '$locati
Client.delOAuthClient($scope.clientRemove.client.id, function (error) {
$scope.clientRemove.busy = false;
if (error) return console.error(error);
if (error && error.statusCode === 412) {
var actionScope = $scope.$new(true);
actionScope.action = '/#/settings';
Client.notify('Not allowed', 'You have to enable the external API in the settings.', false, 'error', actionScope);
return;
} else if (error) {
return console.error(error);
}
$scope.clientRemove.client = {};
@@ -105,7 +119,16 @@ angular.module('Application').controller('TokensController', ['$scope', '$locati
var expiresAt = Date.now() + 100 * 365 * 24 * 60 * 60 * 1000; // ~100 years from now
Client.createTokenByClientId(client.id, '*' /* scope */, expiresAt, '' /* name */, function (error, result) {
if (error) return console.error(error);
if (error && error.statusCode === 412) {
var actionScope = $scope.$new(true);
actionScope.action = '/#/settings';
Client.notify('Not allowed', 'You have to enable the external API in the settings.', false, 'error', actionScope);
return;
} else if (error) {
return console.error(error);
}
$scope.tokenAdd.busy = false;
$scope.tokenAdd.token = result;
+51 -66
View File
@@ -62,7 +62,7 @@
</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="useradd.submit()" ng-disabled="useradd_form.$invalid || useradd.busy"><i class="fa fa-circle-notch fa-spin" ng-show="useradd.busy"></i> Add User</button>
<button type="button" class="btn btn-success" ng-click="useradd.submit()" ng-disabled="useradd_form.$invalid || useradd.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="useradd.busy"></i> Add User</button>
</div>
</div>
</div>
@@ -91,7 +91,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" ng-click="userremove.submit()" ng-disabled="userremove_form.$invalid || userremove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="userremove.busy"></i> Delete</button>
<button type="button" class="btn btn-danger" ng-click="userremove.submit()" ng-disabled="userremove_form.$invalid || userremove.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="userremove.busy"></i> Delete</button>
</div>
</div>
</div>
@@ -151,7 +151,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-success" ng-click="useredit.submit()" ng-disabled="useredit_form.$invalid || useredit.busy"><i class="fa fa-circle-notch fa-spin" ng-show="useredit.busy"></i> Save</button>
<button type="button" class="btn btn-success" ng-click="useredit.submit()" ng-disabled="useredit_form.$invalid || useredit.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="useredit.busy"></i> Save</button>
</div>
</div>
</div>
@@ -187,7 +187,7 @@
</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="groupAdd.submit()" ng-disabled="groupAddForm.$invalid || groupAdd.busy"><i class="fa fa-circle-notch fa-spin" ng-show="groupAdd.busy"></i> Add Group</button>
<button type="button" class="btn btn-success" ng-click="groupAdd.submit()" ng-disabled="groupAddForm.$invalid || groupAdd.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="groupAdd.busy"></i> Add Group</button>
</div>
</div>
</div>
@@ -217,7 +217,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-success" ng-click="groupEdit.submit()" ng-disabled="groupEdit_form.$invalid || groupEdit.busy"><i class="fa fa-circle-notch fa-spin" ng-show="groupEdit.busy"></i> Save</button>
<button type="button" class="btn btn-success" ng-click="groupEdit.submit()" ng-disabled="groupEdit_form.$invalid || groupEdit.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="groupEdit.busy"></i> Save</button>
</div>
</div>
</div>
@@ -252,7 +252,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" ng-click="groupRemove.submit()" ng-disabled="groupRemoveForm.$invalid || groupRemove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="groupRemove.busy"></i> Delete</button>
<button type="button" class="btn btn-danger" ng-click="groupRemove.submit()" ng-disabled="groupRemoveForm.$invalid || groupRemove.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="groupRemove.busy"></i> Delete</button>
</div>
</div>
</div>
@@ -277,7 +277,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-success" ng-click="invitation.email()" ng-disabled="invitation.busy"><i class="fa fa-circle-notch fa-spin" ng-show="invitation.busy"></i> Email link to user</button>
<button type="button" class="btn btn-success" ng-click="invitation.email()" ng-disabled="invitation.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="invitation.busy"></i> Email link to user</button>
</div>
</div>
</div>
@@ -295,67 +295,52 @@
</h1>
</div>
<div class="row">
<div class="col-lg-12">
<div class="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>
<div class="pagination pull-right">
<button class="btn btn-default btn-outline" ng-click="showPrevPage()" ng-disabled="userRefreshBusy || currentPage <= 1"><i class="fa fa-angle-double-left"></i> prev</button>
<button class="btn btn-default btn-outline" ng-click="showNextPage()" ng-disabled="userRefreshBusy || users.length < pageItems.value">next <i class="fa fa-angle-double-right"></i></button>
</div>
</div>
</div>
<div>
<div class="card card-large">
<div class="card card-large">
<div class="grid-item-top">
<div class="row ng-hide" ng-show="userRefreshBusy">
<div class="col-lg-12 text-center">
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
<div class="row ng-hide" ng-show="!ready">
<div class="col-lg-12 text-center">
<h2><i class="fa fa-circle-o-notch fa-spin"></i></h2>
</div>
</div>
</div>
<div class="row ng-hide" ng-hide="userRefreshBusy">
<div class="col-lg-12">
<table class="table table-hover" style="margin: 0;">
<thead>
<tr>
<th style="width: 0.5%;"></th>
<th style="width:45%">User</th>
<th style="width:49.5%" class="hidden-xs hidden-sm">Groups</th>
<th style="width: 5%" class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="user in users">
<td>
<i class="fa fa-briefcase arrow" ng-show="user.admin" uib-tooltip="This user can manage apps, groups and other users"></i>
</td>
<td class="hand elide-table-cell" ng-click="useredit.show(user)" ng-show="user.username">
{{ user.displayName }} &nbsp; <span class="text-muted">{{ user.username }}</span>
</td>
<td class="hand elide-table-cell" ng-click="useredit.show(user)" ng-hide="user.username">
<span class="text-muted" uib-tooltip="User is not activated yet">{{ user.fallbackEmail }}</span>
</td>
<td class="text-left hand elide-table-cell hidden-xs hidden-sm" ng-click="useredit.show(user)">
<span class="group-badge" ng-repeat="groupId in user.groupIds">
{{ groupsById[groupId].name }}
</span>
</td>
<div class="row animateMeOpacity ng-hide" ng-show="ready">
<div class="col-lg-12">
<table class="table table-hover">
<thead>
<tr>
<th style="width: 0.5%;"></th>
<th style="width:45%">User</th>
<th style="width:49.5%" class="hidden-xs hidden-sm">Groups</th>
<th style="width: 5%" class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="user in users">
<td>
<i class="fa fa-briefcase arrow" ng-show="user.admin" uib-tooltip="This user can manage apps, groups and other users"></i>
</td>
<td class="hand elide-table-cell" ng-click="useredit.show(user)" ng-show="user.username">
{{ user.displayName }} &nbsp; <span class="text-muted">{{ user.username }}</span>
</td>
<td class="hand elide-table-cell" ng-click="useredit.show(user)" ng-hide="user.username">
<span class="text-muted" uib-tooltip="User is not activated yet">{{ user.fallbackEmail }}</span>
</td>
<td class="text-left hand elide-table-cell hidden-xs hidden-sm" ng-click="useredit.show(user)">
<span class="group-badge" ng-repeat="groupId in user.groupIds">
{{ groupsById[groupId].name }}
</span>
</td>
<td class="text-right no-wrap" style="vertical-align: bottom">
<button ng-show="!isMe(user)" class="btn btn-xs btn-default" ng-click="invitation.show(user)" uib-tooltip="Create setup link"><i class="fa fa-paper-plane"></i></button>
<button class="btn btn-xs btn-default" ng-click="useredit.show(user)" uib-tooltip="Edit User"><i class="fa fa-pencil-alt"></i></button>
<button ng-show="!isMe(user)" class="btn btn-xs btn-danger" ng-click="userremove.show(user)" uib-tooltip="Remove User"><i class="far fa-trash-alt"></i></button>
</td>
</tr>
</tbody>
</table>
<td class="text-right no-wrap" style="vertical-align: bottom">
<button ng-show="!isMe(user)" class="btn btn-xs btn-default" ng-click="invitation.show(user)" title="Create setup link"><i class="fa fa-paper-plane-o"></i></button>
<button class="btn btn-xs btn-default" ng-click="useredit.show(user)" title="Edit User"><i class="fa fa-pencil"></i></button>
<button ng-show="!isMe(user)" class="btn btn-xs btn-danger" ng-click="userremove.show(user)" title="Remove User"><i class="fa fa-trash-o"></i></button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<br/>
@@ -371,7 +356,7 @@
<div class="grid-item-top">
<div class="row ng-hide" ng-show="!ready">
<div class="col-lg-12 text-center">
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
<h2><i class="fa fa-circle-o-notch fa-spin"></i></h2>
</div>
</div>
<div class="row animateMeOpacity ng-hide" ng-show="ready">
@@ -393,8 +378,8 @@
{{ groupMembers(group) }}
</td>
<td class="text-right" style="vertical-align: bottom">
<button class="btn btn-xs btn-default" ng-click="groupEdit.show(group)" uib-tooltip="Edit Group"><i class="fa fa-pencil-alt"></i></button>
<button class="btn btn-xs btn-danger" ng-click="groupRemove.show(group)" uib-tooltip="Remove Group"><i class="far fa-trash-alt"></i></button>
<button class="btn btn-xs btn-default" ng-click="groupEdit.show(group)" title="Edit Group"><i class="fa fa-pencil"></i></button>
<button class="btn btn-xs btn-danger" ng-click="groupRemove.show(group)" title="Remove Group"><i class="fa fa-trash-o"></i></button>
</td>
</tr>
</tbody>
+17 -51
View File
@@ -2,9 +2,8 @@
/* global angular:false */
/* global Clipboard:false */
/* global asyncForEachParallel:false */
/* global asyncForEach:false */
/* global asyncSeries:false */
/* global $:false */
angular.module('Application').controller('UsersController', ['$scope', '$location', '$timeout', 'Client', function ($scope, $location, $timeout, Client) {
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
@@ -17,16 +16,6 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
$scope.config = Client.getConfig();
$scope.userInfo = Client.getUserInfo();
$scope.userSearchString = '';
$scope.currentPage = 1;
$scope.pageItemCount = [
{ name: 'Show 20 per page', value: 20 },
{ name: 'Show 50 per page', value: 50 },
{ name: 'Show 100 per page', value: 100 }
];
$scope.pageItems = $scope.pageItemCount[0];
$scope.userRefreshBusy = true;
$scope.groupMembers = function (group) {
return group.userIds.filter(function (uid) { return !!$scope.usersById[uid]; }).map(function (uid) { return $scope.usersById[uid].username || $scope.usersById[uid].email; }).join(' ');
};
@@ -451,12 +440,12 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
};
function getUsers(callback) {
var users = [];
var users = [ ];
Client.getUsers($scope.userSearchString, $scope.currentPage, $scope.pageItems.value, function (error, results) {
Client.getUsers(function (error, results) {
if (error) return console.error(error);
asyncForEachParallel(results, function (result, iteratorDone) {
asyncForEach(results, function (result, iteratorDone) {
Client.getUser(result.id, function (error, user) {
if (error) return iteratorDone(error);
@@ -471,12 +460,12 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
}
function getGroups(callback) {
var groups = [];
var groups = [ ];
Client.getGroups(function (error, results) {
if (error) return console.error(error);
asyncForEachParallel(results, function (result, iteratorDone) {
asyncForEach(results, function (result, iteratorDone) {
Client.getGroup(result.id, function (error, group) {
if (error) return iteratorDone(error);
@@ -490,23 +479,6 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
});
}
function refreshUsers() {
$scope.userRefreshBusy = true;
getUsers(function (error, result) {
if (error) return console.error('Unable to get user listing.', error);
angular.copy(result, $scope.users);
$scope.usersById = { };
for (var i = 0; i < result.length; i++) {
$scope.usersById[result[i].id] = result[i];
}
$scope.ready = true;
$scope.userRefreshBusy = false;
});
}
function refresh() {
getGroups(function (error, result) {
if (error) return console.error('Unable to get group listing.', error);
@@ -517,26 +489,20 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
$scope.groupsById[result[i].id] = result[i];
}
refreshUsers();
getUsers(function (error, result) {
if (error) return console.error('Unable to get user listing.', error);
angular.copy(result, $scope.users);
$scope.usersById = { };
for (var i = 0; i < result.length; i++) {
$scope.usersById[result[i].id] = result[i];
}
$scope.ready = true;
});
});
}
$scope.showNextPage = function () {
$scope.currentPage++;
refreshUsers();
};
$scope.showPrevPage = function () {
if ($scope.currentPage > 1) $scope.currentPage--;
else $scope.currentPage = 1;
refreshUsers();
};
$scope.updateFilter = function (fresh) {
if (fresh) $scope.currentPage = 1;
refreshUsers();
};
Client.onReady(refresh);
// setup all the dialog focus handling