Merge pull request #487 from Hornwitser/node-js-16

Bump supported Node.js to v18 and update dependencies
This commit is contained in:
Hornwitser authored and GitHub committed 2023-07-27 23:17:09 +02:00
commit bb58670597
51 files changed
+445 -412

No files matched your search

+9 -9
View File
@@ -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
+2 -2
View File
@@ -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
+5 -5
View File
@@ -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:
+2 -2
View File
@@ -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.
+8 -8
View File
@@ -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"
}
}
+18 -18
View File
@@ -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"
+24 -24
View File
@@ -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");
+6 -6
View File
@@ -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"
+6 -6
View File
@@ -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"
+3
View File
@@ -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"""""'
+7 -7
View File
@@ -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"
+5 -4
View File
@@ -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" },
+4 -1
View File
@@ -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" }],
},
},
+9 -1
View File
@@ -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"] },
+2
View File
@@ -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],
},
},
+3 -1
View File
@@ -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}`);
+14 -15
View File
@@ -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"
+3 -4
View File
@@ -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 = {
+16 -16
View File
@@ -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": {
+3 -4
View File
@@ -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(<App control={control}/>, document.getElementById("root"));
const root = createRoot(document.getElementById("root"));
root.render(<App control={control}/>);
}
+10 -6
View File
@@ -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 (
<ErrorBoundary Component={ErrorCard}>
<ControlContext.Provider value={props.control}>
<BrowserRouter basename={webRoot}>
{page}
</BrowserRouter>
</ControlContext.Provider>
<ConfigProvider
theme={{ algorithm: theme.darkAlgorithm }}
>
<ControlContext.Provider value={props.control}>
<BrowserRouter basename={webRoot}>
{page}
</BrowserRouter>
</ControlContext.Provider>
</ConfigProvider>
</ErrorBoundary>
);
}
@@ -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 <>
<Button {...props.buttonProps} onClick={open}>
<Button {...props.buttonProps} onClick={() => setOpen(true)}>
{props.buttonContent || "Assign"}
</Button>
<Modal
title="Assign Instance"
okText="Assign"
visible={visible}
open={open}
confirmLoading={applying}
onOk={handleAssign}
onCancel={handleCancel}
@@ -8,7 +8,7 @@ import { notifyErrorHandler } from "../util/notify";
export default function CreateSaveModal(props) {
let [visible, setVisible] = useState(false);
let [open, setOpen] = useState(false);
let [creatingSave, setCreatingSave] = useState(false);
let [form] = Form.useForm();
let control = useContext(ControlContext);
@@ -56,7 +56,7 @@ export default function CreateSaveModal(props) {
new libData.InstanceCreateSaveRequest(name, seed, mapGenSettings, mapSettings),
).then(() => {
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}
@@ -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 <PageLayout nav={nav}>
<PageHeader
className="site-page-header"
title={hostId}
/>
<PageHeader title={hostId} />
<p>Host with id {hostId} was not found on the controller.</p>
</PageLayout>;
}
return <PageLayout nav={nav}>
<PageHeader
className="site-page-header"
title={host.name || hostId}
/>
<PageHeader title={host.name || hostId} />
<Descriptions bordered size="small" column={{ xs: 1, sm: 2, xl: 4 }}>
<Descriptions.Item label="Name">{host["name"]}</Descriptions.Item>
+11 -11
View File
@@ -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 <>
<Button
onClick={() => { setVisible(true); }}
onClick={() => { setOpen(true); }}
>Generate Token</Button>
<Modal
title="Generate Host Token"
visible={visible}
open={open}
footer={null}
onCancel={() => {
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 <PageLayout nav={[{ name: "Hosts" }]}>
<PageHeader
className="site-page-header"
title="Hosts"
extra={account.hasPermission("core.host.generate_token") && <GenerateHostTokenButton />}
/>
@@ -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`);
},
})}
/>
@@ -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`);
},
})}
/>;
@@ -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={
<Button
type="text"
onClick={() => { history.push("/instances"); }}
onClick={() => { navigate("/instances"); }}
>
Go back to instances list
</Button>
@@ -126,9 +127,9 @@ export default function InstanceViewPage(props) {
label: "Delete",
});
}
let instanceButtonsMenu = <Menu
items={instanceButtonMenuItems}
onClick={({ key }) => {
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 = <Space>
{
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",
) && <Dropdown placement="bottomRight" trigger={["click"]} overlay={instanceButtonsMenu}>
) && <Dropdown placement="bottomRight" trigger={["click"]} menu={instanceButtonsMenuProps}>
<Button>More <DownOutlined /></Button>
</Dropdown>}
</Space>;
return <PageLayout nav={nav}>
<PageHeader
className="site-page-header"
title={instance.name}
extra={instanceButtons}
/>
@@ -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 <>
<Button
type="primary"
onClick={() => {
setVisible(true);
setOpen(true);
}}
>Create</Button>
<Modal
title="Create Instance"
okText="Create"
visible={visible}
open={open}
onOk={() => { createInstance().catch(notifyErrorHandler("Error creating instance")); }}
onCancel={() => { setVisible(false); }}
onCancel={() => { setOpen(false); }}
destroyOnClose
>
<Form form={form}>
@@ -65,7 +66,6 @@ export default function InstancesPage() {
return <PageLayout nav={[{ name: "Instances" }]}>
<PageHeader
className="site-page-header"
title="Instances"
extra={<>
{account.hasPermission("core.instance.create") && <CreateInstanceButton />}
@@ -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}
@@ -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 <Tooltip title={denied ? clipboard.deniedReason : "Copied!"} visible={denied ? undefined : copiedVisible}>
return <Tooltip title={denied ? clipboard.deniedReason : "Copied!"} open={denied ? undefined : copiedVisible}>
<Button disabled={denied} onClick={() => {
try {
clipboard.writeText(props.content);
@@ -503,19 +504,19 @@ function CopyButton(props) {
}
function ExportButton(props) {
let [visible, setVisible] = useState(false);
let [open, setOpen] = useState(false);
function close() {
setVisible(false);
setOpen(false);
}
let exportString;
if (visible) { exportString = props.modPack.toModPackString(); }
if (open) { exportString = props.modPack.toModPackString(); }
return <>
<Button icon={<ExportOutlined />} onClick={() => { setVisible(true); }}>Export to string</Button>
<Button icon={<ExportOutlined />} onClick={() => { setOpen(true); }}>Export to string</Button>
<Modal
title="Mod Pack String"
visible={visible}
open={open}
onOk={close}
onCancel={close}
destroyOnClose
@@ -593,11 +594,12 @@ function applyModPackChanges(modPack, changes) {
throw new Error(`Unknown change type ${change.type}`);
}
}
return modifiedModPack;
}
export default function ModPackViewPage() {
let account = useAccount();
let history = useHistory();
let navigate = useNavigate();
let control = useContext(ControlContext);
let params = useParams();
let modPackId = Number(params.id);
@@ -681,27 +683,20 @@ export default function ModPackViewPage() {
let nav = [{ name: "Mods", path: "/mods" }, { name: "Mod Packs" }, { name: modPack.name || modPackId }];
if (modifiedModPack.loading) {
return <PageLayout nav={nav}>
<PageHeader
className="site-page-header"
title={modPackId}
/>
<PageHeader title={modPackId} />
<Spin size="large" />
</PageLayout>;
}
if (modifiedModPack.missing) {
return <PageLayout nav={nav}>
<PageHeader
className="site-page-header"
title="Mod Pack Not Found"
/>
<PageHeader title="Mod Pack Not Found" />
<p>Mod pack with id {modPackId} was not found on the controller.</p>
</PageLayout>;
}
return <PageLayout nav={nav}>
<PageHeader
className="site-page-header"
title={modPack.name}
extra={<Space>
<ExportButton modPack={modifiedModPack}/>
@@ -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() {
</Form>
<div
className="ant-notification ant-notification-bottom"
className="sticky-notice"
style={{
visibility: changes.length ? "visible" : "hidden",
inset: "auto auto 24px auto",
position: "sticky",
marginTop: 16,
}}
>
<div className="ant-notification-notice" style={{ width: "auto", maxWidth: 384, marginBottom: 0 }}>
<Row style={{ alignItems: "center", rowGap: 12 }}>
<Col flex="auto">You have unsaved changes</Col>
<Col flex="0 0 auto" style={{ marginLeft: "auto" }}>
<Space>
<Button
onClick={() => {
<Row style={{ alignItems: "center", rowGap: 12 }}>
<Col flex="auto">You have unsaved changes</Col>
<Col flex="0 0 auto" style={{ marginLeft: "auto" }}>
<Space>
<Button
onClick={() => {
form.resetFields();
setChanges([]);
}}
>Revert</Button>
<Button
type="primary"
onClick={() => {
control.send(
new libData.ModPackUpdateRequest(modifiedModPack)
).then(() => {
form.resetFields();
setChanges([]);
}}
>Revert</Button>
<Button
type="primary"
onClick={() => {
control.send(
new libData.ModPackUpdateRequest(modifiedModPack)
).then(() => {
form.resetFields();
setChanges([]);
}).catch(notifyErrorHandler("Error deleting mod pack"));
}}
>Apply</Button>
</Space>
</Col>
</Row>
</div>
}).catch(notifyErrorHandler("Error deleting mod pack"));
}}
>Apply</Button>
</Space>
</Col>
</Row>
</div>
</PageLayout>;
+20 -22
View File
@@ -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 <>
<Button icon={<ImportOutlined />} onClick={() => { setVisible(true); }}>Import string</Button>
<Button icon={<ImportOutlined />} onClick={() => { setOpen(true); }}>Import string</Button>
<Modal
title="Import Mod Pack String"
visible={visible}
open={open}
okText="Import"
cancelText="Cancel"
onCancel={() => { 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 <>
<Button type="primary" icon={<PlusOutlined />} onClick={() => { setVisible(true); }}>Create</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={() => { setOpen(true); }}>Create</Button>
<Modal
title="Create Mod Pack"
visible={visible}
open={open}
okText="Create"
cancelText="Cancel"
onCancel={() => { 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 <PageLayout nav={[{ name: "Mods" }]}>
<PageHeader
className="site-page-header"
title="Mods"
/>
<PageHeader title="Mods" />
<SectionHeader
title="Mod Packs"
extra={<Space>
@@ -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`);
},
})}
/>
@@ -0,0 +1,8 @@
import React from "react";
export default function PageHeader(props) {
return <div className="page-header">
<h2 className="page-header-title">{props.title}</h2>
{props.extra && <div className="page-header-extra">{props.extra}</div>}
</div>;
}
@@ -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`);
},
})}
/>
@@ -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 <PageLayout nav={nav}>
<PageHeader
className="site-page-header"
title={roleId}
/>
<PageHeader title={roleId} />
<Spin size="large" />
</PageLayout>;
}
if (role.missing) {
return <PageLayout nav={nav}>
<PageHeader
className="site-page-header"
title="Role not found"
/>
<PageHeader title="Role not found" />
<p>Role with id {roleId} was not found on the controller.</p>
</PageLayout>;
}
@@ -101,7 +96,6 @@ export default function RoleViewPage() {
}}
>
<PageHeader
className="site-page-header"
title={role["name"]}
extra={<>
{canUpdate && <Button type={edited ? "primary" : "default"} htmlType="submit">Apply</Button>}
@@ -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"));
}}
>
+12 -12
View File
@@ -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 <>
<Button
type="primary"
onClick={() => { setVisible(true); }}
onClick={() => { setOpen(true); }}
>Create</Button>
<Modal
title="Create Role"
okText="Create"
visible={visible}
open={open}
onOk={() => { createRole().catch(notifyErrorHandler("Error creating role")); }}
onCancel={() => { setVisible(false); }}
onCancel={() => { setOpen(false); }}
destroyOnClose
>
<Form form={form}>
@@ -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 <PageLayout nav={[{ name: "Roles" }]}>
<PageHeader
className="site-page-header"
title="Roles"
extra={account.hasPermission("core.role.create") && <CreateRoleButton />}
/>
@@ -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`);
},
})}
/>
+15 -15
View File
@@ -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 <>
<Button disabled={props.disabled} onClick={() => setVisible(true)}>Rename</Button>
<Button disabled={props.disabled} onClick={() => setOpen(true)}>Rename</Button>
<Modal
title="Rename save"
okText="Rename"
visible={visible}
open={open}
onOk={() => form.submit()}
onCancel={() => setVisible(false)}
onCancel={() => setOpen(false)}
>
<Form
form={form}
@@ -39,7 +39,7 @@ function RenameModal(props) {
control.send(
new libData.InstanceRenameSaveRequest(props.instanceId, props.save.name, values.newName)
).then(() => {
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 <>
<Button disabled={props.disabled} onClick={() => setVisible(true)}>Copy</Button>
<Button disabled={props.disabled} onClick={() => setOpen(true)}>Copy</Button>
<Modal
title="Copy save"
okText="Copy"
visible={visible}
open={open}
onOk={() => form.submit()}
onCancel={() => setVisible(false)}
onCancel={() => setOpen(false)}
>
<Form
form={form}
@@ -78,7 +78,7 @@ function CopyModal(props) {
control.send(
new libData.InstanceCopySaveRequest(props.instanceId, props.save.name, values.newName)
).then(() => {
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 <>
<Button disabled={props.disabled} onClick={() => setVisible(true)}>Transfer</Button>
<Button disabled={props.disabled} onClick={() => setOpen(true)}>Transfer</Button>
<Modal
title="Transfer save"
okText="Transfer"
visible={visible}
open={open}
onOk={() => form.submit()}
onCancel={() => setVisible(false)}
onCancel={() => setOpen(false)}
destroyOnClose
>
<Form
@@ -118,7 +118,7 @@ function TransferModal(props) {
wrapperCol={{ span: 18 }}
initialValues={{ transferredName: props.save.name, copy: false }}
onFinish={values => {
setVisible(false);
setOpen(false);
let hide = message.loading("Transferring save...", 0);
control.send(new libData.InstanceTransferSaveRequest(
props.instanceId,
+24 -22
View File
@@ -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 = <Menu
onClick={({ key }) => {
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}
>
<UserOutlined/>
</Dropdown>
@@ -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}
/>
</Sider>
<Layout className="site-layout-content-container">
<Switch>
{combinedPages.map(({path, sidebarPath, content}) => <Route exact path={path} key={path}>
<SetSidebar path={sidebarPath ? sidebarPath : path} />
<ErrorBoundary Component={ErrorPage}>
{content}
</ErrorBoundary>
</Route>)}
<Route>
<SetSidebar path={null} />
</Route>
</Switch>
<Routes>
{combinedPages.map(({path, sidebarPath, content}) => <Route
path={path}
key={path}
element={<>
<SetSidebar path={sidebarPath ? sidebarPath : path} />
<ErrorBoundary Component={ErrorPage}>
{content}
</ErrorBoundary>
</>}
/>)}
<Route element={<SetSidebar path={null} />} />
</Routes>
</Layout>
</Layout>
</Layout>;
@@ -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 <PageLayout nav={nav}>
<PageHeader
className="site-page-header"
title={userName}
/>
<PageHeader title={userName} />
<Spin size="large" />
</PageLayout>;
}
if (user.missing) {
return <PageLayout nav={nav}>
<PageHeader
className="site-page-header"
title="User not found"
/>
<PageHeader title="User not found" />
<p>User with name {userName} was not found on the controller.</p>
</PageLayout>;
}
@@ -112,7 +107,6 @@ export default function UserViewPage() {
return <PageLayout nav={nav}>
<PageHeader
className="site-page-header"
title={<Space>
{userName}
<span>
@@ -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`);
},
})}
/>
+12 -12
View File
@@ -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 <>
<Button
type="primary"
onClick={() => { setVisible(true); }}
onClick={() => { setOpen(true); }}
>Create</Button>
<Modal
title="Create User"
okText="Create"
visible={visible}
open={open}
onOk={() => { createUser().catch(notifyErrorHandler("Error creating user")); }}
onCancel={() => { setVisible(false); }}
onCancel={() => { setOpen(false); }}
destroyOnClose
>
<Form form={form}>
@@ -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 <PageLayout nav={[{ name: "Users" }]}>
<PageHeader
className="site-page-header"
title="Users"
extra={account.hasPermission("core.user.create") && <CreateUserButton />}
/>
@@ -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`);
},
})}
/>
+40 -12
View File
@@ -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;
}
+1
View File
@@ -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";
+4 -4
View File
@@ -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"
+2
View File
@@ -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"] },
+7 -7
View File
@@ -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"
+8 -8
View File
@@ -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"
+1
View File
@@ -55,6 +55,7 @@ class ProgressEvent {
technologies: {
type: "array",
items: {
type: "object",
required: ["name", "level", "progress"],
properties: {
name: { type: "string" },
+4 -4
View File
@@ -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"
+4 -4
View File
@@ -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"
+4
View File
@@ -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" },
+7 -7
View File
@@ -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"
+2 -2
View File
@@ -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}`);
}