diff --git a/packages/web_ui/src/bootstrap.tsx b/packages/web_ui/src/bootstrap.tsx index b4954413..8ef588bd 100644 --- a/packages/web_ui/src/bootstrap.tsx +++ b/packages/web_ui/src/bootstrap.tsx @@ -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 { +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 { 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 { } } } - 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); diff --git a/packages/web_ui/src/components/App.tsx b/packages/web_ui/src/components/App.tsx index e4dcfdc6..3b69114b 100644 --- a/packages/web_ui/src/components/App.tsx +++ b/packages/web_ui/src/components/App.tsx @@ -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
+ +

Reload required

+ + 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. + + +
+
; +} + + 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 = ; + + } else if (connected) { page = ; } else if (token) { diff --git a/packages/web_ui/src/components/PluginsPage.tsx b/packages/web_ui/src/components/PluginsPage.tsx index 399a92de..16a0dc7d 100644 --- a/packages/web_ui/src/components/PluginsPage.tsx +++ b/packages/web_ui/src/components/PluginsPage.tsx @@ -88,11 +88,14 @@ export default function PluginsPage() { if (!plugin.meta.enabled) { return <> Disabled on controller; } + if (!plugin.meta.web.error && !control.pluginInfos.has(plugin.meta.name)) { + return <> Reload page to load; + } if (!plugin.package) { return <> Error loading module; } if (plugin.package.version !== plugin.meta.version) { - return "Version missmatched"; + return <> Version mis-matched; } return plugin.package.version; }, diff --git a/packages/web_ui/src/util/pluginSet.ts b/packages/web_ui/src/util/pluginSet.ts new file mode 100644 index 00000000..76ef9406 --- /dev/null +++ b/packages/web_ui/src/util/pluginSet.ts @@ -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) { + 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]) + )); +} diff --git a/packages/web_ui/src/util/websocket.ts b/packages/web_ui/src/util/websocket.ts index 44069956..b4afb44c 100644 --- a/packages/web_ui/src/util/websocket.ts +++ b/packages/web_ui/src/util/websocket.ts @@ -80,6 +80,8 @@ export class Control extends lib.Link { constructor( connector: ControlConnector, public pluginInfos = new Map(), + /** Key of the plugin set this interface was loaded against */ + public pluginSetKey = "", ) { super(connector);