mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-15 22:13:00 +00:00
Merge pull request #4457 from diocas/fix_4174
Delete server button on admin page
This commit is contained in:
@@ -74,6 +74,7 @@ const ServerDashboard = (props) => {
|
|||||||
shutdownHub,
|
shutdownHub,
|
||||||
startServer,
|
startServer,
|
||||||
stopServer,
|
stopServer,
|
||||||
|
deleteServer,
|
||||||
startAll,
|
startAll,
|
||||||
stopAll,
|
stopAll,
|
||||||
history,
|
history,
|
||||||
@@ -167,6 +168,50 @@ const ServerDashboard = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DeleteServerButton = ({ serverName, userName }) => {
|
||||||
|
if (serverName === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var [isDisabled, setIsDisabled] = useState(false);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="btn btn-danger btn-xs stop-button"
|
||||||
|
// It's not possible to delete unnamed servers
|
||||||
|
disabled={isDisabled}
|
||||||
|
onClick={() => {
|
||||||
|
setIsDisabled(true);
|
||||||
|
deleteServer(userName, serverName)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status < 300) {
|
||||||
|
updateUsers(...slice)
|
||||||
|
.then((data) => {
|
||||||
|
dispatchPageUpdate(
|
||||||
|
data.items,
|
||||||
|
data._pagination,
|
||||||
|
name_filter,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setIsDisabled(false);
|
||||||
|
setErrorAlert(`Failed to update users list.`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setErrorAlert(`Failed to delete server.`);
|
||||||
|
setIsDisabled(false);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setErrorAlert(`Failed to delete server.`);
|
||||||
|
setIsDisabled(false);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete Server
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const StartServerButton = ({ serverName, userName }) => {
|
const StartServerButton = ({ serverName, userName }) => {
|
||||||
var [isDisabled, setIsDisabled] = useState(false);
|
var [isDisabled, setIsDisabled] = useState(false);
|
||||||
return (
|
return (
|
||||||
@@ -278,7 +323,11 @@ const ServerDashboard = (props) => {
|
|||||||
const userServerName = user.name + serverNameDash;
|
const userServerName = user.name + serverNameDash;
|
||||||
const open = collapseStates[userServerName] || false;
|
const open = collapseStates[userServerName] || false;
|
||||||
return [
|
return [
|
||||||
<tr key={`${userServerName}-row`} className="user-row">
|
<tr
|
||||||
|
key={`${userServerName}-row`}
|
||||||
|
data-testid={`user-row-${userServerName}`}
|
||||||
|
className="user-row"
|
||||||
|
>
|
||||||
<td data-testid="user-row-name">
|
<td data-testid="user-row-name">
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
@@ -324,6 +373,10 @@ const ServerDashboard = (props) => {
|
|||||||
userName={user.name}
|
userName={user.name}
|
||||||
style={{ marginRight: 20 }}
|
style={{ marginRight: 20 }}
|
||||||
/>
|
/>
|
||||||
|
<DeleteServerButton
|
||||||
|
serverName={server.name}
|
||||||
|
userName={user.name}
|
||||||
|
/>
|
||||||
<a
|
<a
|
||||||
href={`${base_url}spawn/${user.name}${
|
href={`${base_url}spawn/${user.name}${
|
||||||
server.name ? "/" + server.name : ""
|
server.name ? "/" + server.name : ""
|
||||||
@@ -582,6 +635,7 @@ ServerDashboard.propTypes = {
|
|||||||
shutdownHub: PropTypes.func,
|
shutdownHub: PropTypes.func,
|
||||||
startServer: PropTypes.func,
|
startServer: PropTypes.func,
|
||||||
stopServer: PropTypes.func,
|
stopServer: PropTypes.func,
|
||||||
|
deleteServer: PropTypes.func,
|
||||||
startAll: PropTypes.func,
|
startAll: PropTypes.func,
|
||||||
stopAll: PropTypes.func,
|
stopAll: PropTypes.func,
|
||||||
dispatch: PropTypes.func,
|
dispatch: PropTypes.func,
|
||||||
|
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { act } from "react-dom/test-utils";
|
import { act } from "react-dom/test-utils";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import { render, screen, fireEvent, getByText } from "@testing-library/react";
|
||||||
import { HashRouter, Switch } from "react-router-dom";
|
import { HashRouter, Switch } from "react-router-dom";
|
||||||
import { Provider, useSelector } from "react-redux";
|
import { Provider, useSelector } from "react-redux";
|
||||||
import { createStore } from "redux";
|
import { createStore } from "redux";
|
||||||
@@ -43,6 +43,31 @@ var mockAsync = (data) =>
|
|||||||
var mockAsyncRejection = () =>
|
var mockAsyncRejection = () =>
|
||||||
jest.fn().mockImplementation(() => Promise.reject());
|
jest.fn().mockImplementation(() => Promise.reject());
|
||||||
|
|
||||||
|
var bar_servers = {
|
||||||
|
"": {
|
||||||
|
name: "",
|
||||||
|
last_activity: "2020-12-07T20:58:02.437408Z",
|
||||||
|
started: "2020-12-07T20:58:01.508266Z",
|
||||||
|
pending: null,
|
||||||
|
ready: false,
|
||||||
|
state: { pid: 12345 },
|
||||||
|
url: "/user/bar/",
|
||||||
|
user_options: {},
|
||||||
|
progress_url: "/hub/api/users/bar/progress",
|
||||||
|
},
|
||||||
|
servername: {
|
||||||
|
name: "servername",
|
||||||
|
last_activity: "2020-12-07T20:58:02.437408Z",
|
||||||
|
started: "2020-12-07T20:58:01.508266Z",
|
||||||
|
pending: null,
|
||||||
|
ready: false,
|
||||||
|
state: { pid: 12345 },
|
||||||
|
url: "/user/bar/servername",
|
||||||
|
user_options: {},
|
||||||
|
progress_url: "/hub/api/users/bar/servername/progress",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
var mockAppState = () =>
|
var mockAppState = () =>
|
||||||
Object.assign({}, initialState, {
|
Object.assign({}, initialState, {
|
||||||
user_data: [
|
user_data: [
|
||||||
@@ -78,19 +103,7 @@ var mockAppState = () =>
|
|||||||
pending: null,
|
pending: null,
|
||||||
created: "2020-12-07T18:46:27.115528Z",
|
created: "2020-12-07T18:46:27.115528Z",
|
||||||
last_activity: "2020-12-07T20:43:51.013613Z",
|
last_activity: "2020-12-07T20:43:51.013613Z",
|
||||||
servers: {
|
servers: bar_servers,
|
||||||
"": {
|
|
||||||
name: "",
|
|
||||||
last_activity: "2020-12-07T20:58:02.437408Z",
|
|
||||||
started: "2020-12-07T20:58:01.508266Z",
|
|
||||||
pending: null,
|
|
||||||
ready: false,
|
|
||||||
state: { pid: 12345 },
|
|
||||||
url: "/user/bar/",
|
|
||||||
user_options: {},
|
|
||||||
progress_url: "/hub/api/users/bar/progress",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
user_page: {
|
user_page: {
|
||||||
@@ -150,9 +163,11 @@ test("Renders users from props.user_data into table", async () => {
|
|||||||
|
|
||||||
let foo = screen.getByTestId("user-name-div-foo");
|
let foo = screen.getByTestId("user-name-div-foo");
|
||||||
let bar = screen.getByTestId("user-name-div-bar");
|
let bar = screen.getByTestId("user-name-div-bar");
|
||||||
|
let bar_server = screen.getByTestId("user-name-div-bar-servername");
|
||||||
|
|
||||||
expect(foo).toBeVisible();
|
expect(foo).toBeVisible();
|
||||||
expect(bar).toBeVisible();
|
expect(bar).toBeVisible();
|
||||||
|
expect(bar_server).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Renders correctly the status of a single-user server", async () => {
|
test("Renders correctly the status of a single-user server", async () => {
|
||||||
@@ -162,10 +177,13 @@ test("Renders correctly the status of a single-user server", async () => {
|
|||||||
render(serverDashboardJsx(callbackSpy));
|
render(serverDashboardJsx(callbackSpy));
|
||||||
});
|
});
|
||||||
|
|
||||||
let start = screen.getByText("Start Server");
|
let start_elems = screen.getAllByText("Start Server");
|
||||||
let stop = screen.getByText("Stop Server");
|
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
|
||||||
|
start_elems.forEach((start) => {
|
||||||
|
expect(start).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
expect(start).toBeVisible();
|
let stop = screen.getByText("Stop Server");
|
||||||
expect(stop).toBeVisible();
|
expect(stop).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -176,9 +194,12 @@ test("Renders spawn page link", async () => {
|
|||||||
render(serverDashboardJsx(callbackSpy));
|
render(serverDashboardJsx(callbackSpy));
|
||||||
});
|
});
|
||||||
|
|
||||||
let link = screen.getByText("Spawn Page").closest("a");
|
for (let server in bar_servers) {
|
||||||
let url = new URL(link.href);
|
let row = screen.getByTestId(`user-row-bar${server ? "-" + server : ""}`);
|
||||||
expect(url.pathname).toEqual("/spawn/bar");
|
let link = getByText(row, "Spawn Page").closest("a");
|
||||||
|
let url = new URL(link.href);
|
||||||
|
expect(url.pathname).toEqual("/spawn/bar" + (server ? "/" + server : ""));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Invokes the startServer event on button click", async () => {
|
test("Invokes the startServer event on button click", async () => {
|
||||||
@@ -188,10 +209,11 @@ test("Invokes the startServer event on button click", async () => {
|
|||||||
render(serverDashboardJsx(callbackSpy));
|
render(serverDashboardJsx(callbackSpy));
|
||||||
});
|
});
|
||||||
|
|
||||||
let start = screen.getByText("Start Server");
|
let start_elems = screen.getAllByText("Start Server");
|
||||||
|
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(start);
|
fireEvent.click(start_elems[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(callbackSpy).toHaveBeenCalled();
|
expect(callbackSpy).toHaveBeenCalled();
|
||||||
@@ -453,10 +475,11 @@ test("Shows a UI error dialogue when start user server fails", async () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let start = screen.getByText("Start Server");
|
let start_elems = screen.getAllByText("Start Server");
|
||||||
|
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(start);
|
fireEvent.click(start_elems[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
let errorDialog = screen.getByText("Failed to start server.");
|
let errorDialog = screen.getByText("Failed to start server.");
|
||||||
@@ -487,10 +510,11 @@ test("Shows a UI error dialogue when start user server returns an improper statu
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let start = screen.getByText("Start Server");
|
let start_elems = screen.getAllByText("Start Server");
|
||||||
|
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(start);
|
fireEvent.click(start_elems[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
let errorDialog = screen.getByText("Failed to start server.");
|
let errorDialog = screen.getByText("Failed to start server.");
|
||||||
@@ -656,3 +680,20 @@ test("Interacting with PaginationFooter causes state update and refresh via useE
|
|||||||
// expect(callbackSpy.mock.calls).toHaveLength(2);
|
// expect(callbackSpy.mock.calls).toHaveLength(2);
|
||||||
// expect(callbackSpy).toHaveBeenCalledWith(2, 2, "");
|
// expect(callbackSpy).toHaveBeenCalledWith(2, 2, "");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Server delete button exists for named servers", async () => {
|
||||||
|
let callbackSpy = mockAsync();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(serverDashboardJsx(callbackSpy));
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let server in bar_servers) {
|
||||||
|
if (server === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let row = screen.getByTestId(`user-row-bar-${server}`);
|
||||||
|
let delete_button = getByText(row, "Delete Server");
|
||||||
|
expect(delete_button).toBeEnabled();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@@ -18,6 +18,12 @@ const withAPI = withProps(() => ({
|
|||||||
jhapiRequest("/users/" + name + "/servers/" + (serverName || ""), "POST"),
|
jhapiRequest("/users/" + name + "/servers/" + (serverName || ""), "POST"),
|
||||||
stopServer: (name, serverName = "") =>
|
stopServer: (name, serverName = "") =>
|
||||||
jhapiRequest("/users/" + name + "/servers/" + (serverName || ""), "DELETE"),
|
jhapiRequest("/users/" + name + "/servers/" + (serverName || ""), "DELETE"),
|
||||||
|
deleteServer: (name, serverName = "") =>
|
||||||
|
jhapiRequest(
|
||||||
|
"/users/" + name + "/servers/" + (serverName || ""),
|
||||||
|
"DELETE",
|
||||||
|
{ remove: true },
|
||||||
|
),
|
||||||
startAll: (names) =>
|
startAll: (names) =>
|
||||||
names.map((e) => jhapiRequest("/users/" + e + "/server", "POST")),
|
names.map((e) => jhapiRequest("/users/" + e + "/server", "POST")),
|
||||||
stopAll: (names) =>
|
stopAll: (names) =>
|
||||||
|
Reference in New Issue
Block a user