diff --git a/packages/controller/src/ControlConnection.ts b/packages/controller/src/ControlConnection.ts index fe02d533..5f3fac4b 100644 --- a/packages/controller/src/ControlConnection.ts +++ b/packages/controller/src/ControlConnection.ts @@ -251,6 +251,14 @@ export default class ControlConnection extends BaseConnection { async handleInstanceCreateRequest(request: lib.InstanceCreateRequest) { const instanceConfig = new lib.InstanceConfig("controller"); + if (request.cloneFromId) { + const baseInstance = this._controller.instances.get(request.cloneFromId); + if (!baseInstance) { + throw new lib.RequestError(`Instance with ID ${request.cloneFromId} does not exist`); + } + instanceConfig.update(baseInstance.config.toJSON(), false); + instanceConfig.set("instance.assigned_host", null); // New instances are unassigned + } instanceConfig.update(request.config, false, "control"); await this._controller.instanceCreate(instanceConfig); } diff --git a/packages/ctl/src/commands.ts b/packages/ctl/src/commands.ts index c44d54b9..efe86c4e 100644 --- a/packages/ctl/src/commands.ts +++ b/packages/ctl/src/commands.ts @@ -484,16 +484,21 @@ instanceCommands.add(new lib.Command({ yargs.positional("name", { describe: "Instance name", type: "string" }); yargs.options({ "id": { type: "number", nargs: 1, describe: "Instance id" }, + "from": { type: "number", nargs: 1, describe: "Clone config from instance id" }, }); }], - handler: async function(args: { name: string, id?: number }, control: Control) { + handler: async function(args: { name: string, id?: number, from?: number, }, control: Control) { let instanceConfig = new lib.InstanceConfig("control"); if (args.id !== undefined) { instanceConfig.set("instance.id", args.id); } instanceConfig.set("instance.name", args.name); - const serializedConfig = instanceConfig.toRemote("controller"); - await control.send(new lib.InstanceCreateRequest(serializedConfig)); + await control.send(new lib.InstanceCreateRequest( + instanceConfig.toRemote("controller", [ + "instance.id", "instance.name", + ]), + args.from + )); }, })); diff --git a/packages/lib/src/config/classes.ts b/packages/lib/src/config/classes.ts index 4770346c..2546dbf8 100644 --- a/packages/lib/src/config/classes.ts +++ b/packages/lib/src/config/classes.ts @@ -312,11 +312,16 @@ export class Config< * Serialize the config to a plain JavaScript object * * @param remote - Location this serialised representation is for + * @param filter - When provided, only the given fields are serialized * @returns JSON serializable representation of the config. */ - toRemote(remote: ConfigLocation): Static { + toRemote(remote: ConfigLocation, filter?: (keyof Fields)[]): Static { let fields: Record = {}; for (let [name, value] of Object.entries(this.fields)) { + if (filter && !filter.includes(name as keyof Fields)) { + continue; + } + let def = this.constructor.fieldDefinitions[name]; if ( this.location !== "control" && !this._checkAccess(name, def, this.location, ConfigAccess.write, false) diff --git a/packages/lib/src/data/messages_instance.ts b/packages/lib/src/data/messages_instance.ts index 554a16a6..63643343 100644 --- a/packages/lib/src/data/messages_instance.ts +++ b/packages/lib/src/data/messages_instance.ts @@ -137,14 +137,16 @@ export class InstanceCreateRequest { constructor( public config: Static, + public cloneFromId?: number, ) { } static jsonSchema = Type.Object({ "config": InstanceConfig.jsonSchema, + "cloneFromId": Type.Optional(Type.Number()), }); static fromJSON(json: Static) { - return new this(json.config); + return new this(json.config, json.cloneFromId); } } diff --git a/packages/web_ui/src/components/InstancesPage.tsx b/packages/web_ui/src/components/InstancesPage.tsx index 660b40e2..c15243c5 100644 --- a/packages/web_ui/src/components/InstancesPage.tsx +++ b/packages/web_ui/src/components/InstancesPage.tsx @@ -1,6 +1,6 @@ import React, { useContext, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { Button, Form, Input, Modal } from "antd"; +import { Button, Form, Input, Modal, Select, Tooltip } from "antd"; import * as lib from "@clusterio/lib"; @@ -13,7 +13,7 @@ import { useInstances } from "../model/instance"; import InstanceList from "./InstanceList"; import { notifyErrorHandler } from "../util/notify"; -function CreateInstanceButton() { +function CreateInstanceButton(props: { instances: ReturnType[0] }) { let control = useContext(ControlContext); let navigate = useNavigate(); let [open, setOpen] = useState(false); @@ -28,8 +28,14 @@ function CreateInstanceButton() { let instanceConfig = new lib.InstanceConfig("control"); instanceConfig.set("instance.name", values.instanceName); - const serializedConfig = instanceConfig.toRemote("controller"); - await control.send(new lib.InstanceCreateRequest(serializedConfig)); + + await control.send(new lib.InstanceCreateRequest( + instanceConfig.toRemote("controller", [ + "instance.id", "instance.name", + ]), + values.instanceClone >= 0 ? values.instanceClone : undefined, + )); + setOpen(false); navigate(`/instances/${instanceConfig.get("instance.id")}/view`); } @@ -53,6 +59,16 @@ function CreateInstanceButton() { + + +