mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-02 20:16:04 +01:00
feat: closes #12453, filter events by user/group
This commit is contained in:
@@ -9,5 +9,9 @@
|
|||||||
"filter-type": "Event Type",
|
"filter-type": "Event Type",
|
||||||
"filter-start": "Start Date",
|
"filter-start": "Start Date",
|
||||||
"filter-end": "End 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"
|
"filter-per-page": "Per Page"
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
define('admin/advanced/events', ['bootbox', 'alerts'], function (bootbox, alerts) {
|
define('admin/advanced/events', ['bootbox', 'alerts', 'autocomplete'], function (bootbox, alerts, autocomplete) {
|
||||||
const Events = {};
|
const Events = {};
|
||||||
|
|
||||||
Events.init = function () {
|
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);
|
$('#apply').on('click', Events.refresh);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,11 @@ async function listPlugins() {
|
|||||||
|
|
||||||
async function listEvents(count = 10) {
|
async function listEvents(count = 10) {
|
||||||
await db.init();
|
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...`));
|
console.log(chalk.bold(`\nDisplaying last ${count} administrative events...`));
|
||||||
eventData.forEach((event) => {
|
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})`);
|
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 db = require('../../database');
|
||||||
const events = require('../../events');
|
const events = require('../../events');
|
||||||
const pagination = require('../../pagination');
|
const pagination = require('../../pagination');
|
||||||
|
const user = require('../../user');
|
||||||
|
const groups = require('../../groups');
|
||||||
|
|
||||||
const eventsController = module.exports;
|
const eventsController = module.exports;
|
||||||
|
|
||||||
@@ -11,18 +13,35 @@ eventsController.get = async function (req, res) {
|
|||||||
const itemsPerPage = parseInt(req.query.perPage, 10) || 20;
|
const itemsPerPage = parseInt(req.query.perPage, 10) || 20;
|
||||||
const start = (page - 1) * itemsPerPage;
|
const start = (page - 1) * itemsPerPage;
|
||||||
const stop = start + itemsPerPage - 1;
|
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
|
// Limit by date
|
||||||
let from = req.query.start ? new Date(req.query.start) || undefined : undefined;
|
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();
|
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)
|
from = from && from.setUTCHours(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)
|
to = to && to.setUTCHours(23, 59, 59, 999); // setHours returns a unix timestamp (Number, not Date)
|
||||||
|
|
||||||
const currentFilter = req.query.type || '';
|
const currentFilter = req.query.type || '';
|
||||||
|
|
||||||
const [eventCount, eventData, counts] = await Promise.all([
|
const [eventCount, eventData, counts] = await Promise.all([
|
||||||
db.sortedSetCount(`events:time${currentFilter ? `:${currentFilter}` : ''}`, from || '-inf', to),
|
events.getEventCount({
|
||||||
events.getEvents(currentFilter, start, stop, from || '-inf', to),
|
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}` : ''}`)),
|
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');
|
const eid = await db.incrObjectField('global', 'nextEid');
|
||||||
data.timestamp = Date.now();
|
data.timestamp = Date.now();
|
||||||
data.eid = eid;
|
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([
|
await Promise.all([
|
||||||
db.sortedSetsAdd([
|
db.sortedSetsAdd(setKeys, data.timestamp, eid),
|
||||||
'events:time',
|
|
||||||
`events:time:${data.type}`,
|
|
||||||
], data.timestamp, eid),
|
|
||||||
db.setObject(`event:${eid}`, data),
|
db.setObject(`event:${eid}`, data),
|
||||||
]);
|
]);
|
||||||
plugins.hooks.fire('action:events.log', { data: data });
|
plugins.hooks.fire('action:events.log', { data: data });
|
||||||
};
|
};
|
||||||
|
|
||||||
events.getEvents = async function (filter, start, stop, from, to) {
|
// filter, start, stop, from(optional), to(optional), uids(optional)
|
||||||
// from/to optional
|
events.getEvents = async function (options) {
|
||||||
if (from === undefined) {
|
// backwards compatibility
|
||||||
from = '-inf';
|
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) {
|
// from/to optional
|
||||||
to = '+inf';
|
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);
|
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) => {
|
events.getEventsByEventIds = async (eids) => {
|
||||||
let eventsData = await db.getObjects(eids.map(eid => `event:${eid}`));
|
let eventsData = await db.getObjects(eids.map(eid => `event:${eid}`));
|
||||||
eventsData = eventsData.filter(Boolean);
|
eventsData = eventsData.filter(Boolean);
|
||||||
@@ -163,7 +247,11 @@ async function addUserData(eventsData, field, objectName) {
|
|||||||
events.deleteEvents = async function (eids) {
|
events.deleteEvents = async function (eids) {
|
||||||
const keys = eids.map(eid => `event:${eid}`);
|
const keys = eids.map(eid => `event:${eid}`);
|
||||||
const eventData = await db.getObjectsFields(keys, ['type']);
|
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([
|
await Promise.all([
|
||||||
db.deleteAll(keys),
|
db.deleteAll(keys),
|
||||||
db.sortedSetRemove(sets, eids),
|
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>
|
<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" />
|
<input type="date" id="end" name="end" value="{query.end}" class="form-control" />
|
||||||
</div>
|
</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">
|
<div class="mb-3">
|
||||||
<label class="form-label" for="perPage">[[admin/advanced/events:filter-per-page]]</label>
|
<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" />
|
<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');
|
await socketUser.reset.send({ uid: 0 }, 'regular@test.com');
|
||||||
const [count, eventsData] = await Promise.all([
|
const [count, eventsData] = await Promise.all([
|
||||||
db.sortedSetCount('reset:issueDate', 0, Date.now()),
|
db.sortedSetCount('reset:issueDate', 0, Date.now()),
|
||||||
events.getEvents('', 0, 0),
|
events.getEvents({ filter: '', start: 0, stop: 0 }),
|
||||||
]);
|
]);
|
||||||
assert.strictEqual(count, 2);
|
assert.strictEqual(count, 2);
|
||||||
|
|
||||||
@@ -705,7 +705,7 @@ describe('socket.io', () => {
|
|||||||
);
|
);
|
||||||
const [count, eventsData] = await Promise.all([
|
const [count, eventsData] = await Promise.all([
|
||||||
db.sortedSetCount('reset:issueDate', 0, Date.now()),
|
db.sortedSetCount('reset:issueDate', 0, Date.now()),
|
||||||
events.getEvents('', 0, 0),
|
events.getEvents({ filter: '', start: 0, stop: 0 }),
|
||||||
]);
|
]);
|
||||||
assert.strictEqual(count, 2);
|
assert.strictEqual(count, 2);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user