mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-10 11:33:01 +00:00
Add persistent URL / stateful pagination for users
This commit is contained in:
@@ -21,12 +21,13 @@ const store = createStore(reducers, initialState);
|
|||||||
|
|
||||||
const App = (props) => {
|
const App = (props) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
jhapiRequest("/users", "GET")
|
let { limit, user_page, groups_page } = initialState;
|
||||||
|
jhapiRequest(`/users?offset=${user_page * limit}&limit=${limit}`, "GET")
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((data) => store.dispatch({ type: "USER_DATA", value: data }))
|
.then((data) => store.dispatch({ type: "USER_DATA", value: data }))
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
|
|
||||||
jhapiRequest("/groups", "GET")
|
jhapiRequest(`/groups?offset=${groups_page * limit}&limit=${limit}`, "GET")
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((data) => store.dispatch({ type: "GROUPS_DATA", value: data }))
|
.then((data) => store.dispatch({ type: "GROUPS_DATA", value: data }))
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
|
@@ -2,7 +2,10 @@ import { combineReducers } from "redux";
|
|||||||
|
|
||||||
export const initialState = {
|
export const initialState = {
|
||||||
user_data: undefined,
|
user_data: undefined,
|
||||||
|
user_page: 0,
|
||||||
groups_data: undefined,
|
groups_data: undefined,
|
||||||
|
groups_page: 0,
|
||||||
|
limit: 50,
|
||||||
manage_groups_modal: false,
|
manage_groups_modal: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -10,6 +13,11 @@ export const reducers = (state = initialState, action) => {
|
|||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "USER_DATA":
|
case "USER_DATA":
|
||||||
return Object.assign({}, state, { user_data: action.value });
|
return Object.assign({}, state, { user_data: action.value });
|
||||||
|
case "USER_PAGE":
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
user_page: action.value.page,
|
||||||
|
user_data: action.value.data,
|
||||||
|
});
|
||||||
case "GROUPS_DATA":
|
case "GROUPS_DATA":
|
||||||
return Object.assign({}, state, { groups_data: action.value });
|
return Object.assign({}, state, { groups_data: action.value });
|
||||||
case "TOGGLE_MANAGE_GROUPS_MODAL":
|
case "TOGGLE_MANAGE_GROUPS_MODAL":
|
||||||
|
@@ -31,6 +31,11 @@ const ServerDashboard = (props) => {
|
|||||||
var [sortMethod, setSortMethod] = useState(null);
|
var [sortMethod, setSortMethod] = useState(null);
|
||||||
|
|
||||||
var user_data = useSelector((state) => state.user_data);
|
var user_data = useSelector((state) => state.user_data);
|
||||||
|
var user_page = useSelector((state) => state.user_page);
|
||||||
|
var limit = useSelector((state) => state.limit);
|
||||||
|
var page = parseInt(new URLSearchParams(props.location.search).get("page"));
|
||||||
|
page = isNaN(page) ? 0 : page;
|
||||||
|
var slice = [page * limit, limit];
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
@@ -51,10 +56,26 @@ const ServerDashboard = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var dispatchPageChange = (data, page) => {
|
||||||
|
dispatch({
|
||||||
|
type: "USER_PAGE",
|
||||||
|
value: {
|
||||||
|
data: data,
|
||||||
|
page: page,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (!user_data) {
|
if (!user_data) {
|
||||||
return <div></div>;
|
return <div></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (page != user_page) {
|
||||||
|
updateUsers(...slice)
|
||||||
|
.then((data) => data.json())
|
||||||
|
.then((data) => dispatchPageChange(data, page));
|
||||||
|
}
|
||||||
|
|
||||||
if (sortMethod != null) {
|
if (sortMethod != null) {
|
||||||
user_data = sortMethod(user_data);
|
user_data = sortMethod(user_data);
|
||||||
}
|
}
|
||||||
@@ -116,7 +137,7 @@ const ServerDashboard = (props) => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
Promise.all(startAll(user_data.map((e) => e.name)))
|
Promise.all(startAll(user_data.map((e) => e.name)))
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
updateUsers()
|
updateUsers(...slice)
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
dispatchUserUpdate(data);
|
dispatchUserUpdate(data);
|
||||||
@@ -137,7 +158,7 @@ const ServerDashboard = (props) => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
Promise.all(stopAll(user_data.map((e) => e.name)))
|
Promise.all(stopAll(user_data.map((e) => e.name)))
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
updateUsers()
|
updateUsers(...slice)
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
dispatchUserUpdate(data);
|
dispatchUserUpdate(data);
|
||||||
@@ -177,7 +198,7 @@ const ServerDashboard = (props) => {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
stopServer(e.name)
|
stopServer(e.name)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
updateUsers()
|
updateUsers(...slice)
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
dispatchUserUpdate(data);
|
dispatchUserUpdate(data);
|
||||||
@@ -196,7 +217,7 @@ const ServerDashboard = (props) => {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
startServer(e.name)
|
startServer(e.name)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
updateUsers()
|
updateUsers(...slice)
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
dispatchUserUpdate(data);
|
dispatchUserUpdate(data);
|
||||||
@@ -232,6 +253,25 @@ const ServerDashboard = (props) => {
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<br></br>
|
||||||
|
<p>
|
||||||
|
Displaying users {slice[0]}-{slice[0] + user_data.length}
|
||||||
|
{user_data.length >= limit ? (
|
||||||
|
<button className="btn btn-link">
|
||||||
|
<Link to={`/?page=${page + 1}`}>Next</Link>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
{page >= 1 ? (
|
||||||
|
<button className="btn btn-link">
|
||||||
|
<Link to={`/?page=${page - 1}`}>Previous</Link>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<br></br>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -249,6 +289,9 @@ ServerDashboard.propTypes = {
|
|||||||
history: PropTypes.shape({
|
history: PropTypes.shape({
|
||||||
push: PropTypes.func,
|
push: PropTypes.func,
|
||||||
}),
|
}),
|
||||||
|
location: PropTypes.shape({
|
||||||
|
search: PropTypes.string,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const SortHandler = (props) => {
|
const SortHandler = (props) => {
|
||||||
|
@@ -2,7 +2,8 @@ import { withProps } from "recompose";
|
|||||||
import { jhapiRequest } from "./jhapiUtil";
|
import { jhapiRequest } from "./jhapiUtil";
|
||||||
|
|
||||||
const withAPI = withProps((props) => ({
|
const withAPI = withProps((props) => ({
|
||||||
updateUsers: (cb) => jhapiRequest("/users", "GET"),
|
updateUsers: (offset, limit) =>
|
||||||
|
jhapiRequest(`/users?offset=${offset}&limit=${limit}`, "GET"),
|
||||||
shutdownHub: () => jhapiRequest("/shutdown", "POST"),
|
shutdownHub: () => jhapiRequest("/shutdown", "POST"),
|
||||||
startServer: (name) => jhapiRequest("/users/" + name + "/server", "POST"),
|
startServer: (name) => jhapiRequest("/users/" + name + "/server", "POST"),
|
||||||
stopServer: (name) => jhapiRequest("/users/" + name + "/server", "DELETE"),
|
stopServer: (name) => jhapiRequest("/users/" + name + "/server", "DELETE"),
|
||||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user