From cabc05f7ddcdab8f8d5ac42fda2645afe4a70fb9 Mon Sep 17 00:00:00 2001 From: Min RK Date: Mon, 4 Mar 2024 15:38:56 +0100 Subject: [PATCH] admin: persist page view info in url parameters - persist offset, limit, name_filter in URL parameters, so they are stable across page reload - add UI element to specify items per page This allows specifying a URL, which will show a specific view of a page of users --- jsx/package-lock.json | 40 ++++++++++++ jsx/package.json | 1 + jsx/src/App.jsx | 63 ++++++++++--------- jsx/src/Store.js | 7 +++ jsx/src/components/Groups/Groups.jsx | 40 +++++++++++- .../PaginationFooter/PaginationFooter.jsx | 19 +++++- .../ServerDashboard/ServerDashboard.jsx | 48 +++++++++++++- 7 files changed, 182 insertions(+), 36 deletions(-) diff --git a/jsx/package-lock.json b/jsx/package-lock.json index 6503a515..bd63d1d1 100644 --- a/jsx/package-lock.json +++ b/jsx/package-lock.json @@ -20,6 +20,7 @@ "react-multi-select-component": "^4.3.4", "react-redux": "^7.2.8", "react-router-dom": "^5.3.4", + "react-router-dom-v5-compat": "^6.22.2", "recompose": "npm:react-recompose@^0.33.0", "redux": "^4.2.1", "regenerator-runtime": "^0.13.11" @@ -2408,6 +2409,14 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, + "node_modules/@remix-run/router": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.2.tgz", + "integrity": "sha512-+Rnav+CaoTE5QJc4Jcwh5toUpnVLKYbpU6Ys0zqbakqbaLQHeglLVHPfxOiQqdNmUy5C2lXz5dwC6tQNX2JW2Q==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@restart/hooks": { "version": "0.4.9", "license": "MIT", @@ -8410,6 +8419,37 @@ "react": ">=15" } }, + "node_modules/react-router-dom-v5-compat": { + "version": "6.22.2", + "resolved": "https://registry.npmjs.org/react-router-dom-v5-compat/-/react-router-dom-v5-compat-6.22.2.tgz", + "integrity": "sha512-d7Bo6q3lRwUqtrmuULkpu3NECk+nOT3eNz6PnR5lGIWi0NN7gu6i281cqe3Cfu8v0MmNE41D0g/JdKJhfE7Brw==", + "dependencies": { + "history": "^5.3.0", + "react-router": "6.22.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8", + "react-router-dom": "4 || 5" + } + }, + "node_modules/react-router-dom-v5-compat/node_modules/react-router": { + "version": "6.22.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.2.tgz", + "integrity": "sha512-YD3Dzprzpcq+tBMHBS822tCjnWD3iIZbTeSXMY9LPSG541EfoBGyZ3bS25KEnaZjLcmQpw2AVLkFyfgXY8uvcw==", + "dependencies": { + "@remix-run/router": "1.15.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, "node_modules/react-router-dom/node_modules/history": { "version": "4.10.1", "license": "MIT", diff --git a/jsx/package.json b/jsx/package.json index 9444ced5..30a6a396 100644 --- a/jsx/package.json +++ b/jsx/package.json @@ -40,6 +40,7 @@ "react-multi-select-component": "^4.3.4", "react-redux": "^7.2.8", "react-router-dom": "^5.3.4", + "react-router-dom-v5-compat": "^6.22.2", "recompose": "npm:react-recompose@^0.33.0", "redux": "^4.2.1", "regenerator-runtime": "^0.13.11" diff --git a/jsx/src/App.jsx b/jsx/src/App.jsx index db3dd300..93cc5028 100644 --- a/jsx/src/App.jsx +++ b/jsx/src/App.jsx @@ -13,6 +13,7 @@ import GroupEdit from "./components/GroupEdit/GroupEdit"; import CreateGroup from "./components/CreateGroup/CreateGroup"; import AddUser from "./components/AddUser/AddUser"; import EditUser from "./components/EditUser/EditUser"; +import { CompatRouter } from "react-router-dom-v5-compat"; import "./style/root.css"; @@ -23,34 +24,40 @@ const App = () => {
- - - - - - - - + + + + + + + + + +
diff --git a/jsx/src/Store.js b/jsx/src/Store.js index a54abb81..f933fdfb 100644 --- a/jsx/src/Store.js +++ b/jsx/src/Store.js @@ -17,6 +17,13 @@ export const reducers = (state = initialState, action) => { }), }); + case "USER_LIMIT": + return Object.assign({}, state, { + user_page: Object.assign({}, state.user_page, { + limit: action.value.limit, + }), + }); + case "USER_NAME_FILTER": // set offset to 0 if name filter changed, // otherwise leave it alone diff --git a/jsx/src/components/Groups/Groups.jsx b/jsx/src/components/Groups/Groups.jsx index ef3b4dae..e6332dbc 100644 --- a/jsx/src/components/Groups/Groups.jsx +++ b/jsx/src/components/Groups/Groups.jsx @@ -1,8 +1,10 @@ import React, { useEffect } from "react"; import { useSelector, useDispatch } from "react-redux"; import PropTypes from "prop-types"; +import { debounce } from "lodash"; import { Link } from "react-router-dom"; +import { useSearchParams } from "react-router-dom-v5-compat"; import PaginationFooter from "../PaginationFooter/PaginationFooter"; const Groups = (props) => { @@ -10,9 +12,20 @@ const Groups = (props) => { groups_page = useSelector((state) => state.groups_page), dispatch = useDispatch(); - var offset = groups_page ? groups_page.offset : 0; + let [searchParams, setSearchParams] = useSearchParams(); + + var offset = parseInt(searchParams.get("offset", "0")) || 0; + var limit = parseInt(searchParams.get("limit", "0")) || window.api_page_limit; const setOffset = (offset) => { + console.log("setting offset", offset); + if (offset < 0) { + offset = 0; + } + setSearchParams((params) => { + params.set("offset", offset); + return params; + }); dispatch({ type: "GROUPS_OFFSET", value: { @@ -20,7 +33,23 @@ const Groups = (props) => { }, }); }; - var limit = groups_page ? groups_page.limit : window.api_page_limit; + + const setLimit = (newLimit) => { + if (newLimit < 1) { + newLimit = 10; + } + setSearchParams((params) => { + params.set("limit", newLimit); + return params; + }); + dispatch({ + type: "GROUP_LIMIT", + value: { + limit: newLimit, + }, + }); + }; + var total = groups_page ? groups_page.total : undefined; var { updateGroups, history } = props; @@ -45,6 +74,10 @@ const Groups = (props) => { return
; } + const handleLimit = debounce(async (event) => { + setLimit(event.target.value); + }, 300); + return (
@@ -85,7 +118,8 @@ const Groups = (props) => { visible={groups_data.length} total={total} next={() => setOffset(offset + limit)} - prev={() => setOffset(offset >= limit ? offset - limit : 0)} + prev={() => setOffset(offset - limit)} + handleLimit={handleLimit} />
diff --git a/jsx/src/components/PaginationFooter/PaginationFooter.jsx b/jsx/src/components/PaginationFooter/PaginationFooter.jsx index 942f63a6..40dd017c 100644 --- a/jsx/src/components/PaginationFooter/PaginationFooter.jsx +++ b/jsx/src/components/PaginationFooter/PaginationFooter.jsx @@ -1,16 +1,16 @@ import React from "react"; import PropTypes from "prop-types"; +import { FormControl } from "react-bootstrap"; import "./pagination-footer.css"; const PaginationFooter = (props) => { - let { offset, limit, visible, total, next, prev } = props; + let { offset, limit, visible, total, next, prev, handleLimit } = props; return (

Displaying {offset}-{offset + visible} -

-

+
{offset >= 1 ? ( )} +

); diff --git a/jsx/src/components/ServerDashboard/ServerDashboard.jsx b/jsx/src/components/ServerDashboard/ServerDashboard.jsx index 7f159b6c..a82d2de5 100644 --- a/jsx/src/components/ServerDashboard/ServerDashboard.jsx +++ b/jsx/src/components/ServerDashboard/ServerDashboard.jsx @@ -15,6 +15,9 @@ import { import ReactObjectTableViewer from "../ReactObjectTableViewer/ReactObjectTableViewer"; import { Link } from "react-router-dom"; +// react-router-dom v6 API +// should be able to upgrade to v6 someday +import { useSearchParams } from "react-router-dom-v5-compat"; import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa"; import "./server-dashboard.css"; @@ -30,6 +33,7 @@ RowListItem.propTypes = { const ServerDashboard = (props) => { let base_url = window.base_url || "/"; + let [searchParams, setSearchParams] = useSearchParams(); // sort methods var usernameDesc = (e) => e.sort((a, b) => (a.name > b.name ? 1 : -1)), usernameAsc = (e) => e.sort((a, b) => (a.name < b.name ? 1 : -1)), @@ -54,8 +58,11 @@ const ServerDashboard = (props) => { user_page = useSelector((state) => state.user_page), name_filter = useSelector((state) => state.name_filter); - var offset = user_page ? user_page.offset : 0; - var limit = user_page ? user_page.limit : window.api_page_limit; + // get offset, limit, name filter from URL + var offset = parseInt(searchParams.get("offset", "0")) || 0; + var limit = parseInt(searchParams.get("limit", "0")) || window.api_page_limit; + var searchNameFilter = searchParams.get("name_filter"); + var total = user_page ? user_page.total : undefined; const dispatch = useDispatch(); @@ -82,6 +89,13 @@ const ServerDashboard = (props) => { }; const setOffset = (newOffset) => { + if (newOffset < 0) { + newOffset = 0; + } + setSearchParams((params) => { + params.set("offset", newOffset); + return params; + }); dispatch({ type: "USER_OFFSET", value: { @@ -90,7 +104,27 @@ const ServerDashboard = (props) => { }); }; + const setLimit = (newLimit) => { + if (newLimit < 1) { + newLimit = 10; + } + setSearchParams((params) => { + params.set("limit", newLimit); + return params; + }); + dispatch({ + type: "USER_LIMIT", + value: { + limit: newLimit, + }, + }); + }; + const setNameFilter = (name_filter) => { + setSearchParams((params) => { + params.set("name_filter", name_filter); + return params; + }); dispatch({ type: "USER_NAME_FILTER", value: { @@ -105,6 +139,11 @@ const ServerDashboard = (props) => { .catch((err) => setErrorAlert("Failed to update user list.")); }, [offset, limit, name_filter]); + if (searchNameFilter && name_filter != searchNameFilter) { + // get name_filter from URL + setNameFilter(searchNameFilter); + } + if (!user_data || !user_page) { return
; } @@ -115,6 +154,10 @@ const ServerDashboard = (props) => { setNameFilter(event.target.value); }, 300); + const handleLimit = debounce(async (event) => { + setLimit(event.target.value); + }, 300); + if (sortMethod != null) { user_data = sortMethod(user_data); } @@ -573,6 +616,7 @@ const ServerDashboard = (props) => { total={total} next={() => setOffset(offset + limit)} prev={() => setOffset(offset - limit)} + handleLimit={handleLimit} />