diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index 3fda98ec..93ca71aa 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -638,7 +638,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); */ async syncMods() { this.logger.info("Syncing mods"); - const modPackId = this.config.get("factorio.mod_pack"); + const modPackId = this.config.get("factorio.mod_pack_id"); let modPack; if (modPackId === null) { modPack = await this.sendTo("controller", new lib.ModPackGetDefaultRequest()); diff --git a/packages/lib/src/config/classes.ts b/packages/lib/src/config/classes.ts index 2963ee36..3e26b2c8 100644 --- a/packages/lib/src/config/classes.ts +++ b/packages/lib/src/config/classes.ts @@ -77,6 +77,11 @@ export interface FieldDefinition { * Text used to describe the config field in user interfaces. */ description?: string; + /** + * Web UI component to use for rending this input. Can be extended by + * @{link "@clusterio/web_ui".BaseWebPlugin.inputComponents} + */ + inputComponent?: string; /** * True if this field can be set to null. Defaults to false. */ diff --git a/packages/lib/src/config/definitions.ts b/packages/lib/src/config/definitions.ts index d0cbb114..445a66ad 100644 --- a/packages/lib/src/config/definitions.ts +++ b/packages/lib/src/config/definitions.ts @@ -150,6 +150,7 @@ export class ControllerConfig extends classes.Config { "controller.default_mod_pack_id": { title: "Default Mod Pack", description: "Mod pack used by default for instances.", + inputComponent: "mod_pack", type: "number", optional: true, }, @@ -265,7 +266,7 @@ export interface InstanceConfigFields { "factorio.rcon_port": number | null; "factorio.rcon_password": string | null; "factorio.player_online_autosave_slots": number; - "factorio.mod_pack": number | null; + "factorio.mod_pack_id": number | null; "factorio.enable_save_patching": boolean; "factorio.enable_whitelist": boolean; "factorio.enable_authserver_bans": boolean; @@ -340,8 +341,11 @@ export class InstanceConfig extends classes.Config { type: "number", initialValue: 5, }, - "factorio.mod_pack": { - description: "Mod pack to use on this server", + "factorio.mod_pack_id": { + title: "Mod Pack", + description: + "Mod pack to use on this server, if not set the default configured on the controller will be used", + inputComponent: "mod_pack", restartRequired: true, type: "number", optional: true, diff --git a/packages/web_ui/src/BaseWebPlugin.ts b/packages/web_ui/src/BaseWebPlugin.ts index 5a3a9b98..286ef9af 100644 --- a/packages/web_ui/src/BaseWebPlugin.ts +++ b/packages/web_ui/src/BaseWebPlugin.ts @@ -1,5 +1,5 @@ import type React from "react"; -import type { AccountRole, Logger, PluginWebpackEnvInfo } from "@clusterio/lib"; +import type { AccountRole, FieldDefinition, Logger, PluginWebpackEnvInfo } from "@clusterio/lib"; import type { Control } from "./util/websocket"; /** @@ -21,7 +21,14 @@ export interface PluginLoginForm { * when an authentication token is aquired via this form. */ Component: React.ComponentType<{ setToken(token: string): void }>; -}; +} + +export interface InputComponentProps { + fieldDefinition: FieldDefinition, + value: null | boolean | number | string, + onChange: (value: null | boolean | number | string) => void, +} +export type InputComponent = React.ComponentType; export type UserAccount = { /** Name of the currently logged in account. */ @@ -94,6 +101,11 @@ export default class BaseWebPlugin { * List of pages provided by this plugin */ pages: PluginPage[] = []; + /** + * Additional Config inputComponent types available to render config + * entries with. + */ + inputComponents: Record = {}; /** * Extra react component to add to core components * diff --git a/packages/web_ui/src/bootstrap.tsx b/packages/web_ui/src/bootstrap.tsx index bc3aef2e..5d45395d 100644 --- a/packages/web_ui/src/bootstrap.tsx +++ b/packages/web_ui/src/bootstrap.tsx @@ -6,7 +6,8 @@ import { createRoot } from "react-dom/client"; import * as lib from "@clusterio/lib"; import App from "./components/App"; -import BaseWebPlugin from "./BaseWebPlugin"; +import BaseWebPlugin, { InputComponent } from "./BaseWebPlugin"; +import InputModPack from "./components/InputModPack"; import { Control, ControlConnector } from "./util/websocket"; const { ConsoleTransport, WebConsoleFormat, logger } = lib; @@ -97,6 +98,21 @@ async function loadPlugins(pluginInfos: lib.PluginWebpackEnvInfo[], control: Con return plugins; } +function inputComponentsFromPlugins(plugins: Map) { + const inputComponents: Record = { + "mod_pack": InputModPack, + }; + for (let [pluginName, plugin] of plugins) { + for (const [name, Component] of Object.entries(plugin.inputComponents)) { + if (Object.prototype.hasOwnProperty.call(inputComponents, name)) { + lib.logger.warn(`Plugin ${pluginName} is redefining config inputComponent ${name}`); + } + inputComponents[name] = Component; + } + } + return inputComponents; +} + export default async function bootstrap() { logger.add(new ConsoleTransport({ level: "verbose", @@ -110,6 +126,7 @@ export default async function bootstrap() { let controlConnector = new ControlConnector(wsUrl.href, 120, undefined); let control = new Control(controlConnector); control.plugins = await loadPlugins(pluginInfos, control); + control.inputComponents = inputComponentsFromPlugins(control.plugins); const root = createRoot(document.getElementById("root") as HTMLDivElement); root.render(); diff --git a/packages/web_ui/src/components/BaseConfigTree.tsx b/packages/web_ui/src/components/BaseConfigTree.tsx index be22aeb2..8354090d 100644 --- a/packages/web_ui/src/components/BaseConfigTree.tsx +++ b/packages/web_ui/src/components/BaseConfigTree.tsx @@ -1,13 +1,18 @@ -import * as lib from "@clusterio/lib"; - -import React, { useEffect, useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { Button, Card, Checkbox, Form, FormInstance, Input, InputNumber, Space, Spin, Tooltip, Tree, Typography, } from "antd"; import ReloadOutlined from "@ant-design/icons/ReloadOutlined"; +import * as lib from "@clusterio/lib"; + +import ControlContext from "./ControlContext"; +import { InputComponent, InputComponentProps } from "../BaseWebPlugin"; +import type { Control } from "../util/websocket"; + const { Title } = Typography; + function getInitialValues(config: lib.Config, props: BaseConfigTreeProps) { let initialValues: any = {}; for (let [name, def] of Object.entries(config.constructor.fieldDefinitions)) { @@ -27,7 +32,13 @@ function getInitialValues(config: lib.Config, props: BaseConfigTreeProps) { return initialValues; } -function renderInput(def: lib.FieldDefinition) { +function renderInput(inputComponents: Record, def: lib.FieldDefinition) { + 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>; + const CustomInput = inputComponents[def.inputComponent] as unknown as InputPartial; + return ; + } if (def.type === "boolean") { return ; } @@ -57,6 +68,7 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) { let [errorFields, setErrorFields] = useState(new Map()); let [applying, setApplying] = useState(false); const available = props.available ?? true; + const control = useContext(ControlContext); async function updateConfig() { let serializedConfig = await props.retrieveConfig(); @@ -112,6 +124,7 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) { } const [propsMap, treeData] = computeTreeData( + control, form, initialValues, errorFields, @@ -201,6 +214,7 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) { } function computeTreeData( + control: Control, form: FormInstance, initialValues: any, errorFields: Map, @@ -352,7 +366,7 @@ function computeTreeData( tooltip={def.description} valuePropName={def.type === "boolean" ? "checked" : "value"} > - {renderInput(def)} + {renderInput(control.inputComponents, def)} ; initialValues[fieldName] = value; } diff --git a/packages/web_ui/src/components/InputModPack.tsx b/packages/web_ui/src/components/InputModPack.tsx new file mode 100644 index 00000000..fd16a5cf --- /dev/null +++ b/packages/web_ui/src/components/InputModPack.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { Select } from "antd"; + +import { useModPacks } from "../model/mod_pack"; +import { InputComponentProps } from "../BaseWebPlugin"; + +export default function InputModPack(props: InputComponentProps) { + const [modPacks] = useModPacks(); + return