mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
Show the mod pack an instance uses in the web UI (#1177)
InstanceDetails now carries factorio.mod_pack_id as modPackId, so the instance list and pages can show it without fetching each config. The web UI resolves an unset value to the controller default and tags it "Default". The name links to the mod pack page. Closes #852 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6eaf48aefe
commit
d250394580
9 files changed
+138
-2
No files matched your search
@@ -85,6 +85,7 @@ export default class InstanceRecord {
|
||||
this.config.get("instance.exclude_from_start_all"),
|
||||
this.config.get("instance.is_template"),
|
||||
this.config.get("instance.template_id") ?? undefined,
|
||||
this.config.get("factorio.mod_pack_id") ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ export default class InstanceDetails {
|
||||
public excludeFromStartAll = false,
|
||||
public isTemplate = false,
|
||||
public templateId?: number,
|
||||
/** Value of factorio.mod_pack_id, undefined means the controller default is used */
|
||||
public modPackId?: number,
|
||||
) {}
|
||||
|
||||
get isDeleted() {
|
||||
@@ -65,6 +67,7 @@ export default class InstanceDetails {
|
||||
"excludeFromStartAll": Type.Optional(Type.Boolean()),
|
||||
"isTemplate": Type.Optional(Type.Boolean()),
|
||||
"templateId": Type.Optional(Type.Integer()),
|
||||
"modPackId": Type.Optional(Type.Integer()),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
@@ -80,6 +83,7 @@ export default class InstanceDetails {
|
||||
json.excludeFromStartAll,
|
||||
json.isTemplate,
|
||||
json.templateId,
|
||||
json.modPackId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,6 +118,9 @@ export default class InstanceDetails {
|
||||
if (this.templateId !== undefined) {
|
||||
json.templateId = this.templateId;
|
||||
}
|
||||
if (this.modPackId !== undefined) {
|
||||
json.modPackId = this.modPackId;
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ import type { ColumnType } from "antd/es/table/interface";
|
||||
import { useAccount } from "../model/account";
|
||||
import { useSystems } from "../model/system";
|
||||
import { useHosts } from "../model/host";
|
||||
import { useDefaultModPack, useModPacks } from "../model/mod_pack";
|
||||
import { RestartRequired } from "./system_metrics";
|
||||
import InstanceStatusTag from "./InstanceStatusTag";
|
||||
import InstanceModPack from "./InstanceModPack";
|
||||
import InstanceControlButton, { InstanceControlButtonPermissions } from "./InstanceControlButton";
|
||||
import * as lib from "@clusterio/lib";
|
||||
import Link from "./Link";
|
||||
import { instancePublicAddress } from "../util/instance";
|
||||
import { instanceModPack, instancePublicAddress } from "../util/instance";
|
||||
import useTableQueryState from "../util/useTableQueryState";
|
||||
import useColumnSearch from "../util/useColumnSearch";
|
||||
import useRowNavigation from "../util/useRowNavigation";
|
||||
@@ -29,6 +31,8 @@ export default function InstanceList(props: InstanceListProps) {
|
||||
let account = useAccount();
|
||||
let [hosts] = useHosts();
|
||||
const [systems] = useSystems();
|
||||
const [modPacks] = useModPacks();
|
||||
const defaultModPack = useDefaultModPack();
|
||||
const tableState = useTableQueryState<lib.InstanceDetails>({
|
||||
namespace: "instance", defaultSortKey: "name",
|
||||
});
|
||||
@@ -44,6 +48,15 @@ export default function InstanceList(props: InstanceListProps) {
|
||||
return hosts.get(hostId)?.name ?? String(hostId);
|
||||
}
|
||||
|
||||
function modPackOf(instance: lib.InstanceDetails) {
|
||||
return instanceModPack(instance.modPackId, modPacks, defaultModPack);
|
||||
}
|
||||
|
||||
function modPackName(instance: lib.InstanceDetails) {
|
||||
const { id, modPack } = modPackOf(instance);
|
||||
return modPack?.name ?? (id !== undefined ? String(id) : "");
|
||||
}
|
||||
|
||||
function integerFactorioVersionOrDefault(instance: lib.InstanceDetails) {
|
||||
const v = instance.factorioVersion;
|
||||
if (v && lib.isPartialVersion(v)) {
|
||||
@@ -115,6 +128,15 @@ export default function InstanceList(props: InstanceListProps) {
|
||||
sortOrder: tableState.sortOrder("version"),
|
||||
responsive: ["xl"],
|
||||
},
|
||||
{
|
||||
title: "Mod Pack",
|
||||
key: "modPack",
|
||||
className: "table-link-cell",
|
||||
render: (_, instance) => <InstanceModPack resolved={modPackOf(instance)} />,
|
||||
sorter: (a, b) => strcmp(modPackName(a), modPackName(b)),
|
||||
sortOrder: tableState.sortOrder("modPack"),
|
||||
responsive: ["lg"],
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
key: "status",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import { Space, Tag, Tooltip } from "antd";
|
||||
|
||||
import Link from "./Link";
|
||||
import type { ResolvedModPack } from "../util/instance";
|
||||
|
||||
export default function InstanceModPack(props: { resolved: ResolvedModPack }) {
|
||||
const { id, modPack, isDefault } = props.resolved;
|
||||
if (id === undefined) {
|
||||
return <em>Controller default</em>;
|
||||
}
|
||||
return <Space size="small">
|
||||
<Link to={`/mods/mod-packs/${id}/view`} onClick={e => e.stopPropagation()}>
|
||||
{modPack?.name ?? id}
|
||||
</Link>
|
||||
{isDefault && <Tooltip title="Not set on this instance, the controller default mod pack is used">
|
||||
<Tag>Default</Tag>
|
||||
</Tooltip>}
|
||||
</Space>;
|
||||
}
|
||||
@@ -31,7 +31,9 @@ import { CreateInstanceButton } from "./InstancesPage";
|
||||
import { useHost } from "../model/host";
|
||||
import InstanceStatusTag from "./InstanceStatusTag";
|
||||
import Link from "./Link";
|
||||
import { instancePublicAddress } from "../util/instance";
|
||||
import { instanceModPack, instancePublicAddress } from "../util/instance";
|
||||
import { useDefaultModPack, useModPacks } from "../model/mod_pack";
|
||||
import InstanceModPack from "./InstanceModPack";
|
||||
import useLocalStorage from "../util/useLocalStorage";
|
||||
import useResourceState from "../util/useResourceState";
|
||||
import { MetricRelativeDate } from "./system_metrics";
|
||||
@@ -60,9 +62,18 @@ function TemplateDescription(props: { instance: Readonly<lib.InstanceDetails> })
|
||||
{props.instance.templateId !== undefined && <Descriptions.Item label="Template">
|
||||
<TemplateLink id={props.instance.templateId} />
|
||||
</Descriptions.Item>}
|
||||
<Descriptions.Item label="Mod Pack">
|
||||
<ModPackDescription modPackId={props.instance.modPackId} />
|
||||
</Descriptions.Item>
|
||||
</Descriptions>;
|
||||
}
|
||||
|
||||
function ModPackDescription(props: { modPackId?: number }) {
|
||||
const [modPacks] = useModPacks();
|
||||
const defaultModPack = useDefaultModPack();
|
||||
return <InstanceModPack resolved={instanceModPack(props.modPackId, modPacks, defaultModPack)} />;
|
||||
}
|
||||
|
||||
function TemplateLink(props: { id: number }) {
|
||||
const [template] = useInstance(props.id);
|
||||
return <Link to={`/instances/${props.id}/view`}>{template?.name ?? props.id}</Link>;
|
||||
@@ -117,6 +128,9 @@ function InstanceDescription(props: InstanceDescriptionProps) {
|
||||
<Descriptions.Item label="Version">
|
||||
{instance.factorioVersion ?? "unknown"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Mod Pack">
|
||||
<ModPackDescription modPackId={instance.modPackId} />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Last Start Time">
|
||||
<MetricRelativeDate timeMs={instance.startedAtMs}/>
|
||||
</Descriptions.Item>
|
||||
|
||||
@@ -9,3 +9,29 @@ export function instancePublicAddress(instance: lib.InstanceDetails, host?: lib.
|
||||
}
|
||||
return `${host.publicAddress}:${instance.gamePort}`;
|
||||
}
|
||||
|
||||
export type ResolvedModPack = {
|
||||
/** ID of the mod pack, undefined if the default is used and not known */
|
||||
id?: number;
|
||||
modPack?: lib.ModPack;
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
/** Resolve the mod pack an instance starts with, falling back to the controller default */
|
||||
export function instanceModPack(
|
||||
modPackId: number | undefined,
|
||||
modPacks: ReadonlyMap<number, lib.ModPack>,
|
||||
defaultModPack?: lib.ModPack,
|
||||
): ResolvedModPack {
|
||||
if (modPackId !== undefined) {
|
||||
return { id: modPackId, modPack: modPacks.get(modPackId), isDefault: false };
|
||||
}
|
||||
if (!defaultModPack) {
|
||||
return { isDefault: true };
|
||||
}
|
||||
return {
|
||||
id: defaultModPack.id,
|
||||
modPack: modPacks.get(defaultModPack.id) ?? defaultModPack,
|
||||
isDefault: true,
|
||||
};
|
||||
}
|
||||
@@ -153,6 +153,17 @@ describe("controller/InstanceRecord", function () {
|
||||
assert.equal(details.startedAtMs, 100);
|
||||
assert.equal(details.updatedAtMs, 200);
|
||||
assert.equal(details.excludeFromStartAll, false);
|
||||
assert.equal(details.modPackId, undefined);
|
||||
});
|
||||
|
||||
it("should include the mod pack set in the config", function () {
|
||||
const config = new lib.InstanceConfig("controller", {
|
||||
"instance.id": 1,
|
||||
"instance.name": "TestInstance",
|
||||
"factorio.mod_pack_id": 3,
|
||||
});
|
||||
const details = new InstanceRecord(config, "stopped").toInstanceDetails();
|
||||
assert.equal(details.modPackId, 3);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ describe("lib/data/InstanceDetails", function () {
|
||||
[undefined, true], // excludeFromStartAll
|
||||
[undefined, true], // isTemplate
|
||||
[undefined, 7], // templateId
|
||||
[undefined, 3], // modPackId
|
||||
));
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import assert from "node:assert/strict";
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
import { instanceModPack } from "../../packages/web_ui/src/util/instance.ts";
|
||||
|
||||
function modPack(id, name) {
|
||||
return lib.ModPack.fromJSON({ id, name, factorio_version: "2.0" });
|
||||
}
|
||||
|
||||
describe("web_ui/util/instance", function() {
|
||||
describe("instanceModPack()", function() {
|
||||
const base = modPack(1, "Base");
|
||||
const space = modPack(2, "Space Age");
|
||||
const modPacks = new Map([[1, base], [2, space]]);
|
||||
|
||||
it("should use the mod pack set on the instance", function() {
|
||||
assert.deepEqual(instanceModPack(2, modPacks, base), { id: 2, modPack: space, isDefault: false });
|
||||
});
|
||||
it("should keep the id of a mod pack that is not known", function() {
|
||||
assert.deepEqual(instanceModPack(9, modPacks, base), { id: 9, modPack: undefined, isDefault: false });
|
||||
});
|
||||
it("should fall back to the controller default", function() {
|
||||
assert.deepEqual(instanceModPack(undefined, modPacks, base), { id: 1, modPack: base, isDefault: true });
|
||||
});
|
||||
it("should prefer the subscribed copy of the default mod pack", function() {
|
||||
const stale = modPack(1, "Old name");
|
||||
assert.equal(instanceModPack(undefined, modPacks, stale).modPack, base);
|
||||
assert.equal(instanceModPack(undefined, new Map(), stale).modPack, stale);
|
||||
});
|
||||
it("should mark the default as unknown when it is not available", function() {
|
||||
assert.deepEqual(instanceModPack(undefined, modPacks, undefined), { isDefault: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user