mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-11 03:52:59 +00:00
Add UI pagination, update Redux and API service lib
This commit is contained in:
@@ -12,3 +12,53 @@ admin dashboard codebase.
|
|||||||
- `yarn lint`: Lints JSX with ESLint
|
- `yarn lint`: Lints JSX with ESLint
|
||||||
- `yarn lint --fix`: Lints and fixes errors JSX with ESLint / formats with Prettier
|
- `yarn lint --fix`: Lints and fixes errors JSX with ESLint / formats with Prettier
|
||||||
- `yarn place`: Copies the transpiled React bundle to /share/jupyterhub/static/js/admin-react.js for use.
|
- `yarn place`: Copies the transpiled React bundle to /share/jupyterhub/static/js/admin-react.js for use.
|
||||||
|
|
||||||
|
### Good To Know
|
||||||
|
|
||||||
|
Just some basics on how the React Admin app is built.
|
||||||
|
|
||||||
|
#### General build structure:
|
||||||
|
|
||||||
|
This app is written in JSX, and then transpiled into an ES5 bundle with Babel and Webpack. All JSX components are unit tested with a mixture of Jest and Enzyme and can be run both manually and per-commit. Most logic is separated into components under the `/src/components` directory, each directory containing a `.jsx`, `.test.jsx`, and sometimes a `.css` file. These components are all pulled together, given client-side routes, and connected to the Redux store in `/src/App.jsx` which serves as an entrypoint to the application.
|
||||||
|
|
||||||
|
#### Centralized state and data management with Redux:
|
||||||
|
|
||||||
|
The app use Redux throughout the components via the `useSelector` and `useDispatch` hooks to store and update user and group data from the API. With Redux, this data is available to any connected component. This means that if one component recieves new data, they all do.
|
||||||
|
|
||||||
|
### API functions
|
||||||
|
|
||||||
|
All API functions used by the front end are packaged as a library of props within `/src/util/withAPI.js`. This keeps our web service logic separate from our presentational logic, allowing us to connect API functionality to our components at a high level and keep the code more modular. This connection specifically happens in `/src/App.jsx`, within the route assignments.
|
||||||
|
|
||||||
|
### Pagination
|
||||||
|
|
||||||
|
Indicies of paginated user and group data is stored in a `page` variable in the query string, as well as the `user_page` / `group_page` state variables in Redux. This allows the app to maintain two sources of truth, as well as protect the admin user's place in the collection on page reload. Limit is constant at this point and is held in the Redux state.
|
||||||
|
|
||||||
|
On updates to the paginated data, the app can respond in one of two ways. If a user/group record is either added or deleted, the pagination will reset and data will be pulled back with no offset. Alternatively, if a record is modified, the offset will remain and the change will be shown.
|
||||||
|
|
||||||
|
Code examples:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Pagination limit is pulled in from Redux.
|
||||||
|
var limit = useSelector((state) => state.limit);
|
||||||
|
|
||||||
|
// Page query string is parsed and checked
|
||||||
|
var page = parseInt(new URLQuerySearch(props.location).get("page"));
|
||||||
|
page = isNaN(page) ? 0 : page;
|
||||||
|
|
||||||
|
// A slice is created representing the records to be returned
|
||||||
|
var slice = [page * limit, limit];
|
||||||
|
|
||||||
|
// A user's notebook server status was changed from stopped to running, user data is being refreshed from the slice.
|
||||||
|
startServer().then(() => {
|
||||||
|
updateUsers(...slice)
|
||||||
|
// After data is fetched, the Redux store is updated with the data and a copy of the page number.
|
||||||
|
.then((data) => dispatchPageChange(data, page));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Alternatively, a new user was added, user data is being refreshed from offset 0.
|
||||||
|
addUser().then(() => {
|
||||||
|
updateUsers(0, limit)
|
||||||
|
// After data is fetched, the Redux store is updated with the data and asserts page 0.
|
||||||
|
.then((data) => dispatchPageChange(data, 0));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
@@ -5,25 +5,33 @@ export const initialState = {
|
|||||||
user_page: 0,
|
user_page: 0,
|
||||||
groups_data: undefined,
|
groups_data: undefined,
|
||||||
groups_page: 0,
|
groups_page: 0,
|
||||||
limit: 50,
|
limit: 3,
|
||||||
manage_groups_modal: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const reducers = (state = initialState, action) => {
|
export const reducers = (state = initialState, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "USER_DATA":
|
// Updates the client user model data and stores the page
|
||||||
return Object.assign({}, state, { user_data: action.value });
|
|
||||||
case "USER_PAGE":
|
case "USER_PAGE":
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
user_page: action.value.page,
|
user_page: action.value.page,
|
||||||
user_data: action.value.data,
|
user_data: action.value.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Deprecated - doesn't store pagination values
|
||||||
|
case "USER_DATA":
|
||||||
|
return Object.assign({}, state, { user_data: action.value });
|
||||||
|
|
||||||
|
// Updates the client group model data and stores the page
|
||||||
|
case "GROUPS_PAGE":
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
groups_page: action.value.page,
|
||||||
|
groups_data: action.value.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deprecated - doesn't store pagination values
|
||||||
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":
|
|
||||||
return Object.assign({}, state, {
|
|
||||||
manage_groups_modal: !state.manage_groups_modal,
|
|
||||||
});
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
1
jsx/src/TODO
Normal file
1
jsx/src/TODO
Normal file
@@ -0,0 +1 @@
|
|||||||
|
- When changing route with nothing edited, pass user_data / group_data through location to maintain spot
|
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { compose, withProps } from "recompose";
|
import { compose, withProps } from "recompose";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
@@ -7,18 +7,22 @@ import { jhapiRequest } from "../../util/jhapiUtil";
|
|||||||
|
|
||||||
const AddUser = (props) => {
|
const AddUser = (props) => {
|
||||||
var [users, setUsers] = useState([]),
|
var [users, setUsers] = useState([]),
|
||||||
[admin, setAdmin] = useState(false);
|
[admin, setAdmin] = useState(false),
|
||||||
|
limit = useSelector((state) => state.limit);
|
||||||
|
|
||||||
var dispatch = useDispatch();
|
var dispatch = useDispatch();
|
||||||
|
|
||||||
var dispatchUserData = (data) => {
|
var dispatchPageChange = (data, page) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "USER_DATA",
|
type: "USER_PAGE",
|
||||||
value: data,
|
value: {
|
||||||
|
data: data,
|
||||||
|
page: page,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var { addUsers, failRegexEvent, refreshUserData, history } = props;
|
var { addUsers, failRegexEvent, updateUsers, history } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -78,12 +82,12 @@ const AddUser = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addUsers(filtered_users, admin)
|
addUsers(filtered_users, admin)
|
||||||
.then(
|
.then(() =>
|
||||||
refreshUserData()
|
updateUsers(0, limit)
|
||||||
.then((data) => dispatchUserData(data))
|
.then((data) => dispatchPageChange(data, 0))
|
||||||
|
.then(() => history.push("/"))
|
||||||
.catch((err) => console.log(err))
|
.catch((err) => console.log(err))
|
||||||
)
|
)
|
||||||
.then(() => history.push("/"))
|
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -101,7 +105,7 @@ const AddUser = (props) => {
|
|||||||
AddUser.propTypes = {
|
AddUser.propTypes = {
|
||||||
addUsers: PropTypes.func,
|
addUsers: PropTypes.func,
|
||||||
failRegexEvent: PropTypes.func,
|
failRegexEvent: PropTypes.func,
|
||||||
refreshUserData: PropTypes.func,
|
updateUsers: PropTypes.func,
|
||||||
history: PropTypes.shape({
|
history: PropTypes.shape({
|
||||||
push: PropTypes.func,
|
push: PropTypes.func,
|
||||||
}),
|
}),
|
||||||
|
@@ -1,23 +1,25 @@
|
|||||||
import React, { useState } from "react";
|
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 { Link } from "react-router-dom";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { jhapiRequest } from "../../util/jhapiUtil";
|
|
||||||
|
|
||||||
const CreateGroup = (props) => {
|
const CreateGroup = (props) => {
|
||||||
var [groupName, setGroupName] = useState("");
|
var [groupName, setGroupName] = useState(""),
|
||||||
|
limit = useSelector((state) => state.limit);
|
||||||
|
|
||||||
var dispatch = useDispatch();
|
var dispatch = useDispatch();
|
||||||
|
|
||||||
var dispatchGroupsData = (data) => {
|
var dispatchPageUpdate = (data, page) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "GROUPS_DATA",
|
type: "GROUPS_DATA",
|
||||||
value: data,
|
value: {
|
||||||
|
data: data,
|
||||||
|
page: page,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var { createGroup, refreshGroupsData, history } = props;
|
var { createGroup, updateGroups, history } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -53,11 +55,11 @@ const CreateGroup = (props) => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
createGroup(groupName)
|
createGroup(groupName)
|
||||||
.then(
|
.then(
|
||||||
refreshGroupsData()
|
updateGroups(0, limit)
|
||||||
.then((data) => dispatchGroupsData(data))
|
.then((data) => dispatchPageUpdate(data, 0))
|
||||||
|
.then(history.push("/groups"))
|
||||||
.catch((err) => console.log(err))
|
.catch((err) => console.log(err))
|
||||||
)
|
)
|
||||||
.then(history.push("/groups"))
|
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -74,7 +76,7 @@ const CreateGroup = (props) => {
|
|||||||
|
|
||||||
CreateGroup.propTypes = {
|
CreateGroup.propTypes = {
|
||||||
createGroup: PropTypes.func,
|
createGroup: PropTypes.func,
|
||||||
refreshGroupsData: PropTypes.func,
|
updateGroups: PropTypes.func,
|
||||||
failRegexEvent: PropTypes.func,
|
failRegexEvent: PropTypes.func,
|
||||||
history: PropTypes.shape({
|
history: PropTypes.shape({
|
||||||
push: PropTypes.func,
|
push: PropTypes.func,
|
||||||
|
@@ -1,15 +1,20 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
const EditUser = (props) => {
|
const EditUser = (props) => {
|
||||||
|
var limit = useSelector((state) => state.limit);
|
||||||
|
|
||||||
var dispatch = useDispatch();
|
var dispatch = useDispatch();
|
||||||
|
|
||||||
var dispatchUserData = (data) => {
|
var dispatchPageChange = (data, page) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "USER_DATA",
|
type: "USER_PAGE",
|
||||||
value: data,
|
value: {
|
||||||
|
data: data,
|
||||||
|
page: page,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -18,7 +23,7 @@ const EditUser = (props) => {
|
|||||||
deleteUser,
|
deleteUser,
|
||||||
failRegexEvent,
|
failRegexEvent,
|
||||||
noChangeEvent,
|
noChangeEvent,
|
||||||
refreshUserData,
|
updateUsers,
|
||||||
history,
|
history,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -70,9 +75,9 @@ const EditUser = (props) => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
deleteUser(username)
|
deleteUser(username)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
history.push("/");
|
updateUsers(0, limit)
|
||||||
refreshUserData()
|
.then((data) => dispatchPageChange(data, 0))
|
||||||
.then((data) => dispatchUserData(data))
|
.then(() => history.push("/"))
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
})
|
})
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
@@ -106,9 +111,9 @@ const EditUser = (props) => {
|
|||||||
admin
|
admin
|
||||||
)
|
)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
history.push("/");
|
updateUsers(0, limit)
|
||||||
refreshUserData()
|
.then((data) => dispatchPageChange(data, 0))
|
||||||
.then((data) => dispatchUserData(data))
|
.then(() => history.push("/"))
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
})
|
})
|
||||||
.catch((err) => {});
|
.catch((err) => {});
|
||||||
@@ -119,9 +124,9 @@ const EditUser = (props) => {
|
|||||||
} else {
|
} else {
|
||||||
editUser(username, username, admin)
|
editUser(username, username, admin)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
history.push("/");
|
updateUsers(0, limit)
|
||||||
refreshUserData()
|
.then((data) => dispatchPageChange(data, 0))
|
||||||
.then((data) => dispatchUserData(data))
|
.then(() => history.push("/"))
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
})
|
})
|
||||||
.catch((err) => {});
|
.catch((err) => {});
|
||||||
@@ -153,7 +158,7 @@ EditUser.propTypes = {
|
|||||||
deleteUser: PropTypes.func,
|
deleteUser: PropTypes.func,
|
||||||
failRegexEvent: PropTypes.func,
|
failRegexEvent: PropTypes.func,
|
||||||
noChangeEvent: PropTypes.func,
|
noChangeEvent: PropTypes.func,
|
||||||
refreshUserData: PropTypes.func,
|
updateUsers: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditUser;
|
export default EditUser;
|
||||||
|
@@ -10,14 +10,18 @@ const GroupEdit = (props) => {
|
|||||||
var [selected, setSelected] = useState([]),
|
var [selected, setSelected] = useState([]),
|
||||||
[changed, setChanged] = useState(false),
|
[changed, setChanged] = useState(false),
|
||||||
[added, setAdded] = useState(undefined),
|
[added, setAdded] = useState(undefined),
|
||||||
[removed, setRemoved] = useState(undefined);
|
[removed, setRemoved] = useState(undefined),
|
||||||
|
limit = useSelector((state) => state.limit);
|
||||||
|
|
||||||
var dispatch = useDispatch();
|
var dispatch = useDispatch();
|
||||||
|
|
||||||
const dispatchGroupsData = (data) => {
|
const dispatchPageUpdate = (data, page) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "GROUPS_DATA",
|
type: "GROUPS_PAGE",
|
||||||
value: data,
|
value: {
|
||||||
|
data: data,
|
||||||
|
page: page,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,7 +29,7 @@ const GroupEdit = (props) => {
|
|||||||
addToGroup,
|
addToGroup,
|
||||||
removeFromGroup,
|
removeFromGroup,
|
||||||
deleteGroup,
|
deleteGroup,
|
||||||
refreshGroupsData,
|
updateGroups,
|
||||||
history,
|
history,
|
||||||
location,
|
location,
|
||||||
} = props;
|
} = props;
|
||||||
@@ -88,10 +92,12 @@ const GroupEdit = (props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Promise.all(promiseQueue)
|
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));
|
.catch((err) => console.log(err));
|
||||||
|
|
||||||
history.push("/groups");
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Apply
|
Apply
|
||||||
@@ -103,10 +109,11 @@ const GroupEdit = (props) => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
var groupName = group_data.name;
|
var groupName = group_data.name;
|
||||||
deleteGroup(groupName)
|
deleteGroup(groupName)
|
||||||
.then(
|
.then((e) => {
|
||||||
refreshGroupsData().then((data) => dispatchGroupsData(data))
|
updateGroups(0, limit)
|
||||||
)
|
.then((data) => dispatchPageUpdate(data, 0))
|
||||||
.then(history.push("/groups"))
|
.then(() => history.push("/groups"));
|
||||||
|
})
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -134,7 +141,7 @@ GroupEdit.propTypes = {
|
|||||||
addToGroup: PropTypes.func,
|
addToGroup: PropTypes.func,
|
||||||
removeFromGroup: PropTypes.func,
|
removeFromGroup: PropTypes.func,
|
||||||
deleteGroup: PropTypes.func,
|
deleteGroup: PropTypes.func,
|
||||||
refreshGroupsData: PropTypes.func,
|
updateGroups: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GroupEdit;
|
export default GroupEdit;
|
||||||
|
@@ -4,32 +4,41 @@ import { compose, withProps } from "recompose";
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { jhapiRequest } from "../../util/jhapiUtil";
|
import PaginationFooter from "../PaginationFooter/PaginationFooter";
|
||||||
|
|
||||||
const Groups = (props) => {
|
const Groups = (props) => {
|
||||||
var user_data = useSelector((state) => state.user_data),
|
var user_data = useSelector((state) => state.user_data),
|
||||||
groups_data = useSelector((state) => state.groups_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) {
|
if (!groups_data || !user_data) {
|
||||||
return <div></div>;
|
return <div></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispatchGroupsData = (data) => {
|
const dispatchPageChange = (data, page) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "GROUPS_DATA",
|
type: "GROUPS_PAGE",
|
||||||
value: data,
|
value: {
|
||||||
|
data: data,
|
||||||
|
page: page,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const dispatchUserData = (data) => {
|
if (groups_page != page) {
|
||||||
dispatch({
|
updateGroups(...slice).then((data) => {
|
||||||
type: "USER_DATA",
|
dispatchPageChange(data, page);
|
||||||
value: data,
|
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
@@ -40,37 +49,39 @@ const Groups = (props) => {
|
|||||||
<h4>Groups</h4>
|
<h4>Groups</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-body">
|
<div className="panel-body">
|
||||||
{groups_data.length > 0 ? (
|
<ul className="list-group">
|
||||||
groups_data.map((e, i) => (
|
{groups_data.length > 0 ? (
|
||||||
<div key={"group-edit" + i} className="group-edit-link">
|
groups_data.map((e, i) => (
|
||||||
<h4>
|
<li className="list-group-item" key={"group-item" + i}>
|
||||||
|
<span className="badge badge-pill badge-success">
|
||||||
|
{e.users.length + " users"}
|
||||||
|
</span>
|
||||||
<Link
|
<Link
|
||||||
to={{
|
to={{
|
||||||
pathname: "/group-edit",
|
pathname: "/group-edit",
|
||||||
state: {
|
state: {
|
||||||
group_data: e,
|
group_data: e,
|
||||||
user_data: user_data,
|
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}
|
{e.name}
|
||||||
</Link>
|
</Link>
|
||||||
</h4>
|
</li>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<h4>no groups created...</h4>
|
||||||
</div>
|
</div>
|
||||||
))
|
)}
|
||||||
) : (
|
</ul>
|
||||||
<div>
|
<PaginationFooter
|
||||||
<h4>no groups created...</h4>
|
endpoint="/groups"
|
||||||
</div>
|
page={page}
|
||||||
)}
|
limit={limit}
|
||||||
|
numOffset={slice[0]}
|
||||||
|
numElements={groups_data.length}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-footer">
|
<div className="panel-footer">
|
||||||
<button className="btn btn-light adjacent-span-spacing">
|
<button className="btn btn-light adjacent-span-spacing">
|
||||||
@@ -95,11 +106,14 @@ const Groups = (props) => {
|
|||||||
Groups.propTypes = {
|
Groups.propTypes = {
|
||||||
user_data: PropTypes.array,
|
user_data: PropTypes.array,
|
||||||
groups_data: PropTypes.array,
|
groups_data: PropTypes.array,
|
||||||
refreshUserData: PropTypes.func,
|
updateUsers: PropTypes.func,
|
||||||
refreshGroupsData: PropTypes.func,
|
updateGroups: PropTypes.func,
|
||||||
history: PropTypes.shape({
|
history: PropTypes.shape({
|
||||||
push: PropTypes.func,
|
push: PropTypes.func,
|
||||||
}),
|
}),
|
||||||
|
location: PropTypes.shape({
|
||||||
|
search: PropTypes.string,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Groups;
|
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 "./server-dashboard.css";
|
||||||
import { timeSince } from "../../util/timeSince";
|
import { timeSince } from "../../util/timeSince";
|
||||||
import { jhapiRequest } from "../../util/jhapiUtil";
|
import { jhapiRequest } from "../../util/jhapiUtil";
|
||||||
|
import PaginationFooter from "../PaginationFooter/PaginationFooter";
|
||||||
|
|
||||||
const ServerDashboard = (props) => {
|
const ServerDashboard = (props) => {
|
||||||
// sort methods
|
// sort methods
|
||||||
@@ -30,10 +31,13 @@ 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);
|
user_page = useSelector((state) => state.user_page),
|
||||||
var limit = useSelector((state) => state.limit);
|
limit = useSelector((state) => state.limit),
|
||||||
var page = parseInt(new URLSearchParams(props.location.search).get("page"));
|
page = parseInt(new URLSearchParams(props.location.search).get("page"));
|
||||||
|
|
||||||
|
console.log(user_page);
|
||||||
|
|
||||||
page = isNaN(page) ? 0 : page;
|
page = isNaN(page) ? 0 : page;
|
||||||
var slice = [page * limit, limit];
|
var slice = [page * limit, limit];
|
||||||
|
|
||||||
@@ -49,14 +53,7 @@ const ServerDashboard = (props) => {
|
|||||||
history,
|
history,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
var dispatchUserUpdate = (data) => {
|
var dispatchPageUpdate = (data, page) => {
|
||||||
dispatch({
|
|
||||||
type: "USER_DATA",
|
|
||||||
value: data,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var dispatchPageChange = (data, page) => {
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "USER_PAGE",
|
type: "USER_PAGE",
|
||||||
value: {
|
value: {
|
||||||
@@ -71,9 +68,7 @@ const ServerDashboard = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (page != user_page) {
|
if (page != user_page) {
|
||||||
updateUsers(...slice)
|
updateUsers(...slice).then((data) => dispatchPageUpdate(data, page));
|
||||||
.then((data) => data.json())
|
|
||||||
.then((data) => dispatchPageChange(data, page));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortMethod != null) {
|
if (sortMethod != null) {
|
||||||
@@ -138,9 +133,8 @@ const ServerDashboard = (props) => {
|
|||||||
Promise.all(startAll(user_data.map((e) => e.name)))
|
Promise.all(startAll(user_data.map((e) => e.name)))
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
updateUsers(...slice)
|
updateUsers(...slice)
|
||||||
.then((data) => data.json())
|
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
dispatchUserUpdate(data);
|
dispatchPageUpdate(data, page);
|
||||||
})
|
})
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
return res;
|
return res;
|
||||||
@@ -159,9 +153,8 @@ const ServerDashboard = (props) => {
|
|||||||
Promise.all(stopAll(user_data.map((e) => e.name)))
|
Promise.all(stopAll(user_data.map((e) => e.name)))
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
updateUsers(...slice)
|
updateUsers(...slice)
|
||||||
.then((data) => data.json())
|
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
dispatchUserUpdate(data);
|
dispatchPageUpdate(data, page);
|
||||||
})
|
})
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
return res;
|
return res;
|
||||||
@@ -198,11 +191,9 @@ const ServerDashboard = (props) => {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
stopServer(e.name)
|
stopServer(e.name)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
updateUsers(...slice)
|
updateUsers(...slice).then((data) => {
|
||||||
.then((data) => data.json())
|
dispatchPageUpdate(data, page);
|
||||||
.then((data) => {
|
});
|
||||||
dispatchUserUpdate(data);
|
|
||||||
});
|
|
||||||
return res;
|
return res;
|
||||||
})
|
})
|
||||||
.catch((err) => console.log(err))
|
.catch((err) => console.log(err))
|
||||||
@@ -217,11 +208,9 @@ const ServerDashboard = (props) => {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
startServer(e.name)
|
startServer(e.name)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
updateUsers(...slice)
|
updateUsers(...slice).then((data) => {
|
||||||
.then((data) => data.json())
|
dispatchPageUpdate(data, page);
|
||||||
.then((data) => {
|
});
|
||||||
dispatchUserUpdate(data);
|
|
||||||
});
|
|
||||||
return res;
|
return res;
|
||||||
})
|
})
|
||||||
.catch((err) => console.log(err))
|
.catch((err) => console.log(err))
|
||||||
@@ -253,24 +242,13 @@ const ServerDashboard = (props) => {
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<br></br>
|
<PaginationFooter
|
||||||
<p>
|
endpoint="/"
|
||||||
Displaying users {slice[0]}-{slice[0] + user_data.length}
|
page={page}
|
||||||
{user_data.length >= limit ? (
|
limit={limit}
|
||||||
<button className="btn btn-link">
|
numOffset={slice[0]}
|
||||||
<Link to={`/?page=${page + 1}`}>Next</Link>
|
numElements={user_data.length}
|
||||||
</button>
|
/>
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
{page >= 1 ? (
|
|
||||||
<button className="btn btn-link">
|
|
||||||
<Link to={`/?page=${page - 1}`}>Previous</Link>
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<br></br>
|
<br></br>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
--orange: #f1ad4e;
|
--orange: #f1ad4e;
|
||||||
--blue: #2e7ab6;
|
--blue: #2e7ab6;
|
||||||
--white: #ffffff;
|
--white: #ffffff;
|
||||||
--gray: #f7f7f;
|
--gray: #f7f7f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Color Classes */
|
/* Color Classes */
|
||||||
|
@@ -3,7 +3,14 @@ import { jhapiRequest } from "./jhapiUtil";
|
|||||||
|
|
||||||
const withAPI = withProps((props) => ({
|
const withAPI = withProps((props) => ({
|
||||||
updateUsers: (offset, limit) =>
|
updateUsers: (offset, limit) =>
|
||||||
jhapiRequest(`/users?offset=${offset}&limit=${limit}`, "GET"),
|
jhapiRequest(`/users?offset=${offset}&limit=${limit}`, "GET").then((data) =>
|
||||||
|
data.json()
|
||||||
|
),
|
||||||
|
updateGroups: (offset, limit) =>
|
||||||
|
jhapiRequest(
|
||||||
|
`/groups?offset=${offset}&limit=${limit}`,
|
||||||
|
"GET"
|
||||||
|
).then((data) => data.json()),
|
||||||
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