diff --git a/packages/web_ui/src/components/BaseConfigTree.jsx b/packages/web_ui/src/components/BaseConfigTree.jsx new file mode 100644 index 00000000..7fcc1548 --- /dev/null +++ b/packages/web_ui/src/components/BaseConfigTree.jsx @@ -0,0 +1,249 @@ +import React, { useEffect, useContext, useState } from "react"; +import { Button, Card, Checkbox, Form, Input, InputNumber, Space, Spin, Tree, Typography } from "antd"; + +const { Title } = Typography; + + +export default function BaseConfigTree(props) { + let [config, setConfig] = useState(null); + let [form] = Form.useForm(); + let [changedFields, setChangedFields] = useState(new Set()); + let [errorFields, setErrorFields] = useState(new Map()); + let [applying, setApplying] = useState(false); + + async function updateConfig() { + let serializedConfig = await props.retrieveConfig(); + let newConfig = new props.ConfigClass("control"); + await newConfig.load(serializedConfig); + setConfig(newConfig); + } + useEffect(() => { updateConfig(); }, [props.id]); + + function renderInput(def) { + if (def.type === "boolean") { + return ; + } + if (def.type === "string") { + return ; + } + if (def.type === "number") { + return ; + } + + return `Unknown type ${def.type}`; + } + + if (config === null) { + return ; + } + + 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 props.ConfigClass.groups) { + let group = config.group(name); + let treeNode = { + key: group.name, + title: group.name, + children: [], + }; + + for (let def of GroupClass.definitions.values()) { + if (!group.canAccess(def.name)) { + continue; + } + + let value = group.get(def.name); + let fieldName = `${group.name}.${def.name}`; + let childNode = { + key: fieldName, + children: [], + }; + + if (def.type === "object") { + childNode.title = ; + for (let prop of Object.keys(value)) { + let propPath = `${group.name}.${def.name}.${prop}`; + childNode.children.push({ + key: propPath, + title: { + if (!fieldValue.length) { + return Promise.reject(new Error("Will be removed")); + } + try { + JSON.parse(fieldValue); + } catch (err) { + return Promise.reject(new Error("Must be valid json")); + } + return Promise.resolve(); + }}]} + > + + , + }); + + initialValues[propPath] = JSON.stringify(value[prop]); + propsMap.set(propPath, [fieldName, prop]); + } + + let newPropPath = `${group.name}.${def.name}:add`; + childNode.children.push({ + key: newPropPath, + title: + + + : + + + + + , + }); + + } else { + childNode.title = + {renderInput(def)} + ; + initialValues[fieldName] = value; + } + + treeNode.children.push(childNode); + } + + treeData.push(treeNode); + } + + return <> + + Config + <Space style={{ float: "right" }}> + <Button + type="primary" + size="small" + loading={applying} + onClick={async () => { + setApplying(true); + for (let field of changedFields) { + let value = form.getFieldValue(field); + let request; + if (propsMap.has(field)) { + let [fieldName, prop] = propsMap.get(field); + request = props.setProp(fieldName, prop, value); + + } else { + request = props.setField(field, value); + } + + try { + await request; + setChangedFields(changed => { + let newChanged = new Set(changed); + newChanged.delete(field); + return newChanged; + }); + + } catch (err) { + setErrorFields(errors => { + let newErrors = new Map(errors); + newErrors.set(field, err.message); + return newErrors; + }); + } + } + + await updateConfig(); + setApplying(false); + + if (props.onApply) { + props.onApply(); + } + }} + disabled={changedFields.size === 0} + > + Apply + </Button> + <Button + size="small" + onClick={() => { + form.resetFields(); + setChangedFields(new Set()); + setErrorFields(new Map()); + }} + > + Revert + </Button> + </Space> + + +
+ changedFields.has(node.key)} + /> + +
+ ; +} diff --git a/packages/web_ui/src/components/InstanceConfigTree.jsx b/packages/web_ui/src/components/InstanceConfigTree.jsx index e5a29865..eaefa3c2 100644 --- a/packages/web_ui/src/components/InstanceConfigTree.jsx +++ b/packages/web_ui/src/components/InstanceConfigTree.jsx @@ -1,277 +1,52 @@ -import React, { useEffect, useContext, useState } from "react"; -import { Button, Card, Checkbox, Form, Input, InputNumber, Space, Spin, Tree, Typography } from "antd"; +import React, { useContext } from "react"; +import { Typography } from "antd"; import libLink from "@clusterio/lib/link"; import libConfig from "@clusterio/lib/config"; +import BaseConfigTree from "./BaseConfigTree"; import ControlContext from "./ControlContext"; -const { Title } = Typography; - export default function InstanceConfigTree(props) { let control = useContext(ControlContext); - let [config, setConfig] = useState(null); - let [form] = Form.useForm(); - let [changedFields, setChangedFields] = useState(new Set()); - let [errorFields, setErrorFields] = useState(new Map()); - let [applying, setApplying] = useState(false); - async function updateConfig() { + async function retrieveConfig() { let result = await libLink.messages.getInstanceConfig.send(control, { instance_id: props.id, }); - - let newConfig = new libConfig.InstanceConfig("control"); - await newConfig.load(result.serialized_config); - setConfig(newConfig); + return result.serialized_config; } - useEffect(() => { updateConfig(); }, [props.id]); - - function renderInput(def) { - if (def.type === "boolean") { - return ; - } - if (def.type === "string") { - return ; - } - if (def.type === "number") { - return ; - } - - return `Unknown type ${def.type}`; + async function setField(field, value) { + await libLink.messages.setInstanceConfigField.send(control, { + instance_id: props.id, + field, + value: String(value), + }); } - if (config === null) { - return ; - } - - 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) { - let group = config.group(name); - let treeNode = { - key: group.name, - title: group.name, - children: [], + async function setProp(field, prop, value) { + let data = { + instance_id: props.id, + field, + prop, }; - - for (let def of GroupClass.definitions.values()) { - if (!group.canAccess(def.name)) { - continue; + if (value) { + try { + data.value = JSON.parse(value); + } catch (err) { + return; } - - let value = group.get(def.name); - let fieldName = `${group.name}.${def.name}`; - let childNode = { - key: fieldName, - children: [], - }; - - if (def.type === "object") { - childNode.title = ; - for (let prop of Object.keys(value)) { - let propPath = `${group.name}.${def.name}.${prop}`; - childNode.children.push({ - key: propPath, - title: { - if (!fieldValue.length) { - return Promise.reject(new Error("Will be removed")); - } - try { - JSON.parse(fieldValue); - } catch (err) { - return Promise.reject(new Error("Must be valid json")); - } - return Promise.resolve(); - }}]} - > - - , - }); - - initialValues[propPath] = JSON.stringify(value[prop]); - propsMap.set(propPath, [fieldName, prop]); - } - - let newPropPath = `${group.name}.${def.name}:add`; - childNode.children.push({ - key: newPropPath, - title: - - - : - - - - - , - }); - - } else { - childNode.title = - {renderInput(def)} - ; - initialValues[fieldName] = value; - } - - treeNode.children.push(childNode); } - - treeData.push(treeNode); + await libLink.messages.setInstanceConfigProp.send(control, data); } - return <> - - Config - <Space style={{ float: "right" }}> - <Button - type="primary" - size="small" - loading={applying} - onClick={async () => { - setApplying(true); - for (let field of changedFields) { - let value = form.getFieldValue(field); - let request; - if (propsMap.has(field)) { - let [fieldName, prop] = propsMap.get(field); - let data = { - instance_id: props.id, - field: fieldName, - prop, - }; - 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, { - instance_id: props.id, - field, - value: String(value), - }); - } - - try { - await request; - setChangedFields(changed => { - let newChanged = new Set(changed); - newChanged.delete(field); - return newChanged; - }); - - } catch (err) { - setErrorFields(errors => { - let newErrors = new Map(errors); - newErrors.set(field, err.message); - return newErrors; - }); - } - } - - await updateConfig(); - setApplying(false); - - if (props.onApply) { - props.onApply(); - } - }} - disabled={changedFields.size === 0} - > - Apply - </Button> - <Button - size="small" - onClick={() => { - form.resetFields(); - setChangedFields(new Set()); - setErrorFields(new Map()); - }} - > - Revert - </Button> - </Space> - - -
- changedFields.has(node.key)} - /> - -
- ; + return ; } -