diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 4ac09a14..5808be74 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -12,14 +12,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [12.x, 14.x, 16.x] + node-version: [18.x, 20.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - uses: actions/checkout@v2 - - uses: pnpm/action-setup@v2.2.2 + - uses: pnpm/action-setup@v2.4.0 with: - version: 6 + version: 8 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: @@ -29,13 +29,13 @@ jobs: - run: node packages/lib/build_mod --source-dir packages/host/lua/clusterio_lib --output-dir temp/test/mods - run: wget -q -O temp/test/mods/subspace_storage_1.99.8.zip -L https://github.com/clusterio/subspace_storage/releases/download/1.99.6/subspace_storage_1.99.8.zip - name: Run tests - if: ${{ matrix.node-version != '12.x' }} + if: ${{ matrix.node-version != '18.x' }} run: pnpm test - name: Run coverage - if: ${{ matrix.node-version == '12.x' }} + if: ${{ matrix.node-version == '18.x' }} run: pnpm run ci-cover - name : Upload coverage to Codecov - if: ${{ matrix.node-version == '12.x' }} + if: ${{ matrix.node-version == '18.x' }} uses: codecov/codecov-action@v2 with: files: ./coverage/lcov.info @@ -44,11 +44,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: pnpm/action-setup@v2.2.2 + - uses: pnpm/action-setup@v2.4.0 with: - version: 6 + version: 8 - uses: actions/setup-node@v2 with: - node-version: 16.x + node-version: 18.x - run: pnpm i --no-optional --no-frozen-lockfile - run: pnpm run lint diff --git a/Dockerfile b/Dockerfile index babd6bfa..4f4832f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:12 as subspace_storage_builder +FROM node:20 as subspace_storage_builder RUN apt update && apt install -y git WORKDIR / RUN git clone https://github.com/clusterio/subspace_storage.git @@ -6,7 +6,7 @@ WORKDIR /subspace_storage RUN npm install \ && node build -FROM node:12 as clusterio_builder +FROM node:20 as clusterio_builder RUN apt update \ && apt install -y wget \ && mkdir /clusterio diff --git a/README.md b/README.md index 7de44352..ee76ddfa 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Check out the documentation on [Writing Plugins](/docs/writing-plugins.md) for w ## Installation -Clusterio runs on Node.js v12 and up, it's distributed via npm and comes with a guided install script. +Clusterio runs on Node.js v18 and up, it's distributed via npm and comes with a guided install script. If you already have a recent Node.js installed, you can set it up in a new directory with: npm init "@clusterio" @@ -99,11 +99,11 @@ Otherwise see below for OS specific instructions. ### Ubuntu setup -1. Install Node.js v12 or higher. - For 20.04 LTS or below the version of Node.js provided by the Ubuntu repos are too old and you will have to use the nodesource PPA, otherwise you may skip the first line. +1. Install Node.js v18 or higher. + Note the version of Node.js provided by the Ubuntu/debian repos are too old and you will have to use the nodesource PPA - wget -qO - https://deb.nodesource.com/setup_14.x | sudo -E bash - - sudo apt install nodejs + curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - &&\ + sudo apt-get install -y nodejs 2. Log in as the user you wish to install Clusterio as (do not use root), and then create a new directory and run the Clusterio installer: diff --git a/docs/contributing.md b/docs/contributing.md index 0d767e02..c007ee77 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -181,8 +181,8 @@ The format of this log should be self explanatory, please follow it carefully. ## Supported Node.js and Factorio Versions -Clusterio run on Node.js v12 and up, make sure to check that the Node builtin API's and npm packages you use are supported on these versions. -The GitHub Actions tests are run on whatever version of Node.js v12, v14, and v16 is the default. +Clusterio run on Node.js v18 and up, make sure to check that the Node builtin API's and npm packages you use are supported on these versions. +The GitHub Actions tests are run on whatever version of Node.js v18 and v20 is the default. For Factorio Clusterio 2.0 aims to support version 0.17.69 and up, including the latest experimental release. It's recommended that you use the lua API reference for 0.17.69, as there's no information on what version Factorio API's were introduced in. diff --git a/package.json b/package.json index e5f81591..67323b9e 100644 --- a/package.json +++ b/package.json @@ -19,23 +19,23 @@ ] }, "engines": { - "node": ">=12" + "node": ">=18" }, "devDependencies": { "@clusterio/controller": "workspace:*", "@clusterio/ctl": "workspace:*", "@clusterio/host": "workspace:*", "@clusterio/lib": "workspace:*", - "eslint": "^8.18.0", + "eslint": "^8.45.0", "eslint-plugin-node": "^11.1.0", - "express": "^4.16.2", + "express": "^4.18.2", "form-data": "^4.0.0", - "fs-extra": "^8.1.0", + "fs-extra": "^11.1.1", "jsdoc": "^4.0.2", - "jsonwebtoken": "^8.2.1", - "jszip": "^3.2.2", - "mocha": "^9.2.2", + "jsonwebtoken": "^9.0.1", + "jszip": "^3.10.0", + "mocha": "^10.2.0", "nyc": "^15.1.0", - "phin": "^3.5.1" + "phin": "^3.7.0" } } diff --git a/packages/controller/package.json b/packages/controller/package.json index c5a94a4d..c2ebd05c 100644 --- a/packages/controller/package.json +++ b/packages/controller/package.json @@ -16,31 +16,31 @@ "factorio" ], "engines": { - "node": ">=12" + "node": ">=18" }, "dependencies": { "@clusterio/lib": "^2.0.0-alpha.13", - "busboy": "^0.3.1", - "compression": "^1.7.2", - "express": "^4.16.2", - "fs-extra": "^8.1.0", - "jsonwebtoken": "^8.2.1", - "jszip": "^3.2.2", + "busboy": "^1.6.0", + "compression": "^1.7.4", + "express": "^4.18.2", + "fs-extra": "^11.1.1", + "jsonwebtoken": "^9.0.1", + "jszip": "^3.10.1", "set-blocking": "^2.0.0", - "webpack-cli": "^4.9.2", - "winston": "^3.3.3", - "winston-daily-rotate-file": "^4.5.5", - "ws": "^7.2.3", - "yargs": "^14.2.0" + "webpack-cli": "^5.1.4", + "winston": "^3.10.0", + "winston-daily-rotate-file": "^4.7.1", + "ws": "^8.13.0", + "yargs": "^17.7.2" }, "devDependencies": { "@clusterio/web_ui": "^2.0.0-alpha.13", - "react": "^16.13.1", - "react-dom": "^16.13.1", - "react-router-dom": "^5.2.0", - "webpack": "^5.73.0", - "webpack-dev-middleware": "^5.3.1", - "webpack-merge": "^5.2.0" + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.4.14", + "webpack": "^5.88.2", + "webpack-dev-middleware": "^6.1.1", + "webpack-merge": "^5.9.0" }, "publishConfig": { "access": "public" diff --git a/packages/controller/src/routes.js b/packages/controller/src/routes.js index 04a98bf1..7f6a6ace 100644 --- a/packages/controller/src/routes.js +++ b/packages/controller/src/routes.js @@ -1,5 +1,5 @@ "use strict"; -const Busboy = require("busboy"); +const busboy = require("busboy"); const crypto = require("crypto"); const events = require("events"); const fs = require("fs-extra"); @@ -304,7 +304,7 @@ async function getStream(req, res) { } stream.source.pipe(res); stream.flowing = true; - res.on("finish", () => { + res.on("close", () => { stream.events.emit("close"); }); clearTimeout(stream.timeout); @@ -342,7 +342,7 @@ async function uploadSave(req, res) { res.status(415).json({ request_errors: ["invalid Content-Type"] }); return; } - let mimeType = match[1].toLowerCase(); + let contentMime = match[1].toLowerCase(); let tasks = []; let errors = []; @@ -381,16 +381,16 @@ async function uploadSave(req, res) { } } - if (mimeType === "multipart/form-data") { + if (contentMime === "multipart/form-data") { await new Promise(resolve => { let fields = {}; - let busboy = new Busboy({ headers: req.headers }); - busboy.on("file", (fieldname, stream, filename, transferEncoding, fileMime) => { + let parser = busboy({ headers: req.headers }); + parser.on("file", (name, stream, { filename, mimeType }) => { if (fields.instanceId === undefined) { requestErrors.push("instance_id must come before files uploaded"); } - if (!zipMimes.includes(fileMime)) { + if (!zipMimes.includes(mimeType)) { requestErrors.push("invalid file Content-Type"); } @@ -403,25 +403,25 @@ async function uploadSave(req, res) { return; } - tasks.push(handleFile(fields.instanceId, stream, filename, fileMime)); + tasks.push(handleFile(fields.instanceId, stream, filename, mimeType)); }); - busboy.on("field", (fieldname, value, fieldnameTruncated, valueTruncated, transferEncoding, valueMime) => { - if (fieldname === "instance_id") { + parser.on("field", (name, value, info) => { + if (name === "instance_id") { fields.instanceId = Number.parseInt(value, 10); if (Number.isNaN(fields.instanceId)) { requestErrors.push("invalid instance_id"); } } }); - busboy.on("finish", resolve); - busboy.on("error", (err) => { + parser.on("close", resolve); + parser.on("error", (err) => { logger.error(`Error parsing multipart request in upload-save:\n${err.stack}`); errors.push(err.message); }); - req.pipe(busboy); + req.pipe(parser); }); - } else if (zipMimes.includes(mimeType)) { + } else if (zipMimes.includes(contentMime)) { let filename = req.query.filename; if (typeof filename !== "string") { requestErrors.push("Missing or invalid filename parameter"); @@ -436,7 +436,7 @@ async function uploadSave(req, res) { if (errors.length || requestErrors.length) { req.resume(); } else { - tasks.push(handleFile(instanceId, req, filename, mimeType)); + tasks.push(handleFile(instanceId, req, filename, contentMime)); } } else { @@ -482,7 +482,7 @@ async function uploadMod(req, res) { res.status(415).json({ request_errors: ["invalid Content-Type"] }); return; } - let mimeType = match[1].toLowerCase(); + let contentMime = match[1].toLowerCase(); let tasks = []; let errors = []; @@ -544,11 +544,11 @@ async function uploadMod(req, res) { } } - if (mimeType === "multipart/form-data") { + if (contentMime === "multipart/form-data") { await new Promise(resolve => { - let busboy = new Busboy({ headers: req.headers }); - busboy.on("file", (fieldname, stream, filename, transferEncoding, fileMime) => { - if (!zipMimes.includes(fileMime)) { + let parser = busboy({ headers: req.headers }); + parser.on("file", (name, stream, { filename, mimeType }) => { + if (!zipMimes.includes(mimeType)) { requestErrors.push("invalid file Content-Type"); } @@ -563,15 +563,15 @@ async function uploadMod(req, res) { tasks.push(handleFile(stream, filename)); }); - busboy.on("finish", resolve); - busboy.on("error", (err) => { + parser.on("close", resolve); + parser.on("error", (err) => { logger.error(`Error parsing multipart request in upload-mod:\n${err.stack}`); errors.push(err.message); }); - req.pipe(busboy); + req.pipe(parser); }); - } else if (zipMimes.includes(mimeType)) { + } else if (zipMimes.includes(contentMime)) { let filename = req.query.filename; if (typeof filename !== "string") { requestErrors.push("Missing or invalid filename parameter"); diff --git a/packages/create/package.json b/packages/create/package.json index 2a953042..59f53d62 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -13,14 +13,14 @@ "clusterio" ], "engines": { - "node": ">=12" + "node": ">=18" }, "dependencies": { - "chalk": "^4.1.0", - "fs-extra": "^8.1.0", - "inquirer": "^8.1.0", - "phin": "^3.5.1", - "yargs": "^14.2.0" + "chalk": "^4.1.2", + "fs-extra": "^11.1.1", + "inquirer": "^8.2.4", + "phin": "^3.7.0", + "yargs": "^17.7.2" }, "publishConfig": { "access": "public" diff --git a/packages/ctl/package.json b/packages/ctl/package.json index b7870b6e..de09a5d0 100644 --- a/packages/ctl/package.json +++ b/packages/ctl/package.json @@ -15,16 +15,16 @@ "factorio" ], "engines": { - "node": ">=12" + "node": ">=18" }, "dependencies": { "@clusterio/lib": "^2.0.0-alpha.13", - "as-table": "^1.0.31", - "fs-extra": "^8.1.0", - "phin": "^3.5.1", + "as-table": "^1.0.55", + "fs-extra": "^11.1.1", + "phin": "^3.7.0", "set-blocking": "^2.0.0", - "winston": "^3.3.3", - "yargs": "^14.2.0" + "winston": "^3.10.0", + "yargs": "^17.7.2" }, "publishConfig": { "access": "public" diff --git a/packages/ctl/src/commands.js b/packages/ctl/src/commands.js index c3a437e5..c4c908bb 100644 --- a/packages/ctl/src/commands.js +++ b/packages/ctl/src/commands.js @@ -381,6 +381,9 @@ instanceConfigCommands.add(new libCommand.Command({ // with the all the layers of quote removal at play is difficult. // See the following table for how to pass "That's a \" quote" in // different environments: + // ----------------------- WARNING ------------------------------- + // This table is most likely invalid due a change in yargs parsing + // --------------------------------------------------------------- // cmd : """""That's a \\"" quote""""" // cmd + npx : """""""""""That's a \\\\"""" quote""""""""""" // PowerShell : '"""""That''s a \\"" quote"""""' diff --git a/packages/host/package.json b/packages/host/package.json index dbc0a401..71337771 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -15,17 +15,17 @@ "factorio" ], "engines": { - "node": ">=12" + "node": ">=18" }, "dependencies": { "@clusterio/lib": "^2.0.0-alpha.13", - "fs-extra": "^8.1.0", - "phin": "^3.5.1", - "pidusage": "^2.0.18", + "fs-extra": "^11.1.1", + "phin": "^3.7.0", + "pidusage": "^3.0.2", "set-blocking": "^2.0.0", - "winston": "^3.3.3", - "winston-daily-rotate-file": "^4.5.5", - "yargs": "^14.2.0" + "winston": "^3.10.0", + "winston-daily-rotate-file": "^4.7.1", + "yargs": "^17.7.2" }, "publishConfig": { "access": "public" diff --git a/packages/lib/data/messages_core.js b/packages/lib/data/messages_core.js index 2b74c125..1bd925c6 100644 --- a/packages/lib/data/messages_core.js +++ b/packages/lib/data/messages_core.js @@ -67,7 +67,6 @@ class Address { static jsonSchema = { type: "array", minItems: 2, - maxItems: 3, items: [ { type: "integer", minimum: 0, maximum: 4 }, { type: "integer" }, @@ -175,6 +174,7 @@ class HelloData { } static jsonSchema = { + type: "object", required: ["version", "plugins"], properties: { "version": { type: "string" }, @@ -354,14 +354,13 @@ class AccountDetails { } static jsonSchema = { - additionalProperties: false, + type: "object", required: ["name", "roles"], properties: { "name": { type: "string" }, "roles": { type: "array", items: { - additionalProperties: false, type: "object", required: ["name", "id", "permissions"], properties: { @@ -404,7 +403,7 @@ class ReadyData { } static jsonSchema = { - additionalProperties: false, + type: "object", required: ["src", "sessionToken", "sessionTimeout", "heartbeatInterval"], properties: { "src": Address.jsonSchema, @@ -519,6 +518,7 @@ class ContinueData { } static jsonSchema = { + type: "object", required: ["sessionTimeout", "heartbeatInterval"], properties: { "sessionTimeout": { type: "number" }, @@ -701,6 +701,7 @@ class ResponseError { } static jsonSchema = { + type: "object", required: ["message"], properties: { "message": { type: "string" }, diff --git a/packages/lib/data/messages_host.js b/packages/lib/data/messages_host.js index 40b0b855..fe91ed68 100644 --- a/packages/lib/data/messages_host.js +++ b/packages/lib/data/messages_host.js @@ -27,6 +27,7 @@ class HostDetails { } static jsonSchema = { + type: "object", required: ["agent", "version", "name", "id", "connected"], properties: { "agent": { type: "string" }, @@ -156,6 +157,7 @@ class ControllerConnectionEvent { } static jsonSchema = { + type: "object", required: ["event"], properties: { "event": { type: "string" }, @@ -194,6 +196,7 @@ class SyncUserListsEvent { } static jsonSchema = { + type: "object", required: ["adminlist", "banlist", "whitelist"], properties: { "adminlist": { @@ -204,7 +207,7 @@ class SyncUserListsEvent { type: "array", items: { type: "array", - additionalItems: false, + minItems: 2, items: [{ type: "string" }, { type: "string" }], }, }, diff --git a/packages/lib/data/messages_instance.js b/packages/lib/data/messages_instance.js index 967f972a..d7bbaf0c 100644 --- a/packages/lib/data/messages_instance.js +++ b/packages/lib/data/messages_instance.js @@ -182,6 +182,7 @@ class InstanceConfigGetRequest { } static jsonSchema = { + type: "object", required: ["instanceId"], properties: { "instanceId": { type: "integer" }, @@ -235,6 +236,7 @@ class InstanceConfigSetFieldRequest { } static jsonSchema = { + type: "object", required: ["instanceId", "field", "value"], properties: { "instanceId": { type: "integer" }, @@ -274,6 +276,7 @@ class InstanceConfigSetPropRequest { } static jsonSchema = { + type: "object", required: ["instanceId", "field", "prop"], properties: { "instanceId": { type: "integer" }, @@ -306,6 +309,7 @@ class InstanceAssignRequest { } static jsonSchema = { + type: "object", required: ["instanceId", "hostId"], properties: { "instanceId": { type: "number" }, @@ -556,7 +560,7 @@ class InstanceRenameSaveRequest { properties: { "instanceId": { type: "integer" }, "oldName": { type: "string" }, - "newname": { type: "string" }, + "newName": { type: "string" }, }, }; @@ -1167,6 +1171,7 @@ class InstanceBanlistUpdateEvent { } static jsonSchema = { + type: "object", required: ["name", "banned", "reason"], properties: { "name": { type: "string" }, @@ -1198,6 +1203,7 @@ class InstanceAdminlistUpdateEvent { } static jsonSchema = { + type: "object", required: ["name", "admin"], properties: { "name": { type: "string" }, @@ -1228,6 +1234,7 @@ class InstanceWhitelistUpdateEvent { } static jsonSchema = { + type: "object", required: ["name", "whitelisted"], properties: { "name": { type: "string" }, @@ -1266,6 +1273,7 @@ class InstancePlayerUpdateEvent { } static jsonSchema = { + type: "object", required: ["type", "name"], properties: { "type": { type: "string", enum: ["join", "leave", "import"] }, diff --git a/packages/lib/data/messages_user.js b/packages/lib/data/messages_user.js index e45e1b4d..c651ed95 100644 --- a/packages/lib/data/messages_user.js +++ b/packages/lib/data/messages_user.js @@ -18,6 +18,7 @@ class RawPermission { // TODO refactor into lib/user.Permission } static jsonSchema = { + type: "object", required: ["name", "title", "description"], properties: { name: { type: "string" }, @@ -277,6 +278,7 @@ class RawUser { // TODO refactor into lib/user.User type: "array", items: { type: "array", + minItems: 2, items: [{ type: "integer" }, PlayerStats.jsonSchema], }, }, diff --git a/packages/lib/link/connectors.js b/packages/lib/link/connectors.js index a083e2c8..7f4ed270 100644 --- a/packages/lib/link/connectors.js +++ b/packages/lib/link/connectors.js @@ -2,7 +2,8 @@ "use strict"; const assert = require("assert").strict; const events = require("events"); -const WebSocket = require("isomorphic-ws"); +// eslint-disable-next-line node/no-process-env +const WebSocket = process.env.APP_ENV === "browser" ? window.WebSocket : require("ws"); const libData = require("../data"); const libErrors = require("../errors"); @@ -432,6 +433,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector { _doConnect() { let url = new URL(this._url); url.pathname += "api/socket"; + url.protocol = url.protocol.replace("http", "ws"); // Open WebSocket to controller logger.verbose(`Connector | connecting to ${url}`); diff --git a/packages/lib/package.json b/packages/lib/package.json index c6e82c8e..222124ba 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -12,23 +12,22 @@ "node": ">=12" }, "dependencies": { - "ajv": "^6.12.6", - "chalk": "^4.1.0", + "ajv": "^8.12.0", + "chalk": "^4.1.2", "fast-deep-equal": "^3.1.3", - "fs-extra": "^8.1.0", - "isomorphic-ws": "^4.0.1", - "jimp": "^0.12.1", - "jsonwebtoken": "^8.2.1", - "jszip": "^3.2.2", - "klaw": "^3.0.0", - "rcon-client": "^4.2.0", - "semver": "^7.1.1", + "fs-extra": "^11.1.1", + "jimp": "^0.22.8", + "jsonwebtoken": "^9.0.1", + "jszip": "^3.10.1", + "klaw": "^4.1.0", + "rcon-client": "^4.2.3", + "semver": "^7.5.4", "set-blocking": "^2.0.0", - "triple-beam": "^1.3.0", - "winston": "^3.3.3", - "winston-transport": "^4.4.0", - "ws": "^7.2.3", - "yargs": "^14.2.0" + "triple-beam": "^1.4.1", + "winston": "^3.10.0", + "winston-transport": "^4.5.0", + "ws": "^8.13.0", + "yargs": "^17.7.2" }, "publishConfig": { "access": "public" diff --git a/packages/lib/schema.js b/packages/lib/schema.js index a9e362c7..65f90937 100644 --- a/packages/lib/schema.js +++ b/packages/lib/schema.js @@ -5,11 +5,10 @@ "use strict"; const Ajv = require("ajv"); const ajv = new Ajv({ + allowUnionTypes: true, + strict: "log", + strictTuples: false, verbose: true, - format: "full", - extendRefs: "fail", - strictDefaults: true, - strictKeywords: true, }); module.exports = { diff --git a/packages/web_ui/package.json b/packages/web_ui/package.json index b8b815c9..133370b1 100644 --- a/packages/web_ui/package.json +++ b/packages/web_ui/package.json @@ -15,32 +15,32 @@ "node": ">=12" }, "dependencies": { - "@ant-design/icons": "^4.2.2", - "@babel/core": "^7.13.8", - "@babel/preset-react": "^7.12.13", + "@ant-design/icons": "^5.1.4", + "@babel/core": "^7.22.9", + "@babel/preset-react": "^7.22.5", "@clusterio/lib": "^2.0.0-alpha.13", - "antd": "^4.16.0", + "antd": "^5.7.3", "assert": "^2.0.0", - "babel-loader": "^8.2.2", + "babel-loader": "^9.1.3", "browserify-zlib": "^0.2.0", - "buffer": "^6.0.2", - "clean-webpack-plugin": "^3.0.0", - "css-loader": "^5.1.0", - "events": "^3.2.0", + "buffer": "^6.0.3", + "clean-webpack-plugin": "^4.0.0", + "css-loader": "^6.8.1", + "events": "^3.3.0", "file-loader": "^6.2.0", "null-loader": "^4.0.1", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", "process": "^0.11.10", - "react": "^16.13.1", - "react-dom": "^16.13.1", - "react-router-dom": "^5.2.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.14.2", "stream-browserify": "^3.0.0", - "style-loader": "^2.0.0", + "style-loader": "^3.3.3", "terser-webpack-plugin": "^5.3.9", - "util": "^0.12.3", - "webpack": "^5.73.0", - "webpack-cli": "^4.9.2", + "util": "^0.12.5", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4", "webpack-manifest-plugin": "^5.0.0" }, "publishConfig": { diff --git a/packages/web_ui/src/bootstrap.jsx b/packages/web_ui/src/bootstrap.jsx index d07d1ed3..c0861fda 100644 --- a/packages/web_ui/src/bootstrap.jsx +++ b/packages/web_ui/src/bootstrap.jsx @@ -1,7 +1,7 @@ import "./index.css"; import React from "react"; -import ReactDOM from "react-dom"; +import { createRoot } from "react-dom/client"; import { libConfig, libLink, libLogging, libPlugin } from "@clusterio/lib"; const { ConsoleTransport, WebConsoleFormat, logger } = libLogging; @@ -108,10 +108,9 @@ export default async function bootstrap() { let plugins = await loadPlugins(pluginInfos); let wsUrl = new URL(window.webRoot, document.location); - wsUrl.protocol = wsUrl.protocol.replace("http", "ws"); - let controlConnector = new ControlConnector(wsUrl, 120); let control = new Control(controlConnector, plugins); - ReactDOM.render(, document.getElementById("root")); + const root = createRoot(document.getElementById("root")); + root.render(); } diff --git a/packages/web_ui/src/components/App.jsx b/packages/web_ui/src/components/App.jsx index 20981c70..5422c9f4 100644 --- a/packages/web_ui/src/components/App.jsx +++ b/packages/web_ui/src/components/App.jsx @@ -8,7 +8,7 @@ import SiteLayout from "./SiteLayout"; import ControlContext from "./ControlContext"; import LoginForm from "./LoginForm"; -import { Card, Spin, Typography } from "antd"; +import { Card, ConfigProvider, Spin, Typography, theme } from "antd"; const { Paragraph } = Typography; const { logger } = libLogging; @@ -93,11 +93,15 @@ export default function App(props) { return ( - - - {page} - - + + + + {page} + + + ); } diff --git a/packages/web_ui/src/components/AssignInstanceModal.jsx b/packages/web_ui/src/components/AssignInstanceModal.jsx index f64edfbc..05049f41 100644 --- a/packages/web_ui/src/components/AssignInstanceModal.jsx +++ b/packages/web_ui/src/components/AssignInstanceModal.jsx @@ -11,20 +11,16 @@ const { Paragraph } = Typography; export default function AssignInstanceModal(props) { - let [visible, setVisible] = useState(false); + let [open, setOpen] = useState(false); let [hostList] = useHostList(); let [applying, setApplying] = useState(false); let [form] = Form.useForm(); let control = useContext(ControlContext); - function open() { - setVisible(true); - } - function handleAssign() { let hostId = form.getFieldValue("host"); if (hostId === undefined) { - setVisible(false); + setOpen(false); return; } @@ -36,7 +32,7 @@ export default function AssignInstanceModal(props) { control.send( new libData.InstanceAssignRequest(props.id, hostId) ).then(() => { - setVisible(false); + setOpen(false); if (props.onFinish) { props.onFinish(); } @@ -48,17 +44,17 @@ export default function AssignInstanceModal(props) { } function handleCancel() { - setVisible(false); + setOpen(false); } return <> - { form.resetFields(); - setVisible(false); + setOpen(false); }).catch( notifyErrorHandler("Error creating save") ).finally(() => { @@ -81,7 +81,7 @@ export default function CreateSaveModal(props) { } function handleCancel() { - setVisible(false); + setOpen(false); } return <> @@ -89,7 +89,7 @@ export default function CreateSaveModal(props) { loading={creatingSave} disabled={props.instance.status !== "stopped"} onClick={() => { - setVisible(true); + setOpen(true); }} > Create save @@ -98,7 +98,7 @@ export default function CreateSaveModal(props) { title="Create Save" width={560} okText="Create" - visible={visible} + open={open} confirmLoading={creatingSave} onOk={createSave} onCancel={handleCancel} diff --git a/packages/web_ui/src/components/HostViewPage.jsx b/packages/web_ui/src/components/HostViewPage.jsx index 51f1db69..7548be88 100644 --- a/packages/web_ui/src/components/HostViewPage.jsx +++ b/packages/web_ui/src/components/HostViewPage.jsx @@ -1,5 +1,5 @@ import React from "react"; -import { Descriptions, PageHeader, Spin, Tag, Typography } from "antd"; +import { Descriptions, Spin, Tag, Typography } from "antd"; import { useParams } from "react-router-dom"; import InstanceList from "./InstanceList"; @@ -7,6 +7,7 @@ import LogConsole from "./LogConsole"; import { useAccount } from "../model/account"; import { useInstanceList } from "../model/instance"; import { useHost } from "../model/host"; +import PageHeader from "./PageHeader"; import PageLayout from "./PageLayout"; import PluginExtra from "./PluginExtra"; @@ -29,19 +30,13 @@ export default function HostViewPage(props) { if (host.missing) { return - +

Host with id {hostId} was not found on the controller.

; } return - + {host["name"]} diff --git a/packages/web_ui/src/components/HostsPage.jsx b/packages/web_ui/src/components/HostsPage.jsx index afb8c2dd..e1df0228 100644 --- a/packages/web_ui/src/components/HostsPage.jsx +++ b/packages/web_ui/src/components/HostsPage.jsx @@ -1,12 +1,13 @@ import React, { useContext, useRef, useState, useEffect } from "react"; -import { useHistory } from "react-router-dom"; -import { Button, Form, InputNumber, Modal, PageHeader, Table, Tag, Typography } from "antd"; +import { useNavigate } from "react-router-dom"; +import { Button, Form, InputNumber, Modal, Table, Tag, Typography } from "antd"; import CopyOutlined from "@ant-design/icons/lib/icons/CopyOutlined"; import { libData } from "@clusterio/lib"; import { useAccount } from "../model/account"; import ControlContext from "./ControlContext"; +import PageHeader from "./PageHeader"; import PageLayout from "./PageLayout"; import PluginExtra from "./PluginExtra"; import { useHostList } from "../model/host"; @@ -17,7 +18,7 @@ const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "ba function GenerateHostTokenButton(props) { let control = useContext(ControlContext); - let [visible, setVisible] = useState(false); + let [open, setOpen] = useState(false); let [token, setToken] = useState(null); let [hostId, setHostId] = useState(null); let [form] = Form.useForm(); @@ -53,23 +54,23 @@ function GenerateHostTokenButton(props) { // Generate a new random useEffect(() => { - if (visible) { + if (open) { generateToken().catch(notifyErrorHandler("Error generating token")); } - }, [visible]); + }, [open]); // 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 <> { - setVisible(false); + setOpen(false); setToken(null); form.resetFields(); }} @@ -177,12 +178,11 @@ function CopyButton({ text, message }) { export default function HostsPage() { let account = useAccount(); - let history = useHistory(); + let navigate = useNavigate(); let [hostList] = useHostList(); return } /> @@ -226,7 +226,7 @@ export default function HostsPage() { pagination={false} onRow={(record, rowIndex) => ({ onClick: event => { - history.push(`/hosts/${record.id}/view`); + navigate(`/hosts/${record.id}/view`); }, })} /> diff --git a/packages/web_ui/src/components/InstanceList.jsx b/packages/web_ui/src/components/InstanceList.jsx index 89db9a6f..e9503903 100644 --- a/packages/web_ui/src/components/InstanceList.jsx +++ b/packages/web_ui/src/components/InstanceList.jsx @@ -1,5 +1,5 @@ import React from "react"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { message, Button, Space, Table } from "antd"; import CopyOutlined from "@ant-design/icons/CopyOutlined"; @@ -12,7 +12,7 @@ const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "ba export default function InstanceList(props) { let account = useAccount(); - let history = useHistory(); + let navigate = useNavigate(); let [hostList] = useHostList(); function hostName(hostId) { @@ -102,7 +102,7 @@ export default function InstanceList(props) { pagination={false} onRow={(record, rowIndex) => ({ onClick: event => { - history.push(`/instances/${record.id}/view`); + navigate(`/instances/${record.id}/view`); }, })} />; diff --git a/packages/web_ui/src/components/InstanceViewPage.jsx b/packages/web_ui/src/components/InstanceViewPage.jsx index 74a74335..c9b59679 100644 --- a/packages/web_ui/src/components/InstanceViewPage.jsx +++ b/packages/web_ui/src/components/InstanceViewPage.jsx @@ -1,6 +1,6 @@ import React, { useContext, useState } from "react"; -import { useHistory, useParams } from "react-router-dom"; -import { Alert, Button, Descriptions, Dropdown, Menu, Modal, PageHeader, Space, Spin, Typography } from "antd"; +import { useNavigate, useParams } from "react-router-dom"; +import { Alert, Button, Descriptions, Dropdown, Menu, Modal, Space, Spin, Typography } from "antd"; import DeleteOutlined from "@ant-design/icons/DeleteOutlined"; import DownOutlined from "@ant-design/icons/DownOutlined"; @@ -8,6 +8,7 @@ import { libData } from "@clusterio/lib"; import { useAccount } from "../model/account"; import ControlContext from "./ControlContext"; +import PageHeader from "./PageHeader"; import PageLayout from "./PageLayout"; import PluginExtra from "./PluginExtra"; import InstanceConfigTree from "./InstanceConfigTree"; @@ -59,7 +60,7 @@ export default function InstanceViewPage(props) { let params = useParams(); let instanceId = Number(params.id); - let history = useHistory(); + let navigate = useNavigate(); let account = useAccount(); let control = useContext(ControlContext); @@ -83,7 +84,7 @@ export default function InstanceViewPage(props) { action={ @@ -126,9 +127,9 @@ export default function InstanceViewPage(props) { label: "Delete", }); } - let instanceButtonsMenu = { + let instanceButtonsMenuProps = { + items: instanceButtonMenuItems, + onClick: ({ key }) => { if (key === "export") { setExportingData(true); control.sendTo( @@ -162,13 +163,13 @@ export default function InstanceViewPage(props) { control.send( new libData.InstanceDeleteRequest(instanceId) ).then(() => { - history.push("/instances"); + navigate("/instances"); }).catch(notifyErrorHandler("Error deleting instance")); }, }); } - }} - />; + }, + }; let instanceButtons = { account.hasAnyPermission("core.instance.start", "core.instance.stop") @@ -180,14 +181,13 @@ export default function InstanceViewPage(props) { "core.instance.extract_players", "core.instance.kill", "core.instance.delete", - ) && + ) && } ; return diff --git a/packages/web_ui/src/components/InstancesPage.jsx b/packages/web_ui/src/components/InstancesPage.jsx index b943c04e..36b764cf 100644 --- a/packages/web_ui/src/components/InstancesPage.jsx +++ b/packages/web_ui/src/components/InstancesPage.jsx @@ -1,11 +1,12 @@ import React, { useContext, useState } from "react"; -import { useHistory } from "react-router-dom"; -import { Button, Form, Input, Modal, PageHeader } from "antd"; +import { useNavigate } from "react-router-dom"; +import { Button, Form, Input, Modal } from "antd"; import { libConfig, libData } from "@clusterio/lib"; import { useAccount } from "../model/account"; import ControlContext from "./ControlContext"; +import PageHeader from "./PageHeader"; import PageLayout from "./PageLayout"; import PluginExtra from "./PluginExtra"; import { useInstanceList } from "../model/instance"; @@ -14,8 +15,8 @@ import { notifyErrorHandler } from "../util/notify"; function CreateInstanceButton(props) { let control = useContext(ControlContext); - let history = useHistory(); - let [visible, setVisible] = useState(false); + let navigate = useNavigate(); + let [open, setOpen] = useState(false); let [form] = Form.useForm(); async function createInstance() { @@ -30,23 +31,23 @@ function CreateInstanceButton(props) { instanceConfig.set("instance.name", values.instanceName); let serializedConfig = instanceConfig.serialize("controller"); await control.send(new libData.InstanceCreateRequest(serializedConfig)); - setVisible(false); - history.push(`/instances/${instanceConfig.get("instance.id")}/view`); + setOpen(false); + navigate(`/instances/${instanceConfig.get("instance.id")}/view`); } return <> { createInstance().catch(notifyErrorHandler("Error creating instance")); }} - onCancel={() => { setVisible(false); }} + onCancel={() => { setOpen(false); }} destroyOnClose >
@@ -65,7 +66,6 @@ export default function InstancesPage() { return {account.hasPermission("core.instance.create") && } diff --git a/packages/web_ui/src/components/LoadScenarioModal.jsx b/packages/web_ui/src/components/LoadScenarioModal.jsx index cc290281..e4fba49d 100644 --- a/packages/web_ui/src/components/LoadScenarioModal.jsx +++ b/packages/web_ui/src/components/LoadScenarioModal.jsx @@ -8,7 +8,7 @@ import { notifyErrorHandler } from "../util/notify"; export default function LoadScenarioModal(props) { - let [visible, setVisible] = useState(false); + let [open, setOpen] = useState(false); let [loadingScenario, setLoadingScenario] = useState(false); let [form] = Form.useForm(); let control = useContext(ControlContext); @@ -55,7 +55,7 @@ export default function LoadScenarioModal(props) { { instanceId: props.instance.id }, new libData.InstanceLoadScenarioRequest(scenario, seed, mapGenSettings, mapSettings), ).then(() => { - setVisible(false); + setOpen(false); }).catch( notifyErrorHandler("Error loading scenario") ).finally(() => { @@ -80,7 +80,7 @@ export default function LoadScenarioModal(props) { } function handleCancel() { - setVisible(false); + setOpen(false); } return <> @@ -88,7 +88,7 @@ export default function LoadScenarioModal(props) { loading={loadingScenario} disabled={props.instance.status !== "stopped"} onClick={() => { - setVisible(true); + setOpen(true); }} > Load scenario @@ -97,7 +97,7 @@ export default function LoadScenarioModal(props) { title="Load Scenario" width={560} okText="Load" - visible={visible} + open={open} confirmLoading={loadingScenario} onOk={loadScenario} onCancel={handleCancel} diff --git a/packages/web_ui/src/components/ModPackViewPage.jsx b/packages/web_ui/src/components/ModPackViewPage.jsx index 3a8a4b26..f74ed148 100644 --- a/packages/web_ui/src/components/ModPackViewPage.jsx +++ b/packages/web_ui/src/components/ModPackViewPage.jsx @@ -1,7 +1,7 @@ import React, { Fragment, memo, useCallback, useEffect, useContext, useRef, useState } from "react"; -import { useHistory, useParams } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { - Button, Card, Checkbox, Col, ConfigProvider, Descriptions, Form, Input, PageHeader, Pagination, + Button, Card, Checkbox, Col, ConfigProvider, Descriptions, Form, Input, Pagination, Popconfirm, Row, Table, Tag, Typography, Select, Skeleton, Space, Spin, Switch, Modal, Tooltip, } from "antd"; import InfoCircleOutlined from "@ant-design/icons/InfoCircleOutlined"; @@ -20,6 +20,7 @@ import { useModPack } from "../model/mod_pack"; import { useModList } from "../model/mods"; import { useClipboard } from "../util/clipboard"; import notify, { notifyErrorHandler } from "../util/notify"; +import PageHeader from "./PageHeader"; import PageLayout from "./PageLayout"; import SectionHeader from "./SectionHeader"; import ModDetails from "./ModDetails"; @@ -485,7 +486,7 @@ function CopyButton(props) { }, []); let denied = clipboard.readPermissionState === "denied"; - return + return + - + ; } if (modifiedModPack.missing) { return - +

Mod pack with id {modPackId} was not found on the controller.

; } return @@ -714,7 +709,7 @@ export default function ModPackViewPage() { control.send( new libData.ModPackDeleteRequest(modPack.id) ).then(() => { - history.push("/mods"); + navigate("/mods"); }).catch(notifyErrorHandler("Error deleting mod pack")); }} > @@ -788,40 +783,35 @@ export default function ModPackViewPage() {
-
- - You have unsaved changes - - - + - - - - -
+ }).catch(notifyErrorHandler("Error deleting mod pack")); + }} + >Apply + + +
; diff --git a/packages/web_ui/src/components/ModsPage.jsx b/packages/web_ui/src/components/ModsPage.jsx index 12fd43e4..e158dbdb 100644 --- a/packages/web_ui/src/components/ModsPage.jsx +++ b/packages/web_ui/src/components/ModsPage.jsx @@ -1,6 +1,6 @@ import React, { Fragment, useState, useContext } from "react"; -import { useHistory } from "react-router-dom"; -import { Button, Form, Input, Modal, PageHeader, Popconfirm, Space, Table, Typography, Upload } from "antd"; +import { useNavigate } from "react-router-dom"; +import { Button, Form, Input, Modal, Popconfirm, Space, Table, Typography, Upload } from "antd"; import ImportOutlined from "@ant-design/icons/ImportOutlined"; import PlusOutlined from "@ant-design/icons/PlusOutlined"; import UploadOutlined from "@ant-design/icons/UploadOutlined"; @@ -12,6 +12,7 @@ import { useModList } from "../model/mods"; import { useModPackList } from "../model/mod_pack"; import { notifyErrorHandler } from "../util/notify"; import ControlContext from "./ControlContext"; +import PageHeader from "./PageHeader"; import PageLayout from "./PageLayout"; import PluginExtra from "./PluginExtra"; import SectionHeader from "./SectionHeader"; @@ -24,20 +25,20 @@ const { logger } = libLogging; function ImportModPackButton() { let control = useContext(ControlContext); - let history = useHistory(); - let [visible, setVisible] = useState(false); + let navigate = useNavigate(); + let [open, setOpen] = useState(false); let [form] = Form.useForm(); function close() { - setVisible(false); + setOpen(false); } return <> - + { setVisible(false); }} + onCancel={() => { setOpen(false); }} onOk={() => { (async () => { let values; @@ -48,7 +49,7 @@ function ImportModPackButton() { } const modPack = libData.ModPack.fromModPackString(values.string); await control.send(new libData.ModPackCreateRequest(modPack)); - history.push(`/mods/mod-packs/${modPack.id}/view`); + navigate(`/mods/mod-packs/${modPack.id}/view`); })().catch(notifyErrorHandler("Error creating mod pack")); }} > @@ -73,20 +74,20 @@ function ImportModPackButton() { function CreateModPackButton() { let control = useContext(ControlContext); - let history = useHistory(); - let [visible, setVisible] = useState(false); + let navigate = useNavigate(); + let [open, setOpen] = useState(false); let [form] = Form.useForm(); function close() { - setVisible(false); + setOpen(false); } return <> - + { setVisible(false); }} + onCancel={() => { setOpen(false); }} onOk={() => { (async () => { let values; @@ -100,7 +101,7 @@ function CreateModPackButton() { modPack.factorioVersion = values.factorioVersion; if (values.description) { modPack.description = values.description; } await control.send(new libData.ModPackCreateRequest(modPack.toJSON())); - history.push(`/mods/mod-packs/${modPack.id}/view`); + navigate(`/mods/mod-packs/${modPack.id}/view`); })().catch(notifyErrorHandler("Error creating mod pack")); }} > @@ -130,7 +131,7 @@ function CreateModPackButton() { export default function ModsPage() { let account = useAccount(); let control = useContext(ControlContext); - let history = useHistory(); + let navigate = useNavigate(); let [modList] = useModList(); let [modPackList] = useModPackList(); @@ -184,10 +185,7 @@ export default function ModsPage() { } return - + @@ -219,7 +217,7 @@ export default function ModsPage() { rowKey={modPack => modPack.id} onRow={(modPack, rowIndex) => ({ onClick: event => { - history.push(`/mods/mod-packs/${modPack.id}/view`); + navigate(`/mods/mod-packs/${modPack.id}/view`); }, })} /> diff --git a/packages/web_ui/src/components/PageHeader.jsx b/packages/web_ui/src/components/PageHeader.jsx new file mode 100644 index 00000000..14c42271 --- /dev/null +++ b/packages/web_ui/src/components/PageHeader.jsx @@ -0,0 +1,8 @@ +import React from "react"; + +export default function PageHeader(props) { + return
+

{props.title}

+ {props.extra &&
{props.extra}
} +
; +} diff --git a/packages/web_ui/src/components/PluginsPage.jsx b/packages/web_ui/src/components/PluginsPage.jsx index 3c3c5a46..d914ffec 100644 --- a/packages/web_ui/src/components/PluginsPage.jsx +++ b/packages/web_ui/src/components/PluginsPage.jsx @@ -1,5 +1,5 @@ import React, { useContext, useEffect, useState } from "react"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { Table } from "antd"; import notify from "../util/notify"; @@ -11,7 +11,7 @@ const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "ba export default function PluginsPage() { let plugins = useContext(ControlContext).plugins; - let history = useHistory(); + let navigate = useNavigate(); let [pluginList, setPluginList] = useState([]); useEffect(() => { @@ -79,7 +79,7 @@ export default function PluginsPage() { pagination={false} onRow={plugin => ({ onClick: event => { - history.push(`/plugins/${plugin.meta.name}/view`); + navigate(`/plugins/${plugin.meta.name}/view`); }, })} /> diff --git a/packages/web_ui/src/components/RoleViewPage.jsx b/packages/web_ui/src/components/RoleViewPage.jsx index a8f8248c..60266fe3 100644 --- a/packages/web_ui/src/components/RoleViewPage.jsx +++ b/packages/web_ui/src/components/RoleViewPage.jsx @@ -1,12 +1,13 @@ import React, { useEffect, useContext, useState } from "react"; -import { useHistory, useParams } from "react-router-dom"; -import { Button, Checkbox, Form, Input, PageHeader, Popconfirm, Spin } from "antd"; +import { useNavigate, useParams } from "react-router-dom"; +import { Button, Checkbox, Form, Input, Popconfirm, Spin } from "antd"; import DeleteOutlined from "@ant-design/icons/DeleteOutlined"; import { libData, libUsers } from "@clusterio/lib"; import { useAccount } from "../model/account"; import ControlContext from "./ControlContext"; +import PageHeader from "./PageHeader"; import PageLayout from "./PageLayout"; import PluginExtra from "./PluginExtra"; import { notifyErrorHandler } from "../util/notify"; @@ -42,7 +43,7 @@ export default function RoleViewPage() { let params = useParams(); let roleId = Number(params.id); - let history = useHistory(); + let navigate = useNavigate(); let account = useAccount(); let control = useContext(ControlContext); @@ -53,20 +54,14 @@ export default function RoleViewPage() { let nav = [{ name: "Roles", path: "/roles" }, { name: role["name"] || roleId }]; if (role.loading) { return - + ; } if (role.missing) { return - +

Role with id {roleId} was not found on the controller.

; } @@ -101,7 +96,6 @@ export default function RoleViewPage() { }} > {canUpdate && } @@ -114,7 +108,7 @@ export default function RoleViewPage() { control.send( new libData.RoleDeleteRequest(roleId) ).then(() => { - history.push("/roles"); + navigate("/roles"); }).catch(notifyErrorHandler("Error deleting role")); }} > diff --git a/packages/web_ui/src/components/RolesPage.jsx b/packages/web_ui/src/components/RolesPage.jsx index 62eca08e..b550edcd 100644 --- a/packages/web_ui/src/components/RolesPage.jsx +++ b/packages/web_ui/src/components/RolesPage.jsx @@ -1,12 +1,13 @@ import React, { useContext, useEffect, useState } from "react"; -import { useHistory } from "react-router-dom"; -import { Button, Form, Input, Modal, PageHeader, Table } from "antd"; +import { useNavigate } from "react-router-dom"; +import { Button, Form, Input, Modal, Table } from "antd"; import { libData } from "@clusterio/lib"; import { useAccount } from "../model/account"; import ControlContext from "./ControlContext"; import { notifyErrorHandler } from "../util/notify"; +import PageHeader from "./PageHeader"; import PageLayout from "./PageLayout"; import PluginExtra from "./PluginExtra"; @@ -15,8 +16,8 @@ const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "ba function CreateRoleButton() { let control = useContext(ControlContext); - let history = useHistory(); - let [visible, setVisible] = useState(false); + let navigate = useNavigate(); + let [open, setOpen] = useState(false); let [form] = Form.useForm(); async function createRole() { @@ -29,21 +30,21 @@ function CreateRoleButton() { let result = await control.send( new libData.RoleCreateRequest(values.roleName, values.description || "", []) ); - setVisible(false); - history.push(`/roles/${result.id}/view`); + setOpen(false); + navigate(`/roles/${result.id}/view`); } return <> { createRole().catch(notifyErrorHandler("Error creating role")); }} - onCancel={() => { setVisible(false); }} + onCancel={() => { setOpen(false); }} destroyOnClose >
@@ -61,7 +62,7 @@ function CreateRoleButton() { export default function RolesPage() { let account = useAccount(); let control = useContext(ControlContext); - let history = useHistory(); + let navigate = useNavigate(); let [roles, setRoles] = useState([]); @@ -73,7 +74,6 @@ export default function RolesPage() { return } /> @@ -94,7 +94,7 @@ export default function RolesPage() { rowKey={role => role.id} onRow={(role, rowIndex) => ({ onClick: event => { - history.push(`/roles/${role.id}/view`); + navigate(`/roles/${role.id}/view`); }, })} /> diff --git a/packages/web_ui/src/components/SavesList.jsx b/packages/web_ui/src/components/SavesList.jsx index 04b908d1..50a72ed0 100644 --- a/packages/web_ui/src/components/SavesList.jsx +++ b/packages/web_ui/src/components/SavesList.jsx @@ -19,17 +19,17 @@ import { notifyErrorHandler } from "../util/notify"; function RenameModal(props) { let control = useContext(ControlContext); - let [visible, setVisible] = useState(false); + let [open, setOpen] = useState(false); let [form] = Form.useForm(); return <> - + form.submit()} - onCancel={() => setVisible(false)} + onCancel={() => setOpen(false)} > { - setVisible(false); + setOpen(false); form.resetFields(); }).catch(notifyErrorHandler("Error renaming save")); }} @@ -58,17 +58,17 @@ function RenameModal(props) { function CopyModal(props) { let control = useContext(ControlContext); - let [visible, setVisible] = useState(false); + let [open, setOpen] = useState(false); let [form] = Form.useForm(); return <> - + form.submit()} - onCancel={() => setVisible(false)} + onCancel={() => setOpen(false)} > { - setVisible(false); + setOpen(false); form.resetFields(); }).catch(notifyErrorHandler("Error copying save")); }} @@ -98,18 +98,18 @@ function CopyModal(props) { function TransferModal(props) { let account = useAccount(); let control = useContext(ControlContext); - let [visible, setVisible] = useState(false); + let [open, setOpen] = useState(false); let [form] = Form.useForm(); let [instanceList] = useInstanceList(); return <> - + form.submit()} - onCancel={() => setVisible(false)} + onCancel={() => setOpen(false)} destroyOnClose > { - setVisible(false); + setOpen(false); let hide = message.loading("Transferring save...", 0); control.send(new libData.InstanceTransferSaveRequest( props.instanceId, diff --git a/packages/web_ui/src/components/SiteLayout.jsx b/packages/web_ui/src/components/SiteLayout.jsx index 2d83b3d5..86bcfcef 100644 --- a/packages/web_ui/src/components/SiteLayout.jsx +++ b/packages/web_ui/src/components/SiteLayout.jsx @@ -1,5 +1,5 @@ import React, { useContext, useEffect, useState } from "react"; -import { Switch, Route, useHistory } from "react-router-dom"; +import { Route, Routes, useNavigate } from "react-router-dom"; import { Dropdown, Layout, Menu } from "antd"; import UserOutlined from "@ant-design/icons/UserOutlined"; import webUiPackage from "../../package.json"; @@ -14,7 +14,7 @@ const { Header, Sider } = Layout; export default function SiteLayout(props) { - let history = useHistory(); + let navigate = useNavigate(); let [currentSidebarPath, setCurrentSidebarPath] = useState(null); let account = useAccount(); let plugins = useContext(ControlContext).plugins; @@ -26,20 +26,20 @@ export default function SiteLayout(props) { return null; } - let accountMenu = { + let accountMenuProps = { + onClick: ({ key }) => { if (key === "user") { - history.push(`/users/${account.name}/view`); + navigate(`/users/${account.name}/view`); } else if (key === "logOut") { account.logOut(); } - }} - items={[ + }, + items: [ { label: account.name, key: "user" }, { type: "divider" }, { label: "Log out", danger: true, key: "logOut" }, - ]} - />; + ], + }; let combinedPages = [...pages]; for (let plugin of plugins.values()) { @@ -88,7 +88,7 @@ export default function SiteLayout(props) { className="account-dropdown-header" placement="bottomRight" trigger="click" - overlay={accountMenu} + menu={accountMenuProps} > @@ -107,22 +107,24 @@ export default function SiteLayout(props) { defaultOpenKeys={[...menuGroups.keys()]} selectedKeys={[currentSidebarPath]} style={{ height: "100%", borderRight: 0 }} - onClick={({ key }) => history.push(key)} + onClick={({ key }) => navigate(key)} items={menuItems} /> - - {combinedPages.map(({path, sidebarPath, content}) => - - - {content} - - )} - - - - + + {combinedPages.map(({path, sidebarPath, content}) => + + + {content} + + } + />)} + } /> + ; diff --git a/packages/web_ui/src/components/UserViewPage.jsx b/packages/web_ui/src/components/UserViewPage.jsx index 76b203f6..9bc03dc7 100644 --- a/packages/web_ui/src/components/UserViewPage.jsx +++ b/packages/web_ui/src/components/UserViewPage.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useContext, useState } from "react"; -import { useHistory, useParams } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { - Button, Col, Descriptions, Form, Input, PageHeader, Popconfirm, + Button, Col, Descriptions, Form, Input, Popconfirm, Row, Table, Tag, Select, Space, Spin, Switch, } from "antd"; import DeleteOutlined from "@ant-design/icons/DeleteOutlined"; @@ -11,6 +11,7 @@ import { libErrors, libData } from "@clusterio/lib"; import { useAccount } from "../model/account"; import { useInstanceList } from "../model/instance"; import ControlContext from "./ControlContext"; +import PageHeader from "./PageHeader"; import PageLayout from "./PageLayout"; import PluginExtra from "./PluginExtra"; import SectionHeader from "./SectionHeader"; @@ -28,7 +29,7 @@ export default function UserViewPage() { let params = useParams(); let userName = params.name; - let history = useHistory(); + let navigate = useNavigate(); let account = useAccount(); let control = useContext(ControlContext); @@ -62,20 +63,14 @@ export default function UserViewPage() { let nav = [{ name: "Users", path: "/users" }, { name: userName }]; if (user.loading) { return - + ; } if (user.missing) { return - +

User with name {userName} was not found on the controller.

; } @@ -112,7 +107,6 @@ export default function UserViewPage() { return {userName} @@ -153,7 +147,7 @@ export default function UserViewPage() { control.send( new libData.UserDeleteRequest(userName) ).then(() => { - history.push("/users"); + navigate("/users"); }).catch(notifyErrorHandler("Error deleting user")); }} > @@ -339,7 +333,7 @@ export default function UserViewPage() { rowKey={([id]) => id} onRow={([id], rowIndex) => ({ onClick: event => { - history.push(`/instances/${id}/view`); + navigate(`/instances/${id}/view`); }, })} /> diff --git a/packages/web_ui/src/components/UsersPage.jsx b/packages/web_ui/src/components/UsersPage.jsx index 1d0d82ad..457b0ef2 100644 --- a/packages/web_ui/src/components/UsersPage.jsx +++ b/packages/web_ui/src/components/UsersPage.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useContext, useState } from "react"; -import { useHistory } from "react-router-dom"; -import { Button, Form, Input, Modal, PageHeader, Space, Table, Tag } from "antd"; +import { useNavigate } from "react-router-dom"; +import { Button, Form, Input, Modal, Space, Table, Tag } from "antd"; import { libData } from "@clusterio/lib"; @@ -8,6 +8,7 @@ import { useAccount } from "../model/account"; import ControlContext from "./ControlContext"; import { notifyErrorHandler } from "../util/notify"; import { formatDuration } from "../util/time_format"; +import PageHeader from "./PageHeader"; import PageLayout from "./PageLayout"; import PluginExtra from "./PluginExtra"; import { formatLastSeen, sortLastSeen, useUserList } from "../model/user"; @@ -17,8 +18,8 @@ const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "ba function CreateUserButton() { let control = useContext(ControlContext); - let history = useHistory(); - let [visible, setVisible] = useState(false); + let navigate = useNavigate(); + let [open, setOpen] = useState(false); let [form] = Form.useForm(); async function createUser() { @@ -29,21 +30,21 @@ function CreateUserButton() { } await control.send(new libData.UserCreateRequest(values.userName)); - setVisible(false); - history.push(`/users/${values.userName}/view`); + setOpen(false); + navigate(`/users/${values.userName}/view`); } return <> { createUser().catch(notifyErrorHandler("Error creating user")); }} - onCancel={() => { setVisible(false); }} + onCancel={() => { setOpen(false); }} destroyOnClose > @@ -58,7 +59,7 @@ function CreateUserButton() { export default function UsersPage() { let account = useAccount(); let control = useContext(ControlContext); - let history = useHistory(); + let navigate = useNavigate(); let [userList] = useUserList(); let [roles, setRoles] = useState(new Map()); @@ -73,7 +74,6 @@ export default function UsersPage() { return } /> @@ -120,7 +120,7 @@ export default function UsersPage() { rowKey={user => user.name} onRow={(user, rowIndex) => ({ onClick: event => { - history.push(`/users/${user.name}/view`); + navigate(`/users/${user.name}/view`); }, })} /> diff --git a/packages/web_ui/src/index.css b/packages/web_ui/src/index.css index 267f11bf..276865a8 100644 --- a/packages/web_ui/src/index.css +++ b/packages/web_ui/src/index.css @@ -1,5 +1,3 @@ -@import '~antd/dist/antd.dark.css'; - body { margin: 0; } @@ -67,29 +65,43 @@ body { padding-right: 64px; } -.site-page-header { +.page-header { padding: 0 0 16px; } -.section-header { +.site-layout-content > h2:first-child { + margin-top: 0; +} + +.section-header, .page-header { display: flex; flex-wrap: wrap; align-items: center; +} + +.section-header { margin-top: 16px; margin-bottom: 8px; } -.section-header-title { +.section-header-title, .page-header-title { flex: auto; overflow: hidden; - font-weight: bold; - font-size: 16px; - line-height: 1.5715; white-space: nowrap; text-overflow: ellipsis; } -.section-header-extra { +.section-header-title { + font-weight: bold; + font-size: 16px; + line-height: 1.5715; +} + +.page-header-title { + margin: 4px 0; +} + +.section-header-extra, .page-header-extra { margin-left: auto; } @@ -143,7 +155,8 @@ body { height: 64px; font-size: 18px; line-height: 72px; - text-align: center; + display: flex; + justify-content: center; } .account-dropdown-header::after { @@ -325,8 +338,8 @@ body { border: none; } -tr.no-expanded-padding > td, -.ant-table.ant-table-small .ant-table-tbody > tr.no-expanded-padding > td { +div.ant-table tr.no-expanded-padding > td, +div.ant-table.ant-table-small .ant-table-tbody > tr.no-expanded-padding > td { padding: 0; } @@ -354,3 +367,18 @@ tr.ant-table-row.deleted { tr.ant-table-row.deleted { background: #441c18; } + +.sticky-notice { + position: sticky; + inset: auto auto 24px auto; + background: #1f1f1f; + padding: 24px; + width: auto; + margin: 16px auto 0; + box-shadow: + 0 6px 16px 0 rgba(0, 0, 0, 0.08), + 0 3px 6px -4px rgba(0, 0, 0, 0.12), + 0 9px 28px 8px rgba(0, 0, 0, 0.05); + border-radius: 8px; + max-width: 384px; +} diff --git a/packages/web_ui/src/index.jsx b/packages/web_ui/src/index.jsx index 70bdc2e0..868f907f 100644 --- a/packages/web_ui/src/index.jsx +++ b/packages/web_ui/src/index.jsx @@ -23,6 +23,7 @@ export { default as InstanceStatusTag } from "./components/InstanceStatusTag"; export { default as StartStopInstanceButton } from "./components/StartStopInstanceButton"; export { statusColors } from "./components/InstanceStatusTag"; +export { default as PageHeader } from "./components/PageHeader"; export { default as PageLayout } from "./components/PageLayout"; export { default as SavesList } from "./components/SavesList"; export { default as SectionHeader } from "./components/SectionHeader"; diff --git a/plugins/global_chat/package.json b/plugins/global_chat/package.json index 9f571c07..60ecce5f 100644 --- a/plugins/global_chat/package.json +++ b/plugins/global_chat/package.json @@ -14,7 +14,7 @@ "url": "https://github.com/clusterio/clusterio/issues" }, "engines": { - "node": ">=12" + "node": ">=18" }, "peerDependencies": { "@clusterio/lib": "^2.0.0-alpha.0" @@ -22,9 +22,9 @@ "devDependencies": { "@clusterio/lib": "^2.0.0-alpha.13", "@clusterio/web_ui": "^2.0.0-alpha.13", - "webpack": "^5.36.2", - "webpack-cli": "^4.7.0", - "webpack-merge": "^5.2.0" + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4", + "webpack-merge": "^5.9.0" }, "publishConfig": { "access": "public" diff --git a/plugins/inventory_sync/messages.js b/plugins/inventory_sync/messages.js index a7f003c0..f1e58e98 100644 --- a/plugins/inventory_sync/messages.js +++ b/plugins/inventory_sync/messages.js @@ -47,6 +47,7 @@ AcquireRequest.Response = class Response { } static jsonSchema = { + type: "object", required: ["status"], properties: { "status": { @@ -165,6 +166,7 @@ DownloadRequest.Response = class Response { } static jsonSchema = { + type: "object", required: ["playerData"], properties: { "playerData": { type: ["object", "null"] }, diff --git a/plugins/inventory_sync/package.json b/plugins/inventory_sync/package.json index ffd6117a..ce1a56ea 100644 --- a/plugins/inventory_sync/package.json +++ b/plugins/inventory_sync/package.json @@ -14,7 +14,7 @@ "url": "https://github.com/clusterio/factorioClusterio/issues" }, "engines": { - "node": ">=12" + "node": ">=18" }, "peerDependencies": { "@clusterio/lib": "^2.0.0-alpha.0" @@ -25,12 +25,12 @@ "devDependencies": { "@clusterio/lib": "^2.0.0-alpha.13", "@clusterio/web_ui": "^2.0.0-alpha.13", - "antd": "^4.16.0", - "react": "^16.13.1", - "react-dom": "^16.13.1", - "webpack": "^5.36.2", - "webpack-cli": "^4.7.0", - "webpack-merge": "^5.2.0" + "antd": "^5.7.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4", + "webpack-merge": "^5.9.0" }, "publishConfig": { "access": "public" diff --git a/plugins/player_auth/package.json b/plugins/player_auth/package.json index cc7bf708..b5a886e8 100644 --- a/plugins/player_auth/package.json +++ b/plugins/player_auth/package.json @@ -14,7 +14,7 @@ "url": "https://github.com/clusterio/clusterio/issues" }, "engines": { - "node": ">=12" + "node": ">=18" }, "peerDependencies": { "@clusterio/lib": "^2.0.0-alpha.0" @@ -26,13 +26,13 @@ "devDependencies": { "@clusterio/lib": "^2.0.0-alpha.13", "@clusterio/web_ui": "^2.0.0-alpha.13", - "antd": "^4.16.0", - "phin": "^3.5.1", - "react": "^16.13.1", - "react-dom": "^16.13.1", - "webpack": "^5.36.2", - "webpack-cli": "^4.7.0", - "webpack-merge": "^5.2.0" + "antd": "^5.7.3", + "phin": "^3.7.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4", + "webpack-merge": "^5.9.0" }, "publishConfig": { "access": "public" diff --git a/plugins/research_sync/messages.js b/plugins/research_sync/messages.js index 33a77078..c131021e 100644 --- a/plugins/research_sync/messages.js +++ b/plugins/research_sync/messages.js @@ -55,6 +55,7 @@ class ProgressEvent { technologies: { type: "array", items: { + type: "object", required: ["name", "level", "progress"], properties: { name: { type: "string" }, diff --git a/plugins/research_sync/package.json b/plugins/research_sync/package.json index 0200ed37..80f03da6 100644 --- a/plugins/research_sync/package.json +++ b/plugins/research_sync/package.json @@ -14,7 +14,7 @@ "url": "https://github.com/clusterio/clusterio/issues" }, "engines": { - "node": ">=12" + "node": ">=18" }, "peerDependencies": { "@clusterio/lib": "^2.0.0-alpha.0" @@ -25,9 +25,9 @@ "devDependencies": { "@clusterio/lib": "^2.0.0-alpha.13", "@clusterio/web_ui": "^2.0.0-alpha.13", - "webpack": "^5.36.2", - "webpack-cli": "^4.7.0", - "webpack-merge": "^5.2.0" + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4", + "webpack-merge": "^5.9.0" }, "publishConfig": { "access": "public" diff --git a/plugins/statistics_exporter/package.json b/plugins/statistics_exporter/package.json index 58d5b51e..f3076d0a 100644 --- a/plugins/statistics_exporter/package.json +++ b/plugins/statistics_exporter/package.json @@ -14,7 +14,7 @@ "url": "https://github.com/clusterio/clusterio/issues" }, "engines": { - "node": ">=12" + "node": ">=18" }, "peerDependencies": { "@clusterio/lib": "^2.0.0-alpha.0" @@ -22,9 +22,9 @@ "devDependencies": { "@clusterio/lib": "^2.0.0-alpha.13", "@clusterio/web_ui": "^2.0.0-alpha.13", - "webpack": "^5.36.2", - "webpack-cli": "^4.7.0", - "webpack-merge": "^5.2.0" + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4", + "webpack-merge": "^5.9.0" }, "publishConfig": { "access": "public" diff --git a/plugins/subspace_storage/messages.js b/plugins/subspace_storage/messages.js index 643b8cf4..44a8ec8b 100644 --- a/plugins/subspace_storage/messages.js +++ b/plugins/subspace_storage/messages.js @@ -46,6 +46,7 @@ class PlaceEvent { } static jsonSchema = { + type: "object", required: ["items"], properties: { "items": { @@ -74,6 +75,7 @@ class RemoveRequest { } static jsonSchema = { + type: "object", required: ["items"], properties: { "items": { @@ -113,6 +115,7 @@ class UpdateStorageEvent { } static jsonSchema = { + type: "object", required: ["items"], properties: { "items": { @@ -142,6 +145,7 @@ class SetStorageSubscriptionRequest { } static jsonSchema = { + type: "object", required: ["storage"], properties: { "storage": { type: "boolean" }, diff --git a/plugins/subspace_storage/package.json b/plugins/subspace_storage/package.json index c523de98..40748d6a 100644 --- a/plugins/subspace_storage/package.json +++ b/plugins/subspace_storage/package.json @@ -14,7 +14,7 @@ "url": "https://github.com/clusterio/clusterio/issues" }, "engines": { - "node": ">=12" + "node": ">=18" }, "peerDependencies": { "@clusterio/lib": "^2.0.0-alpha.0" @@ -25,12 +25,12 @@ "devDependencies": { "@clusterio/lib": "^2.0.0-alpha.13", "@clusterio/web_ui": "^2.0.0-alpha.13", - "antd": "^4.16.0", - "react": "^16.13.1", - "react-dom": "^16.13.1", - "webpack": "^5.36.2", - "webpack-cli": "^4.7.0", - "webpack-merge": "^5.2.0" + "antd": "^5.7.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4", + "webpack-merge": "^5.9.0" }, "publishConfig": { "access": "public" diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index 48d62417..cb4f1744 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -246,7 +246,7 @@ describe("Integration of Clusterio", function() { let value = JSON.stringify({ lan: true, public: false }).replace( /"/g, process.platform === "win32" ? '""' : '\\"' ); - await execCtl(`instance config set-prop test factorio.settings visibility "'${value}'"`); + await execCtl(`instance config set-prop test factorio.settings visibility "${value}"`); await execCtl("instance config set-prop test factorio.settings require_user_verification false"); }); }); @@ -442,7 +442,7 @@ describe("Integration of Clusterio", function() { ]; for (let [prop, value] of testConfigs) { - value = `"'${JSON.stringify(value).replace(/"/g, process.platform === "win32" ? '""' : '\\"')}'"`; + value = `"${JSON.stringify(value).replace(/"/g, process.platform === "win32" ? '""' : '\\"')}"`; let args = `test factorio.settings ${prop} ${value}`; await execCtl(`instance config set-prop ${args}`); }