mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 15:14:52 +09:00
Merge pull request #367 from Hornwitser/player-auth-plugin-preparations
Web Plugin preparations
This commit is contained in:
19 files changed
+313
-105
No files matched your search
@@ -423,6 +423,68 @@ class BaseControlPlugin {
|
||||
async addCommands(rootCommand) { }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Plugin supplied login form
|
||||
* @typedef {Object} module:lib/plugin~LoginForm
|
||||
* @property {string} name -
|
||||
* Internal name of the login form, this should start with the
|
||||
* plugin name followed by a dot.
|
||||
* @property {string} title -
|
||||
* Name displayed above this form in the login window.
|
||||
* @property {React.Component} Component -
|
||||
* React component that's rendered for this login form. This is
|
||||
* supplied the setToken function via its props which should be
|
||||
* called when an authentication token is aquired via this
|
||||
* form.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base class for web interface plugins
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
class BaseWebPlugin {
|
||||
constructor(container, packageData, info, logger) {
|
||||
/**
|
||||
* Webpack container for this plugin
|
||||
*/
|
||||
this.container = container;
|
||||
|
||||
/**
|
||||
* Contents of the plugin's package.json file
|
||||
* @type {Object}
|
||||
*/
|
||||
this.package = packageData;
|
||||
|
||||
/**
|
||||
* The plugin's own info module
|
||||
*/
|
||||
this.info = info;
|
||||
|
||||
/**
|
||||
* Logger for this plugin
|
||||
*
|
||||
* Instance of winston Logger for sending log messages from this
|
||||
* plugin. Supported methods and their corresponding log levels are
|
||||
* `error`, `warn`, `audit`, `info` and `verbose`.
|
||||
*/
|
||||
this.logger = logger.child({ plugin: this.info.name });
|
||||
|
||||
/**
|
||||
* List of login forms provided by this plugin
|
||||
*
|
||||
* @type {Array<module:lib/plugin~LoginForm>}
|
||||
*/
|
||||
this.loginForms = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Called immediately after the class is instantiated.
|
||||
*/
|
||||
async init() { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach plugin messages
|
||||
*
|
||||
@@ -489,6 +551,7 @@ module.exports = {
|
||||
BaseInstancePlugin,
|
||||
BaseMasterPlugin,
|
||||
BaseControlPlugin,
|
||||
BaseWebPlugin,
|
||||
|
||||
attachPluginMessages,
|
||||
invokeHook,
|
||||
|
||||
+60
-27
@@ -41,8 +41,10 @@ let loadedPlugins = {};
|
||||
let devMiddleware;
|
||||
let clusterLogger;
|
||||
let db = {
|
||||
instances: new Map(),
|
||||
slaves: new Map(),
|
||||
instances: null,
|
||||
slaves: null,
|
||||
roles: null,
|
||||
users: null,
|
||||
};
|
||||
let slaveConnections = new Map();
|
||||
let controlConnections = [];
|
||||
@@ -78,18 +80,6 @@ let httpServerCloser;
|
||||
let httpsServer;
|
||||
let httpsServerCloser;
|
||||
|
||||
app.use(cookieParser());
|
||||
app.use(bodyParser.json({
|
||||
limit: "10mb",
|
||||
}));
|
||||
app.use(bodyParser.urlencoded({
|
||||
parameterLimit: 100000,
|
||||
limit: "10mb",
|
||||
extended: true,
|
||||
}));
|
||||
app.use(fileUpload());
|
||||
app.use(compression());
|
||||
|
||||
|
||||
// Servers the web interface with the root path set apropriately.
|
||||
function serveWeb(route) {
|
||||
@@ -110,10 +100,6 @@ function serveWeb(route) {
|
||||
};
|
||||
}
|
||||
|
||||
// Set folder to serve static content from (the website)
|
||||
app.use(express.static(path.join(__dirname, "dist", "web")));
|
||||
app.use(express.static("static")); // Used for data export files
|
||||
|
||||
const slaveMappingGauge = new libPrometheus.Gauge(
|
||||
"clusterio_master_slave_mapping",
|
||||
"Mapping of Slave ID to name",
|
||||
@@ -149,6 +135,11 @@ const instanceMappingGauge = new libPrometheus.Gauge(
|
||||
}
|
||||
);
|
||||
|
||||
const hitCounter = new libPrometheus.Counter(
|
||||
"clusterio_master_http_hits_total",
|
||||
"How many HTTP requests in total have been received"
|
||||
);
|
||||
|
||||
const endpointHitCounter = new libPrometheus.Counter(
|
||||
"clusterio_master_http_endpoint_hits_total",
|
||||
"How many requests a particular HTTP endpoint has gotten",
|
||||
@@ -181,6 +172,28 @@ const wsActiveSlavesGauge = new libPrometheus.Gauge(
|
||||
"How many slaves are currently connected to the master"
|
||||
);
|
||||
|
||||
|
||||
app.use((req, res, next) => {
|
||||
hitCounter.inc();
|
||||
next();
|
||||
});
|
||||
app.use(cookieParser());
|
||||
app.use(bodyParser.json({
|
||||
limit: "10mb",
|
||||
}));
|
||||
app.use(bodyParser.urlencoded({
|
||||
parameterLimit: 100000,
|
||||
limit: "10mb",
|
||||
extended: true,
|
||||
}));
|
||||
app.use(fileUpload());
|
||||
app.use(compression());
|
||||
|
||||
// Set folder to serve static content from (the website)
|
||||
app.use(express.static(path.join(__dirname, "dist", "web")));
|
||||
app.use(express.static("static")); // Used for data export files
|
||||
|
||||
|
||||
// Merges samples from sourceResult to destinationResult
|
||||
function mergeSamples(destinationResult, sourceResult) {
|
||||
// Merge metrics received by multiple slaves
|
||||
@@ -275,7 +288,8 @@ app.get("/api/plugins", (req, res) => {
|
||||
for (let pluginInfo of pluginInfos) {
|
||||
let name = pluginInfo.name;
|
||||
let loaded = masterPlugins.has(name);
|
||||
plugins.push({ name, version: pluginInfo.version, loaded });
|
||||
let enabled = loaded && masterConfig.group(pluginInfo.name).get("load_plugin");
|
||||
plugins.push({ name, version: pluginInfo.version, enabled, loaded });
|
||||
}
|
||||
res.send(plugins);
|
||||
});
|
||||
@@ -488,6 +502,14 @@ async function shutdown() {
|
||||
logger.info("Saving configs");
|
||||
await fs.outputFile(masterConfigPath, JSON.stringify(masterConfig.serialize(), null, 4));
|
||||
|
||||
if (devMiddleware) {
|
||||
await new Promise((resolve, reject) => { devMiddleware.close(resolve); });
|
||||
}
|
||||
|
||||
if (!db.slaves) {
|
||||
return;
|
||||
}
|
||||
|
||||
await saveMap(masterConfig.get("master.database_directory"), "slaves.json", db.slaves);
|
||||
await saveInstances(masterConfig.get("master.database_directory"), "instances.json", db.instances);
|
||||
await saveUsers(masterConfig.get("master.database_directory"), "users.json");
|
||||
@@ -496,10 +518,6 @@ async function shutdown() {
|
||||
|
||||
stopAcceptingNewSessions = true;
|
||||
|
||||
if (devMiddleware) {
|
||||
await new Promise((resolve, reject) => { devMiddleware.close(resolve); });
|
||||
}
|
||||
|
||||
let disconnectTasks = [];
|
||||
for (let controlConnection of controlConnections) {
|
||||
controlConnection.connector.setTimeout(masterConfig.get("master.connector_shutdown_timeout"));
|
||||
@@ -1861,6 +1879,7 @@ async function initialize() {
|
||||
})
|
||||
.command("run", "Run master server", yargs => {
|
||||
yargs.option("dev", { hidden: true, type: "boolean", nargs: 0 });
|
||||
yargs.option("dev-plugin", { hidden: true, type: "array" });
|
||||
})
|
||||
.demandCommand(1, "You need to specify a command to run")
|
||||
.strict()
|
||||
@@ -1953,15 +1972,29 @@ async function initialize() {
|
||||
|
||||
async function startServer(args) {
|
||||
// Start webpack development server if enabled
|
||||
if (args.dev) {
|
||||
if (args.dev || args.devPlugin) {
|
||||
logger.warn("Webpack development mode enabled");
|
||||
/* eslint-disable global-require */
|
||||
const webpack = require("webpack");
|
||||
const webpackDevMiddleware = require("webpack-dev-middleware");
|
||||
const webpackConfig = require("./webpack.config");
|
||||
const webpackConfigs = [];
|
||||
if (args.dev) {
|
||||
webpackConfigs.push(require("./webpack.config"));
|
||||
}
|
||||
if (args.devPlugin) {
|
||||
for (let name of args.devPlugin) {
|
||||
let info = pluginInfos.find(i => i.name === name);
|
||||
if (!info) {
|
||||
throw new libErrors.StartupError(`No plugin named ${name}`);
|
||||
}
|
||||
webpackConfigs.push(
|
||||
require(path.posix.join(info.requirePath, "webpack.config"))
|
||||
);
|
||||
}
|
||||
}
|
||||
/* eslint-enable global-require */
|
||||
|
||||
const compiler = webpack(webpackConfig({}));
|
||||
const compiler = webpack(webpackConfigs.map(config => config({})));
|
||||
devMiddleware = webpackDevMiddleware(compiler, {});
|
||||
app.use(devMiddleware);
|
||||
}
|
||||
@@ -2033,7 +2066,7 @@ async function startServer(args) {
|
||||
|
||||
let pluginPackagePath = require.resolve(path.posix.join(pluginInfo.requirePath, "package.json"));
|
||||
let webPath = path.join(path.dirname(pluginPackagePath), "dist", "web");
|
||||
app.use(`/plugin/${pluginInfo.name}`, express.static(webPath));
|
||||
app.use(`/plugins/${pluginInfo.name}`, express.static(webPath));
|
||||
}
|
||||
|
||||
// Load plugins
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-dev-middleware": "^3.7.2",
|
||||
"webpack-dev-middleware": "^4.1.0",
|
||||
"webpack-merge": "^5.2.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Clusterio</title>
|
||||
<script>
|
||||
window.webRoot = "__WEB_ROOT__";
|
||||
window.webRoot = new URL("__WEB_ROOT__", document.location).pathname;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
import("@clusterio/web_ui/src/bootstrap");
|
||||
import("@clusterio/web_ui").then(webUi => {
|
||||
return webUi.bootstrap();
|
||||
}).catch(console.error);
|
||||
@@ -12,6 +12,7 @@ module.exports = (env = {}) => merge(common(env), {
|
||||
contentBase: "./dist/web",
|
||||
},
|
||||
output: {
|
||||
publicPath: "auto",
|
||||
filename: "bundle.js",
|
||||
path: path.resolve(__dirname, "dist", "web"),
|
||||
},
|
||||
@@ -28,7 +29,6 @@ module.exports = (env = {}) => merge(common(env), {
|
||||
"@clusterio/lib/schema": { singleton: true },
|
||||
"@clusterio/lib/users": { singleton: true },
|
||||
"@clusterio/web_ui": { singleton: true },
|
||||
"ajv": {},
|
||||
"antd": { singleton: true },
|
||||
"react": { singleton: true },
|
||||
"react-dom": { singleton: true },
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.4",
|
||||
"browser": "src/index.jsx",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
// Calculate basedir of application based on current location and
|
||||
// the provided web_root from master server.
|
||||
const location = new URL(window.webRoot, document.location);
|
||||
const basename = location.pathname !== "/" ? location.pathname : "";
|
||||
|
||||
export default basename;
|
||||
@@ -4,6 +4,7 @@ import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import libConfig from "@clusterio/lib/config";
|
||||
import libPlugin from "@clusterio/lib/plugin";
|
||||
import { ConsoleTransport, WebConsoleFormat, logger } from "@clusterio/lib/logging";
|
||||
|
||||
import App from "./components/App";
|
||||
@@ -36,51 +37,57 @@ async function loadPlugins() {
|
||||
pluginList = [];
|
||||
}
|
||||
|
||||
let plugins = [];
|
||||
let plugins = new Map();
|
||||
await __webpack_init_sharing__("default");
|
||||
for (let meta of pluginList) {
|
||||
try {
|
||||
await loadScript(`${webRoot}plugin/${meta.name}/remoteEntry.js`);
|
||||
await loadScript(`${webRoot}plugins/${meta.name}/remoteEntry.js`);
|
||||
let container = window[`plugin_${meta.name}`];
|
||||
if (!container) {
|
||||
throw new Error(`Plugin did not expose its container via plugin_${meta.name}`);
|
||||
}
|
||||
await container.init(__webpack_share_scopes__.default);
|
||||
let pluginInfo = (await container.get("./info"))();
|
||||
let pluginPackage = (await container.get("./package.json"))();
|
||||
plugins.push({
|
||||
meta,
|
||||
info: pluginInfo,
|
||||
package: pluginPackage,
|
||||
container,
|
||||
});
|
||||
|
||||
let WebPluginClass = libPlugin.BaseWebPlugin;
|
||||
if (meta.enabled && pluginInfo.webEntrypoint) {
|
||||
let webModule = (await container.get(pluginInfo.webEntrypoint))();
|
||||
if (!webModule.WebPlugin) {
|
||||
throw new Error("Plugin webEntrypoint does not export WebPlugin class");
|
||||
}
|
||||
WebPluginClass = webModule.WebPlugin;
|
||||
}
|
||||
|
||||
let plugin = new WebPluginClass(container, pluginPackage, pluginInfo, logger);
|
||||
await plugin.init();
|
||||
plugins.set(pluginInfo.name, plugin);
|
||||
|
||||
} catch (err) {
|
||||
logger.error(`Failed to load plugin info for ${meta.name}`);
|
||||
if (err) {
|
||||
logger.error(`Failed to load plugin ${meta.name}`);
|
||||
if (err.stack) {
|
||||
logger.error(err.stack);
|
||||
}
|
||||
plugins.push({
|
||||
meta,
|
||||
});
|
||||
}
|
||||
}
|
||||
return plugins;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
export default async function bootstrap() {
|
||||
logger.add(new ConsoleTransport({
|
||||
level: "verbose",
|
||||
format: new WebConsoleFormat(),
|
||||
}));
|
||||
let plugins = await loadPlugins();
|
||||
libConfig.registerPluginConfigGroups(plugins.filter(p => p.info).map(p => p.info));
|
||||
let pluginInfos = [...plugins.values()].map(p => p.info);
|
||||
libConfig.registerPluginConfigGroups(pluginInfos);
|
||||
libConfig.finalizeConfigs();
|
||||
|
||||
let wsUrl = new URL(window.webRoot, document.location);
|
||||
wsUrl.protocol = wsUrl.protocol.replace("http", "ws");
|
||||
|
||||
let controlConnector = new ControlConnector(wsUrl, 10);
|
||||
let control = new Control(controlConnector, []);
|
||||
let control = new Control(controlConnector, plugins);
|
||||
|
||||
ReactDOM.render(<App control={control} plugins={plugins}/>, document.getElementById("root"));
|
||||
}
|
||||
|
||||
load().catch((err) => logger.fatal(err.stack));
|
||||
@@ -4,14 +4,25 @@ import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { logger } from "@clusterio/lib/logging";
|
||||
|
||||
import basename from "../basename";
|
||||
import ErrorBoundary from "./ErrorBoundary";
|
||||
import SiteLayout from "./SiteLayout";
|
||||
import ControlContext from "./ControlContext";
|
||||
import PluginsContext from "./PluginsContext";
|
||||
import LoginForm from "./LoginForm";
|
||||
|
||||
import { Card, Spin } from "antd";
|
||||
import { Card, Spin, Typography } from "antd";
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
|
||||
function ErrorCard(props) {
|
||||
return <div className="login-container">
|
||||
<Card>
|
||||
<h1>An unexpected error occured</h1>
|
||||
<Paragraph code className="error-traceback">{props.error.stack}</Paragraph>
|
||||
</Card>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default function App(props) {
|
||||
let [connected, setConnected] = useState(false);
|
||||
@@ -69,20 +80,19 @@ export default function App(props) {
|
||||
|
||||
} else {
|
||||
page = <div className="login-container">
|
||||
<Card>
|
||||
<h1>Clusterio</h1>
|
||||
<LoginForm setToken={setToken} />
|
||||
</Card>
|
||||
<LoginForm setToken={setToken} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ControlContext.Provider value={props.control}>
|
||||
<PluginsContext.Provider value={props.plugins}>
|
||||
<BrowserRouter basename={basename}>
|
||||
{page}
|
||||
</BrowserRouter>
|
||||
</PluginsContext.Provider>
|
||||
</ControlContext.Provider>
|
||||
<ErrorBoundary Component={ErrorCard}>
|
||||
<ControlContext.Provider value={props.control}>
|
||||
<PluginsContext.Provider value={props.plugins}>
|
||||
<BrowserRouter basename={webRoot}>
|
||||
{page}
|
||||
</BrowserRouter>
|
||||
</PluginsContext.Provider>
|
||||
</ControlContext.Provider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +1,61 @@
|
||||
import React from "react";
|
||||
import { Form, Input, Button } from "antd";
|
||||
import React, { Fragment, useContext } from "react";
|
||||
import { Button, Card, Divider, Form, Image, Input, List, Space, Spin, Typography } from "antd";
|
||||
import LockOutlined from "@ant-design/icons/LockOutlined";
|
||||
|
||||
export default function LoginForm(props) {
|
||||
return (
|
||||
import logo from "../images/logo.png";
|
||||
import PluginsContext from "./PluginsContext";
|
||||
|
||||
const { Paragraph, Text } = Typography;
|
||||
|
||||
|
||||
function TokenAuth(props) {
|
||||
return <>
|
||||
<Paragraph>If you have generated an authentication token from the master server, use that here.</Paragraph>
|
||||
<Form
|
||||
name="login"
|
||||
onFinish={(values) => { props.setToken(values.token); }}
|
||||
layout="inline"
|
||||
>
|
||||
<Form.Item
|
||||
name="token"
|
||||
rules={[
|
||||
{ required: true, message: "Please input authentication token" },
|
||||
]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<LockOutlined className="site-form-item-icon" />}
|
||||
style={{ width: "14em" }}
|
||||
prefix=<LockOutlined className="site-form-item-icon" />
|
||||
type="password"
|
||||
placeholder="Authentication Token"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit">Log in</Button>
|
||||
</Form>
|
||||
);
|
||||
</>;
|
||||
}
|
||||
|
||||
export default function LoginForm(props) {
|
||||
let plugins = useContext(PluginsContext);
|
||||
|
||||
let loginForms = [];
|
||||
for (let plugin of plugins.values()) {
|
||||
loginForms.push(...plugin.loginForms);
|
||||
}
|
||||
|
||||
loginForms.push({
|
||||
name: "core.token",
|
||||
title: "Token",
|
||||
Component: TokenAuth,
|
||||
});
|
||||
|
||||
return <Card style={{ maxWidth: "30em" }}>
|
||||
<Space style={{ width: "100%" }} direction="vertical" align="center">
|
||||
<h1>Clusterio</h1>
|
||||
<Image width={128} height={123} src={logo} />
|
||||
Log in to the cluster using one of the methods below.
|
||||
</Space>
|
||||
{loginForms.map(form => <Fragment key={form.name}>
|
||||
<Divider orientation="center">{form.title}</Divider>
|
||||
<form.Component setToken={props.setToken} />
|
||||
</Fragment>)}
|
||||
</Card>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useContext, useState } from "react";
|
||||
import { useHistory, useParams } from "react-router-dom";
|
||||
import { Alert, Descriptions } from "antd";
|
||||
import { Alert, Descriptions, Spin } from "antd";
|
||||
|
||||
import PluginsContext from "./PluginsContext";
|
||||
import PageLayout from "./PageLayout";
|
||||
@@ -10,27 +10,43 @@ import { notifyErrorHandler } from "../util/notify";
|
||||
export default function PluginViewPage() {
|
||||
let params = useParams();
|
||||
let plugins = useContext(PluginsContext);
|
||||
let [pluginList, setPluginList] = useState(null);
|
||||
let pluginName = params.name;
|
||||
|
||||
let plugin = plugins.find(p => p.meta.name === pluginName);
|
||||
let pluginTitle = plugin.info ? plugin.info.title : plugin.meta.name;
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
let response = await fetch(`${webRoot}api/plugins`);
|
||||
if (response.ok) {
|
||||
setPluginList(await response.json());
|
||||
} else {
|
||||
notify("Failed to load plugin list");
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
let plugin = plugins.get(pluginName);
|
||||
let pluginTitle = plugin ? plugin.info.title : pluginName;
|
||||
let nav = [{ name: "Plugins", path: "/plugins" }, { name: pluginTitle }];
|
||||
if (!plugin) {
|
||||
if (!pluginList) {
|
||||
return <PageLayout nav={nav}>
|
||||
<Descriptions borderd size="small" title={pluginTitle} />
|
||||
<Spin size="large" />
|
||||
</PageLayout>;
|
||||
}
|
||||
|
||||
let pluginMeta = pluginList.find(p => p.name === pluginName);
|
||||
if (!pluginMeta) {
|
||||
return <PageLayout nav={nav}>
|
||||
<h2>Plugin not found</h2>
|
||||
<p>Plugin with name {pluginName} was not found on the master server.</p>
|
||||
</PageLayout>;
|
||||
}
|
||||
|
||||
if (!plugin.info) {
|
||||
if (!plugin) {
|
||||
return <PageLayout nav={nav}>
|
||||
<Descriptions
|
||||
bordered
|
||||
size="small"
|
||||
title={pluginTitle}
|
||||
>
|
||||
<Descriptions.Item label="Version">{plugin.meta.version}</Descriptions.Item>
|
||||
<Descriptions.Item label="Loaded">{plugin.meta.loaded ? "Yes" : "No"}</Descriptions.Item>
|
||||
<Descriptions bordered size="small" title={pluginTitle}>
|
||||
<Descriptions.Item label="Version">{pluginMeta.version}</Descriptions.Item>
|
||||
<Descriptions.Item label="Loaded">{pluginMeta.loaded ? "Yes" : "No"}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Alert
|
||||
style={{
|
||||
@@ -50,13 +66,9 @@ export default function PluginViewPage() {
|
||||
}
|
||||
|
||||
return <PageLayout nav={nav}>
|
||||
<Descriptions
|
||||
bordered
|
||||
size="small"
|
||||
title={pluginTitle}
|
||||
>
|
||||
<Descriptions.Item label="Version">{plugin.meta.version}</Descriptions.Item>
|
||||
<Descriptions.Item label="Loaded" span={2}>{plugin.meta.loaded ? "Yes" : "No"}</Descriptions.Item>
|
||||
<Descriptions bordered size="small" title={pluginTitle}>
|
||||
<Descriptions.Item label="Version">{pluginMeta.version}</Descriptions.Item>
|
||||
<Descriptions.Item label="Loaded" span={2}>{pluginMeta.loaded ? "Yes" : "No"}</Descriptions.Item>
|
||||
<Descriptions.Item label="Description" span={3}>{plugin.info.description}</Descriptions.Item>
|
||||
{plugin.package.homepage ? <Descriptions.Item label="Homepage" span={3}>
|
||||
<a href={plugin.package.homepage}>{plugin.package.homepage}</a>
|
||||
@@ -66,14 +78,14 @@ export default function PluginViewPage() {
|
||||
</Descriptions.Item> : null}
|
||||
<Descriptions.Item label="License" span={3}>{plugin.package.license}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{plugin.meta.version !== plugin.package.version ? <Alert
|
||||
{pluginMeta.version !== plugin.package.version ? <Alert
|
||||
style={{
|
||||
marginTop: "1em",
|
||||
}}
|
||||
message="Version missmatch detected"
|
||||
description={
|
||||
`The version of the web interface module for this plugin (${plugin.package.version}) `+
|
||||
`does not match the version running on the master server (${plugin.meta.version}), `+
|
||||
`does not match the version running on the master server (${pluginMeta.version}), `+
|
||||
"spurious errors may occur. This usually happens when the plugin is updated but the "+
|
||||
"master server has not been restarted yet, but may also be due to an outdated build."
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useContext } from "react";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Table } from "antd";
|
||||
|
||||
import notify from "../util/notify";
|
||||
import PluginsContext from "./PluginsContext";
|
||||
import PageLayout from "./PageLayout";
|
||||
|
||||
@@ -9,6 +10,34 @@ import PageLayout from "./PageLayout";
|
||||
export default function PluginsPage() {
|
||||
let plugins = useContext(PluginsContext);
|
||||
let history = useHistory();
|
||||
let [pluginList, setPluginList] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
let response = await fetch(`${webRoot}api/plugins`);
|
||||
if (response.ok) {
|
||||
setPluginList(await response.json());
|
||||
} else {
|
||||
notify("Failed to load plugin list");
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
let tableContents = [];
|
||||
for (let meta of pluginList) {
|
||||
if (plugins.has(meta.name)) {
|
||||
let plugin = plugins.get(meta.name);
|
||||
tableContents.push({
|
||||
meta,
|
||||
info: plugin.info,
|
||||
package: plugin.package,
|
||||
});
|
||||
} else {
|
||||
tableContents.push({
|
||||
meta,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return <PageLayout nav={[{ name: "Plugins" }]}>
|
||||
<h2>Plugins</h2>
|
||||
@@ -39,7 +68,7 @@ export default function PluginsPage() {
|
||||
responsive: ["sm"],
|
||||
},
|
||||
]}
|
||||
dataSource={plugins}
|
||||
dataSource={tableContents}
|
||||
rowKey={plugin => plugin.meta.name}
|
||||
pagination={false}
|
||||
onRow={plugin => ({
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export { default as bootstrap } from "./bootstrap.jsx";
|
||||
export { default as notify, notifyErrorHandler } from "./util/notify.jsx";
|
||||
|
||||
export * from "./model/instance.jsx";
|
||||
export * from "./model/slave.jsx";
|
||||
|
||||
|
||||
export { default as ControlContext } from "./components/ControlContext";
|
||||
export { default as PluginsContext } from "./components/PluginsContext";
|
||||
|
||||
export { default as BaseConfigTree } from "./components/BaseConfigTree";
|
||||
export { default as InstanceConfigTree } from "./components/InstanceConfigTree";
|
||||
export { default as MasterConfigTree } from "./components/MasterConfigTree";
|
||||
|
||||
export { default as AssignInstanceModal } from "./components/AssignInstanceModal";
|
||||
export { default as InstanceConsole } from "./components/InstanceConsole";
|
||||
export { default as InstanceRcon } from "./components/InstanceRcon";
|
||||
|
||||
export { default as PageLayout } from "./components/PageLayout";
|
||||
@@ -36,17 +36,17 @@ export class ControlConnector extends libLink.WebSocketClientConnector {
|
||||
* @static
|
||||
*/
|
||||
export class Control extends libLink.Link {
|
||||
constructor(connector, controlPlugins) {
|
||||
constructor(connector, plugins) {
|
||||
super("control", "master", connector);
|
||||
libLink.attachAllMessages(this);
|
||||
|
||||
/**
|
||||
* Mapping of plugin names to their instance for loaded plugins.
|
||||
* @type {Map<string, module:lib/plugin.BaseControlPlugin>}
|
||||
* @type {Map<string, module:lib/plugin.BaseWebPlugin>}
|
||||
*/
|
||||
this.plugins = controlPlugins;
|
||||
for (let controlPlugin of controlPlugins.values()) {
|
||||
libPlugin.attachPluginMessages(this, controlPlugin.info, controlPlugin);
|
||||
this.plugins = plugins;
|
||||
for (let plugin of plugins.values()) {
|
||||
libPlugin.attachPluginMessages(this, plugin);
|
||||
}
|
||||
|
||||
this.instanceLogHandlers = new Map();
|
||||
|
||||
@@ -9,6 +9,7 @@ module.exports = (env = {}) => merge(common(env), {
|
||||
context: __dirname,
|
||||
entry: "./web/index.jsx",
|
||||
output: {
|
||||
publicPath: "auto",
|
||||
filename: "bundle.js",
|
||||
path: path.resolve(__dirname, "dist", "web"),
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ module.exports = (env = {}) => merge(common(env), {
|
||||
context: __dirname,
|
||||
entry: "./web/index.jsx",
|
||||
output: {
|
||||
publicPath: "auto",
|
||||
filename: "bundle.js",
|
||||
path: path.resolve(__dirname, "dist", "web"),
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ module.exports = (env = {}) => merge(common(env), {
|
||||
context: __dirname,
|
||||
entry: "./web/index.jsx",
|
||||
output: {
|
||||
publicPath: "auto",
|
||||
filename: "bundle.js",
|
||||
path: path.resolve(__dirname, "dist", "web"),
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ module.exports = (env = {}) => merge(common(env), {
|
||||
context: __dirname,
|
||||
entry: "./web/index.jsx",
|
||||
output: {
|
||||
publicPath: "auto",
|
||||
filename: "bundle.js",
|
||||
path: path.resolve(__dirname, "dist", "web"),
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user