mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 07:24:52 +09:00
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:
5 files changed
+72
-7
No files matched your search
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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])
|
||||
));
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in new issue
Block a user