diff --git a/.gitignore b/.gitignore index 57a52e6f..a2891657 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,6 @@ Factorio* package-lock.json mod_cache.json static/export -packages/master/static/external saves /temp/ diff --git a/packages/master/.gitignore b/packages/master/.gitignore new file mode 100644 index 00000000..4d2280f9 --- /dev/null +++ b/packages/master/.gitignore @@ -0,0 +1,2 @@ +# Webpack output +static diff --git a/packages/master/master.js b/packages/master/master.js index 980e96ac..164b3c53 100755 --- a/packages/master/master.js +++ b/packages/master/master.js @@ -1553,7 +1553,9 @@ async function startServer() { }) .demandCommand(1, "You need to specify a command to run"); }) - .command("run", "Run master server") + .command("run", "Run master server", yargs => { + yargs.option("dev", { hidden: true, type: "boolean", nargs: 0 }); + }) .demandCommand(1, "You need to specify a command to run") .strict() .argv @@ -1634,6 +1636,16 @@ async function startServer() { // If we get here the command was run + if (args.dev) { + console.log("Development mode enabled"); + const webpack = require("webpack"); + const middleware = require("webpack-dev-middleware"); + const webpackConfig = require("./webpack.config"); + + const compiler = webpack(webpackConfig({})); + app.use(middleware(compiler, {})); + } + let secondSigint = false; process.on("SIGINT", () => { if (secondSigint) { diff --git a/packages/master/package.json b/packages/master/package.json index da109f5c..a18ea824 100644 --- a/packages/master/package.json +++ b/packages/master/package.json @@ -6,6 +6,7 @@ "version": "2.0.0-alpha0", "main": "master.js", "scripts": { + "build": "webpack --env.production", "test": "echo \"Error: no test specified\" && exit 1" }, "bin": { @@ -32,5 +33,24 @@ "set-blocking": "^2.0.0", "ws": "^7.2.3", "yargs": "^14.2.0" + }, + "devDependencies": { + "@ant-design/icons": "^4.2.2", + "@babel/core": "^7.11.6", + "@babel/preset-react": "^7.10.4", + "antd": "^4.6.5", + "babel-loader": "^8.1.0", + "clean-webpack-plugin": "^3.0.0", + "css-loader": "^4.3.0", + "fast-deep-equal": "^3.1.3", + "html-webpack-plugin": "^4.5.0", + "null-loader": "^4.0.0", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-router-dom": "^5.2.0", + "style-loader": "^1.2.1", + "webpack": "^4.44.2", + "webpack-cli": "^3.3.12", + "webpack-dev-middleware": "^3.7.2" } } diff --git a/packages/master/web/components/data-table.jsx b/packages/master/web/components/data-table.jsx index 19958ef4..7d2a5718 100644 --- a/packages/master/web/components/data-table.jsx +++ b/packages/master/web/components/data-table.jsx @@ -1,20 +1,9 @@ import React, { Component } from "react"; -import { - Collapse, Row, Col, Modal, Table, - Form, - Input, - Button, - Radio, - Select, - Cascader, - DatePicker, - InputNumber, - TreeSelect, - Switch, - Popover -} from "antd"; +import { Collapse, Row, Col, Modal, Table, Form, Input, Button, Popover } from "antd"; +import PlusOutlined from "@ant-design/icons/PlusOutlined" +import DeleteOutlined from "@ant-design/icons/DeleteOutlined"; + import notify from "../util/notify"; -import { PlusOutlined, DeleteOutlined } from "@ant-design/icons"; const { Panel } = Collapse; @@ -45,10 +34,12 @@ class DataTable extends Component { record: {}, } } + componentDidMount() { this.getData() } - getData = async (pagination, filters, sorter) => { + + async getData(pagination, filters, sorter) { /* pagination = { pageSize: 10, // items per page @@ -84,21 +75,22 @@ class DataTable extends Component { filters, }) } - showEditModal = (record, index) => { + + showEditModal(record, index) { if (!this.state.showEditModal) this.setState({ record: record, showEditModal: true, }) } - hideEditModal = () => { + hideEditModal() { this.setState({ showEditModal: false, inserting: false, record: {} }) } - onEditFinish = async values => { + async onEditFinish(values) { console.log("Finished editing", values) if (this.state.inserting) { // Create new record @@ -116,7 +108,7 @@ class DataTable extends Component { // Force refresh of data this.getData() } - deleteRecord = async record => { + async deleteRecord(record) { delete record.key // Delete "null" values as they seem to cause deletes to fail silently when the value is null and field type is float for (let key in record) { @@ -148,7 +140,7 @@ class DataTable extends Component { object. This way the end user isn't affected by our event handlers. */ let externalOnRow = { - ...this.props.TableProps?.onRow?.(record, rowIndex), + ...(this.props.TableProps && this.props.TableProps.onRow && this.props.TableProps.onRow(record, rowIndex)), } return { onClick: event => { @@ -219,7 +211,7 @@ class DataTable extends Component { })} dataSource={this.state.tableData} pagination={this.state.pagination.total > 10 ? this.state.pagination : false} - onChange={this.getData} + onChange={this.getData.bind(this)} /> {this.props.AddRecord && } {instance.status !== "stopped" && } {instance.status !== "running" && } @@ -283,7 +286,7 @@ export class InstanceView extends Component {
{this.state.editingConfig ? diff --git a/packages/master/web/pages/instances/instancesTable.jsx b/packages/master/web/pages/instances/instancesTable.jsx index a839e1c2..21a86e15 100644 --- a/packages/master/web/pages/instances/instancesTable.jsx +++ b/packages/master/web/pages/instances/instancesTable.jsx @@ -1,8 +1,8 @@ import React, { Component } from "react" -import { Card, Table } from "antd" +import { Card } from "antd" import { listSlaves, listInstances, createInstance } from "../../util/wslink" import DataTable from "../../components/data-table" -const config = require("../../lib/config") +const config = require("@clusterio/lib/config") export class InstancesTable extends Component { constructor(props) { diff --git a/packages/master/web/pages/permissions/permissionsTable.jsx b/packages/master/web/pages/permissions/permissionsTable.jsx index 17274f58..eb63f893 100644 --- a/packages/master/web/pages/permissions/permissionsTable.jsx +++ b/packages/master/web/pages/permissions/permissionsTable.jsx @@ -1,5 +1,5 @@ import React, { Component } from "react" -import { Card, Table } from "antd" +import { Card } from "antd" import { listSlaves, listInstances, listPermissions } from "../../util/wslink" import DataTable from "../../components/data-table" diff --git a/packages/master/web/pages/roles/role.jsx b/packages/master/web/pages/roles/role.jsx index cec23e9b..7634e616 100644 --- a/packages/master/web/pages/roles/role.jsx +++ b/packages/master/web/pages/roles/role.jsx @@ -1,5 +1,8 @@ import React, { Component } from "react" -import { Card, Table, Button, List, Popover, Select, Divider, Input, Form, Col, Row, Checkbox, Space, Transfer } from "antd" + +import { Card, Button, Popover, Select, Transfer } from "antd" +import DeleteOutlined from "@ant-design/icons/DeleteOutlined" + import { listSlaves, listInstances, @@ -21,7 +24,6 @@ import { } from "../../util/wslink" import DataTable from "../../components/data-table" import notify from "../../util/notify" -import { DeleteOutlined, EditOutlined, PlusOutlined } from "@ant-design/icons" const { Option } = Select @@ -31,15 +33,15 @@ export class RoleView extends Component { this.state = { role: {} } + this.messagesEndRef = React.createRef() } - messagesEndRef = React.createRef() navigate(url) { this.props.history.push(url); } async componentDidMount() { await this.getData() } - getData = async () => { + async getData() { let id = this.props.match.params.id let roles = await listRoles() @@ -50,7 +52,7 @@ export class RoleView extends Component { permissions, }) } - handleChange = async targetKeys => { + async handleChange(targetKeys) { this.setState({ role: { ...this.state.role, @@ -62,7 +64,7 @@ export class RoleView extends Component { permissions: targetKeys, }) console.log("Updated user roles", response) - }; + } render() { console.log(this.state) let { role, permissions } = this.state @@ -103,7 +105,7 @@ export class RoleView extends Component { showSearch filterOption={(inputValue, option) => option.name.toLowerCase().indexOf(inputValue.toLowerCase()) > -1} targetKeys={role.permissions} - onChange={this.handleChange} + onChange={this.handleChange.bind(this)} render={item => item.name} /> diff --git a/packages/master/web/pages/roles/rolesTable.jsx b/packages/master/web/pages/roles/rolesTable.jsx index 0e796e97..4e0f0f2a 100644 --- a/packages/master/web/pages/roles/rolesTable.jsx +++ b/packages/master/web/pages/roles/rolesTable.jsx @@ -1,5 +1,5 @@ import React, { Component } from "react" -import { Card, Table } from "antd" +import { Card } from "antd" import { listSlaves, listInstances, listRoles, createRole } from "../../util/wslink" import DataTable from "../../components/data-table" diff --git a/packages/master/web/pages/settings/settings.jsx b/packages/master/web/pages/settings/settings.jsx index 215c3f1a..d4fcf35d 100644 --- a/packages/master/web/pages/settings/settings.jsx +++ b/packages/master/web/pages/settings/settings.jsx @@ -1,5 +1,5 @@ import React, { Component } from "react" -import { Card, Button, Select, Input, Form } from "antd" +import { Card, Button, Input, Form } from "antd" const layout = { labelCol: { diff --git a/packages/master/web/pages/slaves/slavesTable.jsx b/packages/master/web/pages/slaves/slavesTable.jsx index 00c0b247..a2744e55 100644 --- a/packages/master/web/pages/slaves/slavesTable.jsx +++ b/packages/master/web/pages/slaves/slavesTable.jsx @@ -1,5 +1,5 @@ import React, { Component } from "react" -import { Card, Table } from "antd" +import { Card } from "antd"; import { listSlaves, listInstances } from "../../util/wslink" import DataTable from "../../components/data-table" diff --git a/packages/master/web/pages/users/user.jsx b/packages/master/web/pages/users/user.jsx index 3c33cba6..819ecec9 100644 --- a/packages/master/web/pages/users/user.jsx +++ b/packages/master/web/pages/users/user.jsx @@ -1,17 +1,6 @@ import React, { Component } from "react" -import { Card, Table, Button, List, Popover, Select, Divider, Input, Form, Col, Row, Checkbox, Space, Transfer } from "antd" +import { Card, Button, Popover, Select, Transfer } from "antd"; import { - listSlaves, - listInstances, - startInstance, - stopInstance, - setInstanceOutputSubscriptions, - deleteInstance, - assignInstance, - createSave, - getInstanceConfig, - setInstanceConfigField, - sendRcon, deleteUser, listUsers, listRoles, @@ -19,7 +8,7 @@ import { } from "../../util/wslink" import DataTable from "../../components/data-table" import notify from "../../util/notify" -import { DeleteOutlined, EditOutlined, PlusOutlined } from "@ant-design/icons" +import DeleteOutlined from "@ant-design/icons/DeleteOutlined" const { Option } = Select @@ -29,15 +18,15 @@ export class UserView extends Component { this.state = { user: {} } + this.messagesEndRef = React.createRef(); } - messagesEndRef = React.createRef() navigate(url) { this.props.history.push(url); } async componentDidMount() { await this.getData() } - getData = async () => { + async getData() { let name = this.props.match.params.id let users = await listUsers() @@ -48,7 +37,7 @@ export class UserView extends Component { roles, }) } - handleChange = async targetKeys => { + async handleChange(targetKeys) { this.setState({ user: { ...this.state.user, @@ -73,7 +62,7 @@ export class UserView extends Component { showSearch filterOption={(inputValue, option) => option.name.toLowerCase().indexOf(inputValue.toLowerCase()) > -1} targetKeys={user.roles} - onChange={this.handleChange} + onChange={this.handleChange.bind(this)} render={item => item.name} /> } - */ + /** + * Mapping of plugin names to their instance for loaded plugins. + * @type {Map} + */ this.plugins = controlPlugins; for (let controlPlugin of controlPlugins.values()) { plugin.attachPluginMessages(this, controlPlugin.info, controlPlugin); @@ -83,7 +74,7 @@ class Control extends link.Link { async instanceOutputEventHandler(message) { let { instance_id, output } = message.data; - console.log(formatOutputColored(output)); + console.log(formatOutput(output)); window.instanceOutputEventHandler && window.instanceOutputEventHandler({instance_id, output}) } @@ -105,8 +96,3 @@ class Control extends link.Link { await this.connector.close(1001, "Control Quit"); } } - -export { - ControlConnector, - Control -} diff --git a/packages/master/web/util/wslink.jsx b/packages/master/web/util/wslink.jsx index 4ed1e2e2..3ff8e297 100644 --- a/packages/master/web/util/wslink.jsx +++ b/packages/master/web/util/wslink.jsx @@ -1,11 +1,13 @@ import { Control, ControlConnector } from "./../util/websocket" -import * as errors from "./../lib/errors" -import * as link from "./../lib/link" +import * as errors from "@clusterio/lib/errors" +import * as link from "@clusterio/lib/link" +import config from "@clusterio/lib/config" let control, controlConnector // Create WS connection to master. Used by all components through the interface exported here. (async () => { + config.finalizeConfigs(); controlConnector = new ControlConnector( localStorage.getItem("master_url"), 10, // Reconnect delay @@ -16,31 +18,7 @@ let control, controlConnector await controlConnector.connect(); } catch (err) { console.log(err) - if (err instanceof errors.AuthenticationFailed) { - throw new errors.StartupError(err.message); - } - throw err; } - let response = await link.messages.listSlaves.send(control); - - // console.log(response.list); - console.log(link.messages) - let data = {} - // for (let key in { - // ...link.messages, - // }) { - // if (link.messages[key].constructor.name === "Request") { - // let resp, error - // try{ - // resp = await link.messages[key].send(control) - // } catch(e){ - // error = e - // } - // data[key] = resp - // data[key].error = error - // } - // } - // console.log(data) })() let listSlaves = createListFunction("listSlaves") diff --git a/packages/master/webpack.config.js b/packages/master/webpack.config.js new file mode 100644 index 00000000..ebd45171 --- /dev/null +++ b/packages/master/webpack.config.js @@ -0,0 +1,71 @@ +"use strict"; +const path = require("path"); +const webpack = require("webpack"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const { CleanWebpackPlugin } = require("clean-webpack-plugin"); + +module.exports = (env = {}) => ({ + entry: "./web/index.jsx", + mode: env.production ? "production" : "development", + devtool: env.production ? "source-map" : "eval-source-map", + devServer: { + contentBase: "./static", + }, + output: { + filename: "bundle.js", + path: path.resolve(__dirname, "static"), + }, + plugins: [ + new CleanWebpackPlugin(), + new webpack.DefinePlugin({ + "process.env.APP_ENV": JSON.stringify("browser"), + }), + new HtmlWebpackPlugin({ + template: "web/index.html", + }), + + // Make sure ant-design icons use the ES variant + new webpack.NormalModuleReplacementPlugin( + /@ant-design\/icons\/[A-Z]/, + function(resource) { + resource.request = resource.request.replace(/@ant-design\/icons/, "$&/es/icons"); + } + ), + ], + module: { + rules: [ + { + test: /node_modules.fs-extra/, + use: "null-loader", + }, + { + test: /\.css$/, + use: [ + "style-loader", + { + loader: "css-loader", + options: { + sourceMap: !env.production, + }, + }, + ], + }, + { + test: /\.jsx$/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + options: { + presets: ["@babel/preset-react"], + }, + }, + }, + ], + }, + resolve: { + extensions: [".js", ".json", ".jsx"], + }, + node: { + "crypto": "empty", + }, +});