Add hidden and readonly config properties (#665)

This commit is contained in:
Cooldude2606 authored and GitHub committed 2024-10-27 23:02:45 +00:00
commit c49bc07e15
8 files changed
+101 -57

No files matched your search

+1
View File
@@ -36,6 +36,7 @@ Many thanks to the following for contributing to this release:
- Monorepos are now supported for external plugins [#654](https://github.com/clusterio/clusterio/pull/654).
- Changed default role selection to be a dropdown menu rather than id [#664](https://github.com/clusterio/clusterio/pull/664).
- Added credential option to config entries to allow writing sensitive data that can't be read back remotely.
- Allow hidden and readonly graphical representations of config values. [#665](https://github.com/clusterio/clusterio/pull/665).
### Changes
+19 -5
View File
@@ -118,10 +118,21 @@ export interface FieldDefinition {
*/
access?: ConfigLocation[];
/**
* If present treat the config value as a credential and only allow reading
* from the locations specified.
* If present treat the config value as a credential.
* Only the locations specified will be able to read the current value.
* Access can be used to specify which locations are able to overwrite the value.
*/
credential?: ConfigLocation[];
/**
* If present treat the config value as readonly.
* Only the locations specified will be able to update the config value.
* Access can be used to specify which locations are able to read the value.
*/
readonly?: ConfigLocation[];
/**
* True if graphical interfaces should hide the config value
*/
hidden?: boolean;
}
export type ConfigDefs<Fields> = {
@@ -231,11 +242,14 @@ export class Config<
*/
_checkAccess(name: string, def: FieldDefinition, location: ConfigLocation, mode: ConfigAccess, error: boolean) {
// If credential array is present it acts as read permission and the access array as write permission,
// if not present access array acts as a combined read/write permission check.
// If readonly array is present it acts as write permission and the access array as the read permission.
// If neither is present access array acts as a combined read/write permission check.
if (
def.credential && mode & ConfigAccess.read && !(def.credential).includes(location)
|| def.readonly && mode & ConfigAccess.write && !(def.readonly).includes(location)
|| (
(!def.credential || mode & ConfigAccess.write)
&& (!def.readonly || mode & ConfigAccess.read)
&& !(def.access ?? this.defaultAccess).includes(location)
)
) {
@@ -295,7 +309,7 @@ export class Config<
for (let [name, value] of Object.entries(this.fields)) {
let def = this.constructor.fieldDefinitions[name];
if (
!this._checkAccess(name, def, this.location, ConfigAccess.write, false)
this.location !== "control" && !this._checkAccess(name, def, this.location, ConfigAccess.write, false)
|| this.location !== remote && !this._checkAccess(name, def, remote, ConfigAccess.read, false)
) {
continue;
@@ -344,7 +358,7 @@ export class Config<
}
if (
!this._checkAccess(name, def, remote, ConfigAccess.write, false)
this.location !== "control" && !this._checkAccess(name, def, remote, ConfigAccess.write, false)
) {
continue;
}
+3
View File
@@ -223,6 +223,7 @@ export class HostConfig extends classes.Config<HostConfigFields> {
description: "ID of the host",
type: "number",
initialValue: () => Math.random() * 2**31 | 0,
hidden: true,
},
"host.factorio_directory": {
description: "Path to directory to look for factorio installs",
@@ -345,10 +346,12 @@ export class InstanceConfig extends classes.Config<InstanceConfigFields> {
description: "ID of the instance",
type: "number",
initialValue: () => Math.random() * 2**31 | 0,
hidden: true,
},
"instance.assigned_host": {
type: "number",
optional: true,
hidden: true,
},
"instance.auto_start": {
description: "Automatically start this instance when the host hosting it is started up",
+1
View File
@@ -24,6 +24,7 @@ export interface PluginLoginForm {
}
export interface InputComponentProps {
disabled?: boolean,
fieldDefinition: FieldDefinition,
value: null | boolean | number | string,
onChange: (value: null | boolean | number | string) => void,
@@ -32,24 +32,24 @@ function getInitialValues(config: lib.Config<any>, props: BaseConfigTreeProps) {
return initialValues;
}
function renderInput(inputComponents: Record<string, InputComponent>, def: lib.FieldDefinition) {
function renderInput(inputComponents: Record<string, InputComponent>, def: lib.FieldDefinition, readonly: boolean) {
if (def.inputComponent && Object.prototype.hasOwnProperty.call(inputComponents, def.inputComponent)) {
// Field.Item will provide the value and onChange props to the component.
type InputPartial = React.ComponentClass<Omit<InputComponentProps, "value" | "onChange">>;
const CustomInput = inputComponents[def.inputComponent] as unknown as InputPartial;
return <CustomInput fieldDefinition={def} />;
return <CustomInput fieldDefinition={def} disabled={readonly} />;
}
if (def.type === "boolean") {
return <Checkbox/>;
return <Checkbox disabled={readonly} />;
}
if (def.type === "string") {
if (def.credential) {
return <Input.Password autoComplete={def.autoComplete} />;
return <Input.Password autoComplete={def.autoComplete} disabled={readonly} />;
}
return <Input autoComplete={def.autoComplete} />;
return <Input autoComplete={def.autoComplete} disabled={readonly} />;
}
if (def.type === "number") {
return <InputNumber autoComplete={def.autoComplete} />;
return <InputNumber autoComplete={def.autoComplete} disabled={readonly} />;
}
return `Unknown type ${def.type}`;
@@ -264,13 +264,17 @@ function computeTreeData(
};
for (let [field, def] of Object.entries(groupDefs)) {
const canWrite = config.canAccess(`${groupName}.${field}`, lib.ConfigAccess.write);
const canRead = config.canAccess(`${groupName}.${field}`, lib.ConfigAccess.read);
if (def.hidden === true) {
continue;
}
const fieldName = `${groupName}.${field}`;
const canWrite = config.canAccess(fieldName, lib.ConfigAccess.write);
const canRead = config.canAccess(fieldName, lib.ConfigAccess.read);
if (!canWrite && !canRead) {
continue;
}
let fieldName = `${groupName}.${field}`;
let value = config.get(fieldName) as any;
let childNode: ChildNode = {
key: fieldName,
@@ -311,7 +315,7 @@ function computeTreeData(
return Promise.resolve();
}}]}
>
<Input className="json-input" />
<Input className="json-input" disabled={!canWrite} />
</Form.Item>,
});
@@ -319,47 +323,53 @@ function computeTreeData(
propsMap.set(propPath, [fieldName, prop]);
}
let newPropPath = `${groupName}.${field}: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={() => {
let propName = form.getFieldValue(`${newPropPath}.name`);
let propValue = form.getFieldValue(`${newPropPath}.value`);
if (!value || !Object.prototype.hasOwnProperty.call(value, propName)) {
let newConfig = ConfigClass.fromJSON(config!.toJSON(), "control");
(newConfig as lib.Config<any>).setProp(fieldName, propName, null);
setConfig(newConfig);
}
if (canWrite) {
let newPropPath = `${groupName}.${field}: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={() => {
let propName = form.getFieldValue(`${newPropPath}.name`);
let propValue = form.getFieldValue(`${newPropPath}.value`);
if (!value || !Object.prototype.hasOwnProperty.call(value, propName)) {
let newConfig = ConfigClass.fromJSON(config!.toJSON(), "control");
(newConfig as lib.Config<any>).setProp(fieldName, propName, null);
setConfig(newConfig);
}
let propPath = `${groupName}.${field}.${propName}`;
form.setFieldsValue({
[`${newPropPath}.name`]: "",
[`${newPropPath}.value`]: "",
[propPath]: propValue,
});
form.validateFields([propPath]);
onValuesChange({ [propPath]: propValue });
}}
>
Add
</Button>
</Space>,
});
let propPath = `${groupName}.${field}.${propName}`;
form.setFieldsValue({
[`${newPropPath}.name`]: "",
[`${newPropPath}.value`]: "",
[propPath]: propValue,
});
form.validateFields([propPath]);
onValuesChange({ [propPath]: propValue });
}}
>
Add
</Button>
</Space>,
});
}
if (!childNode.children.length) {
continue;
}
} else {
childNode.title = <Form.Item
@@ -373,7 +383,7 @@ function computeTreeData(
tooltip={def.description}
valuePropName={def.type === "boolean" ? "checked" : "value"}
>
{renderInput(control.inputComponents, def)}
{renderInput(control.inputComponents, def, !canWrite)}
</Form.Item>;
initialValues[fieldName] = value;
}
@@ -15,6 +15,6 @@ export default function InputModPack(props: InputComponentProps) {
value: modPack.id,
}))}
allowClear={props.fieldDefinition.optional}
disabled={props.disabled}
/>;
}
@@ -26,5 +26,6 @@ export default function InputRole(props: InputComponentProps) {
value: role.id,
}))}
allowClear={props.fieldDefinition.optional}
disabled={props.disabled}
/>;
}
+14
View File
@@ -27,6 +27,7 @@ describe("lib/config/classes", function() {
"test.json": { type: "object", initialValue: {}, optional: true },
"test.priv": { access: ["local"], type: "string", optional: true },
"test.cred": { credential: ["local"] },
"test.rdo": { readonly: ["local"] },
};
constructor(location, fields) {
@@ -74,6 +75,7 @@ describe("lib/config/classes", function() {
"test.json": {},
"test.priv": null,
"test.cred": null,
"test.rdo": null,
});
});
});
@@ -89,6 +91,7 @@ describe("lib/config/classes", function() {
"test.func": 42,
"test.bool": false,
"test.json": {},
"test.rdo": null,
});
});
});
@@ -151,6 +154,7 @@ describe("lib/config/classes", function() {
"test.json": {},
"test.priv": null,
"test.cred": null,
"test.rdo": null,
"test.alpha": null,
"test.beta": "decay",
"test.gamma": 99,
@@ -185,6 +189,7 @@ describe("lib/config/classes", function() {
"test.json": {},
"test.priv": null,
"test.cred": null,
"test.rdo": null,
"test.test": null,
});
});
@@ -256,6 +261,9 @@ describe("lib/config/classes", function() {
assert.equal(testInstance.canAccess("test.cred", CA.read), true);
assert.equal(testInstance.canAccess("test.cred", CA.read, "local"), true);
assert.equal(testInstance.canAccess("test.cred", CA.read, "remote"), false);
assert.equal(testInstance.canAccess("test.rdo", CA.read), true);
assert.equal(testInstance.canAccess("test.rdo", CA.read, "local"), true);
assert.equal(testInstance.canAccess("test.rdo", CA.read, "remote"), true);
});
it("should return true checking write for fields that are writeable", function() {
let testInstance = new TestConfig("local");
@@ -271,6 +279,9 @@ describe("lib/config/classes", function() {
assert.equal(testInstance.canAccess("test.cred", CA.write), true);
assert.equal(testInstance.canAccess("test.cred", CA.write, "local"), true);
assert.equal(testInstance.canAccess("test.cred", CA.write, "remote"), true);
assert.equal(testInstance.canAccess("test.rdo", CA.write), true);
assert.equal(testInstance.canAccess("test.rdo", CA.write, "local"), true);
assert.equal(testInstance.canAccess("test.rdo", CA.write, "remote"), false);
});
it("should return true for checking readWrite for fields that are readable and writeable", function() {
let testInstance = new TestConfig("local");
@@ -286,6 +297,9 @@ describe("lib/config/classes", function() {
assert.equal(testInstance.canAccess("test.cred", CA.readWrite), true);
assert.equal(testInstance.canAccess("test.cred", CA.readWrite, "local"), true);
assert.equal(testInstance.canAccess("test.cred", CA.readWrite, "remote"), false);
assert.equal(testInstance.canAccess("test.rdo", CA.readWrite), true);
assert.equal(testInstance.canAccess("test.rdo", CA.readWrite, "local"), true);
assert.equal(testInstance.canAccess("test.rdo", CA.readWrite, "remote"), false);
});
});