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:
bbassieandClaude Opus 5 committed 2026-09-09 09:40:00 +00:00
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>
</>;
+10 -3
View File
@@ -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;