Started work on reports feature.
Ylian Saint-Hilaire committed
Sep 8, 2021 at 15:55 UTC
a15a5e779da36c00f99714b2d4fd5646e2a53705
4 files changed
+242
-4
db.js
+1
@@ -1402,6 +1402,7 @@ module.exports.CreateDB = function (parent, func) {
1402
obj.GetEventsWithLimit = function (ids, domain, limit, func) { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func); };
1403
obj.GetUserEvents = function (ids, domain, username, func) { obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func); };
1404
obj.GetUserEventsWithLimit = function (ids, domain, username, limit, func) { obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func); };
1405
+ obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) { obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: 1 }).toArray(func); };
1406
obj.GetUserLoginEvents = function (domain, userid, func) { obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }).project({ action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }).toArray(func); };
1407
obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, func) { obj.eventsfile.find({ domain: domain, nodeid: nodeid }).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func); };
1408
obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, func) { obj.eventsfile.find({ domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } }).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func); };
meshuser.js
+62
@@ -5408,6 +5408,68 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
5408
5409
break;
5410
}
5411
+ case 'report': {
5412
+ // Report request. Validate the input
5413
+ if (common.validateInt(command.type, 1, 1) == false) break; // Validate type
5414
+ if (common.validateInt(command.groupBy, 1, 3) == false) break; // Validate groupBy: 1 = User, 2 = Device, 3 = Day
5415
+ if ((typeof command.start != 'number') || (typeof command.end != 'number') || (command.start >= command.end)) break; // Validate start and end time
5416
+
5417
+ if (command.type == 1) { // This is the remote session report. Shows desktop, terminal, files...
5418
+ // If we are not user administrator on this site, only search for events with our own user id.
5419
+ var ids = [user._id];
5420
+ if ((user.siteadmin & SITERIGHT_MANAGEUSERS) != 0) { ids = ['*']; }
5421
+
5422
+ // Get the events in the time range
5423
+ db.GetEventsTimeRange(ids, domain.id, [5, 10, 12], new Date(command.start * 1000), new Date(command.end * 1000), function (err, docs) {
5424
+ if (err != null) return;
5425
+ var data = { groups: {} };
5426
+
5427
+ // Columns
5428
+ if (command.groupBy == 1) {
5429
+ data.groupFormat = 'user';
5430
+ data.columns = [{ id: 'time', title: "time", format: 'datetime' }, { id: "nodeid", title: "device", format: "node" }, { id: "protocol", title: "session", format: "protocol", align: "center" }, { id: "length", title: "length", format: "seconds", align: "center" } ];
5431
+ } else if (command.groupBy == 2) {
5432
+ data.groupFormat = 'node';
5433
+ data.columns = [{ id: 'time', title: "time", format: 'datetime' }, { id: "userid", title: "user", format: "user" }, { id: "protocol", title: "session", format: "protocol", align: "center" }, { id: "length", title: "length", format: "seconds", align: "center" } ];
5434
+ } else if (command.groupBy == 3) {
5435
+ data.columns = [{ id: 'time', title: "time", format: 'time' }, { id: "nodeid", title: "device", format: "node" }, { id: "userid", title: "user", format: "user" }, { id: "protocol", title: "session", format: "protocol", align: "center" }, { id: "length", title: "length", format: "seconds", align:"center" } ];
5436
+ }
5437
+
5438
+ // Rows
5439
+ for (var i in docs) {
5440
+ var entry = { time: docs[i].time.valueOf() };
5441
+
5442
+ // UserID
5443
+ if (command.groupBy != 1) { entry.userid = docs[i].userid; }
5444
+ if (command.groupBy != 2) { entry.nodeid = docs[i].nodeid; }
5445
+ entry.protocol = docs[i].protocol;
5446
+
5447
+ // Session length
5448
+ if (((docs[i].msgid == 10) || (docs[i].msgid == 12)) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[3] == 'number')) { entry.length = docs[i].msgArgs[3]; }
5449
+
5450
+ if (command.groupBy == 1) { // Add entry to per user group
5451
+ if (data.groups[docs[i].userid] == null) { data.groups[docs[i].userid] = { entries: [] }; }
5452
+ data.groups[docs[i].userid].entries.push(entry);
5453
+ } else if (command.groupBy == 2) { // Add entry to per device group
5454
+ if (data.groups[docs[i].nodeid] == null) { data.groups[docs[i].nodeid] = { entries: [] }; }
5455
+ data.groups[docs[i].nodeid].entries.push(entry);
5456
+ } else if (command.groupBy == 3) { // Add entry to per day group
5457
+ var day;
5458
+ if ((typeof command.l == 'string') && (typeof command.tz == 'string')) {
5459
+ day = new Date(docs[i].time).toLocaleDateString(command.l, { timeZone: command.tz });
5460
+ } else {
5461
+ day = docs[i].time; // TODO
5462
+ }
5463
+ if (data.groups[day] == null) { data.groups[day] = { entries: [] }; }
5464
+ data.groups[day].entries.push(entry);
5465
+ }
5466
+
5467
+ }
5468
+ try { ws.send(JSON.stringify({ action: 'report', data: data })); } catch (ex) { }
5469
+ });
5470
+ }
5471
+ break;
5472
+ }
5473
default: {
5474
// Unknown user action
5475
console.log('Unknown action from user ' + user.name + ': ' + command.action + '.');
public/styles/style.css
+1
-1
@@ -273,7 +273,7 @@ body {
273
}
274
275
/* #UserDummyMenuSpan, */
276
-#MainSubMenuSpan, #MeshSubMenuSpan, #UserSubMenuSpan, #UsersSubMenuSpan, #ServerSubMenuSpan, #MainMenuSpan, #MainSubMenu, #MeshSubMenu, #UserSubMenu, #ServerSubMenu, #UserDummyMenu, #PluginSubMenu {
276
+#MainSubMenuSpan, #MeshSubMenuSpan, #EventsSubMenuSpan, #UserSubMenuSpan, #UsersSubMenuSpan, #ServerSubMenuSpan, #MainMenuSpan, #MainSubMenu, #MeshSubMenu, #UserSubMenu, #ServerSubMenu, #UserDummyMenu, #PluginSubMenu {
277
width: 100%;
278
height: 24px;
279
color: white;
views/default.handlebars
+178
-3
@@ -209,6 +209,15 @@
209
</tr>
210
</table>
211
</div>
212
+ <div id=EventsSubMenuSpan style="display:none">
213
+ <table id=EventsSubMenu cellpadding=0 cellspacing=0 class=style1>
214
+ <tr>
215
+ <td tabindex=0 id=EventsLive class="topbar_td style3x" onclick=go(3,event) onkeypress="if (event.key == 'Enter') go(3)">Events</td>
216
+ <td tabindex=0 id=EventsReport class="topbar_td style3x" onclick=go(60,event) onkeypress="if (event.key == 'Enter') go(60)">Reports</td>
217
+ <td class="topbar_td_end style3"> </td>
218
+ </tr>
219
+ </table>
220
+ </div>
221
<div id=UserSubMenuSpan style="display:none">
222
<table id=UserSubMenu cellpadding=0 cellspacing=0 class=style1>
223
<tr>
@@ -1170,6 +1179,23 @@
1179
</table>
1180
<div id=p52recordings style="overflow-y:auto"></div>
1181
</div>
1182
+ <div id=p60 style="display:none">
1183
+ <div id="p60title">
1184
+ <h1>My Reports</h1>
1185
+ </div>
1186
+ <table class="pTable">
1187
+ <tr>
1188
+ <td class="h1"></td>
1189
+ <td class="style14">
1190
+ <div>
1191
+ <input type=button onclick=generateReportDialog() value="Generate Report..." />
1192
+ </div>
1193
+ </td>
1194
+ <td class="h2"></td>
1195
+ </tr>
1196
+ </table>
1197
+ <div id=p60report style="overflow-y:auto"></div>
1198
+ </div>
1199
<br id="column_l_bottomgap" />
1200
</div>
1201
<div id="footer">
@@ -1793,6 +1819,8 @@
1819
QS('p41events')['max-height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
1820
QS('p52recordings')['height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
1821
QS('p52recordings')['max-height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
1822
+ QS('p60report')['height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
1823
+ QS('p60report')['max-height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
1824
1825
// We are looking at a single device, remove all the back buttons
1826
if ((args.hide & 32) || ('{{currentNode}}'.toLowerCase() != '')) {
@@ -3520,6 +3548,10 @@
3548
mainUpdate(65536);
3549
break;
3550
}
3551
+ case 'report': {
3552
+ renderReport(message.data);
3553
+ break;
3554
+ }
3555
default:
3556
//console.log('Unknown message.action', message.action);
3557
break;
@@ -14871,6 +14903,148 @@
14903
}
14904
}
14905
14906
+
14907
+ //
14908
+ // MY REPORTS
14909
+ //
14910
+
14911
+ function generateReportDialog() {
14912
+ if (xxdialogMode) return;
14913
+ var y = '', x = '', settings = JSON.parse(getstore('_ReportSettings', '{}'));
14914
+
14915
+ var options = { 1 : "Remote Sessions" }
14916
+ for (var i in options) { y += '<option value=' + i + ((settings.type == i)?' selected':'') + '>' + options[i] + '</option>'; }
14917
+ x += addHtmlValue("Type", '<select id=d2reportType style=float:right;width:250px onchange=generateReportDialogValidate()>' + y + '</select>');
14918
+
14919
+ y = '';
14920
+ var options = { 1 : "User", 2: "Device", 3: "Day" }
14921
+ for (var i in options) { y += '<option value=' + i + ((settings.groupBy == i)?' selected':'') + '>' + options[i] + '</option>'; }
14922
+ x += addHtmlValue("Group by", '<select id=d2groupBy style=float:right;width:250px onchange=generateReportDialogValidate()>' + y + '</select>');
14923
+
14924
+ y = '';
14925
+ if (settings.timeRange == null) { settings.timeRange = 1; }
14926
+ var options = { 1 : "Last Day", 7: "Last 7 days", 30: "Last 30 days", 0: "Time range" }
14927
+ for (var i in options) { y += '<option value=' + i + ((settings.timeRange == i)?' selected':'') + '>' + options[i] + '</option>'; }
14928
+ x += addHtmlValue("Time", '<select id=d2timeRange style=float:right;width:250px onchange=generateReportDialogValidate()>' + y + '</select>');
14929
+
14930
+ x += '<div id=d2timeRangeDiv style=display:none>';
14931
+ x += addHtmlValue("Time Range", '<input id=d2timeRangeSelector style=float:right;width:250px class=flatpickr type="text" placeholder="Select Date & Time.." data-id="altinput">');
14932
+ x += '</div>';
14933
+
14934
+ setDialogMode(2, "Generate Report", 3, generateReportDialogEx, x);
14935
+ generateReportDialogValidate();
14936
+
14937
+ var lastWeek = new Date();
14938
+ lastWeek.setDate(lastWeek.getDate() - 7);
14939
+ var rangeTime = flatpickr('#d2timeRangeSelector', { mode: 'range', enableTime: true, maxDate: new Date(), defaultDate: [ lastWeek, new Date() ] });
14940
+ xxdialogTag = rangeTime;
14941
+ }
14942
+
14943
+ function generateReportDialogValidate() {
14944
+ QV('d2timeRangeDiv', Q('d2timeRange').value == 0);
14945
+ }
14946
+
14947
+ function generateReportDialogEx(b, tag) {
14948
+ var start, end;
14949
+ if (Q('d2timeRange').value == 0) {
14950
+ end = Math.floor(tag.selectedDates[1].getTime() / 1000);
14951
+ start = Math.floor(tag.selectedDates[0].getTime() / 1000);
14952
+ } else {
14953
+ end = Math.floor(new Date() / 1000);
14954
+ start = new Date();
14955
+ start = Math.floor(start.setDate(start.getDate() - Q('d2timeRange').value) / 1000);
14956
+ }
14957
+ var tz = null;
14958
+ try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (ex) {}
14959
+ putstore('_ReportSettings', JSON.stringify({ type: parseInt(Q('d2reportType').value), groupBy: parseInt(Q('d2groupBy').value), timeRange: parseInt(Q('d2timeRange').value) }));
14960
+ meshserver.send({ action: 'report', type: parseInt(Q('d2reportType').value), groupBy: parseInt(Q('d2groupBy').value), start: start, end: end, tz: tz, tf: new Date().getTimezoneOffset(), l: getLang() });
14961
+ }
14962
+
14963
+ function renderReport(r) {
14964
+ //console.log('renderReport', r);
14965
+ var colTranslation = { time: "Time", device: "Device", session: "Session", user: "User", length: "Length" }
14966
+ var x = '<table style=width:100%>';
14967
+ x += '<tr>'
14968
+ for (var i in r.columns) {
14969
+ var coltitle;
14970
+ if (colTranslation[r.columns[i].title] != null) { coltitle = colTranslation[r.columns[i].title]; } else { coltitle = EscapeHtml(r.columns[i].title); }
14971
+ if ((i == 0) && ((r.columns[i].format == 'datetime') || (r.columns[i].format == 'time'))) {
14972
+ x += '<th style=width:1%>' + coltitle + '</th>';
14973
+ } else {
14974
+ x += '<th>' + coltitle + '</th>';
14975
+ }
14976
+ }
14977
+ x += '</tr>'
14978
+ for (var i in r.groups) {
14979
+ x += '<tr><td colspan=' + r.columns.length + ' style="border-bottom:1pt solid black"><b>'
14980
+ x += renderReportFormat(i, r.groupFormat);
14981
+ x += '</b></td></tr>'
14982
+ for (var j in r.groups[i].entries) {
14983
+ var e = r.groups[i].entries[j];
14984
+ x += '<tr>'
14985
+ for (var k in r.columns) {
14986
+ var style = '';
14987
+ if (r.columns[k].align) { style = 'text-align:' + EscapeHtml(r.columns[k].align); }
14988
+ if (e[r.columns[k].id] != null) { x += '<td style="' + style + '">' + renderReportFormat(e[r.columns[k].id], r.columns[k].format) + '</td>'; } else { x += '<td></td>'; }
14989
+ if (r.columns[k].format == 'seconds') {
14990
+ var v = e[r.columns[k].id];
14991
+ if (v != null) { if (r.columns[k].subtotal == null) { r.columns[k].subtotal = v; r.columns[k].total = v; } else { r.columns[k].subtotal += v; r.columns[k].total += v; } }
14992
+ }
14993
+ }
14994
+ x += '</tr>'
14995
+ }
14996
+ }
14997
+
14998
+ // Display totals
14999
+ x += '<tr>'
15000
+ for (var i in r.columns) {
15001
+ if (r.columns[i].total != null) {
15002
+ var style = '';
15003
+ if (r.columns[k].align) { style = 'text-align:' + EscapeHtml(r.columns[k].align); }
15004
+ x += '<td style="border-top:1pt solid black;color:#777;' + style + '">' + renderReportFormat(r.columns[i].total, r.columns[i].format); + '</td>';
15005
+ } else {
15006
+ x += '<td></td>';
15007
+ }
15008
+ }
15009
+ x += '</tr>'
15010
+
15011
+ x += '</table>';
15012
+ QH('p60report', x);
15013
+ }
15014
+
15015
+ function renderReportFormat(v, f) {
15016
+ if (f == 'datetime') { return printDateTime(new Date(v)).split(' ').join(' '); }
15017
+ if (f == 'time') { return printTime(new Date(v)).split(' ').join(' '); }
15018
+ if (f == 'protocol') {
15019
+ if (v == 1) return "Terminal";
15020
+ if (v == 2) return "Desktop";
15021
+ if (v == 5) return "Files";
15022
+ EscapeHtml(v);
15023
+ }
15024
+ if (f == 'seconds') {
15025
+ var seconds = v % 60;
15026
+ var minutes = Math.floor(v / 60) & 60;
15027
+ var hours = Math.floor(v / 3600);
15028
+ return zeroPad(hours, 2) + ':' + zeroPad(minutes, 2) + ':' + zeroPad(seconds, 2);
15029
+ }
15030
+ if (f == 'node') {
15031
+ var node = getNodeFromId(v);
15032
+ if (node != null) { return '<div onclick=\'gotoDevice("' + node._id + '",10);haltEvent(event);\' style=float:left;margin-right:4px class="j' + node.icon + '"></div>' + EscapeHtml(node.name); } else { return '<i>' + "Unknown Device" + '</i>'; }
15033
+ }
15034
+ if (f == 'user') {
15035
+ var user = null;
15036
+ if (v == userinfo._id) { user = userinfo; } else { if (users != null) { user = users[v]; } }
15037
+ if (user != null) {
15038
+ var name = user.name;
15039
+ if (user.realname != null) { name += ', ' + user.realname; }
15040
+ return '<div onclick=\'gotoUser("' + user._id + '",10);haltEvent(event);\' style=float:left;margin-right:4px;cursor:pointer class="m2"></div>' + EscapeHtml(name);
15041
+ } else {
15042
+ return '<i>' + "Unknown User" + '</i>';
15043
+ }
15044
+ }
15045
+ return EscapeHtml(v);
15046
+ }
15047
+
15048
//
15049
// NOTIFICATIONS
15050
//
@@ -15638,7 +15812,7 @@
15812
if (xxcurrentView == 17) deviceDetailsStatsClear();
15813
15814
// Edit this line when adding a new screen
15641
- for (var i = 0; i < 53; i++) { QV('p' + i, i == x); }
15815
+ for (var i = 0; i < 61; i++) { QV('p' + i, i == x); }
15816
xxcurrentView = x;
15817
15818
// Get out of fullscreen if needed
@@ -15689,7 +15863,7 @@
15863
// My Account
15864
QC('MainMenuMyAccount').add(mainMenuActiveClass);
15865
QC('LeftMenuMyAccount').add(leftMenuActiveClass);
15692
- } else if (x == 3) {
15866
+ } else if ((x == 3) || (x == 60)) {
15867
// My Events
15868
QC('MainMenuMyEvents').add(mainMenuActiveClass);
15869
QC('LeftMenuMyEvents').add(leftMenuActiveClass);
@@ -15721,7 +15895,8 @@
15895
QV('UserSubMenuSpan', (x >= 30) && (x < 40));
15896
QV('ServerSubMenuSpan', x == 6 || x == 115 || x == 40 || x == 41 || x == 42 || x == 43);
15897
QV('UsersSubMenuSpan', x == 4 || x == 50 || x == 52);
15724
- var panels = { 4: 'UsersGeneral', 10: 'MainDev', 11: 'MainDevDesktop', 12: 'MainDevTerminal', 13: 'MainDevFiles', 14: 'MainDevAmt', 15: 'MainDevConsole', 16: 'MainDevEvents', 17: 'MainDevInfo', 19: 'MainDevPlugins', 20: 'MeshGeneral', 21: 'MeshSummary', 30: 'UserGeneral', 31: 'UserEvents', 6: 'ServerGeneral', 40: 'ServerStats', 41: 'ServerTrace', 42: 'ServerPlugins', 50: 'UsersGroups', 52: 'UsersRecordings', 115: 'ServerConsole' };
15898
+ QV('EventsSubMenuSpan', (x == 3) || (x == 60));
15899
+ var panels = { 3: 'EventsLive', 4: 'UsersGeneral', 10: 'MainDev', 11: 'MainDevDesktop', 12: 'MainDevTerminal', 13: 'MainDevFiles', 14: 'MainDevAmt', 15: 'MainDevConsole', 16: 'MainDevEvents', 17: 'MainDevInfo', 19: 'MainDevPlugins', 20: 'MeshGeneral', 21: 'MeshSummary', 30: 'UserGeneral', 31: 'UserEvents', 6: 'ServerGeneral', 40: 'ServerStats', 41: 'ServerTrace', 42: 'ServerPlugins', 50: 'UsersGroups', 52: 'UsersRecordings', 60: 'EventsReport', 115: 'ServerConsole' };
15900
for (var i in panels) {
15901
QC(panels[i]).remove('style3x');
15902
QC(panels[i]).remove('style3sel');