@ntlab/sipd-tu-bridge-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/controller/security.js +75 -0
- package/controller/ui.js +140 -0
- package/helper/app.js +131 -0
- package/index.js +244 -0
- package/package.json +49 -0
- package/public/css/app.css +16 -0
- package/public/favicon.ico +0 -0
- package/public/images/logo.png +0 -0
- package/views/error/error.ejs +6 -0
- package/views/layout/default.ejs +41 -0
- package/views/layout/xhr.ejs +9 -0
- package/views/security/login.ejs +62 -0
- package/views/slot/addons.ejs +3 -0
- package/views/slot/errhandler.ejs +27 -0
- package/views/slot/errhandler401.ejs +19 -0
- package/views/slot/errhandler500.ejs +11 -0
- package/views/slot/mainmenu.ejs +69 -0
- package/views/ui/activity.ejs +34 -0
- package/views/ui/bridge.ejs +42 -0
- package/views/ui/bridgehandler.ejs +58 -0
- package/views/ui/index.ejs +64 -0
- package/views/ui/info.ejs +17 -0
- package/views/ui/queue.ejs +66 -0
- package/views/ui/status.ejs +84 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Toha <tohenk@yahoo.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MIT License (MIT)
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2026 Toha <tohenk@yahoo.com>
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
7
|
+
* this software and associated documentation files (the "Software"), to deal in
|
|
8
|
+
* the Software without restriction, including without limitation the rights to
|
|
9
|
+
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
10
|
+
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
11
|
+
* so, subject to the following conditions:
|
|
12
|
+
*
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
* copies or substantial portions of the Software.
|
|
15
|
+
*
|
|
16
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
* SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const Controller = require('@ntlab/express-controller');
|
|
26
|
+
const Express = require('express').application;
|
|
27
|
+
|
|
28
|
+
class SecurityController extends Controller {
|
|
29
|
+
buildRoutes() {
|
|
30
|
+
this.addRoute('index', 'get', '/login', (req, res, next) => {
|
|
31
|
+
let redir;
|
|
32
|
+
if (req.params.r) {
|
|
33
|
+
redir = req.params.r;
|
|
34
|
+
} else if (req.query.r) {
|
|
35
|
+
redir = req.query.r;
|
|
36
|
+
}
|
|
37
|
+
res.app.slots.mainmenu.enabled = false;
|
|
38
|
+
res.render('security/login', {redirect: redir ? redir : req.getPath('/')});
|
|
39
|
+
});
|
|
40
|
+
this.addRoute('login', 'post', '/login', (req, res, next) => {
|
|
41
|
+
const result = {
|
|
42
|
+
success: false
|
|
43
|
+
}
|
|
44
|
+
if (req.user.authenticate(req.body.username, req.body.password)) {
|
|
45
|
+
req.user.login();
|
|
46
|
+
result.success = true;
|
|
47
|
+
result.url = req.body.continue ? req.body.continue : req.getPath('/');
|
|
48
|
+
} else {
|
|
49
|
+
result.error = this._('Invalid username and/or password');
|
|
50
|
+
}
|
|
51
|
+
res.json(result);
|
|
52
|
+
});
|
|
53
|
+
this.addRoute('logout', 'get', '/logout', (req, res, next) => {
|
|
54
|
+
if (req.user) {
|
|
55
|
+
req.user.logout();
|
|
56
|
+
}
|
|
57
|
+
res.redirect(req.getPath('/'));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create controller.
|
|
63
|
+
*
|
|
64
|
+
* @param {Express} app Express app
|
|
65
|
+
* @param {string} prefix Path prefix
|
|
66
|
+
* @returns {SecurityController}
|
|
67
|
+
*/
|
|
68
|
+
static create(app, prefix = '/') {
|
|
69
|
+
const controller = new SecurityController({prefix: prefix, name: 'Security'});
|
|
70
|
+
app.use(prefix, controller.router);
|
|
71
|
+
return controller;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = SecurityController.create;
|
package/controller/ui.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MIT License (MIT)
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2026 Toha <tohenk@yahoo.com>
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
7
|
+
* this software and associated documentation files (the "Software"), to deal in
|
|
8
|
+
* the Software without restriction, including without limitation the rights to
|
|
9
|
+
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
10
|
+
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
11
|
+
* so, subject to the following conditions:
|
|
12
|
+
*
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
* copies or substantial portions of the Software.
|
|
15
|
+
*
|
|
16
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
* SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const Controller = require('@ntlab/express-controller');
|
|
28
|
+
const Express = require('express').application;
|
|
29
|
+
|
|
30
|
+
class UiController extends Controller {
|
|
31
|
+
|
|
32
|
+
buildRoutes() {
|
|
33
|
+
this.addRoute('index', 'get', '/', async (req, res, next) => {
|
|
34
|
+
/** @type {import('..').SipdApi} */
|
|
35
|
+
const api = req.app.api;
|
|
36
|
+
for (const bridge of api.bridges) {
|
|
37
|
+
bridge.stat = await bridge.getStats();
|
|
38
|
+
bridge.last = await bridge.getLast();
|
|
39
|
+
bridge.current = await bridge.getCurrent();
|
|
40
|
+
}
|
|
41
|
+
const socketOptions = {reconnection: true};
|
|
42
|
+
if (req.app.get('root') !== '/') {
|
|
43
|
+
socketOptions.path = req.getPath('/socket.io/');
|
|
44
|
+
}
|
|
45
|
+
res.render('ui/index', {
|
|
46
|
+
socket: {
|
|
47
|
+
url: `${req.getUri({noproto: true})}/ui`,
|
|
48
|
+
options: socketOptions,
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
this.addRoute('updates', 'get', '/updates', async (req, res, next) => {
|
|
53
|
+
const result = {updates: {}};
|
|
54
|
+
/** @type {import('..').SipdApi} */
|
|
55
|
+
const api = req.app.api;
|
|
56
|
+
for (const bridge of api.bridges) {
|
|
57
|
+
bridge.stat = await bridge.getStats();
|
|
58
|
+
bridge.last = await bridge.getLast();
|
|
59
|
+
bridge.current = await bridge.getCurrent();
|
|
60
|
+
result.updates[bridge.name] = {
|
|
61
|
+
...bridge.stat,
|
|
62
|
+
last: bridge.last,
|
|
63
|
+
current: bridge.current,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
res.json(result);
|
|
67
|
+
});
|
|
68
|
+
this.addRoute('activity', 'get', '/activity', async (req, res, next) => {
|
|
69
|
+
const result = {};
|
|
70
|
+
/** @type {import('..').SipdApi} */
|
|
71
|
+
const api = req.app.api;
|
|
72
|
+
const logs = await api.getActivity();
|
|
73
|
+
if (logs) {
|
|
74
|
+
result.time = Date.now();
|
|
75
|
+
result.logs = logs;
|
|
76
|
+
}
|
|
77
|
+
res.json(result);
|
|
78
|
+
});
|
|
79
|
+
this.addRoute('queue', 'get', '/queue', async (req, res, next) => {
|
|
80
|
+
/** @type {import('..').SipdApi} */
|
|
81
|
+
const api = req.app.api;
|
|
82
|
+
const result = await api.getQueues(req.params.page || req.query.page, req.params.size || req.query.size);
|
|
83
|
+
result.pages = req.app.locals.pager(result.count, result.size, result.page);
|
|
84
|
+
res.json(result);
|
|
85
|
+
});
|
|
86
|
+
this.addRoute('log', 'get', '/log/:bridge', async (req, res, next) => {
|
|
87
|
+
const result = {};
|
|
88
|
+
if (req.params.bridge) {
|
|
89
|
+
/** @type {import('..').SipdApi} */
|
|
90
|
+
const api = req.app.api;
|
|
91
|
+
const bridge = api.bridges.find(b => b.name === req.params.bridge);
|
|
92
|
+
if (bridge) {
|
|
93
|
+
const logs = await bridge.getLogs();
|
|
94
|
+
if (logs) {
|
|
95
|
+
result.time = Date.now();
|
|
96
|
+
result.logs = logs;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
res.json(result);
|
|
101
|
+
});
|
|
102
|
+
this.addRoute('about', 'get', '/about', (req, res, next) => {
|
|
103
|
+
let about;
|
|
104
|
+
if (req.app.about) {
|
|
105
|
+
about = req.app.about;
|
|
106
|
+
} else {
|
|
107
|
+
const packageInfo = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json')));
|
|
108
|
+
about = {
|
|
109
|
+
title: packageInfo.description,
|
|
110
|
+
version: packageInfo.version,
|
|
111
|
+
author: packageInfo.author.name ? `${packageInfo.author.name} <${packageInfo.author.email}>` :
|
|
112
|
+
packageInfo.author,
|
|
113
|
+
license: packageInfo.license
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
res.json(about);
|
|
117
|
+
});
|
|
118
|
+
this.addRoute('task', 'post', '/task/:op', (req, res, next) => {
|
|
119
|
+
const result = {
|
|
120
|
+
success: false
|
|
121
|
+
}
|
|
122
|
+
res.json(result);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create controller.
|
|
128
|
+
*
|
|
129
|
+
* @param {Express} app Express app
|
|
130
|
+
* @param {string} prefix Path prefix
|
|
131
|
+
* @returns {UiController}
|
|
132
|
+
*/
|
|
133
|
+
static create(app, prefix = '/') {
|
|
134
|
+
const controller = new UiController({prefix, name: 'Ui'});
|
|
135
|
+
app.use(prefix, controller.router);
|
|
136
|
+
return controller;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = UiController.create;
|
package/helper/app.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MIT License (MIT)
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2026 Toha <tohenk@yahoo.com>
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
7
|
+
* this software and associated documentation files (the "Software"), to deal in
|
|
8
|
+
* the Software without restriction, including without limitation the rights to
|
|
9
|
+
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
10
|
+
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
11
|
+
* so, subject to the following conditions:
|
|
12
|
+
*
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
* copies or substantial portions of the Software.
|
|
15
|
+
*
|
|
16
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
* SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const Helper = require('@ntlab/express-middleware/lib/helper');
|
|
26
|
+
const HelperFunctions = require('@ntlab/express-middleware/lib/fn');
|
|
27
|
+
const Controller = require('@ntlab/express-controller');
|
|
28
|
+
const Translator = require('@ntlab/express-controller/translator');
|
|
29
|
+
const Stringify = require('@ntlab/ntlib/stringify');
|
|
30
|
+
const { ScriptManager } = require('@ntlab/ntjs');
|
|
31
|
+
const { minify_sync } = require('terser');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Express app middleware.
|
|
35
|
+
*/
|
|
36
|
+
class AppFunctions extends HelperFunctions {
|
|
37
|
+
|
|
38
|
+
initialize() {
|
|
39
|
+
this.exportFn(this.app.locals, () => this.ViewFunctions());
|
|
40
|
+
this.exportFn(this.res.req, () => this.RequestFunctions());
|
|
41
|
+
this.exportFn(this.res, () => this.ResponseFunctions());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
ViewFunctions() {
|
|
45
|
+
return {
|
|
46
|
+
_: Translator._,
|
|
47
|
+
s: (o, l = 0) => Stringify.from(o, l),
|
|
48
|
+
route: (name, parameters) => this.genRoute(name, parameters),
|
|
49
|
+
path: path => this.genPath(path),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
RequestFunctions() {
|
|
54
|
+
return {
|
|
55
|
+
getUri: (parameters = null) => this.getUri(parameters),
|
|
56
|
+
getPath: path => this.genPath(path),
|
|
57
|
+
getRoute: (name, parameters) => this.genRoute(name, parameters),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
ResponseFunctions() {
|
|
62
|
+
return {
|
|
63
|
+
onrender: res => ScriptManager.require('JQuery').setOption('xhr', res.req.xhr ? true : false),
|
|
64
|
+
onscript: script => this.app.get('env') === 'development' ? script : minify_sync(script, {compress: true, mangle: true}).code,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getUri(parameters = null) {
|
|
69
|
+
let path, noproto = false;
|
|
70
|
+
if (typeof parameters === 'string') {
|
|
71
|
+
path = parameters;
|
|
72
|
+
parameters = {};
|
|
73
|
+
}
|
|
74
|
+
if (typeof parameters === 'object') {
|
|
75
|
+
if (parameters.path) {
|
|
76
|
+
path = parameters.path;
|
|
77
|
+
}
|
|
78
|
+
if (parameters.noproto) {
|
|
79
|
+
noproto = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const [host, port] = this.res.req.headers.host.split(':');
|
|
83
|
+
let uri = `${noproto ? '' : this.res.req.protocol + ':'}//${this.res.req.hostname}`;
|
|
84
|
+
if (port && ((this.res.req.protocol === 'http' && port != 80) || (this.res.req.protocol === 'https' && port != 443))) {
|
|
85
|
+
uri += `:${port}`;
|
|
86
|
+
}
|
|
87
|
+
if (path) {
|
|
88
|
+
uri += this.genPath(path);
|
|
89
|
+
}
|
|
90
|
+
return uri;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getController(name) {
|
|
94
|
+
return Controller.get(name);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
genRoute(name, parameters) {
|
|
98
|
+
const controller = this.getController(name);
|
|
99
|
+
if (!controller) {
|
|
100
|
+
throw new Error(`Unable to find controller ${name}!`);
|
|
101
|
+
}
|
|
102
|
+
const p = {...parameters};
|
|
103
|
+
const route = p.name;
|
|
104
|
+
if (!route) {
|
|
105
|
+
throw new Error('Route name must be specified in parameters!');
|
|
106
|
+
}
|
|
107
|
+
delete p.name;
|
|
108
|
+
return controller.genRoute(route, p);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
genPath(path) {
|
|
112
|
+
if (Array.isArray(path)) {
|
|
113
|
+
path = path.map(p => this.genPath(p));
|
|
114
|
+
} else {
|
|
115
|
+
if (typeof path === 'string' && !path.match(/http(s)?:\/\//) && path.substr(0, 1) === '/') {
|
|
116
|
+
let rootPath = this.app.get('root');
|
|
117
|
+
if (rootPath.substr(-1) === '/') {
|
|
118
|
+
rootPath = rootPath.substr(0, rootPath.length - 1);
|
|
119
|
+
}
|
|
120
|
+
if (rootPath) {
|
|
121
|
+
path = rootPath + path;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return path;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const helper = new Helper(AppFunctions);
|
|
130
|
+
|
|
131
|
+
module.exports = options => helper.handle(options);
|
package/index.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MIT License (MIT)
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2026 Toha <tohenk@yahoo.com>
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
7
|
+
* this software and associated documentation files (the "Software"), to deal in
|
|
8
|
+
* the Software without restriction, including without limitation the rights to
|
|
9
|
+
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
10
|
+
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
11
|
+
* so, subject to the following conditions:
|
|
12
|
+
*
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
* copies or substantial portions of the Software.
|
|
15
|
+
*
|
|
16
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
* SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/* --- BEGIN API --- */
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* SIPD Penatausahaan Bridge main application.
|
|
29
|
+
*
|
|
30
|
+
* @typedef {Object} SipdApi
|
|
31
|
+
* @property {string} title Application title
|
|
32
|
+
* @property {SipdAboutInfo} about Application information
|
|
33
|
+
* @property {string} proto Protocol version
|
|
34
|
+
* @property {string} mode Bridge mode
|
|
35
|
+
* @property {object} config Configuration
|
|
36
|
+
* @property {SipdBridge[]} bridges Bridges
|
|
37
|
+
* @property {AuthenticateFunction} authenticate Perform usename and password authentication
|
|
38
|
+
* @property {GetQueuesFunction} getQueues Get queues
|
|
39
|
+
* @property {StringPromiseFunction} getActivity Get activity logs
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* SIPD Penatausahaan Bridge queue consumer.
|
|
44
|
+
*
|
|
45
|
+
* @typedef {Object} SipdBridge
|
|
46
|
+
* @property {string} name Name
|
|
47
|
+
* @property {number} year Year
|
|
48
|
+
* @property {ObjectPromiseFunction} getStats Get bridge stats
|
|
49
|
+
* @property {StringPromiseFunction} getLogs Get bridge logs
|
|
50
|
+
* @property {ObjectPromiseFunction} getLast Get last queue
|
|
51
|
+
* @property {ObjectPromiseFunction} getCurrent Get current processing queue
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Application information.
|
|
56
|
+
*
|
|
57
|
+
* @typedef {Object} SipdAboutInfo
|
|
58
|
+
* @property {string} title Title
|
|
59
|
+
* @property {string} version Version
|
|
60
|
+
* @property {string} author Author name and email address
|
|
61
|
+
* @property {string} license License, e.g. MIT License
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Perform usename and password authentication.
|
|
66
|
+
*
|
|
67
|
+
* @callback AuthenticateFunction
|
|
68
|
+
* @param {string} username Username
|
|
69
|
+
* @param {string} password Password
|
|
70
|
+
* @returns {boolean}
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get queues.
|
|
75
|
+
*
|
|
76
|
+
* @callback GetQueuesFunction
|
|
77
|
+
* @param {number} page Page number
|
|
78
|
+
* @param {number} size Page size
|
|
79
|
+
* @returns {Promise<object[]>}
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A function which returns object Promise.
|
|
84
|
+
*
|
|
85
|
+
* @callback ObjectPromiseFunction
|
|
86
|
+
* @returns {Promise<object>}
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A function which returns string Promise.
|
|
91
|
+
*
|
|
92
|
+
* @callback StringPromiseFunction
|
|
93
|
+
* @returns {Promise<string>}
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
/* --- END API --- */
|
|
97
|
+
|
|
98
|
+
const createError = require('http-errors');
|
|
99
|
+
const express = require('express');
|
|
100
|
+
const path = require('path');
|
|
101
|
+
const logger = require('morgan');
|
|
102
|
+
const session = require('express-session');
|
|
103
|
+
const FileStore = require('session-file-store')(session);
|
|
104
|
+
const { Helper, Security, Factory } = require('@ntlab/express-middleware');
|
|
105
|
+
const { ScriptManager, ScriptAsset } = require('@ntlab/ntjs');
|
|
106
|
+
const { Assets, CDN } = require('@ntlab/ntjs-assets');
|
|
107
|
+
|
|
108
|
+
// register script repository
|
|
109
|
+
require('@ntlab/ntjs-repo')();
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Express application.
|
|
113
|
+
*
|
|
114
|
+
* @author Toha <tohenk@yahoo.com>
|
|
115
|
+
*/
|
|
116
|
+
class ExpressApp {
|
|
117
|
+
|
|
118
|
+
app = express()
|
|
119
|
+
/** @type {SipdApi} */
|
|
120
|
+
api = null
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Constructor.
|
|
124
|
+
*
|
|
125
|
+
* @param {SipdApi} api App api
|
|
126
|
+
*/
|
|
127
|
+
constructor(api) {
|
|
128
|
+
this.api = api;
|
|
129
|
+
this.initialize();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
initialize() {
|
|
133
|
+
const options = this.api.config || {};
|
|
134
|
+
const rootPath = options.rootPath || '/';
|
|
135
|
+
|
|
136
|
+
// setup application
|
|
137
|
+
this.app.api = this.app.locals.api = this.api;
|
|
138
|
+
this.app.factory = Factory.SemanticUI;
|
|
139
|
+
for (const prop of ['title', 'about', 'authenticate']) {
|
|
140
|
+
this.app[prop] = this.api[prop];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// view engine setup
|
|
144
|
+
this.app.set('views', path.join(__dirname, 'views'));
|
|
145
|
+
this.app.set('view engine', 'ejs');
|
|
146
|
+
this.app.set('query parser', 'extended');
|
|
147
|
+
|
|
148
|
+
// environment
|
|
149
|
+
this.app.set('env', process.env.NODE_ENV || 'development');
|
|
150
|
+
this.app.set('root', rootPath);
|
|
151
|
+
|
|
152
|
+
this.app.use(logger('dev'));
|
|
153
|
+
this.app.use(express.json());
|
|
154
|
+
this.app.use(express.urlencoded({extended: true}));
|
|
155
|
+
this.app.use(rootPath, express.static(path.join(__dirname, 'public')));
|
|
156
|
+
this.app.use(rootPath, express.static(Assets));
|
|
157
|
+
|
|
158
|
+
// session
|
|
159
|
+
const sessiondir = options.sessiondir || path.join(__dirname, 'sessions');
|
|
160
|
+
const secret = options.sessionsecret || 'sipd-tu-bridge';
|
|
161
|
+
this.app.use(session({
|
|
162
|
+
name: options['session-name'] || 'sipd-tu-bridge',
|
|
163
|
+
store: new FileStore({path: sessiondir}),
|
|
164
|
+
secret,
|
|
165
|
+
resave: false,
|
|
166
|
+
saveUninitialized: false,
|
|
167
|
+
cookie: {
|
|
168
|
+
maxAge: 3600000
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// security
|
|
174
|
+
const securityOptions = {};
|
|
175
|
+
if (rootPath !== '/') {
|
|
176
|
+
securityOptions.loginroute = options.getPath('/login');
|
|
177
|
+
securityOptions.logoutroute = options.getPath('/logout');
|
|
178
|
+
}
|
|
179
|
+
this.app.use(Security.core(securityOptions));
|
|
180
|
+
|
|
181
|
+
// app helpers
|
|
182
|
+
this.app.use(Helper.core());
|
|
183
|
+
this.app.use(Helper.menu());
|
|
184
|
+
this.app.use(Helper.pager());
|
|
185
|
+
this.app.use(require('./helper/app')());
|
|
186
|
+
|
|
187
|
+
// controllers
|
|
188
|
+
const Controller = require('@ntlab/express-controller');
|
|
189
|
+
Controller.scan(path.join(__dirname, 'controller'), controller => {
|
|
190
|
+
controller(this.app, rootPath);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// catch 404 and forward to error handler
|
|
194
|
+
this.app.use((req, res, next) => {
|
|
195
|
+
next(createError(404));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// error handler
|
|
199
|
+
this.app.use((err, req, res, next) => {
|
|
200
|
+
// set locals, only providing error in development
|
|
201
|
+
res.locals.message = err.message;
|
|
202
|
+
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
|
203
|
+
res.locals.viewdir = req.app.get('views');
|
|
204
|
+
|
|
205
|
+
// render the error page
|
|
206
|
+
res.status(err.status || 500);
|
|
207
|
+
res.render('error/error');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// relative from layout
|
|
211
|
+
this.app.slots = {
|
|
212
|
+
mainmenu: {
|
|
213
|
+
view: '../slot/mainmenu'
|
|
214
|
+
},
|
|
215
|
+
addons: {
|
|
216
|
+
view: '../slot/addons'
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
ScriptManager.addDefault('SemanticUI');
|
|
221
|
+
ScriptManager.addDefaultAsset(ScriptAsset.STYLESHEET, 'app.css');
|
|
222
|
+
if (options.useCdn) {
|
|
223
|
+
ScriptManager.parseCdn(CDN);
|
|
224
|
+
}
|
|
225
|
+
ScriptManager.require('JQuery/FormPost')
|
|
226
|
+
.setOption('redir-delay', 1000);
|
|
227
|
+
ScriptManager.translator = require('@ntlab/express-controller/translator')._;
|
|
228
|
+
ScriptManager.config = options;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** @type {ExpressApp} */
|
|
233
|
+
let app = null;
|
|
234
|
+
|
|
235
|
+
function run(api) {
|
|
236
|
+
if (app === null) {
|
|
237
|
+
app = new ExpressApp(api);
|
|
238
|
+
} else {
|
|
239
|
+
console.error('User interface already created!');
|
|
240
|
+
}
|
|
241
|
+
return app.app;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
module.exports = run;
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ntlab/sipd-tu-bridge-ui",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SIPD Penatausahaan Bridge Web Interface",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/tohenk/node-sipd-tu-bridge-ui.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"sipd",
|
|
15
|
+
"penatausahaan",
|
|
16
|
+
"kemendagri",
|
|
17
|
+
"spp",
|
|
18
|
+
"spp-ls",
|
|
19
|
+
"lpj",
|
|
20
|
+
"npd",
|
|
21
|
+
"tbp",
|
|
22
|
+
"otomasi",
|
|
23
|
+
"selenium",
|
|
24
|
+
"socket.io",
|
|
25
|
+
"web-interface"
|
|
26
|
+
],
|
|
27
|
+
"author": "Toha <tohenk@yahoo.com>",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/tohenk/node-sipd-tu-bridge-ui/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/tohenk/node-sipd-tu-bridge-ui#readme",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@ntlab/express-controller": "^1.2.0",
|
|
35
|
+
"@ntlab/express-middleware": "^2.4.0",
|
|
36
|
+
"@ntlab/ntjs": "^3.0.0",
|
|
37
|
+
"@ntlab/ntjs-assets": "^2.139.0",
|
|
38
|
+
"@ntlab/ntjs-repo": "^3.0.1",
|
|
39
|
+
"@ntlab/ntlib": "^2.9.0",
|
|
40
|
+
"ejs": "^3.1.10",
|
|
41
|
+
"express": "^5.2.1",
|
|
42
|
+
"express-session": "^1.19.0",
|
|
43
|
+
"http-errors": "^2.0.1",
|
|
44
|
+
"moment": "^2.30.1",
|
|
45
|
+
"morgan": "^1.10.1",
|
|
46
|
+
"session-file-store": "^1.5.0",
|
|
47
|
+
"terser": "^5.46.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
body.with-menu {
|
|
2
|
+
padding-top: 5em;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
body:not([class]) main, #login-container {
|
|
6
|
+
height: 100%;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
#login-container > div {
|
|
10
|
+
margin-top: auto !important;
|
|
11
|
+
margin-bottom: auto !important;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
img.logo {
|
|
15
|
+
margin-right: 1em !important;
|
|
16
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
|
7
|
+
<title><%- sitetitle %></title>
|
|
8
|
+
<%_ stylesheets().forEach(css => { -%>
|
|
9
|
+
<link rel="stylesheet" href="<%- path(css) %>" />
|
|
10
|
+
<%_ }) -%>
|
|
11
|
+
<%_ if (mainmenu = slot('mainmenu')) { -%>
|
|
12
|
+
<%_ mainmenu = include(mainmenu) -%>
|
|
13
|
+
<%_ } -%>
|
|
14
|
+
</head>
|
|
15
|
+
<body<% if (mainmenu) { %> class="with-menu"<% } %>>
|
|
16
|
+
<%_ if (mainmenu) { -%>
|
|
17
|
+
<section class="ui fixed inverted menu">
|
|
18
|
+
<%- mainmenu -%>
|
|
19
|
+
</section>
|
|
20
|
+
<%_ } -%>
|
|
21
|
+
<main class="ui container">
|
|
22
|
+
<%_ if (title.length) { -%>
|
|
23
|
+
<h1 class="ui dividing header"><%= title %></h1>
|
|
24
|
+
<%_ } -%>
|
|
25
|
+
<%- content -%>
|
|
26
|
+
</main>
|
|
27
|
+
<%_ if ((addons = slot('addons')) && (addons = include(addons))) { -%>
|
|
28
|
+
<%- addons -%>
|
|
29
|
+
<%_ } -%>
|
|
30
|
+
<%_ javascripts().forEach(js => { -%>
|
|
31
|
+
<script type="text/javascript" src="<%- path(js) %>"></script>
|
|
32
|
+
<%_ }) -%>
|
|
33
|
+
<%_ if ((scriptContent = scripts()).length) { -%>
|
|
34
|
+
<script type="text/javascript">
|
|
35
|
+
// <![CDATA[
|
|
36
|
+
<%- scriptContent %>
|
|
37
|
+
// ]]>
|
|
38
|
+
</script>
|
|
39
|
+
<%_ } -%>
|
|
40
|
+
</body>
|
|
41
|
+
</html>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<div id="login-container" class="ui center aligned stackable grid">
|
|
2
|
+
<div class="six wide column">
|
|
3
|
+
<h2 class="ui image header">
|
|
4
|
+
<img src="<%= path('/images/logo.png') %>" class="image">
|
|
5
|
+
<div class="content"><%= _('Log-in to %APP%', {APP: apptitle}) %></div>
|
|
6
|
+
</h2>
|
|
7
|
+
<form class="ui large form login" action="<%= route('Security', {name: 'login'}) %>">
|
|
8
|
+
<input type="hidden" name="continue" value="<%= redirect %>">
|
|
9
|
+
<div class="ui left aligned segment">
|
|
10
|
+
<div class="ui red message hidden">
|
|
11
|
+
<i class="red exclamation triangle icon"></i><span class="errmsg"></span>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="field">
|
|
14
|
+
<div class="ui big left icon input">
|
|
15
|
+
<i class="user icon"></i>
|
|
16
|
+
<input type="text" name="username" class="required" placeholder="<%= _('Username') %>" autocomplete="off">
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="field">
|
|
20
|
+
<div class="ui big left icon input">
|
|
21
|
+
<i class="lock icon"></i>
|
|
22
|
+
<input type="password" name="password" class="required" placeholder="<%= _('Password') %>">
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<button class="ui fluid large submit button" type="submit"><%= _('Login') %></button>
|
|
26
|
+
</div>
|
|
27
|
+
</form>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
<%_ script.create('JQuery')
|
|
31
|
+
.useDependencies(['JQuery/Util', 'JQuery/FormPost'])
|
|
32
|
+
.addMiddle(`
|
|
33
|
+
$.login = function(form) {
|
|
34
|
+
const fp = $.formpost(form, {progress: false, xhr: true});
|
|
35
|
+
const err = fp.errhelper;
|
|
36
|
+
err.errorContainer = form.find('.message');
|
|
37
|
+
err.errorFormat = err.ERROR_INPLACE;
|
|
38
|
+
err.inplace = function(el, error) {
|
|
39
|
+
el.find('.errmsg').html(error);
|
|
40
|
+
}
|
|
41
|
+
form
|
|
42
|
+
.on('formrequest', function(e) {
|
|
43
|
+
form.find('input[type=password]').val('');
|
|
44
|
+
})
|
|
45
|
+
.on('formsaved', function(e, json) {
|
|
46
|
+
if (json.url) {
|
|
47
|
+
if (typeof $.loginSuccess === 'function') {
|
|
48
|
+
$.loginSuccess(json.url);
|
|
49
|
+
} else {
|
|
50
|
+
window.location.href = json.url;
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
window.location.reload();
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
;
|
|
57
|
+
form.find('input[name=username]').focus();
|
|
58
|
+
}
|
|
59
|
+
`)
|
|
60
|
+
.addLast(`
|
|
61
|
+
$.login($('form.login'));
|
|
62
|
+
`) -%>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<%_ script.create('JQuery')
|
|
2
|
+
.useDependencies(['JQuery/Util'])
|
|
3
|
+
.addMiddle(`
|
|
4
|
+
$.define('errhandler', {
|
|
5
|
+
handlers: {},
|
|
6
|
+
register(code, handler) {
|
|
7
|
+
const self = this;
|
|
8
|
+
if (!self.handlers[code]) {
|
|
9
|
+
self.handlers[code] = [];
|
|
10
|
+
}
|
|
11
|
+
self.handlers[code].push(handler);
|
|
12
|
+
},
|
|
13
|
+
handle(code) {
|
|
14
|
+
const self = this;
|
|
15
|
+
if (self.handlers[code]) {
|
|
16
|
+
for (let i = 0; i < self.handlers[code].length; i++) {
|
|
17
|
+
if (typeof self.handlers[code][i] === 'function') {
|
|
18
|
+
self.handlers[code][i].call();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
$(document).ajaxError(function(event, xhr, s, e) {
|
|
25
|
+
$.errhandler.handle(xhr.status);
|
|
26
|
+
});
|
|
27
|
+
`) -%>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<%_ script.create('JQuery')
|
|
2
|
+
.useDependencies(['JQuery/Util', 'SemanticUI/Dialog'])
|
|
3
|
+
.addMiddle(`
|
|
4
|
+
$.errhandler.register(401, function() {
|
|
5
|
+
const dlgId = 'err401';
|
|
6
|
+
if ($.ntdlg.isVisible(dlgId)) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
$.ntdlg.dialog(dlgId, '${_('Information')}', '${_('Your session has been expired, please try to login again!')}', $.ntdlg.ICON_ALERT, {
|
|
10
|
+
okay: {
|
|
11
|
+
type: 'green approve',
|
|
12
|
+
caption: '<i class="check icon"></i>${_('Ok')}',
|
|
13
|
+
handler() {
|
|
14
|
+
window.location.reload();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
`) -%>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%_ script.create('JQuery')
|
|
2
|
+
.useDependencies(['JQuery/Util', 'SemanticUI/Dialog/Message'])
|
|
3
|
+
.addMiddle(`
|
|
4
|
+
$.errhandler.register(500, function() {
|
|
5
|
+
const dlgId = 'err500';
|
|
6
|
+
if ($.ntdlg.isVisible(dlgId)) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
$.ntdlg.message(dlgId, '${_('Error')}', '${_('Oops! An Error Occurred, please try again later.<br/>Don\\\'t hesitate to contact us if the problem persist.')}', $.ntdlg.ICON_ERROR);
|
|
10
|
+
});
|
|
11
|
+
`) -%>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<%_ menus = {
|
|
2
|
+
branding: {
|
|
3
|
+
type: 'brand',
|
|
4
|
+
title: apptitle,
|
|
5
|
+
logo: path('/images/logo.png'),
|
|
6
|
+
url: path('/')
|
|
7
|
+
},
|
|
8
|
+
tasks: {
|
|
9
|
+
title: _('Tasks'),
|
|
10
|
+
class: 'right floated',
|
|
11
|
+
items: {
|
|
12
|
+
about: {
|
|
13
|
+
title: _('About')
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
} -%>
|
|
18
|
+
<%_ if (user.authenticated) {
|
|
19
|
+
Object.assign(menus.tasks.items, {
|
|
20
|
+
divider1: {
|
|
21
|
+
type: 'divider'
|
|
22
|
+
},
|
|
23
|
+
logout: {
|
|
24
|
+
title: _('Logout'),
|
|
25
|
+
url: route('Security', {name: 'logout'})
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
} -%>
|
|
29
|
+
<%- menu(menus, {mainmenu: true, indentation: 2}) %>
|
|
30
|
+
<%_ script.create('JQuery')
|
|
31
|
+
.useDependencies(['JQuery/Util', 'SemanticUI/Notification', 'SemanticUI/Dialog/Message'])
|
|
32
|
+
.addMiddle(`
|
|
33
|
+
$.tasks = {
|
|
34
|
+
about() {
|
|
35
|
+
const self = this;
|
|
36
|
+
const f = function() {
|
|
37
|
+
const msg = $.util.template(
|
|
38
|
+
'<p>%TITLE% version %VERSION%<br/>\\n' +
|
|
39
|
+
'© %AUTHOR%<br/>\\n' +
|
|
40
|
+
'Licensed under %LICENSE%</p>', {
|
|
41
|
+
TITLE: self.aboutInfo.title,
|
|
42
|
+
VERSION: self.aboutInfo.version,
|
|
43
|
+
AUTHOR: $('<div>').text(self.aboutInfo.author).html(),
|
|
44
|
+
LICENSE: self.aboutInfo.license
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
$.ntdlg.message('task-about', '${_('About')}', msg, $.ntdlg.ICON_INFO);
|
|
48
|
+
}
|
|
49
|
+
if (!self.aboutInfo) {
|
|
50
|
+
$.get('${route('Ui', {name: 'about'})}')
|
|
51
|
+
.done(function(json) {
|
|
52
|
+
self.aboutInfo = json;
|
|
53
|
+
f();
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
f();
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
init() {
|
|
60
|
+
const self = this;
|
|
61
|
+
$('.menu-about').on('click', function(e) {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
self.about();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
`).addLast(`
|
|
68
|
+
$.tasks.init();
|
|
69
|
+
`) -%>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<div class="ui form">
|
|
2
|
+
<textarea class="activity" rows="30" readonly></textarea>
|
|
3
|
+
</div>
|
|
4
|
+
<%_ script.create('JQuery')
|
|
5
|
+
.addMiddle(`
|
|
6
|
+
$.activity = {
|
|
7
|
+
el: $('textarea.activity'),
|
|
8
|
+
url: '${route('Ui', {name: 'activity'})}',
|
|
9
|
+
load() {
|
|
10
|
+
const self = this;
|
|
11
|
+
$.get(self.url)
|
|
12
|
+
.done(function(json) {
|
|
13
|
+
if (json.logs) {
|
|
14
|
+
self.el.text(json.logs);
|
|
15
|
+
self.time = json.time;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
add(data) {
|
|
20
|
+
const self = this;
|
|
21
|
+
if (data.time >= self.time) {
|
|
22
|
+
const message = data.message + '\\r\\n';
|
|
23
|
+
if (self.el.text().substr(-message.length) !== message) {
|
|
24
|
+
self.el.append(message);
|
|
25
|
+
self.el.scrollTop(self.el[0].scrollHeight - self.el.height());
|
|
26
|
+
self.time = data.time;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
`)
|
|
32
|
+
.addLast(`
|
|
33
|
+
$.activity.load();
|
|
34
|
+
`) -%>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<%_ bridges = api.bridges -%>
|
|
2
|
+
<%_ names = bridges.map(b => b.name) -%>
|
|
3
|
+
<div class="ui stackable grid">
|
|
4
|
+
<div class="row">
|
|
5
|
+
<div class="four wide column">
|
|
6
|
+
<div class="ui fluid vertical pointing menu">
|
|
7
|
+
<%_ bridges.forEach(bridge => { -%>
|
|
8
|
+
<a href="#" class="item" data-bridge="<%= bridge.name %>"><i class="laptop code icon"></i> <%= bridge.name.toUpperCase() %></a>
|
|
9
|
+
<%_ }) -%>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="twelve wide column">
|
|
13
|
+
<%_ bridges.forEach(bridge => { -%>
|
|
14
|
+
<div class="ui form bridge <%= bridge.name %>" style="display: none;">
|
|
15
|
+
<%_ Object.keys(bridge.stat).forEach(stat => { -%>
|
|
16
|
+
<div class="fields">
|
|
17
|
+
<div class="six wide field"><label><%= _(bridge.stat[stat].label) %></label></div>
|
|
18
|
+
<div class="ten wide field" data-key="<%= stat %>"><input type="text" value="<%- bridge.stat[stat].value %>" readonly></div>
|
|
19
|
+
</div>
|
|
20
|
+
<%_ }) -%>
|
|
21
|
+
<div class="field">
|
|
22
|
+
<label><%= _('Logs') %></label>
|
|
23
|
+
<textarea class="log <%= bridge.name %>" rows="20" readonly></textarea>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<%_ }) -%>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
<%_ items = JSON.stringify(names, null, 4) -%>
|
|
31
|
+
<%_ script.create('JQuery')
|
|
32
|
+
.add(`
|
|
33
|
+
$.bridge.init(${items});
|
|
34
|
+
$('a[data-bridge]').on('click', function(e) {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
const a = $(this);
|
|
37
|
+
a.siblings('a').removeClass('active');
|
|
38
|
+
a.addClass('active');
|
|
39
|
+
$(\`.ui.form.bridge\`).hide();
|
|
40
|
+
$(\`.ui.form.bridge.\${a.data('bridge')}\`).show();
|
|
41
|
+
}).filter(':first').click();
|
|
42
|
+
`) -%>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<%_ script.create('JQuery')
|
|
2
|
+
.addMiddle(`
|
|
3
|
+
$.bridge = {
|
|
4
|
+
bridges: {},
|
|
5
|
+
getLog(name) {
|
|
6
|
+
const self = this;
|
|
7
|
+
$.get('${route('Ui', {name: 'log', bridge: 'BRIDGE'})}'.replace(/BRIDGE/, name))
|
|
8
|
+
.done(function(json) {
|
|
9
|
+
if (json.logs) {
|
|
10
|
+
const log = $(self.bridges[name].log);
|
|
11
|
+
log.text(json.logs);
|
|
12
|
+
self.bridges[name].time = json.time;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
addLog(name, data) {
|
|
17
|
+
const self = this;
|
|
18
|
+
if (data.time >= self.bridges[name].time) {
|
|
19
|
+
const log = $(self.bridges[name].log);
|
|
20
|
+
const message = data.message + '\\r\\n';
|
|
21
|
+
if (log.text().substr(-message.length) !== message) {
|
|
22
|
+
log.append(message);
|
|
23
|
+
log.scrollTop(log[0].scrollHeight - log.height());
|
|
24
|
+
self.bridges[name].time = data.time;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
update() {
|
|
29
|
+
const self = this;
|
|
30
|
+
$.get('${route('Ui', {name: 'updates'})}')
|
|
31
|
+
.done(function(json) {
|
|
32
|
+
if (json.updates) {
|
|
33
|
+
for (const bridge of Object.keys(json.updates)) {
|
|
34
|
+
for (const [k, v] of Object.entries(json.updates[bridge])) {
|
|
35
|
+
$(\`[data-bridge="\${bridge}"] [data-key="\${k}"]\`).each(function() {
|
|
36
|
+
const el = $(this);
|
|
37
|
+
if (el.is('input')) {
|
|
38
|
+
el.val(v);
|
|
39
|
+
} else {
|
|
40
|
+
el.html(v ? v : '–');
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
init(bridges) {
|
|
49
|
+
const self = this;
|
|
50
|
+
for (const bridge of bridges) {
|
|
51
|
+
self.bridges[bridge] = {
|
|
52
|
+
log: $(\`.log.\${bridge}\`)
|
|
53
|
+
}
|
|
54
|
+
self.getLog(bridge);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
`) -%>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<div class="ui top attached stackable tabular menu">
|
|
2
|
+
<a class="item" data-tab="status"><%= _('Status') %></a>
|
|
3
|
+
<a class="item" data-tab="activity"><%= _('Activity') %></a>
|
|
4
|
+
<a class="item" data-tab="queue"><%= _('Queue') %></a>
|
|
5
|
+
<%_ if (api.bridges.length) { -%>
|
|
6
|
+
<a class="item" data-tab="bridge"><%= _('Bridge') %> <span class="ui tiny label"><%= api.bridges.length %></span></a>
|
|
7
|
+
<%_ } -%>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="ui bottom attached tab segment" data-tab="status">
|
|
10
|
+
<%- include('status') -%>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="ui bottom attached tab segment" data-tab="activity">
|
|
13
|
+
<%- include('activity') -%>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="ui bottom attached tab segment" data-tab="queue">
|
|
16
|
+
<%- include('queue') -%>
|
|
17
|
+
</div>
|
|
18
|
+
<%- include('bridgehandler') -%>
|
|
19
|
+
<%_ if (api.bridges.length) { -%>
|
|
20
|
+
<div class="ui bottom attached tab segment" data-tab="bridge">
|
|
21
|
+
<%- include('bridge') -%>
|
|
22
|
+
</div>
|
|
23
|
+
<%_ } -%>
|
|
24
|
+
<%_ script.create('JQuery')
|
|
25
|
+
.useDependencies(['SocketIO'])
|
|
26
|
+
.addMiddle(`
|
|
27
|
+
$.uiCon = {
|
|
28
|
+
connected: false,
|
|
29
|
+
data: ${s(socket, 1)},
|
|
30
|
+
init() {
|
|
31
|
+
const self = this;
|
|
32
|
+
self.socket = io.connect(self.data.url, self.data.options);
|
|
33
|
+
self.socket
|
|
34
|
+
.on('connect', function() {
|
|
35
|
+
self.connected = true;
|
|
36
|
+
console.log('Socket connected');
|
|
37
|
+
})
|
|
38
|
+
.on('disconnect', function() {
|
|
39
|
+
self.connected = false;
|
|
40
|
+
console.log('Socket disconnected');
|
|
41
|
+
})
|
|
42
|
+
.on('activity', function(data) {
|
|
43
|
+
if (Array.isArray(data)) {
|
|
44
|
+
for (const msg of data) {
|
|
45
|
+
$.activity.add(msg);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.on('queue', function() {
|
|
50
|
+
$.queue.reload();
|
|
51
|
+
$.bridge.update();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
`)
|
|
56
|
+
.addLast(`
|
|
57
|
+
$('.menu .item').tab({
|
|
58
|
+
autoTabActivation: window.location.hash ? window.location.hash.substr(1) : true,
|
|
59
|
+
onLoad(path, params, history) {
|
|
60
|
+
window.location.hash = path;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
$.uiCon.init();
|
|
64
|
+
`) -%>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<div class="ui fluid card">
|
|
2
|
+
<div class="content">
|
|
3
|
+
<div class="item header"><i class="ui info circle icon"></i><%- _('General Information') %></div>
|
|
4
|
+
</div>
|
|
5
|
+
<div class="extra content">
|
|
6
|
+
<div class="ui form">
|
|
7
|
+
<div class="fields">
|
|
8
|
+
<div class="six wide field"><%= _('Protocol:') %></div>
|
|
9
|
+
<div class="ten wide field"><%= api.proto %></div>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="fields">
|
|
12
|
+
<div class="six wide field"><%= _('Mode:') %></div>
|
|
13
|
+
<div class="ten wide field"><%= api.mode %></div>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<h3 class="ui header x-title"></h3>
|
|
2
|
+
<table class="ui selectable celled table">
|
|
3
|
+
<thead>
|
|
4
|
+
<tr>
|
|
5
|
+
<th><%= _('#') %></th>
|
|
6
|
+
<th><%= _('Id') %></th>
|
|
7
|
+
<th><%= _('Type') %></th>
|
|
8
|
+
<th><%= _('Name') %></th>
|
|
9
|
+
<th><%= _('Status') %></th>
|
|
10
|
+
<th><%= _('Result') %></th>
|
|
11
|
+
<th><%= _('Time') %></th>
|
|
12
|
+
</tr>
|
|
13
|
+
</thead>
|
|
14
|
+
</table>
|
|
15
|
+
<%_ script.create('JQuery')
|
|
16
|
+
.useDependencies('SemanticUI/Loader')
|
|
17
|
+
.addMiddle(`
|
|
18
|
+
$.queue = $.loader($('div[data-tab="queue"] table'), {
|
|
19
|
+
url: '${route('Ui', {name: 'queue', page: 'PAGE'})}',
|
|
20
|
+
formatRow(item) {
|
|
21
|
+
return this.toRow(item);
|
|
22
|
+
},
|
|
23
|
+
loaded() {
|
|
24
|
+
const self = this;
|
|
25
|
+
if (self.loading) {
|
|
26
|
+
self.loading = false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
$.queue.toRow = function(data) {
|
|
31
|
+
return $(
|
|
32
|
+
\`<tr><td>\${data.nr}</td>
|
|
33
|
+
<td>\${this.toData(data.id)}</td>
|
|
34
|
+
<td>\${this.toData(data.type)}</td>
|
|
35
|
+
<td>\${this.toData(data.name)}</td>
|
|
36
|
+
<td>\${this.toStatus(data.status)}</td>
|
|
37
|
+
<td>\${this.toData(data.result)}</td>
|
|
38
|
+
<td>\${this.toData(data.time)}</td>
|
|
39
|
+
</tr>\`);
|
|
40
|
+
}
|
|
41
|
+
$.queue.toData = function(data) {
|
|
42
|
+
return data !== undefined ? data.toString() : '';
|
|
43
|
+
}
|
|
44
|
+
$.queue.toStatus = function(data) {
|
|
45
|
+
const icon = {
|
|
46
|
+
new: 'pause circle outline',
|
|
47
|
+
processing: 'spinner',
|
|
48
|
+
done: 'green check',
|
|
49
|
+
error: 'red times',
|
|
50
|
+
timeout: 'clock outline',
|
|
51
|
+
skipped: 'exclamation',
|
|
52
|
+
}[data];
|
|
53
|
+
return icon ? \`<div data-tooltip="\${data}" data-position="right center"><i class="\${icon} icon"></i></div>\` :
|
|
54
|
+
this.toData(data);
|
|
55
|
+
}
|
|
56
|
+
$.queue.reload = function() {
|
|
57
|
+
const self = this;
|
|
58
|
+
if (!self.loading) {
|
|
59
|
+
self.loading = true;
|
|
60
|
+
self.load();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
`)
|
|
64
|
+
.addLast(`
|
|
65
|
+
$.queue.load();
|
|
66
|
+
`) -%>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<div class="ui centered stackable grid">
|
|
2
|
+
<div class="row"></div>
|
|
3
|
+
<div class="row">
|
|
4
|
+
<div class="twelve wide column">
|
|
5
|
+
<%- include('info') -%>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
<%_ if (api.bridges.length) { -%>
|
|
9
|
+
<div class="row">
|
|
10
|
+
<div class="twelve wide column">
|
|
11
|
+
<div class="ui horizontal divider">
|
|
12
|
+
<%- _('Bridges') %>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<%_ api.bridges.forEach(bridge => { -%>
|
|
17
|
+
<%_ stats = Object.keys(bridge.stat) -%>
|
|
18
|
+
<div class="twelve wide column">
|
|
19
|
+
<div class="ui fluid card">
|
|
20
|
+
<div class="content">
|
|
21
|
+
<div class="ui right floated tiny label"><%= bridge.year %></div>
|
|
22
|
+
<div class="item header"><i class="ui laptop code icon"></i><%= bridge.name.toUpperCase() %></div>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="extra content" data-bridge="<%= bridge.name %>">
|
|
25
|
+
<div class="ui divided items">
|
|
26
|
+
<!-- stat -->
|
|
27
|
+
<div class="center aligned item">
|
|
28
|
+
<div class="ui fluid tiny statistic">
|
|
29
|
+
<div class="value">
|
|
30
|
+
<div class="ui breadcrumb">
|
|
31
|
+
<%_ stats.forEach((stat, i) => { -%>
|
|
32
|
+
<div class="section" data-key="<%= stat %>"><%- bridge.stat[stat].value ? bridge.stat[stat].value : '–' %></div>
|
|
33
|
+
<%_ if (i < stats.length - 1) { -%>
|
|
34
|
+
<div class="divider"> / </div>
|
|
35
|
+
<%_ } -%>
|
|
36
|
+
<%_ }) -%>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="label">
|
|
40
|
+
<div class="ui breadcrumb">
|
|
41
|
+
<%_ stats.forEach((stat, i) => { -%>
|
|
42
|
+
<div class="section"><%- _(bridge.stat[stat].label) %></div>
|
|
43
|
+
<%_ if (i < stats.length - 1) { -%>
|
|
44
|
+
<div class="divider"> / </div>
|
|
45
|
+
<%_ } -%>
|
|
46
|
+
<%_ }) -%>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<!-- stat end -->
|
|
52
|
+
<!-- processing -->
|
|
53
|
+
<div class="center aligned item">
|
|
54
|
+
<div class="ui fluid tiny statistic">
|
|
55
|
+
<div class="value" data-key="current"><%- bridge.current ?? '–' %></div>
|
|
56
|
+
<div class="label"><%- _('Queue in progress') %></div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<!-- processing end -->
|
|
60
|
+
<!-- last -->
|
|
61
|
+
<div class="center aligned item">
|
|
62
|
+
<div class="ui fluid tiny statistic">
|
|
63
|
+
<div class="value" data-key="last"><%- bridge.last ?? '–' %></div>
|
|
64
|
+
<div class="label"><%- _('Last queue') %></div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<!-- last end -->
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
<%_ }) -%>
|
|
73
|
+
<%_ } else { -%>
|
|
74
|
+
<div class="ui placeholder segment" style="min-height: 75vh;">
|
|
75
|
+
<div class="ui icon header">
|
|
76
|
+
<i class="laptop code icon"></i>
|
|
77
|
+
<div class="ui big hidden divider"></div>
|
|
78
|
+
<%= _('Currently, no connected bridge available.') %><br/>
|
|
79
|
+
<%= _('Once a connection established, it will appear here.') %>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<%_ } -%>
|
|
83
|
+
<div class="row"></div>
|
|
84
|
+
</div>
|