Merge pull request #4457 from diocas/fix_4174

Delete server button on admin page
This commit is contained in:
Min RK
2023-06-02 11:46:24 +02:00
committed by GitHub
3 changed files with 128 additions and 27 deletions

View File

@@ -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,

View File

@@ -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();
}
});

View File

@@ -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) =>