mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
Give templates their own table and keep rcon_password per instance
The templates table shows name, inheritor count and a create instance action instead of the instance status and start/stop columns. factorio.rcon_password joins the ports as never inherited. Part of #200 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b5c372f109
commit
b584991f5c
6 files changed
+66
-11
No files matched your search
@@ -537,7 +537,7 @@ ID of the template to inherit config values from.
|
||||
Inherited fields take their value from the template and follow it when it changes, setting a field on this instance stops inheriting it.
|
||||
Object fields like `factorio.settings` are inherited per property, properties set on this instance override the ones from the template.
|
||||
When set on an existing instance fields with the same value as the template or the default value become inherited, except the server name in `factorio.settings`.
|
||||
`instance.name`, `instance.is_template`, `instance.template_id`, the port fields and hidden fields are never inherited.
|
||||
`instance.name`, `instance.is_template`, `instance.template_id`, the port fields, `factorio.rcon_password` and hidden fields are never inherited.
|
||||
A template may itself inherit from another template, but not from itself through a chain of templates.
|
||||
The fields currently inherited are stored in the hidden `instance.inherited_fields` field.
|
||||
In the web interface inherited values are locked and must be unlocked before they can be changed.
|
||||
|
||||
@@ -542,10 +542,10 @@ export class InstanceConfig extends classes.Config<InstanceConfigFields> {
|
||||
return config;
|
||||
}
|
||||
|
||||
/** Fields that are never inherited from a template, ports would clash on a shared host */
|
||||
/** Fields that are never inherited from a template, ports clash on a shared host and secrets stay per instance */
|
||||
static uninheritableFields = [
|
||||
"instance.name", "instance.is_template", "instance.template_id",
|
||||
"factorio.game_port", "factorio.rcon_port", "factorio.lua_udp_port",
|
||||
"factorio.game_port", "factorio.rcon_port", "factorio.rcon_password", "factorio.lua_udp_port",
|
||||
];
|
||||
|
||||
/** True if the field can take its value from a template */
|
||||
|
||||
@@ -23,7 +23,6 @@ type InstanceListProps = {
|
||||
instances: ReadonlyMap<number, Readonly<lib.InstanceDetails>>;
|
||||
size?: SizeType;
|
||||
hideAssignedHost?: boolean;
|
||||
tableNamespace?: string;
|
||||
};
|
||||
|
||||
export default function InstanceList(props: InstanceListProps) {
|
||||
@@ -31,7 +30,7 @@ export default function InstanceList(props: InstanceListProps) {
|
||||
let [hosts] = useHosts();
|
||||
const [systems] = useSystems();
|
||||
const tableState = useTableQueryState<lib.InstanceDetails>({
|
||||
namespace: props.tableNamespace ?? "instance", defaultSortKey: "name",
|
||||
namespace: "instance", defaultSortKey: "name",
|
||||
});
|
||||
const nameSearch = useColumnSearch<lib.InstanceDetails>(
|
||||
tableState, "name", instance => instance.name, "Search instances"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button, Checkbox, Form, Input, Modal, Select, Space, Tooltip, Typography } from "antd";
|
||||
import { Button, Checkbox, Form, Input, Modal, Select, Space, Table, Tooltip, Typography } from "antd";
|
||||
import type { ColumnType } from "antd/es/table/interface";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
@@ -13,6 +14,8 @@ import { useHosts } from "../model/host";
|
||||
import { useInstanceConfig, useInstances } from "../model/instance";
|
||||
import { useSaves } from "../model/saves";
|
||||
import InstanceList from "./InstanceList";
|
||||
import Link from "./Link";
|
||||
import useRowNavigation from "../util/useRowNavigation";
|
||||
import { notifyErrorHandler } from "../util/notify";
|
||||
import type { Control } from "../util/websocket";
|
||||
|
||||
@@ -92,6 +95,7 @@ export function CreateInstanceButton({ buttonText = "Create", buttonType = "prim
|
||||
initial?: CreateInstanceInitial;
|
||||
buttonText?: string;
|
||||
buttonType?: "primary" | "default";
|
||||
buttonSize?: "small";
|
||||
}) {
|
||||
let control = useContext(ControlContext);
|
||||
let account = useAccount();
|
||||
@@ -195,6 +199,7 @@ export function CreateInstanceButton({ buttonText = "Create", buttonType = "prim
|
||||
return <>
|
||||
<Button
|
||||
type={buttonType}
|
||||
size={props.buttonSize}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
@@ -281,12 +286,59 @@ export function CreateInstanceButton({ buttonText = "Create", buttonType = "prim
|
||||
</>;
|
||||
}
|
||||
|
||||
function TemplateList(props: { instances: ReturnType<typeof useInstances>[0] }) {
|
||||
const account = useAccount();
|
||||
const rowNav = useRowNavigation();
|
||||
const all = [...props.instances.values()];
|
||||
const inheritors = (id: number) => all.filter(instance => instance.templateId === id).length;
|
||||
const columns: ColumnType<lib.InstanceDetails>[] = [
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
defaultSortOrder: "ascend",
|
||||
sorter: (a, b) => strcmp(a.name, b.name),
|
||||
render: (_, template) => <Link to={`/instances/${template.id}/view`} style={{ color: "inherit" }}>
|
||||
{template.name}
|
||||
</Link>,
|
||||
},
|
||||
{
|
||||
title: "Inherited by",
|
||||
key: "inheritors",
|
||||
render: (_, template) => `${inheritors(template.id)} instance(s)`,
|
||||
sorter: (a, b) => inheritors(a.id) - inheritors(b.id),
|
||||
},
|
||||
];
|
||||
if (account.hasPermission("core.instance.create")) {
|
||||
columns.push({
|
||||
key: "action",
|
||||
align: "right",
|
||||
// Clicks inside the modal bubble through the React tree to the row
|
||||
render: (_, template) => <span onClick={e => e.stopPropagation()}>
|
||||
<CreateInstanceButton
|
||||
instances={props.instances}
|
||||
initial={{ template: template.id }}
|
||||
buttonText="Create instance"
|
||||
buttonType="default"
|
||||
buttonSize="small"
|
||||
/>
|
||||
</span>,
|
||||
});
|
||||
}
|
||||
return <Table
|
||||
columns={columns}
|
||||
dataSource={all.filter(instance => instance.isTemplate)}
|
||||
rowKey={template => template.id}
|
||||
pagination={false}
|
||||
onRow={record => rowNav(`/instances/${record.id}/view`)}
|
||||
/>;
|
||||
}
|
||||
|
||||
export default function InstancesPage() {
|
||||
let control = useContext(ControlContext);
|
||||
let account = useAccount();
|
||||
let [instances] = useInstances();
|
||||
const plainInstances = new Map([...instances].filter(([, instance]) => !instance.isTemplate));
|
||||
const templates = new Map([...instances].filter(([, instance]) => instance.isTemplate));
|
||||
const hasTemplates = plainInstances.size !== instances.size;
|
||||
|
||||
return <PageLayout nav={[{ name: "Instances" }]}>
|
||||
<PageHeader
|
||||
@@ -321,9 +373,9 @@ export default function InstancesPage() {
|
||||
/>
|
||||
|
||||
<InstanceList instances={plainInstances} />
|
||||
{templates.size > 0 && <>
|
||||
{hasTemplates && <>
|
||||
<Typography.Title level={5} style={{ marginTop: 16 }}>Templates</Typography.Title>
|
||||
<InstanceList instances={templates} hideAssignedHost tableNamespace="template" />
|
||||
<TemplateList instances={instances} />
|
||||
</>}
|
||||
<PluginExtra component="InstancesPage" />
|
||||
</PageLayout>;
|
||||
|
||||
@@ -433,11 +433,14 @@ describe("controller/InstanceManager", function () {
|
||||
assert.equal(child.config.isInherited("factorio.game_port"), false);
|
||||
assert.equal(child.config.isInherited("instance.name"), false);
|
||||
});
|
||||
it("should never inherit ports", function () {
|
||||
it("should never inherit ports or the rcon password", function () {
|
||||
template.config.set("factorio.rcon_password", "secret");
|
||||
const config = new lib.InstanceConfig("controller", { "instance.id": 12, "instance.template_id": 10 });
|
||||
instances.applyTemplate(config, ["instance.id", "instance.template_id"]);
|
||||
assert.equal(config.get("factorio.game_port"), null);
|
||||
assert.equal(config.isInherited("factorio.game_port"), false);
|
||||
assert.equal(config.get("factorio.rcon_password"), null);
|
||||
assert.equal(config.isInherited("factorio.rcon_password"), false);
|
||||
});
|
||||
it("should inherit factorio.settings per property with its own server name", function () {
|
||||
const settings = child.config.get("factorio.settings");
|
||||
|
||||
@@ -14,7 +14,8 @@ describe("lib/config/definitions", function() {
|
||||
for (const name of [
|
||||
"instance.name", "instance.id", "instance.assigned_host", "instance.is_template",
|
||||
"instance.template_id", "instance.inherited_fields", "factorio.host_assigned_game_port",
|
||||
"factorio.game_port", "factorio.rcon_port", "factorio.lua_udp_port", "no.field",
|
||||
"factorio.game_port", "factorio.rcon_port", "factorio.rcon_password", "factorio.lua_udp_port",
|
||||
"no.field",
|
||||
]) {
|
||||
assert.equal(lib.InstanceConfig.isInheritable(name), false, name);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user