mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-18 15:33:02 +00:00
Merge pull request #4722 from minrk/sort-order-admin-ui
server-side sorting of admin page
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",
|
"lodash": "^4.17.21",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-bootstrap": "^2.7.4",
|
"react-bootstrap": "^2.10.1",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.8.0",
|
||||||
"react-multi-select-component": "^4.3.4",
|
"react-multi-select-component": "^4.3.4",
|
||||||
@@ -8209,13 +8209,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-bootstrap": {
|
"node_modules/react-bootstrap": {
|
||||||
"version": "2.7.4",
|
"version": "2.10.1",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.1.tgz",
|
||||||
|
"integrity": "sha512-J3OpRZIvCTQK+Tg/jOkRUvpYLHMdGeU9KqFUBQrV0d/Qr/3nsINpiOJyZMWnM5SJ3ctZdhPA6eCIKpEJR3Ellg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.21.0",
|
"@babel/runtime": "^7.22.5",
|
||||||
"@restart/hooks": "^0.4.9",
|
"@restart/hooks": "^0.4.9",
|
||||||
"@restart/ui": "^1.6.3",
|
"@restart/ui": "^1.6.6",
|
||||||
"@types/react-transition-group": "^4.4.5",
|
"@types/react-transition-group": "^4.4.6",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"dom-helpers": "^5.2.1",
|
"dom-helpers": "^5.2.1",
|
||||||
"invariant": "^2.2.4",
|
"invariant": "^2.2.4",
|
||||||
|
@@ -34,7 +34,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-bootstrap": "^2.7.4",
|
"react-bootstrap": "^2.10.1",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.8.0",
|
||||||
"react-multi-select-component": "^4.3.4",
|
"react-multi-select-component": "^4.3.4",
|
||||||
|
@@ -5,11 +5,12 @@ 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, handleLimit } = props;
|
const { offset, limit, visible, total, next, prev, handleLimit } = props;
|
||||||
return (
|
return (
|
||||||
<div className="pagination-footer">
|
<div className="pagination-footer">
|
||||||
<p>
|
<p>
|
||||||
Displaying {offset + 1}-{offset + visible} {total ? `of ${total}` : ""}
|
Displaying {visible ? offset + 1 : offset}-{offset + visible}{" "}
|
||||||
|
{total ? `of ${total}` : ""}
|
||||||
<br />
|
<br />
|
||||||
{offset >= 1 ? (
|
{offset >= 1 ? (
|
||||||
<button className="btn btn-sm btn-light spaced">
|
<button className="btn btn-sm btn-light spaced">
|
||||||
|
@@ -7,6 +7,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Col,
|
Col,
|
||||||
Row,
|
Row,
|
||||||
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
Card,
|
Card,
|
||||||
CardGroup,
|
CardGroup,
|
||||||
@@ -32,24 +33,8 @@ RowListItem.propTypes = {
|
|||||||
const ServerDashboard = (props) => {
|
const ServerDashboard = (props) => {
|
||||||
const base_url = window.base_url || "/";
|
const base_url = window.base_url || "/";
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
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 [errorAlert, setErrorAlert] = useState(null);
|
||||||
const [sortMethod, setSortMethod] = useState(null);
|
|
||||||
const [collapseStates, setCollapseStates] = useState({});
|
const [collapseStates, setCollapseStates] = useState({});
|
||||||
|
|
||||||
let user_data = useSelector((state) => state.user_data);
|
let user_data = useSelector((state) => state.user_data);
|
||||||
@@ -59,6 +44,8 @@ const ServerDashboard = (props) => {
|
|||||||
usePaginationParams();
|
usePaginationParams();
|
||||||
|
|
||||||
const name_filter = searchParams.get("name_filter") || "";
|
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;
|
const total = user_page ? user_page.total : undefined;
|
||||||
|
|
||||||
@@ -76,7 +63,13 @@ const ServerDashboard = (props) => {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const dispatchPageUpdate = (data, page) => {
|
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);
|
setPagination(page);
|
||||||
|
// persist user data, triggers rerender
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "USER_PAGE",
|
type: "USER_PAGE",
|
||||||
value: {
|
value: {
|
||||||
@@ -87,6 +80,8 @@ const ServerDashboard = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setNameFilter = (new_name_filter) => {
|
const setNameFilter = (new_name_filter) => {
|
||||||
|
// persist ?name_filter parameter
|
||||||
|
// store in url param, clear when value is empty
|
||||||
setSearchParams((params) => {
|
setSearchParams((params) => {
|
||||||
// clear offset when name filter changes
|
// clear offset when name filter changes
|
||||||
if (new_name_filter !== name_filter) {
|
if (new_name_filter !== name_filter) {
|
||||||
@@ -101,26 +96,61 @@ 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 = (new_state_filter) => {
|
||||||
|
// persist ?state filter
|
||||||
|
// store in url param, clear when value is default ('')
|
||||||
|
setSearchParams((params) => {
|
||||||
|
// clear offset when filter changes
|
||||||
|
if (new_state_filter !== state_filter) {
|
||||||
|
params.delete("offset");
|
||||||
|
}
|
||||||
|
if (!new_state_filter) {
|
||||||
|
params.delete("state");
|
||||||
|
} else {
|
||||||
|
params.set("state", new_state_filter);
|
||||||
|
}
|
||||||
|
console.log("setting search params", params.toString());
|
||||||
|
return params;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// the callback to update the displayed user list
|
||||||
|
const updateUsersWithParams = () =>
|
||||||
|
updateUsers({
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
name_filter,
|
||||||
|
sort,
|
||||||
|
state: state_filter,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateUsers(offset, limit, name_filter)
|
updateUsersWithParams()
|
||||||
.then((data) => dispatchPageUpdate(data.items, data._pagination))
|
.then((data) => dispatchPageUpdate(data.items, data._pagination))
|
||||||
.catch((err) => setErrorAlert("Failed to update user list."));
|
.catch((err) => setErrorAlert("Failed to update user list."));
|
||||||
}, [offset, limit, name_filter]);
|
}, [offset, limit, name_filter, sort, state_filter]);
|
||||||
|
|
||||||
if (!user_data || !user_page) {
|
if (!user_data || !user_page) {
|
||||||
return <div data-testid="no-show"></div>;
|
return <div data-testid="no-show"></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var slice = [offset, limit, name_filter];
|
|
||||||
|
|
||||||
const handleSearch = debounce(async (event) => {
|
const handleSearch = debounce(async (event) => {
|
||||||
setNameFilter(event.target.value);
|
setNameFilter(event.target.value);
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
if (sortMethod != null) {
|
|
||||||
user_data = sortMethod(user_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ServerButton = ({ server, user, action, name, extraClass }) => {
|
const ServerButton = ({ server, user, action, name, extraClass }) => {
|
||||||
var [isDisabled, setIsDisabled] = useState(false);
|
var [isDisabled, setIsDisabled] = useState(false);
|
||||||
return (
|
return (
|
||||||
@@ -132,7 +162,7 @@ const ServerDashboard = (props) => {
|
|||||||
action(user.name, server.name)
|
action(user.name, server.name)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status < 300) {
|
if (res.status < 300) {
|
||||||
updateUsers(...slice)
|
updateUsersWithParams()
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
dispatchPageUpdate(data.items, data._pagination);
|
dispatchPageUpdate(data.items, data._pagination);
|
||||||
})
|
})
|
||||||
@@ -170,7 +200,7 @@ const ServerDashboard = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
const DeleteServerButton = ({ server, user }) => {
|
const DeleteServerButton = ({ server, user }) => {
|
||||||
if (server.name === "") {
|
if (!server.name) {
|
||||||
// It's not possible to delete unnamed servers
|
// It's not possible to delete unnamed servers
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -225,12 +255,10 @@ const ServerDashboard = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditUserCell = ({ user }) => {
|
const EditUserButton = ({ user }) => {
|
||||||
return (
|
return (
|
||||||
<td>
|
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary btn-xs"
|
className="btn btn-light btn-xs"
|
||||||
style={{ marginRight: 20 }}
|
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
history.push({
|
history.push({
|
||||||
pathname: "/edit-user",
|
pathname: "/edit-user",
|
||||||
@@ -243,7 +271,6 @@ const ServerDashboard = (props) => {
|
|||||||
>
|
>
|
||||||
Edit User
|
Edit User
|
||||||
</button>
|
</button>
|
||||||
</td>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -337,8 +364,8 @@ const ServerDashboard = (props) => {
|
|||||||
<DeleteServerButton server={server} user={user} />
|
<DeleteServerButton server={server} user={user} />
|
||||||
<AccessServerButton server={server} />
|
<AccessServerButton server={server} />
|
||||||
<SpawnPageButton server={server} user={user} />
|
<SpawnPageButton server={server} user={user} />
|
||||||
|
<EditUserButton user={user} />
|
||||||
</td>
|
</td>
|
||||||
<EditUserCell user={user} />
|
|
||||||
</tr>,
|
</tr>,
|
||||||
<tr key={`${userServerName}-detail`}>
|
<tr key={`${userServerName}-detail`}>
|
||||||
<td
|
<td
|
||||||
@@ -406,6 +433,24 @@ const ServerDashboard = (props) => {
|
|||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col md={3}>
|
||||||
|
{/* div.checkbox required for BS3 CSS */}
|
||||||
|
<div class="checkbox">
|
||||||
|
<label title="check to only show running servers, otherwise show all">
|
||||||
|
<Form.Check
|
||||||
|
inline
|
||||||
|
type="checkbox"
|
||||||
|
name="active_servers"
|
||||||
|
id="active-servers-filter"
|
||||||
|
checked={state_filter == "active"}
|
||||||
|
onChange={(event) => {
|
||||||
|
setStateFilter(event.target.checked ? "active" : null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{"only active servers"}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<Col md="auto" style={{ float: "right", margin: 15 }}>
|
<Col md="auto" style={{ float: "right", margin: 15 }}>
|
||||||
<Link to="/groups">{"> Manage Groups"}</Link>
|
<Link to="/groups">{"> Manage Groups"}</Link>
|
||||||
@@ -417,43 +462,23 @@ const ServerDashboard = (props) => {
|
|||||||
<th id="user-header">
|
<th id="user-header">
|
||||||
User{" "}
|
User{" "}
|
||||||
<SortHandler
|
<SortHandler
|
||||||
sorts={{ asc: usernameAsc, desc: usernameDesc }}
|
currentSort={sort}
|
||||||
callback={(method) => setSortMethod(() => method)}
|
setSort={setSort}
|
||||||
|
sortKey="name"
|
||||||
testid="user-sort"
|
testid="user-sort"
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th id="admin-header">
|
<th id="admin-header">Admin</th>
|
||||||
Admin{" "}
|
<th id="server-header">Server</th>
|
||||||
<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="last-activity-header">
|
<th id="last-activity-header">
|
||||||
Last Activity{" "}
|
Last Activity{" "}
|
||||||
<SortHandler
|
<SortHandler
|
||||||
sorts={{ asc: dateAsc, desc: dateDesc }}
|
currentSort={sort}
|
||||||
callback={(method) => setSortMethod(() => method)}
|
setSort={setSort}
|
||||||
|
sortKey="last_activity"
|
||||||
testid="last-activity-sort"
|
testid="last-activity-sort"
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th id="running-status-header">
|
|
||||||
Running{" "}
|
|
||||||
<SortHandler
|
|
||||||
sorts={{ asc: runningAsc, desc: runningDesc }}
|
|
||||||
callback={(method) => setSortMethod(() => method)}
|
|
||||||
testid="running-status-sort"
|
|
||||||
/>
|
|
||||||
</th>
|
|
||||||
<th id="actions-header">Actions</th>
|
<th id="actions-header">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -466,14 +491,13 @@ const ServerDashboard = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td></td>
|
<td colSpan={4} className="admin-header-buttons">
|
||||||
<td></td>
|
|
||||||
<td>
|
|
||||||
{/* Start all servers */}
|
{/* Start all servers */}
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
className="start-all"
|
className="start-all"
|
||||||
data-testid="start-all"
|
data-testid="start-all"
|
||||||
|
title="start all servers on the current page"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Promise.all(startAll(user_data.map((e) => e.name)))
|
Promise.all(startAll(user_data.map((e) => e.name)))
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@@ -490,7 +514,7 @@ const ServerDashboard = (props) => {
|
|||||||
return res;
|
return res;
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
updateUsers(...slice)
|
updateUsersWithParams()
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
dispatchPageUpdate(data.items, data._pagination);
|
dispatchPageUpdate(data.items, data._pagination);
|
||||||
})
|
})
|
||||||
@@ -510,6 +534,7 @@ const ServerDashboard = (props) => {
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
className="stop-all"
|
className="stop-all"
|
||||||
data-testid="stop-all"
|
data-testid="stop-all"
|
||||||
|
title="stop all servers on the current page"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Promise.all(stopAll(user_data.map((e) => e.name)))
|
Promise.all(stopAll(user_data.map((e) => e.name)))
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@@ -526,7 +551,7 @@ const ServerDashboard = (props) => {
|
|||||||
return res;
|
return res;
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
updateUsers(...slice)
|
updateUsersWithParams()
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
dispatchPageUpdate(data.items, data._pagination);
|
dispatchPageUpdate(data.items, data._pagination);
|
||||||
})
|
})
|
||||||
@@ -540,8 +565,8 @@ const ServerDashboard = (props) => {
|
|||||||
>
|
>
|
||||||
Stop All
|
Stop All
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
{/* spacing between start/stop and Shutdown */}
|
||||||
<td>
|
<span style={{ marginLeft: "56px" }}> </span>
|
||||||
{/* Shutdown Jupyterhub */}
|
{/* Shutdown Jupyterhub */}
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -551,7 +576,6 @@ const ServerDashboard = (props) => {
|
|||||||
Shutdown Hub
|
Shutdown Hub
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
<td></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{servers.flatMap(([user, server]) => serverRow(user, server))}
|
{servers.flatMap(([user, server]) => serverRow(user, server))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -590,30 +614,27 @@ ServerDashboard.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SortHandler = (props) => {
|
const SortHandler = (props) => {
|
||||||
var { sorts, callback, testid } = props;
|
const { currentSort, setSort, sortKey, testid } = props;
|
||||||
|
|
||||||
var [direction, setDirection] = useState(undefined);
|
|
||||||
|
|
||||||
|
const currentlySorted = currentSort && currentSort.endsWith(sortKey);
|
||||||
|
const descending = currentSort && currentSort.startsWith("-");
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="sort-icon"
|
className="sort-icon"
|
||||||
data-testid={testid}
|
data-testid={testid}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!direction) {
|
if (!currentlySorted) {
|
||||||
callback(sorts.desc);
|
setSort(sortKey);
|
||||||
setDirection("desc");
|
} else if (descending) {
|
||||||
} else if (direction == "asc") {
|
setSort(sortKey);
|
||||||
callback(sorts.desc);
|
|
||||||
setDirection("desc");
|
|
||||||
} else {
|
} else {
|
||||||
callback(sorts.asc);
|
setSort("-" + sortKey);
|
||||||
setDirection("asc");
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!direction ? (
|
{!currentlySorted ? (
|
||||||
<FaSort />
|
<FaSort />
|
||||||
) : direction == "asc" ? (
|
) : descending ? (
|
||||||
<FaSortDown />
|
<FaSortDown />
|
||||||
) : (
|
) : (
|
||||||
<FaSortUp />
|
<FaSortUp />
|
||||||
@@ -623,8 +644,9 @@ const SortHandler = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
SortHandler.propTypes = {
|
SortHandler.propTypes = {
|
||||||
sorts: PropTypes.object,
|
currentSort: PropTypes.string,
|
||||||
callback: PropTypes.func,
|
setSort: PropTypes.func,
|
||||||
|
sortKey: PropTypes.string,
|
||||||
testid: PropTypes.string,
|
testid: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -34,9 +34,8 @@ const serverDashboardJsx = (props) => {
|
|||||||
// spies is a dict of properties to mock in
|
// spies is a dict of properties to mock in
|
||||||
// any API calls that will fire during the test should be mocked
|
// any API calls that will fire during the test should be mocked
|
||||||
props = props || {};
|
props = props || {};
|
||||||
const defaultSpy = mockAsync();
|
|
||||||
if (!props.updateUsers) {
|
if (!props.updateUsers) {
|
||||||
props.updateUsers = defaultSpy;
|
props.updateUsers = mockUpdateUsers;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Provider store={createStore(mockReducers, mockAppState())}>
|
<Provider store={createStore(mockReducers, mockAppState())}>
|
||||||
@@ -55,6 +54,14 @@ var mockAsync = (data) =>
|
|||||||
var mockAsyncRejection = () =>
|
var mockAsyncRejection = () =>
|
||||||
jest.fn().mockImplementation(() => Promise.reject());
|
jest.fn().mockImplementation(() => Promise.reject());
|
||||||
|
|
||||||
|
const defaultUpdateUsersParams = {
|
||||||
|
offset: 0,
|
||||||
|
limit: 2,
|
||||||
|
name_filter: "",
|
||||||
|
sort: "id",
|
||||||
|
state: "",
|
||||||
|
};
|
||||||
|
|
||||||
var bar_servers = {
|
var bar_servers = {
|
||||||
"": {
|
"": {
|
||||||
name: "",
|
name: "",
|
||||||
@@ -80,9 +87,21 @@ var bar_servers = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
var mockAppState = () =>
|
/* create new user models */
|
||||||
Object.assign({}, initialState, {
|
const newUser = (name) => {
|
||||||
user_data: [
|
return {
|
||||||
|
kind: "user",
|
||||||
|
name: name,
|
||||||
|
admin: false,
|
||||||
|
groups: [],
|
||||||
|
server: `/user/${name}`,
|
||||||
|
created: "2020-12-07T18:46:27.112695Z",
|
||||||
|
last_activity: "2020-12-07T21:00:33.336354Z",
|
||||||
|
servers: {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const allUsers = [
|
||||||
{
|
{
|
||||||
kind: "user",
|
kind: "user",
|
||||||
name: "foo",
|
name: "foo",
|
||||||
@@ -117,7 +136,15 @@ var mockAppState = () =>
|
|||||||
last_activity: "2020-12-07T20:43:51.013613Z",
|
last_activity: "2020-12-07T20:43:51.013613Z",
|
||||||
servers: bar_servers,
|
servers: bar_servers,
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
|
|
||||||
|
for (var i = 2; i < 10; i++) {
|
||||||
|
allUsers.push(newUser(`test-${i}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
var mockAppState = () =>
|
||||||
|
Object.assign({}, initialState, {
|
||||||
|
user_data: allUsers.slice(0, 2),
|
||||||
user_page: {
|
user_page: {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: 2,
|
limit: 2,
|
||||||
@@ -125,7 +152,7 @@ var mockAppState = () =>
|
|||||||
next: {
|
next: {
|
||||||
offset: 2,
|
offset: 2,
|
||||||
limit: 2,
|
limit: 2,
|
||||||
url: "http://localhost:8000/hub/api/groups?offset=2&limit=2",
|
url: "http://localhost:8000/hub/api/users?offset=2&limit=2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -143,6 +170,40 @@ var mockReducers = jest.fn((state, action) => {
|
|||||||
return state;
|
return state;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mockUpdateUsers = jest.fn(({ offset, limit, sort, name_filter, state }) => {
|
||||||
|
/* mock updating users
|
||||||
|
|
||||||
|
this has tom implement the server-side filtering, sorting, etc.
|
||||||
|
(at least whatever we want to test of it)
|
||||||
|
*/
|
||||||
|
let matchingUsers = allUsers;
|
||||||
|
if (state === "active") {
|
||||||
|
// only first user is active
|
||||||
|
matchingUsers = allUsers.slice(0, 1);
|
||||||
|
}
|
||||||
|
if (name_filter) {
|
||||||
|
matchingUsers = matchingUsers.filter((user) =>
|
||||||
|
user.name.startsWith(name_filter),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = matchingUsers.length;
|
||||||
|
const items = matchingUsers.slice(offset, offset + limit);
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
items: items,
|
||||||
|
_pagination: {
|
||||||
|
offset: offset,
|
||||||
|
limit: limit,
|
||||||
|
total: total,
|
||||||
|
next: {
|
||||||
|
offset: offset + limit,
|
||||||
|
limit: limit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
let searchParams = new URLSearchParams();
|
let searchParams = new URLSearchParams();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -151,6 +212,7 @@ beforeEach(() => {
|
|||||||
return callback(mockAppState());
|
return callback(mockAppState());
|
||||||
});
|
});
|
||||||
searchParams = new URLSearchParams();
|
searchParams = new URLSearchParams();
|
||||||
|
searchParams.set("limit", "2");
|
||||||
|
|
||||||
useSearchParams.mockImplementation(() => [
|
useSearchParams.mockImplementation(() => [
|
||||||
searchParams,
|
searchParams,
|
||||||
@@ -164,6 +226,7 @@ afterEach(() => {
|
|||||||
useSearchParams.mockClear();
|
useSearchParams.mockClear();
|
||||||
useSelector.mockClear();
|
useSelector.mockClear();
|
||||||
mockReducers.mockClear();
|
mockReducers.mockClear();
|
||||||
|
mockUpdateUsers.mockClear();
|
||||||
jest.runAllTimers();
|
jest.runAllTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -267,71 +330,93 @@ test("Invokes the shutdownHub event on button click", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Sorts according to username", async () => {
|
test("Sorts according to username", async () => {
|
||||||
|
let rerender;
|
||||||
|
const testId = "user-sort";
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(serverDashboardJsx());
|
rerender = render(serverDashboardJsx()).rerender;
|
||||||
});
|
});
|
||||||
|
|
||||||
let handler = screen.getByTestId("user-sort");
|
expect(searchParams.get("sort")).toEqual(null);
|
||||||
|
let handler = screen.getByTestId(testId);
|
||||||
fireEvent.click(handler);
|
fireEvent.click(handler);
|
||||||
|
expect(searchParams.get("sort")).toEqual("name");
|
||||||
|
|
||||||
let first = screen.getAllByTestId("user-row-name")[0];
|
|
||||||
expect(first.textContent).toContain("bar");
|
|
||||||
|
|
||||||
fireEvent.click(handler);
|
|
||||||
|
|
||||||
first = screen.getAllByTestId("user-row-name")[0];
|
|
||||||
expect(first.textContent).toContain("foo");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Sorts according to admin", async () => {
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(serverDashboardJsx());
|
rerender(serverDashboardJsx());
|
||||||
|
handler = screen.getByTestId(testId);
|
||||||
});
|
});
|
||||||
|
|
||||||
let handler = screen.getByTestId("admin-sort");
|
|
||||||
fireEvent.click(handler);
|
fireEvent.click(handler);
|
||||||
|
expect(searchParams.get("sort")).toEqual("-name");
|
||||||
|
|
||||||
let first = screen.getAllByTestId("user-row-admin")[0];
|
await act(async () => {
|
||||||
expect(first.textContent).toBe("admin");
|
rerender(serverDashboardJsx());
|
||||||
|
handler = screen.getByTestId(testId);
|
||||||
|
});
|
||||||
|
|
||||||
fireEvent.click(handler);
|
fireEvent.click(handler);
|
||||||
|
expect(searchParams.get("sort")).toEqual("name");
|
||||||
first = screen.getAllByTestId("user-row-admin")[0];
|
|
||||||
expect(first.textContent).toBe("");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Sorts according to last activity", async () => {
|
test("Sorts according to last activity", async () => {
|
||||||
|
let rerender;
|
||||||
|
const testId = "last-activity-sort";
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(serverDashboardJsx());
|
rerender = render(serverDashboardJsx()).rerender;
|
||||||
});
|
});
|
||||||
|
|
||||||
let handler = screen.getByTestId("last-activity-sort");
|
expect(searchParams.get("sort")).toEqual(null);
|
||||||
|
let handler = screen.getByTestId(testId);
|
||||||
fireEvent.click(handler);
|
fireEvent.click(handler);
|
||||||
|
expect(searchParams.get("sort")).toEqual("last_activity");
|
||||||
|
|
||||||
let first = screen.getAllByTestId("user-row-name")[0];
|
|
||||||
expect(first.textContent).toContain("foo");
|
|
||||||
|
|
||||||
fireEvent.click(handler);
|
|
||||||
|
|
||||||
first = screen.getAllByTestId("user-row-name")[0];
|
|
||||||
expect(first.textContent).toContain("bar");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Sorts according to server status (running/not running)", async () => {
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(serverDashboardJsx());
|
rerender(serverDashboardJsx());
|
||||||
|
handler = screen.getByTestId(testId);
|
||||||
});
|
});
|
||||||
|
|
||||||
let handler = screen.getByTestId("running-status-sort");
|
fireEvent.click(handler);
|
||||||
|
expect(searchParams.get("sort")).toEqual("-last_activity");
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
rerender(serverDashboardJsx());
|
||||||
|
handler = screen.getByTestId(testId);
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(handler);
|
||||||
|
expect(searchParams.get("sort")).toEqual("last_activity");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Filter according to server status (running/not running)", async () => {
|
||||||
|
let rerender;
|
||||||
|
await act(async () => {
|
||||||
|
rerender = render(serverDashboardJsx()).rerender;
|
||||||
|
});
|
||||||
|
console.log(rerender);
|
||||||
|
console.log("begin test");
|
||||||
|
const label = "only active servers";
|
||||||
|
let handler = screen.getByLabelText(label);
|
||||||
|
expect(handler.checked).toEqual(false);
|
||||||
fireEvent.click(handler);
|
fireEvent.click(handler);
|
||||||
|
|
||||||
let first = screen.getAllByTestId("user-row-name")[0];
|
// FIXME: need to force a rerender to get updated checkbox
|
||||||
expect(first.textContent).toContain("foo");
|
// I don't think this should be required
|
||||||
|
await act(async () => {
|
||||||
|
rerender(serverDashboardJsx());
|
||||||
|
handler = screen.getByLabelText(label);
|
||||||
|
});
|
||||||
|
expect(searchParams.get("state")).toEqual("active");
|
||||||
|
expect(handler.checked).toEqual(true);
|
||||||
|
|
||||||
fireEvent.click(handler);
|
fireEvent.click(handler);
|
||||||
|
|
||||||
first = screen.getAllByTestId("user-row-name")[0];
|
await act(async () => {
|
||||||
expect(first.textContent).toContain("bar");
|
rerender(serverDashboardJsx());
|
||||||
|
handler = screen.getByLabelText(label);
|
||||||
|
});
|
||||||
|
handler = screen.getByLabelText(label);
|
||||||
|
expect(handler.checked).toEqual(false);
|
||||||
|
expect(searchParams.get("state")).toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Shows server details with button click", async () => {
|
test("Shows server details with button click", async () => {
|
||||||
@@ -494,23 +579,9 @@ test("Shows a UI error dialogue when stop user server returns an improper status
|
|||||||
|
|
||||||
test("Search for user calls updateUsers with name filter", async () => {
|
test("Search for user calls updateUsers with name filter", async () => {
|
||||||
let spy = mockAsync();
|
let spy = mockAsync();
|
||||||
let mockUpdateUsers = jest.fn((offset, limit, name_filter) => {
|
|
||||||
return Promise.resolve({
|
|
||||||
items: [],
|
|
||||||
_pagination: {
|
|
||||||
offset: offset,
|
|
||||||
limit: limit,
|
|
||||||
total: offset + limit * 2,
|
|
||||||
next: {
|
|
||||||
offset: offset + limit,
|
|
||||||
limit: limit,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
searchParams.set("offset", "2");
|
searchParams.set("offset", "2");
|
||||||
render(serverDashboardJsx({ updateUsers: mockUpdateUsers }));
|
render(serverDashboardJsx());
|
||||||
});
|
});
|
||||||
|
|
||||||
let search = screen.getByLabelText("user-search");
|
let search = screen.getByLabelText("user-search");
|
||||||
@@ -538,17 +609,15 @@ test("Search for user calls updateUsers with name filter", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Interacting with PaginationFooter causes state update and refresh via useEffect call", async () => {
|
test("Interacting with PaginationFooter causes state update and refresh via useEffect call", async () => {
|
||||||
let updateUsers = mockAsync();
|
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(serverDashboardJsx({ updateUsers: updateUsers }));
|
render(serverDashboardJsx());
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(updateUsers).toBeCalledWith(0, 100, "");
|
expect(mockUpdateUsers).toBeCalledWith(defaultUpdateUsersParams);
|
||||||
|
|
||||||
var n = 3;
|
var n = 3;
|
||||||
expect(searchParams.get("offset")).toEqual(null);
|
expect(searchParams.get("offset")).toEqual(null);
|
||||||
expect(searchParams.get("limit")).toEqual(null);
|
expect(searchParams.get("limit")).toEqual("2");
|
||||||
|
|
||||||
let next = screen.getByTestId("paginate-next");
|
let next = screen.getByTestId("paginate-next");
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -556,8 +625,8 @@ test("Interacting with PaginationFooter causes state update and refresh via useE
|
|||||||
jest.runAllTimers();
|
jest.runAllTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(searchParams.get("offset")).toEqual("100");
|
expect(searchParams.get("offset")).toEqual("2");
|
||||||
expect(searchParams.get("limit")).toEqual(null);
|
expect(searchParams.get("limit")).toEqual("2");
|
||||||
|
|
||||||
// FIXME: should call updateUsers, does in reality.
|
// FIXME: should call updateUsers, does in reality.
|
||||||
// tests don't reflect reality due to mocked state/useSelector
|
// tests don't reflect reality due to mocked state/useSelector
|
||||||
@@ -590,12 +659,9 @@ test("Start server and confirm pending state", async () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let mockUpdateUsers = jest.fn(() => Promise.resolve(mockAppState()));
|
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(
|
render(
|
||||||
serverDashboardJsx({
|
serverDashboardJsx({
|
||||||
updateUsers: mockUpdateUsers,
|
|
||||||
startServer: mockStartServer,
|
startServer: mockStartServer,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -604,16 +670,17 @@ test("Start server and confirm pending state", async () => {
|
|||||||
let actions = screen.getAllByTestId("user-row-server-activity")[1];
|
let actions = screen.getAllByTestId("user-row-server-activity")[1];
|
||||||
let buttons = getAllByRole(actions, "button");
|
let buttons = getAllByRole(actions, "button");
|
||||||
|
|
||||||
expect(buttons.length).toBe(2);
|
expect(buttons.length).toBe(3);
|
||||||
expect(buttons[0].textContent).toBe("Start Server");
|
expect(buttons[0].textContent).toBe("Start Server");
|
||||||
expect(buttons[1].textContent).toBe("Spawn Page");
|
expect(buttons[1].textContent).toBe("Spawn Page");
|
||||||
|
expect(buttons[2].textContent).toBe("Edit User");
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(buttons[0]);
|
fireEvent.click(buttons[0]);
|
||||||
});
|
});
|
||||||
expect(mockUpdateUsers.mock.calls).toHaveLength(1);
|
expect(mockUpdateUsers.mock.calls).toHaveLength(1);
|
||||||
|
|
||||||
expect(buttons.length).toBe(2);
|
expect(buttons.length).toBe(3);
|
||||||
expect(buttons[0].textContent).toBe("Start Server");
|
expect(buttons[0].textContent).toBe("Start Server");
|
||||||
expect(buttons[0]).toBeDisabled();
|
expect(buttons[0]).toBeDisabled();
|
||||||
expect(buttons[1].textContent).toBe("Spawn Page");
|
expect(buttons[1].textContent).toBe("Spawn Page");
|
||||||
|
@@ -7,6 +7,13 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-light {
|
||||||
|
/* backport bs5 btn-light colors */
|
||||||
|
background-color: #f9fafb;
|
||||||
|
border-color: #f9fafb;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
.server-dashboard-container .btn-light {
|
.server-dashboard-container .btn-light {
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
@@ -43,6 +50,47 @@ tr.noborder > td {
|
|||||||
vertical-align: inherit;
|
vertical-align: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-row .actions > * {
|
.user-row .actions button {
|
||||||
margin-right: 5px;
|
margin: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header-buttons {
|
||||||
|
/* float header action buttons to the right */
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 #user-header {
|
||||||
|
}
|
||||||
|
.admin-table-head #admin-header {
|
||||||
|
width: 64px;
|
||||||
|
}
|
||||||
|
.admin-table-head #last-activity-header {
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
.admin-table-head #actions-header {
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vertical stack server buttons on small windows */
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
.admin-table-head #actions-header {
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
.user-row .actions button {
|
||||||
|
/* full-width buttons when they get collapsed into a single column */
|
||||||
|
margin: 4px 0px 4px 0px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,14 +3,11 @@ const base_url = jhdata.base_url || "/";
|
|||||||
const xsrfToken = jhdata.xsrf_token;
|
const xsrfToken = jhdata.xsrf_token;
|
||||||
|
|
||||||
export const jhapiRequest = (endpoint, method, data) => {
|
export const jhapiRequest = (endpoint, method, data) => {
|
||||||
let api_url = `${base_url}hub/api`;
|
let api_url = new URL(`${base_url}hub/api` + endpoint, location.origin);
|
||||||
let suffix = "";
|
|
||||||
if (xsrfToken) {
|
if (xsrfToken) {
|
||||||
// add xsrf token to url parameter
|
api_url.searchParams.set("_xsrf", xsrfToken);
|
||||||
var sep = endpoint.indexOf("?") === -1 ? "?" : "&";
|
|
||||||
suffix = sep + "_xsrf=" + xsrfToken;
|
|
||||||
}
|
}
|
||||||
return fetch(api_url + endpoint + suffix, {
|
return fetch(api_url, {
|
||||||
method: method,
|
method: method,
|
||||||
json: true,
|
json: true,
|
||||||
headers: {
|
headers: {
|
||||||
|
@@ -17,7 +17,7 @@ export const usePaginationParams = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const _setLimit = (params, limit) => {
|
const _setLimit = (params, limit) => {
|
||||||
if (limit < 10) limit = 10;
|
if (limit < 1) limit = 1;
|
||||||
if (limit === window.api_page_limit) {
|
if (limit === window.api_page_limit) {
|
||||||
params.delete("limit");
|
params.delete("limit");
|
||||||
} else {
|
} else {
|
||||||
|
@@ -2,13 +2,16 @@ import { withProps } from "recompose";
|
|||||||
import { jhapiRequest } from "./jhapiUtil";
|
import { jhapiRequest } from "./jhapiUtil";
|
||||||
|
|
||||||
const withAPI = withProps(() => ({
|
const withAPI = withProps(() => ({
|
||||||
updateUsers: (offset, limit, name_filter) =>
|
updateUsers: (options) => {
|
||||||
jhapiRequest(
|
let params = new URLSearchParams();
|
||||||
`/users?include_stopped_servers&offset=${offset}&limit=${limit}&name_filter=${
|
params.set("include_stopped_servers", "1");
|
||||||
name_filter || ""
|
for (let key in options) {
|
||||||
}`,
|
params.set(key, options[key]);
|
||||||
"GET",
|
}
|
||||||
).then((data) => data.json()),
|
return jhapiRequest(`/users?${params.toString()}`, "GET").then((data) =>
|
||||||
|
data.json(),
|
||||||
|
);
|
||||||
|
},
|
||||||
updateGroups: (offset, limit) =>
|
updateGroups: (offset, limit) =>
|
||||||
jhapiRequest(`/groups?offset=${offset}&limit=${limit}`, "GET").then(
|
jhapiRequest(`/groups?offset=${offset}&limit=${limit}`, "GET").then(
|
||||||
(data) => data.json(),
|
(data) => data.json(),
|
||||||
|
@@ -352,6 +352,10 @@ class APIHandler(BaseHandler):
|
|||||||
if include_stopped_servers:
|
if include_stopped_servers:
|
||||||
# add any stopped servers in the db
|
# add any stopped servers in the db
|
||||||
seen = set(servers.keys())
|
seen = set(servers.keys())
|
||||||
|
if isinstance(user, orm.User):
|
||||||
|
# need high-level User wrapper for spawner model
|
||||||
|
# FIXME: this shouldn't be needed!
|
||||||
|
user = self.users[user]
|
||||||
for name, orm_spawner in user.orm_spawners.items():
|
for name, orm_spawner in user.orm_spawners.items():
|
||||||
if name not in seen and scope_filter(orm_spawner, kind='server'):
|
if name not in seen and scope_filter(orm_spawner, kind='server'):
|
||||||
servers[name] = self.server_model(orm_spawner, user=user)
|
servers[name] = self.server_model(orm_spawner, user=user)
|
||||||
|
@@ -1116,8 +1116,9 @@ async def test_search_on_admin_page(
|
|||||||
displaying = browser.get_by_text("Displaying")
|
displaying = browser.get_by_text("Displaying")
|
||||||
if users_count_db_filtered <= 50:
|
if users_count_db_filtered <= 50:
|
||||||
await expect(filtered_list_on_page).to_have_count(users_count_db_filtered)
|
await expect(filtered_list_on_page).to_have_count(users_count_db_filtered)
|
||||||
|
start = 1 if users_count_db_filtered else 0
|
||||||
await expect(displaying).to_contain_text(
|
await expect(displaying).to_contain_text(
|
||||||
re.compile(f"1-{users_count_db_filtered}")
|
re.compile(f"{start}-{users_count_db_filtered}")
|
||||||
)
|
)
|
||||||
# check that users names contain the search value in the filtered list
|
# check that users names contain the search value in the filtered list
|
||||||
for element in await filtered_list_on_page.get_by_test_id(
|
for element in await filtered_list_on_page.get_by_test_id(
|
||||||
|
Reference in New Issue
Block a user