diff --git a/backend/routes/api.js b/backend/routes/api.js index eacaf04..a302c12 100644 --- a/backend/routes/api.js +++ b/backend/routes/api.js @@ -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); diff --git a/backend/swagger.json b/backend/swagger.json index d994fa4..28fda5c 100644 --- a/backend/swagger.json +++ b/backend/swagger.json @@ -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": [ diff --git a/package-lock.json b/package-lock.json index 2d341df..a6230ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" } diff --git a/package.json b/package.json index 3b74515..b279a71 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/pages/activity.jsx b/src/pages/activity.jsx index 2b40089..59ffa0d 100644 --- a/src/pages/activity.jsx +++ b/src/pages/activity.jsx @@ -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 ; @@ -279,6 +297,7 @@ function Activity() { itemCount={itemCount} onPageChange={handlePageChange} onSortChange={onSortChange} + onFilterChange={onFilterChange} pageCount={data.pages} isBusy={isBusy} /> diff --git a/src/pages/components/activity/activity-table.jsx b/src/pages/components/activity/activity-table.jsx index 8afbb04..65ed9c8 100644 --- a/src/pages/components/activity/activity-table.jsx +++ b/src/pages/components/activity/activity-table.jsx @@ -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 }) => {formatTotalWatchTime(cell.getValue())}, }, { @@ -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) { diff --git a/src/pages/components/item-info/item-activity.jsx b/src/pages/components/item-info/item-activity.jsx index 4cbf38a..ebb52f6 100644 --- a/src/pages/components/item-info/item-activity.jsx +++ b/src/pages/components/item-info/item-activity.jsx @@ -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} /> diff --git a/src/pages/components/library/library-activity.jsx b/src/pages/components/library/library-activity.jsx index 3e5b7d0..0139f24 100644 --- a/src/pages/components/library/library-activity.jsx +++ b/src/pages/components/library/library-activity.jsx @@ -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} /> diff --git a/src/pages/components/user-info/user-activity.jsx b/src/pages/components/user-info/user-activity.jsx index d46dca8..ccdc360 100644 --- a/src/pages/components/user-info/user-activity.jsx +++ b/src/pages/components/user-info/user-activity.jsx @@ -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} />