mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 07:04:52 +09:00
Add config cloning on instance creation (#747)
* Add instance config cloning on creation * Add test for instance cloning * Add tooltip to copy config button
This commit is contained in:
1 parent
0055f28f45
commit
ce85149b4c
6 files changed
+63
-10
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
));
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -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<typeof Config.jsonSchema> {
|
||||
toRemote(remote: ConfigLocation, filter?: (keyof Fields)[]): Static<typeof Config.jsonSchema> {
|
||||
let fields: Record<string, FieldValue> = {};
|
||||
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)
|
||||
|
||||
@@ -137,14 +137,16 @@ export class InstanceCreateRequest {
|
||||
|
||||
constructor(
|
||||
public config: Static<typeof InstanceConfig.jsonSchema>,
|
||||
public cloneFromId?: number,
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"config": InstanceConfig.jsonSchema,
|
||||
"cloneFromId": Type.Optional(Type.Number()),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(json.config);
|
||||
return new this(json.config, json.cloneFromId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<typeof useInstances>[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() {
|
||||
<Form.Item name="instanceName" label="Name">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Tooltip title="Perform a one time copy of an existing config (assigned host is not copied)">
|
||||
<Form.Item name="instanceClone" label="Copy Config">
|
||||
<Select
|
||||
defaultValue={-1}
|
||||
options={[{ id: -1, name: "Default Config" }, ...props.instances.values()]
|
||||
.map(i => ({ value: i.id, label: i.name }))
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Tooltip>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>;
|
||||
@@ -67,7 +83,7 @@ export default function InstancesPage() {
|
||||
<PageHeader
|
||||
title="Instances"
|
||||
extra={<>
|
||||
{account.hasPermission("core.instance.create") && <CreateInstanceButton />}
|
||||
{account.hasPermission("core.instance.create") && <CreateInstanceButton instances={instances}/>}
|
||||
{account.hasPermission("core.instance.start")
|
||||
&& <Button onClick={e => instances.forEach(instance => {
|
||||
if (instance.status === "stopped") {
|
||||
|
||||
@@ -351,6 +351,23 @@ describe("Integration of Clusterio", function() {
|
||||
await execCtl(`instance config set-prop test factorio.settings visibility "${value}"`);
|
||||
await execCtl("instance config set-prop test factorio.settings require_user_verification false");
|
||||
});
|
||||
it("can clone an instance", async function() {
|
||||
slowTest(this);
|
||||
await execCtl("instance create testClone --id 440 --from 44");
|
||||
const instances = await getInstances();
|
||||
assert(instances.has(440), "instance was not created");
|
||||
assert.equal(instances.get(440).status, "unassigned", "incorrect instance status");
|
||||
const config = await getControl().send(new lib.InstanceConfigGetRequest(44));
|
||||
assert.deepEqual(config["factorio.settings"]["visibility"], { lan: true, public: false });
|
||||
assert.equal(config["factorio.settings"]["require_user_verification"], false);
|
||||
});
|
||||
it("errors when cloning an invalid instance", async function() {
|
||||
slowTest(this);
|
||||
assert.rejects(
|
||||
execCtl("instance create testClone --id 440 --from 441"),
|
||||
new lib.RequestError("Instance with ID 441 does not exist")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("instance assign", function() {
|
||||
|
||||
Reference in new issue
Block a user