Skip to content

Commit

Permalink
feat: closes #12453, filter events by user/group
Browse files Browse the repository at this point in the history
  • Loading branch information
barisusakli committed Apr 1, 2024
1 parent 73f9856 commit 4030f18
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 22 deletions.
4 changes: 4 additions & 0 deletions public/language/en-GB/admin/advanced/events.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
17 changes: 16 additions & 1 deletion public/src/admin/advanced/events.js
Original file line number Diff line number Diff line change
@@ -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 () {
Expand Down Expand Up @@ -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);
};

Expand Down
6 changes: 5 additions & 1 deletion src/cli/manage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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})`);
Expand Down
29 changes: 24 additions & 5 deletions src/controllers/admin/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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}` : ''}`)),
]);

Expand Down
114 changes: 101 additions & 13 deletions src/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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),
Expand Down
31 changes: 31 additions & 0 deletions src/upgrades/3.8.0/events-uid-filter.js
Original file line number Diff line number Diff line change
@@ -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,
});
},
};
8 changes: 8 additions & 0 deletions src/views/admin/advanced/events.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
4 changes: 2 additions & 2 deletions test/socket.io.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);

Expand Down

0 comments on commit 4030f18

Please sign in to comment.