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
This commit is contained in:
Min RK
2024-03-04 15:38:56 +01:00
parent 9c3f98d427
commit cabc05f7dd
7 changed files with 182 additions and 36 deletions

40
jsx/package-lock.json generated
View File

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

View File

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

View File

@@ -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 = () => {
<div className="resets">
<Provider store={store}>
<HashRouter>
<Switch>
<Route
exact
path="/"
component={compose(withAPI)(ServerDashboard)}
/>
<Route exact path="/groups" component={compose(withAPI)(Groups)} />
<Route
exact
path="/group-edit"
component={compose(withAPI)(GroupEdit)}
/>
<Route
exact
path="/create-group"
component={compose(withAPI)(CreateGroup)}
/>
<Route
exact
path="/add-users"
component={compose(withAPI)(AddUser)}
/>
<Route
exact
path="/edit-user"
component={compose(withAPI)(EditUser)}
/>
</Switch>
<CompatRouter>
<Switch>
<Route
exact
path="/"
component={compose(withAPI)(ServerDashboard)}
/>
<Route
exact
path="/groups"
component={compose(withAPI)(Groups)}
/>
<Route
exact
path="/group-edit"
component={compose(withAPI)(GroupEdit)}
/>
<Route
exact
path="/create-group"
component={compose(withAPI)(CreateGroup)}
/>
<Route
exact
path="/add-users"
component={compose(withAPI)(AddUser)}
/>
<Route
exact
path="/edit-user"
component={compose(withAPI)(EditUser)}
/>
</Switch>
</CompatRouter>
</HashRouter>
</Provider>
</div>

View File

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

View File

@@ -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 <div data-testid="no-show"></div>;
}
const handleLimit = debounce(async (event) => {
setLimit(event.target.value);
}, 300);
return (
<div className="container" data-testid="container">
<div className="row">
@@ -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}
/>
</div>
<div className="panel-footer">

View File

@@ -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 (
<div className="pagination-footer">
<p>
Displaying {offset}-{offset + visible}
<br></br>
<br></br>
<br />
{offset >= 1 ? (
<button className="btn btn-sm btn-light spaced">
<span
@@ -41,6 +41,19 @@ const PaginationFooter = (props) => {
<span className="inactive-pagination">Next</span>
</button>
)}
<label>
Items per page:
<FormControl
type="number"
min="25"
step="25"
name="pagination-limit"
placeholder={limit}
aria-label="pagination-limit"
defaultValue={limit}
onChange={handleLimit}
/>
</label>
</p>
</div>
);

View File

@@ -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 <div data-testid="no-show"></div>;
}
@@ -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}
/>
<br></br>
</div>