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

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>