Files
factorio-clusterio/packages/web_ui/src/components/PluginViewPage.tsx
T
Danielv123 2e1710d3a9 Show the same page for missing, deleted and unloadable resources
Every view page handled a resource that can't be shown its own way, and
users without the subscribe permission were left on a spinner forever.

Adds useResourceState and a ResourceState component, used by the host,
instance, mod pack, icon reference, user, role and plugin pages and
exported for plugin pages. The single item hooks now also return the
subscription error.

Resolves #1165
2026-11-01 17:40:22 +01:00

138 lines
5.3 KiB
TypeScript

import React, { useEffect, useContext, useState } from "react";
import { useParams } from "react-router-dom";
import { Alert, Descriptions } from "antd";
import * as lib from "@clusterio/lib";
import type { PluginWebApi } from "@clusterio/lib";
import ControlContext from "./ControlContext";
import PageLayout from "./PageLayout";
import ResourceState from "./ResourceState";
import SectionHeader from "./SectionHeader";
import useResourceState from "../util/useResourceState";
import { usePluginInstaller } from "./PluginInstall";
import { PluginMachinesTable, PluginUpdateButton, useLatestVersions, useMachinePlugins } from "./PluginClusterStatus";
import { PluginStatus, computePluginStatuses, pluginStatusKey } from "../util/plugin_status";
type PluginMachinesProps = {
status: PluginStatus,
title: string,
refresh: (addresses: lib.AddressShorthand[]) => void,
};
function PluginMachines(props: PluginMachinesProps) {
const installer = usePluginInstaller(props.refresh);
const { npmPackage } = props.status;
const latestVersions = useLatestVersions(npmPackage ? [npmPackage] : []);
const latest = npmPackage ? latestVersions[npmPackage] : undefined;
return <>
<Descriptions bordered size="small" style={{ marginTop: "1em" }}>
<Descriptions.Item label="Package" span={npmPackage ? 1 : 3}>
{npmPackage ?? "Not published to npm"}
</Descriptions.Item>
{npmPackage && <Descriptions.Item label="Latest version" span={2}>
{latest ?? "Unknown"}
</Descriptions.Item>}
</Descriptions>
<SectionHeader
title="Machines"
extra={<PluginUpdateButton
status={props.status} title={props.title} latest={latest} installer={installer}
/>}
/>
<PluginMachinesTable status={props.status} latest={latest} installer={installer} />
</>;
}
type ControllerPluginProps = {
pluginMeta: PluginWebApi,
pluginInfo?: lib.PluginWebEnvInfo,
title: string,
};
function ControllerPlugin(props: ControllerPluginProps) {
const { pluginMeta, pluginInfo } = props;
const pkg = pluginInfo?.package;
const pluginError = pluginMeta.enabled ? pluginMeta.web.error ?? pluginInfo?.error : undefined;
const homepageUrl = lib.safeExternalUrl(pkg?.homepage);
return <>
<Descriptions bordered size="small" title={props.title}>
<Descriptions.Item label="Version" span={3}>{pluginMeta.version}</Descriptions.Item>
{pluginInfo?.description ? <Descriptions.Item label="Description" span={3}>
{pluginInfo.description}
</Descriptions.Item> : null}
{pkg?.homepage ? <Descriptions.Item label="Homepage" span={3}>
{homepageUrl ? <a href={homepageUrl}>{pkg.homepage}</a> : pkg.homepage}
</Descriptions.Item> : null}
{pkg?.author ? <Descriptions.Item label="Author" span={3}>{pkg.author}</Descriptions.Item> : null}
{pkg?.license ? <Descriptions.Item label="License" span={3}>{pkg.license}</Descriptions.Item> : null}
</Descriptions>
{pluginError && <Alert
style={{
marginTop: "1em",
}}
message={pluginError}
description={
"The web interface was unable to load the web module for this plugin. This is "+
"usually due to an incorrect or missing web build for the plugin. Due to the "+
"module not being loaded, the configs and web controls defined by this plugin may "+
"not be available in the web interface."
}
type="error"
showIcon
/>}
</>;
}
export default function PluginViewPage() {
let params = useParams();
const control = useContext(ControlContext);
let [pluginList, setPluginList] = useState<PluginWebApi[]>();
const [pluginListError, setPluginListError] = useState<Error | null>(null);
let pluginName = String(params.name);
const { machines, unknown, refresh, error: machinesError } = useMachinePlugins();
useEffect(() => {
(async () => {
let response = await fetch(`${webRoot}api/plugins`);
if (!response.ok) {
throw new Error(`Failed to load plugin list: ${response.status} ${response.statusText}`);
}
setPluginList(await response.json());
})().catch(setPluginListError);
}, []);
const pluginMeta = pluginList?.find(p => p.name === pluginName);
// Plugins the controller hasn't loaded are linked to by their npm package or name on the hosts
const status = machines
? computePluginStatuses(machines, unknown).get(pluginMeta ? pluginStatusKey(pluginMeta) : pluginName)
: undefined;
const pluginInfo = control.pluginInfos.get(pluginName);
const pluginTitle = pluginInfo?.title ?? status?.title ?? pluginName;
// Machines are only needed for plugins not on the controller, which isn't known until the list has loaded
const error = pluginListError ?? (pluginList ? machinesError : null);
const state = useResourceState(
pluginList && (pluginMeta ?? status), Boolean(pluginList && machines), error, pluginName
);
let nav = [{ name: "Plugins", path: "/plugins" }, { name: pluginTitle }];
if (state !== "ok") {
return <ResourceState
state={state} type="Plugin" id={pluginTitle} nav={nav} listPath="/plugins" error={error}
/>;
}
return <PageLayout nav={nav}>
{pluginMeta
? <ControllerPlugin
pluginMeta={pluginMeta}
pluginInfo={pluginInfo}
title={pluginTitle}
/>
: <Descriptions bordered size="small" title={pluginTitle}>
<Descriptions.Item label="Loaded on controller">No</Descriptions.Item>
</Descriptions>}
{status && <PluginMachines status={status} title={pluginTitle} refresh={refresh} />}
</PageLayout>;
}