mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-18 07:23:00 +00:00
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:
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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">
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user