mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-17 23:13:00 +00:00
Add UI pagination, update Redux and API service lib
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { compose, withProps } from "recompose";
|
||||
import { Link } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
@@ -7,18 +7,22 @@ import { jhapiRequest } from "../../util/jhapiUtil";
|
||||
|
||||
const AddUser = (props) => {
|
||||
var [users, setUsers] = useState([]),
|
||||
[admin, setAdmin] = useState(false);
|
||||
[admin, setAdmin] = useState(false),
|
||||
limit = useSelector((state) => state.limit);
|
||||
|
||||
var dispatch = useDispatch();
|
||||
|
||||
var dispatchUserData = (data) => {
|
||||
var dispatchPageChange = (data, page) => {
|
||||
dispatch({
|
||||
type: "USER_DATA",
|
||||
value: data,
|
||||
type: "USER_PAGE",
|
||||
value: {
|
||||
data: data,
|
||||
page: page,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
var { addUsers, failRegexEvent, refreshUserData, history } = props;
|
||||
var { addUsers, failRegexEvent, updateUsers, history } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -78,12 +82,12 @@ const AddUser = (props) => {
|
||||
}
|
||||
|
||||
addUsers(filtered_users, admin)
|
||||
.then(
|
||||
refreshUserData()
|
||||
.then((data) => dispatchUserData(data))
|
||||
.then(() =>
|
||||
updateUsers(0, limit)
|
||||
.then((data) => dispatchPageChange(data, 0))
|
||||
.then(() => history.push("/"))
|
||||
.catch((err) => console.log(err))
|
||||
)
|
||||
.then(() => history.push("/"))
|
||||
.catch((err) => console.log(err));
|
||||
}}
|
||||
>
|
||||
@@ -101,7 +105,7 @@ const AddUser = (props) => {
|
||||
AddUser.propTypes = {
|
||||
addUsers: PropTypes.func,
|
||||
failRegexEvent: PropTypes.func,
|
||||
refreshUserData: PropTypes.func,
|
||||
updateUsers: PropTypes.func,
|
||||
history: PropTypes.shape({
|
||||
push: PropTypes.func,
|
||||
}),
|
||||
|
@@ -1,23 +1,25 @@
|
||||
import React, { useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { compose, withProps } from "recompose";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
import { jhapiRequest } from "../../util/jhapiUtil";
|
||||
|
||||
const CreateGroup = (props) => {
|
||||
var [groupName, setGroupName] = useState("");
|
||||
var [groupName, setGroupName] = useState(""),
|
||||
limit = useSelector((state) => state.limit);
|
||||
|
||||
var dispatch = useDispatch();
|
||||
|
||||
var dispatchGroupsData = (data) => {
|
||||
var dispatchPageUpdate = (data, page) => {
|
||||
dispatch({
|
||||
type: "GROUPS_DATA",
|
||||
value: data,
|
||||
value: {
|
||||
data: data,
|
||||
page: page,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
var { createGroup, refreshGroupsData, history } = props;
|
||||
var { createGroup, updateGroups, history } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -53,11 +55,11 @@ const CreateGroup = (props) => {
|
||||
onClick={() => {
|
||||
createGroup(groupName)
|
||||
.then(
|
||||
refreshGroupsData()
|
||||
.then((data) => dispatchGroupsData(data))
|
||||
updateGroups(0, limit)
|
||||
.then((data) => dispatchPageUpdate(data, 0))
|
||||
.then(history.push("/groups"))
|
||||
.catch((err) => console.log(err))
|
||||
)
|
||||
.then(history.push("/groups"))
|
||||
.catch((err) => console.log(err));
|
||||
}}
|
||||
>
|
||||
@@ -74,7 +76,7 @@ const CreateGroup = (props) => {
|
||||
|
||||
CreateGroup.propTypes = {
|
||||
createGroup: PropTypes.func,
|
||||
refreshGroupsData: PropTypes.func,
|
||||
updateGroups: PropTypes.func,
|
||||
failRegexEvent: PropTypes.func,
|
||||
history: PropTypes.shape({
|
||||
push: PropTypes.func,
|
||||
|
@@ -1,15 +1,20 @@
|
||||
import React, { useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const EditUser = (props) => {
|
||||
var limit = useSelector((state) => state.limit);
|
||||
|
||||
var dispatch = useDispatch();
|
||||
|
||||
var dispatchUserData = (data) => {
|
||||
var dispatchPageChange = (data, page) => {
|
||||
dispatch({
|
||||
type: "USER_DATA",
|
||||
value: data,
|
||||
type: "USER_PAGE",
|
||||
value: {
|
||||
data: data,
|
||||
page: page,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -18,7 +23,7 @@ const EditUser = (props) => {
|
||||
deleteUser,
|
||||
failRegexEvent,
|
||||
noChangeEvent,
|
||||
refreshUserData,
|
||||
updateUsers,
|
||||
history,
|
||||
} = props;
|
||||
|
||||
@@ -70,9 +75,9 @@ const EditUser = (props) => {
|
||||
onClick={() => {
|
||||
deleteUser(username)
|
||||
.then((data) => {
|
||||
history.push("/");
|
||||
refreshUserData()
|
||||
.then((data) => dispatchUserData(data))
|
||||
updateUsers(0, limit)
|
||||
.then((data) => dispatchPageChange(data, 0))
|
||||
.then(() => history.push("/"))
|
||||
.catch((err) => console.log(err));
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
@@ -106,9 +111,9 @@ const EditUser = (props) => {
|
||||
admin
|
||||
)
|
||||
.then((data) => {
|
||||
history.push("/");
|
||||
refreshUserData()
|
||||
.then((data) => dispatchUserData(data))
|
||||
updateUsers(0, limit)
|
||||
.then((data) => dispatchPageChange(data, 0))
|
||||
.then(() => history.push("/"))
|
||||
.catch((err) => console.log(err));
|
||||
})
|
||||
.catch((err) => {});
|
||||
@@ -119,9 +124,9 @@ const EditUser = (props) => {
|
||||
} else {
|
||||
editUser(username, username, admin)
|
||||
.then((data) => {
|
||||
history.push("/");
|
||||
refreshUserData()
|
||||
.then((data) => dispatchUserData(data))
|
||||
updateUsers(0, limit)
|
||||
.then((data) => dispatchPageChange(data, 0))
|
||||
.then(() => history.push("/"))
|
||||
.catch((err) => console.log(err));
|
||||
})
|
||||
.catch((err) => {});
|
||||
@@ -153,7 +158,7 @@ EditUser.propTypes = {
|
||||
deleteUser: PropTypes.func,
|
||||
failRegexEvent: PropTypes.func,
|
||||
noChangeEvent: PropTypes.func,
|
||||
refreshUserData: PropTypes.func,
|
||||
updateUsers: PropTypes.func,
|
||||
};
|
||||
|
||||
export default EditUser;
|
||||
|
@@ -10,14 +10,18 @@ const GroupEdit = (props) => {
|
||||
var [selected, setSelected] = useState([]),
|
||||
[changed, setChanged] = useState(false),
|
||||
[added, setAdded] = useState(undefined),
|
||||
[removed, setRemoved] = useState(undefined);
|
||||
[removed, setRemoved] = useState(undefined),
|
||||
limit = useSelector((state) => state.limit);
|
||||
|
||||
var dispatch = useDispatch();
|
||||
|
||||
const dispatchGroupsData = (data) => {
|
||||
const dispatchPageUpdate = (data, page) => {
|
||||
dispatch({
|
||||
type: "GROUPS_DATA",
|
||||
value: data,
|
||||
type: "GROUPS_PAGE",
|
||||
value: {
|
||||
data: data,
|
||||
page: page,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -25,7 +29,7 @@ const GroupEdit = (props) => {
|
||||
addToGroup,
|
||||
removeFromGroup,
|
||||
deleteGroup,
|
||||
refreshGroupsData,
|
||||
updateGroups,
|
||||
history,
|
||||
location,
|
||||
} = props;
|
||||
@@ -88,10 +92,12 @@ const GroupEdit = (props) => {
|
||||
);
|
||||
|
||||
Promise.all(promiseQueue)
|
||||
.then((e) => callback())
|
||||
.then((e) => {
|
||||
updateGroups(0, limit)
|
||||
.then((data) => dispatchPageUpdate(data, 0))
|
||||
.then(() => history.push("/groups"));
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
|
||||
history.push("/groups");
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
@@ -103,10 +109,11 @@ const GroupEdit = (props) => {
|
||||
onClick={() => {
|
||||
var groupName = group_data.name;
|
||||
deleteGroup(groupName)
|
||||
.then(
|
||||
refreshGroupsData().then((data) => dispatchGroupsData(data))
|
||||
)
|
||||
.then(history.push("/groups"))
|
||||
.then((e) => {
|
||||
updateGroups(0, limit)
|
||||
.then((data) => dispatchPageUpdate(data, 0))
|
||||
.then(() => history.push("/groups"));
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
}}
|
||||
>
|
||||
@@ -134,7 +141,7 @@ GroupEdit.propTypes = {
|
||||
addToGroup: PropTypes.func,
|
||||
removeFromGroup: PropTypes.func,
|
||||
deleteGroup: PropTypes.func,
|
||||
refreshGroupsData: PropTypes.func,
|
||||
updateGroups: PropTypes.func,
|
||||
};
|
||||
|
||||
export default GroupEdit;
|
||||
|
@@ -4,32 +4,41 @@ import { compose, withProps } from "recompose";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import { jhapiRequest } from "../../util/jhapiUtil";
|
||||
import PaginationFooter from "../PaginationFooter/PaginationFooter";
|
||||
|
||||
const Groups = (props) => {
|
||||
var user_data = useSelector((state) => state.user_data),
|
||||
groups_data = useSelector((state) => state.groups_data),
|
||||
dispatch = useDispatch();
|
||||
groups_page = useSelector((state) => state.groups_page),
|
||||
user_page = useSelector((state) => state.user_page),
|
||||
limit = useSelector((state) => state.limit),
|
||||
dispatch = useDispatch(),
|
||||
page = parseInt(new URLSearchParams(props.location.search).get("page"));
|
||||
|
||||
var { refreshGroupsData, refreshUserData, history } = props;
|
||||
page = isNaN(page) ? 0 : page;
|
||||
var slice = [page * limit, limit];
|
||||
|
||||
var { updateGroups, history } = props;
|
||||
|
||||
if (!groups_data || !user_data) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
const dispatchGroupsData = (data) => {
|
||||
const dispatchPageChange = (data, page) => {
|
||||
dispatch({
|
||||
type: "GROUPS_DATA",
|
||||
value: data,
|
||||
type: "GROUPS_PAGE",
|
||||
value: {
|
||||
data: data,
|
||||
page: page,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const dispatchUserData = (data) => {
|
||||
dispatch({
|
||||
type: "USER_DATA",
|
||||
value: data,
|
||||
if (groups_page != page) {
|
||||
updateGroups(...slice).then((data) => {
|
||||
dispatchPageChange(data, page);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
@@ -40,37 +49,39 @@ const Groups = (props) => {
|
||||
<h4>Groups</h4>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
{groups_data.length > 0 ? (
|
||||
groups_data.map((e, i) => (
|
||||
<div key={"group-edit" + i} className="group-edit-link">
|
||||
<h4>
|
||||
<ul className="list-group">
|
||||
{groups_data.length > 0 ? (
|
||||
groups_data.map((e, i) => (
|
||||
<li className="list-group-item" key={"group-item" + i}>
|
||||
<span className="badge badge-pill badge-success">
|
||||
{e.users.length + " users"}
|
||||
</span>
|
||||
<Link
|
||||
to={{
|
||||
pathname: "/group-edit",
|
||||
state: {
|
||||
group_data: e,
|
||||
user_data: user_data,
|
||||
callback: () => {
|
||||
refreshGroupsData()
|
||||
.then((data) => dispatchGroupsData(data))
|
||||
.catch((err) => console.log(err));
|
||||
refreshUserData()
|
||||
.then((data) => dispatchUserData(data))
|
||||
.catch((err) => console.log(err));
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{e.name}
|
||||
</Link>
|
||||
</h4>
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<div>
|
||||
<h4>no groups created...</h4>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div>
|
||||
<h4>no groups created...</h4>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</ul>
|
||||
<PaginationFooter
|
||||
endpoint="/groups"
|
||||
page={page}
|
||||
limit={limit}
|
||||
numOffset={slice[0]}
|
||||
numElements={groups_data.length}
|
||||
/>
|
||||
</div>
|
||||
<div className="panel-footer">
|
||||
<button className="btn btn-light adjacent-span-spacing">
|
||||
@@ -95,11 +106,14 @@ const Groups = (props) => {
|
||||
Groups.propTypes = {
|
||||
user_data: PropTypes.array,
|
||||
groups_data: PropTypes.array,
|
||||
refreshUserData: PropTypes.func,
|
||||
refreshGroupsData: PropTypes.func,
|
||||
updateUsers: PropTypes.func,
|
||||
updateGroups: PropTypes.func,
|
||||
history: PropTypes.shape({
|
||||
push: PropTypes.func,
|
||||
}),
|
||||
location: PropTypes.shape({
|
||||
search: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
export default Groups;
|
||||
|
50
jsx/src/components/PaginationFooter/PaginationFooter.jsx
Normal file
50
jsx/src/components/PaginationFooter/PaginationFooter.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import "./pagination-footer.css";
|
||||
|
||||
const PaginationFooter = (props) => {
|
||||
let { endpoint, page, limit, numOffset, numElements } = props;
|
||||
return (
|
||||
<div className="pagination-footer">
|
||||
<p>
|
||||
Displaying {numOffset}-{numOffset + numElements}
|
||||
<br></br>
|
||||
<br></br>
|
||||
{page >= 1 ? (
|
||||
<button className="btn btn-sm btn-light spaced">
|
||||
<Link to={`${endpoint}?page=${page - 1}`}>
|
||||
<span className="active-pagination">Previous</span>
|
||||
</Link>
|
||||
</button>
|
||||
) : (
|
||||
<button className="btn btn-sm btn-light spaced">
|
||||
<span className="inactive-pagination">Previous</span>
|
||||
</button>
|
||||
)}
|
||||
{numElements >= limit ? (
|
||||
<button className="btn btn-sm btn-light spaced">
|
||||
<Link to={`${endpoint}?page=${page + 1}`}>
|
||||
<span className="active-pagination">Next</span>
|
||||
</Link>
|
||||
</button>
|
||||
) : (
|
||||
<button className="btn btn-sm btn-light spaced">
|
||||
<span className="inactive-pagination">Next</span>
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PaginationFooter.propTypes = {
|
||||
endpoint: PropTypes.string,
|
||||
page: PropTypes.number,
|
||||
limit: PropTypes.number,
|
||||
numOffset: PropTypes.number,
|
||||
numElements: PropTypes.number,
|
||||
};
|
||||
|
||||
export default PaginationFooter;
|
14
jsx/src/components/PaginationFooter/pagination-footer.css
Normal file
14
jsx/src/components/PaginationFooter/pagination-footer.css
Normal file
@@ -0,0 +1,14 @@
|
||||
@import url(../../style/root.css);
|
||||
|
||||
.pagination-footer * button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.pagination-footer * .inactive-pagination {
|
||||
color: gray;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pagination-footer * button.spaced {
|
||||
color: var(--blue);
|
||||
}
|
@@ -10,6 +10,7 @@ import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
|
||||
import "./server-dashboard.css";
|
||||
import { timeSince } from "../../util/timeSince";
|
||||
import { jhapiRequest } from "../../util/jhapiUtil";
|
||||
import PaginationFooter from "../PaginationFooter/PaginationFooter";
|
||||
|
||||
const ServerDashboard = (props) => {
|
||||
// sort methods
|
||||
@@ -30,10 +31,13 @@ const ServerDashboard = (props) => {
|
||||
|
||||
var [sortMethod, setSortMethod] = useState(null);
|
||||
|
||||
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"));
|
||||
var user_data = useSelector((state) => state.user_data),
|
||||
user_page = useSelector((state) => state.user_page),
|
||||
limit = useSelector((state) => state.limit),
|
||||
page = parseInt(new URLSearchParams(props.location.search).get("page"));
|
||||
|
||||
console.log(user_page);
|
||||
|
||||
page = isNaN(page) ? 0 : page;
|
||||
var slice = [page * limit, limit];
|
||||
|
||||
@@ -49,14 +53,7 @@ const ServerDashboard = (props) => {
|
||||
history,
|
||||
} = props;
|
||||
|
||||
var dispatchUserUpdate = (data) => {
|
||||
dispatch({
|
||||
type: "USER_DATA",
|
||||
value: data,
|
||||
});
|
||||
};
|
||||
|
||||
var dispatchPageChange = (data, page) => {
|
||||
var dispatchPageUpdate = (data, page) => {
|
||||
dispatch({
|
||||
type: "USER_PAGE",
|
||||
value: {
|
||||
@@ -71,9 +68,7 @@ const ServerDashboard = (props) => {
|
||||
}
|
||||
|
||||
if (page != user_page) {
|
||||
updateUsers(...slice)
|
||||
.then((data) => data.json())
|
||||
.then((data) => dispatchPageChange(data, page));
|
||||
updateUsers(...slice).then((data) => dispatchPageUpdate(data, page));
|
||||
}
|
||||
|
||||
if (sortMethod != null) {
|
||||
@@ -138,9 +133,8 @@ const ServerDashboard = (props) => {
|
||||
Promise.all(startAll(user_data.map((e) => e.name)))
|
||||
.then((res) => {
|
||||
updateUsers(...slice)
|
||||
.then((data) => data.json())
|
||||
.then((data) => {
|
||||
dispatchUserUpdate(data);
|
||||
dispatchPageUpdate(data, page);
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
return res;
|
||||
@@ -159,9 +153,8 @@ const ServerDashboard = (props) => {
|
||||
Promise.all(stopAll(user_data.map((e) => e.name)))
|
||||
.then((res) => {
|
||||
updateUsers(...slice)
|
||||
.then((data) => data.json())
|
||||
.then((data) => {
|
||||
dispatchUserUpdate(data);
|
||||
dispatchPageUpdate(data, page);
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
return res;
|
||||
@@ -198,11 +191,9 @@ const ServerDashboard = (props) => {
|
||||
onClick={() =>
|
||||
stopServer(e.name)
|
||||
.then((res) => {
|
||||
updateUsers(...slice)
|
||||
.then((data) => data.json())
|
||||
.then((data) => {
|
||||
dispatchUserUpdate(data);
|
||||
});
|
||||
updateUsers(...slice).then((data) => {
|
||||
dispatchPageUpdate(data, page);
|
||||
});
|
||||
return res;
|
||||
})
|
||||
.catch((err) => console.log(err))
|
||||
@@ -217,11 +208,9 @@ const ServerDashboard = (props) => {
|
||||
onClick={() =>
|
||||
startServer(e.name)
|
||||
.then((res) => {
|
||||
updateUsers(...slice)
|
||||
.then((data) => data.json())
|
||||
.then((data) => {
|
||||
dispatchUserUpdate(data);
|
||||
});
|
||||
updateUsers(...slice).then((data) => {
|
||||
dispatchPageUpdate(data, page);
|
||||
});
|
||||
return res;
|
||||
})
|
||||
.catch((err) => console.log(err))
|
||||
@@ -253,24 +242,13 @@ const ServerDashboard = (props) => {
|
||||
))}
|
||||
</tbody>
|
||||
</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>
|
||||
<PaginationFooter
|
||||
endpoint="/"
|
||||
page={page}
|
||||
limit={limit}
|
||||
numOffset={slice[0]}
|
||||
numElements={user_data.length}
|
||||
/>
|
||||
<br></br>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user