mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 15:04:52 +09:00
Implement custom input for mod pack config (#547)
Add custom input component support to the Web UI config in order to provide a user friendly dropdown selector for the Mod Pack that is configured as default on the controller and overridden instances.
This commit is contained in:
1 parent
80f0c63113
commit
863d90c7fb
8 files changed
+86
-13
No files matched your search
@@ -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());
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -150,6 +150,7 @@ export class ControllerConfig extends classes.Config<ControllerConfigFields> {
|
||||
"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<InstanceConfigFields> {
|
||||
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,
|
||||
|
||||
@@ -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<InputComponentProps>;
|
||||
|
||||
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<string, InputComponent> = {};
|
||||
/**
|
||||
* Extra react component to add to core components
|
||||
*
|
||||
|
||||
@@ -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<string, BaseWebPlugin>) {
|
||||
const inputComponents: Record<string, InputComponent> = {
|
||||
"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(<App control={control}/>);
|
||||
|
||||
@@ -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<any>, props: BaseConfigTreeProps) {
|
||||
let initialValues: any = {};
|
||||
for (let [name, def] of Object.entries(config.constructor.fieldDefinitions)) {
|
||||
@@ -27,7 +32,13 @@ function getInitialValues(config: lib.Config<any>, props: BaseConfigTreeProps) {
|
||||
return initialValues;
|
||||
}
|
||||
|
||||
function renderInput(def: lib.FieldDefinition) {
|
||||
function renderInput(inputComponents: Record<string, InputComponent>, 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<Omit<InputComponentProps, "value" | "onChange">>;
|
||||
const CustomInput = inputComponents[def.inputComponent] as unknown as InputPartial;
|
||||
return <CustomInput fieldDefinition={def} />;
|
||||
}
|
||||
if (def.type === "boolean") {
|
||||
return <Checkbox/>;
|
||||
}
|
||||
@@ -57,6 +68,7 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) {
|
||||
let [errorFields, setErrorFields] = useState(new Map<string, string>());
|
||||
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<any>,
|
||||
initialValues: any,
|
||||
errorFields: Map<string, string>,
|
||||
@@ -352,7 +366,7 @@ function computeTreeData(
|
||||
tooltip={def.description}
|
||||
valuePropName={def.type === "boolean" ? "checked" : "value"}
|
||||
>
|
||||
{renderInput(def)}
|
||||
{renderInput(control.inputComponents, def)}
|
||||
</Form.Item>;
|
||||
initialValues[fieldName] = value;
|
||||
}
|
||||
|
||||
@@ -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 <Select
|
||||
style={{ minWidth: 175 }}
|
||||
onChange={(value) => props.onChange(value ?? null)}
|
||||
value={props.value}
|
||||
options={[...modPacks.values()].map(modPack => ({
|
||||
label: modPack.name,
|
||||
value: modPack.id,
|
||||
}))}
|
||||
allowClear={props.fieldDefinition.optional}
|
||||
/>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import packageJson from "../../package.json";
|
||||
import BaseWebPlugin from "../BaseWebPlugin";
|
||||
import BaseWebPlugin, { type InputComponent } from "../BaseWebPlugin";
|
||||
|
||||
const { logFilter, logger } = lib;
|
||||
|
||||
@@ -53,6 +53,7 @@ export class Control extends lib.Link {
|
||||
|
||||
/** Plugins loaded in the web interface */
|
||||
public plugins = new Map<string, BaseWebPlugin>();
|
||||
public inputComponents: Record<string, InputComponent> = {};
|
||||
|
||||
/** Updates not handled by the subscription service */
|
||||
accountUpdateHandlers: accountHandler[] = [];
|
||||
|
||||
Reference in new issue
Block a user