Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1de93ab10e | |||
| 7e8381f2fa | |||
| e70ab4a4f2 | |||
| 922c46e9be | |||
| 9db128b5e2 | |||
| 46588c7cca | |||
| 59b37af931 | |||
| 4221ac9595 | |||
| 500da2df25 | |||
| 611462ffd7 | |||
| d689066298 | |||
| 68b8d936e7 | |||
| e8b985a266 |
@@ -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) 2021 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,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,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,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,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,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,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,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 |
@@ -1,24 +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(-48-332.36)">
|
||||
<stop stop-color="#ffffff" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity=".2"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="b" y1="17" x1="40" y2="31" gradientUnits="userSpaceOnUse" x2="54">
|
||||
<stop stop-color="#060606"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<path id="c" d="m10 61v-58h30l14 14v44h-14z"/>
|
||||
</defs>
|
||||
<use fill="#555555" 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=".4" x="10" y="-4" width="30" fill="#ffffff" height="1"/>
|
||||
<rect opacity=".35" x="10" y="-61" width="44" height="1"/>
|
||||
</g>
|
||||
<g fill-rule="evenodd">
|
||||
<path opacity=".5" fill="#ffffff" d="m54 17l-14-14v14z"/>
|
||||
<path opacity=".4" fill="url(#b)" d="m40 17l14 14v-14z"/>
|
||||
</g>
|
||||
<path opacity=".75" color-interpolation-filters="linearRGB" color="#000000" image-rendering="auto" color-rendering="auto" d="m40 26.01l-8 .004-4.441 7.691-1.555-2.695h-2v1h1.428l2.133 3.693 5.02-8.693h6.42v1h1zm-5 3c-1.662 0-3 1.338-3 3 0 1.662 1.338 3 3 3 .773 0 1.469-.298 2-.775v.775h1v-6h-1v.775c-.531-.477-1.227-.775-2-.775m0 1c1.108 0 2 .892 2 2 0 1.108-.892 2-2 2-1.108 0-2-.892-2-2 0-1.108.892-2 2-2m-11.99 6.99c-.006 0-.01.223-.01.5 0 .277.004.5.01.5h17.98c.006 0 .01-.223.01-.5 0-.277-.004-.5-.01-.5zm8 3c-.006 0-.01.223-.01.5 0 .277.004.5.01.5h1.98c.006 0 .01-.223.01-.5 0-.277-.004-.5-.01-.5z" color-interpolation="sRGB" text-rendering="auto" fill="#ffffff" shape-rendering="auto"/>
|
||||
<use fill="url(#a)" xlink:href="#c"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,222 +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-msonenote.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" />
|
||||
</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="640"
|
||||
inkscape:window-height="480"
|
||||
id="namedview37"
|
||||
showgrid="false"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:zoom="1"
|
||||
inkscape:cx="36.561651"
|
||||
inkscape:cy="28.512949"
|
||||
inkscape:current-layer="svg2">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4219" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs4">
|
||||
<linearGradient
|
||||
id="a"
|
||||
y1="61"
|
||||
y2="3"
|
||||
x2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1 0 0-1 0 64)">
|
||||
<stop
|
||||
stop-color="#913d88"
|
||||
id="stop7" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#9b4792"
|
||||
id="stop9" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="b"
|
||||
y1="61"
|
||||
y2="47"
|
||||
x2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1 0 0-1 0 64)">
|
||||
<stop
|
||||
stop-color="#d5a5d0"
|
||||
id="stop12" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#e7cbe4"
|
||||
id="stop14" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="c"
|
||||
y1="17"
|
||||
x1="40"
|
||||
y2="31"
|
||||
x2="54"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
stop-color="#383e51"
|
||||
id="stop17" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#655c6f"
|
||||
stop-opacity="0"
|
||||
id="stop19" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
gradientTransform="matrix(1 0 0-1 0 64)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="0"
|
||||
y2="3"
|
||||
y1="61"
|
||||
id="a-7">
|
||||
<stop
|
||||
id="stop7-5"
|
||||
stop-color="#3a539b" />
|
||||
<stop
|
||||
id="stop9-3"
|
||||
stop-color="#3f5aa9"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
gradientTransform="matrix(1 0 0-1 0 64)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="0"
|
||||
y2="47"
|
||||
y1="61"
|
||||
id="b-5">
|
||||
<stop
|
||||
id="stop12-6"
|
||||
stop-color="#97aad8" />
|
||||
<stop
|
||||
id="stop14-2"
|
||||
stop-color="#c1cae7"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<path
|
||||
d="m40 17l14 14v-14z"
|
||||
id="d"
|
||||
fill-rule="evenodd"
|
||||
fill="url(#c)"
|
||||
opacity=".2" />
|
||||
<linearGradient
|
||||
id="a-3"
|
||||
y1="392.36"
|
||||
y2="336.36"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="0"
|
||||
gradientTransform="translate(-48,-332.36)">
|
||||
<stop
|
||||
stop-color="#ffffff"
|
||||
stop-opacity="0"
|
||||
id="stop4173" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#ffffff"
|
||||
stop-opacity=".2"
|
||||
id="stop4175" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="b-6"
|
||||
y1="17"
|
||||
x1="40"
|
||||
y2="31"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="54">
|
||||
<stop
|
||||
stop-color="#060606"
|
||||
id="stop4178" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-opacity="0"
|
||||
id="stop4180" />
|
||||
</linearGradient>
|
||||
<path
|
||||
id="c-7"
|
||||
d="m10 61v-58h30l14 14v44h-14z" />
|
||||
</defs>
|
||||
<g
|
||||
id="g4242">
|
||||
<use
|
||||
height="100%"
|
||||
width="100%"
|
||||
y="0"
|
||||
x="0"
|
||||
style="fill:#913d88;fill-opacity:1"
|
||||
xlink:href="#c-7"
|
||||
id="use4183" />
|
||||
<rect
|
||||
id="rect4187"
|
||||
height="1"
|
||||
width="30"
|
||||
y="-4"
|
||||
x="10"
|
||||
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
|
||||
id="rect4189"
|
||||
height="1"
|
||||
width="44"
|
||||
y="-61"
|
||||
x="10"
|
||||
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
|
||||
id="path4193"
|
||||
d="M 54,17 40,3 40,17 Z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="opacity:0.5;fill:#ffffff;fill-rule:evenodd" />
|
||||
<path
|
||||
style="opacity:0.2;fill:url(#b-6);fill-rule:evenodd"
|
||||
id="path4195"
|
||||
d="M 40,17 54,31 54,17 Z"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
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: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="path35"
|
||||
d="m 22,22 c -0.554,0 -1,0.446 -1,1 l 0,20 c 0,0.554 0.446,1 1,1 l 18,0 c 0.554,0 1,-0.446 1,-1 l 0,-1 1,0 c 0.554,0 1,-0.446 1,-1 l 0,-4 C 43,36.814 42.936,36.649 42.848,36.5 42.936,36.351 43,36.186 43,36 l 0,-4 C 43,31.814 42.936,31.649 42.848,31.5 42.936,31.351 43,31.186 43,31 l 0,-4 c 0,-0.554 -0.446,-1 -1,-1 l -1,0 0,-3 c 0,-0.554 -0.446,-1 -1,-1 z m 0,1 18,0 0,20 -18,0 z m 1,2 0,1 7,0 0,-1 z m 9,0 0,1 7,0 0,-1 z m 9,2 1,0 0,4 -1,0 z m -18,1 0,1 7,0 0,-1 z m 9,0 0,1 7,0 0,-1 z m -9,3 0,1 7,0 0,-1 z m 9,0 0,1 7,0 0,-1 z m 9,1 1,0 0,4 -1,0 z m -18,2 0,1 7,0 0,-1 z m 9,0 0,1 7,0 0,-1 z m -9,3 0,1 7,0 0,-1 z m 9,0 0,1 7,0 0,-1 z m 9,0 1,0 0,4 -1,0 z m -18,3 0,1 7,0 0,-1 z m 9,0 0,1 7,0 0,-1 z" />
|
||||
<use
|
||||
height="100%"
|
||||
width="100%"
|
||||
y="0"
|
||||
x="0"
|
||||
xlink:href="#c-7"
|
||||
id="use4199"
|
||||
style="fill:url(#a-3)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.6 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="61" y2="3" x2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1 0 0-1 0 64)">
|
||||
<stop stop-color="#3a539b"/>
|
||||
<stop offset="1" stop-color="#3f5aa9"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="b" y1="61" y2="47" x2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1 0 0-1 0 64)">
|
||||
<stop stop-color="#97aad8"/>
|
||||
<stop offset="1" stop-color="#c1cae7"/>
|
||||
</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>
|
||||
<path opacity=".2" fill="url(#c)" fill-rule="evenodd" id="d" d="m40 17l14 14v-14z"/>
|
||||
</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>
|
||||
<path fill="url(#b)" fill-rule="evenodd" d="m54 17l-14-14v14z"/>
|
||||
<use xlink:href="#d"/>
|
||||
<use xlink:href="#d"/>
|
||||
<path opacity=".75" fill="#ffffff" color-rendering="auto" color-interpolation-filters="linearRGB" shape-rendering="auto" image-rendering="auto" text-rendering="auto" color-interpolation="sRGB" color="#000000" d="m21 24c-.554 0-1 .446-1 1v16c0 .554.446 1 1 1h22c.554 0 1-.446 1-1v-16c0-.554-.446-1-1-1zm0 1h.047l7.525 7.182-7.572 7.227zm1.258 0h19.484l-9.742 9.297zm20.695 0h.047v14.408l-7.572-7.227zm-13.775 7.76l2.822 2.695 2.822-2.695 8.178 7.807v.434h-22v-.434z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,25 +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(-48-332.36)">
|
||||
<stop stop-color="#ffffff" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity=".2"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="b" y1="17" x1="40" y2="31" gradientUnits="userSpaceOnUse" x2="54">
|
||||
<stop stop-color="#060606"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</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="#0b0b64" xlink:href="#c"/>
|
||||
<g fill="#ffffff">
|
||||
<path opacity=".1" 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)"/>
|
||||
<path fill="#8282dd" d="m54 17l-14-14v14z"/>
|
||||
</g>
|
||||
<path opacity=".2" fill="url(#b)" fill-rule="evenodd" d="m40 17l14 14v-14z"/>
|
||||
<use opacity=".7" fill="url(#a)" xlink:href="#c"/>
|
||||
<path color-interpolation-filters="linearRGB" color="#000000" image-rendering="auto" color-rendering="auto" d="m22 22c-.554 0-1 .446-1 1v20c0 .554.446 1 1 1h20c.554 0 1-.446 1-1v-20c0-.554-.446-1-1-1zm0 1h20v20h-20v-19zm1 2v1h8v-1zm10 0v1h8v-1zm-9.949 3v1h7.949v-1zm9.949 0v1h8v-1zm-10 3v1h8v-1zm10 0v1h8v-1zm-9.949 3v1h7.949v-1zm9.949 0v1h8v-1zm-10 3v1h8v-1zm10 0v1h8v-1zm-9.949 3v1h7.949v-1zm9.949 0v1h8v-1z" color-interpolation="sRGB" text-rendering="auto" fill="#8282dd" shape-rendering="auto"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.0 KiB |
@@ -1,24 +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(-48-332.36)">
|
||||
<stop stop-color="#ffffff" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity=".2"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="b" y1="17" x1="40" y2="31" gradientUnits="userSpaceOnUse" x2="54">
|
||||
<stop stop-color="#060606"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<path id="c" d="m10 61v-58h30l14 14v44h-14z"/>
|
||||
</defs>
|
||||
<use fill="#4040bf" 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="10" y="-4" width="30" fill="#ffffff" height="1"/>
|
||||
<rect opacity=".25" x="10" y="-61" width="44" height="1"/>
|
||||
</g>
|
||||
<g fill-rule="evenodd">
|
||||
<path opacity=".5" fill="#ffffff" d="m54 17l-14-14v14z"/>
|
||||
<path opacity=".2" fill="url(#b)" d="m40 17l14 14v-14z"/>
|
||||
</g>
|
||||
<path opacity=".75" color-interpolation-filters="linearRGB" color="#000000" image-rendering="auto" color-rendering="auto" d="m22 22c-.554 0-1 .446-1 1v20c0 .554.446 1 1 1h20c.554 0 1-.446 1-1v-20c0-.554-.446-1-1-1zm0 1h20v20h-20v-19zm1 2v1h8v-1zm10 0v1h8v-1zm-9.949 3v1h7.949v-1zm9.949 0v1h8v-1zm-10 3v1h8v-1zm10 0v1h8v-1zm-9.949 3v1h7.949v-1zm9.949 0v1h8v-1zm-10 3v1h8v-1zm10 0v1h8v-1zm-9.949 3v1h7.949v-1zm9.949 0v1h8v-1z" color-interpolation="sRGB" text-rendering="auto" fill="#ffffff" shape-rendering="auto"/>
|
||||
<use fill="url(#a)" xlink:href="#c"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 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="61" y2="3" 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 opacity=".75" fill="url(#b)" d="m54 17l-14-14v14z"/>
|
||||
<path opacity=".2" fill="url(#c)" d="m40 17l14 14v-14z"/>
|
||||
<path opacity=".75" fill="#fbfbfb" d="m21 22v22h22v-22zm1 5h20v16h-20zm10.5 2c-.972 0-1.88.341-2.52.98-.64.64-.98 1.547-.98 2.52h2c0-.528.159-.87.395-1.105.235-.235.578-.395 1.105-.395.528 0 .87.159 1.105.395.235.235.395.578.395 1.105 0 .458-.09.653-.211.826-.521.526-1.152.976-1.658 1.406-.572.429-1.131.992-1.131 1.768v.5h2v-.5c.711-.728 1.77-1.362 2.43-2.029.348-.499.57-1.179.57-1.971 0-.972-.341-1.88-.98-2.52-.64-.64-1.547-.98-2.52-.98m-1.5 10v2h2v-2z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,18 +0,0 @@
|
||||
<svg version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="a" x2="0" y1="392.36" y2="336.36" gradientTransform="translate(-6,-332.36)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fff" stop-opacity="0" offset="0"/>
|
||||
<stop stop-color="#fff" stop-opacity=".2" offset="1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g transform="translate(0,-4)" color="#000000" color-rendering="auto" image-rendering="auto" shape-rendering="auto">
|
||||
<circle cx="37" cy="36" r="24" fill="#fbeaab"/>
|
||||
<path d="m5 11v50h50v-20h-4.096c-2.717 0-4.904-2.187-4.904-4.904v-0.191c0-2.717 2.187-4.904 4.904-4.904h4.096v-20z" fill="#f9d24c"/>
|
||||
<rect x="5" y="60" width="50" height="1" opacity=".25"/>
|
||||
<g fill="#fff">
|
||||
<rect x="5" y="11" width="50" height="1" opacity=".5"/>
|
||||
<path d="m21 25v16.05c-0.635-0.648-1.517-1.053-2.5-1.053-1.939 0-3.5 1.561-3.5 3.5s1.561 3.5 3.5 3.5c1.905 0 3.437-1.509 3.49-3.4h0.01v-13.6h14v8.05c-0.635-0.648-1.517-1.053-2.5-1.053-1.939 0-3.5 1.561-3.5 3.5s1.561 3.5 3.5 3.5 3.5-1.561 3.5-3.5v-15.5h-15zm1 2h14v2h-14z" opacity=".75" stroke-width="2"/>
|
||||
</g>
|
||||
<path d="m5 11v50h50v-9.158a24 24 0 0 0 6-15.841797 24 24 0 0 0-6-15.830078v-9.17z" fill="url(#a)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,20 +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" x2="0" gradientUnits="userSpaceOnUse" gradientTransform="translate(-14-332.36)">
|
||||
<stop stop-color="#ffffff" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity=".15"/>
|
||||
</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="m10 3v58h44v-58h-14z"/>
|
||||
</defs>
|
||||
<use fill="#da2c2c" 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="10" y="60" width="44" height="1"/>
|
||||
<g fill="#ffffff">
|
||||
<rect opacity=".5" x="10" y="3" width="44" height="1"/>
|
||||
<path opacity=".8" d="m26.301 17.981c-.363.013-.752.11-1.179.293-3.813 1.629-1 5.371 3.856 8.793.029-.656.047-1.298.04-1.913-4.147-2.838-5.763-5.656-3.768-6.321 2.139-.885 3.718 1.737 3.768 6.321.26.177.521.351.794.53-.101-4.172-1.016-7.444-3.151-7.685-.114-.013-.239-.02-.36-.016m3.511 7.701l-.497-.788-.631.544-.132.828.832 1.569.418-.201c2.797 1.864 6.138 3.581 9.273 4.76l.931.542 1.287.031c1.186-.703.479-.962 4.789-1.495l-4.856.369c-3.746-1.45-7.94-3.887-11.417-6.159m9.343 6.692c.562.045 4.52-.647 6.264-.815.667-.087 3.31-.277 3.444 1.67.456-1.061-.23-2.066-1.71-2.381-2.287-.512-6.693 1.103-7.998 1.526m2.526-.492c-.68.244-1.828.219-2.562.517 1.041.533 6.371 1.947 8.528 1.542.345-.054 1.289-.316 1.325-1.076-1.728 1.201-6.498.358-7.291-.982m1.421.022c-.559-.179-3.444.709-4.02.49-3.547 1.182-7.432 2.883-10.591 4.698l-.915.181-.707 1.262-.949 1.235 1.07 1.065.837-1.374c3.699-2.218 11.01-5.83 15.277-7.56m-15.277 7.56c-.452.271-.884.539-1.293.799-.474 1.345-1.84 4.302-2.778 4.211 0 0-.172.143-.446.371 1.441.828 3.335-1.76 4.518-5.381m-4.072 5.01c-2.488-1.468 1.599-3.621 2.778-4.211.266-.759.517-1.582.754-2.46-3.715 2.312-6.04 4.709-4.796 6.293.3.383.539.659.816.748m3.979-7.04c.385-.239.79-.474 1.204-.712.732-2.952 1.217-6.347 1.318-9.458-.282-.188-.557-.378-.826-.568-.157 3.502-.808 7.457-1.694 10.738"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect width="1" x="16" y="4" fill="#ffffff" height="56" fill-opacity=".252"/>
|
||||
<rect color-interpolation-filters="linearRGB" x="17" y="4" fill-opacity=".083" color="#000000" image-rendering="auto" color-rendering="auto" width="1" color-interpolation="sRGB" text-rendering="auto" height="56" shape-rendering="auto"/>
|
||||
<use fill="url(#a)" xlink:href="#b"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 192 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="#51db51" 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 transform="scale(1-1)">
|
||||
<rect opacity=".5" x="10" y="-4" width="30" fill="#ffffff" height="1"/>
|
||||
<rect opacity=".25" x="10" y="-61" width="44" height="1"/>
|
||||
</g>
|
||||
<path opacity=".5" fill="#ffffff" d="m54 17l-14-14v14z"/>
|
||||
</g>
|
||||
<path opacity=".2" fill="url(#a)" fill-rule="evenodd" d="m40 17l14 14v-14z"/>
|
||||
<g fill="#ffffff" color-rendering="auto" color-interpolation-filters="linearRGB" shape-rendering="auto" image-rendering="auto" text-rendering="auto" color-interpolation="sRGB" color="#000000">
|
||||
<path opacity=".75" d="m35.734 22.01c-3.983 0-7.211 3.228-7.211 7.207 0 1.082.245 2.103.672 3.02l-1.234 1.461-.107 1.369c-.011.01-1.344.049-1.344.049l-.686.496c0 0 .006 1.495 0 1.475l-.002.002-1.467.023-.621.709-.014 1.34c0 0-1.266-.083-1.395.035-.81.746-1.355 1.277-1.355 1.277l.006 3.506c0 0 .469-.002.688-.002l9.338-9.336c.495.431 1.049.794 1.648 1.078l.002-.002c.936.444 1.979.701 3.084.701 3.983 0 7.211-3.227 7.211-7.207 0-3.979-3.228-7.206-7.213-7.205m1.377 3.434c1.327 0 2.402 1.077 2.402 2.404 0 1.327-1.075 2.402-2.402 2.402-1.328 0-2.404-1.075-2.404-2.402 0-1.327 1.076-2.404 2.404-2.404"/>
|
||||
<path opacity=".5" d="m37.11 25.445c-1.328 0-2.404 1.077-2.404 2.404 0 1.327 1.076 2.402 2.404 2.402 1.327 0 2.402-1.075 2.402-2.402 0-1.327-1.075-2.404-2.402-2.404m0 1.373c.569 0 1.029.462 1.029 1.031 0 .569-.461 1.029-1.029 1.029-.569 0-1.031-.461-1.031-1.029 0-.569.463-1.031 1.031-1.031m-6.109 7.828l-9.342 9.34h3.434l8.154-8.01c-.832-.307-1.59-.757-2.246-1.328"/>
|
||||
</g>
|
||||
<use fill="url(#b)" xlink:href="#c"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 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,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="#51db51" 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 transform="scale(1-1)">
|
||||
<rect opacity=".5" x="10" y="-4" width="30" fill="#ffffff" height="1"/>
|
||||
<rect opacity=".25" x="10" y="-61" width="44" height="1"/>
|
||||
</g>
|
||||
<path opacity=".5" fill="#ffffff" d="m54 17l-14-14v14z"/>
|
||||
</g>
|
||||
<path opacity=".2" fill="url(#a)" fill-rule="evenodd" d="m40 17l14 14v-14z"/>
|
||||
<g fill="#ffffff" color-rendering="auto" color-interpolation-filters="linearRGB" shape-rendering="auto" image-rendering="auto" text-rendering="auto" color-interpolation="sRGB" color="#000000">
|
||||
<path opacity=".75" d="m35.734 22.01c-3.983 0-7.211 3.228-7.211 7.207 0 1.082.245 2.103.672 3.02l-1.234 1.461-.107 1.369c-.011.01-1.344.049-1.344.049l-.686.496c0 0 .006 1.495 0 1.475l-.002.002-1.467.023-.621.709-.014 1.34c0 0-1.266-.083-1.395.035-.81.746-1.355 1.277-1.355 1.277l.006 3.506c0 0 .469-.002.688-.002l9.338-9.336c.495.431 1.049.794 1.648 1.078l.002-.002c.936.444 1.979.701 3.084.701 3.983 0 7.211-3.227 7.211-7.207 0-3.979-3.228-7.206-7.213-7.205m1.377 3.434c1.327 0 2.402 1.077 2.402 2.404 0 1.327-1.075 2.402-2.402 2.402-1.328 0-2.404-1.075-2.404-2.402 0-1.327 1.076-2.404 2.404-2.404"/>
|
||||
<path opacity=".5" d="m37.11 25.445c-1.328 0-2.404 1.077-2.404 2.404 0 1.327 1.076 2.402 2.404 2.402 1.327 0 2.402-1.075 2.402-2.402 0-1.327-1.075-2.404-2.402-2.404m0 1.373c.569 0 1.029.462 1.029 1.031 0 .569-.461 1.029-1.029 1.029-.569 0-1.031-.461-1.031-1.029 0-.569.463-1.031 1.031-1.031m-6.109 7.828l-9.342 9.34h3.434l8.154-8.01c-.832-.307-1.59-.757-2.246-1.328"/>
|
||||
</g>
|
||||
<use fill="url(#b)" xlink:href="#c"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 13 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,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="#f62459"/>
|
||||
<stop offset="1" stop-color="#f73768"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="b" y1="61" y2="47" x2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1 0 0-1 0 64)">
|
||||
<stop stop-color="#ffd6af"/>
|
||||
<stop offset="1" stop-color="#fffcf9"/>
|
||||
</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"/>
|
||||
<path fill="#fffcf9" d="m24 22v7h-3v11h6v4h10v-4h6v-11h-3v-7zm1 1h14v6h-2v-1h-10v1h-2zm2 2v1h10v-1zm-5 5h20v9h-3v-3h-14v3h-3zm14 2v1h4v-1zm-8 8h8v3h-8z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 12 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,24 +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=".1"/>
|
||||
</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="#0c2d63" xlink:href="#c"/>
|
||||
<g transform="scale(1-1)">
|
||||
<rect opacity=".5" x="10" y="-4" width="30" fill="#ffffff" height="1"/>
|
||||
<rect opacity=".25" x="10" y="-61" width="44" height="1"/>
|
||||
</g>
|
||||
<path fill="#1d70f7" d="m54 17l-14-14v14z"/>
|
||||
</g>
|
||||
<path opacity=".2" fill="url(#a)" fill-rule="evenodd" d="m40 17l14 14v-14z"/>
|
||||
<path fill="#1d70f7" d="m21 22v1h22v-1zm0 3v1h22v-1zm0 3v1h11v-1zm0 4v1h22v-1zm0 3v1h19v-1zm0 2v1h11v-1zm0 3v1h22v-1zm0 3v1h16v-1z"/>
|
||||
<use fill="url(#b)" xlink:href="#c"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,24 +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" x2="54" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#383e51"/>
|
||||
<stop offset="1" stop-color="#655c6f" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="b" y1="2.978" y2="60.978" x2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1 0 0-1 0 63.978)">
|
||||
<stop stop-color="#ffffff" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity=".2"/>
|
||||
</linearGradient>
|
||||
<path id="c" d="m10 60.978v-58h30l14 14v44h-14z"/>
|
||||
</defs>
|
||||
<use fill="#8542c2" xlink:href="#c"/>
|
||||
<g transform="scale(1-1)">
|
||||
<rect width="30" x="10" opacity=".5" y="-4" fill="#ffffff" height="1"/>
|
||||
<rect width="44" x="10" opacity=".25" y="-61" height="1"/>
|
||||
</g>
|
||||
<g fill-rule="evenodd">
|
||||
<path opacity=".5" fill="#ffffff" d="m54 17l-14-14v14z"/>
|
||||
<path opacity=".2" fill="url(#a)" d="m40 17l14 14v-14z"/>
|
||||
</g>
|
||||
<path opacity=".75" color-interpolation-filters="linearRGB" color="#4d4d4d" image-rendering="auto" color-rendering="auto" d="m32 21.99999a11 3 0 0 0 -11 3v16a11 3 0 0 0 11 3 11 3 0 0 0 11 -3v-16a11 3 0 0 0 -11 -3m0 1a10 2 0 0 1 10 2 10 2 0 0 1 -10 2 10 2 0 0 1 -10 -2 10 2 0 0 1 10 -2m-10 3.24219a11 3 0 0 0 10 1.75781 11 3 0 0 0 10 -1.75781v3.758a10 2 0 0 1 -10 2 10 2 0 0 1 -10 -2zm0 5a11 3 0 0 0 10 1.75781 11 3 0 0 0 10 -1.75781v4.758a10 2 0 0 1 -10 2 10 2 0 0 1 -10 -2zm0 6a11 3 0 0 0 10 1.75781 11 3 0 0 0 10 -1.75781v3.758a10 2 0 0 1 -10 2 10 2 0 0 1 -10 -2v-1z" color-interpolation="sRGB" text-rendering="auto" fill="#ffffff" shape-rendering="auto"/>
|
||||
<use fill="url(#b)" xlink:href="#c"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 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(46 296.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(-2-36)">
|
||||
<stop stop-color="#ffffff" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity=".2"/>
|
||||
</linearGradient>
|
||||
<path id="c" d="m56 357.36v-58h30l14 14v44h-14z"/>
|
||||
</defs>
|
||||
<g transform="translate(-46-296.36)">
|
||||
<use fill="#1bb2fb" 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="56" y="-300.36" width="30" fill="#ffffff" height="1"/>
|
||||
<rect opacity=".25" x="56" y="-357.36" width="44" height="1"/>
|
||||
</g>
|
||||
<g fill-rule="evenodd">
|
||||
<path opacity=".5" fill="#ffffff" d="m100 313.36l-14-14v14z"/>
|
||||
<path opacity=".2" fill="url(#a)" d="m86 313.36l14 14v-14z"/>
|
||||
</g>
|
||||
<path opacity=".9" color-interpolation-filters="linearRGB" color="#000000" image-rendering="auto" color-rendering="auto" d="m67 318.36v1h22v-1zm0 3v1h22v-1zm0 3v1h11v-1zm0 4v1h22v-1zm0 3v1h9v-1zm13 0v9h9v-9zm1 1h7v7h-7zm-14 1v1h5v-1zm0 3v1h10v-1zm0 3v1h6v-1z" 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.6 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,106 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 64 64">
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="a"
|
||||
y1="17"
|
||||
y2="31"
|
||||
x1="40"
|
||||
x2="54"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(238 78.36)">
|
||||
<stop
|
||||
stop-color="#060606"
|
||||
id="stop2" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-opacity="0"
|
||||
id="stop4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="b"
|
||||
y1="392.36"
|
||||
y2="336.36"
|
||||
x2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(190-254)">
|
||||
<stop
|
||||
stop-color="#ffffff"
|
||||
stop-opacity="0"
|
||||
id="stop7" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#ffffff"
|
||||
stop-opacity=".2"
|
||||
id="stop9" />
|
||||
</linearGradient>
|
||||
<path
|
||||
id="c"
|
||||
d="m248 139.36v-58h30l14 14v44h-14z" />
|
||||
</defs>
|
||||
<g
|
||||
transform="translate(-238-78.36)">
|
||||
<use
|
||||
fill="#209ae7"
|
||||
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="248"
|
||||
y="-82.36"
|
||||
width="30"
|
||||
fill="#ffffff"
|
||||
height="1" />
|
||||
<rect
|
||||
opacity=".25"
|
||||
x="248"
|
||||
y="-139.36"
|
||||
width="44"
|
||||
height="1" />
|
||||
</g>
|
||||
<g
|
||||
fill-rule="evenodd">
|
||||
<path
|
||||
opacity=".5"
|
||||
fill="#ffffff"
|
||||
d="m292 95.36l-14-14v14z" />
|
||||
<path
|
||||
opacity=".2"
|
||||
fill="url(#a)"
|
||||
d="m278 95.36l14 14v-14z" />
|
||||
</g>
|
||||
<use
|
||||
fill="url(#b)"
|
||||
xlink:href="#c" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(1.8333382,0,0,1.8333306,2.6666187,3.6667279)">
|
||||
<rect
|
||||
width="11.69995"
|
||||
height="11.700049"
|
||||
rx="0.66966182"
|
||||
ry="0.66966754"
|
||||
x="10.15"
|
||||
y="10.14995"
|
||||
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.29999995;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:.75" />
|
||||
<path
|
||||
d="m 15.099978,11.209723 v 1.800007 h -0.956246 l 1.856242,2.100009 1.856243,-2.100009 h -0.956246 v -1.800007 z"
|
||||
style="display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:.75;fill-rule:nonzero;stroke:none;stroke-width:0.30053622;marker:none;enable-background:accumulate" />
|
||||
<path
|
||||
style="display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:.75;fill-rule:nonzero;stroke:none;stroke-width:0.30053622;marker:none;enable-background:accumulate"
|
||||
d="m 15.671876,15.409741 c -0.05187,0 -0.08437,0.08847 -0.08437,0.196875 v 0.68438 c -0.208817,0.04755 -0.405463,0.13271 -0.581249,0.243749 l -0.487497,-0.4875 c -0.07666,-0.07666 -0.1602,-0.111678 -0.196875,-0.07501 l -0.459373,0.459377 c -0.03668,0.03667 -0.0017,0.120227 0.075,0.196876 l 0.487498,0.487502 c -0.111038,0.175789 -0.196199,0.372439 -0.243748,0.581253 H 13.49689 c -0.108397,0 -0.196874,0.03252 -0.196874,0.08438 v 0.656253 c 0,0.05187 0.08848,0.08438 0.196874,0.08438 h 0.684373 c 0.04755,0.208816 0.13271,0.405466 0.243748,0.581254 l -0.487498,0.487501 c -0.07664,0.07666 -0.111676,0.160201 -0.075,0.196876 l 0.459373,0.459378 c 0.03667,0.03668 0.120224,0.0017 0.196875,-0.075 l 0.487498,-0.487504 c 0.175785,0.111041 0.372433,0.196202 0.581248,0.243751 v 0.684378 c 0,0.108398 0.03252,0.196876 0.08438,0.196876 h 0.656247 c 0.05186,0 0.08438,-0.08847 0.08438,-0.196876 v -0.684378 c 0.208815,-0.04755 0.405464,-0.132711 0.581248,-0.243751 l 0.487498,0.487503 c 0.07665,0.07666 0.160199,0.111676 0.196874,0.075 l 0.459374,-0.459375 c 0.03667,-0.03667 0.0017,-0.120226 -0.075,-0.196877 L 17.574991,19.10351 c 0.111038,-0.175788 0.1962,-0.372438 0.243748,-0.581254 h 0.684373 c 0.108397,0 0.196872,-0.03252 0.196872,-0.08438 v -0.656253 c 0,-0.05186 -0.08847,-0.08438 -0.196872,-0.08438 h -0.684375 c -0.04755,-0.208814 -0.132709,-0.405464 -0.243748,-0.581251 l 0.487498,-0.487503 c 0.07665,-0.07666 0.111677,-0.160198 0.07501,-0.196876 l -0.459372,-0.459377 c -0.03667,-0.03667 -0.120224,-0.0017 -0.196873,0.07501 l -0.487499,0.4875 C 16.81796,16.423704 16.621312,16.338541 16.412495,16.290994 v -0.68438 c 0,-0.108399 -0.03252,-0.196875 -0.08438,-0.196875 z M 16,17.275374 c 0.45858,0 0.834368,0.375792 0.834368,0.834377 0,0.458588 -0.375789,0.834379 -0.834368,0.834379 -0.458583,0 -0.834373,-0.375791 -0.834373,-0.834379 0,-0.458585 0.37579,-0.834377 0.834373,-0.834377 z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.7 KiB |