Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d3c27ec30 | |||
| b6d3c18944 | |||
| 31282af701 | |||
| 45c8b83d54 | |||
| 29d23c9dcc |
@@ -1,7 +1,6 @@
|
||||
# following files are skipped when exporting using git archive
|
||||
test export-ignore
|
||||
.jshintrc export-ignore
|
||||
.gitlab export-ignore
|
||||
docs export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
dist/
|
||||
node_modules/
|
||||
coverage/
|
||||
.nyc_output/
|
||||
webadmin/dist/
|
||||
installer/src/certs/server.key
|
||||
|
||||
# vim swap files
|
||||
*.swp
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
run_tests:
|
||||
stage: test
|
||||
image: cloudron/base:4.2.0@sha256:46da2fffb36353ef714f97ae8e962bd2c212ca091108d768ba473078319a47f4
|
||||
services:
|
||||
- name: mysql:8.0
|
||||
alias: mysql
|
||||
variables:
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
MYSQL_DATABASE: box
|
||||
BOX_ENV: ci
|
||||
DATABASE_URL: mysql://root:password@mysql/box
|
||||
script:
|
||||
- echo "Running tests..."
|
||||
- mysql -hmysql -uroot -ppassword -e "ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'password';"
|
||||
- mysql -hmysql -uroot -ppassword -e "CREATE DATABASE IF NOT EXISTS box"
|
||||
- npm install
|
||||
- node_modules/.bin/db-migrate up
|
||||
- ln -s /usr/local/node-18.18.0/bin/node /usr/bin/node
|
||||
- node_modules/.bin/mocha --no-timeouts --bail src/test/tokens-test.js
|
||||
- echo "Done!"
|
||||
|
||||
stages:
|
||||
- test
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
Please do not use this issue tracker for support requests and bug reports.
|
||||
This issue tracker is used by the Cloudron development team to track actual
|
||||
bugs in the code.
|
||||
|
||||
Please use the forum at https://forum.cloudron.io to report bugs. For
|
||||
confidential issues, please email us at support@cloudron.io.
|
||||
@@ -1,7 +0,0 @@
|
||||
Please do not use this issue tracker for support requests and feature reports.
|
||||
This issue tracker is used by the Cloudron development team to track issues in
|
||||
the code.
|
||||
|
||||
Please use the forum at https://forum.cloudron.io to report bugs. For
|
||||
confidential issues, please email us at support@cloudron.io.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
The Cloudron Subscription license
|
||||
Copyright (c) 2022 Cloudron UG
|
||||
Copyright (c) 2020 Cloudron UG
|
||||
|
||||
With regard to the Cloudron Software:
|
||||
|
||||
|
||||
@@ -1,54 +1,11 @@
|
||||

|
||||
# Cloudron Dashboard
|
||||
|
||||
# Cloudron
|
||||
This is the front end code of Cloudron. The backend code is [here](https://git.cloudron.io/cloudron/box).
|
||||
|
||||
[Cloudron](https://cloudron.io) is the best way to run apps on your server.
|
||||
## Developing
|
||||
|
||||
Web applications like email, contacts, blog, chat are the backbone of the modern
|
||||
internet. Yet, we live in a world where hosting these essential applications is
|
||||
a complex task.
|
||||
|
||||
We are building the ultimate platform for self-hosting web apps. The Cloudron allows
|
||||
anyone to effortlessly host web applications on their server on their own terms.
|
||||
|
||||
## Features
|
||||
|
||||
* Single click install for apps. Check out the [App Store](https://cloudron.io/appstore.html).
|
||||
|
||||
* Per-app encrypted backups and restores.
|
||||
|
||||
* App updates delivered via the App Store.
|
||||
|
||||
* Secure - Cloudron manages the firewall. All apps are secured with HTTPS. Certificates are
|
||||
installed and renewed automatically.
|
||||
|
||||
* Centralized User & Group management. Control who can access which app.
|
||||
|
||||
* Single Sign On. Use same credentials across all apps.
|
||||
|
||||
* Automatic updates for the Cloudron platform.
|
||||
|
||||
* Trivially migrate to another server keeping your apps and data (for example, switch your
|
||||
infrastructure provider or move to a bigger server).
|
||||
|
||||
* Comprehensive [REST API](https://docs.cloudron.io/api/).
|
||||
|
||||
* [CLI](https://docs.cloudron.io/custom-apps/cli/) to configure apps.
|
||||
|
||||
* Alerts, audit logs, graphs, dns management ... and much more
|
||||
|
||||
## Demo
|
||||
|
||||
Try our demo at https://my.demo.cloudron.io (username: cloudron password: cloudron).
|
||||
|
||||
## Installing
|
||||
|
||||
[Install script](https://docs.cloudron.io/installation/) - [Pricing](https://cloudron.io/pricing.html)
|
||||
|
||||
**Note:** This repo is a small part of what gets installed on your server - there is
|
||||
the dashboard, database addons, graph container, base image etc. Cloudron also relies
|
||||
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.
|
||||
* `npm install`
|
||||
* `gulp develop --api-origin=https://my.example.com`
|
||||
|
||||
## License
|
||||
|
||||
@@ -61,13 +18,3 @@ Just to give some heads up, we are a bit restrictive in merging changes. We are
|
||||
would like to keep our maintenance burden low. It might be best to discuss features first in the [forum](https://forum.cloudron.io),
|
||||
to also figure out how many other people will use it to justify maintenance for a feature.
|
||||
|
||||
# Localization
|
||||
|
||||

|
||||
|
||||
## Support
|
||||
|
||||
* [Documentation](https://docs.cloudron.io/)
|
||||
* [Forum](https://forum.cloudron.io/)
|
||||
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
const constants = require('./src/constants.js'),
|
||||
fs = require('fs'),
|
||||
ldapServer = require('./src/ldapserver.js'),
|
||||
oidc = require('./src/oidc.js'),
|
||||
paths = require('./src/paths.js'),
|
||||
proxyAuth = require('./src/proxyauth.js'),
|
||||
safe = require('safetydance'),
|
||||
server = require('./src/server.js'),
|
||||
directoryServer = require('./src/directoryserver.js');
|
||||
|
||||
let logFd;
|
||||
|
||||
async function setupLogging() {
|
||||
if (constants.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
|
||||
process.stdout.write = process.stderr.write = function (...args) {
|
||||
const callback = typeof args[args.length-1] === 'function' ? args.pop() : function () {}; // callback is required for fs.write
|
||||
fs.write.apply(fs, [logFd, ...args, callback]);
|
||||
};
|
||||
}
|
||||
|
||||
// this is also used as the 'uncaughtException' handler which can only have synchronous functions
|
||||
function exitSync(status) {
|
||||
if (status.error) fs.write(logFd, status.error.stack + '\n', function () {});
|
||||
fs.fsyncSync(logFd);
|
||||
fs.closeSync(logFd);
|
||||
process.exit(status.code);
|
||||
}
|
||||
|
||||
async function startServers() {
|
||||
await setupLogging();
|
||||
await server.start(); // do this first since it also inits the database
|
||||
await proxyAuth.start();
|
||||
await ldapServer.start();
|
||||
|
||||
const conf = await directoryServer.getConfig();
|
||||
if (conf.enabled) await directoryServer.start();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [error] = await safe(startServers());
|
||||
if (error) return exitSync({ error: new Error(`Error starting server: ${JSON.stringify(error)}`), code: 1 });
|
||||
|
||||
// require this here so that logging handler is already setup
|
||||
const debug = require('debug')('box:box');
|
||||
|
||||
process.on('SIGHUP', async function () {
|
||||
debug('Received SIGHUP. Re-reading configs.');
|
||||
const conf = await directoryServer.getConfig();
|
||||
if (conf.enabled) await directoryServer.checkCertificate();
|
||||
});
|
||||
|
||||
process.on('SIGINT', async function () {
|
||||
debug('Received SIGINT. Shutting down.');
|
||||
|
||||
await proxyAuth.stop();
|
||||
await server.stop();
|
||||
await directoryServer.stop();
|
||||
await ldapServer.stop();
|
||||
await oidc.stop();
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async function () {
|
||||
debug('Received SIGTERM. Shutting down.');
|
||||
|
||||
await proxyAuth.stop();
|
||||
await server.stop();
|
||||
await directoryServer.stop();
|
||||
await ldapServer.stop();
|
||||
await oidc.stop();
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error) => exitSync({ error, code: 1 }));
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,6 +0,0 @@
|
||||
# following files are skipped when exporting using git archive
|
||||
test export-ignore
|
||||
docs export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
dist/
|
||||
node_modules/
|
||||
|
||||
# vim swap files
|
||||
*.swp
|
||||
|
||||
# these are not done yet
|
||||
src/translation/ja.json
|
||||
src/translation/pl.json
|
||||
src/translation/si.json
|
||||
src/translation/gl.json
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"unused": true,
|
||||
"esversion": 6,
|
||||
"globalstrict": false,
|
||||
"predef": [
|
||||
"$",
|
||||
"angular",
|
||||
"async",
|
||||
"describe",
|
||||
"it",
|
||||
"before",
|
||||
"after",
|
||||
"require",
|
||||
"monaco",
|
||||
"Mimer",
|
||||
"ISTATES"
|
||||
]
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
The Cloudron Subscription license
|
||||
Copyright (c) 2022 Cloudron UG
|
||||
|
||||
With regard to the Cloudron Software:
|
||||
|
||||
This software and associated documentation files (the "Software") may only be
|
||||
used in production, if you (and any entity that you represent) have agreed to,
|
||||
and are in compliance with, the Cloudron Subscription Terms of Service, available
|
||||
at https://cloudron.io/legal/terms.html (the “Subscription Terms”), or other
|
||||
agreement governing the use of the Software, as agreed by you and Cloudron,
|
||||
and otherwise have a valid Cloudron Subscription. Subject to the foregoing sentence,
|
||||
you are free to modify this Software and publish patches to the Software. You agree
|
||||
that Subscription and/or its licensors (as applicable) retain all right, title and
|
||||
interest in and to all such modifications and/or patches, and all such modifications
|
||||
and/or patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||
exploited with a valid Cloudron subscription. Notwithstanding the foregoing, you may copy
|
||||
and modify the Software for development and testing purposes, without requiring a
|
||||
subscription. You agree that Cloudron and/or its licensors (as applicable) retain
|
||||
all right, title and interest in and to all such modifications. You are not
|
||||
granted any other rights beyond what is expressly stated herein. Subject to the
|
||||
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||
and/or sell the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the Cloudron Software, those
|
||||
components are licensed under the original license provided by the owner of the
|
||||
applicable component.
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# Cloudron Dashboard
|
||||
|
||||
This is the front end code of Cloudron. The backend code is [here](https://git.cloudron.io/cloudron/box).
|
||||
|
||||
## Developing
|
||||
|
||||
* `npm install`
|
||||
* `gulp develop --api-origin=https://my.example.com`
|
||||
|
||||
## License
|
||||
|
||||
Please note that the Cloudron code is under a source-available license. This is not the same as an
|
||||
open source license but ensures the code is available for inspection (and hacking!).
|
||||
|
||||
## Contributions
|
||||
|
||||
Just to give a heads-up, we are a bit restrictive in merging changes. We are a small team and
|
||||
would like to keep our maintenance burden low. It might be best to first discuss features in the [forum](https://forum.cloudron.io),
|
||||
which also helps to determine how many other people will use it to justify maintenance for a feature.
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
const argv = require('yargs').argv,
|
||||
concat = require('gulp-concat'),
|
||||
ejs = require('gulp-ejs'),
|
||||
execSync = require('child_process').execSync,
|
||||
fs = require('fs'),
|
||||
gulp = require('gulp'),
|
||||
sass = require('gulp-sass')(require('sass')),
|
||||
serve = require('gulp-serve'),
|
||||
sourcemaps = require('gulp-sourcemaps');
|
||||
|
||||
if (argv.help || argv.h) {
|
||||
console.log('Supported arguments for "gulp develop":');
|
||||
console.log(' --api-origin <cloudron api uri>');
|
||||
console.log(' --revision <revision>');
|
||||
console.log(' --appstore-console-origin <appstore console uri>');
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const revision = argv.revision || '';
|
||||
|
||||
let apiOrigin = '';
|
||||
if (argv.apiOrigin) {
|
||||
if (argv.apiOrigin.indexOf('https://') === 0) apiOrigin = argv.apiOrigin;
|
||||
else apiOrigin = 'https://' + argv.apiOrigin;
|
||||
}
|
||||
|
||||
var appstore = {
|
||||
consoleOrigin: argv.appstoreConsoleOrigin || ''
|
||||
};
|
||||
|
||||
console.log();
|
||||
console.log('Cloudron API: %s', apiOrigin || 'default');
|
||||
console.log('Building for revision: %s', revision);
|
||||
console.log();
|
||||
console.log('Overriding appstore origin:');
|
||||
console.log(' Console: %s', appstore.consoleOrigin || 'no');
|
||||
console.log();
|
||||
|
||||
gulp.task('fontawesome', function () {
|
||||
return gulp.src('node_modules/@fortawesome/fontawesome-free/**/*')
|
||||
.pipe(gulp.dest('dist/3rdparty/fontawesome/'));
|
||||
});
|
||||
|
||||
gulp.task('noto-sans', function () {
|
||||
return gulp.src('node_modules/@fontsource/noto-sans/**/*')
|
||||
.pipe(gulp.dest('dist/3rdparty/noto-sans/'));
|
||||
});
|
||||
|
||||
gulp.task('bootstrap', function () {
|
||||
return gulp.src('node_modules/bootstrap-sass/assets/javascripts/bootstrap.min.js')
|
||||
.pipe(gulp.dest('dist/3rdparty/js'));
|
||||
});
|
||||
|
||||
gulp.task('moment', function () {
|
||||
return gulp.src('node_modules/moment/min/*')
|
||||
.pipe(gulp.dest('dist/3rdparty/js'));
|
||||
});
|
||||
|
||||
gulp.task('3rdparty-copy', function () {
|
||||
return gulp.src([
|
||||
'src/3rdparty/**/*.js',
|
||||
'src/3rdparty/**/*.map',
|
||||
'src/3rdparty/**/*.css',
|
||||
'src/3rdparty/**/*.otf',
|
||||
'src/3rdparty/**/*.eot',
|
||||
'src/3rdparty/**/*.svg',
|
||||
'src/3rdparty/**/*.gif',
|
||||
'src/3rdparty/**/*.ttf',
|
||||
'node_modules/chart.js/dist/chart.umd.js'
|
||||
]).pipe(gulp.dest('dist/3rdparty/'));
|
||||
});
|
||||
|
||||
gulp.task('3rdparty', gulp.series(['3rdparty-copy', 'moment', 'bootstrap', 'fontawesome', 'noto-sans']));
|
||||
|
||||
// --------------
|
||||
// JavaScript
|
||||
// --------------
|
||||
|
||||
gulp.task('js-index', function () {
|
||||
return gulp.src([
|
||||
'src/js/index.js',
|
||||
'src/js/client.js',
|
||||
'src/js/utils.js',
|
||||
'src/views/*.js'
|
||||
])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('index.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-passwordreset', function () {
|
||||
return gulp.src(['src/js/passwordreset.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('passwordreset.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-setupaccount', function () {
|
||||
return gulp.src(['src/js/setupaccount.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('setupaccount.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-activation', function () {
|
||||
return gulp.src(['src/js/activation.js', 'src/js/client.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('activation.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-setup', function () {
|
||||
return gulp.src(['src/js/setup.js', 'src/js/client.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('setup.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-restore', function () {
|
||||
return gulp.src(['src/js/restore.js', 'src/js/client.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('restore.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js', gulp.series([ 'js-index', 'js-passwordreset', 'js-setupaccount', 'js-activation', 'js-setup', 'js-restore' ]));
|
||||
|
||||
// --------------
|
||||
// HTML
|
||||
// --------------
|
||||
|
||||
gulp.task('html-views', function () {
|
||||
return gulp.src('src/views/**/*.html').pipe(gulp.dest('dist/views'));
|
||||
});
|
||||
|
||||
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-raw']));
|
||||
|
||||
// --------------
|
||||
// CSS
|
||||
// --------------
|
||||
|
||||
gulp.task('css', function () {
|
||||
return gulp.src('src/*.scss')
|
||||
.pipe(sass({ includePaths: [
|
||||
'node_modules/bootstrap-sass/assets/stylesheets/',
|
||||
'node_modules/@fontsource/'
|
||||
]}).on('error', sass.logError))
|
||||
.pipe(gulp.dest('dist'));
|
||||
});
|
||||
|
||||
gulp.task('images', function () {
|
||||
return gulp.src('src/img/**')
|
||||
.pipe(gulp.dest('dist/img'));
|
||||
});
|
||||
|
||||
gulp.task('translation', function () {
|
||||
return gulp.src('src/translation/**')
|
||||
.pipe(gulp.dest('dist/translation'));
|
||||
});
|
||||
|
||||
gulp.task('timezones', function (done) {
|
||||
execSync('./scripts/createTimezones.js ./dist/js/timezones.js');
|
||||
done();
|
||||
});
|
||||
|
||||
// --------------
|
||||
// Utilities
|
||||
// --------------
|
||||
|
||||
gulp.task('clean', function (done) {
|
||||
fs.rm('dist', { recursive: true, force: true }, done);
|
||||
});
|
||||
|
||||
gulp.task('default', gulp.series(['clean', 'html', 'js', 'timezones', '3rdparty', 'translation', 'images', 'css']));
|
||||
|
||||
gulp.task('watch', function (done) {
|
||||
gulp.watch(['src/*.scss'], gulp.series(['css']));
|
||||
gulp.watch(['src/img/*'], gulp.series(['images']));
|
||||
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(['scripts/createTimezones.js', 'src/js/utils.js'], gulp.series(['timezones']));
|
||||
gulp.watch(['src/js/activation.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-activation']));
|
||||
gulp.watch(['src/js/setup.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-setup']));
|
||||
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/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/3rdparty/**/*'], gulp.series(['3rdparty']));
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('serve', serve({ root: 'dist', port: 4000, hostname: '0.0.0.0' }));
|
||||
|
||||
gulp.task('develop', gulp.series(['default', 'watch', 'serve']));
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "dashboard",
|
||||
"version": "1.0.0",
|
||||
"description": "[Cloudron](https://cloudron.io) is the best way to run apps on your server.",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"update-translations": "curl https://translate.cloudron.io/api/components/cloudron/dashboard/file/ -o lang.zip && unzip -jo lang.zip -d ./src/translation/ && rm lang.zip"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "ssh://git@git.cloudron.io:6000/cloudron/dashboard.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@fontsource/noto-sans": "^5.0.21",
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"bootstrap-sass": "^3.4.3",
|
||||
"chart.js": "^4.4.2",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-ejs": "^5.1.0",
|
||||
"gulp-sass": "^5.1.0",
|
||||
"gulp-serve": "^1.4.0",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"moment": "^2.30.1",
|
||||
"sass": "^1.75.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"env": {
|
||||
"browser": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/*
|
||||
AngularJS v1.5.8
|
||||
(c) 2010-2016 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
*/
|
||||
(function(n,c){'use strict';function l(b,a,g){var d=g.baseHref(),k=b[0];return function(b,e,f){var g,h;f=f||{};h=f.expires;g=c.isDefined(f.path)?f.path:d;c.isUndefined(e)&&(h="Thu, 01 Jan 1970 00:00:00 GMT",e="");c.isString(h)&&(h=new Date(h));e=encodeURIComponent(b)+"="+encodeURIComponent(e);e=e+(g?";path="+g:"")+(f.domain?";domain="+f.domain:"");e+=h?";expires="+h.toUTCString():"";e+=f.secure?";secure":"";f=e.length+1;4096<f&&a.warn("Cookie '"+b+"' possibly not set or overflowed because it was too large ("+
|
||||
f+" > 4096 bytes)!");k.cookie=e}}c.module("ngCookies",["ng"]).provider("$cookies",[function(){var b=this.defaults={};this.$get=["$$cookieReader","$$cookieWriter",function(a,g){return{get:function(d){return a()[d]},getObject:function(d){return(d=this.get(d))?c.fromJson(d):d},getAll:function(){return a()},put:function(d,a,m){g(d,a,m?c.extend({},b,m):b)},putObject:function(d,b,a){this.put(d,c.toJson(b),a)},remove:function(a,k){g(a,void 0,k?c.extend({},b,k):b)}}}]}]);c.module("ngCookies").factory("$cookieStore",
|
||||
["$cookies",function(b){return{get:function(a){return b.getObject(a)},put:function(a,c){b.putObject(a,c)},remove:function(a){b.remove(a)}}}]);l.$inject=["$document","$log","$browser"];c.module("ngCookies").provider("$$cookieWriter",function(){this.$get=l})})(window,window.angular);
|
||||
//# sourceMappingURL=angular-cookies.min.js.map
|
||||
@@ -1,6 +0,0 @@
|
||||
/*!
|
||||
* angular-translate - v2.18.3 - 2020-07-08
|
||||
*
|
||||
* Copyright (c) 2020 The angular-translate team, Pascal Precht; Licensed MIT
|
||||
*/
|
||||
!function(e,i){"function"==typeof define&&define.amd?define([],function(){return i()}):"object"==typeof module&&module.exports?module.exports=i():i()}(0,function(){function e(n,a){"use strict";return function(r){if(!(r&&(angular.isArray(r.files)||angular.isString(r.prefix)&&angular.isString(r.suffix))))throw new Error("Couldn't load static files, no files and prefix or suffix specified!");r.files||(r.files=[{prefix:r.prefix,suffix:r.suffix}]);for(var e=function(e){if(!e||!angular.isString(e.prefix)||!angular.isString(e.suffix))throw new Error("Couldn't load static file, no prefix or suffix specified!");var i=[e.prefix,r.key,e.suffix].join("");return angular.isObject(r.fileMap)&&r.fileMap[i]&&(i=r.fileMap[i]),a(angular.extend({url:i,method:"GET"},r.$http)).then(function(e){return e.data},function(){return n.reject(r.key)})},i=[],t=r.files.length,f=0;f<t;f++)i.push(e({prefix:r.files[f].prefix,key:r.key,suffix:r.files[f].suffix}));return n.all(i).then(function(e){for(var i=e.length,r={},t=0;t<i;t++)for(var f in e[t])r[f]=e[t][f];return r})}}return e.$inject=["$q","$http"],angular.module("pascalprecht.translate").factory("$translateStaticFilesLoader",e),e.displayName="$translateStaticFilesLoader","pascalprecht.translate"});
|
||||
@@ -1,6 +0,0 @@
|
||||
/*!
|
||||
* angular-translate - v2.18.3 - 2020-07-08
|
||||
*
|
||||
* Copyright (c) 2020 The angular-translate team, Pascal Precht; Licensed MIT
|
||||
*/
|
||||
!function(t,e){"function"==typeof define&&define.amd?define([],function(){return e()}):"object"==typeof module&&module.exports?module.exports=e():e()}(0,function(){function t(t){"use strict";var n;if(1===angular.version.major&&4<=angular.version.minor){var o=t.get("$cookies");n={get:function(t){return o.get(t)},put:function(t,e){o.put(t,e)}}}else{var r=t.get("$cookieStore");n={get:function(t){return r.get(t)},put:function(t,e){r.put(t,e)}}}return{get:function(t){return n.get(t)},set:function(t,e){n.put(t,e)},put:function(t,e){n.put(t,e)}}}return t.$inject=["$injector"],angular.module("pascalprecht.translate").factory("$translateCookieStorage",t),t.displayName="$translateCookieStorage","pascalprecht.translate"});
|
||||
@@ -1,6 +0,0 @@
|
||||
/*!
|
||||
* angular-translate - v2.18.3 - 2020-07-08
|
||||
*
|
||||
* Copyright (c) 2020 The angular-translate team, Pascal Precht; Licensed MIT
|
||||
*/
|
||||
!function(t,e){"function"==typeof define&&define.amd?define([],function(){return e()}):"object"==typeof module&&module.exports?module.exports=e():e()}(0,function(){function t(a,t){"use strict";var o,e={get:function(t){return o||(o=a.localStorage.getItem(t)),o},set:function(t,e){o=e,a.localStorage.setItem(t,e)},put:function(t,e){o=e,a.localStorage.setItem(t,e)}},r="localStorage"in a;if(r){var n="pascalprecht.translate.storageTest";try{r=null!==a.localStorage&&(a.localStorage.setItem(n,"foo"),a.localStorage.removeItem(n),!0)}catch(t){r=!1}}return r?e:t}return t.$inject=["$window","$translateCookieStorage"],angular.module("pascalprecht.translate").factory("$translateLocalStorage",t),t.displayName="$translateLocalStorageFactory","pascalprecht.translate"});
|
||||
@@ -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,16 +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];
|
||||
});
|
||||
|
||||
var redirectTo = '/';
|
||||
if (localStorage.getItem('redirectToHash')) {
|
||||
redirectTo += localStorage.getItem('redirectToHash');
|
||||
localStorage.removeItem('redirectToHash');
|
||||
}
|
||||
window.location.href = redirectTo;
|
||||
|
||||
</script>
|
||||
@@ -1,133 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="16"
|
||||
height="16"
|
||||
id="svg2"
|
||||
sodipodi:version="0.32"
|
||||
inkscape:version="0.91 r13725"
|
||||
version="1.0"
|
||||
sodipodi:docname="avatar-default-symbolic.svg"
|
||||
inkscape:output_extension="org.inkscape.output.svg.inkscape">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#e7e7e7"
|
||||
borderopacity="1"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="11.964497"
|
||||
inkscape:cx="6.5536056"
|
||||
inkscape:cy="-0.025360958"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
inkscape:showpageshadow="false"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1030"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-global="true">
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="0,112"
|
||||
id="guide2383" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="78.156291,0"
|
||||
id="guide2389" />
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid3672"
|
||||
visible="true"
|
||||
enabled="true" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="22.008699,4.1542523"
|
||||
id="guide2950" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="11.22532,22.008699"
|
||||
id="guide2952" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Calque 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccsccccc"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
id="path3935"
|
||||
d="m -13.771529,5.9050966 c 0.181174,0.8569201 0.2823,1.5051186 0.135325,2.3620387 -1.145861,0.9506717 -4.076448,1.3778558 -4.072056,2.3620387 l -0.393673,2.558875 c 0,0.978388 2.731928,1.771529 6.101933,1.771529 3.370005,0 6.101933,-0.793141 6.101933,-1.771529 L -6.29174,10.629174 c -0.0047,-0.8423279 -2.952548,-1.377856 -4.084358,-2.3620387 -0.09668,-0.7953524 -0.01972,-1.5666863 0.147627,-2.3620387 l -3.543058,0 z" />
|
||||
<path
|
||||
transform="matrix(0.34209356,0,0,0.34209356,-8.638748,-12.26548)"
|
||||
d="m -9.75,73.09375 c -3.766412,0.121068 -7.468069,1.386362 -11.40625,3.25 a 1.25331,1.25331 0 0 0 -0.6875,1.4375 l 0.625,2.53125 a 1.25331,1.25331 0 0 0 0.78125,0.84375 c 0.161757,0.06256 0.275429,0.183794 0.71875,0.3125 2.335298,0.677989 5.907957,1.15625 9.90625,1.15625 3.9982931,0 7.5709518,-0.478261 9.90625,-1.15625 0.44332111,-0.128707 0.55699247,-0.24994 0.71875,-0.3125 a 1.25331,1.25331 0 0 0 0.78125,-0.8125 L 2.25,78.03125 a 1.25331,1.25331 0 0 0 -0.53125,-1.375 C -2.2051532,74.042333 -5.9835879,72.972682 -9.75,73.09375 z"
|
||||
id="path3937"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
inkscape:original="M -9.71875 74.34375 C -13.230599 74.456635 -16.76467 75.641953 -20.625 77.46875 L -20 80 C -19.731211 80.103955 -19.729288 80.147142 -19.375 80.25 C -17.218663 80.876033 -13.703662 81.375 -9.8125 81.375 C -5.9213382 81.375 -2.4063369 80.876033 -0.25 80.25 C 0.10428761 80.147142 0.10621054 80.103955 0.375 80 L 1.03125 77.6875 C -2.7172738 75.190412 -6.2069011 74.230865 -9.71875 74.34375 z "
|
||||
inkscape:radius="1.2531847"
|
||||
sodipodi:type="inkscape:offset" />
|
||||
<rect
|
||||
transform="matrix(0.9205234,-0.39068744,0.39068744,0.9205234,0,0)"
|
||||
ry="1.1810193"
|
||||
rx="1.1810193"
|
||||
y="-2.754653"
|
||||
x="-15.569602"
|
||||
height="2.1871843"
|
||||
width="1.0935922"
|
||||
id="rect3939"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
id="rect3941"
|
||||
width="1.0935922"
|
||||
height="2.1871843"
|
||||
x="6.5567312"
|
||||
y="6.6361833"
|
||||
rx="1.1810193"
|
||||
ry="1.1810193"
|
||||
transform="matrix(-0.9205234,-0.39068744,-0.39068744,0.9205234,0,0)" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccc"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
id="path3943"
|
||||
d="m -12,0 c -1.630647,0 -2.952548,1.2337743 -2.952548,2.7557118 0.01278,0.5632387 0.06085,1.232346 0.393673,2.7557117 0.196837,0.5905097 1.558851,2.1652021 1.574692,2.3620387 0.381733,0.1968365 1.771529,0.1968365 2.165203,0 0,-0.1968366 1.181019,-1.771529 1.377855,-2.3620387 C -9.066594,3.9281919 -9.06754,3.3462214 -9.047452,2.7557118 -9.047452,1.2337743 -10.369352,0 -12,0 z" />
|
||||
<path
|
||||
id="path3157"
|
||||
d="m 38,0 c -1.630647,0 -2.9375,1.2280625 -2.9375,2.75 0.0037,0.1620664 0.01579,0.3963239 0.03125,0.59375 -0.27885,0.118349 -0.299198,0.6610508 -0.0625,1.21875 0.09386,0.2211566 0.213411,0.3909677 0.34375,0.53125 0.03167,0.1567366 0.02336,0.2271022 0.0625,0.40625 0.196837,0.5905097 1.577909,2.1781634 1.59375,2.375 0.381733,0.1968365 1.762576,0.1968365 2.15625,0 0,-0.1968366 1.178164,-1.7844903 1.375,-2.375 C 40.60622,5.3151913 40.62213,5.1903792 40.65625,5.03125 40.764832,4.8997227 40.857512,4.7509639 40.9375,4.5625 41.162363,4.0326858 41.147829,3.5269131 40.90625,3.375 40.920493,3.1615298 40.931227,2.9343906 40.9375,2.75 40.9375,1.2280625 39.630648,0 38,0 z m -1.78125,8.40625 c -1.233461,0.8706787 -3.941711,1.2750309 -3.9375,2.21875 l -0.375,2.5625 c 0,0.519013 0.775005,0.988493 2,1.3125 l 0.1875,0.71875 A 0.42874928,0.42874928 0 0 0 34.375,15.5 c 0.05534,0.0214 0.09834,0.04972 0.25,0.09375 C 35.42389,15.825686 36.63221,16 38,16 39.36779,16 40.60736,15.825686 41.40625,15.59375 41.557907,15.54972 41.569664,15.5214 41.625,15.5 a 0.42874928,0.42874928 0 0 0 0.28125,-0.28125 L 42.125,14.5 c 1.208619,-0.323691 1.96875,-0.797472 1.96875,-1.3125 l -0.375,-2.5625 C 43.714419,9.848863 41.21753,9.3437322 39.9375,8.5 A 0.97584188,0.97584188 0 0 1 39.625,8.75 C 39.020006,9.0524961 38.608286,9 38.09375,9 37.836482,9 37.587947,9.0004922 37.34375,8.96875 37.099553,8.937008 36.902156,8.909026 36.59375,8.75 a 0.97584188,0.97584188 0 0 1 -0.375,-0.34375 z"
|
||||
style="fill:#bebebe;fill-opacity:1;stroke:none"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#bebebe;fill-opacity:1;stroke:none"
|
||||
d="m 8,0.7783785 c -2.256463,0 -4.0648649,1.699373 -4.0648649,3.8054054 0.00509,0.2242649 0.021845,0.5484266 0.043244,0.8216216 -0.3858679,0.1637694 -0.4140257,0.9147514 -0.086487,1.6864865 0.1298861,0.3060329 0.295314,0.5410147 0.4756757,0.7351351 0.043823,0.2168896 0.032325,0.3142604 0.086486,0.5621622 0.1511376,0.4534118 0.7470076,1.3420395 1.2972972,2.0756757 0.05396,0.563421 0.109936,1.132004 0,1.772973 -1.5856236,1.315524 -5.67094212,1.881347 -5.66486451,3.243243 L 0,16 16,16 15.91351,15.481081 c -0.0065,-1.1656 -4.098682,-1.881347 -5.664862,-3.243243 -0.06337,-0.521335 -0.07545,-1.043272 -0.04324,-1.556757 0.501434,-0.7738141 1.172201,-1.7868737 1.34054,-2.2918917 0.0605,-0.2557354 0.08252,-0.4284482 0.129729,-0.6486487 0.150255,-0.1820053 0.278504,-0.3878554 0.38919,-0.6486486 0.311162,-0.7331483 0.291049,-1.4330284 -0.04324,-1.6432432 0.01971,-0.2953966 0.03456,-0.6097082 0.04324,-0.8648649 0,-2.1060324 -1.8084,-3.8054054 -4.064865,-3.8054054 z"
|
||||
id="path3159"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.8 KiB |
@@ -1,40 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 568 400"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="background-image-placeholder.svg"
|
||||
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
|
||||
width="568"
|
||||
height="400"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
id="namedview6"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.0029297"
|
||||
inkscape:cx="109.18014"
|
||||
inkscape:cy="337.01266"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1048"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4" />
|
||||
<!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) -->
|
||||
<path
|
||||
d="m 374.10635,275.08863 v 7.50887 c 0,12.44125 -10.08533,22.52659 -22.52659,22.52659 H 171.36704 c -12.44124,0 -22.52658,-10.08534 -22.52658,-22.52659 V 162.45567 c 0,-12.44124 10.08534,-22.52659 22.52658,-22.52659 h 7.50887 v 22.52659 h -4.69305 a 2.8158239,2.8158239 0 0 0 -2.81582,2.81583 v 114.51018 a 2.8158239,2.8158239 0 0 0 2.81582,2.81582 h 174.58109 a 2.8158239,2.8158239 0 0 0 2.81581,-2.81582 v -4.69305 z M 393.81713,117.4025 H 219.23606 a 2.8158239,2.8158239 0 0 0 -2.81582,2.81582 v 114.51017 a 2.8158239,2.8158239 0 0 0 2.81582,2.81582 h 174.58107 a 2.8158239,2.8158239 0 0 0 2.81581,-2.81582 V 120.21832 a 2.8158239,2.8158239 0 0 0 -2.81581,-2.81582 z m 2.81581,-22.52659 c 12.44126,0 22.5266,10.08534 22.5266,22.52659 v 120.14181 c 0,12.44125 -10.08535,22.5266 -22.5266,22.5266 h -180.2127 c -12.44125,0 -22.5266,-10.08535 -22.5266,-22.5266 V 117.4025 c 0,-12.44125 10.08535,-22.52659 22.5266,-22.52659 z m -123.89623,52.56204 c 0,10.3674 -8.40478,18.77216 -18.77216,18.77216 -10.3674,0 -18.77217,-8.40476 -18.77217,-18.77216 0,-10.3674 8.40477,-18.77216 18.77217,-18.77216 10.3674,0 18.77216,8.40476 18.77216,18.77216 z m -33.78989,45.05318 18.54454,-18.54455 c 2.19916,-2.19915 5.76494,-2.19915 7.96456,0 L 284,192.49113 l 48.58001,-48.58 c 2.19916,-2.19916 5.76493,-2.19916 7.96456,0 l 33.5618,33.56227 v 37.54432 H 238.94683 Z"
|
||||
id="path2"
|
||||
style="fill:#999999;stroke-width:0.469304" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,100 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular, window, document, localStorage, redirectIfNeeded */
|
||||
/* global $ */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
|
||||
app.controller('SetupController', ['$scope', 'Client', function ($scope, Client) {
|
||||
// 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.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
$scope.client = Client;
|
||||
$scope.view = '';
|
||||
$scope.initialized = false;
|
||||
$scope.setupToken = '';
|
||||
$scope.firstTimeLoginUrl = '';
|
||||
|
||||
$scope.owner = {
|
||||
error: null,
|
||||
busy: false,
|
||||
|
||||
email: '',
|
||||
displayName: '',
|
||||
username: '',
|
||||
password: '',
|
||||
|
||||
submit: function () {
|
||||
$scope.owner.busy = true;
|
||||
$scope.owner.error = null;
|
||||
|
||||
var data = {
|
||||
username: $scope.owner.username,
|
||||
password: $scope.owner.password,
|
||||
email: $scope.owner.email,
|
||||
displayName: $scope.owner.displayName,
|
||||
setupToken: $scope.setupToken
|
||||
};
|
||||
|
||||
Client.createAdmin(data, function (error, autoLoginToken) {
|
||||
if (error && error.statusCode === 400) {
|
||||
$scope.owner.busy = false;
|
||||
|
||||
if (error.message === 'Invalid email') {
|
||||
$scope.owner.error = { email: error.message };
|
||||
$scope.owner.email = '';
|
||||
$scope.ownerForm.email.$setPristine();
|
||||
setTimeout(function () { $('#inputEmail').focus(); }, 200);
|
||||
} else {
|
||||
$scope.owner.error = { username: error.message };
|
||||
$scope.owner.username = '';
|
||||
$scope.ownerForm.username.$setPristine();
|
||||
setTimeout(function () { $('#inputUsername').focus(); }, 200);
|
||||
}
|
||||
return;
|
||||
} else if (error) {
|
||||
$scope.owner.busy = false;
|
||||
console.error('Internal error', error);
|
||||
$scope.owner.error = { generic: error.message };
|
||||
return;
|
||||
}
|
||||
|
||||
// set token to autologin on first oidc flow
|
||||
localStorage.cloudronFirstTimeToken = autoLoginToken;
|
||||
|
||||
$scope.firstTimeLoginUrl = '/openid/auth?client_id=cid-webadmin&scope=openid email profile&response_type=code token&redirect_uri=' + window.location.origin + '/authcallback.html';
|
||||
|
||||
setView('finished');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function setView(view) {
|
||||
if (view === 'finished') {
|
||||
$scope.view = 'finished';
|
||||
} else {
|
||||
$scope.view = 'owner';
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
if (redirectIfNeeded(status, 'activation')) return; // redirected to some other view...
|
||||
|
||||
setView(search.view);
|
||||
|
||||
$scope.setupToken = search.setupToken;
|
||||
$scope.initialized = true;
|
||||
|
||||
// Ensure we have a good autofocus
|
||||
setTimeout(function () {
|
||||
$(document).find("[autofocus]:first").focus();
|
||||
}, 250);
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
}]);
|
||||
@@ -1,159 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular, $, showdown */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies']);
|
||||
|
||||
app.filter('markdown2html', function () {
|
||||
var converter = new showdown.Converter({
|
||||
simplifiedAutoLink: true,
|
||||
strikethrough: true,
|
||||
tables: true,
|
||||
openLinksInNewWindow: true
|
||||
});
|
||||
|
||||
return function (text) {
|
||||
return converter.makeHtml(text);
|
||||
};
|
||||
});
|
||||
|
||||
// disable sce for footer https://code.angularjs.org/1.5.8/docs/api/ng/service/$sce
|
||||
app.config(function ($sceProvider) {
|
||||
$sceProvider.enabled(false);
|
||||
});
|
||||
|
||||
app.config(['$translateProvider', function ($translateProvider) {
|
||||
$translateProvider.useStaticFilesLoader({
|
||||
prefix: 'translation/',
|
||||
suffix: '.json'
|
||||
});
|
||||
$translateProvider.preferredLanguage('en');
|
||||
$translateProvider.fallbackLanguage('en');
|
||||
}]);
|
||||
|
||||
// Add shorthand "tr" filter to avoid having ot use "translate"
|
||||
// This is a copy of the code at https://github.com/angular-translate/angular-translate/blob/master/src/filter/translate.js
|
||||
// If we find out how to get that function handle somehow dynamically we can use that, otherwise the copy is required
|
||||
function translateFilterFactory($parse, $translate) {
|
||||
var translateFilter = function (translationId, interpolateParams, interpolation, forceLanguage) {
|
||||
if (!angular.isObject(interpolateParams)) {
|
||||
var ctx = this || {
|
||||
'__SCOPE_IS_NOT_AVAILABLE': 'More info at https://github.com/angular/angular.js/commit/8863b9d04c722b278fa93c5d66ad1e578ad6eb1f'
|
||||
};
|
||||
interpolateParams = $parse(interpolateParams)(ctx);
|
||||
}
|
||||
|
||||
return $translate.instant(translationId, interpolateParams, interpolation, forceLanguage);
|
||||
};
|
||||
|
||||
if ($translate.statefulFilter()) {
|
||||
translateFilter.$stateful = true;
|
||||
}
|
||||
|
||||
return translateFilter;
|
||||
}
|
||||
translateFilterFactory.displayName = 'translateFilterFactory';
|
||||
app.filter('tr', translateFilterFactory);
|
||||
|
||||
|
||||
app.controller('PasswordResetController', ['$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; }, {});
|
||||
|
||||
$scope.initialized = false;
|
||||
$scope.mode = '';
|
||||
$scope.busy = false;
|
||||
$scope.error = false;
|
||||
$scope.branding = null;
|
||||
$scope.username = '';
|
||||
$scope.password = '';
|
||||
$scope.totpToken = '';
|
||||
$scope.passwordResetIdentifier = '';
|
||||
$scope.newPassword = '';
|
||||
$scope.newPasswordRepeat = '';
|
||||
var API_ORIGIN = '<%= apiOrigin %>' || window.location.origin;
|
||||
|
||||
$scope.onPasswordReset = function () {
|
||||
$scope.busy = true;
|
||||
|
||||
var data = {
|
||||
identifier: $scope.passwordResetIdentifier
|
||||
};
|
||||
|
||||
function done(error) {
|
||||
if (error) $scope.error = error.message;
|
||||
$scope.busy = false;
|
||||
$scope.mode = 'passwordResetDone';
|
||||
}
|
||||
|
||||
$http.post(API_ORIGIN + '/api/v1/auth/password_reset_request', data).success(done).error(done);
|
||||
};
|
||||
|
||||
$scope.onNewPassword = function () {
|
||||
$scope.busy = true;
|
||||
|
||||
var data = {
|
||||
resetToken: search.resetToken,
|
||||
password: $scope.newPassword,
|
||||
totpToken: $scope.totpToken
|
||||
};
|
||||
|
||||
function error(data, status) {
|
||||
console.log('error', status);
|
||||
$scope.busy = false;
|
||||
|
||||
if (status === 401) $scope.error = data.message;
|
||||
else if (status === 409) $scope.error = 'Ask your admin for an invite link first';
|
||||
else $scope.error = 'Unknown error';
|
||||
}
|
||||
|
||||
$http.post(API_ORIGIN + '/api/v1/auth/password_reset', data).success(function (data, status) {
|
||||
if (status !== 202) return error(data, status);
|
||||
|
||||
// set token to autologin
|
||||
localStorage.token = data.accessToken;
|
||||
|
||||
$scope.mode = 'newPasswordDone';
|
||||
}).error(function (data, status) {
|
||||
error(data, status);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showPasswordReset = function () {
|
||||
window.document.title = 'Password Reset Request';
|
||||
$scope.mode = 'passwordReset';
|
||||
$scope.passwordResetIdentifier = '';
|
||||
setTimeout(function () { $('#inputPasswordResetIdentifier').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) {
|
||||
$scope.initialized = true;
|
||||
|
||||
if (status !== 200) return;
|
||||
|
||||
if (data.language) $translate.use(data.language);
|
||||
|
||||
$scope.branding = data;
|
||||
}).error(function () {
|
||||
$scope.initialized = false;
|
||||
});
|
||||
|
||||
// Init into the correct view
|
||||
if (search.passwordReset) {
|
||||
$scope.showPasswordReset();
|
||||
} else if (search.resetToken) {
|
||||
$scope.showNewPassword();
|
||||
} else if (search.accessToken || search.access_token) { // auto-login feature
|
||||
localStorage.token = search.accessToken || search.access_token;
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
$scope.showPasswordReset();
|
||||
}
|
||||
}]);
|
||||
@@ -1,40 +0,0 @@
|
||||
/* This file contains helpers which should not be part of client.js */
|
||||
|
||||
angular.module('Application').directive('passwordReveal', 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>';
|
||||
|
||||
return {
|
||||
link: function (scope, elements) {
|
||||
var element = elements[0];
|
||||
|
||||
if (!element.parentNode) {
|
||||
console.error('Wrong password-reveal directive usage. Element has no parent.');
|
||||
return;
|
||||
}
|
||||
|
||||
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,78 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title>Cloudron - Not Found</title>
|
||||
<meta name="description" content="Cloudron - Not Found">
|
||||
|
||||
<!-- Use static style as we can't include local stylesheets -->
|
||||
<style>
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: white;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-family: "Noto Sans", Helvetica, Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.846;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #2196f3;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0a6ebd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('load', (event) => {
|
||||
// https://stackoverflow.com/questions/37437890/check-if-url-has-domain-name-and-not-an-ip
|
||||
const containsLetter = /[a-zA-z]/.test(window.location.hostname); // ignore technicality that IP can contain letters ! http://192.168.0x1.0x1 or http://0xc0.0xa8.1.1
|
||||
const isIPv6 = location.hostname.startsWith('[') && location.hostname.endsWith(']');
|
||||
|
||||
let message;
|
||||
if (!containsLetter || isIPv6) { // ipv4 or ipv6
|
||||
message = 'You cannot view Cloudron dashboard by IP address. Instead, navigate to the domain you configured during setup i.e <b>https://my.domain.example</b> .'
|
||||
+ '<br>If you do not remember your domain, SSH into your server and run <code>cloudron-support --owner-login</code> .'
|
||||
} else { // hostname
|
||||
message = 'You are seeing this page because the DNS record of <b>' + window.location.hostname + '</b> is set to this server\'s IP'
|
||||
+ ' but Cloudron has no app configured for this domain.';
|
||||
}
|
||||
document.getElementById('message').innerHTML = message;
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="content">
|
||||
<p id="message"></p>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,369 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title>Cloudron Restore</title>
|
||||
<meta name="description" content="Cloudron Restore">
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.css?<%= revision %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/autofill-event.js"></script>
|
||||
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script type="text/javascript" src="/js/restore.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="setup" ng-app="Application" ng-controller="RestoreController">
|
||||
|
||||
<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> Cloudron is offline. Reconnecting...</a>
|
||||
|
||||
<div class="main-container ng-cloak text-center" ng-show="busy">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<i class="fa fa-circle-notch fa-spin fa-5x"></i><br/>
|
||||
<h3>{{ message }} ...</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container ng-cloak" ng-show="initialized && !busy">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card" style="max-width: none; padding: 20px;">
|
||||
<form name="configureBackupForm" role="form" novalidate ng-submit="restore()" autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1 text-center">
|
||||
<h2>Cloudron Restore</h2>
|
||||
<p>Provide the backup to restore from</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-bottom: 20px">
|
||||
<div class="col-md-8 col-md-offset-2 text-center">
|
||||
<input type="file" id="backupConfigFileInput" style="display:none"/>
|
||||
<button type="button" class="btn btn-default" onclick="getElementById('backupConfigFileInput').click();">Upload Backup Config</button>
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<p class="has-error text-center" ng-show="error">{{ error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageProviderProvider">Storage provider <sup><a ng-href="https://docs.cloudron.io/backups/#storage-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" id="storageProviderProvider" ng-model="provider" ng-options="a.value as a.name for a in storageProviders" ng-change=clearForm()></select>
|
||||
</div>
|
||||
|
||||
<!-- mountpoint -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.mountPoint }" ng-show="provider === 'mountpoint'">
|
||||
<label class="control-label" for="inputConfigureMountPoint">Mountpoint</label>
|
||||
<input type="text" class="form-control" ng-model="mountPoint" id="inputConfigureMountPoint" name="mountPoint" ng-disabled="busy" placeholder="Folder where filesystem is mounted" ng-required="provider === 'mountpoint'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs' || provider === 'nfs' || provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupHost">Server IP or Hostname</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.host" id="configureBackupHost" name="host" ng-disabled="busy" placeholder="Server IP or hostname" ng-required="provider === 'cifs' || provider === 'nfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="checkbox" ng-show="provider === 'cifs'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mountOptions.seal">Use seal encryption. Requires at least SMB v3</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs' || provider === 'nfs' || provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupRemoteDir">Remote Directory</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.remoteDir" id="configureBackupRemoteDir" name="remoteDir" ng-disabled="busy" placeholder="/share" ng-required="provider === 'cifs' || provider === 'nfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupUsername">Username ({{ provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.username" id="configureBackupUsername" name="cifsUsername" ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupPassword">Password ({{ provider }})</label>
|
||||
<input type="password" class="form-control" ng-model="mountOptions.password" id="configureBackupPassword" name="cifsPassword" ng-disabled="busy" password-reveal>
|
||||
</div>
|
||||
|
||||
<!-- EXT4/XFS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.diskPath }" ng-show="provider === 'ext4' || provider === 'xfs'">
|
||||
<label class="control-label" for="inputConfigureDiskPath">Disk Path</label>
|
||||
<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>
|
||||
<input type="number" class="form-control" ng-model="mountOptions.port" id="configureBackupPort" name="port" ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupUser">SSH User</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.user" id="configureBackupUser" name="user" ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPrivateKey">SSH Private Key</label>
|
||||
<textarea class="form-control" ng-model="mountOptions.privateKey" id="configureBackupPrivateKey" name="privateKey" ng-disabled="busy"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Filesystem -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.backupFolder }" ng-show="provider === 'filesystem'">
|
||||
<label class="control-label" for="inputConfigureBackupFolder">Local backup directory</label>
|
||||
<input type="text" class="form-control" ng-model="backupFolder" id="inputConfigureBackupFolder" name="backupFolder" ng-disabled="busy" placeholder="Directory for backups" ng-required="provider === 'filesystem'">
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.endpoint }" ng-show="provider === 'minio' || provider === 'upcloud-objectstorage' || provider === 'backblaze-b2' || provider === 'cloudflare-r2' || provider === 's3-v4-compat' || provider === 'idrive-e2'">
|
||||
<label class="control-label" for="inputConfigureBackupEndpoint">Endpoint</label>
|
||||
<input type="text" class="form-control" ng-model="endpoint" id="inputConfigureBackupEndpoint" name="endpoint" ng-disabled="busy" placeholder="URL" ng-required="provider === 'minio' || provider === 'upcloud-objectstorage' || provider === 'backblaze-b2' || provider === 'cloudflare-r2' || provider === 's3-v4-compat' || provider === 'idrive-e2'">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="provider === 'minio' || provider === 's3-v4-compat'" >
|
||||
<label>
|
||||
<input type="checkbox" ng-model="acceptSelfSignedCerts" id="inputConfigureBackupSelfSigned">
|
||||
Accept Self-signed certificate
|
||||
</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.bucket }" ng-show="s3like(provider) || provider === 'gcs'">
|
||||
<label class="control-label" for="inputConfigureBackupBucket">Bucket name</label>
|
||||
<input type="text" class="form-control" ng-model="bucket" id="inputConfigureBackupBucket" name="bucket" ng-disabled="busy" ng-required="s3like(provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.prefix }" ng-show="provider !== 'filesystem' && provider !== 'noop'">
|
||||
<label class="control-label" for="inputConfigureBackupPrefix">Prefix</label>
|
||||
<input type="text" class="form-control" ng-model="prefix" id="inputConfigureBackupPrefix" name="prefix" ng-disabled="busy" placeholder="Prefix for backup file names">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 's3'">
|
||||
<label class="control-label" for="inputConfigureBackupS3Region">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupS3Region" ng-model="region" ng-options="a.value as a.name for a in s3Regions" ng-disabled="busy" ng-required="provider === 's3'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 's3-v4-compat'">
|
||||
<label class="control-label" for="inputConfigureBackupS3V4CompatRegion">Region</label>
|
||||
<input class="form-control" type="text" name="region" id="inputConfigureBackupS3V4CompatRegion" ng-model="region" ng-disabled="busy" placeholder="Leave empty to use us-east-1 as default"></input>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'digitalocean-spaces'">
|
||||
<label class="control-label" for="inputConfigureBackupDORegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupDORegion" ng-model="endpoint" ng-options="a.value as a.name for a in doSpacesRegions" ng-disabled="busy" ng-required="provider === 'digitalocean-spaces'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'exoscale-sos'">
|
||||
<label class="control-label" for="inputConfigureBackupExoscaleRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupExoscaleRegion" ng-model="endpoint" ng-options="a.value as a.name for a in exoscaleSosRegions" ng-disabled="busy" ng-required="provider === 'exoscale-sos'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'wasabi'">
|
||||
<label class="control-label" for="inputConfigureBackupWasabiRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupWasabiRegion" ng-model="endpoint" ng-options="a.value as a.name for a in wasabiRegions" ng-disabled="busy" ng-required="provider === 'wasabi'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'scaleway-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupScalewayRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupScalewayRegion" ng-model="endpoint" ng-options="a.value as a.name for a in scalewayRegions" ng-disabled="busy" ng-required="provider === 'scaleway-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'linode-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupLinodeRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupLinodeRegion" ng-model="endpoint" ng-options="a.value as a.name for a in linodeRegions" ng-disabled="busy" ng-required="provider === 'linode-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'ovh-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupOvhRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupOvhRegion" ng-model="endpoint" ng-options="a.value as a.name for a in ovhRegions" ng-disabled="busy" ng-required="provider === 'ovh-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'ionos-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupIonosRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupIonosRegion" ng-model="endpoint" ng-options="a.value as a.name for a in ionosRegions" ng-disabled="busy" ng-required="provider === 'ionos-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'vultr-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupVultrRegion">Region</label>
|
||||
<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)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.secretAccessKey }" ng-show="s3like(provider)">
|
||||
<label class="control-label" for="inputConfigureBackupSecretAccessKey">Secret access key</label>
|
||||
<input type="text" class="form-control" ng-model="secretAccessKey" id="inputConfigureBackupSecretAccessKey" name="secretAccessKey" ng-disabled="busy" ng-required="s3like(provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.gcsKeyInput }" ng-show="provider === 'gcs'">
|
||||
<label class="control-label" for="gcsKeyInput">Service Account Key</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="gcsKey.keyFileName" id="gcsKeyInput" name="cert" onclick="getElementById('gcsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="busy" ng-required="provider === 'gcs'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageFormat">Storage Format</label>
|
||||
<select class="form-control" id="storageFormat" ng-change="key = ''" ng-model="format" ng-options="a.value as a.name for a in formats"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.remotePath }">
|
||||
<label class="control-label" for="inputConfigureRemotePath">Backup Path<sup><a ng-href="https://docs.cloudron.io/backups/#restore-cloudron" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
|
||||
<input type="text" class="form-control" ng-model="remotePath" name="inputConfigureBackupId" placeholder="e.g. 2024-02-20-130007-637/box_v7.4.3.tar.gz" required ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.key }">
|
||||
<label class="control-label" for="inputConfigureBackupPassword">Encryption password <span ng-hide="encrypted">(optional)</span></label>
|
||||
<input type="text" class="form-control" ng-model="password" id="inputConfigureBackupPassword" name="prefix" ng-disabled="busy" placeholder="Passphrase used to encrypt the backups" ng-required="encrypted">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="format === 'rsync' && password.length !== 0">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="encryptedFilenames">Decrypt Filenames</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="skipDnsSetup"><b>Dry run</b></sup>
|
||||
</label>
|
||||
<br/>
|
||||
<small>When enabled, apps are restored but the DNS records are not updated to point to this server. To access the dashboard, this browser's host must have an entry in <code>/etc/hosts</code> for the dashboard domain to this server's IP.
|
||||
See the <a href="https://docs.cloudron.io/backups/#dry-run" target="_blank">docs</a> for more information.</small>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="configureBackupForm.$invalid"/>
|
||||
|
||||
<div uib-collapse="!advancedVisible">
|
||||
<!-- IPv4 provider -->
|
||||
<div class="form-group">
|
||||
<label class="control-label">IPv4 Configuration <sup><a ng-href="https://docs.cloudron.io/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="ipv4Config.provider" ng-options="a.value as a.name for a in ipProviders"></select>
|
||||
</div>
|
||||
|
||||
<!-- IPv4 Fixed -->
|
||||
<div class="form-group" ng-show="ipv4Config.provider === 'fixed'">
|
||||
<label class="control-label">IPv4 Address</label>
|
||||
<input type="text" class="form-control" ng-model="ipv4Config.ip" name="ipv4" ng-required="ipv4Config.provider === 'fixed'">
|
||||
</div>
|
||||
|
||||
<!-- IPv4 Network Interface -->
|
||||
<div class="form-group" ng-show="ipv4Config.provider === 'network-interface'">
|
||||
<label class="control-label">IPv4 Interface Name</label>
|
||||
<input type="text" class="form-control" ng-model="ipv4Config.ifname" name="ifname4" ng-required="ipv4Config.provider === 'network-interface'">
|
||||
</div>
|
||||
|
||||
|
||||
<!-- IPv6 provider -->
|
||||
<div class="form-group">
|
||||
<label class="control-label">IPv6 Configuration <sup><a ng-href="https://docs.cloudron.io/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="ipv6Config.provider" ng-options="a.value as a.name for a in ipProviders"></select>
|
||||
</div>
|
||||
|
||||
<!-- IPv6 Fixed -->
|
||||
<div class="form-group" ng-show="ipv6Config.provider === 'fixed'">
|
||||
<label class="control-label">IPv6 Address</label>
|
||||
<input type="text" class="form-control" ng-model="ipv6Config.ip" name="ipv6" ng-required="ipv6Config.provider === 'fixed'">
|
||||
</div>
|
||||
|
||||
<!-- IPv6 Network Interface -->
|
||||
<div class="form-group" ng-show="ipv6Config.provider === 'network-interface'">
|
||||
<label class="control-label">IPv6 Interface Name</label>
|
||||
<input type="text" class="form-control" ng-model="ipv6Config.ifname" name="ifname6" ng-required="ipv6Config.provider === 'network-interface'">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<a href="" ng-click="advancedVisible = true" ng-hide="advancedVisible">Advanced settings...</a>
|
||||
<a href="" ng-click="advancedVisible = false" ng-show="advancedVisible">Hide Advanced settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="configureBackupForm.$invalid"/><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> Restore</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">©2022 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,393 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title>Cloudron Domain Setup</title>
|
||||
<meta name="description" content="Cloudron Domain Setup">
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.css?<%= revision %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/autofill-event.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/3rdparty/js/clipboard.min.js"></script>
|
||||
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script type="text/javascript" src="/js/setup.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="setup" ng-app="Application" ng-controller="SetupDNSController">
|
||||
|
||||
<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> Cloudron is offline. Reconnecting...</a>
|
||||
|
||||
<div class="main-container ng-cloak text-center" ng-show="state === 'waitingForDnsSetup'">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3 text-center">
|
||||
<i class="fa fa-circle-notch fa-spin fa-5x"></i><br/>
|
||||
<h3>{{ message }} ...</h3>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<p>
|
||||
Please wait while Cloudron is setting up the dashboard.<br/>
|
||||
You can follow the logs on the server at <code class="clipboard hand" data-clipboard-text="/home/yellowtent/platformdata/logs/box.log" uib-tooltip="{{ clipboardDone ? 'Copied' : 'Click to copy' }}" tooltip-placement="right">/home/yellowtent/platformdata/logs/box.log</code>
|
||||
</p>
|
||||
<br/>
|
||||
<br/>
|
||||
<p ng-show="taskMinutesActive >= 4">
|
||||
If setup appears stuck, it can be restarted by running <code class="clipboard hand" data-clipboard-text="systemctl restart box" uib-tooltip="{{ clipboardDone ? 'Copied' : 'Click to copy' }}" tooltip-placement="right">systemctl restart box</code> and reloading this page.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container ng-cloak" ng-show="state === 'initialized'">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card" style="max-width: none; padding: 20px;">
|
||||
<form name="dnsCredentialsForm" role="form" novalidate ng-submit="setDnsCredentials()" autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1 text-center">
|
||||
<h1>Domain Setup</h1>
|
||||
<p class="has-error text-center" ng-show="error.setup">{{ error.setup }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': dnsCredentialsForm.domain.$dirty && dnsCredentialsForm.domain.$invalid }">
|
||||
<label class="control-label">Domain <sup><a ng-href="https://docs.cloudron.io/installation/#domain-setup" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.domain" name="domain" placeholder="example.com" required autofocus ng-disabled="dnsCredentials.busy">
|
||||
<div class="text-danger" ng-show="dnsCredentials.domain.indexOf('my.') === 0 && dnsCredentials.domain.length > 3">Are you sure about this domain? The dashboard will be at <b>my.{{ dnsCredentials.domain }}</b></div>
|
||||
<p style="margin-top: 5px; font-size: 13px;">
|
||||
Apps will be installed on subdomains of this domain. The dashboard will be available on the <b>my</b> subdomain. You can add more domains later.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<h3 class="text-center">Domain Configuration <sup><a ng-href="https://docs.cloudron.io/domains/#dns-providers" class="help" target="_blank" tabindex="-1"><i class="fa fa-question-circle"></i></a></sup> </h3>
|
||||
<p class="has-error text-center" ng-show="error.dnsCredentials">{{ error.dnsCredentials }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">DNS Provider</label>
|
||||
<select class="form-control" ng-model="dnsCredentials.provider" ng-options="a.value as a.name for a in dnsProvider" ng-disabled="dnsCredentials.busy" ng-change="setDefaultTlsProvider()"></select>
|
||||
</div>
|
||||
|
||||
<!-- Route53 -->
|
||||
<div ng-if="provider === 'ami'" ng-show="dnsCredentials.provider === 'route53'">
|
||||
<b class="has-error">This feature is disabled in AWS Marketplace AMI. <a href="https://docs.aws.amazon.com/marketplace/latest/userguide/product-and-ami-policies.html" target="_blank">AWS Marketplace Policy</a> disallows
|
||||
AMIs from requesting IAM credentials from users to access Route53 hosted domains. Please use the Wildcard or Manual provider instead.</b>
|
||||
</div>
|
||||
|
||||
<div ng-if="provider !== 'ami'" class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.accessKeyId.$dirty && dnsCredentialsForm.accessKeyId.$invalid }" ng-show="dnsCredentials.provider === 'route53'">
|
||||
<label class="control-label">Access Key Id</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.accessKeyId" name="accessKeyId" placeholder="Access Key Id" ng-minlength="16" ng-maxlength="32" ng-required="dnsCredentials.provider === 'route53'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div ng-if="provider !== 'ami'" class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.secretAccessKey.$dirty && dnsCredentialsForm.secretAccessKey.$invalid }" ng-show="dnsCredentials.provider === 'route53'">
|
||||
<label class="control-label">Secret Access Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.secretAccessKey" name="secretAccessKey" placeholder="Secret Access Key" ng-required="dnsCredentials.provider === 'route53'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Google Cloud DNS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'gcdns'">
|
||||
<label class="control-label">Service Account Key</label>
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcdnsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="dnsCredentials.gcdnsKey.keyFileName" id="gcdnsKeyInput" name="cert" onclick="getElementById('gcdnsKeyFileInput').click();" style="cursor: pointer;" ng-required="dnsCredentials.provider === 'gcdns'" ng-disabled="dnsCredentials.busy">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcdnsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DigitalOcean -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.digitalOceanToken.$dirty && dnsCredentialsForm.digitalOceanToken.$invalid }" ng-show="dnsCredentials.provider === 'digitalocean'">
|
||||
<label class="control-label">DigitalOcean Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.digitalOceanToken" name="digitalOceanToken" placeholder="API Token" ng-required="dnsCredentials.provider === 'digitalocean'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Gandi -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.gandiApiKey.$dirty && dnsCredentialsForm.gandiApiKey.$invalid }" ng-show="dnsCredentials.provider === 'gandi'">
|
||||
<label class="control-label">Gandi API Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.gandiApiKey" name="gandiApiKey" placeholder="API Key" ng-required="dnsCredentials.provider === 'gandi'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- GoDaddy -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.godaddyApiKey.$dirty && dnsCredentialsForm.godaddyApiKey.$invalid }" ng-show="dnsCredentials.provider === 'godaddy'">
|
||||
<label class="control-label">API Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.godaddyApiKey" name="godaddyApiKey" placeholder="API Key" ng-minlength="1" ng-required="dnsCredentials.provider === 'godaddy'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.godaddyApiSecret.$dirty && dnsCredentialsForm.godaddyApiSecret.$invalid }" ng-show="dnsCredentials.provider === 'godaddy'">
|
||||
<label class="control-label">API Secret</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.godaddyApiSecret" name="godaddyApiSecret" placeholder="API Secret" ng-required="dnsCredentials.provider === 'godaddy'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Netcup -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.netcupCustomerNumber.$dirty && dnsCredentialsForm.netcupCustomerNumber.$invalid }" ng-show="dnsCredentials.provider === 'netcup'">
|
||||
<label class="control-label">Customer Number</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.netcupCustomerNumber" name="netcupCustomerNumber" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'netcup'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.netcupApiKey.$dirty && dnsCredentialsForm.netcupApiKey.$invalid }" ng-show="dnsCredentials.provider === 'netcup'">
|
||||
<label class="control-label">API Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.netcupApiKey" name="netcupApiKey" ng-disabled="dnsCredentials.busy" ng-minlength="1" ng-required="dnsCredentials.provider === 'netcup'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.netcupApiPassword.$dirty && dnsCredentialsForm.netcupApiPassword.$invalid }" ng-show="dnsCredentials.provider === 'netcup'">
|
||||
<label class="control-label">API Password</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.netcupApiPassword" name="netcupApiPassword" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'netcup'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.cloudflareToken.$dirty && dnsCredentialsForm.cloudflareToken.$invalid }" ng-show="dnsCredentials.provider === 'cloudflare'">
|
||||
<label class="control-label">Token Type</label>
|
||||
<select class="form-control" ng-model="dnsCredentials.cloudflareTokenType">
|
||||
<option value="GlobalApiKey">Global API Key</option>
|
||||
<option value="ApiToken">API Token</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.cloudflareToken.$dirty && dnsCredentialsForm.cloudflareToken.$invalid }" ng-show="dnsCredentials.provider === 'cloudflare'">
|
||||
<label class="control-label" ng-show="dnsCredentials.cloudflareTokenType === 'GlobalApiKey'">Global API Key</label>
|
||||
<label class="control-label" ng-show="dnsCredentials.cloudflareTokenType === 'ApiToken'">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.cloudflareToken" name="cloudflareToken" placeholder="API Key/Token" ng-required="dnsCredentials.provider === 'cloudflare'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.cloudflareEmail.$dirty && dnsCredentialsForm.cloudflareEmail.$invalid }" ng-show="dnsCredentials.provider === 'cloudflare' && dnsCredentials.cloudflareTokenType === 'GlobalApiKey'">
|
||||
<label class="control-label">Cloudflare Email</label>
|
||||
<input type="email" class="form-control" ng-model="dnsCredentials.cloudflareEmail" name="cloudflareEmail" placeholder="Cloudflare Account Email" ng-required="dnsCredentials.provider === 'cloudflare' && dnsCredentials.cloudflareTokenType === 'GlobalApiKey'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="dnsCredentials.provider === 'cloudflare'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="dnsCredentials.cloudflareDefaultProxyStatus"> Enable proxying for new DNS records
|
||||
<sup><a ng-href="https://docs.cloudron.io/domains/#cloudflare-dns" class="help" target="_blank" tabIndex="-1"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Name.com -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.nameComUsername.$dirty && dnsCredentialsForm.nameComUsername.$invalid }" ng-show="dnsCredentials.provider === 'namecom'">
|
||||
<label class="control-label">Name.com Username</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.nameComUsername" name="nameComUsername" placeholder="Name.com Username" ng-required="dnsCredentials.provider === 'namecom'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.nameComToken.$dirty && dnsCredentialsForm.nameComToken.$invalid }" ng-show="dnsCredentials.provider === 'namecom'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.nameComToken" name="nameComToken" placeholder="Name.com API Token" ng-required="dnsCredentials.provider === 'namecom'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Namecheap -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.namecheapUsername.$dirty && dnsCredentialsForm.namecheapUsername.$invalid }" ng-show="dnsCredentials.provider === 'namecheap'">
|
||||
<label class="control-label">Namecheap Username</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.namecheapUsername" name="namecheapUsername" placeholder="Namecheap Username" ng-required="dnsCredentials.provider === 'namecheap'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.namecheapApiKey.$dirty && dnsCredentialsForm.namecheapApiKey.$invalid }" ng-show="dnsCredentials.provider === 'namecheap'">
|
||||
<label class="control-label">API Key</label>
|
||||
<p class="small text-info" ng-show="dnsCredentials.provider === 'namecheap'"><b>The server IP needs to be whitelisted for this API Key.</b></p>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.namecheapApiKey" name="namecheapApiKey" placeholder="Namecheap API Key" ng-required="dnsCredentials.provider === 'namecheap'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Linode -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'linode'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.linodeToken" name="linodeToken" ng-required="dnsCredentials.provider === 'linode'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- Bunny -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'bunny'">
|
||||
<label class="control-label">Access Key</label>
|
||||
<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'">
|
||||
<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>
|
||||
|
||||
<!-- Hetzner -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'hetzner'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.hetznerToken" name="hetznerToken" ng-required="dnsCredentials.provider === 'hetzner'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- Vultr -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'vultr'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.vultrToken" name="vultrToken" ng-required="dnsCredentials.provider === 'vultr'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- deSEC -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'desec'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.deSecToken" name="deSecToken" ng-required="dnsCredentials.provider === 'desec'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- Wildcard -->
|
||||
<p class="small text-info" ng-show="dnsCredentials.provider === 'wildcard'">
|
||||
<span>Set up A records for <b>*.{{ dnsCredentials.domain || 'example.com' }}.</b> and <b>{{ dnsCredentials.domain || 'example.com' }}.</b> to this server's IP.</span>
|
||||
</p>
|
||||
|
||||
<!-- Manual -->
|
||||
<p class="small text-info" ng-show="dnsCredentials.provider === 'manual'">
|
||||
<span>Set up an A record for <b>my.{{ dnsCredentials.domain || 'example.com' }}.</b> to this server's IP.<br/></span>
|
||||
</p>
|
||||
|
||||
<p class="small text-info" ng-show="needsPort80(dnsCredentials.provider, dnsCredentials.tlsConfig.provider)">Let's Encrypt requires your server to be reachable on port 80</p>
|
||||
|
||||
<div ng-show="provider === 'ami'">
|
||||
<h3 class="text-center">Owner verification</h3>
|
||||
<p class="has-error text-center" ng-show="error.ami">{{ error.ami }}</p>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': dnsCredentialsForm.instanceId.$dirty && (dnsCredentialsForm.instanceId.$invalid || error.ami) }">
|
||||
<label class="control-label">EC2 Instance Id</label>
|
||||
<input type="text" class="form-control" ng-model="instanceId" id="inputInstanceId" name="instanceId" placeholder="i-0123456789abcdefg" ng-minlength="1" ng-required="provider === 'ami'" autocomplete="off">
|
||||
</div>
|
||||
<p style="margin-top: 5px; font-size: 13px;">Provide the EC2 instance id to verify you have access to this server.</p>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div uib-collapse="!advancedVisible">
|
||||
<div class="form-group">
|
||||
<label class="control-label">DNS Zone Name (Optional) <sup><a ng-href="https://docs.cloudron.io/domains/#zone-name" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.zoneName" name="zoneName" placeholder="Defaults to TLD" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Certificate Provider <sup><a ng-href="https://docs.cloudron.io/certificates/#certificate-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="dnsCredentials.tlsConfig.provider" ng-options="a.value as a.name for a in tlsProvider" ng-disabled="dnsCredentials.busy"></select>
|
||||
</div>
|
||||
|
||||
<!-- IPv4 provider -->
|
||||
<div class="form-group">
|
||||
<label class="control-label">IPv4 Configuration <sup><a ng-href="https://docs.cloudron.io/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="ipv4Config.provider" ng-options="a.value as a.name for a in ipProviders"></select>
|
||||
</div>
|
||||
|
||||
<!-- IPv4 Fixed -->
|
||||
<div class="form-group" ng-show="ipv4Config.provider === 'fixed'">
|
||||
<label class="control-label">IPv4 Address</label>
|
||||
<input type="text" class="form-control" ng-model="ipv4Config.ip" name="ipv4" ng-required="ipv4Config.provider === 'fixed'">
|
||||
</div>
|
||||
|
||||
<!-- IPv4 Network Interface -->
|
||||
<div class="form-group" ng-show="ipv4Config.provider === 'network-interface'">
|
||||
<label class="control-label">IPv4 Interface Name</label>
|
||||
<input type="text" class="form-control" ng-model="ipv4Config.ifname" name="ifname4" ng-required="ipv4Config.provider === 'network-interface'">
|
||||
</div>
|
||||
|
||||
<!-- IPv6 provider -->
|
||||
<div class="form-group">
|
||||
<label class="control-label">IPv6 Configuration <sup><a ng-href="https://docs.cloudron.io/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="ipv6Config.provider" ng-options="a.value as a.name for a in ipProviders"></select>
|
||||
</div>
|
||||
|
||||
<!-- IPv6 Fixed -->
|
||||
<div class="form-group" ng-show="ipv6Config.provider === 'fixed'">
|
||||
<label class="control-label">IPv6 Address</label>
|
||||
<input type="text" class="form-control" ng-model="ipv6Config.ip" name="ipv6" ng-required="ipv6Config.provider === 'fixed'">
|
||||
</div>
|
||||
|
||||
<!-- IPv6 Network Interface -->
|
||||
<div class="form-group" ng-show="ipv6Config.provider === 'network-interface'">
|
||||
<label class="control-label">IPv6 Interface Name</label>
|
||||
<input type="text" class="form-control" ng-model="ipv6Config.ifname" name="ifname6" ng-required="ipv6Config.provider === 'network-interface'">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a href="" ng-click="advancedVisible = true" ng-hide="advancedVisible">Advanced settings...</a>
|
||||
<a href="" ng-click="advancedVisible = false" ng-show="advancedVisible">Hide Advanced settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="dnsCredentialsForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="dnsCredentials.busy"></i> Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center"><small>Looking to <a ng-href="/restore.html{{ search }}">restore?</a></small></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center" ng-show="state === 'waitingForDnsSetup' || state === 'initialized'">
|
||||
<span class="text-muted">©2022 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,278 +0,0 @@
|
||||
<!-- Modal postinstall confirm -->
|
||||
<div class="modal fade" id="appsPostInstallConfirmModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<img ng-src="{{appPostInstallConfirm.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-info-icon"/>
|
||||
<div class="app-info-title">
|
||||
{{ appPostInstallConfirm.app.manifest.title }}<br/>
|
||||
<span class="text-muted text-small">{{ 'app.appInfo.package' | tr }} <a ng-href="/#/appstore/{{appPostInstallConfirm.app.manifest.id}}?version={{appPostInstallConfirm.app.manifest.version}}">v{{ appPostInstallConfirm.app.manifest.version }}</a> </span>
|
||||
<br/>
|
||||
<span ng-show="appPostInstallConfirm.app.manifest.documentationUrl" class="text-small"><a target="_blank" ng-href="{{appPostInstallConfirm.app.manifest.documentationUrl}}">{{ 'app.docsAction' | tr }}</a> </span>
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!--
|
||||
<p ng-show="appPostInstallConfirm.app.manifest.addons.email">{{ 'app.appInfo.ssoEmail' | tr }}</p>
|
||||
<p ng-show="appPostInstallConfirm.app.sso && !appPostInstallConfirm.app.manifest.addons.email">{{ 'app.appInfo.sso' | tr }}</p>
|
||||
-->
|
||||
<div ng-bind-html="appPostInstallConfirm.message | markdown2html"></div>
|
||||
<div ng-show="appPostInstallConfirm.app.manifest.documentationUrl" ng-bind-html="'app.appInfo.appDocsUrl' | tr:{ docsUrl: appPostInstallConfirm.app.manifest.documentationUrl, title: appPostInstallConfirm.app.manifest.title, forumUrl: (appPostInstallConfirm.app.manifest.forumUrl || 'https://forum.cloudron.io') }"></div>
|
||||
|
||||
<div style="margin-top: 10px; margin-bottom: 5px;" ng-show="pendingChecklistItems(appPostInstallConfirm.app)">
|
||||
<label class="control-label">{{ 'app.appInfo.checklist' | tr }}</label>
|
||||
</div>
|
||||
<div ng-repeat="item in appPostInstallConfirm.app.checklist">
|
||||
<div class="checklist-item" ng-hide="item.acknowledged">
|
||||
<span ng-bind-html="item.message | markdown2html"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<a class="btn btn-success" ng-href="{{ 'https://' + appPostInstallConfirm.app.fqdn }}" target="_blank" ng-click="appPostInstallConfirm.submit()">{{ 'app.appInfo.openAction' | tr:{ app: appPostInstallConfirm.app.manifest.title } }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal applinks edit -->
|
||||
<div class="modal fade" id="applinksEditModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'app.editApplinkDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="applinksEditForm" role="form" ng-submit="applinksEdit.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (applinksEditForm.upstreamUri.$dirty && applinksEditForm.upstreamUri.$invalid) || (!applinksEditForm.upstreamUri.$dirty && applinksEdit.error.upstreamUri) }">
|
||||
<label class="control-label">{{ 'app.applinks.upstreamUri' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="applinksEdit.upstreamUri" name="upstreamUri" id="inputUpstreamUri" autofocus autocomplete="off" required>
|
||||
<span class="text-danger" ng-show="applinksEdit.error.upstreamUri">{{ applinksEdit.error.upstreamUri }}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'app.applinks.label' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="applinksEdit.label" name="label" id="inputLabel" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<label class="control-label">{{ 'app.display.icon' | tr }}</label>
|
||||
</div>
|
||||
<div id="previewIcon" class="app-custom-icon" ng-click="applinksEdit.showCustomIconSelector()">
|
||||
<img ng-src="{{ applinksEdit.iconUrl() || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)"/>
|
||||
<i class="picture-edit-indicator fa fa-pencil-alt"></i>
|
||||
</div>
|
||||
<a href="" style="font-weight: normal;" ng-click="applinksEdit.resetCustomIcon()">{{ 'app.applinks.clearIconAction' | tr }}</a> - <span class="text-small">{{ 'app.applinks.clearIconDescription' | tr }}</span>
|
||||
<input type="file" id="applinksEditIconFileInput" style="display: none" accept="image/png"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'app.display.tags' | tr }}</label>
|
||||
<tag-input class="form-control" placeholder="{{ 'app.display.tagsPlaceholder' | tr }}" taglist="applinksEdit.tags" name="tags" uib-tooltip="{{ 'app.display.tagsTooltip' | tr }}"></tag-input>
|
||||
</div>
|
||||
|
||||
<label class="control-label">{{ 'app.accessControl.userManagement.dashboardVisibility' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#dashboard-visibility" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="applinksEdit.accessRestrictionOption" value="any">
|
||||
<span>{{ 'app.accessControl.userManagement.visibleForAllUsers' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="applinksEdit.accessRestrictionOption" value="groups">
|
||||
<span>{{ 'app.accessControl.userManagement.visibleForSelected' | tr }}</span>
|
||||
<span class="label label-danger" ng-show="applinksEdit.accessRestrictionOption === 'groups' && !applinksEdit.isAccessRestrictionValid()">{{ 'appstore.installDialog.errorUserManagementSelectAtLeastOne' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div style="margin-left: 20px; display: flex;">
|
||||
<div>
|
||||
{{ 'appstore.installDialog.users' | tr }}: <multiselect name="accessUsersSelect" class="input-sm stretch" ng-model="applinksEdit.accessRestriction.users" ng-disabled="applinksEdit.accessRestrictionOption !== 'groups'" options="(user.username || user.email) for user in allUsers" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ 'appstore.installDialog.groups' | tr }}: <multiselect name="accessGroupsSelect" class="input-sm stretch" ng-model="applinksEdit.accessRestriction.groups" ng-disabled="applinksEdit.accessRestrictionOption !== 'groups'" options="group.name for group in allGroups" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="applinksEditForm.$invalid || applinksEdit.busyEdit || applinks.busyRemove"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger pull-left" ng-click="applinksEdit.remove()" ng-disabled="applinksEdit.busyRemove || applinksEdit.busyEdit"><i class="fa fa-circle-notch fa-spin" ng-show="applinksEdit.busyRemove"></i> {{ 'app.editApplinkDialog.deleteAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="applinksEdit.submit()" ng-disabled="applinksEditForm.$invalid || applinksEdit.busyRemove || applinksEdit.busyEdit"><i class="fa fa-circle-notch fa-spin" ng-show="applinksEdit.busyEdit"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content content-large">
|
||||
|
||||
<!-- Workaround for select-all issue, see commit message -->
|
||||
<div style="font-size: 1px;"> </div>
|
||||
|
||||
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && user.isAtLeastAdmin">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<br/><br/><br/><br/>
|
||||
<h1><i class="fa fa-cloud-download fa-fw"></i> {{ 'apps.noApps.title' | tr }}</h1>
|
||||
<br/></br>
|
||||
<h3 ng-bind-html="'apps.noApps.description' | tr:{ appStoreLink: '#/appstore' }"></h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && !user.isAtLeastAdmin">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<br/><br/><br/><br/>
|
||||
<h1>{{ 'apps.noAccess.title' | tr }}</h1>
|
||||
<br/></br>
|
||||
<h3>{{ 'apps.noAccess.description' | tr }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="view-header" ng-show="installedApps.length > 0" style="padding-right: 0;">
|
||||
{{ 'apps.title' | tr }}
|
||||
<div class="view-header-search-bar">
|
||||
<form class="form-inline">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" style="width: 300px" placeholder="{{ 'apps.searchPlaceholder' | tr }} ( / )" id="appSearch" ng-model="appSearch"/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-class="{ 'active': showFilter }" ng-click="toggleFilter()"><i class="fas fa-filter"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
<button class="btn btn-default" type="button" ng-click="toggleView()"><i class="fas" ng-class="{ 'fa-list': view === VIEWS.GRID, 'fa-grip': view === VIEWS.LIST }"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<div ng-show="showFilter" class="view-header-filter-bar">
|
||||
<form class="form-inline">
|
||||
<multiselect ng-model="selectedGroup" ng-show="user.isAtLeastAdmin && groups.length > 1" ms-header="{{ selectedGroup.name }}" options="group.name for group in groups" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<multiselect ng-model="selectedState" ng-show="user.isAtLeastAdmin" ms-header="{{ 'apps.stateFilterHeader' | tr }}" ms-selected="{{ selectedState }}" options="state.label for state in states" data-multiple="false"></multiselect>
|
||||
<multiselect ng-model="selectedTags" ng-show="user.isAtLeastAdmin && tags.length > 0" ms-header="{{ 'apps.tagsFilterHeaderAll' | tr }}" ms-selected="{{ 'apps.tagsFilterHeader' | tr:{ tags: selectedTags.join(', ') } }}" options="tag for tag in tags" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<multiselect ng-model="selectedDomain" data-compare-by="domain" ms-selected="{{ selectedDomain.domain }}" options="domain.domain for domain in filterDomains" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<!-- <button class="btn btn-primary" ng-disabled="!selectedTags.length && !selectedState.state && selectedGroup._unset && selectedDomain._alldomains" ng-click="clearAllFilter()">{{ 'apps.filter.clearAll' | tr }}</button> -->
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
|
||||
<div class="app-grid" ng-show="view === VIEWS.GRID">
|
||||
<div class="grid-item" ng-class="{ 'stopped': app.runState === 'stopped' }" ng-repeat="app in installedApps | selectedGroupAccessFilter:selectedGroup | selectedStateFilter:selectedState | selectedTagFilter:selectedTags | selectedDomainFilter:selectedDomain | appSearchFilter:appSearch | orderBy:orderByFilter">
|
||||
<div class="grid-item-content" uib-tooltip="{{ app.fqdn }}" tooltip-append-to-body="true">
|
||||
<a ng-show="app.type !== APP_TYPES.LINK && isOperator(app)" ng-href="#/app/{{ app.id}}/info" class="btn btn-lg btn-default grid-item-action"><i class="fas fa-cog"></i></a>
|
||||
<div ng-show="app.type === APP_TYPES.LINK && isOperator(app)" ng-click="applinksEdit.show(app)" class="btn btn-lg btn-default grid-item-action"><i class="fas fa-cog"></i></div>
|
||||
<a ng-href="{{ app | applicationLink }}" ng-click="onAppClick(app, $event)" target="_blank">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-center" style="padding-left: 5px; padding-right: 5px;">
|
||||
<img ng-src="{{ app.iconUrl || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)" class="app-icon"/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-center">
|
||||
<div class="grid-item-top-title" data-fittext>{{ app.label || app.subdomain || app.fqdn }}</div>
|
||||
<div class="text-muted status" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden" uib-tooltip="{{ app | appProgressMessage }}">
|
||||
{{ app | installationStateLabel }}
|
||||
</div>
|
||||
<div class="status" ng-style="{ 'visibility': isOperator(app) && (app | installationActive) ? 'visible' : 'hidden' }">
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div 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="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>
|
||||
</div>
|
||||
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- we check the version here because the box updater does not know when an app gets updated -->
|
||||
<div class="app-update-badge" ng-click="showAppConfigure(app, 'updates')" ng-show="config.update[app.id].manifest.version && config.update[app.id].manifest.version !== app.manifest.version && (app | installSuccess) && !(app.error || app.runState === 'stopped')" uib-tooltip="Update Available">
|
||||
<i class="fa fa-arrow-up fa-inverse"></i>
|
||||
</div>
|
||||
|
||||
<div class="app-checklist-badge" ng-click="showAppConfigure(app, 'info')" ng-show="pendingChecklistItems(app)">
|
||||
{{ pendingChecklistItems(app) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-list card card-large" ng-show="view === VIEWS.LIST">
|
||||
<table class="table table-hover" style="margin: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 32px" class="hand" ng-click="setOrderBy('status')"><i ng-show="orderBy === 'status'" class="fas fa-arrow-{{ orderByReverse ? 'up' : 'down' }}-long"></i></th>
|
||||
<th style="width: 32px"> </th>
|
||||
<th style="width: 35%" class="hand" ng-click="setOrderBy('location')">{{ 'app.display.label' | tr }} <i ng-show="orderBy === 'location'" class="fas fa-arrow-{{ orderByReverse ? 'up' : 'down' }}-long"></i></th>
|
||||
<th style="width: 30%" class="hand hide-mobile" ng-click="setOrderBy('app')">App Title<i ng-show="orderBy === 'app'" class="fas fa-arrow-{{ orderByReverse ? 'up' : 'down' }}-long"></i></th>
|
||||
<th style="width: 32px"> </th>
|
||||
<th style="width: 32px" class="hand hide-mobile text-center" ng-click="setOrderBy('sso')"><i class="fas fa-user-lock"></i> <i ng-show="orderBy === 'sso'" class="fas fa-arrow-{{ orderByReverse ? 'up' : 'down' }}-long"></i></th>
|
||||
<th style="width:160px" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="app-list-item" ng-repeat="app in installedApps | selectedGroupAccessFilter:selectedGroup | selectedStateFilter:selectedState | selectedTagFilter:selectedTags | selectedDomainFilter:selectedDomain | appSearchFilter:appSearch | orderBy:orderByFilter:orderByReverse" uib-tooltip="{{ app | appProgressMessage }}">
|
||||
<td class="elide-table-cell">
|
||||
<i class="fa fa-circle" ng-class="app | installationStateClass" uib-tooltip="{{ app | installationStateLabel }}"></i>
|
||||
</td>
|
||||
<td class="elide-table-cell app-list-app-link-cell">
|
||||
<a ng-href="{{ app | applicationLink }}" ng-click="onAppClick(app, $event)" target="_blank" class="app-list-app-link">
|
||||
<img ng-src="{{ app.iconUrl || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)" class="app-list-item-icon"/>
|
||||
</a>
|
||||
</td>
|
||||
<td class="elide-table-cell app-list-app-link-cell">
|
||||
<a ng-href="{{ app | applicationLink }}" ng-click="onAppClick(app, $event)" target="_blank" class="app-list-app-link">
|
||||
<span style="font-size: 16px;">{{ app.label || app.subdomain || app.fqdn }}</span><br/>
|
||||
<span class="text-muted text-small">{{ app.fqdn.indexOf('http') === 0 ? app.fqdn : 'https://'+app.fqdn }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="elide-table-cell hide-mobile">{{ app.manifest.title || 'App Link' }}</td>
|
||||
<td class="elide-table-cell hide-mobile text-center">
|
||||
<a class="badge badge-danger" ng-show="pendingChecklistItems(app)" ng-href="#/app/{{ app.id}}/info">{{ pendingChecklistItems(app) }}</a>
|
||||
</td>
|
||||
<td class="elide-table-cell hide-mobile text-center">
|
||||
<div 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 }}"></i>
|
||||
<i class="fas fa-user" ng-show="app.ssoAuth && (!app.manifest.addons.oidc && !app.manifest.addons.email)" uib-tooltip="{{ 'apps.auth.sso' | tr }}"></i>
|
||||
<i class="far fa-user" ng-show="!app.ssoAuth && !app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.nosso' | tr }}"></i>
|
||||
<i class="fas fa-envelope" ng-show="app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.email' | tr }}"></i>
|
||||
</div>
|
||||
</td>
|
||||
<td class="elide-table-cell text-right">
|
||||
<span ng-show="isOperator(app)">
|
||||
<a class="btn btn-xs btn-success" style="padding: 1px 7px;" ng-show="config.update[app.id].manifest.version && config.update[app.id].manifest.version !== app.manifest.version && (app | installSuccess) && !(app.error || app.runState === 'stopped')" ng-href="#/app/{{ app.id}}/updates" uib-tooltip="Update Available"><i class="fa fa-arrow-up"></i></a>
|
||||
|
||||
<div class="btn-group btn-group-xs" role="group">
|
||||
<a class="btn btn-xs btn-default" ng-show="app.type !== APP_TYPES.LINK" ng-href="{{ '/frontend/logs.html?appId=' + app.id }}" target="_blank" tooltip-append-to-body="true" uib-tooltip="{{ 'app.logsActionTooltip' | tr }}"><i class="fas fa-align-left"></i></a>
|
||||
<a class="btn btn-xs btn-default" ng-show="app.type !== APP_TYPES.PROXIED && app.type !== APP_TYPES.LINK" ng-href="{{ '/frontend/terminal.html?id=' + app.id }}" target="_blank" tooltip-append-to-body="true" uib-tooltip="{{ 'app.terminalActionTooltip' | tr }}"><i class="fa fa-terminal"></i></a>
|
||||
<a class="btn btn-xs btn-default" ng-show="app.manifest.addons.localstorage" ng-href="{{ '/frontend/filemanager.html#/home/app/' + app.id }}" target="_blank" tooltip-append-to-body="true" uib-tooltip="{{ 'app.filemanagerActionTooltip' | tr }}"><i class="fas fa-folder"></i></a>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-xs btn-default" ng-show="app.type === APP_TYPES.LINK" ng-click="applinksEdit.show(app)" uib-tooltip="Configure Applink"><i class="fa fa-cog"></i></button>
|
||||
<a class="btn btn-xs btn-default" ng-show="app.type !== APP_TYPES.LINK" ng-href="#/app/{{ app.id}}/info" uib-tooltip="Configure App"><i class="fa fa-cog"></i></a>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br/>
|
||||
<div>
|
||||
{{ 'apps.apps.count' | tr:{ count: (installedApps | selectedGroupAccessFilter:selectedGroup | selectedStateFilter:selectedState | selectedTagFilter:selectedTags | selectedDomainFilter:selectedDomain | appSearchFilter:appSearch).length } }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,385 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
/* global APP_TYPES */
|
||||
/* global onAppClick */
|
||||
/* global localStorage, document, FileReader */
|
||||
|
||||
angular.module('Application').controller('AppsController', ['$scope', '$translate', '$interval', '$location', 'Client', function ($scope, $translate, $interval, $location, Client) {
|
||||
var ALL_DOMAINS_DOMAIN = { _alldomains: true, domain: 'All Domains' }; // dummy record for the single select filter
|
||||
var GROUP_ACCESS_UNSET = { _unset: true, name: 'Select Group' }; // dummy record for the single select filter
|
||||
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
$scope.tags = Client.getAppTags();
|
||||
$scope.states = [
|
||||
{ state: '', label: 'All States' },
|
||||
{ state: 'running', label: 'Running' },
|
||||
{ state: 'stopped', label: 'Stopped' },
|
||||
{ state: 'update_available', label: 'Update Available' },
|
||||
{ state: 'not_responding', label: 'Not Responding' }
|
||||
];
|
||||
$scope.selectedState = $scope.states[0];
|
||||
$scope.selectedTags = [];
|
||||
$scope.selectedGroup = GROUP_ACCESS_UNSET;
|
||||
$scope.selectedDomain = ALL_DOMAINS_DOMAIN;
|
||||
$scope.filterDomains = [ ALL_DOMAINS_DOMAIN ];
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.domains = [];
|
||||
$scope.appSearch = '';
|
||||
$scope.groups = [ GROUP_ACCESS_UNSET ];
|
||||
$scope.APP_TYPES = APP_TYPES;
|
||||
$scope.showFilter = false;
|
||||
$scope.filterActive = false;
|
||||
|
||||
$scope.VIEWS = {
|
||||
GRID: 'grid',
|
||||
LIST: 'list'
|
||||
};
|
||||
$scope.view = $scope.VIEWS.GRID;
|
||||
|
||||
$scope.orderBy = 'location'; // or app, status, sso
|
||||
$scope.orderByReverse = false;
|
||||
|
||||
$scope.allUsers = [];
|
||||
$scope.allGroups = [];
|
||||
|
||||
$translate(['apps.stateFilterHeader', 'apps.domainsFilterHeader', 'apps.groupsFilterHeader', 'app.states.running', 'app.states.stopped', 'app.states.notResponding', 'app.states.updateAvailable']).then(function (tr) {
|
||||
if (tr['apps.domainsFilterHeader']) ALL_DOMAINS_DOMAIN.domain = tr['apps.domainsFilterHeader'];
|
||||
if (tr['apps.groupsFilterHeader']) GROUP_ACCESS_UNSET.name = tr['apps.groupsFilterHeader'];
|
||||
if (tr['apps.stateFilterHeader']) $scope.states[0].label = tr['apps.stateFilterHeader'];
|
||||
if (tr['app.states.running']) $scope.states[1].label = tr['app.states.running'];
|
||||
if (tr['app.states.stopped']) $scope.states[2].label = tr['app.states.stopped'];
|
||||
if (tr['app.states.notResponding']) $scope.states[4].label = tr['app.states.notResponding'];
|
||||
if (tr['app.states.updateAvailable']) $scope.states[3].label = tr['app.states.updateAvailable'];
|
||||
});
|
||||
|
||||
$scope.pendingChecklistItems = function (app) {
|
||||
if (!app.checklist) return 0;
|
||||
return Object.keys(app.checklist).filter(function (key) { return !app.checklist[key].acknowledged; }).length;
|
||||
};
|
||||
|
||||
$scope.setOrderBy = function (by) {
|
||||
if (by === $scope.orderBy) {
|
||||
$scope.orderByReverse = !$scope.orderByReverse;
|
||||
} else {
|
||||
$scope.orderBy = by;
|
||||
$scope.orderByReverse = false;
|
||||
}
|
||||
|
||||
localStorage.appsOrderBy = by;
|
||||
if ($scope.orderByReverse) localStorage.appsOrderByReverse = true;
|
||||
else localStorage.removeItem('appsOrderByReverse');
|
||||
};
|
||||
|
||||
// for sorting/grouping
|
||||
$scope.orderByFilter = function (item) {
|
||||
if ($scope.orderBy === 'app') return item.manifest.title || 'App Link';
|
||||
if ($scope.orderBy === 'status') return item.installationState + '-' + item.runState;
|
||||
if ($scope.orderBy === 'sso') return item.sso;
|
||||
return item.label || item.fqdn;
|
||||
};
|
||||
|
||||
$scope.setView = function (view) {
|
||||
if (view !== $scope.VIEWS.LIST && view !== $scope.VIEWS.GRID) return;
|
||||
|
||||
$scope.view = view;
|
||||
localStorage.appsView = view;
|
||||
};
|
||||
|
||||
$scope.toggleView = function () {
|
||||
$scope.view = $scope.view === $scope.VIEWS.GRID ? $scope.VIEWS.LIST : $scope.VIEWS.GRID;
|
||||
localStorage.appsView = $scope.view;
|
||||
};
|
||||
|
||||
$scope.toggleFilter = function () {
|
||||
$scope.showFilter = !$scope.showFilter;
|
||||
|
||||
if ($scope.showFilter) localStorage.appsShowFilter = true;
|
||||
else localStorage.removeItem('appsShowFilter');
|
||||
|
||||
// clear on hide
|
||||
if (!$scope.showFilter) {
|
||||
$scope.selectedState = $scope.states[0];
|
||||
$scope.selectedTags = [];
|
||||
$scope.selectedGroup = GROUP_ACCESS_UNSET;
|
||||
$scope.selectedDomain = ALL_DOMAINS_DOMAIN;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('selectedTags', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
|
||||
localStorage.selectedTags = newVal.join(',');
|
||||
});
|
||||
|
||||
$scope.$watch('selectedState', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
|
||||
if (newVal === $scope.states[0]) localStorage.removeItem('selectedState');
|
||||
else localStorage.selectedState = newVal.state;
|
||||
});
|
||||
|
||||
$scope.$watch('selectedGroup', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
|
||||
if (newVal === GROUP_ACCESS_UNSET) localStorage.removeItem('selectedGroup');
|
||||
else localStorage.selectedGroup = newVal.id;
|
||||
});
|
||||
|
||||
$scope.$watch('selectedDomain', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
|
||||
if (newVal._alldomains) localStorage.removeItem('selectedDomain');
|
||||
else localStorage.selectedDomain = newVal.domain;
|
||||
});
|
||||
|
||||
$scope.onAppClick = function (app, $event) { onAppClick(app, $event, $scope.isOperator(app), $scope); };
|
||||
|
||||
$scope.appPostInstallConfirm = {
|
||||
app: {},
|
||||
message: '',
|
||||
|
||||
show: function (app) {
|
||||
$scope.appPostInstallConfirm.app = app;
|
||||
$scope.appPostInstallConfirm.message = app.manifest.postInstallMessage;
|
||||
|
||||
$('#appsPostInstallConfirmModal').modal('show');
|
||||
|
||||
return false; // prevent propagation and default
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.appPostInstallConfirm.app.pendingPostInstallConfirmation = false;
|
||||
delete localStorage['confirmPostInstall_' + $scope.appPostInstallConfirm.app.id];
|
||||
|
||||
$('#appsPostInstallConfirmModal').modal('hide');
|
||||
}
|
||||
};
|
||||
|
||||
$scope.applinksEdit = {
|
||||
error: {},
|
||||
busyEdit: false,
|
||||
busyRemove: false,
|
||||
applink: {},
|
||||
id: '',
|
||||
upstreamUri: '',
|
||||
label: '',
|
||||
tags: '',
|
||||
accessRestrictionOption: '',
|
||||
accessRestriction: { users: [], groups: [] },
|
||||
icon: { data: null },
|
||||
|
||||
iconUrl: function () {
|
||||
if ($scope.applinksEdit.icon.data === '__original__') { // user clicked reset
|
||||
// https://png-pixel.com/ white pixel placeholder
|
||||
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=';
|
||||
} else if ($scope.applinksEdit.icon.data) { // user uploaded icon
|
||||
return $scope.applinksEdit.icon.data;
|
||||
} else { // current icon
|
||||
return $scope.applinksEdit.applink.iconUrl;
|
||||
}
|
||||
},
|
||||
|
||||
resetCustomIcon: function () {
|
||||
$scope.applinksEdit.icon.data = '__original__';
|
||||
},
|
||||
|
||||
showCustomIconSelector: function () {
|
||||
$('#applinksEditIconFileInput').click();
|
||||
},
|
||||
|
||||
isAccessRestrictionValid: function () {
|
||||
return !!($scope.applinksEdit.accessRestriction.users.length || $scope.applinksEdit.accessRestriction.groups.length);
|
||||
},
|
||||
|
||||
show: function (applink) {
|
||||
$scope.applinksEdit.error = {};
|
||||
$scope.applinksEdit.busyEdit = false;
|
||||
$scope.applinksEdit.busyRemove = false;
|
||||
$scope.applinksEdit.applink = applink;
|
||||
$scope.applinksEdit.id = applink.id;
|
||||
$scope.applinksEdit.upstreamUri = applink.upstreamUri;
|
||||
$scope.applinksEdit.label = applink.label;
|
||||
$scope.applinksEdit.accessRestrictionOption = applink.accessRestriction ? 'groups' : 'any';
|
||||
$scope.applinksEdit.accessRestriction = { users: [], groups: [] };
|
||||
$scope.applinksEdit.icon = { data: null };
|
||||
|
||||
var userSet, groupSet;
|
||||
if (applink.accessRestriction) {
|
||||
userSet = {};
|
||||
applink.accessRestriction.users.forEach(function (uid) { userSet[uid] = true; });
|
||||
$scope.allUsers.forEach(function (u) { if (userSet[u.id] === true) $scope.applinksEdit.accessRestriction.users.push(u); });
|
||||
|
||||
groupSet = {};
|
||||
if (applink.accessRestriction.groups) applink.accessRestriction.groups.forEach(function (gid) { groupSet[gid] = true; });
|
||||
$scope.allGroups.forEach(function (g) { if (groupSet[g.id] === true) $scope.applinksEdit.accessRestriction.groups.push(g); });
|
||||
}
|
||||
|
||||
// translate for tag-input
|
||||
$scope.applinksEdit.tags = applink.tags ? applink.tags.join(' ') : '';
|
||||
|
||||
$scope.applinksEditForm.$setUntouched();
|
||||
$scope.applinksEditForm.$setPristine();
|
||||
|
||||
$('#applinksEditModal').modal('show');
|
||||
|
||||
return false; // prevent propagation and default
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.applinksEdit.busyEdit = true;
|
||||
$scope.applinksEdit.error = {};
|
||||
|
||||
var accessRestriction = null;
|
||||
if ($scope.applinksEdit.accessRestrictionOption === 'groups') {
|
||||
accessRestriction = { users: [], groups: [] };
|
||||
accessRestriction.users = $scope.applinksEdit.accessRestriction.users.map(function (u) { return u.id; });
|
||||
accessRestriction.groups = $scope.applinksEdit.accessRestriction.groups.map(function (g) { return g.id; });
|
||||
}
|
||||
|
||||
var data = {
|
||||
upstreamUri: $scope.applinksEdit.upstreamUri,
|
||||
label: $scope.applinksEdit.label,
|
||||
accessRestriction: accessRestriction,
|
||||
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;
|
||||
|
||||
if (error && error.statusCode === 400 && error.message.includes('upstreamUri')) {
|
||||
$scope.applinksEdit.error.upstreamUri = error.message;
|
||||
$scope.applinksEditForm.$setUntouched();
|
||||
$scope.applinksEditForm.$setPristine();
|
||||
return;
|
||||
}
|
||||
if (error) return console.error('Failed to update applink', error);
|
||||
|
||||
Client.refreshInstalledApps();
|
||||
|
||||
$('#applinksEditModal').modal('hide');
|
||||
});
|
||||
},
|
||||
|
||||
remove: function () {
|
||||
$scope.applinksEdit.busyRemove = true;
|
||||
|
||||
Client.removeApplink($scope.applinksEdit.id, function (error) {
|
||||
$scope.applinksEdit.busyRemove = false;
|
||||
|
||||
if (error) return console.error('Failed to remove applink', error);
|
||||
|
||||
Client.refreshInstalledApps();
|
||||
|
||||
$('#applinksEditModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showAppConfigure = function (app, view) {
|
||||
$location.path('/app/' + app.id + '/' + view);
|
||||
};
|
||||
|
||||
$scope.isOperator = function (app) {
|
||||
return app.accessLevel === 'operator' || app.accessLevel === 'admin';
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
setTimeout(function () { $('#appSearch').focus(); }, 1);
|
||||
|
||||
// refresh the new list immediately when switching from another view (appstore)
|
||||
Client.refreshInstalledApps(function () {
|
||||
var refreshAppsTimer = $interval(Client.refreshInstalledApps.bind(Client, function () {}), 5000);
|
||||
$scope.$on('$destroy', function () {
|
||||
$interval.cancel(refreshAppsTimer);
|
||||
});
|
||||
});
|
||||
|
||||
$scope.setView(localStorage.appsView);
|
||||
|
||||
$scope.orderBy = localStorage.appsOrderBy || 'location';
|
||||
$scope.orderByReverse = !!localStorage.appsOrderByReverse;
|
||||
$scope.showFilter = !!localStorage.appsShowFilter;
|
||||
|
||||
if (!$scope.user.isAtLeastAdmin) return;
|
||||
|
||||
// load local settings and apply tag filter
|
||||
if (localStorage.selectedTags) {
|
||||
if (!$scope.tags.length) localStorage.removeItem('selectedTags');
|
||||
else $scope.selectedTags = localStorage.selectedTags.split(',');
|
||||
}
|
||||
|
||||
if (localStorage.selectedState) $scope.selectedState = $scope.states.find(function (s) { return s.state === localStorage.selectedState; }) || $scope.states[0];
|
||||
|
||||
Client.getGroups(function (error, result) {
|
||||
if (error) Client.error(error);
|
||||
|
||||
$scope.groups = [ GROUP_ACCESS_UNSET ].concat(result);
|
||||
$scope.allGroups = result;
|
||||
|
||||
if (localStorage.selectedGroup) $scope.selectedGroup = $scope.groups.find(function (g) { return g.id === localStorage.selectedGroup; }) || GROUP_ACCESS_UNSET;
|
||||
});
|
||||
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) Client.error(error);
|
||||
|
||||
$scope.domains = result;
|
||||
$scope.filterDomains = [ALL_DOMAINS_DOMAIN].concat(result);
|
||||
|
||||
if (localStorage.selectedDomain) $scope.selectedDomain = $scope.filterDomains.find(function (d) { return d.domain === localStorage.selectedDomain; }) || ALL_DOMAINS_DOMAIN;
|
||||
});
|
||||
|
||||
Client.getAllUsers(function (error, users) {
|
||||
if (error) Client.error(error);
|
||||
|
||||
$scope.allUsers = users;
|
||||
});
|
||||
});
|
||||
|
||||
$('#applinksEditIconFileInput').get(0).onchange = function (event) {
|
||||
var fr = new FileReader();
|
||||
fr.onload = function () {
|
||||
$scope.$apply(function () {
|
||||
// var file = event.target.files[0];
|
||||
$scope.applinksEdit.icon.data = fr.result;
|
||||
});
|
||||
};
|
||||
fr.readAsDataURL(event.target.files[0]);
|
||||
};
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['applinksAddModal', 'applinksEditModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find('autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('.collapse').on('shown.bs.collapse', function(){
|
||||
$(this).parent().find('.fa-angle-right').removeClass('fa-angle-right').addClass('fa-angle-down');
|
||||
}).on('hidden.bs.collapse', function(){
|
||||
$(this).parent().find('.fa-angle-down').removeClass('fa-angle-down').addClass('fa-angle-right');
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
|
||||
function keyboardHandler(event) {
|
||||
if (event.key === '/') {
|
||||
document.getElementById('appSearch').focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', keyboardHandler);
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
document.removeEventListener('keydown', keyboardHandler);
|
||||
});
|
||||
}]);
|
||||
@@ -1,508 +0,0 @@
|
||||
|
||||
<!-- Modal install app -->
|
||||
<div class="modal fade appstore-install" id="appInstallModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<img ng-src="{{appInstall.app.iconUrl}}" onerror="this.onerror=null; this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
<h3 class="appstore-install-title">{{ appInstall.app.manifest.title }}</h3>
|
||||
<br/>
|
||||
<span class="appstore-install-meta"><a href="{{ appInstall.app.manifest.website }}" target="_blank">{{ appInstall.app.manifest.author }}</a></span>
|
||||
<br/>
|
||||
<span class="appstore-install-meta">{{ 'appstore.installDialog.lastUpdated' | tr:{ date: (appInstall.app.creationDate | prettyDate) } }}</span>
|
||||
<br/>
|
||||
<span class="appstore-install-meta">{{ 'appstore.installDialog.memoryRequirement' | tr:{ size: (appInstall.app.manifest.memoryLimit | prettyBinarySize:'256 MB') } }}</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="collapse" id="collapseInstallForm" data-toggle="false">
|
||||
<form role="form" name="appInstallForm" ng-submit="appInstall.submit()" autocomplete="off">
|
||||
<div class="has-error text-center" ng-show="appInstall.error.other" ng-bind-html="appInstall.error.other"></div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (appInstallForm.location.$dirty && appInstallForm.location.$invalid) || (!appInstallForm.location.$dirty && appInstall.error.location) }">
|
||||
<label class="control-label" for="appInstallLocationInput">{{ 'appstore.installDialog.location' | tr }}</label>
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="appInstall.subdomain" id="appInstallLocationInput" name="location" placeholder="{{ 'appstore.installDialog.locationPlaceholder' | tr }}" autofocus>
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<span>{{ '.' + appInstall.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="appInstall.domain = domain">{{ domain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="appInstall.error.location" class="text-small">{{ appInstall.error.location }}</div>
|
||||
</div>
|
||||
|
||||
<p class="text-small text-warning" ng-show="appInstall.domain.provider === 'noop' || appInstall.domain.provider === 'manual'" ng-bind-html="'appstore.installDialog.manualWarning' | tr:{ location: ((appInstall.subdomain ? appInstall.subdomain + '.' : '') + appInstall.domain.domain) }"></p>
|
||||
|
||||
<div class="has-error text-center" ng-show="appInstall.error.secondaryDomain">{{ appInstall.error.secondaryDomain }}</div>
|
||||
<div ng-repeat="(env, info) in appInstall.app.manifest.httpPorts">
|
||||
<ng-form name="secondaryDomainInfo_form">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!secondaryDomainInfo_form.itemName{{$index}}.$dirty && appInstall.error.secondaryDomain) || (secondaryDomainInfo_form.itemName{{$index}}.$dirty && secondaryDomainInfo_form.itemName{{$index}}.$invalid) }">
|
||||
<label class="control-label" for="secondaryDomainInput{{env}}">
|
||||
{{ info.title }}
|
||||
<sup>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}}"><i class="fa fa-question-circle"></i></a>
|
||||
</sup>
|
||||
</label>
|
||||
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="appInstall.secondaryDomains[env].subdomain" name="location{{$index}}" placeholder="{{ 'appstore.installDialog.locationPlaceholder' | tr }}" autofocus>
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<span>.{{ appInstall.secondaryDomains[env].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="appInstall.secondaryDomains[env].domain = domain">{{ domain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
|
||||
<div class="has-error text-center" ng-show="appInstall.error.port">{{ appInstall.error.port }}</div>
|
||||
<div ng-repeat="(env, info) in appInstall.portInfo">
|
||||
<ng-form name="portInfo_form">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appInstallForm.itemName{{$index}}.$dirty && appInstall.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
|
||||
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="appInstall.portsEnabled[env]">
|
||||
{{ info.title }}
|
||||
<sup>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}}. {{info.portCount >=1 ? (info.portCount + ' ports. ') : ''}}"><i class="fa fa-question-circle"></i></a>
|
||||
</sup>
|
||||
<small style="padding-left: 5px;" ng-show="info.readOnly">{{ 'appstore.installDialog.portReadOnly' | tr }}</small>
|
||||
</label>
|
||||
<input type="number" class="form-control" ng-model="appInstall.ports[env]" ng-disabled="!appInstall.portsEnabled[env]" ng-readonly="info.readOnly" id="inputPortInfo{{env}}" later-name="itemName{{$index}}" min="{{hostPortMin}}" max="{{hostPortMax}}" required>
|
||||
<p class="text-small text-warning text-bold" ng-show="appInstall.domain.provider === 'cloudflare'">{{ 'appstore.installDialog.cloudflarePortWarning' | tr }} </p>
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="isProxyApp(appInstall.app)">
|
||||
<label class="control-label" for="appInstallUpstreamUriInput">Upstream URI</label>
|
||||
<input type="text" class="form-control" ng-model="appInstall.upstreamUri" id="appInstallUpstreamUriInput" name="upstreamUri" ng-required="isProxyApp(appInstall.app)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="appInstall.app.manifest.addons.email">
|
||||
<label class="control-label">{{ 'appstore.installDialog.userManagement' | tr }}</label>
|
||||
<p>{{ 'appstore.installDialog.userManagementMailbox' | tr }}
|
||||
<span ng-bind-html="'appstore.installDialog.configuredForCloudronEmail' | tr:{ emailDocsLink: 'https://docs.cloudron.io/email/' }">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" ng-show="!appInstall.customAuth && !appInstall.app.manifest.addons.email">{{ 'appstore.installDialog.userManagement' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#access-restriction" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<label class="control-label" ng-show="appInstall.customAuth || appInstall.app.manifest.addons.email">{{ 'app.accessControl.userManagement.dashboardVisibility' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#dashboard-visibility" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
|
||||
<p ng-show="appInstall.customAuth || appInstall.app.manifest.addons.email">{{ 'appstore.installDialog.userManagementNone' | tr }}</p>
|
||||
<div class="radio" ng-show="appInstall.optionalSso">
|
||||
<label>
|
||||
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="nosso"> {{ 'appstore.installDialog.userManagementLeaveToApp' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="any">
|
||||
<span ng-show="!appInstall.customAuth">{{ 'appstore.installDialog.userManagementAllUsers' | tr }}</span>
|
||||
<span ng-show="appInstall.customAuth">{{ 'app.accessControl.userManagement.visibleForAllUsers' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="groups">
|
||||
<span ng-show="!appInstall.customAuth">{{ 'appstore.installDialog.userManagementSelectUsers' | tr }}</span>
|
||||
<span ng-show="appInstall.customAuth">{{ 'app.accessControl.userManagement.visibleForSelected' | tr }}</span>
|
||||
<span class="label label-danger" ng-show="appInstall.accessRestrictionOption === 'groups' && !appInstall.isAccessRestrictionValid()">{{ 'appstore.installDialog.errorUserManagementSelectAtLeastOne' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div style="margin-left: 20px;">
|
||||
<div class="col-md-5">
|
||||
{{ 'appstore.installDialog.users' | tr }}:
|
||||
<multiselect ng-model="appInstall.accessRestriction.users" ng-disabled="appInstall.accessRestrictionOption !== 'groups'" options="(user.username || user.email) for user in users" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
{{ 'appstore.installDialog.groups' | tr }}:
|
||||
<multiselect ng-model="appInstall.accessRestriction.groups" ng-disabled="appInstall.accessRestrictionOption !== 'groups'" options="group.name for group in groups" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div class="hide">
|
||||
<label class="control-label" for="appInstallCertificateInput">Certificate (optional)</label>
|
||||
<div class="has-error text-center" ng-show="appInstall.error.cert">{{ appInstall.error.cert }}</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': !appInstallForm.certificate.$dirty && appInstall.error.cert }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="appInstallCertificateFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="appInstall.certificateFileName" id="appInstallCertificateInput" name="certificate" onclick="getElementById('appInstallCertificateFileInput').click();" style="cursor: pointer;" ng-required="appInstall.keyFileName">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('appInstallCertificateFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': !appInstallForm.key.$dirty && appInstall.error.cert }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="appInstallKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="appInstall.keyFileName" id="appInstallKeyInput" name="key" onclick="getElementById('appInstallKeyFileInput').click();" style="cursor: pointer;" ng-required="appInstall.certificateFileName">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('appInstallKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="(appInstall.accessRestrictionOption === 'groups' && !appInstall.isAccessRestrictionValid()) || !appInstall.accessRestrictionOption || appInstallForm.$invalid || busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="collapse" id="collapseMediaLinksCarousel" data-toggle="false">
|
||||
<div ng-repeat="mediaLink in appInstall.mediaLinks" class="slick-item" style="background-image: url('{{mediaLink}}');" ng-show="appInstall.mediaLinks.length == 1"></div>
|
||||
<slick init-onload="true" current-index="0" autoplay="true" arrows="false" autoplay-speed="2000" data="appInstall.mediaLinks" ng-show="appInstall.mediaLinks.length > 1">
|
||||
<div ng-repeat="mediaLink in appInstall.mediaLinks" class="slick-item" style="background-image: url('{{mediaLink}}');"></div>
|
||||
</slick>
|
||||
<br/>
|
||||
<div class="appstore-install-description">
|
||||
<p ng-show="appInstall.app.manifest.upstreamVersion">{{ 'appstore.installDialog.titleAndVersion' | tr:{ title: appInstall.app.manifest.title, version: appInstall.app.manifest.upstreamVersion } }}</p>
|
||||
<div ng-bind-html="appInstall.app.manifest.description | markdown2html"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse" id="collapseResourceConstraint" data-toggle="false">
|
||||
<h4 class="text-danger">{{ 'appstore.installDialog.lowOnResources' | tr }}<sup><a ng-href="https://docs.cloudron.io/apps/#low-resource-warning" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></h4>
|
||||
<p>{{ 'appstore.installDialog.pleaseUpgradeServer' | tr }}</p>
|
||||
</div>
|
||||
<div class="collapse" id="collapseSubscriptionRequired" data-toggle="false">
|
||||
<p>{{ 'appstore.installDialog.subscriptionRequired' | tr }}</p>
|
||||
</div>
|
||||
</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="openSubscriptionSetup()" ng-show="appInstall.state === 'subscriptionRequired'">{{ 'appstore.installDialog.setupSubscriptionAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-show="appInstall.state === 'resourceConstraint'" ng-click="appInstall.showForm(true)">{{ 'appstore.installDialog.installAnywayAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-show="appInstall.state === 'appInfo'" ng-click="appInstall.showForm()">{{ 'appstore.installDialog.installAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-show="appInstall.state === 'installForm'" ng-click="appInstall.submit()" ng-disabled="(appInstall.accessRestrictionOption === 'groups' && !appInstall.isAccessRestrictionValid()) || !appInstall.accessRestrictionOption || appInstallForm.$invalid || appInstall.busy"><i class="fa fa-circle-notch fa-spin" ng-show="appInstall.busy"></i> {{ 'appstore.installDialog.doInstallAction' | tr:{ dnsOverwrite: appInstall.needsOverwrite } }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal app not found -->
|
||||
<div class="modal fade" id="appNotFoundModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'appstore.appNotFoundDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body" ng-bind-html="'appstore.appNotFoundDialog.description' | tr:{ appId: appNotFound.appId, version: appNotFound.version }"></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 applinks add -->
|
||||
<div class="modal fade" id="applinksAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'app.addApplinkDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="applinksAddForm" role="form" ng-submit="applinksAdd.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (applinksAddForm.upstreamUri.$dirty && applinksAddForm.upstreamUri.$invalid) || (!applinksAddForm.upstreamUri.$dirty && applinksAdd.error.upstreamUri) }">
|
||||
<label class="control-label">{{ 'app.applinks.upstreamUri' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="applinksAdd.upstreamUri" name="upstreamUri" id="inputUpstreamUri" autofocus autocomplete="off" required>
|
||||
<span class="text-danger" ng-show="applinksAdd.error.upstreamUri">{{ applinksAdd.error.upstreamUri }}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'app.applinks.label' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="applinksAdd.label" name="label" id="inputLabel" autocomplete="off" placeholder="Leave empty for autodetection">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'app.display.tags' | tr }}</label>
|
||||
<tag-input class="form-control" placeholder="{{ 'app.display.tagsPlaceholder' | tr }}" taglist="applinksAdd.tags" name="tags" uib-tooltip="{{ 'app.display.tagsTooltip' | tr }}"></tag-input>
|
||||
</div>
|
||||
|
||||
<label class="control-label">{{ 'app.accessControl.userManagement.dashboardVisibility' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#dashboard-visibility" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="applinksAdd.accessRestrictionOption" value="any">
|
||||
<span>{{ 'app.accessControl.userManagement.visibleForAllUsers' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="applinksAdd.accessRestrictionOption" value="groups">
|
||||
<span>{{ 'app.accessControl.userManagement.visibleForSelected' | tr }}</span>
|
||||
<span class="label label-danger" ng-show="applinksAdd.accessRestrictionOption === 'groups' && !applinksAdd.isAccessRestrictionValid()">{{ 'appstore.installDialog.errorUserManagementSelectAtLeastOne' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div style="margin-left: 20px; display: flex;">
|
||||
<div>
|
||||
{{ 'appstore.installDialog.users' | tr }}: <multiselect name="accessUsersSelect" class="input-sm stretch" ng-model="applinksAdd.accessRestriction.users" ng-disabled="applinksAdd.accessRestrictionOption !== 'groups'" options="(user.username || user.email) for user in users" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ 'appstore.installDialog.groups' | tr }}: <multiselect name="accessGroupsSelect" class="input-sm stretch" ng-model="applinksAdd.accessRestriction.groups" ng-disabled="applinksAdd.accessRestrictionOption !== 'groups'" options="group.name for group in groups" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="applinksAddForm.$invalid || applinksAdd.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-success" ng-click="applinksAdd.submit()" ng-disabled="applinksAddForm.$invalid || applinksAdd.busy"><i class="fa fa-circle-notch fa-spin" ng-show="applinksAdd.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="!ready" class="loading-banner">
|
||||
<h1><i class="fa fa-circle-notch fa-spin"></i></h1>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
<div class="col-md-12 text-center">
|
||||
<p>{{ 'appstore.accountDialog.description' | tr }}</p>
|
||||
</div>
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<small class="text-danger" ng-show="appstoreLogin.error.generic">{{ appstoreLogin.error.generic }}</small>
|
||||
</div>
|
||||
<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;">
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (appstoreSignupForm.email.$dirty && appstoreSignupForm.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>
|
||||
<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>
|
||||
</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 }">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="form-group" 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>
|
||||
</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="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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- give more vertical spacing so the login form does not appear clipped -->
|
||||
<div ng-show="ready && !validSubscription">
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div class="appstore-layout">
|
||||
|
||||
<div ng-show="ready && validSubscription" class="ng-cloak appstore-toolbar">
|
||||
<div class="appstore-toolbar-content">
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle" type="button" data-toggle="dropdown" ng-class="{ 'btn-primary': '' !== category && 'recent' !== category && 'new' !== category }">
|
||||
{{ categoryButtonLabel(category) }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="" ng-click="showCategory('');"><i class="fas fa-home fa-fw"></i> {{ 'appstore.category.all' | tr }}</a></li>
|
||||
<li><a href="" ng-click="showCategory('new');"><i class="fas fa-rss fa-fw"></i> {{ 'appstore.category.newApps' | tr }}</a></li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li ng-repeat="category in categories | orderBy:'label'"><a href="" ng-click="showCategory(category.id);"><i class="{{ category.icon }} fa-fw"></i> {{ category.label }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle" type="button" data-toggle="dropdown">
|
||||
<i class="{{ userManagementFilterOption.icon }} fa-fw"></i>
|
||||
{{ 'appstore.ssofilter.label' | tr }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="option in userManagementFilterOptions" ng-class="{ 'active': userManagementFilterOption.id && userManagementFilterOption.id === option.id }"><a href="" ng-click="applyUserMangamentFilter(option);"><i class="{{ option.icon }} fa-fw"></i> {{ option.label }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<input type="text" id="appstoreSearch" class="form-control" style="width: auto; flex-grow: 1;" placeholder="{{ 'appstore.searchPlaceholder' | tr }}" ng-model="searchString" ng-change="search()" autofocus>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default" ng-click="openAppProxy()"><i class="fas fa-exchange-alt"></i> {{ 'apps.addAppproxyAction' | tr }}</a></a>
|
||||
<button type="button" class="btn btn-default" ng-click="applinksAdd.show()"><i class="fas fa-link"></i> {{ 'apps.addApplinkAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="ready && validSubscription" class="ng-cloak appstore-grid">
|
||||
<div class="text-center" ng-hide="apps.length || popularApps.length">
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<h3 class="text-muted">{{ 'appstore.noAppsFound' | tr }}</h3>
|
||||
<br/>
|
||||
<a href="https://forum.cloudron.io/category/5/app-requests" target="_blank">{{ 'appstore.appMissing' | tr }}</a>
|
||||
</div>
|
||||
<div class="" ng-show="category === '' && popularApps.length">
|
||||
<div class="row-no-margin">
|
||||
<div class="col-sm-12">
|
||||
<h2>{{ 'appstore.category.popular' | tr }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-no-margin">
|
||||
<div class="col-sm-3 appstore-item" ng-repeat="app in popularApps | userManagementFilter:userManagementFilterOption">
|
||||
<div class="appstore-item-content highlight" ng-click="gotoApp(app)" ng-class="{ 'appstore-item-content-testing': app.releaseState === 'unstable' }">
|
||||
<span class="badge badge-danger appstore-item-badge-testing" ng-show="app.releaseState === 'unstable'">{{ 'appstore.unstable' | tr }}</span>
|
||||
<div class="appstore-item-content-icon col-same-height">
|
||||
<img ng-src="{{app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
</div>
|
||||
<div class="col-same-height">
|
||||
<h4 class="appstore-item-content-title">{{ app.manifest.title }}</h4>
|
||||
<div class="appstore-item-content-tagline text-muted">{{ app.manifest.tagline }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="" ng-show="apps.length">
|
||||
<div class="row-no-margin" ng-show="!category && !searchString">
|
||||
<div class="col-sm-12">
|
||||
<h2>{{ 'appstore.category.all' | tr }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-no-margin">
|
||||
<div class="col-sm-3 appstore-item" ng-repeat="app in apps | userManagementFilter:userManagementFilterOption | orderBy:'-priority' ">
|
||||
<div class="appstore-item-content highlight" ng-click="gotoApp(app)" ng-class="{ 'appstore-item-content-testing': app.releaseState === 'unstable' }">
|
||||
<span class="badge badge-danger appstore-item-badge-testing" ng-show="app.releaseState === 'unstable'">{{ 'appstore.unstable' | tr }}</span>
|
||||
<div class="appstore-item-content-icon col-same-height">
|
||||
<img ng-src="{{app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
</div>
|
||||
<div class="appstore-item-content-description col-same-height">
|
||||
<h4 class="appstore-item-content-title">{{ app.manifest.title }}</h4>
|
||||
<div class="appstore-item-content-tagline text-muted">{{ app.manifest.tagline }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,647 +0,0 @@
|
||||
<!-- Modal 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-2 text-muted">{{ 'backups.backupDetails.id' | tr }}:</div>
|
||||
<div class="col-xs-10 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>
|
||||
<br/>
|
||||
<p class="text-muted">{{ 'backups.backupDetails.list' | tr:{ appCount: backupDetails.backup.contents.length } }}:</p>
|
||||
<span ng-repeat="content in backupDetails.backup.contents | orderBy:['label','fqdn']">
|
||||
<a ng-if="content.fqdn" ng-href="/#/app/{{content.id}}/backups">{{ content.label || content.fqdn }}</a>
|
||||
<a ng-if="!content.fqdn" ng-href="/#/eventlog?search={{content.id}}">{{ content.id }}</a>
|
||||
<span ng-hide="$last">,</span>
|
||||
</span>
|
||||
</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">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.backupEdit.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="editBackupForm" role="form" novalidate ng-submit="editBackup.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="editBackup.error">{{ editBackup.error }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputBackupLabel">{{ 'backups.backupEdit.label' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="editBackup.label" id="inputBackupLabel" name="label" ng-disabled="editBackup.busy" placeholder="" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="editBackup.persist">{{ 'backups.backupEdit.preserved.description' | tr }}</input>
|
||||
|
||||
<sup><a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{ 'backups.backupEdit.preserved.tooltip' | tr: { appsLength: editBackup.backup.contents.length} }}"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
</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="editBackup.submit()" ng-disabled="editBackupForm.$invalid || editBackup.busy"><i class="fa fa-circle-notch fa-spin" ng-show="editBackup.busy"></i><span> {{ 'main.dialog.save' | tr }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal backup failed -->
|
||||
<div class="modal fade" id="createBackupFailedModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.backupFailed.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ createBackup.errorMessage }}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cleanup backups info -->
|
||||
<div class="modal fade" id="cleanupBackupsModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.cleanupBackups.title' | tr }}</h4>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal backup schedule config -->
|
||||
<div class="modal fade" id="backupPolicyModal" 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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal backup config -->
|
||||
<div class="modal fade" id="configureBackupModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.configureBackupStorage.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="configureBackupForm" role="form" novalidate ng-submit="configureBackup.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="configureBackup.error">{{ configureBackup.error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageProviderProvider">{{ 'backups.configureBackupStorage.provider' | tr }} <sup><a ng-href="https://docs.cloudron.io/backups/#storage-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<p class="small text-info" ng-show="backupConfig.provider !== configureBackup.provider">Backups in the old storage location have to be removed manually.</p>
|
||||
<select class="form-control" id="storageProviderProvider" ng-model="configureBackup.provider" ng-options="a.value as a.name for a in storageProviders" ng-change=configureBackup.clearProviderFields()></select>
|
||||
</div>
|
||||
|
||||
<!-- Noop -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'noop'">
|
||||
<p class="has-error">{{ 'backups.configureBackupStorage.noopNote' | tr }}</p>
|
||||
</div>
|
||||
|
||||
<!-- mountpoint -->
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs' || configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupHost">{{ 'backups.configureBackupStorage.server' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.host" id="configureBackupHost" name="host" ng-disabled="configureBackup.busy" placeholder="Server IP or hostname" ng-required="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs' || configureBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'cifs'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.mountOptions.seal">{{ 'backups.configureBackupStorage.cifsSealSupport' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs' || configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupRemoteDir">{{ 'backups.configureBackupStorage.remoteDirectory' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.remoteDir" id="configureBackupRemoteDir" name="remoteDir" ng-disabled="configureBackup.busy" placeholder="/share" ng-required="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs' || configureBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupUsername">{{ 'backups.configureBackupStorage.username' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.username" id="configureBackupUsername" name="cifsUsername" ng-disabled="configureBackup.busy">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupPassword">{{ 'backups.configureBackupStorage.password' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<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'">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPort">{{ 'backups.configureBackupStorage.port' | tr }}</label>
|
||||
<input type="number" class="form-control" ng-model="configureBackup.mountOptions.port" id="configureBackupPort" name="port" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupUser">{{ 'backups.configureBackupStorage.user' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.user" id="configureBackupUser" name="user" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPrivateKey">{{ 'backups.configureBackupStorage.privateKey' | tr }}</label>
|
||||
<textarea class="form-control" ng-model="configureBackup.mountOptions.privateKey" id="configureBackupPrivateKey" name="privateKey" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'sshfs'"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Filesystem -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.backupFolder }" ng-show="configureBackup.provider === 'filesystem'">
|
||||
<label class="control-label" for="inputConfigureBackupFolder">{{ 'backups.configureBackupStorage.localDirectory' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.backupFolder" id="inputConfigureBackupFolder" name="backupFolder" ng-disabled="configureBackup.busy" placeholder="Directory for backups" ng-required="configureBackup.provider === 'filesystem'">
|
||||
</div>
|
||||
|
||||
<!-- Filesystem/SSHFS/CIFS/NFS/EXT4/mountpoint -->
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'filesystem' || mountlike(configureBackup.provider)">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.useHardlinks">{{ 'backups.configureBackupStorage.hardlinksLabel' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- mountpoint -->
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'mountpoint'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.chown">{{ 'backups.configureBackupStorage.chown' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS/GCS/UpCloud/B2/R2 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.endpoint }" ng-show="configureBackup.provider === 'minio' || configureBackup.provider === 'upcloud-objectstorage' || configureBackup.provider === 'backblaze-b2' || configureBackup.provider === 'cloudflare-r2' || configureBackup.provider === 's3-v4-compat' || configureBackup.provider === 'idrive-e2'">
|
||||
<label class="control-label" for="inputConfigureBackupEndpoint">{{ 'backups.configureBackupStorage.s3Endpoint' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.endpoint" id="inputConfigureBackupEndpoint" name="endpoint" ng-disabled="configureBackup.busy" placeholder="URL" ng-required="configureBackup.provider === 'minio' || configureBackup.provider === 'upcloud-objectstorage' || configureBackup.provider === 'backblaze-b2' || configureBackup.provider === 'cloudflare-r2' || configureBackup.provider === 's3-v4-compat' || configureBackup.provider === 'idrive-e2'">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'minio' || configureBackup.provider === 's3-v4-compat'" >
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.acceptSelfSignedCerts">{{ 'backups.configureBackupStorage.acceptSelfSignedCerts' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.bucket }" ng-show="s3like(configureBackup.provider) || configureBackup.provider === 'gcs'">
|
||||
<label class="control-label" for="inputConfigureBackupBucket">{{ 'backups.configureBackupStorage.bucketName' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.bucket" id="inputConfigureBackupBucket" name="bucket" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS/GCS/SSHFS/CIFS/NFS/B2 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.prefix }" ng-show="configureBackup.provider !== 'filesystem' && configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="inputConfigureBackupPrefix">{{ 'backups.configureBackupStorage.prefix' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.prefix" id="inputConfigureBackupPrefix" name="prefix" ng-disabled="configureBackup.busy" placeholder="Prefix for backup file names">
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS/GCS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 's3'">
|
||||
<label class="control-label" for="inputConfigureBackupS3Region">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupS3Region" ng-model="configureBackup.region" ng-options="a.value as a.name for a in s3Regions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 's3'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 's3-v4-compat'">
|
||||
<label class="control-label" for="inputConfigureBackupS3V4CompatRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<input class="form-control" type="text" name="region" id="inputConfigureBackupS3V4CompatRegion" ng-model="configureBackup.region" ng-disabled="configureBackup.busy" placeholder="Leave empty to use us-east-1 as default"></input>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'digitalocean-spaces'">
|
||||
<label class="control-label" for="inputConfigureBackupDORegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupDORegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in doSpacesRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'digitalocean-spaces'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'exoscale-sos'">
|
||||
<label class="control-label" for="inputConfigureBackupExoscaleRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupExoscaleRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in exoscaleSosRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'exoscale-sos'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'wasabi'">
|
||||
<label class="control-label" for="inputConfigureBackupWasabiRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupWasabiRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in wasabiRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'wasabi'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'scaleway-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupScalewayRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupScalewayRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in scalewayRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'scaleway-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'linode-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupLinodeRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupLinodeRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in linodeRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'linode-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'ovh-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupOvhRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupOvhRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in ovhRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'ovh-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'ionos-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupIonosRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupIonosRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in ionosRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'ionos-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'vultr-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupVultrRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<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)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.secretAccessKey }" ng-show="s3like(configureBackup.provider)">
|
||||
<label class="control-label" for="inputConfigureBackupSecretAccessKey">{{ 'backups.configureBackupStorage.s3SecretAccessKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.secretAccessKey" id="inputConfigureBackupSecretAccessKey" name="secretAccessKey" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.gcsKeyInput }" ng-show="configureBackup.provider === 'gcs'">
|
||||
<label class="control-label" for="gcsKeyInput">{{ 'backups.configureBackupStorage.gcsServiceKey' | tr }}</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="configureBackup.gcsKey.keyFileName" id="gcsKeyInput" name="cert" onclick="getElementById('gcsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'gcs'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="storageFormat">{{ 'backups.configureBackupStorage.format' | tr }} <sup><a ng-href="https://docs.cloudron.io/backups/#backup-formats" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<p class="small text-info" ng-show="backupConfig.format !== configureBackup.format">{{ 'backups.configureBackupStorage.formatChangeNote' | tr }}</p>
|
||||
<p class="small text-info" ng-show="configureBackup.format === 'rsync' && (s3like(configureBackup.provider) || configureBackup.provider === 'gcs')">{{ 'backups.configureBackupStorage.s3LikeNote' | tr }} <sup><a ng-href="https://docs.cloudron.io/backups/#amazon-s3" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<select class="form-control" id="storageFormat" ng-model="configureBackup.format" ng-options="a.value as a.name for a in formats"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.password }" ng-show="configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="inputConfigureBackupPassword">{{ 'backups.configureBackupStorage.encryptionPassword' | tr }} <sup><a ng-href="https://docs.cloudron.io/backups/#encryption" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.encryptionDescription' | tr }}</p>
|
||||
<input type="text" class="form-control" name="encryptionPassword" ng-model="configureBackup.password" id="inputConfigureBackupPassword" ng-disabled="configureBackup.busy" placeholder="{{ 'backups.configureBackupStorage.encryptionPasswordPlaceholder' | tr }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.password && configureBackup.password !== SECRET_PLACEHOLDER" ng-class="{ 'has-error': (configureBackupForm.encryptionPassword.$dirty && configureBackup.password !== configureBackup.passwordRepeat) }">
|
||||
<label class="control-label" for="inputConfigureBackupPasswordRepeat">{{ 'backups.configureBackupStorage.encryptionPasswordRepeat' | tr }}</label>
|
||||
<input id="inputConfigureBackupPasswordRepeat" type="text" class="form-control" name="passwordRepeat" ng-model="configureBackup.passwordRepeat" ng-disabled="configureBackup.busy">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="configureBackup.password !== '' && configureBackup.format === 'rsync'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.encryptedFilenames">{{ 'backups.configureBackupStorage.encryptFilenames' | tr }}</input>
|
||||
<sup><a ng-href="https://docs.cloudron.io/backups/#filenames" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<a href="" ng-click="configureBackup.advancedVisible = true" ng-hide="configureBackup.advancedVisible">{{ 'backups.configureBackupStorage.advancedSettings' | tr }}</a>
|
||||
<div uib-collapse="!configureBackup.advancedVisible">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="sliderConfigureBackupMemoryLimit">{{ 'backups.configureBackupStorage.memoryLimit' | tr }}: <b>{{ configureBackup.memoryLimit | prettyBinarySize:'1024 MB' }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.memoryLimitDescription' | tr }}</p>
|
||||
<input type="range" id="sliderConfigureBackupMemoryLimit" ng-model="configureBackup.memoryLimit" step="{{ 256*1024*1024 }}" min="{{ MIN_MEMORY_LIMIT }}" max="{{ MAX_MEMORY_LIMIT }}" />
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="s3like(configureBackup.provider)">
|
||||
<label class="control-label" for="sliderConfigureBackupUploadPartSize">{{ 'backups.configureBackupStorage.uploadPartSize' | tr }}: <b>{{ configureBackup.uploadPartSize | prettyBinarySize:'Default (50 MiB)' }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.uploadPartSizeDescription' | tr }}</p>
|
||||
<input type="range" id="sliderConfigureBackupUploadPartSize" ng-model="configureBackup.uploadPartSize" list="uploadPartSizeTicks" step="{{ 1024*1024 }}" min="{{ 1024*1024 }}" max="{{ 1024*1024*1024 }}" />
|
||||
<datalist id="uploadPartSizeTicks">
|
||||
<option value="{{ 1024*1024 }}"></option>
|
||||
<option value="{{ 64*1024*1024 }}"></option>
|
||||
<option value="{{ 128*1024*1024 }}"></option>
|
||||
<option value="{{ 256*1024*1024 }}"></option>
|
||||
<option value="{{ 512*1024*1024 }}"></option>
|
||||
<option value="{{ 1024*1024*1024 }}"></option>
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.format === 'rsync' && configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="sliderConfigureBackupSyncConcurrency">{{ 'backups.configureBackupStorage.uploadConcurrency' | tr }}: <b>{{ configureBackup.syncConcurrency }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.uploadConcurrencyDescription' | tr }}</p>
|
||||
<input type="range" id="sliderConfigureBackupSyncConcurrency" ng-model="configureBackup.syncConcurrency" step="10" min="10" max="200" />
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.format === 'rsync' && (s3like(configureBackup.provider) || configureBackup.provider === 'gcs')">
|
||||
<label class="control-label" for="sliderConfigureBackupDownloadConcurrency">{{ 'backups.configureBackupStorage.downloadConcurrency' | tr }}: <b>{{ configureBackup.downloadConcurrency }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.downloadConcurrencyDescription' | tr }}</p>
|
||||
<input type="range" id="sliderConfigureBackupDownloadConcurrency" ng-model="configureBackup.downloadConcurrency" step="10" min="10" max="200" />
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.format === 'rsync' && (s3like(configureBackup.provider) || configureBackup.provider === 'gcs')">
|
||||
<label class="control-label" for="sliderConfigureBackupCopyConcurrency">{{ 'backups.configureBackupStorage.copyConcurrency' | tr }}: <b>{{ configureBackup.copyConcurrency }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.copyConcurrencyDescription' | tr }}
|
||||
<span ng-show="configureBackup.provider === 'digitalocean-spaces'">{{ 'backups.configureBackupStorage.copyConcurrencyDigitalOceanNote' | tr }}</span>
|
||||
</p>
|
||||
<input type="range" id="sliderConfigureBackupCopyConcurrency" ng-model="configureBackup.copyConcurrency" step="10" min="10" max="500" />
|
||||
</div>
|
||||
|
||||
</div> <!-- advanced -->
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="configureBackupForm.$invalid || (configureBackup.password !== SECRET_PLACEHOLDER && configureBackup.password !== configureBackup.passwordRepeat)"/>
|
||||
</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="configureBackup.submit()" ng-disabled="configureBackupForm.$invalid || configureBackup.busy || (configureBackup.password !== SECRET_PLACEHOLDER && configureBackup.password !== configureBackup.passwordRepeat)"><i class="fa fa-circle-notch fa-spin" ng-show="configureBackup.busy"></i><span> {{ 'main.dialog.save' | tr }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>{{ 'backups.title' | tr }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'backups.location.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<p>{{ 'backups.location.description' | tr }}
|
||||
<span ng-show="manualBackupApps.length">
|
||||
{{ 'backups.location.disabledList' | tr }}
|
||||
<span ng-repeat="app in manualBackupApps">
|
||||
<a ng-href="/#/app/{{app.id}}/backups">{{app.label || app.fqdn}}</a><span ng-hide="$last">,</span>
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p ng-show="backupConfig.provider === 'noop'" class="text-danger" ng-bind-html="'backups.check.noop' | tr | markdown2html"></p>
|
||||
<p ng-show="backupConfig.provider === 'filesystem'" class="text-danger" ng-bind-html="'backups.check.sameDisk' | tr | markdown2html"></p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'backups.location.provider' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ prettyProviderName(backupConfig.provider) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="backupConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'backups.location.location' | tr }}</span>
|
||||
</div>
|
||||
<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>
|
||||
<span ng-show="backupConfig.provider === 'cifs' || backupConfig.provider === 'nfs' || backupConfig.provider === 'sshfs'">{{ backupConfig.mountOptions.host }}:{{ backupConfig.mountOptions.remoteDir }}{{ (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
</span>
|
||||
|
||||
<span ng-show="backupConfig.provider !== 's3' && backupConfig.provider !== 'minio' && (s3like(backupConfig.provider) || backupConfig.provider === 'gcs')">{{ backupConfig.bucket + (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
<span ng-show="backupConfig.provider === 's3'">{{ backupConfig.region + ' ' + backupConfig.bucket + (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
<span ng-show="backupConfig.provider === 'minio'">{{ backupConfig.endpoint + ' ' + backupConfig.bucket + (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="backupConfig.endpoint && backupConfig.provider !== 'minio'">
|
||||
<div class="col-xs-3">
|
||||
<span class="text-muted">{{ 'backups.location.endpoint' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-9 text-right">
|
||||
<span>{{ backupConfig.endpoint || backupConfig.region }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'backups.location.format' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ backupConfig.format }} <i class="fas fa-lock" ng-show="backupConfig.password" ></i></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-show="user.isAtLeastOwner" ng-click="configureBackup.show()">{{ 'backups.location.configure' | tr }}</button>
|
||||
<button class="btn btn-outline btn-default pull-right" ng-show="user.isAtLeastOwner && mountlike(backupConfig.provider)" ng-disabled="remount.busy" ng-click="remount.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="remount.busy"></i> {{ 'backups.location.remount' | tr }}</button>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<p ng-bind-html=" 'backups.schedule.description' | tr "></p>
|
||||
<div class="row">
|
||||
<div class="col-xs-4">
|
||||
<span class="text-muted">{{ 'backups.schedule.schedule' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-8 text-right" style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;">
|
||||
<span>{{ prettyBackupSchedule(backupPolicy.currentPolicy.schedule) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-4">
|
||||
<span class="text-muted">{{ 'backups.schedule.retentionPolicy' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-8 text-right">
|
||||
<span>{{ prettyBackupRetention(backupPolicy.currentPolicy.retention) }}</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>
|
||||
</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>
|
||||
|
||||
<div class="card card-large">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-show="!backups.length">{{ 'backups.listing.noBackups' | tr }}</p>
|
||||
|
||||
<table class="table table-hover" style="margin: 0;" ng-hide="!backups.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 20px"></th>
|
||||
<th>{{ 'backups.listing.version' | tr }}</th>
|
||||
<th>{{ 'main.table.date' | tr }}</th>
|
||||
<th>{{ 'backups.listing.contents' | tr }}</th>
|
||||
<th class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="backup in backups">
|
||||
<td><i class="fas fa-archive" ng-show="backup.preserveSecs === -1" uib-tooltip="{{ 'backups.listing.tooltipPreservedBackup' | tr }}"></i></td>
|
||||
<td ng-click="backupDetails.show(backup)" class="hand">v{{ backup.packageVersion }}</td>
|
||||
<td ng-click="backupDetails.show(backup)" class="hand">{{ backup.creationTime | prettyLongDate }} <b ng-show="backup.label">({{ backup.label }})</b></td>
|
||||
<td ng-click="backupDetails.show(backup)" class="hand">
|
||||
<span ng-show="!backup.contents.length">{{ 'backups.listing.noApps' | tr }}</span>
|
||||
<span ng-show="backup.contents.length">{{ 'backups.listing.appCount' | tr:{ appCount: backup.contents.length } }}</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="editBackup.show(backup)" uib-tooltip="{{ 'backups.listing.tooltipEditBackup' | tr }}"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="downloadConfig(backup)" uib-tooltip="{{ 'backups.listing.tooltipDownloadBackupConfig' | tr }}"><i class="fas fa-file-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row" ng-show="createBackup.busy">
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ createBackup.percent }}%"></div>
|
||||
</div>
|
||||
<p>{{ createBackup.message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="!createBackup.busy && !createBackup.active && createBackup.errorMessage">
|
||||
<div class="col-md-12">
|
||||
<p class="has-error">{{ createBackup.errorMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,102 +0,0 @@
|
||||
<!-- Modal change avatar -->
|
||||
<div class="modal fade" id="avatarChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'branding.changeLogo.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body branding-avatar-selector">
|
||||
<img id="previewAvatar" width="128" height="128" ng-src="{{ avatarChange.avatarUrl() }}"/>
|
||||
<input type="file" id="avatarFileInput" style="display: none" accept="image/*"/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="grid">
|
||||
<div class="item" ng-repeat="avatar in avatarChange.availableAvatars" style="background-image: url('{{avatar.data || avatar.url}}');" ng-click="avatarChange.setPreviewAvatar(avatar)"></div>
|
||||
<div class="item add" ng-click="avatarChange.showCustomAvatarSelector()"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="avatarChange.setAvatar()"> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>{{ 'branding.title' | tr }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form role="form" name="aboutForm" ng-submit="about.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': about.error.cloudronName }">
|
||||
<label class="control-label">{{ 'branding.cloudronName' | tr }}</label>
|
||||
<div class="control-label" ng-show="about.error.cloudronName">{{about.error.cloudronName}}</div>
|
||||
<input type="text" class="form-control" id="inputCloudronName" name="name" ng-model="about.cloudronName" ng-minlength="1" maxlength="64" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<label class="control-label">{{ 'branding.logo' | tr }}</label>
|
||||
</div>
|
||||
<div class="branding-avatar" ng-click="avatarChange.showChangeAvatar()">
|
||||
<img ng-src="{{ about.avatarUrl() }}"/>
|
||||
<i class="picture-edit-indicator fa fa-pencil-alt"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<label class="control-label">{{ 'branding.backgroundImage' | tr }}</label>
|
||||
<div class="branding-background" ng-click="background.selectNew()">
|
||||
<img ng-src="{{ background.url() }}" onerror="this.src = '/img/background-image-placeholder.svg'"/>
|
||||
<i class="picture-edit-indicator fa fa-pencil-alt"></i>
|
||||
</div>
|
||||
<a href="" ng-show="!background.cleared" ng-click="background.clear()">{{ 'branding.clearBackgroundImage' | tr }}</a>
|
||||
<input type="file" id="backgroundFileInput" style="display: none" accept="image/*"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="(!about.avatar && !aboutForm.$dirty) || aboutForm.$invalid || about.busy"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="about.submit()" ng-disabled="false && (!about.avatar && !aboutForm.$dirty) || aboutForm.$invalid || about.busy"><i class="fa fa-circle-notch fa-spin" ng-show="about.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'branding.footer.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form role="form" name="footerForm" autocomplete="off">
|
||||
<p>{{ 'branding.footer.description' | tr }} <sup><a ng-href="https://docs.cloudron.io/branding/#footer" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<textarea name="footer" class="form-control" ng-model="footer.content" ng-disabled="footer.busy"></textarea>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="footer.submit()" ng-disabled="!footerForm.$dirty || footerForm.$invalid || footer.busy"><i class="fa fa-circle-notch fa-spin" ng-show="footer.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,524 +0,0 @@
|
||||
<!-- 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">{{ 'domains.subscriptionRequired.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'domains.subscriptionRequired.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-success" ng-click="openSubscriptionSetup()">{{ 'domains.subscriptionRequired.setupAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal domain add/configure -->
|
||||
<div class="modal fade" id="domainConfigureModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" ng-show="domainConfigure.adding">{{ 'domains.domainDialog.addTitle' | tr }}</h4>
|
||||
<h4 class="modal-title" ng-hide="domainConfigure.adding">{{ 'domains.domainDialog.editTitle' | tr:{ domain: domainConfigure.domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-show="domainConfigure.adding" ng-bind-html="'domains.domainDialog.addDescription' | tr"></p>
|
||||
|
||||
<form name="domainConfigureForm" role="form" novalidate ng-submit="domainConfigure.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="domainConfigure.error">{{ domainConfigure.error }}</p>
|
||||
|
||||
<div class="form-group" ng-show="domainConfigure.adding">
|
||||
<label class="control-label">{{ 'domains.domainDialog.domain' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.newDomain" name="newDomain" ng-disabled="domainConfigure.busy" placeholder="example.com" ng-required="domainConfigure.adding" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.provider' | tr }} <sup><a ng-href="https://docs.cloudron.io/domains/#dns-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="domainConfigure.provider" ng-options="a.value as a.name for a in dnsProvider" ng-change="domainConfigure.setDefaultTlsProvider()"></select>
|
||||
</div>
|
||||
|
||||
<!-- Route53 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'route53'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.route53AccessKeyId' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.accessKeyId" name="accessKeyId" ng-disabled="domainConfigure.busy" ng-minlength="16" ng-maxlength="32" ng-required="domainConfigure.provider === 'route53'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'route53'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.route53SecretAccessKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.secretAccessKey" name="secretAccessKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'route53'">
|
||||
</div>
|
||||
|
||||
<!-- Google Cloud DNS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'gcdns'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.gcdnsServiceAccountKey' | tr }}</label>
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcdnsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="domainConfigure.gcdnsKey.keyFileName" id="gcdnsKeyInput" name="cert" onclick="getElementById('gcdnsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'gcdns'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcdnsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DigitalOcean -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'digitalocean'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.digitalOceanToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.digitalOceanToken" name="digitalOceanToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'digitalocean'">
|
||||
</div>
|
||||
|
||||
<!-- Gandi -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'gandi'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.gandiApiKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.gandiApiKey" name="gandiApiKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'gandi'">
|
||||
</div>
|
||||
|
||||
<!-- GoDaddy -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'godaddy'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.goDaddyApiKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.godaddyApiKey" name="apiKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'godaddy'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'godaddy'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.goDaddyApiSecret' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.godaddyApiSecret" name="apiSecret" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'godaddy'">
|
||||
</div>
|
||||
|
||||
<!-- Netcup -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'netcup'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.netcupCustomerNumber' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.netcupCustomerNumber" name="netcupCustomerNumber" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'netcup'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'netcup'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.netcupApiKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.netcupApiKey" name="netcupApiKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'netcup'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'netcup'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.netcupApiPassword' | tr }}</label>
|
||||
<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>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.porkbunApikey" name="porkbunApikey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'porkbun'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'porkbun'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.porkbunSecretapikey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.porkbunSecretapikey" name="porkbunSecretapikey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'porkbun'">
|
||||
</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>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.cloudflareToken" name="cloudflareToken" ng-required="domainConfigure.provider === 'cloudflare'" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare' && domainConfigure.cloudflareTokenType === 'GlobalApiKey'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.cloudflareEmail' | tr }}</label>
|
||||
<input type="email" class="form-control" ng-model="domainConfigure.cloudflareEmail" name="cloudflareEmail" ng-required="domainConfigure.provider === 'cloudflare' && domainConfigure.cloudflareTokenType === 'GlobalApiKey'" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="domainConfigure.cloudflareDefaultProxyStatus"> {{ 'domains.domainDialog.cloudflareDefaultProxyStatus' | tr }}
|
||||
<sup><a ng-href="https://docs.cloudron.io/domains/#cloudflare-dns" class="help" target="_blank" tabIndex="-1"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</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>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.linodeToken" name="linodeToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'linode'">
|
||||
</div>
|
||||
|
||||
<!-- Bunny -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'bunny'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.bunnyAccessKey' | tr }}</label>
|
||||
<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>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.hetznerToken" name="hetznerToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'hetzner'">
|
||||
</div>
|
||||
|
||||
<!-- Vultr -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'vultr'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.vultrToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.vultrToken" name="vultrToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'vultr'">
|
||||
</div>
|
||||
|
||||
<!-- deSEC -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'desec'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.deSecToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.deSecToken" name="deSecToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'desec'">
|
||||
</div>
|
||||
|
||||
<!-- Name.com -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecom'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.nameComUsername' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.nameComUsername" name="nameComUsername" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'namecom'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecom'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.nameComApiToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.nameComToken" name="nameComToken" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'namecom'">
|
||||
</div>
|
||||
|
||||
<!-- Namecheap -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecheap'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.namecheapUsername' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.namecheapUsername" name="namecheapUsername" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'namecheap'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecheap'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.namecheapApiKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.namecheapApiKey" name="namecheapApiKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'namecheap'">
|
||||
</div>
|
||||
|
||||
<p class="small text-info text-bold" ng-show="domainConfigure.provider === 'namecheap'" ng-bind-html="'domains.domainDialog.namecheapInfo' | tr"></p>
|
||||
<p class="small text-info text-bold" ng-show="domainConfigure.provider === 'wildcard'" ng-bind-html="'domains.domainDialog.wildcardInfo' | tr:{ domain: domainConfigure.adding ? domainConfigure.newDomain : domainConfigure.domain.domain }"></p>
|
||||
<p class="small text-info text-bold" ng-show="domainConfigure.provider === 'manual'" ng-bind-html="'domains.domainDialog.manualInfo' | tr"></p>
|
||||
<p class="small text-info text-bold" ng-show="needsPort80(domainConfigure.provider, domainConfigure.tlsConfig.provider)" ng-bind-html="'domains.domainDialog.letsEncryptInfo' | tr"></p>
|
||||
|
||||
<a href="" ng-click="domainConfigure.advancedVisible = true" ng-hide="domainConfigure.advancedVisible">{{ 'domains.domainDialog.advancedAction' | tr }}</a>
|
||||
<div uib-collapse="!domainConfigure.advancedVisible">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.zoneName' | tr }} <sup><a ng-href="https://docs.cloudron.io/domains/#zone-name" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.zoneName" name="zoneName" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.certProvider' | tr }} <sup><a ng-href="https://docs.cloudron.io/certificates/#certificate-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="domainConfigure.tlsConfig.provider" ng-options="a.value as a.name for a in tlsProvider"></select>
|
||||
</div>
|
||||
|
||||
<!-- Fallback certificate -->
|
||||
<div ng-show="domainConfigure.tlsConfig.provider !== 'fallback'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.fallbackCert' | tr }}</label>
|
||||
<p ng-bind-html="'domains.domainDialog.fallbackCertInfo' | tr"></p>
|
||||
</div>
|
||||
|
||||
<div ng-show="domainConfigure.tlsConfig.provider === 'fallback'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.fallbackCertCustomCert' | tr }}</label>
|
||||
<p ng-bind-html="'domains.domainDialog.fallbackCertCustomCertInfo' | tr:{ customCertLink: 'https://docs.cloudron.io/certificates/#custom-certificates' }"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (!fallbackCert.cert.$dirty && fallbackCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="fallbackCertFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="{{ 'domains.domainDialog.fallbackCertCertificatePlaceholder' | tr }}" ng-model="domainConfigure.fallbackCert.certificateFileName" name="cert" onclick="getElementById('fallbackCertFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy">
|
||||
<span class="input-group-addon"><i class="fa fa-upload" onclick="getElementById('fallbackCertFileInput').click();"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!fallbackCert.key.$dirty && fallbackCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="fallbackKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="{{ 'domains.domainDialog.fallbackCertKeyPlaceholder' | tr }}" ng-model="domainConfigure.fallbackCert.keyFileName" id="fallbackKeyInput" name="key" onclick="getElementById('fallbackKeyFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy">
|
||||
<span class="input-group-addon"><i class="fa fa-upload" onclick="getElementById('fallbackKeyFileInput').click();"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- advanced -->
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="domainConfigureForm.$invalid || domainConfigure.busy"/>
|
||||
</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="domainConfigure.submit()" ng-disabled="domainConfigureForm.$invalid || domainConfigure.busy"><i class="fa fa-circle-notch fa-spin" ng-show="domainConfigure.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal domain wellknown -->
|
||||
<div class="modal fade" id="domainWellKnownModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'domains.domainWellKnown.title' | tr:{ domain: domainWellKnown.domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'domains.domainDialog.wellKnownDescription' | tr:{ domain: domainWellKnown.domain.domain, docsLink: 'https://docs.cloudron.io/domains/#well-known-locations' }"></p>
|
||||
|
||||
<form name="domainWellKnownForm" role="form" novalidate ng-submit="domainWellKnown.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="domainWellKnown.error">{{ domainWellKnown.error }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.matrixHostname' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainWellKnown.matrixHostname" name="matrixHostname" ng-disabled="domainWellKnown.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.mastodonHostname' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainWellKnown.mastodonHostname" name="mastodonHostname" ng-disabled="domainWellKnown.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.jitsiHostname' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainWellKnown.jitsiHostname" name="jitsiHostname" ng-disabled="domainWellKnown.busy">
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="domainWellKnownForm.$invalid || domainWellKnown.busy"/>
|
||||
</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="domainWellKnown.submit()" ng-disabled="domainWellKnownForm.$invalid || domainWellKnown.busy"><i class="fa fa-circle-notch fa-spin" ng-show="domainWellKnown.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal domain remove -->
|
||||
<div class="modal fade" id="domainRemoveModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'domains.removeDialog.title' | tr:{ domain: domainRemove.domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'domains.removeDialog.description' | tr:{ domain: domainRemove.domain.domain }"></p>
|
||||
<br/>
|
||||
<span class="has-error" ng-show="domainRemove.error">{{ domainRemove.error }}</span>
|
||||
</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="domainRemove.submit()" ng-disabled="domainRemove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="domainRemove.busy"></i> {{ 'domains.removeDialog.removeAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="text-left">
|
||||
<h1>
|
||||
{{ 'domains.title' | tr }}
|
||||
<button class="btn btn-primary btn-outline pull-right" ng-show="domains.length <= pageSize" ng-click="domainAdd.show()"><i class="fa fa-plus"></i> {{ 'domains.addDomain' | tr }}</button>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="users-toolbar" ng-show="domains.length > pageSize">
|
||||
<input type="text" id="domainSearchInput" class="form-control" style="max-width: 350px;" ng-model="domainSearchString" placeholder="{{ 'main.searchPlaceholder' | tr }}"/>
|
||||
<div style="flex-grow: 1;"></div>
|
||||
<button class="btn btn-primary btn-outline pull-right" ng-click="domainAdd.show()"><i class="fa fa-plus"></i> {{ 'domains.addDomain' | tr }}</button>
|
||||
</div>
|
||||
<div class="card card-large">
|
||||
<div class="row ng-hide" ng-show="!ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="ready">
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'domains.domain' | tr }}</th>
|
||||
<th class="text-left hidden-xs hidden-sm">{{ 'domains.provider' | tr }}</th>
|
||||
<th style="width: 100px" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="domain in domains | filter:domainSearchString | limitTo:pageSize:((currentPage-1)*pageSize)">
|
||||
<td class="elide-table-cell hand" ng-click="domainConfigure.show(domain)">
|
||||
{{ domain.domain }}
|
||||
</td>
|
||||
<td class="text-left elide-table-cell hidden-xs hidden-sm hand" ng-click="domainConfigure.show(domain)">
|
||||
{{ prettyProviderName(domain) }}
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="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>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pull-left">
|
||||
{{ 'domains.count' | tr:{ count: (domains | filter:domainSearchString).length } }}
|
||||
</div>
|
||||
<div class="pull-right" ng-show="domains.length > pageSize">
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="showPrevPage()" ng-class="{ 'btn-primary': currentPage > 1 }" ng-disabled="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': (domains | filter:domainSearchString).length > (currentPage * pageSize) }" ng-disabled="(domains | filter:domainSearchString).length <= (currentPage * pageSize)">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-bind-html="'domains.renewCerts.description' | tr"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div ng-show="renewCerts.busy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ renewCerts.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-show="renewCerts.busy">{{ renewCerts.message }}</p>
|
||||
<p ng-hide="renewCerts.busy">
|
||||
<div class="has-error" ng-show="!renewCerts.active">{{ renewCerts.errorMessage }}</div>
|
||||
</p>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="renewCerts.renew()" ng-disabled="renewCerts.busy">{{ '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>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-bind-html="'domains.syncDns.description' | tr"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div ng-show="syncDns.busy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ syncDns.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-show="syncDns.busy">{{ syncDns.message }}</p>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<p ng-bind-html="'domains.changeDashboardDomain.description' | tr"></p>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon" style="background-color: white;">my.</span>
|
||||
<select class="form-control pull-right" style="display: inline-block;" ng-model="changeDashboard.selectedDomain" ng-options="a.domain for a in domains"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div ng-show="changeDashboard.busy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ changeDashboard.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,734 +0,0 @@
|
||||
<!-- 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">{{ 'email.subscriptionDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ 'email.subscriptionDialog.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-success" ng-click="openSubscriptionSetup()">{{ 'email.subscriptionDialog.setupAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal enable email -->
|
||||
<div class="modal fade" id="enableEmailModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.enableEmailDialog.title' | tr:{ domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'email.enableEmailDialog.description' | tr:{ domain: domain.domain, requiredPortsDocsLink: 'https://docs.cloudron.io/email/#required-ports' }"></p>
|
||||
<p class="text-warning" ng-show="domain.provider === 'noop' || domain.provider === 'manual'" ng-bind-html="'email.enableEmailDialog.noProviderInfo' | tr"></p>
|
||||
<p class="text-danger" ng-show="adminDomain.provider === 'cloudflare'" ng-bind-html="'email.enableEmailDialog.cloudflareInfo' | tr:{ adminDomain: config.adminDomain, mailFqdn: config.mailFqdn }"></p>
|
||||
<div ng-hide="domain.provider === 'noop' || domain.provider === 'manual'">
|
||||
<p>
|
||||
<label class="control-label">
|
||||
<input type="checkbox" ng-model="incomingEmail.setupDns"> {{ 'email.enableEmailDialog.setupDnsCheckbox' | tr }}
|
||||
</label>
|
||||
</p>
|
||||
<span ng-bind-html="'email.enableEmailDialog.setupDnsInfo' | tr:{ importEmailDocsLink: 'https://docs.cloudron.io/guides/import-email' }"></span>
|
||||
</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-success" ng-click="incomingEmail.enable()">{{ 'email.enableEmailDialog.enableAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal disable email -->
|
||||
<div class="modal fade" id="disableEmailModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.disableEmailDialog.title' | tr:{ domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-bind-html="'email.disableEmailDialog.description' | tr:{ domain: domain.domain }"></div>
|
||||
<br/>
|
||||
</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="incomingEmail.disable()">{{ 'email.disableEmailDialog.disableAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal add mailbox -->
|
||||
<div class="modal fade" id="mailboxAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.addMailboxDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="mailboxadd_form" role="form" ng-submit="mailboxes.add.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': mailboxes.add.error }">
|
||||
<label class="control-label">{{ 'email.addMailboxDialog.name' | tr }}</label>
|
||||
<div class="control-label" ng-show="mailboxes.add.error">
|
||||
<small>{{ mailboxes.add.error.message }}</small>
|
||||
</div>
|
||||
<div class="input-group form-inline" style="margin-top: 10px;">
|
||||
<input type="text" class="form-control" ng-model="mailboxes.add.name" required autofocus autocomplete="off"/>
|
||||
<div class="input-group-addon">@{{ domain.domain }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'email.addMailboxDialog.owner' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<multiselect ng-model="mailboxes.add.owner" options="o.display for o in owners" data-compare-by="name" data-header-key="header" data-divider-key="divider" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="mailboxadd_form.$invalid || mailboxes.add.busy || !mailboxes.add.owner"/>
|
||||
</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="mailboxes.add.submit()" ng-disabled="mailboxadd_form.$invalid || mailboxes.add.busy || !mailboxes.add.owner"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxes.add.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal edit mailbox -->
|
||||
<div class="modal fade" id="mailboxEditModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.editMailboxDialog.title' | tr:{ name: mailboxes.edit.name, domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'email.editMailboxDialog.owner' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<multiselect ng-model="mailboxes.edit.owner" options="o.display for o in owners" data-compare-by="name" data-header-key="header" data-divider-key="divider" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group aliases">
|
||||
<label class="control-label">{{ 'email.editMailboxDialog.aliases' | tr }}</label>
|
||||
<div class="has-error" ng-show="mailboxes.edit.error">{{ mailboxes.edit.error.message }}</div>
|
||||
|
||||
<div class="row" ng-repeat="alias in mailboxes.edit.aliases | orderBy:'reversedSortingNotation'">
|
||||
<div class="col col-lg-11">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control input-sm" ng-model="alias.name">
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
|
||||
<span>@{{ alias.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="incomingDomain in incomingDomains">
|
||||
<a href="" ng-click="alias.domain = incomingDomain.domain">{{ incomingDomain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-lg-1">
|
||||
<button class="btn btn-danger btn-sm" ng-click="mailboxes.edit.delAlias($event, alias)"><i class="far fa-trash-alt"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="mailboxes.edit.aliases.length === 0">
|
||||
{{ 'email.editMailboxDialog.noAliases' | tr }} <a href="" ng-click="mailboxes.edit.addAlias($event)">{{ 'email.editMailboxDialog.addAliasAction' | tr }}</a>
|
||||
</div>
|
||||
<div ng-show="mailboxes.edit.aliases.length > 0" style="margin-top: 5px;">
|
||||
<a href="" ng-click="mailboxes.edit.addAlias($event)">{{ 'email.editMailboxDialog.addAnotherAliasAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="storageQuota">
|
||||
<input id="storageQuota" type="checkbox" ng-model="mailboxes.edit.storageQuotaEnabled">
|
||||
{{ 'email.editMailboxDialog.enableStorageQuota' | tr }} <b ng-hide="!mailboxes.edit.storageQuotaEnabled">: {{ mailboxes.edit.storageQuota | prettyDecimalSize }}</b>
|
||||
</input>
|
||||
</label>
|
||||
<input type="range" id="storageQuota" ng-disabled="!mailboxes.edit.storageQuotaEnabled" ng-model="mailboxes.edit.storageQuota" step="500000000" min="{{ storageQuotaTicks[0] }}" max="{{ storageQuotaTicks[storageQuotaTicks.length-1] }}" list="storageQuotaTicks" />
|
||||
<datalist id="storageQuotaTicks">
|
||||
<option ng-repeat="quota in storageQuotaTicks" value="{{ quota }}"></option>
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailboxes.edit.enablePop3"> {{ 'email.updateMailboxDialog.enablePop3' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailboxes.edit.active"> {{ 'email.updateMailboxDialog.activeCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</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-success" ng-click="mailboxes.edit.submit()" ng-disabled="mailboxes.edit.busy || !mailboxes.edit.owner"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxes.edit.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal remove mailbox -->
|
||||
<div class="modal fade" id="mailboxRemoveModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.deleteMailboxDialog.title' | tr:{ name: mailboxes.remove.mailbox.name, domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-bind-html="'email.deleteMailboxDialog.description' | tr"></div>
|
||||
<br/>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailboxes.remove.deleteMails">{{ 'email.deleteMailboxDialog.purgeMailboxCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</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-danger" ng-click="mailboxes.remove.submit()" ng-disabled="mailboxes.remove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxes.remove.busy"></i> {{ 'email.deleteMailboxDialog.deleteAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal add mailinglist -->
|
||||
<div class="modal fade" id="mailinglistAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.addMailinglistDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="mailinglistadd_form" role="form" ng-submit="mailinglists.add.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': mailinglists.add.error.name }">
|
||||
<label class="control-label">{{ 'email.addMailinglistDialog.name' | tr }}</label>
|
||||
<div class="control-label" ng-show="mailinglists.add.error.name"><small>{{ mailinglists.add.error.name }}</small></div>
|
||||
<div class="input-group form-inline" style="margin-top: 10px;">
|
||||
<input type="text" class="form-control" ng-model="mailinglists.add.name" required autofocus autocomplete="off"/>
|
||||
<div class="input-group-addon">@{{ domain.domain }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'email.addMailinglistDialog.members' | tr }}</label><br/>
|
||||
<div class="has-error control-label" ng-show="mailinglists.add.error.members"><small>{{ mailinglists.add.error.members }}</small></div>
|
||||
<textarea ng-model="mailinglists.add.membersTxt" class="form-control" rows="5"></textarea>
|
||||
<small>{{ 'email.addMailinglistDialog.membersInfo' | tr }}</small>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailinglists.add.membersOnly">{{ 'email.addMailinglistDialog.membersOnlyCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="mailinglistadd_form.$invalid || mailinglists.add.membersTxt.length === 0 || mailinglists.add.busy"/>
|
||||
</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="mailinglists.add.submit()" ng-disabled="mailinglistadd_form.$invalid || mailinglists.add.membersTxt.length === 0 || mailinglists.add.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailinglists.add.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal edit mailinglist -->
|
||||
<div class="modal fade" id="mailinglistEditModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.editMailinglistDialog.title' | tr:{ name: mailinglists.edit.name, domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="mailinglistedit_form" role="form" ng-submit="mailinglists.edit.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': mailinglists.edit.error.members }">
|
||||
<label class="control-label">{{ 'email.addMailinglistDialog.members' | tr }}</label><br/>
|
||||
<div class="has-error control-label" ng-show="mailinglists.edit.error.members"><small>{{ mailinglists.edit.error.members }}</small></div>
|
||||
<textarea ng-model="mailinglists.edit.membersTxt" class="form-control" rows="5" autofocus></textarea>
|
||||
<small>{{ 'email.addMailinglistDialog.membersInfo' | tr }}</small>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailinglists.edit.membersOnly">{{ 'email.addMailinglistDialog.membersOnlyCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailinglists.edit.active"> {{ 'email.updateMailinglistDialog.activeCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="mailinglistedit_form.$invalid || mailinglists.edit.busy"/>
|
||||
</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="mailinglists.edit.submit()" ng-disabled="mailinglistedit_form.$invalid || mailinglists.edit.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailinglists.edit.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal remove mailinglist -->
|
||||
<div class="modal fade" id="mailinglistRemoveModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.deleteMailinglistDialog.title' | tr:{ name: mailinglists.remove.list.name, domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'email.deleteMailinglistDialog.description' | tr:{ name: mailinglists.remove.list.name, domain: domain.domain }"></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="mailinglists.remove.submit()" ng-disabled="mailinglists.remove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailinglist.remove.busy"></i> {{ 'email.deleteMailinglistDialog.deleteAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal how to connect -->
|
||||
<div class="modal fade" id="howToConnectInfoModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4>{{ 'email.howToConnectInfoModal' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html=" 'email.incoming.howToConnectDescription' | tr:{ domain: domain.domain } "></p>
|
||||
|
||||
<p><b>{{ 'email.incoming.incomingUserInfo' | tr }}</b><br/><i>mailboxname</i>@{{ domain.domain }}</p>
|
||||
<p><b>{{ 'email.incoming.incomingPasswordInfo' | tr }}</b><br/>{{ 'email.incoming.incomingPasswordUsage' | tr }}</p>
|
||||
<p><b>{{ 'email.incoming.incomingServerInfo' | tr }}</b><br/>{{ 'email.incoming.server' | tr }}: <span ng-click-select>{{config.mailFqdn}}</span><br/>{{ 'email.incoming.port' | tr }}: 993 (TLS)</p>
|
||||
<p><b>{{ 'email.incoming.outgointServerInfo' | tr }}</b><br/>{{ 'email.incoming.server' | tr }}: <span ng-click-select>{{config.mailFqdn}}</span><br/>{{ 'email.incoming.port' | tr }}: 587 (STARTTLS) or 465 (TLS)</p>
|
||||
<p><b>{{ 'email.incoming.sieveServerInfo' | tr }}</b><br/>{{ 'email.incoming.server' | tr }}: <span ng-click-select>{{config.mailFqdn}}</span><br/>{{ 'email.incoming.port' | tr }}: 4190 (STARTTLS)</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="!ready" class="loading-banner">
|
||||
<h1><i class="fa fa-circle-notch fa-spin"></i></h1>
|
||||
</div>
|
||||
|
||||
<div class="content" ng-show="ready">
|
||||
<a href="/#/email" class="back-to-view-link"><i class="fas fa-arrow-left"></i> {{ 'email.backAction' | tr }}</a>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>
|
||||
{{ 'email.config.title' | tr:{ domain: domain.domain } }}
|
||||
|
||||
<div class="dropdown pull-right" 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">
|
||||
<i class="fas fa-book"></i>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="https://docs.cloudron.io/email/" target="_blank">{{ 'app.docsAction' | tr }}</a></li>
|
||||
<li ng-class="{ 'disabled': !domain.mailConfig.enabled }"><a href="" ng-click="domain.mailConfig.enabled ? howToConnectInfo.show() : null">{{ 'email.config.clientConfiguration' | tr }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<uib-tabset active="activeTab">
|
||||
<uib-tab index="'mailboxes'" select="setView('mailboxes')" heading="{{ 'email.incoming.tabTitle' | tr }}">
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<h4>{{ 'email.incoming.title' | tr }}</h4>
|
||||
|
||||
<p ng-show="domain.mailConfig.enabled">{{ 'email.incoming.enabled' | tr }}</p>
|
||||
<p ng-hide="domain.mailConfig.enabled">{{ 'email.incoming.disabled' | tr }}</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<button class="pull-right" ng-class="domain.mailConfig.enabled ? 'btn btn-danger' : 'btn btn-primary'" ng-click="incomingEmail.toggleEmailEnabled()" ng-disabled="incomingEmail.busy" ng-show="user.isAtLeastAdmin">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="incomingEmail.busy"></i>
|
||||
{{ domain.mailConfig.enabled ? ('email.incoming.disableAction' | tr) : ('email.incoming.enableAction' | tr) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3 style="margin-bottom: 15px;">{{ 'email.incoming.mailboxes.title' | tr }}
|
||||
<button class="btn btn-primary btn-outline pull-right" ng-click="mailboxes.add.show()" ng-disabled="!domain.mailConfig.enabled" tooltip-enable="!domain.mailConfig.enabled" uib-tooltip="{{ 'email.incoming.mailboxes.disabledTooltip' | tr }}"><i class="fa fa-inbox"></i> {{ 'email.incoming.mailboxes.addAction' | tr }}</button>
|
||||
<input class="form-control pull-right" style="width: 200px;" placeholder="{{ 'main.searchPlaceholder' | tr }}" type="text" ng-model="mailboxes.search" ng-model-options="{ debounce: 1000 }" ng-change="mailboxes.updateFilter()" />
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'email.incoming.mailboxes.name' | tr }}</th>
|
||||
<th>{{ 'email.incoming.mailboxes.owner' | tr }}</th>
|
||||
<th>{{ 'email.incoming.mailboxes.aliases' | tr }}</th>
|
||||
<th>{{ 'email.incoming.mailboxes.usage' | tr }}</th>
|
||||
<th class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="mailbox in mailboxes.mailboxes | filter:mailboxes.search" ng-class="{'text-muted': !mailbox.active}">
|
||||
<td class="hand" ng-click="mailboxes.edit.show(mailbox)">
|
||||
{{ mailbox.name }}
|
||||
</td>
|
||||
<td class="hand" ng-click="mailboxes.edit.show(mailbox)">
|
||||
{{ mailbox.ownerDisplayName }}
|
||||
</td>
|
||||
<td class="hand elide-table-cell" ng-click="mailboxes.edit.show(mailbox)">
|
||||
<span ng-repeat="alias in mailbox.aliases"> {{ alias.name + '@' + alias.domain }}</span>
|
||||
</td>
|
||||
<td class="hand no-wrap" ng-click="mailboxes.edit.show(mailbox)">
|
||||
<span ng-show="mailUsage !== null">
|
||||
{{ mailUsage[mailbox.fullName].quotaValue | prettyDecimalSize }} <span ng-show="mailUsage[mailbox.fullName].quotaLimit">/ {{ mailUsage[mailbox.fullName].quotaLimit | prettyDecimalSize }}</span>
|
||||
</span>
|
||||
<span ng-show="mailUsage === null">
|
||||
{{ 'main.loadingPlaceholder' | tr }} ...
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap">
|
||||
<button class="btn btn-xs btn-default" ng-click="mailboxes.edit.show(mailbox)"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="mailboxes.remove.show(mailbox)"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="mailboxes.showPrevPage()" ng-class="{ 'btn-primary': mailboxes.currentPage > 1 }" ng-disabled="mailboxes.busy || mailboxes.currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="mailboxes.showNextPage()" ng-class="{ 'btn-primary': mailboxes.perPage <= mailboxes.mailboxes.length }" ng-disabled="mailboxes.busy || mailboxes.perPage > mailboxes.mailboxes.length">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3 style="margin-bottom: 15px;">{{ 'email.incoming.mailinglists.title' | tr }}
|
||||
<button class="btn btn-primary btn-outline pull-right" ng-click="mailinglists.add.show()" ng-disabled="!domain.mailConfig.enabled" tooltip-enable="!domain.mailConfig.enabled" uib-tooltip="{{ 'email.incoming.mailboxes.disabledTooltip' | tr }}"><i class="fa fa-list"></i> {{ 'email.incoming.mailboxes.addAction' | tr }}</button>
|
||||
<input class="form-control pull-right" style="width: 200px;" placeholder="{{ 'main.searchPlaceholder' | tr }}" type="text" ng-model="mailinglists.search" ng-model-options="{ debounce: 1000 }" ng-change="mailinglists.updateFilter()" />
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{{ 'email.incoming.mailinglists.description' | tr }}
|
||||
<br/>
|
||||
<br/>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 0.5%;"></th>
|
||||
<th>{{ 'email.incoming.mailinglists.name' | tr }}</th>
|
||||
<th>{{ 'email.incoming.mailinglists.members' | tr }}</th>
|
||||
<th class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="list in mailinglists.mailinglists | filter:mailinglists.search | orderBy:'name'" ng-class="{'text-muted': !list.active}">
|
||||
<td>
|
||||
<i class="fas fa-door-closed" ng-show="list.membersOnly" uib-tooltip="{{ 'email.incoming.mailinglists.membersOnlyTooltip' | tr }}"></i>
|
||||
<i class="fas fa-door-open" ng-show="!list.membersOnly" uib-tooltip="{{ 'email.incoming.mailinglists.everyoneTooltip' | tr }}"></i>
|
||||
</td>
|
||||
<td class="hand" ng-click="mailinglists.edit.show(list)">
|
||||
{{ list.name }}
|
||||
</td>
|
||||
<td class="hand" ng-click="mailinglists.edit.show(list)">
|
||||
{{ list.members.join(', ') }}
|
||||
</td>
|
||||
<td class="text-right no-wrap">
|
||||
<button class="btn btn-xs btn-default" ng-click="mailinglists.edit.show(list)"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="mailinglists.remove.show(list)"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="mailinglists.showPrevPage()" ng-class="{ 'btn-primary': mailinglists.currentPage > 1 }" ng-disabled="mailinglists.busy || mailinglists.currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="mailinglists.showNextPage()" ng-class="{ 'btn-primary': mailinglists.perPage <= mailinglists.mailinglists.length }" ng-disabled="mailinglists.busy || mailinglists.perPage > mailinglists.mailinglists.length">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'email.incoming.catchall.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" ng-bind-html=" 'email.incoming.catchall.description' | tr "></div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<multiselect ng-model="catchall.mailboxes" options="mailbox.display for mailbox in catchall.availableMailboxes" data-compare-by="display" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<button class="btn btn-outline btn-primary" ng-click="catchall.submit()" ng-disabled="catchall.busy || !domain.mailConfig.enabled" tooltip-enable="!domain.mailConfig.enabled" uib-tooltip="{{ 'email.incoming.mailboxes.disabledTooltip' | tr }}">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="catchall.busy"></i> {{ 'email.incoming.catchall.saveAction' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="'outbound'" ng-if="user.isAtLeastAdmin" select="setView('outbound')" heading="{{ 'email.outbound.tabTitle' | tr }}">
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<h4>{{ 'email.outbound.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#relay-outbound-mails" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></h4>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12" ng-bind-html=" 'email.outbound.description' | tr "></div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<select class="form-control" style="width: 50%;" ng-model="mailRelay.preset" ng-options="a.name for a in mailRelayPresets track by a.provider" ng-change="mailRelay.presetChanged()"></select>
|
||||
</div>
|
||||
|
||||
<p class="small text-danger" ng-show="mailRelay.preset.provider === 'noop'">
|
||||
<span ng-if="domain.domain === config.adminDomain">{{ 'email.outbound.noopAdminDomainWarning' | tr }}</span>
|
||||
<span ng-if="domain.domain !== config.adminDomain">{{ 'email.outbound.noopNonAdminDomainWarning' | tr }}</span>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="usesExternalServer(mailRelay.preset.provider)">
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<form name="mailRelayForm" role="form" ng-submit="mailRelay.submit()" autocomplete="off" novalidate>
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailRelayForm.host.$dirty && mailRelayForm.host.$invalid) }">
|
||||
<label class="control-label">{{ 'email.outbound.mailRelay.host' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.host.$dirty && mailRelay.error.host) || (mailRelayForm.host.$dirty && mailRelayForm.host.$invalid)">
|
||||
<small ng-show="!mailRelayForm.host.$dirty && mailRelay.error.host">{{ mailRelay.error.host }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="mailRelay.relay.host" name="host" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailRelayForm.port.$dirty && mailRelayForm.port.$invalid) }">
|
||||
<label class="control-label">{{ 'email.outbound.mailRelay.port' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.port.$dirty && mailRelay.error.port) || (mailRelayForm.port.$dirty && mailRelayForm.port.$invalid)">
|
||||
<small ng-show="!mailRelayForm.port.$dirty && mailRelay.error.port">{{ mailRelay.error.port }}</small>
|
||||
</div>
|
||||
<input type="number" class="form-control" ng-model="mailRelay.relay.port" name="port" required>
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="mailRelay.relay.provider === 'external-smtp' || mailRelay.relay.provider === 'external-smtp-noauth'" >
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailRelay.relay.acceptSelfSignedCerts">{{ 'email.outbound.mailRelay.selfsignedCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Postmark, Sendgrid, SparkPost -->
|
||||
<div ng-show="usesTokenAuth(mailRelay.relay.provider)" class="form-group" ng-class="{ 'has-error': (mailRelayForm.serverApiToken.$dirty && mailRelayForm.serverApiToken.$invalid) }">
|
||||
<label class="control-label">{{ 'email.outbound.mailRelay.apiTokenOrKey' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.serverApiToken.$dirty && mailRelay.error.serverApiToken) || (mailRelayForm.serverApiToken.$dirty && mailRelayForm.serverApiToken.$invalid)">
|
||||
<small ng-show="!mailRelayForm.serverApiToken.$dirty && mailRelay.error.serverApiToken">{{ mailRelay.error.serverApiToken }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="mailRelay.relay.serverApiToken" name="serverApiToken" ng-required="usesTokenAuth(mailRelay.relay.provider)">
|
||||
</div>
|
||||
|
||||
<!-- Other -->
|
||||
<div ng-show="usesPasswordAuth(mailRelay.relay.provider)" class="form-group" ng-class="{ 'has-error': (mailRelayForm.username.$dirty && mailRelayForm.username.$invalid) }">
|
||||
<label class="control-label">{{ 'email.outbound.mailRelay.username' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.username.$dirty && mailRelay.error.username) || (mailRelayForm.username.$dirty && mailRelayForm.username.$invalid)">
|
||||
<small ng-show="!mailRelayForm.username.$dirty && mailRelay.error.username">{{ mailRelay.error.username }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="mailRelay.relay.username" name="username" ng-required="usesPasswordAuth(mailRelay.relay.provider)">
|
||||
</div>
|
||||
|
||||
<div ng-show="usesPasswordAuth(mailRelay.relay.provider)" class="form-group" ng-class="{ 'has-error': (mailRelayForm.password.$dirty && mailRelayForm.password.$invalid) }">
|
||||
<label class="control-label">{{ 'email.outbound.mailRelay.password' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.password.$dirty && mailRelay.error.password) || (mailRelayForm.password.$dirty && mailRelayForm.password.$invalid)">
|
||||
<small ng-show="!mailRelayForm.password.$dirty && mailRelay.error.password">{{ mailRelay.error.password }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="mailRelay.relay.password" name="password" ng-required="usesPasswordAuth(mailRelay.relay.provider)" password-reveal>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="mailRelayForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<button class="btn btn-primary pull-right" ng-click="mailRelay.submit()" ng-disabled="(usesExternalServer(mailRelay.preset.provider) && (!mailRelayForm.$dirty || mailRelayForm.$invalid)) || mailRelay.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailRelay.busy"></i> {{ 'email.outbound.mailRelay.saveAction' | tr }}</button>
|
||||
|
||||
<span class="has-error text-center" ng-show="mailRelay.error">{{ mailRelay.error }}</span>
|
||||
<span class="text-success text-center text-bold" ng-show="mailRelay.success">{{ 'email.outbound.mailRelay.saveSuccess' | tr }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="mailRelay.preset.spfDoc">
|
||||
<br/>
|
||||
<div class="col-md-12">
|
||||
<span class="text-info" ng-bind-html="'email.outbound.mailRelay.spfDocInfo' | tr:{ name: mailRelay.preset.name, spfDocsLink: mailRelay.preset.spfDoc }"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="'settings'" select="setView('settings')" heading="{{ 'email.settings.tabTitle' | tr }}">
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<h4>{{ 'email.masquerading.title' | tr }}</h4>
|
||||
<p ng-bind-html=" 'email.masquerading.description' | tr "></p>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="pull-right" ng-class="domain.mailConfig.mailFromValidation ? 'btn btn-danger' : 'btn btn-primary'" ng-disabled="mailFromValidation.busy" ng-click="mailFromValidation.submit()">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="mailFromValidation.busy"></i> {{ domain.mailConfig.mailFromValidation ? ('email.masquerading.enableAction' | tr) : ('email.masquerading.disableAction' | tr) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
<h4>{{ 'email.signature.title' | tr }}</h4>
|
||||
<p ng-bind-html=" 'email.signature.description' | tr "></p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form role="form" name="bannerForm" ng-submit="banner.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="control-label" style="width: 100%">{{ 'email.signature.plainTextFormat' | tr }}</label>
|
||||
<textarea ng-model="banner.text" class="form-control" rows="4"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" style="width: 100%">{{ 'email.signature.htmlFormat' | tr }}</label>
|
||||
<textarea ng-model="banner.html" class="form-control" rows="4"></textarea>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="banner.$invalid || banner.busy"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="banner.submit()" ng-disabled="banner.$invalid || banner.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="banner.busy"></i> {{ 'email.signature.saveAction' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="'status'" ng-if="user.isAtLeastAdmin" select="setView('status')" heading="{{ 'email.status.tabTitle' | tr }}">
|
||||
<!-- nothing to show if incoming mail is disabled and using a relay -->
|
||||
<div class="card card-large" style="margin-bottom: 15px;" ng-hide="!domain.mailConfig.enabled && domain.mailConfig.relay.provider !== 'cloudron-smtp'">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4>{{ 'email.dnsStatus.title' | tr }}
|
||||
<button class="btn btn-xs btn-primary btn-outline pull-right" ng-click="incomingEmail.setDnsRecords()">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="incomingEmail.setupDnsBusy"></i> {{ 'email.dnsStatus.reSetupAction' | tr }}
|
||||
</button>
|
||||
</h4>
|
||||
<span ng-bind-html="'email.dnsStatus.description' | tr:{ emailDnsDocsLink:'https://docs.cloudron.io/email/#dns-records'}"></span>
|
||||
<br/>
|
||||
<br/>
|
||||
<div ng-repeat="record in expectedDnsRecordsTypes">
|
||||
<div class="row" ng-if="expectedDnsRecords[record.value].expected">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-hide="refreshBusy" ng-class="expectedDnsRecords[record.value].status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_{{ record.value }}">{{ record.name }} record</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="refreshStatus()" ng-disabled="refreshBusy" ng-show="!expectedDnsRecords[record.value].status"><i class="fa fa-sync-alt" ng-class="{ 'fa-pulse': refreshBusy }"></i></button>
|
||||
</p>
|
||||
<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="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>
|
||||
<p style="overflow: auto; white-space: nowrap;">{{ 'email.dnsStatus.expected' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].expected }}</tt></b></p>
|
||||
<p style="overflow: auto; white-space: nowrap;">{{ 'email.dnsStatus.current' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].value ? expectedDnsRecords[record.value].value : ('['+('email.dnsStatus.recordNotSet' | tr)+']') }}</tt></b></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-bottom: 15px;" ng-if="domain.mailConfig.relay.provider !== 'noop'">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4>{{ 'email.smtpStatus.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#smtp-status" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></h4>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-hide="refreshBusy" ng-class="domain.mailStatus.relay.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_outbound_smtp">
|
||||
{{ domain.mailConfig.relay.provider === 'cloudron-smtp' ? ('email.smtpStatus.outboudDirect' | tr) : ('email.smtpStatus.outboudRelay' | tr) }}
|
||||
</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="refreshStatus()" ng-disabled="refreshBusy" ng-show="!domain.mailStatus.relay.status"><i class="fa fa-sync-alt" ng-class="{ 'fa-pulse': refreshBusy }"></i></button>
|
||||
</p>
|
||||
<div id="collapse_outbound_smtp" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<p><b> {{ domain.mailStatus.relay.value }} </b> </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="domain.mailConfig.relay.provider === 'cloudron-smtp'">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-hide="refreshBusy" ng-class="domain.mailStatus.rbl.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_rbl">{{ 'email.smtpStatus.blacklistCheck' | tr }}</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="refreshStatus()" ng-disabled="refreshBusy" ng-show="!domain.mailStatus.rbl.status"><i class="fa fa-sync-alt" ng-class="{ 'fa-pulse': refreshBusy }"></i></button>
|
||||
</p>
|
||||
<div id="collapse_rbl" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<div ng-show="domain.mailStatus.rbl.servers.length" ng-bind-html="'email.smtpStatus.blacklisted' | tr:{ ip: domain.mailStatus.rbl.ip }"></div>
|
||||
<div ng-hide="domain.mailStatus.rbl.servers.length" ng-bind-html="'email.smtpStatus.notBlacklisted' | tr:{ ip: domain.mailStatus.rbl.ip }"></div>
|
||||
<div ng-repeat="server in domain.mailStatus.rbl.servers">
|
||||
<a ng-href="{{server.site}}" target="_blank">{{ server.name }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
</div>
|
||||
@@ -1,100 +0,0 @@
|
||||
<div>
|
||||
<a href="/#/email" class="back-to-view-link"><i class="fas fa-arrow-left"></i> {{ 'email.backAction' | tr }}</a>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<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="#/emails-queue">{{ 'emails.action.queue' | tr }}</a>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="maillog-filter">
|
||||
<input class="form-control" style="width: 200px;" placeholder="{{ 'main.searchPlaceholder' | tr }}" type="text" ng-model="activity.search" ng-model-options="{ debounce: 1000 }" ng-change="activity.updateFilter(true)" />
|
||||
<multiselect ng-model="activity.selectedTypes" ms-header="{{ 'emails.typeFilterHeader' | tr }}" options="a.name for a in activityTypes" data-multiple="true" ng-change="activity.updateFilter(true)" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<select class="form-control" ng-model="activity.pageItems" ng-options="a.name for a in pageItemCount" ng-change="activity.updateFilter(true)"></select>
|
||||
</div>
|
||||
<div class="pagination pull-right">
|
||||
<button class="btn btn-default btn-outline" ng-click="activity.refresh()"><i class="fas fa-sync-alt" ng-class="{ 'fa-spin': busyRefresh }"></i></button>
|
||||
<button class="btn btn-default btn-outline" ng-click="activity.showPrevPage()" ng-disabled="activity.busy || activity.currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
<button class="btn btn-default btn-outline" ng-click="activity.showNextPage()" ng-disabled="activity.busy || activity.perPage > activity.eventLogs.length">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="card card-block" style="max-width: 100%">
|
||||
<div>
|
||||
<center ng-show="activity.busy"><h2><i class="fa fa-circle-notch fa-spin"></i></h2></center>
|
||||
<table ng-hide="activity.busy" class="table table-hover" style="margin: 0; table-layout:fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 5%"><!-- Icon --></th>
|
||||
<th style="width: 15%">{{ 'emails.eventlog.time' | tr }}</th>
|
||||
<th style="width: 25%">{{ 'emails.eventlog.mailFrom' | tr }}</th>
|
||||
<th style="width: 25%">{{ 'emails.eventlog.rcptTo' | tr }}</th>
|
||||
<th style="width: 30%">{{ 'emails.eventlog.details' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ng-hide="activity.eventLogs.length">
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">
|
||||
<br>
|
||||
<br>
|
||||
{{ 'emails.eventlog.empty' | tr }}
|
||||
<br>
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody ng-show="activity.eventLogs.length" ng-repeat="eventlog in activity.eventLogs">
|
||||
<tr ng-click="activity.showEventLogDetails(eventlog)" class="hand">
|
||||
<td class="no-wrap">
|
||||
<i class="fas fa-arrow-circle-left" ng-show="eventlog.type === 'delivered'" uib-tooltip="{{ 'emails.eventlog.type.outgoing' | tr }}"></i>
|
||||
<i class="fas fa-history" ng-show="eventlog.type === 'deferred'" uib-tooltip="{{ 'emails.eventlog.type.deferred' | tr }}"></i>
|
||||
<i class="fas fa-arrow-circle-right" ng-show="eventlog.type === 'received'" uib-tooltip="{{ 'emails.eventlog.type.incoming' | tr }}"></i>
|
||||
<i class="fas fa-align-justify" ng-show="eventlog.type === 'queued' && eventlog.spamStatus.indexOf('Yes,') !== 0" uib-tooltip="{{ 'emails.eventlog.type.queued' | tr }}"></i>
|
||||
<i class="fas fa-trash" ng-show="eventlog.type === 'queued' && eventlog.spamStatus.indexOf('Yes,') === 0" uib-tooltip="{{ 'emails.eventlog.type.queued' | tr }}"></i>
|
||||
<i class="fas fa-minus-circle" ng-show="eventlog.type === 'denied'" uib-tooltip="{{ 'emails.eventlog.type.denied' | tr }}"></i>
|
||||
<i class="fas fa-hand-paper" ng-show="eventlog.type === 'bounce'" uib-tooltip="{{ 'emails.eventlog.type.bounce' | tr }}"></i>
|
||||
<i class="fas fa-filter" ng-show="eventlog.type === 'spam-learn'" uib-tooltip="{{ 'emails.eventlog.type.spamFilterTrained' | tr }}"></i>
|
||||
<i class="fas fa-fill-drip" ng-show="eventlog.type === 'quota'" uib-tooltip="{{ 'emails.eventlog.type.quota' | tr }}"></i>
|
||||
</td>
|
||||
<td class="no-wrap"><span uib-tooltip="{{ eventlog.ts | prettyLongDate }}" class="arrow">{{ eventlog.ts | prettyDate }}</span></td>
|
||||
<td class="elide-table-cell">{{ (eventlog.mailFrom | prettyEmailAddresses) || '-' }}</td>
|
||||
<td class="elide-table-cell">{{ (eventlog.rcptTo | prettyEmailAddresses) || eventlog.mailbox || '-' }}</td>
|
||||
<td>
|
||||
<span ng-show="eventlog.type === 'bounce'">{{ 'emails.eventlog.type.bounceInfo' | tr }}. {{ eventlog.message || eventlog.reason }}</span>
|
||||
<span ng-show="eventlog.type === 'deferred'">{{ 'emails.eventlog.type.deferredInfo' | tr: { delay:eventlog.delay } }}. {{ eventlog.message || eventlog.reason }} </span>
|
||||
<span ng-show="eventlog.type === 'queued'">
|
||||
<span ng-show="eventlog.direction === 'inbound'">{{ 'emails.eventlog.type.inboundInfo' | tr }}</span>
|
||||
<span ng-show="eventlog.direction === 'outbound'">{{ 'emails.eventlog.type.outboundInfo' | tr }}</span>
|
||||
</span>
|
||||
<span ng-show="eventlog.type === 'received'">{{ 'emails.eventlog.type.receivedInfo' | tr }}</span>
|
||||
<span ng-show="eventlog.type === 'delivered'">{{ 'emails.eventlog.type.deliveredInfo' | tr }}</span>
|
||||
<span ng-show="eventlog.type === 'denied'">{{ 'emails.eventlog.type.deniedInfo' | tr }}. {{ eventlog.message || eventlog.reason }} </span>
|
||||
<span ng-show="eventlog.type === 'spam-learn'">{{ 'emails.eventlog.type.spamFilterTrainedInfo' | tr }}</span>
|
||||
<span ng-show="eventlog.type === 'quota'">
|
||||
<span ng-show="eventlog.quotaPercent > 0">{{ 'emails.eventlog.type.overQuotaInfo' | tr: { mailbox: eventlog.mailbox, quotaPercent: eventlog.quotaPercent } }}</span>
|
||||
<span ng-show="eventlog.quotaPercent < 0">{{ 'emails.eventlog.type.underQuotaInfo' | tr: { mailbox: eventlog.mailbox, quotaPercent: -eventlog.quotaPercent } }}</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="activity.activeEventLog === eventlog">
|
||||
<td colspan="6">
|
||||
<pre class="eventlog-details">{{ eventlog | json }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,83 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global $ */
|
||||
/* 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('/'); });
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
|
||||
$scope.pageItemCount = [
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 20 }), value: 20 },
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 50 }), value: 50 },
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 100 }), value: 100 }
|
||||
];
|
||||
|
||||
$scope.activityTypes = [
|
||||
{ name: 'Bounce', value: 'bounce' },
|
||||
{ name: 'Deferred', value: 'deferred' },
|
||||
{ name: 'Delivered', value: 'delivered' },
|
||||
{ name: 'Denied', value: 'denied' },
|
||||
{ name: 'Queued', value: 'queued' },
|
||||
{ name: 'Quota', value: 'quota' },
|
||||
{ name: 'Received', value: 'received' },
|
||||
{ name: 'Spam', value: 'spam' },
|
||||
];
|
||||
|
||||
$scope.activity = {
|
||||
busy: true,
|
||||
eventLogs: [],
|
||||
activeEventLog: null,
|
||||
currentPage: 1,
|
||||
perPage: 20,
|
||||
pageItems: $scope.pageItemCount[0],
|
||||
selectedTypes: [],
|
||||
search: '',
|
||||
|
||||
refresh: function () {
|
||||
$scope.activity.busy = true;
|
||||
|
||||
var types = $scope.activity.selectedTypes.map(function (a) { return a.value; }).join(',');
|
||||
|
||||
Client.getMailEventLogs($scope.activity.search, types, $scope.activity.currentPage, $scope.activity.pageItems.value, function (error, result) {
|
||||
if (error) return console.error('Failed to fetch mail eventlogs.', error);
|
||||
|
||||
$scope.activity.busy = false;
|
||||
|
||||
$scope.activity.eventLogs = result;
|
||||
});
|
||||
},
|
||||
|
||||
showNextPage: function () {
|
||||
$scope.activity.currentPage++;
|
||||
$scope.activity.refresh();
|
||||
},
|
||||
|
||||
showPrevPage: function () {
|
||||
if ($scope.activity.currentPage > 1) $scope.activity.currentPage--;
|
||||
else $scope.activity.currentPage = 1;
|
||||
$scope.activity.refresh();
|
||||
},
|
||||
|
||||
showEventLogDetails: function (eventLog) {
|
||||
if ($scope.activity.activeEventLog === eventLog) $scope.activity.activeEventLog = null;
|
||||
else $scope.activity.activeEventLog = eventLog;
|
||||
},
|
||||
|
||||
updateFilter: function (fresh) {
|
||||
if (fresh) $scope.activity.currentPage = 1;
|
||||
$scope.activity.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.ready = true;
|
||||
|
||||
$scope.activity.refresh();
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,81 +0,0 @@
|
||||
<div>
|
||||
<a href="/#/email" class="back-to-view-link"><i class="fas fa-arrow-left"></i> {{ 'email.backAction' | tr }}</a>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<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>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="maillog-filter">
|
||||
<input class="form-control" style="width: 200px;" placeholder="{{ 'main.searchPlaceholder' | tr }}" type="text" ng-model="queue.search" ng-model-options="{ debounce: 1000 }" ng-change="queue.updateFilter(true)" />
|
||||
<select class="form-control" ng-model="queue.pageItems" ng-options="a.name for a in pageItemCount" ng-change="queue.updateFilter(true)"></select>
|
||||
</div>
|
||||
<div class="pagination pull-right">
|
||||
<button class="btn btn-default btn-outline" ng-click="queue.reload()"><i class="fas fa-sync-alt" ng-class="{ 'fa-spin': queue.busyRefresh }"></i></button>
|
||||
<button class="btn btn-default btn-outline" ng-click="queue.showPrevPage()" ng-disabled="queue.busy || queue.currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
<button class="btn btn-default btn-outline" ng-click="queue.showNextPage()" ng-disabled="queue.busy || queue.perPage > queue.items.length">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="card card-block" style="max-width: 100%">
|
||||
<div>
|
||||
<center ng-show="queue.busy"><h2><i class="fa fa-circle-notch fa-spin"></i></h2></center>
|
||||
<table ng-hide="queue.busy" class="table table-hover" style="margin: 0; table-layout:fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 15%">{{ 'emails.queue.queueTime' | tr }}</th>
|
||||
<th style="width: 25%">{{ 'emails.queue.mailFrom' | tr }}</th>
|
||||
<th style="width: 25%">{{ 'emails.queue.rcptTo' | tr }}</th>
|
||||
<th style="width: 30%">{{ 'emails.queue.details' | tr }}</th>
|
||||
<th class="text-right" style="width: 5%">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ng-hide="queue.items.length">
|
||||
<tr>
|
||||
<td colspan="5" class="text-center">
|
||||
<br>
|
||||
<br>
|
||||
{{ 'emails.queue.empty' | tr }}
|
||||
<br>
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody ng-show="queue.items.length" ng-repeat="item in queue.items">
|
||||
<tr ng-click="queue.showItemDetails(item)" class="hand">
|
||||
<td class="no-wrap"><span uib-tooltip="{{ item.queueTime | prettyLongDate }}" class="arrow">{{ item.queueTime | prettyDate }}</span></td>
|
||||
<td class="elide-table-cell">{{ (item.mailFrom | prettyEmailAddresses) || '-' }}</td>
|
||||
<td class="elide-table-cell">{{ (item.rcptTo | prettyEmailAddresses) || '-' }}</td>
|
||||
<td class="elide-table-cell">
|
||||
<span ng-show="item.queueType === 'delivery'">Delivering</span>
|
||||
<span ng-show="item.queueType === 'tempfail'">Retrying in {{ item.nextAttemptTime | prettyFutureDate }}. {{ item.attempts+1 }} attempt(s) so far.</span>
|
||||
<span ng-show="item.queueType === 'load'">Loading</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap">
|
||||
<!-- resend is broken in haraka -->
|
||||
<!-- <button class="btn btn-xs btn-default" ng-click="queue.resend(item)" uib-tooltip="{{ 'emails.queue.resendTooltip' | tr }}"><i class="fa fa-retweet"></i></button> -->
|
||||
<button class="btn btn-xs btn-default" ng-show="item.queueType === 'tempfail'" ng-click="$event.stopPropagation(); queue.discard(item)" uib-tooltip="{{ 'emails.queue.discardTooltip' | tr }}"><i class="fa fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="queue.activeItem === item">
|
||||
<td colspan="6">
|
||||
<pre class="item-details">{{ item | json }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,95 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global $ */
|
||||
/* 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('/'); });
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
|
||||
$scope.pageItemCount = [
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 20 }), value: 20 },
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 50 }), value: 50 },
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 100 }), value: 100 }
|
||||
];
|
||||
|
||||
$scope.queue = {
|
||||
busy: true,
|
||||
busyRefresh: false,
|
||||
items: [],
|
||||
activeItem: null,
|
||||
currentPage: 1,
|
||||
perPage: 20,
|
||||
pageItems: $scope.pageItemCount[0],
|
||||
search: '',
|
||||
|
||||
refresh: function (showBusy, callback) {
|
||||
if (showBusy) $scope.queue.busy = true;
|
||||
|
||||
Client.listMailQueue($scope.queue.search, $scope.queue.currentPage, $scope.queue.pageItems.value, function (error, result) {
|
||||
if (showBusy) $scope.queue.busy = false;
|
||||
|
||||
if (error) {
|
||||
console.error('Failed to fetch mail eventlogs.', error);
|
||||
} else {
|
||||
$scope.queue.items = result;
|
||||
}
|
||||
|
||||
if (callback) callback();
|
||||
});
|
||||
},
|
||||
|
||||
reload: function () {
|
||||
$scope.queue.busyRefresh = true;
|
||||
$scope.queue.refresh(true, function () {
|
||||
$scope.queue.busyRefresh = false;
|
||||
});
|
||||
},
|
||||
|
||||
resend: function (item) {
|
||||
Client.resendMailQueueItem(item.file, function (error) {
|
||||
if (error) return console.error('Failed to retry item.', error);
|
||||
$scope.queue.refresh(false);
|
||||
});
|
||||
},
|
||||
|
||||
discard: function (item) {
|
||||
Client.delMailQueueItem(item.file, function (error) {
|
||||
if (error) return console.error('Failed to discard item.', error);
|
||||
$scope.queue.refresh(false);
|
||||
});
|
||||
},
|
||||
|
||||
showNextPage: function () {
|
||||
$scope.queue.currentPage++;
|
||||
$scope.queue.refresh(true);
|
||||
},
|
||||
|
||||
showPrevPage: function () {
|
||||
if ($scope.queue.currentPage > 1) $scope.queue.currentPage--;
|
||||
else $scope.queue.currentPage = 1;
|
||||
$scope.queue.refresh(true);
|
||||
},
|
||||
|
||||
showItemDetails: function (item) {
|
||||
if ($scope.queue.activeItem === item) $scope.queue.activeItem = null;
|
||||
else $scope.queue.activeItem = item;
|
||||
},
|
||||
|
||||
updateFilter: function (fresh) {
|
||||
if (fresh) $scope.queue.currentPage = 1;
|
||||
$scope.queue.refresh(false);
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.ready = true;
|
||||
|
||||
$scope.queue.refresh(true);
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,364 +0,0 @@
|
||||
<!-- Modal change max email size -->
|
||||
<div class="modal fade" id="maxEmailSizeChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.changeMailSizeDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-bind-html=" 'emails.changeMailSizeDialog.description' | tr "></div>
|
||||
<br>
|
||||
<form name="maxEmailSizeChangeForm" role="form" novalidate ng-submit="maxEmailSize.submit()" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="maxEmailSizeInput">{{ 'emails.changeMailSizeDialog.size' | tr }} <b>{{ maxEmailSize.size | prettyDecimalSize }}</b></label>
|
||||
<input type="range" id="maxEmailSizeInput" ng-model="maxEmailSize.size" step="1000000" min="1000000" max="1000000000" />
|
||||
</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-click="maxEmailSize.submit()" ng-disabled="maxEmailSize.size === maxEmailSize.currentSize"><i class="fa fa-circle-notch fa-spin" ng-show="maxEmailSize.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change virtual all mail -->
|
||||
<div class="modal fade" id="virtualAllMailChangeModal" 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>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-bind-html=" 'emails.changeVirtualAllMailDialog.description' | tr "></div>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal solr config -->
|
||||
<div class="modal fade" id="solrConfigModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.solrConfig.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html=" 'emails.solrConfig.description' | tr "></p>
|
||||
<!-- only show this when user is trying to enable -->
|
||||
<p class="has-error" ng-show="!solrConfig.currentConfig.enabled && !solrConfig.enoughMemory">{{ 'emails.solrConfig.notEnoughMemory' | tr }}</p>
|
||||
</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-hide="solrConfig.currentConfig.enabled" ng-click="solrConfig.submit(true)" ng-disabled="(!solrConfig.currentConfig.enabled && !solrConfig.enoughMemory) || solrConfig.busy"><i class="fa fa-circle-notch fa-spin" ng-show="solrConfig.busy"></i> {{ 'main.enableAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-show="solrConfig.currentConfig.enabled" ng-click="solrConfig.submit(false)" ng-disabled="solrConfig.busy"><i class="fa fa-circle-notch fa-spin" ng-show="solrConfig.busy"></i> {{ 'main.disableAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change acl -->
|
||||
<div class="modal fade" id="aclChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.aclDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="aclChangeForm" role="form" novalidate ng-submit="acl.submit()" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'emails.aclDialog.dnsblZones' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#dnsbl" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<p class="small">{{ 'emails.aclDialog.dnsblZonesInfo' | tr }}</p>
|
||||
<div class="has-error" ng-show="acl.error.dnsblZones">{{ acl.error.dnsblZones }}</div>
|
||||
<textarea ng-model="acl.dnsblZones" placeholder="{{ 'emails.aclDialog.dnsblZonesPlaceholder' | tr }}" name="dnsblZones" class="form-control" ng-class="{ 'has-error': !aclChangeForm.dnsblZones.$dirty && acl.error.dnsblZones }" 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-click="acl.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="acl.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change spam config -->
|
||||
<div class="modal fade" id="spamConfigChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.spamFilterDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="spamConfigChangeForm" role="form" novalidate ng-submit="spamConfig.submit()" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'emails.spamFilterDialog.blacklisteAddresses' | tr }}</label>
|
||||
<p class="small">{{ 'emails.spamFilterDialog.blacklisteAddressesInfo' | tr }}</p>
|
||||
<div class="has-error" ng-show="spamConfig.error.blocklist">{{ spamConfig.error.blocklist }}</div>
|
||||
<textarea ng-model="spamConfig.blocklist" placeholder="{{ 'emails.spamFilterDialog.blacklisteAddressesPlaceholder' | tr }}" name="blocklist" class="form-control" ng-class="{ 'has-error': !spamConfigChangeForm.blocklist.$dirty && spamConfig.error.blocklist }" rows="4"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'emails.spamFilterDialog.customRules' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#custom-spam-filtering-rules" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<div class="has-error" ng-show="spamConfig.error.config">{{ spamConfig.error.config }}</div>
|
||||
<textarea ng-model="spamConfig.config" placeholder="{{ 'emails.spamFilterDialog.customRulesPlaceholder' | tr }}" class="form-control" name="config" ng-class="{ 'has-error': !spamConfigChangeForm.config.$dirty && spamConfig.error.config }" 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-click="spamConfig.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="spamConfig.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test email -->
|
||||
<div class="modal fade" id="testEmailModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.testMailDialog.title' | tr:{ domain: testEmail.domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="testEmailForm" role="form" novalidate ng-submit="testEmail.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="testEmail.error">{{ testEmail.error.generic }}</p>
|
||||
<p ng-bind-html="'emails.testMailDialog.description' | tr:{ domain: testEmail.domain.domain }"></p>
|
||||
<br/>
|
||||
<div class="form-group" ng-class="{ 'has-error': testEmail.error.key }">
|
||||
<label class="control-label" for="inputTestEmailKey">{{ 'emails.testMailDialog.mailTo' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="testEmail.mailTo" id="inputTestMailTo" name="mailTo" ng-disabled="testEmail.busy" placeholder="{{ 'emails.testMailDialog.mailToPlaceholder' | tr }}" autofocus>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="testEmailForm.$invalid"/>
|
||||
</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="testEmail.submit()" ng-disabled="testEmail.$invalid || testEmail.busy"><i class="fa fa-circle-notch fa-spin" ng-show="testEmail.busy"></i> {{ 'emails.testMailDialog.sendAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="text-left">
|
||||
<h1>
|
||||
{{ 'emails.title' | tr }}
|
||||
|
||||
<div class="pull-right">
|
||||
<a class="btn btn-default" ng-show="user.isAtLeastAdmin" href="#/emails-queue">{{ 'emails.action.queue' | tr }}</a>
|
||||
<a class="btn btn-default" ng-show="user.isAtLeastAdmin" href="#/emails-eventlog">{{ 'eventlog.title' | tr }}</a>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- domain listing -->
|
||||
<div class="text-left">
|
||||
<h3>{{ 'emails.domains.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row ng-hide" ng-hide="ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="ready">
|
||||
<div class="col-xs-12">
|
||||
<table class="table table-hover" style="margin: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 5%"></th>
|
||||
<th style="width: 30%">{{ 'emails.domains.domain' | tr }}</th>
|
||||
<th style="width: 60%">{{ 'emails.domains.config' | tr }}</th>
|
||||
<th style="width: 10%">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="domain in domains">
|
||||
<td>
|
||||
<i class="fa fa-circle" ng-class="{ 'status-active': domain.statusOk, 'status-error': !domain.statusOk }" ng-show="domain.status"></i>
|
||||
<i class="fa fa-circle-notch fa-spin" ng-hide="domain.status"></i>
|
||||
</td>
|
||||
<td class="elide-table-cell no-padding">
|
||||
<a href="/#/email/{{ domain.domain }}" class="email-domain-list-item">{{ domain.domain }}</a>
|
||||
</td>
|
||||
<td class="elide-table-cell no-padding">
|
||||
<a href="/#/email/{{ domain.domain }}" class="email-domain-list-item">
|
||||
<span ng-switch on="domain.loading">
|
||||
<span ng-switch-when="true">{{ 'main.loadingPlaceholder' | tr }} ...</span>
|
||||
<span ng-switch-default>
|
||||
<span ng-switch on="domain.inbound">
|
||||
<span ng-switch-when="true">
|
||||
<span ng-show="domain.loadingUsage">{{ 'emails.domains.stats' | tr:{ mailboxCount: domain.mailboxCount } }} {{ 'main.loadingPlaceholder' | tr }} ... </span>
|
||||
<span ng-show="!domain.loadingUsage">{{ 'emails.domains.stats' | tr:{ mailboxCount: domain.mailboxCount, usage: (domain.usage | prettyDecimalSize) } }}</span>
|
||||
</span>
|
||||
<span ng-switch-default>
|
||||
<span ng-show="domain.outbound">{{ 'emails.domains.outbound' | tr }}</span>
|
||||
<span ng-show="!domain.outbound">{{ 'emails.domains.disabled' | tr }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right no-wrap">
|
||||
<button class="btn btn-xs btn-default" ng-click="testEmail.show(domain)" uib-tooltip="{{ 'emails.domains.testEmailTooltip' | tr }}"><i class="fa fa-paper-plane"></i></button>
|
||||
<a href="/#/email/{{ domain.domain }}" class="btn btn-xs btn-default"><i class="fa fa-pencil-alt"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- mailbox sharing -->
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastAdmin">
|
||||
<h3>{{ 'emails.mailboxSharing.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-show="user.isAtLeastAdmin" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>{{ 'emails.mailboxSharing.description' | tr }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2" style="padding-top: 12px;">
|
||||
<i class="fa fa-circle" ng-class="{ 'status-active': mailboxSharing.enabled, 'status-inactive': !mailboxSharing.enabled }"></i> {{ mailboxSharing.enabled ? 'main.statusEnabled' : 'main.statusDisabled' | tr }}
|
||||
</div>
|
||||
<div class="col-md-10 text-right">
|
||||
<button class="btn" ng-class="{ 'btn-danger': mailboxSharing.enabled, 'btn-primary': !mailboxSharing.enabled }" ng-click="mailboxSharing.submit()" ng-disabled="mailboxSharing.enable === mailboxSharing.enabled"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxSharing.busy"></i> {{ mailboxSharing.enabled ? ('main.disableAction' | tr) : ('main.enableAction' | tr) }} </button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- server location -->
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastAdmin">
|
||||
<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" ng-show="user.isAtLeastAdmin">
|
||||
<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.isAtLeastAdmin">
|
||||
<h3>{{ 'emails.settings.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-show="user.isAtLeastAdmin" 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.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>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ 'emails.settings.aclOverview' | tr:{ dnsblZonesCount: acl.dnsblZonesCount } }} <a href="" ng-click="acl.show()"><i class="fa fa-edit text-small"></i></a></span>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'emails.settings.spamFilter' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ 'emails.settings.spamFilterOverview' | tr:{ blacklistCount: spamConfig.acl.blocklist.length } }} <a href="" ng-click="spamConfig.show()"><i class="fa fa-edit text-small"></i></a></span>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'emails.settings.solrFts' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right" ng-hide="solrConfig.currentConfig">
|
||||
<i class="fa fa-circle-notch fa-spin"></i>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right" ng-show="solrConfig.currentConfig">
|
||||
<span ng-show="solrConfig.currentConfig.enabled">
|
||||
{{ 'emails.settings.solrEnabled' | tr }}
|
||||
<span ng-show="solrConfig.running">/ {{ 'emails.settings.solrRunning' | tr }}</span>
|
||||
<span ng-hide="solrConfig.running">/ {{ 'emails.settings.solrNotRunning' | tr }}</span>
|
||||
</span>
|
||||
<span ng-hide="solrConfig.currentConfig.enabled">{{ 'emails.settings.solrDisabled' | tr }}</span>
|
||||
<a href="" ng-click="solrConfig.show()"><i class="fa fa-edit text-small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,521 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global $, angular, TASK_TYPES */
|
||||
/* global async */
|
||||
|
||||
angular.module('Application').controller('EmailsController', ['$scope', '$location', '$translate', '$timeout', 'Client', function ($scope, $location, $translate, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastMailManager) $location.path('/'); });
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.domains = [];
|
||||
|
||||
$scope.mailLocation = {
|
||||
busy: false,
|
||||
percent: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
|
||||
currentLocation: { domain: null, subdomain: '' },
|
||||
domain: null,
|
||||
subdomain: '',
|
||||
tasks: [],
|
||||
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_CHANGE_MAIL_LOCATION, function (error, tasks) {
|
||||
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();
|
||||
});
|
||||
},
|
||||
|
||||
stop: function () {
|
||||
Client.stopTask($scope.mailLocation.tasks[0].id, function (error) {
|
||||
if (error) console.error(error);
|
||||
$scope.mailLocation.busy = false;
|
||||
$scope.mailLocation.refreshTasks();
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
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.refreshTasks();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
var taskId = $scope.mailLocation.tasks[0].id;
|
||||
|
||||
Client.getTask(taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.mailLocation.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.mailLocation.busy = false;
|
||||
$scope.mailLocation.message = '';
|
||||
$scope.mailLocation.percent = 100;
|
||||
$scope.mailLocation.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
$scope.mailLocation.refreshTasks(); // update the tasks list dropdown
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.mailLocation.busy = true;
|
||||
$scope.mailLocation.percent = data.percent;
|
||||
$scope.mailLocation.message = data.message;
|
||||
window.setTimeout($scope.mailLocation.updateStatus, 1000);
|
||||
});
|
||||
},
|
||||
|
||||
change: 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) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.mailLocation.errorMessage = error.message;
|
||||
$scope.mailLocation.busy = false;
|
||||
} else {
|
||||
$scope.mailLocation.refreshTasks();
|
||||
}
|
||||
|
||||
Client.refreshConfig(); // update config.mailFqdn
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.maxEmailSize = {
|
||||
busy: false,
|
||||
error: null,
|
||||
size: 0,
|
||||
currentSize: 0,
|
||||
|
||||
refresh: function () {
|
||||
Client.getMaxEmailSize(function (error, size) {
|
||||
if (error) return console.error('Failed to get max email size', error);
|
||||
|
||||
$scope.maxEmailSize.currentSize = size;
|
||||
});
|
||||
},
|
||||
|
||||
show: function() {
|
||||
$scope.maxEmailSize.busy = false;
|
||||
$scope.maxEmailSize.error = null;
|
||||
$scope.maxEmailSize.size = $scope.maxEmailSize.currentSize;
|
||||
|
||||
$scope.maxEmailSizeChangeForm.$setUntouched();
|
||||
$scope.maxEmailSizeChangeForm.$setPristine();
|
||||
|
||||
$('#maxEmailSizeChangeModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.maxEmailSize.busy = true;
|
||||
|
||||
Client.setMaxEmailSize(parseInt($scope.maxEmailSize.size), function (error) {
|
||||
$scope.maxEmailSize.busy = false;
|
||||
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.maxEmailSize.currentSize = $scope.maxEmailSize.size;
|
||||
|
||||
$('#maxEmailSizeChangeModal').modal('hide');
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
$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,
|
||||
enabled: null, // null means we have not refreshed yet
|
||||
|
||||
refresh: function () {
|
||||
Client.getMailboxSharing(function (error, enabled) {
|
||||
if (error) return console.error('Failed to get mailbox sharing', error);
|
||||
|
||||
$scope.mailboxSharing.enabled = enabled;
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.mailboxSharing.busy = true;
|
||||
|
||||
Client.setMailboxSharing(!$scope.mailboxSharing.enabled, function (error) {
|
||||
// give sometime for mail server to restart
|
||||
$timeout(function () {
|
||||
$scope.mailboxSharing.busy = false;
|
||||
if (error) return console.error(error);
|
||||
$scope.mailboxSharing.enabled = !$scope.mailboxSharing.enabled;
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.solrConfig = {
|
||||
busy: false,
|
||||
error: {},
|
||||
currentConfig: null, // null means not loaded yet
|
||||
enabled: false,
|
||||
running: false,
|
||||
enoughMemory: false,
|
||||
|
||||
refresh: function () {
|
||||
Client.getService('mail', function (error, result) {
|
||||
if (error) return console.log('Error getting status of mail conatiner', error);
|
||||
|
||||
$scope.solrConfig.enoughMemory = result.config.memoryLimit > (1024*1024*1024*2);
|
||||
$scope.solrConfig.running = result.healthcheck && result.healthcheck.solr.status;
|
||||
|
||||
Client.getSolrConfig(function (error, config) {
|
||||
if (error) return console.error('Failed to get solr config', error);
|
||||
|
||||
$scope.solrConfig.currentConfig = config;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
show: function() {
|
||||
$scope.solrConfig.busy = false;
|
||||
$scope.solrConfig.error = null;
|
||||
$scope.solrConfig.enabled = $scope.solrConfig.currentConfig.enabled;
|
||||
|
||||
$('#solrConfigModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function (newState) {
|
||||
$scope.solrConfig.busy = true;
|
||||
|
||||
Client.setSolrConfig(newState, function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$timeout(function () {
|
||||
$scope.solrConfig.busy = false;
|
||||
// FIXME: these values are fake. but cannot get current status from mail server since it might be restarting
|
||||
$scope.solrConfig.currentConfig.enabled = newState;
|
||||
$scope.solrConfig.running = newState;
|
||||
|
||||
$timeout(function () { $scope.solrConfig.refresh(); }, 20000); // get real values after 20 seconds
|
||||
|
||||
$('#solrConfigModal').modal('hide');
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.spamConfig = {
|
||||
busy: false,
|
||||
error: {},
|
||||
acl: { allowlist: [], blocklist: [] },
|
||||
customConfig: '',
|
||||
|
||||
config: '',
|
||||
blocklist: '', // currently, we don't support allowlist because it requires user to understand a bit more of what he is doing
|
||||
|
||||
refresh: function () {
|
||||
Client.getSpamCustomConfig(function (error, config) {
|
||||
if (error) return console.error('Failed to get custom spam config', error);
|
||||
|
||||
$scope.spamConfig.customConfig = config;
|
||||
});
|
||||
|
||||
Client.getSpamAcl(function (error, acl) {
|
||||
if (error) return console.error('Failed to get spam acl', error);
|
||||
|
||||
$scope.spamConfig.acl = acl;
|
||||
});
|
||||
},
|
||||
|
||||
show: function() {
|
||||
$scope.spamConfig.busy = false;
|
||||
$scope.spamConfig.error = {};
|
||||
|
||||
$scope.spamConfig.blocklist = $scope.spamConfig.acl.blocklist.join('\n');
|
||||
$scope.spamConfig.config = $scope.spamConfig.customConfig;
|
||||
|
||||
$scope.spamConfigChangeForm.$setUntouched();
|
||||
$scope.spamConfigChangeForm.$setPristine();
|
||||
|
||||
$('#spamConfigChangeModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.spamConfig.busy = true;
|
||||
$scope.spamConfig.error = {};
|
||||
|
||||
var blocklist = $scope.spamConfig.blocklist.split('\n').filter(function (l) { return l !== ''; });
|
||||
|
||||
Client.setSpamAcl({ blocklist: blocklist, allowlist: [] }, function (error) {
|
||||
if (error) {
|
||||
$scope.spamConfig.busy = false;
|
||||
$scope.spamConfig.error.blocklist = error.message;
|
||||
$scope.spamConfigChangeForm.blocklist.$setPristine();
|
||||
return;
|
||||
}
|
||||
|
||||
Client.setSpamCustomConfig($scope.spamConfig.config, function (error) {
|
||||
if (error) {
|
||||
$scope.spamConfig.busy = false;
|
||||
$scope.spamConfig.error.config = error.message;
|
||||
$scope.spamConfigChangeForm.config.$setPristine();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.spamConfig.busy = false;
|
||||
|
||||
$scope.spamConfig.refresh();
|
||||
|
||||
$('#spamConfigChangeModal').modal('hide');
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
$scope.acl = {
|
||||
busy: false,
|
||||
error: {},
|
||||
|
||||
dnsblZones: '',
|
||||
dnsblZonesCount: 0,
|
||||
|
||||
refresh: function () {
|
||||
Client.getDnsblConfig(function (error, result) {
|
||||
if (error) return console.error('Failed to get email acl', error);
|
||||
|
||||
$scope.acl.dnsblZones = result.zones.join('\n');
|
||||
$scope.acl.dnsblZonesCount = result.zones.length;
|
||||
});
|
||||
},
|
||||
|
||||
show: function() {
|
||||
$scope.acl.busy = false;
|
||||
$scope.acl.error = {};
|
||||
|
||||
$scope.aclChangeForm.$setUntouched();
|
||||
$scope.aclChangeForm.$setPristine();
|
||||
|
||||
$('#aclChangeModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.acl.busy = true;
|
||||
$scope.acl.error = {};
|
||||
|
||||
var zones = $scope.acl.dnsblZones.split('\n').filter(function (l) { return l !== ''; });
|
||||
|
||||
Client.setDnsblConfig(zones, function (error) {
|
||||
if (error) {
|
||||
$scope.acl.busy = false;
|
||||
$scope.acl.error.dnsblZones = error.message;
|
||||
$scope.aclChangeForm.dnsblZones.$setPristine();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.acl.busy = false;
|
||||
|
||||
$scope.acl.refresh();
|
||||
|
||||
$('#aclChangeModal').modal('hide');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
$scope.testEmail = {
|
||||
busy: false,
|
||||
error: {},
|
||||
|
||||
mailTo: '',
|
||||
|
||||
domain: null,
|
||||
|
||||
clearForm: function () {
|
||||
$scope.testEmail.mailTo = '';
|
||||
},
|
||||
|
||||
show: function (domain) {
|
||||
$scope.testEmail.error = {};
|
||||
$scope.testEmail.busy = false;
|
||||
|
||||
$scope.testEmail.domain = domain;
|
||||
$scope.testEmail.mailTo = $scope.user.email;
|
||||
|
||||
$('#testEmailModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.testEmail.error = {};
|
||||
$scope.testEmail.busy = true;
|
||||
|
||||
Client.sendTestMail($scope.testEmail.domain.domain, $scope.testEmail.mailTo, function (error) {
|
||||
$scope.testEmail.busy = false;
|
||||
|
||||
if (error) {
|
||||
$scope.testEmail.error.generic = error.message;
|
||||
console.error(error);
|
||||
$('#inputTestMailTo').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
$('#testEmailModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function refreshMailStatus(domain, done) {
|
||||
Client.getMailStatusForDomain(domain.domain, function (error, result) {
|
||||
if (error) {
|
||||
console.error('Failed to fetch mail status for domain', domain.domain, error);
|
||||
return done();
|
||||
}
|
||||
|
||||
domain.status = result;
|
||||
|
||||
domain.statusOk = Object.keys(result).every(function (k) {
|
||||
if (k === 'dns') return Object.keys(result.dns).every(function (k) { return result.dns[k].status; });
|
||||
|
||||
if (!('status' in result[k])) return true; // if status is not present, the test was not run
|
||||
|
||||
return result[k].status;
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function refreshMailConfig(domain, done) {
|
||||
Client.getMailConfigForDomain(domain.domain, function (error, mailConfig) {
|
||||
if (error) {
|
||||
console.error('Failed to fetch mail config for domain', domain.domain, error);
|
||||
return done();
|
||||
}
|
||||
|
||||
domain.inbound = mailConfig.enabled;
|
||||
domain.outbound = mailConfig.relay.provider !== 'noop';
|
||||
|
||||
// do this even if no outbound since people forget to remove mailboxes
|
||||
Client.getMailboxCount(domain.domain, function (error, count) {
|
||||
if (error) {
|
||||
console.error('Failed to fetch mailboxes for domain', domain.domain, error);
|
||||
return done();
|
||||
}
|
||||
|
||||
domain.mailboxCount = count;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function refreshMailUsage(domain, done) {
|
||||
Client.getMailUsage(domain.domain, function (error, usage) {
|
||||
if (error) {
|
||||
console.error('Failed to fetch usage for domain', domain.domain, error);
|
||||
return done();
|
||||
}
|
||||
|
||||
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; });
|
||||
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function refreshDomainStatuses() {
|
||||
async.each($scope.domains, function (domain, iteratorDone) {
|
||||
async.series([
|
||||
refreshMailStatus.bind(null, domain),
|
||||
refreshMailConfig.bind(null, domain),
|
||||
], function () {
|
||||
domain.loading = false;
|
||||
iteratorDone();
|
||||
});
|
||||
}, function () {
|
||||
// mail usage is loaded separately with a cancellation check. when there are a lot of domains, it runs a long time in background and slows down loading of new views
|
||||
async.eachLimit($scope.domains, 5, function (domain, itemDone) {
|
||||
if ($scope.$$destroyed) return itemDone(); // abort!
|
||||
refreshMailUsage(domain, function () {
|
||||
domain.loadingUsage = false;
|
||||
itemDone();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Client.onReady(function () {
|
||||
Client.getDomains(function (error, domains) {
|
||||
if (error) return console.error('Unable to get domain listing.', error);
|
||||
|
||||
domains.forEach(function (domain) { domain.loading = true; domain.loadingUsage = true; }); // used by ui to show 'loading'
|
||||
$scope.domains = domains;
|
||||
|
||||
$scope.ready = true;
|
||||
|
||||
if ($scope.user.isAtLeastAdmin) {
|
||||
$scope.mailLocation.refresh();
|
||||
$scope.maxEmailSize.refresh();
|
||||
$scope.virtualAllMail.refresh();
|
||||
$scope.mailboxSharing.refresh();
|
||||
$scope.spamConfig.refresh();
|
||||
$scope.solrConfig.refresh();
|
||||
$scope.acl.refresh();
|
||||
}
|
||||
|
||||
refreshDomainStatuses();
|
||||
});
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['testEmailModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find('[autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,151 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
|
||||
angular.module('Application').controller('EventLogController', ['$scope', '$location', '$translate', 'Client', function ($scope, $location, $translate, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
$scope.busy = false;
|
||||
$scope.busyRefresh = false;
|
||||
$scope.eventLogs = [];
|
||||
$scope.activeEventLog = null;
|
||||
|
||||
// TODO sync this with the eventlog filter
|
||||
$scope.actions = [
|
||||
{ name: '-- All app events --', value: 'app.' },
|
||||
{ name: '-- All user events --', value: 'user.' },
|
||||
{ name: 'app.backup', value: 'app.backup' },
|
||||
{ name: 'app.backup.finish', value: 'app.backup.finish' },
|
||||
{ name: 'app.configure', value: 'app.configure' },
|
||||
{ name: 'app.install', value: 'app.install' },
|
||||
{ name: 'app.restore', value: 'app.restore' },
|
||||
{ name: 'app.uninstall', value: 'app.uninstall' },
|
||||
{ name: 'app.update', value: 'app.update' },
|
||||
{ name: 'app.update.finish', value: 'app.update.finish' },
|
||||
{ name: 'app.login', value: 'app.login' },
|
||||
{ name: 'app.oom', value: 'app.oom' },
|
||||
{ name: 'app.down', value: 'app.down' },
|
||||
{ name: 'app.up', value: 'app.up' },
|
||||
{ name: 'app.start', value: 'app.start' },
|
||||
{ name: 'app.stop', value: 'app.stop' },
|
||||
{ name: 'app.restart', value: 'app.restart' },
|
||||
{ name: 'backup.cleanup', value: 'backup.cleanup.start' },
|
||||
{ name: 'backup.cleanup.finish', value: 'backup.cleanup.finish' },
|
||||
{ name: 'backup.finish', value: 'backup.finish' },
|
||||
{ name: 'backup.start', value: 'backup.start' },
|
||||
{ name: 'certificate.new', value: 'certificate.new' },
|
||||
{ name: 'certificate.renew', value: 'certificate.renew' },
|
||||
{ name: 'certificate.cleanup', value: 'certificate.cleanup' },
|
||||
{ name: 'cloudron.activate', value: 'cloudron.activate' },
|
||||
{ name: 'cloudron.provision', value: 'cloudron.provision' },
|
||||
{ name: 'cloudron.restore', value: 'cloudron.restore' },
|
||||
{ name: 'cloudron.start', value: 'cloudron.start' },
|
||||
{ name: 'cloudron.update', value: 'cloudron.update' },
|
||||
{ name: 'cloudron.update.finish', value: 'cloudron.update.finish' },
|
||||
{ name: 'dashboard.domain.update', value: 'dashboard.domain.update' },
|
||||
{ name: 'directoryserver.configure', value: 'directoryserver.configure' },
|
||||
{ name: 'dyndns.update', value: 'dyndns.update' },
|
||||
{ name: 'domain.add', value: 'domain.add' },
|
||||
{ name: 'domain.update', value: 'domain.update' },
|
||||
{ name: 'domain.remove', value: 'domain.remove' },
|
||||
{ name: 'externalldap.configure', value: 'externalldap.configure' },
|
||||
{ name: 'mail.location', value: 'mail.location' },
|
||||
{ name: 'mail.enabled', value: 'mail.enabled' },
|
||||
{ name: 'mail.box.add', value: 'mail.box.add' },
|
||||
{ name: 'mail.box.update', value: 'mail.box.update' },
|
||||
{ name: 'mail.box.remove', value: 'mail.box.remove' },
|
||||
{ name: 'mail.list.add', value: 'mail.list.add' },
|
||||
{ name: 'mail.list.update', value: 'mail.list.update' },
|
||||
{ name: 'mail.list.remove', value: 'mail.list.remove' },
|
||||
{ name: 'service.configure', value: 'service.configure' },
|
||||
{ name: 'service.rebuild', value: 'service.rebuild' },
|
||||
{ name: 'service.restart', value: 'service.restart' },
|
||||
{ name: 'support.ticket', value: 'support.ticket' },
|
||||
{ name: 'support.ssh', value: 'support.ssh' },
|
||||
{ name: 'user.add', value: 'user.add' },
|
||||
{ name: 'user.login', value: 'user.login' },
|
||||
{ name: 'user.login.ghost', value: 'user.login.ghost' },
|
||||
{ name: 'user.logout', value: 'user.logout' },
|
||||
{ name: 'user.remove', value: 'user.remove' },
|
||||
{ name: 'user.transfer', value: 'user.transfer' },
|
||||
{ name: 'user.update', value: 'user.update' },
|
||||
{ name: 'userdirectory.profileconfig.update', value: 'userdirectory.profileconfig.update '},
|
||||
{ name: 'volume.add', value: 'volume.add' },
|
||||
{ name: 'volume.update', value: 'volume.update' },
|
||||
{ name: 'volume.remove', value: 'volume.update' },
|
||||
];
|
||||
|
||||
$scope.pageItemCount = [
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 20 }), value: 20 },
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 50 }), value: 50 },
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 100 }), value: 100 }
|
||||
];
|
||||
|
||||
$scope.currentPage = 1;
|
||||
$scope.pageItems = $scope.pageItemCount[0];
|
||||
$scope.action = '';
|
||||
$scope.selectedActions = [];
|
||||
$scope.search = '';
|
||||
|
||||
function fetchEventLogs(background, callback) {
|
||||
callback = callback || function (error) { if (error) console.error(error); };
|
||||
background = background || false;
|
||||
|
||||
if (!background) $scope.busy = true;
|
||||
|
||||
var actions = $scope.selectedActions.map(function (a) { return a.value; }).join(', ');
|
||||
|
||||
Client.getEventLogs(actions, $scope.search || null, $scope.currentPage, $scope.pageItems.value, function (error, result) {
|
||||
$scope.busy = false;
|
||||
|
||||
if (error) return callback(error);
|
||||
|
||||
$scope.eventLogs = [];
|
||||
result.forEach(function (e) {
|
||||
$scope.eventLogs.push({ raw: e, details: Client.eventLogDetails(e), source: Client.eventLogSource(e) });
|
||||
});
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
$scope.refresh = function () {
|
||||
$scope.busyRefresh = true;
|
||||
|
||||
fetchEventLogs(true, function () {
|
||||
$scope.busyRefresh = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showNextPage = function () {
|
||||
$scope.currentPage++;
|
||||
fetchEventLogs();
|
||||
};
|
||||
|
||||
$scope.showPrevPage = function () {
|
||||
if ($scope.currentPage > 1) $scope.currentPage--;
|
||||
else $scope.currentPage = 1;
|
||||
|
||||
fetchEventLogs();
|
||||
};
|
||||
|
||||
$scope.updateFilter = function (fresh) {
|
||||
if (fresh) $scope.currentPage = 1;
|
||||
fetchEventLogs();
|
||||
};
|
||||
|
||||
$scope.showEventLogDetails = function (eventLog) {
|
||||
if ($scope.activeEventLog === eventLog) $scope.activeEventLog = null;
|
||||
else $scope.activeEventLog = eventLog;
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.search = $location.search().search || ''; // sent from the backups view when app is deleted
|
||||
fetchEventLogs();
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,315 +0,0 @@
|
||||
<!-- Modal sysinfo -->
|
||||
<div class="modal fade" id="sysinfoModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'network.configureIp.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="sysinfoForm" role="form" novalidate ng-submit="sysinfo.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'network.ip.provider' | tr }} <sup><a ng-href="https://docs.cloudron.io/networking/#ipv4" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="sysinfo.newProvider" ng-options="a.value as a.name for a in sysinfoProvider"></select>
|
||||
<p class="has-error" ng-show="sysinfo.error.generic">{{ sysinfo.error.generic }}</p>
|
||||
</div>
|
||||
|
||||
<div ng-show="sysinfo.newProvider === 'generic'">
|
||||
{{ 'network.configureIp.providerGenericDescription' | tr }} <sup><a ng-href="https://ipv4.api.cloudron.io/api/v1/helper/public_ip" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</div>
|
||||
|
||||
<!-- Fixed -->
|
||||
<div class="form-group" ng-show="sysinfo.newProvider === 'fixed'" ng-class="{ 'has-error': (!sysinfoForm.ipv4.$dirty && sysinfo.error.ipv4) }">
|
||||
<label class="control-label">{{ 'network.ipv4.address' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="sysinfo.newIPv4" name="ipv4" ng-disabled="sysinfo.busy" ng-required="sysinfo.newProvider === 'fixed'">
|
||||
<p class="has-error" ng-show="sysinfo.error.ipv4">{{ sysinfo.error.ipv4 }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Network Interface -->
|
||||
<div class="form-group" ng-show="sysinfo.newProvider === 'network-interface'" ng-class="{ 'has-error': (!sysinfoForm.ifname.$dirty && sysinfo.error.ifname) }">
|
||||
<label class="control-label">{{ 'network.ip.interface' | tr }}</label>
|
||||
<p>{{ 'network.ip.interfaceDescription' | tr }} <code>ip -f inet -br addr</code></p>
|
||||
<input type="text" class="form-control" ng-model="sysinfo.newIfname" name="ifname" ng-disabled="sysinfo.busy" ng-required="sysinfo.newProvider === 'network-interface'">
|
||||
<p class="has-error" ng-show="sysinfo.error.ifname">{{ sysinfo.error.ifname }}</p>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="sysinfoForm.$invalid || sysinfo.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="button" class="btn btn-success" ng-click="sysinfo.submit()" ng-disabled="sysinfoForm.$invalid || sysinfo.busy"><i class="fa fa-circle-notch fa-spin" ng-show="sysinfo.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal block list -->
|
||||
<div class="modal fade" id="blocklistModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'network.firewall.configure.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="blocklistChangeForm" role="form" novalidate ng-submit="blocklist.submit()" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'network.firewall.blockedIpRanges' | tr }}</label>
|
||||
<p class="small">{{ 'network.firewall.configure.description' | tr }}</p>
|
||||
<div class="has-error" ng-show="blocklist.error.blocklist">{{ blocklist.error.blocklist }}</div>
|
||||
<textarea ng-model="blocklist.blocklist" placeholder="{{ 'network.firewall.configure.blocklistPlaceholder' | tr }}" name="blocklist" class="form-control" ng-class="{ 'has-error': !blocklistChangeForm.blocklist.$dirty && blocklist.error.blocklist }" 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="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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal IPv6 -->
|
||||
<div class="modal fade" id="ipv6ConfigureModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'network.configureIpv6.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="ipv6ConfigureForm" role="form" novalidate ng-submit="ipv6Configure.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'network.ip.provider' | tr }} <sup><a ng-href="https://docs.cloudron.io/networking/#ipv6" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="ipv6Configure.newProvider" ng-options="a.value as a.name for a in ipv6ConfigureProvider"></select>
|
||||
<p class="has-error" ng-show="ipv6Configure.error.generic">{{ ipv6Configure.error.generic }}</p>
|
||||
</div>
|
||||
|
||||
<div ng-show="ipv6Configure.newProvider === 'generic'">
|
||||
{{ 'network.configureIp.providerGenericDescription' | tr }} <sup><a ng-href="https://ipv6.api.cloudron.io/api/v1/helper/public_ip" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</div>
|
||||
|
||||
<!-- Fixed -->
|
||||
<div class="form-group" ng-show="ipv6Configure.newProvider === 'fixed'" ng-class="{ 'has-error': (!ipv6ConfigureForm.ipv4.$dirty && ipv6Configure.error.ipv6) }">
|
||||
<label class="control-label">{{ 'network.ipv6.address' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="ipv6Configure.newIPv6" name="ipv6" ng-disabled="ipv6Configure.busy" ng-required="ipv6Configure.newProvider === 'fixed'">
|
||||
<p class="has-error" ng-show="ipv6Configure.error.ipv6">{{ ipv6Configure.error.ipv6 }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Network Interface -->
|
||||
<div class="form-group" ng-show="ipv6Configure.newProvider === 'network-interface'" ng-class="{ 'has-error': (!ipv6ConfigureForm.ifname.$dirty && ipv6Configure.error.ifname) }">
|
||||
<label class="control-label">{{ 'network.ip.interface' | tr }}</label>
|
||||
<p>{{ 'network.ip.interfaceDescription' | tr }} <code>ip -f inet6 -br addr</code></p>
|
||||
<input type="text" class="form-control" ng-model="ipv6Configure.newIfname" name="ifname" ng-disabled="ipv6Configure.busy" ng-required="ipv6Configure.newProvider === 'network-interface'">
|
||||
<p class="has-error" ng-show="ipv6Configure.error.ifname">{{ ipv6Configure.error.ifname }}</p>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="ipv6ConfigureForm.$invalid || ipv6Configure.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="button" class="btn btn-success" ng-click="ipv6Configure.submit()" ng-disabled="ipv6ConfigureForm.$invalid || ipv6Configure.busy"><i class="fa fa-circle-notch fa-spin" ng-show="ipv6Configure.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="text-left">
|
||||
<h1>{{ 'network.title' | tr }}</h1>
|
||||
</div>
|
||||
|
||||
<!-- IPv4 -->
|
||||
<div class="text-left">
|
||||
<h3>{{ 'network.ip.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{ 'network.ip.description' | tr }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'network.ip.provider' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ prettyIpProviderName(sysinfo.provider) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="sysinfo.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'network.ip.address' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span ng-show="sysinfo.ipv4">{{ sysinfo.ipv4 }}</span>
|
||||
<span ng-show="!sysinfo.ipv4">{{ sysinfo.serverIPv4 }} ({{ 'network.ip.detected' | tr }})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="sysinfo.ifname">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'network.ip.interface' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ sysinfo.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="sysinfo.show()">{{ 'network.ip.configure' | tr }}</button>
|
||||
</div>
|
||||
</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">
|
||||
<h3>{{ 'network.firewall.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'network.firewall.blockedIpRanges' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<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 class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'network.trustedIpRanges' | 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>
|
||||
</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>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>{{ 'network.dyndns.description' | tr }}</p>
|
||||
<p class="text-danger" ng-show="dyndnsConfigure.error"><br/>{{ dyndnsConfigure.error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-2" style="padding-top: 12px;">
|
||||
<i class="fa fa-circle" ng-class="{ 'status-active': dyndnsConfigure.isEnabled, 'status-inactive': !dyndnsConfigure.isEnabled }"></i> {{ dyndnsConfigure.isEnabled ? 'main.statusEnabled' : 'main.statusDisabled' | tr }}
|
||||
</div>
|
||||
<div class="col-md-10 text-right">
|
||||
<button class="btn btn-outline btn-primary" ng-hide="dyndnsConfigure.isEnabled" ng-click="dyndnsConfigure.setEnabled(true)" ng-disabled="dyndnsConfigure.busy"><i class="fa fa-circle-notch fa-spin" ng-show="dyndnsConfigure.busy"></i> {{ 'main.enableAction' | tr }}</button>
|
||||
<button class="btn btn-outline btn-danger" ng-show="dyndnsConfigure.isEnabled" ng-click="dyndnsConfigure.setEnabled(false)" ng-disabled="dyndnsConfigure.busy"><i class="fa fa-circle-notch fa-spin" ng-show="dyndnsConfigure.busy"></i> {{ 'main.disableAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,342 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global $, TASK_TYPES */
|
||||
|
||||
angular.module('Application').controller('NetworkController', ['$scope', '$location', '$timeout', 'Client', function ($scope, $location, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
// keep in sync with sysinfo.js
|
||||
$scope.sysinfoProvider = [
|
||||
{ name: 'Disabled', value: 'noop' },
|
||||
{ name: 'Public IP', value: 'generic' },
|
||||
{ name: 'Static IP Address', value: 'fixed' },
|
||||
{ name: 'Network Interface', value: 'network-interface' }
|
||||
];
|
||||
|
||||
$scope.ipv6ConfigureProvider = [
|
||||
{ name: 'Disabled', value: 'noop' },
|
||||
{ name: 'Public IP', value: 'generic' },
|
||||
{ name: 'Static IP Address', value: 'fixed' },
|
||||
{ name: 'Network Interface', value: 'network-interface' }
|
||||
];
|
||||
|
||||
$scope.prettyIpProviderName = function (provider) {
|
||||
switch (provider) {
|
||||
case 'noop': return 'Disabled';
|
||||
case 'generic': return 'Public IP';
|
||||
case 'fixed': return 'Static IP Address';
|
||||
case 'network-interface': return 'Network Interface';
|
||||
default: return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.dyndnsConfigure = {
|
||||
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();
|
||||
});
|
||||
},
|
||||
|
||||
setEnabled: function (enabled) {
|
||||
$scope.dyndnsConfigure.busy = true;
|
||||
$scope.dyndnsConfigure.error = '';
|
||||
|
||||
Client.setDynamicDnsConfig(enabled, function (error) {
|
||||
$scope.dyndnsConfigure.busy = false;
|
||||
|
||||
if (error) $scope.dyndnsConfigure.error = error.message;
|
||||
else $scope.dyndnsConfigure.isEnabled = enabled;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.ipv6Configure = {
|
||||
busy: false,
|
||||
error: {},
|
||||
displayError: '',
|
||||
|
||||
serverIPv6: '',
|
||||
|
||||
provider: '',
|
||||
ipv6: '',
|
||||
ifname: '',
|
||||
|
||||
// configure dialog
|
||||
newProvider: '',
|
||||
newIPv6: '',
|
||||
newIfname: '',
|
||||
|
||||
refresh: function () {
|
||||
Client.getIPv6Config(function (error, result) {
|
||||
if (error) {
|
||||
$scope.ipv6Configure.displayError = error.message;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
$scope.ipv6Configure.provider = result.provider;
|
||||
$scope.ipv6Configure.ipv6 = result.ip || '';
|
||||
$scope.ipv6Configure.ifname = result.ifname || '';
|
||||
if (result.provider === 'noop') return;
|
||||
|
||||
Client.getServerIpv6(function (error, result) {
|
||||
if (error) {
|
||||
$scope.ipv6Configure.displayError = error.message;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
$scope.ipv6Configure.serverIPv6 = result.ip;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.ipv6Configure.error = {};
|
||||
$scope.ipv6Configure.newProvider = $scope.ipv6Configure.provider;
|
||||
$scope.ipv6Configure.newIPv6 = $scope.ipv6Configure.ipv6;
|
||||
$scope.ipv6Configure.newIfname = $scope.ipv6Configure.ifname;
|
||||
|
||||
$('#ipv6ConfigureModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.ipv6Configure.error = {};
|
||||
$scope.ipv6Configure.busy = true;
|
||||
|
||||
var config = {
|
||||
provider: $scope.ipv6Configure.newProvider
|
||||
};
|
||||
|
||||
if (config.provider === 'fixed') {
|
||||
config.ip = $scope.ipv6Configure.newIPv6;
|
||||
} else if (config.provider === 'network-interface') {
|
||||
config.ifname = $scope.ipv6Configure.newIfname;
|
||||
}
|
||||
|
||||
Client.setIPv6Config(config, function (error) {
|
||||
$scope.ipv6Configure.busy = false;
|
||||
if (error && error.message.indexOf('ipv') !== -1) {
|
||||
$scope.ipv6Configure.error.ipv6 = error.message;
|
||||
$scope.ipv6ConfigureForm.$setPristine();
|
||||
$scope.ipv6ConfigureForm.$setUntouched();
|
||||
return;
|
||||
} else if (error && (error.message.indexOf('interface') !== -1 || error.message.indexOf('IPv6') !== -1)) {
|
||||
$scope.ipv6Configure.error.ifname = error.message;
|
||||
$scope.ipv6ConfigureForm.$setPristine();
|
||||
$scope.ipv6ConfigureForm.$setUntouched();
|
||||
return;
|
||||
} else if (error) {
|
||||
$scope.ipv6Configure.error.generic = error.message;
|
||||
$scope.ipv6ConfigureForm.$setPristine();
|
||||
$scope.ipv6ConfigureForm.$setUntouched();
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.ipv6Configure.refresh();
|
||||
|
||||
$('#ipv6ConfigureModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.blocklist = {
|
||||
busy: false,
|
||||
error: {},
|
||||
blocklist: '',
|
||||
currentBlocklist: '',
|
||||
currentBlocklistLength: 0,
|
||||
|
||||
refresh: function () {
|
||||
Client.getBlocklist(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.blocklist.currentBlocklist = result;
|
||||
$scope.blocklist.currentBlocklistLength = result.split('\n').filter(function (l) { return l.length !== 0 && l[0] !== '#'; }).length;
|
||||
});
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.blocklist.error = {};
|
||||
$scope.blocklist.blocklist = $scope.blocklist.currentBlocklist;
|
||||
|
||||
$('#blocklistModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.blocklist.error = {};
|
||||
$scope.blocklist.busy = true;
|
||||
|
||||
Client.setBlocklist($scope.blocklist.blocklist, function (error) {
|
||||
$scope.blocklist.busy = false;
|
||||
if (error) {
|
||||
$scope.blocklist.error.blocklist = error.message;
|
||||
$scope.blocklist.error.ip = error.message;
|
||||
$scope.blocklistChangeForm.$setPristine();
|
||||
$scope.blocklistChangeForm.$setUntouched();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.blocklist.refresh();
|
||||
|
||||
$('#blocklistModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$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: {},
|
||||
|
||||
serverIPv4: '',
|
||||
|
||||
provider: '',
|
||||
ipv4: '',
|
||||
ifname: '',
|
||||
|
||||
// configure dialog
|
||||
newProvider: '',
|
||||
newIPv4: '',
|
||||
newIfname: '',
|
||||
|
||||
refresh: function () {
|
||||
Client.getIPv4Config(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.sysinfo.provider = result.provider;
|
||||
$scope.sysinfo.ipv4 = result.ip || '';
|
||||
$scope.sysinfo.ifname = result.ifname || '';
|
||||
|
||||
Client.getServerIpv4(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.sysinfo.serverIPv4 = result.ip;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.sysinfo.error = {};
|
||||
$scope.sysinfo.newProvider = $scope.sysinfo.provider;
|
||||
$scope.sysinfo.newIPv4 = $scope.sysinfo.ipv4;
|
||||
$scope.sysinfo.newIfname = $scope.sysinfo.ifname;
|
||||
|
||||
$('#sysinfoModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.sysinfo.error = {};
|
||||
$scope.sysinfo.busy = true;
|
||||
|
||||
var config = {
|
||||
provider: $scope.sysinfo.newProvider
|
||||
};
|
||||
|
||||
if (config.provider === 'fixed') {
|
||||
config.ip = $scope.sysinfo.newIPv4;
|
||||
} else if (config.provider === 'network-interface') {
|
||||
config.ifname = $scope.sysinfo.newIfname;
|
||||
}
|
||||
|
||||
Client.setIPv4Config(config, function (error) {
|
||||
$scope.sysinfo.busy = false;
|
||||
if (error && error.message.indexOf('ipv') !== -1) {
|
||||
$scope.sysinfo.error.ipv4 = error.message;
|
||||
$scope.sysinfoForm.$setPristine();
|
||||
$scope.sysinfoForm.$setUntouched();
|
||||
return;
|
||||
} else if (error && (error.message.indexOf('interface') !== -1 || error.message.indexOf('IPv4') !== -1)) {
|
||||
$scope.sysinfo.error.ifname = error.message;
|
||||
$scope.sysinfoForm.$setPristine();
|
||||
$scope.sysinfoForm.$setUntouched();
|
||||
return;
|
||||
} else if (error) {
|
||||
$scope.sysinfo.error.generic = error.message;
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.sysinfo.refresh();
|
||||
|
||||
$('#sysinfoModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.sysinfo.refresh();
|
||||
|
||||
$scope.dyndnsConfigure.refresh();
|
||||
$scope.ipv6Configure.refresh();
|
||||
$scope.trustedIps.refresh();
|
||||
if ($scope.user.isAtLeastOwner) $scope.blocklist.refresh();
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,39 +0,0 @@
|
||||
<div class="content content-large">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>{{ 'notifications.title' | tr }}
|
||||
|
||||
<div class="title-toolbar">
|
||||
<button class="btn btn-default" ng-click="showPrevPage()" ng-disabled="busy || currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
<button class="btn btn-default" ng-click="showNextPage()" ng-disabled="busy || perPage > notifications.length">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
<button class="btn btn-primary" ng-click="clearAll()" ng-disabled="!$parent.notificationCount || clearAllBusy"><i class="fa fa-circle-notch fa-spin" ng-show="clearAllBusy"></i><i class="fa fa-check" ng-hide="clearAllBusy"></i> {{ 'notifications.markAllAsRead' | tr }}</button>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-12 text-center" ng-show="busy">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" ng-hide="busy || notifications.length">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h3 class="text-center" style="margin: 20px;">{{ 'notifications.nonePending' | tr }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-large notification-item" ng-repeat="notification in notifications" ng-class="{'notification-unread': !notification.acknowledged }" ng-click="notification.isCollapsed = !notification.isCollapsed" ng-style="{ borderLeftColor: (notification | notificationTypeToColor) }">
|
||||
<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>
|
||||
<div uib-collapse="notification.isCollapsed" expanding="ack(notification)">
|
||||
<br/>
|
||||
<p ng-hide="notification.messageJson" ng-click="$event.stopPropagation();" style="cursor: auto; overflow: auto;" ng-bind-html="notification.message | markdown2html"></p>
|
||||
<pre ng-show="notification.messageJson" ng-click="$event.stopPropagation();" style="cursor: auto">{{ notification.messageJson | json }}</pre>
|
||||
<button type="button" class="btn btn-danger pull-right" ng-click="$event.stopPropagation(); $parent.reboot.show()" ng-show="notification.title === 'Reboot Required'" ng-disabled="reboot.busy"><i class="fa fa-circle-notch fa-spin" ng-show="reboot.busy"></i> {{ 'main.action.reboot' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,90 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global async */
|
||||
/* global angular */
|
||||
|
||||
angular.module('Application').controller('NotificationsController', ['$scope', '$location', '$timeout', '$translate', '$interval', 'Client', function ($scope, $location, $timeout, $translate, $interval, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.clearAllBusy = false;
|
||||
|
||||
$scope.notifications = [];
|
||||
$scope.activeNotification = null;
|
||||
$scope.busy = true;
|
||||
$scope.currentPage = 1;
|
||||
$scope.perPage = 20;
|
||||
|
||||
$scope.refresh = function () {
|
||||
Client.getNotifications({}, $scope.currentPage, $scope.perPage, function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
// collapse by default
|
||||
result.forEach(function (r) { r.isCollapsed = true; });
|
||||
|
||||
// attempt to translate or parse the message as json
|
||||
result.forEach(function (r) {
|
||||
try {
|
||||
r.messageJson = JSON.parse(r.message);
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
$scope.notifications = result;
|
||||
|
||||
$scope.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showNextPage = function () {
|
||||
$scope.currentPage++;
|
||||
$scope.refresh();
|
||||
};
|
||||
|
||||
$scope.showPrevPage = function () {
|
||||
if ($scope.currentPage > 1) $scope.currentPage--;
|
||||
else $scope.currentPage = 1;
|
||||
|
||||
$scope.refresh();
|
||||
};
|
||||
|
||||
$scope.ack = function (notification) {
|
||||
if (notification.acknowledged) return;
|
||||
|
||||
Client.ackNotification(notification.id, true, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
notification.acknowledged = true;
|
||||
$scope.$parent.notificationAcknowledged();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.clearAll = function () {
|
||||
$scope.clearAllBusy = true;
|
||||
|
||||
Client.getNotifications({ acknowledged: false }, 1, 1000, function (error, results) { // hopefully 1k unread is sufficient
|
||||
if (error) console.error(error);
|
||||
|
||||
async.eachLimit(results, 20, function (notification, callback) {
|
||||
Client.ackNotification(notification.id, true, function (error) {
|
||||
if (error) console.error(error);
|
||||
callback();
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
// refresh the main navbar indicator
|
||||
$scope.$parent.notificationAcknowledged();
|
||||
$scope.refresh();
|
||||
|
||||
$scope.clearAllBusy = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
var refreshTimer = $interval($scope.refresh, 60 * 1000); // keep this interval in sync with the notification count indicator in main.js
|
||||
$scope.$on('$destroy', function () {
|
||||
$interval.cancel(refreshTimer);
|
||||
});
|
||||
$scope.refresh();
|
||||
});
|
||||
}]);
|
||||
@@ -1,557 +0,0 @@
|
||||
|
||||
<!-- Modal change avatar -->
|
||||
<div class="modal fade" id="avatarChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'profile.changeAvatar.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body settings-avatar-selector">
|
||||
<div style="margin: auto; text-align: left">
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="avatarType" ng-model="avatarChange.type" value="">
|
||||
{{ 'profile.changeAvatar.noAvatar' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="avatarType" ng-model="avatarChange.type" value="gravatar">
|
||||
<span ng-bind-html="'profile.changeAvatar.useGravatar' | tr:{ gravatarLink: 'https://gravatar.com/' }"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="avatarType" ng-model="avatarChange.type" value="custom">
|
||||
{{ 'profile.changeAvatar.useCustomPicture' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="avatarChange.type === 'custom'" class="preview-avatar">
|
||||
<img id="previewAvatar" width="128" height="128" class="copy" ng-click="avatarChange.showCustomAvatarSelector()"/>
|
||||
<input type="file" id="avatarFileInput" style="display: none" accept="image/*"/>
|
||||
</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-success" ng-click="avatarChange.doChangeAvatar()" ng-disabled="avatarChange.busy || (avatarChange.typeOrig === avatarChange.type && !avatarChange.pictureChanged) || (avatarChange.type === 'custom' && !avatarChange.pictureChanged)"><i class="fa fa-circle-notch fa-spin" ng-show="avatarChange.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change backgroundImage -->
|
||||
<div class="modal fade" id="backgroundImageChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'profile.changeBackgroundImage.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body settings-avatar-selector">
|
||||
<div class="preview-avatar">
|
||||
<img id="previewBackgroundImage" width="100%" height="400px" class="copy" style="object-fit: cover;" ng-click="backgroundImageChange.showCustomBackgroundImageSelector()"/>
|
||||
<input type="file" id="backgroundImageFileInput" style="display: none" accept="image/png, image/jpeg"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger pull-left" ng-click="backgroundImageChange.unset()" ng-disabled="backgroundImageChange.busy">Remove Background Image</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="backgroundImageChange.submit()" ng-disabled="backgroundImageChange.busy || !backgroundImageChange.pictureChanged"><i class="fa fa-circle-notch fa-spin" ng-show="backgroundImageChange.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change password -->
|
||||
<div class="modal fade" id="passwordChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'profile.changePassword.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="passwordChangeForm" role="form" novalidate ng-submit="passwordchange.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!passwordChangeForm.password.$dirty && passwordchange.error.password) || (passwordChangeForm.password.$dirty && passwordChangeForm.password.$invalid) }">
|
||||
<label class="control-label" for="inputPasswordChangePassword">{{ 'profile.changePassword.currentPassword' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!passwordChangeForm.password.$dirty && passwordchange.error.password) || (passwordChangeForm.password.$dirty && passwordChangeForm.password.$invalid)">
|
||||
<small ng-show="!passwordChangeForm.password.$dirty && passwordchange.error.password">Wrong password</small>
|
||||
<small ng-show="passwordChangeForm.password.$dirty && passwordChangeForm.password.$error.required">A password is required</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordchange.password" id="inputPasswordChangePassword" name="password" required autofocus password-reveal>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword) || (passwordChangeForm.newPassword.$dirty && passwordChangeForm.newPassword.$invalid) }">
|
||||
<label class="control-label" for="inputPasswordChangeNewPassword">{{ 'profile.changePassword.newPassword' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword) || (passwordChangeForm.newPassword.$dirty && passwordChangeForm.newPassword.$invalid)">
|
||||
<small ng-show="!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword">{{ passwordchange.error.newPassword }}<br/><br/></small>
|
||||
<small ng-show=" passwordChangeForm.newPassword.$dirty && passwordChangeForm.newPassword.$invalid">{{ 'profile.changePassword.errorPasswordInvalid' | tr }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordchange.newPassword" id="inputPasswordChangeNewPassword" name="newPassword" ng-minlength="8" ng-maxlength="256" required autofocus password-reveal>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword) || (passwordChangeForm.newPasswordRepeat.$dirty && passwordChangeForm.newPasswordRepeat.$error.required) || (passwordChangeForm.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat) }">
|
||||
<label class="control-label" for="inputPasswordChangeNewPasswordRepeat">{{ 'profile.changePassword.newPasswordRepeat' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword) || (passwordChangeForm.newPasswordRepeat.$dirty && passwordChangeForm.newPasswordRepeat.$error.required) || (passwordChangeForm.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat)">
|
||||
<small ng-show="passwordChangeForm.newPasswordRepeat.$dirty && passwordChangeForm.newPasswordRepeat.$error.required">{{ 'profile.changePassword.errorPasswordRequired' | tr }}</small>
|
||||
<small ng-show="passwordChangeForm.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat && passwordchange.newPasswordRepeat">{{ 'profile.changePassword.errorPasswordsDontMatch' | tr }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordchange.newPasswordRepeat" id="inputPasswordChangeNewPasswordRepeat" name="newPasswordRepeat" required autofocus password-reveal>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="passwordChangeForm.$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="passwordchange.submit()" ng-disabled="passwordChangeForm.$invalid || passwordchange.busy"><i class="fa fa-circle-notch fa-spin" ng-show="passwordchange.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change email -->
|
||||
<div class="modal fade" id="emailChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'profile.changeEmail.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="emailChangeForm" role="form" novalidate ng-submit="emailChange.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (emailChangeForm.email.$dirty && emailChangeForm.email.$invalid) || (!emailChangeForm.email.$dirty && emailChange.error.email)}">
|
||||
<label class="control-label" for="inputEmailChangeEmail">{{ 'profile.changeEmail.email' | tr }}</label>
|
||||
<input type="email" class="form-control" ng-model="emailChange.email" id="inputEmailChangeEmail" name="email" required autofocus>
|
||||
<div class="control-label" ng-show="(!emailChangeForm.email.$dirty && emailChange.error.email) || (emailChangeForm.email.$dirty && emailChangeForm.email.$invalid)">
|
||||
<small ng-show="emailChangeForm.email.$error.required">{{ 'profile.changeEmail.errorEmailRequired' | tr }}</small>
|
||||
<small ng-show="(emailChangeForm.email.$dirty && emailChangeForm.email.$invalid) && !emailChangeForm.email.$error.required">{{ 'profile.changeEmail.errorEmailInvalid' | tr }}</small>
|
||||
<small ng-show="!emailChangeForm.email.$dirty && emailChange.error.email">{{ emailChange.error.email }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (emailChange.error.password && !emailChangeForm.password.$dirty) }">
|
||||
<label class="control-label" for="inputEmailChangePassword">{{ 'profile.changeEmail.password' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="emailChange.password" id="inputEmailChangePassword" name="password" required autofocus password-reveal>
|
||||
<div class="control-label" ng-show="emailChange.error.password && !emailChangeForm.password.$dirty">
|
||||
<small ng-show="emailChange.error.password">{{ 'profile.changeEmail.errorWrongPassword' | tr }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="emailChangeForm.$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="emailChange.submit()" ng-disabled="emailChangeForm.$invalid || emailChange.busy"><i class="fa fa-circle-notch fa-spin" ng-show="emailChange.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change fallback email -->
|
||||
<div class="modal fade" id="fallbackEmailChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'profile.changeFallbackEmail.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="fallbackEmailChangeForm" role="form" novalidate ng-submit="fallbackEmailChange.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': fallbackEmailChange.error.generic || (fallbackEmailChangeForm.email.$dirty && fallbackEmailChangeForm.email.$invalid) || (!fallbackEmailChangeForm.email.$dirty && fallbackEmailChange.error.email)}">
|
||||
<label class="control-label" for="inputFallbackEmailChangeEmail">{{ 'profile.changeFallbackEmail.email' | tr }}</label>
|
||||
<input type="email" class="form-control" ng-model="fallbackEmailChange.email" id="inputFallbackEmailChangeEmail" name="email" required autofocus>
|
||||
<div class="control-label" ng-show="fallbackEmailChange.error.generic || (!fallbackEmailChangeForm.email.$dirty && fallbackEmailChange.error.email) || (fallbackEmailChangeForm.email.$dirty && fallbackEmailChangeForm.email.$invalid)">
|
||||
<small ng-show="fallbackEmailChangeForm.email.$error.required">{{ 'profile.changeFallbackEmail.errorEmailRequired' | tr }}</small>
|
||||
<small ng-show="(fallbackEmailChangeForm.email.$dirty && fallbackEmailChangeForm.email.$invalid) && !fallbackEmailChangeForm.email.$error.required">{{ 'profile.changeFallbackEmail.errorEmailInvalid' | tr }}</small>
|
||||
<small ng-show="!fallbackEmailChangeForm.email.$dirty && fallbackEmailChange.error.email">{{ fallbackEmailChange.error.email }}</small>
|
||||
<small ng-show="fallbackEmailChange.error.generic">{{ fallbackEmailChange.error.generic }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!fallbackEmailChangeForm.password.$dirty && fallbackEmailChange.error.password) || (fallbackEmailChangeForm.password.$dirty && fallbackEmailChangeForm.password.$invalid) }">
|
||||
<label class="control-label" for="inputFallbackEmailChangePassword">{{ 'profile.changeFallbackEmail.password' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="fallbackEmailChange.password" id="inputFallbackEmailChangePassword" name="password" required autofocus password-reveal>
|
||||
<div class="control-label" ng-show="(!fallbackEmailChangeForm.password.$dirty && fallbackEmailChange.error.password) || (fallbackEmailChangeForm.password.$dirty && fallbackEmailChangeForm.password.$invalid)">
|
||||
<small ng-show="!fallbackEmailChangeForm.password.$dirty && fallbackEmailChange.error.password">{{ 'profile.changeFallbackEmail.errorWrongPassword' | tr }}</small>
|
||||
<small ng-show="fallbackEmailChangeForm.password.$dirty && fallbackEmailChangeForm.password.$error.required">{{ 'profile.changeFallbackEmail.errorPasswordRequired' | tr }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="fallbackEmailChangeForm.$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="fallbackEmailChange.submit()" ng-disabled="fallbackEmailChangeForm.$invalid || fallbackEmailChange.busy"><i class="fa fa-circle-notch fa-spin" ng-show="fallbackEmailChange.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change displayName -->
|
||||
<div class="modal fade" id="displayNameChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'profile.changeDisplayName.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="displayNameChangeForm" role="form" novalidate ng-submit="displayNameChange.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (displayNameChangeForm.displayName.$dirty && displayNameChangeForm.displayName.$invalid) || (!displayNameChangeForm.displayName.$dirty && displayNameChange.error.displayName)}">
|
||||
<input type="text" class="form-control" ng-model="displayNameChange.displayName" id="inputDisplayNameChangeDisplayName" name="displayName" required autofocus>
|
||||
<div class="control-label" ng-show="(!displayNameChangeForm.displayName.$dirty && displayNameChange.error.displayName) || (displayNameChangeForm.displayName.$dirty && displayNameChangeForm.displayName.$invalid)">
|
||||
<small ng-show="displayNameChangeForm.displayName.$error.required">{{ 'profile.changeDisplayName.errorDisplayNameRequired' | tr }}</small>
|
||||
<small ng-show="(displayNameChangeForm.displayName.$dirty && displayNameChangeForm.displayName.$invalid) && !displayNameChangeForm.displayName.$error.required">{{ 'profile.changeDisplayName.errorNameInvalid' | tr }}</small>
|
||||
<small ng-show="!displayNameChangeForm.email.$dirty && displayNameChange.error.displayName">{{ displayNameChange.error.displayName }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="displayNameChangeForm.$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="displayNameChange.submit()" ng-disabled="displayNameChangeForm.$invalid || displayNameChange.busy"><i class="fa fa-circle-notch fa-spin" ng-show="displayNameChange.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal enable twofactor authentication -->
|
||||
<div class="modal fade" id="twoFactorAuthenticationEnableModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'profile.enable2FA.title' | tr }}</h4>
|
||||
</div>
|
||||
<p class="modal-body" ng-show="twoFactorAuthentication.mandatory2FAHelp && !twoFactorAuthentication.secret">{{ 'profile.enable2FA.description' | tr }}</p>
|
||||
<div class="modal-body text-center" ng-show="!twoFactorAuthentication.mandatory2FAHelp && !twoFactorAuthentication.secret && !twoFactorAuthentication.notSupportedError">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="twoFactorAuthentication.notSupportedError">
|
||||
<p class="text-danger">{{ twoFactorAuthentication.notSupportedError }}</p>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="twoFactorAuthentication.secret">
|
||||
<p ng-bind-html="'profile.enable2FA.authenticatorAppDescription' | tr:{ googleAuthenticatorPlayStoreLink: 'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2', googleAuthenticatorITunesLink: 'https://itunes.apple.com/us/app/google-authenticator/id388497605', freeOTPPlayStoreLink: 'https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp', freeOTPITunesLink: 'https://itunes.apple.com/us/app/freeotp-authenticator/id872559395' }"></p>
|
||||
<center>
|
||||
<img ng-src="{{ twoFactorAuthentication.qrcode }}"/>
|
||||
<p>{{ twoFactorAuthentication.secret }}</p>
|
||||
</center>
|
||||
<br/>
|
||||
<form name="twoFactorAuthenticationEnableForm" role="form" novalidate ng-submit="twoFactorAuthentication.enable()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!twoFactorAuthenticationEnableForm.totpToken.$dirty && twoFactorAuthentication.error) || (twoFactorAuthenticationEnableForm.totpToken.$dirty && twoFactorAuthenticationEnableForm.totpToken.$invalid) }">
|
||||
<label class="control-label">{{ 'profile.enable2FA.token' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!twoFactorAuthenticationEnableForm.totpToken.$dirty && twoFactorAuthentication.error) || (twoFactorAuthenticationEnableForm.totpToken.$dirty && twoFactorAuthenticationEnableForm.totpToken.$invalid)">
|
||||
<small>{{ twoFactorAuthentication.error }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="twoFactorAuthentication.totpToken" id="twoFactorAuthenticationTotpTokenInput" name="totpToken" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="twoFactorAuthenticationEnableForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal" ng-if="!twoFactorAuthentication.mandatory2FA">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="twoFactorAuthentication.enable()" ng-show="twoFactorAuthentication.secret" ng-disabled="twoFactorAuthenticationEnableForm.$invalid || twoFactorAuthentication.busy"><i class="fa fa-circle-notch fa-spin" ng-show="twoFactorAuthentication.busy"></i> {{ 'profile.enable2FA.enable' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="twoFactorAuthentication.getSecret()" ng-show="twoFactorAuthentication.mandatory2FAHelp" >{{ 'profile.enable2FA.setup2FA' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal disable twofactor authentication -->
|
||||
<div class="modal fade" id="twoFactorAuthenticationDisableModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'profile.disable2FA.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="twoFactorAuthenticationDisableForm" role="form" novalidate ng-submit="twoFactorAuthentication.disable()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!twoFactorAuthenticationDisableForm.password.$dirty && twoFactorAuthentication.error) || (twoFactorAuthenticationDisableForm.password.$dirty && twoFactorAuthenticationDisableForm.password.$invalid) }">
|
||||
<label class="control-label">{{ 'profile.disable2FA.password' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!twoFactorAuthenticationDisableForm.password.$dirty && twoFactorAuthentication.error) || (twoFactorAuthenticationDisableForm.password.$dirty && twoFactorAuthenticationDisableForm.password.$invalid)">
|
||||
<small>{{ twoFactorAuthentication.error }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="twoFactorAuthentication.password" id="twoFactorAuthenticationPasswordInput" name="password" required autofocus password-reveal>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="twoFactorAuthenticationDisableForm.$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="twoFactorAuthentication.disable()" ng-disabled="twoFactorAuthenticationDisableForm.$invalid || twoFactorAuthentication.busy"><i class="fa fa-circle-notch fa-spin" ng-show="twoFactorAuthentication.busy"></i> {{ 'profile.disable2FA.disable' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal add app password -->
|
||||
<div class="modal fade" id="appPasswordAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'profile.createAppPassword.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-hide="appPasswordAdd.password">
|
||||
<form name="appPasswordAddForm" role="form" novalidate ng-submit="appPasswordAdd.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (appPasswordAddForm.name.$dirty && appPasswordAddForm.name.$invalid) || (!appPasswordAddForm.name.$dirty && appPasswordAdd.error.name)}">
|
||||
<label class="control-label">{{ 'profile.createAppPassword.name' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!appPasswordAddForm.name.$dirty && appPasswordAdd.error.name) || (appPasswordAddForm.name.$dirty && appPasswordAddForm.name.$invalid)">
|
||||
<small ng-show="appPasswordAddForm.name.$error.required">{{ 'profile.createAppPassword.errorNameRequired' | tr }}</small>
|
||||
<small ng-show="appPasswordAdd.error.name">{{ appPasswordAdd.error.name }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="appPasswordAdd.name" id="inputAppPasswordAddName" name="name" required autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (appPasswordAddForm.identifier.$dirty && appPasswordAddForm.identifier.$invalid) || (!appPasswordAddForm.identifier.$dirty && appPasswordAdd.error.identifier)}">
|
||||
<label class="control-label">{{ 'profile.createAppPassword.app' | tr }}</label>
|
||||
<select class="form-control" ng-model="appPasswordAdd.identifier" ng-options="a.id as a.label for a in appPassword.identifiers" required></select>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="appPasswordAddForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div ng-show="appPasswordAdd.password">
|
||||
{{ 'profile.createAppPassword.description' | tr }}
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="input-group">
|
||||
<input type="text" id="newAppPassword" class="form-control" name="appPassword" ng-model="appPasswordAdd.password.password" required readonly/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" id="newAppPasswordClipboardButton" data-clipboard-target="#newAppPassword"><i class="fa fa-clipboard"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
<br/>
|
||||
<p>{{ 'profile.createAppPassword.copyNow' | tr }}</p>
|
||||
</div>
|
||||
</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="appPasswordAdd.submit()" ng-hide="appPasswordAdd.password" ng-disabled="appPasswordAddForm.$invalid || appPasswordAdd.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="appPasswordAdd.busy"></i> {{ 'profile.createAppPassword.generatePassword' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal add api token -->
|
||||
<div class="modal fade" id="apiTokenAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'profile.createApiToken.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-hide="tokens.add.accessToken">
|
||||
<form name="apiTokenAddForm" role="form" novalidate ng-submit="tokens.add.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (apiTokenAddForm.name.$dirty && apiTokenAddForm.name.$invalid) || (!apiTokenAddForm.name.$dirty && tokens.add.error)}">
|
||||
<label class="control-label">{{ 'profile.createApiToken.name' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!apiTokenAddForm.name.$dirty && tokens.add.error) || (apiTokenAddForm.name.$dirty && apiTokenAddForm.name.$invalid)">
|
||||
<small ng-show="apiTokenAddForm.name.$error.required">{{ 'profile.createApiToken.errorNameRequired' | tr }}</small>
|
||||
<small ng-show="tokens.add.error.name">{{ tokens.add.error }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" id="inputApiTokenName" ng-model="tokens.add.name" name="name" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'profile.createApiToken.access' | tr }}</label>
|
||||
|
||||
<div class="radio">
|
||||
<label><input type="radio" ng-model="tokens.add.scope" value="r"> {{ 'profile.apiTokens.readonly' | tr }}</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label><input type="radio" ng-model="tokens.add.scope" value="rw"> {{ 'profile.apiTokens.readwrite' | tr }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="apiTokenAddForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
<div ng-show="tokens.add.accessToken">
|
||||
{{ 'profile.createApiToken.description' | tr }}
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="input-group">
|
||||
<input type="text" id="accessTokenToken" class="form-control" name="accessToken" ng-model="tokens.add.accessToken" required readonly/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" id="newAccessTokenClipboardButton" data-clipboard-target="#accessTokenToken"><i class="fa fa-clipboard"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
<br/>
|
||||
<p>{{ 'profile.createApiToken.copyNow' | tr }}</p>
|
||||
</div>
|
||||
</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="tokens.add.submit()" ng-hide="tokens.add.accessToken" ng-disabled="apiTokenAddForm.$invalid || tokens.add.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="tokens.add.busy"></i> {{ 'profile.createApiToken.generateToken' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>{{ 'profile.title' | tr }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-3" style="min-width: 150px;">
|
||||
<div class="settings-avatar" style="background-image: url('{{ user.avatarUrl }}');" ng-click="avatarChange.showChangeAvatar()">
|
||||
<i class="picture-edit-indicator fa fa-pencil-alt"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-9">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">{{ 'main.username' | tr }}</td>
|
||||
<td class="text-right" style="vertical-align: top;">
|
||||
{{ user.username }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">{{ 'main.displayName' | tr }}</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">
|
||||
{{ user.displayName }} <a href="" ng-click="displayNameChange.show()" ng-hide="user.source || config.profileLocked"><i class="fa fa-edit text-small"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">{{ 'profile.primaryEmail' | tr }}</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">
|
||||
{{ user.email }} <a href="" ng-click="emailChange.show()" ng-hide="user.source || config.profileLocked"><i class="fa fa-edit text-small"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">{{ 'profile.passwordRecoveryEmail' | tr }}</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">
|
||||
{{ user.fallbackEmail }} <a href="" ng-click="fallbackEmailChange.show()" ng-hide="user.source || config.profileLocked"><i class="fa fa-edit text-small"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-hide="user.source">
|
||||
<td colspan="2" class="text-right">
|
||||
<a href="" ng-click="sendPasswordReset()">{{ 'profile.passwordResetAction' | tr }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td colspan="2"> </td></tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: middle;">{{ 'profile.language' | tr }}</td>
|
||||
<td class="text-right" style="vertical-align: middle;">
|
||||
<multiselect ng-model="language" options="lang.display for lang in languages" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<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 && config.external2FA) ? ('profile.enable2FANotAvailable' | tr) : '' }}" ng-disabled="user.source && config.external2FA" ng-class="user.twoFactorAuthenticationEnabled ? 'btn-danger' : 'btn-success'" ng-click="twoFactorAuthentication.show()">{{ user.twoFactorAuthenticationEnabled ? 'profile.disable2FAAction' : 'profile.enable2FAAction' | tr }}</button> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'profile.appPasswords.title' | tr }}<button class="btn btn-primary btn-sm pull-right" ng-click="appPasswordAdd.show()"><i class="fa fa-plus"></i> {{ 'profile.appPasswords.newPassword' | tr }}</button></h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p>{{ 'profile.appPasswords.description' | tr }}</p>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 45%">{{ 'profile.appPasswords.name' | tr }}</th>
|
||||
<th style="width: 45%">{{ 'profile.appPasswords.app' | tr }}</th>
|
||||
<th style="width: 10%" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-show="appPassword.passwords.length === 0">
|
||||
<td colspan="3" class="text-center">{{ 'profile.appPasswords.noPasswordsPlaceholder' | tr }}</td>
|
||||
</tr>
|
||||
<tr ng-repeat="password in appPassword.passwords">
|
||||
<td class="text-left elide-table-cell">
|
||||
<span uib-tooltip="{{ password.creationTime | prettyLongDate }}" class="arrow">{{ password.name }}</span>
|
||||
</td>
|
||||
<td class="text-left elide-table-cell">
|
||||
<span class="arrow">{{ password.label }}</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-danger pull-right" ng-click="appPassword.del(password.id)" uib-tooltip="{{ 'profile.appPasswords.deletePasswordTooltip' | tr }}"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br ng-show="user.isAtLeastAdmin"/>
|
||||
|
||||
<div class="text-left" ng-show="user.isAtLeastAdmin">
|
||||
<h3>{{ 'profile.apiTokens.title' | tr }} <button class="btn btn-primary btn-sm pull-right" ng-click="tokens.add.show()"><i class="fa fa-plus"></i> {{ 'profile.apiTokens.newApiToken' | tr }}</button></h3>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-show="user.isAtLeastAdmin">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p ng-bind-html="'profile.apiTokens.description' | tr:{ apiDocsLink: 'https://docs.cloudron.io/api.html' }"></p>
|
||||
<table class="table table-hover" style="margin: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 35%">{{ 'profile.apiTokens.name' | tr }}</th>
|
||||
<th style="width: 35%">{{ 'profile.apiTokens.lastUsed' | tr }}</th>
|
||||
<th style="width: 20%">{{ 'profile.apiTokens.scope' | tr }}</th>
|
||||
<th style="width: 10%" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-show="tokens.apiTokens.length === 0">
|
||||
<td colspan="3" class="text-center">{{ 'profile.apiTokens.noTokensPlaceholder' | tr }}</td>
|
||||
</tr>
|
||||
<tr ng-repeat="token in tokens.apiTokens">
|
||||
<td class="elide-table-cell" style="text-overflow: ellipsis; white-space: nowrap;">
|
||||
{{ token.name || 'unnamed' }}
|
||||
</td>
|
||||
<td class="elide-table-cell" style="text-overflow: ellipsis; white-space: nowrap;">
|
||||
<span ng-show="token.lastUsedTime">{{ token.lastUsedTime | prettyLongDate }}</span>
|
||||
<span ng-show="!token.lastUsedTime">{{ 'profile.apiTokens.neverUsed' | tr }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span ng-show="token.scope['*'] === 'rw'">{{ 'profile.apiTokens.readwrite' | tr }}</span>
|
||||
<span ng-hide="token.scope['*'] === 'rw'">{{ 'profile.apiTokens.readonly' | tr }}</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-danger" ng-click="tokens.revokeToken(token)" uib-tooltip="{{ 'profile.apiTokens.revokeTokenTooltip' | tr }}"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'profile.loginTokens.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,159 +0,0 @@
|
||||
<!-- Modal service configure -->
|
||||
<div class="modal fade" id="serviceConfigureModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'services.configure.title' | tr:{ name: serviceConfigure.service.displayName } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="serviceConfigureForm" role="form" novalidate ng-submit="serviceConfigure.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="serviceConfigure.error">{{ serviceConfigure.error }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" style="display: block;" for="memoryLimit">
|
||||
{{ 'services.memoryLimit' | tr }}: <b>{{ serviceConfigure.memoryLimit | prettyBinarySize:'' }}</b>
|
||||
<button type="button" class="btn btn-xs btn-default pull-right" ng-click="serviceConfigure.resetToDefaults()">{{ 'services.configure.resetToDefaults' | tr }}</button>
|
||||
</label>
|
||||
<div style="padding: 0 10px;">
|
||||
<input type="range" id="memoryLimit" ng-model="serviceConfigure.memoryLimit" step="134217728" min="{{ serviceConfigure.memoryTicks[0] }}" max="{{ serviceConfigure.memoryTicks[serviceConfigure.memoryTicks.length-1] }}" list="memoryLimitTicks" />
|
||||
<datalist id="memoryLimitTicks">
|
||||
<option ng-repeat="limit in serviceConfigure.memoryTicks" value="{{ limit }}"></option>
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<br>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="serviceConfigure.recoveryMode"><b>{{ 'services.configure.enableRecoveryMode' | tr }}</b></input>
|
||||
</label>
|
||||
</div>
|
||||
<p ng-bind-html="'services.configure.recoveryModeDescription' | tr:{ docsLink: 'https://docs.cloudron.io/troubleshooting/#unresponsive-service' }"></p>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="serviceConfigureForm.$invalid || serviceConfigure.busy"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="serviceConfigure.submit()" ng-disabled="serviceConfigureForm.$invalid || serviceConfigure.busy"><i class="fa fa-circle-notch fa-spin" ng-show="serviceConfigure.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>{{ 'services.title' | tr }}
|
||||
<button class="btn btn-default pull-right" ng-click="refreshAll()">{{ 'services.refresh' | tr }}</button>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>{{ 'services.description' | tr }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="row ng-hide" ng-show="!servicesReady">
|
||||
<div class="col-md-12 text-center">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="servicesReady">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 5%;"></th>
|
||||
<th style="width: 20%">{{ 'services.service' | tr }}</th>
|
||||
<th style="width: 50%">{{ 'services.memoryUsage' | tr }}</th>
|
||||
<th style="width: 20%" class="text-center no-wrap">{{ 'services.memoryLimit' | tr }}</th>
|
||||
<th style="width: 5%" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><i class="fa fa-circle status-active" uib-tooltip="active"></i></td>
|
||||
<td class="elide-table-cell">cloudron</td>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-repeat="service in services | filter:{ isRedis: false } | orderBy:'name'">
|
||||
<td>
|
||||
<span ng-switch on="service.status" ng-show="service.status">
|
||||
<span ng-switch-when="active">
|
||||
<i class="fa fa-circle status-active" uib-tooltip="active"></i>
|
||||
</span>
|
||||
<span ng-switch-when="starting">
|
||||
<i class="fa fa-circle status-starting" uib-tooltip="starting" ng-show="!service.config.recoveryMode"></i>
|
||||
<i class="fa fa-circle status-inactive" uib-tooltip="recovery mode" ng-show="service.config.recoveryMode"></i>
|
||||
</span>
|
||||
<span ng-switch-default>
|
||||
<i class="fa fa-circle status-error" uib-tooltip="{{ service.status }}"></i>
|
||||
</span>
|
||||
</span>
|
||||
<i class="fa fa-circle-notch fa-spin" ng-hide="service.status"></i>
|
||||
</td>
|
||||
<td class="elide-table-cell">
|
||||
{{ service.displayName }}
|
||||
</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>
|
||||
</td>
|
||||
<td class="elide-table-cell text-center">
|
||||
<span ng-show="service.config.memoryLimit">{{ service.config.memoryLimit | prettyBinarySize }}</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="serviceConfigure.show(service)" uib-tooltip="{{ 'services.configureActionTooltip' | tr }}" ng-disabled="service.status === 'disabled' || !service.config.memoryLimit"><i class="fa fa-pencil-alt"></i></button>
|
||||
<!-- restart is always clickable so that a user can rebuild mongodb in disabled state when using VMs where CPU flags can be dynamically changed -->
|
||||
<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="{{ service.status === 'disabled' ? '' : ('/frontend/logs.html?id=' + service.name) }}" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}" ng-disabled="service.status === 'disabled'"><i class="fa fa-file-alt"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="hasRedisServices" ng-click="redisServicesExpanded = !redisServicesExpanded" class="hand">
|
||||
<td>
|
||||
<i class="fas fa-angle-right" ng-class="{'fa-rotate-90': redisServicesExpanded }"></i>
|
||||
</td>
|
||||
<td colspan="4">redis</td>
|
||||
</tr>
|
||||
<tr ng-show="redisServicesExpanded" ng-repeat="service in services | filter:{ isRedis: true } | orderBy:'name'">
|
||||
<td>
|
||||
<i class="fa fa-circle" uib-tooltip="{{ service.status }}" ng-class="{ 'status-active': service.status === 'active', 'status-starting': service.status === 'starting', 'status-error': (service.status !== 'starting' && service.status !== 'active') }" ng-show="service.status"></i>
|
||||
<i class="fa fa-circle-notch fa-spin" ng-hide="service.status"></i>
|
||||
</td>
|
||||
<td class="elide-table-cell">
|
||||
{{ service.displayName }}
|
||||
</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 }}%"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="elide-table-cell text-center">
|
||||
<span ng-show="service.config.memoryLimit">{{ service.config.memoryLimit | prettyBinarySize }}</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="serviceConfigure.show(service)" uib-tooltip="{{ '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>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,190 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
|
||||
angular.module('Application').controller('ServicesController', ['$scope', '$location', '$timeout', 'Client', function ($scope, $location, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.servicesReady = false;
|
||||
$scope.services = [];
|
||||
$scope.hasRedisServices = false;
|
||||
$scope.redisServicesExpanded = false;
|
||||
$scope.memory = null;
|
||||
|
||||
function refresh(serviceName, callback) {
|
||||
callback = callback || function () {};
|
||||
|
||||
Client.getService(serviceName, function (error, result) {
|
||||
if (error) return console.log('Error getting status of ' + serviceName + ':' + error.message);
|
||||
|
||||
var service = $scope.services.find(function (s) { return s.name === serviceName; });
|
||||
if (!service) callback(new Error('no such service' + serviceName)); // cannot happen
|
||||
|
||||
service.status = result.status;
|
||||
service.config = result.config;
|
||||
service.memoryUsed = result.memoryUsed;
|
||||
service.memoryPercent = result.memoryPercent;
|
||||
service.defaultMemoryLimit = result.defaultMemoryLimit;
|
||||
|
||||
callback(null, service);
|
||||
});
|
||||
}
|
||||
|
||||
function waitForActive(serviceName) {
|
||||
refresh(serviceName, function (error, result) {
|
||||
if (result.status === 'active') return;
|
||||
|
||||
setTimeout(function () { waitForActive(serviceName); }, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.restartService = function (serviceName) {
|
||||
$scope.services.find(function (s) { return s.name === serviceName; }).status = 'starting';
|
||||
|
||||
Client.restartService(serviceName, function (error) {
|
||||
if (error && error.statusCode === 404) {
|
||||
Client.rebuildService(serviceName, function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
// show "busy" indicator for 3 seconds to show some ui activity
|
||||
setTimeout(function () { waitForActive(serviceName); }, 3000);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
refresh(serviceName);
|
||||
return Client.error(error);
|
||||
}
|
||||
|
||||
// show "busy" indicator for 3 seconds to show some ui activity
|
||||
setTimeout(function () { waitForActive(serviceName); }, 3000);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.serviceConfigure = {
|
||||
error: null,
|
||||
busy: false,
|
||||
service: null,
|
||||
|
||||
// form model
|
||||
memoryLimit: 0,
|
||||
memoryTicks: [],
|
||||
|
||||
recoveryMode: false,
|
||||
|
||||
show: function (service) {
|
||||
$scope.serviceConfigure.reset();
|
||||
|
||||
$scope.serviceConfigure.service = service;
|
||||
$scope.serviceConfigure.recoveryMode = !!service.config.recoveryMode;
|
||||
|
||||
$scope.serviceConfigure.memoryTicks = [];
|
||||
// we max system memory and current service memory for the case where the user configured the service on another server with more resources
|
||||
var nearest256m = Math.ceil(Math.max($scope.memory.memory, service.config.memoryLimit) / (256*1024*1024)) * 256 * 1024 * 1024;
|
||||
var startTick = service.defaultMemoryLimit;
|
||||
|
||||
for (var i = startTick; i <= nearest256m; i *= 2) {
|
||||
$scope.serviceConfigure.memoryTicks.push(i);
|
||||
}
|
||||
|
||||
// for firefox widget update
|
||||
$timeout(function() {
|
||||
$scope.serviceConfigure.memoryLimit = service.config.memoryLimit;
|
||||
}, 500);
|
||||
|
||||
$('#serviceConfigureModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.serviceConfigure.busy = true;
|
||||
$scope.serviceConfigure.error = null;
|
||||
|
||||
var data = {
|
||||
memoryLimit: parseInt($scope.serviceConfigure.memoryLimit),
|
||||
recoveryMode: $scope.serviceConfigure.recoveryMode
|
||||
};
|
||||
|
||||
Client.configureService($scope.serviceConfigure.service.name, data, function (error) {
|
||||
$scope.serviceConfigure.busy = false;
|
||||
if (error) {
|
||||
$scope.serviceConfigure.error = error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.serviceConfigure.recoveryMode === true) {
|
||||
refresh($scope.serviceConfigure.service.name);
|
||||
} else {
|
||||
waitForActive($scope.serviceConfigure.service.name);
|
||||
}
|
||||
|
||||
$('#serviceConfigureModal').modal('hide');
|
||||
$scope.serviceConfigure.reset();
|
||||
});
|
||||
},
|
||||
|
||||
resetToDefaults: function () {
|
||||
$scope.serviceConfigure.memoryLimit = $scope.serviceConfigure.service.defaultMemoryLimit;
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
$scope.serviceConfigure.busy = false;
|
||||
$scope.serviceConfigure.error = null;
|
||||
$scope.serviceConfigure.service = null;
|
||||
|
||||
$scope.serviceConfigure.memoryLimit = 0;
|
||||
$scope.serviceConfigure.memoryTicks = [];
|
||||
|
||||
$scope.serviceConfigureForm.$setPristine();
|
||||
$scope.serviceConfigureForm.$setUntouched();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.refreshAll = function (callback) {
|
||||
Client.getServices(function (error, result) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$scope.services = result.map(function (name) {
|
||||
var displayName = name;
|
||||
var isRedis = false;
|
||||
|
||||
if (name.indexOf('redis') === 0) {
|
||||
isRedis = true;
|
||||
var app = Client.getCachedAppSync(name.slice('redis:'.length));
|
||||
if (app) {
|
||||
displayName = 'Redis (' + (app.label || app.fqdn) + ')';
|
||||
} else {
|
||||
displayName = 'Redis (unknown app)';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: name,
|
||||
displayName: displayName,
|
||||
isRedis: isRedis
|
||||
};
|
||||
});
|
||||
$scope.hasRedisServices = !!$scope.services.find(function (service) { return service.isRedis; });
|
||||
|
||||
// just kick off the status fetching
|
||||
$scope.services.forEach(function (s) { refresh(s.name); });
|
||||
|
||||
if (callback) return callback();
|
||||
});
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
Client.memory(function (error, memory) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.memory = memory;
|
||||
|
||||
$scope.refreshAll(function () {
|
||||
$scope.servicesReady = true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}]);
|
||||
@@ -1,364 +0,0 @@
|
||||
<!-- Modal update -->
|
||||
<div class="modal fade" id="updateModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">{{ 'settings.updateDialog.title' | tr }} <b>{{config.update.box.version}}</b> </h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-hide="installedApps | canUpdate">
|
||||
<p>{{ 'settings.updateDialog.blockingApps' | tr }}</p>
|
||||
<ul>
|
||||
<li ng-repeat="app in installedApps | inProgressApps">{{app.fqdn}}</li>
|
||||
</ul>
|
||||
<span>{{ 'settings.updateDialog.blockingAppsInfo' | tr }}</span>
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div ng-show="installedApps | canUpdate">
|
||||
<p class="text-danger" ng-show="config.update.box.unstable">{{ 'settings.updateDialog.unstableWarning' | tr }}</p>
|
||||
<p>{{ 'settings.updateDialog.changes' | tr }}:</p>
|
||||
<ul>
|
||||
<li ng-repeat="change in config.update.box.changelog" ng-bind-html="change | markdown2html"></li>
|
||||
</ul>
|
||||
<br/>
|
||||
<p ng-show="update.error.generic" class="text-danger">{{ update.error.generic }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<label ng-show="installedApps | canUpdate" class="checkbox-inline pull-left">
|
||||
<input type="checkbox" ng-model="update.skipBackup">{{ 'settings.updateDialog.skipBackupCheckbox' | tr }}
|
||||
</label>
|
||||
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn" ng-show="installedApps | canUpdate" ng-class="config.update.box.unstable ? 'btn-danger' : 'btn-success'" ng-click="update.startUpdate()" ng-disabled="update.busy"><i class="fa fa-circle-notch fa-spin" ng-show="update.busy"></i> {{ 'settings.updateDialog.updateAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal update schedule config -->
|
||||
<div class="modal fade" id="updateScheduleModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'settings.updateScheduleDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="updateScheduleForm" role="form" novalidate ng-submit="updateSchedule.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="updateSchedule.error">{{ updateSchedule.error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<p ng-bind-html=" 'settings.updateScheduleDialog.description' | tr "></p>
|
||||
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="updateSchedule.type" value="never"> {{ 'settings.updateScheduleDialog.disableCheckbox' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="updateSchedule.type" value="pattern"> {{ 'settings.updateScheduleDialog.enableCheckbox' | tr }}
|
||||
<span class="label label-danger" ng-show="updateSchedule.type === 'pattern' && !updateSchedule.isScheduleValid()">{{ 'settings.updateScheduleDialog.selectOne' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div style="margin-left: 20px;">
|
||||
<div class="col-md-5">
|
||||
{{ 'settings.updateScheduleDialog.days' | tr }}: <multiselect class="input-sm stretch" ng-model="updateSchedule.days" ng-disabled="updateSchedule.type !== 'pattern'" options="a.name for a in cronDays" data-multiple="true"></multiselect>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
{{ 'settings.updateScheduleDialog.hours' | tr }}: <multiselect class="input-sm stretch" ng-model="updateSchedule.hours" ng-disabled="updateSchedule.type !== 'pattern'" options="a.name for a in cronHours" data-multiple="true"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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="updateSchedule.submit()" ng-disabled="updateSchedule.$invalid || updateSchedule.busy"><i class="fa fa-circle-notch fa-spin" ng-show="updateSchedule.busy"></i><span> {{ 'main.dialog.save' | tr }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal registry config -->
|
||||
<div class="modal fade" id="registryConfigModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'settings.privateDockerRegistryDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="has-error text-center" ng-show="registryConfig.error">{{ registryConfig.error }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="registryConfigProvider">{{ 'settings.registryConfig.provider' | tr }} <sup><a ng-href="https://docs.cloudron.io/settings/#private-docker-registry" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" id="registryConfigProvider" ng-model="registryConfig.provider" ng-options="a.value as a.name for a in registryConfigProviders"></select>
|
||||
</div>
|
||||
|
||||
<div uib-collapse="registryConfig.provider === 'noop'">
|
||||
|
||||
<form name="registryConfigForm" role="form" novalidate ng-submit="registryConfig.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="registryConfigServerAddress">{{ 'settings.privateDockerRegistry.server' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="registryConfig.serverAddress" id="registryConfigServerAddress" name="serveraddress" ng-disabled="registryConfig.busy" placeholder="docker.io" ng-required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="registryConfigUsername">{{ 'settings.privateDockerRegistry.username' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="registryConfig.username" id="registryConfigUsername" name="registryUsername" ng-disabled="registryConfig.busy" ng-required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="registryConfigEmail">{{ 'settings.privateDockerRegistryDialog.email' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="registryConfig.email" id="registryConfigEmail" name="registryEmail" ng-disabled="registryConfig.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="registryConfigPassword">{{ 'settings.privateDockerRegistryDialog.passwordToken' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="registryConfig.password" id="registryConfigPassword" name="registryPassword" ng-disabled="registryConfig.busy" ng-required password-reveal>
|
||||
</div>
|
||||
</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-success" ng-click="registryConfig.submit()" ng-disabled="registryConfigForm.$invalid || registryConfig.busy"><i class="fa fa-circle-notch fa-spin" ng-show="registryConfig.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>{{ 'settings.title' | tr }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="text-left" ng-show="user.isAtLeastOwner && !config.isDemo">
|
||||
<h3>{{ 'settings.appstoreAccount.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="user.isAtLeastOwner && !config.isDemo">
|
||||
<div ng-show="subscriptionBusy" style="height: 155px;">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-center">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="!subscriptionBusy">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">{{ 'settings.appstoreAccount.description' | tr }}</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row" ng-show="!subscription">
|
||||
<div class="col-xs-12 text-center">
|
||||
<a class="btn btn-success" ng-href="/#/appstore">{{ 'settings.appstoreAccount.setupAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="subscription && !subscription.externalCustomer">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'settings.appstoreAccount.email' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<a ng-href="{{ config.consoleServerOrigin }}?email={{ subscription.emailEncoded }}" target="_blank">{{ subscription.email }} <i ng-show="!subscription.emailVerified" class="fas fa-exclamation-triangle text-danger" uib-tooltip="{{ 'settings.appstoreAccount.emailNotVerified' | tr }}"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="subscription">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'settings.appstoreAccount.cloudronId' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ subscription.cloudronId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="subscription">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'settings.appstoreAccount.subscription' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ subscription.plan.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="subscription">
|
||||
<div class="col-xs-12 text-right">
|
||||
<b class="text-danger" ng-show="subscription.cancel_at">{{ 'settings.appstoreAccount.subscriptionEndsAt' | tr }} {{ (subscription.cancel_at*1000) | prettyShortDate }}</b>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row" ng-show="subscription">
|
||||
<div class="col-xs-12">
|
||||
<a class="btn btn-success pull-right" ng-click="openSubscriptionSetup()">{{ subscription.plan.id === 'free' ? ('settings.appstoreAccount.subscriptionSetupAction' | tr) : (subscription.cancel_at ? ('settings.appstoreAccount.subscriptionReactivateAction' | tr) : ('settings.appstoreAccount.subscriptionChangeAction' | tr)) }} </a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>{{ 'settings.timezone.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<p ng-bind-html=" 'settings.timezone.description' | tr:{ timeZone: timeZone.currentTimeZone.display } "></p>
|
||||
<p class="text-danger" ng-show="timeZone.error"><br/>{{ timeZone.error }}</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<multiselect class="pull-right" ng-model="timeZone.timeZone" ng-disabled="timeZone.busy" options="tz.id for tz in timeZone.availableTimeZones" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="timeZone.submit()" ng-disabled="timeZone.busy || timeZone.timeZone === timeZone.currentTimeZone"><i class="fa fa-circle-notch fa-spin" ng-show="timeZone.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>{{ 'settings.language.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<p>{{ 'settings.language.description' | tr }}</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<multiselect class="pull-right" ng-model="language.language" ng-disabled="language.busy" options="lang.display for lang in language.availableLanguages" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="language.submit()" ng-disabled="language.busy || language.language === language.currentLanguage"><i class="fa fa-circle-notch fa-spin" ng-show="language.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<span ng-bind-html=" 'settings.updates.description' | tr "></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-4">
|
||||
<span class="text-muted">{{ 'settings.updates.version' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-8 text-right">
|
||||
v{{ config.version }} ({{ config.ubuntuVersion }})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-4">
|
||||
<span class="text-muted">{{ 'settings.updates.schedule' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-8 text-right" style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;">
|
||||
<span ng-show="updateSchedule.currentPattern !== 'never'">{{ prettyAutoUpdateSchedule(updateSchedule.currentPattern) }}</span>
|
||||
<span ng-show="updateSchedule.currentPattern === 'never'">{{ 'settings.updates.disabled' | tr }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<br/>
|
||||
<div ng-if="update.busy" class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ update.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="update.busy">
|
||||
<div class="col-md-12">
|
||||
<p >{{ update.message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="updateSchedule.show()">{{ 'settings.updates.changeScheduleAction' | tr }}</button>
|
||||
<button class="btn btn-default pull-right" ng-show="(!config.update.box || config.update.box.version === config.version) && !update.busy" ng-disabled="update.checking" ng-click="update.checkNow()"><i class="fa fa-circle-notch fa-spin" ng-show="update.checking"></i> {{ 'settings.updates.checkForUpdatesAction' | tr }}</button>
|
||||
<button ng-class="config.update.box.unstable ? 'btn btn-danger pull-right' : 'btn btn-success pull-right'" ng-show="config.update.box && config.update.box.version !== config.version && !update.busy" ng-click="update.show()">{{ 'settings.updates.updateAvailableAction' | tr }}</button>
|
||||
<button class="btn btn-danger pull-right" ng-show="config.update.box && update.busy" ng-click="update.stopUpdate()">{{ 'settings.updates.stopUpdateAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>{{ 'settings.privateDockerRegistry.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<span ng-bind-html="'settings.privateDockerRegistry.description' | tr:{ customAppsLink: 'https://docs.cloudron.io/custom-apps/tutorial/' }"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row" ng-show="registryConfig.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'settings.privateDockerRegistry.server' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ registryConfig.currentConfig.serverAddress || ('settings.privateDockerRegistry.serverNotSet' | tr) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="registryConfig.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'settings.privateDockerRegistry.username' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ registryConfig.currentConfig.username || registryConfig.currentConfig.email || ('settings.privateDockerRegistry.usernameNotSet' | tr) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<button class="btn btn-primary pull-right" ng-click="registryConfig.show()">{{ 'settings.privateDockerRegistry.configureAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,63 +0,0 @@
|
||||
<div class="content">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>{{ 'support.title' | tr }}</h1>
|
||||
</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" ng-if="troubleshoot">
|
||||
<h3>Troubleshoot</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-if="troubleshoot">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<p>Troubleshooting tools</p>
|
||||
<div>
|
||||
<button class="btn btn-default pull-right" ng-click="repairAll()"><i ng-show="repairAllBusy" class="fa fa-circle-notch fa-spin"></i> Repair All</button>
|
||||
<button class="btn btn-default pull-right" ng-click="updateAll()"><i ng-show="updateAllBusy" class="fa fa-circle-notch fa-spin"></i> Update All</button>
|
||||
</div>
|
||||
<p class="text-small text-warning">{{ troubleshootingMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>{{ 'support.remoteSupport.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row" ng-hide="ready">
|
||||
<h2 class="text-center"><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
<div class="row" ng-show="ready">
|
||||
<div class="col-lg-12">
|
||||
<p>{{ 'support.remoteSupport.description' | tr }}</p>
|
||||
<div>
|
||||
<b>{{ 'support.remoteSupport.warning' | tr }}</b>
|
||||
<br/>
|
||||
<br/>
|
||||
<b class="pull-left text-danger text-bold" ng-show="toggleSshSupportError">{{ toggleSshSupportError }}</b>
|
||||
<button class="btn pull-right" ng-class="!sshSupportEnabled ? 'btn-danger' : 'btn-primary'" ng-click="toggleSshSupport()">{{ sshSupportEnabled ? ('support.remoteSupport.disableAction' | tr) : ('support.remoteSupport.enableAction' | tr) }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,96 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
/* global ISTATES */
|
||||
/* global async */
|
||||
|
||||
angular.module('Application').controller('SupportController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastOwner) $location.path('/'); });
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
|
||||
$scope.toggleSshSupportError = '';
|
||||
$scope.sshSupportEnabled = false;
|
||||
|
||||
$scope.troubleshoot = $location.search().troubleshoot;
|
||||
|
||||
$scope.updateAllBusy = false;
|
||||
$scope.repairAllBusy = false;
|
||||
$scope.troubleshootingMessage = '';
|
||||
|
||||
$scope.updateAll = function () {
|
||||
$scope.updateAllBusy = true;
|
||||
$scope.troubleshootingMessage = '';
|
||||
let count = 0, unstable = 0;
|
||||
|
||||
Client.checkForUpdates(function (error) {
|
||||
if (error) Client.error(error);
|
||||
|
||||
async.eachSeries(Object.keys($scope.config.update), function (appId, iteratorDone) {
|
||||
if ($scope.config.update[appId].unstable) { ++unstable; return iteratorDone(); }
|
||||
|
||||
Client.updateApp(appId, $scope.config.update[appId].manifest, { skipBackup: false }, function (error) {
|
||||
if (error) Client.error(error);
|
||||
else ++count;
|
||||
|
||||
iteratorDone();
|
||||
});
|
||||
}, function () {
|
||||
$scope.troubleshootingMessage = `${count} apps updated. ${unstable} apps with unstable updates skipped.`;
|
||||
$scope.updateAllBusy = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.repairAll = function () {
|
||||
$scope.repairAllBusy = true;
|
||||
$scope.troubleshootingMessage = '';
|
||||
let count = 0;
|
||||
|
||||
Client.refreshInstalledApps(function () {
|
||||
async.eachSeries($scope.installedApps, function (app, iteratorDone) {
|
||||
if (app.installationState !== ISTATES.ERROR) return iteratorDone();
|
||||
|
||||
Client.repairApp(app.id, {}, function (error) {
|
||||
if (error) Client.error(error);
|
||||
else ++count;
|
||||
|
||||
iteratorDone();
|
||||
});
|
||||
}, function () {
|
||||
$scope.troubleshootingMessage = `${count} apps repaired.`;
|
||||
$scope.repairAllBusy = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.toggleSshSupport = function () {
|
||||
$scope.toggleSshSupportError = '';
|
||||
|
||||
Client.enableRemoteSupport(!$scope.sshSupportEnabled, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 412 || error.statusCode === 417) $scope.toggleSshSupportError = error.message;
|
||||
else console.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.sshSupportEnabled = !$scope.sshSupportEnabled;
|
||||
});
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
Client.getRemoteSupport(function (error, enabled) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.sshSupportEnabled = enabled;
|
||||
|
||||
$scope.ready = true;
|
||||
});
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,141 +0,0 @@
|
||||
<div class="container">
|
||||
|
||||
<div class="row">
|
||||
<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>
|
||||
<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 }}
|
||||
<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">
|
||||
<button class="btn btn-sm btn-primary dropdown-toggle" type="button" data-toggle="dropdown">
|
||||
{{ graphs.period | trKeyFromPeriod | tr }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="" ng-click="graphs.setPeriod(6)">{{ 6 | trKeyFromPeriod | tr }}</a></li>
|
||||
<li><a href="" ng-click="graphs.setPeriod(12)">{{ 12 | trKeyFromPeriod | tr }}</a></li>
|
||||
<li><a href="" ng-click="graphs.setPeriod(24)">{{ 24 | trKeyFromPeriod | tr }}</a></li>
|
||||
<li><a href="" ng-click="graphs.setPeriod(24*7)">{{ 24*7 | trKeyFromPeriod | tr }}</a></li>
|
||||
<li><a href="" ng-click="graphs.setPeriod(24*30)">{{ 24*30 | trKeyFromPeriod | tr }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</h3>
|
||||
|
||||
<div class="card" style="min-height: 300px;">
|
||||
<label>{{ 'system.cpuUsage.title' | tr }}</label>
|
||||
<canvas id="graphsCPUChart" style="width: 100%;"></canvas>
|
||||
<div class="text-muted text-center text-small">{{ 'system.cpuUsage.graphSubtext' | tr:{ threshold: '20 %'} }}</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<label>{{ 'system.systemMemory.title' | tr }}</label>
|
||||
<canvas id="graphsSystemMemoryChart" style="width: 100%;"></canvas>
|
||||
<div class="text-muted text-center text-small">{{ 'system.systemMemory.graphSubtext' | tr:{ threshold: '1 GiB'} }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h3 class="graphs-toolbar">
|
||||
{{ 'system.diskUsage.title' | tr }}
|
||||
<span class="small disks-last-updated" ng-show="!disks.busy && disks.ts">Last updated: {{ disks.ts | prettyDate }}</span>
|
||||
<div class="graphs-toolbar-actions">
|
||||
<button class="btn btn-sm btn-default" style="margin-right: 5px;" ng-click="disks.refresh()" ng-disabled="disks.busy || disks.busyRefresh"><i class="fas fa-sync-alt" ng-class="{ 'fa-spin': disks.busyRefresh }"></i></button>
|
||||
</div>
|
||||
</h3>
|
||||
|
||||
<div class="card">
|
||||
<div class="row" ng-show="disks.busy">
|
||||
<div class="col-md-12 text-center">
|
||||
<h2 style="margin: 60px 0;"><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="!disks.busy && !disks.ts">
|
||||
<div class="col-md-12 text-center">
|
||||
<button class="btn btn-primary" style="margin: 60px 0;" ng-click="disks.refresh()" ng-disabled="disks.busyRefresh"><i class="fas fa-sync-alt fa-spin" ng-show="disks.busyRefresh"></i> Analyze Disk</button>
|
||||
</div>
|
||||
</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>
|
||||
<div class="text-muted" style="white-space:nowrap;" ng-show="disk.available && disk.size" ng-bind-html="'system.diskUsage.usedInfo' | tr:{ used: (disk.used | prettyDiskSize), size: (disk.size | prettyDiskSize) }"></div>
|
||||
<div class="text-muted" style="white-space:nowrap;" ng-hide="disk.available && disk.size">{{ 'system.diskUsage.notAvailableYet' | tr }}</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<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>
|
||||
<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 }}</span>
|
||||
<span ng-show="content.type === 'swap'">{{ content.label }}</span>
|
||||
<span ng-show="content.type === 'app'">
|
||||
<a href="/#/app/{{ content.app.id }}/storage" ng-hide="content.uninstalled">{{ content.label }}</a>
|
||||
<span ng-show="content.uninstalled">{{ 'system.diskUsage.uninstalledApp' | tr }}</span>
|
||||
</span>
|
||||
<span ng-show="content.type === 'volume'"><a href="/#/volumes">{{ content.label }}</a></span>
|
||||
<small class="text-muted">{{ content.usage | prettyDiskSize }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,368 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
/* global TASK_TYPES */
|
||||
/* global Chart */
|
||||
|
||||
angular.module('Application').controller('SystemController', ['$scope', '$location', '$timeout', 'Client', function ($scope, $location, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.memory = null;
|
||||
$scope.cpus = null;
|
||||
$scope.info = null;
|
||||
$scope.volumesById = {};
|
||||
|
||||
// https://stackoverflow.com/questions/1484506/random-color-generator
|
||||
function rainbow(numOfSteps, step) {
|
||||
// This function generates vibrant, "evenly spaced" colours (i.e. no clustering). This is ideal for creating easily distinguishable vibrant markers in Google Maps and other apps.
|
||||
// Adam Cole, 2011-Sept-14
|
||||
// HSV to RBG adapted from: http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
|
||||
var r, g, b;
|
||||
var h = step / numOfSteps;
|
||||
var i = ~~(h * 6);
|
||||
var f = h * 6 - i;
|
||||
var q = 1 - f;
|
||||
switch(i % 6){
|
||||
case 0: r = 1; g = f; b = 0; break;
|
||||
case 1: r = q; g = 1; b = 0; break;
|
||||
case 2: r = 0; g = 1; b = f; break;
|
||||
case 3: r = 0; g = q; b = 1; break;
|
||||
case 4: r = f; g = 0; b = 1; break;
|
||||
case 5: r = 1; g = 0; b = q; break;
|
||||
}
|
||||
var c = '#' + ('00' + (~ ~(r * 255)).toString(16)).slice(-2) + ('00' + (~ ~(g * 255)).toString(16)).slice(-2) + ('00' + (~ ~(b * 255)).toString(16)).slice(-2);
|
||||
return (c);
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array
|
||||
function shuffle(a) {
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[a[i], a[j]] = [a[j], a[i]];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
var colorIndex = 0;
|
||||
var colors = [];
|
||||
function resetColors(n) {
|
||||
colorIndex = 0;
|
||||
colors = [];
|
||||
for (var i = 0; i < n; i++) colors.push(rainbow(n, i));
|
||||
shuffle(colors);
|
||||
}
|
||||
|
||||
function getNextColor() {
|
||||
return colors[colorIndex++];
|
||||
}
|
||||
|
||||
$scope.disks = {
|
||||
busy: true,
|
||||
busyRefresh: false,
|
||||
ts: 0,
|
||||
taskId: '',
|
||||
disks: [],
|
||||
|
||||
show: function () {
|
||||
Client.diskUsage(function (error, result) {
|
||||
if (error) return console.error('Failed to refresh disk usage.', error);
|
||||
|
||||
if (!result.usage) {
|
||||
$scope.disks.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.disks.ts = result.usage.ts;
|
||||
|
||||
// [ { filesystem, type, size, used, available, capacity, mountpoint }]
|
||||
$scope.disks.disks = Object.keys(result.usage.disks).map(function (k) { return result.usage.disks[k]; });
|
||||
|
||||
$scope.disks.disks.forEach(function (disk) {
|
||||
var usageOther = disk.used;
|
||||
|
||||
resetColors(disk.contents.length);
|
||||
|
||||
// if this disk is a volume amend it and remove it from contents
|
||||
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 content.label = content.app.label || content.app.fqdn;
|
||||
} else if (content.type === 'volume') {
|
||||
content.volume = $scope.volumesById[content.id];
|
||||
content.label = content.volume ? content.volume.name : 'Removed volume';
|
||||
}
|
||||
|
||||
// ensure a label for ui
|
||||
content.label = content.label || content.id;
|
||||
|
||||
usageOther -= content.usage;
|
||||
});
|
||||
|
||||
disk.contents.sort(function (x, y) { return y.usage - x.usage; }); // sort by usage
|
||||
|
||||
if ($scope.disks.disks[0] === disk) { // the root mount point is the first disk. keep this 'contains' in the end
|
||||
disk.contents.push({
|
||||
type: 'standard',
|
||||
label: 'Everything else (Ubuntu, etc)',
|
||||
id: 'other',
|
||||
color: '#555555',
|
||||
usage: usageOther
|
||||
});
|
||||
} else {
|
||||
disk.contents.push({
|
||||
type: 'standard',
|
||||
label: 'Used',
|
||||
id: 'other',
|
||||
color: '#555555',
|
||||
usage: usageOther
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$scope.disks.busy = false;
|
||||
});
|
||||
},
|
||||
|
||||
checkStatus: function () {
|
||||
Client.getLatestTaskByType(TASK_TYPES.TASK_UPDATE_DISK_USAGE, function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!task) return;
|
||||
|
||||
$scope.disks.taskId = task.id;
|
||||
$scope.disks.busyRefresh = true;
|
||||
$scope.disks.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
Client.getTask($scope.disks.taskId, function (error, data) {
|
||||
if (error) return $timeout($scope.disks.updateStatus, 3000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.disks.busyRefresh = false;
|
||||
$scope.disks.taskId = '';
|
||||
$scope.disks.show();
|
||||
return;
|
||||
}
|
||||
|
||||
$timeout($scope.disks.updateStatus, 3000);
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
$scope.disks.busyRefresh = true;
|
||||
|
||||
Client.refreshDiskUsage(function (error, taskId) {
|
||||
if (error) {
|
||||
$scope.disks.busyRefresh = false;
|
||||
return console.error('Failed to refresh disk usage.', error);
|
||||
}
|
||||
|
||||
$scope.disks.taskId = taskId;
|
||||
$timeout($scope.disks.updateStatus, 3000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.graphs = {
|
||||
busy: false,
|
||||
period: 6,
|
||||
memoryChart: null,
|
||||
diskChart: null,
|
||||
|
||||
setPeriod: function (hours) {
|
||||
$scope.graphs.period = hours;
|
||||
$scope.graphs.refresh();
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
$scope.graphs.busy = true;
|
||||
|
||||
Client.getSystemGraphs($scope.graphs.period * 60, function (error, result) {
|
||||
if (error) return console.error('Failed to fetch system graphs:', error);
|
||||
|
||||
var cpuCount = result.cpuCount;
|
||||
|
||||
// in minutes
|
||||
var timePeriod = $scope.graphs.period * 60;
|
||||
|
||||
// keep in sync with graphs.js
|
||||
var timeBucketSizeMinutes = timePeriod > (24 * 60) ? (6*60) : 5;
|
||||
var steps = Math.floor(timePeriod/timeBucketSizeMinutes);
|
||||
|
||||
var labels = new Array(steps).fill(0);
|
||||
labels = labels.map(function (v, index) {
|
||||
var dateTime = new Date(Date.now() - ((timePeriod - (index * timeBucketSizeMinutes)) * 60 * 1000));
|
||||
|
||||
if ($scope.graphs.period > 24) {
|
||||
return dateTime.toLocaleDateString();
|
||||
} else {
|
||||
return dateTime.toLocaleTimeString();
|
||||
}
|
||||
});
|
||||
|
||||
function fillGraph(canvasId, contents, chartPropertyName, divisor, max, format, formatDivisor) {
|
||||
if (!contents || !contents[0]) return; // no data available yet
|
||||
|
||||
var datasets = [];
|
||||
|
||||
resetColors(contents.length);
|
||||
contents.forEach(function (content, index) {
|
||||
|
||||
// fill holes with previous value
|
||||
var cur = 0;
|
||||
content.data.forEach(function (d) {
|
||||
if (d[0] === null) d[0] = cur;
|
||||
else cur = d[0];
|
||||
});
|
||||
|
||||
var datapoints = Array(steps).map(function () { return '0'; });
|
||||
|
||||
// walk backwards and fill up the datapoints
|
||||
content.data.reverse().forEach(function (d, index) {
|
||||
datapoints[datapoints.length-1-index] = (d[0] / divisor).toFixed(2);
|
||||
});
|
||||
|
||||
var color = index === 0 ? '#2196F3' : getNextColor();
|
||||
datasets.push({
|
||||
label: content.label,
|
||||
backgroundColor: color + '4F',
|
||||
borderColor: color, // FIXME give real distinct colors
|
||||
borderWidth: 1,
|
||||
radius: 0,
|
||||
data: datapoints,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
tension: 0.4
|
||||
});
|
||||
});
|
||||
|
||||
var graphData = {
|
||||
labels: labels,
|
||||
datasets: datasets
|
||||
};
|
||||
|
||||
var options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
aspectRatio: 2.5,
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { autoSkipPadding: 50, maxRotation: 0 }
|
||||
},
|
||||
y: {
|
||||
ticks: { maxTicksLimit: 6 },
|
||||
min: 0,
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (format) options.scales.y.ticks.callback = function (value) { return (formatDivisor ? (value/formatDivisor).toFixed(0) : value) + ' ' + format; };
|
||||
if (max) options.scales.y.max = max;
|
||||
|
||||
var ctx = $(canvasId).get(0).getContext('2d');
|
||||
|
||||
if ($scope.graphs[chartPropertyName]) $scope.graphs[chartPropertyName].destroy();
|
||||
$scope.graphs[chartPropertyName] = new Chart(ctx, { type: 'line', data: graphData, options: options });
|
||||
}
|
||||
|
||||
var cpuThreshold = 20;
|
||||
var appsWithHighCPU = Object.keys(result.apps).map(function (appId) {
|
||||
result.apps[appId].id = appId;
|
||||
|
||||
var app = Client.getInstalledAppsByAppId()[appId];
|
||||
if (!app) result.apps[appId].label = appId;
|
||||
else result.apps[appId].label = app.label || app.fqdn;
|
||||
|
||||
return result.apps[appId];
|
||||
}).filter(function (app) {
|
||||
if (!app.cpu) return false; // not sure why we get empty objects
|
||||
return app.cpu.some(function (d) { return d[0] > cpuThreshold; });
|
||||
}).map(function (app) {
|
||||
return { data: app.cpu, label: app.label };
|
||||
});
|
||||
|
||||
var memoryThreshold = 1024 * 1024 * 1024;
|
||||
var appsWithHighMemory = Object.keys(result.apps).map(function (appId) {
|
||||
result.apps[appId].id = appId;
|
||||
|
||||
var app = Client.getInstalledAppsByAppId()[appId];
|
||||
if (!app) result.apps[appId].label = appId;
|
||||
else result.apps[appId].label = app.label || app.fqdn;
|
||||
|
||||
return result.apps[appId];
|
||||
}).filter(function (app) {
|
||||
if (!app.memory) return false; // not sure why we get empty objects
|
||||
return app.memory.some(function (d) { return d[0] > memoryThreshold; });
|
||||
}).map(function (app) {
|
||||
return { data: app.memory, label: app.label };
|
||||
});
|
||||
|
||||
fillGraph('#graphsCPUChart', [{ data: result.cpu, label: 'CPU' }].concat(appsWithHighCPU), 'cpuChart', 1, cpuCount * 100, '%');
|
||||
fillGraph('#graphsSystemMemoryChart', [{ data: result.memory, label: 'Memory' }].concat(appsWithHighMemory), 'memoryChart', 1024 * 1024, Number.parseInt($scope.memory.memory / 1024 / 1024), 'GiB', 1024);
|
||||
|
||||
$scope.graphs.busy = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
$scope.memory = memory;
|
||||
|
||||
Client.getVolumes(function (error, volumes) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.volumesById = {};
|
||||
volumes.forEach(function (v) { $scope.volumesById[v.id] = v; });
|
||||
|
||||
$scope.graphs.refresh();
|
||||
|
||||
$scope.disks.show();
|
||||
$scope.disks.checkStatus();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Client.onReconnect(function () {
|
||||
$scope.reboot.busy = false;
|
||||
});
|
||||
}]);
|
||||
@@ -1,539 +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-directory/#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>
|
||||
|
||||
<p class="text-small text-warning" ng-show="externalLdap.provider === 'noop' && externalLdap.currentConfig.provider !== 'noop'">
|
||||
{{ 'users.externalLdap.disableWarning' | tr }}
|
||||
</p>
|
||||
|
||||
<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.saveBusy"><i class="fa fa-circle-notch fa-spin" ng-show="externalLdap.saveBusy"></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-directory/#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 }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="externalLdap.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 externalLdap.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 card-large">
|
||||
<div class="row">
|
||||
<div class="col-md-12">{{ 'users.externalLdap.description' | tr }}</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<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.busy" 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-12">
|
||||
<p ng-show="externalLdap.busy">{{ externalLdap.message }}</p>
|
||||
<p ng-hide="externalLdap.busy">
|
||||
<div class="has-error" ng-show="!externalLdap.active">{{ externalLdap.errorMessage }}</div>
|
||||
</p>
|
||||
<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 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-directory/#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>
|
||||
<p class="text-small text-warning text-bold" ng-show="adminDomain.provider === 'cloudflare'">{{ 'users.exposedLdap.cloudflarePortWarning' | tr }} </p>
|
||||
</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" ng-bind-html=" '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 }} <sup><a ng-href="https://docs.cloudron.io/user-directory/#endpoints" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></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,486 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global Clipboard */
|
||||
/* global $, TASK_TYPES */
|
||||
|
||||
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.adminDomain = null;
|
||||
$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 () {
|
||||
$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;
|
||||
|
||||
// prevent the current user from getting locked out. if user ignores this, they have to use cloudron-support --admin-login
|
||||
if ($scope.profileConfig.mandatory2FA && !$scope.userInfo.twoFactorAuthenticationEnabled) {
|
||||
if ($scope.userInfo.source && $scope.config.external2FA) return; // no need for warning if 2fa is external
|
||||
Client.notify('', $translate.instant('users.settings.require2FAWarning'), true, 'error', '#/profile');
|
||||
}
|
||||
}, 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: '', // last task error
|
||||
tasks: [],
|
||||
|
||||
error: {}, // save error
|
||||
saveBusy: false,
|
||||
|
||||
// fields
|
||||
provider: 'noop',
|
||||
autoCreate: true,
|
||||
url: '',
|
||||
acceptSelfSignedCerts: false,
|
||||
baseDn: '',
|
||||
filter: '',
|
||||
groupBaseDn: '',
|
||||
bindDn: '',
|
||||
bindPassword: '',
|
||||
usernameField: '',
|
||||
|
||||
currentConfig: {},
|
||||
|
||||
init: function () {
|
||||
Client.getExternalLdapConfig(function (error, result) {
|
||||
if (error) return console.error('Unable to get external ldap config.', error);
|
||||
$scope.externalLdap.currentConfig = result;
|
||||
$scope.externalLdap.refreshTasks();
|
||||
});
|
||||
},
|
||||
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_SYNC_EXTERNAL_LDAP, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
$scope.externalLdap.tasks = tasks.slice(0, 10);
|
||||
if ($scope.externalLdap.tasks.length && $scope.externalLdap.tasks[0].active) $scope.externalLdap.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
var taskId = $scope.externalLdap.tasks[0].id;
|
||||
|
||||
Client.getTask(taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.externalLdap.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.externalLdap.busy = false;
|
||||
$scope.externalLdap.message = '';
|
||||
$scope.externalLdap.percent = 100; // indicates that 'result' is valid
|
||||
$scope.externalLdap.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
$scope.externalLdap.refreshTasks(); // update the tasks list dropdown
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.externalLdap.busy = true;
|
||||
$scope.externalLdap.percent = data.percent;
|
||||
$scope.externalLdap.message = data.message;
|
||||
window.setTimeout($scope.externalLdap.updateStatus, 500);
|
||||
});
|
||||
},
|
||||
|
||||
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');
|
||||
},
|
||||
|
||||
sync: function () {
|
||||
$scope.externalLdap.busy = true;
|
||||
$scope.externalLdap.percent = 0;
|
||||
$scope.externalLdap.message = '';
|
||||
$scope.externalLdap.errorMessage = '';
|
||||
|
||||
Client.startExternalLdapSync(function (error) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.externalLdap.errorMessage = error.message;
|
||||
$scope.externalLdap.busy = false;
|
||||
} else {
|
||||
$scope.externalLdap.refreshTasks();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.externalLdap.saveBusy = 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.saveBusy = 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;
|
||||
$scope.externalLdap.error.generic = error.message;
|
||||
} 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.init();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$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.init();
|
||||
$scope.profileConfig.refresh();
|
||||
$scope.userDirectoryConfig.refresh();
|
||||
$scope.refreshOIDCClients();
|
||||
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) return console.error('Unable to list domains.', error);
|
||||
$scope.adminDomain = result.filter(function (d) { return d.domain === $scope.config.adminDomain; })[0];
|
||||
});
|
||||
});
|
||||
|
||||
// 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,533 +0,0 @@
|
||||
<!-- Modal add user -->
|
||||
<div class="modal fade" id="userAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'users.addUserDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="useraddForm" role="form" ng-submit="userAdd.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (useraddForm.displayName.$dirty && useraddForm.displayName.$invalid) || (!useraddForm.displayName.$dirty && userAdd.error.displayName) }">
|
||||
<label class="control-label">{{ 'users.user.fullName' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="userAdd.displayName" name="displayName" id="inputUserAddDisplayName" autofocus autocomplete="off" placeholder="{{ 'users.user.displayNamePlaceholder' | tr }}">
|
||||
<div class="control-label" ng-show="(!useraddForm.displayName.$dirty && userAdd.error.displayName) || (useraddForm.displayName.$dirty && useraddForm.displayName.$invalid) || (!useraddForm.displayName.$dirty && userAdd.error.displayName)">
|
||||
<small ng-show="useraddForm.displayName.$error.displayName">{{ 'users.user.errorNotValidFullName' | tr }}</small>
|
||||
<small ng-show="!useraddForm.displayName.$dirty && userAdd.error.displayName">{{ userAdd.error.displayName }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (useraddForm.email.$dirty && useraddForm.email.$invalid) || (!useraddForm.email.$dirty && userAdd.error.email) }">
|
||||
<label class="control-label">{{ 'users.user.primaryEmail' | tr }} <sup><a ng-href="https://docs.cloudron.io/profile/#primary-email" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></label>
|
||||
<input type="email" class="form-control" ng-model="userAdd.email" name="email" id="inputUserAddEmail" required>
|
||||
<div class="control-label" ng-show="(!useraddForm.email.$dirty && userAdd.error.email) || (useraddForm.email.$dirty && useraddForm.email.$invalid) || (!useraddForm.email.$dirty && userAdd.error.email)">
|
||||
<small ng-show="useraddForm.email.$error.required">{{ 'users.user.errorEmailRequired' | tr }}</small>
|
||||
<small ng-show="useraddForm.email.$error.email">{{ 'users.user.errorInvalidEmail' | tr }}</small>
|
||||
<small ng-show="!useraddForm.email.$dirty && userAdd.error.email">{{ userAdd.error.email }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (useraddForm.fallbackEmail.$dirty && useraddForm.fallbackEmail.$invalid) || (!useraddForm.fallbackEmail.$dirty && userAdd.error.fallbackEmail) }">
|
||||
<label class="control-label">{{ 'users.user.recoveryEmail' | tr }} <sup><a ng-href="https://docs.cloudron.io/profile/#password-recovery-email" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></label>
|
||||
<input type="email" class="form-control" ng-model="userAdd.fallbackEmail" name="fallbackEmail" id="inputUserAddFallbackEmail" placeholder="{{ 'users.user.fallbackEmailPlaceholder' | tr }}">
|
||||
<div class="control-label" ng-show="(!useraddForm.fallbackEmail.$dirty && userAdd.error.fallbackEmail) || (useraddForm.fallbackEmail.$dirty && useraddForm.fallbackEmail.$invalid) || (!useraddForm.fallbackEmail.$dirty && userAdd.error.fallbackEmail)">
|
||||
<small ng-show="useraddForm.fallbackEmail.$error.required">{{ 'users.user.errorEmailRequired' | tr }}</small>
|
||||
<small ng-show="useraddForm.fallbackEmail.$error.fallbackEmail">{{ 'users.user.errorInvalidEmail' | tr }}</small>
|
||||
<small ng-show="!useraddForm.fallbackEmail.$dirty && userAdd.error.fallbackEmail">{{ userAdd.error.fallbackEmail }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (useraddForm.username.$dirty && useraddForm.username.$invalid) || (!useraddForm.username.$dirty && userAdd.error.username) }">
|
||||
<label class="control-label">{{ 'users.user.username' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="userAdd.username" name="username" id="inputUserAddUsername" ng-required="config.profileLocked" placeholder="{{ config.profileLocked ? '' : ('users.user.usernamePlaceholder' | tr) }}">
|
||||
<div class="control-label" ng-show="(!useraddForm.username.$dirty && userAdd.error.username) || (useraddForm.username.$dirty && useraddForm.username.$invalid) || (!useraddForm.username.$dirty && userAdd.error.username)">
|
||||
<small ng-show="useraddForm.username.$error.username">{{ 'users.user.errorInvalidUsername' | tr }}</small>
|
||||
<small ng-show="!useraddForm.username.$dirty && userAdd.error.username">{{ userAdd.error.username }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="userInfo.isAtLeastAdmin">
|
||||
<label class="control-label">{{ 'users.user.role' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#roles" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<div class="control-label">
|
||||
<select class="form-control" ng-model="userAdd.role" ng-options="role.id as role.name disable when role.disabled for role in roles"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'users.user.groups' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<div ng-show="groups.length === 0">{{ 'users.user.noGroups' | tr }}</div>
|
||||
<!-- local groups. they can have local and external users . angular cannot filter empty strings - https://github.com/angular/angular.js/issues/7890 -->
|
||||
<multiselect ng-show="groups.length !== 0" ng-model="userAdd.selectedLocalGroups" options="group.name for group in groups | filter:{ source: '!ldap' }" data-compare-by="name" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="userAdd.sendInvite" id="inputUserAddSendInvite"> {{ 'users.addUserDialog.sendInviteCheckbox' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="useraddForm.$invalid || userAdd.busy"/>
|
||||
</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="userAdd.submit()" ng-disabled="useraddForm.$invalid || userAdd.busy"><i class="fa fa-circle-notch fa-spin" ng-show="userAdd.busy"></i> {{ 'users.addUserDialog.addUserAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal remove user -->
|
||||
<div class="modal fade" id="userRemoveModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'users.deleteUserDialog.title' | tr:{ username: (userRemove.userInfo.username || userRemove.userInfo.email) } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-bold text-danger" ng-show="userRemove.error">{{ userRemove.error }}</p>
|
||||
<p ng-hide="userRemove.error">{{ 'users.deleteUserDialog.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="userRemove.submit()" ng-hide="userRemove.error" ng-disabled="userRemove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="userRemove.busy"></i> {{ 'users.deleteUserDialog.deleteAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal edit user -->
|
||||
<div class="modal fade" id="userEditModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'users.editUserDialog.title' | tr:{ username: (userEdit.userInfo.username || userEdit.userInfo.email) } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-show="userEdit.source">
|
||||
<p class="text-warning">{{ 'users.editUserDialog.externalLdapWarning' | tr }}</p>
|
||||
<p><label>{{ 'users.user.displayName' | tr }}</label><br/><input type="text" class="form-control" ng-disabled="true" ng-model="userEdit.displayName">
|
||||
<p><label>{{ 'users.user.email' | tr }}</label><br/><input type="text" class="form-control" ng-disabled="true" ng-model="userEdit.email"></p>
|
||||
</div>
|
||||
|
||||
<form name="useredit_form" role="form" ng-submit="userEdit.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="userEdit.error.generic">{{ userEdit.error.generic }}</p>
|
||||
|
||||
<!-- when user profiles are locked, this provides a way for the admin to set the username -->
|
||||
<div class="form-group" ng-hide="userEdit.source || userEdit.userInfo.username" ng-class="{ 'has-error': (useredit_form.username.$dirty && useredit_form.username.$invalid) || (!useredit_form.username.$dirty && userEdit.error.username) }">
|
||||
<label class="control-label">{{ 'users.user.username' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!useredit_form.username.$dirty && userEdit.error.username) || (useredit_form.username.$dirty && useredit_form.username.$invalid) || (!useredit_form.username.$dirty && userEdit.error.username)">
|
||||
<small ng-show="!useredit_form.username.$dirty && userEdit.error.username">{{ userEdit.error.username }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="userEdit.username" name="username" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group" ng-hide="userEdit.source" ng-class="{ 'has-error': (useredit_form.displayName.$dirty && useredit_form.displayName.$invalid) || (!useredit_form.displayName.$dirty && userEdit.error.displayName) }">
|
||||
<label class="control-label">{{ 'users.user.displayName' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!useredit_form.displayName.$dirty && userEdit.error.displayName) || (useredit_form.displayName.$dirty && useredit_form.displayName.$invalid) || (!useredit_form.displayName.$dirty && userEdit.error.displayName)">
|
||||
<small ng-show="useredit_form.displayName.$error.required">{{ 'users.user.errorDisplayNameRequired' | tr }}</small>
|
||||
<small ng-show="!useredit_form.displayName.$dirty && userEdit.error.displayName">{{ userEdit.error.displayName }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="userEdit.displayName" name="displayName" required autofocus autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group" ng-hide="userEdit.source" ng-class="{ 'has-error': (useredit_form.email.$dirty && useredit_form.email.$invalid) || (!useredit_form.email.$dirty && userEdit.error.email) }">
|
||||
<label class="control-label">{{ 'users.user.primaryEmail' | tr }} <sup><a ng-href="https://docs.cloudron.io/profile/#primary-email" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></label>
|
||||
<div class="control-label" ng-show="(!useredit_form.email.$dirty && userEdit.error.email) || (useredit_form.email.$dirty && useredit_form.email.$invalid) || (!useredit_form.email.$dirty && userEdit.error.email)">
|
||||
<small ng-show="useredit_form.email.$error.required">{{ 'users.user.errorEmailRequired' | tr }}</small>
|
||||
<small ng-show="useredit_form.email.$error.email">{{ 'users.user.errorInvalidEmail' | tr }}</small>
|
||||
<small ng-show="!useredit_form.email.$dirty && userEdit.error.email">{{ userEdit.error.email }}</small>
|
||||
</div>
|
||||
<input type="email" class="form-control" ng-model="userEdit.email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group" ng-hide="userEdit.source" ng-class="{ 'has-error': (useredit_form.fallbackEmail.$dirty && useredit_form.fallbackEmail.$invalid) || (!useredit_form.fallbackEmail.$dirty && userEdit.error.fallbackEmail) }">
|
||||
<label class="control-label">{{ 'users.user.recoveryEmail' | tr }} <sup><a ng-href="https://docs.cloudron.io/profile/#password-recovery-email" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></label>
|
||||
<div class="control-label" ng-show="(!useredit_form.fallbackEmail.$dirty && userEdit.error.fallbackEmail) || (useredit_form.fallbackEmail.$dirty && useredit_form.fallbackEmail.$invalid) || (!useredit_form.fallbackEmail.$dirty && userEdit.error.fallbackEmail)">
|
||||
<small ng-show="useredit_form.fallbackEmail.$error.fallbackEmail">{{ 'users.user.errorInvalidEmail' | tr }}</small>
|
||||
<small ng-show="!useredit_form.fallbackEmail.$dirty && userEdit.error.fallbackEmail">{{ userEdit.error.fallbackEmail }}</small>
|
||||
</div>
|
||||
<input type="fallbackEmail" class="form-control" ng-model="userEdit.fallbackEmail" name="fallbackEmail">
|
||||
</div>
|
||||
<div class="form-group" ng-show="!isMe(userEdit.userInfo) && userInfo.isAtLeastAdmin">
|
||||
<label class="control-label">{{ 'users.user.role' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#roles" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<div class="control-label">
|
||||
<select class="form-control" ng-model="userEdit.role" ng-options="role.id as role.name disable when role.disabled for role in roles"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'users.user.groups' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<div ng-switch on="groups.length">
|
||||
<div ng-switch-when="0">{{ 'users.user.noGroups' | tr }}</div>
|
||||
<div ng-switch-default>
|
||||
<!-- local groups. they can have local and external users . angular cannot filter empty strings - https://github.com/angular/angular.js/issues/7890 -->
|
||||
<multiselect ng-show="hasLocalGroups" ng-model="userEdit.selectedLocalGroups" options="group.name for group in groups | filter:{ source: '!ldap' }" data-compare-by="id" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="userEdit.externalGroups.length">
|
||||
<!-- remote groups. cannot be edited -->
|
||||
<label class="control-label">{{ 'users.user.ldapGroups' | tr }}</label>
|
||||
<div><span ng-repeat="group in userEdit.externalGroups">{{ group.name }}</span></div>
|
||||
</div>
|
||||
<div class="form-group" ng-hide="isMe(userEdit.userInfo)">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="userEdit.active"> {{ 'users.user.activeCheckbox' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#disable-user" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="useredit_form.$invalid || userEdit.busy"/>
|
||||
</form>
|
||||
<hr/>
|
||||
<div ng-hide="userEdit.source && config.external2FA">
|
||||
<p ng-hide="userEdit.userInfo.twoFactorAuthenticationEnabled">{{ 'users.passwordResetDialog.no2FASetup' | tr }}</p>
|
||||
<p ng-show="userEdit.userInfo.twoFactorAuthenticationEnabled">{{ 'users.passwordResetDialog.2FAIsSetup' | tr }}</p>
|
||||
<button type="button" class="btn btn-danger" ng-click="userEdit.reset2FA()" ng-disabled="!userEdit.userInfo.twoFactorAuthenticationEnabled || userEdit.reset2FABusy"><i class="fa fa-circle-notch fa-spin" ng-show="userEdit.reset2FABusy"></i> {{ 'users.passwordResetDialog.reset2FAAction' | tr }}</button>
|
||||
</div>
|
||||
<div ng-show="userEdit.source && config.external2FA"> {{ 'users.user.external2FA' | tr }}</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-success" ng-click="userEdit.submit()" ng-disabled="useredit_form.$invalid || userEdit.busy"><i class="fa fa-circle-notch fa-spin" ng-show="userEdit.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal add group -->
|
||||
<div class="modal fade" id="groupAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'users.addGroupDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="groupAddForm" role="form" novalidate ng-submit="groupAdd.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (groupAddForm.name.$dirty && groupAddForm.name.$invalid) || (!groupAddForm.name.$dirty && groupAdd.error.name) }">
|
||||
<label class="control-label" for="groupAddName">{{ 'users.group.name' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!groupAddForm.name.$dirty && groupAdd.error.name) || (groupAddForm.name.$dirty && groupAddForm.name.$invalid) || (!groupAddForm.name.$dirty && groupAdd.error.name)">
|
||||
<small ng-show="groupAddForm.name.$error.required">{{ 'users.group.errorNameRequired' | tr }}</small>
|
||||
<small ng-show="groupAddForm.name.$error.minlength">{{ 'users.group.errorNameTooShort' | tr }}</small>
|
||||
<small ng-show="groupAddForm.name.$error.maxlength">{{ 'users.group.errorNameTooLong' | tr }}</small>
|
||||
<small ng-show="!groupAddForm.name.$dirty && groupAdd.error.name">{{ groupAdd.error.name }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="groupAdd.name" id="groupAddName" name="name" ng-maxlength="200" ng-minlength="1" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'users.group.users' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<multiselect ng-model="groupAdd.selectedUsers" options="(user.username || user.email) for user in allUsers" data-compare-by="email" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="groupAddForm.$invalid || groupAdd.busy"/>
|
||||
</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="groupAdd.submit()" ng-disabled="groupAddForm.$invalid || groupAdd.busy"><i class="fa fa-circle-notch fa-spin" ng-show="groupAdd.busy"></i> {{ 'users.group.addGroupAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal edit group -->
|
||||
<div class="modal fade" id="groupEditModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'users.editGroupDialog.title' | tr:{ name: groupEdit.groupInfo.name } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-warning" ng-show="groupEdit.source">{{ 'users.editGroupDialog.externalLdapWarning' | tr }}</p>
|
||||
|
||||
<form name="groupEdit_form" role="form" ng-submit="groupEdit.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': groupEditForm.groupName.$invalid }">
|
||||
<label class="control-label">{{ 'users.group.name' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="groupEdit.name" name="groupName" ng-disabled="groupEdit.busy || groupEdit.source" autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'users.group.users' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<multiselect ng-hide="groupEdit.source" ng-model="groupEdit.selectedUsers" ng-disabled="groupEdit.busy" options="(user.username || user.email) for user in allUsers" data-compare-by="email" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<div ng-show="groupEdit.source"><span ng-repeat="user in groupEdit.selectedUsers"> {{ (user.username || user.email) }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Access to Apps</label>
|
||||
<div class="control-label">
|
||||
<multiselect ng-model="groupEdit.selectedApps" options="(app.label || app.fqdn) for app in groupEdit.apps" data-compare-by="fqdn" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="groupEdit_form.$invalid || groupEdit.busy"/>
|
||||
</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="groupEdit.submit()" ng-disabled="groupEdit_form.$invalid || groupEdit.busy"><i class="fa fa-circle-notch fa-spin" ng-show="groupEdit.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal remove group -->
|
||||
<div class="modal fade" id="groupRemoveModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'users.deleteGroupDialog.title' | tr:{ name: groupRemove.group.name } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-show="groupRemove.memberCount" class="text-danger">
|
||||
<b>{{ 'users.deleteGroupDialog.description' | tr:{ memberCount: groupRemove.memberCount } }}</b>
|
||||
<br/>
|
||||
<br/>
|
||||
</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-danger" ng-click="groupRemove.submit()" ng-disabled="groupRemove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="groupRemove.busy"></i> {{ 'users.deleteGroupDialog.deleteAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal password reset -->
|
||||
<div class="modal fade" id="passwordResetModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'users.passwordResetDialog.title' | tr:{ username: (passwordReset.user.username || passwordReset.user.email) } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'users.passwordResetDialog.descriptionLink' | tr }}</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="passwordResetLinkInput" class="form-control" ng-value="passwordReset.resetLink" readonly/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-primary" id="passwordResetLinkClipboardButton" type="button" data-clipboard-target="#passwordResetLinkInput"><i class="fa fa-clipboard"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'users.passwordResetDialog.descriptionEmail' | tr }}</label>
|
||||
<div class="input-group">
|
||||
<input type="email" ng-change="passwordReset.emailError = null" class="form-control" ng-model="passwordReset.email"/>
|
||||
<span class="input-group-btn">
|
||||
<button type="button" class="btn btn-primary" ng-click="passwordReset.sendEmail()" ng-disabled="passwordReset.busy"><i class="fa fa-circle-notch fa-spin" ng-show="passwordReset.busy"></i> {{ 'users.passwordResetDialog.sendAction' | tr }}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-success text-small text-bold" ng-show="passwordReset.emailError === ''">{{ 'profile.passwordResetNotification.body' | tr:{ email: passwordReset.email } }}</div>
|
||||
<div class="text-danger text-small text-bold" ng-show="passwordReset.emailError !== ''">{{ passwordReset.emailError }}</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal invitation -->
|
||||
<div class="modal fade" id="invitationModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'users.invitationDialog.title' | tr:{ username: (invitation.user.username || invitation.user.email) } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'users.invitationDialog.descriptionLink' | tr }}</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="invitationLinkInput" class="form-control" ng-value="invitation.inviteLink" readonly/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-primary" id="invitationLinkClipboardButton" type="button" data-clipboard-target="#invitationLinkInput"><i class="fa fa-clipboard"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'users.invitationDialog.descriptionEmail' | tr }}</label>
|
||||
<div class="input-group">
|
||||
<input type="email" class="form-control" ng-model="invitation.email"/>
|
||||
<span class="input-group-btn">
|
||||
<button type="button" class="btn btn-primary" ng-click="invitation.sendEmail()" ng-disabled="invitation.busy"><i class="fa fa-circle-notch fa-spin" ng-show="invitation.busy"></i> {{ 'users.invitationDialog.sendAction' | tr }}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal set ghost -->
|
||||
<div class="modal fade" id="setGhostModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'users.setGhostDialog.title' | tr: { username: setGhost.user.username} }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ 'users.setGhostDialog.description' | tr }}</p>
|
||||
<form name="setGhostForm" role="form" novalidate ng-submit="setGhost.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': setGhost.error }">
|
||||
<label class="control-label" for="setGhostPassword">{{ 'users.setGhostDialog.password' | tr }}</label>
|
||||
<div class="control-label" ng-show="setGhost.error">
|
||||
<small ng-show="setGhost.error">{{ setGhost.error }}</small>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input type="text" id="setGhostPassword" class="form-control" name="ghostPassword" ng-model="setGhost.password" required ng-readonly="setGhost.success"/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" ng-hide="setGhost.success" type="button" uib-tooltip="{{ 'users.setGhostDialog.generatePassword' | tr }}Generate Password" ng-click="setGhost.generatePassword()"><i class="fa fa-key"></i></button>
|
||||
<button class="btn btn-default" ng-show="setGhost.success" type="button" id="setGhostClipboardButton" data-clipboard-target="#setGhostPassword"><i class="fa fa-clipboard"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="setGhostForm.$invalid || setGhost.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" ng-show="setGhost.success" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-default" ng-hide="setGhost.success" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-hide="setGhost.success" ng-click="setGhost.submit()" ng-disabled="setGhostForm.$invalid || setGhost.busy"><i class="fa fa-circle-notch fa-spin" ng-show="setGhost.busy"></i> {{ 'users.setGhostDialog.setPassword' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content content-large">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>
|
||||
{{ 'main.navbar.users' | tr }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="users-toolbar">
|
||||
<input type="text" id="userSearchInput" class="form-control" style="max-width: 350px;" ng-model="userSearchString" ng-model-options="{ debounce: 1000 }" ng-change="updateFilter()" placeholder="{{ 'main.searchPlaceholder' | tr }}"/>
|
||||
<multiselect ng-model="userStateFilter" ms-header="{{ 'apps.stateFilterHeader' | tr }}" ms-selected="{{ userStateFilter }}" options="state.label for state in userStates" data-multiple="false"></multiselect>
|
||||
<div style="flex-grow: 1;"></div>
|
||||
<button class="btn btn-primary btn-outline" ng-click="userAdd.show()">
|
||||
<i class="fa fa-user-plus"></i> {{ 'users.newUserAction' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="card card-large">
|
||||
<div class="grid-item-top">
|
||||
<div class="row ng-hide" ng-show="userRefreshBusy">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row ng-hide" ng-hide="userRefreshBusy">
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-hover" style="margin: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 0.5%;"></th>
|
||||
<th style="width:45%">{{ 'users.users.user' | tr }}</th>
|
||||
<th style="width:49.5%" class="hidden-xs hidden-sm">{{ 'users.users.groups' | tr }}</th>
|
||||
<th style="width: 5%" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-show="users.length === 0">
|
||||
<td colspan="5" class="text-center text-muted">{{ 'users.users.empty' | tr }}</td>
|
||||
</tr>
|
||||
<tr ng-repeat="user in users" ng-class="{'text-muted': !user.active}">
|
||||
<td style="min-width: 33.5px;">
|
||||
<i class="fas fa-crown arrow" ng-show="user.active && user.role === 'owner'" uib-tooltip="{{ 'users.users.superadminTooltip' | tr }}"></i>
|
||||
<i class="fa fa-user-tie arrow" ng-show="user.active && user.role === 'admin'" uib-tooltip="{{ 'users.users.adminTooltip' | tr }}"></i>
|
||||
<i class="fas fa-users-cog arrow" ng-show="user.active && user.role === 'usermanager'" uib-tooltip="{{ 'users.users.usermanagerTooltip' | tr }}"></i>
|
||||
<i class="fas fa-mail-bulk arrow" ng-show="user.active && user.role === 'mailmanager'" uib-tooltip="{{ 'users.users.mailmanagerTooltip' | tr }}"></i>
|
||||
<i class="fa fa-ban" ng-show="!user.active" uib-tooltip="{{ 'users.users.inactiveTooltip' | tr }}"></i>
|
||||
</td>
|
||||
<td class="hand elide-table-cell" ng-click="canEdit(user) && userEdit.show(user)" ng-show="user.username">
|
||||
{{ user.displayName }} <span class="text-muted">{{ user.username }}</span> <i ng-show="user.source" class="far fa-address-book" uib-tooltip="{{ 'users.users.externalLdapTooltip' | tr }}"></i>
|
||||
</td>
|
||||
<td class="hand elide-table-cell" ng-click="canEdit(user) && userEdit.show(user)" ng-hide="user.username">
|
||||
<span class="text-muted" uib-tooltip="{{ 'users.users.notActivatedYetTooltip' | tr }}">{{ user.email }}</span>
|
||||
</td>
|
||||
<td class="text-left hand elide-table-cell hidden-xs hidden-sm" ng-click="canEdit(user) && userEdit.show(user)">
|
||||
<span class="group-badge" ng-repeat="groupId in user.groupIds">
|
||||
{{ groupsById[groupId].name }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button ng-disabled="!canEdit(user)" ng-show="!user.inviteAccepted && !isMe(user) && !user.source" class="btn btn-xs btn-default" ng-click="invitation.show(user)" uib-tooltip="{{ 'users.users.invitationTooltip' | tr }}"><i class="fas fa-paper-plane"></i></button>
|
||||
<button ng-disabled="!canEdit(user)" ng-show="user.inviteAccepted && !user.source" class="btn btn-xs btn-default" ng-click="passwordReset.show(user)" uib-tooltip="{{ 'users.users.resetPasswordTooltip' | tr }}"><i class="fas fa-key"></i></button>
|
||||
<button ng-disabled="!canImpersonate(user)" class="btn btn-xs btn-default" ng-click="setGhost.show(user)" uib-tooltip="{{ 'users.users.setGhostTooltip' | tr }}"><i class="fas fa-user-secret"></i></button>
|
||||
<button ng-disabled="!canEdit(user)" class="btn btn-xs btn-default" ng-click="userEdit.show(user)" uib-tooltip="{{ 'users.users.editUserTooltip' | tr }}"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button ng-disabled="!canEdit(user) || isMe(user)" class="btn btn-xs btn-danger" ng-click="userRemove.show(user)" uib-tooltip="{{ 'users.users.removeUserTooltip' | tr }}"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br/>
|
||||
<div class="pull-left">
|
||||
{{ 'users.users.count' | tr:{ count: allUsers.length } }}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3 style="margin-bottom: 15px;">
|
||||
{{ 'users.groups.title' | tr }}
|
||||
<button class="btn btn-primary btn-outline pull-right" ng-click="groupAdd.show()">
|
||||
<i class="fa fa-plus"></i> {{ 'users.groups.newGroupAction' | tr }}
|
||||
</button>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
<div class="grid-item-top">
|
||||
<div class="row ng-hide" ng-show="!ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="ready">
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-hover" style="margin: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 45%">{{ 'users.groups.name' | tr }}</th>
|
||||
<th style="width: 49.5%" class="hidden-xs hidden-sm">{{ 'users.groups.users' | tr }}</th>
|
||||
<th style="width: 5%" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="group in groups">
|
||||
<td class="hand elide-table-cell" style="text-overflow: ellipsis; white-space: nowrap;" ng-click="groupEdit.show(group)">
|
||||
{{ group.name }} <i ng-show="group.source" class="far fa-address-book" uib-tooltip="{{ 'users.groups.externalLdapTooltip' | tr }}"></i>
|
||||
</td>
|
||||
<td class="hand elide-table-cell" style="text-overflow: ellipsis; white-space: nowrap;" ng-click="groupEdit.show(group)">
|
||||
{{ groupMembers(group) }}
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="groupEdit.show(group)" uib-tooltip="Edit Group"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="groupRemove.show(group)" uib-tooltip="Remove Group"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,810 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global Clipboard */
|
||||
/* global async */
|
||||
/* global ROLES */
|
||||
/* global $ */
|
||||
|
||||
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.ready = false;
|
||||
$scope.users = []; // users of current page
|
||||
$scope.allUsersById = [];
|
||||
$scope.groups = [];
|
||||
$scope.hasLocalGroups = false;
|
||||
$scope.groupsById = { };
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.userInfo = Client.getUserInfo();
|
||||
|
||||
$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.userRefreshBusy = true;
|
||||
|
||||
$scope.userStates = [
|
||||
{ state: 'ALL', value: null, label: 'All Users' },
|
||||
{ state: 'ACTIVE', value: true, label: 'Active Users' },
|
||||
{ state: 'INACTIVE', value: false, label: 'Inactive Users' }
|
||||
];
|
||||
$translate(['users.stateFilter.all', 'users.stateFilter.active', 'users.stateFilter.inactive']).then(function (tr) {
|
||||
if (tr['users.stateFilter.all']) $scope.userStates.find(function (a) { return a.state === 'ALL'; }).label = tr['users.stateFilter.all'];
|
||||
if (tr['users.stateFilter.active']) $scope.userStates.find(function (a) { return a.state === 'ACTIVE'; }).label = tr['users.stateFilter.active'];
|
||||
if (tr['users.stateFilter.inactive']) $scope.userStates.find(function (a) { return a.state === 'INACTIVE'; }).label = tr['users.stateFilter.inactive'];
|
||||
});
|
||||
|
||||
$scope.userStateFilter = $scope.userStates[0];
|
||||
$scope.$watch('userStateFilter', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
$scope.updateFilter();
|
||||
});
|
||||
|
||||
$scope.groupMembers = function (group) {
|
||||
return group.userIds.filter(function (uid) { return !!$scope.allUsersById[uid]; }).map(function (uid) { return $scope.allUsersById[uid].username || $scope.allUsersById[uid].email; }).join(' ');
|
||||
};
|
||||
|
||||
$scope.canEdit = function (user) {
|
||||
let roleInt1 = $scope.roles.findIndex(function (role) { return role.id === $scope.userInfo.role; });
|
||||
let roleInt2 = $scope.roles.findIndex(function (role) { return role.id === user.role; });
|
||||
|
||||
return (roleInt1 - roleInt2) >= 0;
|
||||
};
|
||||
|
||||
$scope.canImpersonate = function (user) {
|
||||
// only admins can impersonate
|
||||
if (!$scope.userInfo.isAtLeastAdmin) return false;
|
||||
|
||||
// only users with username can be impersonated
|
||||
if (!user.username) return false;
|
||||
|
||||
// normal admins cannot impersonate owners
|
||||
if (!$scope.userInfo.isAtLeastOwner && [ ROLES.OWNER ].indexOf(user.role) !== -1) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
$scope.userRemove = {
|
||||
busy: false,
|
||||
error: null,
|
||||
userInfo: {},
|
||||
|
||||
show: function (userInfo) {
|
||||
$scope.userRemove.error = null;
|
||||
$scope.userRemove.userInfo = userInfo;
|
||||
|
||||
$('#userRemoveModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.userRemove.busy = true;
|
||||
|
||||
Client.removeUser($scope.userRemove.userInfo.id, function (error) {
|
||||
$scope.userRemove.busy = false;
|
||||
|
||||
if (error && error.statusCode === 403) return $scope.userRemove.error = error.message;
|
||||
else if (error) return console.error('Unable to delete user.', error);
|
||||
|
||||
$scope.userRemove.userInfo = {};
|
||||
|
||||
refreshCurrentPage();
|
||||
refreshAllUsers();
|
||||
|
||||
$('#userRemoveModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.userAdd = {
|
||||
busy: false,
|
||||
alreadyTaken: false,
|
||||
error: {},
|
||||
email: '',
|
||||
fallbackEmail: '',
|
||||
username: '',
|
||||
displayName: '',
|
||||
selectedLocalGroups: [],
|
||||
role: 'user',
|
||||
sendInvite: false,
|
||||
|
||||
show: function () {
|
||||
$scope.userAdd.error = {};
|
||||
$scope.userAdd.email = '';
|
||||
$scope.userAdd.fallbackEmail = '';
|
||||
$scope.userAdd.username = '';
|
||||
$scope.userAdd.displayName = '';
|
||||
$scope.userAdd.selectedLocalGroups = [];
|
||||
$scope.userAdd.role = 'user';
|
||||
$scope.userAdd.sendInvite = false;
|
||||
|
||||
$scope.useraddForm.$setUntouched();
|
||||
$scope.useraddForm.$setPristine();
|
||||
|
||||
$('#userAddModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.userAdd.busy = true;
|
||||
|
||||
$scope.userAdd.alreadyTaken = false;
|
||||
$scope.userAdd.error.email = null;
|
||||
$scope.userAdd.error.fallbackEmail = null;
|
||||
$scope.userAdd.error.username = null;
|
||||
$scope.userAdd.error.displayName = null;
|
||||
|
||||
var user = {
|
||||
username: $scope.userAdd.username || null,
|
||||
email: $scope.userAdd.email,
|
||||
fallbackEmail: $scope.userAdd.fallbackEmail,
|
||||
displayName: $scope.userAdd.displayName,
|
||||
role: $scope.userAdd.role
|
||||
};
|
||||
|
||||
Client.addUser(user, function (error, userId) {
|
||||
if (error) {
|
||||
$scope.userAdd.busy = false;
|
||||
|
||||
if (error.statusCode === 409) {
|
||||
if (error.message.toLowerCase().indexOf('email') !== -1) {
|
||||
$scope.userAdd.error.email = 'Email already taken';
|
||||
$scope.useraddForm.email.$setPristine();
|
||||
$('#inputUserAddEmail').focus();
|
||||
} else if (error.message.toLowerCase().indexOf('username') !== -1 || error.message.toLowerCase().indexOf('mailbox') !== -1) {
|
||||
$scope.userAdd.error.username = 'Username already taken';
|
||||
$scope.useraddForm.username.$setPristine();
|
||||
$('#inputUserAddUsername').focus();
|
||||
} else {
|
||||
// should not happen!!
|
||||
console.error(error.message);
|
||||
}
|
||||
return;
|
||||
} else if (error.statusCode === 400) {
|
||||
if (error.message.toLowerCase().indexOf('email') !== -1) {
|
||||
$scope.userAdd.error.email = 'Invalid Email';
|
||||
$scope.userAdd.error.emailAttempted = $scope.userAdd.email;
|
||||
$scope.useraddForm.email.$setPristine();
|
||||
$('#inputUserAddEmail').focus();
|
||||
} else if (error.message.toLowerCase().indexOf('username') !== -1) {
|
||||
$scope.userAdd.error.username = error.message;
|
||||
$scope.useraddForm.username.$setPristine();
|
||||
$('#inputUserAddUsername').focus();
|
||||
} else {
|
||||
console.error('Unable to create user.', error.statusCode, error.message);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
return console.error('Unable to create user.', error.statusCode, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
var localGroupIds = $scope.userAdd.selectedLocalGroups.map(function (g) { return g.id; });
|
||||
|
||||
Client.setLocalGroups(userId, localGroupIds, function (error) {
|
||||
$scope.userAdd.busy = false;
|
||||
|
||||
if (error) return console.error(error);
|
||||
|
||||
if ($scope.userAdd.sendInvite) Client.sendInviteEmail(userId, user.email, function (error) { if (error) console.error('Failed to send invite.', error); });
|
||||
|
||||
refreshCurrentPage();
|
||||
refreshAllUsers();
|
||||
|
||||
$('#userAddModal').modal('hide');
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.userEdit = {
|
||||
busy: false,
|
||||
reset2FABusy: false,
|
||||
error: {},
|
||||
userInfo: {},
|
||||
|
||||
// form fields
|
||||
username: '',
|
||||
email: '',
|
||||
fallbackEmail: '',
|
||||
aliases: {},
|
||||
displayName: '',
|
||||
active: false,
|
||||
source: '',
|
||||
selectedLocalGroups: [],
|
||||
externalGroups: [],
|
||||
role: '',
|
||||
|
||||
show: function (userInfo) {
|
||||
$scope.userEdit.error = {};
|
||||
$scope.userEdit.username = userInfo.username;
|
||||
$scope.userEdit.email = userInfo.email;
|
||||
$scope.userEdit.displayName = userInfo.displayName;
|
||||
$scope.userEdit.fallbackEmail = userInfo.fallbackEmail;
|
||||
$scope.userEdit.userInfo = userInfo;
|
||||
$scope.userEdit.selectedLocalGroups = userInfo.groupIds.map(function (gid) { return $scope.groupsById[gid]; }).filter(function (g) { return g.source === ''; });
|
||||
$scope.userEdit.externalGroups = userInfo.groupIds.map(function (gid) { return $scope.groupsById[gid]; }).filter(function (g) { return g.source !== ''; });
|
||||
$scope.userEdit.active = userInfo.active;
|
||||
$scope.userEdit.source = userInfo.source;
|
||||
$scope.userEdit.role = userInfo.role;
|
||||
|
||||
$scope.useredit_form.$setPristine();
|
||||
$scope.useredit_form.$setUntouched();
|
||||
|
||||
$('#userEditModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.userEdit.error = {};
|
||||
$scope.userEdit.busy = true;
|
||||
|
||||
var userId = $scope.userEdit.userInfo.id;
|
||||
|
||||
async.series([
|
||||
function setRole(next) {
|
||||
if (userId === $scope.userInfo.id) return next(); // cannot set role on self
|
||||
Client.setRole(userId, $scope.userEdit.role, next);
|
||||
},
|
||||
function setActive(next) {
|
||||
if (userId === $scope.userInfo.id) return next(); // cannot set role on self
|
||||
Client.setActive(userId, $scope.userEdit.active, next);
|
||||
},
|
||||
function updateUserProfile(next) {
|
||||
if ($scope.userEdit.source) return next(); // cannot update profile of external user
|
||||
// username is settable only if it was empty previously. it's editable for the "lock" profiles feature
|
||||
var data = {};
|
||||
if (!$scope.userEdit.userInfo.username) data.username = $scope.userEdit.username;
|
||||
data.email = $scope.userEdit.email;
|
||||
data.displayName = $scope.userEdit.displayName;
|
||||
data.fallbackEmail = $scope.userEdit.fallbackEmail;
|
||||
Client.updateUserProfile(userId, data, next);
|
||||
},
|
||||
function setLocalGroups(next) {
|
||||
var localGroupIds = $scope.userEdit.selectedLocalGroups.map(function (g) { return g.id; });
|
||||
Client.setLocalGroups(userId, localGroupIds, next);
|
||||
}
|
||||
], function (error) {
|
||||
$scope.userEdit.busy = false;
|
||||
|
||||
if (error) {
|
||||
if (error.statusCode === 409) {
|
||||
if (error.message.toLowerCase().indexOf('email') !== -1) {
|
||||
$scope.userEdit.error.email = 'Email already taken';
|
||||
} else if (error.message.toLowerCase().indexOf('username') !== -1) {
|
||||
$scope.userEdit.error.username = 'Username already taken';
|
||||
}
|
||||
$scope.useredit_form.email.$setPristine();
|
||||
$('#inputUserEditEmail').focus();
|
||||
} else {
|
||||
$scope.userEdit.error.generic = error.message;
|
||||
console.error('Unable to update user:', error);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
refreshUsersCurrentPage(false /* busy indicator */);
|
||||
refreshGroups();
|
||||
$('#userEditModal').modal('hide');
|
||||
});
|
||||
},
|
||||
|
||||
reset2FA: function () {
|
||||
$scope.userEdit.reset2FABusy = true;
|
||||
|
||||
Client.disableTwoFactorAuthenticationByUserId($scope.userEdit.userInfo.id, function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$timeout(function () {
|
||||
$scope.userEdit.userInfo.twoFactorAuthenticationEnabled = false;
|
||||
$scope.userEdit.reset2FABusy = false;
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.groupAdd = {
|
||||
busy: false,
|
||||
error: {},
|
||||
name: '',
|
||||
selectedUsers: [],
|
||||
|
||||
show: function () {
|
||||
$scope.groupAdd.busy = false;
|
||||
|
||||
$scope.groupAdd.error = {};
|
||||
$scope.groupAdd.name = '';
|
||||
$scope.groupAdd.selectedUsers = [];
|
||||
|
||||
$scope.groupAddForm.$setUntouched();
|
||||
$scope.groupAddForm.$setPristine();
|
||||
|
||||
$('#groupAddModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.groupAdd.busy = true;
|
||||
$scope.groupAdd.error = {};
|
||||
|
||||
Client.createGroup($scope.groupAdd.name, function (error, result) {
|
||||
if (error) {
|
||||
$scope.groupAdd.busy = false;
|
||||
|
||||
if (error.statusCode === 409) {
|
||||
$scope.groupAdd.error.name = 'Name already taken';
|
||||
$scope.groupAddForm.name.$setPristine();
|
||||
$('#groupAddName').focus();
|
||||
return;
|
||||
} else if (error.statusCode === 400) {
|
||||
$scope.groupAdd.error.name = error.message;
|
||||
$scope.groupAddForm.name.$setPristine();
|
||||
$('#groupAddName').focus();
|
||||
return;
|
||||
} else {
|
||||
return console.error('Unable to create group.', error.statusCode, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
var userIds = $scope.groupAdd.selectedUsers.map(function (u) { return u.id; });
|
||||
|
||||
Client.setGroupMembers(result.id, userIds, function (error) {
|
||||
$scope.groupAdd.busy = false;
|
||||
|
||||
if (error) return console.error('Unable to add memebers.', error.statusCode, error.message);
|
||||
|
||||
refreshCurrentPage();
|
||||
|
||||
$('#groupAddModal').modal('hide');
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.groupEdit = {
|
||||
busy: false,
|
||||
error: {},
|
||||
groupInfo: {},
|
||||
name: '',
|
||||
source: '',
|
||||
selectedUsers: [],
|
||||
selectedApps: [],
|
||||
selectedAppsOriginal: [],
|
||||
apps: [],
|
||||
|
||||
show: function (groupInfo) {
|
||||
$scope.groupEdit.error = {};
|
||||
$scope.groupEdit.groupInfo = groupInfo;
|
||||
$scope.groupEdit.name = groupInfo.name;
|
||||
$scope.groupEdit.source = groupInfo.source;
|
||||
$scope.groupEdit.selectedUsers = groupInfo.userIds.map(function (uid) { return $scope.allUsersById[uid]; });
|
||||
$scope.groupEdit.apps = Client.getInstalledApps();
|
||||
|
||||
$scope.groupEdit.selectedApps = Client.getInstalledApps().filter(function (app) {
|
||||
if (app.accessRestriction === null || !Array.isArray(app.accessRestriction.groups)) return false;
|
||||
return app.accessRestriction.groups.indexOf(groupInfo.id) !== -1;
|
||||
});
|
||||
angular.copy($scope.groupEdit.selectedApps, $scope.groupEdit.selectedAppsOriginal);
|
||||
|
||||
$scope.groupEdit_form.$setPristine();
|
||||
$scope.groupEdit_form.$setUntouched();
|
||||
|
||||
$('#groupEditModal').modal('show');
|
||||
},
|
||||
|
||||
updateAccessRestriction: function () {
|
||||
// find apps where ACL has changed
|
||||
var addedApps = $scope.groupEdit.selectedApps.filter(function (a) {
|
||||
return !$scope.groupEdit.selectedAppsOriginal.find(function (b) { return b.id === a.id; });
|
||||
});
|
||||
var removedApps = $scope.groupEdit.selectedAppsOriginal.filter(function (a) {
|
||||
return !$scope.groupEdit.selectedApps.find(function (b) { return b.id === a.id; });
|
||||
});
|
||||
|
||||
async.eachSeries(addedApps, function (app, callback) {
|
||||
var accessRestriction = app.accessRestriction;
|
||||
if (!accessRestriction) accessRestriction = { users: [], groups: [] };
|
||||
if (!Array.isArray(accessRestriction.groups)) accessRestriction.groups = [];
|
||||
|
||||
accessRestriction.groups.push($scope.groupEdit.groupInfo.id);
|
||||
|
||||
Client.configureApp(app.id, 'access_restriction', { accessRestriction: accessRestriction }, callback);
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
$scope.groupEdit.busy = false;
|
||||
return console.error('Unable to set added app access.', error.statusCode, error.message);
|
||||
}
|
||||
|
||||
async.eachSeries(removedApps, function (app, callback) {
|
||||
var accessRestriction = app.accessRestriction;
|
||||
if (!accessRestriction) accessRestriction = { users: [], groups: [] };
|
||||
if (!Array.isArray(accessRestriction.groups)) accessRestriction.groups = [];
|
||||
|
||||
var deleted = accessRestriction.groups.splice(accessRestriction.groups.indexOf($scope.groupEdit.groupInfo.id), 1);
|
||||
|
||||
// if not found return early
|
||||
if (deleted.length === 0) return callback();
|
||||
|
||||
Client.configureApp(app.id, 'access_restriction', { accessRestriction: accessRestriction }, callback);
|
||||
}, function (error) {
|
||||
$scope.groupEdit.busy = false;
|
||||
if (error) return console.error('Unable to set removed app access.', error.statusCode, error.message);
|
||||
|
||||
refreshCurrentPage();
|
||||
|
||||
// refresh apps to reflect change
|
||||
Client.refreshInstalledApps();
|
||||
|
||||
$('#groupEditModal').modal('hide');
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.groupEdit.busy = true;
|
||||
$scope.groupEdit.error = {};
|
||||
|
||||
if ($scope.groupEdit.source) return $scope.groupEdit.updateAccessRestriction(); // cannot update name or members of external groups
|
||||
|
||||
Client.setGroupName($scope.groupEdit.groupInfo.id, $scope.groupEdit.name, function (error) {
|
||||
if (error) {
|
||||
$scope.groupEdit.busy = false;
|
||||
|
||||
if (error.statusCode === 409) {
|
||||
$scope.groupEdit.error.name = 'Name already taken';
|
||||
$scope.groupEditForm.name.$setPristine();
|
||||
$('#groupEditName').focus();
|
||||
return;
|
||||
} else if (error.statusCode === 400) {
|
||||
$scope.groupEdit.error.name = error.message;
|
||||
$scope.groupEditForm.name.$setPristine();
|
||||
$('#groupEditName').focus();
|
||||
return;
|
||||
} else {
|
||||
return console.error('Unable to edit group.', error.statusCode, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
var userIds = $scope.groupEdit.selectedUsers.map(function (u) { return u.id; });
|
||||
|
||||
Client.setGroupMembers($scope.groupEdit.groupInfo.id, userIds, function (error) {
|
||||
if (error) {
|
||||
$scope.groupEdit.busy = false;
|
||||
return console.error('Unable to set group members.', error.statusCode, error.message);
|
||||
}
|
||||
|
||||
$scope.groupEdit.updateAccessRestriction();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.groupRemove = {
|
||||
busy: false,
|
||||
error: {},
|
||||
group: null,
|
||||
memberCount: 0,
|
||||
|
||||
show: function (group) {
|
||||
$scope.groupRemove.busy = false;
|
||||
|
||||
$scope.groupRemove.error = {};
|
||||
|
||||
$scope.groupRemove.group = angular.copy(group);
|
||||
|
||||
Client.getGroup(group.id, function (error, result) {
|
||||
if (error) return console.error('Unable to fetch group information.', error.statusCode, error.message);
|
||||
|
||||
$scope.groupRemove.memberCount = result.userIds.length;
|
||||
|
||||
$('#groupRemoveModal').modal('show');
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.groupRemove.busy = true;
|
||||
$scope.groupRemove.error = {};
|
||||
|
||||
Client.removeGroup($scope.groupRemove.group.id, function (error) {
|
||||
$scope.groupRemove.busy = false;
|
||||
|
||||
if (error) return console.error('Unable to remove group.', error.statusCode, error.message);
|
||||
|
||||
refreshCurrentPage();
|
||||
$('#groupRemoveModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.isMe = function (user) {
|
||||
return user.username === Client.getUserInfo().username;
|
||||
};
|
||||
|
||||
$scope.passwordReset = {
|
||||
busy: false,
|
||||
resetLink: '',
|
||||
user: null,
|
||||
email: '',
|
||||
emailError: null,
|
||||
|
||||
show: function (user) {
|
||||
$scope.passwordReset.busy = false;
|
||||
$scope.passwordReset.resetLink = '';
|
||||
$scope.passwordReset.user = user;
|
||||
$scope.passwordReset.email = user.fallbackEmail || user.email;
|
||||
$scope.passwordReset.emailError = null;
|
||||
|
||||
Client.getPasswordResetLink(user.id, function (error, result) {
|
||||
if (error) return console.error('Failed to get password reset link.', error);
|
||||
|
||||
$scope.passwordReset.resetLink = result.passwordResetLink;
|
||||
|
||||
$('#passwordResetModal').modal('show');
|
||||
});
|
||||
},
|
||||
|
||||
sendEmail: function () {
|
||||
$scope.passwordReset.busy = true;
|
||||
$scope.passwordReset.emailError = null;
|
||||
|
||||
Client.sendPasswordResetEmail($scope.passwordReset.user.id, $scope.passwordReset.email, function (error) {
|
||||
$scope.passwordReset.busy = false;
|
||||
|
||||
if (error) {
|
||||
$scope.passwordReset.emailError = error.message;
|
||||
} else {
|
||||
$scope.passwordReset.emailError = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.invitation = {
|
||||
busy: false,
|
||||
inviteLink: '',
|
||||
user: null,
|
||||
email: '',
|
||||
|
||||
show: function (user) {
|
||||
$scope.invitation.busy = false;
|
||||
$scope.invitation.inviteLink = '';
|
||||
$scope.invitation.user = user;
|
||||
$scope.invitation.email = user.email;
|
||||
|
||||
Client.getInviteLink(user.id, function (error, result) {
|
||||
if (error) return console.error('Failed to get invite link.', error);
|
||||
|
||||
$scope.invitation.inviteLink = result.inviteLink;
|
||||
|
||||
$('#invitationModal').modal('show');
|
||||
});
|
||||
},
|
||||
|
||||
sendEmail: function () {
|
||||
$scope.invitation.busy = true;
|
||||
|
||||
Client.sendInviteEmail($scope.invitation.user.id, $scope.invitation.email, function (error) {
|
||||
if (error) return console.error('Failed to send invite email.', error);
|
||||
|
||||
$scope.invitation.busy = false;
|
||||
|
||||
Client.notify($translate.instant('users.invitationNotification.title'), $translate.instant('users.invitationNotification.body', { email: $scope.invitation.email }), false, 'success');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// https://stackoverflow.com/questions/1497481/javascript-password-generator
|
||||
function generatePassword() {
|
||||
var length = 12,
|
||||
charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
|
||||
retVal = '';
|
||||
for (var i = 0, n = charset.length; i < length; ++i) {
|
||||
retVal += charset.charAt(Math.floor(Math.random() * n));
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
$scope.setGhost = {
|
||||
busy: false,
|
||||
error: null,
|
||||
success: false,
|
||||
user: null,
|
||||
password: '',
|
||||
|
||||
show: function (user) {
|
||||
$scope.setGhost.busy = false;
|
||||
$scope.setGhost.success = false;
|
||||
$scope.setGhost.error = null;
|
||||
$scope.setGhost.user = user;
|
||||
$scope.setGhost.password = '';
|
||||
|
||||
$('#setGhostModal').modal('show');
|
||||
},
|
||||
|
||||
generatePassword: function () {
|
||||
$scope.setGhost.password = generatePassword();
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.setGhost.busy = true;
|
||||
|
||||
Client.setGhost($scope.setGhost.user.id, $scope.setGhost.password, null, function (error) {
|
||||
$scope.setGhost.busy = false;
|
||||
|
||||
if (error) {
|
||||
$scope.setGhost.error = error.message;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
$scope.setGhost.success = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function getUsersCurrentPage(callback) {
|
||||
var users = [];
|
||||
|
||||
Client.getUsers($scope.userSearchString, $scope.userStateFilter.value, $scope.currentPage, $scope.pageItems, function (error, results) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
async.eachOf(results, function (result, index, iteratorDone) {
|
||||
Client.getUser(result.id, function (error, user) {
|
||||
if (error) return iteratorDone(error);
|
||||
|
||||
users[index] = user; // keep the sorting order
|
||||
|
||||
iteratorDone();
|
||||
});
|
||||
}, function (error) {
|
||||
callback(error, users);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function refreshUsersCurrentPage(showBusy) { // loads users on current page only
|
||||
if (showBusy) $scope.userRefreshBusy = true;
|
||||
|
||||
getUsersCurrentPage(function (error, result) {
|
||||
if (error) return console.error('Unable to get user listing.', error);
|
||||
|
||||
angular.copy(result, $scope.users);
|
||||
|
||||
$scope.ready = true;
|
||||
$scope.userRefreshBusy = false;
|
||||
});
|
||||
}
|
||||
|
||||
function refreshGroups(callback) {
|
||||
Client.getGroups(function (error, result) {
|
||||
if (error) {
|
||||
if (callback) return callback(error);
|
||||
else return console.error('Unable to get group listing.', error);
|
||||
}
|
||||
|
||||
angular.copy(result, $scope.groups);
|
||||
$scope.groupsById = { };
|
||||
$scope.hasLocalGroups = false;
|
||||
for (var i = 0; i < result.length; i++) {
|
||||
$scope.groupsById[result[i].id] = result[i];
|
||||
if (result[i].source === '') $scope.hasLocalGroups = true;
|
||||
}
|
||||
|
||||
if (callback) callback();
|
||||
});
|
||||
}
|
||||
|
||||
function refreshCurrentPage() {
|
||||
refreshGroups(function (error) {
|
||||
if (error) return console.error('Unable to get group listing.', error);
|
||||
refreshUsersCurrentPage(true /* busy indicator */);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.showNextPage = function () {
|
||||
$scope.currentPage++;
|
||||
refreshUsersCurrentPage(false /* no busy indicator */);
|
||||
};
|
||||
|
||||
$scope.showPrevPage = function () {
|
||||
if ($scope.currentPage > 1) $scope.currentPage--;
|
||||
else $scope.currentPage = 1;
|
||||
refreshUsersCurrentPage(false /* no busy indicator */);
|
||||
};
|
||||
|
||||
$scope.updateFilter = function () {
|
||||
refreshUsersCurrentPage(false /* no busy indicator */);
|
||||
};
|
||||
|
||||
function refreshAllUsers() { // this loads all users on Cloudron, not just current page
|
||||
Client.getAllUsers(function (error, results) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.allUsers = results;
|
||||
|
||||
$scope.allUsersById = {};
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
$scope.allUsersById[results[i].id] = results[i];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Client.onReady(function () {
|
||||
refreshCurrentPage();
|
||||
refreshAllUsers();
|
||||
|
||||
// Order matters for permissions used in canEdit
|
||||
$scope.roles = [
|
||||
{ id: 'user', name: $translate.instant('users.role.user'), disabled: false },
|
||||
{ id: 'usermanager', name: $translate.instant('users.role.usermanager'), disabled: false },
|
||||
{ id: 'mailmanager', name: $translate.instant('users.role.mailmanager'), disabled: false },
|
||||
{ id: 'admin', name: $translate.instant('users.role.admin'), disabled: !$scope.user.isAtLeastAdmin },
|
||||
{ id: 'owner', name: $translate.instant('users.role.owner'), disabled: !$scope.user.isAtLeastOwner }
|
||||
];
|
||||
|
||||
// give search the initial focus
|
||||
setTimeout(function () { $('#userSearchInput').focus(); }, 1);
|
||||
});
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
|
||||
new Clipboard('#passwordResetLinkClipboardButton').on('success', function(e) {
|
||||
$('#passwordResetLinkClipboardButton').tooltip({
|
||||
title: 'Copied!',
|
||||
trigger: 'manual'
|
||||
}).tooltip('show');
|
||||
|
||||
$timeout(function () { $('#passwordResetLinkClipboardButton').tooltip('hide'); }, 2000);
|
||||
|
||||
e.clearSelection();
|
||||
}).on('error', function(/*e*/) {
|
||||
$('#passwordResetLinkClipboardButton').tooltip({
|
||||
title: 'Press Ctrl+C to copy',
|
||||
trigger: 'manual'
|
||||
}).tooltip('show');
|
||||
|
||||
$timeout(function () { $('#passwordResetLinkClipboardButton').tooltip('hide'); }, 2000);
|
||||
});
|
||||
|
||||
new Clipboard('#invitationLinkClipboardButton').on('success', function(e) {
|
||||
$('#invitationLinkClipboardButton').tooltip({
|
||||
title: 'Copied!',
|
||||
trigger: 'manual'
|
||||
}).tooltip('show');
|
||||
|
||||
$timeout(function () { $('#invitationLinkClipboardButton').tooltip('hide'); }, 2000);
|
||||
|
||||
e.clearSelection();
|
||||
}).on('error', function(/*e*/) {
|
||||
$('#invitationLinkClipboardButton').tooltip({
|
||||
title: 'Press Ctrl+C to copy',
|
||||
trigger: 'manual'
|
||||
}).tooltip('show');
|
||||
|
||||
$timeout(function () { $('#invitationLinkClipboardButton').tooltip('hide'); }, 2000);
|
||||
});
|
||||
|
||||
new Clipboard('#setGhostClipboardButton').on('success', function(e) {
|
||||
$('#setGhostClipboardButton').tooltip({
|
||||
title: 'Copied!',
|
||||
trigger: 'manual'
|
||||
}).tooltip('show');
|
||||
|
||||
$timeout(function () { $('#setGhostClipboardButton').tooltip('hide'); }, 2000);
|
||||
|
||||
e.clearSelection();
|
||||
}).on('error', function(/*e*/) {
|
||||
$('#setGhostClipboardButton').tooltip({
|
||||
title: 'Press Ctrl+C to copy',
|
||||
trigger: 'manual'
|
||||
}).tooltip('show');
|
||||
|
||||
$timeout(function () { $('#setGhostClipboardButton').tooltip('hide'); }, 2000);
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,238 +0,0 @@
|
||||
<!-- modal volume add -->
|
||||
<div class="modal fade" id="volumeAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'volumes.addVolumeDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="volumeAddForm" role="form" novalidate ng-submit="volumeAdd.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="volumeAdd.error">{{ volumeAdd.error }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'volumes.name' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="volumeAdd.name" name="name" ng-disabled="volumeAdd.busy" ng-required="true" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="mountType">{{ 'volumes.mountType' | tr }}</label>
|
||||
<select class="form-control" id="mountType" ng-model="volumeAdd.mountType" ng-options="a.value as a.name for a in mountTypes"></select>
|
||||
<p class="small">
|
||||
<span class="text-warning" ng-show="volumeAdd.mountType === 'mountpoint'" ng-bind-html="'volumes.addVolumeDialog.mountpointWarning' | tr"></span>
|
||||
<span class="text-info" ng-hide="volumeAdd.mountType === 'mountpoint' || volumeAdd.mountType === 'filesystem'" ng-bind-html="'volumes.addVolumeDialog.mountTypeInfo' | tr"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'filesystem'">
|
||||
<label class="control-label">{{ 'volumes.localDirectory' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="volumeAdd.hostPath" name="hostPath" ng-disabled="volumeAdd.busy" placeholder="/srv/shared" ng-required="volumeAdd.mountType === 'filesystem'" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'mountpoint'">
|
||||
<label class="control-label">{{ 'volumes.hostPath' | tr }}</label>
|
||||
<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'">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'cifs' || volumeAdd.mountType === 'nfs' || volumeAdd.mountType === 'sshfs'">
|
||||
<label class="control-label" for="volumeAddHost">{{ 'volumes.addVolumeDialog.server' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="volumeAdd.host" id="volumeAddHost" name="host" ng-disabled="volumeAdd.busy" placeholder="Server IP or hostname" ng-required="volumeAdd.mountType === 'cifs' || volumeAdd.mountType === 'nfs' || volumeAdd.mountType === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="volumeAdd.mountType === 'cifs'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="volumeAdd.seal">{{ 'backups.configureBackupStorage.cifsSealSupport' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'sshfs'">
|
||||
<label class="control-label" for="volumeAddPort">{{ 'volumes.addVolumeDialog.port' | tr }}</label>
|
||||
<input type="number" class="form-control" ng-model="volumeAdd.port" id="volumeAddPort" name="port" ng-disabled="volumeAdd.busy" ng-required="volumeAdd.mountType === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'cifs' || volumeAdd.mountType === 'nfs' || volumeAdd.mountType === 'sshfs'">
|
||||
<label class="control-label" for="volumeAddRemoteDir">{{ 'volumes.addVolumeDialog.remoteDirectory' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="volumeAdd.remoteDir" id="volumeAddRemoteDir" name="remoteDir" ng-disabled="volumeAdd.busy" placeholder="/share" ng-required="volumeAdd.mountType === 'cifs' || volumeAdd.mountType === 'nfs' || volumeAdd.mountType === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'cifs'">
|
||||
<label class="control-label" for="volumeAddUsername">{{ 'volumes.addVolumeDialog.username' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="volumeAdd.username" id="volumeAddUsername" name="cifsUsername" ng-disabled="volumeAdd.busy" ng-required="volumeAdd.mountType === 'cifs'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'cifs'">
|
||||
<label class="control-label" for="volumeAddPassword">{{ 'volumes.addVolumeDialog.password' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="volumeAdd.password" id="volumeAddPassword" name="cifsPassword" ng-disabled="volumeAdd.busy" password-reveal ng-required="volumeAdd.mountType === 'cifs'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'sshfs'">
|
||||
<label class="control-label" for="volumeAddUser">{{ 'volumes.addVolumeDialog.user' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="volumeAdd.user" id="volumeAddUser" name="user" ng-disabled="volumeAdd.busy" ng-required="volumeAdd.mountType === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'sshfs'">
|
||||
<label class="control-label" for="volumeAddPrivateKey">{{ 'volumes.addVolumeDialog.privateKey' | tr }}</label>
|
||||
<textarea class="form-control" ng-model="volumeAdd.privateKey" id="volumeAddPrivateKey" name="privateKey" ng-disabled="volumeAdd.busy" ng-required="volumeAdd.mountType === 'sshfs'"></textarea>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="volumeAddForm.$invalid || volumeAdd.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="volumeAdd.submit()" ng-disabled="volumeAddForm.$invalid || volumeAdd.busy"><i class="fa fa-circle-notch fa-spin" ng-show="volumeAdd.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'volumes.removeVolumeDialog.title' | tr:{ volume:volumeRemove.volume.name } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'volumes.removeVolumeDialog.description' | tr:{ volume:volumeRemove.volume.name }"></p>
|
||||
<p class="has-error" ng-show="volumeRemove.error">{{ volumeRemove.error }}</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="volumeRemove.submit()" ng-disabled="volumeRemove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="volumeRemove.busy"></i> {{ 'volumes.removeVolumeDialog.removeAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="text-left">
|
||||
<h1>{{ 'volumes.title' | tr }} <button class="btn btn-primary btn-outline pull-right" ng-click="volumeAdd.show()"><i class="fa fa-plus"></i> {{ 'volumes.addVolumeAction' | tr }}</button></h1>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
<p ng-bind-html="'volumes.description' | tr"></p>
|
||||
|
||||
<div class="row ng-hide" ng-show="!ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="ready">
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-hover" style="margin-top: 10px; table-layout: fixed;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 5%"></th>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="volume in volumes">
|
||||
<td>
|
||||
<i class="fa fa-circle" ng-style="{ color: volume.status.state === 'active' ? '#27CE65' : '#d9534f' }" ng-show="volume.status" uib-tooltip="{{ volume.status.message }}"></i>
|
||||
<i class="fa fa-circle-notch fa-spin" ng-hide="volume.status"></i>
|
||||
</td>
|
||||
<td class="wrap-table-cell">
|
||||
{{ volume.name }}
|
||||
</td>
|
||||
<td class="wrap-table-cell">
|
||||
{{ volume.mountType }}
|
||||
</td>
|
||||
<td class="text-left wrap-table-cell hidden-xs hidden-sm" ng-show="volume.mountType !== 'mountpoint' && volume.mountType !== 'filesystem'">{{ volume.mountOptions.host || volume.mountOptions.diskPath || volume.hostPath }}{{ volume.mountOptions.remoteDir }}</td>
|
||||
<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>
|
||||
<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>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,368 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
/* 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('/'); });
|
||||
|
||||
var refreshVolumesTimerId = null;
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.volumes = [];
|
||||
$scope.devices = [];
|
||||
$scope.ready = false;
|
||||
|
||||
$scope.mountTypes = [
|
||||
{ name: 'CIFS', value: 'cifs' },
|
||||
{ name: 'EXT4', value: 'ext4' },
|
||||
{ name: 'Filesystem', value: 'filesystem' },
|
||||
{ name: 'Filesystem (Mountpoint)', value: 'mountpoint' },
|
||||
{ name: 'NFS', value: 'nfs' },
|
||||
{ name: 'SSHFS', value: 'sshfs' },
|
||||
{ name: 'XFS', value: 'xfs' },
|
||||
];
|
||||
|
||||
function refreshVolumes(callback) {
|
||||
let refreshAgain = false;
|
||||
|
||||
Client.getVolumes(function (error, results) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.volumes = results;
|
||||
|
||||
async.eachSeries($scope.volumes, function (volume, iteratorDone) {
|
||||
Client.getVolumeStatus(volume.id, function (error, result) {
|
||||
if (error) {
|
||||
console.error('Failed to fetch volume status', volume.name, error);
|
||||
iteratorDone();
|
||||
}
|
||||
|
||||
volume.status = result;
|
||||
if (volume.status.state === 'activating') refreshAgain = true;
|
||||
|
||||
iteratorDone();
|
||||
});
|
||||
}, function () {
|
||||
if (!refreshAgain) {
|
||||
clearTimeout(refreshVolumesTimerId);
|
||||
refreshVolumesTimerId = null;
|
||||
} else if (!refreshVolumesTimerId) {
|
||||
refreshVolumesTimerId = setTimeout(refreshVolumes, 5000);
|
||||
}
|
||||
|
||||
if (callback) callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// same as box/mounts.js
|
||||
$scope.isMountProvider = function (provider) {
|
||||
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;
|
||||
|
||||
Client.remountVolume(volume.id, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
// give the backend some time
|
||||
$timeout(function () {
|
||||
volume.remounting = false;
|
||||
refreshVolumes(function (error) { if (error) console.error('Failed to refresh volume states.', error); });
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.volumeAdd = {
|
||||
error: null,
|
||||
busy: false,
|
||||
|
||||
name: '',
|
||||
hostPath: '',
|
||||
|
||||
mountType: 'mountpoint',
|
||||
host: '',
|
||||
remoteDir: '',
|
||||
username: '',
|
||||
password: '',
|
||||
ext4Disk: null, // { path, type }
|
||||
xfsDisk: null, // { path, type }
|
||||
user: '',
|
||||
seal: true,
|
||||
port: 22,
|
||||
privateKey: '',
|
||||
|
||||
reset: function () {
|
||||
$scope.volumeAdd.error = null;
|
||||
$scope.volumeAdd.busy = false;
|
||||
$scope.volumeAdd.name = '';
|
||||
$scope.volumeAdd.hostPath = '';
|
||||
$scope.volumeAdd.mountType = 'mountpoint';
|
||||
$scope.volumeAdd.host = '';
|
||||
$scope.volumeAdd.remoteDir = '';
|
||||
$scope.volumeAdd.username = '';
|
||||
$scope.volumeAdd.password = '';
|
||||
$scope.volumeAdd.ext4Disk = null;
|
||||
$scope.volumeAdd.xfsDisk = null;
|
||||
$scope.volumeAdd.user = '';
|
||||
$scope.volumeAdd.seal = true;
|
||||
$scope.volumeAdd.port = 22;
|
||||
$scope.volumeAdd.privateKey = '';
|
||||
|
||||
$scope.volumeAddForm.$setPristine();
|
||||
$scope.volumeAddForm.$setUntouched();
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.volumeAdd.reset();
|
||||
|
||||
$scope.ext4BlockDevices = [];
|
||||
$scope.xfsBlockDevices = [];
|
||||
|
||||
Client.getBlockDevices(function (error, result) {
|
||||
if (error) console.error('Failed to list blockdevices:', error);
|
||||
|
||||
// only offer unmounted disks
|
||||
result = result.filter(function (d) { return !d.mountpoint; });
|
||||
|
||||
// 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'; });
|
||||
|
||||
$('#volumeAddModal').modal('show');
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.volumeAdd.busy = true;
|
||||
$scope.volumeAdd.error = null;
|
||||
|
||||
var mountOptions = null;
|
||||
|
||||
if ($scope.volumeAdd.mountType === 'cifs') {
|
||||
mountOptions = {
|
||||
host: $scope.volumeAdd.host,
|
||||
remoteDir: $scope.volumeAdd.remoteDir,
|
||||
username: $scope.volumeAdd.username,
|
||||
password: $scope.volumeAdd.password,
|
||||
seal: $scope.volumeAdd.seal
|
||||
};
|
||||
} else if ($scope.volumeAdd.mountType === 'nfs') {
|
||||
mountOptions = {
|
||||
host: $scope.volumeAdd.host,
|
||||
remoteDir: $scope.volumeAdd.remoteDir,
|
||||
};
|
||||
} else if ($scope.volumeAdd.mountType === 'sshfs') {
|
||||
mountOptions = {
|
||||
host: $scope.volumeAdd.host,
|
||||
port: $scope.volumeAdd.port,
|
||||
remoteDir: $scope.volumeAdd.remoteDir,
|
||||
user: $scope.volumeAdd.user,
|
||||
privateKey: $scope.volumeAdd.privateKey,
|
||||
};
|
||||
} else if ($scope.volumeAdd.mountType === 'ext4') {
|
||||
mountOptions = {
|
||||
diskPath: $scope.volumeAdd.ext4Disk.path
|
||||
};
|
||||
} else if ($scope.volumeAdd.mountType === 'xfs') {
|
||||
mountOptions = {
|
||||
diskPath: $scope.volumeAdd.xfsDisk.path
|
||||
};
|
||||
} else if ($scope.volumeAdd.mountType === 'mountpoint' || $scope.volumeAdd.mountType === 'filesystem') {
|
||||
mountOptions = {
|
||||
hostPath: $scope.volumeAdd.hostPath
|
||||
};
|
||||
}
|
||||
|
||||
Client.addVolume($scope.volumeAdd.name, $scope.volumeAdd.mountType, mountOptions, function (error) {
|
||||
$scope.volumeAdd.busy = false;
|
||||
if (error) {
|
||||
$scope.volumeAdd.error = error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
$('#volumeAddModal').modal('hide');
|
||||
$scope.volumeAdd.reset();
|
||||
|
||||
refreshVolumes();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.volumeEdit = {
|
||||
error: null,
|
||||
busy: false,
|
||||
|
||||
// cannot be changed
|
||||
volume: '',
|
||||
name: '',
|
||||
mountType: 'mountpoint',
|
||||
|
||||
// can be changed
|
||||
host: '',
|
||||
remoteDir: '',
|
||||
username: '',
|
||||
password: '',
|
||||
user: '',
|
||||
seal: true,
|
||||
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 = true;
|
||||
$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,
|
||||
volume: null,
|
||||
|
||||
reset: function () {
|
||||
$scope.volumeRemove.busy = false;
|
||||
$scope.volumeRemove.error = null;
|
||||
$scope.volumeRemove.volume = null;
|
||||
},
|
||||
|
||||
show: function (volume) {
|
||||
$scope.volumeRemove.reset();
|
||||
|
||||
$scope.volumeRemove.volume = volume;
|
||||
|
||||
$('#volumeRemoveModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.volumeRemove.busy = true;
|
||||
$scope.volumeRemove.error = null;
|
||||
|
||||
Client.removeVolume($scope.volumeRemove.volume.id, function (error) {
|
||||
if (error && (error.statusCode === 403 || error.statusCode === 409)) {
|
||||
$scope.volumeRemove.error = error.message;
|
||||
} else if (error) {
|
||||
Client.error(error);
|
||||
} else {
|
||||
$('#volumeRemoveModal').modal('hide');
|
||||
$scope.volumeRemove.reset();
|
||||
|
||||
refreshVolumes();
|
||||
}
|
||||
|
||||
$scope.volumeRemove.busy = false;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
refreshVolumes(function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.ready = true;
|
||||
});
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['volumeAddModal', 'volumeRemoveModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find('[autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,21 +0,0 @@
|
||||
const js = require('@eslint/js');
|
||||
const globals = require('globals');
|
||||
|
||||
module.exports = [
|
||||
js.configs.recommended,
|
||||
{
|
||||
files: ["**/*.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
ecmaVersion: 13,
|
||||
sourceType: "commonjs"
|
||||
},
|
||||
rules: {
|
||||
semi: "error",
|
||||
"prefer-const": "error"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
# Dashboard Filemanager
|
||||
|
||||
Local development via:
|
||||
|
||||
```
|
||||
VITE_API_ORIGIN=my.example.com npm run dev
|
||||
```
|
||||
|
||||
It requires an access token in `localStorage.token`.
|
||||
|
||||
The default local urls look like:
|
||||
|
||||
FileManager: `http://localhost:5173/filemanager.html#/home/app/<appId>`
|
||||
Terminal: `http://localhost:5173/terminal.html?id=<appId>`
|
||||
LogViewer: `http://localhost:5173/logs.html?appId=<appId>`
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
./node_modules/.bin/vite build --base=/frontend/
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
echo "=> Set API origin"
|
||||
export VITE_API_ORIGIN="my.nebulon.space"
|
||||
|
||||
echo "=> Run vite locally"
|
||||
npm run dev
|
||||
@@ -1,22 +0,0 @@
|
||||
import globals from 'globals';
|
||||
import js from '@eslint/js';
|
||||
import pluginVue from 'eslint-plugin-vue';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...pluginVue.configs['flat/essential'],
|
||||
{
|
||||
files: ["**/*.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
ecmaVersion: 13,
|
||||
sourceType: 'module'
|
||||
},
|
||||
rules: {
|
||||
semi: "error",
|
||||
"prefer-const": "error"
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>File Manager</title>
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/filemanager.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Logs</title>
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/logs.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --base=/frontend/ --strictPort --port 4001",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/noto-sans": "^5.1.0",
|
||||
"@xterm/addon-attach": "^0.11.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"anser": "^2.1.1",
|
||||
"combokeys": "^3.0.1",
|
||||
"filesize": "^10.1.6",
|
||||
"marked": "^14.1.2",
|
||||
"moment": "^2.30.1",
|
||||
"pankow": "^2.2.1",
|
||||
"pankow-viewers": "^1.0.7",
|
||||
"vue": "^3.5.6",
|
||||
"vue-i18n": "^10.0.1",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.10.0",
|
||||
"@vitejs/plugin-vue": "^5.1.3",
|
||||
"eslint": "^9.10.0",
|
||||
"eslint-plugin-vue": "^9.28.0",
|
||||
"vite": "^5.4.5"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,139 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="64"
|
||||
version="1.1"
|
||||
viewBox="0 0 64 64"
|
||||
height="64"
|
||||
id="svg2"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="android-package-archive.svg">
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1360"
|
||||
inkscape:window-height="708"
|
||||
id="namedview33"
|
||||
showgrid="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:zoom="5.6568542"
|
||||
inkscape:cx="50.861875"
|
||||
inkscape:cy="21.2677"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-nodes="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4162" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs4">
|
||||
<linearGradient
|
||||
id="linearGradient4300-2">
|
||||
<stop
|
||||
id="stop4302-4"
|
||||
style="stop-color:#3a539b" />
|
||||
<stop
|
||||
id="stop4304-1"
|
||||
style="stop-color:#3f5aa9"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient6251">
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:0"
|
||||
offset="0"
|
||||
id="stop6253" />
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:0.2"
|
||||
offset="1"
|
||||
id="stop6255" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient6251"
|
||||
id="linearGradient7145-0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(-58,-335.3622)"
|
||||
x1="58"
|
||||
y1="392.36221"
|
||||
x2="58"
|
||||
y2="336.36221" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient6251"
|
||||
id="linearGradient5849"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1,0,0,0.84587337,-47,-272.73372)"
|
||||
x1="58"
|
||||
y1="403.41098"
|
||||
x2="58"
|
||||
y2="323.82297" />
|
||||
</defs>
|
||||
<metadata
|
||||
id="metadata84">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<rect
|
||||
y="11.000853"
|
||||
x="7"
|
||||
height="49.999977"
|
||||
width="49.999977"
|
||||
id="rect5837"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#9bd916;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
<rect
|
||||
width="50"
|
||||
x="7"
|
||||
y="60.000854"
|
||||
height="1.0000085"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.25;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
id="rect5839" />
|
||||
<rect
|
||||
width="50"
|
||||
x="7"
|
||||
y="11.000853"
|
||||
height="1.0000085"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.5;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
id="rect5841" />
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.75;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke-width:2;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m 22,39 c -2.98896,7.5e-4 -5.84891,1.21778 -7.92188,3.37109 l -2.7246,-2.72461 c -0.0942,-0.0974 -0.2239,-0.15234 -0.35938,-0.15234 -0.4494,9e-5 -0.67059,0.54683 -0.34766,0.85937 l 2.76954,2.76954 C 11.85252,45.07416 11.00038,47.49972 11,50 l 0,7 22,0 0,-7 c -0.004,-2.4983 -0.85795,-4.92089 -2.42188,-6.86914 l 2.77735,-2.77735 c 0.32293,-0.31254 0.10175,-0.85928 -0.34766,-0.85937 -0.13548,0 -0.26516,0.055 -0.35937,0.15234 L 29.91602,42.3789 C 27.8458,40.22417 24.98808,39.00438 22,39 Z m 0,1 c 5.52285,0 10,4.47715 10,10 l 0,6 -20,0 0,-6 c 0,-5.52285 4.47715,-10 10,-10 z m -4.5,5 C 16.67157,45 16,45.67157 16,46.5 16,47.32843 16.67157,48 17.5,48 18.32843,48 19,47.32843 19,46.5 19,45.67157 18.32843,45 17.5,45 Z m 9,0 C 25.67157,45 25,45.67157 25,46.5 25,47.32843 25.67157,48 26.5,48 27.32843,48 28,47.32843 28,46.5 28,45.67157 27.32843,45 26.5,45 Z"
|
||||
id="path5845"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccccccccccsccccsssssssssss" />
|
||||
<rect
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.55199998;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#linearGradient5849);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
id="rect5847"
|
||||
width="49.999977"
|
||||
height="49.999977"
|
||||
x="7"
|
||||
y="11.000853" />
|
||||
<path
|
||||
style="opacity:0.75;fill:#ffffff;fill-opacity:1"
|
||||
d="M 40 12 L 40 14 L 42 14 L 42 12 L 40 12 z M 42 14 L 42 16 L 44 16 L 44 14 L 42 14 z M 42 16 L 40 16 L 40 18 L 42 18 L 42 16 z M 42 18 L 42 20 L 44 20 L 44 18 L 42 18 z M 42 20 L 40 20 L 40 22 L 42 22 L 42 20 z M 42 22 L 42 24 L 44 24 L 44 22 L 42 22 z M 42 24 L 40 24 L 40 26 L 42 26 L 42 24 z M 40 27 L 40 31 L 41 31 L 41 35 L 43 35 L 43 31 L 44 31 L 44 27 L 40 27 z M 41 28 L 43 28 L 43 30 L 41 30 L 41 28 z "
|
||||
id="rect4173-3" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.4 KiB |
@@ -1,28 +0,0 @@
|
||||
<svg width="64" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" height="64" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient id="a" y1="17" y2="31" x1="40" x2="54" gradientUnits="userSpaceOnUse" gradientTransform="translate(302 78.36)">
|
||||
<stop stop-color="#060606"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="b" y1="392.36" y2="336.36" x2="0" gradientUnits="userSpaceOnUse" gradientTransform="translate(254-254)">
|
||||
<stop stop-color="#ffffff" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity=".2"/>
|
||||
</linearGradient>
|
||||
<path id="c" d="m312 139.36v-58h30l14 14v44h-14z"/>
|
||||
</defs>
|
||||
<g transform="translate(-302-78.36)">
|
||||
<g color-rendering="auto" color-interpolation-filters="linearRGB" shape-rendering="auto" image-rendering="auto" text-rendering="auto" color-interpolation="sRGB" color="#000000">
|
||||
<use fill="#fc963a" xlink:href="#c"/>
|
||||
<g transform="scale(1-1)">
|
||||
<rect opacity=".5" x="312" y="-82.36" width="30" fill="#ffffff" height="1"/>
|
||||
<rect opacity=".25" x="312" y="-139.36" width="44" height="1"/>
|
||||
</g>
|
||||
</g>
|
||||
<g fill-rule="evenodd">
|
||||
<path opacity=".5" fill="#ffffff" d="m356 95.36l-14-14v14z"/>
|
||||
<path opacity=".1" fill="url(#a)" d="m342 95.36l14 14v-14z"/>
|
||||
</g>
|
||||
<use fill="url(#b)" xlink:href="#c"/>
|
||||
<path opacity=".75" color-interpolation-filters="linearRGB" color="#000000" image-rendering="auto" color-rendering="auto" d="m324.04 100.33v3a18.999996 18.999996 0 0 1 19 19h3a21.999996 21.999996 0 0 0 -22 -22m0 7v3a11.999996 11.999996 0 0 1 12 12h3a14.999996 14.999996 0 0 0 -15 -15m3.5 8c-1.939 0-3.5 1.561-3.5 3.5 0 1.939 1.561 3.5 3.5 3.5 1.939 0 3.5-1.561 3.5-3.5 0-1.939-1.561-3.5-3.5-3.5" color-interpolation="sRGB" text-rendering="auto" fill="#ffffff" shape-rendering="auto"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:version="1.1-dev (d80adc983d, 2020-06-15)" sodipodi:docname="application-certificate.svg" id="svg35" version="1.1" height="64" viewBox="0 0 64 64" width="64">
|
||||
<sodipodi:namedview inkscape:current-layer="svg35" showgrid="false" id="namedview37" inkscape:window-height="480" inkscape:window-width="640" inkscape:pageshadow="2" inkscape:pageopacity="0" guidetolerance="10" gridtolerance="10" objecttolerance="10" borderopacity="1" bordercolor="#666666" pagecolor="#ffffff" />
|
||||
<defs id="defs17">
|
||||
<linearGradient gradientTransform="matrix(1 0 0-1 0 64)" gradientUnits="userSpaceOnUse" x2="0" y2="61" y1="3" id="a">
|
||||
<stop id="stop2" stop-color="#cf000f" />
|
||||
<stop id="stop4" stop-color="#d91e18" offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient gradientTransform="matrix(1 0 0-1 0 64)" gradientUnits="userSpaceOnUse" x2="0" y2="47" y1="61" id="b">
|
||||
<stop offset="0" stop-color="#fb9fa2" id="stop9" />
|
||||
<stop offset="1" stop-color="#fb7d80" id="stop7" />
|
||||
</linearGradient>
|
||||
<linearGradient gradientUnits="userSpaceOnUse" x2="54" y2="31" x1="40" y1="17" id="c">
|
||||
<stop id="stop12" stop-color="#383e51" />
|
||||
<stop id="stop14" stop-opacity="0" stop-color="#655c6f" offset="1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path id="path19" d="m10 61v-58h30l14 14v44h-14z" fill="url(#a)" />
|
||||
<g id="g25" transform="scale(1-1)">
|
||||
<rect id="rect21" fill-opacity=".412" height="1" fill="#ffffff" y="-4" x="10" width="30" />
|
||||
<rect id="rect23" fill-opacity=".294" height="1" fill="#2e3132" y="-61" x="10" width="44" />
|
||||
</g>
|
||||
<g id="g31" fill-rule="evenodd">
|
||||
<path id="path27" d="m54 17l-14-14v14z" fill="url(#b)" />
|
||||
<path id="path29" d="m40 17l14 14v-14z" fill="url(#c)" opacity=".2" />
|
||||
</g>
|
||||
<path id="path33" d="m32 19.333c-4.432 0-8 3.568-8 8 0 2.585 1.22 4.869 3.111 6.33v12.337l4.889-3.556 4.889 3.556v-12.337c1.891-1.461 3.111-3.745 3.111-6.33 0-4.432-3.568-8-8-8" fill="#fcbcbe" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,26 +0,0 @@
|
||||
<svg width="64" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" height="64">
|
||||
<defs>
|
||||
<linearGradient id="a" y1="3" y2="61" x2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1 0 0-1 0 64)">
|
||||
<stop stop-color="#5e6b78"/>
|
||||
<stop offset="1" stop-color="#768492"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="b" y1="61" y2="47" x2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1 0 0-1 0 64)">
|
||||
<stop stop-color="#dedede"/>
|
||||
<stop offset="1" stop-color="#fbfbfb"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="c" y1="17" x1="40" y2="31" x2="54" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#383e51"/>
|
||||
<stop offset="1" stop-color="#655c6f" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#a)" d="m10 61v-58h30l14 14v44h-14z"/>
|
||||
<g transform="scale(1-1)">
|
||||
<rect width="30" x="10" y="-4" fill="#ffffff" height="1" fill-opacity=".412"/>
|
||||
<rect width="44" x="10" y="-61" fill="#2e3132" height="1" fill-opacity=".294"/>
|
||||
</g>
|
||||
<g fill-rule="evenodd">
|
||||
<path fill="url(#b)" d="m54 17l-14-14v14z"/>
|
||||
<path opacity=".2" fill="url(#c)" d="m40 17l14 14v-14z"/>
|
||||
</g>
|
||||
<path opacity=".75" fill="#fbfbfb" d="m30.3 20c-1.254 0-2.889.693-4.384 1.857-.897.377-1.527 1.249-1.527 2.263 0 .318.061.628.179.917-1.55.294-2.724 1.629-2.724 3.23 0 .333.051.661.152.978-.639.617-1 1.454-1 2.339 0 .692.22 1.334.595 1.865-.389.55-.595 1.193-.595 1.868 0 1.197.667 2.293 1.724 2.871-.018.148-.027.297-.027.446 0 1.614 1.078 3.041 2.64 3.527.68 1.137 1.916 1.838 3.27 1.838 1.484 0 2.771-.839 3.393-2.057.622 1.218 1.909 2.057 3.393 2.057 1.354 0 2.592-.701 3.272-1.838 1.563-.486 2.638-1.913 2.638-3.527 0-.149-.009-.298-.027-.446 1.057-.578 1.724-1.675 1.724-2.871 0-.676-.204-1.318-.593-1.868.374-.53.593-1.173.593-1.865 0-.885-.361-1.723-1-2.339.101-.317.152-.645.152-.978 0-1.6-1.174-2.936-2.724-3.23.118-.289.179-.599.179-.917 0-1.014-.63-1.886-1.527-2.263-1.495-1.164-3.129-1.857-4.384-1.857-.697 0-1.316.336-1.697.85-.381-.514-.999-.85-1.697-.85m0 .778c.716 0 1.299.57 1.299 1.27v5.03c-.357-.277-.808-.444-1.299-.444-.22 0-.398.174-.398.389 0 .215.178.389.398.389.716 0 1.299.57 1.299 1.27v9.344c-.494-.624-1.191-1.094-2.01-1.31-.212-.056-.431.067-.488.275-.057.207.069.421.281.477 1.306.343 2.217 1.505 2.217 2.826 0 1.615-1.344 2.929-2.995 2.929-1.115 0-2.13-.602-2.65-1.569-.052-.096-.143-.166-.25-.194-1.305-.343-2.215-1.504-2.215-2.824 0-.197.02-.396.06-.589.037-.179-.058-.359-.228-.433-.929-.404-1.529-1.304-1.529-2.296 0-.451.119-.882.347-1.264.594.512 1.371.824 2.223.824.22 0 .398-.174.398-.389 0-.215-.178-.389-.398-.389-1.418 0-2.57-1.129-2.57-2.515 0-.746.337-1.45.924-1.929.133-.108.178-.288.113-.444-.126-.302-.189-.62-.189-.944 0-1.386 1.152-2.513 2.57-2.513 1.418 0 2.572 1.127 2.572 2.513 0 .215.178.389.398.389.22 0 .398-.174.398-.389 0-1.744-1.395-3.174-3.151-3.283-.159-.26-.242-.557-.242-.864 0-.693.43-1.288 1.043-1.546.024-.007.046-.017.068-.029.19-.071.395-.109.61-.109.666 0 1.261.364 1.552.949.096.193.333.273.53.179.198-.094.279-.325.183-.519-.332-.669-.961-1.149-1.685-1.319 1.015-.605 2.01-.949 2.812-.949m3.393 0c.798 0 1.797.344 2.812.949-.724.17-1.353.651-1.685 1.319-.096.193-.014.425.183.519.198.094.436.014.532-.179.291-.586.885-.949 1.55-.949.215 0 .421.038.61.109.022.011.047.022.07.029.613.258 1.043.853 1.043 1.546 0 .307-.085.604-.244.864-1.756.109-3.149 1.539-3.149 3.283 0 .215.178.389.398.389.22 0 .398-.174.398-.389 0-1.386 1.152-2.513 2.57-2.513 1.418 0 2.572 1.127 2.572 2.513 0 .324-.064.641-.189.944-.065.157-.019.336.113.444.587.48.924 1.183.924 1.929 0 1.386-1.154 2.515-2.572 2.515-.22 0-.398.174-.398.389 0 .215.178.389.398.389.852 0 1.629-.312 2.223-.824.228.382.349.813.349 1.264 0 .991-.6 1.892-1.529 2.296-.17.074-.265.254-.228.433.04.193.06.392.06.589 0 1.32-.912 2.48-2.217 2.824-.107.028-.196.099-.248.194-.52.967-1.537 1.569-2.652 1.569-1.652 0-2.995-1.314-2.995-2.929 0-.109-.006-.219-.016-.326.01-.033.016-.067.016-.103v-12.841c0-.7.583-1.27 1.299-1.27.22 0 .398-.174.398-.389 0-.215-.178-.389-.398-.389-.49 0-.941.167-1.299.444v-3.373c0-.7.583-1.27 1.299-1.27m5.938 6.686c-.22 0-.398.174-.398.389 0 1.386-1.152 2.513-2.57 2.513-.697 0-1.349-.266-1.837-.753-.154-.153-.407-.156-.564-.006-.157.15-.16.396-.006.549.551.55 1.264.887 2.036.969.183 1.183 1.23 2.093 2.49 2.093.22 0 .398-.174.398-.389 0-.215-.178-.389-.398-.389-.824 0-1.514-.569-1.683-1.325 1.65-.211 2.929-1.592 2.929-3.262 0-.215-.178-.389-.398-.389m-14.42.416c-.22 0-.398.172-.398.387 0 1.67 1.279 3.054 2.929 3.264-.169.756-.859 1.323-1.683 1.323-.22 0-.398.174-.398.389 0 .215.178.389.398.389 1.26 0 2.308-.91 2.49-2.093.772-.082 1.484-.416 2.036-.967.154-.153.151-.401-.006-.551-.157-.15-.408-.148-.562.006-.488.487-1.14.755-1.837.755-1.418 0-2.572-1.129-2.572-2.515 0-.215-.178-.387-.398-.387m8.483 5.804c-.22 0-.398.174-.398.389 0 .215.178.389.398.389 1.418 0 2.572 1.129 2.572 2.515 0 .089-.006.177-.016.265-1.234.489-2.106 1.673-2.106 3.052 0 .215.178.389.398.389.22 0 .398-.174.398-.389 0-1.386 1.154-2.515 2.572-2.515.22 0 .398-.174.398-.389 0-.215-.178-.389-.398-.389-.153 0-.305.011-.452.031 0-.019.002-.037.002-.055 0-1.815-1.511-3.293-3.368-3.293m-4.664.829c-1.856 0-3.368 1.478-3.368 3.293 0 .019.002.036.002.055-.148-.019-.299-.031-.452-.031-.22 0-.398.174-.398.389 0 .215.178.389.398.389 1.418 0 2.572 1.129 2.572 2.515 0 .215.178.389.398.389.22 0 .398-.174.398-.389 0-1.379-.872-2.564-2.106-3.052-.009-.088-.016-.176-.016-.265 0-1.386 1.154-2.515 2.572-2.515.22 0 .398-.174.398-.389 0-.215-.178-.389-.398-.389"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.4 KiB |
@@ -1,22 +0,0 @@
|
||||
<svg width="64" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" height="64" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient id="a" y1="392.36" y2="336.36" gradientUnits="userSpaceOnUse" x2="0" gradientTransform="translate(518 82)">
|
||||
<stop stop-color="#ffffff" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity=".2"/>
|
||||
</linearGradient>
|
||||
<path color-rendering="auto" color-interpolation-filters="linearRGB" shape-rendering="auto" image-rendering="auto" text-rendering="auto" id="b" color-interpolation="sRGB" color="#000000" d="m542 417.36v58h44v-58h-14z"/>
|
||||
</defs>
|
||||
<g transform="translate(-532-414.36)">
|
||||
<use fill="#f9d24c" xlink:href="#b"/>
|
||||
<g color-rendering="auto" color-interpolation-filters="linearRGB" shape-rendering="auto" image-rendering="auto" text-rendering="auto" color-interpolation="sRGB" color="#000000">
|
||||
<rect opacity=".25" x="542" y="474.36" width="44" height="1"/>
|
||||
<rect opacity=".5" x="542" y="417.36" width="44" fill="#ffffff" height="1"/>
|
||||
</g>
|
||||
<rect width="1" x="548" y="418.36" fill="#ffffff" height="56" fill-opacity=".321"/>
|
||||
<g color-rendering="auto" color-interpolation-filters="linearRGB" shape-rendering="auto" image-rendering="auto" text-rendering="auto" color-interpolation="sRGB" color="#000000">
|
||||
<rect x="549" y="418.36" fill-opacity=".057" width="1" height="56"/>
|
||||
<path opacity=".75" fill="#ffffff" d="m566 453.28l-6.916-6.917 6.916-6.911 2.307 2.302-4.614 4.609 2.307 2.308 6.916-6.917-6.03-6.02c-.489-.489-1.29-.489-1.779 0l-9.739 9.74c-.495.489-.495 1.29 0 1.786l9.739 9.74c.489.489 1.29.489 1.779 0l9.739-9.74c.495-.495.495-1.296 0-1.786l-1.412-1.412zm0 0"/>
|
||||
</g>
|
||||
<use fill="url(#a)" xlink:href="#b"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,113 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="64"
|
||||
version="1.1"
|
||||
viewBox="0 0 64 64"
|
||||
height="64"
|
||||
id="svg74"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="application-x-gzip.svg">
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1360"
|
||||
inkscape:window-height="708"
|
||||
id="namedview96"
|
||||
showgrid="false"
|
||||
inkscape:zoom="5.6568542"
|
||||
inkscape:cx="-5.9847467"
|
||||
inkscape:cy="40.374841"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg74"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-nodes="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4177" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs4">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient6251">
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:0"
|
||||
offset="0"
|
||||
id="stop6253" />
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:0.2"
|
||||
offset="1"
|
||||
id="stop6255" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient6251"
|
||||
id="linearGradient4850"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1,0,0,0.84587337,-46.999997,-272.73456)"
|
||||
x1="58"
|
||||
y1="393.95328"
|
||||
x2="58"
|
||||
y2="324.65894" />
|
||||
</defs>
|
||||
<metadata
|
||||
id="metadata84">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<rect
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#febf10;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
id="rect4838"
|
||||
width="49.999977"
|
||||
height="49.999977"
|
||||
x="7"
|
||||
y="10.999992" />
|
||||
<rect
|
||||
id="rect4840"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.25;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
height="1.0000085"
|
||||
y="59.999992"
|
||||
x="7"
|
||||
width="50" />
|
||||
<rect
|
||||
id="rect4842"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.5;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
height="1.0000085"
|
||||
y="10.999992"
|
||||
x="7"
|
||||
width="50" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="opacity:0.75;fill:#ffffff;fill-opacity:1"
|
||||
d="m 30,12 0,2 2,0 0,-2 -2,0 z m 2,2 0,2 2,0 0,-2 -2,0 z m 0,2 -2,0 0,2 2,0 0,-2 z m 0,2 0,2 2,0 0,-2 -2,0 z m 0,2 -2,0 0,2 2,0 0,-2 z m 0,2 0,2 2,0 0,-2 -2,0 z m 0,2 -2,0 0,2 2,0 0,-2 z m -2,3 0,4 1,0 0,4 2,0 0,-4 1,0 0,-4 -4,0 z m 1,1 2,0 0,2 -2,0 0,-2 z"
|
||||
id="rect4173-3-3" />
|
||||
<rect
|
||||
y="10.999992"
|
||||
x="7"
|
||||
height="49.999977"
|
||||
width="49.999977"
|
||||
id="rect4848"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#linearGradient4850);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -1,27 +0,0 @@
|
||||
<svg width="64" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" height="64" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient id="a" y1="17" y2="31" x1="40" x2="54" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#060606"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="b" y1="392.36" y2="336.36" x2="0" gradientUnits="userSpaceOnUse" gradientTransform="translate(-48-332.36)">
|
||||
<stop stop-color="#ffffff" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity=".2"/>
|
||||
</linearGradient>
|
||||
<path id="c" d="m10 61v-58h30l14 14v44h-14z"/>
|
||||
</defs>
|
||||
<use fill="#ffad37" xlink:href="#c"/>
|
||||
<g color-rendering="auto" color-interpolation-filters="linearRGB" shape-rendering="auto" image-rendering="auto" text-rendering="auto" color-interpolation="sRGB" color="#000000">
|
||||
<g fill="#ffffff">
|
||||
<path opacity=".15" d="m10 4v1h2v2h-2v1h2v2h-2v1h2v2h-2v1h2v2h-2v1h2v2h-2v1h2v2h-2v1h2v2h-2v1h2v2h-2v1h2v2h-2v1h2v2h-2v1h2v2h-2v1h2v2h-2v1h2v2h-2v1h2v2h-2v1h2v2h-2v1h2v2h-2v1h2v2h-2v1h2v2h-2v1h2v2h1v-2h2v2h1v-2h2v2h1v-2h2v2h1v-2h2v2h1v-2h2v2h1v-2h2v2h1v-2h2v2h1v-2h2v2h1v-2h2v2h1v-2h2v2h1v-2h2v2h1v-2h2v2h1v-2h2v2h1v-2h2v-1h-2v-2h2v-1h-2v-2h2v-1h-2v-2h2v-1h-2v-2h2v-1h-2v-2h2v-1h-2v-2h2v-1h-2v-2h2v-1h-2v-2h2v-1h-2v-2h2v-1h-2v-2h2v-1h-2v-2h2v-1h-2v-2h2v-1h-2v-2h2v-1h-2v-2h2l-1-1h-1v-1l-1-1v2h-2v-2h2l-1-1h-1v-1l-1-1v2h-2v-2h2l-1-1h-1v-1l-1-1v2h-2v-2h2l-1-1h-1v-1l-1-1v2h-2v-2h2l-1-1h-29zm3 1h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm-24 3h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm-27 3h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm-30 3h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm-33 3h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm-36 3h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm-36 3h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm-36 3h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm-36 3h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm-36 3h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm-36 3h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm-36 3h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm3 0h2v2h-2zm-36 3h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm-36 3h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm-36 3h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm-36 3h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm3 0h2v2h-2v-1zm-36 3h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm-36 3h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2z"/>
|
||||
<rect opacity=".5" x="10" y="-4" width="30" height="1" transform="scale(1-1)"/>
|
||||
</g>
|
||||
<rect opacity=".25" x="10" y="-61" width="44" height="1" transform="scale(1-1)"/>
|
||||
</g>
|
||||
<g fill-rule="evenodd">
|
||||
<path fill="#a46022" d="m54 17l-14-14v14z"/>
|
||||
<path opacity=".2" fill="url(#a)" d="m40 17l14 14v-14z"/>
|
||||
</g>
|
||||
<path color-interpolation-filters="linearRGB" color="#000000" image-rendering="auto" color-rendering="auto" d="m24 23v3h1v5.057a2.5 2.5 0 0 0 -2 2.44336 2.5 2.5 0 0 0 2 2.44531v5.05h-1v3h3v-3h-1v-3.529a13 10.500004 0 0 0 12 6.5293 13 10.500004 0 0 0 1 -.041v-.996a12 9.500009 0 0 1 -1 .0371 12 9.500009 0 0 1 -11.61914 -7.16406 2.5 2.5 0 0 0 1.61914 -2.33595 2.5 2.5 0 0 0 -1.61719 -2.33789 12 9.500009 0 0 1 11.61719 -7.16211 12 9.500009 0 0 1 1 .0391v-1.01a13 10.500004 0 0 0 -1 -.0332 13 10.500004 0 0 0 -12 6.51172v-3.512h1v-3zm1 1h1v1h-1zm.5 8a1.5 1.5 0 0 1 1.5 1.5 1.5 1.5 0 0 1 -1.5 1.5 1.5 1.5 0 0 1 -1.5 -1.5 1.5 1.5 0 0 1 1.5 -1.5m-.5 10h1v1h-1z" color-interpolation="sRGB" text-rendering="auto" fill="#99581d" shape-rendering="auto"/>
|
||||
<use fill="url(#b)" xlink:href="#c"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -1,26 +0,0 @@
|
||||
<svg width="64" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" height="64" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient id="a" y1="17" x1="40" y2="31" gradientUnits="userSpaceOnUse" x2="54" gradientTransform="translate(372 1234.36)">
|
||||
<stop stop-color="#060606"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="b" y1="392.36" y2="336.36" gradientUnits="userSpaceOnUse" x2="0" gradientTransform="translate(324 902)">
|
||||
<stop stop-color="#ffffff" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity=".2"/>
|
||||
</linearGradient>
|
||||
<path id="c" d="m382 1295.36v-58h30l14 14v44h-14z"/>
|
||||
</defs>
|
||||
<g transform="translate(-372-1234.36)">
|
||||
<use fill="#ffa555" xlink:href="#c"/>
|
||||
<g color-rendering="auto" color-interpolation-filters="linearRGB" shape-rendering="auto" image-rendering="auto" text-rendering="auto" color-interpolation="sRGB" color="#000000" transform="scale(1-1)">
|
||||
<rect opacity=".5" x="382" y="-1238.36" width="30" fill="#ffffff" height="1"/>
|
||||
<rect opacity=".25" x="382" y="-1295.36" width="44" height="1"/>
|
||||
</g>
|
||||
<g fill-rule="evenodd">
|
||||
<path opacity=".5" fill="#ffffff" d="m426 1251.36l-14-14v14z"/>
|
||||
<path opacity=".1" fill="url(#a)" d="m412 1251.36l14 14v-14z"/>
|
||||
</g>
|
||||
<path opacity=".75" color-interpolation-filters="linearRGB" color="#000000" image-rendering="auto" color-rendering="auto" d="m401 1256.36c-2.216 0-4 1.784-4 4v3c0 1.662-1.338 3-3 3v1c2.216 0 4-1.784 4-4v-3c0-1.662 1.338-3 3-3zm-7 11v1c1.662 0 3 1.338 3 3v3c0 2.216 1.784 4 4 4v-1c-1.662 0-3-1.338-3-3v-3c0-2.216-1.784-4-4-4m13-11v1c1.662 0 3 1.338 3 3v3c0 2.216 1.784 4 4 4v-1c-1.662 0-3-1.338-3-3v-3c0-2.216-1.784-4-4-4m7 11c-2.216 0-4 1.784-4 4v3c0 1.662-1.338 3-3 3v1c2.216 0 4-1.784 4-4v-3c0-1.662 1.338-3 3-3z" color-interpolation="sRGB" text-rendering="auto" fill="#ffffff" shape-rendering="auto"/>
|
||||
<use fill="url(#b)" xlink:href="#c"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,137 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="64"
|
||||
viewBox="0 0 64 64"
|
||||
height="64"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="application-json.svg">
|
||||
<metadata
|
||||
id="metadata39">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1382"
|
||||
id="namedview37"
|
||||
showgrid="false"
|
||||
inkscape:zoom="3.6875"
|
||||
inkscape:cx="32"
|
||||
inkscape:cy="32"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="36"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<linearGradient
|
||||
id="a"
|
||||
y1="17"
|
||||
y2="31"
|
||||
x1="40"
|
||||
x2="54"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
stop-color="#060606"
|
||||
id="stop7" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-opacity="0"
|
||||
id="stop9" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="b"
|
||||
y1="392.36"
|
||||
y2="336.36"
|
||||
x2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(384,822)">
|
||||
<stop
|
||||
stop-color="#ffffff"
|
||||
stop-opacity="0"
|
||||
id="stop12" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#ffffff"
|
||||
stop-opacity=".2"
|
||||
id="stop14" />
|
||||
</linearGradient>
|
||||
<path
|
||||
id="c"
|
||||
d="m442 1215.36v-58h30l14 14v44h-14z" />
|
||||
</defs>
|
||||
<use
|
||||
height="100%"
|
||||
width="100%"
|
||||
y="0"
|
||||
x="0"
|
||||
style="fill:#cf74e0"
|
||||
id="use19"
|
||||
xlink:href="#c"
|
||||
transform="translate(-432,-1154.36)" />
|
||||
<rect
|
||||
x="10"
|
||||
y="-3.9999855"
|
||||
width="30"
|
||||
height="1"
|
||||
id="rect23"
|
||||
style="color:#000000;opacity:0.5;color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:#ffffff;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
|
||||
transform="scale(1,-1)" />
|
||||
<rect
|
||||
x="10"
|
||||
y="-60.999985"
|
||||
width="44"
|
||||
height="1"
|
||||
id="rect25"
|
||||
style="color:#000000;opacity:0.25;color-interpolation:sRGB;color-interpolation-filters:linearRGB;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
|
||||
transform="scale(1,-1)" />
|
||||
<path
|
||||
d="M 54,17 40,3 40,17 Z"
|
||||
id="path29"
|
||||
inkscape:connector-curvature="0"
|
||||
style="opacity:0.5;fill:#ffffff;fill-rule:evenodd" />
|
||||
<path
|
||||
d="M 40,17 54,31 54,17 Z"
|
||||
id="path31"
|
||||
style="opacity:0.1;fill:url(#a);fill-rule:evenodd"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="color:#000000;opacity:0.75;color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:#ffffff;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path33-6"
|
||||
d="m 29,22 c -2.216,0 -4,1.784 -4,4 l 0,3 c 0,1.662 -1.338,3 -3,3 l 0,1 c 2.216,0 4,-1.784 4,-4 l 0,-3 c 0,-1.662 1.338,-3 3,-3 z m -7,11 0,1 c 1.662,0 3,1.338 3,3 l 0,3 c 0,2.216 1.784,4 4,4 l 0,-1 c -1.662,0 -3,-1.338 -3,-3 l 0,-3 c 0,-2.216 -1.784,-4 -4,-4 m 13,-11 0,1 c 1.662,0 3,1.338 3,3 l 0,3 c 0,2.216 1.784,4 4,4 l 0,-1 c -1.662,0 -3,-1.338 -3,-3 l 0,-3 c 0,-2.216 -1.784,-4 -4,-4 m 7,11 c -2.216,0 -4,1.784 -4,4 l 0,3 c 0,1.662 -1.338,3 -3,3 l 0,1 c 2.216,0 4,-1.784 4,-4 l 0,-3 c 0,-1.662 1.338,-3 3,-3 z" />
|
||||
<use
|
||||
height="100%"
|
||||
width="100%"
|
||||
y="0"
|
||||
x="0"
|
||||
style="fill:url(#b)"
|
||||
id="use35"
|
||||
xlink:href="#c"
|
||||
transform="translate(-432,-1154.36)" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,27 +0,0 @@
|
||||
<svg width="64" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" height="64" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient id="a" y1="17" x1="40" y2="31" gradientUnits="userSpaceOnUse" x2="54">
|
||||
<stop stop-color="#060606"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="b" y1="392.36" y2="336.36" gradientUnits="userSpaceOnUse" x2="0" gradientTransform="translate(-48-332.36)">
|
||||
<stop stop-color="#ffffff" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity=".2"/>
|
||||
</linearGradient>
|
||||
<path id="c" d="m10 61v-58h30l14 14v44h-14z"/>
|
||||
</defs>
|
||||
<g color-rendering="auto" color-interpolation-filters="linearRGB" shape-rendering="auto" image-rendering="auto" text-rendering="auto" color-interpolation="sRGB" color="#000000">
|
||||
<use fill="#555555" xlink:href="#c"/>
|
||||
<g transform="scale(1-1)">
|
||||
<rect opacity=".4" x="10" y="-4" width="30" fill="#ffffff" height="1"/>
|
||||
<rect opacity=".35" x="10" y="-61" width="44" height="1"/>
|
||||
</g>
|
||||
<path opacity=".5" fill="#ffffff" fill-rule="evenodd" d="m54 17l-14-14v14z"/>
|
||||
</g>
|
||||
<path opacity=".4" fill="url(#a)" fill-rule="evenodd" d="m40 17l14 14v-14z"/>
|
||||
<g color-rendering="auto" color-interpolation-filters="linearRGB" shape-rendering="auto" image-rendering="auto" text-rendering="auto" color-interpolation="sRGB" color="#000000">
|
||||
<path fill="#343434" d="m20 24v22h26v-22zm18 1c.552 0 1 .448 1 1 0 .552-.448 1-1 1-.552 0-1-.448-1-1 0-.552.448-1 1-1m3 0c.552 0 1 .448 1 1 0 .552-.448 1-1 1-.552 0-1-.448-1-1 0-.552.448-1 1-1m3 0c.552 0 1 .448 1 1 0 .552-.448 1-1 1-.552 0-1-.448-1-1 0-.552.448-1 1-1m-23 3h24v17h-24zm8.799 4l2.713 4.721-3.094 5.279h1.184l2.502-4.416 2.5 4.416h1.131c.013-.01.027-.019.039-.029l-3.057-5.25 2.697-4.721h-1.211l-2.1 3.857-2.1-3.857z"/>
|
||||
<path fill="#e0e0e0" d="m19 23v22h26v-22zm18 1c.552 0 1 .448 1 1 0 .552-.448 1-1 1-.552 0-1-.448-1-1 0-.552.448-1 1-1m3 0c.552 0 1 .448 1 1 0 .552-.448 1-1 1-.552 0-1-.448-1-1 0-.552.448-1 1-1m3 0c.552 0 1 .448 1 1 0 .552-.448 1-1 1-.552 0-1-.448-1-1 0-.552.448-1 1-1m-23 3h24v17h-24zm8.799 4l2.713 4.721-3.094 5.279h1.184l2.502-4.416 2.5 4.416h1.131c.013-.01.027-.019.039-.029l-3.057-5.25 2.697-4.721h-1.211l-2.1 3.857-2.1-3.857z"/>
|
||||
</g>
|
||||
<use fill="url(#b)" xlink:href="#c"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |