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:
CyferShepard
2024-12-27 00:23:22 +02:00
parent ab4784858d
commit 7eae08c797
13 changed files with 889 additions and 393 deletions

View 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,
};

View File

@@ -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,

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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>
);

View File

@@ -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}

View 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;

View File

@@ -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>
);

View File

@@ -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>
);

View File

@@ -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>
);

View File

@@ -1,3 +1,4 @@
.Activity {
/* margin-top: 10px; */
position: relative;
}

View File

@@ -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);
}
}