diff --git a/packages/web_ui/src/components/ControllerPage.tsx b/packages/web_ui/src/components/ControllerPage.tsx index 97a2aa43..9e241bb5 100644 --- a/packages/web_ui/src/components/ControllerPage.tsx +++ b/packages/web_ui/src/components/ControllerPage.tsx @@ -1,5 +1,5 @@ import React, {useContext, useState} from "react"; -import { Button, Descriptions, Flex, Popconfirm, Space, Tooltip, Typography } from "antd"; +import { Descriptions, Flex, Popconfirm, Space, Tooltip, Typography } from "antd"; import { ExclamationCircleOutlined } from "@ant-design/icons"; import * as lib from "@clusterio/lib"; @@ -19,13 +19,65 @@ import PageLayout from "./PageLayout"; import { notifyErrorHandler } from "../util/notify"; import webUiPackage from "../../package.json"; import { hasNpmButtonPermission, NpmButton } from "./NpmButton"; +import VariableDropdownButton, { VariableDropdownButtonProps } from "./VariableDropdownButton"; const { Title } = Typography; +type ControllerControlButtonProps = { canRestart?: boolean, restartRequired?: boolean }; + +function ControllerControlButton({ canRestart, restartRequired }: ControllerControlButtonProps) { + const account = useAccount(); + const control = useContext(ControlContext); + const actions: VariableDropdownButtonProps["actions"] = []; + + if (account.hasPermission("core.controller.restart") && canRestart) { + actions.push({ + key: "restart", + label: ( + + Restart {restartRequired ? : undefined} + + ), + onClick: () => { + control.send( + new lib.ControllerRestartRequest() + ).catch(notifyErrorHandler("Error restarting controller")); + }, + }); + } + + if (account.hasPermission("core.controller.stop")) { + actions.push({ + key: "stop", + danger: true, + label: ( + + Stopping the controller will leave the cluster inoperable until someone with + access to the system it runs on manually starts it again.
+ Are you sure you want to stop the controller? + } + placement="bottomRight" + okText="Stop" + okButtonProps={{ danger: true }} + onConfirm={() => { + control.send( + new lib.ControllerStopRequest() + ).catch(notifyErrorHandler("Error stopping controller")); + }} + > + Stop +
+ ), + }); + } + + return ; +} + export default function ControllerPage() { - let account = useAccount(); - const control = useContext(ControlContext); + const account = useAccount(); const [systems] = useSystems(); const system = systems.get("controller"); const [maxLevel, setMaxLevel] = useState("info"); @@ -34,43 +86,7 @@ export default function ControllerPage() { - { - account.hasPermission("core.controller.stop") - && - Stopping the controller will leave the cluster inoperable until someone with - access to the system it runs on manually starts it again.
- Are you sure you want to stop the controller? - } - placement="bottomRight" - okText="Stop" - okButtonProps={{ danger: true }} - onConfirm={() => { - control.send( - new lib.ControllerStopRequest() - ).catch(notifyErrorHandler("Error stopping controller")); - }} - > - -
- } - { - account.hasPermission("core.controller.restart") - && - - - } + { hasNpmButtonPermission(true) && diff --git a/packages/web_ui/src/components/HostViewPage.tsx b/packages/web_ui/src/components/HostViewPage.tsx index bd689ee4..93374cfa 100644 --- a/packages/web_ui/src/components/HostViewPage.tsx +++ b/packages/web_ui/src/components/HostViewPage.tsx @@ -22,9 +22,64 @@ import { import { formatTimestamp } from "../util/time_format"; import { useSystems } from "../model/system"; import { hasNpmButtonPermission, NpmButton } from "./NpmButton"; +import VariableDropdownButton, { VariableDropdownButtonProps } from "./VariableDropdownButton"; const { Title } = Typography; +type HostControlButtonProps = { hostId: number, canRestart?: boolean, restartRequired?: boolean }; + +function HostControlButton({ hostId, canRestart, restartRequired }: HostControlButtonProps) { + const account = useAccount(); + const control = useContext(ControlContext); + const actions: VariableDropdownButtonProps["actions"] = []; + + if (account.hasPermission("core.host.restart") && canRestart) { + actions.push({ + key: "restart", + label: ( + + Restart {restartRequired ? : undefined} + + ), + onClick: () => { + control.sendTo( + { hostId }, + new lib.HostRestartRequest() + ).catch(notifyErrorHandler("Error restarting host")); + }, + }); + } + + if (account.hasPermission("core.host.stop")) { + actions.push({ + key: "stop", + danger: true, + label: ( + + Stopping this host will leave it inaccessible until someone with + access to the system it runs on manually starts it again.
+ Are you sure you want to stop the host? + } + placement="bottomRight" + okText="Stop" + okButtonProps={{ danger: true }} + onConfirm={() => { + control.sendTo( + { hostId }, + new lib.HostStopRequest() + ).catch(notifyErrorHandler("Error stopping host")); + }} + > + Stop +
+ ), + }); + } + + return ; +} + export default function HostViewPage() { let params = useParams(); let hostId = Number(params.id); @@ -50,6 +105,7 @@ export default function HostViewPage() { } let hostButtons = + { account.hasPermission("core.host.revoke_token") && } - { - account.hasPermission("core.host.stop") - && - Stopping this host will leave it inaccessible until someone with - access to the system it runs on manually starts it again.
- Are you sure you want to stop the host? - } - placement="bottomRight" - okText="Stop" - okButtonProps={{ danger: true }} - onConfirm={() => { - control.sendTo( - { hostId }, - new lib.HostStopRequest() - ).catch(notifyErrorHandler("Error stopping host")); - }} - > - -
- } - { - account.hasPermission("core.host.restart") - && - - - } { hasNpmButtonPermission(false) && diff --git a/packages/web_ui/src/components/InstanceControlButton.tsx b/packages/web_ui/src/components/InstanceControlButton.tsx index dcc401b9..2f1636e3 100644 --- a/packages/web_ui/src/components/InstanceControlButton.tsx +++ b/packages/web_ui/src/components/InstanceControlButton.tsx @@ -1,14 +1,14 @@ import React, { useContext, useState } from "react"; -import { Button, MenuProps } from "antd"; +import { Popconfirm } 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"; +import VariableDropdownButton, { VariableDropdownButtonProps } from "./VariableDropdownButton"; -type InstanceControlButtonProps = DropdownButtonProps & { +type InstanceControlButtonProps = Omit & { instance: lib.InstanceDetails; onFinish?: () => void; }; @@ -34,94 +34,89 @@ export default function InstanceControlButton(props: InstanceControlButtonProps) } } - const onStopStartClick: DropdownButtonProps["onClick"] = (event) => { - event.stopPropagation(); - setSwitching(true); + const actions: VariableDropdownButtonProps["actions"] = []; - if (showStart) { - control.sendTo( - { instanceId }, - new lib.InstanceStartRequest(undefined), - ).catch( - notifyErrorHandler("Error starting instance") - ).finally(actionFinish); + if (account.hasPermission("core.instance.start") && showStart !== switching) { + actions.push({ + key: "start", + label: "Start", + onClick: () => { + setSwitching(true); + 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); + } else if (account.hasPermission("core.instance.stop")) { // else if because it is stop or start + actions.push({ + key: "stop", + label: "Stop", + danger: true, + onClick: () => { + setSwitching(true); + 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": + if (account.hasPermission("core.instance.restart")) { + actions.push({ + key: "restart", + label: "Restart", + disabled: instanceStatus !== "running", + onClick: () => { + setSwitching(true); 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({ + actions.push({ key: "kill", - label: "Kill", + danger: true, disabled: instanceStatus === "stopped", + label: ( + Killing this instance may leave you save and player data corrupted. + Are you sure you want to kill the instance? + } + placement="bottomRight" + okText="Kill" + okButtonProps={{ danger: true }} + onConfirm={() => { + control.sendTo( + { instanceId }, + new lib.InstanceKillRequest(), + ).catch( + notifyErrorHandler("Error killing instance") + ).finally(actionFinish); + }} + > + Kill + ), }); } - 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"} - - : ; - + return ; } diff --git a/packages/web_ui/src/components/VariableDropdownButton.tsx b/packages/web_ui/src/components/VariableDropdownButton.tsx new file mode 100644 index 00000000..c7078784 --- /dev/null +++ b/packages/web_ui/src/components/VariableDropdownButton.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { Button, Dropdown } from "antd"; + +import { MenuItemType } from "antd/es/menu/interface"; +import { DropdownButtonProps } from "antd/es/dropdown"; + +export type VariableDropdownButtonProps = DropdownButtonProps & { + actions: (MenuItemType & { onClick?: () => void })[], +}; + +export default function VariableDropdownButton(props: VariableDropdownButtonProps) { + const menuActions = props.actions; + const mainAction = menuActions.shift(); + if (!mainAction) { + return <>; + } + + const buttonProps: DropdownButtonProps = { + ...props, + danger: mainAction.danger, + onClick: (event) => { + event.stopPropagation(); + if (mainAction.onClick) { + mainAction.onClick(); + } + }, + }; + + return menuActions.length === 0 + ? + : e.domEvent.stopPropagation() }} + buttonsRender={([left, right]) => [ + left, React.cloneElement(right as any, { onClick: (e: any) => e.stopPropagation() }), + ]} + > + {mainAction.label} + ; +}