import type * as lib from "@clusterio/lib"; import React, { useEffect, useState } from "react"; import { Button, Card, Checkbox, Form, Input, InputNumber, Space, Spin, Tooltip, Tree, Typography } from "antd"; import ReloadOutlined from "@ant-design/icons/ReloadOutlined"; const { Title } = Typography; function getInitialValues(config: lib.Config, props: BaseConfigTreeProps) { let initialValues: any = {}; for (let [name, GroupClass] of props.ConfigClass.groups) { let group = config.group(name); for (let def of GroupClass.definitions.values()) { if (!group.canAccess(def.name)) { continue; } let value = group.get(def.name) as any; let fieldName = `${group.name}.${def.name}`; if (def.type === "object") { for (let prop of Object.keys(value)) { let propPath = `${group.name}.${def.name}.${prop}`; initialValues[propPath] = JSON.stringify(value[prop]); } } else { initialValues[fieldName] = value; } } } return initialValues; } type BaseConfigTreeProps = { ConfigClass: typeof lib.Config; retrieveConfig: () => Promise; setField: (field:string, value:any) => Promise; setProp: (field:string, prop:string, value:any) => Promise; id?: number; onApply?: () => void; }; export default function BaseConfigTree(props: BaseConfigTreeProps) { 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(); const newConfig = props.ConfigClass.fromJSON(serializedConfig, "control"); setConfig(newConfig); return newConfig; } useEffect(() => { updateConfig().then(updatedConfig => { form.setFieldsValue(getInitialValues(updatedConfig, props)); }); }, [props.id]); function renderInput(def: lib.FieldDefinition) { 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: any = {}; function onValuesChange(changedValues: object) { let newChangedFields = new Set(changedFields); let changed = false; for (let [key, value] of Object.entries(changedValues)) { if (key.endsWith(":add.name") || key.endsWith(":add.value")) { continue; } if (initialValues[key] === value) { changed = newChangedFields.delete(key) || changed; } else { changed = !newChangedFields.has(key) || changed; newChangedFields.add(key); } } if (changed) { setChangedFields(newChangedFields); } } let restartTip = ; type ChildNode = { key: string; children: any[]; title?: React.ReactElement; }; type TreeNode = { key: string; title: string; children: ChildNode[]; }; let treeData = []; let propsMap = new Map(); for (let [name, GroupClass] of props.ConfigClass.groups) { let group = config.group(name); let treeNode: 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) as any; let fieldName = `${group.name}.${def.name}`; let childNode: ChildNode = { key: fieldName, children: [], }; if (def.type === "object") { let restartRequiredProps = new Set(def.restartRequiredProps || []); childNode.title = {def.title || def.name} {!def.restartRequiredProps && def.restartRequired && restartTip} } tooltip={def.description} />; for (let prop of Object.keys(value)) { let propPath = `${group.name}.${def.name}.${prop}`; let restart = Boolean( def.restartRequiredProps && Number(def.restartRequired) ^ Number(restartRequiredProps.has(prop)) ); childNode.children.push({ key: propPath, title: {`"${prop}"`} {restart && restartTip} } rules={[{ validator: (_, fieldValue) => { 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 = {def.title || def.name} {def.restartRequired && restartTip} } validateStatus={errorFields.has(fieldName) ? "error" : undefined} help={errorFields.get(fieldName)} tooltip={def.description} valuePropName={def.type === "boolean" ? "checked" : "value"} > {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 === null ? "" : String(value)); } try { await request; setChangedFields(changed => { let newChanged = new Set(changed); newChanged.delete(field); return newChanged; }); } catch (err: any) { 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)} />
; }