feat: closes #12453, filter events by user/group

This commit is contained in:
Barış Soner Uşaklı
2024-04-01 18:19:58 -04:00
parent 73f985684c
commit 4030f18a04
8 changed files with 191 additions and 22 deletions

View File

@@ -9,5 +9,9 @@
"filter-type": "Event Type",
"filter-start": "Start Date",
"filter-end": "End Date",
"filter-user": "Filter by User",
"filter-user.placeholder": "Type user name to filter...",
"filter-group": "Filter by Group",
"filter-group.placeholder": "Type group name to filter...",
"filter-per-page": "Per Page"
}

View File

@@ -1,7 +1,7 @@
'use strict';
define('admin/advanced/events', ['bootbox', 'alerts'], function (bootbox, alerts) {
define('admin/advanced/events', ['bootbox', 'alerts', 'autocomplete'], function (bootbox, alerts, autocomplete) {
const Events = {};
Events.init = function () {
@@ -30,6 +30,21 @@ define('admin/advanced/events', ['bootbox', 'alerts'], function (bootbox, alerts
});
});
$('#user-group-select').on('change', function () {
const val = $(this).val();
$('#username').toggleClass('hidden', val !== 'username');
if (val !== 'username') {
$('#username').val('');
}
$('#group').toggleClass('hidden', val !== 'group');
if (val !== 'group') {
$('#group').val('');
}
});
autocomplete.user($('#username'));
autocomplete.group($('#group'));
$('#apply').on('click', Events.refresh);
};

View File

@@ -130,7 +130,11 @@ async function listPlugins() {
async function listEvents(count = 10) {
await db.init();
const eventData = await events.getEvents('', 0, count - 1);
const eventData = await events.getEvents({
filter: '',
start: 0,
stop: count - 1,
});
console.log(chalk.bold(`\nDisplaying last ${count} administrative events...`));
eventData.forEach((event) => {
console.log(` * ${chalk.green(String(event.timestampISO))} ${chalk.yellow(String(event.type))}${event.text ? ` ${event.text}` : ''} (uid: ${event.uid ? event.uid : 0})`);

View File

@@ -3,6 +3,8 @@
const db = require('../../database');
const events = require('../../events');
const pagination = require('../../pagination');
const user = require('../../user');
const groups = require('../../groups');
const eventsController = module.exports;
@@ -11,18 +13,35 @@ eventsController.get = async function (req, res) {
const itemsPerPage = parseInt(req.query.perPage, 10) || 20;
const start = (page - 1) * itemsPerPage;
const stop = start + itemsPerPage - 1;
let uids;
if (req.query.username) {
uids = [await user.getUidByUsername(req.query.username)];
} else if (req.query.group) {
uids = await groups.getMembers(req.query.group, 0, -1);
}
// Limit by date
let from = req.query.start ? new Date(req.query.start) || undefined : undefined;
let to = req.query.end ? new Date(req.query.end) || undefined : new Date();
from = from && from.setHours(0, 0, 0, 0); // setHours returns a unix timestamp (Number, not Date)
to = to && to.setHours(23, 59, 59, 999); // setHours returns a unix timestamp (Number, not Date)
from = from && from.setUTCHours(0, 0, 0, 0); // setHours returns a unix timestamp (Number, not Date)
to = to && to.setUTCHours(23, 59, 59, 999); // setHours returns a unix timestamp (Number, not Date)
const currentFilter = req.query.type || '';
const [eventCount, eventData, counts] = await Promise.all([
db.sortedSetCount(`events:time${currentFilter ? `:${currentFilter}` : ''}`, from || '-inf', to),
events.getEvents(currentFilter, start, stop, from || '-inf', to),
events.getEventCount({
filter: currentFilter,
uids,
from: from || '-inf',
to,
}),
events.getEvents({
filter: currentFilter,
uids,
start,
stop,
from: from || '-inf',
to,
}),
db.sortedSetsCard([''].concat(events.types).map(type => `events:time${type ? `:${type}` : ''}`)),
]);

View File

@@ -87,30 +87,114 @@ events.log = async function (data) {
const eid = await db.incrObjectField('global', 'nextEid');
data.timestamp = Date.now();
data.eid = eid;
const setKeys = [
'events:time',
`events:time:${data.type}`,
];
if (data.hasOwnProperty('uid') && data.uid) {
setKeys.push(`events:time:uid:${data.uid}`);
}
await Promise.all([
db.sortedSetsAdd([
'events:time',
`events:time:${data.type}`,
], data.timestamp, eid),
db.sortedSetsAdd(setKeys, data.timestamp, eid),
db.setObject(`event:${eid}`, data),
]);
plugins.hooks.fire('action:events.log', { data: data });
};
events.getEvents = async function (filter, start, stop, from, to) {
// from/to optional
if (from === undefined) {
from = '-inf';
// filter, start, stop, from(optional), to(optional), uids(optional)
events.getEvents = async function (options) {
// backwards compatibility
if (arguments.length > 1) {
// eslint-disable-next-line prefer-rest-params
const args = Array.prototype.slice.call(arguments);
options = {
filter: args[0],
start: args[1],
stop: args[2],
from: args[3],
to: args[4],
};
}
if (to === undefined) {
to = '+inf';
// from/to optional
const from = options.hasOwnProperty('from') ? options.from : '-inf';
const to = options.hasOwnProperty('to') ? options.to : '+inf';
const { filter, start, stop, uids } = options;
let eids = [];
if (Array.isArray(uids)) {
if (filter === '') {
eids = await db.getSortedSetRevRangeByScore(
uids.map(uid => `events:time:uid:${uid}`),
start,
stop === -1 ? -1 : stop - start + 1,
to,
from
);
} else {
eids = await Promise.all(
uids.map(
uid => db.getSortedSetRevIntersect({
sets: [`events:time:uid:${uid}`, `events:time:${filter}`],
start: 0,
stop: -1,
weights: [1, 0],
withScores: true,
})
)
);
eids = _.flatten(eids)
.filter(
i => (from === '-inf' || i.score >= from) && (to === '+inf' || i.score <= to)
)
.sort((a, b) => b.score - a.score)
.slice(start, stop + 1)
.map(i => i.value);
}
} else {
eids = await db.getSortedSetRevRangeByScore(
`events:time${filter ? `:${filter}` : ''}`,
start,
stop === -1 ? -1 : stop - start + 1,
to,
from
);
}
const eids = await db.getSortedSetRevRangeByScore(`events:time${filter ? `:${filter}` : ''}`, start, stop === -1 ? -1 : stop - start + 1, to, from);
return await events.getEventsByEventIds(eids);
};
events.getEventCount = async (options) => {
const { filter, uids, from, to } = options;
if (Array.isArray(uids)) {
if (filter === '') {
const counts = await Promise.all(
uids.map(uid => db.sortedSetCount(`events:time:uid:${uid}`, from, to))
);
return counts.reduce((prev, cur) => prev + cur, 0);
}
const eids = await Promise.all(
uids.map(
uid => db.getSortedSetRevIntersect({
sets: [`events:time:uid:${uid}`, `events:time:${filter}`],
start: 0,
stop: -1,
weights: [1, 0],
withScores: true,
})
)
);
return _.flatten(eids).filter(
i => (from === '-inf' || i.score >= from) && (to === '+inf' || i.score <= to)
).length;
}
return await db.sortedSetCount(`events:time${filter ? `:${filter}` : ''}`, from || '-inf', to);
};
events.getEventsByEventIds = async (eids) => {
let eventsData = await db.getObjects(eids.map(eid => `event:${eid}`));
eventsData = eventsData.filter(Boolean);
@@ -163,7 +247,11 @@ async function addUserData(eventsData, field, objectName) {
events.deleteEvents = async function (eids) {
const keys = eids.map(eid => `event:${eid}`);
const eventData = await db.getObjectsFields(keys, ['type']);
const sets = _.uniq(['events:time'].concat(eventData.map(e => `events:time:${e.type}`)));
const sets = _.uniq(
['events:time']
.concat(eventData.map(e => `events:time:${e.type}`))
.concat(eventData.map(e => `events:time:uid:${e.uid}`))
);
await Promise.all([
db.deleteAll(keys),
db.sortedSetRemove(sets, eids),

View File

@@ -0,0 +1,31 @@
/* eslint-disable no-await-in-loop */
'use strict';
const db = require('../../database');
const batch = require('../../batch');
module.exports = {
name: 'Add user filter to acp events',
timestamp: Date.UTC(2024, 3, 1),
method: async function () {
const { progress } = this;
await batch.processSortedSet(`events:time`, async (eids) => {
const eventData = await db.getObjects(eids.map(eid => `event:${eid}`));
const bulkAdd = [];
eventData.forEach((event) => {
if (event && event.hasOwnProperty('uid') && event.uid && event.eid) {
bulkAdd.push(
[`events:time:uid:${event.uid}`, event.timestamp || Date.now(), event.eid]
);
}
});
await db.sortedSetAddBulk(bulkAdd);
progress.incr(eids.length);
}, {
batch: 500,
progress,
});
},
};

View File

@@ -50,6 +50,14 @@
<label class="form-label" for="end">[[admin/advanced/events:filter-end]]</label>
<input type="date" id="end" name="end" value="{query.end}" class="form-control" />
</div>
<div class="mb-3 d-flex flex-column gap-3">
<select id="user-group-select" class="form-select">
<option value="username" {{{ if (query.username != "") }}}selected{{{ end }}}>[[admin/advanced/events:filter-user]]</option>
<option value="group" {{{ if (query.group != "") }}}selected{{{ end }}}>[[admin/advanced/events:filter-group]]</option>
</select>
<input type="text" id="username" name="username" value="{query.username}" class="form-control {{{ if (query.group != "") }}}hidden{{{ end }}}" placeholder="[[admin/advanced/events:filter-user.placeholder]]"/>
<input type="text" id="group" name="group" value="{query.group}" class="form-control {{{ if (query.group == "") }}}hidden{{{ end }}} {{{ if (query.username != "") }}}hidden{{{ end }}}" placeholder="[[admin/advanced/events:filter-group.placeholder]]" />
</div>
<div class="mb-3">
<label class="form-label" for="perPage">[[admin/advanced/events:filter-per-page]]</label>
<input type="text" id="perPage" name="perPage" value="{query.perPage}" class="form-control" />

View File

@@ -687,7 +687,7 @@ describe('socket.io', () => {
await socketUser.reset.send({ uid: 0 }, 'regular@test.com');
const [count, eventsData] = await Promise.all([
db.sortedSetCount('reset:issueDate', 0, Date.now()),
events.getEvents('', 0, 0),
events.getEvents({ filter: '', start: 0, stop: 0 }),
]);
assert.strictEqual(count, 2);
@@ -705,7 +705,7 @@ describe('socket.io', () => {
);
const [count, eventsData] = await Promise.all([
db.sortedSetCount('reset:issueDate', 0, Date.now()),
events.getEvents('', 0, 0),
events.getEvents({ filter: '', start: 0, stop: 0 }),
]);
assert.strictEqual(count, 2);