Started work on reports feature.

This commit is contained in:
Ylian Saint-Hilaire 2021-09-08 15:55:07 -07:00
parent e2105d6844
commit a15a5e779d
4 changed files with 242 additions and 4 deletions

1
db.js
View File

@ -1402,6 +1402,7 @@ module.exports.CreateDB = function (parent, func) {
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); };
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); };
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); };
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); };
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); };
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); };
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); };

View File

@ -5408,6 +5408,68 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
break;
}
case 'report': {
// Report request. Validate the input
if (common.validateInt(command.type, 1, 1) == false) break; // Validate type
if (common.validateInt(command.groupBy, 1, 3) == false) break; // Validate groupBy: 1 = User, 2 = Device, 3 = Day
if ((typeof command.start != 'number') || (typeof command.end != 'number') || (command.start >= command.end)) break; // Validate start and end time
if (command.type == 1) { // This is the remote session report. Shows desktop, terminal, files...
// If we are not user administrator on this site, only search for events with our own user id.
var ids = [user._id];
if ((user.siteadmin & SITERIGHT_MANAGEUSERS) != 0) { ids = ['*']; }
// Get the events in the time range
db.GetEventsTimeRange(ids, domain.id, [5, 10, 12], new Date(command.start * 1000), new Date(command.end * 1000), function (err, docs) {
if (err != null) return;
var data = { groups: {} };
// Columns
if (command.groupBy == 1) {
data.groupFormat = 'user';
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" } ];
} else if (command.groupBy == 2) {
data.groupFormat = 'node';
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" } ];
} else if (command.groupBy == 3) {
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" } ];
}
// Rows
for (var i in docs) {
var entry = { time: docs[i].time.valueOf() };
// UserID
if (command.groupBy != 1) { entry.userid = docs[i].userid; }
if (command.groupBy != 2) { entry.nodeid = docs[i].nodeid; }
entry.protocol = docs[i].protocol;
// Session length
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]; }
if (command.groupBy == 1) { // Add entry to per user group
if (data.groups[docs[i].userid] == null) { data.groups[docs[i].userid] = { entries: [] }; }
data.groups[docs[i].userid].entries.push(entry);
} else if (command.groupBy == 2) { // Add entry to per device group
if (data.groups[docs[i].nodeid] == null) { data.groups[docs[i].nodeid] = { entries: [] }; }
data.groups[docs[i].nodeid].entries.push(entry);
} else if (command.groupBy == 3) { // Add entry to per day group
var day;
if ((typeof command.l == 'string') && (typeof command.tz == 'string')) {
day = new Date(docs[i].time).toLocaleDateString(command.l, { timeZone: command.tz });
} else {
day = docs[i].time; // TODO
}
if (data.groups[day] == null) { data.groups[day] = { entries: [] }; }
data.groups[day].entries.push(entry);
}
}
try { ws.send(JSON.stringify({ action: 'report', data: data })); } catch (ex) { }
});
}
break;
}
default: {
// Unknown user action
console.log('Unknown action from user ' + user.name + ': ' + command.action + '.');

View File

@ -273,7 +273,7 @@ body {
}
/* #UserDummyMenuSpan, */
#MainSubMenuSpan, #MeshSubMenuSpan, #UserSubMenuSpan, #UsersSubMenuSpan, #ServerSubMenuSpan, #MainMenuSpan, #MainSubMenu, #MeshSubMenu, #UserSubMenu, #ServerSubMenu, #UserDummyMenu, #PluginSubMenu {
#MainSubMenuSpan, #MeshSubMenuSpan, #EventsSubMenuSpan, #UserSubMenuSpan, #UsersSubMenuSpan, #ServerSubMenuSpan, #MainMenuSpan, #MainSubMenu, #MeshSubMenu, #UserSubMenu, #ServerSubMenu, #UserDummyMenu, #PluginSubMenu {
width: 100%;
height: 24px;
color: white;

View File

@ -209,6 +209,15 @@
</tr>
</table>
</div>
<div id=EventsSubMenuSpan style="display:none">
<table id=EventsSubMenu cellpadding=0 cellspacing=0 class=style1>
<tr>
<td tabindex=0 id=EventsLive class="topbar_td style3x" onclick=go(3,event) onkeypress="if (event.key == 'Enter') go(3)">Events</td>
<td tabindex=0 id=EventsReport class="topbar_td style3x" onclick=go(60,event) onkeypress="if (event.key == 'Enter') go(60)">Reports</td>
<td class="topbar_td_end style3">&nbsp;</td>
</tr>
</table>
</div>
<div id=UserSubMenuSpan style="display:none">
<table id=UserSubMenu cellpadding=0 cellspacing=0 class=style1>
<tr>
@ -1170,6 +1179,23 @@
</table>
<div id=p52recordings style="overflow-y:auto"></div>
</div>
<div id=p60 style="display:none">
<div id="p60title">
<h1>My Reports</h1>
</div>
<table class="pTable">
<tr>
<td class="h1"></td>
<td class="style14">
<div>
<input type=button onclick=generateReportDialog() value="Generate Report..." />
</div>
</td>
<td class="h2"></td>
</tr>
</table>
<div id=p60report style="overflow-y:auto"></div>
</div>
<br id="column_l_bottomgap" />
</div>
<div id="footer">
@ -1793,6 +1819,8 @@
QS('p41events')['max-height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
QS('p52recordings')['height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
QS('p52recordings')['max-height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
QS('p60report')['height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
QS('p60report')['max-height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
// We are looking at a single device, remove all the back buttons
if ((args.hide & 32) || ('{{currentNode}}'.toLowerCase() != '')) {
@ -3520,6 +3548,10 @@
mainUpdate(65536);
break;
}
case 'report': {
renderReport(message.data);
break;
}
default:
//console.log('Unknown message.action', message.action);
break;
@ -14871,6 +14903,148 @@
}
}
//
// MY REPORTS
//
function generateReportDialog() {
if (xxdialogMode) return;
var y = '', x = '', settings = JSON.parse(getstore('_ReportSettings', '{}'));
var options = { 1 : "Remote Sessions" }
for (var i in options) { y += '<option value=' + i + ((settings.type == i)?' selected':'') + '>' + options[i] + '</option>'; }
x += addHtmlValue("Type", '<select id=d2reportType style=float:right;width:250px onchange=generateReportDialogValidate()>' + y + '</select>');
y = '';
var options = { 1 : "User", 2: "Device", 3: "Day" }
for (var i in options) { y += '<option value=' + i + ((settings.groupBy == i)?' selected':'') + '>' + options[i] + '</option>'; }
x += addHtmlValue("Group by", '<select id=d2groupBy style=float:right;width:250px onchange=generateReportDialogValidate()>' + y + '</select>');
y = '';
if (settings.timeRange == null) { settings.timeRange = 1; }
var options = { 1 : "Last Day", 7: "Last 7 days", 30: "Last 30 days", 0: "Time range" }
for (var i in options) { y += '<option value=' + i + ((settings.timeRange == i)?' selected':'') + '>' + options[i] + '</option>'; }
x += addHtmlValue("Time", '<select id=d2timeRange style=float:right;width:250px onchange=generateReportDialogValidate()>' + y + '</select>');
x += '<div id=d2timeRangeDiv style=display:none>';
x += addHtmlValue("Time Range", '<input id=d2timeRangeSelector style=float:right;width:250px class=flatpickr type="text" placeholder="Select Date & Time.." data-id="altinput">');
x += '</div>';
setDialogMode(2, "Generate Report", 3, generateReportDialogEx, x);
generateReportDialogValidate();
var lastWeek = new Date();
lastWeek.setDate(lastWeek.getDate() - 7);
var rangeTime = flatpickr('#d2timeRangeSelector', { mode: 'range', enableTime: true, maxDate: new Date(), defaultDate: [ lastWeek, new Date() ] });
xxdialogTag = rangeTime;
}
function generateReportDialogValidate() {
QV('d2timeRangeDiv', Q('d2timeRange').value == 0);
}
function generateReportDialogEx(b, tag) {
var start, end;
if (Q('d2timeRange').value == 0) {
end = Math.floor(tag.selectedDates[1].getTime() / 1000);
start = Math.floor(tag.selectedDates[0].getTime() / 1000);
} else {
end = Math.floor(new Date() / 1000);
start = new Date();
start = Math.floor(start.setDate(start.getDate() - Q('d2timeRange').value) / 1000);
}
var tz = null;
try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (ex) {}
putstore('_ReportSettings', JSON.stringify({ type: parseInt(Q('d2reportType').value), groupBy: parseInt(Q('d2groupBy').value), timeRange: parseInt(Q('d2timeRange').value) }));
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() });
}
function renderReport(r) {
//console.log('renderReport', r);
var colTranslation = { time: "Time", device: "Device", session: "Session", user: "User", length: "Length" }
var x = '<table style=width:100%>';
x += '<tr>'
for (var i in r.columns) {
var coltitle;
if (colTranslation[r.columns[i].title] != null) { coltitle = colTranslation[r.columns[i].title]; } else { coltitle = EscapeHtml(r.columns[i].title); }
if ((i == 0) && ((r.columns[i].format == 'datetime') || (r.columns[i].format == 'time'))) {
x += '<th style=width:1%>' + coltitle + '</th>';
} else {
x += '<th>' + coltitle + '</th>';
}
}
x += '</tr>'
for (var i in r.groups) {
x += '<tr><td colspan=' + r.columns.length + ' style="border-bottom:1pt solid black"><b>'
x += renderReportFormat(i, r.groupFormat);
x += '</b></td></tr>'
for (var j in r.groups[i].entries) {
var e = r.groups[i].entries[j];
x += '<tr>'
for (var k in r.columns) {
var style = '';
if (r.columns[k].align) { style = 'text-align:' + EscapeHtml(r.columns[k].align); }
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>'; }
if (r.columns[k].format == 'seconds') {
var v = e[r.columns[k].id];
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; } }
}
}
x += '</tr>'
}
}
// Display totals
x += '<tr>'
for (var i in r.columns) {
if (r.columns[i].total != null) {
var style = '';
if (r.columns[k].align) { style = 'text-align:' + EscapeHtml(r.columns[k].align); }
x += '<td style="border-top:1pt solid black;color:#777;' + style + '">' + renderReportFormat(r.columns[i].total, r.columns[i].format); + '</td>';
} else {
x += '<td></td>';
}
}
x += '</tr>'
x += '</table>';
QH('p60report', x);
}
function renderReportFormat(v, f) {
if (f == 'datetime') { return printDateTime(new Date(v)).split(' ').join('&nbsp'); }
if (f == 'time') { return printTime(new Date(v)).split(' ').join('&nbsp'); }
if (f == 'protocol') {
if (v == 1) return "Terminal";
if (v == 2) return "Desktop";
if (v == 5) return "Files";
EscapeHtml(v);
}
if (f == 'seconds') {
var seconds = v % 60;
var minutes = Math.floor(v / 60) & 60;
var hours = Math.floor(v / 3600);
return zeroPad(hours, 2) + ':' + zeroPad(minutes, 2) + ':' + zeroPad(seconds, 2);
}
if (f == 'node') {
var node = getNodeFromId(v);
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>'; }
}
if (f == 'user') {
var user = null;
if (v == userinfo._id) { user = userinfo; } else { if (users != null) { user = users[v]; } }
if (user != null) {
var name = user.name;
if (user.realname != null) { name += ', ' + user.realname; }
return '<div onclick=\'gotoUser("' + user._id + '",10);haltEvent(event);\' style=float:left;margin-right:4px;cursor:pointer class="m2"></div>' + EscapeHtml(name);
} else {
return '<i>' + "Unknown User" + '</i>';
}
}
return EscapeHtml(v);
}
//
// NOTIFICATIONS
//
@ -15638,7 +15812,7 @@
if (xxcurrentView == 17) deviceDetailsStatsClear();
// Edit this line when adding a new screen
for (var i = 0; i < 53; i++) { QV('p' + i, i == x); }
for (var i = 0; i < 61; i++) { QV('p' + i, i == x); }
xxcurrentView = x;
// Get out of fullscreen if needed
@ -15689,7 +15863,7 @@
// My Account
QC('MainMenuMyAccount').add(mainMenuActiveClass);
QC('LeftMenuMyAccount').add(leftMenuActiveClass);
} else if (x == 3) {
} else if ((x == 3) || (x == 60)) {
// My Events
QC('MainMenuMyEvents').add(mainMenuActiveClass);
QC('LeftMenuMyEvents').add(leftMenuActiveClass);
@ -15721,7 +15895,8 @@
QV('UserSubMenuSpan', (x >= 30) && (x < 40));
QV('ServerSubMenuSpan', x == 6 || x == 115 || x == 40 || x == 41 || x == 42 || x == 43);
QV('UsersSubMenuSpan', x == 4 || x == 50 || x == 52);
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' };
QV('EventsSubMenuSpan', (x == 3) || (x == 60));
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' };
for (var i in panels) {
QC(panels[i]).remove('style3x');
QC(panels[i]).remove('style3sel');