Merge pull request #956 from bbassie/feature/plugin-change-refresh

Reload the web interface when the controller's plugins change
This commit is contained in:
Cooldude2606 authored and GitHub committed 2026-08-10 16:57:30 +01:00
commit 15a8215a4f
5 files changed
+72 -7

No files matched your search

+6 -4
View File
@@ -11,6 +11,7 @@ import InputRole from "./components/InputRole";
import InputModPack from "./components/InputModPack";
import { InputTargetVersion, InputPartialVersion, InputFullVersion } from "./components/InputVersion";
import { Control, ControlConnector } from "./util/websocket";
import { loadedPluginSetKey } from "./util/pluginSet";
const { ConsoleTransport, WebConsoleFormat, logger } = lib;
@@ -29,7 +30,7 @@ async function loadScript(url: string) {
return result;
}
async function loadPluginInfos(): Promise<lib.PluginWebpackEnvInfo[]> {
async function loadPluginInfos(): Promise<[lib.PluginWebpackEnvInfo[], string]> {
let response = await fetch(`${webRoot}api/plugins`);
let pluginList: lib.PluginWebApi[];
if (response.ok) {
@@ -39,6 +40,7 @@ async function loadPluginInfos(): Promise<lib.PluginWebpackEnvInfo[]> {
logger.error("Failed to get plugin data, running without plugins");
pluginList = [];
}
let pluginSetKey = loadedPluginSetKey(pluginList);
let pluginInfos: lib.PluginWebpackEnvInfo[] = [];
await __webpack_init_sharing__("default");
@@ -73,7 +75,7 @@ async function loadPluginInfos(): Promise<lib.PluginWebpackEnvInfo[]> {
}
}
}
return pluginInfos;
return [pluginInfos, pluginSetKey];
}
async function loadPlugins(pluginInfos: lib.PluginWebpackEnvInfo[], control: Control) {
@@ -132,13 +134,13 @@ export default async function bootstrap() {
level: "verbose",
format: new WebConsoleFormat(),
}));
let pluginInfos = await loadPluginInfos();
let [pluginInfos, pluginSetKey] = await loadPluginInfos();
lib.registerPluginMessages(pluginInfos);
lib.addPluginConfigFields(pluginInfos);
let wsUrl = new URL(webRoot, document.location.href);
let controlConnector = new ControlConnector(wsUrl.href, 120);
let control = new Control(controlConnector, new Map(pluginInfos.map(p => [p.name, p])));
let control = new Control(controlConnector, new Map(pluginInfos.map(p => [p.name, p])), pluginSetKey);
control.plugins = await loadPlugins(pluginInfos, control);
control.inputComponents = inputComponentsFromPlugins(control.plugins);
+36 -2
View File
@@ -9,8 +9,9 @@ import ErrorBoundary from "./ErrorBoundary";
import SiteLayout from "./SiteLayout";
import ControlContext from "./ControlContext";
import LoginForm from "./LoginForm";
import { pluginSetKey } from "../util/pluginSet";
import { Card, ConfigProvider, Spin, Typography, theme } from "antd";
import { Button, Card, ConfigProvider, Spin, Typography, theme } from "antd";
const { Paragraph } = Typography;
@@ -28,11 +29,26 @@ function ErrorCard(props: ErrorProps) {
}
function PluginsChangedCard() {
return <div className="login-container">
<Card>
<h1>Reload required</h1>
<Paragraph>
The plugins the controller is running have changed since this page was opened.
So the page has been closed and must be reloaded to continue functioning as expected.
</Paragraph>
<Button type="primary" onClick={() => window.location.reload()}>Reload</Button>
</Card>
</div>;
}
type AppProps = {
control: Control;
};
export default function App(props: AppProps) {
let [connected, setConnected] = useState(false);
let [pluginsChanged, setPluginsChanged] = useState(false);
let [token, setToken] = useState(localStorage.getItem("controller_token") || null);
let connector = props.control.connector;
@@ -73,6 +89,21 @@ export default function App(props: AppProps) {
};
}, [token, props.control]);
// The controller reports the plugins it is running in the handshake
// This is used to detect when plugins change and a restart is required
useEffect(() => {
function onHello(data: any) {
if (pluginSetKey(data.plugins) !== props.control.pluginSetKey) {
setPluginsChanged(true);
}
}
connector.on("hello", onHello);
return () => {
connector.off("hello", onHello);
};
}, [props.control]);
useEffect(() => {
if (token && !connected) {
if (props.control.loggingOut) {
@@ -86,7 +117,10 @@ export default function App(props: AppProps) {
}, [token, connected]);
let page;
if (connected) {
if (pluginsChanged) {
page = <PluginsChangedCard />;
} else if (connected) {
page = <SiteLayout/>;
} else if (token) {
@@ -88,11 +88,14 @@ export default function PluginsPage() {
if (!plugin.meta.enabled) {
return <><InfoCircleFilled style={{ color: "#1668dc" }} /> Disabled on controller</>;
}
if (!plugin.meta.web.error && !control.pluginInfos.has(plugin.meta.name)) {
return <><InfoCircleFilled style={{ color: "#1668dc" }} /> Reload page to load</>;
}
if (!plugin.package) {
return <><CloseCircleFilled style={{ color: "#dc4446" }} /> Error loading module</>;
}
if (plugin.package.version !== plugin.meta.version) {
return "Version missmatched";
return <><CloseCircleFilled style={{ color: "#dc4446" }} /> Version mis-matched</>;
}
return plugin.package.version;
},
+24
View File
@@ -0,0 +1,24 @@
import type { PluginWebApi } from "@clusterio/lib";
/**
* Canonical key for a set of plugins loaded on the controller.
* It is assumed that all plugins provided are currently loaded.
*
* @param plugins - name to version mapping of loaded plugins.
* @returns value comparable with === against another key.
*/
export function pluginSetKey(plugins: Record<string, string>) {
return JSON.stringify(Object.entries(plugins).sort(([a], [b]) => a.localeCompare(b)));
}
/**
* Canonical key for the plugins loaded when this page was started
*
* @param pluginList - plugin list as served by /api/plugins.
* @returns value comparable with === against a key from the handshake.
*/
export function loadedPluginSetKey(pluginList: PluginWebApi[]) {
return pluginSetKey(Object.fromEntries(
pluginList.filter(plugin => plugin.loaded).map(plugin => [plugin.name, plugin.version])
));
}
+2
View File
@@ -80,6 +80,8 @@ export class Control extends lib.Link {
constructor(
connector: ControlConnector,
public pluginInfos = new Map<string, lib.PluginWebpackEnvInfo>(),
/** Key of the plugin set this interface was loaded against */
public pluginSetKey = "",
) {
super(connector);