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">&nbsp;</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('&nbsp'); }
15017 + if (f == 'time') { return printTime(new Date(v)).split(' ').join('&nbsp'); }
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');