Files
factorio-clusterio/packages/web_ui/src/components/BaseConfigTree.tsx
T
Hornwitser 7314dc99c8 Fix code style errors
Remove extra whitespace, use more consistent spacing, add missing
semicolons, using dangling commas, break up long lines, consistent
multiline operator placement, add missing JSDoc strings and so on.
2023-11-19 04:48:38 +01:00

326 lines
8.3 KiB
TypeScript

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<lib.SerializedConfig>;
setField: (field:string, value:any) => Promise<void>;
setProp: (field:string, prop:string, value:any) => Promise<void>;
id?: number;
onApply?: () => void;
};
export default function BaseConfigTree(props: BaseConfigTreeProps) {
let [config, setConfig] = useState<lib.Config|null>(null);
let [form] = Form.useForm();
let [changedFields, setChangedFields] = useState<Set<string>>(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);
return newConfig;
}
useEffect(() => {
updateConfig().then(updatedConfig => {
form.setFieldsValue(getInitialValues(updatedConfig, props));
});
}, [props.id]);
function renderInput(def: lib.FieldDefinition) {
if (def.type === "boolean") {
return <Checkbox/>;
}
if (def.type === "string") {
return <Input/>;
}
if (def.type === "number") {
return <InputNumber/>;
}
return `Unknown type ${def.type}`;
}
if (config === null) {
return <Spin/>;
}
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 = <Tooltip
className="ant-form-item-tooltip"
title="A restart is required for this setting to take effect"
><ReloadOutlined/></Tooltip>;
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 = <Form.Item
label={<>
{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: <Form.Item
name={propPath}
label={<>
{`"${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();
}}]}
>
<Input className="json-input" />
</Form.Item>,
});
initialValues[propPath] = JSON.stringify(value[prop]);
propsMap.set(propPath, [fieldName, prop]);
}
let newPropPath = `${group.name}.${def.name}:add`;
childNode.children.push({
key: newPropPath,
title: <Space>
<Form.Item
name={`${newPropPath}.name`}
noStyle={true}
>
<Input />
</Form.Item>:
<Form.Item
name={`${newPropPath}.value`}
noStyle={true}
>
<Input className="json-input" />
</Form.Item>
<Button
size="small"
onClick={async () => {
let propName = form.getFieldValue(`${newPropPath}.name`);
let propValue = form.getFieldValue(`${newPropPath}.value`);
if (!Object.prototype.hasOwnProperty.call(value, propName)) {
let newConfig = new props.ConfigClass("control");
await newConfig.load(config!.serialize());
newConfig.setProp(fieldName, propName, null);
setConfig(newConfig);
}
let propPath = `${group.name}.${def.name}.${propName}`;
form.setFieldsValue({
[`${newPropPath}.name`]: "",
[`${newPropPath}.value`]: "",
[propPath]: propValue,
});
form.validateFields([propPath]);
onValuesChange({ [propPath]: propValue });
}}
>
Add
</Button>
</Space>,
});
} else {
childNode.title = <Form.Item
name={fieldName}
label={<>
{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)}
</Form.Item>;
initialValues[fieldName] = value;
}
treeNode.children.push(childNode);
}
treeData.push(treeNode);
}
return <>
<Title level={5} style={{ marginTop: 16 }}>
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>
</Title>
<Card size="small" className="config-container">
<Form
size="small"
form={form}
initialValues={initialValues}
onValuesChange={onValuesChange}
>
<Tree
treeData={treeData}
selectable={false}
defaultExpandAll={true}
filterTreeNode={(node) => changedFields.has(node.key)}
/>
</Form>
</Card>
</>;
}