mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-30 10:35:55 +01:00
feat: closes #12453, filter events by user/group
This commit is contained in:
@@ -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"
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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})`);
|
||||
|
||||
@@ -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}` : ''}`)),
|
||||
]);
|
||||
|
||||
|
||||
114
src/events.js
114
src/events.js
@@ -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),
|
||||
|
||||
31
src/upgrades/3.8.0/events-uid-filter.js
Normal file
31
src/upgrades/3.8.0/events-uid-filter.js
Normal 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,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user