Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0874c3839 |
@@ -2591,7 +2591,6 @@
|
||||
* support: fix crash when opening tickets with 0 length files
|
||||
|
||||
[7.4.0]
|
||||
* **IMPORTANT**: This is the last release of Cloudron to support Ubuntu 18.04. Please [upgrade](https://docs.cloudron.io/guides/upgrade-ubuntu-20/) to Ubuntu 20.04 (Focal Fossa) at the earliest.
|
||||
* Update base image to jammy
|
||||
* backups: Add idrive e2
|
||||
* Support proxyAuth for proxy app
|
||||
@@ -2629,93 +2628,3 @@
|
||||
* Fix ipv4 vs ipv6 detection
|
||||
* Fix misleading pending security updates message
|
||||
|
||||
[7.4.3]
|
||||
* **IMPORTANT**: This is the last release of Cloudron to support Ubuntu 18.04. Please [upgrade](https://docs.cloudron.io/guides/upgrade-ubuntu-20/) to Ubuntu 20.04 (Focal Fossa) at the earliest.
|
||||
* postgresql: fix for supporting Taiga with postgres 14
|
||||
|
||||
[7.5.0]
|
||||
* **IMPORTANT**: This is the last release of Cloudron to support CPUs without AVX support. AVX support is required for MongoDB 5.0. See https://forum.cloudron.io/topic/8785/avx-support-in-your-vps-server for more information.
|
||||
* acme: handle LE validation type cache logic
|
||||
* improve viewing of logs
|
||||
* redis: update to 7.0.11
|
||||
* ionos profitbricks: add new regions Berlin and Logrono
|
||||
* docker: update to 23.0.6
|
||||
* network: trusted IPs
|
||||
* mail: fix crash when editing quota of new mailboxes
|
||||
* mail: update haraka to 3.0.2
|
||||
* mail: fix issue where client IP was leaked in headers
|
||||
* mail: skip SPF check of authenticated senders
|
||||
* filemanager: new UI, support for large folders and lazy loading
|
||||
* oidc: make UI translatable
|
||||
* oidc: dashboard login uses oidc
|
||||
* web terminal: Copy selected terminal text with ctrl shift c
|
||||
* Expose alias domains as `CLOUDRON_ALIAS_DOMAINS`
|
||||
|
||||
[7.5.1]
|
||||
* **IMPORTANT**: This is the last release of Cloudron to support CPUs without AVX support. AVX support is required for MongoDB 5.0. See https://forum.cloudron.io/topic/8785/avx-support-in-your-vps-server for more information.
|
||||
* mail: Fix issue where mail usage sizes where reported incorrectly
|
||||
* filemanager: Only init vue app after we fetch language files to avoid UI shaking
|
||||
* mail: Clear the correct mail status notification
|
||||
* filemanager: allow pasting on non-folders to cwd
|
||||
* mail: give resolver more time
|
||||
* dashboard: backup logs links are grayed out because of z-index
|
||||
* branding: make oidc login does not use cloudron name
|
||||
* translation: fix crash when translated text has single quote (french)
|
||||
* dyndns: show logs
|
||||
* mail: server location get it's own section
|
||||
* optional services: redis & turn . joins sendmail, recvmail
|
||||
* backups: encrypted backups must have .enc extension
|
||||
* mail: add virtual all mail mailbox
|
||||
* redirections: use 301 (permanent) instead of 302 (temporary) for redirections. this is better for SEO links
|
||||
* graphs: show old backup size if > 1GB
|
||||
* docker: fix image pruning
|
||||
* Major overhaul of the REST API
|
||||
* Fix import via SSHFS and CIFS
|
||||
|
||||
[7.5.2]
|
||||
* mail: Fix default max mail size to 25MB (and not 25MiB)
|
||||
* dashboard: disable 2fa setup for external users
|
||||
* filemanager: Always show app or volume name
|
||||
* filemanager: fix logs button link
|
||||
* backups: add Contabo object storage
|
||||
* Fix incorrect migration of directory server setting
|
||||
* support: Add explicit billing issue ticket type
|
||||
* Fix broken directory server config migration
|
||||
* system: fix crash updating disk usage
|
||||
* Fix crash in renew certs call from cron
|
||||
|
||||
[7.6.0]
|
||||
* Update MongoDB to 5.0. Important: this release requires AVX support in CPU
|
||||
* turn: add ddos mitigation settings
|
||||
* api: return json when route not found
|
||||
* oidc: loginRedirectUri can be empty string
|
||||
* New base image 4.2.0. `cloudron/base:4.2.0@sha256:46da2fffb36353ef714f97ae8e962bd2c212ca091108d768ba473078319a47f4`
|
||||
* mail: add option to enable/disable Virtual All mailbox
|
||||
* volumes: edit options for network mounts
|
||||
* oidc: fix issue with redirects not working on iOS apps
|
||||
* app proxy: Host header is set to match the proxy domain instead of the target domain
|
||||
* notifications: Add color coded indicator to notifications
|
||||
* oidc: add oidc logo as login indicator for apps
|
||||
* dyndns: update dns every 10 mins
|
||||
|
||||
[7.6.1]
|
||||
* Cleanup backup validation mount point
|
||||
* dashboard: remove nginx config of old domain when domain changed
|
||||
* Show disk consumption of docker volumes for /run and /tmp of apps separately
|
||||
* dns: add dnsimple automation
|
||||
* roles: admin role can access branding and networking
|
||||
* dns: add ovh backend
|
||||
|
||||
[7.6.2]
|
||||
* mail: fix issue with redis emitting warnings non-stop
|
||||
* mail: fix issue where doublle header was sent
|
||||
* ovh: fix nameserver matching
|
||||
* logviewer: preserve horizontal scroll position
|
||||
* redis: use default instead of redisuser
|
||||
* dockerproxy: allow child containers to access volumes
|
||||
* dashboard: Show system information
|
||||
* Fix linode object storage
|
||||
* postgres: enable cube, vector and earthdistance extensions
|
||||
* Add ability to register a Cloudron with a setupToken only
|
||||
* support: replace ticket section with help section
|
||||
* firewall: increase blocklist size to 262144
|
||||
|
||||
@@ -50,6 +50,17 @@ the dashboard, database addons, graph container, base image etc. Cloudron also r
|
||||
on external services such as the App Store for apps to be installed. As such, don't
|
||||
clone this repo and npm install and expect something to work.
|
||||
|
||||
## Development
|
||||
|
||||
This is the backend code of Cloudron. The frontend code is [here](https://git.cloudron.io/cloudron/dashboard).
|
||||
|
||||
The way to develop is to first install a full instance of Cloudron in a VM. Then you can use the [hotfix](https://git.cloudron.io/cloudron/cloudron-machine)
|
||||
tool to patch the VM with the latest code.
|
||||
|
||||
```
|
||||
SSH_PASSPHRASE=sshkeypassword cloudron-machine hotfix --cloudron my.example.com --release 6.0.0 --ssh-key keyname
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Please note that the Cloudron code is under a source-available license. This is not the same as an
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const constants = require('./src/constants.js'),
|
||||
fs = require('fs'),
|
||||
const fs = require('fs'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
oidc = require('./src/oidc.js'),
|
||||
paths = require('./src/paths.js'),
|
||||
proxyAuth = require('./src/proxyauth.js'),
|
||||
safe = require('safetydance'),
|
||||
server = require('./src/server.js'),
|
||||
settings = require('./src/settings.js'),
|
||||
directoryServer = require('./src/directoryserver.js');
|
||||
|
||||
let logFd;
|
||||
|
||||
async function setupLogging() {
|
||||
if (constants.TEST) return;
|
||||
if (process.env.BOX_ENV === 'test') return;
|
||||
|
||||
logFd = fs.openSync(paths.BOX_LOG_FILE, 'a');
|
||||
// we used to write using a stream before but it caches internally and there is no way to flush it when things crash
|
||||
@@ -39,7 +39,7 @@ async function startServers() {
|
||||
await proxyAuth.start();
|
||||
await ldap.start();
|
||||
|
||||
const conf = await directoryServer.getConfig();
|
||||
const conf = await settings.getDirectoryServerConfig();
|
||||
if (conf.enabled) await directoryServer.start();
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ async function main() {
|
||||
|
||||
process.on('SIGHUP', async function () {
|
||||
debug('Received SIGHUP. Re-reading configs.');
|
||||
const conf = await directoryServer.getConfig();
|
||||
const conf = await settings.getDirectoryServerConfig();
|
||||
if (conf.enabled) await directoryServer.checkCertificate();
|
||||
});
|
||||
|
||||
@@ -79,6 +79,8 @@ async function main() {
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error) => exitSync({ error, code: 1 }));
|
||||
|
||||
console.log(`Cloudron is up and running. Logs are at ${paths.BOX_LOG_FILE}`); // this goes to journalctl
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
const database = require('./src/database.js');
|
||||
|
||||
const crashNotifier = require('./src/crashnotifier.js');
|
||||
|
||||
// This is triggered by systemd with the crashed unit name as argument
|
||||
async function main() {
|
||||
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <unitName>');
|
||||
|
||||
const unitName = process.argv[2];
|
||||
console.log('Started crash notifier for', unitName);
|
||||
|
||||
// eventlog api needs the db
|
||||
await database.initialize();
|
||||
|
||||
await crashNotifier.sendFailureLogs(unitName);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const argv = require('yargs').argv,
|
||||
var argv = require('yargs').argv,
|
||||
autoprefixer = require('gulp-autoprefixer'),
|
||||
concat = require('gulp-concat'),
|
||||
cssnano = require('gulp-cssnano'),
|
||||
ejs = require('gulp-ejs'),
|
||||
execSync = require('child_process').execSync,
|
||||
fs = require('fs'),
|
||||
gulp = require('gulp'),
|
||||
sass = require('gulp-sass')(require('sass')),
|
||||
rimraf = require('rimraf'),
|
||||
sass = require('gulp-sass')(require('node-sass')),
|
||||
serve = require('gulp-serve'),
|
||||
sourcemaps = require('gulp-sourcemaps');
|
||||
|
||||
@@ -53,11 +53,33 @@ gulp.task('bootstrap', function () {
|
||||
.pipe(gulp.dest('dist/3rdparty/js'));
|
||||
});
|
||||
|
||||
gulp.task('monaco', function () {
|
||||
return gulp.src('node_modules/monaco-editor/min/**/*')
|
||||
.pipe(gulp.dest('dist/3rdparty/'));
|
||||
});
|
||||
|
||||
gulp.task('moment', function () {
|
||||
return gulp.src('node_modules/moment/min/*')
|
||||
.pipe(gulp.dest('dist/3rdparty/js'));
|
||||
});
|
||||
|
||||
gulp.task('xterm-core', function () {
|
||||
return gulp.src('node_modules/xterm/**/*')
|
||||
.pipe(gulp.dest('dist/3rdparty/xterm'));
|
||||
});
|
||||
|
||||
gulp.task('xterm-addon-attach', function () {
|
||||
return gulp.src('node_modules/xterm-addon-attach/**/*')
|
||||
.pipe(gulp.dest('dist/3rdparty/xterm-addon-attach'));
|
||||
});
|
||||
|
||||
gulp.task('xterm-addon-fit', function () {
|
||||
return gulp.src('node_modules/xterm-addon-fit/**/*')
|
||||
.pipe(gulp.dest('dist/3rdparty/xterm-addon-fit'));
|
||||
});
|
||||
|
||||
gulp.task('xterm', gulp.series(['xterm-core', 'xterm-addon-attach', 'xterm-addon-fit']));
|
||||
|
||||
gulp.task('3rdparty-copy', function () {
|
||||
return gulp.src([
|
||||
'src/3rdparty/**/*.js',
|
||||
@@ -72,7 +94,7 @@ gulp.task('3rdparty-copy', function () {
|
||||
]).pipe(gulp.dest('dist/3rdparty/'));
|
||||
});
|
||||
|
||||
gulp.task('3rdparty', gulp.series(['3rdparty-copy', 'moment', 'bootstrap', 'fontawesome']));
|
||||
gulp.task('3rdparty', gulp.series(['3rdparty-copy', 'moment', 'monaco', 'xterm', 'bootstrap', 'fontawesome']));
|
||||
|
||||
// --------------
|
||||
// JavaScript
|
||||
@@ -82,6 +104,7 @@ gulp.task('js-index', function () {
|
||||
return gulp.src([
|
||||
'src/js/index.js',
|
||||
'src/js/client.js',
|
||||
'src/js/main.js',
|
||||
'src/js/utils.js',
|
||||
'src/views/*.js'
|
||||
])
|
||||
@@ -92,11 +115,38 @@ gulp.task('js-index', function () {
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-passwordreset', function () {
|
||||
return gulp.src(['src/js/passwordreset.js', 'src/js/utils.js'])
|
||||
gulp.task('js-logs', function () {
|
||||
return gulp.src(['src/js/logs.js', 'src/js/client.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('passwordreset.js', { newLine: ';' }))
|
||||
.pipe(concat('logs.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-filemanager', function () {
|
||||
return gulp.src(['src/js/filemanager.js', 'src/js/client.js', 'src/js/utils.js', 'src/components/*.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('filemanager.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-terminal', function () {
|
||||
return gulp.src(['src/js/terminal.js', 'src/js/client.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('terminal.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-login', function () {
|
||||
return gulp.src(['src/js/login.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('login.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
@@ -137,7 +187,7 @@ gulp.task('js-restore', function () {
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js', gulp.series([ 'js-index', 'js-passwordreset', 'js-setupaccount', 'js-setup', 'js-setupdns', 'js-restore' ]));
|
||||
gulp.task('js', gulp.series([ 'js-index', 'js-logs', 'js-filemanager', 'js-terminal', 'js-login', 'js-setupaccount', 'js-setup', 'js-setupdns', 'js-restore' ]));
|
||||
|
||||
// --------------
|
||||
// HTML
|
||||
@@ -147,6 +197,10 @@ gulp.task('html-views', function () {
|
||||
return gulp.src('src/views/**/*.html').pipe(gulp.dest('dist/views'));
|
||||
});
|
||||
|
||||
gulp.task('html-components', function () {
|
||||
return gulp.src('src/components/**/*.html').pipe(gulp.dest('dist/components'));
|
||||
});
|
||||
|
||||
gulp.task('html-templates', function () {
|
||||
return gulp.src('src/templates/**/*').pipe(gulp.dest('dist/templates'));
|
||||
});
|
||||
@@ -155,7 +209,7 @@ gulp.task('html-raw', function () {
|
||||
return gulp.src('src/*.html').pipe(ejs({ apiOrigin: apiOrigin, revision: revision }, {}, { ext: '.html' })).pipe(gulp.dest('dist'));
|
||||
});
|
||||
|
||||
gulp.task('html', gulp.series(['html-views', 'html-templates', 'html-raw']));
|
||||
gulp.task('html', gulp.series(['html-views', 'html-components', 'html-templates', 'html-raw']));
|
||||
|
||||
// --------------
|
||||
// CSS
|
||||
@@ -191,7 +245,8 @@ gulp.task('timezones', function (done) {
|
||||
// --------------
|
||||
|
||||
gulp.task('clean', function (done) {
|
||||
fs.rm('dist', { recursive: true, force: true }, done);
|
||||
rimraf.sync('dist');
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('default', gulp.series(['clean', 'html', 'js', 'timezones', '3rdparty', 'translation', 'images', 'css']));
|
||||
@@ -202,14 +257,18 @@ gulp.task('watch', function (done) {
|
||||
gulp.watch(['src/translation/*'], gulp.series(['translation']));
|
||||
gulp.watch(['src/**/*.html'], gulp.series(['html']));
|
||||
gulp.watch(['src/views/*.html'], gulp.series(['html-views']));
|
||||
gulp.watch(['src/components/*.html'], gulp.series(['html-components']));
|
||||
gulp.watch(['src/templates/*.html'], gulp.series(['html-templates']));
|
||||
gulp.watch(['scripts/createTimezones.js', 'src/js/utils.js'], gulp.series(['timezones']));
|
||||
gulp.watch(['src/js/setup.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-setup']));
|
||||
gulp.watch(['src/js/setupdns.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-setupdns']));
|
||||
gulp.watch(['src/js/restore.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-restore']));
|
||||
gulp.watch(['src/js/passwordreset.js', 'src/js/utils.js'], gulp.series(['js-passwordreset']));
|
||||
gulp.watch(['src/js/logs.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-logs']));
|
||||
gulp.watch(['src/js/filemanager.js', 'src/js/client.js', 'src/js/utils.js', 'src/components/*.js'], gulp.series(['js-filemanager']));
|
||||
gulp.watch(['src/js/terminal.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-terminal']));
|
||||
gulp.watch(['src/js/login.js', 'src/js/utils.js'], gulp.series(['js-login']));
|
||||
gulp.watch(['src/js/setupaccount.js', 'src/js/utils.js'], gulp.series(['js-setupaccount']));
|
||||
gulp.watch(['src/js/index.js', 'src/js/client.js', 'src/views/*.js', 'src/js/utils.js'], gulp.series(['js-index']));
|
||||
gulp.watch(['src/js/index.js', 'src/js/client.js', 'src/js/main.js', 'src/views/*.js', 'src/js/utils.js'], gulp.series(['js-index']));
|
||||
gulp.watch(['src/3rdparty/**/*'], gulp.series(['3rdparty']));
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
"author": "",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||
"bootstrap-sass": "^3.4.3",
|
||||
"chart.js": "^4.3.0",
|
||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||
"bootstrap-sass": "^3.4.1",
|
||||
"chart.js": "^4.1.1",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-autoprefixer": "^8.0.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
@@ -25,8 +25,13 @@
|
||||
"gulp-serve": "^1.4.0",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"sass": "^1.63.3",
|
||||
"yargs": "^17.7.2"
|
||||
"monaco-editor": "^0.34.0",
|
||||
"node-sass": "^7.0.3",
|
||||
"rimraf": "^3.0.2",
|
||||
"xterm": "^5.1.0",
|
||||
"xterm-addon-attach": "^0.8.0",
|
||||
"xterm-addon-fit": "^0.7.0",
|
||||
"yargs": "^17.5.1"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"env": {
|
||||
|
||||
@@ -24,7 +24,7 @@ function getAccessToken(callback) {
|
||||
let username = readlineSync.question('Username: ', {});
|
||||
let password = readlineSync.question('Password: ', { noEchoBack: true });
|
||||
|
||||
superagent.post(`https://${cloudronDomain}/api/v1/auth/login`, { username: username, password: password }).end(function (error, result) {
|
||||
superagent.post(`https://${cloudronDomain}/api/v1/cloudron/login`, { username: username, password: password }).end(function (error, result) {
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.log('Login failed');
|
||||
return getAccessToken(callback);
|
||||
@@ -63,4 +63,4 @@ getAccessToken(function (accessToken) {
|
||||
|
||||
console.log('Done');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,7 @@ function getAccessToken(callback) {
|
||||
let username = readlineSync.question('Username: ', {});
|
||||
let password = readlineSync.question('Password: ', { noEchoBack: true });
|
||||
|
||||
superagent.post(`https://${cloudronDomain}/api/v1/auth/login`, { username: username, password: password }).end(function (error, result) {
|
||||
superagent.post(`https://${cloudronDomain}/api/v1/cloudron/login`, { username: username, password: password }).end(function (error, result) {
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.log('Login failed');
|
||||
return getAccessToken(callback);
|
||||
@@ -66,4 +66,4 @@ getAccessToken(function (accessToken) {
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,639 @@
|
||||
(function($, angular) {
|
||||
|
||||
// eslint-disable-next-line angular/file-name, angular/no-service-method
|
||||
angular.module('ui.bootstrap.contextMenu', [])
|
||||
.service('CustomService', function () {
|
||||
'use strict';
|
||||
|
||||
return {
|
||||
initialize: function (item) {
|
||||
console.log('got here', item);
|
||||
}
|
||||
};
|
||||
|
||||
})
|
||||
.constant('ContextMenuEvents', {
|
||||
// Triggers when all the context menus have been closed
|
||||
ContextMenuAllClosed: 'context-menu-all-closed',
|
||||
// Triggers when any single conext menu is called.
|
||||
// Closing all context menus triggers this for each level open
|
||||
ContextMenuClosed: 'context-menu-closed',
|
||||
// Triggers right before the very first context menu is opened
|
||||
ContextMenuOpening: 'context-menu-opening',
|
||||
// Triggers right after any context menu is opened
|
||||
ContextMenuOpened: 'context-menu-opened'
|
||||
})
|
||||
.directive('contextMenu', ['$rootScope', 'ContextMenuEvents', '$parse', '$q', 'CustomService', '$sce', '$document', '$window', '$compile',
|
||||
function ($rootScope, ContextMenuEvents, $parse, $q, custom, $sce, $document, $window, $compile) {
|
||||
|
||||
var _contextMenus = [];
|
||||
// Contains the element that was clicked to show the context menu
|
||||
var _clickedElement = null;
|
||||
var DEFAULT_ITEM_TEXT = '"New Item';
|
||||
var _emptyText = 'empty';
|
||||
|
||||
function createAndAddOptionText(params) {
|
||||
// Destructuring:
|
||||
var $scope = params.$scope;
|
||||
var item = params.item;
|
||||
var event = params.event;
|
||||
var modelValue = params.modelValue;
|
||||
var $promises = params.$promises;
|
||||
var nestedMenu = params.nestedMenu;
|
||||
var $li = params.$li;
|
||||
var leftOriented = String(params.orientation).toLowerCase() === 'left';
|
||||
|
||||
var optionText = null;
|
||||
|
||||
if (item.html) {
|
||||
if (angular.isFunction(item.html)) {
|
||||
// runs the function that expects a jQuery/jqLite element
|
||||
optionText = item.html($scope);
|
||||
} else {
|
||||
// Incase we want to compile html string to initialize their custom directive in html string
|
||||
if (item.compile) {
|
||||
optionText = $compile(item.html)($scope);
|
||||
} else {
|
||||
// Assumes that the developer already placed a valid jQuery/jqLite element
|
||||
optionText = item.html;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
var $a = $('<a>');
|
||||
var $anchorStyle = {};
|
||||
|
||||
if (leftOriented) {
|
||||
$anchorStyle.textAlign = 'right';
|
||||
$anchorStyle.paddingLeft = '8px';
|
||||
} else {
|
||||
$anchorStyle.textAlign = 'left';
|
||||
$anchorStyle.paddingRight = '8px';
|
||||
}
|
||||
|
||||
$a.css($anchorStyle);
|
||||
$a.addClass('dropdown-item');
|
||||
$a.attr({ tabindex: '-1', href: '#' });
|
||||
|
||||
var textParam = item.text || item[0];
|
||||
var text = DEFAULT_ITEM_TEXT;
|
||||
|
||||
if (typeof textParam === 'string') {
|
||||
text = textParam;
|
||||
} else if (typeof textParam === 'function') {
|
||||
text = textParam.call($scope, $scope, event, modelValue);
|
||||
}
|
||||
|
||||
var $promise = $q.when(text);
|
||||
$promises.push($promise);
|
||||
$promise.then(function (pText) {
|
||||
if (nestedMenu) {
|
||||
var $arrow;
|
||||
var $boldStyle = {
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: 'bold'
|
||||
};
|
||||
|
||||
if (leftOriented) {
|
||||
$arrow = '<';
|
||||
$boldStyle.float = 'left';
|
||||
} else {
|
||||
$arrow = '>';
|
||||
$boldStyle.float = 'right';
|
||||
}
|
||||
|
||||
var $bold = $('<strong style="font-family:monospace;font-weight:bold;float:right;">' + $arrow + '</strong>');
|
||||
$bold.css($boldStyle);
|
||||
$a.css('cursor', 'default');
|
||||
$a.append($bold);
|
||||
}
|
||||
$a.append(pText);
|
||||
});
|
||||
|
||||
optionText = $a;
|
||||
}
|
||||
|
||||
$li.append(optionText);
|
||||
|
||||
return optionText;
|
||||
};
|
||||
|
||||
/**
|
||||
* Process each individual item
|
||||
*
|
||||
* Properties of params:
|
||||
* - $scope
|
||||
* - event
|
||||
* - modelValue
|
||||
* - level
|
||||
* - item
|
||||
* - $ul
|
||||
* - $li
|
||||
* - $promises
|
||||
*/
|
||||
function processItem(params) {
|
||||
var nestedMenu = extractNestedMenu(params);
|
||||
|
||||
// if html property is not defined, fallback to text, otherwise use default text
|
||||
// if first item in the item array is a function then invoke .call()
|
||||
// if first item is a string, then text should be the string.
|
||||
|
||||
var text = DEFAULT_ITEM_TEXT;
|
||||
var currItemParam = angular.extend({}, params);
|
||||
var item = params.item;
|
||||
var enabled = item.enabled === undefined ? item[2] : item.enabled;
|
||||
|
||||
currItemParam.nestedMenu = nestedMenu;
|
||||
currItemParam.enabled = resolveBoolOrFunc(enabled, params);
|
||||
currItemParam.text = createAndAddOptionText(currItemParam);
|
||||
|
||||
registerCurrentItemEvents(currItemParam);
|
||||
|
||||
};
|
||||
|
||||
/*
|
||||
* Registers the appropriate mouse events for options if the item is enabled.
|
||||
* Otherwise, it ensures that clicks to the item do not propagate.
|
||||
*/
|
||||
function registerCurrentItemEvents (params) {
|
||||
// Destructuring:
|
||||
var item = params.item;
|
||||
var $ul = params.$ul;
|
||||
var $li = params.$li;
|
||||
var $scope = params.$scope;
|
||||
var modelValue = params.modelValue;
|
||||
var level = params.level;
|
||||
var event = params.event;
|
||||
var text = params.text;
|
||||
var nestedMenu = params.nestedMenu;
|
||||
var enabled = params.enabled;
|
||||
var orientation = String(params.orientation).toLowerCase();
|
||||
var customClass = params.customClass;
|
||||
|
||||
if (enabled) {
|
||||
var openNestedMenu = function ($event) {
|
||||
removeContextMenus(level + 1);
|
||||
/*
|
||||
* The object here needs to be constructed and filled with data
|
||||
* on an "as needed" basis. Copying the data from event directly
|
||||
* or cloning the event results in unpredictable behavior.
|
||||
*/
|
||||
/// adding the original event in the object to use the attributes of the mouse over event in the promises
|
||||
var ev = {
|
||||
pageX: orientation === 'left' ? event.pageX - $ul[0].offsetWidth + 1 : event.pageX + $ul[0].offsetWidth - 1,
|
||||
pageY: $ul[0].offsetTop + $li[0].offsetTop - 3,
|
||||
// eslint-disable-next-line angular/window-service
|
||||
view: event.view || window,
|
||||
target: event.target,
|
||||
event: $event
|
||||
};
|
||||
|
||||
/*
|
||||
* At this point, nestedMenu can only either be an Array or a promise.
|
||||
* Regardless, passing them to `when` makes the implementation singular.
|
||||
*/
|
||||
$q.when(nestedMenu).then(function(promisedNestedMenu) {
|
||||
if (angular.isFunction(promisedNestedMenu)) {
|
||||
// support for dynamic subitems
|
||||
promisedNestedMenu = promisedNestedMenu.call($scope, $scope, event, modelValue, text, $li);
|
||||
}
|
||||
var nestedParam = {
|
||||
$scope : $scope,
|
||||
event : ev,
|
||||
options : promisedNestedMenu,
|
||||
modelValue : modelValue,
|
||||
level : level + 1,
|
||||
orientation: orientation,
|
||||
customClass: customClass
|
||||
};
|
||||
renderContextMenu(nestedParam);
|
||||
});
|
||||
};
|
||||
|
||||
$li.on('click', function ($event) {
|
||||
if($event.which == 1) {
|
||||
$event.preventDefault();
|
||||
$scope.$apply(function () {
|
||||
|
||||
var cleanupFunction = function () {
|
||||
$(event.currentTarget).removeClass('context');
|
||||
removeAllContextMenus();
|
||||
};
|
||||
var clickFunction = angular.isFunction(item.click)
|
||||
? item.click
|
||||
: (angular.isFunction(item[1])
|
||||
? item[1]
|
||||
: null);
|
||||
|
||||
if (clickFunction) {
|
||||
var res = clickFunction.call($scope, $scope, event, modelValue, text, $li);
|
||||
if(res === undefined || res) {
|
||||
cleanupFunction();
|
||||
}
|
||||
} else {
|
||||
cleanupFunction();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$li.on('mouseover', function ($event) {
|
||||
$scope.$apply(function () {
|
||||
if (nestedMenu) {
|
||||
openNestedMenu($event);
|
||||
} else {
|
||||
removeContextMenus(level + 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
setElementDisabled($li);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param params - an object containing the `item` parameter
|
||||
* @returns an Array or a Promise containing the children,
|
||||
* or null if the option has no submenu
|
||||
*/
|
||||
function extractNestedMenu(params) {
|
||||
// Destructuring:
|
||||
var item = params.item;
|
||||
|
||||
// New implementation:
|
||||
if (item.children) {
|
||||
if (angular.isFunction(item.children)) {
|
||||
// Expects a function that returns a Promise or an Array
|
||||
return item.children();
|
||||
} else if (angular.isFunction(item.children.then) || angular.isArray(item.children)) {
|
||||
// Returns the promise
|
||||
// OR, returns the actual array
|
||||
return item.children;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
} else {
|
||||
// nestedMenu is either an Array or a Promise that will return that array.
|
||||
// NOTE: This might be changed soon as it's a hangover from an old implementation
|
||||
|
||||
return angular.isArray(item[1]) ||
|
||||
(item[1] && angular.isFunction(item[1].then)) ? item[1] : angular.isArray(item[2]) ||
|
||||
(item[2] && angular.isFunction(item[2].then)) ? item[2] : angular.isArray(item[3]) ||
|
||||
(item[3] && angular.isFunction(item[3].then)) ? item[3] : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for the actual rendering of the context menu.
|
||||
*
|
||||
* The parameters in params are:
|
||||
* - $scope = the scope of this context menu
|
||||
* - event = the event that triggered this context menu
|
||||
* - options = the options for this context menu
|
||||
* - modelValue = the value of the model attached to this context menu
|
||||
* - level = the current context menu level (defauts to 0)
|
||||
* - customClass = the custom class to be used for the context menu
|
||||
*/
|
||||
function renderContextMenu (params) {
|
||||
/// <summary>Render context menu recursively.</summary>
|
||||
|
||||
// Destructuring:
|
||||
var $scope = params.$scope;
|
||||
var event = params.event;
|
||||
var options = params.options;
|
||||
var modelValue = params.modelValue;
|
||||
var level = params.level;
|
||||
var customClass = params.customClass;
|
||||
|
||||
// Initialize the container. This will be passed around
|
||||
var $ul = initContextMenuContainer(params);
|
||||
params.$ul = $ul;
|
||||
|
||||
// Register this level of the context menu
|
||||
_contextMenus.push($ul);
|
||||
|
||||
/*
|
||||
* This object will contain any promises that we have
|
||||
* to wait for before trying to adjust the context menu.
|
||||
*/
|
||||
var $promises = [];
|
||||
params.$promises = $promises;
|
||||
|
||||
angular.forEach(options, function (item) {
|
||||
|
||||
if (item === null) {
|
||||
appendDivider($ul);
|
||||
} else {
|
||||
// If displayed is anything other than a function or a boolean
|
||||
var displayed = resolveBoolOrFunc(item.displayed, params);
|
||||
|
||||
// Only add the <li> if the item is displayed
|
||||
if (displayed) {
|
||||
var $li = $('<li>');
|
||||
var itemParams = angular.extend({}, params);
|
||||
itemParams.item = item;
|
||||
itemParams.$li = $li;
|
||||
|
||||
if (typeof item[0] === 'object') {
|
||||
custom.initialize($li, item);
|
||||
} else {
|
||||
processItem(itemParams);
|
||||
}
|
||||
if (resolveBoolOrFunc(item.hasTopDivider, itemParams, false)) {
|
||||
appendDivider($ul);
|
||||
}
|
||||
$ul.append($li);
|
||||
if (resolveBoolOrFunc(item.hasBottomDivider, itemParams, false)) {
|
||||
appendDivider($ul);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ($ul.children().length === 0) {
|
||||
var $emptyLi = angular.element('<li>');
|
||||
setElementDisabled($emptyLi);
|
||||
$emptyLi.html('<a>' + _emptyText + '</a>');
|
||||
$ul.append($emptyLi);
|
||||
}
|
||||
|
||||
$document.find('body').append($ul);
|
||||
|
||||
doAfterAllPromises(params);
|
||||
|
||||
$rootScope.$broadcast(ContextMenuEvents.ContextMenuOpened, {
|
||||
context: _clickedElement,
|
||||
contextMenu: $ul,
|
||||
params: params
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* calculate if drop down menu would go out of screen at left or bottom
|
||||
* calculation need to be done after element has been added (and all texts are set; thus the promises)
|
||||
* to the DOM the get the actual height
|
||||
*/
|
||||
function doAfterAllPromises (params) {
|
||||
|
||||
// Desctructuring:
|
||||
var $ul = params.$ul;
|
||||
var $promises = params.$promises;
|
||||
var level = params.level;
|
||||
var event = params.event;
|
||||
var leftOriented = String(params.orientation).toLowerCase() === 'left';
|
||||
|
||||
$q.all($promises).then(function () {
|
||||
var topCoordinate = event.pageY;
|
||||
var menuHeight = angular.element($ul[0]).prop('offsetHeight');
|
||||
var winHeight = $window.pageYOffset + event.view.innerHeight;
|
||||
|
||||
/// the 20 pixels in second condition are considering the browser status bar that sometimes overrides the element
|
||||
if (topCoordinate > menuHeight && winHeight - topCoordinate < menuHeight + 20) {
|
||||
topCoordinate = event.pageY - menuHeight;
|
||||
/// If the element is a nested menu, adds the height of the parent li to the topCoordinate to align with the parent
|
||||
if(level && level > 0) {
|
||||
topCoordinate += event.event.currentTarget.offsetHeight;
|
||||
}
|
||||
} else if(winHeight <= menuHeight) {
|
||||
// If it really can't fit, reset the height of the menu to one that will fit
|
||||
angular.element($ul[0]).css({ 'height': winHeight - 5, 'overflow-y': 'scroll' });
|
||||
// ...then set the topCoordinate height to 0 so the menu starts from the top
|
||||
topCoordinate = 0;
|
||||
} else if(winHeight - topCoordinate < menuHeight) {
|
||||
var reduceThresholdY = 5;
|
||||
if(topCoordinate < reduceThresholdY) {
|
||||
reduceThresholdY = topCoordinate;
|
||||
}
|
||||
topCoordinate = winHeight - menuHeight - reduceThresholdY;
|
||||
}
|
||||
|
||||
var leftCoordinate = event.pageX;
|
||||
var menuWidth = angular.element($ul[0]).prop('offsetWidth');
|
||||
var winWidth = event.view.innerWidth + window.pageXOffset;
|
||||
var padding = 5;
|
||||
|
||||
if (leftOriented) {
|
||||
if (winWidth - leftCoordinate > menuWidth && leftCoordinate < menuWidth + padding) {
|
||||
leftCoordinate = padding;
|
||||
} else if (leftCoordinate < menuWidth) {
|
||||
var reduceThresholdX = 5;
|
||||
if (winWidth - leftCoordinate < reduceThresholdX + padding) {
|
||||
reduceThresholdX = winWidth - leftCoordinate + padding;
|
||||
}
|
||||
leftCoordinate = menuWidth + reduceThresholdX + padding;
|
||||
} else {
|
||||
leftCoordinate = leftCoordinate - menuWidth;
|
||||
}
|
||||
} else {
|
||||
if (leftCoordinate > menuWidth && winWidth - leftCoordinate - padding < menuWidth) {
|
||||
leftCoordinate = winWidth - menuWidth - padding;
|
||||
} else if(winWidth - leftCoordinate < menuWidth) {
|
||||
var reduceThresholdX = 5;
|
||||
if(leftCoordinate < reduceThresholdX + padding) {
|
||||
reduceThresholdX = leftCoordinate + padding;
|
||||
}
|
||||
leftCoordinate = winWidth - menuWidth - reduceThresholdX - padding;
|
||||
}
|
||||
}
|
||||
|
||||
$ul.css({
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
left: leftCoordinate + 'px',
|
||||
top: topCoordinate + 'px'
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the container of the context menu (a <ul> element),
|
||||
* applies the appropriate styles and then returns that container
|
||||
*
|
||||
* @return a <ul> jqLite/jQuery element
|
||||
*/
|
||||
function initContextMenuContainer(params) {
|
||||
// Destructuring
|
||||
var customClass = params.customClass;
|
||||
|
||||
var $ul = $('<ul>');
|
||||
$ul.addClass('dropdown-menu');
|
||||
$ul.attr({ 'role': 'menu' });
|
||||
$ul.css({
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
left: params.event.pageX + 'px',
|
||||
top: params.event.pageY + 'px',
|
||||
'z-index': 10000
|
||||
});
|
||||
|
||||
if(customClass) { $ul.addClass(customClass); }
|
||||
|
||||
return $ul;
|
||||
}
|
||||
|
||||
function isTouchDevice() {
|
||||
return 'ontouchstart' in window || navigator.maxTouchPoints; // works on most browsers | works on IE10/11 and Surface
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the context menus with level greater than or equal
|
||||
* to the value passed. If undefined, null or 0, all context menus
|
||||
* are removed.
|
||||
*/
|
||||
function removeContextMenus (level) {
|
||||
while (_contextMenus.length && (!level || _contextMenus.length > level)) {
|
||||
var cm = _contextMenus.pop();
|
||||
$rootScope.$broadcast(ContextMenuEvents.ContextMenuClosed, { context: _clickedElement, contextMenu: cm });
|
||||
cm.remove();
|
||||
}
|
||||
if(!level) {
|
||||
$rootScope.$broadcast(ContextMenuEvents.ContextMenuAllClosed, { context: _clickedElement });
|
||||
}
|
||||
}
|
||||
|
||||
function removeOnScrollEvent(e) {
|
||||
removeAllContextMenus(e);
|
||||
}
|
||||
|
||||
function removeOnOutsideClickEvent(e) {
|
||||
|
||||
var $curr = $(e.target);
|
||||
var shouldRemove = true;
|
||||
|
||||
while($curr.length) {
|
||||
if ($curr.hasClass('dropdown-menu')) {
|
||||
shouldRemove = false;
|
||||
break;
|
||||
} else {
|
||||
$curr = $curr.parent();
|
||||
}
|
||||
}
|
||||
if (shouldRemove) {
|
||||
removeAllContextMenus(e);
|
||||
}
|
||||
}
|
||||
|
||||
function removeAllContextMenus(e) {
|
||||
$document.find('body').off('mousedown touchstart', removeOnOutsideClickEvent);
|
||||
$document.off('scroll', removeOnScrollEvent);
|
||||
$(_clickedElement).removeClass('context');
|
||||
removeContextMenus();
|
||||
$rootScope.$broadcast('');
|
||||
}
|
||||
|
||||
function isBoolean(a) {
|
||||
return a === false || a === true;
|
||||
}
|
||||
|
||||
/** Resolves a boolean or a function that returns a boolean
|
||||
* Returns true by default if the param is null or undefined
|
||||
* @param a - the parameter to be checked
|
||||
* @param params - the object for the item's parameters
|
||||
* @param defaultValue - the default boolean value to use if the parameter is
|
||||
* neither a boolean nor function. True by default.
|
||||
*/
|
||||
function resolveBoolOrFunc(a, params, defaultValue) {
|
||||
var item = params.item;
|
||||
var $scope = params.$scope;
|
||||
var event = params.event;
|
||||
var modelValue = params.modelValue;
|
||||
|
||||
defaultValue = isBoolean(defaultValue) ? defaultValue : true;
|
||||
|
||||
if (isBoolean(a)) {
|
||||
return a;
|
||||
} else if (angular.isFunction(a)) {
|
||||
return a.call($scope, $scope, event, modelValue);
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
function appendDivider($ul) {
|
||||
var $li = angular.element('<li>');
|
||||
$li.addClass('divider');
|
||||
$ul.append($li);
|
||||
}
|
||||
|
||||
function setElementDisabled($li) {
|
||||
$li.on('click', function ($event) {
|
||||
$event.preventDefault();
|
||||
});
|
||||
$li.addClass('disabled');
|
||||
}
|
||||
|
||||
return function ($scope, element, attrs) {
|
||||
var openMenuEvents = ['contextmenu'];
|
||||
_emptyText = $scope.$eval(attrs.contextMenuEmptyText) || 'empty';
|
||||
|
||||
if(attrs.contextMenuOn && typeof(attrs.contextMenuOn) === 'string'){
|
||||
openMenuEvents = attrs.contextMenuOn.split(',');
|
||||
}
|
||||
|
||||
angular.forEach(openMenuEvents, function (openMenuEvent) {
|
||||
element.on(openMenuEvent.trim(), function (event) {
|
||||
// Cleanup any leftover contextmenus(there are cases with longpress on touch where we
|
||||
// still see multiple contextmenus)
|
||||
removeAllContextMenus();
|
||||
|
||||
if(!attrs.allowEventPropagation) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// Don't show context menu if on touch device and element is draggable
|
||||
if(isTouchDevice() && element.attr('draggable') === 'true') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove if the user clicks outside
|
||||
$document.find('body').on('mousedown touchstart', removeOnOutsideClickEvent);
|
||||
// Remove the menu when the scroll moves
|
||||
$document.on('scroll', removeOnScrollEvent);
|
||||
|
||||
_clickedElement = event.currentTarget;
|
||||
$(_clickedElement).addClass('context');
|
||||
|
||||
$scope.$apply(function () {
|
||||
var options = $scope.$eval(attrs.contextMenu);
|
||||
var customClass = attrs.contextMenuClass;
|
||||
var modelValue = $scope.$eval(attrs.model);
|
||||
var orientation = attrs.contextMenuOrientation;
|
||||
|
||||
$q.when(options).then(function(promisedMenu) {
|
||||
if (angular.isFunction(promisedMenu)) {
|
||||
// support for dynamic items
|
||||
promisedMenu = promisedMenu.call($scope, $scope, event, modelValue);
|
||||
}
|
||||
var params = {
|
||||
'$scope' : $scope,
|
||||
'event' : event,
|
||||
'options' : promisedMenu,
|
||||
'modelValue' : modelValue,
|
||||
'level' : 0,
|
||||
'customClass' : customClass,
|
||||
'orientation': orientation
|
||||
};
|
||||
$rootScope.$broadcast(ContextMenuEvents.ContextMenuOpening, { context: _clickedElement });
|
||||
renderContextMenu(params);
|
||||
});
|
||||
});
|
||||
|
||||
// Remove all context menus if the scope is destroyed
|
||||
$scope.$on('$destroy', function () {
|
||||
removeAllContextMenus();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (attrs.closeMenuOn) {
|
||||
$scope.$on(attrs.closeMenuOn, function () {
|
||||
removeAllContextMenus();
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
// eslint-disable-next-line angular/window-service
|
||||
})(window.angular.element, window.angular);
|
||||
@@ -1,32 +0,0 @@
|
||||
// Custom library to add password show/hide icons to input element with `password-reveal` attribute
|
||||
// util.js has the angular version, this is for plain js
|
||||
|
||||
window.addEventListener('load', function () {
|
||||
var svgEye = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye" class="svg-inline--fa fa-eye fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"></path></svg>';
|
||||
var svgEyeSlash = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye-slash" class="svg-inline--fa fa-eye-slash fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"></path></svg>';
|
||||
|
||||
document.querySelectorAll('[password-reveal]').forEach(function (element) {
|
||||
var eye = document.createElement('i');
|
||||
eye.innerHTML = svgEyeSlash;
|
||||
eye.style.width = '18px';
|
||||
eye.style.height = '18px';
|
||||
eye.style.position = 'relative';
|
||||
eye.style.float = 'right';
|
||||
eye.style.marginTop = '-24px';
|
||||
eye.style.marginRight = '10px';
|
||||
eye.style.cursor = 'pointer';
|
||||
|
||||
eye.addEventListener('click', function () {
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
eye.innerHTML = svgEye;
|
||||
} else {
|
||||
element.type = 'password';
|
||||
eye.innerHTML = svgEyeSlash;
|
||||
}
|
||||
});
|
||||
|
||||
element.parentNode.style.position = 'relative';
|
||||
element.parentNode.insertBefore(eye, element.nextSibling);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
<script>
|
||||
|
||||
var tmp = window.location.hash.slice(1).split('&');
|
||||
|
||||
tmp.forEach(function (pair) {
|
||||
if (pair.indexOf('access_token=') === 0) localStorage.token = pair.split('=')[1];
|
||||
});
|
||||
|
||||
window.location.href = '/';
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,80 @@
|
||||
|
||||
<!-- Modal image/video viewer -->
|
||||
<div class="modal fade" id="{{ 'mediaViewerModal-' + $id }}" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" style="max-width: 1280px; max-height: calc(100% - 60px);">
|
||||
<div class="modal-content" style="height: 100%; height: 100%; display: flex; background-color: #000; background-clip: border-box;">
|
||||
<img ng-show="mediaViewer.type === 'image'" ng-src="{{ mediaViewer.src }}" style="display: block; margin: auto; max-width: 100%; max-height: 100%;" />
|
||||
<video ng-show="mediaViewer.type === 'video'" controls preload="auto" autoplay ng-src="{{ mediaViewer.src | trustUrl}}" style="display: block; margin: auto; max-width: 100%; max-height: 100%;"></video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- main content -->
|
||||
|
||||
<div class="toolbar">
|
||||
<div class="btn-group" role="group" style="display: block;">
|
||||
<!-- TODO figure out why a line break in code between the two buttons results in a gap visually without any margin/padding set -->
|
||||
<button class="btn btn-primary" ng-click="goDirectoryUp()" ng-disabled="cwd === ''"><i class="fas fa-arrow-left"></i></button><button class="btn btn-primary" ng-disabled="busyRefresh" ng-click="refresh()"><i class="fas fa-redo" ng-class="{ 'fa-spin': busyRefresh }"></i></button>
|
||||
</div>
|
||||
<div class="btn-group path-parts" role="group">
|
||||
<button class="btn btn-default" ng-disabled="cwd === ''" ng-click="changeDirectory('/')" ng-drop="drop($event, '/')" ng-dragleave="dragExit($event, '/')" ng-dragover="dragEnter($event, '/')"><i class="fas fa-home"></i> {{ rootDirLabel }} </button><button class="btn btn-default" ng-disabled="part.path === cwd" ng-click="changeDirectory(part.path)" ng-drop="drop($event, part.path)" ng-dragleave="dragExit($event, part.path)" ng-dragover="dragEnter($event, part.path)" ng-repeat="part in cwdParts">{{ part.name }}</button>
|
||||
</div>
|
||||
<div style="display: block;">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fas fa-plus"></i> {{ 'filemanager.toolbar.new' | tr }}</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="hand" ng-click="onNewFile()">{{ 'filemanager.toolbar.newFile' | tr }}</a></li>
|
||||
<li><a class="hand" ng-click="onNewFolder()">{{ 'filemanager.toolbar.newFolder' | tr }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fas fa-upload"></i> {{ 'filemanager.toolbar.upload' | tr }}</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a class="hand" ng-click="onUploadFile()">{{ 'filemanager.toolbar.uploadFile' | tr }}</a></li>
|
||||
<li><a class="hand" ng-click="onUploadFolder()">{{ 'filemanager.toolbar.uploadFolder' | tr }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-list-header">
|
||||
<table class="table" style="margin: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 42px"> </th>
|
||||
<th style="">{{ 'filemanager.list.name' | tr }}</th>
|
||||
<th style="width:100px">{{ 'filemanager.list.owner' | tr }}</th>
|
||||
<th style="width: 80px">{{ 'filemanager.list.size' | tr }}</th>
|
||||
<th style="width:100px">{{ 'filemanager.list.mtime' | tr }}</th>
|
||||
<th style="width: 45px;"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="file-list" ng-class="{ 'entry-hovered': dropToBody, 'busy': busy }" context-menu="menuOptionsBlank" ng-mousedown="onClearSelection($event)" ng-drop="drop($event, null)" ng-dragleave="dragExit($event, null)" ng-dragover="dragEnter($event, null)">
|
||||
<table class="table table-hover" style="margin: 0;">
|
||||
<tbody>
|
||||
<tr ng-show="busy && !busyRefresh">
|
||||
<td colspan="6"><center><h2><i class="fa fa-circle-notch fa-spin"></i></h2></center></td>
|
||||
</tr>
|
||||
<tr ng-show="!(busy && !busyRefresh) && entries.length === 0">
|
||||
<td colspan="" class="text-center">{{ 'filemanager.list.empty' | tr }}</td>
|
||||
</tr>
|
||||
<tr style="cursor: default" ng-hide="busy && !busyRefresh" entry-hashkey="{{ entry['$$hashKey'] }}" ng-repeat="entry in entries" ng-mouseup="onMouseup($event, entry)" draggable="true" ng-dragstart="dragStart($event, entry)" ng-drop="drop($event, entry)" context-menu="menuOptions" ng-mousedown="onMousedown($event, entry)" model="entry" ng-dragleave="dragExit($event, entry)" ng-dragover="dragEnter($event, entry)" ng-class="{ 'entry-hovered': entry.hovered, 'entry-selected': isSelected(entry) }">
|
||||
<td style="width: 42px; height: 42px" ng-dblclick="open(entry)" class="text-center">
|
||||
<i ng-show="!entry.previewUrl" class="fas fa-lg {{ entry.icon }}" ng-class="{ 'text-primary': entry.isDirectory && !isSelected(entry) }"></i>
|
||||
<img ng-show="entry.previewUrl" ng-src="{{ entry.previewUrl }}" height="42" width="42" style="object-fit: cover;"/>
|
||||
</td>
|
||||
<td class="elide-table-cell" ng-dblclick="open(entry)" style="padding-left: 5px;">{{ entry.fileName }}<span ng-show="entry.isSymbolicLink" class="text-muted" style="margin-left: 20px;">{{ 'filemanager.list.symlink' | tr:{ target: entry.target } }}</span></td>
|
||||
<td style="width:100px" class="elide-table-cell" ng-dblclick="open(entry)">{{ entry.uid | prettyOwner }}</td>
|
||||
<td style="width: 80px" class="elide-table-cell" ng-dblclick="open(entry)">{{ entry.size | prettyDecimalSize }}</td>
|
||||
<td style="width:100px" class="elide-table-cell" ng-dblclick="open(entry)" uib-tooltip="{{ entry.mtime | prettyLongDate }}" tooltip-append-to-body="true">{{ entry.mtime | prettyDate }}</td>
|
||||
<td style="width: 45px">
|
||||
<button type="button" class="btn btn-xs btn-default context-menu-action" context-menu="menuOptions" model="entry" context-menu-on="click" ng-click="onEntryContextMenu($event, entry)"><i class="fas fa-ellipsis-h"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,513 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global sanitize, isModalVisible */
|
||||
|
||||
angular.module('Application').component('filetree', {
|
||||
bindings: {
|
||||
backendId: '<',
|
||||
backendType: '<',
|
||||
view: '<',
|
||||
clipboard: '<',
|
||||
onUploadFile: '&',
|
||||
onUploadFolder: '&',
|
||||
onNewFile: '&',
|
||||
onNewFolder: '&',
|
||||
onRenameEntry: '&',
|
||||
onExtractEntry: '&',
|
||||
onChownEntries: '&',
|
||||
onDeleteEntries: '&',
|
||||
onCopyEntries: '&',
|
||||
onCutEntries: '&',
|
||||
onPasteEntries: '&'
|
||||
},
|
||||
templateUrl: 'components/filetree.html?<%= revision %>',
|
||||
controller: [ '$scope', '$translate', '$timeout', 'Client', FileTreeController ]
|
||||
});
|
||||
|
||||
function FileTreeController($scope, $translate, $timeout, Client) {
|
||||
var ctrl = this;
|
||||
|
||||
$scope.backendId = this.backendId;
|
||||
$scope.backendType = this.backendType;
|
||||
$scope.view = this.view;
|
||||
|
||||
$scope.busy = true;
|
||||
$scope.busyRefresh = false;
|
||||
$scope.client = Client;
|
||||
$scope.cwd = null;
|
||||
$scope.cwdParts = [];
|
||||
$scope.rootDirLabel = '';
|
||||
$scope.entries = [];
|
||||
$scope.selected = []; // holds selected entries
|
||||
$scope.dropToBody = false;
|
||||
$scope.applicationLink = '';
|
||||
|
||||
// register so parent can call child
|
||||
$scope.$parent.registerChild($scope);
|
||||
|
||||
function isArchive(f) {
|
||||
return f.match(/\.tgz$/) ||
|
||||
f.match(/\.tar$/) ||
|
||||
f.match(/\.7z$/) ||
|
||||
f.match(/\.zip$/) ||
|
||||
f.match(/\.tar\.gz$/) ||
|
||||
f.match(/\.tar\.xz$/) ||
|
||||
f.match(/\.tar\.bz2$/);
|
||||
}
|
||||
|
||||
$scope.menuOptions = []; // shown for entries
|
||||
$scope.menuOptionsBlank = []; // shown for empty space in folder
|
||||
|
||||
function sort() {
|
||||
return $scope.entries.sort(function (a, b) {
|
||||
if (a.fileName.toLowerCase() < b.fileName.toLowerCase()) return -1;
|
||||
return 1;
|
||||
}).sort(function (a, b) {
|
||||
if ((a.isDirectory && b.isDirectory) || (!a.isDirectory && !b.isDirectory)) return 0;
|
||||
if (a.isDirectory && !b.isDirectory) return -1;
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.isSelected = function (entry) {
|
||||
return $scope.selected.indexOf(entry) !== -1;
|
||||
};
|
||||
|
||||
function download(entry) {
|
||||
var filePath = sanitize($scope.cwd + '/' + entry.fileName);
|
||||
|
||||
Client.filesGet($scope.backendId, $scope.backendType, filePath, 'download', function (error) {
|
||||
if (error) return Client.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.dragStart = function ($event, entry) {
|
||||
var filePaths = $scope.selected.map(function (entry) { return sanitize($scope.cwd + '/' + entry.fileName); });
|
||||
$event.originalEvent.dataTransfer.setData('application/cloudron-filemanager', JSON.stringify(filePaths));
|
||||
};
|
||||
|
||||
$scope.dragEnter = function ($event, entry) {
|
||||
$event.originalEvent.stopPropagation();
|
||||
$event.originalEvent.preventDefault();
|
||||
|
||||
// if entry is string, we come from breadcrumb
|
||||
if (entry && typeof entry === 'string') $event.currentTarget.classList.add('entry-hovered');
|
||||
else if (entry && entry.isDirectory) entry.hovered = true;
|
||||
else $scope.dropToBody = true;
|
||||
|
||||
$event.originalEvent.dataTransfer.dropEffect = 'move';
|
||||
};
|
||||
|
||||
$scope.dragExit = function ($event, entry) {
|
||||
$event.originalEvent.stopPropagation();
|
||||
$event.originalEvent.preventDefault();
|
||||
|
||||
// if entry is string, we come from breadcrumb
|
||||
if (entry && typeof entry === 'string') $event.currentTarget.classList.remove('entry-hovered');
|
||||
else if (entry && entry.isDirectory) entry.hovered = false;
|
||||
$scope.dropToBody = false;
|
||||
|
||||
$event.originalEvent.dataTransfer.dropEffect = 'move';
|
||||
};
|
||||
|
||||
$scope.drop = function (event, entry) {
|
||||
event.originalEvent.stopPropagation();
|
||||
event.originalEvent.preventDefault();
|
||||
|
||||
$scope.dropToBody = false;
|
||||
|
||||
if (!event.originalEvent.dataTransfer.items[0]) return;
|
||||
|
||||
var targetFolder;
|
||||
if (entry === null) targetFolder = $scope.cwd + '/';
|
||||
else if (typeof entry === 'string') targetFolder = sanitize(entry);
|
||||
else targetFolder = sanitize($scope.cwd + '/' + (entry && entry.isDirectory ? entry.fileName : ''));
|
||||
|
||||
var dataTransfer = event.originalEvent.dataTransfer;
|
||||
var dragContent = dataTransfer.getData('application/cloudron-filemanager');
|
||||
|
||||
// check if we have internal drag'n'drop
|
||||
if (dragContent) {
|
||||
var moved = 0;
|
||||
|
||||
// we expect a JSON.stringified Array here
|
||||
try {
|
||||
dragContent = JSON.parse(dragContent);
|
||||
} catch (e) {
|
||||
console.error('Wrong drag content.', e);
|
||||
return;
|
||||
}
|
||||
|
||||
// move files
|
||||
async.eachLimit(dragContent, 5, function (oldFilePath, callback) {
|
||||
var fileName = oldFilePath.split('/').slice(-1);
|
||||
var newFilePath = sanitize(targetFolder + '/' + fileName);
|
||||
|
||||
// if we drop the item on itself
|
||||
if (oldFilePath === targetFolder) return callback();
|
||||
|
||||
// if nothing changes
|
||||
if (newFilePath === oldFilePath) return callback();
|
||||
|
||||
moved++;
|
||||
|
||||
// TODO this will overwrite files in destination!
|
||||
Client.filesRename($scope.backendId, $scope.backendType, oldFilePath, newFilePath, callback);
|
||||
}, function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
// only refresh if anything has changed
|
||||
if (moved) $scope.refresh();
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// figure if a folder was dropped on a modern browser, in this case the first would have to be a directory
|
||||
var folderItem;
|
||||
try {
|
||||
folderItem = dataTransfer.items[0].webkitGetAsEntry();
|
||||
if (folderItem.isFile) return $scope.$parent.uploadFiles(event.originalEvent.dataTransfer.files, targetFolder, false);
|
||||
} catch (e) {
|
||||
return $scope.$parent.uploadFiles(event.originalEvent.dataTransfer.files, targetFolder, false);
|
||||
}
|
||||
|
||||
// if we got here we have a folder drop and a modern browser
|
||||
// now traverse the folder tree and create a file list
|
||||
var fileList = [];
|
||||
function traverseFileTree(item, path, callback) {
|
||||
if (item.isFile) {
|
||||
// Get file
|
||||
item.file(function (file) {
|
||||
fileList.push(file);
|
||||
callback();
|
||||
});
|
||||
} else if (item.isDirectory) {
|
||||
// Get folder contents
|
||||
var dirReader = item.createReader();
|
||||
dirReader.readEntries(function (entries) {
|
||||
async.each(entries, function (entry, callback) {
|
||||
traverseFileTree(entry, path + item.name + '/', callback);
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
traverseFileTree(folderItem, '', function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.$parent.uploadFiles(fileList, targetFolder, false);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.refresh = function () {
|
||||
$scope.$parent.refresh();
|
||||
};
|
||||
|
||||
function amendIcons() {
|
||||
$scope.entries.forEach(function (e) {
|
||||
e.icon = 'fa-file';
|
||||
e.previewUrl = null;
|
||||
|
||||
if (e.isDirectory) e.icon = 'fa-folder';
|
||||
if (e.isSymbolicLink) e.icon = 'fa-link';
|
||||
if (e.isFile) {
|
||||
var mimeType = Mimer().get(e.fileName.toLowerCase());
|
||||
var mimeGroup = mimeType.split('/')[0];
|
||||
if (mimeGroup === 'text') e.icon = 'fa-file-alt';
|
||||
// if (mimeGroup === 'image') e.icon = 'fa-file-image';
|
||||
if (mimeGroup === 'image') {
|
||||
e.icon = 'fa-file-image';
|
||||
e.previewUrl = Client.filesGetLink($scope.backendId, $scope.backendType, sanitize($scope.cwd + '/' + e.fileName));
|
||||
}
|
||||
if (mimeGroup === 'video') e.icon = 'fa-file-video';
|
||||
if (mimeGroup === 'audio') e.icon = 'fa-file-audio';
|
||||
if (mimeType === 'text/csv') e.icon = 'fa-file-csv';
|
||||
if (mimeType === 'application/pdf') e.icon = 'fa-file-pdf';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// called from the parent
|
||||
$scope.onRefresh = function () {
|
||||
$scope.selected = [];
|
||||
$scope.busy = true;
|
||||
$scope.busyRefresh = true;
|
||||
|
||||
Client.filesGet($scope.backendId, $scope.backendType, $scope.cwd, 'data', function (error, result) {
|
||||
if (error && error.statusCode !== 404) return Client.error(error);
|
||||
|
||||
$scope.entries = result ? result.entries : [];
|
||||
amendIcons();
|
||||
sort();
|
||||
|
||||
$scope.busyRefresh = false;
|
||||
$scope.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
function openDirectory(path) {
|
||||
$scope.cwd = path;
|
||||
$scope.selected = [];
|
||||
|
||||
$scope.cwdParts = path.split('/').filter(function (p) { return !!p; }).map(function (p, i) { return { name: decodeURIComponent(p), path: path.split('/').slice(0, i+1).join('/') }; });
|
||||
|
||||
// refresh will set busy to false once done
|
||||
$scope.refresh();
|
||||
}
|
||||
|
||||
function openFile(entry) {
|
||||
var mimeType = Mimer().get(entry.fileName);
|
||||
var mimeGroup = mimeType.split('/')[0];
|
||||
var path = sanitize($scope.cwd + '/' + entry.fileName);
|
||||
|
||||
if (mimeGroup === 'video' || mimeGroup === 'image') {
|
||||
$scope.mediaViewer.show(entry);
|
||||
} else if (mimeType === 'application/pdf') {
|
||||
Client.filesGet($scope.backendId, $scope.backendType, path, 'open', function (error) { if (error) return Client.error(error); });
|
||||
} else if (mimeGroup === 'text' || mimeGroup === 'application') {
|
||||
$scope.$parent.textEditor.show($scope.cwd, entry);
|
||||
} else {
|
||||
Client.filesGet($scope.backendId, $scope.backendType, path, 'open', function (error) { if (error) return Client.error(error); });
|
||||
}
|
||||
|
||||
$scope.busy = false;
|
||||
}
|
||||
|
||||
$scope.open = function (entry) {
|
||||
if (entry.isDirectory) openDirectory(sanitize($scope.cwd + '/' + entry.fileName));
|
||||
else if (entry.isFile) openFile(entry);
|
||||
};
|
||||
|
||||
$scope.goDirectoryUp = function () {
|
||||
openDirectory(sanitize($scope.cwd + '/..'));
|
||||
};
|
||||
|
||||
$scope.changeDirectory = function (path) {
|
||||
openDirectory(sanitize(path));
|
||||
};
|
||||
|
||||
$scope.onClearSelection = function ($event) {
|
||||
// we don't stop propagation if targets don't match we got the whole list click event
|
||||
if ($event.currentTarget !== $event.target) return;
|
||||
|
||||
$scope.selected = [];
|
||||
};
|
||||
|
||||
$scope.onMousedown = function ($event, entry) {
|
||||
if ($event.button === 2) {
|
||||
$scope.onMouseup($event, entry);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onMouseup = function ($event, entry) {
|
||||
var i = $scope.selected.indexOf(entry);
|
||||
var multi = ($event.ctrlKey || $event.metaKey);
|
||||
var shift = $event.shiftKey;
|
||||
|
||||
if (shift) {
|
||||
if ($scope.selected.length === 0) {
|
||||
$scope.selected = [ entry ];
|
||||
} else {
|
||||
var pos = $scope.entries.indexOf(entry);
|
||||
var selectedPositions = $scope.selected.map(function (s) { return $scope.entries.indexOf(s); }).sort();
|
||||
|
||||
if (pos < selectedPositions[0]) {
|
||||
$scope.selected = $scope.entries.slice(pos, selectedPositions[0]+1);
|
||||
} else if (selectedPositions[1] && pos > selectedPositions[1]) {
|
||||
$scope.selected = $scope.entries.slice(selectedPositions[1], pos+1);
|
||||
} else {
|
||||
$scope.selected = $scope.entries.slice(selectedPositions[0], pos+1);
|
||||
}
|
||||
}
|
||||
} else if (multi) {
|
||||
if (i === -1) {
|
||||
$scope.selected.push(entry);
|
||||
} else if ($event.button === 0) { // only do this on left click
|
||||
$scope.selected.splice(i, 1);
|
||||
}
|
||||
} else {
|
||||
$scope.selected = [ entry ];
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onEntryContextMenu = function ($event, entry) {
|
||||
if ($scope.selected.indexOf(entry) !== -1) return;
|
||||
$scope.selected.push(entry);
|
||||
};
|
||||
|
||||
$scope.actionSelectAll = function () {
|
||||
$scope.selected = $scope.entries.slice();
|
||||
};
|
||||
|
||||
// just events to the parent controller
|
||||
$scope.onUploadFile = function () { ctrl.onUploadFile({ cwd: $scope.cwd }); };
|
||||
$scope.onUploadFolder = function () { ctrl.onUploadFolder({ cwd: $scope.cwd }); };
|
||||
$scope.onNewFile = function () { ctrl.onNewFile({ cwd: $scope.cwd }); };
|
||||
$scope.onNewFolder = function () { ctrl.onNewFolder({ cwd: $scope.cwd }); };
|
||||
|
||||
$scope.mediaViewer = {
|
||||
type: '',
|
||||
src: '',
|
||||
entry: null,
|
||||
|
||||
show: function (entry) {
|
||||
var filePath = sanitize($scope.cwd + '/' + entry.fileName);
|
||||
|
||||
$scope.mediaViewer.entry = entry;
|
||||
$scope.mediaViewer.type = Mimer().get(entry.fileName).split('/')[0];
|
||||
$scope.mediaViewer.src = Client.filesGetLink($scope.backendId, $scope.backendType, filePath);
|
||||
|
||||
$('#mediaViewerModal-' + $scope.$id).modal('show');
|
||||
},
|
||||
|
||||
close: function () {
|
||||
// set an empty pixel image to bust the cached img to avoid flickering on slow load
|
||||
$scope.mediaViewer.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==';
|
||||
|
||||
$('#mediaViewerModal-' + $scope.$id).modal('hide');
|
||||
}
|
||||
};
|
||||
|
||||
$translate(['filemanager.list.menu.edit', 'filemanager.list.menu.cut', 'filemanager.list.menu.copy', 'filemanager.list.menu.paste', 'filemanager.list.menu.rename', 'filemanager.list.menu.chown', 'filemanager.list.menu.extract', 'filemanager.list.menu.download', 'filemanager.list.menu.delete' ]).then(function (tr) {
|
||||
$scope.menuOptions = [
|
||||
{
|
||||
text: tr['filemanager.list.menu.edit'],
|
||||
displayed: function ($itemScope, $event, entry) { return !entry.isDirectory && !entry.isSymbolicLink; },
|
||||
enabled: function () { return $scope.selected.length === 1; },
|
||||
hasBottomDivider: true,
|
||||
click: function ($itemScope, $event, entry) { $scope.open(entry); }
|
||||
}, {
|
||||
text: tr['filemanager.list.menu.cut'],
|
||||
click: function ($itemScope, $event, entry) { ctrl.onCutEntries({ cwd: $scope.cwd, entries: $scope.selected.slice() }); }
|
||||
}, {
|
||||
text: tr['filemanager.list.menu.copy'],
|
||||
click: function ($itemScope, $event, entry) { ctrl.onCopyEntries({ cwd: $scope.cwd, entries: $scope.selected.slice() }); }
|
||||
}, {
|
||||
text: tr['filemanager.list.menu.paste'],
|
||||
hasBottomDivider: true,
|
||||
enabled: function () { return ctrl.clipboard.length; },
|
||||
click: function ($itemScope, $event, entry) { ctrl.onPasteEntries({ cwd: $scope.cwd, entry: entry }); }
|
||||
}, {
|
||||
text: tr['filemanager.list.menu.rename'],
|
||||
enabled: function () { return $scope.selected.length === 1; },
|
||||
click: function ($itemScope, $event, entry) { ctrl.onRenameEntry({ cwd: $scope.cwd, entry: entry }); }
|
||||
}, {
|
||||
text: tr['filemanager.list.menu.chown'],
|
||||
click: function ($itemScope, $event, entry) { ctrl.onChownEntries({ cwd: $scope.cwd, entries: $scope.selected }); }
|
||||
}, {
|
||||
text: tr['filemanager.list.menu.extract'],
|
||||
displayed: function ($itemScope, $event, entry) { return !entry.isDirectory && isArchive(entry.fileName); },
|
||||
click: function ($itemScope, $event, entry) { ctrl.onExtractEntry({ cwd: $scope.cwd, entry: entry }); }
|
||||
}, {
|
||||
text: tr['filemanager.list.menu.download'],
|
||||
enabled: function () { return $scope.selected.length === 1; },
|
||||
click: function ($itemScope, $event, entry) { download(entry); }
|
||||
}, {
|
||||
text: tr['filemanager.list.menu.delete'],
|
||||
hasTopDivider: true,
|
||||
click: function ($itemScope, $event, entry) { ctrl.onDeleteEntries({ cwd: $scope.cwd, entries: $scope.selected }); }
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
$translate(['filemanager.toolbar.newFile', 'filemanager.toolbar.newFolder', 'filemanager.list.menu.paste', 'filemanager.list.menu.selectAll' ]).then(function (tr) {
|
||||
$scope.menuOptionsBlank = [
|
||||
{
|
||||
text: tr['filemanager.toolbar.newFile'],
|
||||
click: function ($itemScope, $event) { ctrl.onNewFile({ cwd: $scope.cwd }); }
|
||||
}, {
|
||||
text: tr['filemanager.toolbar.newFolder'],
|
||||
click: function ($itemScope, $event) { ctrl.onNewFolder({ cwd: $scope.cwd }); }
|
||||
}, {
|
||||
text: tr['filemanager.list.menu.paste'],
|
||||
hasTopDivider: true,
|
||||
hasBottomDivider: true,
|
||||
enabled: function () { return ctrl.clipboard.length; },
|
||||
click: function ($itemScope, $event) { ctrl.onPasteEntries({ cwd: $scope.cwd, entry: null }); }
|
||||
}, {
|
||||
text: tr['filemanager.list.menu.selectAll'],
|
||||
click: function ($itemScope, $event) { $scope.actionSelectAll(); }
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
function scrollInView(element) {
|
||||
if (!element) return;
|
||||
|
||||
// This assumes the DOM tree being that rigid
|
||||
function isVisible(ele) {
|
||||
var container = ele.parentElement.parentElement.parentElement;
|
||||
var eleTop = ele.offsetTop;
|
||||
var eleBottom = eleTop + ele.clientHeight;
|
||||
|
||||
var containerTop = container.scrollTop;
|
||||
var containerBottom = containerTop + container.clientHeight;
|
||||
|
||||
// The element is fully visible in the container
|
||||
return (
|
||||
(eleTop >= containerTop && eleBottom <= containerBottom) ||
|
||||
// Some part of the element is visible in the container
|
||||
(eleTop < containerTop && containerTop < eleBottom) ||
|
||||
(eleTop < containerBottom && containerBottom < eleBottom)
|
||||
);
|
||||
}
|
||||
|
||||
if (!isVisible(element)) element.scrollIntoView();
|
||||
}
|
||||
|
||||
function openSelected() {
|
||||
if (!$scope.selected.length) return;
|
||||
|
||||
$scope.open($scope.selected[0]);
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
var entries = sort();
|
||||
|
||||
if (!$scope.selected.length) return $scope.selected = [ entries[0] ];
|
||||
|
||||
var curIndex = $scope.entries.indexOf($scope.selected[0]);
|
||||
if (curIndex !== -1 && curIndex < $scope.entries.length-1) {
|
||||
var entry = entries[++curIndex];
|
||||
$scope.selected = [ entry ];
|
||||
scrollInView(document.querySelector('[entry-hashkey="' + entry['$$hashKey'] + '"]'));
|
||||
}
|
||||
}
|
||||
|
||||
function selectPrev() {
|
||||
var entries = sort();
|
||||
|
||||
if (!$scope.selected.length) return $scope.selected = [ entries.slice(-1) ];
|
||||
|
||||
var curIndex = $scope.entries.indexOf($scope.selected[0]);
|
||||
if (curIndex !== -1 && curIndex !== 0) {
|
||||
var entry = entries[--curIndex];
|
||||
$scope.selected = [ entry ];
|
||||
scrollInView(document.querySelector('[entry-hashkey="' + entry['$$hashKey'] + '"]'));
|
||||
}
|
||||
}
|
||||
|
||||
openDirectory('.');
|
||||
|
||||
$('.file-list').on('scroll', function (event) {
|
||||
if (event.target.scrollTop > 10) event.target.classList.add('top-scroll-indicator');
|
||||
else event.target.classList.remove('top-scroll-indicator');
|
||||
});
|
||||
|
||||
// handle shortcuts
|
||||
window.addEventListener('keydown', function (event) {
|
||||
if ($scope.$parent.activeView !== $scope.view || $scope.$parent.viewerOpen || isModalVisible()) return;
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
$scope.$apply(selectNext);
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
$scope.$apply(selectPrev);
|
||||
} else if (event.key === 'Enter') {
|
||||
$scope.$apply(openSelected);
|
||||
} else if (event.key === 'Backspace') {
|
||||
if ($scope.view === 'fileTree') $scope.goDirectoryUp();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="Application">
|
||||
<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 Filemanager</title>
|
||||
<meta name="description" content="Cloudron Filemanager">
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar">
|
||||
|
||||
<!-- CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.css?<%= revision %>"/>
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-animate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-base64.min.js?<%= revision %>"></script>
|
||||
<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-ui-notification.js?<%= revision %>"></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?<%= revision %>"></script>
|
||||
|
||||
<!-- Angular translate https://angular-translate.github.io/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- colors -->
|
||||
<script type="text/javascript" src="/3rdparty/js/colors.js?<%= revision %>"></script>
|
||||
|
||||
<!-- moment -->
|
||||
<script type="text/javascript" src="/3rdparty/js/moment-with-locales.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- https://github.com/data-uri/mimer -->
|
||||
<script type="text/javascript" src="/3rdparty/js/mimer.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- https://github.com/Templarian/ui.bootstrap.contextMenu -->
|
||||
<script type="text/javascript" src="/3rdparty/js/contextMenu.js?<%= revision %>"></script>
|
||||
|
||||
<!-- WARNING this adds an AMD loader! Make sure script tag includes like mimer are above -->
|
||||
<!-- monaco-editor -->
|
||||
<script type="text/javascript" src="/3rdparty/vs/loader.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Main Application -->
|
||||
<script type="text/javascript" src="/js/filemanager.js?<%= revision %>"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="filemanager" ng-drop="drop($event)" ng-dragover="dragEnter($event)" ng-dragleave="dragExit($event)" ng-controller="FileManagerController">
|
||||
|
||||
<a class="offline-banner animateMe" ng-show="client.offline" ng-cloak href="https://docs.cloudron.io/troubleshooting/" target="_blank"><i class="fa fa-circle-notch fa-spin"></i> {{ 'main.offline' | tr }}</a>
|
||||
|
||||
<div class="restart-banner animateMe" ng-show="restartBusy" ng-cloak><i class="fa fa-circle-notch fa-spin"></i> {{ 'filemanager.status.restartingApp' | tr}}</div>
|
||||
|
||||
<!-- Modal delete entries -->
|
||||
<div class="modal fade" id="entriesDeleteModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<p class="text-bold text-danger" ng-show="deleteEntries.error">{{ deleteEntries.error }}</p>
|
||||
<h4 ng-hide="deleteEntries.error">{{ 'filemanager.removeDialog.reallyDelete' | tr }}</h4>
|
||||
<ul>
|
||||
<li ng-repeat="entry in deleteEntries.entries">{{ entry.fileName }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.no' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="deleteEntries.submit()" ng-hide="deleteEntries.error" ng-disabled="deleteEntries.busy"><i class="fa fa-circle-notch fa-spin" ng-show="deleteEntries.busy"></i> {{ 'main.dialog.yes' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal rename entry -->
|
||||
<div class="modal fade" id="renameEntryModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'filemanager.renameDialog.title' | tr:{ fileName: renameEntry.entry.fileName } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form role="form" name="renameEntryForm" ng-submit="renameEntry.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (renameEntryForm.newName.$dirty && renameEntryForm.newName.$invalid) }">
|
||||
<label class="control-label">{{ 'filemanager.renameDialog.newName' | tr }}</label>
|
||||
<div class="control-label" ng-show="renameEntry.error">{{ renameEntry.error }}</div>
|
||||
<input type="text" class="form-control" id="inputNewName" name="newName" ng-model="renameEntry.newName" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="renameEntryForm.$invalid || renameEntry.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="renameEntry.submit()" ng-hide="renameEntry.error" ng-disabled="renameEntry.busy"><i class="fa fa-circle-notch fa-spin" ng-show="renameEntry.busy"></i> {{ 'filemanager.renameDialog.rename' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal extract -->
|
||||
<div class="modal fade" id="extractModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'filemanager.extractDialog.title' | tr:{ fileName: extractStatus.fileName } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-show="extractStatus.error">
|
||||
<p class="text-danger">{{ extractStatus.error }}</p>
|
||||
</div>
|
||||
<div class="progress progress-striped active" ng-hide="extractStatus.error">
|
||||
<div class="progress-bar" role="progressbar" style="width: 100%">
|
||||
</div>
|
||||
</div>
|
||||
<p class="no-wrap" ng-hide="extractStatus.error">{{ extractStatus.fileName }}</p>
|
||||
</div>
|
||||
<div class="modal-footer" style="text-align: left;">
|
||||
<small ng-hide="extractStatus.error">{{ 'filemanager.extractDialog.closeWarning' | tr }}</small>
|
||||
<button class="btn btn-primary pull-right" ng-show="extractStatus.error" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal chown entry -->
|
||||
<div class="modal fade" id="chownEntriesModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'filemanager.chownDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form role="form" name="chownEntryForm" ng-submit="chownEntries.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (chownEntryForm.newOwner.$dirty && chownEntries.error) }">
|
||||
<label class="control-label">{{ 'filemanager.chownDialog.newOwner' | tr }}</label>
|
||||
<div class="control-label" for="inputNewOwner" ng-show="chownEntries.error">{{ chownEntries.error }}</div>
|
||||
<select class="form-control" id="inputNewOwner" name="newOwner" ng-model="chownEntries.newOwner" ng-options="a.value as a.name for a in OWNERS" ng-disabled="chownEntries.busy"></select>
|
||||
</div>
|
||||
<div class="form-group" ng-show="chownEntries.showRecursiveOption">
|
||||
<input type="checkbox" id="inputNewOwnerRecursive" ng-model="chownEntries.recursive">
|
||||
<label class="control-label" for="inputNewOwnerRecursive">{{ 'filemanager.chownDialog.recursiveCheckbox' | tr }}</label>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="chownEntryForm.$invalid || chownEntries.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="chownEntries.submit()" ng-hide="chownEntries.error" ng-disabled="chownEntries.busy"><i class="fa fa-circle-notch fa-spin" ng-show="chownEntries.busy"></i> {{ 'filemanager.chownDialog.change' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal new file -->
|
||||
<div class="modal fade" id="newFileModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'filemanager.newFileDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form role="form" name="newFileForm" ng-submit="newFile.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': newFile.error || (newFileForm.fileName.$dirty && newFileForm.fileName.$invalid) }">
|
||||
<input type="text" class="form-control" id="inputFileName" name="fileName" ng-model="newFile.name" required autofocus>
|
||||
<div class="control-label" ng-show="newFile.error === 'exists'">{{ 'filemanager.newFile.errorAlreadyExists' | tr }}</div>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="newFileForm.$invalid || newFile.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="newFile.submit()" ng-disabled="newFile.busy"><i class="fa fa-circle-notch fa-spin" ng-show="newFile.busy"></i> {{ 'filemanager.newFileDialog.create' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal new directory -->
|
||||
<div class="modal fade" id="newFolderModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'filemanager.newDirectoryDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form role="form" name="newFolderForm" ng-submit="newFolder.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': newFolder.error || (newFolderForm.directoryName.$dirty && newFolderForm.directoryName.$invalid) }">
|
||||
<input type="text" class="form-control" id="inputDirectoryName" name="directoryName" ng-model="newFolder.name" required autofocus>
|
||||
<div class="control-label" ng-show="newFolder.error === 'exists'">{{ 'filemanager.newDirectory.errorAlreadyExists' | tr }}</div>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="newDirectoryForm.$invalid || newFolder.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="newFolder.submit()" ng-disabled="newFolder.busy"><i class="fa fa-circle-notch fa-spin" ng-show="newFolder.busy"></i> {{ 'filemanager.newDirectoryDialog.create' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal editor close -->
|
||||
<div class="modal fade" id="textEditorCloseModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'filemanager.textEditorCloseDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-bold text-danger">{{ 'filemanager.textEditorCloseDialog.details' | tr }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" ng-click="textEditor.onClose()">{{ 'filemanager.textEditorCloseDialog.dontSave' | tr }}</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="textEditor.saveAndClose()"><i class="fa fa-circle-notch fa-spin" ng-show="textEditor.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal upload -->
|
||||
<div class="modal fade" id="uploadModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'filemanager.uploadingDialog.title' | tr:{ countDone: uploadStatus.countDone, count: uploadStatus.count } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-show="uploadStatus.error">
|
||||
<p class="text-danger" ng-show="uploadStatus.error === 'exists'">{{ 'filemanager.uploadingDialog.errorAlreadyExists' | tr }}</p>
|
||||
<p class="text-danger" ng-show="uploadStatus.error === 'generic'">{{ 'filemanager.uploadingDialog.errorFailed' | tr }}</p>
|
||||
</div>
|
||||
<span><b>{{ uploadStatus.sizeDone | prettyDecimalSize }}</b> (total {{ uploadStatus.size | prettyDecimalSize }})</span>
|
||||
<div class="progress progress-striped active" ng-hide="uploadStatus.error">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ uploadStatus.percentDone || 0 }}%"></div>
|
||||
</div>
|
||||
<p class="no-wrap" ng-hide="uploadStatus.error">{{ uploadStatus.fileName }}</p>
|
||||
</div>
|
||||
<div class="modal-footer" style="text-align: left;">
|
||||
<small ng-hide="uploadStatus.error">{{ 'filemanager.uploadingDialog.closeWarning' | tr }}</small>
|
||||
<button class="btn btn-default pull-right" ng-show="uploadStatus.error" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button class="btn btn-primary pull-right" ng-show="uploadStatus.error === 'generic'" ng-click="retryUpload(false)">{{ 'filemanager.uploadingDialog.retry' | tr }}</button>
|
||||
<button class="btn btn-danger pull-right" ng-show="uploadStatus.error === 'exists'" ng-click="retryUpload(true)">{{ 'filemanager.uploadingDialog.overwrite' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="animateMe ng-hide layout-root" ng-show="initialized">
|
||||
<div class="row" ng-hide="title">
|
||||
<div class="col-md-12 text-center">
|
||||
<h3>{{ 'filemanager.notFound' | tr }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="file" id="uploadFileInput" style="display: none" multiple/>
|
||||
<input type="file" id="uploadFolderInput" style="display: none" multiple webkitdirectory directory/>
|
||||
|
||||
<div class="container card" ng-show="title" style="max-width: unset;">
|
||||
<h4 class="text-left">
|
||||
{{ title }}
|
||||
|
||||
<div class="pull-right">
|
||||
<div class="btn-group" ng-show="volumes.length">
|
||||
<button type="button" class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fas fa-folder"></i> <span class="caret"></span></button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li ng-repeat="volume in volumes"><a class="hand" ng-href="{{ '/filemanager.html?type=volume&id=' + volume.id }}" target="_blank"><i class="fas fa-folder fa-fw"></i> {{ volume.name }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-class="{ 'active': splitView }" ng-click="toggleSplitView()"><i class="fas fa-columns"></i></button>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-sm btn-default" ng-show="backendType === 'app'" ng-click="onRestartApp()" uib-tooltip="{{ 'filemanager.toolbar.restartApp' | tr }}" tooltip-placement="bottom" tooltip-append-to-body="true"><i class="fas fa-sync-alt"></i></button>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-show="backendType === 'mail'" ng-click="onRestartMail()" uib-tooltip="{{ 'filemanager.toolbar.restartApp' | tr }}" tooltip-placement="bottom" tooltip-append-to-body="true"><i class="fas fa-sync-alt"></i></button>
|
||||
<a type="button" class="btn btn-sm btn-default" ng-show="backendType === 'app'" ng-href="/logs.html?{{ backendType === 'app' ? 'appId=' + backendId : 'id=mail' }}" target="_blank" uib-tooltip="{{ 'filemanager.toolbar.openLogs' | tr }}" tooltip-placement="bottom" tooltip-append-to-body="true"><i class="fas fa-align-left"></i></a>
|
||||
<a type="button" class="btn btn-sm btn-default" ng-show="backendType === 'app'" ng-href="{{ '/terminal.html?id=' + backendId }}" target="_blank" uib-tooltip="{{ 'filemanager.toolbar.openTerminal' | tr }}" tooltip-placement="bottom" tooltip-append-to-body="true"><i class="fa fa-terminal"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
|
||||
<div class="file-trees">
|
||||
<filetree ng-class="{ 'two-pane': splitView }"
|
||||
on-upload-folder="onUploadFolder(cwd)"
|
||||
on-upload-file="onUploadFile(cwd)"
|
||||
on-new-file="newFile.show(cwd)"
|
||||
on-new-folder="newFolder.show(cwd)"
|
||||
on-copy-entries="actionCopy(cwd, entries)"
|
||||
on-cut-entries="actionCut(cwd, entries)"
|
||||
on-paste-entries="actionPaste(cwd, entries)"
|
||||
on-delete-entries="deleteEntries.show(cwd, entries)"
|
||||
on-rename-entry="renameEntry.show(cwd, entry)"
|
||||
on-extract-entry="extractEntry(cwd, entry)"
|
||||
on-chown-entries="chownEntries.show(cwd, entries)"
|
||||
backend-type="backendType" backend-id="backendId" view="VIEW.LEFT" clipboard="clipboard"
|
||||
ng-click="setActiveView(VIEW.LEFT)"></filetree>
|
||||
<filetree ng-show="splitView" class="two-pane"
|
||||
on-upload-folder="onUploadFolder(cwd)"
|
||||
on-upload-file="onUploadFile(cwd)"
|
||||
on-new-file="newFile.show(cwd)"
|
||||
on-new-folder="newFolder.show(cwd)"
|
||||
on-copy-entries="actionCopy(cwd, entries)"
|
||||
on-cut-entries="actionCut(cwd, entries)"
|
||||
on-paste-entries="actionPaste(cwd, entries)"
|
||||
on-delete-entries="deleteEntries.show(cwd, entries)"
|
||||
on-rename-entry="renameEntry.show(cwd, entry)"
|
||||
on-extract-entry="extractEntry(cwd, entry)"
|
||||
on-chown-entries="chownEntries.show(cwd, entries)"
|
||||
backend-type="backendType" backend-id="backendId" view="VIEW.RIGHT" clipboard="clipboard"
|
||||
ng-click="setActiveView(VIEW.RIGHT)"></filetree>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="textEditor.visible" class="text-editor">
|
||||
<div>
|
||||
<div class="toolbar">
|
||||
<div><span>{{ textEditor.entry.fileName }}</span></div>
|
||||
<button type="button" class="btn btn-primary" ng-click="textEditor.maybeClose()">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="textEditor.save()" ng-disabled="textEditor.busy"><i class="fa fa-circle-notch fa-spin" ng-show="textEditor.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="textEditorContainer" style="flex-grow: 2; border: 0px solid black"></div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center ng-cloak">
|
||||
<span class="text-muted" ng-bind-html="status.footer | markdown2html"></span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -80,6 +80,9 @@
|
||||
<!-- Anugular Multiselect https://github.com/sebastianha/angular-bootstrap-multiselect -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-bootstrap-multiselect.js?<%= revision %>"></script>
|
||||
|
||||
<!-- colors -->
|
||||
<script type="text/javascript" src="/3rdparty/js/colors.js?<%= revision %>"></script>
|
||||
|
||||
<!-- moment -->
|
||||
<script type="text/javascript" src="/3rdparty/js/moment-with-locales.min.js?<%= revision %>"></script>
|
||||
|
||||
@@ -156,7 +159,7 @@
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-class="{ active: isActive('/apps')}" href="#/apps"><i class="fa fa-grip fa-fw"></i> {{ 'apps.title' | tr }}</a>
|
||||
<a ng-class="{ active: isActive('/apps')}" href="#/apps"><i class="fa fa-th fa-fw"></i> {{ 'apps.title' | tr }}</a>
|
||||
</li>
|
||||
<li ng-show="user.isAtLeastAdmin">
|
||||
<a ng-class="{ active: isActive('/appstore')}" href="#/appstore"><i class="fa fa-cloud-download-alt fa-fw"></i> {{ 'appstore.title' | tr }}</a>
|
||||
@@ -184,7 +187,6 @@
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/network"><i class="fas fa-network-wired fa-fw"></i> {{ 'network.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/services"><i class="fa fa-cogs fa-fw"></i> {{ 'services.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> {{ 'settings.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/usersettings"><i class="fa fa-users-gear fa-fw"></i> {{ 'users.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/volumes"><i class="fa fa-hdd fa-fw"></i> {{ 'volumes.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin" class="divider"></li>
|
||||
<li ng-show="user.isAtLeastOwner"><a href="#/support"><i class="fa fa-comment fa-fw"></i> {{ 'support.title' | tr }}</a></li>
|
||||
|
||||
@@ -0,0 +1,890 @@
|
||||
'use strict';
|
||||
|
||||
require.config({ paths: { 'vs': '3rdparty/vs' }});
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ngDrag', 'ui.bootstrap', 'ui.bootstrap.contextMenu']);
|
||||
|
||||
angular.module('Application').filter('prettyOwner', function () {
|
||||
return function (uid) {
|
||||
if (uid === 0) return 'root';
|
||||
if (uid === 33) return 'www-data';
|
||||
if (uid === 1000) return 'cloudron';
|
||||
if (uid === 1001) return 'git';
|
||||
|
||||
return uid;
|
||||
};
|
||||
});
|
||||
|
||||
// disable sce for footer https://code.angularjs.org/1.5.8/docs/api/ng/service/$sce
|
||||
app.config(function ($sceProvider) {
|
||||
$sceProvider.enabled(false);
|
||||
});
|
||||
|
||||
app.filter('trustUrl', ['$sce', function ($sce) {
|
||||
return function (recordingUrl) {
|
||||
return $sce.trustAsResourceUrl(recordingUrl);
|
||||
};
|
||||
}]);
|
||||
|
||||
// https://stackoverflow.com/questions/25621321/angularjs-ng-drag
|
||||
var ngDragEventDirectives = {};
|
||||
angular.forEach(
|
||||
'drag dragend dragenter dragexit dragleave dragover dragstart drop'.split(' '),
|
||||
function(eventName) {
|
||||
var directiveName = 'ng' + eventName.charAt(0).toUpperCase() + eventName.slice(1);
|
||||
|
||||
ngDragEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse/*, $rootScope */) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
compile: function($element, attr) {
|
||||
var fn = $parse(attr[directiveName], null, true);
|
||||
|
||||
return function ngDragEventHandler(scope, element) {
|
||||
element.on(eventName, function(event) {
|
||||
var callback = function() {
|
||||
fn(scope, {$event: event});
|
||||
};
|
||||
|
||||
scope.$apply(callback);
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
}];
|
||||
}
|
||||
);
|
||||
angular.module('ngDrag', []).directive(ngDragEventDirectives);
|
||||
|
||||
function sanitize(filePath) {
|
||||
filePath = filePath.split('/').filter(function (a) { return !!a; }).reduce(function (a, v) {
|
||||
if (v === '.'); // do nothing
|
||||
else if (v === '..') a.pop();
|
||||
else a.push(v);
|
||||
return a;
|
||||
}, []).map(function (p) {
|
||||
// small detour to safely handle special characters and whitespace
|
||||
return encodeURIComponent(decodeURIComponent(p));
|
||||
}).join('/');
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function isModalVisible() {
|
||||
return !!document.getElementsByClassName('modal in').length;
|
||||
}
|
||||
|
||||
var VIEW = {
|
||||
LEFT: 'left',
|
||||
RIGHT: 'right'
|
||||
};
|
||||
|
||||
var OWNERS = [
|
||||
{ name: 'cloudron', value: 1000 },
|
||||
{ name: 'www-data', value: 33 },
|
||||
{ name: 'git', value: 1001 },
|
||||
{ name: 'root', value: 0 }
|
||||
];
|
||||
|
||||
app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Client', function ($scope, $translate, $timeout, Client) {
|
||||
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; }, {});
|
||||
|
||||
// expose enums
|
||||
$scope.VIEW = VIEW;
|
||||
$scope.OWNERS = OWNERS;
|
||||
|
||||
$scope.initialized = false;
|
||||
$scope.status = null;
|
||||
$scope.client = Client;
|
||||
$scope.title = '';
|
||||
$scope.backendId = search.id;
|
||||
$scope.backendType = search.type;
|
||||
$scope.volumes = [];
|
||||
$scope.splitView = !!window.localStorage.splitView;
|
||||
$scope.activeView = VIEW.LEFT;
|
||||
$scope.viewerOpen = false;
|
||||
|
||||
$scope.clipboard = []; // holds cut or copied entries
|
||||
$scope.clipboardCut = false; // if action is cut or copy
|
||||
|
||||
|
||||
// add a hook for children to refresh both tree views
|
||||
|
||||
$scope.children = [];
|
||||
$scope.registerChild = function (child) { $scope.children.push(child); };
|
||||
$scope.refresh = function () {
|
||||
$scope.children.forEach(function (child) {
|
||||
child.onRefresh();
|
||||
});
|
||||
};
|
||||
|
||||
function collectFiles(entry, callback) {
|
||||
var pathFrom = entry.pathFrom;
|
||||
|
||||
if (entry.isDirectory) {
|
||||
Client.filesGet($scope.backendId, $scope.backendType, entry.fullFilePath, 'data', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (!result.entries) return callback(new Error('not a folder'));
|
||||
|
||||
// amend fullFilePath
|
||||
result.entries.forEach(function (e) {
|
||||
e.fullFilePath = sanitize(entry.fullFilePath + '/' + e.fileName);
|
||||
e.pathFrom = pathFrom; // we stash the original path for pasting
|
||||
});
|
||||
|
||||
var collectedFiles = [];
|
||||
async.eachLimit(result.entries, 5, function (entry, callback) {
|
||||
collectFiles(entry, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
collectedFiles = collectedFiles.concat(result);
|
||||
|
||||
callback();
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, collectedFiles);
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, [ entry ]);
|
||||
}
|
||||
|
||||
// entries need to be an actual copy
|
||||
$scope.actionCut = function (cwd, entries) {
|
||||
$scope.clipboard = entries; //$scope.selected.slice();
|
||||
$scope.clipboard.forEach(function (entry) {
|
||||
entry.fullFilePath = sanitize(cwd + '/' + entry.fileName);
|
||||
});
|
||||
$scope.clipboardCut = true;
|
||||
};
|
||||
|
||||
// entries need to be an actual copy
|
||||
$scope.actionCopy = function (cwd, entries) {
|
||||
$scope.clipboard = entries; //$scope.selected.slice();
|
||||
$scope.clipboard.forEach(function (entry) {
|
||||
entry.fullFilePath = sanitize(cwd + '/' + entry.fileName);
|
||||
entry.pathFrom = cwd; // we stash the original path for pasting
|
||||
});
|
||||
$scope.clipboardCut = false;
|
||||
};
|
||||
|
||||
$scope.actionPaste = function (cwd, destinationEntry) {
|
||||
if ($scope.clipboardCut) {
|
||||
// move files
|
||||
async.eachLimit($scope.clipboard, 5, function (entry, callback) {
|
||||
var newFilePath = sanitize(cwd + '/' + ((destinationEntry && destinationEntry.isDirectory) ? destinationEntry.fileName : '') + '/' + entry.fileName);
|
||||
|
||||
// TODO this will overwrite files in destination!
|
||||
Client.filesRename($scope.backendId, $scope.backendType, entry.fullFilePath, newFilePath, callback);
|
||||
}, function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
// clear clipboard
|
||||
$scope.clipboard = [];
|
||||
|
||||
$scope.refresh();
|
||||
});
|
||||
} else {
|
||||
// copy files
|
||||
|
||||
// first collect all files recursively
|
||||
var collectedFiles = [];
|
||||
|
||||
async.eachLimit($scope.clipboard, 5, function (entry, callback) {
|
||||
collectFiles(entry, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
collectedFiles = collectedFiles.concat(result);
|
||||
|
||||
callback();
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
async.eachLimit(collectedFiles, 5, function (entry, callback) {
|
||||
var newFilePath = sanitize(cwd + '/' + ((destinationEntry && destinationEntry.isDirectory) ? destinationEntry.fileName : '') + '/' + entry.fullFilePath.slice(entry.pathFrom.length));
|
||||
|
||||
// This will NOT overwrite but finds a unique new name to copy to
|
||||
// we prefix with a / to ensure we don't do relative target paths
|
||||
Client.filesCopy($scope.backendId, $scope.backendType, entry.fullFilePath, '/' + newFilePath, callback);
|
||||
}, function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
// clear clipboard
|
||||
$scope.clipboard = [];
|
||||
|
||||
$scope.refresh();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// handle uploads
|
||||
|
||||
$scope.uploadStatus = {
|
||||
error: null,
|
||||
busy: false,
|
||||
fileName: '',
|
||||
count: 0,
|
||||
countDone: 0,
|
||||
size: 0,
|
||||
done: 0,
|
||||
percentDone: 0,
|
||||
files: [],
|
||||
targetFolder: ''
|
||||
};
|
||||
|
||||
$scope.uploadFiles = function (files, targetFolder, overwrite) {
|
||||
if (!files || !files.length) return;
|
||||
|
||||
overwrite = !!overwrite;
|
||||
|
||||
// prevent it from getting closed
|
||||
$('#uploadModal').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
});
|
||||
|
||||
$scope.uploadStatus.files = files;
|
||||
$scope.uploadStatus.targetFolder = targetFolder;
|
||||
$scope.uploadStatus.error = null;
|
||||
$scope.uploadStatus.busy = true;
|
||||
$scope.uploadStatus.count = files.length;
|
||||
$scope.uploadStatus.countDone = 0;
|
||||
$scope.uploadStatus.size = 0;
|
||||
$scope.uploadStatus.sizeDone = 0;
|
||||
$scope.uploadStatus.done = 0;
|
||||
$scope.uploadStatus.percentDone = 0;
|
||||
|
||||
for (var i = 0; i < files.length; ++i) {
|
||||
$scope.uploadStatus.size += files[i].size;
|
||||
}
|
||||
|
||||
async.eachSeries(files, function (file, callback) {
|
||||
var filePath = sanitize(targetFolder + '/' + (file.webkitRelativePath || file.name));
|
||||
|
||||
$scope.uploadStatus.fileName = file.name;
|
||||
|
||||
Client.filesUpload($scope.backendId, $scope.backendType, filePath, file, overwrite, function (loaded) {
|
||||
$scope.uploadStatus.percentDone = ($scope.uploadStatus.done+loaded) * 100 / $scope.uploadStatus.size;
|
||||
$scope.uploadStatus.sizeDone = loaded;
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
$scope.uploadStatus.done += file.size;
|
||||
$scope.uploadStatus.percentDone = $scope.uploadStatus.done * 100 / $scope.uploadStatus.size;
|
||||
$scope.uploadStatus.countDone++;
|
||||
|
||||
callback();
|
||||
});
|
||||
}, function (error) {
|
||||
$scope.uploadStatus.busy = false;
|
||||
|
||||
if (error && error.statusCode === 409) {
|
||||
$scope.uploadStatus.error = 'exists';
|
||||
return;
|
||||
} else if (error) {
|
||||
console.error(error);
|
||||
$scope.uploadStatus.error = 'generic';
|
||||
return;
|
||||
}
|
||||
|
||||
$('#uploadModal').modal('hide');
|
||||
|
||||
$scope.uploadStatus.fileName = '';
|
||||
$scope.uploadStatus.count = 0;
|
||||
$scope.uploadStatus.size = 0;
|
||||
$scope.uploadStatus.sizeDone = 0;
|
||||
$scope.uploadStatus.done = 0;
|
||||
$scope.uploadStatus.percentDone = 100;
|
||||
$scope.uploadStatus.files = [];
|
||||
$scope.uploadStatus.targetFolder = '';
|
||||
|
||||
$scope.refresh();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.retryUpload = function (overwrite) {
|
||||
$scope.uploadFiles($scope.uploadStatus.files, $scope.uploadStatus.targetFolder, !!overwrite);
|
||||
};
|
||||
|
||||
|
||||
// file and folder upload hooks, stashing $scope.uploadCwd for now
|
||||
|
||||
$scope.uploadCwd = '';
|
||||
$('#uploadFileInput').on('change', function (e ) {
|
||||
$scope.uploadFiles(e.target.files || [], $scope.uploadCwd, false);
|
||||
});
|
||||
$scope.onUploadFile = function (cwd) {
|
||||
$scope.uploadCwd = cwd;
|
||||
$('#uploadFileInput').click();
|
||||
};
|
||||
|
||||
$('#uploadFolderInput').on('change', function (e ) {
|
||||
$scope.uploadFiles(e.target.files || [], $scope.uploadCwd, false);
|
||||
});
|
||||
$scope.onUploadFolder = function (cwd) {
|
||||
$scope.uploadCwd = cwd;
|
||||
$('#uploadFolderInput').click();
|
||||
};
|
||||
|
||||
|
||||
// handle delete
|
||||
|
||||
$scope.deleteEntries = {
|
||||
busy: false,
|
||||
error: null,
|
||||
cwd: '',
|
||||
entries: [],
|
||||
|
||||
show: function (cwd, entries) {
|
||||
$scope.deleteEntries.error = null;
|
||||
$scope.deleteEntries.cwd = cwd;
|
||||
$scope.deleteEntries.entries = entries;
|
||||
|
||||
$('#entriesDeleteModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.deleteEntries.busy = true;
|
||||
|
||||
async.eachLimit($scope.deleteEntries.entries, 5, function (entry, callback) {
|
||||
var filePath = sanitize($scope.deleteEntries.cwd + '/' + entry.fileName);
|
||||
|
||||
Client.filesRemove($scope.backendId, $scope.backendType, filePath, callback);
|
||||
}, function (error) {
|
||||
$scope.deleteEntries.busy = false;
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$scope.refresh();
|
||||
|
||||
$('#entriesDeleteModal').modal('hide');
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
// rename entry
|
||||
|
||||
$scope.renameEntry = {
|
||||
busy: false,
|
||||
error: null,
|
||||
entry: null,
|
||||
cwd: '',
|
||||
newName: '',
|
||||
|
||||
show: function (cwd, entry) {
|
||||
$scope.renameEntry.error = null;
|
||||
$scope.renameEntry.cwd = cwd;
|
||||
$scope.renameEntry.entry = entry;
|
||||
$scope.renameEntry.newName = entry.fileName;
|
||||
$scope.renameEntry.busy = false;
|
||||
|
||||
$('#renameEntryModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.renameEntry.busy = true;
|
||||
|
||||
var oldFilePath = sanitize($scope.renameEntry.cwd + '/' + $scope.renameEntry.entry.fileName);
|
||||
var newFilePath = sanitize(($scope.renameEntry.newName[0] === '/' ? '' : ($scope.renameEntry.cwd + '/')) + $scope.renameEntry.newName);
|
||||
|
||||
Client.filesRename($scope.backendId, $scope.backendType, oldFilePath, newFilePath, function (error) {
|
||||
$scope.renameEntry.busy = false;
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$scope.refresh();
|
||||
|
||||
$('#renameEntryModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// chown entries
|
||||
|
||||
$scope.chownEntries = {
|
||||
busy: false,
|
||||
error: null,
|
||||
entries: [],
|
||||
newOwner: 0,
|
||||
recursive: false,
|
||||
showRecursiveOption: false,
|
||||
|
||||
show: function (cwd, entries) {
|
||||
$scope.chownEntries.error = null;
|
||||
$scope.chownEntries.cwd = cwd;
|
||||
$scope.chownEntries.entries = entries;
|
||||
// set default uid from first file
|
||||
$scope.chownEntries.newOwner = entries[0].uid;
|
||||
$scope.chownEntries.busy = false;
|
||||
|
||||
// default for directories is recursive
|
||||
$scope.chownEntries.recursive = !!entries.find(function (entry) { return entry.isDirectory; });
|
||||
$scope.chownEntries.showRecursiveOption = false;
|
||||
|
||||
$('#chownEntriesModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.chownEntries.busy = true;
|
||||
|
||||
async.eachLimit($scope.chownEntries.entries, 5, function (entry, callback) {
|
||||
var filePath = sanitize($scope.chownEntries.cwd + '/' + entry.fileName);
|
||||
|
||||
Client.filesChown($scope.backendId, $scope.backendType, filePath, $scope.chownEntries.newOwner, $scope.chownEntries.recursive, callback);
|
||||
}, function (error) {
|
||||
$scope.chownEntries.busy = false;
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$scope.refresh();
|
||||
|
||||
$('#chownEntriesModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// new file
|
||||
|
||||
$scope.newFile = {
|
||||
busy: false,
|
||||
error: null,
|
||||
cwd: '',
|
||||
name: '',
|
||||
|
||||
show: function (cwd) {
|
||||
$scope.newFile.error = null;
|
||||
$scope.newFile.name = '';
|
||||
$scope.newFile.busy = false;
|
||||
$scope.newFile.cwd = cwd;
|
||||
|
||||
$scope.newFileForm.$setUntouched();
|
||||
$scope.newFileForm.$setPristine();
|
||||
|
||||
$('#newFileModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.newFile.busy = true;
|
||||
$scope.newFile.error = null;
|
||||
|
||||
var filePath = sanitize($scope.newFile.cwd + '/' + $scope.newFile.name);
|
||||
|
||||
Client.filesUpload($scope.backendId, $scope.backendType, filePath, new File([], $scope.newFile.name), false, function () {}, function (error) {
|
||||
$scope.newFile.busy = false;
|
||||
if (error && error.statusCode === 409) return $scope.newFile.error = 'exists';
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$scope.refresh();
|
||||
|
||||
$('#newFileModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// new folder
|
||||
|
||||
$scope.newFolder = {
|
||||
busy: false,
|
||||
error: null,
|
||||
cwd: '',
|
||||
name: '',
|
||||
|
||||
show: function (cwd) {
|
||||
$scope.newFolder.error = null;
|
||||
$scope.newFolder.name = '';
|
||||
$scope.newFolder.busy = false;
|
||||
$scope.newFolder.cwd = cwd;
|
||||
|
||||
$scope.newFolderForm.$setUntouched();
|
||||
$scope.newFolderForm.$setPristine();
|
||||
|
||||
$('#newFolderModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.newFolder.busy = true;
|
||||
$scope.newFolder.error = null;
|
||||
|
||||
var filePath = sanitize($scope.newFolder.cwd + '/' + $scope.newFolder.name);
|
||||
|
||||
Client.filesCreateDirectory($scope.backendId, $scope.backendType, filePath, function (error) {
|
||||
$scope.newFolder.busy = false;
|
||||
if (error && error.statusCode === 409) return $scope.newFolder.error = 'exists';
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$scope.refresh();
|
||||
|
||||
$('#newFolderModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// extract archives
|
||||
|
||||
$scope.extractStatus = {
|
||||
error: null,
|
||||
busy: false,
|
||||
fileName: ''
|
||||
};
|
||||
|
||||
$scope.extractEntry = function (cwd, entry) {
|
||||
var filePath = sanitize(cwd + '/' + entry.fileName);
|
||||
|
||||
if (entry.isDirectory) return;
|
||||
|
||||
// prevent it from getting closed
|
||||
$('#extractModal').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
});
|
||||
|
||||
$scope.extractStatus.fileName = entry.fileName;
|
||||
$scope.extractStatus.error = null;
|
||||
$scope.extractStatus.busy = true;
|
||||
|
||||
Client.filesExtract($scope.backendId, $scope.backendType, filePath, function (error) {
|
||||
$scope.extractStatus.busy = false;
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.extractStatus.error = $translate.instant('filemanager.extract.error', error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
$('#extractModal').modal('hide');
|
||||
|
||||
$scope.refresh();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// split view handling
|
||||
|
||||
$scope.toggleSplitView = function () {
|
||||
$scope.splitView = !$scope.splitView;
|
||||
if (!$scope.splitView) {
|
||||
$scope.activeView = VIEW.LEFT;
|
||||
delete window.localStorage.splitView;
|
||||
} else {
|
||||
window.localStorage.splitView = true;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setActiveView = function (view) {
|
||||
$scope.activeView = view;
|
||||
};
|
||||
|
||||
|
||||
// monaco text editor
|
||||
|
||||
var LANGUAGES = [];
|
||||
require(['vs/editor/editor.main'], function() { LANGUAGES = monaco.languages.getLanguages(); });
|
||||
|
||||
function getLanguage(filename) {
|
||||
var ext = '.' + filename.split('.').pop();
|
||||
var language = LANGUAGES.find(function (l) {
|
||||
if (!l.extensions) return false;
|
||||
return !!l.extensions.find(function (e) { return e === ext; });
|
||||
}) || '';
|
||||
return language ? language.id : '';
|
||||
}
|
||||
|
||||
$scope.textEditor = {
|
||||
busy: false,
|
||||
cwd: null,
|
||||
entry: null,
|
||||
editor: null,
|
||||
unsaved: false,
|
||||
visible: false,
|
||||
|
||||
show: function (cwd, entry) {
|
||||
$scope.textEditor.cwd = cwd;
|
||||
$scope.textEditor.entry = entry;
|
||||
$scope.textEditor.busy = false;
|
||||
$scope.textEditor.unsaved = false;
|
||||
$scope.textEditor.visible = true;
|
||||
|
||||
// clear model if any
|
||||
if ($scope.textEditor.editor && $scope.textEditor.editor.getModel()) $scope.textEditor.editor.setModel(null);
|
||||
|
||||
$scope.viewerOpen = true;
|
||||
// document.getElementById('textEditorModal').style['display'] = 'flex';
|
||||
|
||||
var filePath = sanitize($scope.textEditor.cwd + '/' + entry.fileName);
|
||||
var language = getLanguage(entry.fileName);
|
||||
|
||||
Client.filesGet($scope.backendId, $scope.backendType, filePath, 'data', function (error, result) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
if (!$scope.textEditor.editor) {
|
||||
$timeout(function () {
|
||||
$scope.textEditor.editor = monaco.editor.create(document.getElementById('textEditorContainer'), {
|
||||
value: result,
|
||||
language: language,
|
||||
theme: 'vs-dark'
|
||||
});
|
||||
$scope.textEditor.editor.getModel().onDidChangeContent(function () { $scope.textEditor.unsaved = true; });
|
||||
}, 200);
|
||||
} else {
|
||||
$scope.textEditor.editor.setModel(monaco.editor.createModel(result, language));
|
||||
$scope.textEditor.editor.getModel().onDidChangeContent(function () { $scope.textEditor.unsaved = true; }); // have to re-attach whenever model changes
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
save: function (callback) {
|
||||
$scope.textEditor.busy = true;
|
||||
|
||||
var newContent = $scope.textEditor.editor.getValue();
|
||||
var filePath = sanitize($scope.textEditor.cwd + '/' + $scope.textEditor.entry.fileName);
|
||||
var file = new File([newContent], 'file');
|
||||
|
||||
Client.filesUpload($scope.backendId, $scope.backendType, filePath, file, true, function () {}, function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$scope.refresh();
|
||||
|
||||
$timeout(function () {
|
||||
$scope.textEditor.unsaved = false;
|
||||
$scope.textEditor.busy = false;
|
||||
if (typeof callback === 'function') return callback();
|
||||
}, 1000);
|
||||
});
|
||||
},
|
||||
|
||||
close: function () {
|
||||
$scope.textEditor.visible = false;
|
||||
$scope.viewerOpen = false;
|
||||
$('#textEditorCloseModal').modal('hide');
|
||||
},
|
||||
|
||||
onClose: function () {
|
||||
$scope.textEditor.visible = false;
|
||||
$scope.viewerOpen = false;
|
||||
$('#textEditorCloseModal').modal('hide');
|
||||
},
|
||||
|
||||
saveAndClose: function () {
|
||||
$scope.textEditor.save(function () {
|
||||
$scope.textEditor.onClose();
|
||||
});
|
||||
},
|
||||
|
||||
maybeClose: function () {
|
||||
if (!$scope.textEditor.unsaved) return $scope.textEditor.onClose();
|
||||
$('#textEditorCloseModal').modal('show');
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
// restart app or mail logic
|
||||
|
||||
$scope.restartBusy = false;
|
||||
|
||||
$scope.onRestartApp = function () {
|
||||
$scope.restartBusy = true;
|
||||
|
||||
function waitUntilRestarted(callback) {
|
||||
Client.getApp($scope.backendId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result.installationState === ISTATES.INSTALLED) return callback();
|
||||
setTimeout(waitUntilRestarted.bind(null, callback), 2000);
|
||||
});
|
||||
}
|
||||
|
||||
Client.restartApp($scope.backendId, function (error) {
|
||||
if (error) console.error('Failed to restart app.', error);
|
||||
|
||||
waitUntilRestarted(function (error) {
|
||||
if (error) console.error('Failed wait for restart.', error);
|
||||
|
||||
$scope.restartBusy = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onRestartMail = function () {
|
||||
$scope.restartBusy = true;
|
||||
|
||||
function waitUntilRestarted(callback) {
|
||||
Client.getService('mail', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result.status === 'active') return callback();
|
||||
setTimeout(waitUntilRestarted.bind(null, callback), 2000);
|
||||
});
|
||||
}
|
||||
|
||||
Client.restartService('mail', function (error) {
|
||||
if (error) console.error('Failed to restart mail.', error);
|
||||
|
||||
waitUntilRestarted(function (error) {
|
||||
if (error) console.error('Failed wait for restart.', error);
|
||||
|
||||
$scope.restartBusy = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// init code
|
||||
|
||||
function fetchVolumesInfo(mounts) {
|
||||
$scope.volumes = [];
|
||||
|
||||
async.each(mounts, function (mount, callback) {
|
||||
Client.getVolume(mount.volumeId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
$scope.volumes.push(result);
|
||||
|
||||
callback();
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) console.error('Failed to fetch volumes info.', error);
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
|
||||
Client.getStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
if (!status.activated) {
|
||||
console.log('Not activated yet, redirecting', status);
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
// check version and force reload if needed
|
||||
if (!localStorage.version) {
|
||||
localStorage.version = status.version;
|
||||
} else if (localStorage.version !== status.version) {
|
||||
localStorage.version = status.version;
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
$scope.status = status;
|
||||
|
||||
console.log('Running filemanager version ', localStorage.version);
|
||||
|
||||
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
|
||||
Client.refreshUserInfo(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
var getter;
|
||||
if ($scope.backendType === 'app') {
|
||||
getter = Client.getApp.bind(Client, $scope.backendId);
|
||||
} else if ($scope.backendType === 'volume') {
|
||||
getter = Client.getVolume.bind(Client, $scope.backendId);
|
||||
} else if ($scope.backendType === 'mail') {
|
||||
getter = function (next) { next(null, null); };
|
||||
}
|
||||
|
||||
getter(function (error, result) {
|
||||
if (error) {
|
||||
$scope.initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// fine to do async
|
||||
if ($scope.backendType === 'app') fetchVolumesInfo(result.mounts || []);
|
||||
|
||||
switch ($scope.backendType) {
|
||||
case 'app':
|
||||
$scope.title = result.label || result.fqdn;
|
||||
$scope.rootDirLabel = '/app/data/';
|
||||
$scope.applicationLink = 'https://' + result.fqdn;
|
||||
break;
|
||||
case 'volume':
|
||||
$scope.title = result.name;
|
||||
$scope.rootDirLabel = result.hostPath;
|
||||
break;
|
||||
case 'mail':
|
||||
$scope.title = 'mail';
|
||||
$scope.rootDirLabel = 'mail';
|
||||
break;
|
||||
}
|
||||
|
||||
window.document.title = $scope.title + ' - ' + $translate.instant('filemanager.title');
|
||||
|
||||
// now mark the Client to be ready
|
||||
Client.setReady();
|
||||
|
||||
// openPath('');
|
||||
|
||||
$scope.initialized = true;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
|
||||
// toplevel key input handling
|
||||
|
||||
window.addEventListener('keydown', function (event) {
|
||||
if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 's') {
|
||||
if (!$scope.textEditor.visible) return;
|
||||
|
||||
event.preventDefault();
|
||||
$scope.$apply($scope.textEditor.save);
|
||||
} else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'c') {
|
||||
if ($scope.textEditor.visible) return;
|
||||
if ($scope.selected.length === 0) return;
|
||||
if (isModalVisible()) return;
|
||||
|
||||
event.preventDefault();
|
||||
$scope.$apply($scope.actionCopy);
|
||||
} else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'x') {
|
||||
if ($scope.textEditor.visible) return;
|
||||
if ($scope.selected.length === 0) return;
|
||||
if (isModalVisible()) return;
|
||||
|
||||
event.preventDefault();
|
||||
$scope.$apply($scope.actionCut);
|
||||
} else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'v') {
|
||||
if ($scope.textEditor.visible) return;
|
||||
if ($scope.clipboard.length === 0) return;
|
||||
if (isModalVisible()) return;
|
||||
|
||||
event.preventDefault();
|
||||
$scope.$apply($scope.actionPaste);
|
||||
} else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'a') {
|
||||
if ($scope.textEditor.visible) return;
|
||||
if (isModalVisible()) return;
|
||||
|
||||
event.preventDefault();
|
||||
$scope.$apply($scope.actionSelectAll);
|
||||
} else if(event.key === 'Escape') {
|
||||
if ($scope.textEditor.visible) return $scope.$apply($scope.textEditor.maybeClose);
|
||||
else $scope.$apply(function () { $scope.selected = []; });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['newFileModal', 'newFolderModal', 'renameEntryModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find('[autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
|
||||
// selects filename (without extension)
|
||||
['renameEntryModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
var elem = $(this).find('[autofocus]:first');
|
||||
var text = elem.val();
|
||||
elem[0].setSelectionRange(0, text.indexOf('.'));
|
||||
});
|
||||
});
|
||||
}]);
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
/* global ERROR,ISTATES,HSTATES,RSTATES,APP_TYPES,NOTIFICATION_TYPES */
|
||||
/* global ERROR,ISTATES,HSTATES,RSTATES,APP_TYPES */
|
||||
|
||||
// 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; }, {});
|
||||
@@ -50,9 +50,6 @@ app.config(['$routeProvider', function ($routeProvider) {
|
||||
}).when('/users', {
|
||||
controller: 'UsersController',
|
||||
templateUrl: 'views/users.html?<%= revision %>'
|
||||
}).when('/usersettings', {
|
||||
controller: 'UserSettingsController',
|
||||
templateUrl: 'views/user-settings.html?<%= revision %>'
|
||||
}).when('/app/:appId/:view?', {
|
||||
controller: 'AppController',
|
||||
templateUrl: 'views/app.html?<%= revision %>'
|
||||
@@ -96,7 +93,8 @@ app.config(['$routeProvider', function ($routeProvider) {
|
||||
controller: 'NotificationsController',
|
||||
templateUrl: 'views/notifications.html?<%= revision %>'
|
||||
}).when('/oidc', {
|
||||
redirectTo: '/usersettings'
|
||||
controller: 'OidcController',
|
||||
templateUrl: 'views/oidc.html?<%= revision %>'
|
||||
}).when('/settings', {
|
||||
controller: 'SettingsController',
|
||||
templateUrl: 'views/settings.html?<%= revision %>'
|
||||
@@ -118,26 +116,6 @@ app.config(['$routeProvider', function ($routeProvider) {
|
||||
}).otherwise({ redirectTo: '/'});
|
||||
}]);
|
||||
|
||||
app.filter('notificadtionTypeToColor', function () {
|
||||
return function (n) {
|
||||
switch (n.type) {
|
||||
case NOTIFICATION_TYPES.ALERT_REBOOT:
|
||||
case NOTIFICATION_TYPES.ALERT_APP_OOM:
|
||||
case NOTIFICATION_TYPES.ALERT_MAIL_STATUS:
|
||||
case NOTIFICATION_TYPES.ALERT_CERTIFICATE_RENEWAL_FAILED:
|
||||
case NOTIFICATION_TYPES.ALERT_DISK_SPACE:
|
||||
case NOTIFICATION_TYPES.ALERT_BACKUP_CONFIG:
|
||||
case NOTIFICATION_TYPES.ALERT_BACKUP_FAILED:
|
||||
return '#ff4c4c';
|
||||
case NOTIFICATION_TYPES.ALERT_BOX_UPDATE:
|
||||
case NOTIFICATION_TYPES.ALERT_MANUAL_APP_UPDATE:
|
||||
return '#f0ad4e';
|
||||
default:
|
||||
return '#2196f3';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('capitalize', function () {
|
||||
return function (s) {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
@@ -319,7 +297,6 @@ app.filter('installationStateLabel', function () {
|
||||
case ISTATES.PENDING_LOCATION_CHANGE:
|
||||
case ISTATES.PENDING_CONFIGURE:
|
||||
case ISTATES.PENDING_RECREATE_CONTAINER:
|
||||
case ISTATES.PENDING_SERVICES_CHANGE:
|
||||
case ISTATES.PENDING_DEBUG:
|
||||
return 'Configuring' + waiting;
|
||||
case ISTATES.PENDING_RESIZE:
|
||||
@@ -630,219 +607,3 @@ app.config(['fitTextConfigProvider', function (fitTextConfigProvider) {
|
||||
max: 24
|
||||
};
|
||||
}]);
|
||||
|
||||
app.controller('MainController', ['$scope', '$route', '$timeout', '$location', '$interval', 'Notification', 'Client', function ($scope, $route, $timeout, $location, $interval, Notification, Client) {
|
||||
$scope.initialized = false; // used to animate the UI
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
$scope.config = {};
|
||||
$scope.client = Client;
|
||||
$scope.subscription = {};
|
||||
$scope.notificationCount = 0;
|
||||
$scope.hideNavBarActions = $location.path() === '/logs';
|
||||
$scope.backgroundImageUrl = '';
|
||||
|
||||
$scope.reboot = {
|
||||
busy: false,
|
||||
|
||||
show: function () {
|
||||
$scope.reboot.busy = false;
|
||||
$('#rebootModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.reboot.busy = true;
|
||||
|
||||
Client.reboot(function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$('#rebootModal').modal('hide');
|
||||
|
||||
// trigger refetch to show offline banner
|
||||
$timeout(function () { Client.getStatus(function () {}); }, 5000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.isActive = function (url) {
|
||||
if (!$route.current) return false;
|
||||
return $route.current.$$route.originalPath.indexOf(url) === 0;
|
||||
};
|
||||
|
||||
$scope.logout = function (event) {
|
||||
event.stopPropagation();
|
||||
$scope.initialized = false;
|
||||
Client.logout();
|
||||
};
|
||||
|
||||
$scope.openSubscriptionSetup = function () {
|
||||
Client.openSubscriptionSetup($scope.subscription);
|
||||
};
|
||||
|
||||
// NOTE: this function is exported and called from the appstore.js
|
||||
$scope.updateSubscriptionStatus = function () {
|
||||
Client.getSubscription(function (error, subscription) {
|
||||
if (error && error.statusCode === 412) return; // not yet registered
|
||||
if (error && error.statusCode === 402) return; // invalid appstore token
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.subscription = subscription;
|
||||
});
|
||||
};
|
||||
|
||||
function refreshNotifications() {
|
||||
if (!Client.getUserInfo().isAtLeastAdmin) return;
|
||||
|
||||
Client.getNotifications({ acknowledged: false }, 1, 100, function (error, results) { // counter maxes out at 100
|
||||
if (error) console.error(error);
|
||||
else $scope.notificationCount = results.length;
|
||||
});
|
||||
}
|
||||
|
||||
// update state of acknowledged notification
|
||||
$scope.notificationAcknowledged = function () {
|
||||
refreshNotifications();
|
||||
};
|
||||
|
||||
function redirectOnMandatory2FA() {
|
||||
if (Client.getConfig().mandatory2FA && !Client.getUserInfo().twoFactorAuthenticationEnabled) {
|
||||
$location.path('/profile').search({ setup2fa: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Make it redirect if the browser URL is changed directly - https://forum.cloudron.io/topic/7510/bug-in-2fa-force
|
||||
$scope.$on('$routeChangeStart', function (/* event */) {
|
||||
if ($scope.initialized) redirectOnMandatory2FA();
|
||||
});
|
||||
|
||||
var gPlatformStatusNotification = null;
|
||||
function trackPlatformStatus() {
|
||||
Client.getPlatformStatus(function (error, result) {
|
||||
if (error) return console.error('Failed to get platform status.', error);
|
||||
|
||||
// see box/src/platform.js
|
||||
if (result.message === 'Ready') {
|
||||
if (gPlatformStatusNotification) {
|
||||
gPlatformStatusNotification.kill();
|
||||
gPlatformStatusNotification = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gPlatformStatusNotification) {
|
||||
var options = { title: 'Platform status', message: result.message, delay: 'notimeout', replaceMessage: true, closeOnClick: false };
|
||||
|
||||
Notification.primary(options).then(function (result) {
|
||||
gPlatformStatusNotification = result;
|
||||
$timeout(trackPlatformStatus, 5000);
|
||||
});
|
||||
} else {
|
||||
gPlatformStatusNotification.message = result.message;
|
||||
$timeout(trackPlatformStatus, 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function redirectIfNeeded(status) {
|
||||
if (!status.activated) {
|
||||
console.log('Not activated yet, redirecting', status);
|
||||
if (status.restore.active || status.restore.errorMessage) { // show the error message in restore page
|
||||
window.location.href = '/restore.html' + window.location.search;
|
||||
} else if (status.adminFqdn) {
|
||||
window.location.href = 'https://' + status.adminFqdn + '/setup.html' + (window.location.search);
|
||||
} else {
|
||||
window.location.href = '/setupdns.html' + window.location.search;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// support local development with localhost check
|
||||
if (window.location.hostname !== status.adminFqdn && window.location.hostname !== 'localhost' && !window.location.hostname.startsWith('192.')) {
|
||||
// user is accessing by IP or by the old admin location (pre-migration)
|
||||
window.location.href = '/setupdns.html' + window.location.search;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// this loads the very first thing when accessing via IP or domain
|
||||
function init() {
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
if (redirectIfNeeded(status)) return;
|
||||
|
||||
// check version and force reload if needed
|
||||
if (!localStorage.version) {
|
||||
localStorage.version = status.version;
|
||||
} else if (localStorage.version !== status.version) {
|
||||
localStorage.version = status.version;
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
console.log('Running dashboard version ', localStorage.version);
|
||||
|
||||
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
|
||||
Client.refreshUserInfo(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
Client.refreshConfig(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
Client.refreshAvailableLanguages(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
Client.refreshInstalledApps(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
// now mark the Client to be ready
|
||||
Client.setReady();
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
if (Client.getUserInfo().hasBackgroundImage) {
|
||||
document.getElementById('mainContentContainer').style.backgroundImage = 'url("' + Client.getBackgroundImageUrl() + '")';
|
||||
document.getElementById('mainContentContainer').classList.add('has-background');
|
||||
}
|
||||
|
||||
$scope.initialized = true;
|
||||
|
||||
redirectOnMandatory2FA();
|
||||
|
||||
$interval(refreshNotifications, 60 * 1000);
|
||||
refreshNotifications();
|
||||
|
||||
Client.getSubscription(function (error, subscription) {
|
||||
if (error && error.statusCode === 412) return; // not yet registered
|
||||
if (error && error.statusCode === 402) return; // invalid appstore token
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.subscription = subscription;
|
||||
|
||||
// only track platform status if we are registered
|
||||
trackPlatformStatus();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Client.onConfig(function (config) {
|
||||
if (config.cloudronName) {
|
||||
document.title = config.cloudronName;
|
||||
}
|
||||
});
|
||||
|
||||
init();
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['updateModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find('[autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -57,7 +57,7 @@ translateFilterFactory.displayName = 'translateFilterFactory';
|
||||
app.filter('tr', translateFilterFactory);
|
||||
|
||||
|
||||
app.controller('PasswordResetController', ['$scope', '$translate', '$http', function ($scope, $translate, $http) {
|
||||
app.controller('LoginController', ['$scope', '$translate', '$http', function ($scope, $translate, $http) {
|
||||
// Stupid angular location provider either wants html5 location mode or not, do the query parsing on my own
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.indexOf('=') === -1 ? [item, true] : [item.slice(0, item.indexOf('=')), item.slice(item.indexOf('=')+1)]; }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
@@ -65,7 +65,7 @@ app.controller('PasswordResetController', ['$scope', '$translate', '$http', func
|
||||
$scope.mode = '';
|
||||
$scope.busy = false;
|
||||
$scope.error = false;
|
||||
$scope.branding = null;
|
||||
$scope.status = null;
|
||||
$scope.username = '';
|
||||
$scope.password = '';
|
||||
$scope.totpToken = '';
|
||||
@@ -74,6 +74,50 @@ app.controller('PasswordResetController', ['$scope', '$translate', '$http', func
|
||||
$scope.newPasswordRepeat = '';
|
||||
var API_ORIGIN = '<%= apiOrigin %>' || window.location.origin;
|
||||
|
||||
$scope.onLogin = function () {
|
||||
$scope.busy = true;
|
||||
$scope.error = false;
|
||||
|
||||
var data = {
|
||||
username: $scope.username,
|
||||
password: $scope.password,
|
||||
totpToken: $scope.totpToken
|
||||
};
|
||||
|
||||
function error(data, status) {
|
||||
$scope.busy = false;
|
||||
$scope.error = {};
|
||||
|
||||
if (!data || status !== 401) return $scope.error.internal = true;
|
||||
|
||||
if (data.message === 'Username and password does not match') {
|
||||
$scope.error.password = true;
|
||||
$scope.password = '';
|
||||
setTimeout(function () { $('#inputPassword').focus(); }, 200);
|
||||
} else if (data.message.indexOf('totpToken') !== -1) {
|
||||
$scope.error.totpToken = true;
|
||||
$scope.totpToken = '';
|
||||
setTimeout(function () { $('#inputTotpToken').focus(); }, 200);
|
||||
} else {
|
||||
$scope.error.generic = true;
|
||||
}
|
||||
|
||||
$scope.loginForm.$setPristine();
|
||||
}
|
||||
|
||||
$http.post(API_ORIGIN + '/api/v1/cloudron/login', data).success(function (data, status) {
|
||||
if (status !== 200) return error(data, status);
|
||||
|
||||
localStorage.token = data.accessToken;
|
||||
|
||||
// prevent redirecting to random domains
|
||||
var returnTo = search.returnTo || '/';
|
||||
if (returnTo.indexOf('/') !== 0) returnTo = '/';
|
||||
|
||||
window.location.href = returnTo;
|
||||
}).error(error);
|
||||
};
|
||||
|
||||
$scope.onPasswordReset = function () {
|
||||
$scope.busy = true;
|
||||
|
||||
@@ -86,7 +130,7 @@ app.controller('PasswordResetController', ['$scope', '$translate', '$http', func
|
||||
$scope.mode = 'passwordResetDone';
|
||||
}
|
||||
|
||||
$http.post(API_ORIGIN + '/api/v1/auth/password_reset_request', data).success(done).error(done);
|
||||
$http.post(API_ORIGIN + '/api/v1/cloudron/password_reset_request', data).success(done).error(done);
|
||||
};
|
||||
|
||||
$scope.onNewPassword = function () {
|
||||
@@ -107,7 +151,7 @@ app.controller('PasswordResetController', ['$scope', '$translate', '$http', func
|
||||
else $scope.error = 'Unknown error';
|
||||
}
|
||||
|
||||
$http.post(API_ORIGIN + '/api/v1/auth/password_reset', data).success(function (data, status) {
|
||||
$http.post(API_ORIGIN + '/api/v1/cloudron/password_reset', data).success(function (data, status) {
|
||||
if (status !== 202) return error(data, status);
|
||||
|
||||
// set token to autologin
|
||||
@@ -126,20 +170,28 @@ app.controller('PasswordResetController', ['$scope', '$translate', '$http', func
|
||||
setTimeout(function () { $('#inputPasswordResetIdentifier').focus(); }, 200);
|
||||
};
|
||||
|
||||
$scope.showLogin = function () {
|
||||
if ($scope.status) window.document.title = $scope.status.cloudronName + ' Login';
|
||||
$scope.mode = 'login';
|
||||
$scope.error = false;
|
||||
setTimeout(function () { $('#inputUsername').focus(); }, 200);
|
||||
};
|
||||
|
||||
$scope.showNewPassword = function () {
|
||||
window.document.title = 'Set New Password';
|
||||
$scope.mode = 'newPassword';
|
||||
setTimeout(function () { $('#inputNewPassword').focus(); }, 200);
|
||||
};
|
||||
|
||||
$http.get(API_ORIGIN + '/api/v1/auth/branding').success(function (data, status) {
|
||||
$http.get(API_ORIGIN + '/api/v1/cloudron/status').success(function (data, status) {
|
||||
$scope.initialized = true;
|
||||
|
||||
if (status !== 200) return;
|
||||
|
||||
if (data.language) $translate.use(data.language);
|
||||
|
||||
$scope.branding = data;
|
||||
if ($scope.mode === 'login') window.document.title = data.cloudronName + ' Login';
|
||||
$scope.status = data;
|
||||
}).error(function () {
|
||||
$scope.initialized = false;
|
||||
});
|
||||
@@ -153,6 +205,6 @@ app.controller('PasswordResetController', ['$scope', '$translate', '$http', func
|
||||
localStorage.token = search.accessToken || search.access_token;
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
$scope.showPasswordReset();
|
||||
$scope.showLogin();
|
||||
}
|
||||
}]);
|
||||
@@ -0,0 +1,202 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global moment */
|
||||
/* global $ */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification']);
|
||||
|
||||
app.controller('LogsController', ['$scope', '$translate', 'Client', function ($scope, $translate, Client) {
|
||||
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; }, {});
|
||||
|
||||
$scope.initialized = false;
|
||||
$scope.client = Client;
|
||||
$scope.selected = '';
|
||||
$scope.activeEventSource = null;
|
||||
$scope.lines = 100;
|
||||
$scope.selectedAppInfo = null;
|
||||
$scope.selectedTaskInfo = null;
|
||||
|
||||
function ab2str(buf) {
|
||||
return String.fromCharCode.apply(null, new Uint16Array(buf));
|
||||
}
|
||||
|
||||
$scope.clear = function () {
|
||||
var logViewer = $('.logs-container');
|
||||
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) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.activeEventSource = result;
|
||||
result.onmessage = function handleMessage(message) {
|
||||
var data;
|
||||
|
||||
try {
|
||||
data = JSON.parse(message.data);
|
||||
} catch (e) {
|
||||
return console.error(e);
|
||||
}
|
||||
|
||||
// check if we want to auto scroll (this is before the appending, as that skews the check)
|
||||
var tmp = $('.logs-container');
|
||||
var autoScroll = tmp[0].scrollTop > (tmp[0].scrollHeight - tmp.innerHeight() - 24);
|
||||
|
||||
var logLine = $('<div class="log-line">');
|
||||
// realtimeTimestamp is 0 if line is blank or some parse error
|
||||
var timeString = data.realtimeTimestamp ? moment(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))));
|
||||
tmp.append(logLine);
|
||||
|
||||
if (autoScroll) tmp[0].lastChild.scrollIntoView({ behavior: 'instant', block: 'end' });
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function select(ids, callback) {
|
||||
if (ids.id && ids.id.indexOf('redis:') === 0) {
|
||||
$scope.selected = {
|
||||
name: 'Redis',
|
||||
type: 'service',
|
||||
value: ids.id,
|
||||
url: Client.makeURL('/api/v1/services/' + ids.id + '/logs')
|
||||
};
|
||||
callback();
|
||||
} else if (ids.id) {
|
||||
var BUILT_IN_LOGS = [
|
||||
{ name: 'Box', type: 'platform', value: 'box', url: Client.makeURL('/api/v1/cloudron/logs/box') },
|
||||
{ name: 'Graphite', type: 'service', value: 'graphite', url: Client.makeURL('/api/v1/services/graphite/logs') },
|
||||
{ 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: 'Nginx', type: 'service', value: 'nginx', url: Client.makeURL('/api/v1/services/nginx/logs') },
|
||||
{ name: 'Unbound', type: 'service', value: 'unbound', url: Client.makeURL('/api/v1/services/unbound/logs') },
|
||||
{ name: 'SFTP', type: 'service', value: 'sftp', url: Client.makeURL('/api/v1/services/sftp/logs') },
|
||||
{ name: 'TURN/STUN', type: 'service', value: 'turn', url: Client.makeURL('/api/v1/services/turn/logs') },
|
||||
];
|
||||
|
||||
$scope.selected = BUILT_IN_LOGS.find(function (e) { return e.value === ids.id; });
|
||||
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)
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
|
||||
Client.getStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
if (!status.activated) {
|
||||
console.log('Not activated yet, redirecting', status);
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
// check version and force reload if needed
|
||||
if (!localStorage.version) {
|
||||
localStorage.version = status.version;
|
||||
} else if (localStorage.version !== status.version) {
|
||||
localStorage.version = status.version;
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
console.log('Running log version ', localStorage.version);
|
||||
|
||||
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
|
||||
Client.refreshUserInfo(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
Client.refreshConfig(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
select({ id: search.id, taskId: search.taskId, appId: search.appId, crashId: search.crashId }, function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
// now mark the Client to be ready
|
||||
Client.setReady();
|
||||
|
||||
$scope.initialized = true;
|
||||
|
||||
showLogs();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
$translate([ 'logs.title' ]).then(function (tr) {
|
||||
if (tr['logs.title'] !== 'logs.title') window.document.title = tr['logs.title'];
|
||||
});
|
||||
}]);
|
||||
@@ -0,0 +1,217 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
|
||||
angular.module('Application').controller('MainController', ['$scope', '$route', '$timeout', '$location', '$interval', 'Notification', 'Client', function ($scope, $route, $timeout, $location, $interval, Notification, Client) {
|
||||
$scope.initialized = false; // used to animate the UI
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
$scope.config = {};
|
||||
$scope.client = Client;
|
||||
$scope.subscription = {};
|
||||
$scope.notificationCount = 0;
|
||||
$scope.hideNavBarActions = $location.path() === '/logs';
|
||||
$scope.backgroundImageUrl = '';
|
||||
|
||||
$scope.reboot = {
|
||||
busy: false,
|
||||
|
||||
show: function () {
|
||||
$scope.reboot.busy = false;
|
||||
$('#rebootModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.reboot.busy = true;
|
||||
|
||||
Client.reboot(function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$('#rebootModal').modal('hide');
|
||||
|
||||
// trigger refetch to show offline banner
|
||||
$timeout(function () { Client.getStatus(function () {}); }, 5000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.isActive = function (url) {
|
||||
if (!$route.current) return false;
|
||||
return $route.current.$$route.originalPath.indexOf(url) === 0;
|
||||
};
|
||||
|
||||
$scope.logout = function (event) {
|
||||
event.stopPropagation();
|
||||
$scope.initialized = false;
|
||||
Client.logout();
|
||||
};
|
||||
|
||||
$scope.openSubscriptionSetup = function () {
|
||||
Client.openSubscriptionSetup($scope.subscription);
|
||||
};
|
||||
|
||||
// NOTE: this function is exported and called from the appstore.js
|
||||
$scope.updateSubscriptionStatus = function () {
|
||||
Client.getSubscription(function (error, subscription) {
|
||||
if (error && error.statusCode === 412) return; // not yet registered
|
||||
if (error && error.statusCode === 402) return; // invalid appstore token
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.subscription = subscription;
|
||||
});
|
||||
};
|
||||
|
||||
function refreshNotifications() {
|
||||
if (!Client.getUserInfo().isAtLeastAdmin) return;
|
||||
|
||||
Client.getNotifications({ acknowledged: false }, 1, 100, function (error, results) { // counter maxes out at 100
|
||||
if (error) console.error(error);
|
||||
else $scope.notificationCount = results.length;
|
||||
});
|
||||
}
|
||||
|
||||
// update state of acknowledged notification
|
||||
$scope.notificationAcknowledged = function () {
|
||||
refreshNotifications();
|
||||
};
|
||||
|
||||
function redirectOnMandatory2FA() {
|
||||
if (Client.getConfig().mandatory2FA && !Client.getUserInfo().twoFactorAuthenticationEnabled) {
|
||||
$location.path('/profile').search({ setup2fa: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Make it redirect if the browser URL is changed directly - https://forum.cloudron.io/topic/7510/bug-in-2fa-force
|
||||
$scope.$on('$routeChangeStart', function (/* event */) {
|
||||
if ($scope.initialized) redirectOnMandatory2FA();
|
||||
});
|
||||
|
||||
var gPlatformStatusNotification = null;
|
||||
function trackPlatformStatus() {
|
||||
Client.getPlatformStatus(function (error, result) {
|
||||
if (error) return console.error('Failed to get platform status.', error);
|
||||
|
||||
// see box/src/platform.js
|
||||
if (result.message === 'Ready') {
|
||||
if (gPlatformStatusNotification) {
|
||||
gPlatformStatusNotification.kill();
|
||||
gPlatformStatusNotification = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gPlatformStatusNotification) {
|
||||
var options = { title: 'Platform status', message: result.message, delay: 'notimeout', replaceMessage: true, closeOnClick: false };
|
||||
|
||||
Notification.primary(options).then(function (result) {
|
||||
gPlatformStatusNotification = result;
|
||||
$timeout(trackPlatformStatus, 5000);
|
||||
});
|
||||
} else {
|
||||
gPlatformStatusNotification.message = result.message;
|
||||
$timeout(trackPlatformStatus, 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
Client.getStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
// WARNING if anything about the routing is changed here test these use-cases:
|
||||
//
|
||||
// 1. Caas
|
||||
// 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
|
||||
window.location.href = '/restore.html' + window.location.search;
|
||||
} else {
|
||||
window.location.href = (status.adminFqdn ? '/setup.html' : '/setupdns.html') + window.location.search;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// support local development with localhost check
|
||||
if (window.location.hostname !== status.adminFqdn && window.location.hostname !== 'localhost' && !window.location.hostname.startsWith('192.')) {
|
||||
// user is accessing by IP or by the old admin location (pre-migration)
|
||||
window.location.href = '/setupdns.html' + window.location.search;
|
||||
return;
|
||||
}
|
||||
|
||||
// check version and force reload if needed
|
||||
if (!localStorage.version) {
|
||||
localStorage.version = status.version;
|
||||
} else if (localStorage.version !== status.version) {
|
||||
localStorage.version = status.version;
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
console.log('Running dashboard version ', localStorage.version);
|
||||
|
||||
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
|
||||
Client.refreshUserInfo(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
Client.refreshConfig(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
Client.refreshAvailableLanguages(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
Client.refreshInstalledApps(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
// now mark the Client to be ready
|
||||
Client.setReady();
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
if (Client.getUserInfo().hasBackgroundImage) {
|
||||
document.getElementById('mainContentContainer').style.backgroundImage = 'url("' + Client.getBackgroundImageUrl() + '")';
|
||||
document.getElementById('mainContentContainer').classList.add('has-background');
|
||||
}
|
||||
|
||||
$scope.initialized = true;
|
||||
|
||||
redirectOnMandatory2FA();
|
||||
|
||||
$interval(refreshNotifications, 60 * 1000);
|
||||
refreshNotifications();
|
||||
|
||||
Client.getSubscription(function (error, subscription) {
|
||||
if (error && error.statusCode === 412) return; // not yet registered
|
||||
if (error && error.statusCode === 402) return; // invalid appstore token
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.subscription = subscription;
|
||||
|
||||
// only track platform status if we are registered
|
||||
trackPlatformStatus();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Client.onConfig(function (config) {
|
||||
if (config.cloudronName) {
|
||||
document.title = config.cloudronName;
|
||||
}
|
||||
});
|
||||
|
||||
init();
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['updateModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find('[autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
}]);
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
/* global $, angular, tld, SECRET_PLACEHOLDER, STORAGE_PROVIDERS, BACKUP_FORMATS */
|
||||
/* global REGIONS_S3, REGIONS_WASABI, REGIONS_DIGITALOCEAN, REGIONS_EXOSCALE, REGIONS_SCALEWAY, REGIONS_LINODE, REGIONS_OVH, REGIONS_IONOS, REGIONS_UPCLOUD, REGIONS_VULTR, REGIONS_CONTABO */
|
||||
/* global REGIONS_S3, REGIONS_WASABI, REGIONS_DIGITALOCEAN, REGIONS_EXOSCALE, REGIONS_SCALEWAY, REGIONS_LINODE, REGIONS_OVH, REGIONS_IONOS, REGIONS_UPCLOUD, REGIONS_VULTR */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
@@ -41,8 +41,6 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
$scope.encrypted = false; // only used if a backup config contains that flag
|
||||
$scope.setupToken = '';
|
||||
$scope.skipDnsSetup = false;
|
||||
$scope.disk = null;
|
||||
$scope.blockDevices = [];
|
||||
|
||||
$scope.mountOptions = {
|
||||
host: '',
|
||||
@@ -56,11 +54,6 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
privateKey: ''
|
||||
};
|
||||
|
||||
$scope.$watch('disk', function (newValue) {
|
||||
if (!newValue) return;
|
||||
$scope.mountOptions.diskPath = '/dev/disk/by-uuid/' + newValue.uuid;
|
||||
});
|
||||
|
||||
$scope.sysinfo = {
|
||||
provider: 'generic',
|
||||
ipv4: '',
|
||||
@@ -92,7 +85,6 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
$scope.ionosRegions = REGIONS_IONOS;
|
||||
$scope.upcloudRegions = REGIONS_UPCLOUD;
|
||||
$scope.vultrRegions = REGIONS_VULTR;
|
||||
$scope.contaboRegions = REGIONS_CONTABO;
|
||||
|
||||
$scope.storageProviders = STORAGE_PROVIDERS;
|
||||
|
||||
@@ -102,12 +94,11 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' || provider === 'exoscale-sos'
|
||||
|| provider === 'digitalocean-spaces' || provider === 'wasabi' || provider === 'scaleway-objectstorage'
|
||||
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'backblaze-b2' || provider === 'cloudflare-r2'
|
||||
|| provider === 'ionos-objectstorage' || provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage' || provider === 'idrive-e2'
|
||||
|| provider === 'contabo-objectstorage';
|
||||
|| provider === 'ionos-objectstorage' || provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage' || provider === 'idrive-e2';
|
||||
};
|
||||
|
||||
$scope.mountlike = function (provider) {
|
||||
return provider === 'disk' || provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs';
|
||||
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs';
|
||||
};
|
||||
|
||||
$scope.restore = function () {
|
||||
@@ -160,10 +151,6 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
} else if (backupConfig.provider === 'vultr-objectstorage') {
|
||||
backupConfig.region = $scope.vultrRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'contabo-objectstorage') {
|
||||
backupConfig.region = $scope.contaboRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
backupConfig.s3ForcePathStyle = true; // https://docs.contabo.com/docs/products/Object-Storage/technical-description (no virtual buckets)
|
||||
} else if (backupConfig.provider === 'upcloud-objectstorage') {
|
||||
var m = /^.*\.(.*)\.upcloudobjects.com$/.exec(backupConfig.endpoint);
|
||||
backupConfig.region = m ? m[1] : 'us-east-1'; // let it fail in validation phase if m is not valid
|
||||
@@ -208,7 +195,7 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
backupConfig.mountOptions.port = $scope.mountOptions.port;
|
||||
backupConfig.mountOptions.privateKey = $scope.mountOptions.privateKey;
|
||||
}
|
||||
} else if (backupConfig.provider === 'disk' || backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs') {
|
||||
} else if (backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs') {
|
||||
backupConfig.mountOptions.diskPath = $scope.mountOptions.diskPath;
|
||||
} else if (backupConfig.provider === 'mountpoint') {
|
||||
backupConfig.mountPoint = $scope.mountPoint;
|
||||
@@ -243,7 +230,7 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
provider: $scope.sysinfo.provider
|
||||
};
|
||||
if ($scope.sysinfo.provider === 'fixed') {
|
||||
sysinfoConfig.ip = $scope.sysinfo.ipv4;
|
||||
sysinfoConfig.ipv4 = $scope.sysinfo.ipv4;
|
||||
} else if ($scope.sysinfo.provider === 'network-interface') {
|
||||
sysinfoConfig.ifname = $scope.sysinfo.ifname;
|
||||
}
|
||||
@@ -297,7 +284,7 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
function waitForRestore() {
|
||||
$scope.busy = true;
|
||||
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
Client.getStatus(function (error, status) {
|
||||
if (!error && !status.restore.active) { // restore finished
|
||||
if (status.restore.errorMessage) {
|
||||
$scope.busy = false;
|
||||
@@ -359,7 +346,7 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
};
|
||||
|
||||
function init() {
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
Client.getStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
if (status.restore.active) return waitForRestore();
|
||||
@@ -371,25 +358,10 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
return;
|
||||
}
|
||||
|
||||
Client.getProvisionBlockDevices(function (error, result) {
|
||||
if (error) {
|
||||
console.error('Failed to list blockdevices:', error);
|
||||
} else {
|
||||
// only offer non /, /boot or /home disks
|
||||
result = result.filter(function (d) { return d.mountpoint !== '/' && d.mountpoint !== '/home' && d.mountpoint !== '/boot'; });
|
||||
// only offer xfs and ext4 disks
|
||||
result = result.filter(function (d) { return d.type === 'xfs' || d.type === 'ext4'; });
|
||||
|
||||
// amend label for UI
|
||||
result.forEach(function (d) { d.label = d.path; });
|
||||
}
|
||||
|
||||
$scope.blockDevices = result;
|
||||
|
||||
$scope.instanceId = search.instanceId;
|
||||
$scope.setupToken = search.setupToken;
|
||||
$scope.initialized = true;
|
||||
});
|
||||
$scope.status = status;
|
||||
$scope.instanceId = search.instanceId;
|
||||
$scope.setupToken = search.setupToken;
|
||||
$scope.initialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -71,24 +71,22 @@ app.controller('SetupController', ['$scope', 'Client', function ($scope, Client)
|
||||
return;
|
||||
}
|
||||
|
||||
// if we are here from https://ip/setup.html ,go to https://admin/setup.html
|
||||
// if we are here from the ip first go to the real domain if already setup
|
||||
if (status.adminFqdn && status.adminFqdn !== window.location.hostname) {
|
||||
window.location.href = 'https://' + status.adminFqdn + '/setup.html';
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
// if we don't have a domain yet, first go to domain setup
|
||||
if (!status.adminFqdn) {
|
||||
window.location.href = '/setupdns.html';
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.activated) {
|
||||
window.location.href = '/';
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function setView(view) {
|
||||
@@ -100,10 +98,10 @@ app.controller('SetupController', ['$scope', 'Client', function ($scope, Client)
|
||||
}
|
||||
|
||||
function init() {
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
Client.getStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
if (redirectIfNeeded(status)) return;
|
||||
redirectIfNeeded(status);
|
||||
setView(search.view);
|
||||
|
||||
$scope.setupToken = search.setupToken;
|
||||
|
||||
@@ -70,7 +70,7 @@ app.controller('SetupAccountController', ['$scope', '$translate', '$http', funct
|
||||
$scope.busy = false;
|
||||
$scope.error = null;
|
||||
$scope.view = 'setup';
|
||||
$scope.branding = null;
|
||||
$scope.status = null;
|
||||
|
||||
$scope.profileLocked = !!search.profileLocked;
|
||||
$scope.existingUsername = !!search.username;
|
||||
@@ -119,7 +119,7 @@ app.controller('SetupAccountController', ['$scope', '$translate', '$http', funct
|
||||
}
|
||||
}
|
||||
|
||||
$http.post(API_ORIGIN + '/api/v1/auth/setup_account', data).success(function (data, status) {
|
||||
$http.post(API_ORIGIN + '/api/v1/cloudron/setup_account', data).success(function (data, status) {
|
||||
if (status !== 201) return error(data, status);
|
||||
|
||||
// set token to autologin
|
||||
@@ -133,14 +133,14 @@ app.controller('SetupAccountController', ['$scope', '$translate', '$http', funct
|
||||
$scope.view = 'noUsername';
|
||||
$scope.initialized = true;
|
||||
} else {
|
||||
$http.get(API_ORIGIN + '/api/v1/auth/branding').success(function (data, status) {
|
||||
$http.get(API_ORIGIN + '/api/v1/cloudron/status').success(function (data, status) {
|
||||
$scope.initialized = true;
|
||||
|
||||
if (status !== 200) return;
|
||||
|
||||
if (data.language) $translate.use(data.language);
|
||||
|
||||
$scope.branding = data;
|
||||
$scope.status = data;
|
||||
}).error(function () {
|
||||
$scope.initialized = false;
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
/* global $, tld, angular, Clipboard, ENDPOINTS_OVH */
|
||||
/* global $, tld, angular, Clipboard */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
@@ -55,8 +55,6 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
}
|
||||
};
|
||||
|
||||
$scope.ovhEndpoints = ENDPOINTS_OVH;
|
||||
|
||||
$scope.needsPort80 = function (dnsProvider, tlsProvider) {
|
||||
return ((dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') &&
|
||||
(tlsProvider === 'letsencrypt-prod' || tlsProvider === 'letsencrypt-staging'));
|
||||
@@ -84,7 +82,6 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
{ name: 'Bunny', value: 'bunny' },
|
||||
{ name: 'Cloudflare', value: 'cloudflare' },
|
||||
{ name: 'DigitalOcean', value: 'digitalocean' },
|
||||
{ name: 'DNSimple', value: 'dnsimple' },
|
||||
{ name: 'Gandi LiveDNS', value: 'gandi' },
|
||||
{ name: 'GoDaddy', value: 'godaddy' },
|
||||
{ name: 'Google Cloud DNS', value: 'gcdns' },
|
||||
@@ -93,7 +90,6 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
{ name: 'Name.com', value: 'namecom' },
|
||||
{ name: 'Namecheap', value: 'namecheap' },
|
||||
{ name: 'Netcup', value: 'netcup' },
|
||||
{ name: 'OVH', value: 'ovh' },
|
||||
{ name: 'Porkbun', value: 'porkbun' },
|
||||
{ name: 'Vultr', value: 'vultr' },
|
||||
{ name: 'Wildcard', value: 'wildcard' },
|
||||
@@ -116,7 +112,6 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
godaddyApiSecret: '',
|
||||
linodeToken: '',
|
||||
bunnyAccessKey: '',
|
||||
dnsimpleAccessToken: '',
|
||||
hetznerToken: '',
|
||||
vultrToken: '',
|
||||
nameComUsername: '',
|
||||
@@ -126,10 +121,6 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
netcupCustomerNumber: '',
|
||||
netcupApiKey: '',
|
||||
netcupApiPassword: '',
|
||||
ovhEndpoint: 'ovh-eu',
|
||||
ovhConsumerKey: '',
|
||||
ovhAppKey: '',
|
||||
ovhAppSecret: '',
|
||||
porkbunSecretapikey: '',
|
||||
porkbunApikey: '',
|
||||
|
||||
@@ -213,8 +204,6 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
config.token = $scope.dnsCredentials.linodeToken;
|
||||
} else if (provider === 'bunny') {
|
||||
config.token = $scope.dnsCredentials.bunnyAccessKey;
|
||||
} else if (provider === 'dnsimple') {
|
||||
config.accessToken = $scope.dnsCredentials.dnsimpleAccessToken;
|
||||
} else if (provider === 'hetzner') {
|
||||
config.token = $scope.dnsCredentials.hetznerToken;
|
||||
} else if (provider === 'vultr') {
|
||||
@@ -229,11 +218,6 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
config.customerNumber = $scope.dnsCredentials.netcupCustomerNumber;
|
||||
config.apiKey = $scope.dnsCredentials.netcupApiKey;
|
||||
config.apiPassword = $scope.dnsCredentials.netcupApiPassword;
|
||||
} else if (provider === 'ovh') {
|
||||
config.endpoint = $scope.dnsCredentials.ovhEndpoint;
|
||||
config.consumerKey = $scope.dnsCredentials.ovhConsumerKey;
|
||||
config.appKey = $scope.dnsCredentials.ovhAppKey;
|
||||
config.appSecret = $scope.dnsCredentials.ovhAppSecret;
|
||||
} else if (provider === 'porkbun') {
|
||||
config.apikey = $scope.dnsCredentials.porkbunApikey;
|
||||
config.secretapikey = $scope.dnsCredentials.porkbunSecretapikey;
|
||||
@@ -252,7 +236,7 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
provider: $scope.sysinfo.provider
|
||||
};
|
||||
if ($scope.sysinfo.provider === 'fixed') {
|
||||
sysinfoConfig.ip = $scope.sysinfo.ipv4;
|
||||
sysinfoConfig.ipv4 = $scope.sysinfo.ipv4;
|
||||
} else if ($scope.sysinfo.provider === 'network-interface') {
|
||||
sysinfoConfig.ifname = $scope.sysinfo.ifname;
|
||||
}
|
||||
@@ -265,7 +249,7 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
config: config,
|
||||
tlsConfig: tlsConfig
|
||||
},
|
||||
ipv4Config: sysinfoConfig,
|
||||
sysinfoConfig: sysinfoConfig,
|
||||
providerToken: $scope.instanceId,
|
||||
setupToken: $scope.setupToken
|
||||
};
|
||||
@@ -292,7 +276,7 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
function waitForDnsSetup() {
|
||||
$scope.state = 'waitingForDnsSetup';
|
||||
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
Client.getStatus(function (error, status) {
|
||||
if (!error && !status.setup.active) {
|
||||
if (!status.adminFqdn || status.setup.errorMessage) { // setup reset or errored. start over
|
||||
$scope.error.setup = status.setup.errorMessage;
|
||||
@@ -311,7 +295,7 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
Client.getStatus(function (error, status) {
|
||||
if (error) {
|
||||
// During domain migration, the box code restarts and can result in getStatus() failing temporarily
|
||||
console.error(error);
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular, $, Terminal, AttachAddon, FitAddon, ISTATES */
|
||||
|
||||
// create main application module
|
||||
angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification']);
|
||||
|
||||
angular.module('Application').controller('TerminalController', ['$scope', '$translate', '$timeout', '$location', 'Client', function ($scope, $translate, $timeout, $location, Client) {
|
||||
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; }, {});
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
|
||||
$scope.apps = [];
|
||||
$scope.selected = '';
|
||||
$scope.terminal = null;
|
||||
$scope.terminalSocket = null;
|
||||
$scope.fitAddon = null;
|
||||
$scope.restartAppBusy = false;
|
||||
$scope.appBusy = false;
|
||||
$scope.selectedAppInfo = null;
|
||||
$scope.schedulerTasks = [];
|
||||
|
||||
$scope.downloadFile = {
|
||||
error: '',
|
||||
filePath: '',
|
||||
busy: false,
|
||||
|
||||
downloadUrl: function () {
|
||||
if (!$scope.downloadFile.filePath) return '';
|
||||
|
||||
var filePath = encodeURIComponent($scope.downloadFile.filePath);
|
||||
|
||||
return Client.apiOrigin + '/api/v1/apps/' + $scope.selected.value + '/download?file=' + filePath + '&access_token=' + Client.getToken();
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.downloadFile.busy = false;
|
||||
$scope.downloadFile.error = '';
|
||||
$scope.downloadFile.filePath = '';
|
||||
$('#downloadFileModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.downloadFile.busy = true;
|
||||
|
||||
Client.checkDownloadableFile($scope.selected.value, $scope.downloadFile.filePath, function (error) {
|
||||
$scope.downloadFile.busy = false;
|
||||
|
||||
if (error) {
|
||||
$scope.downloadFile.error = 'The requested file does not exist.';
|
||||
return;
|
||||
}
|
||||
|
||||
// we have to click the link to make the browser do the download
|
||||
// don't know how to prevent the browsers
|
||||
$('#fileDownloadLink')[0].click();
|
||||
|
||||
$('#downloadFileModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.uploadProgress = {
|
||||
busy: false,
|
||||
total: 0,
|
||||
current: 0,
|
||||
|
||||
show: function () {
|
||||
$scope.uploadProgress.total = 0;
|
||||
$scope.uploadProgress.current = 0;
|
||||
|
||||
$('#uploadProgressModal').modal('show');
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
$('#uploadProgressModal').modal('hide');
|
||||
}
|
||||
};
|
||||
|
||||
$scope.uploadFile = function () {
|
||||
var fileUpload = document.querySelector('#fileUpload');
|
||||
|
||||
fileUpload.onchange = function (e) {
|
||||
if (e.target.files.length === 0) return;
|
||||
|
||||
$scope.uploadProgress.busy = true;
|
||||
$scope.uploadProgress.show();
|
||||
|
||||
Client.uploadFile($scope.selected.value, e.target.files[0], function progress(e) {
|
||||
$scope.uploadProgress.total = e.total;
|
||||
$scope.uploadProgress.current = e.loaded;
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.uploadProgress.busy = false;
|
||||
$scope.uploadProgress.hide();
|
||||
});
|
||||
};
|
||||
|
||||
fileUpload.click();
|
||||
};
|
||||
|
||||
$scope.usesAddon = function (addon) {
|
||||
if (!$scope.selected || !$scope.selected.addons) return false;
|
||||
return !!Object.keys($scope.selected.addons).find(function (a) { return a === addon; });
|
||||
};
|
||||
|
||||
function reset() {
|
||||
if ($scope.terminal) {
|
||||
$scope.terminal.dispose();
|
||||
$scope.terminal = null;
|
||||
}
|
||||
|
||||
if ($scope.terminalSocket) {
|
||||
$scope.terminalSocket = null;
|
||||
}
|
||||
|
||||
$scope.selectedAppInfo = null;
|
||||
}
|
||||
|
||||
$scope.restartApp = function () {
|
||||
$scope.restartAppBusy = true;
|
||||
$scope.appBusy = true;
|
||||
|
||||
var appId = $scope.selected.value;
|
||||
|
||||
function waitUntilRestarted(callback) {
|
||||
refreshApp(appId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result.installationState === ISTATES.INSTALLED) return callback();
|
||||
setTimeout(waitUntilRestarted.bind(null, callback), 2000);
|
||||
});
|
||||
}
|
||||
|
||||
Client.restartApp(appId, function (error) {
|
||||
if (error) console.error('Failed to restart app.', error);
|
||||
|
||||
waitUntilRestarted(function (error) {
|
||||
if (error) console.error('Failed wait for restart.', error);
|
||||
|
||||
$scope.restartAppBusy = false;
|
||||
$scope.appBusy = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function createTerminalSocket(callback) {
|
||||
var appId = $scope.selected.value;
|
||||
|
||||
Client.createExec(appId, { cmd: [ '/bin/bash' ], tty: true, lang: 'C.UTF-8' }, function (error, execId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
try {
|
||||
// websocket cannot use relative urls
|
||||
var url = Client.apiOrigin.replace('https', 'wss') + '/api/v1/apps/' + appId + '/exec/' + execId + '/startws?tty=true&rows=' + $scope.terminal.rows + '&columns=' + $scope.terminal.cols + '&access_token=' + Client.getToken();
|
||||
$scope.terminalSocket = new WebSocket(url);
|
||||
$scope.terminal.loadAddon(new AttachAddon.AttachAddon($scope.terminalSocket));
|
||||
|
||||
$scope.terminalSocket.onclose = function () {
|
||||
// retry in one second
|
||||
$scope.terminalReconnectTimeout = setTimeout(function () {
|
||||
showTerminal(true);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
callback();
|
||||
} catch (e) {
|
||||
callback(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function refreshApp(id, callback) {
|
||||
Client.getApp(id, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
$scope.selectedAppInfo = result;
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function showTerminal(retry) {
|
||||
reset();
|
||||
|
||||
if (!$scope.selected) return;
|
||||
|
||||
var appId = $scope.selected.value;
|
||||
|
||||
refreshApp(appId, function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
var result = $scope.selectedAppInfo;
|
||||
|
||||
$scope.schedulerTasks = result.manifest.addons.scheduler ? Object.keys(result.manifest.addons.scheduler).map(function (k) { return { name: k, command: result.manifest.addons.scheduler[k].command }; }) : [];
|
||||
|
||||
$scope.terminal = new Terminal();
|
||||
|
||||
$scope.fitAddon = new FitAddon.FitAddon();
|
||||
$scope.terminal.loadAddon($scope.fitAddon);
|
||||
|
||||
$scope.terminal.open(document.querySelector('#terminalContainer'));
|
||||
|
||||
window.terminal = $scope.terminal;
|
||||
|
||||
// Let the browser handle paste
|
||||
$scope.terminal.attachCustomKeyEventHandler(function (e) {
|
||||
if (e.key === 'v' && (e.ctrlKey || e.metaKey)) return false;
|
||||
});
|
||||
|
||||
if (retry) $scope.terminal.writeln('Reconnecting...');
|
||||
else $scope.terminal.writeln('Connecting...');
|
||||
|
||||
// we have to give it some time to setup the terminal to make it fit, there is no event unfortunately
|
||||
setTimeout(function () {
|
||||
if (!$scope.terminal) return;
|
||||
|
||||
// this is here so that the text wraps correctly after the fit!
|
||||
// var YELLOW = '\u001b[33m'; // https://gist.github.com/dainkaplan/4651352
|
||||
// var NC = '\u001b[0m';
|
||||
// $scope.terminal.writeln(YELLOW + 'If you resize the browser window, press Ctrl+D to start a new session with the current size.' + NC);
|
||||
|
||||
// we have to first write something on reconnect after app restart..not sure why
|
||||
$scope.fitAddon.fit();
|
||||
|
||||
// create exec container after we fit() since we cannot resize exec container post-creation
|
||||
createTerminalSocket(function (error) { if (error) console.error(error); });
|
||||
|
||||
$scope.terminal.focus();
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.terminalInject = function (addon, extra) {
|
||||
if (!$scope.terminalSocket) return;
|
||||
|
||||
var cmd, manifestVersion = $scope.selected.manifest.manifestVersion;
|
||||
if (addon === 'mysql') {
|
||||
if (manifestVersion === 1) {
|
||||
cmd = 'mysql --user=${MYSQL_USERNAME} --password=${MYSQL_PASSWORD} --host=${MYSQL_HOST} ${MYSQL_DATABASE}';
|
||||
} else {
|
||||
cmd = 'mysql --user=${CLOUDRON_MYSQL_USERNAME} --password=${CLOUDRON_MYSQL_PASSWORD} --host=${CLOUDRON_MYSQL_HOST} ${CLOUDRON_MYSQL_DATABASE}';
|
||||
}
|
||||
} else if (addon === 'postgresql') {
|
||||
if (manifestVersion === 1) {
|
||||
cmd = 'PGPASSWORD=${POSTGRESQL_PASSWORD} psql -h ${POSTGRESQL_HOST} -p ${POSTGRESQL_PORT} -U ${POSTGRESQL_USERNAME} -d ${POSTGRESQL_DATABASE}';
|
||||
} else {
|
||||
cmd = 'PGPASSWORD=${CLOUDRON_POSTGRESQL_PASSWORD} psql -h ${CLOUDRON_POSTGRESQL_HOST} -p ${CLOUDRON_POSTGRESQL_PORT} -U ${CLOUDRON_POSTGRESQL_USERNAME} -d ${CLOUDRON_POSTGRESQL_DATABASE}';
|
||||
}
|
||||
} else if (addon === 'mongodb') {
|
||||
if (manifestVersion === 1) {
|
||||
cmd = 'mongo -u "${MONGODB_USERNAME}" -p "${MONGODB_PASSWORD}" ${MONGODB_HOST}:${MONGODB_PORT}/${MONGODB_DATABASE}';
|
||||
} else {
|
||||
cmd = 'mongosh -u "${CLOUDRON_MONGODB_USERNAME}" -p "${CLOUDRON_MONGODB_PASSWORD}" ${CLOUDRON_MONGODB_HOST}:${CLOUDRON_MONGODB_PORT}/${CLOUDRON_MONGODB_DATABASE}';
|
||||
}
|
||||
} else if (addon === 'redis') {
|
||||
if (manifestVersion === 1) {
|
||||
cmd = 'redis-cli -h "${REDIS_HOST}" -p "${REDIS_PORT}" -a "${REDIS_PASSWORD}"';
|
||||
} else {
|
||||
cmd = 'redis-cli -h "${CLOUDRON_REDIS_HOST}" -p "${CLOUDRON_REDIS_PORT}" -a "${CLOUDRON_REDIS_PASSWORD}"';
|
||||
}
|
||||
} else if (addon === 'scheduler' && extra) {
|
||||
cmd = extra.command;
|
||||
}
|
||||
|
||||
if (!cmd) return;
|
||||
|
||||
cmd += ' ';
|
||||
|
||||
$scope.terminalSocket.send(cmd);
|
||||
$scope.terminal.focus();
|
||||
};
|
||||
|
||||
// terminal right click handling
|
||||
$scope.terminalClear = function () {
|
||||
if (!$scope.terminal) return;
|
||||
$scope.terminal.clear();
|
||||
$scope.terminal.focus();
|
||||
};
|
||||
|
||||
$scope.terminalCopy = function () {
|
||||
if (!$scope.terminal) return;
|
||||
|
||||
// execCommand('copy') would copy any selection from the page, so do this only if terminal has a selection
|
||||
if (!$scope.terminal.getSelection()) return;
|
||||
|
||||
document.execCommand('copy');
|
||||
$scope.terminal.focus();
|
||||
};
|
||||
|
||||
$('.contextMenuBackdrop').on('click', function () {
|
||||
$('#terminalContextMenu').hide();
|
||||
$('.contextMenuBackdrop').hide();
|
||||
|
||||
$scope.terminal.focus();
|
||||
});
|
||||
|
||||
$('#terminalContainer').on('contextmenu', function (e) {
|
||||
if (!$scope.terminal) return true;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
$('.contextMenuBackdrop').show();
|
||||
$('#terminalContextMenu').css({
|
||||
display: 'block',
|
||||
left: e.pageX,
|
||||
top: e.pageY
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
window.addEventListener('resize', function () {
|
||||
if ($scope.fitAddon) $scope.fitAddon.fit();
|
||||
});
|
||||
|
||||
Client.getStatus(function (error, status) {
|
||||
if (error) return $scope.error(error);
|
||||
|
||||
if (!status.activated) {
|
||||
console.log('Not activated yet, closing or redirecting', status);
|
||||
window.close();
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
// check version and force reload if needed
|
||||
if (!localStorage.version) {
|
||||
localStorage.version = status.version;
|
||||
} else if (localStorage.version !== status.version) {
|
||||
localStorage.version = status.version;
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
console.log('Running terminal version ', localStorage.version);
|
||||
|
||||
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
|
||||
Client.refreshUserInfo(function (error) {
|
||||
if (error) return $scope.error(error);
|
||||
|
||||
Client.refreshConfig(function (error) {
|
||||
if (error) return $scope.error(error);
|
||||
|
||||
refreshApp(search.id, function (error, app) {
|
||||
$scope.selected = {
|
||||
type: 'app',
|
||||
value: app.id,
|
||||
name: app.fqdn + ' (' + app.manifest.title + ')',
|
||||
addons: app.manifest.addons,
|
||||
manifest: app.manifest
|
||||
};
|
||||
|
||||
// now mark the Client to be ready
|
||||
Client.setReady();
|
||||
|
||||
$scope.initialized = true;
|
||||
|
||||
showTerminal();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$translate([ 'terminal.title' ]).then(function (tr) {
|
||||
if (tr['terminal.title'] !== 'terminal.title') window.document.title = tr['terminal.title'];
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['downloadFileModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find('[autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
}]);
|
||||
@@ -6,8 +6,8 @@
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src <%= apiOrigin %> 'unsafe-inline' 'unsafe-eval' 'self'; img-src <%= apiOrigin %> 'self'" />
|
||||
|
||||
<!-- this gets changed once we get the status (because angular has not loaded yet, we see template string for a flash) -->
|
||||
<title>Cloudron Password Reset</title>
|
||||
<meta name="description" content="Cloudron Password Reset">
|
||||
<title>Cloudron Login</title>
|
||||
<meta name="description" content="Cloudron Login">
|
||||
|
||||
<link id="favicon" href="<%= apiOrigin %>/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
@@ -47,14 +47,54 @@
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script type="text/javascript" src="/js/passwordreset.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/js/login.js?<%= revision %>"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body ng-app="Application" ng-controller="PasswordResetController">
|
||||
<body ng-app="Application" ng-controller="LoginController">
|
||||
|
||||
<div class="layout-root ng-cloak" ng-show="initialized">
|
||||
|
||||
<div class="layout-content" ng-show="mode === 'login'">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" style="margin-top: -84px" src="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h1><small>{{ 'login.loginTo' | tr }}</small> {{ status.cloudronName || 'Cloudron' }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4 class="has-error" ng-show="error && (error.generic || error.password)">{{ 'login.errorIncorrectCredentials' | tr }}</h4>
|
||||
<h4 class="has-error" ng-show="error && error.totpToken">{{ 'login.errorIncorrect2FAToken' | tr }}</h4>
|
||||
<h4 class="has-error" ng-show="error && error.internal">{{ 'login.errorInternal' | tr }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form name="loginForm" ng-submit="onLogin()">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputUsername">{{ 'login.username' | tr }}</label>
|
||||
<input type="text" class="form-control" id="inputUsername" name="username" ng-model="username" ng-disabled="busy" autofocus required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': error.password }">
|
||||
<label class="control-label" for="inputPassword">{{ 'login.password' | tr }}</label>
|
||||
<input type="password" class="form-control" name="password" id="inputPassword" ng-model="password" ng-disabled="busy" required password-reveal>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': error.totpToken }">
|
||||
<label class="control-label" for="inputTotpToken">{{ 'login.2faToken' | tr }}</label>
|
||||
<input type="text" class="form-control" name="totpToken" id="inputTotpToken" ng-model="totpToken" ng-disabled="busy" value="">
|
||||
</div>
|
||||
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || loginForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'login.signInAction' | tr }}</button>
|
||||
</form>
|
||||
<a ng-href="" class="hand" ng-click="showPasswordReset()">{{ 'login.resetPasswordAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-content" ng-show="mode === 'passwordReset'">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
<div class="row">
|
||||
@@ -73,11 +113,9 @@
|
||||
<input type="text" class="form-control" id="inputPasswordResetIdentifier" name="passwordResetIdentifier" ng-model="passwordResetIdentifier" ng-disabled="busy" autofocus required>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="card-form-bottom-bar">
|
||||
<a href="/" class="hand">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
<button class="btn btn-primary btn-outline" type="submit" ng-disabled="busy || passwordResetForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.resetAction' | tr }}</button>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || passwordResetForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.resetAction' | tr }}</button>
|
||||
</form>
|
||||
<a ng-href="" class="hand" ng-click="showLogin()">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,7 +129,7 @@
|
||||
<br/>
|
||||
<h2>{{ 'passwordReset.emailSent.title' | tr }}</h2>
|
||||
<br/>
|
||||
<a href="/" class="btn btn-primary">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
<button class="btn btn-primary" ng-click="showLogin()">{{ 'passwordReset.backToLoginAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,11 +173,10 @@
|
||||
<label class="control-label" for="inputPasswordResetTotpToken">{{ 'login.2faToken' | tr }}</label>
|
||||
<input type="text" class="form-control" name="passwordResetTotpToken" id="inputPasswordResetTotpToken" ng-model="totpToken" ng-disabled="busy" value="">
|
||||
</div>
|
||||
<div class="card-form-bottom-bar">
|
||||
<a href="/" class="hand">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
<button class="btn btn-primary btn-outline" type="submit" ng-disabled="busy || newPasswordForm.$invalid || newPassword !== newPasswordRepeat"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.passwordChanged.submitAction' | tr }}</button>
|
||||
</div>
|
||||
<br/>
|
||||
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || newPasswordForm.$invalid || newPassword !== newPasswordRepeat"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.passwordChanged.submitAction' | tr }}</button>
|
||||
</form>
|
||||
<a ng-href="" class="hand" ng-click="showLogin()">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,7 +197,7 @@
|
||||
</div>
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted" ng-bind-html="branding.footer | markdown2html"></span>
|
||||
<span class="text-muted" ng-bind-html="status.footer | markdown2html"></span>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="Application" ng-controller="LogsController">
|
||||
<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 Logs</title>
|
||||
<meta name="description" content="Cloudron Logs">
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar">
|
||||
|
||||
<!-- CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.css?<%= revision %>"/>
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-animate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-base64.min.js?<%= revision %>"></script>
|
||||
<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-ui-notification.js?<%= revision %>"></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?<%= revision %>"></script>
|
||||
|
||||
<!-- Angular translate https://angular-translate.github.io/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- colors -->
|
||||
<script type="text/javascript" src="/3rdparty/js/colors.js?<%= revision %>"></script>
|
||||
|
||||
<!-- moment -->
|
||||
<script type="text/javascript" src="/3rdparty/js/moment-with-locales.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Main Application -->
|
||||
<script type="text/javascript" src="/js/logs.js?<%= revision %>"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="logs">
|
||||
|
||||
<a class="offline-banner animateMe" ng-show="client.offline" ng-cloak href="https://docs.cloudron.io/troubleshooting/" target="_blank"><i class="fa fa-circle-notch fa-spin"></i> {{ 'main.offline' | tr }}</a>
|
||||
|
||||
<div class="animateMe ng-hide layout-root" ng-show="initialized">
|
||||
<div class="logs-controls">
|
||||
<h3 style="display: inline-block;">{{ selected.name }}</h3>
|
||||
|
||||
<!-- logs actions -->
|
||||
<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.title' | tr }}</a>
|
||||
<a class="btn btn-primary" ng-href="{{ '/filemanager.html?type=app&id=' + selected.value }}" target="_blank" ng-show="selected.type === 'app'"><i class="fas fa-folder"></i> {{ 'filemanager.title' | tr }}</a>
|
||||
<a class="btn btn-primary" ng-click="clear()"><i class="fa fa-trash"></i> {{ 'logs.clear' | tr }}</a>
|
||||
<a class="btn btn-primary" ng-href="{{ selected.url }}&format=short&lines=-1"><i class="fa fa-download"></i> {{ 'logs.download' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logs-container"></div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,8 +4,8 @@
|
||||
<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 - Not Found</title>
|
||||
<meta name="description" content="Cloudron - Not Found">
|
||||
<title>Cloudron Not Found</title>
|
||||
<meta name="description" content="Cloudron Not Found">
|
||||
|
||||
<!-- Use static style as we can't include local stylesheets -->
|
||||
<style>
|
||||
|
||||
@@ -137,12 +137,6 @@
|
||||
<input type="text" class="form-control" ng-model="mountOptions.diskPath" id="inputConfigureDiskPath" name="diskPath" ng-disabled="busy" placeholder="Directory for backups" ng-required="provider === 'ext4' || provider === 'xfs'">
|
||||
</div>
|
||||
|
||||
<!-- Disk -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.diskPath }" ng-show="provider === 'disk'">
|
||||
<label class="control-label">Device</label>
|
||||
<select class="form-control" ng-model="disk" ng-options="item as item.label for item in blockDevices track by item.path" ng-required="provider === 'disk'"></select>
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPort">SSH Port</label>
|
||||
@@ -241,11 +235,6 @@
|
||||
<select class="form-control" name="region" id="inputConfigureBackupVultrRegion" ng-model="endpoint" ng-options="a.value as a.name for a in vultrRegions" ng-disabled="busy" ng-required="provider === 'vultr-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'contabo-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupContaboRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupContaboRegion" ng-model="endpoint" ng-options="a.value as a.name for a in contaboRegions" ng-disabled="busy" ng-required="provider === 'contabo-objectstorage'"></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)">
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" style="margin-top: -84px" src="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h1><small>{{ 'setupAccount.welcomeTo' | tr }}</small> {{ branding.cloudronName || 'Cloudron' }}</h1>
|
||||
<h1><small>{{ 'setupAccount.welcomeTo' | tr }}</small> {{ status.cloudronName || 'Cloudron' }}</h1>
|
||||
<h3>{{ 'setupAccount.description' | tr }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,7 +154,7 @@
|
||||
</div>
|
||||
|
||||
<footer class="text-center ng-cloak">
|
||||
<span class="text-muted" ng-bind-html="branding.footer | markdown2html"></span>
|
||||
<span class="text-muted" ng-bind-html="status.footer | markdown2html"></span>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -226,39 +226,15 @@
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.bunnyAccessKey" name="bunnyAccessKey" ng-required="dnsCredentials.provider === 'bunny'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- dnsimple -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'dnsimple'">
|
||||
<label class="control-label">Access Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.dnsimpleAccessToken" name="dnsimpleAccessToken" ng-required="dnsCredentials.provider === 'dnsimple'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- OVH -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'ovh'">
|
||||
<label class="control-label" for="inputConfigureOvhEndpoint">Endpoint</label>
|
||||
<select class="form-control" name="endpoint" id="inputConfigureOvhEndpoint" ng-model="dnsCredentials.ovhEndpoint" ng-options="a.value as a.name for a in ovhEndpoints" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'ovh'"></select>
|
||||
</p>
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'ovh'">
|
||||
<label class="control-label">Consumer Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.ovhConsumerKey" name="ovhConsumerKey" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'ovh'">
|
||||
</p>
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'ovh'">
|
||||
<label class="control-label">Application Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.ovhAppKey" name="ovhAppKey" ng-disabled="dnsCredentials.busy" ng-minlength="1" ng-required="dnsCredentials.provider === 'ovh'">
|
||||
</p>
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'ovh'">
|
||||
<label class="control-label">Application Secret</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.ovhAppSecret" name="ovhAppSecret" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'ovh'">
|
||||
</p>
|
||||
|
||||
<!-- Porkbun -->
|
||||
<p class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.porkbunApikey.$dirty && dnsCredentialsForm.porkbunApikey.$invalid }" ng-show="dnsCredentials.provider === 'porkbun'">
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.porkbunApikey.$dirty && dnsCredentialsForm.porkbunApikey.$invalid }" ng-show="dnsCredentials.provider === 'porkbun'">
|
||||
<label class="control-label">API Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.porkbunApikey" name="porkbunApikey" placeholder="API Key" ng-minlength="1" ng-required="dnsCredentials.provider === 'porkbun'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
<p class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.porkbunSecretapikey.$dirty && dnsCredentialsForm.porkbunSecretapikey.$invalid }" ng-show="dnsCredentials.provider === 'porkbun'">
|
||||
<label class="control-label">API Secret</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.porkbunSecretapikey" name="porkbunSecretapikey" placeholder="API Secret" ng-required="dnsCredentials.provider === 'porkbun'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.porkbunSecretapikey.$dirty && dnsCredentialsForm.porkbunSecretapikey.$invalid }" ng-show="dnsCredentials.provider === 'porkbun'">
|
||||
<label class="control-label">API Secret</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.porkbunSecretapikey" name="porkbunSecretapikey" placeholder="API Secret" ng-required="dnsCredentials.provider === 'porkbun'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Hetzner -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'hetzner'">
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="Application" ng-controller="TerminalController">
|
||||
<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 Terminal</title>
|
||||
<meta name="description" content="Cloudron Terminal">
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar">
|
||||
|
||||
<!-- CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.css?<%= revision %>"/>
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-animate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-base64.min.js?<%= revision %>"></script>
|
||||
<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-ui-notification.js?<%= revision %>"></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?<%= revision %>"></script>
|
||||
|
||||
<!-- Angular translate https://angular-translate.github.io/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Clipboard handling -->
|
||||
<script type="text/javascript" src="/3rdparty/js/clipboard.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- xterm -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/xterm/css/xterm.css?<%= revision %>" />
|
||||
<script src="/3rdparty/xterm/lib/xterm.js?<%= revision %>"></script>
|
||||
<script src="/3rdparty/xterm-addon-attach/lib/xterm-addon-attach.js?<%= revision %>"></script>
|
||||
<script src="/3rdparty/xterm-addon-fit/lib/xterm-addon-fit.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Main Application -->
|
||||
<script type="text/javascript" src="/js/terminal.js?<%= revision %>"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body style="overflow: hidden;">
|
||||
|
||||
<!-- Modal download file -->
|
||||
<div class="modal fade" id="downloadFileModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'terminal.download.title' | tr:{ name: selected.name } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="downloadFileForm" ng-submit="downloadFile.submit()">
|
||||
<div class="form-group" ng-class="{ 'has-error': downloadFileForm.filePath.$dirty && downloadFile.error }">
|
||||
<label class="control-label" for="inputDownloadFilePath">{{ 'terminal.download.filePath' | tr }}</label>
|
||||
<div class="control-label" ng-show="{ 'has-error': downloadFileForm.filePath.$dirty && downloadFile.error }">
|
||||
<small>{{ downloadFile.error }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="filePath" ng-model="downloadFile.filePath" required autofocus>
|
||||
</div>
|
||||
<input id="inputDownloadFilePath" class="ng-hide" type="submit" ng-disabled="!downloadFile.filePath"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a id="fileDownloadLink" class="" ng-href="{{ downloadFile.downloadUrl() }}" target="_blank"></a>
|
||||
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</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> {{ 'terminal.download.download' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal upload progress -->
|
||||
<div class="modal fade" id="uploadProgressModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'terminal.upload.title' | tr:{ name: selected.name } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<span><b>{{ uploadProgress.current | prettyDecimalSize }}</b> (total {{ uploadProgress.total | prettyDecimalSize }})</span>
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ 100*(uploadProgress.current/uploadProgress.total) }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="animateMe ng-hide layout-root terminal-view" ng-show="initialized">
|
||||
|
||||
<div class="terminal-controls">
|
||||
<h3 style="display: inline-block;">{{ selected.name }}</h3>
|
||||
|
||||
<input type="file" id="fileUpload" class="hide"/>
|
||||
|
||||
<div class="pull-right">
|
||||
<div class="btn-group" ng-show="usesAddon('scheduler')">
|
||||
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" ng-disabled="appBusy">
|
||||
{{ 'terminal.scheduler' | tr }} <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in schedulerTasks"><a href="" ng-click="terminalInject('scheduler', task)">{{ task.name }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- addon actions -->
|
||||
<button class="btn btn-success" ng-click="terminalInject('mysql')" ng-show="usesAddon('mysql')" ng-disabled="appBusy">MySQL</button>
|
||||
<button class="btn btn-success" ng-click="terminalInject('postgresql')" ng-show="usesAddon('postgresql')" ng-disabled="appBusy">Postgres</button>
|
||||
<button class="btn btn-success" ng-click="terminalInject('mongodb')" ng-show="usesAddon('mongodb')" ng-disabled="appBusy">MongoDB</button>
|
||||
<button class="btn btn-success" ng-click="terminalInject('redis')" ng-show="usesAddon('redis')" ng-disabled="appBusy">Redis</button>
|
||||
|
||||
<!-- terminal actions -->
|
||||
<button class="btn btn-primary" ng-click="restartApp()" ng-show="selected.type === 'app'" ng-disabled="appBusy"><i class="fa fa-sync-alt" ng-class="{ 'fa-spin': restartAppBusy }"></i> {{ 'terminal.restart' | tr }}</button>
|
||||
<button class="btn btn-primary" ng-click="uploadFile()" ng-show="selected.type === 'app' && !uploadProgress.busy" ng-disabled="appBusy"><i class="fa fa-upload"></i> {{ 'terminal.uploadToTmp' | tr }}</button>
|
||||
<button class="btn btn-primary" ng-click="uploadProgress.show()" ng-show="uploadProgress.busy" ng-disabled="appBusy"><i class="fa fa-circle-notch fa-spin"></i> {{ 'terminal.uploading' | tr }}</button>
|
||||
<button class="btn btn-primary" ng-click="downloadFile.show()" ng-show="selected.type === 'app'" ng-disabled="appBusy"><i class="fa fa-download"></i> {{ 'terminal.downloadAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="terminal-container" id="terminalContainer" ng-hide="appBusy"></div>
|
||||
<div class="terminal-container placeholder" ng-show="appBusy">
|
||||
<h4>
|
||||
<span ng-show="restartAppBusy">{{ 'terminal.busy.restarting' | tr }}</span>
|
||||
<span ng-show="selectedAppInfo.installationState === 'pending_debug' && selectedAppInfo.debugMode">{{ 'terminal.busy.restartingInPausedMode' | tr }}</span>
|
||||
<span ng-show="selectedAppInfo.installationState === 'pending_debug' && !selectedAppInfo.debugMode ">{{ 'terminal.busy.resuming' | tr }}</span>
|
||||
<span ng-show="selectedAppInfo.installationState === 'pending_installed'">{{ 'terminal.busy.installing' | tr }}</span>
|
||||
</h4>
|
||||
|
||||
<div class="progress" ng-show="appBusy" style="width: 80%">
|
||||
<div class="progress-bar progress-bar-striped active" role="progressbar" style="width: 100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contextMenuBackdrop">
|
||||
<ul class="dropdown-menu" id="terminalContextMenu" style="position: absolute; display:none;">
|
||||
<li><a href="" ng-click="terminalCopy()">{{ 'terminal.contextmenu.copy' | tr }}</a></li>
|
||||
<li class="disabled"><a>{{ 'terminal.contextmenu.pasteInfo' | tr }}</a></li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li><a href="" ng-click="terminalClear()">{{ 'terminal.contextmenu.clear' | tr }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -320,10 +320,6 @@ h1, h2, h3 {
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.offscreen {
|
||||
position: absolute;
|
||||
left: -999em;
|
||||
@@ -689,7 +685,7 @@ multiselect {
|
||||
}
|
||||
|
||||
.card {
|
||||
min-height: 523px;
|
||||
min-height: 488px;
|
||||
}
|
||||
|
||||
@media(min-width:768px) {
|
||||
@@ -828,19 +824,6 @@ multiselect {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Login and password forms
|
||||
// ----------------------------
|
||||
|
||||
.card-form-bottom-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card-form-bottom-bar > * {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Appstore view
|
||||
// ----------------------------
|
||||
@@ -1088,10 +1071,6 @@ multiselect {
|
||||
max-width: 970px;
|
||||
}
|
||||
|
||||
.card-expand {
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #5CB85C;
|
||||
}
|
||||
@@ -1778,14 +1757,6 @@ tag-input {
|
||||
.logs {
|
||||
background: black;
|
||||
|
||||
.logs-error {
|
||||
color: white;
|
||||
width: 100%;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
margin-top: 200px;
|
||||
}
|
||||
|
||||
.logs-controls {
|
||||
margin: 5px;
|
||||
|
||||
@@ -2052,13 +2023,8 @@ tag-input {
|
||||
.has-background {
|
||||
|
||||
h1, h2, h3 {
|
||||
filter: drop-shadow(0 0 0.5px black);
|
||||
color: white;
|
||||
-webkit-text-stroke: 0.3px black;
|
||||
|
||||
.btn {
|
||||
color: white;
|
||||
-webkit-text-stroke: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
|
||||
@@ -51,8 +51,7 @@
|
||||
"save": "Gem",
|
||||
"close": "Luk",
|
||||
"no": "Nej",
|
||||
"yes": "Ja",
|
||||
"delete": "Slet"
|
||||
"yes": "Ja"
|
||||
},
|
||||
"username": "Brugernavn",
|
||||
"displayName": "Vis navn",
|
||||
@@ -62,8 +61,7 @@
|
||||
},
|
||||
"action": {
|
||||
"reboot": "Genstart",
|
||||
"logs": "Logfiler",
|
||||
"showLogs": "Vis logfiler"
|
||||
"logs": "Logfiler"
|
||||
},
|
||||
"clipboard": {
|
||||
"copied": "Kopieret til udklipsholderen",
|
||||
@@ -89,9 +87,7 @@
|
||||
"statusEnabled": "Aktiveret",
|
||||
"statusDisabled": "Slået fra",
|
||||
"loadingPlaceholder": "Indlæsning",
|
||||
"disableAction": "Deaktiver",
|
||||
"settings": "Indstillinger",
|
||||
"saveAction": "Gem"
|
||||
"disableAction": "Deaktiver"
|
||||
},
|
||||
"appstore": {
|
||||
"category": {
|
||||
@@ -307,7 +303,7 @@
|
||||
"externalLdapTooltip": "Fra ekstern LDAP-mappe"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Brugerindstillinger",
|
||||
"title": "Indstillinger",
|
||||
"allowProfileEditCheckbox": "Tillad brugere at redigere deres navn og e-mail",
|
||||
"require2FACheckbox": "Kræv, at brugerne skal oprette 2FA",
|
||||
"subscriptionRequired": "Disse funktioner er kun tilgængelige i de betalte abonnementer.",
|
||||
@@ -507,8 +503,7 @@
|
||||
},
|
||||
"changeBackgroundImage": {
|
||||
"title": "Indstil baggrundsbillede"
|
||||
},
|
||||
"enable2FANotAvailable": "Ikke tilgængelig for brugere fra ekstern autentificeringskilde"
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
"location": {
|
||||
@@ -540,7 +535,7 @@
|
||||
"noApps": "Ingen apps",
|
||||
"cleanupBackups": "Oprydning af sikkerhedskopier",
|
||||
"backupNow": "Backup nu",
|
||||
"stopTask": "Stop Backup",
|
||||
"stopTask": "Stop {{ taskType === 'backup' ? 'Backup' : 'Oprydning' }}",
|
||||
"tooltipEditBackup": "Rediger sikkerhedskopi"
|
||||
},
|
||||
"backupDetails": {
|
||||
@@ -634,8 +629,7 @@
|
||||
"preserved": {
|
||||
"description": "Vedvarende backup uanset opbevaringspolitik",
|
||||
"tooltip": "Dette vil også bevare mailen og {{ appsLength }} app-backuppen(erne)."
|
||||
},
|
||||
"remotePath": "Ekstern sti"
|
||||
}
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
@@ -676,8 +670,7 @@
|
||||
"solrRunning": "Kører",
|
||||
"solrNotRunning": "Ikke kørende",
|
||||
"acl": "ACL for post",
|
||||
"aclOverview": "{{ dnsblZonesCount }} DNSBL-zone(r)",
|
||||
"virtualAllMail": "Mappen \"Al post\""
|
||||
"aclOverview": "{{ dnsblZonesCount }} DNSBL-zone(r)"
|
||||
},
|
||||
"eventlog": {
|
||||
"empty": "Logbogen er tom.",
|
||||
@@ -769,10 +762,6 @@
|
||||
},
|
||||
"action": {
|
||||
"queue": "Kø"
|
||||
},
|
||||
"changeVirtualAllMailDialog": {
|
||||
"title": "Mappen \"Al post\"",
|
||||
"description": "Mappen \"All Mail\" er en enkelt mappe, der indeholder alle mails i din indbakke. Mappen kan være nyttig i mailklienter, der ikke understøtter rekursiv mappesøgning."
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
@@ -798,8 +787,7 @@
|
||||
},
|
||||
"dyndns": {
|
||||
"description": "Aktiver denne indstilling for at holde alle dine DNS-poster synkroniseret med en skiftende IP-adresse. Dette er nyttigt, når Cloudron kører i et netværk med en ofte skiftende offentlig IP-adresse, f.eks. en hjemmeforbindelse.",
|
||||
"title": "Dynamisk DNS",
|
||||
"showLogsAction": "Vis logfiler"
|
||||
"title": "Dynamisk DNS"
|
||||
},
|
||||
"title": "Netværk",
|
||||
"configureIp": {
|
||||
@@ -816,13 +804,7 @@
|
||||
},
|
||||
"configureIpv6": {
|
||||
"title": "Konfigurer IPv6-udbyder"
|
||||
},
|
||||
"trustedIps": {
|
||||
"title": "Konfigurer tillidsfulde IP'er",
|
||||
"summary": "{{ trustCount }} IP'er, der er tillid til",
|
||||
"description": "Der vil blive stolet på HTTP-headere fra matchende IP-adresser"
|
||||
},
|
||||
"trustedIpRanges": "Tillid til IP'er og områder "
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"configure": {
|
||||
@@ -937,8 +919,7 @@
|
||||
"submitAction": "Indsend",
|
||||
"reportPlaceholder": "Beskriv dit problem",
|
||||
"emailPlaceholder": "Angiv om nødvendigt en anden e-mail-adresse end ovenstående, så du kan kontaktes",
|
||||
"emailVerifyAction": "Bekræft nu",
|
||||
"typeBilling": "Problemer med fakturering"
|
||||
"emailVerifyAction": "Bekræft nu"
|
||||
},
|
||||
"remoteSupport": {
|
||||
"description": "Aktiver denne indstilling for at give supportteknikere mulighed for at oprette forbindelse til denne server via SSH.",
|
||||
@@ -1032,8 +1013,7 @@
|
||||
"hetznerToken": "Hetzner Token",
|
||||
"porkbunSecretapikey": "Hemmelig API-nøgle",
|
||||
"cloudflareDefaultProxyStatus": "Aktiver proxying for nye DNS-poster",
|
||||
"porkbunApikey": "API-nøgle",
|
||||
"bunnyAccessKey": "Bunny Access Key"
|
||||
"porkbunApikey": "API-nøgle"
|
||||
},
|
||||
"title": "Domæner og certs",
|
||||
"addDomain": "Tilføj domæne",
|
||||
@@ -1043,7 +1023,7 @@
|
||||
"tooltipRemove": "Fjern domæne",
|
||||
"changeDashboardDomain": {
|
||||
"title": "Ændre Dashboard-domæne",
|
||||
"description": "Dette vil flytte dashboardet til <code>my</code>subdomænet i det valgte domæne.",
|
||||
"description": "Dette vil flytte instrumentbrættet og e-mail-serveren til den<code>my</code>underdomæne til det valgte domæne.",
|
||||
"changeAction": "Ændre domæne",
|
||||
"cancelAction": "Annuller",
|
||||
"showLogsAction": "Vis logs"
|
||||
@@ -1062,8 +1042,7 @@
|
||||
"domainWellKnown": {
|
||||
"title": "Well-Known locations på {{ domain }}"
|
||||
},
|
||||
"tooltipWellKnown": "Indstil well-known lokationer",
|
||||
"count": "Samlede domæner: {{ count }}"
|
||||
"tooltipWellKnown": "Indstil well-known lokationer"
|
||||
},
|
||||
"notifications": {
|
||||
"markAllAsRead": "Markér alle som læst",
|
||||
@@ -1168,8 +1147,7 @@
|
||||
"cut": "Skær",
|
||||
"copy": "Kopier",
|
||||
"paste": "Indsæt",
|
||||
"selectAll": "Vælg alle",
|
||||
"open": "Åben"
|
||||
"selectAll": "Vælg alle"
|
||||
},
|
||||
"mtime": "Ændret"
|
||||
},
|
||||
@@ -1184,19 +1162,7 @@
|
||||
},
|
||||
"status": {
|
||||
"restartingApp": "genstart af app"
|
||||
},
|
||||
"extractionInProgress": "Udvinding i gang",
|
||||
"uploader": {
|
||||
"exitWarning": "Upload er stadig i gang. Skal vi virkelig lukke denne side?",
|
||||
"uploading": "Uploading"
|
||||
},
|
||||
"textEditor": {
|
||||
"undo": "Fortryd",
|
||||
"redo": "Omarbejdning",
|
||||
"save": "Gem"
|
||||
},
|
||||
"pasteInProgress": "Indsætning i gang",
|
||||
"deleteInProgress": "Sletning i gang"
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"incoming": {
|
||||
@@ -1450,8 +1416,7 @@
|
||||
"title": "Datakatalog",
|
||||
"dataDirPlaceholder": "Lad det være tomt for at bruge platformens standard",
|
||||
"moveAction": "Flyt data",
|
||||
"diskUsage": "Appen bruger i øjeblikket {{ size }} af lagerplads (pr. {{ date }}).",
|
||||
"mountTypeWarning": "Destinationsfilsystemet skal understøtte filtilladelser og ejerskab, for at flytningen kan fungere"
|
||||
"diskUsage": "Appen bruger i øjeblikket {{ size }} af lagerplads (pr. {{ date }})."
|
||||
},
|
||||
"mounts": {
|
||||
"title": "Montering",
|
||||
@@ -1715,17 +1680,6 @@
|
||||
"label": "Etiket",
|
||||
"clearIconAction": "Ryd ikon",
|
||||
"clearIconDescription": "Dette vil forsøge at hente appens favicon ved lagring."
|
||||
},
|
||||
"servicesTabTitle": "Tjenester",
|
||||
"turn": {
|
||||
"title": "TURN Opsætning",
|
||||
"enable": "Konfigurer appen til at bruge den indbyggede TURN-server",
|
||||
"disable": "Du må ikke konfigurere appens TURN-indstillinger. Appens TURN-indstillinger skal ikke konfigureres. Du kan konfigurere dem inde i appen."
|
||||
},
|
||||
"redis": {
|
||||
"title": "Redis-konfiguration",
|
||||
"enable": "Konfigurer appen til at bruge Redis",
|
||||
"disable": "Deaktiver Redis"
|
||||
}
|
||||
},
|
||||
"passwordReset": {
|
||||
@@ -1854,9 +1808,7 @@
|
||||
"logs": {
|
||||
"title": "Logfiler",
|
||||
"clear": "Klart udsyn",
|
||||
"download": "Download komplette logs",
|
||||
"notFoundError": "Ingen sådan opgave eller app",
|
||||
"logsGoneError": "Logfil(er) ikke fundet"
|
||||
"download": "Download komplette logs"
|
||||
},
|
||||
"login": {
|
||||
"loginTo": "Log ind på",
|
||||
@@ -1865,9 +1817,7 @@
|
||||
"password": "Adgangskode",
|
||||
"2faToken": "2FA-token (hvis aktiveret)",
|
||||
"signInAction": "Log ind",
|
||||
"resetPasswordAction": "Nulstil adgangskode",
|
||||
"errorIncorrect2FAToken": "2FA-token er ugyldig",
|
||||
"errorInternal": "Intern fejl, prøv igen senere"
|
||||
"resetPasswordAction": "Nulstil adgangskode"
|
||||
},
|
||||
"lang": {
|
||||
"en": "English",
|
||||
@@ -1881,48 +1831,9 @@
|
||||
"zh_Hans": "Kinesisk (forenklet)",
|
||||
"es": "Spansk",
|
||||
"ru": "Russisk",
|
||||
"pt": "Portugisisk",
|
||||
"da": "Dansk"
|
||||
"pt": "Portugisisk"
|
||||
},
|
||||
"supportConfig": {
|
||||
"emailNotVerified": "Du bedes først bekræfte e-mailen på cloudron.io-kontoen for at sikre, at vi kan kontakte dig."
|
||||
},
|
||||
"oidc": {
|
||||
"newClientDialog": {
|
||||
"title": "Tilføj klient",
|
||||
"description": "Tilføj nye OpenID connect-klientindstillinger.",
|
||||
"createAction": "Opret"
|
||||
},
|
||||
"client": {
|
||||
"name": "Navn",
|
||||
"id": "Klient-id",
|
||||
"secret": "Klientens secret",
|
||||
"signingAlgorithm": "Signeringsalgoritme",
|
||||
"loginRedirectUri": "Url til tilbagekaldelse af login (kommasepareret, hvis der er mere end én)",
|
||||
"logoutRedirectUri": "Url til tilbagekaldelse af logout (valgfrit)"
|
||||
},
|
||||
"title": "OpenID Connect-udbyder",
|
||||
"description": "Cloudron kan fungere som OpenID Connect-udbyder for interne apps og eksterne tjenester.",
|
||||
"editClientDialog": {
|
||||
"title": "Rediger klient {{ client }}"
|
||||
},
|
||||
"deleteClientDialog": {
|
||||
"title": "Virkelig slette klient {{ client }}?",
|
||||
"description": "Dette vil afbryde forbindelsen til alle eksterne OpenID-apps fra denne Cloudron, der bruger dette klient-id."
|
||||
},
|
||||
"env": {
|
||||
"discoveryUrl": "URL til opdagelse",
|
||||
"logoutUrl": "URL til logout",
|
||||
"profileEndpoint": "Profil slutpunkt",
|
||||
"keysEndpoint": "Nøgler Slutpunkt",
|
||||
"tokenEndpoint": "Token slutpunkt",
|
||||
"authEndpoint": "Auth-slutpunkt"
|
||||
},
|
||||
"clients": {
|
||||
"title": "Klienter",
|
||||
"newClient": "Ny klient",
|
||||
"empty": "Ingen klienten endnu"
|
||||
}
|
||||
},
|
||||
"automation": "Automatisering"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@
|
||||
"subscriptionRequired": "Diese Funktionen sind nur im Abo enthalten.",
|
||||
"require2FACheckbox": "User müssen Zwei-Faktor-Authentifizierung (2FA) aktivieren",
|
||||
"allowProfileEditCheckbox": "Erlaube Usern ihren Namen und E-Mail-Adresse zu ändern",
|
||||
"title": "User Einstellungen",
|
||||
"title": "Einstellungen",
|
||||
"require2FAWarning": "Richte 2FA ein um nicht ausgesperrt zu werden."
|
||||
},
|
||||
"groups": {
|
||||
@@ -900,7 +900,7 @@
|
||||
"title": "Logfiles"
|
||||
},
|
||||
"listing": {
|
||||
"stopTask": "Stop Backup",
|
||||
"stopTask": "Stop {{ taskType === 'backup' ? 'Backup' : 'Cleanup' }}",
|
||||
"backupNow": "Backup jetzt erstellen",
|
||||
"cleanupBackups": "Backups löschen",
|
||||
"tooltipDownloadBackupConfig": "Konfiguration herunterladen",
|
||||
@@ -1366,8 +1366,7 @@
|
||||
"paste": "Einfügen",
|
||||
"copy": "Kopieren",
|
||||
"cut": "Ausschneiden",
|
||||
"edit": "Bearbeiten",
|
||||
"open": "Öffnen"
|
||||
"edit": "Bearbeiten"
|
||||
},
|
||||
"symlink": "Symlink zu {{ target }}",
|
||||
"mtime": "Geändert"
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
"auth": {
|
||||
"sso": "Log in with Cloudron credentials",
|
||||
"nosso": "Log in with dedicated account",
|
||||
"email": "Log in with email address",
|
||||
"openid": "Log in with Cloudron OpenID"
|
||||
"email": "Log in with email address"
|
||||
},
|
||||
"addAppAction": "Add App",
|
||||
"addAppproxyAction": "Add App Proxy",
|
||||
@@ -57,8 +56,7 @@
|
||||
},
|
||||
"action": {
|
||||
"reboot": "Reboot",
|
||||
"logs": "Logs",
|
||||
"showLogs": "Show Logs"
|
||||
"logs": "Logs"
|
||||
},
|
||||
"clipboard": {
|
||||
"copied": "Copied to clipboard",
|
||||
@@ -91,8 +89,7 @@
|
||||
"statusEnabled": "Enabled",
|
||||
"statusDisabled": "Disabled",
|
||||
"loadingPlaceholder": "Loading",
|
||||
"settings": "Settings",
|
||||
"saveAction": "Save"
|
||||
"settings": "Settings"
|
||||
},
|
||||
"appstore": {
|
||||
"title": "App Store",
|
||||
@@ -170,10 +167,7 @@
|
||||
"loginAction": "Login",
|
||||
"createAccountAction": "Create Account",
|
||||
"switchToSignUpAction": "Don't have an account yet? Sign up",
|
||||
"switchToLoginAction": "Already have an account? Log in",
|
||||
"setupWithTokenAction": "Setup",
|
||||
"setupToken": "Setup Token",
|
||||
"titleToken": "Sign up with Setup Token"
|
||||
"switchToLoginAction": "Already have an account? Log in"
|
||||
},
|
||||
"categoryLabel": "Category",
|
||||
"ssofilter": {
|
||||
@@ -212,7 +206,7 @@
|
||||
"externalLdapTooltip": "From external LDAP directory"
|
||||
},
|
||||
"settings": {
|
||||
"title": "User Settings",
|
||||
"title": "Settings",
|
||||
"allowProfileEditCheckbox": "Allow users to edit their name and email",
|
||||
"require2FACheckbox": "Require users to set up 2FA",
|
||||
"subscriptionRequired": "These features are only available in the paid plans.",
|
||||
@@ -511,8 +505,7 @@
|
||||
},
|
||||
"changeBackgroundImage": {
|
||||
"title": "Set Background Image"
|
||||
},
|
||||
"enable2FANotAvailable": "Not available for users from external authentication source"
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
"title": "Backups",
|
||||
@@ -544,7 +537,7 @@
|
||||
"tooltipDownloadBackupConfig": "Download Backup Configuration",
|
||||
"cleanupBackups": "Cleanup Backups",
|
||||
"backupNow": "Backup now",
|
||||
"stopTask": "Stop Backup",
|
||||
"stopTask": "Stop {{ taskType === 'backup' ? 'Backup' : 'Cleanup' }}",
|
||||
"tooltipEditBackup": "Edit Backup",
|
||||
"tooltipPreservedBackup": "This backup will be preserved"
|
||||
},
|
||||
@@ -638,8 +631,7 @@
|
||||
"preserved": {
|
||||
"description": "Persist backup regardless of retention policy",
|
||||
"tooltip": "This will also preserve the mail and {{ appsLength }} app backup(s)."
|
||||
},
|
||||
"remotePath": "Remote Path"
|
||||
}
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
@@ -670,7 +662,7 @@
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"info": "These settings are global and apply to all domains.",
|
||||
"location": "Mail Server Location",
|
||||
"location": "Mail server location",
|
||||
"maxMailSize": "Maximum email size",
|
||||
"spamFilter": "Spam filtering",
|
||||
"spamFilterOverview": "{{ blacklistCount }} address(es) on the blocklist.",
|
||||
@@ -681,8 +673,7 @@
|
||||
"solrRunning": "Running",
|
||||
"solrNotRunning": "Not Running",
|
||||
"acl": "Mail ACL",
|
||||
"aclOverview": "{{ dnsblZonesCount }} DNSBL zone(s)",
|
||||
"virtualAllMail": "\"All Mail\" Folder"
|
||||
"aclOverview": "{{ dnsblZonesCount }} DNSBL zone(s)"
|
||||
},
|
||||
"eventlog": {
|
||||
"title": "Email Event Log",
|
||||
@@ -716,7 +707,7 @@
|
||||
},
|
||||
"changeDomainDialog": {
|
||||
"title": "Change Email Server Location",
|
||||
"description": "This will move the IMAP and SMTP server to the specified location.",
|
||||
"description": "Cloudron will make the necessary DNS changes across all the domains and restart the mail server. Desktop & Mobile email clients have to be re-configured to use this new location as the IMAP and SMTP server.",
|
||||
"location": "Location",
|
||||
"locationPlaceholder": "Leave empty to use bare domain",
|
||||
"manualInfo": "Add an A record manually for {{ domain }} to this Cloudron's public IP"
|
||||
@@ -773,10 +764,6 @@
|
||||
},
|
||||
"action": {
|
||||
"queue": "Queue"
|
||||
},
|
||||
"changeVirtualAllMailDialog": {
|
||||
"title": "\"All Mail\" Folder",
|
||||
"description": "The \"All Mail\" folder is a single folder that contains all the mails in your Inbox. The folder can be useful in mail clients that do not support recursive folder search."
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
@@ -803,8 +790,7 @@
|
||||
},
|
||||
"dyndns": {
|
||||
"title": "Dynamic DNS",
|
||||
"description": "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.",
|
||||
"showLogsAction": "Show Logs"
|
||||
"description": "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."
|
||||
},
|
||||
"configureIp": {
|
||||
"title": "Configure IP Provider",
|
||||
@@ -820,13 +806,7 @@
|
||||
},
|
||||
"configureIpv6": {
|
||||
"title": "Configure IPv6 Provider"
|
||||
},
|
||||
"trustedIps": {
|
||||
"description": "HTTP headers from matching IP addresses will be trusted",
|
||||
"title": "Configure Trusted IPs",
|
||||
"summary": "{{ trustCount }} IPs trusted"
|
||||
},
|
||||
"trustedIpRanges": "Trusted IPs & Ranges "
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"title": "Services",
|
||||
@@ -942,8 +922,7 @@
|
||||
"reportPlaceholder": "Describe your issue",
|
||||
"emailPlaceholder": "If needed, provide an email address different from above to reach you",
|
||||
"emailVerifyAction": "Verify now",
|
||||
"emailNotVerified": "Your cloudron.io account email {{ email }} is not verified. Please verify it to open support tickets.",
|
||||
"typeBilling": "Billing Issue"
|
||||
"emailNotVerified": "Your cloudron.io account email {{ email }} is not verified. Please verify it to open support tickets."
|
||||
},
|
||||
"remoteSupport": {
|
||||
"title": "Remote Support",
|
||||
@@ -952,10 +931,6 @@
|
||||
"warning": "Do not enable this option unless requested by the Cloudron support team.",
|
||||
"disableAction": "Disable SSH support access",
|
||||
"enableAction": "Enable SSH support access"
|
||||
},
|
||||
"help": {
|
||||
"title": "Help",
|
||||
"description": "Please use the following resources for help and support:\n* [Cloudron Forum]({{ forumLink }}) - Please use the Support and App specific categories for questions.\n* [Cloudron Docs & Knowledge Base]({{ docsLink }})\n* [Custom App Packaging & API]({{ packagingLink }})\n"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
@@ -980,19 +955,7 @@
|
||||
"graphTitle": "Percentage",
|
||||
"graphSubtext": "Only apps using more than {{ threshold }} of cpu are shown"
|
||||
},
|
||||
"selectPeriodLabel": "Select Period",
|
||||
"info": {
|
||||
"platformVersion": "Platform Version",
|
||||
"title": "Info",
|
||||
"vendor": "Vendor",
|
||||
"product": "Product",
|
||||
"memory": "Memory",
|
||||
"uptime": "Uptime",
|
||||
"activationTime": "Cloudron Creation Time"
|
||||
},
|
||||
"graphs": {
|
||||
"title": "Graphs"
|
||||
}
|
||||
"selectPeriodLabel": "Select Period"
|
||||
},
|
||||
"eventlog": {
|
||||
"title": "Event Log",
|
||||
@@ -1017,7 +980,7 @@
|
||||
},
|
||||
"changeDashboardDomain": {
|
||||
"title": "Change Dashboard Domain",
|
||||
"description": "This will move the dashboard to the <code>my</code>subdomain of the selected domain.",
|
||||
"description": "This will move the dashboard and the email server to the <code>my</code>subdomain of the selected domain.",
|
||||
"changeAction": "Change Domain",
|
||||
"cancelAction": "Cancel",
|
||||
"showLogsAction": "Show Logs"
|
||||
@@ -1074,12 +1037,7 @@
|
||||
"cloudflareDefaultProxyStatus": "Enable proxying for new DNS records",
|
||||
"porkbunApikey": "API Key",
|
||||
"porkbunSecretapikey": "Secret API Key",
|
||||
"bunnyAccessKey": "Bunny Access Key",
|
||||
"dnsimpleAccessToken": "Access Token",
|
||||
"ovhEndpoint": "Endpoint",
|
||||
"ovhConsumerKey": "Consumer Key",
|
||||
"ovhAppKey": "Application Key",
|
||||
"ovhAppSecret": "Application Secret"
|
||||
"bunnyAccessKey": "Bunny Access Key"
|
||||
},
|
||||
"removeDialog": {
|
||||
"title": "Really remove {{ domain }}?",
|
||||
@@ -1108,9 +1066,7 @@
|
||||
"logs": {
|
||||
"title": "Logs",
|
||||
"clear": "Clear View",
|
||||
"download": "Download Full Logs",
|
||||
"notFoundError": "No such task or app",
|
||||
"logsGoneError": "Log file(s) not found"
|
||||
"download": "Download Full Logs"
|
||||
},
|
||||
"terminal": {
|
||||
"title": "Terminal",
|
||||
@@ -1208,8 +1164,7 @@
|
||||
"cut": "Cut",
|
||||
"copy": "Copy",
|
||||
"paste": "Paste",
|
||||
"selectAll": "Select All",
|
||||
"open": "Open"
|
||||
"selectAll": "Select All"
|
||||
},
|
||||
"mtime": "Modified"
|
||||
},
|
||||
@@ -1224,19 +1179,7 @@
|
||||
},
|
||||
"status": {
|
||||
"restartingApp": "restarting app"
|
||||
},
|
||||
"uploader": {
|
||||
"uploading": "Uploading",
|
||||
"exitWarning": "Upload still in progress. Really close this page?"
|
||||
},
|
||||
"textEditor": {
|
||||
"undo": "Undo",
|
||||
"redo": "Redo",
|
||||
"save": "Save"
|
||||
},
|
||||
"extractionInProgress": "Extraction in progress",
|
||||
"pasteInProgress": "Pasting in progress",
|
||||
"deleteInProgress": "Deletion in progress"
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"backAction": "Back to Email",
|
||||
@@ -1510,8 +1453,7 @@
|
||||
"description": "If the server is running out of disk space, use this to move the app's data to a <a href=\"/#/volumes\">volume</a>. Any data here is part of the app's backup.",
|
||||
"dataDirPlaceholder": "Leave empty to use platform default",
|
||||
"moveAction": "Move Data",
|
||||
"diskUsage": "The app is currently using {{ size }} of storage (as of {{ date }}).",
|
||||
"mountTypeWarning": "The destination file system must support file permissions and ownership for the move to work"
|
||||
"diskUsage": "The app is currently using {{ size }} of storage (as of {{ date }})."
|
||||
},
|
||||
"mounts": {
|
||||
"title": "Mounts",
|
||||
@@ -1755,17 +1697,6 @@
|
||||
"label": "Label",
|
||||
"clearIconAction": "Clear Icon",
|
||||
"clearIconDescription": "This will try to fetch the app's favicon on save."
|
||||
},
|
||||
"servicesTabTitle": "Services",
|
||||
"turn": {
|
||||
"title": "TURN Setup",
|
||||
"enable": "Configure the app to use the built-in TURN server",
|
||||
"disable": "Do not configure the app's TURN settings. The app's TURN settings are left alone. You can configure it inside the app."
|
||||
},
|
||||
"redis": {
|
||||
"title": "Redis Configuration",
|
||||
"enable": "Configure the app to use Redis",
|
||||
"disable": "Disable Redis"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
@@ -1895,11 +1826,7 @@
|
||||
"mountStatus": "Mount Status",
|
||||
"type": "Type",
|
||||
"localDirectory": "Local Directory",
|
||||
"remountActionTooltip": "Remount Volume",
|
||||
"editVolumeDialog": {
|
||||
"title": "Edit volume {{ name }}"
|
||||
},
|
||||
"editActionTooltip": "Edit Volume"
|
||||
"remountActionTooltip": "Remount Volume"
|
||||
},
|
||||
"newLoginEmail": {
|
||||
"subject": "[<%= cloudron %>] New login on your account",
|
||||
@@ -1952,6 +1879,5 @@
|
||||
"newClient": "New client",
|
||||
"empty": "No clients yet"
|
||||
}
|
||||
},
|
||||
"automation": "Automation"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"switchToLoginAction": "¿Ya tienes una cuenta? Inicia sesión",
|
||||
"switchToSignUpAction": "¿No tienes una cuenta todavía? Regístrate",
|
||||
"createAccountAction": "Crear Cuenta",
|
||||
"loginAction": "Iniciar Sesión",
|
||||
"loginAction": "Iniciar sesión",
|
||||
"errorWrongPassword": "Contraseña errónea",
|
||||
"licenseCheckbox": "Acepto la <a href=\"{{ licenseLink }}\" target=\"_blank\">licencia de Cloudron</a>",
|
||||
"chooseAnOption": "Por favor escoge una opción…",
|
||||
@@ -97,14 +97,12 @@
|
||||
},
|
||||
"action": {
|
||||
"logs": "Registros",
|
||||
"reboot": "Reiniciar",
|
||||
"showLogs": "Mostrar registros"
|
||||
"reboot": "Reiniciar"
|
||||
},
|
||||
"pagination": {
|
||||
"perPageSelector": "Mostrar {{ n }} por página",
|
||||
"next": "siguiente",
|
||||
"prev": "anterior",
|
||||
"itemCount": "Encontrado {{ count }}"
|
||||
"prev": "anterior"
|
||||
},
|
||||
"table": {
|
||||
"date": "Fecha"
|
||||
@@ -117,8 +115,7 @@
|
||||
"no": "No",
|
||||
"close": "Cerrar",
|
||||
"save": "Guardar",
|
||||
"cancel": "Cancelar",
|
||||
"delete": "Borrar"
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"logout": "Salir",
|
||||
"offline": "Cloudron está desconectado. Reconectando…",
|
||||
@@ -140,10 +137,7 @@
|
||||
},
|
||||
"enableAction": "Habilitar",
|
||||
"statusEnabled": "Habilitado",
|
||||
"statusDisabled": "Deshabilitado",
|
||||
"loadingPlaceholder": "Cargando",
|
||||
"settings": "Ajustes",
|
||||
"saveAction": "Guardar"
|
||||
"statusDisabled": "Deshabilitado"
|
||||
},
|
||||
"apps": {
|
||||
"domainsFilterHeader": "Todos los Dominios",
|
||||
@@ -168,8 +162,7 @@
|
||||
"auth": {
|
||||
"nosso": "Inicia sesión con una cuenta dedicada",
|
||||
"sso": "Inicia sesión con las credenciales de Cloudron",
|
||||
"email": "Inicia sesión con el correo electrónico",
|
||||
"openid": "Iniciar sesión con Cloudron OpenID"
|
||||
"email": "Inicia sesión con el correo electrónico"
|
||||
},
|
||||
"addAppAction": "Añadir Aplicación",
|
||||
"addAppproxyAction": "Añadir Proxi de la Aplicación",
|
||||
@@ -221,7 +214,7 @@
|
||||
"subscriptionRequired": "Estas características solo están habilitadas para planes de pago.",
|
||||
"require2FACheckbox": "Requerir que los usuarios configuren 2FA",
|
||||
"allowProfileEditCheckbox": "Permitir a los usuarios editar su nombre y correo",
|
||||
"title": "Ajustes de usuario",
|
||||
"title": "Ajustes",
|
||||
"require2FAWarning": "Configura primero 2FA para tu cuenta para evitar que la bloqueen."
|
||||
},
|
||||
"groups": {
|
||||
@@ -407,7 +400,7 @@
|
||||
"description": "Ten cuidado al cargar estos registros en un servidor público, ya que pueden contener información confidencial."
|
||||
},
|
||||
"listing": {
|
||||
"stopTask": "Parar Backup",
|
||||
"stopTask": "Parar {{ taskType === 'backup' ? 'Backup' : 'Cleanup' }}",
|
||||
"backupNow": "Hacer Copia de Seguridad Ahora",
|
||||
"cleanupBackups": "Borrar Copias de Seguridad",
|
||||
"tooltipDownloadBackupConfig": "Descarga Configuración de la Copia de Seguridad",
|
||||
@@ -524,8 +517,7 @@
|
||||
"preserved": {
|
||||
"description": "Copia de seguridad persistente independientemente de la política de retención",
|
||||
"tooltip": "Esto también conservará el correo y las copias de seguridad de la aplicación {{ appsLength }}."
|
||||
},
|
||||
"remotePath": "Ruta remota"
|
||||
}
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@@ -615,7 +607,7 @@
|
||||
"errorPasswordsDontMatch": "Las contraseñas no coinciden",
|
||||
"errorPasswordRequired": "Se requiere una contraseña",
|
||||
"newPasswordRepeat": "Repite nueva contraseña",
|
||||
"newPassword": "Nueva Contraseña",
|
||||
"newPassword": "Nueva contraseña",
|
||||
"currentPassword": "Contraseña actual",
|
||||
"title": "Cambia tu contraseña"
|
||||
},
|
||||
@@ -636,8 +628,7 @@
|
||||
},
|
||||
"changeBackgroundImage": {
|
||||
"title": "Establecer imagen de fondo"
|
||||
},
|
||||
"enable2FANotAvailable": "No disponible para usuarios de una fuente de autentificación externa"
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"eventlog": {
|
||||
@@ -684,8 +675,7 @@
|
||||
"info": "Esta configuración es global y se aplica a todos los dominios.",
|
||||
"title": "Ajustes",
|
||||
"acl": "Correo ACL",
|
||||
"aclOverview": "{{ dnsblZonesCount }} zona(s) DNSBL",
|
||||
"virtualAllMail": "Carpeta \"Todos los correos\""
|
||||
"aclOverview": "{{ dnsblZonesCount }} zona(s) DNSBL"
|
||||
},
|
||||
"domains": {
|
||||
"testEmailTooltip": "Enviar Email de prueba",
|
||||
@@ -728,7 +718,7 @@
|
||||
"manualInfo": "Agrega un registro A manualmente para el {{dominio}} a la IP pública de este Cloudron",
|
||||
"locationPlaceholder": "Dejar vacío para usar el dominio desnudo",
|
||||
"location": "Ubicación",
|
||||
"description": "Esto moverá el servidor IMAP y SMTP a la ubicación especificada.",
|
||||
"description": "Cloudron realizará los cambios de DNS necesarios en todos los dominios y reiniciará el servidor de correo. Los clientes de correo electrónico de escritorio y móviles deben reconfigurarse para usar esta nueva ubicación como servidor IMAP y SMTP.",
|
||||
"title": "Cambiar ubicación del Servidor de Correo"
|
||||
},
|
||||
"aclDialog": {
|
||||
@@ -756,10 +746,6 @@
|
||||
},
|
||||
"action": {
|
||||
"queue": "Cola"
|
||||
},
|
||||
"changeVirtualAllMailDialog": {
|
||||
"title": "Carpeta \"Todos los correos\"",
|
||||
"description": "La carpeta \"Todos los correos\" es una carpeta única que contiene todos los correos electrónicos de su bandeja de entrada. La carpeta puede resultar útil en clientes de correo que no admiten la búsqueda recursiva de carpetas."
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
@@ -804,8 +790,7 @@
|
||||
},
|
||||
"dyndns": {
|
||||
"description": "Habilite esta opción para mantener todos sus registros DNS sincronizados con una dirección IP cambiante. Esto es útil cuando Cloudron se ejecuta en una red con una dirección IP pública que cambia con frecuencia, como una conexión doméstica.",
|
||||
"title": "DNS Dinámico",
|
||||
"showLogsAction": "Mostrar registros"
|
||||
"title": "DNS Dinámico"
|
||||
},
|
||||
"ipv4": {
|
||||
"address": "Dirección IPv4"
|
||||
@@ -817,13 +802,7 @@
|
||||
},
|
||||
"configureIpv6": {
|
||||
"title": "Configurar Proveedor de IPv6"
|
||||
},
|
||||
"trustedIps": {
|
||||
"summary": "{{ trustCount }} IPs confiables",
|
||||
"description": "Se confiará en los encabezados HTTP de direcciones IP coincidentes",
|
||||
"title": "Configurar IP confiables"
|
||||
},
|
||||
"trustedIpRanges": "Rangos e IPs confiables "
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"configure": {
|
||||
@@ -843,7 +822,7 @@
|
||||
"service": "Servicio",
|
||||
"description": "Los servicios de Cloudron implementan funcionalidades como bases de datos, correo electrónico y autentificación.",
|
||||
"title": "Servicios",
|
||||
"refresh": "Refrescar"
|
||||
"refresh": "Actualizar"
|
||||
},
|
||||
"settings": {
|
||||
"appstoreAccount": {
|
||||
@@ -922,7 +901,7 @@
|
||||
"domains": {
|
||||
"title": "Dominios y Certificados",
|
||||
"changeDashboardDomain": {
|
||||
"description": "Esto moverá el panel al subdominio <code>my</code> del dominio seleccionado.",
|
||||
"description": "Esto moverá el Panel y el Servidor de Correo al subdominio <code>my</code> del dominio seleccionado.",
|
||||
"showLogsAction": "Mostrar Registros",
|
||||
"cancelAction": "Cancelar",
|
||||
"changeAction": "Cambiar Dominio",
|
||||
@@ -971,11 +950,7 @@
|
||||
"vultrToken": "Token Vultr",
|
||||
"jitsiHostname": "Ubicación de Jitsi",
|
||||
"wellKnownDescription": "Cloudron utilizará los valores para responder a las URLs <code>/.well-known/</code> . Ten en cuenta que la aplicación debe estar disponible en el dominio desnudo <code>{{ domain }}</code> para que esto funcione. Consulta <a href=\"{{docsLink}}\" target=\"_blank\">esta documentación</a> para más información.",
|
||||
"hetznerToken": "Token de Hetzner",
|
||||
"bunnyAccessKey": "Clave de acceso Bunny",
|
||||
"cloudflareDefaultProxyStatus": "Habilitar proxy para nuevos registros DNS",
|
||||
"porkbunApikey": "Clave API",
|
||||
"porkbunSecretapikey": "Clave API secreta"
|
||||
"hetznerToken": "Token de Hetzner"
|
||||
},
|
||||
"subscriptionRequired": {
|
||||
"setupAction": "Configura tu suscripción",
|
||||
@@ -1007,8 +982,7 @@
|
||||
"domainWellKnown": {
|
||||
"title": "Ubicaciones Well-known de {{ domain }}"
|
||||
},
|
||||
"tooltipWellKnown": "Establece las ubicaciones Well-Known",
|
||||
"count": "Dominios totales: {{ count }}"
|
||||
"tooltipWellKnown": "Establece las ubicaciones Well-Known"
|
||||
},
|
||||
"app": {
|
||||
"appInfo": {
|
||||
@@ -1064,8 +1038,7 @@
|
||||
"dataDirPlaceholder": "Dejar vacío para usar la plataforma predeterminada",
|
||||
"description": "Si el servidor se está quedando sin espacio en disco, usa esto para mover los datos de la aplicación a un <a href=\"/#/volumes\">volumen</a>. Cualquier dato aquí es parte de la copia de seguridad de la aplicación.",
|
||||
"moveAction": "Mover datos",
|
||||
"diskUsage": "Actualmente, la aplicación está usando {{ size }} de almacenamiento (hasta el {{ date }}).",
|
||||
"mountTypeWarning": "El sistema de archivos de destino debe admitir permisos y propiedad de los archivos para que el traslado funcione"
|
||||
"diskUsage": "Actualmente, la aplicación está usando {{ size }} de almacenamiento (hasta el {{ date }})."
|
||||
}
|
||||
},
|
||||
"logsActionTooltip": "Registros",
|
||||
@@ -1125,8 +1098,7 @@
|
||||
"saveAction": "Guardar",
|
||||
"description": "La configuración de esta opción anulará cualquier encabezado CSP enviado por la propia aplicación",
|
||||
"title": "Política de seguridad de contenido"
|
||||
},
|
||||
"hstsPreload": "Habilitar la carga previa de HSTS para este sitio y todos los subdominios"
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"from": {
|
||||
@@ -1235,8 +1207,7 @@
|
||||
"description": "Todos los datos generados entre ahora y la última copia de seguridad conocida se perderán de forma irrevocable. Se recomienda crear una copia de seguridad de los datos actuales antes de intentar una importación.",
|
||||
"title": "Importar Backup",
|
||||
"uploadAction": "Subir Configuración de Backup",
|
||||
"importAction": "Importar",
|
||||
"remotePath": "Ruta del Backup"
|
||||
"importAction": "Importar"
|
||||
},
|
||||
"restoreDialog": {
|
||||
"warning": "Todos los datos generados entre ahora y la última copia de seguridad conocida se perderán de forma irrevocable. Se recomienda crear una copia de seguridad de los datos actuales antes de intentar una restauración.",
|
||||
@@ -1339,17 +1310,6 @@
|
||||
"label": "Etiqueta",
|
||||
"clearIconAction": "Borrar icono",
|
||||
"clearIconDescription": "Esto intentará obtener el favicon de la aplicación al guardar."
|
||||
},
|
||||
"servicesTabTitle": "Servicios",
|
||||
"turn": {
|
||||
"title": "Configuración de TURN",
|
||||
"enable": "Configura la aplicación para utilizar el servidor TURN integrado",
|
||||
"disable": "No configures los ajustes de la aplicación TURN. Su configuración se deja como está. Puedes hacer los ajustes dentro de la aplicación."
|
||||
},
|
||||
"redis": {
|
||||
"title": "Configuración de Redis",
|
||||
"enable": "Configura la aplicación para usar Redis",
|
||||
"disable": "Deshabilitar Redis"
|
||||
}
|
||||
},
|
||||
"lang": {
|
||||
@@ -1364,8 +1324,7 @@
|
||||
"en": "Inglés",
|
||||
"es": "Español",
|
||||
"ru": "Ruso",
|
||||
"pt": "Portugués",
|
||||
"da": "Danés"
|
||||
"pt": "Portugués"
|
||||
},
|
||||
"system": {
|
||||
"title": "Información del Sistema",
|
||||
@@ -1386,8 +1345,7 @@
|
||||
"title": "Uso del Disco",
|
||||
"usedInfo": "{{ used }} usados de {{ size }}",
|
||||
"uninstalledApp": "Aplicación desinstalada",
|
||||
"volumeContent": "Este disco es el volumen <code>{{ name }}</code>",
|
||||
"diskSpeed": "Velocidad: {{ speed }} MB/seg"
|
||||
"volumeContent": "Este disco es el volumen <code>{{ name }}</code>"
|
||||
},
|
||||
"selectPeriodLabel": "Seleccionar Periodo"
|
||||
},
|
||||
@@ -1418,8 +1376,7 @@
|
||||
"sshCheckbox": "Permitir que los ingenieros de soporte se conecten a este servidor a través de SSH",
|
||||
"emailPlaceholder": "Si es necesario, proporciona una dirección de correo electrónico diferente de la anterior para contactarte",
|
||||
"emailVerifyAction": "Verificar ahora",
|
||||
"emailNotVerified": "El correo electrónico de su cuenta cloudron.io {{email}} no está verificado. Verifíquelo para abrir tickets de soporte.",
|
||||
"typeBilling": "Problema de facturación"
|
||||
"emailNotVerified": "El correo electrónico de su cuenta cloudron.io {{email}} no está verificado. Verifíquelo para abrir tickets de soporte."
|
||||
},
|
||||
"title": "Soporte"
|
||||
},
|
||||
@@ -1446,7 +1403,7 @@
|
||||
"removeVolumeActionTooltip": "Borrar Volumen",
|
||||
"openFileManagerActionTooltip": "Abrir Gestor de Archivos",
|
||||
"name": "Nombre",
|
||||
"hostPath": "Objetivo",
|
||||
"hostPath": "Punto de montaje",
|
||||
"addVolumeAction": "Añade un Volumen",
|
||||
"title": "Volúmenes",
|
||||
"description": "Los volúmenes son sistemas de archivos locales o remotos. Se pueden usar como el almacenamiento de datos principal de una aplicación o como una ubicación de almacenamiento compartida entre aplicaciones.",
|
||||
@@ -1458,11 +1415,7 @@
|
||||
"title": "Actualizar Volumen {{ volume }}"
|
||||
},
|
||||
"tooltipEdit": "Editar Volumen",
|
||||
"remountActionTooltip": "Volver a montar Volumen",
|
||||
"editVolumeDialog": {
|
||||
"title": "Editar volumen {{ name }}"
|
||||
},
|
||||
"editActionTooltip": "Editar Volumen"
|
||||
"remountActionTooltip": "Volver a montar Volumen"
|
||||
},
|
||||
"eventlog": {
|
||||
"filterAllEvents": "Todos los Eventos",
|
||||
@@ -1541,8 +1494,7 @@
|
||||
"copy": "Copiar",
|
||||
"paste": "Pegar",
|
||||
"selectAll": "Seleccionar todo",
|
||||
"download": "Descargar",
|
||||
"open": "Abrir"
|
||||
"download": "Descargar"
|
||||
},
|
||||
"mtime": "Modificado"
|
||||
},
|
||||
@@ -1557,26 +1509,12 @@
|
||||
},
|
||||
"extract": {
|
||||
"error": "La extracción falló: {{ message }}"
|
||||
},
|
||||
"extractionInProgress": "Extracción en progreso",
|
||||
"uploader": {
|
||||
"exitWarning": "Subida en progreso... ¿quieres realmente cerrar esta página?",
|
||||
"uploading": "Subiendo"
|
||||
},
|
||||
"textEditor": {
|
||||
"undo": "Deshacer",
|
||||
"redo": "Rehacer",
|
||||
"save": "Guardar"
|
||||
},
|
||||
"pasteInProgress": "Pegado en progreso",
|
||||
"deleteInProgress": "Borrado en progreso"
|
||||
}
|
||||
},
|
||||
"logs": {
|
||||
"download": "Descarga los Registros Completos",
|
||||
"clear": "Borrar Vista",
|
||||
"title": "Registros",
|
||||
"notFoundError": "No existe esa tarea o aplicación",
|
||||
"logsGoneError": "Archivo(s) de registro no encontrados"
|
||||
"title": "Registros"
|
||||
},
|
||||
"email": {
|
||||
"signature": {
|
||||
@@ -1812,7 +1750,7 @@
|
||||
"newPassword": {
|
||||
"errorLength": "La contraseña debe tener al menos 8 y un máximo de 265 caracteres",
|
||||
"title": "Establecer nueva contraseña",
|
||||
"password": "Nueva Contraseña",
|
||||
"password": "Nueva contraseña",
|
||||
"passwordRepeat": "Repetir Contraseña",
|
||||
"errorMismatch": "Las contraseñas no coinciden"
|
||||
},
|
||||
@@ -1872,10 +1810,8 @@
|
||||
"username": "Nombre de usuario",
|
||||
"password": "Contraseña",
|
||||
"2faToken": "Token 2FA (si está habilitado)",
|
||||
"signInAction": "Iniciar Sesión",
|
||||
"resetPasswordAction": "Resetear contraseña",
|
||||
"errorIncorrect2FAToken": "El token 2FA es inválido",
|
||||
"errorInternal": "Error interno, prueba de nuevo más tarde"
|
||||
"signInAction": "Iniciar sesión",
|
||||
"resetPasswordAction": "Resetear contraseña"
|
||||
},
|
||||
"newLoginEmail": {
|
||||
"subject": "[<% = cloudron%>] Nuevo inicio de sesión en tu cuenta",
|
||||
@@ -1891,43 +1827,5 @@
|
||||
"mounts": {
|
||||
"description": "Las aplicaciones pueden acceder a <a href=\"/#/volumes\">volúmenes</a> montados a través del directorio <code>/media/{volume name}</code>. Estos datos no están incluidos en la copia de seguridad de la aplicación."
|
||||
}
|
||||
},
|
||||
"oidc": {
|
||||
"newClientDialog": {
|
||||
"title": "Añadir Cliente",
|
||||
"description": "Agrega una nueva configuración de cliente de conexión de OpenID.",
|
||||
"createAction": "Crear"
|
||||
},
|
||||
"client": {
|
||||
"name": "Nombre",
|
||||
"id": "ID de cliente",
|
||||
"secret": "Secreto de cliente",
|
||||
"signingAlgorithm": "Algoritmo de firma",
|
||||
"loginRedirectUri": "URL de devolución de llamada de inicio de sesión (separadas por comas si hay más de una)",
|
||||
"logoutRedirectUri": "URL de devolución de llamada de cierre de sesión (opcional)"
|
||||
},
|
||||
"title": "Proveedor de conexión OpenID",
|
||||
"description": "Cloudron puede actuar como proveedor de OpenID Connect para aplicaciones internas y servicios externos.",
|
||||
"editClientDialog": {
|
||||
"title": "Editar cliente {{ client }}"
|
||||
},
|
||||
"deleteClientDialog": {
|
||||
"title": "¿Realmente quieres borrar el cliente {{ client }}?",
|
||||
"description": "Esto desconectará todas las aplicaciones OpenID externas de este Cloudron que utilicen este ID de cliente."
|
||||
},
|
||||
"env": {
|
||||
"discoveryUrl": "URL de descubrimiento",
|
||||
"logoutUrl": "URL de cierre de sesión",
|
||||
"profileEndpoint": "Punto final del perfil",
|
||||
"keysEndpoint": "Punto final de claves",
|
||||
"tokenEndpoint": "Punto final del Token",
|
||||
"authEndpoint": "Punto final de autenticación"
|
||||
},
|
||||
"clients": {
|
||||
"title": "Clientes",
|
||||
"newClient": "Nuevo cliente",
|
||||
"empty": "No hay clientes aún"
|
||||
}
|
||||
},
|
||||
"automation": "Automatización"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,7 @@
|
||||
"pagination": {
|
||||
"prev": "préc.",
|
||||
"next": "suiv.",
|
||||
"perPageSelector": "Afficher {{ n }} par page",
|
||||
"itemCount": "Trouvé {{ count }}"
|
||||
"perPageSelector": "Afficher {{ n }} par page"
|
||||
},
|
||||
"action": {
|
||||
"logs": "Journaux",
|
||||
@@ -86,8 +85,7 @@
|
||||
"users": "Utilisateurs"
|
||||
},
|
||||
"disableAction": "Désactiver",
|
||||
"enableAction": "Activer",
|
||||
"loadingPlaceholder": "Chargement"
|
||||
"enableAction": "Activer"
|
||||
},
|
||||
"users": {
|
||||
"title": "Annuaire des utilisateurs",
|
||||
@@ -523,7 +521,7 @@
|
||||
"tooltipDownloadBackupConfig": "Télécharger le fichier de configuration de la sauvegarde",
|
||||
"cleanupBackups": "Supprimer toutes les sauvegardes",
|
||||
"backupNow": "Faire une sauvegarder maintenant",
|
||||
"stopTask": "Interrompre la sauvegarde",
|
||||
"stopTask": "Interrompre {{ taskType === 'backup' ? 'la sauvegarde' : 'Nettoyer' }}",
|
||||
"noBackups": "Aucune sauvegarde n'a encore été effectuée.",
|
||||
"contents": "Contenu",
|
||||
"version": "Version",
|
||||
@@ -1741,9 +1739,7 @@
|
||||
"usageInfo": "{{ available | prettyDiskSize }}</b> sur <b>{{ size | prettyDiskSize }}</b> disponible(s)",
|
||||
"mountedAt": "{{ filesystem }} <small>monté sur</small> {{ mountpoint }}",
|
||||
"title": "Utilisation du disque",
|
||||
"usedInfo": "{{ used }} utilisé de {{ size }}",
|
||||
"uninstalledApp": "Désinstaller App",
|
||||
"diskSpeed": "Vitesse : {{ speed }} MB/sec"
|
||||
"usedInfo": "{{ used }} utilisé de {{ size }}"
|
||||
},
|
||||
"title": "Info système"
|
||||
},
|
||||
|
||||
@@ -727,7 +727,7 @@
|
||||
"title": "Logs"
|
||||
},
|
||||
"listing": {
|
||||
"stopTask": "Ferma Backup",
|
||||
"stopTask": "Ferma {{ taskType === 'backup' ? 'Backup' : 'Pulizia' }}",
|
||||
"backupNow": "Esegui il backup adesso",
|
||||
"cleanupBackups": "Pulizia Backup",
|
||||
"tooltipDownloadBackupConfig": "Scarica Configurazione Backup",
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
"auth": {
|
||||
"nosso": "Log in met specifiek account",
|
||||
"sso": "Log in met Cloudron aanmeldgegevens",
|
||||
"email": "Log in met e-mailadres",
|
||||
"openid": "Log in met Cloudron OpenID"
|
||||
"email": "Log in met e-mailadres"
|
||||
},
|
||||
"addAppAction": "App toevoegen",
|
||||
"addAppproxyAction": "App Proxy toevoegen",
|
||||
@@ -39,8 +38,7 @@
|
||||
"save": "Opslaan",
|
||||
"close": "Sluiten",
|
||||
"no": "Nee",
|
||||
"yes": "Ja",
|
||||
"delete": "Verwijder"
|
||||
"yes": "Ja"
|
||||
},
|
||||
"username": "Gebruikersnaam",
|
||||
"displayName": "Naam",
|
||||
@@ -56,8 +54,7 @@
|
||||
},
|
||||
"action": {
|
||||
"reboot": "Herstart",
|
||||
"logs": "Logbestanden",
|
||||
"showLogs": "Toon logbestanden"
|
||||
"logs": "Logbestanden"
|
||||
},
|
||||
"clipboard": {
|
||||
"copied": "Gekopieerd naar klembord",
|
||||
@@ -90,9 +87,7 @@
|
||||
"enableAction": "Inschakelen",
|
||||
"statusEnabled": "Ingeschakeld",
|
||||
"statusDisabled": "Uitgeschakeld",
|
||||
"loadingPlaceholder": "Laden",
|
||||
"settings": "Instellingen",
|
||||
"saveAction": "Opslaan"
|
||||
"loadingPlaceholder": "Laden"
|
||||
},
|
||||
"appstore": {
|
||||
"title": "App Store",
|
||||
@@ -165,10 +160,7 @@
|
||||
"loginAction": "Inloggen",
|
||||
"createAccountAction": "Account aanmaken",
|
||||
"switchToSignUpAction": "Nog geen account? Registreer",
|
||||
"switchToLoginAction": "Al een account? Log in",
|
||||
"setupWithTokenAction": "Instellen",
|
||||
"setupToken": "Instel Token",
|
||||
"titleToken": "Inloggen met Instel Token"
|
||||
"switchToLoginAction": "Al een account? Log in"
|
||||
},
|
||||
"searchPlaceholder": "Zoek voor alternatieven zoals Github, Dropbox, Slack, Trello, …",
|
||||
"appNotFoundDialog": {
|
||||
@@ -212,7 +204,7 @@
|
||||
"newGroupAction": "Nieuwe groep"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Gebruiker instellingen",
|
||||
"title": "Instellingen",
|
||||
"require2FACheckbox": "Gebruikers moeten 2FA activeren",
|
||||
"subscriptionRequired": "Deze functies zijn alleen beschikbaar voor betaalde abonnementen.",
|
||||
"subscriptionRequiredAction": "Abonnement nemen",
|
||||
@@ -511,8 +503,7 @@
|
||||
},
|
||||
"changeBackgroundImage": {
|
||||
"title": "Stel achtergrond afbeelding in"
|
||||
},
|
||||
"enable2FANotAvailable": "Niet beschikbaar voor gebruikers met een externe authenticatie bron"
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
"title": "Backups",
|
||||
@@ -542,7 +533,7 @@
|
||||
"noApps": "Geen apps",
|
||||
"cleanupBackups": "Backups opschonen",
|
||||
"backupNow": "Backup maken",
|
||||
"stopTask": "Stop Backup",
|
||||
"stopTask": "Stop {{ taskType === 'backup' ? 'Backup' : 'Cleanup' }}",
|
||||
"appCount": "{{ appCount }} apps",
|
||||
"tooltipDownloadBackupConfig": "Download Backup Configuratie",
|
||||
"tooltipEditBackup": "Bewerk Backup",
|
||||
@@ -638,8 +629,7 @@
|
||||
"description": "Backup behouden, ongeacht het bewaarbeleid"
|
||||
},
|
||||
"title": "Bewerk Backup",
|
||||
"label": "Label",
|
||||
"remotePath": "Extern pad"
|
||||
"label": "Label"
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
@@ -670,7 +660,7 @@
|
||||
"settings": {
|
||||
"title": "Instellingen",
|
||||
"info": "Deze instellingen zijn generiek voor alle domeinen.",
|
||||
"location": "Mail Server Locatie",
|
||||
"location": "Mail server locatie",
|
||||
"maxMailSize": "Maximale e-mail grootte",
|
||||
"spamFilter": "Spam filtering",
|
||||
"spamFilterOverview": "{{ blacklistCount }} adres(sen) op de blokkeerlijst.",
|
||||
@@ -681,8 +671,7 @@
|
||||
"solrFts": "Zoek volledige tekst (Solr)",
|
||||
"solrDisabled": "Uitgeschakeld",
|
||||
"acl": "E-mail ACL",
|
||||
"aclOverview": "{{ dnsblZonesCount }} DNSBL zone(s)",
|
||||
"virtualAllMail": "\"Alle E-mail\" map"
|
||||
"aclOverview": "{{ dnsblZonesCount }} DNSBL zone(s)"
|
||||
},
|
||||
"eventlog": {
|
||||
"title": "E-mail logboek",
|
||||
@@ -719,7 +708,7 @@
|
||||
"manualInfo": "Voeg handmatig een A record toe voor {{ domain }} die verwijst naar het IP van deze Cloudron",
|
||||
"locationPlaceholder": "Leeg laten om hoofddomein te gebruiken",
|
||||
"title": "E-mail server locatie aanpassen",
|
||||
"description": "Dit verhuist de IMAP en SMTP server naar de aangegeven lokatie."
|
||||
"description": "Cloudron zorgt voor de benodigde DNS aanpassingen van alle domeinen en herstart de e-mail server. Desktop & mobiele e-mailprogramma's moeten opnieuw geconfigureerd worden met deze nieuwe locatie als IMAP en SMTP server."
|
||||
},
|
||||
"changeMailSizeDialog": {
|
||||
"title": "Maximale e-mail grootte aanpassen",
|
||||
@@ -773,10 +762,6 @@
|
||||
},
|
||||
"action": {
|
||||
"queue": "Wachtrij"
|
||||
},
|
||||
"changeVirtualAllMailDialog": {
|
||||
"title": "\"Alle E-mail\" map",
|
||||
"description": "De \"Alle E-mail\" map is een enkele map die alle e-mails bevat van je mailbox. Deze map kan handig zijn indien een e-mailprogramma \"zoek in alle mappen\" niet ondersteunt."
|
||||
}
|
||||
},
|
||||
"domains": {
|
||||
@@ -827,12 +812,7 @@
|
||||
"cloudflareDefaultProxyStatus": "Inschakelen proxy voor nieuwe DNS regels",
|
||||
"porkbunApikey": "API sleutel",
|
||||
"porkbunSecretapikey": "Geheime API sleutel",
|
||||
"bunnyAccessKey": "Bunny toegangssleutel",
|
||||
"dnsimpleAccessToken": "Toegangstoken",
|
||||
"ovhEndpoint": "Eindpunt",
|
||||
"ovhConsumerKey": "Consumer sleutel",
|
||||
"ovhAppKey": "Applicatie sleutel",
|
||||
"ovhAppSecret": "Applicatie geheim"
|
||||
"bunnyAccessKey": "Bunny toegangssleutel"
|
||||
},
|
||||
"title": "Domeinen & Certificaten",
|
||||
"addDomain": "Domein toevoegen",
|
||||
@@ -851,7 +831,7 @@
|
||||
"cancelAction": "Annuleer",
|
||||
"showLogsAction": "Toon logbestanden",
|
||||
"title": "Dashboard-domein aanpassen",
|
||||
"description": "Hierdoor verhuist het Dashboard naar het <code>my</code> subdomein van het geselecteerde domein."
|
||||
"description": "Hierdoor verhuist het Dashboard en de e-mailserver naar het <code>my</code> subdomein van het geselecteerde domein."
|
||||
},
|
||||
"subscriptionRequired": {
|
||||
"title": "Abonnement verplicht",
|
||||
@@ -872,8 +852,7 @@
|
||||
"domainWellKnown": {
|
||||
"title": "Well-Known locaties van {{ domain }}"
|
||||
},
|
||||
"tooltipWellKnown": "Well-Known Locaties instellen",
|
||||
"count": "Totaal domeinen: {{ count }}"
|
||||
"tooltipWellKnown": "Well-Known Locaties instellen"
|
||||
},
|
||||
"app": {
|
||||
"email": {
|
||||
@@ -981,8 +960,7 @@
|
||||
"dataDirPlaceholder": "Laat leeg om platformstandaard te gebruiken",
|
||||
"moveAction": "Verplaats data",
|
||||
"description": "Als de server onvoldoende schijfruimte heeft, gebruik dit om de app data te verplaatsen naar een <a href=\"/#/volumes\">volume</a>. Alle data daar is onderdeel van de app's backup.",
|
||||
"diskUsage": "De app gebruikt momenteel {{ size }} aan opslag (sinds {{ date }}).",
|
||||
"mountTypeWarning": "Het bestemmingsbestandssysteem moet bestandsmachtigingen en eigendom ondersteunen om de verhuizing te laten werken"
|
||||
"diskUsage": "De app gebruikt momenteel {{ size }} aan opslag (sinds {{ date }})."
|
||||
},
|
||||
"mounts": {
|
||||
"title": "Koppelpunten",
|
||||
@@ -1186,7 +1164,7 @@
|
||||
"service": "Dienst (start eenmalig)"
|
||||
},
|
||||
"title": "Crontab",
|
||||
"saveAction": "Opslaan",
|
||||
"saveAction": "Bewaar",
|
||||
"addCommonPattern": "Voeg gemeenschappelijk patroon toe",
|
||||
"description": "Eigen app-specifieke cron jobs kunnen hier toegevoegd worden. Let op: standaard cron jobs voor deze applicatie zijn al geïntegreerd in de app en hoef je hier niet te configureren."
|
||||
},
|
||||
@@ -1204,17 +1182,6 @@
|
||||
"label": "Label",
|
||||
"clearIconAction": "Icoon verwijderen",
|
||||
"clearIconDescription": "Hiermee wordt geprobeerd de favicon van de app op te halen na opslaan."
|
||||
},
|
||||
"servicesTabTitle": "Diensten",
|
||||
"turn": {
|
||||
"title": "TURN Instellen",
|
||||
"enable": "Configureer de app om de ingebouwde TURN server te gebruiken",
|
||||
"disable": "Configureer de TURN-instellingen van de app niet. De TURN-instellingen van de app worden met rust gelaten. Je kunt het in de app configureren."
|
||||
},
|
||||
"redis": {
|
||||
"title": "Redis configuratie",
|
||||
"enable": "Configureer de app om Redis te gebruiken",
|
||||
"disable": "Redis uitschakelen"
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
@@ -1241,8 +1208,7 @@
|
||||
},
|
||||
"dyndns": {
|
||||
"title": "Dynamische DNS",
|
||||
"description": "Schakel deze optie in om je DNS records synchroon te houden met je veranderende IP adres. Dit is handig als je Cloudron opgenomen is in een netwerk waarbij het publieke IP adres steeds wisselt zoals in een thuissituatie.",
|
||||
"showLogsAction": "Toon logbestanden"
|
||||
"description": "Schakel deze optie in om je DNS records synchroon te houden met je veranderende IP adres. Dit is handig als je Cloudron opgenomen is in een netwerk waarbij het publieke IP adres steeds wisselt zoals in een thuissituatie."
|
||||
},
|
||||
"configureIp": {
|
||||
"title": "Configureer IP aanbieder",
|
||||
@@ -1258,13 +1224,7 @@
|
||||
},
|
||||
"configureIpv6": {
|
||||
"title": "Configureer IPv6 aanbieder"
|
||||
},
|
||||
"trustedIps": {
|
||||
"description": "HTTP headers van bijbehorende IP adressen worden vertrouwd",
|
||||
"summary": "{{ trustCount }} IP’s vertrouwd",
|
||||
"title": "Configureer vertrouwde IP’s"
|
||||
},
|
||||
"trustedIpRanges": "Vertrouwde IP’s & bereiken "
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"title": "Diensten",
|
||||
@@ -1380,8 +1340,7 @@
|
||||
"emailInfo": "(E-mail van het abonnement is {{ email }})",
|
||||
"sshCheckbox": "Sta toe dat ondersteuningsmedewerkers toegang krijgen tot deze server middels SSH",
|
||||
"emailVerifyAction": "Verifieer nu",
|
||||
"emailNotVerified": "Je cloudron.io account e-mail {{ email }} is niet geverifieerd. Verifieer het om support tickets te kunnen openen.",
|
||||
"typeBilling": "Factureringsprobleem"
|
||||
"emailNotVerified": "Je cloudron.io account e-mail {{ email }} is niet geverifieerd. Verifieer het om support tickets te kunnen openen."
|
||||
},
|
||||
"remoteSupport": {
|
||||
"title": "Ondersteuning op afstand",
|
||||
@@ -1390,10 +1349,6 @@
|
||||
"disableAction": "SSH ondersteuningstoegang uitschakelen",
|
||||
"enableAction": "SSH ondersteuningstoegang inschakelen",
|
||||
"description": "Met het inschakelen van deze optie geeft je ondersteuningsmedewerkers toegang tot deze server middels SSH."
|
||||
},
|
||||
"help": {
|
||||
"title": "Hulp",
|
||||
"description": "Om problemen op te lossen met Cloudron hebben we verschillende bronnen:\n* [Kennisbank & App Docs]({{ docsLink }})\n* [Eigen App Packaging & API]({{ packagingLink }})\n* [Forum]({{ forumLink }})"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
@@ -1418,19 +1373,7 @@
|
||||
"graphTitle": "Percentage",
|
||||
"graphSubtext": "Alleen apps die meer dan {{ threshold }} van de CPU gebruiken worden getoond"
|
||||
},
|
||||
"selectPeriodLabel": "Selecteer periode",
|
||||
"info": {
|
||||
"title": "Info",
|
||||
"vendor": "Leverancier",
|
||||
"memory": "Geheugen",
|
||||
"uptime": "Uptime",
|
||||
"activationTime": "Cloudron installatie tijd",
|
||||
"platformVersion": "Platform Versie",
|
||||
"product": "Product"
|
||||
},
|
||||
"graphs": {
|
||||
"title": "Grafieken"
|
||||
}
|
||||
"selectPeriodLabel": "Selecteer periode"
|
||||
},
|
||||
"eventlog": {
|
||||
"title": "Logboek",
|
||||
@@ -1450,9 +1393,7 @@
|
||||
"logs": {
|
||||
"title": "Logbestanden",
|
||||
"clear": "Leegmaken",
|
||||
"download": "Download volledige logbestanden",
|
||||
"notFoundError": "Geen taak of app gevonden",
|
||||
"logsGoneError": "Log bestand(en) niet gevonden"
|
||||
"download": "Download volledige logbestanden"
|
||||
},
|
||||
"terminal": {
|
||||
"title": "Terminal",
|
||||
@@ -1550,8 +1491,7 @@
|
||||
"paste": "Plakken",
|
||||
"copy": "Kopiëren",
|
||||
"cut": "Knippen",
|
||||
"edit": "Bewerk",
|
||||
"open": "Open"
|
||||
"edit": "Bewerk"
|
||||
},
|
||||
"mtime": "Bewerkt"
|
||||
},
|
||||
@@ -1566,25 +1506,13 @@
|
||||
},
|
||||
"newDirectory": {
|
||||
"errorAlreadyExists": "Bestaat al"
|
||||
},
|
||||
"uploader": {
|
||||
"exitWarning": "Uploaden nog bezig. Weet je zeker dat je deze pagina wilt sluiten?",
|
||||
"uploading": "Uploaden"
|
||||
},
|
||||
"extractionInProgress": "Bezig met uitpakken",
|
||||
"textEditor": {
|
||||
"undo": "Ongedaan maken",
|
||||
"redo": "Opnieuw doen",
|
||||
"save": "Opslaan"
|
||||
},
|
||||
"pasteInProgress": "Bezig met plakken",
|
||||
"deleteInProgress": "Bezig met verwijderen"
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"backAction": "Terug naar e-mail",
|
||||
"config": {
|
||||
"title": "E-mailconfiguratie {{ domain }}",
|
||||
"clientConfiguration": "Configureren E-mail programma's"
|
||||
"clientConfiguration": "Configureren E-mail clients"
|
||||
},
|
||||
"incoming": {
|
||||
"disableAction": "Uitschakelen",
|
||||
@@ -1630,7 +1558,7 @@
|
||||
"incomingPasswordUsage": "Wachtwoord van de eigenaar van de mailbox",
|
||||
"enabled": "Cloudron e-mailserver is geconfigureerd voor inkomende e-mails voor dit domein.",
|
||||
"disabled": "Cloudron e-mailserver ontvangt geen inkomende e-mails voor dit domein.",
|
||||
"howToConnectDescription": "Gebruik onderstaande gegevens om e-mail programma's in te stellen."
|
||||
"howToConnectDescription": "Gebruik onderstaande gegevens om e-mail clients in te stellen."
|
||||
},
|
||||
"outbound": {
|
||||
"tabTitle": "Uitgaand",
|
||||
@@ -1757,7 +1685,7 @@
|
||||
"updateMailinglistDialog": {
|
||||
"activeCheckbox": "Mailing-lijst is actief"
|
||||
},
|
||||
"howToConnectInfoModal": "Configureren e-mail programma's",
|
||||
"howToConnectInfoModal": "Configureren e-mail clients",
|
||||
"mailboxImportDialog": {
|
||||
"title": "Importeer Mailboxen",
|
||||
"description": "Upload een JSON of CSV bestand met een schema zoals beschreven in onze <a href=\"{{ docsLink }}\" target=\"_blank\">documentatie</a>.",
|
||||
@@ -1775,9 +1703,7 @@
|
||||
"password": "Wachtwoord",
|
||||
"resetPasswordAction": "Herstel wachtwoord",
|
||||
"2faToken": "2FA Token (indien ingeschakeld)",
|
||||
"errorIncorrectCredentials": "Onjuiste gebruikersnaam of wachtwoord",
|
||||
"errorIncorrect2FAToken": "2FA token is niet geldig",
|
||||
"errorInternal": "Interne fout, probeer later opnieuw"
|
||||
"errorIncorrectCredentials": "Onjuiste gebruikersnaam of wachtwoord"
|
||||
},
|
||||
"passwordReset": {
|
||||
"title": "Wachtwoord herstellen",
|
||||
@@ -1837,11 +1763,7 @@
|
||||
"mountStatus": "Koppel status",
|
||||
"localDirectory": "Lokale map",
|
||||
"type": "Type",
|
||||
"remountActionTooltip": "Her-koppel Volume",
|
||||
"editVolumeDialog": {
|
||||
"title": "Bewerk volume {{ name }}"
|
||||
},
|
||||
"editActionTooltip": "Bewerk Volume"
|
||||
"remountActionTooltip": "Her-koppel Volume"
|
||||
},
|
||||
"lang": {
|
||||
"it": "Italiaans",
|
||||
@@ -1915,43 +1837,5 @@
|
||||
"mounts": {
|
||||
"description": "Apps kunnen toegang krijgen tot <a href=\"/#/volumes\">volumes</a> via <code>/media/{volume name}</code> directory. Deze data is niet opgenomen in de app backup."
|
||||
}
|
||||
},
|
||||
"oidc": {
|
||||
"newClientDialog": {
|
||||
"title": "Client toevoegen",
|
||||
"description": "Nieuwe OpenID Connect client instellingen toevoegen.",
|
||||
"createAction": "Aanmaken"
|
||||
},
|
||||
"client": {
|
||||
"name": "Naam",
|
||||
"id": "Client ID",
|
||||
"secret": "Client geheim",
|
||||
"signingAlgorithm": "Ondertekeningsalgoritme",
|
||||
"loginRedirectUri": "Login callback URL (met komma gescheiden indien meer dan één)",
|
||||
"logoutRedirectUri": "Logout callback URL (optioneel)"
|
||||
},
|
||||
"title": "OpenID Connect aanbieder",
|
||||
"description": "Cloudron kan als een OpenID Connect aanbieder voor interne apps en externe diensten fungeren.",
|
||||
"editClientDialog": {
|
||||
"title": "Bewerk Client {{ client }}"
|
||||
},
|
||||
"deleteClientDialog": {
|
||||
"title": "Weet je zeker dat je Client {{ client }} wilt verwijderen?",
|
||||
"description": "Hiermee worden alle externe OpenID apps met dit Client ID losgekoppeld."
|
||||
},
|
||||
"env": {
|
||||
"discoveryUrl": "Discovery URL",
|
||||
"logoutUrl": "Logout URL",
|
||||
"profileEndpoint": "Profiel Eindpunt",
|
||||
"keysEndpoint": "Sleutels Eindpunt",
|
||||
"tokenEndpoint": "Token Eindpunt",
|
||||
"authEndpoint": "Auth Eindpunt"
|
||||
},
|
||||
"clients": {
|
||||
"title": "Clients",
|
||||
"newClient": "Nieuwe Client",
|
||||
"empty": "Nog geen Clients"
|
||||
}
|
||||
},
|
||||
"automation": "Automatisering"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
"auth": {
|
||||
"sso": "Войдите, используя учётную запись Cloudron",
|
||||
"email": "Войдите, используя email",
|
||||
"nosso": "Войдите, используя Вашу учётную запись",
|
||||
"openid": "Войти с помощью Cloudron OpenID"
|
||||
"nosso": "Войдите, используя Вашу учётную запись"
|
||||
},
|
||||
"noAccess": {
|
||||
"description": "После открытия доступа приложения отобразятся здесь.",
|
||||
@@ -57,8 +56,7 @@
|
||||
"save": "Сохранить",
|
||||
"close": "Закрыть",
|
||||
"no": "Нет",
|
||||
"yes": "Да",
|
||||
"delete": "Удалить"
|
||||
"yes": "Да"
|
||||
},
|
||||
"username": "Имя пользователя",
|
||||
"displayName": "Отображаемое имя",
|
||||
@@ -74,8 +72,7 @@
|
||||
},
|
||||
"action": {
|
||||
"reboot": "Перезагрузка",
|
||||
"logs": "Логи",
|
||||
"showLogs": "Показать логи"
|
||||
"logs": "Логи"
|
||||
},
|
||||
"searchPlaceholder": "Поиск",
|
||||
"multiselect": {
|
||||
@@ -90,9 +87,7 @@
|
||||
"enableAction": "Включить",
|
||||
"statusEnabled": "Включено",
|
||||
"statusDisabled": "Выключено",
|
||||
"loadingPlaceholder": "Загрузка",
|
||||
"settings": "Настройки",
|
||||
"saveAction": "Сохранить"
|
||||
"loadingPlaceholder": "Загрузка"
|
||||
},
|
||||
"appstore": {
|
||||
"category": {
|
||||
@@ -209,7 +204,7 @@
|
||||
"externalLdapTooltip": "Из внешнего LDAP каталога"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Настройки пользователя",
|
||||
"title": "Настройки",
|
||||
"allowProfileEditCheckbox": "Разрешить пользователям редактировать своё имя и адрес электронной почты",
|
||||
"require2FACheckbox": "Требовать от пользователей настройки 2FA",
|
||||
"subscriptionRequired": "Данные функции доступны только в платной подписке.",
|
||||
@@ -225,7 +220,7 @@
|
||||
"subscriptionRequired": "Данная функция доступна только в платной подписке.",
|
||||
"subscriptionRequiredAction": "Настроить подписку сейчас",
|
||||
"noopInfo": "LDAP аутентификация не настроена.",
|
||||
"provider": "Поставщик",
|
||||
"provider": "Источник",
|
||||
"server": "URL сервера",
|
||||
"acceptSelfSignedCert": "Принимать самоподписанный сертификат",
|
||||
"baseDn": "Корневой элемент",
|
||||
@@ -235,7 +230,7 @@
|
||||
"groupBaseDn": "Групповой корневой элемент",
|
||||
"groupFilter": "Фильтр группы",
|
||||
"groupnameField": "Поле с именем группы",
|
||||
"auth": "Авторизоваться",
|
||||
"auth": "Войти",
|
||||
"autocreateUsersOnLogin": "Автоматически создавать пользователей после их входа в Cloudron",
|
||||
"showLogsAction": "Показать логи",
|
||||
"syncAction": "Синхронизировать",
|
||||
@@ -264,10 +259,10 @@
|
||||
"errorEmailRequired": "Требуется адрес электронной почты",
|
||||
"errorInvalidUsername": "Неверное имя пользователя",
|
||||
"usernamePlaceholder": "Необязательно. Если не указано, пользователь может выбрать во время регистрации",
|
||||
"displayName": "Отображаемое имя",
|
||||
"displayName": "Показать имя",
|
||||
"email": "Электронная почта",
|
||||
"primaryEmail": "Основной адрес электронной почты",
|
||||
"recoveryEmail": "Электронная почта для восстановления пароля",
|
||||
"recoveryEmail": "Пароль восстановления электронной почты",
|
||||
"errorDisplayNameRequired": "Требуется имя",
|
||||
"activeCheckbox": "Пользователь активен",
|
||||
"fallbackEmailPlaceholder": "Необязательно. Если не указано, будет использоваться основной почтовый ящик",
|
||||
@@ -295,7 +290,7 @@
|
||||
"description": "Ссылка для сброса пароля отправлена на электронную почту {{ email }}:",
|
||||
"sendEmailLinkAction": "Отправить ссылку пользователю по электронной почте",
|
||||
"emailSent": "Отправлено",
|
||||
"newLinkAction": "Отправить ссылку для сброса",
|
||||
"newLinkAction": "Отправить ссылку для сброса пароля",
|
||||
"reset2FAAction": "Сбросить 2FA",
|
||||
"sendAction": "Отправить письмо",
|
||||
"descriptionLink": "Скопировать ссылку для сброса пароля",
|
||||
@@ -410,7 +405,7 @@
|
||||
"changePassword": {
|
||||
"currentPassword": "Текущий пароль",
|
||||
"errorPasswordInvalid": "Пароль должен быть не менее 8 и не более 265 символов",
|
||||
"title": "Изменить ваш пароль",
|
||||
"title": "Изменить пароль",
|
||||
"newPassword": "Новый пароль",
|
||||
"newPasswordRepeat": "Повторите новый пароль",
|
||||
"errorPasswordRequired": "Требуется пароль",
|
||||
@@ -508,8 +503,7 @@
|
||||
},
|
||||
"changeBackgroundImage": {
|
||||
"title": "Установить фоновое изображение"
|
||||
},
|
||||
"enable2FANotAvailable": "Недоступно для пользователей из удалённых источников"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"uninstallDialog": {
|
||||
@@ -646,8 +640,7 @@
|
||||
"moveAction": "Переместить данные",
|
||||
"dataDirPlaceholder": "Оставьте пустым, чтобы сохранить настройку по умолчанию",
|
||||
"description": "Если на диске заканчивается место, вы можете перенести данные приложения в <a href=\"/#/volumes\">том</a>. Любые данные по этому пути станут частью резервной копии приложения.",
|
||||
"diskUsage": "Приложение использует {{ size }} хранилища (по состоянию на {{ date }}).",
|
||||
"mountTypeWarning": "Чтобы перемещение прошло успешно конечная файловая система должна поддерживать разрешения и права доступа к файлам"
|
||||
"diskUsage": "Приложение использует {{ size }} хранилища (по состоянию на {{ date }})."
|
||||
},
|
||||
"mounts": {
|
||||
"volume": "Том",
|
||||
@@ -840,17 +833,6 @@
|
||||
"label": "Метка",
|
||||
"clearIconAction": "Очистить иконку",
|
||||
"clearIconDescription": "Это действие попытается загрузить favicon после сохранения."
|
||||
},
|
||||
"servicesTabTitle": "Службы",
|
||||
"turn": {
|
||||
"title": "Настроить TURN",
|
||||
"enable": "Настроить использование встроенного TURN сервера в приложении",
|
||||
"disable": "Не настраивать TURN сервер для данного приложения. Вы можете настроить его самостоятельно внутри самого приложения."
|
||||
},
|
||||
"redis": {
|
||||
"title": "Настроить Redis",
|
||||
"enable": "Настроить использование Redis в приложении",
|
||||
"disable": "Отключить Redis"
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
@@ -866,7 +848,7 @@
|
||||
"remount": "Перемонтировать хранилище"
|
||||
},
|
||||
"listing": {
|
||||
"stopTask": "Остановить Backup",
|
||||
"stopTask": "Остановить {{ taskType === 'backup' ? 'Backup' : 'Cleanup' }}",
|
||||
"title": "Список резервных копий",
|
||||
"noBackups": "Ещё не было создано ни одной резервной копии.",
|
||||
"version": "Версия",
|
||||
@@ -977,8 +959,7 @@
|
||||
"preserved": {
|
||||
"description": "Хранить резервную копию, игнорируя политику хранения",
|
||||
"tooltip": "Также будет сохранена почта и {{ appsLength } резервных копий."
|
||||
},
|
||||
"remotePath": "Удаленный путь"
|
||||
}
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
@@ -1020,8 +1001,7 @@
|
||||
"acl": "Почтовый ACL (Access Control List)",
|
||||
"maxMailSize": "Максимальный размер письма",
|
||||
"solrFts": "Полный поиск по тексту (Solr)",
|
||||
"aclOverview": "{{ dnsblZonesCount }} DNSBL зон",
|
||||
"virtualAllMail": "Папка \"Вся почта\""
|
||||
"aclOverview": "{{ dnsblZonesCount }} DNSBL зон"
|
||||
},
|
||||
"eventlog": {
|
||||
"title": "Журнал событий электронной почты",
|
||||
@@ -1055,7 +1035,7 @@
|
||||
},
|
||||
"changeDomainDialog": {
|
||||
"title": "Изменить расположение сервера электронной почты",
|
||||
"description": "Данное действие перенесёт IMAP и SMTP сервер в указанное расположение.",
|
||||
"description": "Cloudron внесет необходимые изменения в DNS во всех доменах и перезапустит почтовый сервер. Настольные и мобильные почтовые клиенты должны быть повторно настроены для использования нового расположения сервера IMAP и SMTP.",
|
||||
"location": "Расположение",
|
||||
"locationPlaceholder": "Оставьте пустым, чтобы использовать основной домен",
|
||||
"manualInfo": "Вручную добавьте A запись для {{ domain }}, указав публичный IP Вашего Cloudron"
|
||||
@@ -1112,10 +1092,6 @@
|
||||
},
|
||||
"action": {
|
||||
"queue": "Очередь"
|
||||
},
|
||||
"changeVirtualAllMailDialog": {
|
||||
"title": "Папка \"Вся почта\"",
|
||||
"description": "Папка \"Вся почта\" содержит все электронные письма из вашего почтового ящика. Данная папка может быть полезна в том случае, когда ваш почтовый клиент не поддерживает рекурсивный поиск по папкам."
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
@@ -1142,8 +1118,7 @@
|
||||
},
|
||||
"dyndns": {
|
||||
"title": "Динамический DNS",
|
||||
"description": "Включите эту опцию, чтобы синхронизировать все ваши DNS-записи с изменяющимся IP-адресом. Это полезно, когда Cloudron работает в сети с часто меняющимся общедоступным IP-адресом, например, в домашних сетях.",
|
||||
"showLogsAction": "Показать логи"
|
||||
"description": "Включите эту опцию, чтобы синхронизировать все ваши DNS-записи с изменяющимся IP-адресом. Это полезно, когда Cloudron работает в сети с часто меняющимся общедоступным IP-адресом, например, в домашних сетях."
|
||||
},
|
||||
"configureIp": {
|
||||
"title": "Настроить источник IP",
|
||||
@@ -1159,13 +1134,7 @@
|
||||
},
|
||||
"configureIpv6": {
|
||||
"title": "Настройка IPv6"
|
||||
},
|
||||
"trustedIps": {
|
||||
"summary": "{{ trustCount }} IP доверены",
|
||||
"title": "Настроить доверенные IP",
|
||||
"description": "HTTP заголовки от совпадающих IP адресов будут доверены"
|
||||
},
|
||||
"trustedIpRanges": "Доверенные IP и диапазоны "
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"title": "Службы",
|
||||
@@ -1281,8 +1250,7 @@
|
||||
"emailInfo": "(Электронная почта подписки - {{ email }})",
|
||||
"subscriptionRequired": "Тикеты поддержки доступны только в платной подписке.",
|
||||
"subscriptionRequiredDescription": "Вы можете найти ответы на свои вопросы в нашей <a href=\"{{ supportViewLink }}\" target=\"_blank\">документации</a>или попросить помощи на <a href=\"{{ forumLink }}\" target=\"_blank\">форуме</a>.",
|
||||
"emailNotVerified": "Ваш адрес электронной почты {{ email }} в cloudron.io не подтверждён. Пожалуйста, подтвердите его для доступа к тикетам поддержки.",
|
||||
"typeBilling": "Проблема с выставлением счетов"
|
||||
"emailNotVerified": "Ваш адрес электронной почты {{ email }} в cloudron.io не подтверждён. Пожалуйста, подтвердите его для доступа к тикетам поддержки."
|
||||
},
|
||||
"remoteSupport": {
|
||||
"subscriptionRequired": "Удалённая поддержка доступна только в платной подписке.",
|
||||
@@ -1342,7 +1310,7 @@
|
||||
"changeAction": "Изменить домен",
|
||||
"cancelAction": "Отменить",
|
||||
"showLogsAction": "Показать логи",
|
||||
"description": "Данное действие переместит панель управления на <code>my</code> поддомен выбранного домена."
|
||||
"description": "Данное действие переместит панель управления и сервер электронной почты на <code>my</code> поддомен выбранного домена."
|
||||
},
|
||||
"subscriptionRequired": {
|
||||
"title": "Требуется подписка",
|
||||
@@ -1395,13 +1363,7 @@
|
||||
"hetznerToken": "Токен Hetzner",
|
||||
"cloudflareDefaultProxyStatus": "Активировать прокси для новых DNS записей",
|
||||
"porkbunApikey": "API Ключ",
|
||||
"porkbunSecretapikey": "Secret API Ключ",
|
||||
"bunnyAccessKey": "Ключ доступа Bunny",
|
||||
"dnsimpleAccessToken": "Токен доступа",
|
||||
"ovhEndpoint": "Конечная точка",
|
||||
"ovhConsumerKey": "Ключ пользователя",
|
||||
"ovhAppKey": "Ключ приложения",
|
||||
"ovhAppSecret": "Секрет приложения"
|
||||
"porkbunSecretapikey": "Secret API Ключ"
|
||||
},
|
||||
"addDomain": "Добавить домен",
|
||||
"removeDialog": {
|
||||
@@ -1418,8 +1380,7 @@
|
||||
"domainWellKnown": {
|
||||
"title": "Общеизвестные расположения {{ domain }}"
|
||||
},
|
||||
"tooltipWellKnown": "Установить общеизвестные расположения",
|
||||
"count": "Всего доменов: {{ count }}"
|
||||
"tooltipWellKnown": "Установить общеизвестные расположения"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Уведомления",
|
||||
@@ -1431,9 +1392,7 @@
|
||||
"logs": {
|
||||
"title": "Логи",
|
||||
"clear": "Очистить обзор",
|
||||
"download": "Скачать полные логи",
|
||||
"notFoundError": "Задача или приложение не существует",
|
||||
"logsGoneError": "Файл(ы) журнала не найден(ы)"
|
||||
"download": "Скачать полные логи"
|
||||
},
|
||||
"terminal": {
|
||||
"title": "Терминал",
|
||||
@@ -1518,8 +1477,7 @@
|
||||
"cut": "Вырезать",
|
||||
"paste": "Вставить",
|
||||
"selectAll": "Выбрать все",
|
||||
"copy": "Скопировать",
|
||||
"open": "Открыть"
|
||||
"copy": "Скопировать"
|
||||
},
|
||||
"symlink": "Символическая ссылка на {{ target }}",
|
||||
"mtime": "Изменён"
|
||||
@@ -1547,19 +1505,7 @@
|
||||
},
|
||||
"status": {
|
||||
"restartingApp": "перезапускаем приложение"
|
||||
},
|
||||
"extractionInProgress": "Идёт извлечение",
|
||||
"uploader": {
|
||||
"exitWarning": "Загрузка ещё не завершена. Вы уверены, что хотите закрыть страницу?",
|
||||
"uploading": "Загружаем"
|
||||
},
|
||||
"textEditor": {
|
||||
"undo": "Отменить операцию",
|
||||
"redo": "Повторить операцию",
|
||||
"save": "Сохранить"
|
||||
},
|
||||
"pasteInProgress": "Выполняется копирование / перемещение",
|
||||
"deleteInProgress": "Выполняется удаление"
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"outbound": {
|
||||
@@ -1756,9 +1702,7 @@
|
||||
"loginTo": "Войти в",
|
||||
"username": "Имя пользователя",
|
||||
"2faToken": "2FA Токен (если включен)",
|
||||
"errorIncorrectCredentials": "Неправильное имя пользователя или пароль",
|
||||
"errorIncorrect2FAToken": "Неверный 2FA токен",
|
||||
"errorInternal": "Внутренняя ошибка, попробуйте позже"
|
||||
"errorIncorrectCredentials": "Неправильное имя пользователя или пароль"
|
||||
},
|
||||
"passwordReset": {
|
||||
"title": "Сброс пароля",
|
||||
@@ -1818,11 +1762,7 @@
|
||||
"title": "Тома",
|
||||
"hostPath": "Назначение",
|
||||
"description": "Тома - локальные или удаленные файловые системы. Они могут быть использованы для хранения данных приложений или для создания общей директории для нескольких приложений.",
|
||||
"localDirectory": "Локальный каталог",
|
||||
"editVolumeDialog": {
|
||||
"title": "Редактирование тома {{ name }}"
|
||||
},
|
||||
"editActionTooltip": "Редактировать том"
|
||||
"localDirectory": "Локальный каталог"
|
||||
},
|
||||
"lang": {
|
||||
"en": "Английский",
|
||||
@@ -1836,8 +1776,7 @@
|
||||
"zh_Hans": "Китайский (Упрощенный)",
|
||||
"es": "Испанский",
|
||||
"ru": "Русский",
|
||||
"pt": "Португальский",
|
||||
"da": "Датский"
|
||||
"pt": "Португальский"
|
||||
},
|
||||
"setupAccount": {
|
||||
"username": "Имя пользователя",
|
||||
@@ -1896,43 +1835,5 @@
|
||||
"mounts": {
|
||||
"description": "Приложения могут получить доступ к смонтированным <a href=\"/#/volumes\">томам</a> по пути <code>/media/{имя тома}</code>. Данные таких томов не будут включаться в резервные копии приложения."
|
||||
}
|
||||
},
|
||||
"oidc": {
|
||||
"newClientDialog": {
|
||||
"createAction": "Создать",
|
||||
"title": "Добавить клиента",
|
||||
"description": "Добавить настройки нового клиента OpenID connect."
|
||||
},
|
||||
"client": {
|
||||
"name": "Имя",
|
||||
"id": "ID Клиента",
|
||||
"secret": "Секрет",
|
||||
"signingAlgorithm": "Метод подписи",
|
||||
"loginRedirectUri": "URL обратного вызова (если больше одного, отделите их запятой)",
|
||||
"logoutRedirectUri": "URL обратного вызова для выхода из системы (необязательно)"
|
||||
},
|
||||
"clients": {
|
||||
"title": "Клиенты",
|
||||
"newClient": "Новый клиент",
|
||||
"empty": "Клиенты не найдены"
|
||||
},
|
||||
"title": "Поставщик OpenID Сonnect",
|
||||
"description": "Cloudron может выступать в качестве поставщика OpenID connect для внутренних приложений и внешних сервисов.",
|
||||
"editClientDialog": {
|
||||
"title": "Редактировать клиента {{ client }}"
|
||||
},
|
||||
"deleteClientDialog": {
|
||||
"title": "Вы точно хотите удалить клиента {{ client }}?",
|
||||
"description": "Это действие отключит все внешние OpenID приложения, использующие данный клиент ID, от Cloudron."
|
||||
},
|
||||
"env": {
|
||||
"discoveryUrl": "URL обнаружения",
|
||||
"logoutUrl": "URL выхода из системы",
|
||||
"profileEndpoint": "Конечная точка профиля",
|
||||
"keysEndpoint": "Конечная точка ключей",
|
||||
"tokenEndpoint": "Конечная точка токена",
|
||||
"authEndpoint": "Конечная точка аутентификации"
|
||||
}
|
||||
},
|
||||
"automation": "Автоматизация"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,18 +18,12 @@
|
||||
"title": "Chưa có app cài đặt!",
|
||||
"description": "Cài đặt một vài app nhé? Hãy xem trong <a href=\"{{ appStoreLink }}\">Cửa hàng App</a>"
|
||||
},
|
||||
"groupsFilterHeader": "Tất cả Nhóm",
|
||||
"groupsFilterHeader": "Chọn nhóm",
|
||||
"auth": {
|
||||
"email": "Đăng nhập bằng email",
|
||||
"sso": "Đăng nhập với tên & mật khẩu trên Cloudron",
|
||||
"nosso": "Đăng nhập vào tài khoản riêng"
|
||||
},
|
||||
"addAppAction": "Thêm App",
|
||||
"addApplinkAction": "Thêm đường link App",
|
||||
"filter": {
|
||||
"clearAll": "Xoá tất cả"
|
||||
},
|
||||
"addAppproxyAction": "Thêm proxy cho app"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"logout": "Thoát",
|
||||
@@ -38,8 +32,7 @@
|
||||
"save": "Lưu",
|
||||
"close": "Đóng",
|
||||
"no": "Không",
|
||||
"yes": "Có",
|
||||
"delete": "Xoá"
|
||||
"yes": "Có"
|
||||
},
|
||||
"username": "Tên đăng nhập",
|
||||
"displayName": "Tên hiển thị",
|
||||
@@ -49,8 +42,7 @@
|
||||
"pagination": {
|
||||
"prev": "trước",
|
||||
"next": "tiếp",
|
||||
"perPageSelector": "Hiển thị {{ n }} trên một trang",
|
||||
"itemCount": "Đã tìm thấy {{ count }}"
|
||||
"perPageSelector": "Hiển thị {{ n }} trên một trang"
|
||||
},
|
||||
"action": {
|
||||
"reboot": "Khởi động lại",
|
||||
@@ -87,9 +79,7 @@
|
||||
"users": "Người dùng"
|
||||
},
|
||||
"enableAction": "Bật",
|
||||
"disableAction": "Tắt",
|
||||
"loadingPlaceholder": "Đang tải",
|
||||
"settings": "Cài đặt"
|
||||
"disableAction": "Tắt"
|
||||
},
|
||||
"appstore": {
|
||||
"title": "Cửa hàng App",
|
||||
@@ -144,8 +134,7 @@
|
||||
"configuredForCloudronEmail": "App này đã được cấu hình sẵn để sử dụng với <a href=\"{{ emailDocsLink }}\" target=\"_blank\">Cloudron Email</a>.",
|
||||
"doInstallAction": "Tải về {{ dnsOverwrite ? 'and overwrite DNS' : '' }}",
|
||||
"cloudflarePortWarning": "Cần tắt proxy Cloudflare để tên miền app này có thể truy cập được vào cổng",
|
||||
"titleAndVersion": "App này đóng gói phần mềm {{ title }} {{ version }}",
|
||||
"portReadOnly": "chỉ-đọc"
|
||||
"titleAndVersion": "App này đóng gói phần mềm {{ title }} {{ version }}"
|
||||
},
|
||||
"appNotFoundDialog": {
|
||||
"title": "Không tìm thấy app",
|
||||
@@ -267,7 +256,7 @@
|
||||
"subscriptionRequired": "Chức năng này chỉ có trong gói trả phí.",
|
||||
"require2FACheckbox": "Yêu cầu người dùng cài đặt Mã xác minh 2 bước",
|
||||
"allowProfileEditCheckbox": "Cho phép người dùng chỉnh sửa tên và email",
|
||||
"title": "Cài đặt Người dùng",
|
||||
"title": "Cài đặt",
|
||||
"require2FAWarning": "Hãy cài đặt Mã xác minh 2 Bước cho tài khoản của bạn trước đề phòng bị khoá ra khỏi TK."
|
||||
},
|
||||
"groups": {
|
||||
@@ -339,9 +328,8 @@
|
||||
"label": "Giới hạn quyền truy cập"
|
||||
},
|
||||
"secret": {
|
||||
"label": "Mật khẩu bind",
|
||||
"description": "Tất cả những yêu cầu LDAP cần phải được xác minh với mã bí mật này và tên người dùng user DN <i>{{ userDN }}</i>",
|
||||
"url": "URL máy chủ"
|
||||
"label": "Mã bí mật",
|
||||
"description": "Tất cả những yêu cầu LDAP cần phải được xác minh với mã bí mật này và tên người dùng user DN <i>{{ userDN }}</i>"
|
||||
}
|
||||
},
|
||||
"userImportDialog": {
|
||||
@@ -447,8 +435,7 @@
|
||||
"description": "Mã API mới:",
|
||||
"copyNow": "Xin copy mã API này bây giờ. Nó sẽ không được hiển thị lại vì lý do an ninh.",
|
||||
"generateToken": "Tạo mã API",
|
||||
"name": "Tên cho mã API",
|
||||
"access": "Truy cập API"
|
||||
"name": "Tên cho mã API"
|
||||
},
|
||||
"enable2FAAction": "Bật xác minh hai bước",
|
||||
"primaryEmail": "Email chính",
|
||||
@@ -471,10 +458,7 @@
|
||||
"name": "Tên",
|
||||
"expiresAt": "Hết hiệu lực vào",
|
||||
"lastUsed": "Lần dùng cuối",
|
||||
"neverUsed": "chưa từng dùng",
|
||||
"readonly": "Chỉ đọc",
|
||||
"scope": "Mức độ bao phủ",
|
||||
"readwrite": "Đọc và Ghi"
|
||||
"neverUsed": "chưa từng dùng"
|
||||
},
|
||||
"loginTokens": {
|
||||
"title": "Mã đăng nhập",
|
||||
@@ -556,7 +540,7 @@
|
||||
"mountPoint": "Điểm mount",
|
||||
"noopNote": "Lựa chọn này sẽ làm hỏng tính năng sao lưu và khôi phục của Cloudron và chỉ nên dùng khi test hệ thống. Xin đảm bảo rằng server được sao lưu toàn bộ bằng những phương tiện khác.",
|
||||
"format": "Định dạng lưu trữ",
|
||||
"encryptedFilenames": "Tên tập tin đã mã hoá",
|
||||
"encryptedFilenames": "Mã hoá tên tập tin",
|
||||
"chown": "Hệ thống tập tin bên ngoài có hỗ trợ chown",
|
||||
"username": "Tên đăng nhập",
|
||||
"server": "IP hoặc hostname máy chủ",
|
||||
@@ -568,8 +552,7 @@
|
||||
"user": "Người dùng",
|
||||
"privateKey": "Mật mã riêng",
|
||||
"diskPath": "Đường dẫn đến ổ đĩa",
|
||||
"cifsSealSupport": "Dùng mã hoá SEAL. Cần SMB thấp nhất là phiên bản v3",
|
||||
"encryptFilenames": "Mã hoá tên tập tin"
|
||||
"cifsSealSupport": "Dùng mã hoá SEAL. Cần SMB thấp nhất là phiên bản v3"
|
||||
},
|
||||
"cleanupBackups": {
|
||||
"description": "Các bản sao lưu được dọn sạch tự động dựa trên thời gian lưu giữ. Thao tác này sẽ xoá ngay lập tức các bản sao lưu đang có.",
|
||||
@@ -601,7 +584,7 @@
|
||||
"title": "Log"
|
||||
},
|
||||
"listing": {
|
||||
"stopTask": "Dừng Backup",
|
||||
"stopTask": "Dừng {{ taskType === 'backup' ? 'Backup' : 'Cleanup' }}",
|
||||
"backupNow": "Sao lưu ngay bây giờ",
|
||||
"cleanupBackups": "Dọn sạch bản sao lưu",
|
||||
"tooltipDownloadBackupConfig": "Tải xuống cấu hình bản sao lưu",
|
||||
@@ -896,13 +879,7 @@
|
||||
},
|
||||
"configureIpv6": {
|
||||
"title": "Cài đặt nhà cung cấp IPv6"
|
||||
},
|
||||
"trustedIps": {
|
||||
"summary": "{{ trustCount }} địa chỉ IP được tin tưởng",
|
||||
"description": "Những HTTP header từ những địa chỉ IP trùng khớp sẽ được chấp thuận cho qua",
|
||||
"title": "Thiết lập những địa chỉ IP đáng tin cậy"
|
||||
},
|
||||
"trustedIpRanges": "Địa chỉ IP & Vùng được tin cậy "
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"typeFilterHeader": "Tất cả sự kiện",
|
||||
@@ -937,7 +914,7 @@
|
||||
"locationPlaceholder": "Để trống để dùng tên miền gốc",
|
||||
"location": "Vị trí",
|
||||
"title": "Thay đổi vị trí đặt mail server",
|
||||
"description": "Hành động này sẽ di chuyển server IMAP và SMTP đến vị trí được xác định."
|
||||
"description": "Cloudron sẽ thay đổi những giá trị DNS cần thiết cho tất cả tên miền và khởi động lại mail server. Những client nhận mail trên máy tính hay điện thoại cần được cài đặt lại để sử dụng vị trí mới này làm IMAP và SMTP server."
|
||||
},
|
||||
"eventlog": {
|
||||
"searchPlaceholder": "Tìm kiếm",
|
||||
@@ -956,10 +933,7 @@
|
||||
"queued": "Xếp hàng",
|
||||
"outgoing": "Gửi mail ra",
|
||||
"incoming": "Nhận mail vào",
|
||||
"deferred": "Trì hoãn lại",
|
||||
"overQuotaInfo": "Hộp thư {{ mailbox }} đã đầy {{ quotaPercent }}%",
|
||||
"underQuotaInfo": "Hộp thư {{ mailbox }} đã rơi xuống còn {{ quotaPercent }}% của hạn mức",
|
||||
"quota": "Hạn mức hộp thư"
|
||||
"deferred": "Trì hoãn lại"
|
||||
},
|
||||
"empty": "Log sự kiện hiện đang trống.",
|
||||
"details": "Chi tiết",
|
||||
@@ -976,8 +950,8 @@
|
||||
"solrEnabled": "Đã bật",
|
||||
"solrDisabled": "Đã tắt",
|
||||
"changeDomainProgress": "Thay đổi tên miền email:",
|
||||
"spamFilterOverview": "{{ blacklistCount }} email có trong danh sách bị chặn.",
|
||||
"location": "Nơi đặt máy chủ mail",
|
||||
"spamFilterOverview": "{{ blacklistCount }} email có trong danh sách đen.",
|
||||
"location": "Nơi đặt mail server",
|
||||
"spamFilter": "Lọc spam",
|
||||
"maxMailSize": "Kích cỡ mail tối đa",
|
||||
"info": "Các cài đặt này áp dụng cho tất cả các tên miền.",
|
||||
@@ -1007,19 +981,6 @@
|
||||
"dnsblZonesInfo": "Địa chỉ IP đang muốn kết nối đến được dò tìm trong những danh sách IP bị chặn này",
|
||||
"dnsblZonesPlaceholder": "Tên vùng (ghi xuống dòng)",
|
||||
"title": "Đổi danh sách quản lý truy cập mail"
|
||||
},
|
||||
"queue": {
|
||||
"empty": "Danh sách mail chờ đang trống",
|
||||
"title": "Danh sách mail chờ gửi",
|
||||
"rcptTo": "Gửi cho",
|
||||
"mailFrom": "Đến từ",
|
||||
"details": "Chi tiết",
|
||||
"discardTooltip": "Bỏ qua",
|
||||
"queueTime": "Thời gian chờ",
|
||||
"resendTooltip": "Gửi lại ngay"
|
||||
},
|
||||
"action": {
|
||||
"queue": "Cho vào hàng chờ gửi sau"
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
@@ -1048,11 +1009,10 @@
|
||||
"selectPeriodLabel": "Chọn khoảng thời gian",
|
||||
"cpuUsage": {
|
||||
"graphTitle": "Phần trăm sử dụng",
|
||||
"title": "Dung lượng CPU",
|
||||
"graphSubtext": "Chỉ những app sử dụng hơn {{ threshold }} cpu mới được hiển thị"
|
||||
"title": "Dung lượng CPU"
|
||||
},
|
||||
"systemMemory": {
|
||||
"graphSubtext": "Chỉ những app sử dụng hơn {{ threshold }} bộ nhớ mới được hiển thị",
|
||||
"graphSubtext": "Các giá trị bộ nhớ riêng từng app không hiển thị chồng lên nhau",
|
||||
"title": "Bộ nhớ hệ thống"
|
||||
},
|
||||
"diskUsage": {
|
||||
@@ -1060,11 +1020,7 @@
|
||||
"diskContent": "Ổ đĩa {{ type }} này hiện chứa",
|
||||
"usageInfo": "Còn {{ available | prettyDiskSize }}</b> trống trong tổng <b>{{ size | prettyDiskSize }}</b>",
|
||||
"mountedAt": "{{ filesystem }} <small>được gắn ở</small> {{ mountpoint }}",
|
||||
"title": "Dung lượng ổ đĩa",
|
||||
"usedInfo": "{{ used }} đã dùng trong tổng {{ size }}",
|
||||
"volumeContent": "Ổ đĩa này thuộc volume <code>{{ name }}</code>",
|
||||
"uninstalledApp": "App đã xoá",
|
||||
"diskSpeed": "Tốc độ: {{ speed }} MB/s"
|
||||
"title": "Dung lượng ổ đĩa"
|
||||
},
|
||||
"title": "Hệ thống"
|
||||
},
|
||||
@@ -1309,9 +1265,7 @@
|
||||
"logs": {
|
||||
"download": "Tải xuống tất cả log",
|
||||
"clear": "Làm sạch phần xem log",
|
||||
"title": "Log",
|
||||
"notFoundError": "Không có tác vụ hay app đó",
|
||||
"logsGoneError": "Tập tin log không được tìm thấy"
|
||||
"title": "Log"
|
||||
},
|
||||
"notifications": {
|
||||
"clearAll": "Xoá hết",
|
||||
@@ -1369,11 +1323,7 @@
|
||||
"wellKnownDescription": "Những giá trị nhập vào này sẽ được dùng bởi Cloudron để phản hồi về những đường link <code>/.well-known/</code>. Lưu ý rằng một app cần được đang chạy cài đặt sẵn trên tên miền gốc <code>{{ domain }}</code> để tính năng này có thể hoạt động được. Xem phần <a href=\"{{docsLink}}\" target=\"_blank\">hướng dẫn sử dụng</a> để biết thêm thông tin.",
|
||||
"vultrToken": "Mật mã Vultr",
|
||||
"jitsiHostname": "Vị trí Jitsi",
|
||||
"hetznerToken": "Mật mã Hetzner",
|
||||
"cloudflareDefaultProxyStatus": "Bật tính năng proxy cho những bản ghi DNS mới",
|
||||
"porkbunSecretapikey": "Mã bí mật API",
|
||||
"bunnyAccessKey": "Mã truy cập Bunny",
|
||||
"porkbunApikey": "Key API"
|
||||
"hetznerToken": "Mật mã Hetzner"
|
||||
},
|
||||
"subscriptionRequired": {
|
||||
"description": "Để thêm tên miền, hãy đăng ký gói trả phí.",
|
||||
@@ -1408,8 +1358,7 @@
|
||||
"domainWellKnown": {
|
||||
"title": "Những vị trí Well-Known của {{ domain }}"
|
||||
},
|
||||
"tooltipWellKnown": "Cài đặt những vị trí Well-Known",
|
||||
"count": "Tổng số tên miền: {{ count }}"
|
||||
"tooltipWellKnown": "Cài đặt những vị trí Well-Known"
|
||||
},
|
||||
"app": {
|
||||
"appInfo": {
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
"appCount": "{{ appCount }} 个应用",
|
||||
"cleanupBackups": "清理备份",
|
||||
"backupNow": "现在备份",
|
||||
"stopTask": "停止 备份",
|
||||
"stopTask": "停止 {{ taskType === 'backup' ? '备份' : '清理' }}",
|
||||
"tooltipDownloadBackupConfig": "下载备份配置",
|
||||
"tooltipPreservedBackup": "该备份将会被保留",
|
||||
"tooltipEditBackup": "编辑备份"
|
||||
|
||||
@@ -69,46 +69,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal backup details -->
|
||||
<div class="modal fade" id="backupDetailsModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" style="width: 750px">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.backupDetails.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-xs-1 text-muted">{{ 'backups.backupDetails.id' | tr }}:</div>
|
||||
<div class="col-xs-11 text-right">{{ backupDetails.backup.id }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupEdit.label' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">{{ backupDetails.backup.label }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupEdit.remotePath' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">{{ backupDetails.backup.remotePath }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupDetails.date' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">{{ backupDetails.backup.creationTime | prettyLongDate }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupDetails.version' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">v{{ backupDetails.backup.packageVersion }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupDetails.format' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">{{ backupDetails.backup.format }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal edit individual backup (label and retention sec) -->
|
||||
<div class="modal fade" id="editBackupModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
@@ -408,11 +368,6 @@
|
||||
<select class="form-control" name="region" id="inputimportBackupVultrRegion" ng-model="importBackup.endpoint" ng-options="a.value as a.name for a in vultrRegions" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'vultr-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.region }" ng-show="importBackup.provider === 'contabo-objectstorage'">
|
||||
<label class="control-label" for="inputimportBackupContaboRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputimportBackupContaboRegion" ng-model="importBackup.endpoint" ng-options="a.value as a.name for a in contaboRegions" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'contabo-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.accessKeyId }" ng-show="s3like(importBackup.provider)">
|
||||
<label class="control-label" for="inputImportBackupAccessKeyId">{{ 'backups.configureBackupStorage.s3AccessKeyId' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.accessKeyId" id="inputImportBackupAccessKeyId" name="accessKeyId" ng-disabled="importBackup.busy" ng-required="s3like(importBackup.provider)">
|
||||
@@ -626,9 +581,9 @@
|
||||
<i ng-hide="app.installationState === 'pending_start' || app.installationState === 'pending_stop'" class="fas" ng-class="{ 'fa-power-off': !uninstall.startButton, 'fa-play': uninstall.startButton }"></i>
|
||||
</button>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a class="btn btn-sm btn-default" ng-href="{{ '/frontend/logs.html?appId=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.logsActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-align-left"></i></a>
|
||||
<a class="btn btn-sm btn-default" ng-if="app.type !== APP_TYPES.PROXIED" ng-href="{{ '/frontend/terminal.html?id=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.terminalActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fa fa-terminal"></i></a>
|
||||
<a class="btn btn-sm btn-default" ng-if="app.manifest.addons.localstorage" ng-href="{{ '/frontend/filemanager.html#/home/app/' + app.id }}" target="_blank" uib-tooltip="{{ 'app.filemanagerActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-folder"></i></a>
|
||||
<a class="btn btn-sm btn-default" ng-href="{{ '/logs.html?appId=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.logsActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-align-left"></i></a>
|
||||
<a class="btn btn-sm btn-default" ng-if="app.type !== APP_TYPES.PROXIED" ng-href="{{ '/terminal.html?id=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.terminalActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fa fa-terminal"></i></a>
|
||||
<a class="btn btn-sm btn-default" ng-if="app.manifest.addons.localstorage" ng-href="{{ '/filemanager.html?type=app&id=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.filemanagerActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-folder"></i></a>
|
||||
</div>
|
||||
<div class="dropdown" style="display: inline-block">
|
||||
<button class="btn btn-sm btn-default dropdown-toggle" type="button" data-toggle="dropdown" uib-tooltip="{{ 'app.docsActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom">
|
||||
@@ -677,7 +632,6 @@
|
||||
<div ng-click="setView('proxy')" ng-class="{ 'active': view === 'proxy' }" ng-show="app.type === APP_TYPES.PROXIED">Proxy</div>
|
||||
<div ng-click="setView('access')" ng-class="{ 'active': view === 'access' }" ng-show="app.accessLevel === 'admin'">{{ 'app.accessControlTabTitle' | tr }}</div>
|
||||
<div ng-click="setView('resources')" ng-class="{ 'active': view === 'resources' }" ng-show="app.type !== APP_TYPES.PROXIED">{{ 'app.resourcesTabTitle' | tr }}</div>
|
||||
<div ng-click="setView('services')" ng-class="{ 'active': view === 'services' }" ng-show="app.type !== APP_TYPES.PROXIED && (app.manifest.addons.turn.optional || app.manifest.addons.redis.optional)">{{ 'app.servicesTabTitle' | tr }}</div>
|
||||
<div ng-click="setView('storage')" ng-class="{ 'active': view === 'storage' }" ng-show="app.accessLevel === 'admin' && app.type !== APP_TYPES.PROXIED">{{ 'app.storageTabTitle' | tr }}</div>
|
||||
<div ng-click="setView('graphs')" ng-class="{ 'active': view === 'graphs' }" ng-show="app.type !== APP_TYPES.PROXIED">{{ 'app.graphsTabTitle' | tr }}</div>
|
||||
<div ng-click="setView('security')" ng-class="{ 'active': view === 'security' }">{{ 'app.securityTabTitle' | tr }}</div>
|
||||
@@ -1011,61 +965,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-show="view === 'services'">
|
||||
<div class="row" ng-show="app.manifest.addons.turn.optional">
|
||||
<div class="col-md-12">
|
||||
<label class="control-label">{{ 'app.turn.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#turn" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="services.enableTurn" value="1"> {{ 'app.turn.enable' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="services.enableTurn" value="0"> {{ 'app.turn.disable' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 text-right">
|
||||
<br/>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="services.submitTurn()" ng-disabled="app.enableTurn === services.enableTurn || services.busy || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="services.busy"></i> {{ 'main.saveAction' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr ng-show="app.manifest.addons.turn.optional && app.manifest.addons.redis.optional">
|
||||
|
||||
<div class="row" ng-show="app.manifest.addons.redis.optional">
|
||||
<div class="col-md-12">
|
||||
<label class="control-label">{{ 'app.redis.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#redis" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="services.enableRedis" value="1"> {{ 'app.redis.enable' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="services.enableRedis" value="0"> {{ 'app.redis.disable' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 text-right">
|
||||
<br/>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="services.submitRedis()" ng-disabled="app.enablRedis === services.enableRedis || services.busy || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="services.busy"></i> {{ 'main.saveAction' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="card" ng-show="view === 'storage'">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@@ -1074,7 +973,6 @@
|
||||
<p ng-bind-html="'app.storage.appdata.description' | tr:{ storagePath: ('/home/yellowtent/appsdata/' + app.id) }"></p>
|
||||
<form role="form" name="storageDataDirForm" ng-submit="storage.submitDataDir()" autocomplete="off">
|
||||
<select class="form-control" ng-model="storage.location" ng-options="location.displayName for location in storage.locationOptions track by location.id"></select>
|
||||
<p class="text-warning" ng-show="storage.location.type === 'volume' && storage.location.mountType === 'mountpoint'" ng-bind-html="'app.storage.appdata.mountTypeWarning' | tr"></p>
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -1087,7 +985,6 @@
|
||||
<input class="ng-hide" type="submit" ng-disabled="!storageDataDirForm.$dirty || storageDataDirForm.$invalid || storage.busyDataDir || app.error || app.taskId"/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
@@ -1124,7 +1021,6 @@
|
||||
</select>
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: middle">
|
||||
<a class="btn btn-xs btn-default" ng-href="{{ '/frontend/filemanager.html#/home/volume/' + mount.volume.id }}" target="_blank" uib-tooltip="{{ 'volumes.openFileManagerActionTooltip' | tr }}"><i class="fas fa-folder"></i></a>
|
||||
<button class="btn btn-danger btn-xs" ng-click="storage.delMount($event, $index)"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1505,8 +1401,8 @@
|
||||
<tr ng-repeat="backup in backups.backups">
|
||||
<td><i class="fas fa-archive" ng-show="backup.preserveSecs === -1" uib-tooltip="{{ 'backups.listing.tooltipPreservedBackup' | tr }}"></i></td>
|
||||
<!-- <td><div class="hand clipboard" data-clipboard-text="{{ backup.id }}" uib-tooltip="{{ copyBackupIdDone ? ('main.clipboard.copied' | tr) : ('main.clipboard.clickToCopyBackupId' | tr) }}" tooltip-placement="right"><i class="fa fa-copy"></i></div></td> -->
|
||||
<td ng-click="backupDetails.show(backup)" class="hand"><div>v{{ backup.packageVersion }}</div></td>
|
||||
<td ng-click="backupDetails.show(backup)" class="hand"><span uib-tooltip="{{ backup.creationTime | prettyLongDate }}">{{ backup.creationTime | prettyDate }} <b ng-show="backup.label">({{ backup.label }})</b></span></td>
|
||||
<td><div>v{{ backup.packageVersion }}</div></td>
|
||||
<td><span uib-tooltip="{{ backup.creationTime | prettyLongDate }}">{{ backup.creationTime | prettyDate }} <b ng-show="backup.label">({{ backup.label }})</b></span></td>
|
||||
<td class="text-center" style="vertical-align: bottom">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-xs btn-default dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/* global Clipboard */
|
||||
/* global SECRET_PLACEHOLDER */
|
||||
/* global APP_TYPES, STORAGE_PROVIDERS, BACKUP_FORMATS */
|
||||
/* global REGIONS_S3, REGIONS_WASABI, REGIONS_DIGITALOCEAN, REGIONS_EXOSCALE, REGIONS_SCALEWAY, REGIONS_LINODE, REGIONS_OVH, REGIONS_IONOS, REGIONS_UPCLOUD, REGIONS_VULTR, REGIONS_CONTABO */
|
||||
/* global REGIONS_S3, REGIONS_WASABI, REGIONS_DIGITALOCEAN, REGIONS_EXOSCALE, REGIONS_SCALEWAY, REGIONS_LINODE, REGIONS_OVH, REGIONS_IONOS, REGIONS_UPCLOUD, REGIONS_VULTR */
|
||||
|
||||
angular.module('Application').controller('AppController', ['$scope', '$location', '$translate', '$timeout', '$interval', '$route', '$routeParams', 'Client', function ($scope, $location, $translate, $timeout, $interval, $route, $routeParams, Client) {
|
||||
$scope.s3Regions = REGIONS_S3;
|
||||
@@ -23,7 +23,6 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
$scope.ionosRegions = REGIONS_IONOS;
|
||||
$scope.upcloudRegions = REGIONS_UPCLOUD;
|
||||
$scope.vultrRegions = REGIONS_VULTR;
|
||||
$scope.contaboRegions = REGIONS_VULTR;
|
||||
|
||||
$scope.storageProviders = STORAGE_PROVIDERS;
|
||||
|
||||
@@ -586,54 +585,6 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
},
|
||||
};
|
||||
|
||||
$scope.services = {
|
||||
error: {},
|
||||
|
||||
busy: false,
|
||||
enableTurn: '1', // curse of radio buttons
|
||||
enableRedis: '1',
|
||||
|
||||
show: function () {
|
||||
var app = $scope.app;
|
||||
|
||||
$scope.services.error = {};
|
||||
$scope.services.enableTurn = app.enableTurn ? '1' : '0';
|
||||
$scope.services.enableRedis = app.enableRedis ? '1' : '0';
|
||||
},
|
||||
|
||||
submitTurn: function () {
|
||||
$scope.services.busy = true;
|
||||
$scope.services.error = {};
|
||||
|
||||
Client.configureApp($scope.app.id, 'turn', { enable: $scope.services.enableTurn === '1' }, function (error) {
|
||||
if (error && error.statusCode === 400) {
|
||||
$scope.services.busy = false;
|
||||
$scope.services.error.turn = true;
|
||||
return;
|
||||
}
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$timeout(function () { $scope.services.busy = false; }, 1000);
|
||||
});
|
||||
},
|
||||
|
||||
submitRedis: function () {
|
||||
$scope.services.busy = true;
|
||||
$scope.services.error = {};
|
||||
|
||||
Client.configureApp($scope.app.id, 'redis', { enable: $scope.services.enableRedis === '1' }, function (error) {
|
||||
if (error && error.statusCode === 400) {
|
||||
$scope.services.busy = false;
|
||||
$scope.services.error.redis = true;
|
||||
return;
|
||||
}
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$timeout(function () { $scope.services.busy = false; }, 1000);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
$scope.storage = {
|
||||
error: {},
|
||||
|
||||
@@ -662,7 +613,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
];
|
||||
|
||||
$scope.volumes.forEach(function (volume) {
|
||||
$scope.storage.locationOptions.push({ id: volume.id, type: 'volume', value: volume.id, displayName: 'Volume - ' + volume.name, mountType: volume.mountType });
|
||||
$scope.storage.locationOptions.push({ id: volume.id, type: 'volume', value: volume.id, displayName: 'Volume - ' + volume.name });
|
||||
});
|
||||
|
||||
$scope.storage.location = $scope.storage.locationOptions.find(function (l) { return l.id === (app.storageVolumeId || 'default'); });
|
||||
@@ -1232,15 +1183,6 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
}
|
||||
};
|
||||
|
||||
$scope.backupDetails = {
|
||||
backup: null,
|
||||
|
||||
show: function (backup) {
|
||||
$scope.backupDetails.backup = backup;
|
||||
$('#backupDetailsModal').modal('show');
|
||||
}
|
||||
};
|
||||
|
||||
$scope.backups = {
|
||||
busy: false,
|
||||
busyCreate: false,
|
||||
@@ -1317,8 +1259,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
|| provider === 'exoscale-sos' || provider === 'digitalocean-spaces'
|
||||
|| provider === 'scaleway-objectstorage' || provider === 'wasabi' || provider === 'backblaze-b2' || provider === 'cloudflare-r2'
|
||||
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'ionos-objectstorage'
|
||||
|| provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage' || provider === 'idrive-e2'
|
||||
|| provider === 'contabo-objectstorage';
|
||||
|| provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage' || provider === 'idrive-e2';
|
||||
};
|
||||
|
||||
$scope.mountlike = function (provider) {
|
||||
@@ -1416,10 +1357,6 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
} else if (backupConfig.provider === 'vultr-objectstorage') {
|
||||
backupConfig.region = $scope.vultrRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'contabo-objectstorage') {
|
||||
backupConfig.region = $scope.contaboRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
backupConfig.s3ForcePathStyle = true; // https://docs.contabo.com/docs/products/Object-Storage/technical-description (no virtual buckets)
|
||||
} else if (backupConfig.provider === 'upcloud-objectstorage') {
|
||||
var m = /^.*\.(.*)\.upcloudobjects.com$/.exec(backupConfig.endpoint);
|
||||
backupConfig.region = m ? m[1] : 'us-east-1'; // let it fail in validation phase if m is not valid
|
||||
@@ -1516,10 +1453,6 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
|
||||
$('#importBackupModal').modal('hide');
|
||||
|
||||
// clear potential post-install flag
|
||||
$scope.app.pendingPostInstallConfirmation = false;
|
||||
delete localStorage['confirmPostInstall_' + $scope.app.id];
|
||||
|
||||
refreshApp($scope.app.id, function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
@@ -2107,15 +2040,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
};
|
||||
|
||||
Object.keys($scope.backupConfig).forEach(function (k) {
|
||||
var v = $scope.backupConfig[k];
|
||||
if (v && typeof v === 'object') { // to hide mountOptions.password and the likes
|
||||
tmp[k] = {};
|
||||
Object.keys(v).forEach(function (j) {
|
||||
if (v[j] !== SECRET_PLACEHOLDER) tmp[k][j] = v[j];
|
||||
});
|
||||
} else {
|
||||
if ($scope.backupConfig[k] !== SECRET_PLACEHOLDER) tmp[k] = v;
|
||||
}
|
||||
if ($scope.backupConfig[k] !== SECRET_PLACEHOLDER) tmp[k] = $scope.backupConfig[k];
|
||||
});
|
||||
|
||||
var filename = 'app-backup-config-' + (new Date()).toISOString().replace(/:|T/g,'-').replace(/\..*/,'') + ' (' + $scope.app.fqdn + ')' + '.json';
|
||||
|
||||
@@ -182,8 +182,7 @@
|
||||
</div>
|
||||
|
||||
<div class="usermanagement-indicator" ng-show="app.type !== APP_TYPES.LINK">
|
||||
<i class="fa-brands fa-openid" ng-show="app.ssoAuth && app.manifest.addons.oidc" uib-tooltip="{{ 'apps.auth.openid' | tr }}" tooltip-placement="right"></i>
|
||||
<i class="fas fa-user" ng-show="app.ssoAuth && (!app.manifest.addons.oidc && !app.manifest.addons.email)" uib-tooltip="{{ 'apps.auth.sso' | tr }}" tooltip-placement="right"></i>
|
||||
<i class="fas fa-user" ng-show="app.ssoAuth && !app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.sso' | tr }}" tooltip-placement="right"></i>
|
||||
<i class="far fa-user" ng-show="!app.ssoAuth && !app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.nosso' | tr }}" tooltip-placement="right"></i>
|
||||
<i class="fas fa-envelope" ng-show="app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.email' | tr }}" tooltip-placement="right"></i>
|
||||
</div>
|
||||
|
||||
@@ -187,19 +187,21 @@ angular.module('Application').controller('AppsController', ['$scope', '$translat
|
||||
accessRestriction.groups = $scope.applinksEdit.accessRestriction.groups.map(function (g) { return g.id; });
|
||||
}
|
||||
|
||||
var icon;
|
||||
if ($scope.applinksEdit.icon.data === '__original__') { // user reset the icon
|
||||
icon = '';
|
||||
} else if ($scope.applinksEdit.icon.data) { // user loaded custom icon
|
||||
icon = $scope.applinksEdit.icon.data.replace(/^data:image\/[a-z]+;base64,/, '');
|
||||
}
|
||||
|
||||
var data = {
|
||||
upstreamUri: $scope.applinksEdit.upstreamUri,
|
||||
label: $scope.applinksEdit.label,
|
||||
accessRestriction: accessRestriction,
|
||||
icon: icon,
|
||||
tags: $scope.applinksEdit.tags.split(' ').map(function (t) { return t.trim(); }).filter(function (t) { return !!t; })
|
||||
};
|
||||
|
||||
if ($scope.applinksEdit.icon.data === '__original__') { // user reset the icon
|
||||
data.icon = '';
|
||||
} else if ($scope.applinksEdit.icon.data) { // user loaded custom icon
|
||||
data.icon = $scope.applinksEdit.icon.data.replace(/^data:image\/[a-z]+;base64,/, '');
|
||||
}
|
||||
|
||||
Client.updateApplink($scope.applinksEdit.id, data, function (error) {
|
||||
$scope.applinksEdit.busyEdit = false;
|
||||
|
||||
|
||||
@@ -281,9 +281,8 @@
|
||||
<!-- appstore login -->
|
||||
<div ng-show="ready && !validSubscription" class="container card card-small appstore-login ng-cloak">
|
||||
<div class="col-md-12 text-center">
|
||||
<h1 ng-show="appstoreLogin.setupType === 'signup'">{{ 'appstore.accountDialog.titleSignUp' | tr }}</h1>
|
||||
<h1 ng-show="appstoreLogin.setupType === 'login'">{{ 'appstore.accountDialog.titleLogin' | tr }}</h1>
|
||||
<h1 ng-show="appstoreLogin.setupType === 'setupToken'">{{ 'appstore.accountDialog.titleToken' | tr }}</h1>
|
||||
<h1 ng-show="appstoreLogin.register">{{ 'appstore.accountDialog.titleSignUp' | tr }}</h1>
|
||||
<h1 ng-hide="appstoreLogin.register">{{ 'appstore.accountDialog.titleLogin' | tr }}</h1>
|
||||
</div>
|
||||
<div class="col-md-12 text-center">
|
||||
<p>{{ 'appstore.accountDialog.description' | tr }}</p>
|
||||
@@ -294,121 +293,54 @@
|
||||
<div class="col-md-12">
|
||||
<br/>
|
||||
|
||||
<div ng-show="appstoreLogin.setupType === 'signup'">
|
||||
<form name="appstoreSignupForm" role="form" novalidate ng-submit="appstoreLogin.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<form name="appstoreLoginForm" role="form" novalidate ng-submit="appstoreLogin.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (appstoreSignupForm.email.$dirty && appstoreSignupForm.email.$invalid) || appstoreLogin.error.generic }">
|
||||
<div class="form-group" ng-class="{ 'has-error': (appstoreLoginForm.email.$dirty && appstoreLoginForm.email.$invalid) || appstoreLogin.error.generic }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.email' | tr }}</label>
|
||||
<input type="email" class="form-control" ng-model="appstoreLogin.email" id="inputAppstoreSignupEmail" name="email" required autofocus>
|
||||
<div class="control-label" ng-show="(!appstoreSignupForm.email.$dirty && appstoreLogin.error.email) || (appstoreSignupForm.email.$dirty && appstoreSignupForm.email.$invalid) || appstoreLogin.error.email">
|
||||
<small class="text-danger" ng-show="appstoreLogin.error.email">{{ appstoreLogin.error.email }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appstoreSignupForm.password.$dirty && appstoreLogin.error.signupPassword) || (appstoreSignupForm.password.$dirty && appstoreSignupForm.password.$invalid) || appstoreLogin.error.generic }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.password' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="appstoreLogin.password" id="inputAppstoreSignupPassword" name="password" required password-reveal>
|
||||
<div class="control-label" ng-show="(!appstoreSignupForm.password.$dirty && appstoreLogin.error.signupPassword) || (appstoreSignupForm.password.$dirty && appstoreSignupForm.password.$invalid)">
|
||||
<small ng-show="!appstoreSignupForm.password.$dirty && appstoreLogin.error.signupPassword">{{ 'appstore.accountDialog.errorWrongPassword' | tr }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="appstoreLogin.termsAccepted" ng-required="true"><span ng-bind-html="'appstore.accountDialog.licenseCheckbox' | tr:{ licenseLink: 'https://cloudron.io/legal/license.html' }"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<center>
|
||||
<button type="submit" class="btn btn-lg btn-success" ng-disabled="appstoreSignupForm.$invalid || appstoreLogin.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="appstoreLogin.busy"></i> {{ 'appstore.accountDialog.createAccountAction' | tr }}
|
||||
</button>
|
||||
</center>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div ng-show="appstoreLogin.setupType === 'login'">
|
||||
<form name="appstoreLoginForm" role="form" novalidate ng-submit="appstoreLogin.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (appstoreLoginForm.email.$dirty && appstoreLoginForm.email.$invalid) || appstoreLogin.error.generic }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.email' | tr }}</label>
|
||||
<input type="email" class="form-control" ng-model="appstoreLogin.email" name="email" required autofocus>
|
||||
<input type="email" class="form-control" ng-model="appstoreLogin.email" id="inputAppstoreLoginEmail" name="email" required autofocus>
|
||||
<div class="control-label" ng-show="(!appstoreLoginForm.email.$dirty && appstoreLogin.error.email) || (appstoreLoginForm.email.$dirty && appstoreLoginForm.email.$invalid) || appstoreLogin.error.email">
|
||||
<small class="text-danger" ng-show="appstoreLogin.error.email">{{ appstoreLogin.error.email }}</small>
|
||||
<small class="text-danger" ng-show="appstoreLogin.error.email">{{ appstoreLogin.error.email }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appstoreLoginForm.password.$dirty && appstoreLogin.error.loginPassword) || (appstoreLoginForm.password.$dirty && appstoreLoginForm.password.$invalid) || appstoreLogin.error.generic }">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appstoreLoginForm.password.$dirty && appstoreLogin.error.password) || (appstoreLoginForm.password.$dirty && appstoreLoginForm.password.$invalid) || appstoreLogin.error.generic }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.password' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="appstoreLogin.password" id="inputAppstoreLoginPassword" name="password" required password-reveal>
|
||||
<div class="control-label" ng-show="(!appstoreLoginForm.password.$dirty && appstoreLogin.error.loginPassword) || (appstoreLoginForm.password.$dirty && appstoreLoginForm.password.$invalid)">
|
||||
<small ng-show="!appstoreLoginForm.password.$dirty && appstoreLogin.error.loginPassword">{{ 'appstore.accountDialog.errorWrongPassword' | tr }}</small>
|
||||
<div class="control-label" ng-show="(!appstoreLoginForm.password.$dirty && appstoreLogin.error.password) || (appstoreLoginForm.password.$dirty && appstoreLoginForm.password.$invalid)">
|
||||
<small ng-show="!appstoreLoginForm.password.$dirty && appstoreLogin.error.password">{{ 'appstore.accountDialog.errorWrongPassword' | tr }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': appstoreLogin.error.totpToken }">
|
||||
<div class="form-group" ng-hide="appstoreLogin.register" ng-class="{ 'has-error': appstoreLogin.error.totpToken }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.2faToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="appstoreLogin.totpToken" id="inputAppstoreLoginTotpToken" name="totpToken">
|
||||
<div class="control-label" ng-show="appstoreLogin.error.totpToken">
|
||||
<small ng-show="appstoreLogin.error.totpToken">{{ appstoreLogin.error.totpToken }}</small>
|
||||
<small ng-show="appstoreLogin.error.totpToken">{{ appstoreLogin.error.totpToken }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="appstoreLogin.termsAccepted" ng-required="true"><span ng-bind-html="'appstore.accountDialog.licenseCheckbox' | tr:{ licenseLink: 'https://cloudron.io/legal/license.html' }"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="appstoreLogin.termsAccepted"><span ng-bind-html="'appstore.accountDialog.licenseCheckbox' | tr:{ licenseLink: 'https://cloudron.io/legal/license.html' }"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<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">{{ 'appstore.accountDialog.loginAction' | tr }}</span><span ng-show="appstoreLogin.register">{{ 'appstore.accountDialog.createAccountAction' | tr }}</span>
|
||||
</button>
|
||||
|
||||
<br/>
|
||||
|
||||
<center>
|
||||
<button type="submit" class="btn btn-lg btn-success" ng-disabled="appstoreLoginForm.$invalid || appstoreLogin.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="appstoreLogin.busy"></i> {{ 'appstore.accountDialog.loginAction' | tr }}
|
||||
</button>
|
||||
</center>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div ng-show="appstoreLogin.setupType === 'setupToken'">
|
||||
<form name="appstoreSetupTokenForm" role="form" novalidate ng-submit="appstoreLogin.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': appstoreLogin.error.setupToken }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.setupToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="appstoreLogin.setupToken" id="inputAppstoreSetupToken" name="setupToken" ng-required="true">
|
||||
<div class="control-label" ng-show="appstoreLogin.error.setupToken">
|
||||
<small ng-show="appstoreLogin.error.setupToken">{{ appstoreLogin.error.setupToken }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="appstoreLogin.termsAccepted" ng-required="true"><span ng-bind-html="'appstore.accountDialog.licenseCheckbox' | tr:{ licenseLink: 'https://cloudron.io/legal/license.html' }"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<center>
|
||||
<button type="submit" class="btn btn-lg btn-success" ng-disabled="appstoreSetupTokenForm.$invalid || appstoreLogin.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="appstoreLogin.busy"></i> {{ 'appstore.accountDialog.setupWithTokenAction' | tr }}
|
||||
</button>
|
||||
</center>
|
||||
</form>
|
||||
</div>
|
||||
<a href="" ng-click="appstoreLogin.register = true" ng-hide="appstoreLogin.register">{{ 'appstore.accountDialog.switchToSignUpAction' | tr }}</a>
|
||||
<a href="" ng-click="appstoreLogin.register = false" ng-show="appstoreLogin.register">{{ 'appstore.accountDialog.switchToLoginAction' | tr }}</a>
|
||||
</center>
|
||||
|
||||
<br/>
|
||||
|
||||
<center>
|
||||
<a href="" ng-click="appstoreLogin.setupType = 'signup'" ng-show="appstoreLogin.setupType === 'login'">{{ 'appstore.accountDialog.switchToSignUpAction' | tr }}</a>
|
||||
<a href="" ng-click="appstoreLogin.setupType = 'login'" ng-show="appstoreLogin.setupType === 'signup' || appstoreLogin.setupType === 'setupToken'">{{ 'appstore.accountDialog.switchToLoginAction' | tr }}</a>
|
||||
<span ng-show="appstoreLogin.setupType !== 'setupToken'"> or <a href="" ng-click="appstoreLogin.setupType = 'setupToken'">use a setup token</a></span>
|
||||
</center>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
$scope.cachedCategory = ''; // used to cache the selected category while searching
|
||||
$scope.searchString = '';
|
||||
$scope.validSubscription = false;
|
||||
$scope.unstableApps = false;
|
||||
$scope.subscription = {};
|
||||
$scope.memory = null; // { memory, swap }
|
||||
|
||||
@@ -44,7 +45,6 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
// If new categories added make sure the translation below exists
|
||||
$scope.categories = [
|
||||
{ id: 'analytics', icon: 'fa fa-chart-line', label: 'Analytics'},
|
||||
{ id: 'automation', icon: 'fa fa-robot', label: 'Automation'},
|
||||
{ id: 'blog', icon: 'fa fa-font', label: 'Blog'},
|
||||
{ id: 'chat', icon: 'fa fa-comments', label: 'Chat'},
|
||||
{ id: 'crm', icon: 'fab fa-connectdevelop', label: 'CRM'},
|
||||
@@ -64,7 +64,6 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
{ id: 'notes', icon: 'fa fa-sticky-note', label: 'Notes'},
|
||||
{ id: 'project', icon: 'fas fa-project-diagram', label: 'Project Management'},
|
||||
{ id: 'sync', icon: 'fa fa-sync-alt', label: 'File Sync'},
|
||||
{ id: 'voip', icon: 'fa fa-headset', label: 'VoIP'},
|
||||
{ id: 'vpn', icon: 'fa fa-user-secret', label: 'VPN'},
|
||||
{ id: 'wiki', icon: 'fab fa-wikipedia-w', label: 'Wiki'},
|
||||
];
|
||||
@@ -416,24 +415,22 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
email: '',
|
||||
password: '',
|
||||
totpToken: '',
|
||||
setupType: 'login',
|
||||
register: true,
|
||||
termsAccepted: false,
|
||||
setupToken: '',
|
||||
|
||||
submit: function () {
|
||||
$scope.appstoreLogin.error = {};
|
||||
$scope.appstoreLogin.busy = true;
|
||||
|
||||
var func = $scope.appstoreLogin.setupToken ? Client.registerCloudronWithSetupToken.bind(null, $scope.appstoreLogin.setupToken) : Client.registerCloudron.bind(null, $scope.appstoreLogin.email, $scope.appstoreLogin.password, $scope.appstoreLogin.totpToken, $scope.appstoreLogin.setupType === 'register');
|
||||
func(function (error) {
|
||||
Client.registerCloudron($scope.appstoreLogin.email, $scope.appstoreLogin.password, $scope.appstoreLogin.totpToken, $scope.appstoreLogin.register, function (error) {
|
||||
if (error) {
|
||||
$scope.appstoreLogin.busy = false;
|
||||
|
||||
if (error.statusCode === 409) {
|
||||
$scope.appstoreLogin.error.email = 'An account with this email already exists';
|
||||
$scope.appstoreLogin.password = '';
|
||||
$scope.appstoreSignupForm.email.$setPristine();
|
||||
$scope.appstoreSignupForm.password.$setPristine();
|
||||
$scope.appstoreLoginForm.email.$setPristine();
|
||||
$scope.appstoreLoginForm.password.$setPristine();
|
||||
$('#inputAppstoreLoginEmail').focus();
|
||||
} else if (error.statusCode === 412) {
|
||||
if (error.message.indexOf('TOTP token missing') !== -1) {
|
||||
@@ -444,7 +441,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
$scope.appstoreLogin.totpToken = '';
|
||||
setTimeout(function () { $('#inputAppstoreLoginTotpToken').focus(); }, 0);
|
||||
} else {
|
||||
$scope.appstoreLogin.error.loginPassword = 'Wrong email or password';
|
||||
$scope.appstoreLogin.error.password = 'Wrong email or password';
|
||||
$scope.appstoreLogin.password = '';
|
||||
$('#inputAppstoreLoginPassword').focus();
|
||||
$scope.appstoreLoginForm.password.$setPristine();
|
||||
@@ -456,18 +453,11 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
$scope.appstoreLogin.password = '';
|
||||
$scope.appstoreLoginForm.email.$setPristine();
|
||||
$scope.appstoreLoginForm.password.$setPristine();
|
||||
$scope.appstoreSignupForm.email.$setPristine();
|
||||
$scope.appstoreSignupForm.password.$setPristine();
|
||||
$('#inputAppstoreLoginEmail').focus();
|
||||
} else {
|
||||
console.error(error);
|
||||
$scope.appstoreLogin.error.generic = error.message;
|
||||
}
|
||||
} else if (error.statusCode === 402) {
|
||||
$scope.appstoreLogin.error.setupToken = 'Invalid or expired setup token';
|
||||
$scope.appstoreLogin.setupToken = '';
|
||||
$scope.appstoreSetupTokenForm.setupToken.$setPristine();
|
||||
$('#inputAppstoreSetupToken').focus();
|
||||
} else {
|
||||
console.error(error);
|
||||
$scope.appstoreLogin.error.generic = error.message || 'Please retry later';
|
||||
@@ -787,12 +777,10 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
getSubscription(function (error, validSubscription) {
|
||||
if (error) console.error('Failed to get subscription.', error);
|
||||
|
||||
// autofocus login
|
||||
if (!validSubscription) setTimeout(function () { $('[name=appstoreLoginForm]').find('[autofocus]:first').focus(); }, 1000);
|
||||
|
||||
$scope.validSubscription = validSubscription;
|
||||
$scope.ready = true;
|
||||
|
||||
|
||||
// refresh everything in background
|
||||
Client.getAppstoreApps(function (error) { if (error) console.error('Failed to fetch apps.', error); });
|
||||
Client.refreshConfig(); // refresh domain, user, group limit etc
|
||||
@@ -839,5 +827,10 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
});
|
||||
});
|
||||
|
||||
// autofocus if appstore login is shown
|
||||
$scope.$watch('validSubscription', function (newValue/*, oldValue */) {
|
||||
if (!newValue) setTimeout(function () { $('[name=appstoreLoginForm]').find('[autofocus]:first').focus(); }, 1000);
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
|
||||
@@ -14,10 +14,6 @@
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupEdit.label' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">{{ backupDetails.backup.label }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupEdit.remotePath' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">{{ backupDetails.backup.remotePath }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupDetails.date' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">{{ backupDetails.backup.creationTime | prettyLongDate }}</div>
|
||||
@@ -105,47 +101,47 @@
|
||||
<div class="modal-body">{{ 'backups.cleanupBackups.description' | tr }}</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="cleanupBackups.start()">{{ 'backups.cleanupBackups.cleanupNow' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="createBackup.startCleanup()">{{ 'backups.cleanupBackups.cleanupNow' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal backup schedule config -->
|
||||
<div class="modal fade" id="backupPolicyModal" tabindex="-1" role="dialog">
|
||||
<!-- modal backup config -->
|
||||
<div class="modal fade" id="configureScheduleAndRetentionModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.configureBackupSchedule.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="backupPolicyForm" role="form" novalidate ng-submit="backupPolicy.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="backupPolicy.error">{{ backupPolicy.error.generic }}</p>
|
||||
<form name="configureScheduleAndRetentionForm" role="form" novalidate ng-submit="configureScheduleAndRetention.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="configureScheduleAndRetention.error">{{ configureScheduleAndRetention.error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="backupSchedule">{{ 'backups.configureBackupSchedule.schedule' | tr }}</label>
|
||||
<p ng-bind-html="'backups.configureBackupSchedule.scheduleDescription' | tr"></p>
|
||||
|
||||
<div class="row" style="margin-left: 20px;">
|
||||
<div class="col-md-5" ng-class="{ 'has-error': !backupPolicy.days.length }">
|
||||
{{ 'backups.configureBackupSchedule.days' | tr }}: <multiselect id="backupSchedule" class="input-sm stretch" ng-model="backupPolicy.days" options="a.name for a in cronDays" data-multiple="true" ng-required></multiselect>
|
||||
<div class="col-md-5" ng-class="{ 'has-error': !configureScheduleAndRetention.days.length }">
|
||||
{{ 'backups.configureBackupSchedule.days' | tr }}: <multiselect id="backupSchedule" class="input-sm stretch" ng-model="configureScheduleAndRetention.days" options="a.name for a in cronDays" data-multiple="true" ng-required></multiselect>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5" ng-class="{ 'has-error': !backupPolicy.hours.length }">
|
||||
{{ 'backups.configureBackupSchedule.hours' | tr }}: <multiselect class="input-sm stretch" ng-model="backupPolicy.hours" options="a.name for a in cronHours" data-multiple="true"></multiselect>
|
||||
<div class="col-md-5" ng-class="{ 'has-error': !configureScheduleAndRetention.hours.length }">
|
||||
{{ 'backups.configureBackupSchedule.hours' | tr }}: <multiselect class="input-sm stretch" ng-model="configureScheduleAndRetention.hours" options="a.name for a in cronHours" data-multiple="true"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="backupRetention">{{ 'backups.configureBackupSchedule.retentionPolicy' | tr }}</label>
|
||||
<select class="form-control" id="backupRetention" ng-model="backupPolicy.retention" ng-options="a.value as a.name for a in backupRetentions"></select>
|
||||
<select class="form-control" id="backupRetention" ng-model="configureScheduleAndRetention.retentionPolicy" ng-options="a.value as a.name for a in retentionPolicies"></select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="backupPolicy.submit()" ng-disabled="!backupPolicy.valid() || backupPolicy.busy"><i class="fa fa-circle-notch fa-spin" ng-show="backupPolicy.busy"></i><span> {{ 'main.dialog.save' | tr }}</span></button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="configureScheduleAndRetention.submit()" ng-disabled="!configureScheduleAndRetention.valid() || configureScheduleAndRetention.busy"><i class="fa fa-circle-notch fa-spin" ng-show="configureScheduleAndRetention.busy"></i><span> {{ 'main.dialog.save' | tr }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -177,7 +173,7 @@
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.mountPoint || (configureBackupForm.mountPoint.$dirty && !configureBackup.mountPoint) }" ng-show="configureBackup.provider === 'mountpoint'">
|
||||
<label class="control-label" for="inputConfigureMountPoint">{{ 'backups.configureBackupStorage.mountPoint' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountPoint" id="inputConfigureMountPoint" name="mountPoint" ng-disabled="configureBackup.busy" placeholder="/mnt/backups" ng-required="configureBackup.provider === 'mountpoint'">
|
||||
<p ng-show="configureBackup.provider === 'mountpoint'" ng-bind-html="'backups.configureBackupStorage.mountPointDescription' | tr:{ providerDocsLink: 'https://docs.cloudron.io/backups/#'+configureBackup.provider }"></p>
|
||||
<p ng-show="configureBackup.provider === 'mointpoint'" ng-bind-html="'backups.configureBackupStorage.mountPointDescription' | tr:{ providerDocsLink: 'https://docs.cloudron.io/backups/#'+configureBackup.provider }"></p>
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
@@ -211,16 +207,10 @@
|
||||
<input type="password" class="form-control" ng-model="configureBackup.mountOptions.password" id="configureBackupPassword" name="cifsPassword" ng-disabled="configureBackup.busy" password-reveal>
|
||||
</div>
|
||||
|
||||
<!-- EXT4/XFS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.diskPath || !configureBackup.mountOptions.diskPath }" ng-show="configureBackup.provider === 'xfs' || configureBackup.provider === 'ext4'">
|
||||
<!-- EXT4 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.diskPath || !configureBackup.mountOptions.diskPath }" ng-show="configureBackup.provider === 'ext4' || configureBackup.provider === 'xfs'">
|
||||
<label class="control-label" for="inputConfigureDiskPath">{{ 'backups.configureBackupStorage.diskPath' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.diskPath" id="inputConfigureDiskPath" name="diskPath" ng-disabled="configureBackup.busy" placeholder="/dev/disk/by-uuid/uuid" ng-required="configureBackup.provider === 'xfs' || configureBackup.provider === 'ext4'">
|
||||
</div>
|
||||
|
||||
<!-- Disk -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.diskPath || !configureBackup.mountOptions.diskPath }" ng-show="configureBackup.provider === 'disk'">
|
||||
<label class="control-label">{{ 'backups.configureBackupStorage.diskPath' | tr }}</label>
|
||||
<select class="form-control" ng-model="configureBackup.disk" ng-options="item as item.label for item in configureBackup.blockDevices track by item.path" ng-required="configureBackup.provider === 'disk'"></select>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.diskPath" id="inputConfigureDiskPath" name="diskPath" ng-disabled="configureBackup.busy" placeholder="/dev/disk/by-uuid/uuid" ng-required="configureBackup.provider === 'ext4' || configureBackup.provider === 'xfs'">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
@@ -335,11 +325,6 @@
|
||||
<select class="form-control" name="region" id="inputConfigureBackupVultrRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in vultrRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'vultr-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'contabo-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupContaboRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupContaboRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in contaboRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'contabo-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">{{ 'backups.configureBackupStorage.s3AccessKeyId' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.accessKeyId" id="inputConfigureBackupAccessKeyId" name="accessKeyId" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
|
||||
@@ -483,8 +468,8 @@
|
||||
<div class="col-xs-6 text-right no-wrap">
|
||||
<span ng-show="backupConfig.provider === 'filesystem'">{{ backupConfig.backupFolder }}</span>
|
||||
<span ng-show="mountlike(backupConfig.provider)">
|
||||
<i class="fa fa-circle" ng-style="{ color: mountStatus.state === 'active' ? '#27CE65' : '#d9534f' }" ng-show="mountStatus" uib-tooltip="{{ mountStatus.message }}"></i>
|
||||
<span ng-show="backupConfig.provider === 'disk' || backupConfig.provider === 'filesystem' || backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs' || backupConfig.provider === 'mountpoint'">{{ backupConfig.mountOptions.diskPath || backupConfig.mountPoint }}{{ (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
<i class="fa fa-circle" ng-style="{ color: backupConfig.mountStatus.state === 'active' ? '#27CE65' : '#d9534f' }" ng-show="backupConfig.mountStatus" uib-tooltip="{{ backupConfig.mountStatus.message }}"></i>
|
||||
<span ng-show="backupConfig.provider === 'filesystem' || backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs' || backupConfig.provider === 'mountpoint'">{{ backupConfig.mountOptions.diskPath || backupConfig.mountPoint }}{{ (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
<span ng-show="backupConfig.provider === 'cifs' || backupConfig.provider === 'nfs' || backupConfig.provider === 'sshfs'">{{ backupConfig.mountOptions.host }}:{{ backupConfig.mountOptions.remoteDir }}{{ (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
</span>
|
||||
|
||||
@@ -522,23 +507,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>
|
||||
{{ 'backups.schedule.title' | tr }}
|
||||
<!-- <a class="btn btn-sm btn-default pull-right" ng-href="/frontend/logs.html?taskId={{cleanupBackups.taskId}}" target="_blank" uib-tooltip="{{ 'backups.logs.showLogs' | tr }}"><i class="fas fa-align-left"></i></a> -->
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="cleanupTasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'backups.logs.showLogs' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in cleanupTasks">
|
||||
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
<div class="text-left">
|
||||
<h3>{{ 'backups.schedule.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
@@ -548,7 +518,7 @@
|
||||
<span class="text-muted">{{ 'backups.schedule.schedule' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ prettyBackupSchedule(backupPolicy.currentPolicy.schedule) }}</span>
|
||||
<span>{{ prettyBackupSchedule(backupConfig.schedulePattern) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -556,34 +526,19 @@
|
||||
<span class="text-muted">{{ 'backups.schedule.retentionPolicy' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ prettyBackupRetention(backupPolicy.currentPolicy.retention) }}</span>
|
||||
<span>{{ prettyBackupRetentionPolicy(backupConfig.retentionPolicy) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-default" ng-click="cleanupBackups.ask()" ng-disabled="cleanupBackups.busy" style="margin-right: 5px"><i class="fa fa-circle-notch fa-spin" ng-show="cleanupBackups.busy"></i> {{ 'backups.listing.cleanupBackups' | tr }}</button>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-show="user.isAtLeastOwner" ng-click="backupPolicy.show()">{{ 'backups.schedule.configure' | tr }}</button>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-show="user.isAtLeastOwner" ng-click="configureScheduleAndRetention.show()">{{ 'backups.schedule.configure' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>
|
||||
{{ 'backups.listing.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="backupTasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'backups.logs.showLogs' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in backupTasks">
|
||||
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
<div class="text-left">
|
||||
<h3>{{ 'backups.listing.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
@@ -639,8 +594,22 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-default" ng-click="createBackup.cleanupBackups()" ng-show="!createBackup.busy" style="margin-right: 5px">{{ 'backups.listing.cleanupBackups' | tr }}</button>
|
||||
<button class="btn btn-outline btn-primary" ng-click="createBackup.startBackup()" ng-show="!createBackup.busy">{{ 'backups.listing.backupNow' | tr }}</button>
|
||||
<button class="btn btn-outline btn-danger" ng-click="createBackup.stopTask()" ng-show="createBackup.busy">{{ 'backups.listing.stopTask' | tr }}</button>
|
||||
<button class="btn btn-outline btn-danger" ng-click="createBackup.stopTask()" ng-show="createBackup.busy">{{ 'backups.listing.stopTask' | tr:{ taskType: createBackup.taskType } }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'backups.logs.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>{{ 'backups.logs.description' | tr }}</p>
|
||||
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{createBackup.taskId}}" ng-disabled="!createBackup.taskId" target="_blank">{{ 'backups.logs.showLogs' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
/* global $, angular, TASK_TYPES, SECRET_PLACEHOLDER, STORAGE_PROVIDERS, BACKUP_FORMATS, APP_TYPES */
|
||||
/* global REGIONS_S3, REGIONS_WASABI, REGIONS_DIGITALOCEAN, REGIONS_EXOSCALE, REGIONS_SCALEWAY, REGIONS_LINODE, REGIONS_OVH, REGIONS_IONOS, REGIONS_UPCLOUD, REGIONS_VULTR , REGIONS_CONTABO */
|
||||
/* global REGIONS_S3, REGIONS_WASABI, REGIONS_DIGITALOCEAN, REGIONS_EXOSCALE, REGIONS_SCALEWAY, REGIONS_LINODE, REGIONS_OVH, REGIONS_IONOS, REGIONS_UPCLOUD, REGIONS_VULTR */
|
||||
|
||||
angular.module('Application').controller('BackupsController', ['$scope', '$location', '$rootScope', '$timeout', 'Client', function ($scope, $location, $rootScope, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
@@ -12,13 +12,11 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.memory = null; // { memory, swap }
|
||||
$scope.mountStatus = null; // { state, message }
|
||||
|
||||
$scope.manualBackupApps = [];
|
||||
|
||||
$scope.backupConfig = {};
|
||||
$scope.backups = [];
|
||||
$scope.backupTasks = [];
|
||||
$scope.cleanupTasks = [];
|
||||
|
||||
$scope.s3Regions = REGIONS_S3;
|
||||
$scope.wasabiRegions = REGIONS_WASABI;
|
||||
@@ -30,13 +28,12 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.ionosRegions = REGIONS_IONOS;
|
||||
$scope.upcloudRegions = REGIONS_UPCLOUD;
|
||||
$scope.vultrRegions = REGIONS_VULTR;
|
||||
$scope.contaboRegions = REGIONS_CONTABO;
|
||||
|
||||
$scope.storageProviders = STORAGE_PROVIDERS.concat([
|
||||
{ name: 'No-op (Only for testing)', value: 'noop' }
|
||||
]);
|
||||
|
||||
$scope.backupRetentions = [
|
||||
$scope.retentionPolicies = [
|
||||
{ name: '2 days', value: { keepWithinSecs: 2 * 24 * 60 * 60 }},
|
||||
{ name: '1 week', value: { keepWithinSecs: 7 * 24 * 60 * 60 }}, // default
|
||||
{ name: '1 month', value: { keepWithinSecs: 30 * 24 * 60 * 60 }},
|
||||
@@ -86,8 +83,8 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
return prettyDay + ' at ' + prettyHour;
|
||||
};
|
||||
|
||||
$scope.prettyBackupRetention = function (retention) {
|
||||
var tmp = $scope.backupRetentions.find(function (p) { return angular.equals(p.value, retention); });
|
||||
$scope.prettyBackupRetentionPolicy = function (retentionPolicy) {
|
||||
var tmp = $scope.retentionPolicies.find(function (p) { return angular.equals(p.value, retentionPolicy); });
|
||||
return tmp ? tmp.name : '';
|
||||
};
|
||||
|
||||
@@ -122,9 +119,11 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
taskId: '',
|
||||
taskType: TASK_TYPES.TASK_BACKUP,
|
||||
|
||||
init: function () {
|
||||
Client.getLatestTaskByType(TASK_TYPES.TASK_BACKUP, function (error, task) {
|
||||
checkStatus: function () {
|
||||
// TODO support both task types TASK_BACKUP and TASK_CLEAN_BACKUPS
|
||||
Client.getLatestTaskByType($scope.createBackup.taskType, function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!task) return;
|
||||
@@ -144,8 +143,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.createBackup.percent = 100; // indicates that 'result' is valid
|
||||
$scope.createBackup.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
getBackupTasks();
|
||||
|
||||
return fetchBackups();
|
||||
}
|
||||
|
||||
@@ -161,6 +158,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.createBackup.percent = 0;
|
||||
$scope.createBackup.message = '';
|
||||
$scope.createBackup.errorMessage = '';
|
||||
$scope.createBackup.taskType = TASK_TYPES.TASK_BACKUP;
|
||||
|
||||
Client.startBackup(function (error, taskId) {
|
||||
if (error) {
|
||||
@@ -179,13 +177,31 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
return;
|
||||
}
|
||||
|
||||
getBackupTasks();
|
||||
|
||||
$scope.createBackup.taskId = taskId;
|
||||
$scope.createBackup.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
cleanupBackups: function () {
|
||||
$('#cleanupBackupsModal').modal('show');
|
||||
},
|
||||
|
||||
startCleanup: function () {
|
||||
$scope.createBackup.busy = true;
|
||||
$scope.createBackup.percent = 0;
|
||||
$scope.createBackup.message = '';
|
||||
$scope.createBackup.errorMessage = '';
|
||||
$scope.createBackup.taskType = TASK_TYPES.TASK_CLEAN_BACKUPS;
|
||||
|
||||
$('#cleanupBackupsModal').modal('hide');
|
||||
|
||||
Client.cleanupBackups(function (error, taskId) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.createBackup.taskId = taskId;
|
||||
$scope.createBackup.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
stopTask: function () {
|
||||
Client.stopTask($scope.createBackup.taskId, function (error) {
|
||||
@@ -198,7 +214,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
}
|
||||
|
||||
$scope.createBackup.busy = false;
|
||||
getBackupTasks();
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -206,62 +221,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
}
|
||||
};
|
||||
|
||||
$scope.cleanupBackups = {
|
||||
busy: false,
|
||||
taskId: 0,
|
||||
|
||||
init: function () {
|
||||
Client.getLatestTaskByType(TASK_TYPES.TASK_CLEAN_BACKUPS, function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!task) return;
|
||||
|
||||
$scope.cleanupBackups.taskId = task.id;
|
||||
$scope.cleanupBackups.updateStatus();
|
||||
|
||||
getCleanupTasks();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
Client.getTask($scope.cleanupBackups.taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.cleanupBackups.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.cleanupBackups.busy = false;
|
||||
|
||||
getCleanupTasks();
|
||||
fetchBackups();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.cleanupBackups.busy = true;
|
||||
$scope.cleanupBackups.message = data.message;
|
||||
window.setTimeout($scope.cleanupBackups.updateStatus, 3000);
|
||||
});
|
||||
},
|
||||
|
||||
ask: function () {
|
||||
$('#cleanupBackupsModal').modal('show');
|
||||
},
|
||||
|
||||
start: function () {
|
||||
$scope.cleanupBackups.busy = true;
|
||||
|
||||
$('#cleanupBackupsModal').modal('hide');
|
||||
|
||||
Client.cleanupBackups(function (error, taskId) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.cleanupBackups.taskId = taskId;
|
||||
$scope.cleanupBackups.updateStatus();
|
||||
|
||||
getCleanupTasks();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.listBackups = {
|
||||
};
|
||||
|
||||
@@ -270,12 +229,11 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
|| provider === 'exoscale-sos' || provider === 'digitalocean-spaces'
|
||||
|| provider === 'scaleway-objectstorage' || provider === 'wasabi' || provider === 'backblaze-b2' || provider === 'cloudflare-r2'
|
||||
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'ionos-objectstorage'
|
||||
|| provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage' || provider === 'idrive-e2'
|
||||
|| provider === 'contabo-objectstorage';
|
||||
|| provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage' || provider === 'idrive-e2';
|
||||
};
|
||||
|
||||
$scope.mountlike = function (provider) {
|
||||
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs' || provider === 'disk';
|
||||
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs';
|
||||
};
|
||||
|
||||
// https://stackoverflow.com/questions/3665115/how-to-create-a-file-in-memory-for-user-to-download-but-not-through-server#18197341
|
||||
@@ -303,7 +261,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
if ($scope.backupConfig[k] !== SECRET_PLACEHOLDER) tmp[k] = $scope.backupConfig[k];
|
||||
});
|
||||
|
||||
var filename = 'cloudron-backup-config-' + (new Date()).toISOString().replace(/:|T/g,'-').replace(/\..*/,'') + ' (' + $scope.config.adminFqdn + ')' + '.json';
|
||||
var filename = 'cloudron-backup-config-' + (new Date()).toISOString().replace(/:|T/g,'-').replace(/\..*/,'') + '.json';
|
||||
download(filename, JSON.stringify(tmp, null, 4));
|
||||
};
|
||||
|
||||
@@ -349,76 +307,68 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
}
|
||||
};
|
||||
|
||||
$scope.backupPolicy = {
|
||||
$scope.configureScheduleAndRetention = {
|
||||
busy: false,
|
||||
error: {},
|
||||
|
||||
currentPolicy: null,
|
||||
|
||||
retention: null,
|
||||
retentionPolicy: $scope.retentionPolicies[0],
|
||||
days: [],
|
||||
hours: [],
|
||||
|
||||
init: function () {
|
||||
Client.getBackupPolicy(function (error, policy) {
|
||||
if (error) Client.error(error);
|
||||
$scope.backupPolicy.currentPolicy = policy;
|
||||
});
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.backupPolicy.error = {};
|
||||
$scope.backupPolicy.busy = false;
|
||||
$scope.configureScheduleAndRetention.error = {};
|
||||
$scope.configureScheduleAndRetention.busy = false;
|
||||
|
||||
var selectedRetention = $scope.backupRetentions.find(function (x) { return angular.equals(x.value, $scope.backupPolicy.currentPolicy.retention); });
|
||||
if (!selectedRetention) selectedRetention = $scope.backupRetentions[0];
|
||||
var selectedPolicy = $scope.retentionPolicies.find(function (x) { return angular.equals(x.value, $scope.backupConfig.retentionPolicy); });
|
||||
if (!selectedPolicy) selectedPolicy = $scope.retentionPolicies[0];
|
||||
|
||||
$scope.backupPolicy.retention = selectedRetention.value;
|
||||
$scope.configureScheduleAndRetention.retentionPolicy = selectedPolicy.value;
|
||||
|
||||
var tmp = $scope.backupPolicy.currentPolicy.schedule.split(' ');
|
||||
var tmp = $scope.backupConfig.schedulePattern.split(' ');
|
||||
var hours = tmp[2].split(','), days = tmp[5].split(',');
|
||||
if (days[0] === '*') {
|
||||
$scope.backupPolicy.days = angular.copy($scope.cronDays, []);
|
||||
$scope.configureScheduleAndRetention.days = angular.copy($scope.cronDays, []);
|
||||
} else {
|
||||
$scope.backupPolicy.days = days.map(function (day) { return $scope.cronDays[parseInt(day, 10)]; });
|
||||
$scope.configureScheduleAndRetention.days = days.map(function (day) { return $scope.cronDays[parseInt(day, 10)]; });
|
||||
}
|
||||
$scope.backupPolicy.hours = hours.map(function (hour) { return $scope.cronHours[parseInt(hour, 10)]; });
|
||||
$scope.configureScheduleAndRetention.hours = hours.map(function (hour) { return $scope.cronHours[parseInt(hour, 10)]; });
|
||||
|
||||
$('#backupPolicyModal').modal('show');
|
||||
$('#configureScheduleAndRetentionModal').modal('show');
|
||||
},
|
||||
|
||||
valid: function () {
|
||||
return $scope.backupPolicy.days.length && $scope.backupPolicy.hours.length;
|
||||
return $scope.configureScheduleAndRetention.days.length && $scope.configureScheduleAndRetention.hours.length;
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
if (!$scope.backupPolicy.days.length) return;
|
||||
if (!$scope.backupPolicy.hours.length) return;
|
||||
if (!$scope.configureScheduleAndRetention.days.length) return;
|
||||
if (!$scope.configureScheduleAndRetention.hours.length) return;
|
||||
|
||||
$scope.backupPolicy.error = {};
|
||||
$scope.backupPolicy.busy = true;
|
||||
$scope.configureScheduleAndRetention.error = {};
|
||||
$scope.configureScheduleAndRetention.busy = true;
|
||||
|
||||
// start with the full backupConfig since the api requires all fields
|
||||
var backupConfig = $scope.backupConfig;
|
||||
backupConfig.retentionPolicy = $scope.configureScheduleAndRetention.retentionPolicy;
|
||||
|
||||
var daysPattern;
|
||||
if ($scope.backupPolicy.days.length === 7) daysPattern = '*';
|
||||
else daysPattern = $scope.backupPolicy.days.map(function (d) { return d.value; });
|
||||
if ($scope.configureScheduleAndRetention.days.length === 7) daysPattern = '*';
|
||||
else daysPattern = $scope.configureScheduleAndRetention.days.map(function (d) { return d.value; });
|
||||
|
||||
var hoursPattern;
|
||||
if ($scope.backupPolicy.hours.length === 24) hoursPattern = '*';
|
||||
else hoursPattern = $scope.backupPolicy.hours.map(function (d) { return d.value; });
|
||||
if ($scope.configureScheduleAndRetention.hours.length === 24) hoursPattern = '*';
|
||||
else hoursPattern = $scope.configureScheduleAndRetention.hours.map(function (d) { return d.value; });
|
||||
|
||||
var policy = {
|
||||
retention: $scope.backupPolicy.retention,
|
||||
schedule: '00 00 ' + hoursPattern + ' * * ' + daysPattern
|
||||
};
|
||||
backupConfig.schedulePattern ='00 00 ' + hoursPattern + ' * * ' + daysPattern;
|
||||
|
||||
Client.setBackupPolicy(policy, function (error) {
|
||||
$scope.backupPolicy.busy = false;
|
||||
Client.setBackupConfig(backupConfig, function (error) {
|
||||
$scope.configureScheduleAndRetention.busy = false;
|
||||
|
||||
if (error) {
|
||||
if (error.statusCode === 424) {
|
||||
$scope.backupPolicy.error.generic = error.message;
|
||||
$scope.configureScheduleAndRetention.error.generic = error.message;
|
||||
} else if (error.statusCode === 400) {
|
||||
$scope.backupPolicy.error.generic = error.message;
|
||||
$scope.configureScheduleAndRetention.error.generic = error.message;
|
||||
} else {
|
||||
console.error('Unable to change schedule or retention.', error);
|
||||
}
|
||||
@@ -426,18 +376,13 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
return;
|
||||
}
|
||||
|
||||
$('#backupPolicyModal').modal('hide');
|
||||
$('#configureScheduleAndRetentionModal').modal('hide');
|
||||
|
||||
$scope.backupPolicy.init();
|
||||
getBackupConfig();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('configureBackup.disk', function (newValue) {
|
||||
if (!newValue) return;
|
||||
$scope.configureBackup.mountOptions.diskPath = '/dev/disk/by-uuid/' + newValue.uuid;
|
||||
});
|
||||
|
||||
$scope.configureBackup = {
|
||||
busy: false,
|
||||
error: {},
|
||||
@@ -469,8 +414,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
downloadConcurrency: '',
|
||||
syncConcurrency: '', // sort of similar to upload
|
||||
|
||||
blockDevices: [],
|
||||
disk: null,
|
||||
mountOptions: {
|
||||
host: '',
|
||||
remoteDir: '',
|
||||
@@ -505,7 +448,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.configureBackup.syncConcurrency = $scope.configureBackup.provider === 's3' ? 20 : 10;
|
||||
$scope.configureBackup.copyConcurrency = $scope.configureBackup.provider === 's3' ? 500 : 10;
|
||||
|
||||
$scope.configureBackup.disk = null;
|
||||
$scope.configureBackup.mountOptions = { host: '', remoteDir: '', username: '', password: '', diskPath: '', seal: false, user: '', port: 22, privateKey: '' };
|
||||
},
|
||||
|
||||
@@ -540,13 +482,12 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.configureBackup.useHardlinks = !$scope.backupConfig.noHardlinks;
|
||||
$scope.configureBackup.chown = $scope.backupConfig.chown;
|
||||
|
||||
var limits = $scope.backupConfig.limits || {};
|
||||
$scope.configureBackup.memoryLimit = limits.memoryLimit;
|
||||
$scope.configureBackup.memoryLimit = $scope.backupConfig.memoryLimit;
|
||||
|
||||
$scope.configureBackup.uploadPartSize = limits.uploadPartSize || ($scope.configureBackup.provider === 'scaleway-objectstorage' ? 100 * 1024 * 1024 : 10 * 1024 * 1024);
|
||||
$scope.configureBackup.downloadConcurrency = limits.downloadConcurrency || ($scope.backupConfig.provider === 's3' ? 30 : 10);
|
||||
$scope.configureBackup.syncConcurrency = limits.syncConcurrency || ($scope.backupConfig.provider === 's3' ? 20 : 10);
|
||||
$scope.configureBackup.copyConcurrency = limits.copyConcurrency || ($scope.backupConfig.provider === 's3' ? 500 : 10);
|
||||
$scope.configureBackup.uploadPartSize = $scope.backupConfig.uploadPartSize || ($scope.configureBackup.provider === 'scaleway-objectstorage' ? 100 * 1024 * 1024 : 10 * 1024 * 1024);
|
||||
$scope.configureBackup.downloadConcurrency = $scope.backupConfig.downloadConcurrency || ($scope.backupConfig.provider === 's3' ? 30 : 10);
|
||||
$scope.configureBackup.syncConcurrency = $scope.backupConfig.syncConcurrency || ($scope.backupConfig.provider === 's3' ? 20 : 10);
|
||||
$scope.configureBackup.copyConcurrency = $scope.backupConfig.copyConcurrency || ($scope.backupConfig.provider === 's3' ? 500 : 10);
|
||||
|
||||
var totalMemory = Math.max(($scope.memory.memory + $scope.memory.swap) * 1.5, 2 * 1024 * 1024);
|
||||
$scope.configureBackup.memoryTicks = [ $scope.MIN_MEMORY_LIMIT ];
|
||||
@@ -572,28 +513,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
privateKey: mountOptions.privateKey || ''
|
||||
};
|
||||
|
||||
Client.getBlockDevices(function (error, result) {
|
||||
if (error) return console.error('Failed to list blockdevices:', error);
|
||||
|
||||
// only offer non /, /boot or /home disks
|
||||
result = result.filter(function (d) { return d.mountpoint !== '/' && d.mountpoint !== '/home' && d.mountpoint !== '/boot'; });
|
||||
// only offer xfs and ext4 disks
|
||||
result = result.filter(function (d) { return d.type === 'xfs' || d.type === 'ext4'; });
|
||||
|
||||
// amend label for UI
|
||||
result.forEach(function (d) {
|
||||
d.label = d.path;
|
||||
|
||||
// pre-select current if set
|
||||
if (d.path === $scope.configureBackup.mountOptions.diskPath || ('/dev/disk/by-uuid/' + d.uuid) === $scope.configureBackup.mountOptions.diskPath) {
|
||||
$scope.configureBackup.disk = d;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.configureBackup.blockDevices = result;
|
||||
|
||||
$('#configureBackupModal').modal('show');
|
||||
});
|
||||
$('#configureBackupModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
@@ -603,12 +523,10 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
var backupConfig = {
|
||||
provider: $scope.configureBackup.provider,
|
||||
format: $scope.configureBackup.format,
|
||||
memoryLimit: $scope.configureBackup.memoryLimit,
|
||||
// required for api call to provide all fields
|
||||
schedulePattern: $scope.backupConfig.schedulePattern,
|
||||
retentionPolicy: $scope.backupConfig.retentionPolicy,
|
||||
limits: {
|
||||
memoryLimit: $scope.configureBackup.memoryLimit,
|
||||
},
|
||||
retentionPolicy: $scope.backupConfig.retentionPolicy
|
||||
};
|
||||
if ($scope.configureBackup.password) {
|
||||
backupConfig.password = $scope.configureBackup.password;
|
||||
@@ -652,10 +570,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
} else if (backupConfig.provider === 'vultr-objectstorage') {
|
||||
backupConfig.region = $scope.vultrRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'contabo-objectstorage') {
|
||||
backupConfig.region = $scope.contaboRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
backupConfig.s3ForcePathStyle = true; // https://docs.contabo.com/docs/products/Object-Storage/technical-description (no virtual buckets)
|
||||
} else if (backupConfig.provider === 'upcloud-objectstorage') { // the UI sets region and endpoint
|
||||
var m = /^.*\.(.*)\.upcloudobjects.com$/.exec(backupConfig.endpoint);
|
||||
backupConfig.region = m ? m[1] : 'us-east-1'; // let it fail in validation phase if m is not valid
|
||||
@@ -701,7 +615,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
backupConfig.mountOptions.port = $scope.configureBackup.mountOptions.port;
|
||||
backupConfig.mountOptions.privateKey = $scope.configureBackup.mountOptions.privateKey;
|
||||
}
|
||||
} else if (backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs' || backupConfig.provider === 'disk') {
|
||||
} else if (backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs') {
|
||||
backupConfig.mountOptions.diskPath = $scope.configureBackup.mountOptions.diskPath;
|
||||
} else if (backupConfig.provider === 'mountpoint') {
|
||||
backupConfig.mountPoint = $scope.configureBackup.mountPoint;
|
||||
@@ -713,12 +627,12 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
backupConfig.noHardlinks = !$scope.configureBackup.useHardlinks;
|
||||
}
|
||||
|
||||
backupConfig.limits.uploadPartSize = $scope.configureBackup.uploadPartSize;
|
||||
backupConfig.uploadPartSize = $scope.configureBackup.uploadPartSize;
|
||||
|
||||
if (backupConfig.format === 'rsync') {
|
||||
backupConfig.limits.downloadConcurrency = $scope.configureBackup.downloadConcurrency;
|
||||
backupConfig.limits.syncConcurrency = $scope.configureBackup.syncConcurrency;
|
||||
backupConfig.limits.copyConcurrency = $scope.configureBackup.copyConcurrency;
|
||||
backupConfig.downloadConcurrency = $scope.configureBackup.downloadConcurrency;
|
||||
backupConfig.syncConcurrency = $scope.configureBackup.syncConcurrency;
|
||||
backupConfig.copyConcurrency = $scope.configureBackup.copyConcurrency;
|
||||
}
|
||||
|
||||
Client.setBackupConfig(backupConfig, function (error) {
|
||||
@@ -813,35 +727,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.backupConfig = backupConfig;
|
||||
$scope.mountStatus = null;
|
||||
|
||||
if (!$scope.mountlike($scope.backupConfig.provider)) return;
|
||||
|
||||
Client.getBackupMountStatus(function (error, mountStatus) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.mountStatus = mountStatus;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getBackupTasks() {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_BACKUP, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!tasks.length) return;
|
||||
|
||||
$scope.backupTasks = tasks.slice(0, 10);
|
||||
});
|
||||
}
|
||||
|
||||
function getCleanupTasks() {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_CLEAN_BACKUPS, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!tasks.length) return;
|
||||
|
||||
$scope.cleanupTasks = tasks.slice(0, 10);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -857,12 +742,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.manualBackupApps = Client.getInstalledApps().filter(function (app) { return app.type !== APP_TYPES.LINK && !app.enableBackup; });
|
||||
|
||||
// show backup status
|
||||
$scope.createBackup.init();
|
||||
$scope.cleanupBackups.init();
|
||||
$scope.backupPolicy.init();
|
||||
|
||||
getBackupTasks();
|
||||
getCleanupTasks();
|
||||
$scope.createBackup.checkStatus();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
/* global $:false */
|
||||
|
||||
angular.module('Application').controller('BrandingController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
Client.onReady(function () { if (Client.getUserInfo().role !== 'owner') $location.path('/'); });
|
||||
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
@@ -98,25 +98,6 @@
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.netcupApiPassword" name="netcupApiPassword" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'netcup'">
|
||||
</div>
|
||||
|
||||
<!-- OVH -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'ovh'">
|
||||
<label class="control-label" for="inputConfigureOvhEndpoint">{{ 'domains.domainDialog.ovhEndpoint' | tr }}</label>
|
||||
<select class="form-control" name="endpoint" id="inputConfigureOvhEndpoint" ng-model="domainConfigure.ovhEndpoint" ng-options="a.value as a.name for a in ovhEndpoints" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'ovh'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'ovh'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.ovhConsumerKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.ovhConsumerKey" name="ovhConsumerKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'ovh'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'ovh'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.ovhAppKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.ovhAppKey" name="ovhAppKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'ovh'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'ovh'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.ovhAppSecret' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.ovhAppSecret" name="ovhAppSecret" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'ovh'">
|
||||
</div>
|
||||
|
||||
<!-- Porkbun -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'porkbun'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.porkbunApikey' | tr }}</label>
|
||||
@@ -128,14 +109,6 @@
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.cloudflareTokenType' | tr }}</label>
|
||||
<select class="form-control" ng-model="domainConfigure.cloudflareTokenType">
|
||||
<option value="GlobalApiKey">{{ 'domains.domainDialog.cloudflareTokenTypeGlobalApiKey' | tr }}</option>
|
||||
<option value="ApiToken">{{ 'domains.domainDialog.cloudflareTokenTypeApiToken' | tr }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
<label class="control-label" ng-show="domainConfigure.cloudflareTokenType === 'GlobalApiKey'">{{ 'domains.domainDialog.cloudflareTokenTypeGlobalApiKey' | tr }}</label>
|
||||
<label class="control-label" ng-show="domainConfigure.cloudflareTokenType === 'ApiToken'">{{ 'domains.domainDialog.cloudflareTokenTypeApiToken' | tr }}</label>
|
||||
@@ -153,6 +126,14 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.cloudflareTokenType' | tr }}</label>
|
||||
<select class="form-control" ng-model="domainConfigure.cloudflareTokenType">
|
||||
<option value="GlobalApiKey">{{ 'domains.domainDialog.cloudflareTokenTypeGlobalApiKey' | tr }}</option>
|
||||
<option value="ApiToken">{{ 'domains.domainDialog.cloudflareTokenTypeApiToken' | tr }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Linode -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'linode'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.linodeToken' | tr }}</label>
|
||||
@@ -165,12 +146,6 @@
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.bunnyAccessKey" name="bunnyAccessKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'bunny'">
|
||||
</div>
|
||||
|
||||
<!-- dnsimple -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'dnsimple'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.dnsimpleAccessToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.dnsimpleAccessToken" name="dnsimpleAccessToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'dnsimple'">
|
||||
</div>
|
||||
|
||||
<!-- Hetzner -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'hetzner'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.hetznerToken' | tr }}</label>
|
||||
@@ -356,9 +331,9 @@
|
||||
{{ prettyProviderName(domain) }}
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="domainWellKnown.show(domain)" uib-tooltip="{{ 'domains.tooltipWellKnown' | tr }}"><i class="fa fa-atlas"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="domainConfigure.show(domain)" uib-tooltip="{{ 'domains.tooltipEdit' | tr }}"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="domainRemove.show(domain)" uib-tooltip="{{ 'domains.tooltipRemove' | tr }}" ng-disabled="domain.domain === config.adminDomain"><i class="far fa-trash-alt"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="domainWellKnown.show(domain)" title="{{ 'domains.tooltipWellKnown' | tr }}"><i class="fa fa-atlas"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="domainConfigure.show(domain)" title="{{ 'domains.tooltipEdit' | tr }}"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="domainRemove.show(domain)" title="{{ 'domains.tooltipRemove' | tr }}" ng-disabled="domain.domain === config.adminDomain"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -375,22 +350,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>
|
||||
{{ 'domains.renewCerts.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="renewCerts.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'domains.renewCerts.showLogsAction' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in renewCerts.tasks">
|
||||
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
<div class="text-left">
|
||||
<h3>{{ 'domains.renewCerts.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -414,27 +375,14 @@
|
||||
<p ng-hide="renewCerts.busy">
|
||||
<div class="has-error" ng-show="!renewCerts.active">{{ renewCerts.errorMessage }}</div>
|
||||
</p>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="renewCerts.renew()" ng-disabled="renewCerts.busy">{{ 'domains.renewCerts.renewAllAction' | tr }}</button>
|
||||
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{renewCerts.taskId}}" ng-disabled="!renewCerts.taskId" target="_blank">{{ 'domains.renewCerts.showLogsAction' | tr }}</a>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="renewCerts.renew()" ng-disabled="renewCerts.busy" style="margin-right: 10px">{{ 'domains.renewCerts.renewAllAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>
|
||||
{{ 'domains.syncDns.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="syncDns.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'domains.renewCerts.showLogsAction' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in syncDns.tasks">
|
||||
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
<div class="text-left">
|
||||
<h3>{{ 'domains.syncDns.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -458,27 +406,14 @@
|
||||
<p ng-hide="syncDns.busy">
|
||||
<div class="has-error" ng-show="!syncDns.active">{{ syncDns.errorMessage }}</div>
|
||||
</p>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="syncDns.sync()" ng-disabled="syncDns.busy">{{ 'domains.syncDns.syncAction' | tr }}</button>
|
||||
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{syncDns.taskId}}" ng-disabled="!syncDns.taskId" target="_blank">{{ 'domains.syncDns.showLogsAction' | tr }}</a>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="syncDns.sync()" ng-disabled="syncDns.busy" style="margin-right: 10px">{{ 'domains.syncDns.syncAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>
|
||||
{{ 'domains.changeDashboardDomain.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="changeDashboard.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'domains.renewCerts.showLogsAction' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in changeDashboard.tasks">
|
||||
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
<div class="text-left">
|
||||
<h3>{{ 'domains.changeDashboardDomain.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -503,15 +438,16 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<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-4 text-right">
|
||||
<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">{{ 'domains.changeDashboardDomain.changeAction' | tr }}</button>
|
||||
<button class="btn btn-outline btn-danger" ng-click="changeDashboard.stop()" ng-show="changeDashboard.busy" style="margin-right: 10px">{{ 'domains.changeDashboardDomain.cancelAction' | tr }}</button>
|
||||
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{changeDashboard.taskId}}" ng-show="changeDashboard.busy" target="_blank">{{ 'domains.changeDashboardDomain.showLogsAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
/* global async */
|
||||
/* global angular */
|
||||
/* global $, TASK_TYPES, ENDPOINTS_OVH */
|
||||
/* global $, TASK_TYPES */
|
||||
|
||||
angular.module('Application').controller('DomainsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
@@ -11,7 +11,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.domains = [];
|
||||
$scope.ready = false;
|
||||
$scope.domainSearchString = '';
|
||||
$scope.pageSize = localStorage.cloudronPageSize || 10;
|
||||
$scope.pageSize = 10;
|
||||
$scope.currentPage = 1;
|
||||
|
||||
$scope.showNextPage = function () {
|
||||
@@ -47,7 +47,6 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
{ name: 'Bunny', value: 'bunny' },
|
||||
{ name: 'Cloudflare', value: 'cloudflare' },
|
||||
{ name: 'DigitalOcean', value: 'digitalocean' },
|
||||
{ name: 'DNSimple', value: 'dnsimple' },
|
||||
{ name: 'Gandi LiveDNS', value: 'gandi' },
|
||||
{ name: 'GoDaddy', value: 'godaddy' },
|
||||
{ name: 'Google Cloud DNS', value: 'gcdns' },
|
||||
@@ -56,7 +55,6 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
{ name: 'Name.com', value: 'namecom' },
|
||||
{ name: 'Namecheap', value: 'namecheap' },
|
||||
{ name: 'Netcup', value: 'netcup' },
|
||||
{ name: 'OVH', value: 'ovh' },
|
||||
{ name: 'Porkbun', value: 'porkbun' },
|
||||
{ name: 'Vultr', value: 'vultr' },
|
||||
{ name: 'Wildcard', value: 'wildcard' },
|
||||
@@ -70,14 +68,12 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
case 'route53': return 'AWS Route53';
|
||||
case 'cloudflare': return 'Cloudflare';
|
||||
case 'digitalocean': return 'DigitalOcean';
|
||||
case 'dnsimple': return 'dnsimple';
|
||||
case 'gandi': return 'Gandi LiveDNS';
|
||||
case 'hetzner': return 'Hetzner DNS';
|
||||
case 'linode': return 'Linode';
|
||||
case 'namecom': return 'Name.com';
|
||||
case 'namecheap': return 'Namecheap';
|
||||
case 'netcup': return 'Netcup';
|
||||
case 'ovh': return 'OVH';
|
||||
case 'gcdns': return 'Google Cloud';
|
||||
case 'godaddy': return 'GoDaddy';
|
||||
case 'vultr': return 'Vultr';
|
||||
@@ -89,8 +85,6 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
}
|
||||
};
|
||||
|
||||
$scope.ovhEndpoints = ENDPOINTS_OVH;
|
||||
|
||||
$scope.needsPort80 = function (dnsProvider, tlsProvider) {
|
||||
return ((dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') &&
|
||||
(tlsProvider === 'letsencrypt-prod' || tlsProvider === 'letsencrypt-staging'));
|
||||
@@ -255,7 +249,6 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
cloudflareTokenType: 'GlobalApiKey',
|
||||
linodeToken: '',
|
||||
bunnyAccessKey: '',
|
||||
dnsimpleAccessToken: '',
|
||||
hetznerToken: '',
|
||||
vultrToken: '',
|
||||
nameComToken: '',
|
||||
@@ -265,10 +258,6 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
netcupCustomerNumber: '',
|
||||
netcupApiKey: '',
|
||||
netcupApiPassword: '',
|
||||
ovhEndpoint: 'ovh-eu',
|
||||
ovhConsumerKey: '',
|
||||
ovhAppKey: '',
|
||||
ovhAppSecret: '',
|
||||
porkbunSecretapikey: '',
|
||||
porkbunApikey: '',
|
||||
|
||||
@@ -318,7 +307,6 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.domainConfigure.digitalOceanToken = domain.provider === 'digitalocean' ? domain.config.token : '';
|
||||
$scope.domainConfigure.linodeToken = domain.provider === 'linode' ? domain.config.token : '';
|
||||
$scope.domainConfigure.bunnyAccessKey = domain.provider === 'bunny' ? domain.config.accessKey : '';
|
||||
$scope.domainConfigure.dnsimpleAccessToken = domain.provider === 'dnsimple' ? domain.config.accessToken : '';
|
||||
$scope.domainConfigure.hetznerToken = domain.provider === 'hetzner' ? domain.config.token : '';
|
||||
$scope.domainConfigure.vultrToken = domain.provider === 'vultr' ? domain.config.token : '';
|
||||
$scope.domainConfigure.gandiApiKey = domain.provider === 'gandi' ? domain.config.token : '';
|
||||
@@ -340,11 +328,6 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.domainConfigure.netcupApiKey = domain.provider === 'netcup' ? domain.config.apiKey : '';
|
||||
$scope.domainConfigure.netcupApiPassword = domain.provider === 'netcup' ? domain.config.apiPassword : '';
|
||||
|
||||
$scope.domainConfigure.ovhEndpoint = domain.provider === 'ovh' ? domain.config.endpoint : '';
|
||||
$scope.domainConfigure.ovhConsumerKey = domain.provider === 'ovh' ? domain.config.consumerKey : '';
|
||||
$scope.domainConfigure.ovhAppKey = domain.provider === 'ovh' ? domain.config.appKey : '';
|
||||
$scope.domainConfigure.ovhAppSecret = domain.provider === 'ovh' ? domain.config.appSecret : '';
|
||||
|
||||
$scope.domainConfigure.porkbunApikey = domain.provider === 'porkbun' ? domain.config.apikey : '';
|
||||
$scope.domainConfigure.porkbunSecretapikey = domain.provider === 'porkbun' ? domain.config.secretapikey : '';
|
||||
|
||||
@@ -396,8 +379,6 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
data.token = $scope.domainConfigure.linodeToken;
|
||||
} else if (provider === 'bunny') {
|
||||
data.accessKey = $scope.domainConfigure.bunnyAccessKey;
|
||||
} else if (provider === 'dnsimple') {
|
||||
data.accessToken = $scope.domainConfigure.dnsimpleAccessToken;
|
||||
} else if (provider === 'hetzner') {
|
||||
data.token = $scope.domainConfigure.hetznerToken;
|
||||
} else if (provider === 'vultr') {
|
||||
@@ -422,11 +403,6 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
data.customerNumber = $scope.domainConfigure.netcupCustomerNumber;
|
||||
data.apiKey = $scope.domainConfigure.netcupApiKey;
|
||||
data.apiPassword = $scope.domainConfigure.netcupApiPassword;
|
||||
} else if (provider === 'ovh') {
|
||||
data.endpoint = $scope.domainConfigure.ovhEndpoint;
|
||||
data.consumerKey = $scope.domainConfigure.ovhConsumerKey;
|
||||
data.appKey = $scope.domainConfigure.ovhAppKey;
|
||||
data.appSecret = $scope.domainConfigure.ovhAppSecret;
|
||||
} else if (provider === 'porkbun') {
|
||||
data.apikey = $scope.domainConfigure.porkbunApikey;
|
||||
data.secretapikey = $scope.domainConfigure.porkbunSecretapikey;
|
||||
@@ -496,10 +472,6 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.domainConfigure.netcupCustomerNumber = '';
|
||||
$scope.domainConfigure.netcupApiKey = '';
|
||||
$scope.domainConfigure.netcupApiPassword = '';
|
||||
$scope.domainConfigure.ovhEndpoint = '';
|
||||
$scope.domainConfigure.ovhConsumerKey = '';
|
||||
$scope.domainConfigure.ovhAppKey = '';
|
||||
$scope.domainConfigure.ovhAppSecret = '';
|
||||
$scope.domainConfigure.porkbunApikey = '';
|
||||
$scope.domainConfigure.porkbunSecretapikey = '';
|
||||
$scope.domainConfigure.vultrToken = '';
|
||||
@@ -517,20 +489,21 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
percent: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
tasks: [],
|
||||
taskId: '',
|
||||
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_CHECK_CERTS, function (error, tasks) {
|
||||
checkStatus: function () {
|
||||
Client.getLatestTaskByType(TASK_TYPES.TASK_CHECK_CERTS, function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
$scope.renewCerts.tasks = tasks.slice(0, 10);
|
||||
if ($scope.renewCerts.tasks.length && $scope.renewCerts.tasks[0].active) $scope.renewCerts.updateStatus();
|
||||
|
||||
if (!task) return;
|
||||
|
||||
$scope.renewCerts.taskId = task.id;
|
||||
$scope.renewCerts.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
var taskId = $scope.renewCerts.tasks[0].id;
|
||||
|
||||
Client.getTask(taskId, function (error, data) {
|
||||
Client.getTask($scope.renewCerts.taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.renewCerts.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
@@ -539,8 +512,6 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.renewCerts.percent = 100; // indicates that 'result' is valid
|
||||
$scope.renewCerts.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
$scope.renewCerts.refreshTasks(); // update the tasks list dropdown
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -558,13 +529,15 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.renewCerts.errorMessage = '';
|
||||
|
||||
// always rebuild the nginx configs when triggered via the UI. we assume user is clicking this because something is wrong
|
||||
Client.renewCerts({ rebuild: true }, function (error /*, taskId */) {
|
||||
Client.renewCerts({ rebuild: true }, function (error, taskId) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.renewCerts.errorMessage = error.message;
|
||||
|
||||
$scope.renewCerts.busy = false;
|
||||
} else {
|
||||
$scope.renewCerts.refreshTasks();
|
||||
$scope.renewCerts.taskId = taskId;
|
||||
$scope.renewCerts.updateStatus();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -575,19 +548,21 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
percent: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
tasks: [],
|
||||
taskId: '',
|
||||
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_SYNC_DNS_RECORDS, function (error, tasks) {
|
||||
checkStatus: function () {
|
||||
Client.getLatestTaskByType(TASK_TYPES.TASK_SYNC_DNS_RECORDS, function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
$scope.syncDns.tasks = tasks.slice(0, 10);
|
||||
if ($scope.syncDns.tasks.length && $scope.syncDns.tasks[0].active) $scope.syncDns.updateStatus();
|
||||
|
||||
if (!task) return;
|
||||
|
||||
$scope.syncDns.taskId = task.id;
|
||||
$scope.syncDns.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
var taskId = $scope.syncDns.tasks[0].id;
|
||||
Client.getTask(taskId, function (error, data) {
|
||||
Client.getTask($scope.syncDns.taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.syncDns.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
@@ -596,8 +571,6 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.syncDns.percent = 100; // indicates that 'result' is valid
|
||||
$scope.syncDns.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
$scope.syncDns.refreshTasks(); // update the tasks list dropdown
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -614,13 +587,15 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.syncDns.message = '';
|
||||
$scope.syncDns.errorMessage = '';
|
||||
|
||||
Client.setDnsRecords({}, function (error /*, taskId */) {
|
||||
Client.setDnsRecords({}, function (error, taskId) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.syncDns.errorMessage = error.message;
|
||||
|
||||
$scope.syncDns.busy = false;
|
||||
} else {
|
||||
$scope.syncDns.refreshTasks();
|
||||
$scope.syncDns.taskId = taskId;
|
||||
$scope.syncDns.updateStatus();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -674,21 +649,24 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
taskId: '',
|
||||
selectedDomain: null,
|
||||
adminDomain: null,
|
||||
tasks: [],
|
||||
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_PREPARE_DASHBOARD_LOCATION, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
$scope.changeDashboard.tasks = tasks.slice(0, 10);
|
||||
if ($scope.changeDashboard.tasks.length && $scope.changeDashboard.tasks[0].active) $scope.changeDashboard.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
stop: function () {
|
||||
Client.stopTask($scope.changeDashboard.taskId, function (error) {
|
||||
if (error) console.error(error);
|
||||
$scope.changeDashboard.busy = false;
|
||||
$scope.changeDashboard.refreshTasks();
|
||||
});
|
||||
},
|
||||
|
||||
// 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();
|
||||
});
|
||||
},
|
||||
|
||||
@@ -743,7 +721,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.changeDashboard.busy = false;
|
||||
} else {
|
||||
$scope.changeDashboard.taskId = taskId;
|
||||
$scope.changeDashboard.refreshTasks();
|
||||
$scope.changeDashboard.updateStatus();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -756,9 +734,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.ready = true;
|
||||
});
|
||||
|
||||
$scope.renewCerts.refreshTasks();
|
||||
$scope.syncDns.refreshTasks();
|
||||
$scope.changeDashboard.refreshTasks();
|
||||
$scope.renewCerts.checkStatus();
|
||||
});
|
||||
|
||||
document.getElementById('gcdnsKeyFileInput').onchange = readFileLocally($scope.domainConfigure.gcdnsKey, 'content', 'keyFileName');
|
||||
|
||||
@@ -725,7 +725,7 @@
|
||||
<div id="collapse_dns_{{ record.value }}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<p ng-show="record.name === 'MX' && domain.provider === 'namecheap'">{{ 'email.dnsStatus.namecheapInfo' | tr }} <sup><a ng-href="https://docs.cloudron.io/domains/#namecheap-dns" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<p ng-show="record.name === 'PTR'">{{ 'email.dnsStatus.ptrInfo' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#ptr-record" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<p ng-show="record.name === 'PTR'">{{ 'email.dnsStatus.ptrInfo' | tr }} <sup><a ng-href="https://docs.cloudron.io/troubleshooting/#ptr-record" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<p ng-show="expectedDnsRecords[record.value].name">{{ 'email.dnsStatus.hostname' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].name }}</tt></b></p>
|
||||
<p ng-hide="expectedDnsRecords[record.value].name">{{ 'email.dnsStatus.domain' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].domain }}</tt></b></p>
|
||||
<p>{{ 'email.dnsStatus.type' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].type }}</tt></b></p>
|
||||
|
||||
@@ -64,12 +64,6 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
Client.openSubscriptionSetup($scope.$parent.subscription);
|
||||
};
|
||||
|
||||
function updateMailUsage(mailboxName, quotaLimit) {
|
||||
if (!$scope.mailUsage) $scope.mailUsage = {};
|
||||
if (!$scope.mailUsage[mailboxName]) $scope.mailUsage[mailboxName] = {};
|
||||
$scope.mailUsage[mailboxName].quotaLimit = quotaLimit;
|
||||
}
|
||||
|
||||
function refreshMailUsage() {
|
||||
Client.getMailUsage($scope.domain.domain, function (error, usage) {
|
||||
if (error) console.error(error);
|
||||
@@ -652,7 +646,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
}
|
||||
|
||||
function done() {
|
||||
updateMailUsage($scope.mailboxes.edit.name + '@' + $scope.domain.domain, $scope.mailboxes.edit.storageQuotaEnabled ? $scope.mailboxes.edit.storageQuota : 0); // hack to avoid refresh
|
||||
$scope.mailUsage[$scope.mailboxes.edit.name + '@' + $scope.domain.domain].quotaLimit = $scope.mailboxes.edit.storageQuotaEnabled ? $scope.mailboxes.edit.storageQuota : 0; // hack to avoid refresh
|
||||
|
||||
$scope.mailboxes.edit.busy = false;
|
||||
$scope.mailboxes.edit.error = null;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<h1>
|
||||
{{ 'emails.eventlog.title' | tr }}
|
||||
|
||||
<a class="btn btn-default btn-outline pull-right" href="/frontend/logs.html?id=mail" target="_blank">{{ 'main.action.logs' | tr }}</a>
|
||||
<a class="btn btn-default btn-outline pull-right" href="/logs.html?id=mail" target="_blank">{{ 'main.action.logs' | tr }}</a>
|
||||
<a class="btn btn-default btn-outline pull-right" href="#/emails-queue">{{ 'emails.action.queue' | tr }}</a>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
/* global angular */
|
||||
|
||||
angular.module('Application').controller('EmailsEventlogController', ['$scope', '$location', '$translate', '$timeout', 'Client', function ($scope, $location, $translate, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastOwner) $location.path('/'); });
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<h1>
|
||||
{{ 'emails.queue.title' | tr }}
|
||||
|
||||
<a class="btn btn-default btn-outline pull-right" href="/frontend/logs.html?id=mail" target="_blank">{{ 'main.action.logs' | tr }}</a>
|
||||
<a class="btn btn-default btn-outline pull-right" href="/logs.html?id=mail" target="_blank">{{ 'main.action.logs' | tr }}</a>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
/* global angular */
|
||||
|
||||
angular.module('Application').controller('EmailsQueueController', ['$scope', '$location', '$translate', '$timeout', 'Client', function ($scope, $location, $translate, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastOwner) $location.path('/'); });
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
@@ -1,3 +1,50 @@
|
||||
<!-- Modal change mail server domain -->
|
||||
<div class="modal fade" id="mailLocationModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.changeDomainDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-bind-html=" 'emails.changeDomainDialog.description' | tr "></div>
|
||||
<br>
|
||||
|
||||
<form name="mailLocationForm" role="form" novalidate ng-submit="mailLocation.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailLocationForm.subdomain.$dirty && mailLocationForm.subdomain.$invalid) || (!mailLocationForm.subdomain.$dirty && mailLocation.error)}">
|
||||
<label class="control-label">{{ 'emails.changeDomainDialog.location' | tr }}</label>
|
||||
|
||||
<div class="has-error" ng-show="mailLocation.error">{{ mailLocation.error.message }}</div>
|
||||
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="mailLocation.subdomain" id="mailLocationLocationInput" name="location" placeholder="{{ 'emails.changeDomainDialog.locationPlaceholder' | tr }}" autofocus>
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<span>{{ (!mailLocation.subdomain ? '' : '.') + mailLocation.domain.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="domain in domains">
|
||||
<a href="" ng-click="mailLocation.domain = domain">{{ domain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-warning text-bold" ng-show="mailLocation.domain.provider === 'manual'" ng-bind-html="'emails.changeDomainDialog.manualInfo' | tr:{ domain: mailLocation.domain.domain }"></p>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="mailLocationForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="mailLocation.submit()" ng-disabled="mailLocationForm.$invalid || mailLocation.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailLocation.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change max email size -->
|
||||
<div class="modal fade" id="maxEmailSizeChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
@@ -24,19 +71,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change virtual all mail -->
|
||||
<div class="modal fade" id="virtualAllMailChangeModal" tabindex="-1" role="dialog">
|
||||
<!-- Modal change mailbox sharing -->
|
||||
<div class="modal fade" id="mailboxSharingChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.changeVirtualAllMailDialog.title' | tr }}</h4>
|
||||
<h4 class="modal-title">{{ 'emails.mailboxSharingDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-bind-html=" 'emails.changeVirtualAllMailDialog.description' | tr "></div>
|
||||
<div ng-bind-html=" 'emails.mailboxSharingDialog.description' | tr "></div>
|
||||
<br>
|
||||
<form name="mailboxSharingChangeForm" role="form" novalidate ng-submit="mailboxSharing.submit()" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailboxSharing.enable">{{ 'emails.mailboxSharing.mailboxSharingCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button class="btn btn-primary" ng-click="virtualAllMail.submit(!virtualAllMail.enabled)"><i class="fa fa-circle-notch fa-spin" ng-show="virtualAllMail.busy"></i> {{ virtualAllMail.enabled ? ('main.disableAction' | tr) : ('main.enableAction' | tr) }} </button>
|
||||
<button type="button" class="btn btn-success" ng-click="mailboxSharing.submit()" ng-disabled="mailboxSharing.enable === mailboxSharing.enabled"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxSharing.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -158,11 +216,12 @@
|
||||
<div class="pull-right">
|
||||
<a class="btn btn-default" ng-show="user.isAtLeastOwner" href="#/emails-queue">{{ 'emails.action.queue' | tr }}</a>
|
||||
<a class="btn btn-default" ng-show="user.isAtLeastOwner" href="#/emails-eventlog">{{ 'eventlog.title' | tr }}</a>
|
||||
<!-- hidden for now, until we see a purpose -->
|
||||
<!-- <a class="btn btn-sm btn-default" ng-disabled="user.isAtLeastOwner" href="/filemanager.html?id=mail&type=mail" target="_blank" uib-tooltip="{{ 'app.filemanagerActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-folder"></i></a> -->
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- domain listing -->
|
||||
<div class="text-left">
|
||||
<h3>{{ 'emails.domains.title' | tr }}</h3>
|
||||
</div>
|
||||
@@ -212,8 +271,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- mailbox sharing -->
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastOwner">
|
||||
<div class="text-left" ng-show="user.isAtLeastOwner">
|
||||
<h3>{{ 'emails.mailboxSharing.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
@@ -233,94 +291,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- server location -->
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastOwner">
|
||||
<h3>
|
||||
{{ 'emails.settings.location' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="mailLocation.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'main.action.showLogs' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in mailLocation.tasks">
|
||||
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<p ng-bind-html="'emails.changeDomainDialog.description' | tr"></p>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="mailLocation.subdomain" id="mailLocationLocationInput" name="location" placeholder="{{ 'emails.changeDomainDialog.locationPlaceholder' | tr }}" autofocus>
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<span>{{ (!mailLocation.subdomain ? '' : '.') + mailLocation.domain.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="domain in domains">
|
||||
<a href="" ng-click="mailLocation.domain = domain">{{ domain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div ng-show="mailLocation.busy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ mailLocation.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-warning text-bold" ng-show="mailLocation.domain.provider === 'manual'" ng-bind-html="'emails.changeDomainDialog.manualInfo' | tr:{ domain: mailLocation.domain.domain }"></p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p ng-show="mailLocation.busy">{{ mailLocation.message }}</p>
|
||||
<p ng-hide="mailLocation.busy">
|
||||
<div class="has-error" ng-show="!mailLocation.active">{{ mailLocation.errorMessage }}</div>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
<!-- save is always enabled so that user can "redo" the task -->
|
||||
<button class="btn btn-outline btn-primary" ng-click="mailLocation.change()" ng-hide="mailLocation.busy">{{ 'main.dialog.save' | tr }}</button>
|
||||
<button class="btn btn-outline btn-danger" ng-click="mailLocation.stop()" ng-show="mailLocation.busy" style="margin-right: 10px">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- settings -->
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastOwner">
|
||||
<div class="text-left" ng-show="user.isAtLeastOwner">
|
||||
<h3>{{ 'emails.settings.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-show="user.isAtLeastOwner" style="margin-bottom: 15px;">
|
||||
<p ng-bind-html=" 'emails.settings.info' | tr "></p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'emails.settings.location' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ mailLocation.currentLocation.subdomain + (!mailLocation.currentLocation.subdomain ? '' : '.') + mailLocation.currentLocation.domain.domain }}
|
||||
<a ng-hide="mailLocation.busy" href="" ng-click="mailLocation.show()"><i class="fa fa-edit text-small"></i></a> <!-- ng-disabled does not work for links -->
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'emails.settings.maxMailSize' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ maxEmailSize.currentSize | prettyDecimalSize }} <a href="" ng-click="maxEmailSize.show()"><i class="fa fa-edit text-small"></i></a></span>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'emails.settings.virtualAllMail' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ virtualAllMail.enabled ? 'main.statusEnabled' : 'main.statusDisabled' | tr }} <a href="" ng-click="virtualAllMail.show()"><i class="fa fa-edit text-small"></i></a></span>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'emails.settings.acl' | tr }}</span>
|
||||
</div>
|
||||
@@ -349,6 +341,19 @@
|
||||
<a href="" ng-click="solrConfig.show()"><i class="fa fa-edit text-small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="mailLocation.busy">
|
||||
<div class="col-md-12" style="margin-top: 10px;">
|
||||
{{ 'emails.settings.changeDomainProgress' | tr }}
|
||||
<div style="display: flex; margin: 4px 0;">
|
||||
<div class="progress progress-striped active animateMe" style="flex-grow: 1;">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ mailLocation.percent }}%"></div>
|
||||
</div>
|
||||
<div ng-show="mailLocation.taskMinutesActive >= 2" class="text-danger hand" style="margin: 0 4px;" ng-click="mailLocation.stopTask()" uib-tooltip="Cancel Task"><i class="fas fa-times"></i></div>
|
||||
</div>
|
||||
<p>{{ mailLocation.message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -10,30 +10,39 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.domains = [];
|
||||
|
||||
// this is required because we need to rewrite the MAIL_SERVER_NAME env var
|
||||
$scope.reconfigureEmailApps = function () {
|
||||
var installedApps = Client.getInstalledApps();
|
||||
for (var i = 0; i < installedApps.length; i++) {
|
||||
if (!installedApps[i].manifest.addons.email) continue;
|
||||
|
||||
Client.repairApp(installedApps[i].id, { }, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.mailLocation = {
|
||||
busy: false,
|
||||
percent: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
|
||||
error: null,
|
||||
currentLocation: { domain: null, subdomain: '' },
|
||||
domain: null,
|
||||
subdomain: '',
|
||||
tasks: [],
|
||||
taskId: null,
|
||||
percent: 0,
|
||||
taskMinutesActive: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
reconfigure: false,
|
||||
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_CHANGE_MAIL_LOCATION, function (error, tasks) {
|
||||
stopTask: function () {
|
||||
Client.getLatestTaskByType(TASK_TYPES.TASK_CHANGE_MAIL_LOCATION, function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
$scope.mailLocation.tasks = tasks.slice(0, 10);
|
||||
if ($scope.mailLocation.tasks.length && $scope.mailLocation.tasks[0].active) $scope.mailLocation.updateStatus();
|
||||
});
|
||||
},
|
||||
if (!task.id) return;
|
||||
|
||||
stop: function () {
|
||||
Client.stopTask($scope.mailLocation.tasks[0].id, function (error) {
|
||||
if (error) console.error(error);
|
||||
$scope.mailLocation.busy = false;
|
||||
$scope.mailLocation.refreshTasks();
|
||||
Client.stopTask(task.id, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -41,26 +50,46 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
Client.getMailLocation(function (error, location) {
|
||||
if (error) return console.error('Failed to get max email location', error);
|
||||
|
||||
$scope.mailLocation.currentLocation.subdomain = $scope.mailLocation.subdomain = location.subdomain;
|
||||
$scope.mailLocation.currentLocation.domain = $scope.mailLocation.domain = $scope.domains.find(function (d) { return location.domain === d.domain; });
|
||||
$scope.mailLocation.currentLocation.subdomain = location.subdomain;
|
||||
$scope.mailLocation.currentLocation.domain = $scope.domains.find(function (d) { return location.domain === d.domain; });
|
||||
|
||||
$scope.mailLocation.refreshTasks();
|
||||
Client.getLatestTaskByType(TASK_TYPES.TASK_CHANGE_MAIL_LOCATION, function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
if (!task) return;
|
||||
|
||||
$scope.mailLocation.taskId = task.id;
|
||||
$scope.mailLocation.reconfigure = task.active; // if task is active when this view reloaded, reconfigure email apps when task done
|
||||
$scope.mailLocation.updateStatus();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
var taskId = $scope.mailLocation.tasks[0].id;
|
||||
show: function () {
|
||||
$scope.mailLocation.busy = false;
|
||||
$scope.mailLocation.error = null;
|
||||
|
||||
Client.getTask(taskId, function (error, data) {
|
||||
$scope.mailLocation.domain = $scope.mailLocation.currentLocation.domain;
|
||||
$scope.mailLocation.subdomain = $scope.mailLocation.currentLocation.subdomain;
|
||||
|
||||
$scope.mailLocationForm.$setUntouched();
|
||||
$scope.mailLocationForm.$setPristine();
|
||||
|
||||
$('#mailLocationModal').modal('show');
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
Client.getTask($scope.mailLocation.taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.mailLocation.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.mailLocation.taskId = null;
|
||||
$scope.mailLocation.busy = false;
|
||||
$scope.mailLocation.message = '';
|
||||
$scope.mailLocation.percent = 100;
|
||||
$scope.mailLocation.percent = 0;
|
||||
$scope.taskMinutesActive = 0;
|
||||
$scope.mailLocation.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
$scope.mailLocation.refreshTasks(); // update the tasks list dropdown
|
||||
if ($scope.mailLocation.reconfigure) $scope.reconfigureEmailApps();
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -68,26 +97,32 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
$scope.mailLocation.busy = true;
|
||||
$scope.mailLocation.percent = data.percent;
|
||||
$scope.mailLocation.message = data.message;
|
||||
$scope.mailLocation.taskMinutesActive = moment().diff(moment(data.creationTime), 'minutes');
|
||||
|
||||
window.setTimeout($scope.mailLocation.updateStatus, 1000);
|
||||
});
|
||||
},
|
||||
|
||||
change: function () {
|
||||
submit: function () {
|
||||
$scope.mailLocation.busy = true;
|
||||
$scope.mailLocation.percent = 0;
|
||||
$scope.mailLocation.message = '';
|
||||
$scope.mailLocation.errorMessage = '';
|
||||
|
||||
Client.setMailLocation($scope.mailLocation.subdomain, $scope.mailLocation.domain.domain, function (error, result) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.mailLocation.errorMessage = error.message;
|
||||
$scope.mailLocation.busy = false;
|
||||
} else {
|
||||
$scope.mailLocation.refreshTasks();
|
||||
$scope.mailLocation.error = error;
|
||||
return;
|
||||
}
|
||||
|
||||
// update UI immediately
|
||||
$scope.mailLocation.currentLocation = { subdomain: $scope.mailLocation.subdomain, domain: $scope.mailLocation.domain };
|
||||
|
||||
$scope.mailLocation.taskId = result.taskId;
|
||||
$scope.mailLocation.reconfigure = true; // reconfigure email apps when task done
|
||||
$scope.mailLocation.updateStatus();
|
||||
|
||||
Client.refreshConfig(); // update config.mailFqdn
|
||||
|
||||
$('#mailLocationModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -133,41 +168,6 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
}
|
||||
};
|
||||
|
||||
$scope.virtualAllMail = {
|
||||
busy: false,
|
||||
error: null,
|
||||
enabled: false,
|
||||
|
||||
refresh: function () {
|
||||
Client.getVirtualAllMail(function (error, enabled) {
|
||||
if (error) return console.error('Failed to get max email size', error);
|
||||
|
||||
$scope.virtualAllMail.enabled = enabled;
|
||||
});
|
||||
},
|
||||
|
||||
show: function() {
|
||||
$scope.virtualAllMail.busy = false;
|
||||
$scope.virtualAllMail.error = null;
|
||||
|
||||
$('#virtualAllMailChangeModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function (enable) {
|
||||
$scope.virtualAllMail.busy = true;
|
||||
|
||||
Client.setVirtualAllMail(enable, function (error) {
|
||||
$scope.virtualAllMail.busy = false;
|
||||
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.virtualAllMail.enabled = enable;
|
||||
|
||||
$('#virtualAllMailChangeModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.mailboxSharing = {
|
||||
busy: false,
|
||||
error: null,
|
||||
@@ -438,9 +438,8 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
if (error) return console.error('Failed to fetch usage for domain', domain.domain, error);
|
||||
|
||||
domain.usage = 0;
|
||||
// we used to use quotaValue here but it's quite different wrt diskSize. so choose diskSize consistently
|
||||
// also, quotaValue can be missing for deleted mailboxes that are on disk but removed from dovecot/ldap itself
|
||||
Object.keys(usage).forEach(function (m) { domain.usage += usage[m].diskSize; });
|
||||
// quotaValue can be missing for deleted mailboxes that are on disk but removed from dovecot/ldap itself
|
||||
Object.keys(usage).forEach(function (m) { domain.usage += (usage[m].quotaValue || usage[m].diskSize); });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -457,7 +456,6 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
if ($scope.user.isAtLeastOwner) {
|
||||
$scope.mailLocation.refresh();
|
||||
$scope.maxEmailSize.refresh();
|
||||
$scope.virtualAllMail.refresh();
|
||||
$scope.mailboxSharing.refresh();
|
||||
$scope.spamConfig.refresh();
|
||||
$scope.solrConfig.refresh();
|
||||
|
||||
@@ -32,6 +32,7 @@ angular.module('Application').controller('EventLogController', ['$scope', '$loca
|
||||
{ name: 'app.start', value: 'app.start' },
|
||||
{ name: 'app.stop', value: 'app.stop' },
|
||||
{ name: 'app.restart', value: 'app.restart' },
|
||||
{ name: 'Apptask Crash', value: 'app.task.crash' },
|
||||
{ name: 'backup.cleanup', value: 'backup.cleanup.start' },
|
||||
{ name: 'backup.cleanup.finish', value: 'backup.cleanup.finish' },
|
||||
{ name: 'backup.finish', value: 'backup.finish' },
|
||||
@@ -73,6 +74,7 @@ angular.module('Application').controller('EventLogController', ['$scope', '$loca
|
||||
{ name: 'volume.add', value: 'volume.add' },
|
||||
{ name: 'volume.update', value: 'volume.update' },
|
||||
{ name: 'volume.remove', value: 'volume.update' },
|
||||
{ name: 'System Crash', value: 'system.crash' }
|
||||
];
|
||||
|
||||
$scope.pageItemCount = [
|
||||
|
||||
@@ -65,33 +65,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-disabled="blocklist.busy" ng-click="blocklist.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="blocklist.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Trusted IPs -->
|
||||
<div class="modal fade" id="trustedIpsModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'network.trustedIps.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="trustedIpsChangeForm" role="form" novalidate ng-submit="trustedIps.submit()" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'network.trustedIpRanges' | tr }}</label>
|
||||
<p class="small">{{ 'network.trustedIps.description' | tr }}</p>
|
||||
<div class="has-error" ng-show="trustedIps.error.trustedIps">{{ trustedIps.error.trustedIps }}</div>
|
||||
<textarea ng-model="trustedIps.trustedIps" placeholder="{{ 'network.firewall.configure.blocklistPlaceholder' | tr }}" name="trustedIps" class="form-control" ng-class="{ 'has-error': !trustedIpsChangeForm.trustedIps.$dirty && trustedIps.error.trustedIps }" rows="4"></textarea>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-disabled="trustedIps.busy" ng-click="trustedIps.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="trustedIps.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="blocklist.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="blocklist.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -199,63 +173,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IPv6 -->
|
||||
<div class="text-left section-header">
|
||||
<h3>{{ 'network.ipv6.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{ 'network.ipv6.description' | tr }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-xs-2">
|
||||
<span class="text-muted">{{ 'network.ip.provider' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-10 text-right">
|
||||
<span>{{ prettyIpProviderName(ipv6Configure.provider) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="ipv6Configure.provider !== 'noop'">
|
||||
<div class="col-xs-2">
|
||||
<span class="text-muted">{{ 'network.ip.address' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-10 text-right">
|
||||
<span ng-show="ipv6Configure.ipv6">{{ ipv6Configure.ipv6 }}</span>
|
||||
<span ng-show="!ipv6Configure.ipv6 && ipv6Configure.serverIPv6">{{ ipv6Configure.serverIPv6 }} ({{ 'network.ip.detected' | tr }})</span>
|
||||
<span ng-show="ipv6Configure.displayError" class="text-danger">{{ ipv6Configure.displayError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="ipv6Configure.ifname">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'network.ip.interface' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ ipv6Configure.ifname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-6 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="ipv6Configure.show()">{{ 'network.ip.configure' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Firewall -->
|
||||
<div class="text-left section-header">
|
||||
<div class="text-left" ng-show="user.isAtLeastOwner">
|
||||
<h3>{{ 'network.firewall.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card" ng-show="user.isAtLeastOwner">
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'network.firewall.blockedIpRanges' | tr }}</span>
|
||||
@@ -264,34 +187,61 @@
|
||||
<span>{{ 'network.firewall.blocklist' | tr:{ blockCount: blocklist.currentBlocklistLength } }} <a href="" ng-click="blocklist.show()"><i class="fa fa-edit text-small"></i></a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IPv6 -->
|
||||
<div class="text-left">
|
||||
<h3>{{ 'network.ipv6.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{ 'network.ipv6.description' | tr }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-xs-2">
|
||||
<span class="text-muted">{{ 'network.ip.provider' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-10 text-right">
|
||||
<span>{{ prettyIpProviderName(ipv6Configure.provider) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="ipv6Configure.provider !== 'noop'">
|
||||
<div class="col-xs-2">
|
||||
<span class="text-muted">{{ 'network.ip.address' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-10 text-right">
|
||||
<span ng-show="ipv6Configure.ipv6">{{ ipv6Configure.ipv6 }}</span>
|
||||
<span ng-show="!ipv6Configure.ipv6 && ipv6Configure.serverIPv6">{{ ipv6Configure.serverIPv6 }} ({{ 'network.ip.detected' | tr }})</span>
|
||||
<span ng-show="ipv6Configure.displayError" class="text-danger">{{ ipv6Configure.displayError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="ipv6Configure.ifname">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'network.trustedIpRanges' | tr }}</span>
|
||||
<span class="text-muted">{{ 'network.ip.interface' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ 'network.trustedIps.summary' | tr:{ trustCount: trustedIps.currentTrustedIpsLength } }} <a href="" ng-click="trustedIps.show()"><i class="fa fa-edit text-small"></i></a></span>
|
||||
<span>{{ ipv6Configure.ifname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-6 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="ipv6Configure.show()">{{ 'network.ip.configure' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic DNS -->
|
||||
<div class="text-left section-header">
|
||||
<h3>
|
||||
{{ 'network.dyndns.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="dyndnsConfigure.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'network.dyndns.showLogsAction' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in dyndnsConfigure.tasks">
|
||||
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
<div class="text-left">
|
||||
<h3>{{ 'network.dyndns.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global $, TASK_TYPES */
|
||||
/* global $ */
|
||||
|
||||
angular.module('Application').controller('NetworkController', ['$scope', '$location', '$timeout', 'Client', function ($scope, $location, $timeout, Client) {
|
||||
angular.module('Application').controller('NetworkController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.user = Client.getUserInfo();
|
||||
@@ -37,25 +37,12 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
|
||||
busy: false,
|
||||
error: '',
|
||||
isEnabled: false,
|
||||
tasks: [],
|
||||
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_SYNC_DYNDNS, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
$scope.dyndnsConfigure.tasks = tasks.slice(0, 10);
|
||||
if ($scope.dyndnsConfigure.tasks.length && $scope.dyndnsConfigure.tasks[0].active) {
|
||||
$timeout($scope.renewCerts.refreshTasks, 5000);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
Client.getDynamicDnsConfig(function (error, enabled) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.dyndnsConfigure.isEnabled = enabled;
|
||||
|
||||
$scope.dyndnsConfigure.refreshTasks();
|
||||
});
|
||||
},
|
||||
|
||||
@@ -96,7 +83,7 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
|
||||
}
|
||||
|
||||
$scope.ipv6Configure.provider = result.provider;
|
||||
$scope.ipv6Configure.ipv6 = result.ip || '';
|
||||
$scope.ipv6Configure.ipv6 = result.ipv6 || '';
|
||||
$scope.ipv6Configure.ifname = result.ifname || '';
|
||||
if (result.provider === 'noop') return;
|
||||
|
||||
@@ -106,7 +93,7 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
$scope.ipv6Configure.serverIPv6 = result.ip;
|
||||
$scope.ipv6Configure.serverIPv6 = result.ipv6;
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -129,7 +116,7 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
|
||||
};
|
||||
|
||||
if (config.provider === 'fixed') {
|
||||
config.ip = $scope.ipv6Configure.newIPv6;
|
||||
config.ipv6 = $scope.ipv6Configure.newIPv6;
|
||||
} else if (config.provider === 'network-interface') {
|
||||
config.ifname = $scope.ipv6Configure.newIfname;
|
||||
}
|
||||
@@ -205,50 +192,6 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
|
||||
}
|
||||
};
|
||||
|
||||
$scope.trustedIps = {
|
||||
busy: false,
|
||||
error: {},
|
||||
trustedIps: '',
|
||||
currentTrustedIps: '',
|
||||
currentTrustedIpsLength: 0,
|
||||
|
||||
refresh: function () {
|
||||
Client.getTrustedIps(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.trustedIps.currentTrustedIps = result;
|
||||
$scope.trustedIps.currentTrustedIpsLength = result.split('\n').filter(function (l) { return l.length !== 0 && l[0] !== '#'; }).length;
|
||||
});
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.trustedIps.error = {};
|
||||
$scope.trustedIps.trustedIps = $scope.trustedIps.currentTrustedIps;
|
||||
|
||||
$('#trustedIpsModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.trustedIps.error = {};
|
||||
$scope.trustedIps.busy = true;
|
||||
|
||||
Client.setTrustedIps($scope.trustedIps.trustedIps, function (error) {
|
||||
$scope.trustedIps.busy = false;
|
||||
if (error) {
|
||||
$scope.trustedIps.error.trustedIps = error.message;
|
||||
$scope.trustedIps.error.ip = error.message;
|
||||
$scope.trustedIpsChangeForm.$setPristine();
|
||||
$scope.trustedIpsChangeForm.$setUntouched();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.trustedIps.refresh();
|
||||
|
||||
$('#trustedIpsModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.sysinfo = {
|
||||
busy: false,
|
||||
error: {},
|
||||
@@ -265,17 +208,17 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
|
||||
newIfname: '',
|
||||
|
||||
refresh: function () {
|
||||
Client.getIPv4Config(function (error, result) {
|
||||
Client.getSysinfoConfig(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.sysinfo.provider = result.provider;
|
||||
$scope.sysinfo.ipv4 = result.ip || '';
|
||||
$scope.sysinfo.ipv4 = result.ipv4 || '';
|
||||
$scope.sysinfo.ifname = result.ifname || '';
|
||||
|
||||
Client.getServerIpv4(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.sysinfo.serverIPv4 = result.ip;
|
||||
$scope.sysinfo.serverIPv4 = result.ipv4;
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -298,12 +241,12 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
|
||||
};
|
||||
|
||||
if (config.provider === 'fixed') {
|
||||
config.ip = $scope.sysinfo.newIPv4;
|
||||
config.ipv4 = $scope.sysinfo.newIPv4;
|
||||
} else if (config.provider === 'network-interface') {
|
||||
config.ifname = $scope.sysinfo.newIfname;
|
||||
}
|
||||
|
||||
Client.setIPv4Config(config, function (error) {
|
||||
Client.setSysinfoConfig(config, function (error) {
|
||||
$scope.sysinfo.busy = false;
|
||||
if (error && error.message.indexOf('ipv') !== -1) {
|
||||
$scope.sysinfo.error.ipv4 = error.message;
|
||||
@@ -333,7 +276,6 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
|
||||
|
||||
$scope.dyndnsConfigure.refresh();
|
||||
$scope.ipv6Configure.refresh();
|
||||
$scope.trustedIps.refresh();
|
||||
if ($scope.user.isAtLeastOwner) $scope.blocklist.refresh();
|
||||
});
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card notification-item" ng-repeat="notification in notifications" ng-class="{'notification-unread': !notification.acknowledged }" ng-click="notification.isCollapsed = !notification.isCollapsed" ng-style="{ borderLeftColor: (notification | notificadtionTypeToColor) }">
|
||||
<div class="card notification-item" ng-repeat="notification in notifications" ng-class="{'notification-unread': !notification.acknowledged }" ng-click="notification.isCollapsed = !notification.isCollapsed">
|
||||
<div class="row">
|
||||
<div class="col-xs-12" ng-class="{ 'notification-details': notification.detailsShown }">
|
||||
<span class="notification-title">{{ notification.title }}</span> <small class="text-muted" uib-tooltip="{{ notification.creationTime | prettyLongDate }}">{{ notification.creationTime | prettyDate }}</small>
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
|
||||
<!-- Modal client add -->
|
||||
<div class="modal fade" id="clientAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'oidc.newClientDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ 'oidc.newClientDialog.description' | tr }}
|
||||
<br/>
|
||||
<br/>
|
||||
<form name="clientAddForm" role="form" novalidate ng-submit="clientAdd.submit()" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="clientName">{{ 'oidc.client.name' | tr }}</label>
|
||||
<input type="text" id="clientName" class="form-control" name="clientName" ng-model="clientAdd.name" autofocus required/>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': clientAdd.error.id }">
|
||||
<label class="control-label" for="clientId">{{ 'oidc.client.id' | tr }}</label>
|
||||
<input type="text" id="clientId" class="form-control" name="clientId" ng-model="clientAdd.id" required/>
|
||||
<div class="control-label" ng-show="clientAdd.error.id">
|
||||
<small>{{ clientAdd.error.id }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="clientSecret">{{ 'oidc.client.secret' | tr }}</label>
|
||||
<input type="text" id="clientSecret" class="form-control" name="clientSecret" ng-model="clientAdd.secret" required/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="loginRedirectUri">{{ 'oidc.client.loginRedirectUri' | tr }}</label>
|
||||
<input type="text" id="loginRedirectUri" class="form-control" name="loginRedirectUri" ng-model="clientAdd.loginRedirectUri" required/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="logoutRedirectUri">{{ 'oidc.client.logoutRedirectUri' | tr }}</label>
|
||||
<input type="url" id="logoutRedirectUri" class="form-control" name="logoutRedirectUri" ng-model="clientAdd.logoutRedirectUri"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'oidc.client.signingAlgorithm' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<select class="form-control" ng-model="clientAdd.tokenSignatureAlgorithm">
|
||||
<option value="RS256">RS256</option>
|
||||
<option value="EdDSA">EdDSA</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="clientAddForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</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> {{ 'oidc.newClientDialog.createAction' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal client edit -->
|
||||
<div class="modal fade" id="clientEditModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'oidc.editClientDialog.title' | tr:{ client: clientEdit.id } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="clientEditForm" role="form" novalidate ng-submit="clientEdit.submit()" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputEditClientName">{{ 'oidc.client.name' | tr }}</label>
|
||||
<input type="text" id="inputEditClientName" class="form-control" name="clientName" ng-model="clientEdit.name" autofocus required/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputEditClientSecret">{{ 'oidc.client.secret' | tr }}</label>
|
||||
<input type="text" id="inputEditClientSecret" class="form-control" name="clientSecret" ng-model="clientEdit.secret" required/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputEditLoginRedirectUri">{{ 'oidc.client.loginRedirectUri' | tr }}</label>
|
||||
<input type="text" id="inputEditLoginRedirectUri" class="form-control" name="loginRedirectUri" ng-model="clientEdit.loginRedirectUri" required/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputEditLogoutRedirectUri">{{ 'oidc.client.logoutRedirectUri' | tr }}</label>
|
||||
<input type="url" id="inputEditLogoutRedirectUri" class="form-control" name="logoutRedirectUri" ng-model="clientEdit.logoutRedirectUri"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'oidc.client.signingAlgorithm' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<select class="form-control" ng-model="clientEdit.tokenSignatureAlgorithm">
|
||||
<option value="RS256">RS256</option>
|
||||
<option value="EdDSA">EdDSA</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="clientEditForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="clientEdit.submit()" ng-disabled="clientEditForm.$invalid || clientEdit.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="clientEdit.busy"></i> {{ 'main.dialog.save' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal client delete -->
|
||||
<div class="modal fade" id="clientDeleteModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'oidc.deleteClientDialog.title' | tr:{ client: deleteClient.id } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ 'oidc.deleteClientDialog.description' | tr }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="deleteClient.submit()" ng-disabled="deleteClient.busy"><i class="fa fa-circle-notch fa-spin" ng-show="deleteClient.busy"></i> {{ 'main.dialog.delete' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>{{ 'oidc.title' | tr }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">{{ 'oidc.env.discoveryUrl' | tr }}</td>
|
||||
<td class="text-right" style="vertical-align: top;" ng-click-select>https://{{ config.adminFqdn }}/.well-known/openid-configuration</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">{{ 'oidc.env.authEndpoint' | tr }}</td>
|
||||
<td class="text-right" style="vertical-align: top;" ng-click-select>https://{{ config.adminFqdn }}/openid/auth</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">{{ 'oidc.env.tokenEndpoint' | tr }}</td>
|
||||
<td class="text-right" style="vertical-align: top;" ng-click-select>https://{{ config.adminFqdn }}/openid/token</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">{{ 'oidc.env.keysEndpoint' | tr }}</td>
|
||||
<td class="text-right" style="vertical-align: top;" ng-click-select>https://{{ config.adminFqdn }}/openid/jwks</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">{{ 'oidc.env.profileEndpoint' | tr }}</td>
|
||||
<td class="text-right" style="vertical-align: top;" ng-click-select>https://{{ config.adminFqdn }}/openid/me</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">{{ 'oidc.env.logoutUrl' | tr }}</td>
|
||||
<td class="text-right" style="vertical-align: top;" ng-click-select>https://{{ config.adminFqdn }}/openid/session/end</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'oidc.clients.title' | tr }} <button class="btn btn-primary btn-sm pull-right" ng-click="clientAdd.show()"><i class="fa fa-plus"></i> {{ 'oidc.clients.newClient' | tr }}</button></h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 33%">{{ 'oidc.client.name' | tr }}</th>
|
||||
<th style="width: 33%">{{ 'oidc.client.id' | tr }}</th>
|
||||
<th style="width: 33%">{{ 'oidc.client.signingAlgorithm' | tr }}</th>
|
||||
<th style="width: 10%" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-show="clients.length === 0">
|
||||
<td colspan="3" class="text-center">{{ 'oidc.clients.empty' | tr }}</td>
|
||||
</tr>
|
||||
<tr ng-repeat="client in clients">
|
||||
<td class="text-left elide-table-cell hand" ng-click="clientEdit.show(client)">
|
||||
{{ client.name }}
|
||||
</td>
|
||||
<td class="text-left elide-table-cell hand" ng-click="clientEdit.show(client)">
|
||||
{{ client.id }}
|
||||
</td>
|
||||
<td class="text-left elide-table-cell hand" ng-click="clientEdit.show(client)">
|
||||
{{ client.tokenSignatureAlgorithm }}
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-danger" ng-click="deleteClient.show(client)" uib-tooltip="Delete"><i class="far fa-trash-alt"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="clientEdit.show(client)" uib-tooltip="Edit"><i class="far fa fa-pencil-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,153 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
|
||||
angular.module('Application').controller('OidcController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.clients = [];
|
||||
|
||||
$scope.refreshClients = function () {
|
||||
Client.getOidcClients(function (error, result) {
|
||||
if (error) return console.error('Failed to load oidc clients', error);
|
||||
|
||||
$scope.clients = result;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.clientAdd = {
|
||||
busy: false,
|
||||
error: {},
|
||||
id: '',
|
||||
name: '',
|
||||
secret: '',
|
||||
loginRedirectUri: '',
|
||||
logoutRedirectUri: '',
|
||||
tokenSignatureAlgorithm: '',
|
||||
|
||||
show: function () {
|
||||
$scope.clientAdd.id = '';
|
||||
$scope.clientAdd.secret = '';
|
||||
$scope.clientAdd.name = '';
|
||||
$scope.clientAdd.loginRedirectUri = '';
|
||||
$scope.clientAdd.logoutRedirectUri = '';
|
||||
$scope.clientAdd.tokenSignatureAlgorithm = 'RS256';
|
||||
$scope.clientAdd.busy = false;
|
||||
$scope.clientAdd.error = null;
|
||||
$scope.clientAddForm.$setPristine();
|
||||
|
||||
$('#clientAddModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.clientAdd.busy = true;
|
||||
$scope.clientAdd.error = {};
|
||||
|
||||
Client.addOidcClient($scope.clientAdd.id, $scope.clientAdd.name, $scope.clientAdd.secret, $scope.clientAdd.loginRedirectUri, $scope.clientAdd.logoutRedirectUri, $scope.clientAdd.tokenSignatureAlgorithm, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 409) {
|
||||
$scope.clientAdd.error.id = 'Client ID already exists';
|
||||
$('#clientId').focus();
|
||||
} else {
|
||||
console.error('Unable to add openid client.', error);
|
||||
}
|
||||
|
||||
$scope.clientAdd.busy = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.refreshClients();
|
||||
$scope.clientAdd.busy = false;
|
||||
|
||||
$('#clientAddModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.clientEdit = {
|
||||
busy: false,
|
||||
error: {},
|
||||
id: '',
|
||||
name: '',
|
||||
secret: '',
|
||||
loginRedirectUri: '',
|
||||
logoutRedirectUri: '',
|
||||
tokenSignatureAlgorithm: '',
|
||||
|
||||
show: function (client) {
|
||||
$scope.clientEdit.id = client.id;
|
||||
$scope.clientEdit.name = client.name;
|
||||
$scope.clientEdit.secret = client.secret;
|
||||
$scope.clientEdit.loginRedirectUri = client.loginRedirectUri;
|
||||
$scope.clientEdit.logoutRedirectUri = client.logoutRedirectUri;
|
||||
$scope.clientEdit.tokenSignatureAlgorithm = client.tokenSignatureAlgorithm;
|
||||
$scope.clientEdit.busy = false;
|
||||
$scope.clientEdit.error = null;
|
||||
$scope.clientEditForm.$setPristine();
|
||||
|
||||
$('#clientEditModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.clientEdit.busy = true;
|
||||
$scope.clientEdit.error = {};
|
||||
|
||||
Client.updateOidcClient($scope.clientEdit.id, $scope.clientEdit.name, $scope.clientEdit.secret, $scope.clientEdit.loginRedirectUri, $scope.clientEdit.logoutRedirectUri, $scope.clientEdit.tokenSignatureAlgorithm, function (error) {
|
||||
if (error) {
|
||||
console.error('Unable to edit openid client.', error);
|
||||
|
||||
$scope.clientEdit.busy = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.refreshClients();
|
||||
$scope.clientEdit.busy = false;
|
||||
|
||||
$('#clientEditModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.deleteClient = {
|
||||
busy: false,
|
||||
error: {},
|
||||
id: '',
|
||||
|
||||
show: function (client) {
|
||||
$scope.deleteClient.busy = false;
|
||||
$scope.deleteClient.id = client.id;
|
||||
|
||||
$('#clientDeleteModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
Client.delOidcClient($scope.deleteClient.id, function (error) {
|
||||
$scope.deleteClient.busy = false;
|
||||
|
||||
if (error) return console.error('Failed to delete openid client', error);
|
||||
|
||||
$scope.refreshClients();
|
||||
|
||||
$('#clientDeleteModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.refreshClients();
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['clientAddModal', 'clientEditmodal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find('[autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -437,7 +437,8 @@
|
||||
<br/>
|
||||
<button class="btn btn-default" ng-click="backgroundImageChange.show()">Set Background Image</button>
|
||||
<button class="btn btn-primary pull-right" ng-click="passwordchange.show()" ng-hide="user.source">{{ 'profile.changePasswordAction' | tr }}</button>
|
||||
<button class="btn pull-right" uib-tooltip="{{ user.source ? ('profile.enable2FANotAvailable' | tr) : '' }}" ng-disabled="user.source" ng-class="user.twoFactorAuthenticationEnabled ? 'btn-danger' : 'btn-success'" ng-click="twoFactorAuthentication.show()">{{ user.twoFactorAuthenticationEnabled ? 'profile.disable2FAAction' : 'profile.enable2FAAction' | tr }}</button> </div>
|
||||
<button class="btn pull-right" ng-class="user.twoFactorAuthenticationEnabled ? 'btn-danger' : 'btn-success'" ng-click="twoFactorAuthentication.show()">{{ user.twoFactorAuthenticationEnabled ? 'profile.disable2FAAction' : 'profile.enable2FAAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -540,8 +541,8 @@
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p>{{ 'profile.loginTokens.description' | tr:{ webadminTokenCount: tokens.webadminTokens.length, cliTokenCount: tokens.cliTokens.length } }}</p>
|
||||
<button class="btn btn-outline btn-danger pull-right" ng-click="tokens.revokeAllWebAndCliTokens()" ng-disabled="tokens.busy"><i class="fa fa-circle-notch fa-spin" ng-show="tokens.busy"></i> {{ 'profile.loginTokens.logoutAll' | tr }}</button>
|
||||
<p>{{ 'profile.loginTokens.description' | tr:{ webadminTokenCount: tokens.webadminTokens.length, cliTokenCount: cliTokens.length } }}</p>
|
||||
<button class="btn btn-outline btn-danger pull-right" ng-click="logoutFromAll()" ng-disabled="tokens.busy"><i class="fa fa-circle-notch fa-spin" ng-show="tokens.busy"></i> {{ 'profile.loginTokens.logoutAll' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
/* global async, Clipboard */
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
/* global TOKEN_TYPES */
|
||||
|
||||
angular.module('Application').controller('ProfileController', ['$scope', '$translate', '$location', 'Client', '$timeout', function ($scope, $translate, $location, Client, $timeout) {
|
||||
$scope.user = Client.getUserInfo();
|
||||
@@ -637,10 +636,9 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans
|
||||
$scope.tokens.busy = false;
|
||||
$scope.tokens.allTokens = result;
|
||||
|
||||
// dashboard and development clientIds were issued with 7.5.0
|
||||
$scope.tokens.webadminTokens = result.filter(function (c) { return c.clientId === TOKEN_TYPES.ID_WEBADMIN || c.clientId === TOKEN_TYPES.ID_DEVELOPMENT || c.clientId === 'dashboard' || c.clientId === 'development'; });
|
||||
$scope.tokens.cliTokens = result.filter(function (c) { return c.clientId === TOKEN_TYPES.ID_CLI; });
|
||||
$scope.tokens.apiTokens = result.filter(function (c) { return c.clientId === TOKEN_TYPES.ID_SDK; });
|
||||
$scope.tokens.webadminTokens = result.filter(function (c) { return c.clientId === 'cid-webadmin'; });
|
||||
$scope.tokens.cliTokens = result.filter(function (c) { return c.clientId === 'cid-cli'; });
|
||||
$scope.tokens.apiTokens = result.filter(function (c) { return c.clientId === 'cid-sdk'; });
|
||||
});
|
||||
},
|
||||
|
||||
@@ -711,6 +709,14 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans
|
||||
}
|
||||
};
|
||||
|
||||
$scope.logoutFromAll = function () {
|
||||
Client.destroyOidcSession(function (error) {
|
||||
if (error) console.error('Failed to destroy oidc session', error);
|
||||
|
||||
$scope.tokens.revokeAllWebAndCliTokens();
|
||||
});
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.appPassword.refresh();
|
||||
$scope.tokens.refresh();
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<td class="elide-table-cell"></td>
|
||||
<td class="elide-table-cell text-center"></td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<a class="btn btn-xs btn-default" href="/frontend/logs.html?id=box" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}"><i class="fa fa-file-alt"></i></a>
|
||||
<a class="btn btn-xs btn-default" href="/logs.html?id=box" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}"><i class="fa fa-file-alt"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-repeat="service in services | filter:{ isRedis: false } | orderBy:'name'">
|
||||
@@ -105,7 +105,7 @@
|
||||
</td>
|
||||
<td class="elide-table-cell">
|
||||
<div class="progress progress-striped" ng-show="service.config.memoryLimit">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ service.memoryPercent }}%">{{ service.memoryPercent }}%</div>
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ service.memoryPercent }}%"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="elide-table-cell text-center">
|
||||
@@ -114,7 +114,7 @@
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="serviceConfigure.show(service)" uib-tooltip="{{ 'services.configureActionTooltip' | tr }}" ng-disabled="!service.config.memoryLimit"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="restartService(service.name)" uib-tooltip="{{ 'services.restartActionTooltip' | tr }}"><i class="fa fa-sync-alt" ng-class="{ 'fa-spin': service.status === 'starting' && !service.config.recoveryMode }"></i></button>
|
||||
<a class="btn btn-xs btn-default" ng-href="{{ '/frontend/logs.html?id=' + service.name }}" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}"><i class="fa fa-file-alt"></i></a>
|
||||
<a class="btn btn-xs btn-default" ng-href="{{ '/logs.html?id=' + service.name }}" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}"><i class="fa fa-file-alt"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="hasRedisServices" ng-click="redisServicesExpanded = !redisServicesExpanded" class="hand">
|
||||
@@ -142,7 +142,7 @@
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="serviceConfigure.show(service)" uib-tooltip="{{ 'services.configureActionTooltip' | tr }}" ng-show="service.config.memoryLimit"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="restartService(service.name)" uib-tooltip="{{ 'services.restartActionTooltip' | tr }}"><i class="fa fa-sync-alt" ng-class="{ 'fa-spin': service.status === 'starting' && !service.config.recoveryMode }"></i></button>
|
||||
<a class="btn btn-xs btn-default" ng-href="{{ '/frontend/logs.html?id=' + service.name }}" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}"><i class="fa fa-file-alt"></i></a>
|
||||
<a class="btn btn-xs btn-default" ng-href="{{ '/logs.html?id=' + service.name }}" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}"><i class="fa fa-file-alt"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -119,7 +119,7 @@ angular.module('Application').controller('ServicesController', ['$scope', '$loca
|
||||
},
|
||||
|
||||
resetToDefaults: function () {
|
||||
$scope.serviceConfigure.memoryLimit = 256 * 1024 * 1024; // 256MB default
|
||||
$scope.serviceConfigure.memoryLimit = 536870912; // 512MB default
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<div class="text-left">
|
||||
<h3>{{ 'settings.timezone.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
@@ -228,7 +228,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<div class="text-left">
|
||||
<h3>{{ 'settings.language.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
@@ -251,22 +251,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>
|
||||
{{ 'settings.updates.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="update.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'settings.updates.showLogsAction' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in update.tasks">
|
||||
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
<div class="text-left">
|
||||
<h3>{{ 'settings.updates.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
@@ -300,6 +286,7 @@
|
||||
<div class="row" ng-show="update.busy">
|
||||
<div class="col-md-12">
|
||||
<p >{{ update.message }}</p>
|
||||
<p class="has-error" ng-show="update.errorMessage">{{ update.errorMessage }}. <a ng-class="warning" ng-href="/logs.html?taskId={{update.taskId}}" target="_blank">{{ 'settings.updates.showLogsAction' | tr }}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -313,7 +300,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<div class="text-left">
|
||||
<h3>{{ 'settings.privateDockerRegistry.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false, TASK_TYPES */
|
||||
/* global $:false */
|
||||
|
||||
angular.module('Application').controller('SettingsController', ['$scope', '$location', '$translate', '$rootScope', '$timeout', 'Client', function ($scope, $location, $translate, $rootScope, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
@@ -87,16 +87,8 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
percent: 0,
|
||||
message: 'Downloading',
|
||||
errorMessage: '', // this shows inline
|
||||
taskId: '',
|
||||
skipBackup: false,
|
||||
tasks: [],
|
||||
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_UPDATE, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
$scope.update.tasks = tasks.slice(0, 10);
|
||||
if ($scope.update.tasks.length && $scope.update.tasks[0].active) $scope.update.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
checkNow: function () {
|
||||
$scope.update.checking = true;
|
||||
@@ -116,9 +108,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
},
|
||||
|
||||
stopUpdate: function () {
|
||||
var taskId = $scope.update.tasks[0].id;
|
||||
|
||||
Client.stopTask(taskId, function (error) {
|
||||
Client.stopTask($scope.update.taskId, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 409) {
|
||||
$scope.update.errorMessage = 'No update is currently in progress';
|
||||
@@ -134,8 +124,18 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
});
|
||||
},
|
||||
|
||||
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.getProvisionStatus(function (error, status) {
|
||||
Client.getStatus(function (error, status) {
|
||||
if (error) return $scope.error(error);
|
||||
|
||||
if (window.localStorage.version !== status.version) window.location.reload(true);
|
||||
@@ -143,9 +143,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
var taskId = $scope.update.tasks[0].id;
|
||||
|
||||
Client.getTask(taskId, function (error, data) {
|
||||
Client.getTask($scope.update.taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.update.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
@@ -156,8 +154,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
|
||||
if (!data.errorMessage) $scope.update.reloadIfNeeded(); // assume success
|
||||
|
||||
$scope.update.refreshTasks(); // redundant... update the tasks list dropdown
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -176,7 +172,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
$scope.update.message = '';
|
||||
$scope.update.errorMessage = '';
|
||||
|
||||
Client.update({ skipBackup: $scope.update.skipBackup }, function (error /*, taskId */) {
|
||||
Client.update({ skipBackup: $scope.update.skipBackup }, function (error, taskId) {
|
||||
if (error) {
|
||||
$scope.update.error.generic = error.message;
|
||||
$scope.update.busy = false;
|
||||
@@ -185,7 +181,8 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
|
||||
$('#updateModal').modal('hide');
|
||||
|
||||
$scope.update.refreshTasks();
|
||||
$scope.update.taskId = taskId;
|
||||
$scope.update.updateStatus();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -433,7 +430,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
});
|
||||
});
|
||||
|
||||
$scope.update.refreshTasks();
|
||||
$scope.update.checkStatus();
|
||||
|
||||
if ($scope.user.isAtLeastOwner) getSubscription();
|
||||
});
|
||||
|
||||
@@ -5,20 +5,6 @@
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'support.help.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div ng-bind-html="'support.help.description' | tr:{ docsLink: 'https://docs.cloudron.io/?support_view', packagingLink: 'https://docs.cloudron.io/custom-apps/tutorial/?support_view', forumLink: 'https://forum.cloudron.io/' } | markdown2html"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="text-left">
|
||||
<h3>{{ 'support.ticket.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
@@ -39,20 +25,14 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>Use this form to open support tickets. You can also write directly to <a href="mailto:support@cloudron.io">support@cloudron.io.</p>
|
||||
<ul>
|
||||
<li><a href="https://docs.cloudron.io/apps/?support_view" target="_blank">Knowledge Base & App Docs</a></li>
|
||||
<li><a href="https://docs.cloudron.io/custom-apps/tutorial/?support_view" target="_blank">Custom App Packaging & API</li>
|
||||
<li><a href="https://forum.cloudron.io/" target="_blank">Forum</a></li>
|
||||
</ul>
|
||||
<div ng-bind-html="supportConfig.ticketFormBody | markdown2html"></div>
|
||||
|
||||
<form name="feedbackForm" ng-submit="submitFeedback()">
|
||||
<form ng-show="supportConfig.submitTickets" name="feedbackForm" ng-submit="submitFeedback()">
|
||||
<div class="form-group">
|
||||
<label>{{ 'support.ticket.type' | tr }}</label>
|
||||
<select class="form-control" name="type" style="width: 50%;" ng-model="feedback.type" required ng-disabled="!subscription.emailVerified">
|
||||
<option value="app_error">{{ 'support.ticket.typeApp' | tr }}</option>
|
||||
<option value="ticket">{{ 'support.ticket.typeBug' | tr }}</option>
|
||||
<option value="billing">{{ 'support.ticket.typeBilling' | tr }}</option>
|
||||
<option value="email_error">{{ 'support.ticket.typeEmail' | tr }}</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -87,9 +67,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<div class="text-left">
|
||||
<h3>{{ 'support.remoteSupport.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,69 +9,70 @@ angular.module('Application').controller('SupportController', ['$scope', '$locat
|
||||
$scope.ready = false;
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
// $scope.apps = Client.getInstalledApps();
|
||||
// $scope.appsById = {};
|
||||
$scope.apps = Client.getInstalledApps();
|
||||
$scope.appsById = {};
|
||||
$scope.supportConfig = null;
|
||||
|
||||
// $scope.feedback = {
|
||||
// error: null,
|
||||
// result: null,
|
||||
// busy: false,
|
||||
// enableSshSupport: false,
|
||||
// subject: '',
|
||||
// type: 'app_error',
|
||||
// description: '',
|
||||
// appId: '',
|
||||
// altEmail: ''
|
||||
// };
|
||||
$scope.feedback = {
|
||||
error: null,
|
||||
result: null,
|
||||
busy: false,
|
||||
enableSshSupport: false,
|
||||
subject: '',
|
||||
type: 'app_error',
|
||||
description: '',
|
||||
appId: '',
|
||||
altEmail: ''
|
||||
};
|
||||
|
||||
$scope.toggleSshSupportError = '';
|
||||
$scope.sshSupportEnabled = false;
|
||||
// $scope.subscription = null;
|
||||
$scope.subscription = null;
|
||||
|
||||
// function resetFeedback() {
|
||||
// $scope.feedback.enableSshSupport = false;
|
||||
// $scope.feedback.subject = '';
|
||||
// $scope.feedback.description = '';
|
||||
// $scope.feedback.type = 'app_error';
|
||||
// $scope.feedback.appId = '';
|
||||
// $scope.feedback.altEmail = '';
|
||||
function resetFeedback() {
|
||||
$scope.feedback.enableSshSupport = false;
|
||||
$scope.feedback.subject = '';
|
||||
$scope.feedback.description = '';
|
||||
$scope.feedback.type = 'app_error';
|
||||
$scope.feedback.appId = '';
|
||||
$scope.feedback.altEmail = '';
|
||||
|
||||
// $scope.feedbackForm.$setUntouched();
|
||||
// $scope.feedbackForm.$setPristine();
|
||||
// }
|
||||
$scope.feedbackForm.$setUntouched();
|
||||
$scope.feedbackForm.$setPristine();
|
||||
}
|
||||
|
||||
// $scope.submitFeedback = function () {
|
||||
// $scope.feedback.busy = true;
|
||||
// $scope.feedback.result = null;
|
||||
// $scope.feedback.error = null;
|
||||
$scope.submitFeedback = function () {
|
||||
$scope.feedback.busy = true;
|
||||
$scope.feedback.result = null;
|
||||
$scope.feedback.error = null;
|
||||
|
||||
// var data = {
|
||||
// enableSshSupport: $scope.feedback.enableSshSupport,
|
||||
// subject: $scope.feedback.subject,
|
||||
// description: $scope.feedback.description,
|
||||
// type: $scope.feedback.type,
|
||||
// appId: $scope.feedback.appId,
|
||||
// altEmail: $scope.feedback.altEmail
|
||||
// };
|
||||
var data = {
|
||||
enableSshSupport: $scope.feedback.enableSshSupport,
|
||||
subject: $scope.feedback.subject,
|
||||
description: $scope.feedback.description,
|
||||
type: $scope.feedback.type,
|
||||
appId: $scope.feedback.appId,
|
||||
altEmail: $scope.feedback.altEmail
|
||||
};
|
||||
|
||||
// Client.createTicket(data, function (error, result) {
|
||||
// if (error) {
|
||||
// $scope.feedback.error = error.message;
|
||||
// } else {
|
||||
// $scope.feedback.result = result;
|
||||
// resetFeedback();
|
||||
// }
|
||||
Client.createTicket(data, function (error, result) {
|
||||
if (error) {
|
||||
$scope.feedback.error = error.message;
|
||||
} else {
|
||||
$scope.feedback.result = result;
|
||||
resetFeedback();
|
||||
}
|
||||
|
||||
// $scope.feedback.busy = false;
|
||||
$scope.feedback.busy = false;
|
||||
|
||||
// // refresh state
|
||||
// Client.getRemoteSupport(function (error, enabled) {
|
||||
// if (error) return console.error(error);
|
||||
// refresh state
|
||||
Client.getRemoteSupport(function (error, enabled) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
// $scope.sshSupportEnabled = enabled;
|
||||
// });
|
||||
// });
|
||||
// };
|
||||
$scope.sshSupportEnabled = enabled;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.toggleSshSupport = function () {
|
||||
$scope.toggleSshSupportError = '';
|
||||
@@ -88,22 +89,27 @@ angular.module('Application').controller('SupportController', ['$scope', '$locat
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
Client.getRemoteSupport(function (error, enabled) {
|
||||
Client.getSubscription(function (error, result) {
|
||||
if (error && error.statusCode === 402) return $scope.ready = true; // not yet registered
|
||||
if (error && error.statusCode === 412) return $scope.ready = true; // invalid appstore token
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.sshSupportEnabled = enabled;
|
||||
$scope.subscription = result;
|
||||
|
||||
// Client.getSubscription(function (error, result) {
|
||||
// if (error && error.statusCode === 402) return $scope.ready = true; // not yet registered
|
||||
// if (error && error.statusCode === 412) return $scope.ready = true; // invalid appstore token
|
||||
// if (error) return console.error(error);
|
||||
Client.getSupportConfig(function (error, supportConfig) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
// $scope.subscription = result;
|
||||
$scope.supportConfig = supportConfig;
|
||||
|
||||
// Client.getInstalledApps().forEach(function (app) { $scope.appsById[app.id] = app; });
|
||||
Client.getRemoteSupport(function (error, enabled) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.ready = true;
|
||||
// });
|
||||
Client.getInstalledApps().forEach(function (app) { $scope.appsById[app.id] = app; });
|
||||
|
||||
$scope.sshSupportEnabled = enabled;
|
||||
$scope.ready = true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,54 +4,17 @@
|
||||
<div class="col-md-12">
|
||||
<h1>
|
||||
{{ 'system.title' | tr }}
|
||||
<a class="btn btn-default pull-right" href="/frontend/logs.html?id=box" target="_blank">{{ 'main.action.logs' | tr }}</a>
|
||||
<a class="btn btn-default pull-right" href="/logs.html?id=box" target="_blank">{{ 'main.action.logs' | tr }}</a>
|
||||
<button class="btn btn-default pull-right" ng-click="$parent.reboot.show()">{{ 'main.action.reboot' | tr }}</button>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
||||
<h3 class="graphs-toolbar">
|
||||
{{ 'system.info.title' | tr }}
|
||||
</h3>
|
||||
|
||||
<div class="card card-expand">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 text-muted">{{ 'system.info.platformVersion' | tr }}</div>
|
||||
<div class="col-xs-8 text-right">v{{ config.version }} ({{ config.ubuntuVersion }})</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-4 text-muted">{{ 'system.info.vendor' | tr }}</div>
|
||||
<div class="col-xs-8 text-right">{{ info.sysVendor }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-4 text-muted">{{ 'system.info.product' | tr }}</div>
|
||||
<div class="col-xs-8 text-right">{{ info.productName }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-4 text-muted">CPU</div>
|
||||
<div class="col-xs-8 text-right">{{ cpus.length + ' Core "' + cpus[0].model + '"' }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-4 text-muted">{{ 'system.info.memory' | tr }}</div>
|
||||
<div class="col-xs-8 text-right">{{ memory.memory | prettyDiskSize }} RAM <span ng-show="memory.swap">& {{ memory.swap | prettyDiskSize }} Swap</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-4 text-muted">{{ 'system.info.uptime' | tr }}</div>
|
||||
<div class="col-xs-8 text-right">{{ info.uptimeSecs }}</div>
|
||||
</div>
|
||||
<div class="row" ng-show="info.activationTime">
|
||||
<div class="col-xs-4 text-muted">{{ 'system.info.activationTime' | tr }}</div>
|
||||
<div class="col-xs-8 text-right">{{ info.activationTime | prettyDate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
|
||||
<h3 class="graphs-toolbar">
|
||||
{{ 'system.graphs.title' | tr }}
|
||||
Graphs
|
||||
<div class="graphs-toolbar-actions">
|
||||
<button class="btn btn-sm btn-default" style="margin-right: 5px;" ng-click="graphs.refresh()" ng-disabled="graphs.busy"><i class="fas fa-sync-alt" ng-class="{ 'fa-spin': graphs.busy }"></i></button>
|
||||
<div class="dropdown">
|
||||
@@ -105,7 +68,6 @@
|
||||
</div>
|
||||
<div ng-hide="disks.busy" class="ng-hide">
|
||||
<div class="row" ng-repeat="disk in disks.disks" style="margin-bottom: 20px;">
|
||||
<hr style="margin: 5px 0px;" ng-show="$index !== 0"/>
|
||||
<div class="col-md-12">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between;">
|
||||
<h3 class="no-wrap" style="font-size: 20px;" ng-bind-html="'system.diskUsage.mountedAt' | tr:{ filesystem: disk.filesystem, mountpoint: disk.mountpoint }"></h3>
|
||||
@@ -116,12 +78,11 @@
|
||||
<div class="progress-bar" ng-repeat="content in disk.contents" style="width: {{ content.usage / disk.size * 100 }}%; background-color: {{ content.color }};" uib-tooltip="{{ content.label + ' ' + (content.usage | prettyDiskSize) }}"></div>
|
||||
<div class="text-center text-muted" style="font-size: 12px; line-height: 20px;">{{ disk.available | prettyDiskSize }}</div>
|
||||
</div>
|
||||
<div class="text-right text-muted" style="margin-top: 10px;" ng-show="disk.speed !== -1">{{ 'system.diskUsage.diskSpeed' | tr:{ speed: disk.speed } }}</div>
|
||||
<div class="text-right text-muted" style="margin-top: 10px;">{{ 'system.diskUsage.diskSpeed' | tr:{ speed: disk.speed } }}</div>
|
||||
<p ng-hide="disk.volume">{{ 'system.diskUsage.diskContent' | tr }}:</p>
|
||||
<p ng-show="disk.volume" ng-bind-html="'system.diskUsage.volumeContent' | tr:{ name: disk.volume.name }"></p>
|
||||
<div ng-repeat="content in disk.contents" class="disk-content">
|
||||
<span class="color-indicator" style="background-color: {{ content.color }};"> </span>
|
||||
<span ng-show="content.type === 'cloudron-backup-default'">{{ content.path }} (Old Backups)</span>
|
||||
<span ng-show="content.type === 'standard'">{{ content.label || content.id }}</span>
|
||||
<span ng-show="content.type === 'swap'">{{ content.id }}</span>
|
||||
<span ng-show="content.type === 'app'">
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
/* global TASK_TYPES */
|
||||
/* global Chart */
|
||||
|
||||
angular.module('Application').controller('SystemController', ['$scope', '$location', '$timeout', 'Client', function ($scope, $location, $timeout, Client) {
|
||||
@@ -10,8 +9,6 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.memory = null;
|
||||
$scope.cpus = null;
|
||||
$scope.info = null;
|
||||
$scope.volumesById = {};
|
||||
|
||||
// https://stackoverflow.com/questions/1484506/random-color-generator
|
||||
@@ -88,18 +85,14 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati
|
||||
disk.contents.forEach(function (content) { if (content.path === disk.mountpoint) disk.volume = $scope.volumesById[content.id]; });
|
||||
disk.contents = disk.contents.filter(function (content) { return content.path !== disk.mountpoint; });
|
||||
|
||||
// only show old backups if the size is significant
|
||||
disk.contents = disk.contents.filter(function (content) { return content.id !== 'cloudron-backup-default' || content.usage > 1024*1024*1024; });
|
||||
|
||||
disk.contents.forEach(function (content) {
|
||||
content.color = getNextColor();
|
||||
|
||||
if (content.type === 'app') {
|
||||
content.app = Client.getInstalledAppsByAppId()[content.id];
|
||||
if (!content.app) content.uninstalled = true;
|
||||
} else if (content.type === 'volume') {
|
||||
content.volume = $scope.volumesById[content.id];
|
||||
}
|
||||
if (content.type === 'volume') content.volume = $scope.volumesById[content.id];
|
||||
|
||||
usageOther -= content.usage;
|
||||
});
|
||||
@@ -324,20 +317,6 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
Client.cpus(function (error, cpus) {
|
||||
if (error) console.error(error);
|
||||
$scope.cpus = cpus;
|
||||
});
|
||||
|
||||
Client.systemInfo(function (error, info) {
|
||||
if (error) console.error(error);
|
||||
|
||||
// prettify for UI
|
||||
info.uptimeSecs = moment.duration(info.uptimeSecs, 'seconds').locale(navigator.language).humanize();
|
||||
|
||||
$scope.info = info;
|
||||
});
|
||||
|
||||
Client.memory(function (error, memory) {
|
||||
if (error) console.error(error);
|
||||
|
||||
|
||||
@@ -1,532 +0,0 @@
|
||||
<!-- Modal external ldap -->
|
||||
<div class="modal fade" id="externalLdapModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'users.externalLdapDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="has-error text-center" ng-show="externalLdap.error.generic">{{ externalLdap.error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="ldapProvider">{{ 'users.externalLdap.provider' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#external-directory" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" id="ldapProvider" ng-model="externalLdap.provider" ng-options="a.value as a.name for a in ldapProvider"></select>
|
||||
</div>
|
||||
|
||||
<div uib-collapse="externalLdap.provider === 'noop'">
|
||||
<form name="externalLdapConfigForm" role="form" novalidate ng-submit="externalLdap.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.url }">
|
||||
<label class="control-label" for="inputExternalLdapConfigUrl">{{ 'users.externalLdap.server' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="externalLdap.url" id="inputExternalLdapConfigUrl" name="url" ng-disabled="externalLdap.busy" placeholder="ldaps://example.com:636" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="externalLdap.acceptSelfSignedCerts"> {{ 'users.externalLdap.acceptSelfSignedCert' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
<p class="has-error" ng-show="externalLdap.error.acceptSelfSignedCerts">{{ 'users.externalLdap.errorSelfSignedCert' | tr }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.baseDn }" ng-show="externalLdap.provider !== 'cloudron'">
|
||||
<label class="control-label" for="inputExternalLdapConfigBaseDn">{{ 'users.externalLdap.baseDn' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="externalLdap.baseDn" id="inputExternalLdapConfigBaseDn" name="baseDn" ng-disabled="externalLdap.busy" placeholder="ou=users,dc=example,dc=com" ng-required="externalLdap.provider !== 'cloudron'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.filter }" ng-show="externalLdap.provider !== 'cloudron'">
|
||||
<label class="control-label" for="inputExternalLdapConfigFilter">{{ 'users.externalLdap.filter' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="externalLdap.filter" id="inputExternalLdapConfigFilter" name="filter" ng-disabled="externalLdap.busy" placeholder="(objectClass=inetOrgPerson)" ng-required="externalLdap.provider !== 'cloudron'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.usernameField }" ng-show="externalLdap.provider !== 'cloudron'">
|
||||
<label class="control-label" for="inputExternalLdapConfigUsernameField">{{ 'users.externalLdap.usernameField' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="externalLdap.usernameField" id="inputExternalLdapConfigUsernameField" name="usernameField" ng-disabled="externalLdap.busy" placeholder="uid or sAMAcountName">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="externalLdap.syncGroups"> {{ 'users.externalLdap.syncGroups' | tr }}</sup>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.groupBaseDn }" ng-show="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
|
||||
<label class="control-label" for="inputExternalLdapConfigGroupBaseDn">{{ 'users.externalLdap.groupBaseDn' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="externalLdap.groupBaseDn" id="inputExternalLdapConfigGroupBaseDn" name="groupBaseDn" ng-disabled="externalLdap.busy" placeholder="ou=groups,dc=example,dc=com" ng-required="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.groupFilter }" ng-show="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
|
||||
<label class="control-label" for="inputExternalLdapConfigGroupFilter">{{ 'users.externalLdap.groupFilter' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="externalLdap.groupFilter" id="inputExternalLdapConfigGroupFilter" name="groupFilter" ng-disabled="externalLdap.busy" placeholder="(objectClass=groupOfNames)" ng-required="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.groupnameField }" ng-show="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
|
||||
<label class="control-label" for="inputExternalLdapConfigGroupnameField">{{ 'users.externalLdap.groupnameField' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="externalLdap.groupnameField" id="inputExternalLdapConfigGroupnameField" name="groupnameField" ng-disabled="externalLdap.busy" placeholder="cn" ng-required="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.credentials }" ng-show="externalLdap.provider !== 'cloudron'">
|
||||
<label class="control-label" for="inputExternalLdapConfigBindDn">{{ 'users.externalLdap.bindUsername' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="externalLdap.bindDn" id="inputExternalLdapConfigBindDn" name="bindDn" ng-disabled="externalLdap.busy" placeholder="uid=admin,ou=Users,dc=example,dc=com">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.credentials }">
|
||||
<label class="control-label" for="inputExternalLdapConfigBindPassword">{{ 'users.externalLdap.bindPassword' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="externalLdap.bindPassword" id="inputExternalLdapConfigBindPassword" name="bindPassword" ng-disabled="externalLdap.busy" placeholder="" password-reveal>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="externalLdap.autoCreate"> {{ 'users.externalLdap.autocreateUsersOnLogin' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="externalLdapConfigForm.$invalid"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="externalLdap.submit()" ng-disabled="externalLdapConfigForm.$invalid || externalLdap.busy"><i class="fa fa-circle-notch fa-spin" ng-show="externalLdap.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Modal client add -->
|
||||
<div class="modal fade" id="oidcClientAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'oidc.newClientDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ 'oidc.newClientDialog.description' | tr }}
|
||||
<br/>
|
||||
<br/>
|
||||
<form name="clientAddForm" role="form" novalidate ng-submit="clientAdd.submit()" autocomplete="off">
|
||||
<p class="text-danger" ng-show="clientAdd.error">{{ clientAdd.error }}</p>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="clientName">{{ 'oidc.client.name' | tr }}</label>
|
||||
<input type="text" id="clientName" class="form-control" name="clientName" ng-model="clientAdd.name" autofocus required/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="loginRedirectUri">{{ 'oidc.client.loginRedirectUri' | tr }}</label>
|
||||
<input type="text" id="loginRedirectUri" class="form-control" name="loginRedirectUri" ng-model="clientAdd.loginRedirectUri" required/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'oidc.client.signingAlgorithm' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<select class="form-control" ng-model="clientAdd.tokenSignatureAlgorithm">
|
||||
<option value="RS256">RS256</option>
|
||||
<option value="EdDSA">EdDSA</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="clientAddForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</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> {{ 'oidc.newClientDialog.createAction' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal client edit -->
|
||||
<div class="modal fade" id="oidcClientEditModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'oidc.editClientDialog.title' | tr:{ client: clientEdit.name } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="clientEditForm" role="form" novalidate ng-submit="clientEdit.submit()" autocomplete="off">
|
||||
<p class="text-danger" ng-show="clientEdit.error">{{ clientEdit.error }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'oidc.client.id' | tr }}</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="clientIdInput" class="form-control" ng-value="clientEdit.id" readonly/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-primary" id="clientIdInputClipboardButton" type="button" data-clipboard-target="#clientIdInput"><i class="fa fa-clipboard"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'oidc.client.secret' | tr }}</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="clientSecretInput" class="form-control" ng-value="clientEdit.secret" readonly/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-primary" id="clientSecretInputClipboardButton" type="button" data-clipboard-target="#clientSecretInput"><i class="fa fa-clipboard"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputEditClientName">{{ 'oidc.client.name' | tr }}</label>
|
||||
<input type="text" id="inputEditClientName" class="form-control" name="clientName" ng-model="clientEdit.name" autofocus required/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputEditLoginRedirectUri">{{ 'oidc.client.loginRedirectUri' | tr }}</label>
|
||||
<input type="text" id="inputEditLoginRedirectUri" class="form-control" name="loginRedirectUri" ng-model="clientEdit.loginRedirectUri" required/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'oidc.client.signingAlgorithm' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<select class="form-control" ng-model="clientEdit.tokenSignatureAlgorithm">
|
||||
<option value="RS256">RS256</option>
|
||||
<option value="EdDSA">EdDSA</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="clientEditForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="clientEdit.submit()" ng-disabled="clientEditForm.$invalid || clientEdit.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="clientEdit.busy"></i> {{ 'main.dialog.save' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal client delete -->
|
||||
<div class="modal fade" id="oidcClientDeleteModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'oidc.deleteClientDialog.title' | tr:{ client: deleteClient.id } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ 'oidc.deleteClientDialog.description' | tr }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="deleteClient.submit()" ng-disabled="deleteClient.busy"><i class="fa fa-circle-notch fa-spin" ng-show="deleteClient.busy"></i> {{ 'main.dialog.delete' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content content-large">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>
|
||||
{{ 'users.title' | tr }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
<form name="profileConfigForm" role="form" novalidate ng-submit="profileConfig.submit()" autocomplete="off">
|
||||
<fieldset ng-disabled="profileConfig.busy">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="profileConfig.editableUserProfiles"> {{ 'users.settings.allowProfileEditCheckbox' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#lock-profile" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="profileConfig.mandatory2FA"> {{ 'users.settings.require2FACheckbox' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<span class="has-error" ng-show="profileConfig.errorMessage">{{ profileConfig.errorMessage }}</span>
|
||||
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="profileConfig.submit()" ng-disabled="!profileConfigForm.$dirty || profileConfig.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="profileConfig.busy"></i> {{ 'users.settings.saveAction' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>{{ 'users.externalLdap.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
<div class="row">
|
||||
<div class="col-md-12">{{ 'users.externalLdap.description' | tr }}</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row" ng-hide="config.features.externalLdap">
|
||||
<div class="col-md-12">
|
||||
{{ 'users.externalLdap.subscriptionRequired' | tr }} <a href="" class="pull-right" ng-click="openSubscriptionSetup()">{{ 'users.externalLdap.subscriptionRequiredAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="config.features.externalLdap">
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider === 'noop'">
|
||||
<div class="col-xs-12">
|
||||
<span class="text-muted">{{ 'users.externalLdap.noopInfo' | tr }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.provider' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.provider }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.server' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.url }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.acceptSelfSignedCert' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.acceptSelfSignedCerts ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.baseDn' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.baseDn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.filter' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.filter }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.usernameField' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.usernameField || 'uid' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.syncGroups' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.syncGroups ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron' && externalLdap.currentConfig.syncGroups">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.groupBaseDn' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.groupBaseDn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron' && externalLdap.currentConfig.syncGroups">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.groupFilter' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.groupFilter }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron' && externalLdap.currentConfig.syncGroups">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.groupnameField' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.groupnameField }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.auth' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.bindDn ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.autocreateUsersOnLogin' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.autoCreate ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<br/>
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div ng-show="externalLdap.syncBusy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ externalLdap.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p ng-show="externalLdap.syncBusy">{{ externalLdap.message }}</p>
|
||||
<p ng-hide="externalLdap.syncBusy">
|
||||
<div class="has-error" ng-show="!externalLdap.active">{{ externalLdap.errorMessage }}</div>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 text-right">
|
||||
<button class="btn btn-primary pull-right" ng-click="externalLdap.show()">{{ 'users.externalLdap.configureAction' | tr }}</button>
|
||||
<button class="btn btn-success pull-right" ng-disabled="externalLdap.currentConfig.provider === 'noop'" ng-click="externalLdap.sync()"><i class="fa fa-circle-notch fa-spin" ng-show="externalLdap.syncBusy"></i> {{ 'users.externalLdap.syncAction' | tr }}</button>
|
||||
<a class="btn btn-primary pull-right" ng-show="externalLdap.taskId" ng-href="/frontend/logs.html?taskId={{ externalLdap.taskId }}" target="_blank">{{ 'users.externalLdap.showLogsAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>{{ 'users.exposedLdap.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div>{{ 'users.exposedLdap.description' | tr }}</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<form name="userDirectoryConfigForm" role="form" novalidate ng-submit="userDirectoryConfig.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="userDirectoryConfig.enabled" ng-disabled="userDirectoryConfig.busy"> {{ 'users.exposedLdap.enabled' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#directory-server" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'users.exposedLdap.secret.url' | tr }}</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="userDirectoryUrlInput" ng-value="'ldaps://' + config.adminFqdn + ':636'" readonly name="userDirectoryUrl" class="form-control"/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" id="userDirectoryUrlClipboardButton" data-clipboard-target="#userDirectoryUrlInput"><i class="fa fa-clipboard"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'users.exposedLdap.secret.label' | tr }}</label>
|
||||
<p class="small" ng-bind-html=" 'users.exposedLdap.secret.description' | tr:{ userDN: 'cn=admin,ou=system,dc=cloudron' }"></p>
|
||||
<input type="password" ng-model="userDirectoryConfig.secret" ng-disabled="!userDirectoryConfig.enabled || userDirectoryConfig.busy" name="userDirectorySecret" class="form-control" ng-class="{ 'has-error': !userDirectoryConfigForm.secret.$dirty && userDirectoryConfig.error.secret }" password-reveal/>
|
||||
<div class="has-error" ng-show="userDirectoryConfig.error.secret">{{ userDirectoryConfig.error.secret }}</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'users.exposedLdap.ipRestriction.label' | tr }}</label>
|
||||
<p class="small">{{ 'users.exposedLdap.ipRestriction.description' | tr }}</p>
|
||||
<textarea ng-model="userDirectoryConfig.allowlist" ng-disabled="!userDirectoryConfig.enabled || userDirectoryConfig.busy" placeholder="{{ 'users.exposedLdap.ipRestriction.placeholder' | tr }}" name="allowlist" class="form-control" ng-class="{ 'has-error': !userDirectoryConfigForm.allowlist.$dirty && userDirectoryConfig.error.allowlist }" rows="4"></textarea>
|
||||
<div class="has-error" ng-show="userDirectoryConfig.error.allowlist">{{ userDirectoryConfig.error.allowlist }}</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<span class="has-error" ng-show="userDirectoryConfig.error.generic">{{ userDirectoryConfig.error.generic }}</span>
|
||||
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="userDirectoryConfig.submit()" ng-disabled="!userDirectoryConfigForm.$dirty || userDirectoryConfig.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="userDirectoryConfig.busy"></i> {{ 'users.settings.saveAction' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>{{ 'oidc.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">{{ 'oidc.env.discoveryUrl' | tr }}</td>
|
||||
<td class="text-right" style="vertical-align: top;" ng-click-select>https://{{ config.adminFqdn }}/.well-known/openid-configuration</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
<div>
|
||||
<h4>{{ 'oidc.clients.title' | tr }} <button class="btn btn-primary btn-sm pull-right" ng-click="clientAdd.show()"><i class="fa fa-plus"></i> {{ 'oidc.clients.newClient' | tr }}</button></h4>
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 80%">{{ 'oidc.client.name' | tr }}</th>
|
||||
<th style="width: 20%" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-show="oidcClients.length === 0">
|
||||
<td colspan="3" class="text-center">{{ 'oidc.clients.empty' | tr }}</td>
|
||||
</tr>
|
||||
<tr ng-repeat="client in oidcClients">
|
||||
<td class="text-left elide-table-cell hand" ng-click="clientEdit.show(client)">
|
||||
{{ client.name }}
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-danger" ng-click="deleteClient.show(client)" uib-tooltip="Delete"><i class="far fa-trash-alt"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="clientEdit.show(client)" uib-tooltip="Edit"><i class="fa fa-pencil-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,473 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global Clipboard */
|
||||
/* global $ */
|
||||
|
||||
angular.module('Application').controller('UserSettingsController', ['$scope', '$location', '$translate', '$timeout', 'Client', function ($scope, $location, $translate, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.ldapProvider = [
|
||||
{ name: 'Active Directory', value: 'ad' },
|
||||
{ name: 'Cloudron', value: 'cloudron' },
|
||||
{ name: 'Jumpcloud', value: 'jumpcloud' },
|
||||
{ name: 'Okta', value: 'okta' },
|
||||
{ name: 'Univention Corporate Server (UCS)', value: 'univention' },
|
||||
{ name: 'Other', value: 'other' },
|
||||
{ name: 'Disabled', value: 'noop' }
|
||||
];
|
||||
|
||||
$translate(['users.externalLdap.providerOther', 'users.externalLdap.providerDisabled']).then(function (tr) {
|
||||
if (tr['users.externalLdap.providerOther']) $scope.ldapProvider.find(function (p) { return p.value === 'other'; }).name = tr['users.externalLdap.providerOther'];
|
||||
if (tr['users.externalLdap.providerDisabled']) $scope.ldapProvider.find(function (p) { return p.value === 'noop'; }).name = tr['users.externalLdap.providerDisabled'];
|
||||
});
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.userInfo = Client.getUserInfo();
|
||||
$scope.oidcClients = [];
|
||||
|
||||
$scope.profileConfig = {
|
||||
editableUserProfiles: true,
|
||||
mandatory2FA: false,
|
||||
errorMessage: '',
|
||||
|
||||
refresh: function () {
|
||||
Client.getProfileConfig(function (error, result) {
|
||||
if (error) return console.error('Unable to get directory config.', error);
|
||||
|
||||
$scope.profileConfig.editableUserProfiles = !result.lockUserProfiles;
|
||||
$scope.profileConfig.mandatory2FA = !!result.mandatory2FA;
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
// prevent the current user from getting locked out
|
||||
if ($scope.profileConfig.mandatory2FA && !$scope.userInfo.twoFactorAuthenticationEnabled) return Client.notify('', $translate.instant('users.settings.require2FAWarning'), true, 'error', '#/profile');
|
||||
|
||||
$scope.profileConfig.error = '';
|
||||
$scope.profileConfig.busy = true;
|
||||
$scope.profileConfig.success = false;
|
||||
|
||||
var data = {
|
||||
lockUserProfiles: !$scope.profileConfig.editableUserProfiles,
|
||||
mandatory2FA: $scope.profileConfig.mandatory2FA
|
||||
};
|
||||
|
||||
Client.setProfileConfig(data, function (error) {
|
||||
if (error) $scope.profileConfig.errorMessage = error.message;
|
||||
|
||||
$scope.profileConfig.success = true;
|
||||
|
||||
$scope.profileConfigForm.$setUntouched();
|
||||
$scope.profileConfigForm.$setPristine();
|
||||
|
||||
Client.refreshConfig(); // refresh the $scope.config
|
||||
|
||||
$timeout(function () {
|
||||
$scope.profileConfig.busy = false;
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.userDirectoryConfig = {
|
||||
enabled: false,
|
||||
secret: '',
|
||||
allowlist: '',
|
||||
error: null,
|
||||
|
||||
refresh: function () {
|
||||
Client.getUserDirectoryConfig(function (error, result) {
|
||||
if (error) return console.error('Unable to get exposed ldap config.', error);
|
||||
|
||||
$scope.userDirectoryConfig.enabled = !!result.enabled;
|
||||
$scope.userDirectoryConfig.allowlist = result.allowlist;
|
||||
$scope.userDirectoryConfig.secret = result.secret;
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.userDirectoryConfig.error = null;
|
||||
$scope.userDirectoryConfig.busy = true;
|
||||
$scope.userDirectoryConfig.success = false;
|
||||
|
||||
var data = {
|
||||
enabled: $scope.userDirectoryConfig.enabled,
|
||||
secret: $scope.userDirectoryConfig.secret,
|
||||
allowlist: $scope.userDirectoryConfig.allowlist
|
||||
};
|
||||
|
||||
Client.setUserDirectoryConfig(data, function (error) {
|
||||
$scope.userDirectoryConfig.busy = false;
|
||||
|
||||
if (error && error.statusCode === 400) {
|
||||
if (error.message.indexOf('secret') !== -1) return $scope.userDirectoryConfig.error = { secret: error.message };
|
||||
else return $scope.userDirectoryConfig.error = { allowlist: error.message };
|
||||
}
|
||||
if (error) return $scope.userDirectoryConfig.error = { generic: error.message };
|
||||
|
||||
$scope.userDirectoryConfigForm.$setUntouched();
|
||||
$scope.userDirectoryConfigForm.$setPristine();
|
||||
|
||||
$scope.userDirectoryConfig.success = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.externalLdap = {
|
||||
busy: false,
|
||||
percent: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
error: {},
|
||||
taskId: 0,
|
||||
|
||||
syncBusy: false,
|
||||
|
||||
// fields
|
||||
provider: 'noop',
|
||||
autoCreate: false,
|
||||
url: '',
|
||||
acceptSelfSignedCerts: false,
|
||||
baseDn: '',
|
||||
filter: '',
|
||||
groupBaseDn: '',
|
||||
bindDn: '',
|
||||
bindPassword: '',
|
||||
usernameField: '',
|
||||
|
||||
currentConfig: {},
|
||||
|
||||
checkStatus: function () {
|
||||
Client.getLatestTaskByType('syncExternalLdap', function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!task) return;
|
||||
|
||||
$scope.externalLdap.taskId = task.id;
|
||||
$scope.externalLdap.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
sync: function () {
|
||||
$scope.externalLdap.syncBusy = true;
|
||||
|
||||
Client.startExternalLdapSync(function (error, taskId) {
|
||||
if (error) {
|
||||
$scope.externalLdap.syncBusy = false;
|
||||
console.error('Unable to start ldap syncer task.', error);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.externalLdap.taskId = taskId;
|
||||
$scope.externalLdap.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function() {
|
||||
Client.getExternalLdapConfig(function (error, result) {
|
||||
if (error) return console.error('Unable to get external ldap config.', error);
|
||||
|
||||
$scope.externalLdap.currentConfig = result;
|
||||
$scope.externalLdap.checkStatus();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
Client.getTask($scope.externalLdap.taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.externalLdap.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.externalLdap.syncBusy = false;
|
||||
$scope.externalLdap.message = '';
|
||||
$scope.externalLdap.percent = 100; // indicates that 'result' is valid
|
||||
$scope.externalLdap.errorMessage = data.success ? '' : data.error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.externalLdap.syncBusy = true;
|
||||
$scope.externalLdap.percent = data.percent;
|
||||
$scope.externalLdap.message = data.message;
|
||||
window.setTimeout($scope.externalLdap.updateStatus, 3000);
|
||||
});
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.externalLdap.busy = false;
|
||||
$scope.externalLdap.error = {};
|
||||
|
||||
$scope.externalLdap.provider = $scope.externalLdap.currentConfig.provider;
|
||||
$scope.externalLdap.url = $scope.externalLdap.currentConfig.url;
|
||||
$scope.externalLdap.acceptSelfSignedCerts = $scope.externalLdap.currentConfig.acceptSelfSignedCerts;
|
||||
$scope.externalLdap.baseDn = $scope.externalLdap.currentConfig.baseDn;
|
||||
$scope.externalLdap.filter = $scope.externalLdap.currentConfig.filter;
|
||||
$scope.externalLdap.syncGroups = $scope.externalLdap.currentConfig.syncGroups;
|
||||
$scope.externalLdap.groupBaseDn = $scope.externalLdap.currentConfig.groupBaseDn;
|
||||
$scope.externalLdap.groupFilter = $scope.externalLdap.currentConfig.groupFilter;
|
||||
$scope.externalLdap.groupnameField = $scope.externalLdap.currentConfig.groupnameField;
|
||||
$scope.externalLdap.bindDn = $scope.externalLdap.currentConfig.bindDn;
|
||||
$scope.externalLdap.bindPassword = $scope.externalLdap.currentConfig.bindPassword;
|
||||
$scope.externalLdap.usernameField = $scope.externalLdap.currentConfig.usernameField;
|
||||
$scope.externalLdap.autoCreate = $scope.externalLdap.currentConfig.autoCreate;
|
||||
|
||||
$('#externalLdapModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.externalLdap.busy = true;
|
||||
$scope.externalLdap.error = {};
|
||||
|
||||
var config = {
|
||||
provider: $scope.externalLdap.provider
|
||||
};
|
||||
|
||||
if ($scope.externalLdap.provider === 'cloudron') {
|
||||
config.url = $scope.externalLdap.url;
|
||||
config.acceptSelfSignedCerts = $scope.externalLdap.acceptSelfSignedCerts;
|
||||
config.autoCreate = $scope.externalLdap.autoCreate;
|
||||
config.syncGroups = $scope.externalLdap.syncGroups;
|
||||
config.bindPassword = $scope.externalLdap.bindPassword;
|
||||
|
||||
// those values are known and thus overwritten
|
||||
config.baseDn = 'ou=users,dc=cloudron';
|
||||
config.filter = '(objectClass=inetOrgPerson)';
|
||||
config.usernameField = 'username';
|
||||
config.groupBaseDn = 'ou=groups,dc=cloudron';
|
||||
config.groupFilter = '(objectClass=group)';
|
||||
config.groupnameField = 'cn';
|
||||
config.bindDn = 'cn=admin,ou=system,dc=cloudron';
|
||||
} else if ($scope.externalLdap.provider !== 'noop') {
|
||||
config.url = $scope.externalLdap.url;
|
||||
config.acceptSelfSignedCerts = $scope.externalLdap.acceptSelfSignedCerts;
|
||||
config.baseDn = $scope.externalLdap.baseDn;
|
||||
config.filter = $scope.externalLdap.filter;
|
||||
config.usernameField = $scope.externalLdap.usernameField;
|
||||
config.syncGroups = $scope.externalLdap.syncGroups;
|
||||
config.groupBaseDn = $scope.externalLdap.groupBaseDn;
|
||||
config.groupFilter = $scope.externalLdap.groupFilter;
|
||||
config.groupnameField = $scope.externalLdap.groupnameField;
|
||||
config.autoCreate = $scope.externalLdap.autoCreate;
|
||||
|
||||
if ($scope.externalLdap.bindDn) {
|
||||
config.bindDn = $scope.externalLdap.bindDn;
|
||||
config.bindPassword = $scope.externalLdap.bindPassword;
|
||||
}
|
||||
}
|
||||
|
||||
Client.setExternalLdapConfig(config, function (error) {
|
||||
$scope.externalLdap.busy = false;
|
||||
|
||||
if (error) {
|
||||
if (error.statusCode === 424) {
|
||||
if (error.code === 'SELF_SIGNED_CERT_IN_CHAIN') $scope.externalLdap.error.acceptSelfSignedCerts = true;
|
||||
else $scope.externalLdap.error.url = true;
|
||||
} else if (error.statusCode === 400 && error.message === 'invalid baseDn') {
|
||||
$scope.externalLdap.error.baseDn = true;
|
||||
} else if (error.statusCode === 400 && error.message === 'invalid filter') {
|
||||
$scope.externalLdap.error.filter = true;
|
||||
} else if (error.statusCode === 400 && error.message === 'invalid groupBaseDn') {
|
||||
$scope.externalLdap.error.groupBaseDn = true;
|
||||
} else if (error.statusCode === 400 && error.message === 'invalid groupFilter') {
|
||||
$scope.externalLdap.error.groupFilter = true;
|
||||
} else if (error.statusCode === 400 && error.message === 'invalid groupnameField') {
|
||||
$scope.externalLdap.error.groupnameField = true;
|
||||
} else if (error.statusCode === 400 && error.message === 'invalid bind credentials') {
|
||||
$scope.externalLdap.error.credentials = true;
|
||||
} else if (error.statusCode === 400 && error.message === 'invalid usernameField') {
|
||||
$scope.externalLdap.error.usernameField = true;
|
||||
} else {
|
||||
console.error('Failed to set external LDAP config:', error);
|
||||
$scope.externalLdap.error.generic = error.message;
|
||||
}
|
||||
} else {
|
||||
$('#externalLdapModal').modal('hide');
|
||||
$scope.externalLdap.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.refreshOIDCClients = function () {
|
||||
Client.getOidcClients(function (error, result) {
|
||||
if (error) return console.error('Failed to load oidc clients', error);
|
||||
|
||||
$scope.oidcClients = result;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.clientAdd = {
|
||||
busy: false,
|
||||
error: null,
|
||||
name: '',
|
||||
loginRedirectUri: '',
|
||||
tokenSignatureAlgorithm: '',
|
||||
|
||||
show: function () {
|
||||
$scope.clientAdd.name = '';
|
||||
$scope.clientAdd.loginRedirectUri = '';
|
||||
$scope.clientAdd.tokenSignatureAlgorithm = 'RS256';
|
||||
$scope.clientAdd.busy = false;
|
||||
$scope.clientAdd.error = null;
|
||||
$scope.clientAddForm.$setPristine();
|
||||
|
||||
$('#oidcClientAddModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.clientAdd.busy = true;
|
||||
$scope.clientAdd.error = null;
|
||||
|
||||
Client.addOidcClient($scope.clientAdd.name, $scope.clientAdd.loginRedirectUri, $scope.clientAdd.tokenSignatureAlgorithm, function (error) {
|
||||
if (error) {
|
||||
$scope.clientAdd.error = error.message;
|
||||
console.error('Unable to add openid client.', error);
|
||||
$scope.clientAdd.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.refreshOIDCClients();
|
||||
$scope.clientAdd.busy = false;
|
||||
|
||||
$('#oidcClientAddModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.clientEdit = {
|
||||
busy: false,
|
||||
error: null,
|
||||
id: '',
|
||||
secret: '',
|
||||
name: '',
|
||||
loginRedirectUri: '',
|
||||
tokenSignatureAlgorithm: '',
|
||||
|
||||
show: function (client) {
|
||||
$scope.clientEdit.id = client.id;
|
||||
$scope.clientEdit.secret = client.secret;
|
||||
$scope.clientEdit.name = client.name;
|
||||
$scope.clientEdit.loginRedirectUri = client.loginRedirectUri;
|
||||
$scope.clientEdit.tokenSignatureAlgorithm = client.tokenSignatureAlgorithm;
|
||||
$scope.clientEdit.busy = false;
|
||||
$scope.clientEdit.error = null;
|
||||
$scope.clientEditForm.$setPristine();
|
||||
|
||||
$('#oidcClientEditModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.clientEdit.busy = true;
|
||||
$scope.clientEdit.error = null;
|
||||
|
||||
Client.updateOidcClient($scope.clientEdit.id, $scope.clientEdit.name, $scope.clientEdit.loginRedirectUri, $scope.clientEdit.tokenSignatureAlgorithm, function (error) {
|
||||
if (error) {
|
||||
$scope.clientEdit.error = error.message;
|
||||
console.error('Unable to edit openid client.', error);
|
||||
$scope.clientEdit.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.refreshOIDCClients();
|
||||
$scope.clientEdit.busy = false;
|
||||
|
||||
$('#oidcClientEditModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.deleteClient = {
|
||||
busy: false,
|
||||
error: {},
|
||||
id: '',
|
||||
|
||||
show: function (client) {
|
||||
$scope.deleteClient.busy = false;
|
||||
$scope.deleteClient.id = client.id;
|
||||
|
||||
$('#oidcClientDeleteModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
Client.delOidcClient($scope.deleteClient.id, function (error) {
|
||||
$scope.deleteClient.busy = false;
|
||||
|
||||
if (error) return console.error('Failed to delete openid client', error);
|
||||
|
||||
$scope.refreshOIDCClients();
|
||||
|
||||
$('#oidcClientDeleteModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.externalLdap.refresh();
|
||||
$scope.profileConfig.refresh();
|
||||
$scope.userDirectoryConfig.refresh();
|
||||
$scope.refreshOIDCClients();
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['oidcClientAddModal', 'oidcClientEditModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find('[autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
|
||||
new Clipboard('#userDirectoryUrlClipboardButton').on('success', function(e) {
|
||||
$('#userDirectoryUrlClipboardButton').tooltip({
|
||||
title: 'Copied!',
|
||||
trigger: 'manual'
|
||||
}).tooltip('show');
|
||||
|
||||
$timeout(function () { $('#userDirectoryUrlClipboardButton').tooltip('hide'); }, 2000);
|
||||
|
||||
e.clearSelection();
|
||||
}).on('error', function(/*e*/) {
|
||||
$('#userDirectoryUrlClipboardButton').tooltip({
|
||||
title: 'Press Ctrl+C to copy',
|
||||
trigger: 'manual'
|
||||
}).tooltip('show');
|
||||
|
||||
$timeout(function () { $('#userDirectoryUrlClipboardButton').tooltip('hide'); }, 2000);
|
||||
});
|
||||
|
||||
new Clipboard('#clientIdInputClipboardButton').on('success', function(e) {
|
||||
$('#clientIdInputClipboardButton').tooltip({
|
||||
title: 'Copied!',
|
||||
trigger: 'manual'
|
||||
}).tooltip('show');
|
||||
|
||||
$timeout(function () { $('#clientIdInputClipboardButton').tooltip('hide'); }, 2000);
|
||||
|
||||
e.clearSelection();
|
||||
}).on('error', function(/*e*/) {
|
||||
$('#clientIdInputClipboardButton').tooltip({
|
||||
title: 'Press Ctrl+C to copy',
|
||||
trigger: 'manual'
|
||||
}).tooltip('show');
|
||||
|
||||
$timeout(function () { $('#clientIdInputClipboardButton').tooltip('hide'); }, 2000);
|
||||
});
|
||||
|
||||
new Clipboard('#clientSecretInputClipboardButton').on('success', function(e) {
|
||||
$('#clientSecretInputClipboardButton').tooltip({
|
||||
title: 'Copied!',
|
||||
trigger: 'manual'
|
||||
}).tooltip('show');
|
||||
|
||||
$timeout(function () { $('#clientSecretInputClipboardButton').tooltip('hide'); }, 2000);
|
||||
|
||||
e.clearSelection();
|
||||
}).on('error', function(/*e*/) {
|
||||
$('#clientSecretInputClipboardButton').tooltip({
|
||||
title: 'Press Ctrl+C to copy',
|
||||
trigger: 'manual'
|
||||
}).tooltip('show');
|
||||
|
||||
$timeout(function () { $('#clientSecretInputClipboardButton').tooltip('hide'); }, 2000);
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,3 +1,39 @@
|
||||
<!-- Modal subscription -->
|
||||
<div class="modal fade" id="subscriptionRequiredModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'users.subscriptionDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>To add more users, please setup a paid plain.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="openSubscriptionSetup()">{{ 'users.subscriptionDialog.setupAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal subscription group -->
|
||||
<div class="modal fade" id="subscriptionRequiredGroupModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'users.subscriptionDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>User groups are part of the business plan.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="openSubscriptionSetup()">{{ 'users.subscriptionDialog.setupAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal make user local -->
|
||||
<div class="modal fade" id="makeLocalModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
@@ -447,11 +483,112 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal external ldap -->
|
||||
<div class="modal fade" id="externalLdapModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'users.externalLdapDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="has-error text-center" ng-show="externalLdap.error.generic">{{ externalLdap.error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="ldapProvider">{{ 'users.externalLdap.provider' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#external-directory" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" id="ldapProvider" ng-model="externalLdap.provider" ng-options="a.value as a.name for a in ldapProvider"></select>
|
||||
</div>
|
||||
|
||||
<div uib-collapse="externalLdap.provider === 'noop'">
|
||||
<form name="externalLdapConfigForm" role="form" novalidate ng-submit="externalLdap.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.url }">
|
||||
<label class="control-label" for="inputExternalLdapConfigUrl">{{ 'users.externalLdap.server' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="externalLdap.url" id="inputExternalLdapConfigUrl" name="url" ng-disabled="externalLdap.busy" placeholder="ldaps://example.com:636" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="externalLdap.acceptSelfSignedCerts"> {{ 'users.externalLdap.acceptSelfSignedCert' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
<p class="has-error" ng-show="externalLdap.error.acceptSelfSignedCerts">{{ 'users.externalLdap.errorSelfSignedCert' | tr }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.baseDn }" ng-show="externalLdap.provider !== 'cloudron'">
|
||||
<label class="control-label" for="inputExternalLdapConfigBaseDn">{{ 'users.externalLdap.baseDn' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="externalLdap.baseDn" id="inputExternalLdapConfigBaseDn" name="baseDn" ng-disabled="externalLdap.busy" placeholder="ou=users,dc=example,dc=com" ng-required="externalLdap.provider !== 'cloudron'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.filter }" ng-show="externalLdap.provider !== 'cloudron'">
|
||||
<label class="control-label" for="inputExternalLdapConfigFilter">{{ 'users.externalLdap.filter' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="externalLdap.filter" id="inputExternalLdapConfigFilter" name="filter" ng-disabled="externalLdap.busy" placeholder="(objectClass=inetOrgPerson)" ng-required="externalLdap.provider !== 'cloudron'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.usernameField }" ng-show="externalLdap.provider !== 'cloudron'">
|
||||
<label class="control-label" for="inputExternalLdapConfigUsernameField">{{ 'users.externalLdap.usernameField' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="externalLdap.usernameField" id="inputExternalLdapConfigUsernameField" name="usernameField" ng-disabled="externalLdap.busy" placeholder="uid or sAMAcountName">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="externalLdap.syncGroups"> {{ 'users.externalLdap.syncGroups' | tr }}</sup>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.groupBaseDn }" ng-show="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
|
||||
<label class="control-label" for="inputExternalLdapConfigGroupBaseDn">{{ 'users.externalLdap.groupBaseDn' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="externalLdap.groupBaseDn" id="inputExternalLdapConfigGroupBaseDn" name="groupBaseDn" ng-disabled="externalLdap.busy" placeholder="ou=groups,dc=example,dc=com" ng-required="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.groupFilter }" ng-show="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
|
||||
<label class="control-label" for="inputExternalLdapConfigGroupFilter">{{ 'users.externalLdap.groupFilter' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="externalLdap.groupFilter" id="inputExternalLdapConfigGroupFilter" name="groupFilter" ng-disabled="externalLdap.busy" placeholder="(objectClass=groupOfNames)" ng-required="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.groupnameField }" ng-show="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
|
||||
<label class="control-label" for="inputExternalLdapConfigGroupnameField">{{ 'users.externalLdap.groupnameField' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="externalLdap.groupnameField" id="inputExternalLdapConfigGroupnameField" name="groupnameField" ng-disabled="externalLdap.busy" placeholder="cn" ng-required="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.credentials }" ng-show="externalLdap.provider !== 'cloudron'">
|
||||
<label class="control-label" for="inputExternalLdapConfigBindDn">{{ 'users.externalLdap.bindUsername' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="externalLdap.bindDn" id="inputExternalLdapConfigBindDn" name="bindDn" ng-disabled="externalLdap.busy" placeholder="uid=admin,ou=Users,dc=example,dc=com">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.credentials }">
|
||||
<label class="control-label" for="inputExternalLdapConfigBindPassword">{{ 'users.externalLdap.bindPassword' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="externalLdap.bindPassword" id="inputExternalLdapConfigBindPassword" name="bindPassword" ng-disabled="externalLdap.busy" placeholder="" password-reveal>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="externalLdap.autoCreate"> {{ 'users.externalLdap.autocreateUsersOnLogin' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="externalLdapConfigForm.$invalid"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="externalLdap.submit()" ng-disabled="externalLdapConfigForm.$invalid || externalLdap.busy"><i class="fa fa-circle-notch fa-spin" ng-show="externalLdap.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content content-large">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>
|
||||
{{ 'main.navbar.users' | tr }}
|
||||
{{ 'users.title' | tr }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -535,7 +672,6 @@
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="showPrevPage()" ng-class="{ 'btn-primary': currentPage > 1 }" ng-disabled="userRefreshBusy || currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
<span style="margin: 0 5px; line-height: 1.5; font-size: 12px;">{{ currentPage }}</span>
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="showNextPage()" ng-class="{ 'btn-primary': users.length > pageItems }" ng-disabled="userRefreshBusy || users.length < pageItems">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -591,4 +727,265 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left" style="margin-top: 50px;" ng-show="user.isAtLeastAdmin">
|
||||
<h3>{{ 'users.settings.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" ng-show="user.isAtLeastAdmin">
|
||||
<form name="profileConfigForm" role="form" novalidate ng-submit="profileConfig.submit()" autocomplete="off">
|
||||
<fieldset ng-disabled="profileConfig.busy">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="profileConfig.editableUserProfiles"> {{ 'users.settings.allowProfileEditCheckbox' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#lock-profile" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="profileConfig.mandatory2FA"> {{ 'users.settings.require2FACheckbox' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<span class="has-error" ng-show="profileConfig.errorMessage">{{ profileConfig.errorMessage }}</span>
|
||||
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="profileConfig.submit()" ng-disabled="!profileConfigForm.$dirty || profileConfig.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="profileConfig.busy"></i> {{ 'users.settings.saveAction' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="text-left" style="margin-top: 50px;" ng-show="user.isAtLeastAdmin">
|
||||
<h3>{{ 'users.externalLdap.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" ng-show="user.isAtLeastAdmin">
|
||||
<div class="row">
|
||||
<div class="col-md-12">{{ 'users.externalLdap.description' | tr }}</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row" ng-hide="config.features.externalLdap">
|
||||
<div class="col-md-12">
|
||||
{{ 'users.externalLdap.subscriptionRequired' | tr }} <a href="" class="pull-right" ng-click="openSubscriptionSetup()">{{ 'users.externalLdap.subscriptionRequiredAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="config.features.externalLdap">
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider === 'noop'">
|
||||
<div class="col-xs-12">
|
||||
<span class="text-muted">{{ 'users.externalLdap.noopInfo' | tr }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.provider' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.provider }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.server' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.url }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.acceptSelfSignedCert' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.acceptSelfSignedCerts ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.baseDn' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.baseDn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.filter' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.filter }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.usernameField' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.usernameField || 'uid' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.syncGroups' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.syncGroups ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron' && externalLdap.currentConfig.syncGroups">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.groupBaseDn' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.groupBaseDn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron' && externalLdap.currentConfig.syncGroups">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.groupFilter' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.groupFilter }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron' && externalLdap.currentConfig.syncGroups">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.groupnameField' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.groupnameField }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.auth' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.bindDn ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.autocreateUsersOnLogin' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.autoCreate ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<br/>
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div ng-show="externalLdap.syncBusy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ externalLdap.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p ng-show="externalLdap.syncBusy">{{ externalLdap.message }}</p>
|
||||
<p ng-hide="externalLdap.syncBusy">
|
||||
<div class="has-error" ng-show="!externalLdap.active">{{ externalLdap.errorMessage }}</div>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 text-right">
|
||||
<button class="btn btn-primary pull-right" ng-click="externalLdap.show()">{{ 'users.externalLdap.configureAction' | tr }}</button>
|
||||
<button class="btn btn-success pull-right" ng-disabled="externalLdap.currentConfig.provider === 'noop'" ng-click="externalLdap.sync()"><i class="fa fa-circle-notch fa-spin" ng-show="externalLdap.syncBusy"></i> {{ 'users.externalLdap.syncAction' | tr }}</button>
|
||||
<a class="btn btn-primary pull-right" ng-show="externalLdap.taskId" ng-href="/logs.html?taskId={{ externalLdap.taskId }}" target="_blank">{{ 'users.externalLdap.showLogsAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left" style="margin-top: 50px;" ng-show="user.isAtLeastAdmin">
|
||||
<h3>{{ 'users.exposedLdap.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" ng-show="user.isAtLeastAdmin">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div>{{ 'users.exposedLdap.description' | tr }}</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<form name="userDirectoryConfigForm" role="form" novalidate ng-submit="userDirectoryConfig.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="userDirectoryConfig.enabled" ng-disabled="userDirectoryConfig.busy"> {{ 'users.exposedLdap.enabled' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#directory-server" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'users.exposedLdap.secret.url' | tr }}</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="userDirectoryUrlInput" ng-value="'ldaps://' + config.adminFqdn + ':636'" readonly name="userDirectoryUrl" class="form-control"/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" id="userDirectoryUrlClipboardButton" data-clipboard-target="#userDirectoryUrlInput"><i class="fa fa-clipboard"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'users.exposedLdap.secret.label' | tr }}</label>
|
||||
<p class="small" ng-bind-html=" 'users.exposedLdap.secret.description' | tr:{ userDN: 'cn=admin,ou=system,dc=cloudron' }"></p>
|
||||
<input type="password" ng-model="userDirectoryConfig.secret" ng-disabled="!userDirectoryConfig.enabled || userDirectoryConfig.busy" name="userDirectorySecret" class="form-control" ng-class="{ 'has-error': !userDirectoryConfigForm.secret.$dirty && userDirectoryConfig.error.secret }" password-reveal/>
|
||||
<div class="has-error" ng-show="userDirectoryConfig.error.secret">{{ userDirectoryConfig.error.secret }}</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'users.exposedLdap.ipRestriction.label' | tr }}</label>
|
||||
<p class="small">{{ 'users.exposedLdap.ipRestriction.description' | tr }}</p>
|
||||
<textarea ng-model="userDirectoryConfig.allowlist" ng-disabled="!userDirectoryConfig.enabled || userDirectoryConfig.busy" placeholder="{{ 'users.exposedLdap.ipRestriction.placeholder' | tr }}" name="allowlist" class="form-control" ng-class="{ 'has-error': !userDirectoryConfigForm.allowlist.$dirty && userDirectoryConfig.error.allowlist }" rows="4"></textarea>
|
||||
<div class="has-error" ng-show="userDirectoryConfig.error.allowlist">{{ userDirectoryConfig.error.allowlist }}</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<span class="has-error" ng-show="userDirectoryConfig.error.generic">{{ userDirectoryConfig.error.generic }}</span>
|
||||
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="userDirectoryConfig.submit()" ng-disabled="!userDirectoryConfigForm.$dirty || userDirectoryConfig.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="userDirectoryConfig.busy"></i> {{ 'users.settings.saveAction' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left" style="margin-top: 50px;" ng-show="user.isAtLeastAdmin">
|
||||
<h3>{{ 'oidc.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" ng-show="user.isAtLeastAdmin">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="line-height: 34px;">
|
||||
{{ 'oidc.description' | tr }}
|
||||
<a href="/#/oidc" class="btn btn-outline btn-primary pull-right">{{ 'main.settings' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,21 @@
|
||||
angular.module('Application').controller('UsersController', ['$scope', '$location', '$translate', '$timeout', 'Client', function ($scope, $location, $translate, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastUserManager) $location.path('/'); });
|
||||
|
||||
$scope.ldapProvider = [
|
||||
{ name: 'Active Directory', value: 'ad' },
|
||||
{ name: 'Cloudron', value: 'cloudron' },
|
||||
{ name: 'Jumpcloud', value: 'jumpcloud' },
|
||||
{ name: 'Okta', value: 'okta' },
|
||||
{ name: 'Univention Corporate Server (UCS)', value: 'univention' },
|
||||
{ name: 'Other', value: 'other' },
|
||||
{ name: 'Disabled', value: 'noop' }
|
||||
];
|
||||
|
||||
$translate(['users.externalLdap.providerOther', 'users.externalLdap.providerDisabled']).then(function (tr) {
|
||||
if (tr['users.externalLdap.providerOther']) $scope.ldapProvider.find(function (p) { return p.value === 'other'; }).name = tr['users.externalLdap.providerOther'];
|
||||
if (tr['users.externalLdap.providerDisabled']) $scope.ldapProvider.find(function (p) { return p.value === 'noop'; }).name = tr['users.externalLdap.providerDisabled'];
|
||||
});
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.users = []; // users of current page
|
||||
$scope.allUsersById = [];
|
||||
@@ -16,13 +31,18 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
$scope.groupsById = { };
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.userInfo = Client.getUserInfo();
|
||||
$scope.domains = [];
|
||||
|
||||
$scope.openSubscriptionSetup = function () {
|
||||
Client.openSubscriptionSetup($scope.$parent.subscription);
|
||||
};
|
||||
|
||||
$scope.roles = [];
|
||||
$scope.allUsers = []; // all the users and not just current page, have to load this for group assignment
|
||||
|
||||
$scope.userSearchString = '';
|
||||
$scope.currentPage = 1;
|
||||
$scope.pageItems = localStorage.cloudronPageSize || 15;
|
||||
$scope.pageItems = 15;
|
||||
$scope.userRefreshBusy = true;
|
||||
|
||||
$scope.userStates = [
|
||||
@@ -825,6 +845,263 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
}
|
||||
};
|
||||
|
||||
$scope.profileConfig = {
|
||||
editableUserProfiles: true,
|
||||
mandatory2FA: false,
|
||||
errorMessage: '',
|
||||
|
||||
refresh: function () {
|
||||
Client.getProfileConfig(function (error, result) {
|
||||
if (error) return console.error('Unable to get directory config.', error);
|
||||
|
||||
$scope.profileConfig.editableUserProfiles = !result.lockUserProfiles;
|
||||
$scope.profileConfig.mandatory2FA = !!result.mandatory2FA;
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
// prevent the current user from getting locked out
|
||||
if ($scope.profileConfig.mandatory2FA && !$scope.userInfo.twoFactorAuthenticationEnabled) return Client.notify('', $translate.instant('users.settings.require2FAWarning'), true, 'error', '#/profile');
|
||||
|
||||
$scope.profileConfig.error = '';
|
||||
$scope.profileConfig.busy = true;
|
||||
$scope.profileConfig.success = false;
|
||||
|
||||
var data = {
|
||||
lockUserProfiles: !$scope.profileConfig.editableUserProfiles,
|
||||
mandatory2FA: $scope.profileConfig.mandatory2FA
|
||||
};
|
||||
|
||||
Client.setProfileConfig(data, function (error) {
|
||||
if (error) $scope.profileConfig.errorMessage = error.message;
|
||||
|
||||
$scope.profileConfig.success = true;
|
||||
|
||||
$scope.profileConfigForm.$setUntouched();
|
||||
$scope.profileConfigForm.$setPristine();
|
||||
|
||||
Client.refreshConfig(); // refresh the $scope.config
|
||||
|
||||
$timeout(function () {
|
||||
$scope.profileConfig.busy = false;
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.userDirectoryConfig = {
|
||||
enabled: false,
|
||||
secret: '',
|
||||
allowlist: '',
|
||||
error: null,
|
||||
|
||||
refresh: function () {
|
||||
Client.getUserDirectoryConfig(function (error, result) {
|
||||
if (error) return console.error('Unable to get exposed ldap config.', error);
|
||||
|
||||
$scope.userDirectoryConfig.enabled = !!result.enabled;
|
||||
$scope.userDirectoryConfig.allowlist = result.allowlist;
|
||||
$scope.userDirectoryConfig.secret = result.secret;
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.userDirectoryConfig.error = null;
|
||||
$scope.userDirectoryConfig.busy = true;
|
||||
$scope.userDirectoryConfig.success = false;
|
||||
|
||||
var data = {
|
||||
enabled: $scope.userDirectoryConfig.enabled,
|
||||
secret: $scope.userDirectoryConfig.secret,
|
||||
allowlist: $scope.userDirectoryConfig.allowlist
|
||||
};
|
||||
|
||||
Client.setUserDirectoryConfig(data, function (error) {
|
||||
$scope.userDirectoryConfig.busy = false;
|
||||
|
||||
if (error && error.statusCode === 400) {
|
||||
if (error.message.indexOf('secret') !== -1) return $scope.userDirectoryConfig.error = { secret: error.message };
|
||||
else return $scope.userDirectoryConfig.error = { allowlist: error.message };
|
||||
}
|
||||
if (error) return $scope.userDirectoryConfig.error = { generic: error.message };
|
||||
|
||||
$scope.userDirectoryConfigForm.$setUntouched();
|
||||
$scope.userDirectoryConfigForm.$setPristine();
|
||||
|
||||
$scope.userDirectoryConfig.success = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.externalLdap = {
|
||||
busy: false,
|
||||
percent: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
error: {},
|
||||
taskId: 0,
|
||||
|
||||
syncBusy: false,
|
||||
|
||||
// fields
|
||||
provider: 'noop',
|
||||
autoCreate: false,
|
||||
url: '',
|
||||
acceptSelfSignedCerts: false,
|
||||
baseDn: '',
|
||||
filter: '',
|
||||
groupBaseDn: '',
|
||||
bindDn: '',
|
||||
bindPassword: '',
|
||||
usernameField: '',
|
||||
|
||||
currentConfig: {},
|
||||
|
||||
checkStatus: function () {
|
||||
Client.getLatestTaskByType('syncExternalLdap', function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!task) return;
|
||||
|
||||
$scope.externalLdap.taskId = task.id;
|
||||
$scope.externalLdap.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
sync: function () {
|
||||
$scope.externalLdap.syncBusy = true;
|
||||
|
||||
Client.startExternalLdapSync(function (error, taskId) {
|
||||
if (error) {
|
||||
$scope.externalLdap.syncBusy = false;
|
||||
console.error('Unable to start ldap syncer task.', error);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.externalLdap.taskId = taskId;
|
||||
$scope.externalLdap.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
Client.getTask($scope.externalLdap.taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.externalLdap.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.externalLdap.syncBusy = false;
|
||||
$scope.externalLdap.message = '';
|
||||
$scope.externalLdap.percent = 100; // indicates that 'result' is valid
|
||||
$scope.externalLdap.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
refreshGroups();
|
||||
refreshUsers();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.externalLdap.syncBusy = true;
|
||||
$scope.externalLdap.percent = data.percent;
|
||||
$scope.externalLdap.message = data.message;
|
||||
window.setTimeout($scope.externalLdap.updateStatus, 3000);
|
||||
});
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.externalLdap.busy = false;
|
||||
$scope.externalLdap.error = {};
|
||||
|
||||
$scope.externalLdap.provider = $scope.externalLdap.currentConfig.provider;
|
||||
$scope.externalLdap.url = $scope.externalLdap.currentConfig.url;
|
||||
$scope.externalLdap.acceptSelfSignedCerts = $scope.externalLdap.currentConfig.acceptSelfSignedCerts;
|
||||
$scope.externalLdap.baseDn = $scope.externalLdap.currentConfig.baseDn;
|
||||
$scope.externalLdap.filter = $scope.externalLdap.currentConfig.filter;
|
||||
$scope.externalLdap.syncGroups = $scope.externalLdap.currentConfig.syncGroups;
|
||||
$scope.externalLdap.groupBaseDn = $scope.externalLdap.currentConfig.groupBaseDn;
|
||||
$scope.externalLdap.groupFilter = $scope.externalLdap.currentConfig.groupFilter;
|
||||
$scope.externalLdap.groupnameField = $scope.externalLdap.currentConfig.groupnameField;
|
||||
$scope.externalLdap.bindDn = $scope.externalLdap.currentConfig.bindDn;
|
||||
$scope.externalLdap.bindPassword = $scope.externalLdap.currentConfig.bindPassword;
|
||||
$scope.externalLdap.usernameField = $scope.externalLdap.currentConfig.usernameField;
|
||||
$scope.externalLdap.autoCreate = $scope.externalLdap.currentConfig.autoCreate;
|
||||
|
||||
$('#externalLdapModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.externalLdap.busy = true;
|
||||
$scope.externalLdap.error = {};
|
||||
|
||||
var config = {
|
||||
provider: $scope.externalLdap.provider
|
||||
};
|
||||
|
||||
if ($scope.externalLdap.provider === 'cloudron') {
|
||||
config.url = $scope.externalLdap.url;
|
||||
config.acceptSelfSignedCerts = $scope.externalLdap.acceptSelfSignedCerts;
|
||||
config.autoCreate = $scope.externalLdap.autoCreate;
|
||||
config.syncGroups = $scope.externalLdap.syncGroups;
|
||||
config.bindPassword = $scope.externalLdap.bindPassword;
|
||||
|
||||
// those values are known and thus overwritten
|
||||
config.baseDn = 'ou=users,dc=cloudron';
|
||||
config.filter = '(objectClass=inetOrgPerson)';
|
||||
config.usernameField = 'username';
|
||||
config.groupBaseDn = 'ou=groups,dc=cloudron';
|
||||
config.groupFilter = '(objectClass=group)';
|
||||
config.groupnameField = 'cn';
|
||||
config.bindDn = 'cn=admin,ou=system,dc=cloudron';
|
||||
} else if ($scope.externalLdap.provider !== 'noop') {
|
||||
config.url = $scope.externalLdap.url;
|
||||
config.acceptSelfSignedCerts = $scope.externalLdap.acceptSelfSignedCerts;
|
||||
config.baseDn = $scope.externalLdap.baseDn;
|
||||
config.filter = $scope.externalLdap.filter;
|
||||
config.usernameField = $scope.externalLdap.usernameField;
|
||||
config.syncGroups = $scope.externalLdap.syncGroups;
|
||||
config.groupBaseDn = $scope.externalLdap.groupBaseDn;
|
||||
config.groupFilter = $scope.externalLdap.groupFilter;
|
||||
config.groupnameField = $scope.externalLdap.groupnameField;
|
||||
config.autoCreate = $scope.externalLdap.autoCreate;
|
||||
|
||||
if ($scope.externalLdap.bindDn) {
|
||||
config.bindDn = $scope.externalLdap.bindDn;
|
||||
config.bindPassword = $scope.externalLdap.bindPassword;
|
||||
}
|
||||
}
|
||||
|
||||
Client.setExternalLdapConfig(config, function (error) {
|
||||
$scope.externalLdap.busy = false;
|
||||
|
||||
if (error) {
|
||||
if (error.statusCode === 424) {
|
||||
if (error.code === 'SELF_SIGNED_CERT_IN_CHAIN') $scope.externalLdap.error.acceptSelfSignedCerts = true;
|
||||
else $scope.externalLdap.error.url = true;
|
||||
} else if (error.statusCode === 400 && error.message === 'invalid baseDn') {
|
||||
$scope.externalLdap.error.baseDn = true;
|
||||
} else if (error.statusCode === 400 && error.message === 'invalid filter') {
|
||||
$scope.externalLdap.error.filter = true;
|
||||
} else if (error.statusCode === 400 && error.message === 'invalid groupBaseDn') {
|
||||
$scope.externalLdap.error.groupBaseDn = true;
|
||||
} else if (error.statusCode === 400 && error.message === 'invalid groupFilter') {
|
||||
$scope.externalLdap.error.groupFilter = true;
|
||||
} else if (error.statusCode === 400 && error.message === 'invalid groupnameField') {
|
||||
$scope.externalLdap.error.groupnameField = true;
|
||||
} else if (error.statusCode === 400 && error.message === 'invalid bind credentials') {
|
||||
$scope.externalLdap.error.credentials = true;
|
||||
} else if (error.statusCode === 400 && error.message === 'invalid usernameField') {
|
||||
$scope.externalLdap.error.usernameField = true;
|
||||
} else {
|
||||
console.error('Failed to set external LDAP config:', error);
|
||||
$scope.externalLdap.error.generic = error.message;
|
||||
}
|
||||
} else {
|
||||
$('#externalLdapModal').modal('hide');
|
||||
|
||||
loadExternalLdapConfig();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function getUsers(callback) {
|
||||
var users = [];
|
||||
|
||||
@@ -882,6 +1159,15 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
});
|
||||
}
|
||||
|
||||
function loadExternalLdapConfig() {
|
||||
Client.getExternalLdapConfig(function (error, result) {
|
||||
if (error) return console.error('Unable to get external ldap config.', error);
|
||||
|
||||
$scope.externalLdap.currentConfig = result;
|
||||
$scope.externalLdap.checkStatus();
|
||||
});
|
||||
}
|
||||
|
||||
$scope.showNextPage = function () {
|
||||
$scope.currentPage++;
|
||||
refreshUsers();
|
||||
@@ -910,8 +1196,20 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
});
|
||||
}
|
||||
|
||||
function getAllDomains() {
|
||||
Client.getDomains(function (error, domains) {
|
||||
if (error) return console.error('Unable to get domains:', error);
|
||||
|
||||
$scope.domains = domains;
|
||||
});
|
||||
}
|
||||
|
||||
Client.onReady(function () {
|
||||
refresh();
|
||||
if ($scope.user.isAtLeastAdmin) loadExternalLdapConfig();
|
||||
if ($scope.user.isAtLeastAdmin) $scope.profileConfig.refresh();
|
||||
if ($scope.user.isAtLeastAdmin) $scope.userDirectoryConfig.refresh();
|
||||
if ($scope.user.isAtLeastAdmin) getAllDomains();
|
||||
refreshAllUsers();
|
||||
|
||||
// Order matters for permissions used in canEdit
|
||||
@@ -930,7 +1228,7 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
// setup all the dialog focus handling
|
||||
['userAddModal', 'userRemoveModal', 'userEditModal', 'groupAddModal', 'groupEditModal', 'groupRemoveModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find('[autofocus]:first').focus();
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -988,5 +1286,23 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
$timeout(function () { $('#setGhostClipboardButton').tooltip('hide'); }, 2000);
|
||||
});
|
||||
|
||||
new Clipboard('#userDirectoryUrlClipboardButton').on('success', function(e) {
|
||||
$('#userDirectoryUrlClipboardButton').tooltip({
|
||||
title: 'Copied!',
|
||||
trigger: 'manual'
|
||||
}).tooltip('show');
|
||||
|
||||
$timeout(function () { $('#userDirectoryUrlClipboardButton').tooltip('hide'); }, 2000);
|
||||
|
||||
e.clearSelection();
|
||||
}).on('error', function(/*e*/) {
|
||||
$('#userDirectoryUrlClipboardButton').tooltip({
|
||||
title: 'Press Ctrl+C to copy',
|
||||
trigger: 'manual'
|
||||
}).tooltip('show');
|
||||
|
||||
$timeout(function () { $('#userDirectoryUrlClipboardButton').tooltip('hide'); }, 2000);
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
|
||||
@@ -34,14 +34,10 @@
|
||||
<input type="text" class="form-control" ng-model="volumeAdd.hostPath" name="hostPath" ng-disabled="volumeAdd.busy" placeholder="/mnt/data" ng-required="volumeAdd.mountType === 'mountpoint'" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'ext4'">
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'ext4' || volumeAdd.mountType === 'xfs'">
|
||||
<label class="control-label">{{ 'volumes.addVolumeDialog.diskPath' | tr }}</label>
|
||||
<select class="form-control" ng-model="volumeAdd.ext4Disk" ng-options="item as item.label for item in ext4BlockDevices track by item.path" ng-required="volumeAdd.mountType === 'ext4'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'xfs'">
|
||||
<label class="control-label">{{ 'volumes.addVolumeDialog.diskPath' | tr }}</label>
|
||||
<select class="form-control" ng-model="volumeAdd.xfsDisk" ng-options="item as item.label for item in xfsBlockDevices track by item.path" ng-required="volumeAdd.mountType === 'xfs'"></select>
|
||||
<select class="form-control" ng-model="volumeAdd.diskPath" ng-options="item.path as item.label for item in blockDevices track by item.path"></select>
|
||||
<input type="text" class="form-control" style="margin-top: 5px;" ng-show="volumeAdd.diskPath.path === 'custom'" ng-model="volumeAdd.customDiskPath" ng-disabled="volumeAdd.busy" placeholder="/dev/disk/by-uuid/uuid" ng-required="(volumeAdd.mountType === 'ext4' || volumeAdd.mountType === 'xfs') && volumeAdd.diskPath.path === 'custom'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'cifs' || volumeAdd.mountType === 'nfs' || volumeAdd.mountType === 'sshfs'">
|
||||
@@ -97,74 +93,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal volume edit -->
|
||||
<div class="modal fade" id="volumeEditModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'volumes.editVolumeDialog.title' | tr:{ name: volumeEdit.name } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="volumeEditForm" role="form" novalidate ng-submit="volumeEdit.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="volumeEdit.error">{{ volumeEdit.error }}</p>
|
||||
|
||||
<div><b>{{ 'volumes.mountType' | tr }}:</b> {{ volumeEdit.mountType }}</div>
|
||||
<br/>
|
||||
|
||||
<div class="form-group" ng-show="volumeEdit.mountType === 'cifs' || volumeEdit.mountType === 'nfs' || volumeEdit.mountType === 'sshfs'">
|
||||
<label class="control-label" for="volumeAddHost">{{ 'volumes.addVolumeDialog.server' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="volumeEdit.host" id="volumeEditHost" name="host" ng-disabled="volumeEdit.busy" placeholder="Server IP or hostname" ng-required="volumeEdit.mountType === 'cifs' || volumeEdit.mountType === 'nfs' || volumeEdit.mountType === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="volumeEdit.mountType === 'cifs'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="volumeEdit.seal">{{ 'backups.configureBackupStorage.cifsSealSupport' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeEdit.mountType === 'sshfs'">
|
||||
<label class="control-label" for="volumeEditPort">{{ 'volumes.addVolumeDialog.port' | tr }}</label>
|
||||
<input type="number" class="form-control" ng-model="volumeEdit.port" id="volumeEditPort" name="port" ng-disabled="volumeEdit.busy" ng-required="volumeEdit.mountType === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeEdit.mountType === 'cifs' || volumeEdit.mountType === 'nfs' || volumeEdit.mountType === 'sshfs'">
|
||||
<label class="control-label" for="volumeEditRemoteDir">{{ 'volumes.addVolumeDialog.remoteDirectory' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="volumeEdit.remoteDir" id="volumeAddRemoteDir" name="remoteDir" ng-disabled="volumeEdit.busy" placeholder="/share" ng-required="volumeEdit.mountType === 'cifs' || volumeAdd.mountType === 'nfs' || volumeAdd.mountType === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeEdit.mountType === 'cifs'">
|
||||
<label class="control-label" for="volumeEditUsername">{{ 'volumes.addVolumeDialog.username' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="volumeEdit.username" id="volumeEditUsername" name="cifsUsername" ng-disabled="volumeEdit.busy" ng-required="volumeEdit.mountType === 'cifs'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeEdit.mountType === 'cifs'">
|
||||
<label class="control-label" for="volumeEditPassword">{{ 'volumes.addVolumeDialog.password' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="volumeEdit.password" id="volumeEditPassword" name="cifsPassword" ng-disabled="volumeEdit.busy" password-reveal ng-required="volumeEdit.mountType === 'cifs'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeEdit.mountType === 'sshfs'">
|
||||
<label class="control-label" for="volumeEditUser">{{ 'volumes.addVolumeDialog.user' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="volumeEdit.user" id="volumeEditAddUser" name="user" ng-disabled="volumeEdit.busy" ng-required="volumeEdit.mountType === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeEdit.mountType === 'sshfs'">
|
||||
<label class="control-label" for="volumeEditPrivateKey">{{ 'volumes.addVolumeDialog.privateKey' | tr }}</label>
|
||||
<textarea class="form-control" ng-model="volumeEdit.privateKey" id="volumeEditPrivateKey" name="privateKey" ng-disabled="volumeEdit.busy" ng-required="volumeEdit.mountType === 'sshfs'"></textarea>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="volumeEditForm.$invalid || volumeEdit.busy"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="volumeEdit.submit()" ng-disabled="volumeEditForm.$invalid || volumeEdit.busy"><i class="fa fa-circle-notch fa-spin" ng-show="volumeEdit.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal volume remove -->
|
||||
<div class="modal fade" id="volumeRemoveModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
@@ -206,7 +134,7 @@
|
||||
<th style="width: 20%" class="text-left">{{ 'volumes.name' | tr }}</th>
|
||||
<th style="width: 15%" class="text-left">{{ 'volumes.type' | tr }}</th>
|
||||
<th style="width: 45%" class="text-left">{{ 'volumes.hostPath' | tr }}</th>
|
||||
<th style="width: 122px" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
<th style="width: 15%" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -225,8 +153,7 @@
|
||||
<td class="text-left wrap-table-cell hidden-xs hidden-sm" ng-show="volume.mountType === 'mountpoint' || volume.mountType === 'filesystem'">{{ volume.hostPath }}</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="remount(volume)" ng-show="isMountProvider(volume.mountType)" ng-disabled="volume.remounting" uib-tooltip="{{ 'volumes.remountActionTooltip' | tr }}"><i class="fa fa-sync-alt" ng-class="{ 'fa-spin': volume.remounting }"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="volumeEdit.show(volume)" ng-show="isNetworkProvider(volume.mountType)" ng-disabled="volume.remounting" uib-tooltip="{{ 'volumes.editActionTooltip' | tr }}"><i class="fa fa-pencil-alt"></i></button>
|
||||
<a class="btn btn-xs btn-default" ng-href="{{ '/frontend/filemanager.html#/home/volume/' + volume.id }}" target="_blank" uib-tooltip="{{ 'volumes.openFileManagerActionTooltip' | tr }}"><i class="fas fa-folder"></i></a>
|
||||
<a class="btn btn-xs btn-default" ng-href="{{ '/filemanager.html?type=volume&id=' + volume.id }}" target="_blank" uib-tooltip="{{ 'volumes.openFileManagerActionTooltip' | tr }}"><i class="fas fa-folder"></i></a>
|
||||
<button class="btn btn-xs btn-danger" ng-click="volumeRemove.show(volume)" uib-tooltip="{{ 'volumes.removeVolumeActionTooltip' | tr }}"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
/* global async */
|
||||
|
||||
angular.module('Application').controller('VolumesController', ['$scope', '$location', '$timeout', 'Client', function ($scope, $location, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastUserManager) $location.path('/'); });
|
||||
|
||||
var refreshVolumesTimerId = null;
|
||||
|
||||
@@ -62,10 +62,6 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
|
||||
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'ext4' || provider === 'xfs';
|
||||
};
|
||||
|
||||
$scope.isNetworkProvider = function (provider) {
|
||||
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs';
|
||||
};
|
||||
|
||||
$scope.remount = function (volume) {
|
||||
volume.remounting = true;
|
||||
|
||||
@@ -92,8 +88,8 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
|
||||
remoteDir: '',
|
||||
username: '',
|
||||
password: '',
|
||||
ext4Disk: null, // { path, type }
|
||||
xfsDisk: null, // { path, type }
|
||||
diskPath: {}, // { path, type }
|
||||
customDiskPath: '',
|
||||
user: '',
|
||||
seal: false,
|
||||
port: 22,
|
||||
@@ -109,8 +105,8 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
|
||||
$scope.volumeAdd.remoteDir = '';
|
||||
$scope.volumeAdd.username = '';
|
||||
$scope.volumeAdd.password = '';
|
||||
$scope.volumeAdd.ext4Disk = null;
|
||||
$scope.volumeAdd.xfsDisk = null;
|
||||
$scope.volumeAdd.diskPath = {};
|
||||
$scope.volumeAdd.customDiskPath = '';
|
||||
$scope.volumeAdd.user = '';
|
||||
$scope.volumeAdd.seal = false;
|
||||
$scope.volumeAdd.port = 22;
|
||||
@@ -123,8 +119,7 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
|
||||
show: function () {
|
||||
$scope.volumeAdd.reset();
|
||||
|
||||
$scope.ext4BlockDevices = [];
|
||||
$scope.xfsBlockDevices = [];
|
||||
$scope.blockDevices = [];
|
||||
|
||||
Client.getBlockDevices(function (error, result) {
|
||||
if (error) console.error('Failed to list blockdevices:', error);
|
||||
@@ -135,8 +130,11 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
|
||||
// amend label for UI
|
||||
result.forEach(function (d) { d.label = d.path; });
|
||||
|
||||
$scope.ext4BlockDevices = result.filter(function (d) { return d.type === 'ext4'; });
|
||||
$scope.xfsBlockDevices = result.filter(function (d) { return d.type === 'xfs'; });
|
||||
// add custom fake option
|
||||
result.push({ path: 'custom', label: 'Custom Path' });
|
||||
|
||||
$scope.blockDevices = result;
|
||||
$scope.volumeAdd.diskPath = $scope.blockDevices[0];
|
||||
|
||||
$('#volumeAddModal').modal('show');
|
||||
});
|
||||
@@ -169,13 +167,9 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
|
||||
user: $scope.volumeAdd.user,
|
||||
privateKey: $scope.volumeAdd.privateKey,
|
||||
};
|
||||
} else if ($scope.volumeAdd.mountType === 'ext4') {
|
||||
} else if ($scope.volumeAdd.mountType === 'ext4' || $scope.volumeAdd.mountType === 'xfs') {
|
||||
mountOptions = {
|
||||
diskPath: $scope.volumeAdd.ext4Disk.path
|
||||
};
|
||||
} else if ($scope.volumeAdd.mountType === 'xfs') {
|
||||
mountOptions = {
|
||||
diskPath: $scope.volumeAdd.xfsDisk.path
|
||||
diskPath: $scope.volumeAdd.diskPath === 'custom' ? $scope.volumeAdd.customDiskPath : $scope.volumeAdd.diskPath
|
||||
};
|
||||
} else if ($scope.volumeAdd.mountType === 'mountpoint' || $scope.volumeAdd.mountType === 'filesystem') {
|
||||
mountOptions = {
|
||||
@@ -198,117 +192,6 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
|
||||
}
|
||||
};
|
||||
|
||||
$scope.volumeEdit = {
|
||||
error: null,
|
||||
busy: false,
|
||||
|
||||
// cannot be changed
|
||||
volume: '',
|
||||
name: '',
|
||||
mountType: 'mountpoint',
|
||||
|
||||
// can be changed
|
||||
host: '',
|
||||
remoteDir: '',
|
||||
username: '',
|
||||
password: '',
|
||||
user: '',
|
||||
seal: false,
|
||||
port: 22,
|
||||
privateKey: '',
|
||||
|
||||
reset: function () {
|
||||
$scope.volumeEdit.error = null;
|
||||
$scope.volumeEdit.busy = false;
|
||||
$scope.volumeEdit.volume = null;
|
||||
$scope.volumeEdit.name = '';
|
||||
$scope.volumeEdit.mountType = '';
|
||||
$scope.volumeEdit.host = '';
|
||||
$scope.volumeEdit.seal = '';
|
||||
$scope.volumeEdit.port = '';
|
||||
$scope.volumeEdit.remoteDir = '';
|
||||
$scope.volumeEdit.username = '';
|
||||
$scope.volumeEdit.password = '';
|
||||
$scope.volumeEdit.user = '';
|
||||
$scope.volumeEdit.privateKey = '';
|
||||
|
||||
$scope.volumeEditForm.$setPristine();
|
||||
$scope.volumeEditForm.$setUntouched();
|
||||
},
|
||||
|
||||
show: function (volume) {
|
||||
$scope.volumeEdit.reset();
|
||||
|
||||
Client.getVolume(volume.id, function (error, result) {
|
||||
if (error) console.error('Failed to get volume:', error);
|
||||
|
||||
$scope.volumeEdit.volume = volume;
|
||||
$scope.volumeEdit.mountType = result.mountType;
|
||||
$scope.volumeEdit.name = result.name;
|
||||
$scope.volumeEdit.host = result.mountOptions.host;
|
||||
$scope.volumeEdit.seal = result.mountOptions.seal;
|
||||
$scope.volumeEdit.port = result.mountOptions.port;
|
||||
$scope.volumeEdit.remoteDir = result.mountOptions.remoteDir;
|
||||
$scope.volumeEdit.username = result.mountOptions.username;
|
||||
$scope.volumeEdit.password = result.mountOptions.password;
|
||||
$scope.volumeEdit.user = result.mountOptions.user;
|
||||
$scope.volumeEdit.privateKey = result.mountOptions.privateKey;
|
||||
|
||||
$('#volumeEditModal').modal('show');
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.volumeEdit.busy = true;
|
||||
$scope.volumeEdit.error = null;
|
||||
|
||||
var mountOptions = null;
|
||||
|
||||
if ($scope.volumeEdit.mountType === 'cifs') {
|
||||
mountOptions = {
|
||||
host: $scope.volumeEdit.host,
|
||||
remoteDir: $scope.volumeEdit.remoteDir,
|
||||
username: $scope.volumeEdit.username,
|
||||
password: $scope.volumeEdit.password,
|
||||
seal: $scope.volumeEdit.seal
|
||||
};
|
||||
} else if ($scope.volumeEdit.mountType === 'nfs') {
|
||||
mountOptions = {
|
||||
host: $scope.volumeEdit.host,
|
||||
remoteDir: $scope.volumeEdit.remoteDir,
|
||||
};
|
||||
} else if ($scope.volumeEdit.mountType === 'sshfs') {
|
||||
mountOptions = {
|
||||
host: $scope.volumeEdit.host,
|
||||
port: $scope.volumeEdit.port,
|
||||
remoteDir: $scope.volumeEdit.remoteDir,
|
||||
user: $scope.volumeEdit.user,
|
||||
privateKey: $scope.volumeEdit.privateKey,
|
||||
};
|
||||
} else {
|
||||
console.error('Should not come here. Only network volumes can be edited');
|
||||
|
||||
$('#volumeEditModal').modal('hide');
|
||||
$scope.volumeEdit.reset();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Client.updateVolume($scope.volumeEdit.volume.id, mountOptions, function (error) {
|
||||
$scope.volumeEdit.busy = false;
|
||||
if (error) {
|
||||
$scope.volumeEdit.error = error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
$('#volumeEditModal').modal('hide');
|
||||
$scope.volumeEdit.reset();
|
||||
|
||||
refreshVolumes();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.volumeRemove = {
|
||||
busy: false,
|
||||
error: null,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Vue 3 + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
./node_modules/.bin/vite build --base=/filemanager/
|
||||
@@ -2,12 +2,12 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar" />
|
||||
<link rel="icon" href="/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Logs</title>
|
||||
<title>FileManager</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/logs.js"></script>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "my-vue-app",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"combokeys": "^3.0.1",
|
||||
"filesize": "^10.0.7",
|
||||
"pankow": "^0.1.2",
|
||||
"primeicons": "^6.0.1",
|
||||
"primevue": "^3.27.0",
|
||||
"superagent": "^8.0.9",
|
||||
"vue": "^3.2.47",
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.2.1",
|
||||
"vite": "^4.3.3"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |