mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-18 15:33:02 +00:00
server-side sorting of admin page
removes in-page sort, which removes sort by server name, sort by running Running column switches from sort to filter, matching the `?state` query parameter in the API needs some CSS on the column widths to avoid jumps when toggling active servers
This commit is contained in:
13
jsx/package-lock.json
generated
13
jsx/package-lock.json
generated
@@ -14,7 +14,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^17.0.2",
|
||||
"react-bootstrap": "^2.7.4",
|
||||
"react-bootstrap": "^2.10.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-icons": "^4.8.0",
|
||||
"react-multi-select-component": "^4.3.4",
|
||||
@@ -8209,13 +8209,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-bootstrap": {
|
||||
"version": "2.7.4",
|
||||
"license": "MIT",
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.1.tgz",
|
||||
"integrity": "sha512-J3OpRZIvCTQK+Tg/jOkRUvpYLHMdGeU9KqFUBQrV0d/Qr/3nsINpiOJyZMWnM5SJ3ctZdhPA6eCIKpEJR3Ellg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0",
|
||||
"@babel/runtime": "^7.22.5",
|
||||
"@restart/hooks": "^0.4.9",
|
||||
"@restart/ui": "^1.6.3",
|
||||
"@types/react-transition-group": "^4.4.5",
|
||||
"@restart/ui": "^1.6.6",
|
||||
"@types/react-transition-group": "^4.4.6",
|
||||
"classnames": "^2.3.2",
|
||||
"dom-helpers": "^5.2.1",
|
||||
"invariant": "^2.2.4",
|
||||
|
@@ -34,7 +34,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^17.0.2",
|
||||
"react-bootstrap": "^2.7.4",
|
||||
"react-bootstrap": "^2.10.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-icons": "^4.8.0",
|
||||
"react-multi-select-component": "^4.3.4",
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
Button,
|
||||
Col,
|
||||
Row,
|
||||
Form,
|
||||
FormControl,
|
||||
Card,
|
||||
CardGroup,
|
||||
@@ -32,21 +33,6 @@ RowListItem.propTypes = {
|
||||
const ServerDashboard = (props) => {
|
||||
const base_url = window.base_url || "/";
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
// sort methods
|
||||
const usernameDesc = (e) => e.sort((a, b) => (a.name > b.name ? 1 : -1)),
|
||||
usernameAsc = (e) => e.sort((a, b) => (a.name < b.name ? 1 : -1)),
|
||||
adminDesc = (e) => e.sort((a) => (a.admin ? -1 : 1)),
|
||||
adminAsc = (e) => e.sort((a) => (a.admin ? 1 : -1)),
|
||||
dateDesc = (e) =>
|
||||
e.sort((a, b) =>
|
||||
new Date(a.last_activity) - new Date(b.last_activity) > 0 ? -1 : 1,
|
||||
),
|
||||
dateAsc = (e) =>
|
||||
e.sort((a, b) =>
|
||||
new Date(a.last_activity) - new Date(b.last_activity) > 0 ? 1 : -1,
|
||||
),
|
||||
runningAsc = (e) => e.sort((a) => (a.server == null ? -1 : 1)),
|
||||
runningDesc = (e) => e.sort((a) => (a.server == null ? 1 : -1));
|
||||
|
||||
const [errorAlert, setErrorAlert] = useState(null);
|
||||
const [sortMethod, setSortMethod] = useState(null);
|
||||
@@ -59,6 +45,8 @@ const ServerDashboard = (props) => {
|
||||
usePaginationParams();
|
||||
|
||||
const name_filter = searchParams.get("name_filter") || "";
|
||||
const sort = searchParams.get("sort") || "id";
|
||||
const state_filter = searchParams.get("state") || "";
|
||||
|
||||
const total = user_page ? user_page.total : undefined;
|
||||
|
||||
@@ -76,7 +64,13 @@ const ServerDashboard = (props) => {
|
||||
} = props;
|
||||
|
||||
const dispatchPageUpdate = (data, page) => {
|
||||
// trigger page update in state
|
||||
// in response to fetching updated user list
|
||||
// data is list of user records
|
||||
// page is _pagination part of response
|
||||
// persist page info in url query
|
||||
setPagination(page);
|
||||
// persist user data, triggers rerender
|
||||
dispatch({
|
||||
type: "USER_PAGE",
|
||||
value: {
|
||||
@@ -87,6 +81,8 @@ const ServerDashboard = (props) => {
|
||||
};
|
||||
|
||||
const setNameFilter = (new_name_filter) => {
|
||||
// persist ?name_filter parameter
|
||||
// store in url param, clear when value is empty
|
||||
setSearchParams((params) => {
|
||||
// clear offset when name filter changes
|
||||
if (new_name_filter !== name_filter) {
|
||||
@@ -101,26 +97,56 @@ const ServerDashboard = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const setSort = (sort) => {
|
||||
// persist ?sort parameter
|
||||
// store in url param, clear when value is default ('id')
|
||||
setSearchParams((params) => {
|
||||
if (sort === "id") {
|
||||
params.delete("id");
|
||||
} else {
|
||||
params.set("sort", sort);
|
||||
}
|
||||
return params;
|
||||
});
|
||||
};
|
||||
|
||||
const setStateFilter = (state_filter) => {
|
||||
// persist ?state filter
|
||||
// store in url param, clear when value is default ('')
|
||||
setSearchParams((params) => {
|
||||
if (!state_filter) {
|
||||
params.delete("state");
|
||||
} else {
|
||||
params.set("state", state_filter);
|
||||
}
|
||||
return params;
|
||||
});
|
||||
};
|
||||
|
||||
// the callback to update the displayed user list
|
||||
const updateUsersWithParams = () =>
|
||||
updateUsers({
|
||||
offset,
|
||||
limit,
|
||||
name_filter,
|
||||
sort,
|
||||
state: state_filter,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
updateUsers(offset, limit, name_filter)
|
||||
updateUsersWithParams()
|
||||
.then((data) => dispatchPageUpdate(data.items, data._pagination))
|
||||
.catch((err) => setErrorAlert("Failed to update user list."));
|
||||
}, [offset, limit, name_filter]);
|
||||
}, [offset, limit, name_filter, sort, state_filter]);
|
||||
|
||||
if (!user_data || !user_page) {
|
||||
return <div data-testid="no-show"></div>;
|
||||
}
|
||||
|
||||
var slice = [offset, limit, name_filter];
|
||||
|
||||
const handleSearch = debounce(async (event) => {
|
||||
setNameFilter(event.target.value);
|
||||
}, 300);
|
||||
|
||||
if (sortMethod != null) {
|
||||
user_data = sortMethod(user_data);
|
||||
}
|
||||
|
||||
const ServerButton = ({ server, user, action, name, extraClass }) => {
|
||||
var [isDisabled, setIsDisabled] = useState(false);
|
||||
return (
|
||||
@@ -132,7 +158,7 @@ const ServerDashboard = (props) => {
|
||||
action(user.name, server.name)
|
||||
.then((res) => {
|
||||
if (res.status < 300) {
|
||||
updateUsers(...slice)
|
||||
updateUsersWithParams()
|
||||
.then((data) => {
|
||||
dispatchPageUpdate(data.items, data._pagination);
|
||||
})
|
||||
@@ -417,42 +443,39 @@ const ServerDashboard = (props) => {
|
||||
<th id="user-header">
|
||||
User{" "}
|
||||
<SortHandler
|
||||
sorts={{ asc: usernameAsc, desc: usernameDesc }}
|
||||
callback={(method) => setSortMethod(() => method)}
|
||||
currentSort={sort}
|
||||
setSort={setSort}
|
||||
sortKey="name"
|
||||
testid="user-sort"
|
||||
/>
|
||||
</th>
|
||||
<th id="admin-header">
|
||||
Admin{" "}
|
||||
<SortHandler
|
||||
sorts={{ asc: adminAsc, desc: adminDesc }}
|
||||
callback={(method) => setSortMethod(() => method)}
|
||||
testid="admin-sort"
|
||||
/>
|
||||
</th>
|
||||
<th id="server-header">
|
||||
Server{" "}
|
||||
<SortHandler
|
||||
sorts={{ asc: usernameAsc, desc: usernameDesc }}
|
||||
callback={(method) => setSortMethod(() => method)}
|
||||
testid="server-sort"
|
||||
/>
|
||||
</th>
|
||||
<th id="admin-header">Admin</th>
|
||||
<th id="server-header">Server</th>
|
||||
<th id="last-activity-header">
|
||||
Last Activity{" "}
|
||||
<SortHandler
|
||||
sorts={{ asc: dateAsc, desc: dateDesc }}
|
||||
callback={(method) => setSortMethod(() => method)}
|
||||
currentSort={sort}
|
||||
setSort={setSort}
|
||||
sortKey="last_activity"
|
||||
testid="last-activity-sort"
|
||||
/>
|
||||
</th>
|
||||
<th id="running-status-header">
|
||||
Running{" "}
|
||||
<SortHandler
|
||||
sorts={{ asc: runningAsc, desc: runningDesc }}
|
||||
callback={(method) => setSortMethod(() => method)}
|
||||
testid="running-status-sort"
|
||||
/>
|
||||
<label title="only show active servers">
|
||||
<Form.Check
|
||||
inline
|
||||
type="checkbox"
|
||||
name="active_servers"
|
||||
aria-label="only show active servers"
|
||||
checked={state_filter == "active"}
|
||||
onChange={(event) => {
|
||||
setStateFilter(event.target.checked ? "active" : null);
|
||||
}}
|
||||
/>
|
||||
{state_filter == "active"
|
||||
? " Active servers"
|
||||
: " All servers"}
|
||||
</label>
|
||||
</th>
|
||||
<th id="actions-header">Actions</th>
|
||||
</tr>
|
||||
@@ -490,7 +513,7 @@ const ServerDashboard = (props) => {
|
||||
return res;
|
||||
})
|
||||
.then((res) => {
|
||||
updateUsers(...slice)
|
||||
updateUsersWithParams()
|
||||
.then((data) => {
|
||||
dispatchPageUpdate(data.items, data._pagination);
|
||||
})
|
||||
@@ -526,7 +549,7 @@ const ServerDashboard = (props) => {
|
||||
return res;
|
||||
})
|
||||
.then((res) => {
|
||||
updateUsers(...slice)
|
||||
updateUsersWithParams()
|
||||
.then((data) => {
|
||||
dispatchPageUpdate(data.items, data._pagination);
|
||||
})
|
||||
@@ -590,30 +613,27 @@ ServerDashboard.propTypes = {
|
||||
};
|
||||
|
||||
const SortHandler = (props) => {
|
||||
var { sorts, callback, testid } = props;
|
||||
|
||||
var [direction, setDirection] = useState(undefined);
|
||||
const { currentSort, setSort, sortKey, testid } = props;
|
||||
|
||||
const currentlySorted = currentSort && currentSort.endsWith(sortKey);
|
||||
const descending = currentSort && currentSort.startsWith("-");
|
||||
return (
|
||||
<div
|
||||
className="sort-icon"
|
||||
data-testid={testid}
|
||||
onClick={() => {
|
||||
if (!direction) {
|
||||
callback(sorts.desc);
|
||||
setDirection("desc");
|
||||
} else if (direction == "asc") {
|
||||
callback(sorts.desc);
|
||||
setDirection("desc");
|
||||
if (!currentlySorted) {
|
||||
setSort(sortKey);
|
||||
} else if (descending) {
|
||||
setSort(sortKey);
|
||||
} else {
|
||||
callback(sorts.asc);
|
||||
setDirection("asc");
|
||||
setSort("-" + sortKey);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!direction ? (
|
||||
{!currentlySorted ? (
|
||||
<FaSort />
|
||||
) : direction == "asc" ? (
|
||||
) : descending ? (
|
||||
<FaSortDown />
|
||||
) : (
|
||||
<FaSortUp />
|
||||
@@ -623,8 +643,9 @@ const SortHandler = (props) => {
|
||||
};
|
||||
|
||||
SortHandler.propTypes = {
|
||||
sorts: PropTypes.object,
|
||||
callback: PropTypes.func,
|
||||
currentSort: PropTypes.string,
|
||||
setSort: PropTypes.func,
|
||||
sortKey: PropTypes.string,
|
||||
testid: PropTypes.string,
|
||||
};
|
||||
|
||||
|
@@ -46,3 +46,51 @@ tr.noborder > td {
|
||||
.user-row .actions > * {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.form-check-inline {
|
||||
/* inline form check doesn't get inline css
|
||||
I _think_ this is because our bootstrap css is outdated
|
||||
*/
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* column widths for dashboard
|
||||
|
||||
goals:
|
||||
|
||||
- want stable width for running-status
|
||||
so clicking the running filter doesn't cause a jump
|
||||
- shrink fixed-content columns (action, admin)
|
||||
- allow variable content columns (username, server name)
|
||||
to claim remaining space
|
||||
|
||||
*/
|
||||
|
||||
.admin-table-head label {
|
||||
/* clear margin-bottom to keep label on baseline */
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.admin-table-head #user-header {
|
||||
}
|
||||
.admin-table-head #admin-header {
|
||||
width: 64px;
|
||||
}
|
||||
.admin-table-head #server-header {
|
||||
}
|
||||
.admin-table-head #last-activity-header {
|
||||
width: 180px;
|
||||
}
|
||||
.admin-table-head #running-status-header {
|
||||
width: 300px;
|
||||
}
|
||||
.admin-table-head #actions-header {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
/* vertical stack server buttons on small windows */
|
||||
@media (max-width: 991px) {
|
||||
.admin-table-head #running-status-header {
|
||||
width: 140px;
|
||||
}
|
||||
}
|
||||
|
@@ -2,13 +2,16 @@ import { withProps } from "recompose";
|
||||
import { jhapiRequest } from "./jhapiUtil";
|
||||
|
||||
const withAPI = withProps(() => ({
|
||||
updateUsers: (offset, limit, name_filter) =>
|
||||
jhapiRequest(
|
||||
`/users?include_stopped_servers&offset=${offset}&limit=${limit}&name_filter=${
|
||||
name_filter || ""
|
||||
}`,
|
||||
"GET",
|
||||
).then((data) => data.json()),
|
||||
updateUsers: (options) => {
|
||||
let params = new URLSearchParams();
|
||||
params["include_stopped_servers"] = "1";
|
||||
for (let key in options) {
|
||||
params.set(key, options[key]);
|
||||
}
|
||||
return jhapiRequest(`/users?${params.toString()}`, "GET").then((data) =>
|
||||
data.json(),
|
||||
);
|
||||
},
|
||||
updateGroups: (offset, limit) =>
|
||||
jhapiRequest(`/groups?offset=${offset}&limit=${limit}`, "GET").then(
|
||||
(data) => data.json(),
|
||||
|
Reference in New Issue
Block a user