Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb1326ac72 |
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"unused": true,
|
||||
"globalstrict": true,
|
||||
"predef": [ "angular", "$" ],
|
||||
"esnext": true
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
The Cloudron Subscription license
|
||||
Copyright (c) 2019 Cloudron UG
|
||||
Copyright (c) 2018 Cloudron UG
|
||||
|
||||
With regard to the Cloudron Software:
|
||||
|
||||
|
||||
@@ -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
@@ -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 }));
|
||||
|
||||
Generated
+1764
-2942
File diff suppressed because it is too large
Load Diff
+6
-11
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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}
|
||||
Vendored
+2337
File diff suppressed because it is too large
Load Diff
+4
File diff suppressed because one or more lines are too long
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
+2671
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 434 KiB |
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
-271
@@ -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>");}]);
|
||||
@@ -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
@@ -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">©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">©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 |
@@ -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
@@ -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">© 2019 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted">© 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>
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+30
-210
@@ -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
@@ -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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
'`': '`',
|
||||
'=': '='
|
||||
};
|
||||
|
||||
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
@@ -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
@@ -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 = '/';
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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
@@ -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
@@ -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">©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">©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
@@ -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">©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">©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
@@ -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> <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"/> 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">©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">©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
@@ -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
@@ -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
|
||||
// ----------------------------
|
||||
|
||||
@@ -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">©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
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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;"> </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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"/> 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"/> 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
@@ -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
@@ -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> 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>
|
||||
<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
@@ -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
@@ -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('/'); });
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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">×</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"> </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"> </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
@@ -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
@@ -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
@@ -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; });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
}]);
|
||||
@@ -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
@@ -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
@@ -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 }} <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 }} <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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user