diff --git a/packages/web_ui/src/components/InstanceControlButton.tsx b/packages/web_ui/src/components/InstanceControlButton.tsx new file mode 100644 index 00000000..dcc401b9 --- /dev/null +++ b/packages/web_ui/src/components/InstanceControlButton.tsx @@ -0,0 +1,127 @@ +import React, { useContext, useState } from "react"; +import { Button, MenuProps } from "antd"; + +import * as lib from "@clusterio/lib"; + +import ControlContext from "./ControlContext"; +import { useAccount } from "../model/account"; +import { notifyErrorHandler } from "../util/notify"; +import Dropdown, { DropdownButtonProps } from "antd/es/dropdown"; + +type InstanceControlButtonProps = DropdownButtonProps & { + instance: lib.InstanceDetails; + onFinish?: () => void; +}; + +export const InstanceControlButtonPermissions = [ + "core.instance.start", "core.instance.restart", "core.instance.stop", "core.instance.kill", +] as const; + +export default function InstanceControlButton(props: InstanceControlButtonProps) { + const account = useAccount(); + const control = useContext(ControlContext); + const [switching, setSwitching] = useState(false); + + const instanceId = props.instance.id; + const instanceStatus = props.instance.status; + const showStart = ["stopped", "stopping"].includes(instanceStatus); + const showStop = ["running", "starting"].includes(instanceStatus); + + function actionFinish() { + setSwitching(false); + if (props.onFinish) { + props.onFinish(); + } + } + + const onStopStartClick: DropdownButtonProps["onClick"] = (event) => { + event.stopPropagation(); + setSwitching(true); + + if (showStart) { + control.sendTo( + { instanceId }, + new lib.InstanceStartRequest(undefined), + ).catch( + notifyErrorHandler("Error starting instance") + ).finally(actionFinish); + + } else { + control.sendTo( + { instanceId }, + new lib.InstanceStopRequest(), + ).catch( + notifyErrorHandler("Error stopping instance") + ).finally(actionFinish); + + } + }; + + const onMenuClick: MenuProps["onClick"] = (event) => { + event.domEvent.stopPropagation(); + setSwitching(true); + + switch (event.key) { + case "restart": + control.sendTo( + { instanceId }, + new lib.InstanceRestartRequest(undefined), + ).catch( + notifyErrorHandler("Error restarting instance") + ).finally(actionFinish); + break; + + case "kill": + control.sendTo( + { instanceId }, + new lib.InstanceKillRequest(), + ).catch( + notifyErrorHandler("Error killing instance") + ).finally(actionFinish); + break; + + default: + notifyErrorHandler("Unknown control action"); + } + }; + + const extraControls: MenuProps["items"] = []; + if (account.hasPermission("core.instance.restart")) { + extraControls.push({ + key: "restart", + label: "Restart", + disabled: instanceStatus !== "running", + }); + } + if (account.hasPermission("core.instance.kill")) { + extraControls.push({ + key: "kill", + label: "Kill", + disabled: instanceStatus === "stopped", + }); + } + + const buttonProps: DropdownButtonProps = { + ...props, + type: "primary", + loading: switching, + onClick: onStopStartClick, + disabled: !(showStart || showStop), + danger: showStop !== switching, // Used as an XOR, also below + }; + + return extraControls.length > 0 + ? [ // This is needed to prevent propagation to the table row + left, React.cloneElement(right as any, { onClick: (e: any) => e.stopPropagation() }), + ]} + > + {showStart !== switching ? "Start" : "Stop"} + + : ; + +} diff --git a/packages/web_ui/src/components/InstanceList.tsx b/packages/web_ui/src/components/InstanceList.tsx index 3c3a67ab..fe299f00 100644 --- a/packages/web_ui/src/components/InstanceList.tsx +++ b/packages/web_ui/src/components/InstanceList.tsx @@ -8,7 +8,7 @@ import type { ColumnsType } from "antd/es/table"; import { useAccount } from "../model/account"; import { useHosts } from "../model/host"; import InstanceStatusTag from "./InstanceStatusTag"; -import StartStopInstanceButton from "./StartStopInstanceButton"; +import InstanceControlButton, { InstanceControlButtonPermissions } from "./InstanceControlButton"; import * as lib from "@clusterio/lib"; import Link from "./Link"; import { instancePublicAddress } from "../util/instance"; @@ -98,13 +98,10 @@ export default function InstanceList(props: InstanceListProps) { }, ]; - if (account.hasAnyPermission("core.instance.start", "core.instance.stop")) { + if (account.hasAnyPermission(...InstanceControlButtonPermissions)) { columns.push({ key: "action", - render: (_, instance) => , + render: (_, instance) => , responsive: ["sm"], align: "right", width: 100, diff --git a/packages/web_ui/src/components/InstanceViewPage.tsx b/packages/web_ui/src/components/InstanceViewPage.tsx index b2ad9d8d..cf1d76dc 100644 --- a/packages/web_ui/src/components/InstanceViewPage.tsx +++ b/packages/web_ui/src/components/InstanceViewPage.tsx @@ -15,7 +15,7 @@ import InstanceConfigTree from "./InstanceConfigTree"; import LogConsole, { SelectMaxLogLevel } from "./LogConsole"; import InstanceRcon from "./InstanceRcon"; import AssignInstanceModal from "./AssignInstanceModal"; -import StartStopInstanceButton from "./StartStopInstanceButton"; +import InstanceControlButton, { InstanceControlButtonPermissions } from "./InstanceControlButton"; import LoadScenarioModal from "./LoadScenarioModal"; import SavesList from "./SavesList"; import { notifyErrorHandler } from "../util/notify"; @@ -91,13 +91,6 @@ function InstanceButtons(props: { instance: lib.InstanceDetails }) { label: "Extract players from save", }); } - if (account.hasPermission("core.instance.kill")) { - instanceButtonMenuItems.push({ - disabled: ["unknown", "unassigned", "stopped"].includes(instance.status!), - key: "kill", - label: "Kill process", - }); - } if (account.hasPermission("core.instance.delete")) { if (instanceButtonMenuItems.length) { instanceButtonMenuItems.push({ type: "divider" }); @@ -130,12 +123,6 @@ function InstanceButtons(props: { instance: lib.InstanceDetails }) { new lib.InstanceExtractPlayersRequest(), ).catch(notifyErrorHandler("Error extracting player data")); - } else if (key === "kill") { - control.sendTo( - { instanceId }, - new lib.InstanceKillRequest(), - ).catch(notifyErrorHandler("Error killing instance")); - } else if (key === "delete") { Modal.confirm({ autoFocusButton: "cancel", @@ -155,8 +142,8 @@ function InstanceButtons(props: { instance: lib.InstanceDetails }) { }; return { - account.hasAnyPermission("core.instance.start", "core.instance.stop") - && + account.hasAnyPermission(...InstanceControlButtonPermissions) + && } ; -} diff --git a/packages/web_ui/src/index.ts b/packages/web_ui/src/index.ts index 2053560a..748e201f 100644 --- a/packages/web_ui/src/index.ts +++ b/packages/web_ui/src/index.ts @@ -22,7 +22,7 @@ export { default as AssignInstanceModal } from "./components/AssignInstanceModal export { default as InstanceList } from "./components/InstanceList"; export { default as InstanceRcon } from "./components/InstanceRcon"; export { default as InstanceStatusTag, statusColors } from "./components/InstanceStatusTag"; -export { default as StartStopInstanceButton } from "./components/StartStopInstanceButton"; +export { default as InstanceControlButton, InstanceControlButtonPermissions } from "./components/InstanceControlButton"; export { default as PageHeader } from "./components/PageHeader"; export { default as PageLayout } from "./components/PageLayout";