diff --git a/packages/master/src/routes.js b/packages/master/src/routes.js index 612aad16..4052554a 100644 --- a/packages/master/src/routes.js +++ b/packages/master/src/routes.js @@ -117,7 +117,7 @@ function getPlugins(req, res) { if (web.main === "remoteEntry.js") { web.error = "Incompatible old remoteEntry.js entrypoint."; } - plugins.push({ name, version: pluginInfo.version, enabled, loaded, web }); + plugins.push({ name, version: pluginInfo.version, enabled, loaded, web, requirePath: pluginInfo.requirePath }); } res.send(plugins); } diff --git a/packages/web_ui/src/components/SlavesPage.jsx b/packages/web_ui/src/components/SlavesPage.jsx index 77e2fd9c..41748808 100644 --- a/packages/web_ui/src/components/SlavesPage.jsx +++ b/packages/web_ui/src/components/SlavesPage.jsx @@ -1,6 +1,7 @@ -import React, { useContext, useRef, useState } from "react"; +import React, { useContext, useRef, useState, useEffect } from "react"; import { useHistory } from "react-router-dom"; -import { Button, Form, Input, Modal, PageHeader, Table, Tag } from "antd"; +import { Button, Form, Input, Modal, PageHeader, Table, Tag, Typography } from "antd"; +import CopyOutlined from "@ant-design/icons/lib/icons/CopyOutlined"; import { libLink } from "@clusterio/lib"; @@ -9,7 +10,7 @@ import ControlContext from "./ControlContext"; import PageLayout from "./PageLayout"; import PluginExtra from "./PluginExtra"; import { useSlaveList } from "../model/slave"; -import { notifyErrorHandler } from "../util/notify"; +import notify, { notifyErrorHandler } from "../util/notify"; const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "base" }).compare; @@ -18,12 +19,24 @@ function GenerateSlaveTokenButton(props) { let control = useContext(ControlContext); let [visible, setVisible] = useState(false); let [token, setToken] = useState(null); + let [slaveId, setSlaveId] = useState(null); let [form] = Form.useForm(); let tokenTextAreaRef = useRef(null); + let [pluginList, setPluginList] = useState([]); + useEffect(() => { + (async () => { + let response = await fetch(`${webRoot}api/plugins`); + if (response.ok) { + const plugins = await response.json(); + setPluginList(plugins); + } else { + notify("Failed to load plugin list"); + } + })(); + }, []); async function generateToken() { let values = form.getFieldsValue(); - let slaveId = null; if (values.slaveId) { slaveId = Number.parseInt(values.slaveId, 10); if (Number.isNaN(slaveId)) { @@ -35,8 +48,11 @@ function GenerateSlaveTokenButton(props) { let result = await libLink.messages.generateSlaveToken.send(control, { slave_id: slaveId }); setToken(result.token); + setSlaveId(slaveId); } + // Only install plugins that aren't filesystem paths. Npm modules have max 1 forward slash in their name. + const pluginString = pluginList.map(p => `"${p.requirePath}"`).filter(x => x.split("/") <= 1).join(" "); return <> - - - - - + {token !== null && <> + + Slave auth token: + +
+ + {token} +
+ + You can set the token on an existing slave with the following command: + +
+ +

npx clusterioslave config set slave.master_token <token>

+

npx clusterioslave config set slave.id <slaveId>

+
+ + Example slave setup commands: + +
+ +

> mkdir clusterio

+

> cd clusterio

+

+ > npm init "@clusterio" -- + --master-token {token} + --mode "slave" + --download-headless + --master-url {document.location.origin}/ + --slave-name "Slave {slaveId || "?"}" + --public-address localhost + {pluginString.length ? "--plugins" : ""} {pluginString} +

+

> ./run-slave

+
+ } ; } +function CopyButton({ text, message }) { + let [clipboardPermision, setClipboardPermission] = useState("granted"); + useEffect(() => { + (async () => { + let result = await navigator.permissions.query({ name: "clipboard-write" }); + // result.state is "granted", "denied" or "prompt" + setClipboardPermission(result.state); + result.onchange = function () { + setClipboardPermission(result.state); + }; + })(); + }, []); + + async function checkClipboardPermission() { + let result = await navigator.permissions.query({ name: "clipboard-write" }); + // result.state is "granted", "denied" or "prompt" + setClipboardPermission(result.state); + result.onchange = function () { + setClipboardPermission(result.state); + }; + return result.state === "granted"; + } + + return ; +} + export default function SlavesPage() { let account = useAccount();