From 201eb89dcdb0f64caa425f40e23301ccaba748d7 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sat, 16 Jan 2021 11:01:15 +0100 Subject: [PATCH 1/6] Assume bad JSON is string in set-prop Assume value is supposed to be a string if it fails to parse as JSON and it does not start and end in a [], {}, or "" pair. --- packages/ctl/ctl.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/ctl/ctl.js b/packages/ctl/ctl.js index cf3f28d8..ef444ea0 100755 --- a/packages/ctl/ctl.js +++ b/packages/ctl/ctl.js @@ -158,11 +158,32 @@ instanceConfigCommands.add(new libCommand.Command({ }], handler: async function(args, control) { let instanceId = await libCommand.resolveInstance(control, args.instance); + let value = args.value; + try { + value = JSON.parse(value); + } catch (err) { + // If this is looks like an array, object or string literal + // throw the parse error, otherwise assume this is a string. + // The resoning behind this is that correctly quoting the string + // with the all the layers of quote removal at play is difficult. + // See the following table for how to pass "That's a \" quote" in + // different environments: + // cmd : """""That's a \\"" quote""""" + // cmd + npx : """""""""""That's a \\\\"""" quote""""""""""" + // PowerShell : '"""""That''s a \\"" quote"""""' + // PowerShell + npx : '"""""""""""That''s a \\\\"""" quote"""""""""""' + // bash : '""That'\''s a \" quote""' + // bash + npx : '""That'\''s a \" quote""' + // bash + npx -s sh : "'\"\"That'\\''s a \\\" quote\"\"'" + if (/^(\[.*]|{.*}|".*")$/.test(value)) { + throw new libErrors.CommandError(`In parsing value '${value}': ${err.message}`); + } + } await libLink.messages.setInstanceConfigProp.send(control, { instance_id: instanceId, field: args.field, prop: args.prop, - value: JSON.parse(args.value), + value, }); }, })); From b84c5b429369ebb5022e6b19b9fffdaa99143765 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Mon, 18 Jan 2021 08:17:14 +0100 Subject: [PATCH 2/6] Add common settings for Factorio servers Add settings of interest to server administrators that appear in server-settings.json to the factorio.settings config. This makes it easier to see which settings can be set there. --- docs/configuration.md | 8 ++++++++ packages/lib/config/definitions.js | 12 ++++++++---- packages/master/master.js | 27 +++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 185213a8..889a1eaa 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -5,6 +5,14 @@ Configuration Master Configuration -------------------- +### master.name + +Name of the cluster. Used to distinguish it from other clusters and is +prepended to the name of servers in the server list. + +Defaluts to "Your Cluster". + + ### master.database_directory Directory used to store cluster data to. Needs to be writeable by the diff --git a/packages/lib/config/definitions.js b/packages/lib/config/definitions.js index 3c453eb9..5c6fff11 100644 --- a/packages/lib/config/definitions.js +++ b/packages/lib/config/definitions.js @@ -11,6 +11,13 @@ const classes = require("./classes"); */ class MasterGroup extends classes.ConfigGroup { } MasterGroup.groupName = "master"; +MasterGroup.define({ + name: "name", + title: "Name", + description: "Name of the cluster", + type: "string", + initial_value: "Your Cluster", +}); MasterGroup.define({ name: "database_directory", title: "Database directory", @@ -262,10 +269,7 @@ FactorioGroup.define({ name: "settings", description: "Settings overridden in server-settings.json", type: "object", - initial_value: { - "tags": ["clusterio"], - "auto_pause": false, - }, + initial_value: {}, // See create instance handler in master. }); FactorioGroup.define({ name: "verbose_logging", diff --git a/packages/master/master.js b/packages/master/master.js index eff99e7c..c0e6a677 100755 --- a/packages/master/master.js +++ b/packages/master/master.js @@ -741,6 +741,33 @@ class ControlConnection extends BaseConnection { if (db.instances.has(instanceId)) { throw new libErrors.RequestError(`Instance with ID ${instanceId} already exists`); } + + // Add common settings for the Factorio server + let settings = { + "name": `${masterConfig.get("master.name")} - ${instanceConfig.get("instance.name")}`, + "description": `Clusterio instance for ${masterConfig.get("master.name")}`, + "tags": ["clusterio"], + "max_players": 0, + "visibility": { "public": true, "lan": true }, + "username": "", + "token": "", + "game_password": "", + "require_user_verification": true, + "max_upload_in_kilobytes_per_second": 0, + "max_upload_slots": 5, + "ignore_player_limit_for_returning_players": false, + "allow_commands": "admins-only", + "autosave_interval": 10, + "autosave_slots": 5, + "afk_autokick_interval": 0, + "auto_pause": false, + "only_admins_can_pause_the_game": true, + "autosave_only_on_server": true, + + ...instanceConfig.get("factorio.settings"), + }; + instanceConfig.set("factorio.settings", settings); + db.instances.set(instanceId, { config: instanceConfig, status: "unassigned" }); } From dea3a30caec6320783aaadeaa4c34123afb95bbc Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 22 Jan 2021 12:20:27 +0100 Subject: [PATCH 3/6] Allow optional top level properties in messages Add optional arguments to Request and Event to control which properties are required to be present in the payload. This allows gracefully extending the protocol, reducing overhead for optional data, and leaving properties out for additional behaviour. --- docs/writing-plugins.md | 34 +++++++++++++++++++++++++++++----- packages/lib/link/messages.js | 35 ++++++++++++++++++++++++++++++----- test/lib/link/messages.js | 6 ++++++ 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/docs/writing-plugins.md b/docs/writing-plugins.md index 211f7aab..ae5ab831 100644 --- a/docs/writing-plugins.md +++ b/docs/writing-plugins.md @@ -381,8 +381,10 @@ the event, for example the following could be defined in `info.js`: type: "foo_frobber:start_frobnication", links: ["master-slave", "slave-instance"], forwardTo: "instance", + eventRequired: ["frobnication_type"], eventProperties: { "frobnication_type": { type: "string" }, + "urgent": { type: "boolean" }, }, }), }, @@ -390,8 +392,8 @@ the event, for example the following could be defined in `info.js`: This specifies an event that can be sent from the master to a slave, and from a slave to an instance. It also specifies that the event must contain the property `frobnication_type`, with a string value in the -data payload. It will also be forwarded by slaves to a specific -instance. +data payload and that it may optionally contain a boolean `urgent` +property. It will also be forwarded by slaves to a specific instance. The following properties are recognized by the Event constructor: @@ -434,12 +436,19 @@ it's sent to, but not back from where it came from. Currently, only that slave except for the instance it came from. from an instance will cause it to be broadcast to all instances of +#### eventRequired + +By default all properties defined in `eventProperties` are required to +be present in the payload for the event. This can be overriden by +specifying an array of properties which are required here. This is +equivalent to using the `required` keyword in JSON schema. + #### eventProperties Object with properties mapping to a JSON schema of that property that specifies what's valid to send in the event. This is equivalent to using the `properties` keyword in JSON schema, except that the -properties specified are implicitly required and additional properties +properties specified are by default required and additional properties are not allowed. See [this guide][guide] for an introduction to writing JSON schemas. @@ -466,8 +475,10 @@ the event. For example, the following could be defined in `info.js`: type: "foo_frobber:report_frobnication", links: ["master-slave", "slave-instance"], forwardTo: "instance", + requestRequired: ["verbosity"], requestProperties: { "verbosity": { type: "integer" }, + "special": { type: "boolean" }, }, responseProperties: { "report": { @@ -481,7 +492,8 @@ the event. For example, the following could be defined in `info.js`: This specifies a request that can be sent from the master to a slave, and from a slave to an instance. The request data must contain the property `verbosity` with an integer number as the value, as well as the -`instance_id` property (implied by `forwardTo: "instance"`), and the +`instance_id` property (implied by `forwardTo: "instance"`) and it may +also contain a boolean `special` property. It also defines that the response sent must contain a `report` property mapping to an array of strings. When received by a slave, it will also be forwarded to the instance specified by `instance_id`. @@ -520,15 +532,27 @@ instance, or `"instance"` to indicate it should be forwarded to the instances specified by the `instance_id` request property. This works by using a default handler for the request by the links that forward it. +#### requestRequired + +By default all properties defined in `requestProperties` are required to +be present in the payload for the request. This can be overriden by +specifying an array of properties which are required here. This is +equivalent to using the `required` keyword in JSON schema. + #### requestProperties Object with properties mapping to a JSON schema of that property that specifies what's valid to send in the request. This is equivalent to using the `properties` keyword in JSON schema, except that the -properties specified are implicitly required and additional properties +properties specified are by default required and additional properties are not allowed. See [this guide][guide] for an introduction to writing JSON schemas +#### responseRequired + +Same as the requestRequired only for the response sent back by the +target. + #### responseProperties Same as the requestProperties only for the response sent back by the diff --git a/packages/lib/link/messages.js b/packages/lib/link/messages.js index 283a395f..aea0e040 100644 --- a/packages/lib/link/messages.js +++ b/packages/lib/link/messages.js @@ -70,16 +70,24 @@ class Request extends Message { * Optional target to forward this request to. 'instance' add * instance_id into the requestProperties and forward to the given * instance. 'master' forwards it to the master server. + * @param {?Array} requestRequired - + * List of properties that are required to be present in the data + * payload of this request. Defaults to all. * @param {Object} requestProperties - * Mapping of property values to JSON schema specifications for the * properties that are valid in the data payload of this requst. + * @param {?Array} responseRequired - + * List of properties that are required to be present in the data + * payload of the repsonse to this request. Defaults to all. * @param {Object} responseProperties - * Mapping of property values to JSON schema specifications for the * properties that are valid in the data payload of the response to * this requst. */ constructor({ - type, permission, links, forwardTo = null, requestProperties = {}, responseProperties = {}, + type, permission, links, forwardTo = null, + requestRequired = null, requestProperties = {}, + responseRequired = null, responseProperties = {}, }) { super(); this.type = type; @@ -98,7 +106,12 @@ class Request extends Message { throw new Error(`permission is not allowed on ${this.type} request as it is not over control-master link`); } + if (!requestRequired) { + requestRequired = Object.keys(requestProperties); + } + if (forwardTo === "instance") { + requestRequired = ["instance_id", ...requestRequired]; requestProperties = { "instance_id": { type: "integer" }, ...requestProperties, @@ -114,12 +127,16 @@ class Request extends Message { "type": { const: this.requestType }, "data": { additionalProperties: false, - required: Object.keys(requestProperties), + required: requestRequired, properties: requestProperties, }, }, }); + if (!responseRequired) { + responseRequired = Object.keys(responseProperties); + } + this._responseValidator = libSchema.compile({ $schema: "http://json-schema.org/draft-07/schema#", properties: { @@ -128,7 +145,7 @@ class Request extends Message { anyOf: [ { additionalProperties: false, - required: ["seq", ...Object.keys(responseProperties)], + required: ["seq", ...responseRequired], properties: { "seq": { type: "integer" }, ...responseProperties, @@ -760,11 +777,14 @@ class Event extends Message { * links that are downstream, so sending an event set to instance * broadcast that's sent to a slave will only be broadcast to that * slave's instances. + * @param {?Array} eventRequired - + * List of properties required to be present in the event payload. + * Defaults to all. * @param {Object} eventProperties - * Mapping of property values to JSON schema specifications for the * properties that are valid in the data payload of this event. */ - constructor({ type, links, forwardTo = null, broadcastTo = null, eventProperties = {} }) { + constructor({ type, links, forwardTo = null, broadcastTo = null, eventRequired = null, eventProperties = {} }) { super(); this.type = type; this.links = links; @@ -772,7 +792,12 @@ class Event extends Message { this.broadcastTo = broadcastTo; this.handlerSuffix = "EventHandler"; + if (!eventRequired) { + eventRequired = Object.keys(eventProperties); + } + if (forwardTo === "instance") { + eventRequired = ["instance_id", ...eventRequired]; eventProperties = { "instance_id": { type: "integer" }, ...eventProperties, @@ -793,7 +818,7 @@ class Event extends Message { "type": { const: this.eventType }, "data": { additionalProperties: false, - required: Object.keys(eventProperties), + required: eventRequired, properties: eventProperties, }, }, diff --git a/test/lib/link/messages.js b/test/lib/link/messages.js index 97ad641e..b2005273 100644 --- a/test/lib/link/messages.js +++ b/test/lib/link/messages.js @@ -17,11 +17,15 @@ describe("lib/link/messages", function() { let testRequest = new libLink.Request({ type: "test", links: ["source-target"], + requestRequired: ["test"], requestProperties: { "test": { type: "string" }, + "optional": { type: "number" }, }, + responseRequired: ["test"], responseProperties: { "test": { type: "string" }, + "optional": { type: "number" }, }, }); @@ -127,8 +131,10 @@ describe("lib/link/messages", function() { let testEvent = new libLink.Event({ type: "test", links: ["source-target"], + eventRequired: ["test"], eventProperties: { "test": { type: "string" }, + "optional": { type: "number" }, }, }); From 0e21060ea3d1b206aadce89121c25c886fd239e5 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 22 Jan 2021 12:53:53 +0100 Subject: [PATCH 4/6] Allow removing properties with set-prop Implement removal of properties in ConfigGroup.setProp, set_instance_config_prop, and the set-prop clusterioctl command. --- packages/ctl/ctl.js | 22 ++++++++++++---------- packages/lib/config/classes.js | 6 +++++- packages/lib/link/messages.js | 1 + test/lib/config/classes.js | 6 ++++++ 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/ctl/ctl.js b/packages/ctl/ctl.js index ef444ea0..c58e3bda 100755 --- a/packages/ctl/ctl.js +++ b/packages/ctl/ctl.js @@ -150,7 +150,7 @@ instanceConfigCommands.add(new libCommand.Command({ })); instanceConfigCommands.add(new libCommand.Command({ - definition: ["set-prop ", "Set property of field in instance config", (yargs) => { + definition: ["set-prop [value]", "Set property of field in instance config", (yargs) => { yargs.positional("instance", { describe: "Instance to set config on", type: "string" }); yargs.positional("field", { describe: "Field to set", type: "string" }); yargs.positional("prop", { describe: "Property to set", type: "string" }); @@ -158,9 +158,15 @@ instanceConfigCommands.add(new libCommand.Command({ }], handler: async function(args, control) { let instanceId = await libCommand.resolveInstance(control, args.instance); - let value = args.value; + let request = { + instance_id: instanceId, + field: args.field, + prop: args.prop, + }; try { - value = JSON.parse(value); + if (args.value !== undefined) { + request.value = JSON.parse(args.value); + } } catch (err) { // If this is looks like an array, object or string literal // throw the parse error, otherwise assume this is a string. @@ -176,15 +182,11 @@ instanceConfigCommands.add(new libCommand.Command({ // bash + npx : '""That'\''s a \" quote""' // bash + npx -s sh : "'\"\"That'\\''s a \\\" quote\"\"'" if (/^(\[.*]|{.*}|".*")$/.test(value)) { - throw new libErrors.CommandError(`In parsing value '${value}': ${err.message}`); + throw new libErrors.CommandError(`In parsing value '${args.value}': ${err.message}`); } + request.value = args.value; } - await libLink.messages.setInstanceConfigProp.send(control, { - instance_id: instanceId, - field: args.field, - prop: args.prop, - value, - }); + await libLink.messages.setInstanceConfigProp.send(control, request); }, })); instanceCommands.add(instanceConfigCommands); diff --git a/packages/lib/config/classes.js b/packages/lib/config/classes.js index 38a30e19..df5f2ae2 100644 --- a/packages/lib/config/classes.js +++ b/packages/lib/config/classes.js @@ -448,7 +448,11 @@ class ConfigGroup { let prev = this._fields.get(name); let updated = {...prev || {}}; - updated[prop] = value; + if (value !== undefined) { + updated[prop] = value; + } else { + delete updated[prop]; + } this._fields.set(name, updated); if (this._config && !isDeepStrictEqual(updated, prev)) { this._config.emit("fieldChanged", this, name, prev); diff --git a/packages/lib/link/messages.js b/packages/lib/link/messages.js index aea0e040..a68ae538 100644 --- a/packages/lib/link/messages.js +++ b/packages/lib/link/messages.js @@ -407,6 +407,7 @@ messages.setInstanceConfigProp = new Request({ type: "set_instance_config_prop", links: ["control-master"], permission: "core.instance.update_config", + requestRequired: ["instance_id", "field", "prop"], requestProperties: { "instance_id": { type: "integer" }, "field": { type: "string" }, diff --git a/test/lib/config/classes.js b/test/lib/config/classes.js index 48db16ec..e566713e 100644 --- a/test/lib/config/classes.js +++ b/test/lib/config/classes.js @@ -397,6 +397,12 @@ describe("lib/config/classes", function() { testInstance.setProp("json", "test", true); assert.deepEqual(testInstance.get("json"), { test: true }); }); + + it("should unset field if passed undefined", function() { + testInstance.set("json", { test: true, extra: "yes" }); + testInstance.setProp("json", "extra", undefined); + assert.deepEqual(testInstance.get("json"), { test: true }); + }); }); }); From 211727199281b1b39718a1dce3c25604eec98592 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sat, 23 Jan 2021 13:40:17 +0100 Subject: [PATCH 5/6] Add option to read config value from stdin Add --stdin option to the config set commands which reads the value to set via stdin instead of on the command line. This allows a more secure way of entering values and avoids having to deal with multiple layers of quoting and escapes for special characters. --- packages/ctl/ctl.js | 23 +++++++++++++++++++---- packages/lib/helpers.js | 24 ++++++++++++++++++++++++ packages/lib/shared_commands.js | 12 ++++++++++-- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/packages/ctl/ctl.js b/packages/ctl/ctl.js index c58e3bda..0c6dd755 100755 --- a/packages/ctl/ctl.js +++ b/packages/ctl/ctl.js @@ -23,6 +23,7 @@ const libCommand = require("@clusterio/lib/command"); const libSharedCommands = require("@clusterio/lib/shared_commands"); const { ConsoleTransport, levels, logger } = require("@clusterio/lib/logging"); const libLoggingUtils = require("@clusterio/lib/logging_utils"); +const libHelpers = require("@clusterio/lib/helpers"); function print(...content) { @@ -134,13 +135,21 @@ instanceConfigCommands.add(new libCommand.Command({ })); instanceConfigCommands.add(new libCommand.Command({ - definition: ["set ", "Set field in instance config", (yargs) => { + definition: ["set [value]", "Set field in instance config", (yargs) => { yargs.positional("instance", { describe: "Instance to set config on", type: "string" }); yargs.positional("field", { describe: "Field to set", type: "string" }); yargs.positional("value", { describe: "Value to set", type: "string" }); + yargs.options({ + "stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" }, + }); }], handler: async function(args, control) { let instanceId = await libCommand.resolveInstance(control, args.instance); + if (args.stdin) { + args.value = (await libHelpers.readStream(process.stdin)).toString().replace(/\r?\n$/, ""); + } else if (args.value === undefined) { + args.value = ""; + } await libLink.messages.setInstanceConfigField.send(control, { instance_id: instanceId, field: args.field, @@ -155,9 +164,15 @@ instanceConfigCommands.add(new libCommand.Command({ yargs.positional("field", { describe: "Field to set", type: "string" }); yargs.positional("prop", { describe: "Property to set", type: "string" }); yargs.positional("value", { describe: "JSON parsed value to set", type: "string" }); + yargs.options({ + "stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" }, + }); }], handler: async function(args, control) { let instanceId = await libCommand.resolveInstance(control, args.instance); + if (args.stdin) { + args.value = (await libHelpers.readStream(process.stdin)).toString().replace(/\r?\n$/, ""); + } let request = { instance_id: instanceId, field: args.field, @@ -168,8 +183,8 @@ instanceConfigCommands.add(new libCommand.Command({ request.value = JSON.parse(args.value); } } catch (err) { - // If this is looks like an array, object or string literal - // throw the parse error, otherwise assume this is a string. + // If this is from stdin or looks like an array, object or string + // literal throw the parse error, otherwise assume this is a string. // The resoning behind this is that correctly quoting the string // with the all the layers of quote removal at play is difficult. // See the following table for how to pass "That's a \" quote" in @@ -181,7 +196,7 @@ instanceConfigCommands.add(new libCommand.Command({ // bash : '""That'\''s a \" quote""' // bash + npx : '""That'\''s a \" quote""' // bash + npx -s sh : "'\"\"That'\\''s a \\\" quote\"\"'" - if (/^(\[.*]|{.*}|".*")$/.test(value)) { + if (args.stdin || /^(\[.*]|{.*}|".*")$/.test(args.value)) { throw new libErrors.CommandError(`In parsing value '${args.value}': ${err.message}`); } request.value = args.value; diff --git a/packages/lib/helpers.js b/packages/lib/helpers.js index cf1a657c..cbbed42e 100644 --- a/packages/lib/helpers.js +++ b/packages/lib/helpers.js @@ -52,8 +52,32 @@ async function timeout(promise, time, timeoutResult) { } } + +/** + * Read stream to the end and return its content + * + * Reads the stream given asynchronously until the end is reached and + * returns all the data which was read from the stream. + * + * @param {Readable} stream - byte stream to read to the end. + * @returns {Buffer} content of the stream. + */ +async function readStream(stream) { + let chunks = []; + for await (let chunk of stream) { + // Support using ^Z to end input on Windows + if (process.platform === "win32" && stream.isTTY && chunk.toString() === "\x1a\r\n") { + break; + } + chunks.push(chunk); + } + return Buffer.concat(chunks); +} + + module.exports = { basicType, wait, timeout, + readStream, }; diff --git a/packages/lib/shared_commands.js b/packages/lib/shared_commands.js index 5b4f39a3..5decd578 100644 --- a/packages/lib/shared_commands.js +++ b/packages/lib/shared_commands.js @@ -8,6 +8,7 @@ const path = require("path"); const libConfig = require("@clusterio/lib/config"); const { logger } = require("@clusterio/lib/logging"); +const libHelpers = require("@clusterio/lib/helpers"); function print(...content) { @@ -100,7 +101,11 @@ async function handlePluginCommand(args, pluginList, pluginListPath) { */ function configCommand(yargs) { yargs - .command("set [value]", "Set config field") + .command("set [value]", "Set config field", yargs => { + yargs.options({ + "stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" }, + }); + }) .command("show ", "Show value of the given config field") .command("list", "List all configuration fields and their values") .demandCommand(1, "You need to specify a command to run") @@ -141,7 +146,10 @@ async function handleConfigCommand(args, instance, configPath) { } } else if (command === "set") { - if (args.value === undefined) { + if (args.stdin) { + args.value = (await libHelpers.readStream(process.stdin)).toString().replace(/\r?\n$/, ""); + + } else if (args.value === undefined) { args.value = null; } From a2c72b7fd133a839151ae68cea0573df1661bdff Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 24 Jan 2021 20:41:37 +0100 Subject: [PATCH 6/6] Implement add and remove properties in web config Implement removing properties for object config fields by setting them to an empty string and adding new properties with a corresponding value. --- .../src/components/InstanceViewPage.jsx | 100 +++++++++++++----- 1 file changed, 74 insertions(+), 26 deletions(-) diff --git a/packages/web_ui/src/components/InstanceViewPage.jsx b/packages/web_ui/src/components/InstanceViewPage.jsx index 116b7e54..af981afa 100644 --- a/packages/web_ui/src/components/InstanceViewPage.jsx +++ b/packages/web_ui/src/components/InstanceViewPage.jsx @@ -359,6 +359,23 @@ function InstanceConfigTree(props) { } let initialValues = {}; + function onValuesChange(changedValues) { + let newChangedFields = new Set(changedFields); + let changed = false; + for (let [key, value] of Object.entries(changedValues)) { + if (initialValues[key] === value) { + changed = newChangedFields.delete(key) || changed; + } else { + changed = !newChangedFields.has(key) || changed; + newChangedFields.add(key); + } + } + + if (changed) { + setChangedFields(newChangedFields); + } + } + let treeData = []; let propsMap = new Map(); for (let [name, GroupClass] of libConfig.InstanceConfig.groups) { @@ -387,6 +404,9 @@ function InstanceConfigTree(props) { name={propPath} label={`"${prop}"`} rules={[{ validator: (_, fieldValue) => { + if (!fieldValue.length) { + return Promise.reject(new Error("Will be removed")); + } try { JSON.parse(fieldValue); } catch (err) { @@ -403,6 +423,49 @@ function InstanceConfigTree(props) { propsMap.set(propPath, [fieldName, prop]); } + let newPropPath = `${group.name}.${def.name}:add`; + childNode.children.push({ + key: newPropPath, + title: + + + : + + + + + , + }); + } else { childNode.title = Config @@ -455,18 +501,20 @@ function InstanceConfigTree(props) { let request; if (propsMap.has(field)) { let [fieldName, prop] = propsMap.get(field); - try { - value = JSON.parse(value); - } catch (err) { - continue; - } - - request = libLink.messages.setInstanceConfigProp.send(control, { + let data = { instance_id: props.id, field: fieldName, prop, - value, - }); + }; + if (value) { + try { + data.value = JSON.parse(value); + } catch (err) { + continue; + } + } + + request = libLink.messages.setInstanceConfigProp.send(control, data); } else { request = libLink.messages.setInstanceConfigField.send(control, {