@ntlab/sipd-tu-bridge-ui 1.5.0 → 1.6.1

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/controller/ui.js CHANGED
@@ -43,6 +43,7 @@ class UiController extends Controller {
43
43
  bridge.stat = await bridge.getStats();
44
44
  bridge.last = await bridge.getLast();
45
45
  bridge.current = await bridge.getCurrent();
46
+ bridge.logs = await bridge.getLogFiles();
46
47
  }
47
48
  const socketOptions = {};
48
49
  if (req.app.get('root') !== '/') {
@@ -114,6 +115,37 @@ class UiController extends Controller {
114
115
  result.pages = req.app.locals.pager(result.count, result.size, result.page);
115
116
  res.json(result);
116
117
  });
118
+ this.addRoute('task', 'post', '/task/:op', async (req, res, next) => {
119
+ const result = {
120
+ success: false
121
+ }
122
+ let retval;
123
+ /** @type {import('..').SipdApi} */
124
+ const api = req.app.api;
125
+ switch (req.params.op) {
126
+ case 'log':
127
+ if (req.body.seq) {
128
+ if (req.body.log) {
129
+ retval = await api.query({cmd: 'log-file', log: req.body.log, seq: req.body.seq});
130
+ } else {
131
+ retval = await api.query({cmd: 'log-file', seq: req.body.seq});
132
+ }
133
+ }
134
+ break;
135
+ case 'remove':
136
+ if (req.body.queue) {
137
+ retval = await api.query({cmd: 'remove-queue', queue: req.body.queue});
138
+ }
139
+ if (req.body.error) {
140
+ retval = await api.query({cmd: 'clean-err', error: req.body.error});
141
+ }
142
+ break;
143
+ case 'restart':
144
+ retval = await api.query({cmd: 'restart'});
145
+ break;
146
+ }
147
+ res.json({...result, ...(retval || {})});
148
+ });
117
149
  this.addRoute('about', 'get', '/about', (req, res, next) => {
118
150
  let about;
119
151
  if (req.app.about) {
@@ -130,24 +162,6 @@ class UiController extends Controller {
130
162
  }
131
163
  res.json(about);
132
164
  });
133
- this.addRoute('task', 'post', '/task/:op', async (req, res, next) => {
134
- const result = {
135
- success: false
136
- }
137
- /** @type {import('..').SipdApi} */
138
- const api = req.app.api;
139
- switch (req.params.op) {
140
- case 'remove':
141
- if (req.body.error) {
142
- Object.assign(result, await api.query({cmd: 'clean-err', error: req.body.error}));
143
- }
144
- break;
145
- case 'restart':
146
- Object.assign(result, await api.query({cmd: 'restart'}));
147
- break;
148
- }
149
- res.json(result);
150
- });
151
165
  }
152
166
 
153
167
  /**
package/index.js CHANGED
@@ -22,7 +22,20 @@
22
22
  * SOFTWARE.
23
23
  */
24
24
 
25
- /* --- BEGIN API --- */
25
+ const createError = require('http-errors');
26
+ const express = require('express');
27
+ const path = require('path');
28
+ const logger = require('morgan');
29
+ const session = require('express-session');
30
+ const FileStore = require('session-file-store')(session);
31
+ const { Helper, Security, Factory } = require('@ntlab/express-middleware');
32
+ const { ScriptManager, ScriptAsset } = require('@ntlab/ntjs');
33
+ const { Assets, CDN } = require('@ntlab/ntjs-assets');
34
+
35
+ // register script repository
36
+ require('@ntlab/ntjs-repo')();
37
+
38
+ /* --- BEGIN API V1 --- */
26
39
 
27
40
  /**
28
41
  * SIPD Penatausahaan Bridge main application.
@@ -35,10 +48,10 @@
35
48
  * @property {object} config Configuration
36
49
  * @property {SipdBridge[]} bridges Bridges
37
50
  * @property {AuthenticateFunction} authenticate Perform usename and password authentication
38
- * @property {PagedObjectsPromiseFunction} getQueues Get queues
39
- * @property {StringPromiseFunction} getActivity Get activity logs
40
- * @property {ObjectPromiseFunction} getCount Get activity count
41
- * @property {PagedObjectsPromiseFunction} getErrors Get captured errors
51
+ * @property {PagedObjectsFunction} getQueues Get queues
52
+ * @property {ActivityFunction} getActivity Get activity logs
53
+ * @property {ObjectFunction} getCount Get activity count
54
+ * @property {PagedObjectsFunction} getErrors Get captured errors
42
55
  * @property {QueryFunction} query Perform API query
43
56
  */
44
57
 
@@ -48,10 +61,11 @@
48
61
  * @typedef {Object} SipdBridge
49
62
  * @property {string} name Name
50
63
  * @property {number} year Year
51
- * @property {ObjectPromiseFunction} getStats Get bridge stats
52
- * @property {StringPromiseFunction} getLogs Get bridge logs
53
- * @property {ObjectPromiseFunction} getLast Get last queue
54
- * @property {ObjectPromiseFunction} getCurrent Get current processing queue
64
+ * @property {ObjectFunction} getStats Get bridge stats
65
+ * @property {ActivityFunction} getLogs Get bridge logs
66
+ * @property {LogFilesFunction} getLogFiles Get bridge addiitonal log files
67
+ * @property {ObjectFunction} getLast Get last queue
68
+ * @property {ObjectFunction} getCurrent Get current processing queue
55
69
  */
56
70
 
57
71
  /**
@@ -74,50 +88,46 @@
74
88
  */
75
89
 
76
90
  /**
77
- * A function which returns paged objects Promise.
91
+ * Get objects at specified page with size of limit. If none specified
92
+ * it returns first page with default size limit (either 10 or 25).
78
93
  *
79
- * @callback PagedObjectsPromiseFunction
80
- * @param {number} page Page number
81
- * @param {number} size Page size
94
+ * @callback PagedObjectsFunction
95
+ * @param {?number} page Page number
96
+ * @param {?number} size Page size
82
97
  * @returns {Promise<object[]>}
83
98
  */
84
99
 
85
100
  /**
86
- * A function which returns object Promise.
101
+ * Get miscellanous object such as queue or log.
87
102
  *
88
- * @callback ObjectPromiseFunction
103
+ * @callback ObjectFunction
89
104
  * @returns {Promise<object>}
90
105
  */
91
106
 
92
107
  /**
93
- * A function which returns string Promise.
108
+ * Get string content such as activity logs.
94
109
  *
95
- * @callback StringPromiseFunction
110
+ * @callback ActivityFunction
111
+ * @param {?string} seq Sequence number
96
112
  * @returns {Promise<string>}
97
113
  */
98
114
 
99
115
  /**
100
- * A function which returns object Promise.
116
+ * Query api and return result object.
101
117
  *
102
118
  * @callback QueryFunction
103
119
  * @param {object} data Query data
104
120
  * @returns {Promise<object>}
105
121
  */
106
122
 
107
- /* --- END API --- */
108
-
109
- const createError = require('http-errors');
110
- const express = require('express');
111
- const path = require('path');
112
- const logger = require('morgan');
113
- const session = require('express-session');
114
- const FileStore = require('session-file-store')(session);
115
- const { Helper, Security, Factory } = require('@ntlab/express-middleware');
116
- const { ScriptManager, ScriptAsset } = require('@ntlab/ntjs');
117
- const { Assets, CDN } = require('@ntlab/ntjs-assets');
123
+ /**
124
+ * Get additional log files.
125
+ *
126
+ * @callback LogFilesFunction
127
+ * @returns {Promise<[{name: string, seq: string, time: number}]>}
128
+ */
118
129
 
119
- // register script repository
120
- require('@ntlab/ntjs-repo')();
130
+ /* --- END API --- */
121
131
 
122
132
  /**
123
133
  * Express application.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ntlab/sipd-tu-bridge-ui",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "description": "SIPD Penatausahaan Bridge Web Interface",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -34,7 +34,7 @@
34
34
  "@ntlab/express-controller": "^1.3.0",
35
35
  "@ntlab/express-middleware": "^2.6.0",
36
36
  "@ntlab/ntjs": "^3.1.0",
37
- "@ntlab/ntjs-assets": "^2.141.0",
37
+ "@ntlab/ntjs-assets": "^2.144.0",
38
38
  "@ntlab/ntjs-repo": "^3.1.0",
39
39
  "@ntlab/ntlib": "^2.9.1",
40
40
  "ejs": "^3.1.10",
@@ -1,3 +1,8 @@
1
+ :root {
2
+ --viewport-h-limit: 70vh;
3
+ --viewport-w-limit: 70vw;
4
+ }
5
+
1
6
  body.with-menu {
2
7
  padding-top: 5em;
3
8
  }
@@ -15,6 +20,14 @@ img.logo {
15
20
  margin-right: 1em !important;
16
21
  }
17
22
 
23
+ .v-max {
24
+ max-height: var(--viewport-h-limit) !important;
25
+ }
26
+
27
+ .v-min {
28
+ min-height: var(--viewport-h-limit) !important;
29
+ }
30
+
18
31
  @media only screen and (min-width: 1200px) {
19
32
  .ui.container.queue-name {
20
33
  max-width: 20em;
@@ -47,10 +60,10 @@ img.logo {
47
60
 
48
61
  @media only screen and (min-width: 768px) and (max-width: 991.98px) {
49
62
  .ui.container.queue-name {
50
- max-width: 10em;
63
+ max-width: 8em;
51
64
  }
52
65
  .ui.container.queue-result {
53
- max-width: 12.5em;
66
+ max-width: 10em;
54
67
  }
55
68
  .ui.container.err-message {
56
69
  max-width: 12.5em;
@@ -65,7 +78,7 @@ img.logo {
65
78
  .ui.container.queue-result,
66
79
  .ui.container.err-message,
67
80
  .ui.container.err-data {
68
- max-width: 75vw;
81
+ max-width: var(--viewport-w-limit);
69
82
  margin-left: 0 !important;
70
83
  margin-right: 0 !important;
71
84
  }
@@ -12,12 +12,29 @@
12
12
  <div class="twelve wide column">
13
13
  <%_ bridges.forEach(bridge => { -%>
14
14
  <div class="ui form bridge <%= bridge.name %>" data-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"><input type="text" value="<%- bridge.stat[stat].value %>" data-key="<%= stat %>" readonly></div>
15
+ <div class="ui two column stackable grid">
16
+ <div class="column">
17
+ <%_ Object.keys(bridge.stat).forEach(stat => { -%>
18
+ <div class="two fields">
19
+ <div class="field"><label><%= _(bridge.stat[stat].label) %></label></div>
20
+ <div class="field"><input type="text" value="<%- bridge.stat[stat].value %>" data-key="<%= stat %>" readonly></div>
21
+ </div>
22
+ <%_ }) -%>
23
+ </div>
24
+ <div class="column">
25
+ <div class="two fields">
26
+ <div class="field"><label><%= _('Archived logs') %></label></div>
27
+ <div class="field">
28
+ <select class="ui dropdown" data-log="<%= bridge.name %>" data-title="<%= _('Bridge <span class="ui info text">%NAME%</span> log %LOG%') %>">
29
+ <option value=""><%= _('- Select log -') %></option>
30
+ <%_ bridge.logs.forEach(log => { -%>
31
+ <option value="<%= log.seq %>"><%= log.name %></option>
32
+ <%_ }) -%>
33
+ </select>
34
+ </div>
35
+ </div>
36
+ </div>
19
37
  </div>
20
- <%_ }) -%>
21
38
  <div class="field">
22
39
  <label><%= _('Logs') %></label>
23
40
  <textarea class="log <%= bridge.name %>" rows="20" readonly></textarea>
@@ -39,4 +56,5 @@ $('a[data-bridge]').on('click', function(e) {
39
56
  $(\`.ui.form.bridge\`).hide();
40
57
  $(\`.ui.form.bridge.\${a.data('bridge')}\`).show();
41
58
  }).filter(':first').click();
59
+ $('.ui.dropdown').dropdown();
42
60
  `) -%>
@@ -92,11 +92,11 @@ $.error.handle = function(el) {
92
92
  let content, size = 'large';
93
93
  const img = el.find('img'), pre = el.find('pre');
94
94
  if (img.length) {
95
- content = \`<img class="ui fluid image" src="\${img.attr('src')}" style="max-height: 70vh;">\`;
95
+ content = \`<img class="ui fluid image v-max" src="\${img.attr('src')}">\`;
96
96
  size = 'fullscreen';
97
97
  }
98
98
  if (pre.length) {
99
- content = \`<div class="ui fluid scrolling container"><pre>\${pre.text()}</pre></div>\`;
99
+ content = \`<div class="ui fluid scrolling container"><pre>\${$.safeStr(pre.text())}</pre></div>\`;
100
100
  }
101
101
  const dlg = $.ntdlg.create('err-view-dlg', el.data('title'), content, {
102
102
  size,
@@ -113,7 +113,7 @@ $.error.handle = function(el) {
113
113
  $.ntdlg.confirm(
114
114
  'err-delete-confirm-dlg',
115
115
  '${_('Confirm')}',
116
- '${_('Are you sure want to remove error <code>%ERR%</code>?')}'.replace(/%ERR%/g, el.data('filename')),
116
+ '${_('Are you sure want to remove error <span class="ui error text">%ERR%</span>?')}'.replace(/%ERR%/g, el.data('filename')),
117
117
  $.ntdlg.ICON_QUESTION,
118
118
  function() {
119
119
  $.post('${route('Ui', {name: 'task', op: 'remove'})}', {error: el.data('filename')})
@@ -1,11 +1,11 @@
1
1
  <div class="ui top attached stackable tabular menu">
2
2
  <a class="item" data-tab="status"><%= _('Status') %></a>
3
3
  <a class="item" data-tab="activity"><%= _('Activity') %></a>
4
- <a class="item" data-tab="queue"><%= _('Queue') %> <span class="ui hidden tiny label"></span></a>
4
+ <a class="item" data-tab="queue"><%= _('Queue') %> <span class="ui hidden blue tiny label"></span></a>
5
5
  <%_ if (api.bridges.length) { -%>
6
- <a class="item" data-tab="bridge"><%= _('Bridge') %> <span class="ui tiny label"><%= api.bridges.length %></span></a>
6
+ <a class="item" data-tab="bridge"><%= _('Bridge') %> <span class="ui green tiny label"><%= api.bridges.length %></span></a>
7
7
  <%_ } -%>
8
- <a class="item" data-tab="error"><%= _('Error') %> <span class="ui hidden tiny label"></span></a>
8
+ <a class="item" data-tab="error"><%= _('Error') %> <span class="ui hidden red tiny label"></span></a>
9
9
  </div>
10
10
  <div class="ui bottom attached tab segment" data-tab="status">
11
11
  <%- indent(include('status'), 2) -%>
@@ -84,5 +84,18 @@ $('.menu [data-tab]').tab({
84
84
  window.location.hash = path;
85
85
  }
86
86
  });
87
+ $('select[data-log]').on('change', function(e) {
88
+ e.preventDefault();
89
+ const el = $(this);
90
+ const seq = el.val();
91
+ if (seq !== '') {
92
+ const log = el.data('log');
93
+ const logname = el.find(\`option[value="\${seq}"]\`).text();
94
+ const title = el.data('title')
95
+ .replace(/%LOG%/g, logname)
96
+ .replace(/%NAME%/g, log?.toUpperCase());
97
+ $.viewLog(title, log, seq);
98
+ }
99
+ });
87
100
  $.uiCon.init();
88
101
  `) -%>
@@ -9,11 +9,12 @@
9
9
  <th><%= _('Status') %></th>
10
10
  <th><%= _('Result') %></th>
11
11
  <th><%= _('Time') %></th>
12
+ <th><%= _('Action') %></th>
12
13
  </tr>
13
14
  </thead>
14
15
  </table>
15
16
  <%_ script.create('JQuery')
16
- .useDependencies('SemanticUI/Loader')
17
+ .useDependencies(['SemanticUI/Loader', 'SemanticUI/Dialog/Confirm'])
17
18
  .add(`
18
19
  $.queue = $.loader($('div[data-tab="queue"] table'), {
19
20
  url: '${route('Ui', {name: 'queue', page: 'PAGE'})}',
@@ -30,6 +31,11 @@ $.queue = $.loader($('div[data-tab="queue"] table'), {
30
31
  counter.addClass('hidden');
31
32
  }
32
33
  }
34
+ $('[data-status!="processing"] [data-op="delete"]').hide();
35
+ $('a.queue-clicker').on('click', function(e) {
36
+ e.preventDefault();
37
+ self.handle($(this));
38
+ });
33
39
  if (self.loading) {
34
40
  self.loading = false;
35
41
  }
@@ -37,13 +43,16 @@ $.queue = $.loader($('div[data-tab="queue"] table'), {
37
43
  });
38
44
  $.queue.toRow = function(data) {
39
45
  return $(\`
40
- <tr><td>\${data.nr}</td>
46
+ <tr data-status="\${data.status}"><td>\${data.nr}</td>
41
47
  <td>\${$.toStr(data.id)}</td>
42
48
  <td>\${$.toStr(data.type)}</td>
43
49
  <td><div class="ui scrolling container queue-name">\${$.hidePayload($.toStr(data.name))}</div></td>
44
50
  <td>\${this.toStatus(data.status)}</td>
45
51
  <td><div class="ui scrolling container queue-result">\${$.hidePayload($.toStr(data.result))}</div></td>
46
52
  <td>\${$.toStr(data.time)}</td>
53
+ <td>
54
+ <a href="#" class="queue-clicker" data-op="delete" data-queue="\${data.id}" role="button"><i class="trash alternate outline red icon"></i></a>
55
+ </td>
47
56
  </tr>\`);
48
57
  }
49
58
  $.queue.toStatus = function(data) {
@@ -58,6 +67,28 @@ $.queue.toStatus = function(data) {
58
67
  return icon ? \`<div data-tooltip="\${data}" data-position="right center"><i class="\${icon} icon"></i></div>\` :
59
68
  $.toStr(data);
60
69
  }
70
+ $.queue.handle = function(el) {
71
+ const self = this;
72
+ switch (el.data('op')) {
73
+ case 'delete':
74
+ $.ntdlg.confirm(
75
+ 'queue-delete-confirm-dlg',
76
+ '${_('Confirm')}',
77
+ '${_('Are you sure want to remove queue <span class="ui error text">%QUEUE%</span>?')}'.replace(/%QUEUE%/g, el.data('queue')),
78
+ $.ntdlg.ICON_QUESTION,
79
+ function() {
80
+ $.post('${route('Ui', {name: 'task', op: 'remove'})}', {queue: el.data('queue')})
81
+ .done(function(json) {
82
+ $.tasks.notify(json);
83
+ if (json.success) {
84
+ self.reload();
85
+ }
86
+ });
87
+ }
88
+ );
89
+ break;
90
+ }
91
+ }
61
92
  $.queue.reload = function() {
62
93
  const self = this;
63
94
  if (!self.loading) {
@@ -18,7 +18,7 @@
18
18
  <div class="twelve wide column">
19
19
  <div class="ui fluid card">
20
20
  <div class="content">
21
- <div class="ui right floated tiny label"><%= bridge.year %></div>
21
+ <div class="ui red right floated tiny label"><%= bridge.year %></div>
22
22
  <div class="item header"><i class="ui laptop code icon"></i><%= bridge.name.toUpperCase() %></div>
23
23
  </div>
24
24
  <div class="extra content" data-bridge="<%= bridge.name %>">
@@ -71,12 +71,16 @@
71
71
  </div>
72
72
  <%_ }) -%>
73
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.') %>
74
+ <div class="row">
75
+ <div class="twelve wide column">
76
+ <div class="ui placeholder segment v-min">
77
+ <div class="ui icon header">
78
+ <i class="laptop code icon"></i>
79
+ <div class="ui big hidden divider"></div>
80
+ <%= _('Currently, no configured bridge available.') %><br/>
81
+ <%= _('Once a bridge is configured, it will appear here.') %>
82
+ </div>
83
+ </div>
80
84
  </div>
81
85
  </div>
82
86
  <%_ } -%>
package/views/ui/util.ejs CHANGED
@@ -38,6 +38,30 @@ $.addLog = function(el, message) {
38
38
  }
39
39
  return false;
40
40
  }
41
+ $.viewLog = function(title, log, seq) {
42
+ $.post('${route('Ui', {name: 'task', op: 'log'})}', {log, seq})
43
+ .done(function(json) {
44
+ if (json.success) {
45
+ const logs = document.createTextNode(json.logs);
46
+ const content = \`<div class="ui fluid scrolling container v-min v-max"><pre></pre></div>\`;
47
+ const dlg = $.ntdlg.create('log-view-dlg', title, content, {
48
+ size: 'fullscreen',
49
+ show() {
50
+ dlg.find('pre').append(logs);
51
+ },
52
+ buttons: {
53
+ okay: {
54
+ type: 'green approve',
55
+ caption: '<i class="check icon"></i>${_('Ok')}',
56
+ }
57
+ }
58
+ });
59
+ $.ntdlg.show(dlg);
60
+ } else {
61
+ $.tasks.notify(json);
62
+ }
63
+ });
64
+ }
41
65
  $.hidePayload = function(message) {
42
66
  if (message) {
43
67
  message = message.replace(
@@ -50,11 +74,20 @@ $.hidePayload = function(message) {
50
74
  $.isPayload = function(data) {
51
75
  return Array.isArray(data) || typeof data === 'object' && data.constructor.name === 'Object';
52
76
  }
77
+ $.safeStr = function(s) {
78
+ if (s !== '') {
79
+ const tn = document.createTextNode(s);
80
+ const pn = document.createElement('p');
81
+ pn.appendChild(tn);
82
+ s = pn.innerHTML;
83
+ }
84
+ return s;
85
+ }
53
86
  $.toStr = function(o) {
54
87
  if ($.isPayload(o)) {
55
88
  return JSON.stringify(o, null, ' ');
56
89
  } else if (o !== undefined && o !== null) {
57
- return o.toString();
90
+ return $.safeStr(o.toString());
58
91
  }
59
92
  return '';
60
93
  }