Make the control buttons consistent

This commit is contained in:
Cooldude2606 committed 2025-07-15 00:49:33 +01:00
1 parent f11d91bd6b
commit 59256e77d1
4 files changed
+226 -155

No files matched your search

@@ -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: (
<Tooltip title={restartRequired ? "Restart Required" : null}>
Restart {restartRequired ? <ExclamationCircleOutlined style={{ color: "yellow" }}/> : undefined}
</Tooltip>
),
onClick: () => {
control.send(
new lib.ControllerRestartRequest()
).catch(notifyErrorHandler("Error restarting controller"));
},
});
}
if (account.hasPermission("core.controller.stop")) {
actions.push({
key: "stop",
danger: true,
label: (
<Popconfirm
title={<>
Stopping the controller will leave the cluster inoperable until someone with
access to the system it runs on manually starts it again.<br />
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
</Popconfirm>
),
});
}
return <VariableDropdownButton actions={actions} />;
}
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<keyof typeof lib.levels>("info");
@@ -34,43 +86,7 @@ export default function ControllerPage() {
<PageHeader
title="Controller"
extra={<Space>
{
account.hasPermission("core.controller.stop")
&& <Popconfirm
title={<>
Stopping the controller will leave the cluster inoperable until someone with
access to the system it runs on manually starts it again.<br />
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"));
}}
>
<Button danger>Stop</Button>
</Popconfirm>
}
{
account.hasPermission("core.controller.restart")
&& <Tooltip title={system?.restartRequired ? "Restart Required" : null}>
<Button
disabled={system?.canRestart === false}
onClick={() => {
control.send(
new lib.ControllerRestartRequest()
).catch(notifyErrorHandler("Error restarting controller"));
}}
>
Restart
{!system?.restartRequired ? undefined
: <ExclamationCircleOutlined style={{ color: "yellow" }}/>}
</Button>
</Tooltip>
}
<ControllerControlButton canRestart={system?.canRestart} restartRequired={system?.restartRequired}/>
{
hasNpmButtonPermission(true)
&& <NpmButton target="controller" canRestart={system?.canRestart}/>
+56 -39
View File
@@ -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: (
<Tooltip title={restartRequired ? "Restart Required" : null}>
Restart {restartRequired ? <ExclamationCircleOutlined style={{ color: "yellow" }}/> : undefined}
</Tooltip>
),
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: (
<Popconfirm
title={<>
Stopping this host will leave it inaccessible until someone with
access to the system it runs on manually starts it again.<br />
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
</Popconfirm>
),
});
}
return <VariableDropdownButton actions={actions} />;
}
export default function HostViewPage() {
let params = useParams();
let hostId = Number(params.id);
@@ -50,6 +105,7 @@ export default function HostViewPage() {
}
let hostButtons = <Space>
<HostControlButton hostId={hostId} canRestart={system?.canRestart} restartRequired={system?.restartRequired}/>
{
account.hasPermission("core.host.revoke_token")
&& <Popconfirm
@@ -68,45 +124,6 @@ export default function HostViewPage() {
</Button>
</Popconfirm>
}
{
account.hasPermission("core.host.stop")
&& <Popconfirm
title={<>
Stopping this host will leave it inaccessible until someone with
access to the system it runs on manually starts it again.<br />
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"));
}}
>
<Button danger>Stop</Button>
</Popconfirm>
}
{
account.hasPermission("core.host.restart")
&& <Tooltip title={system?.restartRequired ? "Restart Required" : null}>
<Button
disabled={system?.canRestart === false}
onClick={() => {
control.sendTo(
{ hostId },
new lib.HostRestartRequest()
).catch(notifyErrorHandler("Error restarting host"));
}}
>
Restart
{!system?.restartRequired ? undefined
: <ExclamationCircleOutlined style={{ color: "yellow" }} />}
</Button>
</Tooltip>
}
{
hasNpmButtonPermission(false)
&& <NpmButton target={{ hostId }} canRestart={system?.canRestart} disabled={!host["connected"]}/>
@@ -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<VariableDropdownButtonProps, "actions"> & {
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: (<Popconfirm
title={<>
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
</Popconfirm>),
});
}
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
? <Dropdown.Button
{...buttonProps}
menu={{ items: extraControls, onClick: onMenuClick }}
buttonsRender={([left, right]) => [ // 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"}
</Dropdown.Button>
: <Button {...buttonProps}>
{showStart !== switching ? "Start" : "Stop"}
</Button>;
return <VariableDropdownButton
{...props}
type="primary"
actions={actions}
loading={switching}
disabled={!(showStart || showStop)}
/>;
}
@@ -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
? <Button {...buttonProps}>
{mainAction.label}
</Button>
: <Dropdown.Button
{...buttonProps}
// stopPropagation is needed to prevent propagation to table rows
menu={{ items: menuActions, onClick: (e: any) => e.domEvent.stopPropagation() }}
buttonsRender={([left, right]) => [
left, React.cloneElement(right as any, { onClick: (e: any) => e.stopPropagation() }),
]}
>
{mainAction.label}
</Dropdown.Button>;
}