mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
Add hidden and readonly config properties (#665)
This commit is contained in:
8 files changed
+101
-57
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
/>;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user