mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 10:34:10 +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:
40
jsx/package-lock.json
generated
40
jsx/package-lock.json
generated
@@ -20,6 +20,7 @@
|
|||||||
"react-multi-select-component": "^4.3.4",
|
"react-multi-select-component": "^4.3.4",
|
||||||
"react-redux": "^7.2.8",
|
"react-redux": "^7.2.8",
|
||||||
"react-router-dom": "^5.3.4",
|
"react-router-dom": "^5.3.4",
|
||||||
|
"react-router-dom-v5-compat": "^6.22.2",
|
||||||
"recompose": "npm:react-recompose@^0.33.0",
|
"recompose": "npm:react-recompose@^0.33.0",
|
||||||
"redux": "^4.2.1",
|
"redux": "^4.2.1",
|
||||||
"regenerator-runtime": "^0.13.11"
|
"regenerator-runtime": "^0.13.11"
|
||||||
@@ -2408,6 +2409,14 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
|
"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": {
|
"node_modules/@restart/hooks": {
|
||||||
"version": "0.4.9",
|
"version": "0.4.9",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -8410,6 +8419,37 @@
|
|||||||
"react": ">=15"
|
"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": {
|
"node_modules/react-router-dom/node_modules/history": {
|
||||||
"version": "4.10.1",
|
"version": "4.10.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@@ -40,6 +40,7 @@
|
|||||||
"react-multi-select-component": "^4.3.4",
|
"react-multi-select-component": "^4.3.4",
|
||||||
"react-redux": "^7.2.8",
|
"react-redux": "^7.2.8",
|
||||||
"react-router-dom": "^5.3.4",
|
"react-router-dom": "^5.3.4",
|
||||||
|
"react-router-dom-v5-compat": "^6.22.2",
|
||||||
"recompose": "npm:react-recompose@^0.33.0",
|
"recompose": "npm:react-recompose@^0.33.0",
|
||||||
"redux": "^4.2.1",
|
"redux": "^4.2.1",
|
||||||
"regenerator-runtime": "^0.13.11"
|
"regenerator-runtime": "^0.13.11"
|
||||||
|
@@ -13,6 +13,7 @@ import GroupEdit from "./components/GroupEdit/GroupEdit";
|
|||||||
import CreateGroup from "./components/CreateGroup/CreateGroup";
|
import CreateGroup from "./components/CreateGroup/CreateGroup";
|
||||||
import AddUser from "./components/AddUser/AddUser";
|
import AddUser from "./components/AddUser/AddUser";
|
||||||
import EditUser from "./components/EditUser/EditUser";
|
import EditUser from "./components/EditUser/EditUser";
|
||||||
|
import { CompatRouter } from "react-router-dom-v5-compat";
|
||||||
|
|
||||||
import "./style/root.css";
|
import "./style/root.css";
|
||||||
|
|
||||||
@@ -23,13 +24,18 @@ const App = () => {
|
|||||||
<div className="resets">
|
<div className="resets">
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
|
<CompatRouter>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path="/"
|
path="/"
|
||||||
component={compose(withAPI)(ServerDashboard)}
|
component={compose(withAPI)(ServerDashboard)}
|
||||||
/>
|
/>
|
||||||
<Route exact path="/groups" component={compose(withAPI)(Groups)} />
|
<Route
|
||||||
|
exact
|
||||||
|
path="/groups"
|
||||||
|
component={compose(withAPI)(Groups)}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path="/group-edit"
|
path="/group-edit"
|
||||||
@@ -51,6 +57,7 @@ const App = () => {
|
|||||||
component={compose(withAPI)(EditUser)}
|
component={compose(withAPI)(EditUser)}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
</CompatRouter>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
</div>
|
</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":
|
case "USER_NAME_FILTER":
|
||||||
// set offset to 0 if name filter changed,
|
// set offset to 0 if name filter changed,
|
||||||
// otherwise leave it alone
|
// otherwise leave it alone
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import { debounce } from "lodash";
|
||||||
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { useSearchParams } from "react-router-dom-v5-compat";
|
||||||
import PaginationFooter from "../PaginationFooter/PaginationFooter";
|
import PaginationFooter from "../PaginationFooter/PaginationFooter";
|
||||||
|
|
||||||
const Groups = (props) => {
|
const Groups = (props) => {
|
||||||
@@ -10,9 +12,20 @@ const Groups = (props) => {
|
|||||||
groups_page = useSelector((state) => state.groups_page),
|
groups_page = useSelector((state) => state.groups_page),
|
||||||
dispatch = useDispatch();
|
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) => {
|
const setOffset = (offset) => {
|
||||||
|
console.log("setting offset", offset);
|
||||||
|
if (offset < 0) {
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
setSearchParams((params) => {
|
||||||
|
params.set("offset", offset);
|
||||||
|
return params;
|
||||||
|
});
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "GROUPS_OFFSET",
|
type: "GROUPS_OFFSET",
|
||||||
value: {
|
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 total = groups_page ? groups_page.total : undefined;
|
||||||
|
|
||||||
var { updateGroups, history } = props;
|
var { updateGroups, history } = props;
|
||||||
@@ -45,6 +74,10 @@ const Groups = (props) => {
|
|||||||
return <div data-testid="no-show"></div>;
|
return <div data-testid="no-show"></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleLimit = debounce(async (event) => {
|
||||||
|
setLimit(event.target.value);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container" data-testid="container">
|
<div className="container" data-testid="container">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
@@ -85,7 +118,8 @@ const Groups = (props) => {
|
|||||||
visible={groups_data.length}
|
visible={groups_data.length}
|
||||||
total={total}
|
total={total}
|
||||||
next={() => setOffset(offset + limit)}
|
next={() => setOffset(offset + limit)}
|
||||||
prev={() => setOffset(offset >= limit ? offset - limit : 0)}
|
prev={() => setOffset(offset - limit)}
|
||||||
|
handleLimit={handleLimit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-footer">
|
<div className="panel-footer">
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import { FormControl } from "react-bootstrap";
|
||||||
|
|
||||||
import "./pagination-footer.css";
|
import "./pagination-footer.css";
|
||||||
|
|
||||||
const PaginationFooter = (props) => {
|
const PaginationFooter = (props) => {
|
||||||
let { offset, limit, visible, total, next, prev } = props;
|
let { offset, limit, visible, total, next, prev, handleLimit } = props;
|
||||||
return (
|
return (
|
||||||
<div className="pagination-footer">
|
<div className="pagination-footer">
|
||||||
<p>
|
<p>
|
||||||
Displaying {offset}-{offset + visible}
|
Displaying {offset}-{offset + visible}
|
||||||
<br></br>
|
<br />
|
||||||
<br></br>
|
|
||||||
{offset >= 1 ? (
|
{offset >= 1 ? (
|
||||||
<button className="btn btn-sm btn-light spaced">
|
<button className="btn btn-sm btn-light spaced">
|
||||||
<span
|
<span
|
||||||
@@ -41,6 +41,19 @@ const PaginationFooter = (props) => {
|
|||||||
<span className="inactive-pagination">Next</span>
|
<span className="inactive-pagination">Next</span>
|
||||||
</button>
|
</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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -15,6 +15,9 @@ import {
|
|||||||
import ReactObjectTableViewer from "../ReactObjectTableViewer/ReactObjectTableViewer";
|
import ReactObjectTableViewer from "../ReactObjectTableViewer/ReactObjectTableViewer";
|
||||||
|
|
||||||
import { Link } from "react-router-dom";
|
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 { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
|
||||||
|
|
||||||
import "./server-dashboard.css";
|
import "./server-dashboard.css";
|
||||||
@@ -30,6 +33,7 @@ RowListItem.propTypes = {
|
|||||||
|
|
||||||
const ServerDashboard = (props) => {
|
const ServerDashboard = (props) => {
|
||||||
let base_url = window.base_url || "/";
|
let base_url = window.base_url || "/";
|
||||||
|
let [searchParams, setSearchParams] = useSearchParams();
|
||||||
// sort methods
|
// sort methods
|
||||||
var usernameDesc = (e) => e.sort((a, b) => (a.name > b.name ? 1 : -1)),
|
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)),
|
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),
|
user_page = useSelector((state) => state.user_page),
|
||||||
name_filter = useSelector((state) => state.name_filter);
|
name_filter = useSelector((state) => state.name_filter);
|
||||||
|
|
||||||
var offset = user_page ? user_page.offset : 0;
|
// get offset, limit, name filter from URL
|
||||||
var limit = user_page ? user_page.limit : window.api_page_limit;
|
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;
|
var total = user_page ? user_page.total : undefined;
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -82,6 +89,13 @@ const ServerDashboard = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setOffset = (newOffset) => {
|
const setOffset = (newOffset) => {
|
||||||
|
if (newOffset < 0) {
|
||||||
|
newOffset = 0;
|
||||||
|
}
|
||||||
|
setSearchParams((params) => {
|
||||||
|
params.set("offset", newOffset);
|
||||||
|
return params;
|
||||||
|
});
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "USER_OFFSET",
|
type: "USER_OFFSET",
|
||||||
value: {
|
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) => {
|
const setNameFilter = (name_filter) => {
|
||||||
|
setSearchParams((params) => {
|
||||||
|
params.set("name_filter", name_filter);
|
||||||
|
return params;
|
||||||
|
});
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "USER_NAME_FILTER",
|
type: "USER_NAME_FILTER",
|
||||||
value: {
|
value: {
|
||||||
@@ -105,6 +139,11 @@ const ServerDashboard = (props) => {
|
|||||||
.catch((err) => setErrorAlert("Failed to update user list."));
|
.catch((err) => setErrorAlert("Failed to update user list."));
|
||||||
}, [offset, limit, name_filter]);
|
}, [offset, limit, name_filter]);
|
||||||
|
|
||||||
|
if (searchNameFilter && name_filter != searchNameFilter) {
|
||||||
|
// get name_filter from URL
|
||||||
|
setNameFilter(searchNameFilter);
|
||||||
|
}
|
||||||
|
|
||||||
if (!user_data || !user_page) {
|
if (!user_data || !user_page) {
|
||||||
return <div data-testid="no-show"></div>;
|
return <div data-testid="no-show"></div>;
|
||||||
}
|
}
|
||||||
@@ -115,6 +154,10 @@ const ServerDashboard = (props) => {
|
|||||||
setNameFilter(event.target.value);
|
setNameFilter(event.target.value);
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
|
const handleLimit = debounce(async (event) => {
|
||||||
|
setLimit(event.target.value);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
if (sortMethod != null) {
|
if (sortMethod != null) {
|
||||||
user_data = sortMethod(user_data);
|
user_data = sortMethod(user_data);
|
||||||
}
|
}
|
||||||
@@ -573,6 +616,7 @@ const ServerDashboard = (props) => {
|
|||||||
total={total}
|
total={total}
|
||||||
next={() => setOffset(offset + limit)}
|
next={() => setOffset(offset + limit)}
|
||||||
prev={() => setOffset(offset - limit)}
|
prev={() => setOffset(offset - limit)}
|
||||||
|
handleLimit={handleLimit}
|
||||||
/>
|
/>
|
||||||
<br></br>
|
<br></br>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user