Merge branch 'jupyterhub:main' into group_property_feature

This commit is contained in:
Vlad Vifor
2022-03-30 11:25:53 +02:00
committed by GitHub
37 changed files with 1121 additions and 675 deletions

View File

@@ -3,7 +3,17 @@ import regeneratorRuntime from "regenerator-runtime";
import { useSelector, useDispatch } from "react-redux";
import PropTypes from "prop-types";
import { Button, Col, Row, FormControl } from "react-bootstrap";
import {
Button,
Col,
Row,
FormControl,
Card,
CardGroup,
Collapse,
} from "react-bootstrap";
import ReactObjectTableViewer from "react-object-table-viewer";
import { Link } from "react-router-dom";
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
@@ -40,6 +50,7 @@ const ServerDashboard = (props) => {
var [errorAlert, setErrorAlert] = useState(null);
var [sortMethod, setSortMethod] = useState(null);
var [disabledButtons, setDisabledButtons] = useState({});
const [collapseStates, setCollapseStates] = useState({});
var user_data = useSelector((state) => state.user_data),
user_page = useSelector((state) => state.user_page),
@@ -189,6 +200,131 @@ const ServerDashboard = (props) => {
);
};
const serverRow = (user, server) => {
const { servers, ...userNoServers } = user;
const serverNameDash = server.name ? `-${server.name}` : "";
const userServerName = user.name + serverNameDash;
const open = collapseStates[userServerName] || false;
return [
<tr key={`${userServerName}-row`} className="user-row">
<td data-testid="user-row-name">
<span>
<Button
onClick={() =>
setCollapseStates({
...collapseStates,
[userServerName]: !open,
})
}
aria-controls={`${userServerName}-collapse`}
aria-expanded={open}
data-testid={`${userServerName}-collapse-button`}
variant={open ? "secondary" : "primary"}
size="sm"
>
<span className="caret"></span>
</Button>{" "}
</span>
<span data-testid={`user-name-div-${userServerName}`}>
{user.name}
</span>
</td>
<td data-testid="user-row-admin">{user.admin ? "admin" : ""}</td>
<td data-testid="user-row-server">
{server.name ? (
<p className="text-secondary">{server.name}</p>
) : (
<p style={{ color: "lightgrey" }}>[MAIN]</p>
)}
</td>
<td data-testid="user-row-last-activity">
{server.last_activity ? timeSince(server.last_activity) : "Never"}
</td>
<td data-testid="user-row-server-activity">
{server.started ? (
// Stop Single-user server
<>
<StopServerButton serverName={server.name} userName={user.name} />
<AccessServerButton url={server.url} />
</>
) : (
// Start Single-user server
<>
<StartServerButton
serverName={server.name}
userName={user.name}
style={{ marginRight: 20 }}
/>
<a
href={`${base_url}spawn/${user.name}${
server.name && "/" + server.name
}`}
>
<button
className="btn btn-secondary btn-xs"
style={{ marginRight: 20 }}
>
Spawn Page
</button>
</a>
</>
)}
</td>
<EditUserCell user={user} />
</tr>,
<tr>
<td
colSpan={6}
style={{ padding: 0 }}
data-testid={`${userServerName}-td`}
>
<Collapse in={open} data-testid={`${userServerName}-collapse`}>
<CardGroup
id={`${userServerName}-card-group`}
style={{ width: "100%", margin: "0 auto", float: "none" }}
>
<Card style={{ width: "100%", padding: 3, margin: "0 auto" }}>
<Card.Title>User</Card.Title>
<ReactObjectTableViewer
className="table-striped table-bordered admin-table-head"
style={{
padding: "3px 6px",
margin: "auto",
}}
keyStyle={{
padding: "4px",
}}
valueStyle={{
padding: "4px",
}}
data={userNoServers}
/>
</Card>
<Card style={{ width: "100%", padding: 3, margin: "0 auto" }}>
<Card.Title>Server</Card.Title>
<ReactObjectTableViewer
className="table-striped table-bordered admin-table-head"
style={{
padding: "3px 6px",
margin: "auto",
}}
keyStyle={{
padding: "4px",
}}
valueStyle={{
padding: "4px",
}}
data={server}
/>
</Card>
</CardGroup>
</Collapse>
</td>
</tr>,
];
};
let servers = user_data.flatMap((user) => {
let userServers = Object.values({
"": user.server || {},
@@ -234,7 +370,7 @@ const ServerDashboard = (props) => {
<Link to="/groups">{"> Manage Groups"}</Link>
</Col>
</Row>
<table className="table table-striped table-bordered table-hover">
<table className="table table-bordered table-hover">
<thead className="admin-table-head">
<tr>
<th id="user-header">
@@ -373,63 +509,7 @@ const ServerDashboard = (props) => {
</Button>
</td>
</tr>
{servers.map(([user, server], i) => {
server.name = server.name || "";
return (
<tr key={i + "row"} className="user-row">
<td data-testid="user-row-name">{user.name}</td>
<td data-testid="user-row-admin">
{user.admin ? "admin" : ""}
</td>
<td data-testid="user-row-server">
{server.name ? (
<p class="text-secondary">{server.name}</p>
) : (
<p style={{ color: "lightgrey" }}>[MAIN]</p>
)}
</td>
<td data-testid="user-row-last-activity">
{server.last_activity
? timeSince(server.last_activity)
: "Never"}
</td>
<td data-testid="user-row-server-activity">
{server.started ? (
// Stop Single-user server
<>
<StopServerButton
serverName={server.name}
userName={user.name}
/>
<AccessServerButton url={server.url} />
</>
) : (
// Start Single-user server
<>
<StartServerButton
serverName={server.name}
userName={user.name}
/>
<a
href={`${base_url}spawn/${user.name}${
server.name && "/" + server.name
}`}
>
<button
className="btn btn-secondary btn-xs"
style={{ marginRight: 20 }}
>
Spawn Page
</button>
</a>
</>
)}
</td>
<EditUserCell user={user} />
</tr>
);
})}
{servers.flatMap(([user, server]) => serverRow(user, server))}
</tbody>
</table>
<PaginationFooter

View File

@@ -76,8 +76,8 @@ test("Renders users from props.user_data into table", async () => {
render(serverDashboardJsx(callbackSpy));
});
let foo = screen.getByText("foo");
let bar = screen.getByText("bar");
let foo = screen.getByTestId("user-name-div-foo");
let bar = screen.getByTestId("user-name-div-bar");
expect(foo).toBeVisible();
expect(bar).toBeVisible();
@@ -156,12 +156,12 @@ test("Sorts according to username", async () => {
fireEvent.click(handler);
let first = screen.getAllByTestId("user-row-name")[0];
expect(first.textContent).toBe("bar");
expect(first.textContent).toContain("bar");
fireEvent.click(handler);
first = screen.getAllByTestId("user-row-name")[0];
expect(first.textContent).toBe("foo");
expect(first.textContent).toContain("foo");
});
test("Sorts according to admin", async () => {
@@ -194,12 +194,12 @@ test("Sorts according to last activity", async () => {
fireEvent.click(handler);
let first = screen.getAllByTestId("user-row-name")[0];
expect(first.textContent).toBe("foo");
expect(first.textContent).toContain("foo");
fireEvent.click(handler);
first = screen.getAllByTestId("user-row-name")[0];
expect(first.textContent).toBe("bar");
expect(first.textContent).toContain("bar");
});
test("Sorts according to server status (running/not running)", async () => {
@@ -213,12 +213,53 @@ test("Sorts according to server status (running/not running)", async () => {
fireEvent.click(handler);
let first = screen.getAllByTestId("user-row-name")[0];
expect(first.textContent).toBe("foo");
expect(first.textContent).toContain("foo");
fireEvent.click(handler);
first = screen.getAllByTestId("user-row-name")[0];
expect(first.textContent).toBe("bar");
expect(first.textContent).toContain("bar");
});
test("Shows server details with button click", async () => {
let callbackSpy = mockAsync();
await act(async () => {
render(serverDashboardJsx(callbackSpy));
});
let button = screen.getByTestId("foo-collapse-button");
let collapse = screen.getByTestId("foo-collapse");
let collapseBar = screen.getByTestId("bar-collapse");
// expect().toBeVisible does not work here with collapse.
expect(collapse).toHaveClass("collapse");
expect(collapse).not.toHaveClass("show");
expect(collapseBar).not.toHaveClass("show");
await act(async () => {
fireEvent.click(button);
});
clock.tick(400);
expect(collapse).toHaveClass("collapse show");
expect(collapseBar).not.toHaveClass("show");
await act(async () => {
fireEvent.click(button);
});
clock.tick(400);
expect(collapse).toHaveClass("collapse");
expect(collapse).not.toHaveClass("show");
expect(collapseBar).not.toHaveClass("show");
await act(async () => {
fireEvent.click(button);
});
clock.tick(400);
expect(collapse).toHaveClass("collapse show");
expect(collapseBar).not.toHaveClass("show");
});
test("Renders nothing if required data is not available", async () => {