mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 23:04:52 +09:00
Basic functionality of remote npm
This commit is contained in:
13 files changed
+496
No files matched your search
@@ -119,6 +119,9 @@ export default class ControlConnection extends BaseConnection {
|
||||
this.handle(lib.UserDeleteRequest, this.handleUserDeleteRequest.bind(this));
|
||||
this.handle(lib.UserBulkImportRequest, this.handleUserBulkImportRequest.bind(this));
|
||||
this.handle(lib.UserBulkExportRequest, this.handleUserBulkExportRequest.bind(this));
|
||||
this.handle(lib.RemoteUpdateRequest, this.handleRemoteUpdateRequest.bind(this));
|
||||
this.handle(lib.PluginUpdateRequest, this.handlePluginUpdateRequest.bind(this));
|
||||
this.handle(lib.PluginInstallRequest, this.handlePluginInstallRequest.bind(this));
|
||||
this.handle(lib.DebugDumpWsRequest, this.handleDebugDumpWsRequest.bind(this));
|
||||
this.handle(lib.ModPortalDownloadRequest, this.handleModPortalDownloadRequest.bind(this));
|
||||
}
|
||||
@@ -1049,6 +1052,34 @@ export default class ControlConnection extends BaseConnection {
|
||||
return usersToSend;
|
||||
}
|
||||
|
||||
async handleRemoteUpdateRequest(request: lib.RemoteUpdateRequest) {
|
||||
if (!this._controller.config.get("controller.allow_remote_updates")) {
|
||||
throw new lib.RequestError("Remote updates are disabled on this machine");
|
||||
}
|
||||
return lib.updatePackage("@clusterio/controller");
|
||||
}
|
||||
|
||||
async handlePluginUpdateRequest(request: lib.PluginUpdateRequest) {
|
||||
if (!this._controller.config.get("controller.allow_plugin_updates")) {
|
||||
throw new lib.RequestError("Plugin updates are disabled on this machine");
|
||||
}
|
||||
if (!this._controller.pluginInfos.some(plugin => plugin.npmPackage === request.pluginPackage)) {
|
||||
throw new lib.RequestError(`Plugin ${request.pluginPackage} is not installed on this machine`);
|
||||
}
|
||||
return lib.updatePackage(request.pluginPackage);
|
||||
}
|
||||
|
||||
async handlePluginInstallRequest(request: lib.PluginInstallRequest) {
|
||||
if (!this._controller.config.get("controller.allow_plugin_install")) {
|
||||
throw new lib.RequestError("Plugin installs are disabled on this machine");
|
||||
}
|
||||
const npmRequest = await fetch(`https://www.npmjs.com/package/${encodeURI(request.pluginPackage)}`);
|
||||
if (!npmRequest.ok) {
|
||||
throw new lib.RequestError(`Unknown plugin: ${request.pluginPackage}`);
|
||||
}
|
||||
return lib.installPackage(request.pluginPackage);
|
||||
}
|
||||
|
||||
async handleDebugDumpWsRequest(request: lib.DebugDumpWsRequest) {
|
||||
this.ws_dumper = data => {
|
||||
if (this.connector.connected) {
|
||||
|
||||
@@ -688,6 +688,18 @@ async function inquirerMissingArgs(args) {
|
||||
}
|
||||
}
|
||||
|
||||
if (["standalone", "controller", "host"].includes(answers.mode)) {
|
||||
answers = inquirer.prompt([
|
||||
{
|
||||
type: "confirm",
|
||||
name: "remoteNpm",
|
||||
// TODO maybe link remote install to this once it isn't free text
|
||||
message: "Allow remote updates via npm?",
|
||||
default: true,
|
||||
},
|
||||
], answers);
|
||||
}
|
||||
|
||||
if (answers.mode === "plugin-template") {
|
||||
if (args["plugin-template"] && args["plugin-template"].length > 0) {
|
||||
answers.pluginTemplate = args["plugin-template"];
|
||||
@@ -824,6 +836,9 @@ async function main() {
|
||||
.option("factorio-dir", {
|
||||
nargs: 1, describe: "Path to Factorio installation [standalone/host]", type: "string",
|
||||
})
|
||||
.option("remote-npm", {
|
||||
nargs: 0, description: "Allow remote usage of npm [standalone/controller/host]", type: "boolean",
|
||||
})
|
||||
.option("plugins", {
|
||||
array: true, describe: "Plugins to install", type: "string",
|
||||
})
|
||||
@@ -889,6 +904,9 @@ async function main() {
|
||||
logger.info("Setting up controller");
|
||||
await execController(["bootstrap", "create-admin", answers.admin]);
|
||||
await execController(["config", "set", "controller.http_port", answers.httpPort]);
|
||||
await execController(["config", "set", "controller.allow_remote_updates", answers.remoteNpm]);
|
||||
await execController(["config", "set", "controller.allow_plugin_updates", answers.remoteNpm]);
|
||||
// await execController(["config", "set", "controller.allow_plugin_install", answers.remoteNpm]);
|
||||
let result = await execController(["bootstrap", "generate-user-token", answers.admin]);
|
||||
adminToken = result.stdout.split("\n").slice(-2)[0];
|
||||
}
|
||||
@@ -908,6 +926,9 @@ async function main() {
|
||||
await execHost(["config", "set", "host.controller_token", hostToken]);
|
||||
await execHost(["config", "set", "host.public_address", answers.publicAddress]);
|
||||
await execHost(["config", "set", "host.factorio_directory", answers.factorioDir]);
|
||||
await execHost(["config", "set", "host.allow_remote_updates", answers.remoteNpm]);
|
||||
await execHost(["config", "set", "host.allow_plugin_updates", answers.remoteNpm]);
|
||||
// await execHost(["config", "set", "host.allow_plugin_install", answers.remoteNpm]);
|
||||
}
|
||||
|
||||
if (answers.mode === "host") {
|
||||
@@ -919,6 +940,9 @@ async function main() {
|
||||
await execHost(["config", "set", "host.controller_token", answers.controllerToken]);
|
||||
await execHost(["config", "set", "host.public_address", answers.publicAddress]);
|
||||
await execHost(["config", "set", "host.factorio_directory", answers.factorioDir]);
|
||||
await execHost(["config", "set", "host.allow_remote_updates", answers.remoteNpm]);
|
||||
await execHost(["config", "set", "host.allow_plugin_updates", answers.remoteNpm]);
|
||||
// await execHost(["config", "set", "host.allow_plugin_install", answers.remoteNpm]);
|
||||
}
|
||||
|
||||
if (!dev && ["standalone", "controller", "host"].includes(answers.mode)) {
|
||||
|
||||
@@ -95,6 +95,47 @@ controllerCommands.add(new lib.Command({
|
||||
await control.send(new lib.ControllerRestartRequest());
|
||||
},
|
||||
}));
|
||||
controllerCommands.add(new lib.Command({
|
||||
definition: ["update", "Update the controller", (yargs) => {
|
||||
yargs.option("restart", { alias: "r", type: "boolean", description: "Restart after update" });
|
||||
}],
|
||||
handler: async function(args: { restart: boolean }, control: Control) {
|
||||
await control.sendTo("controller", new lib.RemoteUpdateRequest());
|
||||
if (args.restart) {
|
||||
await control.send(new lib.ControllerRestartRequest());
|
||||
} else {
|
||||
print("Controller updated; a restart is required to apply the changes.");
|
||||
}
|
||||
},
|
||||
}));
|
||||
controllerCommands.add(new lib.Command({
|
||||
definition: ["update-plugin", "Update a plugin on the controller", (yargs) => {
|
||||
yargs.positional("plugin", { describe: "Plugin to update", type: "string" });
|
||||
yargs.option("restart", { alias: "r", type: "boolean", description: "Restart after update" });
|
||||
}],
|
||||
handler: async function(args: { plugin: string, restart: boolean }, control: Control) {
|
||||
await control.sendTo("controller", new lib.PluginUpdateRequest(args.plugin));
|
||||
if (args.restart) {
|
||||
await control.send(new lib.ControllerRestartRequest());
|
||||
} else {
|
||||
print("Plugin updated; a restart is required to apply the changes.");
|
||||
}
|
||||
},
|
||||
}));
|
||||
controllerCommands.add(new lib.Command({
|
||||
definition: ["install-plugin", "Install a plugin on the controller", (yargs) => {
|
||||
yargs.positional("plugin", { describe: "Plugin to install", type: "string" });
|
||||
yargs.option("restart", { alias: "r", type: "boolean", description: "Restart after update" });
|
||||
}],
|
||||
handler: async function(args: { plugin: string, restart: boolean }, control: Control) {
|
||||
await control.sendTo("controller", new lib.PluginInstallRequest(args.plugin));
|
||||
if (args.restart) {
|
||||
await control.send(new lib.ControllerRestartRequest());
|
||||
} else {
|
||||
print("Plugin installed; a restart is required to apply the changes.");
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const controllerConfigCommands = new lib.CommandTree({
|
||||
name: "config", alias: ["c"], description: "controller config management",
|
||||
@@ -322,6 +363,53 @@ hostCommands.add(new lib.Command({
|
||||
}
|
||||
},
|
||||
}));
|
||||
hostCommands.add(new lib.Command({
|
||||
definition: ["update", "Update the host", (yargs) => {
|
||||
yargs.positional("host", { describe: "Host to update", type: "string" });
|
||||
yargs.option("restart", { alias: "r", type: "boolean", description: "Restart after update" });
|
||||
}],
|
||||
handler: async function(args: { host: string, restart: boolean }, control: Control) {
|
||||
let hostId = await lib.resolveHost(control, args.host);
|
||||
await control.sendTo({ hostId }, new lib.RemoteUpdateRequest());
|
||||
if (args.restart) {
|
||||
await control.sendTo({ hostId }, new lib.HostRestartRequest());
|
||||
} else {
|
||||
print("Host updated; a restart is required to apply the changes.");
|
||||
}
|
||||
},
|
||||
}));
|
||||
hostCommands.add(new lib.Command({
|
||||
definition: ["update-plugin", "Update a plugin on a host", (yargs) => {
|
||||
yargs.positional("host", { describe: "Host to update on", type: "string" });
|
||||
yargs.positional("plugin", { describe: "Plugin to update", type: "string" });
|
||||
yargs.option("restart", { alias: "r", type: "boolean", description: "Restart after update" });
|
||||
}],
|
||||
handler: async function(args: { host: string, plugin: string, restart: boolean }, control: Control) {
|
||||
let hostId = await lib.resolveHost(control, args.host);
|
||||
await control.sendTo({ hostId }, new lib.PluginUpdateRequest(args.plugin));
|
||||
if (args.restart) {
|
||||
await control.sendTo({ hostId }, new lib.HostRestartRequest());
|
||||
} else {
|
||||
print("Plugin updated; a restart is required to apply the changes.");
|
||||
}
|
||||
},
|
||||
}));
|
||||
hostCommands.add(new lib.Command({
|
||||
definition: ["install-plugin", "Install a plugin on a host", (yargs) => {
|
||||
yargs.positional("host", { describe: "Host to install on", type: "string" });
|
||||
yargs.positional("plugin", { describe: "Plugin to install", type: "string" });
|
||||
yargs.option("restart", { alias: "r", type: "boolean", description: "Restart after update" });
|
||||
}],
|
||||
handler: async function(args: { host: string, plugin: string, restart: boolean }, control: Control) {
|
||||
let hostId = await lib.resolveHost(control, args.host);
|
||||
await control.sendTo({ hostId }, new lib.PluginInstallRequest(args.plugin));
|
||||
if (args.restart) {
|
||||
await control.sendTo({ hostId }, new lib.HostRestartRequest());
|
||||
} else {
|
||||
print("Plugin installed; a restart is required to apply the changes.");
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const hostConfigCommands = new lib.CommandTree({
|
||||
name: "config", alias: ["c"], description: "Host config management",
|
||||
|
||||
@@ -392,6 +392,9 @@ export default class Host extends lib.Link {
|
||||
this.handle(lib.InstancePullSaveRequest, this.handleInstancePullSaveRequest.bind(this));
|
||||
this.handle(lib.InstancePushSaveRequest, this.handleInstancePushSaveRequest.bind(this));
|
||||
this.handle(lib.InstanceDeleteInternalRequest, this.handleInstanceDeleteInternalRequest.bind(this));
|
||||
this.handle(lib.RemoteUpdateRequest, this.handleRemoteUpdateRequest.bind(this));
|
||||
this.handle(lib.PluginUpdateRequest, this.handlePluginUpdateRequest.bind(this));
|
||||
this.handle(lib.PluginInstallRequest, this.handlePluginInstallRequest.bind(this));
|
||||
}
|
||||
|
||||
async loadPlugins() {
|
||||
@@ -1043,6 +1046,34 @@ export default class Host extends lib.Link {
|
||||
await fs.remove(instanceInfo.path);
|
||||
}
|
||||
|
||||
async handleRemoteUpdateRequest(request: lib.RemoteUpdateRequest) {
|
||||
if (!this.config.get("host.allow_remote_updates")) {
|
||||
throw new lib.RequestError("Remote updates are disabled on this machine");
|
||||
}
|
||||
return lib.updatePackage("@clusterio/host");
|
||||
}
|
||||
|
||||
async handlePluginUpdateRequest(request: lib.PluginUpdateRequest) {
|
||||
if (!this.config.get("host.allow_plugin_updates")) {
|
||||
throw new lib.RequestError("Plugin updates are disabled on this machine");
|
||||
}
|
||||
if (!this.pluginInfos.some(plugin => plugin.npmPackage === request.pluginPackage)) {
|
||||
throw new lib.RequestError(`Plugin ${request.pluginPackage} is not installed on this machine`);
|
||||
}
|
||||
return lib.updatePackage(request.pluginPackage);
|
||||
}
|
||||
|
||||
async handlePluginInstallRequest(request: lib.PluginInstallRequest) {
|
||||
if (!this.config.get("host.allow_plugin_install")) {
|
||||
throw new lib.RequestError("Plugin installs are disabled on this machine");
|
||||
}
|
||||
const npmRequest = await fetch(`https://www.npmjs.com/package/${encodeURI(request.pluginPackage)}`);
|
||||
if (!npmRequest.ok) {
|
||||
throw new lib.RequestError(`Unknown plugin: ${request.pluginPackage}`);
|
||||
}
|
||||
return lib.installPackage(request.pluginPackage);
|
||||
}
|
||||
|
||||
sendHostUpdate() {
|
||||
this.send(
|
||||
new lib.HostInfoUpdateEvent(
|
||||
|
||||
@@ -30,6 +30,7 @@ export * from "./src/zip_ops";
|
||||
export * from "./src/subscriptions";
|
||||
export * from "./src/datastore";
|
||||
export * from "./src/load_plugin_list";
|
||||
export * from "./src/rce_ops";
|
||||
|
||||
export { default as ExponentialBackoff } from "./src/ExponentialBackoff";
|
||||
export { default as ModStore } from "./src/ModStore";
|
||||
|
||||
@@ -28,6 +28,9 @@ export interface ControllerConfigFields {
|
||||
"controller.autosave_interval": number;
|
||||
"controller.mod_portal_cache_duration_minutes": number;
|
||||
"controller.mod_portal_page_size": number;
|
||||
"controller.allow_remote_updates": boolean;
|
||||
"controller.allow_plugin_updates": boolean;
|
||||
"controller.allow_plugin_install": boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,6 +214,27 @@ export class ControllerConfig extends classes.Config<ControllerConfigFields> {
|
||||
type: "number",
|
||||
initialValue: 1000,
|
||||
},
|
||||
"controller.allow_remote_updates": {
|
||||
description: "When true, allows a remote event to trigger a clusterio update via npm",
|
||||
type: "boolean",
|
||||
initialValue: true,
|
||||
readonly: ["controller"],
|
||||
hidden: true,
|
||||
},
|
||||
"controller.allow_plugin_updates": {
|
||||
description: "When true, allows a remote event to trigger a plugin update via npm",
|
||||
type: "boolean",
|
||||
initialValue: true,
|
||||
readonly: ["controller"],
|
||||
hidden: true,
|
||||
},
|
||||
"controller.allow_plugin_install": {
|
||||
description: "When true, allows a remote event to trigger a plugin install via npm",
|
||||
type: "boolean",
|
||||
initialValue: false,
|
||||
readonly: ["controller"],
|
||||
hidden: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -228,6 +252,9 @@ export interface HostConfigFields {
|
||||
"host.factorio_username": string | null,
|
||||
"host.factorio_token": string | null,
|
||||
"host.max_reconnect_delay": number;
|
||||
"host.allow_remote_updates": boolean;
|
||||
"host.allow_plugin_updates": boolean;
|
||||
"host.allow_plugin_install": boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -323,6 +350,27 @@ export class HostConfig extends classes.Config<HostConfigFields> {
|
||||
type: "number",
|
||||
initialValue: 60,
|
||||
},
|
||||
"host.allow_remote_updates": {
|
||||
description: "When true, allows a remote event to trigger a clusterio update via npm",
|
||||
type: "boolean",
|
||||
initialValue: true,
|
||||
readonly: ["host"],
|
||||
hidden: true,
|
||||
},
|
||||
"host.allow_plugin_updates": {
|
||||
description: "When true, allows a remote event to trigger a plugin update via npm",
|
||||
type: "boolean",
|
||||
initialValue: true,
|
||||
readonly: ["host"],
|
||||
hidden: true,
|
||||
},
|
||||
"host.allow_plugin_install": {
|
||||
description: "When true, allows a remote event to trigger a plugin install via npm",
|
||||
type: "boolean",
|
||||
initialValue: false,
|
||||
readonly: ["host"],
|
||||
hidden: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -352,6 +352,58 @@ export class SystemInfoUpdateEvent {
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteUpdateRequest {
|
||||
declare ["constructor"]: typeof RemoteUpdateRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static dst = ["controller", "host"] as const;
|
||||
static permission = "core.rce.remote_update";
|
||||
}
|
||||
|
||||
export class PluginUpdateRequest {
|
||||
declare ["constructor"]: typeof PluginUpdateRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static dst = ["controller", "host"] as const;
|
||||
static permission = "core.rce.plugin_update";
|
||||
|
||||
constructor(
|
||||
public pluginPackage: string,
|
||||
) {}
|
||||
|
||||
static jsonSchema = Type.String();
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(json);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.pluginPackage;
|
||||
}
|
||||
}
|
||||
|
||||
export class PluginInstallRequest {
|
||||
declare ["constructor"]: typeof PluginInstallRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static dst = ["controller", "host"] as const;
|
||||
static permission = "core.rce.plugin_install";
|
||||
|
||||
constructor(
|
||||
public pluginPackage: string,
|
||||
) {}
|
||||
|
||||
static jsonSchema = Type.String();
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(json);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.pluginPackage;
|
||||
}
|
||||
}
|
||||
|
||||
export class DebugDumpWsRequest {
|
||||
declare ["constructor"]: typeof DebugDumpWsRequest;
|
||||
static type = "request" as const;
|
||||
|
||||
@@ -30,6 +30,9 @@ export const dataClasses: (RequestClass<unknown, unknown> | EventClass<unknown>)
|
||||
controller.LogMessageEvent,
|
||||
controller.SystemInfoRequest,
|
||||
controller.SystemInfoUpdateEvent,
|
||||
controller.RemoteUpdateRequest,
|
||||
controller.PluginUpdateRequest,
|
||||
controller.PluginInstallRequest,
|
||||
controller.DebugDumpWsRequest,
|
||||
controller.DebugWsMessageEvent,
|
||||
|
||||
|
||||
@@ -525,6 +525,22 @@ definePermission({
|
||||
description: "Query past entries in the cluster log. Required to see past entries in instance console.",
|
||||
});
|
||||
|
||||
definePermission({
|
||||
name: "core.rce.remote_update",
|
||||
title: "Clusterio remote update",
|
||||
description: "Start a remote update of the clusterio packages",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.rce.plugin_update",
|
||||
title: "Plugin remote update",
|
||||
description: "Start a remote update of a third party plugin package",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.rce.plugin_install",
|
||||
title: "Plugin remote install",
|
||||
description: "Start a remote install of a third party plugin package",
|
||||
});
|
||||
|
||||
definePermission({
|
||||
name: "core.debug.dump_ws",
|
||||
title: "Dump WebSocket",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import util from "util";
|
||||
import path from "path";
|
||||
import { exec } from "child_process";
|
||||
import { logger } from "./logging";
|
||||
const execAsync = util.promisify(exec);
|
||||
|
||||
async function isDev() {
|
||||
// dev: <devRoot>/packages/lib/dist/src/rce_ops.js
|
||||
// prod: <prodRoot>/node_modules/@clusterio/lib/dist/src/rce_ops.js
|
||||
return __dirname.split(path.sep).at(-5) === "packages"; // opposed to "@clusterio"
|
||||
}
|
||||
|
||||
async function logExec(cmd: string) {
|
||||
logger.audit(`RCE | ${cmd}`);
|
||||
if (!isDev()) {
|
||||
await execAsync(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updatePackage(name: string) {
|
||||
return logExec(`npm update --save ${name}`);
|
||||
}
|
||||
|
||||
export async function installPackage(name: string) {
|
||||
return logExec(`npm install --save ${name}`);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import PageHeader from "./PageHeader";
|
||||
import PageLayout from "./PageLayout";
|
||||
import { notifyErrorHandler } from "../util/notify";
|
||||
import webUiPackage from "../../package.json";
|
||||
import { hasNpmButtonPermission, NpmButton } from "./NpmButton";
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
@@ -65,6 +66,10 @@ export default function ControllerPage() {
|
||||
Restart
|
||||
</Button>
|
||||
}
|
||||
{
|
||||
hasNpmButtonPermission()
|
||||
&& <NpmButton target="controller" canRestart={system?.canRestart}/>
|
||||
}
|
||||
</Space>}
|
||||
/>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "./system_metrics";
|
||||
import { formatTimestamp } from "../util/time_format";
|
||||
import { useSystems } from "../model/system";
|
||||
import { hasNpmButtonPermission, NpmButton } from "./NpmButton";
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
@@ -101,6 +102,10 @@ export default function HostViewPage() {
|
||||
Restart
|
||||
</Button>
|
||||
}
|
||||
{
|
||||
hasNpmButtonPermission()
|
||||
&& <NpmButton target={{ hostId }} canRestart={system?.canRestart} disabled={!host["connected"]}/>
|
||||
}
|
||||
</Space>;
|
||||
|
||||
return <PageLayout nav={nav}>
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { useContext, useState } from "react";
|
||||
import { Button, Checkbox, Form, FormInstance, Input, Modal, Radio, Select } from "antd";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
import { useAccount } from "../model/account";
|
||||
import { notifyErrorHandler } from "../util/notify";
|
||||
import ControlContext from "./ControlContext";
|
||||
|
||||
type NpmActionProps = {
|
||||
setApplyAction(func: () => Promise<void>): void,
|
||||
form: FormInstance,
|
||||
target: lib.AddressShorthand,
|
||||
}
|
||||
|
||||
function NpmRemoteUpdate({ setApplyAction, form, target }: NpmActionProps) {
|
||||
const control = useContext(ControlContext);
|
||||
|
||||
setApplyAction(async () => {
|
||||
await control.sendTo(target, new lib.RemoteUpdateRequest());
|
||||
});
|
||||
|
||||
return <>
|
||||
</>;
|
||||
}
|
||||
|
||||
function NpmPluginUpdate({ setApplyAction, form, target }: NpmActionProps) {
|
||||
const control = useContext(ControlContext);
|
||||
|
||||
setApplyAction(async () => {
|
||||
const values = form.getFieldsValue();
|
||||
if (!values.plugin || values.plugin === "") {
|
||||
throw new Error("Plugin not provided");
|
||||
}
|
||||
await control.sendTo(target, new lib.PluginUpdateRequest(values.plugin));
|
||||
});
|
||||
|
||||
return <>
|
||||
<Form.Item label="Plugin" name="plugin">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</>;
|
||||
|
||||
// eslint-disable-next-line multiline-comment-style
|
||||
/* TODO Allow control access to plugins on hosts
|
||||
return <>
|
||||
<Form.Item label="Plugin" name="plugin">
|
||||
<Select
|
||||
showSearch
|
||||
placeholder="Select a plugin"
|
||||
optionLabelProp="label"
|
||||
options={plugins.map(plugin => ({
|
||||
value: plugin.npmPackage,
|
||||
label: `${plugin.name} (v${plugin.version})`,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>; */
|
||||
}
|
||||
|
||||
function NpmPluginInstall({ setApplyAction, form, target }: NpmActionProps) {
|
||||
const control = useContext(ControlContext);
|
||||
|
||||
setApplyAction(async () => {
|
||||
const values = form.getFieldsValue();
|
||||
if (!values.newPlugin || values.newPlugin === "") {
|
||||
throw new Error("Plugin not provided");
|
||||
}
|
||||
await control.sendTo(target, new lib.PluginInstallRequest(values.newPlugin));
|
||||
});
|
||||
|
||||
// TODO custom npm search based on tags
|
||||
return <>
|
||||
<Form.Item label="Plugin" name="newPlugin">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</>;
|
||||
}
|
||||
|
||||
export function NpmButton(props: { target: lib.AddressShorthand, canRestart?: boolean, disabled?: boolean }) {
|
||||
const control = useContext(ControlContext);
|
||||
const account = useAccount();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [formAction, setFormAction] = useState<string | undefined>(undefined);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
function onValuesChange({ action } : { action?: string }) {
|
||||
if (action) {
|
||||
setFormAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
let applyAction: () => Promise<void>;
|
||||
const setApplyAction = (func: () => Promise<void>) => { applyAction = func; };
|
||||
async function onOk() {
|
||||
if (!applyAction) {
|
||||
form.setFields([{ name: "action", errors: ["Action is required"] }]);
|
||||
return;
|
||||
}
|
||||
await applyAction();
|
||||
|
||||
const values = form.getFieldsValue();
|
||||
if (values.restart) {
|
||||
// Ternary not used because TS disliked it
|
||||
if (props.target === "controller") {
|
||||
await control.sendTo(props.target, new lib.ControllerRestartRequest());
|
||||
} else {
|
||||
await control.sendTo(props.target, new lib.HostRestartRequest());
|
||||
}
|
||||
}
|
||||
|
||||
setFormAction(undefined);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
// TODO retrieve config so options can be disabled rather than using the error messages
|
||||
return <>
|
||||
<Button
|
||||
type="default"
|
||||
disabled={props.disabled}
|
||||
onClick={() => { setOpen(true); }}
|
||||
>Updates</Button>
|
||||
<Modal
|
||||
title="Updates"
|
||||
okText="Apply"
|
||||
open={open}
|
||||
okButtonProps={{disabled: formAction === undefined}}
|
||||
onOk={() => { onOk().catch(notifyErrorHandler(`Error running ${formAction}`)); }}
|
||||
onCancel={() => { setOpen(false); }}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} onValuesChange={onValuesChange} clearOnDestroy>
|
||||
<Form.Item label="Action" name="action">
|
||||
<Radio.Group value={formAction}>
|
||||
{account.hasPermission("core.rce.remote_update")
|
||||
? <Radio.Button value="remote_update">Update Clusterio</Radio.Button> : undefined}
|
||||
{account.hasPermission("core.rce.plugin_update")
|
||||
? <Radio.Button value="plugin_update">Update Plugin</Radio.Button> : undefined}
|
||||
{account.hasPermission("core.rce.plugin_install")
|
||||
? <Radio.Button value="plugin_install">Install Plugin</Radio.Button> : undefined}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{
|
||||
formAction !== undefined
|
||||
&& account.hasPermission(`core.${props.target === "controller" ? "controller" : "host"}.restart`)
|
||||
&& <Form.Item label="Restart on completion" name="restart">
|
||||
<Checkbox disabled={props.canRestart === false}/>
|
||||
</Form.Item>
|
||||
}
|
||||
{formAction === "remote_update"
|
||||
? <NpmRemoteUpdate {...{setApplyAction, form, target: props.target}}/> : undefined}
|
||||
{formAction === "plugin_update"
|
||||
? <NpmPluginUpdate {...{setApplyAction, form, target: props.target}}/> : undefined}
|
||||
{formAction === "plugin_install"
|
||||
? <NpmPluginInstall {...{setApplyAction, form, target: props.target}}/> : undefined}
|
||||
</Form>
|
||||
</Modal>
|
||||
</>;
|
||||
}
|
||||
|
||||
export function hasNpmButtonPermission() {
|
||||
const account = useAccount();
|
||||
return account.hasAnyPermission(
|
||||
"core.rce.remote_update", "core.rce.plugin_update", "core.rce.plugin_install"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user