Add Plugins page to web interface

Add page listing the plugins installed on the master along with a
detailed page for each plugin with fields from their package.json.
Failure to load the module and version missmatch is also reported.
This adds ./packages.json as required part of the webpack module.
This commit is contained in:
Hornwitser committed 2021-03-01 15:03:25 +01:00
1 parent 3a3a7597bf
commit 6c9d3d8893
11 files changed
+150 -2

No files matched your search

+1
View File
@@ -55,6 +55,7 @@ Version 2.0.0
them to be displayed by name in queries that join with the mapping.
- Added option to configure timeout for exported Prometheus metrics.
- Added option to set address to bind HTTP(S) port to.
- Added an overview of the installed plugins to the web interface.
### Changes
+1 -1
View File
@@ -279,7 +279,7 @@ app.get("/api/plugins", (req, res) => {
for (let pluginInfo of pluginInfos) {
let name = pluginInfo.name;
let enabled = masterPlugins.has(name) && masterConfig.group(name).get("enabled");
plugins.push({ name, enabled });
plugins.push({ name, version: pluginInfo.version, enabled });
}
res.send(plugins);
});
+2 -1
View File
@@ -11,5 +11,6 @@ module.exports = [
"/users/:id/view",
"/roles",
"/roles/:id/view",
"/settings",
"/plugins",
"/plugins/:name/view",
];
+2
View File
@@ -44,9 +44,11 @@ async function loadPlugins() {
let container = window[`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,
});
@@ -0,0 +1,84 @@
import React, { useEffect, useContext, useState } from "react";
import { useHistory, useParams } from "react-router-dom";
import { Alert, Descriptions } from "antd";
import PluginsContext from "./PluginsContext";
import PageLayout from "./PageLayout";
import { notifyErrorHandler } from "../util/notify";
export default function PluginViewPage() {
let params = useParams();
let plugins = useContext(PluginsContext);
let pluginName = params.name;
let plugin = plugins.find(plugin => plugin.meta.name === pluginName);
let pluginTitle = plugin.info ? plugin.info.title : plugin.meta.name;
let nav = [{ name: "Plugins", path: "/plugins" }, { name: pluginTitle }];
if (!plugin) {
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) {
return <PageLayout nav={nav}>
<Descriptions
bordered
size="small"
title={pluginTitle}
>
<Descriptions.Item label="Version">{plugin.meta.version}</Descriptions.Item>
<Descriptions.Item label="Enabled">{plugin.meta.enabled ? "Yes" : "No"}</Descriptions.Item>
</Descriptions>
<Alert
style={{
marginTop: "1em",
}}
message="Error loading web module"
description={
"The web interface was unable to load the webpack module for this plugin. This is "+
"usually due to an incorrect or missing webpack build for the plugin. Due to the "+
"module not being loaded, the configs and web controls defined by this plugin will "+
"not be available in the web interface."
}
type="error"
showIcon
/>
</PageLayout>;
}
return <PageLayout nav={nav}>
<Descriptions
bordered
size="small"
title={pluginTitle}
>
<Descriptions.Item label="Version">{plugin.meta.version}</Descriptions.Item>
<Descriptions.Item label="Enabled" span={2}>{plugin.meta.enabled ? "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>
</Descriptions.Item> : null}
{plugin.package.author ? <Descriptions.Item label="Author" span={3}>
{plugin.package.author}
</Descriptions.Item> : null}
<Descriptions.Item label="License" span={3}>{plugin.package.license}</Descriptions.Item>
</Descriptions>
{plugin.meta.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}), `+
"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."
}
type="warning"
showIcon
/> : null}
</PageLayout>;
}
@@ -0,0 +1,52 @@
import React, { useContext } from "react";
import { useHistory } from "react-router-dom";
import { Table } from "antd";
import PluginsContext from "./PluginsContext";
import PageLayout from "./PageLayout";
export default function PluginsPage() {
let plugins = useContext(PluginsContext);
let history = useHistory();
return <PageLayout nav={[{ name: "Plugins" }]}>
<h2>Plugins</h2>
<Table
columns={[
{
title: "Name",
key: "name",
render: plugin => plugin.info ? plugin.info.title : plugin.meta.name,
},
{
title: "Version",
key: "version",
render: plugin => {
if (!plugin.package) {
return "Error loading module";
}
if (plugin.package.version !== plugin.meta.version) {
return "Version missmatched";
}
return plugin.package.version;
}
},
{
title: "Enabled",
dataIndex: ["meta", "enabled"],
render: enabled => enabled ? "Yes" : null,
responsive: ["sm"],
},
]}
dataSource={plugins}
rowKey={plugin => plugin.meta.name}
pagination={false}
onRow={plugin => ({
onClick: event => {
history.push(`/plugins/${plugin.meta.name}/view`);
}
})}
/>
</PageLayout>;
}
@@ -17,11 +17,14 @@ import UsersPage from "./UsersPage";
import UserViewPage from "./UserViewPage";
import RolesPage from "./RolesPage";
import RoleViewPage from "./RoleViewPage";
import PluginsPage from "./PluginsPage";
import PluginViewPage from "./PluginViewPage";
const pages = [
{ path: "/instances/:id/view", sidebarPath: "/instances", content: <InstanceViewPage /> },
{ path: "/users/:name/view", sidebarPath: "/users", content: <UserViewPage /> },
{ path: "/roles/:id/view", sidebarPath: "/roles", content: <RoleViewPage /> },
{ path: "/plugins/:name/view", sidebarPath: "/plugins", content: <PluginViewPage /> },
];
const sidebar = [
@@ -30,6 +33,7 @@ const sidebar = [
{ name: "Instances", path: "/instances", content: <InstancesPage />},
{ name: "Users", path: "/users", content: <UsersPage /> },
{ name: "Roles", path: "/roles", content: <RolesPage /> },
{ name: "Plugins", path: "/plugins", content: <PluginsPage /> },
];
+1
View File
@@ -19,6 +19,7 @@ module.exports = (env = {}) => merge(common(env), {
filename: "remoteEntry.js",
exposes: {
"./info": "./info.js",
"./package.json": "./package.json",
},
shared: {
"@clusterio/lib": { import: false },
+1
View File
@@ -19,6 +19,7 @@ module.exports = (env = {}) => merge(common(env), {
filename: "remoteEntry.js",
exposes: {
"./info": "./info.js",
"./package.json": "./package.json",
},
shared: {
"@clusterio/lib": { import: false },
@@ -19,6 +19,7 @@ module.exports = (env = {}) => merge(common(env), {
filename: "remoteEntry.js",
exposes: {
"./info": "./info.js",
"./package.json": "./package.json",
},
shared: {
"@clusterio/lib": { import: false },
@@ -19,6 +19,7 @@ module.exports = (env = {}) => merge(common(env), {
filename: "remoteEntry.js",
exposes: {
"./info": "./info.js",
"./package.json": "./package.json",
},
shared: {
"@clusterio/lib": { import: false },