mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Add host, server name and start options to the create instance modal
Accounts with core.instance.assign get a host selector, defaulting to unassigned. Picking a host offers "Start after creation", which creates a new save on first run when no save is copied, and together with a config to copy it offers "Copy Save". Copying a config also offers a server name, prefilled from the source, which overwrites factorio.settings.name so the clone does not show up under the source name in the server browser. useInstanceConfig now clears the config while loading and drops errors, so an account without core.instance.get_config just does not get the server name field. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c997fd8ea7
commit
9b17fa2b93
2 files changed
+99
-48
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button, Checkbox, Form, Input, Modal, Select, Space, Tooltip } from "antd";
|
||||
import { Button, Checkbox, Form, Input, Modal, Select, Space, Tooltip, Typography } from "antd";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
@@ -9,28 +9,45 @@ import ControlContext from "./ControlContext";
|
||||
import PageHeader from "./PageHeader";
|
||||
import PageLayout from "./PageLayout";
|
||||
import PluginExtra from "./PluginExtra";
|
||||
import { useInstances } from "../model/instance";
|
||||
import { useHosts } from "../model/host";
|
||||
import { useInstanceConfig, useInstances } from "../model/instance";
|
||||
import { useSaves } from "../model/saves";
|
||||
import InstanceList from "./InstanceList";
|
||||
import { notifyErrorHandler } from "../util/notify";
|
||||
|
||||
type SaveMode = "none" | "copy" | "save_and_copy";
|
||||
|
||||
const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare;
|
||||
|
||||
function CreateInstanceButton(props: { instances: ReturnType<typeof useInstances>[0] }) {
|
||||
let control = useContext(ControlContext);
|
||||
let account = useAccount();
|
||||
let navigate = useNavigate();
|
||||
let [open, setOpen] = useState(false);
|
||||
let [creating, setCreating] = useState(false);
|
||||
let [form] = Form.useForm();
|
||||
let [hosts] = useHosts();
|
||||
let [saves] = useSaves();
|
||||
|
||||
const canAssign = account.hasPermission("core.instance.assign");
|
||||
const cloneId: number | undefined = Form.useWatch("instanceClone", form);
|
||||
const host: number | string | undefined = Form.useWatch("host", form);
|
||||
const saveMode: SaveMode = Form.useWatch("saveMode", form) ?? "none";
|
||||
const hostId = typeof host === "number" ? host : undefined;
|
||||
const source = cloneId !== undefined && cloneId >= 0 ? props.instances.get(cloneId) : undefined;
|
||||
const sourceRunning = source?.status === "running";
|
||||
const activeSave = source ? [...saves.values()].find(
|
||||
save => save.instanceId === source.id && (sourceRunning ? save.loaded : save.loadByDefault)
|
||||
) : undefined;
|
||||
const sourceConfig = useInstanceConfig(source?.id);
|
||||
const sourceSettings = sourceConfig?.["factorio.settings"] as Record<string, unknown> | undefined;
|
||||
const sourceServerName = typeof sourceSettings?.name === "string" ? sourceSettings.name : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (open && sourceServerName !== undefined) {
|
||||
form.setFieldValue("serverName", sourceServerName);
|
||||
}
|
||||
}, [open, sourceServerName]);
|
||||
|
||||
async function createInstance() {
|
||||
let values = form.getFieldsValue();
|
||||
@@ -38,7 +55,7 @@ function CreateInstanceButton(props: { instances: ReturnType<typeof useInstances
|
||||
form.setFields([{ name: "instanceName", errors: ["Name is required"] }]);
|
||||
return;
|
||||
}
|
||||
const copySave = source !== undefined && saveMode !== "none";
|
||||
const copySave = source !== undefined && hostId !== undefined && saveMode !== "none";
|
||||
if (copySave && !activeSave) {
|
||||
form.setFields([{ name: "saveMode", errors: ["The source instance has no save to copy"] }]);
|
||||
return;
|
||||
@@ -50,30 +67,41 @@ function CreateInstanceButton(props: { instances: ReturnType<typeof useInstances
|
||||
|
||||
let instanceConfig = new lib.InstanceConfig("control");
|
||||
instanceConfig.set("instance.name", values.instanceName);
|
||||
const fields: (keyof lib.InstanceConfigFields)[] = ["instance.id", "instance.name"];
|
||||
if (source && sourceSettings) {
|
||||
if (!values.serverName) {
|
||||
form.setFields([{ name: "serverName", errors: ["Server name is required"] }]);
|
||||
return;
|
||||
}
|
||||
instanceConfig.set("factorio.settings", { ...sourceSettings, name: values.serverName });
|
||||
fields.push("factorio.settings");
|
||||
}
|
||||
const instanceId = instanceConfig.get("instance.id");
|
||||
|
||||
setCreating(true);
|
||||
let created = false;
|
||||
try {
|
||||
await control.send(new lib.InstanceCreateRequest(
|
||||
instanceConfig.toRemote("controller", [
|
||||
"instance.id", "instance.name",
|
||||
]),
|
||||
instanceConfig.toRemote("controller", fields),
|
||||
source?.id,
|
||||
));
|
||||
created = true;
|
||||
|
||||
if (hostId !== undefined) {
|
||||
await control.send(new lib.InstanceAssignRequest(instanceId, hostId));
|
||||
}
|
||||
|
||||
let saveName: string | undefined;
|
||||
if (copySave) {
|
||||
await control.send(new lib.InstanceAssignRequest(instanceId, source.assignedHost));
|
||||
if (saveMode === "save_and_copy") {
|
||||
await control.sendTo({ instanceId: source.id }, new lib.InstanceSaveGameRequest());
|
||||
}
|
||||
const saveName = await control.send(new lib.InstanceTransferSaveRequest(
|
||||
saveName = await control.send(new lib.InstanceTransferSaveRequest(
|
||||
source.id, activeSave!.name, instanceId, activeSave!.name, true,
|
||||
));
|
||||
if (values.startAfter) {
|
||||
await control.sendTo({ instanceId }, new lib.InstanceStartRequest(saveName));
|
||||
}
|
||||
}
|
||||
if (values.startAfter && hostId !== undefined) {
|
||||
await control.sendTo({ instanceId }, new lib.InstanceStartRequest(saveName));
|
||||
}
|
||||
} finally {
|
||||
setCreating(false);
|
||||
@@ -100,13 +128,30 @@ function CreateInstanceButton(props: { instances: ReturnType<typeof useInstances
|
||||
onCancel={() => { setOpen(false); }}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form form={form} initialValues={{ instanceClone: -1, saveMode: "none", startAfter: false }}>
|
||||
<Form
|
||||
form={form}
|
||||
initialValues={{ host: "null", instanceClone: -1, saveMode: "none", startAfter: false }}
|
||||
>
|
||||
<Form.Item name="instanceName" label="Name">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Tooltip
|
||||
title="Perform a one time copy of an existing config (the host is only copied along with a save)"
|
||||
>
|
||||
{canAssign && <Form.Item name="host" label="Host">
|
||||
<Select showSearch optionFilterProp="name">
|
||||
<Select.Option value={"null"}>
|
||||
<Typography.Text italic>Unassigned</Typography.Text>
|
||||
</Select.Option>
|
||||
{[...hosts.values()].sort((a, b) => strcmp(a.name, b.name)).map(h => <Select.Option
|
||||
key={h.id}
|
||||
value={h.id}
|
||||
name={h.name}
|
||||
disabled={!h.connected}
|
||||
>
|
||||
{h.name}
|
||||
{!h.connected && " (offline)"}
|
||||
</Select.Option>)}
|
||||
</Select>
|
||||
</Form.Item>}
|
||||
<Tooltip title="Perform a one time copy of the config of an existing instance">
|
||||
<Form.Item name="instanceClone" label="Copy Config">
|
||||
<Select
|
||||
options={[{ id: -1, name: "Default Config" }, ...props.instances.values()]
|
||||
@@ -115,36 +160,35 @@ function CreateInstanceButton(props: { instances: ReturnType<typeof useInstances
|
||||
/>
|
||||
</Form.Item>
|
||||
</Tooltip>
|
||||
{source && <>
|
||||
<Form.Item
|
||||
name="saveMode"
|
||||
label="Copy Save"
|
||||
extra={
|
||||
saveMode !== "none" ? "The new instance is assigned to the host of the source" : undefined
|
||||
}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: "none", label: "None" },
|
||||
{
|
||||
value: "copy",
|
||||
label: activeSave ? `Copy ${activeSave.name}` : "Copy active save",
|
||||
disabled: !activeSave,
|
||||
},
|
||||
{
|
||||
value: "save_and_copy",
|
||||
label: activeSave
|
||||
? `Save the running game and copy ${activeSave.name}`
|
||||
: "Save and copy",
|
||||
disabled: !sourceRunning || !activeSave,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{source && sourceSettings && <Tooltip title="Name of the server as shown in the in-game server browser">
|
||||
<Form.Item name="serverName" label="Server Name">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="startAfter" valuePropName="checked">
|
||||
<Checkbox disabled={saveMode === "none"}>Start after creation</Checkbox>
|
||||
</Form.Item>
|
||||
</>}
|
||||
</Tooltip>}
|
||||
{source && hostId !== undefined && <Form.Item name="saveMode" label="Copy Save">
|
||||
<Select
|
||||
options={[
|
||||
{ value: "none", label: "None" },
|
||||
{
|
||||
value: "copy",
|
||||
label: activeSave ? `Copy ${activeSave.name}` : "Copy active save",
|
||||
disabled: !activeSave,
|
||||
},
|
||||
{
|
||||
value: "save_and_copy",
|
||||
label: "Save the running game and copy it",
|
||||
disabled: !sourceRunning || !activeSave,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Form.Item>}
|
||||
{hostId !== undefined && <Form.Item
|
||||
name="startAfter"
|
||||
valuePropName="checked"
|
||||
extra={saveMode === "none" ? "A new save is created if none is copied" : undefined}
|
||||
>
|
||||
<Checkbox>Start after creation</Checkbox>
|
||||
</Form.Item>}
|
||||
</Form>
|
||||
</Modal>
|
||||
</>;
|
||||
|
||||
@@ -19,10 +19,17 @@ export function useInstanceConfig(id?: number) {
|
||||
const [config, setConfig] = useState(undefined as undefined | Static<typeof Config.jsonSchema>);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
control.send(new InstanceConfigGetRequest(id))
|
||||
.then(conf => setConfig(conf));
|
||||
setConfig(undefined);
|
||||
if (id === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Responses for a previous id must not overwrite the current one.
|
||||
let current = true;
|
||||
control.send(new InstanceConfigGetRequest(id))
|
||||
.then(conf => { if (current) { setConfig(conf); } })
|
||||
.catch(() => {}); // Config is optional, the caller falls back to not having it
|
||||
return () => { current = false; };
|
||||
}, [id]);
|
||||
|
||||
return config;
|
||||
|
||||
Reference in new issue
Block a user