Files
factorio-clusterio/packages/web_ui/src/components/HostViewPage.tsx
T
Cooldude2606 73cc019d2f Sets restart required when a plugin is updated (#794)
* Template type for load plugin config

* Add plugin update checking

* Add restart icon to instance list

* Add tests

* Log package name when failing to read package json
2025-11-01 16:11:11 +00:00

187 lines
6.9 KiB
TypeScript

import React, { useContext, useState } from "react";
import { Descriptions, Spin, Tag, Typography, Button, Space, Modal, Popconfirm, Flex, Tooltip } from "antd";
import { ExclamationCircleOutlined } from "@ant-design/icons";
import { useParams } from "react-router-dom";
import * as lib from "@clusterio/lib";
import notify, { notifyErrorHandler } from "../util/notify";
import ControlContext from "./ControlContext";
import HostConfigTree from "./HostConfigTree";
import InstanceList from "./InstanceList";
import LogConsole, { SelectMaxLogLevel } from "./LogConsole";
import { useAccount } from "../model/account";
import { useInstances } from "../model/instance";
import { useHost } from "../model/host";
import PageHeader from "./PageHeader";
import PageLayout from "./PageLayout";
import PluginExtra from "./PluginExtra";
import {
MetricCpuRatio, MetricCpuUsed, MetricMemoryRatio, MetricMemoryUsed,
MetricDiskUsed, MetricDiskRatio,
} from "./system_metrics";
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: (
!restartRequired ? "Restart" : <Tooltip title={"Restart Required"}><Space>
<ExclamationCircleOutlined style={{ color: "yellow" }}/>
Restart
</Space></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);
let control = useContext(ControlContext);
let account = useAccount();
let [instances] = useInstances();
const [systems] = useSystems();
const system = systems.get(hostId);
const [host, synced] = useHost(hostId);
const [maxLevel, setMaxLevel] = useState<keyof typeof lib.levels>("info");
const canShowNpm = hasNpmButtonPermission(false);
const hostInstances = new Map([...instances].filter(([_id, instance]) => instance.assignedHost === hostId));
let nav = [{ name: "Hosts", path: "/hosts" }, { name: host?.name ?? String(hostId) }];
if (!host) {
if (!synced) {
return <PageLayout nav={nav}><Spin size="large" /></PageLayout>;
}
return <PageLayout nav={nav}>
<PageHeader title={String(hostId)} />
<p>Host with id {hostId} was not found on the controller.</p>
</PageLayout>;
}
let hostButtons = <Space>
<HostControlButton hostId={hostId} canRestart={system?.canRestart} restartRequired={system?.restartRequired}/>
{
account.hasPermission("core.host.revoke_token")
&& <Popconfirm
title={`Revoke tokens of ${host.name}?`}
placement="bottomRight"
okText="Revoke Tokens"
okButtonProps={{ danger: true }}
onConfirm={() => {
control.send(new lib.HostRevokeTokensRequest(host.id!))
.then(() => notify("Host tokens revoked"))
.catch(notifyErrorHandler(`Error revoking tokens for host id:${host.id}`));
}}
>
<Button danger>
Revoke tokens
</Button>
</Popconfirm>
}
{canShowNpm && (
<NpmButton target={{ hostId }} canRestart={system?.canRestart} disabled={!host["connected"]}/>
)}
</Space>;
return <PageLayout nav={nav}>
<PageHeader
title={host.name || String(hostId)}
extra={hostButtons}
/>
<Descriptions bordered size="small" column={{ xs: 1, md: 2, lg: 2, xl: 2, xxl: 2 }}>
<Descriptions.Item label="Name">{host["name"]}</Descriptions.Item>
<Descriptions.Item label="Connected">
<Tag color={host["connected"] ? "#389e0d" : "#cf1322"}>
{host["connected"] ? "Connected" : "Disconnected"}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="Version">{host.version}</Descriptions.Item>
<Descriptions.Item label="Node.js">{system?.node}</Descriptions.Item>
<Descriptions.Item label="OS Kernel">{system?.kernel}</Descriptions.Item>
<Descriptions.Item label="Machine">{system?.machine}</Descriptions.Item>
<Descriptions.Item label="Hostname" span={2}>{system?.hostname}</Descriptions.Item>
<Descriptions.Item label="Connected From" span={2}>{host.remoteAddress}</Descriptions.Item>
<Descriptions.Item label="CPU Model" span={2}>{system?.cpuModel}</Descriptions.Item>
<Descriptions.Item label="CPU Usage"><MetricCpuRatio system={system} /></Descriptions.Item>
<Descriptions.Item label="Cores"><MetricCpuUsed system={system} /></Descriptions.Item>
<Descriptions.Item label="Memory Usage"><MetricMemoryRatio system={system} /></Descriptions.Item>
<Descriptions.Item label="Memory"><MetricMemoryUsed system={system} /></Descriptions.Item>
<Descriptions.Item label="Disk Usage"><MetricDiskRatio system={system} /></Descriptions.Item>
<Descriptions.Item label="Disk"><MetricDiskUsed system={system} /></Descriptions.Item>
<Descriptions.Item label="Public Address">{host.publicAddress}</Descriptions.Item>
{
host.tokenValidAfter
? <Descriptions.Item label="Tokens valid after:">
{formatTimestamp(host.tokenValidAfter*1000)}
</Descriptions.Item>
: null
}
</Descriptions>
{account.hasPermission("core.instance.list") && <>
<Title level={5} style={{ marginTop: 16 }}>Instances</Title>
<InstanceList instances={hostInstances} size="small" hideAssignedHost />
</>}
{account.hasPermission("core.log.follow") && <>
<Flex justify="space-between" align="baseline">
<Title level={5} style={{ marginTop: 16 }}>Console</Title>
<SelectMaxLogLevel
value={maxLevel}
onChange={setMaxLevel}
hidden={["server", "http"]}
/>
</Flex>
<LogConsole hosts={[hostId]} maxLevel={maxLevel}/>
</>}
{account.hasPermission("core.host.get_config") && <HostConfigTree id={hostId} available={host.connected} />}
<PluginExtra component="HostViewPage" host={host} />
</PageLayout>;
}