mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
added pagination to backend history endpoints
changed frontend activity elements to use server-side paging Bug in MaterialReactTable causing page index to reset was found aswell as a bug that prevents expanding subrows when using manual pagination. Solution is WIP Added a busy loader to activity tables
This commit is contained in:
106
backend/classes/db-helper.js
Normal file
106
backend/classes/db-helper.js
Normal file
@@ -0,0 +1,106 @@
|
||||
const { pool } = require("../db.js");
|
||||
|
||||
function wrapField(field) {
|
||||
if (field === "*") {
|
||||
return field;
|
||||
}
|
||||
if (field.includes(" as ")) {
|
||||
const [column, alias] = field.split(" as ");
|
||||
return `${column
|
||||
.split(".")
|
||||
.map((part) => (part == "*" ? part : `"${part}"`))
|
||||
.join(".")} as "${alias}"`;
|
||||
}
|
||||
return field
|
||||
.split(".")
|
||||
.map((part) => (part == "*" ? part : `"${part}"`))
|
||||
.join(".");
|
||||
}
|
||||
|
||||
function buildWhereClause(conditions) {
|
||||
if (!Array.isArray(conditions)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return conditions
|
||||
.map((condition, index) => {
|
||||
if (Array.isArray(condition)) {
|
||||
return `(${buildWhereClause(condition)})`;
|
||||
} else if (typeof condition === "object") {
|
||||
const { column, operator, value, type } = condition;
|
||||
const conjunction = index === 0 ? "" : type ? type.toUpperCase() : "AND";
|
||||
return `${conjunction} ${wrapField(column)} ${operator} '${value}'`;
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.join(" ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
async function query({
|
||||
select = ["*"],
|
||||
table,
|
||||
alias,
|
||||
joins = [],
|
||||
where = [],
|
||||
order_by = "Id",
|
||||
sort_order = "desc",
|
||||
pageNumber = 1,
|
||||
pageSize = 50,
|
||||
}) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
// Build the base query
|
||||
let countQuery = `SELECT COUNT(*) FROM ${wrapField(table)} AS ${wrapField(alias)}`;
|
||||
let query = `SELECT ${select.map(wrapField).join(", ")} FROM ${wrapField(table)} AS ${wrapField(alias)}`;
|
||||
|
||||
// Add joins
|
||||
joins.forEach((join) => {
|
||||
const joinConditions = join.conditions
|
||||
.map((condition, index) => {
|
||||
const conjunction = index === 0 ? "" : condition.type ? condition.type.toUpperCase() : "AND";
|
||||
return `${conjunction} ${wrapField(condition.first)} ${condition.operator} ${wrapField(condition.second)}`;
|
||||
})
|
||||
.join(" ");
|
||||
const joinQuery = ` ${join.type.toUpperCase()} JOIN ${join.table} AS ${join.alias} ON ${joinConditions}`;
|
||||
query += joinQuery;
|
||||
countQuery += joinQuery;
|
||||
});
|
||||
|
||||
// Add where conditions
|
||||
const whereClause = buildWhereClause(where);
|
||||
if (whereClause) {
|
||||
query += ` WHERE ${whereClause}`;
|
||||
countQuery += ` WHERE ${whereClause}`;
|
||||
}
|
||||
|
||||
// Add order by and pagination
|
||||
query += ` ORDER BY ${wrapField(order_by)} ${sort_order}`;
|
||||
query += ` LIMIT ${pageSize} OFFSET ${(pageNumber - 1) * pageSize}`;
|
||||
|
||||
// Execute the query
|
||||
const result = await client.query(query);
|
||||
|
||||
// Count total rows
|
||||
const countResult = await client.query(countQuery);
|
||||
const totalRows = parseInt(countResult.rows[0].count, 10);
|
||||
|
||||
// Return the structured response
|
||||
return {
|
||||
pages: Math.ceil(totalRows / pageSize),
|
||||
results: result.rows,
|
||||
};
|
||||
} catch (error) {
|
||||
// console.timeEnd("queryWithPagingAndJoins");
|
||||
console.error("Error occurred while executing query:", error.message);
|
||||
return {
|
||||
pages: 0,
|
||||
results: [],
|
||||
};
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
module.exports = {
|
||||
query,
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
const { Pool } = require("pg");
|
||||
const pgp = require("pg-promise")();
|
||||
const { update_query: update_query_map } = require("./models/bulk_insert_update_handler");
|
||||
const moment = require("moment");
|
||||
|
||||
const _POSTGRES_USER = process.env.POSTGRES_USER;
|
||||
const _POSTGRES_PASSWORD = process.env.POSTGRES_PASSWORD;
|
||||
@@ -20,6 +19,9 @@ const pool = new Pool({
|
||||
database: _POSTGRES_DATABASE,
|
||||
password: _POSTGRES_PASSWORD,
|
||||
port: _POSTGRES_PORT,
|
||||
max: 20, // Maximum number of connections in the pool
|
||||
idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
|
||||
connectionTimeoutMillis: 2000, // Return an error after 2 seconds if connection could not be established
|
||||
});
|
||||
|
||||
pool.on("error", (err, client) => {
|
||||
@@ -163,6 +165,7 @@ async function querySingle(sql, params) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
pool: pool,
|
||||
query: query,
|
||||
deleteBulk: deleteBulk,
|
||||
insertBulk: insertBulk,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
const express = require("express");
|
||||
|
||||
const db = require("../db");
|
||||
const dbHelper = require("../classes/db-helper");
|
||||
|
||||
const pgp = require("pg-promise")();
|
||||
const { randomUUID } = require("crypto");
|
||||
|
||||
@@ -16,9 +18,8 @@ const { tables } = require("../global/backup_tables");
|
||||
const router = express.Router();
|
||||
|
||||
//Functions
|
||||
function groupActivity(rows, limit) {
|
||||
function groupActivity(rows) {
|
||||
const groupedResults = {};
|
||||
let objectsAdded = 0
|
||||
rows.every((row) => {
|
||||
const key = row.NowPlayingItemId + row.EpisodeId + row.UserId;
|
||||
if (groupedResults[key]) {
|
||||
@@ -35,9 +36,8 @@ function groupActivity(rows, limit) {
|
||||
results: [],
|
||||
};
|
||||
groupedResults[key].results.push(row);
|
||||
objectsAdded++;
|
||||
}
|
||||
return (objectsAdded < limit);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Update GroupedResults with playbackDurationSum
|
||||
@@ -1088,22 +1088,41 @@ router.post("/setExcludedBackupTable", async (req, res) => {
|
||||
|
||||
//DB Queries - History
|
||||
router.get("/getHistory", async (req, res) => {
|
||||
const { limit = 50 } = req.query;
|
||||
const { size = 50, page = 1 } = req.query;
|
||||
|
||||
try {
|
||||
const { rows } = await db.query(`
|
||||
SELECT a.*, e."IndexNumber" "EpisodeNumber",e."ParentIndexNumber" "SeasonNumber" , i."ParentId"
|
||||
FROM jf_playback_activity a
|
||||
left join jf_library_episodes e
|
||||
on a."EpisodeId"=e."EpisodeId"
|
||||
and a."SeasonId"=e."SeasonId"
|
||||
left join jf_library_items i
|
||||
on i."Id"=a."NowPlayingItemId" or e."SeriesId"=i."Id"
|
||||
order by a."ActivityDateInserted" desc`);
|
||||
const result = await dbHelper.query({
|
||||
select: ["a.*", "e.IndexNumber as EpisodeNumber", "e.ParentIndexNumber as SeasonNumber", "i.ParentId"],
|
||||
table: "jf_playback_activity",
|
||||
alias: "a",
|
||||
joins: [
|
||||
{
|
||||
type: "left",
|
||||
table: "jf_library_episodes",
|
||||
alias: "e",
|
||||
conditions: [
|
||||
{ first: "a.EpisodeId", operator: "=", second: "e.EpisodeId", type: "and" },
|
||||
{ first: "a.SeasonId", operator: "=", second: "e.SeasonId", type: "and" },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "left",
|
||||
table: "jf_library_items",
|
||||
alias: "i",
|
||||
conditions: [
|
||||
{ first: "i.Id", operator: "=", second: "a.NowPlayingItemId", type: "or" },
|
||||
{ first: "e.SeriesId", operator: "=", second: "i.Id", type: "or" },
|
||||
],
|
||||
},
|
||||
],
|
||||
order_by: "a.ActivityDateInserted",
|
||||
sort_order: "desc",
|
||||
pageNumber: page,
|
||||
pageSize: size,
|
||||
});
|
||||
|
||||
const groupedResults = groupActivity(rows, limit);
|
||||
|
||||
res.send(Object.values(groupedResults));
|
||||
const groupedResults = groupActivity(result.results);
|
||||
res.send({ current_page: page, pages: result.pages, results: Object.values(groupedResults) });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
@@ -1111,7 +1130,7 @@ router.get("/getHistory", async (req, res) => {
|
||||
|
||||
router.post("/getLibraryHistory", async (req, res) => {
|
||||
try {
|
||||
const { limit = 50 } = req.query;
|
||||
const { size = 50, page = 1 } = req.query;
|
||||
const { libraryid } = req.body;
|
||||
|
||||
if (libraryid === undefined) {
|
||||
@@ -1120,20 +1139,36 @@ router.post("/getLibraryHistory", async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { rows } = await db.query(
|
||||
`select a.* , e."IndexNumber" "EpisodeNumber",e."ParentIndexNumber" "SeasonNumber"
|
||||
from jf_playback_activity a
|
||||
join jf_library_items i
|
||||
on i."Id"=a."NowPlayingItemId"
|
||||
left join jf_library_episodes e
|
||||
on a."EpisodeId"=e."EpisodeId"
|
||||
and a."SeasonId"=e."SeasonId"
|
||||
where i."ParentId"=$1
|
||||
order by a."ActivityDateInserted" desc`,
|
||||
[libraryid]
|
||||
);
|
||||
const groupedResults = groupActivity(rows, limit);
|
||||
res.send(Object.values(groupedResults));
|
||||
const result = await dbHelper.query({
|
||||
select: ["a.*", "e.IndexNumber as EpisodeNumber", "e.ParentIndexNumber as SeasonNumber", "i.ParentId"],
|
||||
table: "jf_playback_activity",
|
||||
alias: "a",
|
||||
joins: [
|
||||
{
|
||||
type: "inner",
|
||||
table: "jf_library_items",
|
||||
alias: "i",
|
||||
conditions: [{ first: "i.Id", operator: "=", second: "a.NowPlayingItemId" }],
|
||||
},
|
||||
{
|
||||
type: "left",
|
||||
table: "jf_library_episodes",
|
||||
alias: "e",
|
||||
conditions: [
|
||||
{ first: "a.EpisodeId", operator: "=", second: "e.EpisodeId" },
|
||||
{ first: "a.SeasonId", operator: "=", second: "e.SeasonId" },
|
||||
],
|
||||
},
|
||||
],
|
||||
where: [{ column: "i.ParentId", operator: "=", value: libraryid }],
|
||||
order_by: "ActivityDateInserted",
|
||||
sort_order: "desc",
|
||||
pageNumber: page,
|
||||
pageSize: size,
|
||||
});
|
||||
|
||||
const groupedResults = groupActivity(result.results);
|
||||
res.send({ current_page: page, pages: result.pages, results: Object.values(groupedResults) });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(503);
|
||||
@@ -1143,6 +1178,7 @@ router.post("/getLibraryHistory", async (req, res) => {
|
||||
|
||||
router.post("/getItemHistory", async (req, res) => {
|
||||
try {
|
||||
const { size = 50, page = 1 } = req.query;
|
||||
const { itemid } = req.body;
|
||||
|
||||
if (itemid === undefined) {
|
||||
@@ -1151,24 +1187,47 @@ router.post("/getItemHistory", async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { rows } = await db.query(
|
||||
`select a.*, e."IndexNumber" "EpisodeNumber",e."ParentIndexNumber" "SeasonNumber"
|
||||
from jf_playback_activity a
|
||||
left join jf_library_episodes e
|
||||
on a."EpisodeId"=e."EpisodeId"
|
||||
and a."SeasonId"=e."SeasonId"
|
||||
where
|
||||
(a."EpisodeId"=$1 OR a."SeasonId"=$1 OR a."NowPlayingItemId"=$1)
|
||||
order by a."ActivityDateInserted" desc;`,
|
||||
[itemid]
|
||||
);
|
||||
const result = await dbHelper.query({
|
||||
select: ["a.*", "e.IndexNumber as EpisodeNumber", "e.ParentIndexNumber as SeasonNumber"],
|
||||
table: "jf_playback_activity",
|
||||
alias: "a",
|
||||
joins: [
|
||||
{
|
||||
type: "left",
|
||||
table: "jf_library_episodes",
|
||||
alias: "e",
|
||||
conditions: [
|
||||
{ first: "a.EpisodeId", operator: "=", second: "e.EpisodeId" },
|
||||
{ first: "a.SeasonId", operator: "=", second: "e.SeasonId" },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "left",
|
||||
table: "jf_library_items",
|
||||
alias: "i",
|
||||
conditions: [
|
||||
{ first: "i.Id", operator: "=", second: "a.NowPlayingItemId", type: "or" },
|
||||
{ first: "e.SeriesId", operator: "=", second: "i.Id", type: "or" },
|
||||
],
|
||||
},
|
||||
],
|
||||
where: [
|
||||
{ column: "a.EpisodeId", operator: "=", value: itemid, type: "or" },
|
||||
{ column: "a.SeasonId", operator: "=", value: itemid, type: "or" },
|
||||
{ column: "a.NowPlayingItemId", operator: "=", value: itemid, type: "or" },
|
||||
],
|
||||
order_by: "ActivityDateInserted",
|
||||
sort_order: "desc",
|
||||
pageNumber: page,
|
||||
pageSize: size,
|
||||
});
|
||||
|
||||
const groupedResults = rows.map((item) => ({
|
||||
...item,
|
||||
results: [],
|
||||
}));
|
||||
// const groupedResults = rows.map((item) => ({
|
||||
// ...item,
|
||||
// results: [],
|
||||
// }));
|
||||
|
||||
res.send(groupedResults);
|
||||
res.send({ current_page: page, pages: result.pages, results: result.results });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(503);
|
||||
@@ -1178,7 +1237,7 @@ router.post("/getItemHistory", async (req, res) => {
|
||||
|
||||
router.post("/getUserHistory", async (req, res) => {
|
||||
try {
|
||||
const { limit = 50 } = req.query;
|
||||
const { size = 50, page = 1 } = req.query;
|
||||
const { userid } = req.body;
|
||||
|
||||
if (userid === undefined) {
|
||||
@@ -1187,22 +1246,38 @@ router.post("/getUserHistory", async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { rows } = await db.query(
|
||||
`select a.*, e."IndexNumber" "EpisodeNumber",e."ParentIndexNumber" "SeasonNumber" , i."ParentId"
|
||||
from jf_playback_activity a
|
||||
left join jf_library_episodes e
|
||||
on a."EpisodeId"=e."EpisodeId"
|
||||
and a."SeasonId"=e."SeasonId"
|
||||
left join jf_library_items i
|
||||
on i."Id"=a."NowPlayingItemId" or e."SeriesId"=i."Id"
|
||||
where a."UserId"=$1
|
||||
order by a."ActivityDateInserted" desc`,
|
||||
[userid]
|
||||
);
|
||||
const result = await dbHelper.query({
|
||||
select: ["a.*", "e.IndexNumber as EpisodeNumber", "e.ParentIndexNumber as SeasonNumber", "i.ParentId"],
|
||||
table: "jf_playback_activity",
|
||||
alias: "a",
|
||||
joins: [
|
||||
{
|
||||
type: "left",
|
||||
table: "jf_library_episodes",
|
||||
alias: "e",
|
||||
conditions: [
|
||||
{ first: "a.EpisodeId", operator: "=", second: "e.EpisodeId" },
|
||||
{ first: "a.SeasonId", operator: "=", second: "e.SeasonId" },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "left",
|
||||
table: "jf_library_items",
|
||||
alias: "i",
|
||||
conditions: [
|
||||
{ first: "i.Id", operator: "=", second: "a.NowPlayingItemId", type: "or" },
|
||||
{ first: "e.SeriesId", operator: "=", second: "i.Id", type: "or" },
|
||||
],
|
||||
},
|
||||
],
|
||||
where: [{ column: "a.UserId", operator: "=", value: userid }],
|
||||
order_by: "ActivityDateInserted",
|
||||
sort_order: "desc",
|
||||
pageNumber: page,
|
||||
pageSize: size,
|
||||
});
|
||||
|
||||
const groupedResults = groupActivity(rows, limit);
|
||||
|
||||
res.send(Object.values(groupedResults));
|
||||
res.send({ current_page: page, pages: result.pages, results: result.results });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(503);
|
||||
|
||||
717
package-lock.json
generated
717
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jfstat",
|
||||
"version": "1.1.2",
|
||||
"version": "1.1.3",
|
||||
"private": true,
|
||||
"main": "src/index.jsx",
|
||||
"scripts": {
|
||||
@@ -15,12 +15,12 @@
|
||||
"start": "cd backend && node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.14",
|
||||
"@mui/material": "^5.15.14",
|
||||
"@mui/x-data-grid": "^6.2.1",
|
||||
"@mui/x-date-pickers": "^7.0.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@mui/icons-material": "^6.3.0",
|
||||
"@mui/material": "^6.3.0",
|
||||
"@mui/x-data-grid": "^7.23.3",
|
||||
"@mui/x-date-pickers": "^7.23.3",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
@@ -48,7 +48,7 @@
|
||||
"i18next-fs-backend": "^2.3.1",
|
||||
"i18next-http-backend": "^2.4.3",
|
||||
"knex": "^2.4.2",
|
||||
"material-react-table": "^2.12.1",
|
||||
"material-react-table": "^3.1.0",
|
||||
"moment": "^2.29.4",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"passport": "^0.6.0",
|
||||
|
||||
@@ -25,6 +25,12 @@ function Activity() {
|
||||
);
|
||||
const [libraries, setLibraries] = useState([]);
|
||||
const [showLibraryFilters, setShowLibraryFilters] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
|
||||
const handlePageChange = (newPage) => {
|
||||
setCurrentPage(newPage);
|
||||
};
|
||||
|
||||
function setItemLimit(limit) {
|
||||
setItemCount(limit);
|
||||
@@ -64,7 +70,8 @@ function Activity() {
|
||||
};
|
||||
|
||||
const fetchHistory = () => {
|
||||
const url = `/api/getHistory`;
|
||||
setIsBusy(true);
|
||||
const url = `/api/getHistory?size=${itemCount}&page=${currentPage}`;
|
||||
axios
|
||||
.get(url, {
|
||||
headers: {
|
||||
@@ -74,9 +81,11 @@ function Activity() {
|
||||
})
|
||||
.then((data) => {
|
||||
setData(data.data);
|
||||
setIsBusy(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
setIsBusy(false);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -111,9 +120,11 @@ function Activity() {
|
||||
});
|
||||
};
|
||||
|
||||
if (!data && config) {
|
||||
fetchHistory();
|
||||
fetchLibraries();
|
||||
if (config) {
|
||||
if (!data || data.current_page !== currentPage) {
|
||||
fetchHistory();
|
||||
fetchLibraries();
|
||||
}
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
@@ -122,7 +133,7 @@ function Activity() {
|
||||
|
||||
const intervalId = setInterval(fetchHistory, 60000 * 60);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config]);
|
||||
}, [data, config, itemCount, currentPage]);
|
||||
|
||||
if (!data) {
|
||||
return <Loading />;
|
||||
@@ -145,10 +156,10 @@ function Activity() {
|
||||
);
|
||||
}
|
||||
|
||||
let filteredData = data;
|
||||
let filteredData = data.results;
|
||||
|
||||
if (searchQuery) {
|
||||
filteredData = data.filter((item) =>
|
||||
filteredData = data.results.filter((item) =>
|
||||
(!item.SeriesName ? item.NowPlayingItemName : item.SeriesName + " - " + item.NowPlayingItemName)
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase())
|
||||
@@ -240,7 +251,13 @@ function Activity() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="Activity">
|
||||
<ActivityTable data={filteredData} itemCount={itemCount} />
|
||||
<ActivityTable
|
||||
data={filteredData}
|
||||
itemCount={itemCount}
|
||||
onPageChange={handlePageChange}
|
||||
pageCount={data.pages}
|
||||
isBusy={isBusy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ import StreamInfo from "./stream_info";
|
||||
import "../../css/activity/activity-table.css";
|
||||
import i18next from "i18next";
|
||||
import IpInfoModal from "../ip-info";
|
||||
// import Loading from "../general/loading";
|
||||
import BusyLoader from "../general/busyLoader.jsx";
|
||||
import { MRT_TablePagination, MaterialReactTable, useMaterialReactTable } from "material-react-table";
|
||||
import { Box, ThemeProvider, Typography, createTheme } from "@mui/material";
|
||||
|
||||
@@ -34,7 +34,9 @@ function formatTotalWatchTime(seconds) {
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
timeString += `${minutes} ${minutes === 1 ? i18next.t("UNITS.MINUTE").toLowerCase() : i18next.t("UNITS.MINUTES").toLowerCase()} `;
|
||||
timeString += `${minutes} ${
|
||||
minutes === 1 ? i18next.t("UNITS.MINUTE").toLowerCase() : i18next.t("UNITS.MINUTES").toLowerCase()
|
||||
} `;
|
||||
}
|
||||
|
||||
if (remainingSeconds > 0) {
|
||||
@@ -60,16 +62,29 @@ export default function ActivityTable(props) {
|
||||
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;
|
||||
|
||||
const [rowSelection, setRowSelection] = React.useState({});
|
||||
const [pagination, setPagination] = React.useState({
|
||||
pageSize: 10,
|
||||
pageIndex: 0,
|
||||
pageSize: 10, //customize the default page size
|
||||
});
|
||||
|
||||
const [modalState, setModalState] = React.useState(false);
|
||||
const [modalData, setModalData] = React.useState();
|
||||
|
||||
const handlePageChange = (updater) => {
|
||||
setPagination((old) => {
|
||||
const newPaginationState = typeof updater === "function" ? updater(old) : updater;
|
||||
console.log(newPaginationState);
|
||||
const newPage = newPaginationState.pageIndex; // MaterialReactTable uses 0-based index
|
||||
if (props.onPageChange) {
|
||||
props.onPageChange(newPage + 1);
|
||||
}
|
||||
return newPaginationState;
|
||||
});
|
||||
};
|
||||
//IP MODAL
|
||||
|
||||
const ipv4Regex = new RegExp(
|
||||
@@ -266,10 +281,15 @@ export default function ActivityTable(props) {
|
||||
enableExpanding: true,
|
||||
enableDensityToggle: false,
|
||||
enableTopToolbar: Object.keys(rowSelection).length > 0,
|
||||
manualPagination: true,
|
||||
autoResetPageIndex: false,
|
||||
initialState: {
|
||||
expanded: false,
|
||||
showGlobalFilter: true,
|
||||
pagination: { pageSize: 10, pageIndex: 0 },
|
||||
pagination: {
|
||||
pageSize: 10,
|
||||
pageIndex: 0,
|
||||
},
|
||||
sorting: [
|
||||
{
|
||||
id: "Date",
|
||||
@@ -277,6 +297,7 @@ export default function ActivityTable(props) {
|
||||
},
|
||||
],
|
||||
},
|
||||
pageCount: pages,
|
||||
showAlertBanner: false,
|
||||
enableHiding: false,
|
||||
enableFullScreenToggle: false,
|
||||
@@ -343,7 +364,7 @@ export default function ActivityTable(props) {
|
||||
return row.results;
|
||||
},
|
||||
paginateExpandedRows: false,
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePageChange,
|
||||
getRowId: (row) => row.Id,
|
||||
muiExpandButtonProps: ({ row }) => ({
|
||||
children: row.getIsExpanded() ? <IndeterminateCircleFillIcon /> : <AddCircleFillIcon />,
|
||||
@@ -417,6 +438,8 @@ export default function ActivityTable(props) {
|
||||
|
||||
return (
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
{isBusy && <BusyLoader />}
|
||||
|
||||
<IpInfoModal show={ipModalVisible} onHide={() => setIPModalVisible(false)} ipAddress={ipAddressLookup} />
|
||||
<Modal
|
||||
show={confirmDeleteShow}
|
||||
|
||||
12
src/pages/components/general/busyLoader.jsx
Normal file
12
src/pages/components/general/busyLoader.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
import "../../css/loading.css";
|
||||
|
||||
function BusyLoader() {
|
||||
return (
|
||||
<div className="loading busy">
|
||||
<div className="loading__spinner"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BusyLoader;
|
||||
@@ -13,6 +13,12 @@ function ItemActivity(props) {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [streamTypeFilter, setStreamTypeFilter] = useState("All");
|
||||
const [config, setConfig] = useState();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
|
||||
const handlePageChange = (newPage) => {
|
||||
setCurrentPage(newPage);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
@@ -30,8 +36,9 @@ function ItemActivity(props) {
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setIsBusy(true);
|
||||
const itemData = await axios.post(
|
||||
`/api/getItemHistory`,
|
||||
`/api/getItemHistory?size=${itemCount}&page=${currentPage}`,
|
||||
{
|
||||
itemid: props.itemid,
|
||||
},
|
||||
@@ -43,6 +50,7 @@ function ItemActivity(props) {
|
||||
}
|
||||
);
|
||||
setData(itemData.data);
|
||||
setIsBusy(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
@@ -54,16 +62,16 @@ function ItemActivity(props) {
|
||||
|
||||
const intervalId = setInterval(fetchData, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, props.itemid, token]);
|
||||
}, [data, props.itemid, token, itemCount, currentPage]);
|
||||
|
||||
if (!data) {
|
||||
if (!data || !data.results) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
let filteredData = data;
|
||||
let filteredData = data.results;
|
||||
|
||||
if (searchQuery) {
|
||||
filteredData = data.filter(
|
||||
filteredData = data.results.filter(
|
||||
(item) =>
|
||||
(!item.SeriesName ? item.NowPlayingItemName : item.SeriesName + " - " + item.NowPlayingItemName)
|
||||
.toLowerCase()
|
||||
@@ -136,7 +144,13 @@ function ItemActivity(props) {
|
||||
</div>
|
||||
|
||||
<div className="Activity">
|
||||
<ActivityTable data={filteredData} itemCount={itemCount} />
|
||||
<ActivityTable
|
||||
data={filteredData}
|
||||
itemCount={itemCount}
|
||||
onPageChange={handlePageChange}
|
||||
pageCount={data.pages}
|
||||
isBusy={isBusy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,12 @@ function LibraryActivity(props) {
|
||||
localStorage.getItem("PREF_LIBRARY_ACTIVITY_StreamTypeFilter") ?? "All"
|
||||
);
|
||||
const [config, setConfig] = useState();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
|
||||
const handlePageChange = (newPage) => {
|
||||
setCurrentPage(newPage);
|
||||
};
|
||||
|
||||
function setItemLimit(limit) {
|
||||
setItemCount(limit);
|
||||
@@ -42,8 +48,9 @@ function LibraryActivity(props) {
|
||||
}
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const libraryrData = await axios.post(
|
||||
`/api/getLibraryHistory`,
|
||||
setIsBusy(true);
|
||||
const libraryData = await axios.post(
|
||||
`/api/getLibraryHistory?size=${itemCount}&page=${currentPage}`,
|
||||
{
|
||||
libraryid: props.LibraryId,
|
||||
},
|
||||
@@ -54,28 +61,29 @@ function LibraryActivity(props) {
|
||||
},
|
||||
}
|
||||
);
|
||||
setData(libraryrData.data);
|
||||
setData(libraryData.data);
|
||||
setIsBusy(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
if (!data || data.current_page !== currentPage) {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchData, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, props.LibraryId, token]);
|
||||
}, [data, props.LibraryId, token, itemCount, currentPage]);
|
||||
|
||||
if (!data) {
|
||||
if (!data || !data.results) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
let filteredData = data;
|
||||
let filteredData = data.results;
|
||||
|
||||
if (searchQuery) {
|
||||
filteredData = data.filter((item) =>
|
||||
filteredData = data.results.filter((item) =>
|
||||
(!item.SeriesName ? item.NowPlayingItemName : item.SeriesName + " - " + item.NowPlayingItemName)
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase())
|
||||
@@ -147,7 +155,13 @@ function LibraryActivity(props) {
|
||||
</div>
|
||||
|
||||
<div className="Activity">
|
||||
<ActivityTable data={filteredData} itemCount={itemCount} />
|
||||
<ActivityTable
|
||||
data={filteredData}
|
||||
itemCount={itemCount}
|
||||
onPageChange={handlePageChange}
|
||||
pageCount={data.pages}
|
||||
isBusy={isBusy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -20,6 +20,8 @@ function UserActivity(props) {
|
||||
const [libraries, setLibraries] = useState([]);
|
||||
const [showLibraryFilters, setShowLibraryFilters] = useState(false);
|
||||
const [config, setConfig] = useState();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
@@ -51,11 +53,16 @@ function UserActivity(props) {
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageChange = (newPage) => {
|
||||
setCurrentPage(newPage);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchHistory = async () => {
|
||||
try {
|
||||
setIsBusy(true);
|
||||
const itemData = await axios.post(
|
||||
`/api/getUserHistory`,
|
||||
`/api/getUserHistory?size=${itemCount}&page=${currentPage}`,
|
||||
{
|
||||
userid: props.UserId,
|
||||
},
|
||||
@@ -67,6 +74,7 @@ function UserActivity(props) {
|
||||
}
|
||||
);
|
||||
setData(itemData.data);
|
||||
setIsBusy(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
@@ -102,16 +110,16 @@ function UserActivity(props) {
|
||||
|
||||
const intervalId = setInterval(fetchHistory, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [props.UserId, token]);
|
||||
}, [props.UserId, token, itemCount, currentPage]);
|
||||
|
||||
if (!data) {
|
||||
if (!data || !data.results) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
let filteredData = data;
|
||||
let filteredData = data.results;
|
||||
|
||||
if (searchQuery) {
|
||||
filteredData = data.filter((item) =>
|
||||
filteredData = data.results.filter((item) =>
|
||||
(!item.SeriesName ? item.NowPlayingItemName : item.SeriesName + " - " + item.NowPlayingItemName)
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase())
|
||||
@@ -204,7 +212,13 @@ function UserActivity(props) {
|
||||
</div>
|
||||
|
||||
<div className="Activity">
|
||||
<ActivityTable data={filteredData} itemCount={itemCount} />
|
||||
<ActivityTable
|
||||
data={filteredData}
|
||||
itemCount={itemCount}
|
||||
onPageChange={handlePageChange}
|
||||
pageCount={data.pages}
|
||||
isBusy={isBusy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.Activity {
|
||||
/* margin-top: 10px; */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -1,38 +1,48 @@
|
||||
@import './variables.module.css';
|
||||
@import "./variables.module.css";
|
||||
.loading {
|
||||
margin: 0px;
|
||||
height: calc(100vh - 100px);
|
||||
|
||||
margin: 0px;
|
||||
height: calc(100vh - 100px);
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--background-color);
|
||||
transition: opacity 800ms ease-in;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.loading::before
|
||||
{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--background-color);
|
||||
transition: opacity 800ms ease-in;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.busy {
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 55px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent background */
|
||||
z-index: 9999; /* High z-index to be above other elements */
|
||||
}
|
||||
|
||||
.loading::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
.loading__spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 5px solid #ccc;
|
||||
border-top-color: #333;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
.loading__spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 5px solid #ccc;
|
||||
border-top-color: #333;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user