added filtering to all activity table fields and api history endpoints

This commit is contained in:
CyferShepard
2025-01-26 13:51:04 +02:00
parent b1afafe179
commit e428398019
9 changed files with 613 additions and 78 deletions

View File

@@ -39,6 +39,25 @@ const unGroupedSortMap = [
{ field: "PlaybackDuration", column: "a.PlaybackDuration" },
];
const filterFields = [
{ field: "UserName", column: `LOWER(u."Name")` },
{ field: "RemoteEndPoint", column: `LOWER(a."RemoteEndPoint")` },
{
field: "NowPlayingItemName",
column: `LOWER(
CASE
WHEN a."SeriesName" is null THEN a."NowPlayingItemName"
ELSE CONCAT(a."SeriesName" , ' : S' , a."SeasonNumber" , 'E' , a."EpisodeNumber" , ' - ' , a."NowPlayingItemName")
END
)`,
},
{ field: "Client", column: `LOWER(a."Client")` },
{ field: "DeviceName", column: `LOWER(a."DeviceName")` },
{ field: "ActivityDateInserted", column: "a.ActivityDateInserted", isColumn: true },
{ field: "PlaybackDuration", column: `a.PlaybackDuration`, isColumn: true, applyToCTE: true },
{ field: "TotalPlays", column: `COALESCE("TotalPlays",1)` },
];
//Functions
function groupRecentlyAdded(rows) {
const groupedResults = {};
@@ -121,6 +140,82 @@ async function purgeLibraryItems(id, withActivity, purgeAll = false) {
}
}
function buildFilterList(query, filtersArray) {
if (filtersArray.length > 0) {
query.where = query.where || [];
filtersArray.forEach((filter) => {
const findField = filterFields.find((item) => item.field === filter.field);
const column = findField?.column || "a.ActivityDateInserted";
const isColumn = findField?.isColumn || false;
const applyToCTE = findField?.applyToCTE || false;
if (filter.min) {
query.where.push({
column: column,
operator: ">=",
value: filter.min,
});
if (applyToCTE) {
if (query.cte) {
if (!query.cte.where) {
query.cte.where = [];
}
query.cte.where.push({
column: column,
operator: ">=",
value: filter.min,
});
}
}
}
if (filter.max) {
query.where.push({
column: column,
operator: "<=",
value: filter.max,
});
if (applyToCTE) {
if (query.cte) {
if (!query.cte.where) {
query.cte.where = [];
}
query.cte.where.push({
column: column,
operator: "<=",
value: filter.max,
});
}
}
}
if (filter.value) {
const whereClause = {
operator: "LIKE",
value: filter.value.toLowerCase(),
};
if (isColumn) {
whereClause.column = column;
} else {
whereClause.field = column;
}
query.where.push(whereClause);
if (applyToCTE) {
if (query.cte) {
if (!query.cte.where) {
query.cte.where = [];
}
query.cte.where.push(whereClause);
}
}
}
});
}
}
//////////////////////////////
router.get("/getconfig", async (req, res) => {
try {
const config = await new configClass().getConfig();
@@ -1080,7 +1175,55 @@ router.post("/setExcludedBackupTable", async (req, res) => {
//DB Queries - History
router.get("/getHistory", async (req, res) => {
const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true } = req.query;
const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true, filters } = req.query;
let filtersArray = [];
if (filters) {
try {
filtersArray = JSON.parse(filters);
} catch (error) {
return res.status(400).json({
error: "Invalid filters parameter",
example: [
{
field: "ActivityDateInserted",
min: "2024-12-31T22:00:00.000Z",
max: "2024-12-31T22:00:00.000Z",
},
{
field: "PlaybackDuration",
min: "1",
max: "10",
},
{
field: "TotalPlays",
min: "1",
max: "10",
},
{
field: "DeviceName",
value: "test",
},
{
field: "Client",
value: "test",
},
{
field: "NowPlayingItemName",
value: "test",
},
{
field: "RemoteEndPoint",
value: "127.0.0.1",
},
{
field: "UserName",
value: "test",
},
],
});
}
}
const sortField = groupedSortMap.find((item) => item.field === sort)?.column || "a.ActivityDateInserted";
@@ -1130,6 +1273,12 @@ router.get("/getHistory", async (req, res) => {
{ first: "a.UserId", operator: "=", second: "ar.UserId", type: "and" },
],
},
{
type: "left",
table: "jf_users",
alias: "u",
conditions: [{ first: "a.UserId", operator: "=", second: "u.Id" }],
},
],
order_by: sortField,
@@ -1152,6 +1301,8 @@ router.get("/getHistory", async (req, res) => {
},
];
}
buildFilterList(query, filtersArray);
const result = await dbHelper.query(query);
result.results = result.results.map((item) => ({
@@ -1163,6 +1314,10 @@ router.get("/getHistory", async (req, res) => {
response.search = search;
}
if (filtersArray.length > 0) {
response.filters = filtersArray;
}
res.send(response);
} catch (error) {
console.log(error);
@@ -1171,7 +1326,55 @@ router.get("/getHistory", async (req, res) => {
router.post("/getLibraryHistory", async (req, res) => {
try {
const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true } = req.query;
const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true, filters } = req.query;
let filtersArray = [];
if (filters) {
try {
filtersArray = JSON.parse(filters);
} catch (error) {
return res.status(400).json({
error: "Invalid filters parameter",
example: [
{
field: "ActivityDateInserted",
min: "2024-12-31T22:00:00.000Z",
max: "2024-12-31T22:00:00.000Z",
},
{
field: "PlaybackDuration",
min: "1",
max: "10",
},
{
field: "TotalPlays",
min: "1",
max: "10",
},
{
field: "DeviceName",
value: "test",
},
{
field: "Client",
value: "test",
},
{
field: "NowPlayingItemName",
value: "test",
},
{
field: "RemoteEndPoint",
value: "127.0.0.1",
},
{
field: "UserName",
value: "test",
},
],
});
}
}
const { libraryid } = req.body;
if (libraryid === undefined) {
@@ -1236,6 +1439,12 @@ router.post("/getLibraryHistory", async (req, res) => {
{ first: "a.UserId", operator: "=", second: "ar.UserId", type: "and" },
],
},
{
type: "left",
table: "jf_users",
alias: "u",
conditions: [{ first: "a.UserId", operator: "=", second: "u.Id" }],
},
],
order_by: sortField,
@@ -1259,6 +1468,8 @@ router.post("/getLibraryHistory", async (req, res) => {
];
}
buildFilterList(query, filtersArray);
const result = await dbHelper.query(query);
result.results = result.results.map((item) => ({
@@ -1270,6 +1481,9 @@ router.post("/getLibraryHistory", async (req, res) => {
if (search && search.length > 0) {
response.search = search;
}
if (filtersArray.length > 0) {
response.filters = filtersArray;
}
res.send(response);
} catch (error) {
console.log(error);
@@ -1280,7 +1494,7 @@ router.post("/getLibraryHistory", async (req, res) => {
router.post("/getItemHistory", async (req, res) => {
try {
const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true } = req.query;
const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true, filters } = req.query;
const { itemid } = req.body;
if (itemid === undefined) {
@@ -1289,6 +1503,55 @@ router.post("/getItemHistory", async (req, res) => {
return;
}
let filtersArray = [];
if (filters) {
try {
filtersArray = JSON.parse(filters);
filtersArray = filtersArray.filter((filter) => filter.field !== "TotalPlays");
} catch (error) {
return res.status(400).json({
error: "Invalid filters parameter",
example: [
{
field: "ActivityDateInserted",
min: "2024-12-31T22:00:00.000Z",
max: "2024-12-31T22:00:00.000Z",
},
{
field: "PlaybackDuration",
min: "1",
max: "10",
},
{
field: "TotalPlays",
min: "1",
max: "10",
},
{
field: "DeviceName",
value: "test",
},
{
field: "Client",
value: "test",
},
{
field: "NowPlayingItemName",
value: "test",
},
{
field: "RemoteEndPoint",
value: "127.0.0.1",
},
{
field: "UserName",
value: "test",
},
],
});
}
}
const sortField = unGroupedSortMap.find((item) => item.field === sort)?.column || "a.ActivityDateInserted";
const query = {
@@ -1306,6 +1569,14 @@ router.post("/getItemHistory", async (req, res) => {
],
table: "jf_playback_activity_with_metadata",
alias: "a",
joins: [
{
type: "left",
table: "jf_users",
alias: "u",
conditions: [{ first: "a.UserId", operator: "=", second: "u.Id" }],
},
],
where: [
[
{ column: "a.EpisodeId", operator: "=", value: itemid },
@@ -1334,12 +1605,18 @@ router.post("/getItemHistory", async (req, res) => {
];
}
buildFilterList(query, filtersArray);
const result = await dbHelper.query(query);
const response = { current_page: page, pages: result.pages, size: size, sort: sort, desc: desc, results: result.results };
if (search && search.length > 0) {
response.search = search;
}
if (filters) {
response.filters = JSON.parse(filters);
}
res.send(response);
} catch (error) {
console.log(error);
@@ -1350,7 +1627,56 @@ router.post("/getItemHistory", async (req, res) => {
router.post("/getUserHistory", async (req, res) => {
try {
const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true } = req.query;
const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true, filters } = req.query;
let filtersArray = [];
if (filters) {
try {
filtersArray = JSON.parse(filters);
filtersArray = filtersArray.filter((filter) => filter.field !== "TotalPlays");
} catch (error) {
return res.status(400).json({
error: "Invalid filters parameter",
example: [
{
field: "ActivityDateInserted",
min: "2024-12-31T22:00:00.000Z",
max: "2024-12-31T22:00:00.000Z",
},
{
field: "PlaybackDuration",
min: "1",
max: "10",
},
{
field: "TotalPlays",
min: "1",
max: "10",
},
{
field: "DeviceName",
value: "test",
},
{
field: "Client",
value: "test",
},
{
field: "NowPlayingItemName",
value: "test",
},
{
field: "RemoteEndPoint",
value: "127.0.0.1",
},
{
field: "UserName",
value: "test",
},
],
});
}
}
const { userid } = req.body;
if (userid === undefined) {
@@ -1376,6 +1702,14 @@ router.post("/getUserHistory", async (req, res) => {
],
table: "jf_playback_activity_with_metadata",
alias: "a",
joins: [
{
type: "left",
table: "jf_users",
alias: "u",
conditions: [{ first: "a.UserId", operator: "=", second: "u.Id" }],
},
],
where: [[{ column: "a.UserId", operator: "=", value: userid }]],
order_by: sortField,
sort_order: desc ? "desc" : "asc",
@@ -1397,6 +1731,9 @@ router.post("/getUserHistory", async (req, res) => {
},
];
}
buildFilterList(query, filtersArray);
const result = await dbHelper.query(query);
const response = { current_page: page, pages: result.pages, size: size, sort: sort, desc: desc, results: result.results };
@@ -1405,6 +1742,10 @@ router.post("/getUserHistory", async (req, res) => {
response.search = search;
}
if (filters) {
response.filters = JSON.parse(filters);
}
res.send(response);
} catch (error) {
console.log(error);
@@ -1448,7 +1789,7 @@ router.post("/getActivityTimeLine", async (req, res) => {
return;
}
const {rows} = await db.query(`SELECT * FROM fs_get_user_activity($1, $2);`, [userId, libraries]);
const { rows } = await db.query(`SELECT * FROM fs_get_user_activity($1, $2);`, [userId, libraries]);
res.send(rows);
} catch (error) {
console.log(error);

View File

@@ -2042,12 +2042,20 @@
"name": "desc",
"in": "query",
"type": "string"
},
{
"name": "filters",
"in": "query",
"type": "string"
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request"
},
"401": {
"description": "Unauthorized"
},
@@ -2117,6 +2125,11 @@
"in": "query",
"type": "string"
},
{
"name": "filters",
"in": "query",
"type": "string"
},
{
"name": "body",
"in": "body",
@@ -2208,6 +2221,11 @@
"in": "query",
"type": "string"
},
{
"name": "filters",
"in": "query",
"type": "string"
},
{
"name": "body",
"in": "body",
@@ -2299,6 +2317,11 @@
"in": "query",
"type": "string"
},
{
"name": "filters",
"in": "query",
"type": "string"
},
{
"name": "body",
"in": "body",
@@ -2391,6 +2414,66 @@
}
}
},
"/api/getActivityTimeLine": {
"post": {
"tags": [
"API"
],
"description": "",
"parameters": [
{
"name": "authorization",
"in": "header",
"type": "string"
},
{
"name": "x-api-token",
"in": "header",
"type": "string"
},
{
"name": "req",
"in": "query",
"type": "string"
},
{
"name": "body",
"in": "body",
"schema": {
"type": "object",
"properties": {
"userId": {
"example": "any"
},
"libraries": {
"example": "any"
}
}
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Not Found"
},
"503": {
"description": "Service Unavailable"
}
}
}
},
"/stats/getLibraryOverview": {
"get": {
"tags": [

120
package-lock.json generated
View File

@@ -65,7 +65,7 @@
"semver": "^7.5.3",
"sequelize": "^6.29.0",
"socket.io": "^4.7.2",
"socket.io-client": "^4.7.2",
"socket.io-client": "^4.8.1",
"swagger-autogen": "^2.23.5",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0",
@@ -7574,6 +7574,20 @@
"node": ">=4"
}
},
"node_modules/bufferutil": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz",
"integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==",
"hasInstallScript": true,
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/builtin-modules": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
@@ -9279,23 +9293,23 @@
}
},
"node_modules/engine.io-client": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz",
"integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==",
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-client/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "2.1.2"
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -9307,29 +9321,9 @@
}
},
"node_modules/engine.io-client/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/engine.io-client/node_modules/ws": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/engine.io-parser": {
"version": "5.2.1",
@@ -15384,6 +15378,18 @@
"node": ">= 6.13.0"
}
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"optional": true,
"peer": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -19660,13 +19666,13 @@
}
},
"node_modules/socket.io-client": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz",
"integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==",
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.5.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
@@ -19674,11 +19680,11 @@
}
},
"node_modules/socket.io-client/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "2.1.2"
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -19690,9 +19696,9 @@
}
},
"node_modules/socket.io-client/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
@@ -21415,6 +21421,20 @@
"requires-port": "^1.0.0"
}
},
"node_modules/utf-8-validate": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
"integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
"hasInstallScript": true,
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/utf8": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz",
@@ -22605,9 +22625,9 @@
}
},
"node_modules/ws": {
"version": "8.14.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
"integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"engines": {
"node": ">=10.0.0"
},
@@ -22635,9 +22655,9 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}

View File

@@ -72,7 +72,7 @@
"semver": "^7.5.3",
"sequelize": "^6.29.0",
"socket.io": "^4.7.2",
"socket.io-client": "^4.7.2",
"socket.io-client": "^4.8.1",
"swagger-autogen": "^2.23.5",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0",

View File

@@ -28,6 +28,7 @@ function Activity() {
const [showLibraryFilters, setShowLibraryFilters] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [sorting, setSorting] = useState({ column: "ActivityDateInserted", desc: true });
const [filterParams, setFilterParams] = useState([]);
const [isBusy, setIsBusy] = useState(false);
const handlePageChange = (newPage) => {
@@ -38,6 +39,10 @@ function Activity() {
setSorting({ column: sort.column, desc: sort.desc });
};
const onFilterChange = (filter) => {
setFilterParams(filter);
};
function setItemLimit(limit) {
setItemCount(parseInt(limit));
localStorage.setItem("PREF_ACTIVITY_ItemCount", limit);
@@ -87,9 +92,21 @@ function Activity() {
const fetchHistory = () => {
setIsBusy(true);
const url = `/api/getHistory?size=${itemCount}&page=${currentPage}&search=${debouncedSearchQuery}&sort=${sorting.column}&desc=${sorting.desc}`;
const url = `/api/getHistory`;
if (filterParams) {
console.log(JSON.stringify(filterParams));
}
axios
.get(url, {
params: {
size: itemCount,
page: currentPage,
search: debouncedSearchQuery,
sort: sorting.column,
desc: sorting.desc,
filters: filterParams != undefined ? JSON.stringify(filterParams) : null,
},
headers: {
Authorization: `Bearer ${config.token}`,
"Content-Type": "application/json",
@@ -143,7 +160,8 @@ function Activity() {
(data.size && data.size !== itemCount) ||
(data?.search ?? "") !== debouncedSearchQuery.trim() ||
(data?.sort ?? "") !== sorting.column ||
(data?.desc ?? true) !== sorting.desc
(data?.desc ?? true) !== sorting.desc ||
JSON.stringify(data?.filters ?? []) !== JSON.stringify(filterParams ?? [])
) {
fetchHistory();
fetchLibraries();
@@ -156,7 +174,7 @@ function Activity() {
const intervalId = setInterval(fetchHistory, 60000 * 60);
return () => clearInterval(intervalId);
}, [data, config, itemCount, currentPage, debouncedSearchQuery, sorting]);
}, [data, config, itemCount, currentPage, debouncedSearchQuery, sorting, filterParams]);
if (!data) {
return <Loading />;
@@ -279,6 +297,7 @@ function Activity() {
itemCount={itemCount}
onPageChange={handlePageChange}
onSortChange={onSortChange}
onFilterChange={onFilterChange}
pageCount={data.pages}
isBusy={isBusy}
/>

View File

@@ -60,8 +60,6 @@ const token = localStorage.getItem("token");
export default function ActivityTable(props) {
const twelve_hr = JSON.parse(localStorage.getItem("12hr"));
const [data, setData] = React.useState(props.data ?? []);
const uniqueUserNames = [...new Set(data.map((item) => item.UserName))];
const uniqueClients = [...new Set(data.map((item) => item.Client))];
const pages = props.pageCount || 1;
const isBusy = props.isBusy;
@@ -72,6 +70,8 @@ export default function ActivityTable(props) {
});
const [sorting, setSorting] = React.useState([{ id: "Date", desc: true }]);
const [columnFilters, setColumnFilters] = React.useState([]);
const [modalState, setModalState] = React.useState(false);
const [modalData, setModalData] = React.useState();
@@ -152,8 +152,6 @@ export default function ActivityTable(props) {
{
accessorKey: "UserName",
header: i18next.t("USER"),
filterVariant: "select",
filterSelectOptions: uniqueUserNames,
Cell: ({ row }) => {
row = row.original;
return (
@@ -207,8 +205,6 @@ export default function ActivityTable(props) {
{
accessorKey: "Client",
header: i18next.t("ACTIVITY_TABLE.CLIENT"),
filterVariant: "select",
filterSelectOptions: uniqueClients,
Cell: ({ row }) => {
row = row.original;
return (
@@ -246,8 +242,8 @@ export default function ActivityTable(props) {
accessorKey: "PlaybackDuration",
header: i18next.t("ACTIVITY_TABLE.TOTAL_PLAYBACK"),
minSize: 200,
filterFn: (row, id, filterValue) => formatTotalWatchTime(row.getValue(id)).startsWith(filterValue),
// filterFn: (row, id, filterValue) => formatTotalWatchTime(row.getValue(id)).startsWith(filterValue),
filterVariant: "range",
Cell: ({ cell }) => <span>{formatTotalWatchTime(cell.getValue())}</span>,
},
{
@@ -276,6 +272,35 @@ export default function ActivityTable(props) {
});
};
const handleFilteringChange = (updater) => {
setColumnFilters((old) => {
const newFilterState = typeof updater === "function" ? updater(old) : updater;
const modifiedFilterState = newFilterState.map((filter) => ({ ...filter }));
modifiedFilterState.map((filter) => {
filter.field = fieldMap.find((field) => field.header == filter.id)?.accessorKey ?? filter.id;
delete filter.id;
if (Array.isArray(filter.value)) {
filter.min = filter.value[0];
filter.max = filter.value[1];
delete filter.value;
} else {
const val = filter.value;
delete filter.value;
filter.value = val;
}
return filter;
});
if (props.onFilterChange) {
props.onFilterChange(modifiedFilterState);
}
return newFilterState;
});
};
useEffect(() => {
setData(props.data);
}, [props.data]);
@@ -300,8 +325,10 @@ export default function ActivityTable(props) {
enableExpandAll: false,
enableExpanding: true,
enableDensityToggle: false,
enableFilters: false,
enableFilters: true,
manualFiltering: true,
onSortingChange: handleSortingChange,
onColumnFiltersChange: handleFilteringChange,
enableTopToolbar: Object.keys(rowSelection).length > 0,
manualPagination: true,
manualSorting: true,
@@ -378,7 +405,7 @@ export default function ActivityTable(props) {
},
},
},
state: { rowSelection, pagination, sorting },
state: { rowSelection, pagination, sorting, columnFilters },
filterFromLeafRows: true,
getSubRows: (row) => {
if (Array.isArray(row.results) && row.results.length == 1) {

View File

@@ -16,6 +16,7 @@ function ItemActivity(props) {
const [config, setConfig] = useState();
const [currentPage, setCurrentPage] = useState(1);
const [sorting, setSorting] = useState({ column: "ActivityDateInserted", desc: true });
const [filterParams, setFilterParams] = useState([]);
const [isBusy, setIsBusy] = useState(false);
const handlePageChange = (newPage) => {
@@ -26,6 +27,10 @@ function ItemActivity(props) {
setSorting({ column: sort.column, desc: sort.desc });
};
const onFilterChange = (filter) => {
setFilterParams(filter);
};
function setItemLimit(limit) {
setItemCount(parseInt(limit));
localStorage.setItem("PREF_ACTIVITY_ItemCount", limit);
@@ -59,7 +64,7 @@ function ItemActivity(props) {
try {
setIsBusy(true);
const itemData = await axios.post(
`/api/getItemHistory?size=${itemCount}&page=${currentPage}&search=${debouncedSearchQuery}&sort=${sorting.column}&desc=${sorting.desc}`,
`/api/getItemHistory`,
{
itemid: props.itemid,
},
@@ -68,6 +73,14 @@ function ItemActivity(props) {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
params: {
size: itemCount,
page: currentPage,
search: debouncedSearchQuery,
sort: sorting.column,
desc: sorting.desc,
filters: filterParams != undefined ? JSON.stringify(filterParams) : null,
},
}
);
setData(itemData.data);
@@ -83,14 +96,15 @@ function ItemActivity(props) {
(data.size && data.size !== itemCount) ||
(data?.search ?? "") !== debouncedSearchQuery.trim() ||
(data?.sort ?? "") !== sorting.column ||
(data?.desc ?? true) !== sorting.desc
(data?.desc ?? true) !== sorting.desc ||
JSON.stringify(data?.filters ?? []) !== JSON.stringify(filterParams ?? [])
) {
fetchData();
}
const intervalId = setInterval(fetchData, 60000 * 5);
return () => clearInterval(intervalId);
}, [data, props.itemid, token, itemCount, currentPage, debouncedSearchQuery, sorting]);
}, [data, props.itemid, token, itemCount, currentPage, debouncedSearchQuery, sorting, filterParams]);
if (!data || !data.results) {
return <></>;
@@ -177,6 +191,7 @@ function ItemActivity(props) {
itemCount={itemCount}
onPageChange={handlePageChange}
onSortChange={onSortChange}
onFilterChange={onFilterChange}
pageCount={data.pages}
isBusy={isBusy}
/>

View File

@@ -19,6 +19,7 @@ function LibraryActivity(props) {
const [config, setConfig] = useState();
const [currentPage, setCurrentPage] = useState(1);
const [sorting, setSorting] = useState({ column: "ActivityDateInserted", desc: true });
const [filterParams, setFilterParams] = useState([]);
const [isBusy, setIsBusy] = useState(false);
const handlePageChange = (newPage) => {
@@ -29,6 +30,10 @@ function LibraryActivity(props) {
setSorting({ column: sort.column, desc: sort.desc });
};
const onFilterChange = (filter) => {
setFilterParams(filter);
};
function setItemLimit(limit) {
setItemCount(parseInt(limit));
localStorage.setItem("PREF_LIBRARY_ACTIVITY_ItemCount", limit);
@@ -66,7 +71,7 @@ function LibraryActivity(props) {
try {
setIsBusy(true);
const libraryData = await axios.post(
`/api/getLibraryHistory?size=${itemCount}&page=${currentPage}&search=${debouncedSearchQuery}&sort=${sorting.column}&desc=${sorting.desc}`,
`/api/getLibraryHistory`,
{
libraryid: props.LibraryId,
},
@@ -75,6 +80,14 @@ function LibraryActivity(props) {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
params: {
size: itemCount,
page: currentPage,
search: debouncedSearchQuery,
sort: sorting.column,
desc: sorting.desc,
filters: filterParams != undefined ? JSON.stringify(filterParams) : null,
},
}
);
setData(libraryData.data);
@@ -90,14 +103,15 @@ function LibraryActivity(props) {
(data.size && data.size !== itemCount) ||
(data?.search ?? "") !== debouncedSearchQuery.trim() ||
(data?.sort ?? "") !== sorting.column ||
(data?.desc ?? true) !== sorting.desc
(data?.desc ?? true) !== sorting.desc ||
JSON.stringify(data?.filters ?? []) !== JSON.stringify(filterParams ?? [])
) {
fetchData();
}
const intervalId = setInterval(fetchData, 60000 * 5);
return () => clearInterval(intervalId);
}, [data, props.LibraryId, token, itemCount, currentPage, debouncedSearchQuery, sorting]);
}, [data, props.LibraryId, token, itemCount, currentPage, debouncedSearchQuery, sorting, filterParams]);
if (!data || !data.results) {
return <></>;
@@ -183,6 +197,7 @@ function LibraryActivity(props) {
itemCount={itemCount}
onPageChange={handlePageChange}
onSortChange={onSortChange}
onFilterChange={onFilterChange}
pageCount={data.pages}
isBusy={isBusy}
/>

View File

@@ -23,6 +23,7 @@ function UserActivity(props) {
const [config, setConfig] = useState();
const [currentPage, setCurrentPage] = useState(1);
const [sorting, setSorting] = useState({ column: "ActivityDateInserted", desc: true });
const [filterParams, setFilterParams] = useState([]);
const [isBusy, setIsBusy] = useState(false);
function setItemLimit(limit) {
@@ -78,12 +79,16 @@ function UserActivity(props) {
setSorting({ column: sort.column, desc: sort.desc });
};
const onFilterChange = (filter) => {
setFilterParams(filter);
};
useEffect(() => {
const fetchHistory = async () => {
try {
setIsBusy(true);
const itemData = await axios.post(
`/api/getUserHistory?size=${itemCount}&page=${currentPage}&search=${debouncedSearchQuery}&sort=${sorting.column}&desc=${sorting.desc}`,
`/api/getUserHistory`,
{
userid: props.UserId,
},
@@ -92,6 +97,14 @@ function UserActivity(props) {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
params: {
size: itemCount,
page: currentPage,
search: debouncedSearchQuery,
sort: sorting.column,
desc: sorting.desc,
filters: filterParams != undefined ? JSON.stringify(filterParams) : null,
},
}
);
setData(itemData.data);
@@ -132,7 +145,8 @@ function UserActivity(props) {
(data.size && data.size !== itemCount) ||
(data?.search ?? "") !== debouncedSearchQuery.trim() ||
(data?.sort ?? "") !== sorting.column ||
(data?.desc ?? true) !== sorting.desc
(data?.desc ?? true) !== sorting.desc ||
JSON.stringify(data?.filters ?? []) !== JSON.stringify(filterParams ?? [])
) {
fetchHistory();
}
@@ -141,7 +155,7 @@ function UserActivity(props) {
const intervalId = setInterval(fetchHistory, 60000 * 5);
return () => clearInterval(intervalId);
}, [props.UserId, token, itemCount, currentPage, debouncedSearchQuery, sorting]);
}, [props.UserId, token, itemCount, currentPage, debouncedSearchQuery, sorting, filterParams]);
if (!data || !data.results) {
return <></>;
@@ -248,6 +262,7 @@ function UserActivity(props) {
itemCount={itemCount}
onPageChange={handlePageChange}
onSortChange={onSortChange}
onFilterChange={onFilterChange}
pageCount={data.pages}
isBusy={isBusy}
/>