Merge pull request #320 from Hornwitser/web-interface

Import new web interface
This commit is contained in:
Hornwitser authored and GitHub committed 2020-09-28 18:09:41 +02:00
commit 363bb54902
90 files changed
+1919 -1456

No files matched your search

+15 -2
View File
@@ -2,7 +2,6 @@
module.exports = {
"env": {
"node": true,
"browser": true,
"commonjs": true,
"es6": true,
},
@@ -21,6 +20,18 @@ module.exports = {
"no-invalid-this": "off",
},
},
{
"files": ["packages/master/web/**/*.jsx"],
"env": {
"browser": true,
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
},
},
},
],
"rules": {
@@ -44,6 +55,8 @@ module.exports = {
{
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline",
"functions": "only-multiline",
},
],
@@ -229,7 +242,7 @@ module.exports = {
"semi": "error",
"semi-spacing": "error",
"semi-style": "off",
"sort-imports": "error",
"sort-imports": "off",
"sort-keys": "off",
"sort-vars": "error",
"space-before-blocks": "warn",
-1
View File
@@ -41,7 +41,6 @@ Factorio*
package-lock.json
mod_cache.json
static/export
packages/master/static/external
saves
/temp/
-2
View File
@@ -27,13 +27,11 @@
"@clusterio/slave": "file:./packages/slave",
"eslint": "^7.2.0",
"fs-extra": "^8.1.0",
"html5-validator": "^1.2.1",
"jsdoc": "^3.6.3",
"jsonwebtoken": "^8.2.1",
"jszip": "^3.2.2",
"lerna": "^3.22.1",
"mocha": "^6.2.0",
"mocha.parallel": "^0.15.5",
"nyc": "^14.1.1",
"phin": "^3.4.1"
}
+4 -4
View File
@@ -2,8 +2,8 @@
"use strict";
const events = require("events");
const util = require("util");
const isDeepStrictEqual = require("../is_deep_strict_equal");
const { basicType } = require("@clusterio/lib/helpers");
@@ -415,7 +415,7 @@ class ConfigGroup {
let prev = this._fields.get(name);
this._fields.set(name, value);
if (this._config && !util.isDeepStrictEqual(value, prev)) {
if (this._config && !isDeepStrictEqual(value, prev)) {
this._config.emit("fieldChanged", this, name, prev);
}
}
@@ -452,7 +452,7 @@ class ConfigGroup {
updated[prop] = value;
this._fields.set(name, updated);
if (this._config && !util.isDeepStrictEqual(updated, prev)) {
if (this._config && !isDeepStrictEqual(updated, prev)) {
this._config.emit("fieldChanged", this, name, prev);
}
}
@@ -502,7 +502,7 @@ class ConfigGroup {
let prev = this._fields.get(name);
this._fields.set(name, value);
if (notify && this._config && !util.isDeepStrictEqual(value, prev)) {
if (notify && this._config && !isDeepStrictEqual(value, prev)) {
this._config.emit("fieldChanged", this, name, prev);
}
}
+7
View File
@@ -0,0 +1,7 @@
"use strict";
// eslint-disable-next-line no-process-env
if (process.env.APP_ENV === "browser") {
module.exports = require("fast-deep-equal/es6");
} else {
module.exports = require("util").isDeepStrictEqual;
}
+25 -25
View File
@@ -1,7 +1,7 @@
// Connection adapters for Links
"use strict";
const events = require("events");
const WebSocket = require("ws");
const WebSocket = require("isomorphic-ws");
const schema = require("@clusterio/lib/schema");
const errors = require("@clusterio/lib/errors");
@@ -236,11 +236,17 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
// Open WebSocket to master
console.log(`SOCKET | connecting to ${url}`);
this._socket = new WebSocket(url, {
// For now we do not verify TLS certificates since the default setup is
// to create a self-signed certificate.
rejectUnauthorized: false,
});
// eslint-disable-next-line no-process-env
if (process.env.APP_ENV === "browser") {
this._socket = new WebSocket(url);
} else {
this._socket = new WebSocket(url, {
// For now we do not verify TLS certificates since the default setup is
// to create a self-signed certificate.
rejectUnauthorized: false,
});
}
this._attachSocketHandlers();
}
@@ -277,11 +283,11 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
}
_attachSocketHandlers() {
this._socket.on("close", (code, reason) => {
console.log(`SOCKET | Close (code: ${code}, reason: ${reason})`);
this._socket.onclose = event => {
console.log(`SOCKET | Close (code: ${event.code}, reason: ${event.reason})`);
// Authentication failed
if (code === 4003) {
this.emit("error", new errors.AuthenticationFailed(reason));
if (event.code === 4003) {
this.emit("error", new errors.AuthenticationFailed(event.reason));
this._state = "closing";
}
@@ -303,26 +309,20 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
}
this.stopHeartbeat();
});
};
this._socket.on("error", err => {
this._socket.onerror = event => {
// It's assumed that close is always called by ws
console.error("SOCKET | Error:", err);
});
console.error("SOCKET | Error:", event);
};
this._socket.on("open", () => {
this._socket.onopen = () => {
console.log("SOCKET | Open");
});
this._socket.on("ping", data => {
console.log(`SOCKET | Ping (data: ${data}`);
});
this._socket.on("pong", data => {
console.log(`SOCKET | Pong (data: ${data}`);
});
};
// Handle messages
this._socket.on("message", data => {
let message = JSON.parse(data);
this._socket.onmessage = event => {
let message = JSON.parse(event.data);
if (this._state === "handshake") {
this._processHandshake(message);
@@ -341,7 +341,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
} else {
throw new Error(`Received message in unexpected state ${this._state}`);
}
});
};
}
_processHandshake(message) {
+3 -1
View File
@@ -66,6 +66,7 @@ class Request extends Message {
this.links = links;
this.permission = permission || null;
this.forwardTo = forwardTo;
this.handlerSuffix = "RequestHandler";
this.requestType = type + "_request";
this.responseType = type + "_response";
@@ -681,6 +682,7 @@ class Event extends Message {
this.links = links;
this.forwardTo = forwardTo;
this.broadcastTo = broadcastTo;
this.handlerSuffix = "EventHandler";
if (forwardTo === "instance") {
eventProperties = {
@@ -956,7 +958,7 @@ messages.playerEvent = new Event({
*/
function attachAllMessages(link) {
for (let [name, message] of Object.entries(messages)) {
message.attach(link, link[name + message.constructor.name + "Handler"]);
message.attach(link, link[name + message.handlerSuffix]);
}
}
+1
View File
@@ -13,6 +13,7 @@
"dependencies": {
"fs-extra": "^8.1.0",
"ini": "^1.3.5",
"isomorphic-ws": "^4.0.1",
"jimp": "^0.12.1",
"jsonwebtoken": "^8.2.1",
"jszip": "^3.2.2",
+1 -1
View File
@@ -433,7 +433,7 @@ function attachPluginMessages(link, pluginInfo, plugin) {
throw new Error(`Type of ${name} message must start with "${pluginInfo.name}:"`);
}
let handler = name + messageFormat.constructor.name + "Handler";
let handler = name + messageFormat.handlerSuffix;
if (plugin === null || !plugin[handler]) {
messageFormat.attach(link);
+2
View File
@@ -0,0 +1,2 @@
# Webpack output
static
+49 -36
View File
@@ -23,8 +23,6 @@ const path = require("path");
const fs = require("fs-extra");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const moment = require("moment");
const request = require("request");
const setBlocking = require("set-blocking");
const events = require("events");
const yargs = require("yargs");
@@ -38,9 +36,11 @@ let masterConfigPath;
let stopAcceptingNewSessions = false;
let debugEvents = new events.EventEmitter();
let pluginList = {};
let devMiddleware;
// homebrew modules
const generateSSLcert = require("./src/generate_ssl_cert");
const routes = require("./src/routes");
const database = require("@clusterio/lib/database");
const schema = require("@clusterio/lib/schema");
const link = require("@clusterio/lib/link");
@@ -72,23 +72,26 @@ app.use(bodyParser.urlencoded({
app.use(fileUpload());
app.use(compression());
// dynamic HTML generations with EJS
app.set("view engine", "ejs");
app.set("views", ["views", "plugins"]);
// give ejs access to some interesting information
app.use(function(req, res, next){
let externalAddress = masterConfig.get("master.external_address");
res.locals.root = externalAddress ? new URL(externalAddress).pathname : "/";
res.locals.res = res;
res.locals.req = req;
res.locals.masterPlugins = masterPlugins;
res.locals.slaves = db.slaves;
res.locals.moment = moment;
next();
});
// Servers the web interface with the root path set apropriately.
function serveWeb(route) {
// The depth is is the number of slashes in the route minus one, but due
// to lenient matching on trailing slashes in the route we need to
// compensate if the request path contains a slash but not the route,
// and vice versa.
let routeDepth = (route.match(/\//g) || []).length - 1 - (route.slice(-1) === "/");
return function(req, res, next) {
let depth = routeDepth + (req.path.slice(-1) === "/");
let webRoot = "../".repeat(depth) || "./";
fs.readFile(path.join(__dirname, "web", "index.html"), "utf8").then((content) => {
res.type("text/html");
res.send(content.replace(/__WEB_ROOT__/g, webRoot));
}).catch(err => {
next(err);
});
};
}
require("./src/routes")(app);
// Set folder to serve static content from (the website)
app.use(express.static(path.join(__dirname, "static")));
app.use(express.static("static")); // Used for data export files
@@ -390,6 +393,10 @@ async function shutdown() {
stopAcceptingNewSessions = true;
if (devMiddleware) {
await new Promise((resolve, reject) => { devMiddleware.close(resolve); });
}
let disconnectTasks = [];
for (let controlConnection of controlConnections) {
controlConnection.connector.setTimeout(masterConfig.get("master.connector_shutdown_timeout"));
@@ -438,24 +445,6 @@ async function shutdown() {
}
}
// wrap a request in an promise
async function downloadPage(url) {
return new Promise((resolve, reject) => {
request(url, (error, response, body) => {
resolve(body);
});
});
}
app.get("/api/modmeta", async function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
res.header("Access-Control-Allow-Origin", "*");
res.setHeader("Content-Type", "application/json");
let modData = await downloadPage("https://mods.factorio.com/api/mods/" + req.query.modname);
res.send(modData);
});
/**
* Base class for master server connections
*
@@ -1590,7 +1579,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
@@ -1671,6 +1662,18 @@ async function startServer() {
// If we get here the command was run
// Start webpack development server if enabled
if (args.dev) {
console.log("Webpack development mode enabled");
const webpack = require("webpack");
const webpackDevMiddleware = require("webpack-dev-middleware");
const webpackConfig = require("./webpack.config");
const compiler = webpack(webpackConfig({}));
devMiddleware = webpackDevMiddleware(compiler, {});
app.use(devMiddleware);
}
let secondSigint = false;
process.on("SIGINT", () => {
if (secondSigint) {
@@ -1716,6 +1719,16 @@ async function startServer() {
});
}
// Add routes for the web interface
for (let route of routes) {
app.get(route, serveWeb(route));
}
for (let pluginInfo of pluginInfos) {
for (let route of pluginInfo.routes || []) {
app.get(route, serveWeb(route));
}
}
// Load plugins
await pluginManagement(pluginInfos);
-106
View File
@@ -1,106 +0,0 @@
"use strict";
var http = require("http");
var https = require("https");
var fs = require("fs-extra");
function makeDirectory(path){
return new Promise((resolve, reject) => {
fs.mkdirp(path, function(err){
if(err){
console.error("Unable to mkdirp "+path);
console.error(err);
resolve(false);
} else {
console.log("mkdirp "+path);
resolve(true);
}
});
});
}
function download(url, file, callback){
return new Promise((resolve, reject) => {
let pathArray = file.split("/");
pathArray.pop(); // remove filename from the end
if(pathArray.length >= 1){
// this might be an invalid path!
fs.stat(pathArray.join("/"), (err, stat) => {
if(err){
makeDirectory(pathArray.join("/")).then(() => {
doDownload(url, file, callback);
});
} else {
doDownload(url, file, callback);
}
});
} else {
doDownload(url, file, callback);
}
function doDownload(url, file, callback){
var writeStream = fs.createWriteStream(file);
let protocol = http;
if(url.split(":")[0] == "https") protocol = https;
var request = protocol.get(url, function(response) {
response.pipe(writeStream);
response.on("end", () => {
if(callback && typeof callback == "function") callback(file);
console.log("Downloaded "+file+" from "+url);
resolve();
});
});
}
});
}
var directories = [
"sharedMods",
"instances",
];
let downloadList = [
{
url: "https://code.jquery.com/jquery.js",
file: "static/external/jquery.js",
},
{
url: "https://code.jquery.com/ui/1.12.1/jquery-ui.js",
file: "static/external/jquery-ui.js",
},
{
url: "https://momentjs.com/downloads/moment.min.js",
file: "static/external/moment.min.js",
},
{
url: "https://craig.global.ssl.fastly.net/js/mousetrap/mousetrap.min.js",
file: "static/external/mousetrap.min.js",
},
{
url: "https://fonts.googleapis.com/css?family=Roboto",
file: "static/external/css/Roboto.css",
},
];
// initiate folder creations and downloads
function processMkdirRecursive(callback){
let item = directories.shift();
makeDirectory(item).then(() => {
if(directories.length == 0){
if(callback) callback();
} else {
processMkdirRecursive(callback);
}
});
}
function processDownloadRecursive(callback){
let item = downloadList.shift();
download(item.url, item.file, item.callback).then(() => {
if(downloadList.length == 0){
if(callback) callback();
} else {
processDownloadRecursive(callback);
}
});
}
if (module === require.main) {
processMkdirRecursive(processDownloadRecursive);
}
+20 -5
View File
@@ -6,8 +6,8 @@
"version": "2.0.0-alpha0",
"main": "master.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"install": "node npm_post_install"
"build": "webpack --env.production",
"test": "echo \"Error: no test specified\" && exit 1"
},
"bin": {
"clusteriomaster": "./master.js"
@@ -23,18 +23,33 @@
"body-parser": "^1.18.2",
"compression": "^1.7.2",
"cookie-parser": "^1.4.3",
"ejs": "^2.5.7",
"express": "^4.16.2",
"express-fileupload": "1.2.0",
"fs-extra": "^8.1.0",
"jsonwebtoken": "^8.2.1",
"jszip": "^3.2.2",
"moment": "^2.22.2",
"node-factorio-api": "^0.3.8",
"node-forge": "^0.10.0",
"request": "^2.83.0",
"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",
"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"
}
}
+15 -14
View File
@@ -1,15 +1,16 @@
"use strict";
module.exports = function(app) {
app.get("/", function(req, res) {
res.render("index");
});
app.get("/nodes", function(req, res) {
res.render("nodes");
});
app.get("/settings", function(req, res) {
res.render("settings");
});
app.get("/nodeDetails", function(req, res) {
res.render("nodeDetails");
});
};
// Routes used in the web interface and served by the master server
module.exports = [
"/",
"/instances",
"/instances/:id/view",
"/slaves",
"/slaves/:id/view",
"/users",
"/users/:id/view",
"/roles",
"/roles/:id/view",
"/permissions",
"/permissions/:id/view",
"/settings",
];
-12
View File
@@ -1,12 +0,0 @@
/*!
* Bootswatch v4.1.3
* Homepage: https://bootswatch.com
* Copyright 2012-2018 Thomas Park
* Licensed under MIT
* Based on Bootstrap
*//*!
* Bootstrap v4.1.3 (https://getbootstrap.com/)
* Copyright 2011-2018 The Bootstrap Authors
* Copyright 2011-2018 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#ee5f5b;--orange:#fd7e14;--yellow:#f89406;--green:#62c462;--teal:#20c997;--cyan:#5bc0de;--white:#fff;--gray:#7A8288;--gray-dark:#3A3F44;--primary:#3A3F44;--secondary:#7A8288;--success:#62c462;--info:#5bc0de;--warning:#f89406;--danger:#ee5f5b;--light:#e9ecef;--dark:#272B30;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-family-monospace:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}*,*::before,*::after{-webkit-box-sizing:border-box;box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";font-size:0.9375rem;font-weight:400;line-height:1.5;color:#aaa;text-align:left;background-color:#272B30}[tabindex="-1"]:focus{outline:0 !important}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:0.5rem}p{margin-top:0;margin-bottom:1rem}abbr[title],abbr[data-original-title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#fff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#d9d9d9;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):hover,a:not([href]):not([tabindex]):focus{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}pre,code,kbd,samp{font-family:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:0.75rem;padding-bottom:0.75rem;color:#7A8288;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:0.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{padding:0;border-style:none}input[type="radio"],input[type="checkbox"]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="date"],input[type="time"],input[type="datetime-local"],input[type="month"]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{outline-offset:-2px;-webkit-appearance:none}[type="search"]::-webkit-search-cancel-button,[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none !important}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{margin-bottom:0.5rem;font-family:inherit;font-weight:500;line-height:1.2;color:inherit}h1,.h1{font-size:2.34375rem}h2,.h2{font-size:1.875rem}h3,.h3{font-size:1.640625rem}h4,.h4{font-size:1.40625rem}h5,.h5{font-size:1.171875rem}h6,.h6{font-size:0.9375rem}.lead{font-size:1.171875rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-heigLine truncated
-11
View File
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="/ms-icon-70x70.png"/>
<square150x150logo src="/ms-icon-150x150.png"/>
<square310x310logo src="/ms-icon-310x310.png"/>
<TileColor>#ffffff</TileColor>
</tile>
</msapplication>
</browserconfig>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,41 +0,0 @@
{
"name": "App",
"icons": [
{
"src": "\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

-185
View File
@@ -1,185 +0,0 @@
/*! terminal.js v2.0 | (c) 2014 Erik Österberg | https://github.com/eosterberg/terminaljs */
var Terminal = (function () {
// PROMPT_TYPE
var PROMPT_INPUT = 1, PROMPT_PASSWORD = 2, PROMPT_CONFIRM = 3
var fireCursorInterval = function (inputField, terminalObj) {
var cursor = terminalObj._cursor
setTimeout(function () {
if (inputField.parentElement && terminalObj._shouldBlinkCursor) {
cursor.style.visibility = cursor.style.visibility === 'visible' ? 'hidden' : 'visible'
fireCursorInterval(inputField, terminalObj)
} else {
cursor.style.visibility = 'visible'
}
}, 500)
}
var firstPrompt = true;
promptInput = function (terminalObj, message, PROMPT_TYPE, callback) {
var shouldDisplayInput = (PROMPT_TYPE === PROMPT_INPUT)
var inputField = document.createElement('input')
inputField.style.position = 'absolute'
inputField.style.zIndex = '-100'
inputField.style.outline = 'none'
inputField.style.border = 'none'
inputField.style.opacity = '0'
inputField.style.fontSize = '0.2em'
terminalObj._inputLine.textContent = ''
terminalObj._input.style.display = 'block'
terminalObj.html.appendChild(inputField)
fireCursorInterval(inputField, terminalObj)
if (message.length) terminalObj.print(PROMPT_TYPE === PROMPT_CONFIRM ? message + ' (y/n)' : message)
inputField.onblur = function () {
terminalObj._cursor.style.display = 'none'
}
inputField.onfocus = function () {
inputField.value = terminalObj._inputLine.textContent
terminalObj._cursor.style.display = 'inline'
}
terminalObj.html.onclick = function () {
inputField.focus()
}
inputField.onkeydown = function (e) {
if (e.which === 37 || e.which === 39 || e.which === 38 || e.which === 40 || e.which === 9) {
e.preventDefault()
} else if (shouldDisplayInput && e.which !== 13) {
setTimeout(function () {
terminalObj._inputLine.textContent = inputField.value
}, 1)
}
}
inputField.onkeyup = function (e) {
if (PROMPT_TYPE === PROMPT_CONFIRM || e.which === 13) {
terminalObj._input.style.display = 'none'
var inputValue = inputField.value
if (shouldDisplayInput) terminalObj.print(inputValue)
terminalObj.html.removeChild(inputField)
if (typeof(callback) === 'function') {
if (PROMPT_TYPE === PROMPT_CONFIRM) {
callback(inputValue.toUpperCase()[0] === 'Y' ? true : false)
} else callback(inputValue)
}
}
}
if (firstPrompt) {
firstPrompt = false
setTimeout(function () { inputField.focus() }, 50)
} else {
inputField.focus()
}
}
var terminalBeep
var TerminalConstructor = function (id) {
if (!terminalBeep) {
terminalBeep = document.createElement('audio')
var source = '<source src="http://www.erikosterberg.com/terminaljs/beep.'
terminalBeep.innerHTML = source + 'mp3" type="audio/mpeg">' + source + 'ogg" type="audio/ogg">'
terminalBeep.volume = 0.05
}
this.html = document.createElement('div')
this.html.className = 'Terminal'
if (typeof(id) === 'string') { this.html.id = id }
this._innerWindow = document.createElement('div')
this._output = document.createElement('p')
this._inputLine = document.createElement('span') //the span element where the users input is put
this._cursor = document.createElement('span')
this._input = document.createElement('p') //the full element administering the user input, including cursor
this._shouldBlinkCursor = true
this.beep = function () {
terminalBeep.load()
terminalBeep.play()
}
this.print = function (message) {
var newLine = document.createElement('div')
newLine.textContent = message
this._output.appendChild(newLine)
}
this.input = function (message, callback) {
promptInput(this, message, PROMPT_INPUT, callback)
}
this.password = function (message, callback) {
promptInput(this, message, PROMPT_PASSWORD, callback)
}
this.confirm = function (message, callback) {
promptInput(this, message, PROMPT_CONFIRM, callback)
}
this.clear = function () {
this._output.innerHTML = ''
}
this.sleep = function (milliseconds, callback) {
setTimeout(callback, milliseconds)
}
this.setTextSize = function (size) {
this._output.style.fontSize = size
this._input.style.fontSize = size
}
this.setTextColor = function (col) {
this.html.style.color = col
this._cursor.style.background = col
}
this.setBackgroundColor = function (col) {
this.html.style.background = col
}
this.setWidth = function (width) {
this.html.style.width = width
}
this.setHeight = function (height) {
this.html.style.height = height
}
this.blinkingCursor = function (bool) {
bool = bool.toString().toUpperCase()
this._shouldBlinkCursor = (bool === 'TRUE' || bool === '1' || bool === 'YES')
}
this._input.appendChild(this._inputLine)
this._input.appendChild(this._cursor)
this._innerWindow.appendChild(this._output)
this._innerWindow.appendChild(this._input)
this.html.appendChild(this._innerWindow)
this.setBackgroundColor('black')
this.setTextColor('white')
this.setTextSize('1em')
this.setWidth('100%')
this.setHeight('100%')
this.html.style.fontFamily = 'Monaco, Courier'
this.html.style.margin = '0'
this._innerWindow.style.padding = '10px'
this._input.style.margin = '0'
this._output.style.margin = '0'
this._cursor.style.background = 'white'
this._cursor.innerHTML = 'C' //put something in the cursor..
this._cursor.style.display = 'none' //then hide it
this._input.style.display = 'none'
}
return TerminalConstructor
}())
-9
View File
@@ -1,9 +0,0 @@
export function getParameterByName(name, url) {
if(!url) url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if(!results) return null;
if(!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
-218
View File
@@ -1,218 +0,0 @@
(function (){
ensureSetting("Periodically update storage screen", "boolean", true);
}())
function ensureSetting(setting, type, defaultSetting) {
let settings
try {
settings = JSON.parse(localStorage.settings);
} catch (e) {
}
if(!settings) settings = {};
if(typeof settings[setting] != type) {
settings[setting] = defaultSetting;
console.log("Fixed setting: " + setting + " | " + defaultSetting)
}
localStorage.settings = JSON.stringify(settings)
}
// debug tools
debugTools = {
addItem: function(name, count){
console.log(`Attempting to add ${count} ${name}`)
postJSON("api/place", {
instanceName: "unknown",
instanceID: "unknown",
unique: "unknown",
name: name,
count: count,
}, function(data){
console.log(data);
});
},
removeItem: function(name, count){
console.log(`Attempting to remove ${count} ${name}`)
postJSON("api/remove", {
instanceName: "unknown",
instanceID: "unknown",
unique: "unknown",
name: name,
count: count,
}, function(data){
console.log(data);
});
}
}
let cachedInstanceNames = {};
async function getInstanceName(instanceID){
let instance = cachedInstanceNames[instanceID];
if(!instance){
let slaves = await getJSON("/api/slaves");
try {
for(id in slaves){
cachedInstanceNames[id] = slaves[id].instanceName;
}
} catch(e){
console.log("Error while looking for instance name!");
console.log(e);
}
return cachedInstanceNames[instanceID] || instanceID;
} else {
return cachedInstanceNames[instanceID];
}
}
var g = {}
contents = {
"iron-plate":100,
"copper-plate":7312,
}
// nice functions
// hash a string to a hash
function djb2(str){
var hash = 5381;
for (var i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash) + str.charCodeAt(i); /* hash * 33 + c */
}
return hash;
}
// get queryString parameters
function getParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
// callback(err, json)
function getJSON(url, callback) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'json';
xhr.onload = function() {
var status = xhr.status;
if (status == 200) {
if(callback) callback(null, xhr.response);
resolve(xhr.response);
} else {
if(callback) callback(status);
reject(status);
}
};
// triggers if connection is refused
xhr.onerror = function(e){
if(callback) callback(e);
reject(e);
};
xhr.send();
});
};
// callback(err, json)
function postJSON(url, data, callback) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
xhr.responseType = 'json';
xhr.onload = function() {
var status = xhr.status;
if (status == 200) {
if(callback) callback(null, xhr.response);
resolve(xhr.response);
} else {
if(callback) callback(status);
reject(status);
}
};
// triggers if connection is refused
xhr.onerror = function(e){
if(callback) callback(e);
reject(e);
};
xhr.send(JSON.stringify(data));
});
};
// return Boolean
function isJSON(string){
let stringIsJson = false;
let x
try {
x = JSON.parse(string);
} catch (e){
return false;
}
if(typeof x == "object" || typeof string == "object"){
stringIsJson = true;
}
return stringIsJson;
}
// hash a string to a color
function hashColor(str) {
var hash = djb2(str);
var r = (hash & 0xFF0000) >> 16;
var g = (hash & 0x00FF00) >> 8;
var b = hash & 0x0000FF;
return "#" + ("0" + r.toString(16)).substr(-2) + ("0" + g.toString(16)).substr(-2) + ("0" + b.toString(16)).substr(-2);
}
// exactly what you would expect it to, returns String
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
// hide the HTML object passed as argument #1
function hideThis(object) {
object.style.visibility = "hidden";
}
Date.prototype.yyyymmdd = function(time) { // http://stackoverflow.com/questions/3066586/get-string-in-yyyymmdd-format-from-js-date-object
var mm = this.getMonth() + 1; // getMonth() is zero-based
var dd = this.getDate();
if(mm<10)mm = "0"+mm
if(dd<10)dd = "0"+dd
console.log(this.getFullYear())
return this.getFullYear()+""+mm+""+dd+ ""; // padding
};
var date = new Date();
function replaceAll(target, search, replacement) {
return target.split(search).join(replacement);
};
function getItemIconClass(name) {
if (factorioItemMetadata.has(name)) {
return `item-${name}`;
} else {
return "item-unknown-item";
}
}
window.factorioLocale = new Map();
$.getJSON(`${root}export/locale.json`, (locale) => {
window.factorioLocale = new Map(locale);
});
window.factorioItemMetadata = new Map();
$.getJSON(`${root}export/item-metadata.json`, (itemMetadata) => {
window.factorioItemMetadata = new Map(itemMetadata);
console.log(factorioItemMetadata.get("electronic-circuit"));
let style = document.createElement("style");
style.type = "text/css";
document.head.appendChild(style);
for (let [name, meta] of itemMetadata) {
style.sheet.insertRule([
`.item-${name} {`,
` background-image: url("${root}export/item-spritesheet.png");`,
" background-repeat: no-repeat;",
` background-position: -${meta.x}px -${meta.y}px;`,
` height: ${meta.size}px;`,
` width: ${meta.size}px;`,
"}\n",
].join(""));
}
});
-41
View File
@@ -1,41 +0,0 @@
{
"name": "App",
"icons": [
{
"src": "\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}
-3
View File
@@ -1,3 +0,0 @@
#node-stats .breadcrumb-item {
height: auto;
}
-255
View File
@@ -1,255 +0,0 @@
function getParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
/// code below here
// declaring globals
var chartsByID = {};
// update the online indicator dot
setInterval(function(){
getJSON("/api/slaves", function(err, slaveData){
let indicator = document.querySelector("#online-indicator");
$(indicator).removeClass('fa-signal');
$(indicator).removeClass('fa-dizzy');
$(indicator).removeClass('fa-plug');
if(err){
// our request threw an error, most likely master is unavailable so we show yellow indicator
indicator.style.color = "yellow";
$(indicator).addClass('fa-plug');
indicator.title = "Master server unavailable";
// compare with Math.floor(x/100000) allows us to check if they are within 10s of each other
} else if(Math.floor(slaveData[getParameterByName("instanceID")].time/100000) === Math.floor(Date.now()/100000)){
indicator.style.color = "green";
$(indicator).addClass('fa-signal');
indicator.title = "Slave is online";
} else {
indicator.style.color = "red";
$(indicator).addClass('fa-dizzy');
indicator.title = "Slave is offline";
}
});
}, 1000);
function regexModDetails(modFileName) {
let m;
let results = [];
const regex = /(.+)_(\d+\.\d+\.\d+)\.zip/gm;
while ((m = regex.exec(modFileName)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (m.index === regex.lastIndex) {
regex.lastIndex++;
}
// The result can be accessed through the `m`-variable.
m.forEach((match, _) => {
results.push(match);
});
}
return results
}
populateSlaveInfo();
function populateSlaveInfo(){
let instanceID = getParameterByName("instanceID");
if(!instanceID){
throw "We need instanceID! Also, *funny joke* but it doesn't work after I standardized on calling it instanceID";
}
getJSON("/api/slaves", function(err, slaveData){
/*
{"-1530507550":{
"time":"1495475926375",
"rconPort":"8714",
"rconPassword":"hidden",
"serverPort":"14345",
"unique":"-1530507550",
"publicIP":"localhost",
"mods":[
{
"modName":"clusterio_1.0.0.zip",
"hash":"341feb6c60918c83f7f3a6a14a4a308aef18dda6"
},{
"modName":"clusterio_1.0.1.zip",
"hash":"aee7a4b67ece19bddf236312fe408a5d4b67248d"
},{
"modName":"clusterio_1.1.0.zip",
"hash":"18e2f7e402d716e3dd1c1458b7fab2d0bb29eccf"
}],
"playerCount":"0",
"instanceName":"t",
"mac":"44-85-00-D2-7E-D6"}
}
*/
let slave = slaveData[instanceID];
let HTML = '<div class="jumbotron mt-5"><div id="header">' +
'<h1 class="display-4"><i id="online-indicator" class="mr-3 fas"></i> ' + slave.instanceName+"</h1>";
HTML += '<div class="stats" id="node-stats">' +
'<nav class="mt-3" aria-label="breadcrumb">\n' +
' <ol class="breadcrumb align-content-center"><li class="breadcrumb-item">Host: '+slave.mac+'</li>' +
'<li class="breadcrumb-item">Unique: '+slave.unique+'</li>';
if(slave.publicIP !== "localhost"){
HTML += '<li class="breadcrumb-item">IP: '+slave.publicIP+':'+slave.serverPort+'</li>';
} else {
HTML += '<li class="breadcrumb-item">This server is not configured for incoming connections</li>';
}
if(slave.meta)
for(let key in slave.meta){
let t = slave.meta[key];
if(typeof t == "string"){
HTML += "<li class=\"breadcrumb-item\">"+key+": "+t+"</li>";
}
}
HTML += '<li class="breadcrumb-item">Last seen: <span id="lastSeenDate">'+moment(Number(slave.time)).fromNow()+"</span></li>";
HTML += '<li class="breadcrumb-item">Online players: '+slave.playerCount+"</li></ol></nav>";
HTML += "</div>"; // end of displayBody
// list mods and other metadata
HTML += '<h2 class="subtitle d-inline">Mods</h2><a id="fetch-mod-data" class="btn btn-primary float-right m-2">Fetch Mod Data</a>' +
'<table id="modlist" class="table table-striped table-hover">' +
' <thead>\n' +
' <tr>\n' +
' <th scope="col">#</th>\n' +
' <th scope="col">Version</th>\n' +
' <th scope="col">Name</th>\n' +
' <th scope="col">Summary</th>\n' +
' </tr>\n' +
' </thead>' +
' <tbody>';
for(let i = 0; i < slave.mods[0].modName.length; i++){
let modRegexResults = regexModDetails(slave.mods[0].modName[i]);
HTML += `<tr>
<th scope="row">${i + 1}</th>
<td>${modRegexResults[2]}</td>
<td>${modRegexResults[1]}</td>
<td></td>
</tr>`
}
HTML += "</ul>";
HTML += "</div>"; // end of left container
// terminal
// HTML += '<div id="terminal"></div>';
document.querySelector("#body > #details").innerHTML = HTML;
// makeTerminal();
$('#fetch-mod-data').on("click", downloadSaveModData);
});
}
function downloadSaveModData() {
let modRows = $("#modlist tbody tr");
for (let i = 0; i < modRows.length; i++) {
let modName = modRows[i].children[2].innerText;
$.getJSON( "/api/modmeta?modname=" + modName, function( data ) {
modRows[i].children[2].innerHTML = data.title;
modRows[i].children[3].innerHTML = data.summary
});
}
}
var slaveLogin = {};
function makeTerminal(){
myTerminal = new Terminal();
document.querySelector("#terminal").appendChild(myTerminal.html);
// use jQuery to allow the user to drag the window around
$("#terminal").draggable({
handle:".titleBar",
});
myTerminal.print('Welcome to Clusterio rcon!');
myTerminal.input('', handleTerminalInput);
if(localStorage.terminalMinimized && localStorage.terminalMinimized === "true"){
minimizeTerminal(true);
}
function print(string){
myTerminal.print(string);
}
function handleTerminalInput(inputString) {
// show clearly that it was a command that was typed, not terminal output
let lastLine = document.querySelector("#terminal div > p:nth-child(1) > div:last-child");
lastLine.innerHTML = "> " + lastLine.innerHTML;
argv = inputString.split(' ');
if(argv[0][0] === '/'){
if(slaveLogin && slaveLogin.name && slaveLogin.pass){
print('Running: '+inputString);
} else {
print('Not identified to communicate with any slave! Run "help" for more information.');
}
} else if(argv[0] === 'help'){
if(argv[1] === 'login'){
print('To send commands to a slave, you are required to identify yourself. You can do this with the login command. The name will be the name of the slave as displayed on master. The password is the rcon password of the slave, as per /instances/[name]/config.json');
} else if(argv[1] === "/c"){
print('To run a command, start with /. You can for example do /c game.print("hello world!")');
} else if(argv[1] === "issues"){
print("Issues are reported to clusterio on github or espernet IRC.");
print(" - http://github.com/clusterio/factorioClusterio/issues");
print(" - EsperNet #factorio Danielv123");
} else {
print('To identify yourself to a slave, use "login [name] [password]"');
print(' - /c [command] - Run command');
print(' - login [name] [passs] - Connect to remote slave');
print(' - issues - Report an issue');
}
} else if(argv[0] === 'issues'){
print("Issues are reported to clusterio on github or espernet IRC.");
print(" - http://github.com/clusterio/factorioClusterio/issues");
print(" - EsperNet #factorio Danielv123");
} else {
print('Invalid command!');
}
// keep handling input (unless something is seriously wrong)
myTerminal.input('',handleTerminalInput);
}
}
function minimizeTerminal(instant){
localStorage.terminalMinimized = true;
$('#terminal').draggable('disable');
if(!instant){
$('#terminal')[0].style.transition = 'left 0.2s linear, top 0.2s linear';
}
document.querySelector('.Terminal').style.height = 0;
let ter = $('#terminal')[0];
// if the window just spawned in and we are doing the first minimize, our position won't be set yet. We also don't want to save this blank position.
if(ter.style.left && ter.style.top){
localStorage.oldTerminalPosition = JSON.stringify({x:ter.style.left, y:ter.style.top});
}
ter.style.left = (window.innerWidth - ter.offsetWidth)+'px';
ter.style.top = (window.innerHeight - 25)+'px';
// if we did it instant we still want to set this property for after.
if(instant){
setTimeout(() => $('#terminal')[0].style.transition = 'left 0.2s linear, top 0.2s linear',200);
}
}
function maximizeTerminal(instant){
localStorage.terminalMinimized = false;
document.querySelector('.Terminal').style.height = '400px';
let ter = $('#terminal')[0];
// recover our old saved position
let oldTerminalPosition = JSON.parse(localStorage.oldTerminalPosition);
ter.style.left = oldTerminalPosition.x;
ter.style.top = oldTerminalPosition.y;
// remove our animation once its finished
// since instant is a true/false value, Number will convert it to 1 or 0. Since 0 is falsy, || will turn it into 200.
setTimeout(() => $('#terminal')[0].style.transition = 'none', (Number(instant) || 200));
$('#terminal').draggable('enable');
}
-35
View File
@@ -1,35 +0,0 @@
if(localStorage.settings) {
let settings = JSON.parse(localStorage.settings);
let HTML = "";
for(let key in settings) {
HTML += drawSetting(key, settings[key]);
}
document.querySelector("#body").innerHTML = document.querySelector("#body").innerHTML + HTML;
}
window.onload = function(){
// loop through all settings and add onclick events for them
let sliders = document.querySelectorAll(".settings-box .switch .slider");
for(let i = 0; i < sliders.length; i++) {
sliders[i].onclick = function() {
let settings = JSON.parse(localStorage.settings);
let selectedSliderParent = $(this.parentElement.parentElement);
// walk through DOM structure created by drawSetting and update localstorage from that
console.log(selectedSliderParent.find('.settings-text').html() + " = " + (selectedSliderParent.find("label input").val() === 'on'));
settings[selectedSliderParent.find('.settings-text').html()] = (selectedSliderParent.find("label input").val() === 'on');
localStorage.settings = JSON.stringify(settings);
}
}
};
function drawSetting(settingText, checked) {
let HTML = '<div class="settings-box">' +
'<label class="switch"><input type="checkbox" checked="' + checked + '"' +
'><div class="slider round"></div></label><span class="settings-text ml-3 align-middle">' + settingText + '</span></div>';
return HTML;
}
-88
View File
@@ -1,88 +0,0 @@
nav {
background-color: #25272a;
}
nav li {
height: 80px;
vertical-align: middle;
}
.navbar .nav-title {
font-family: 'Orbitron', sans-serif;
font-size: 25px;
display: inline-block;
vertical-align: bottom;
}
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.settingsBox {
position: relative;
display: block;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #ab671e;
}
input:focus + .slider {
box-shadow: 0 0 1px #ab671e;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
.item-unknown-item {
background: url("pictures/unknown-item.png");
height: 32px;
width: 32px;
}
+6
View File
@@ -0,0 +1,6 @@
// Calculate basedir of application based on current location and
// the provided web_root from master server.
const location = new URL(window.web_root, document.location);
const basename = location.pathname !== "/" ? location.pathname : "";
export default basename;
+13
View File
@@ -0,0 +1,13 @@
import React from "react";
import { BrowserRouter } from "react-router-dom";
import basename from "../basename";
import Layout from "../layout";
export default function App() {
return (
<BrowserRouter basename={basename}>
<Layout />
</BrowserRouter>
);
}
@@ -0,0 +1,287 @@
import React, { Component } from "react";
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";
const { Panel } = Collapse;
/**
* Display data in table. Allows for editing data by clicking rows in the table.
* Basic usage: <DataTable Server="SQL08" DataSource="sql" Database="HDG1" Table="[dbo].[ReportInfo]" />
*
* DataFunction - String, corresponds to export from util/wslink. Same as ws message name.
* columns - Array of objects, optional, same as <Table />. Note the lowercase c
* Editable - Boolean - Allow editing
* AddRecord - Boolean - Allow adding new records
* CustomDataFilterFunction - function(tableData) - Filter tabledata and return results
* RecordIdField - String - By default we assume the column "Id" is a unique identifier for a record. If that isn't the case, supply the column name here
*/
class DataTable extends Component {
constructor(props) {
super(props);
if (!this.props.DataFunction) throw new Error("DataFunction not defined");
this.state = {
pagination: {
pageSize: 100,
current: 1,
},
filters: this.props.Filters,
sorter: {},
tableData: [],
showEditModal: false,
record: {},
};
}
componentDidMount() {
this.getData();
}
async getData(pagination, filters, sorter) {
/*
pagination = {
pageSize: 10, // items per page
current: 1, // page number
total: 123, // Total amount of items
}
filters = {
ReportId: 123
}
sorter = {}
*/
pagination = pagination || this.state.pagination;
filters = filters || this.state.filters;
sorter = sorter || this.state.sorter;
// console.log(pagination, filters, sorter)
let data = await this.props.DataFunction();
// await fetchApi(this.state.dataUrl, "post", {
// database: this.props.Database,
// table: this.props.Table,
// dataSource: this.props.DataSource,
// pagination: this.props.RecordCount? {...pagination, pageSize: this.props.RecordCount, current: 1}:pagination,
// filters,
// sorter,
// recordIdField: this.props.RecordIdField || "Id", // Sort by this
// })
console.log(data);
data = data || {};
this.setState({
tableData: this.props.CustomDataFilterFunction ? this.props.CustomDataFilterFunction(data) : data,
pagination: { ...pagination, ...data.pagination },
sorter,
filters,
});
}
showEditModal(record, index) {
if (!this.state.showEditModal) {
this.setState({
record: record,
showEditModal: true,
});
}
}
hideEditModal() {
this.setState({
showEditModal: false,
inserting: false,
record: {},
});
}
async onEditFinish(values) {
console.log("Finished editing", values);
if (this.state.inserting) { // Create new record
await this.props.AddRecord.insert(values);
} else { // Edit existing record, capable of doing sparse updates
await this.props.Editable.insert(values);
}
this.setState({
showEditModal: false,
inserting: false,
record: {},
});
// Force refresh of data
this.getData();
}
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) {
if (record[key] === null) delete record[key];
}
var status = {};
// await fetchApi(`/api/${this.props.Server}/tableData/record`, "delete", {
// database: this.props.Database,
// table: this.props.Table,
// dataSource: this.props.DataSource,
// record,
// }, undefined, true)
console.log("Deleted record", status);
if (status.ok) notify("success", status.msg, "success");
if (!status.ok) notify("error", status.msg, "error");
// Force refresh of data
this.getData();
}
render() {
let tableProps = {
...this.props.TableProps || {},
onRow: (record, rowIndex) => {
/*
To allow for extending functionality, we take a TableProps argument from props and pass it through to the Table component.
Since we are also using onRow internaly here, we handle the externally provided events ourselves by passing along the event
object. This way the end user isn't affected by our event handlers.
*/
let externalOnRow = {
...(this.props.TableProps && this.props.TableProps.onRow && this.props.TableProps.onRow(record, rowIndex)),
};
return {
onClick: event => {
if (event.target.type !== "button") {
console.log("onRow", record, rowIndex);
if (this.props.Editable) this.showEditModal(record, rowIndex);
if (externalOnRow.onClick) externalOnRow.onClick(event);
}
},
onDoubleClick: event => {
if (externalOnRow.onDoubleClick) externalOnRow.onDoubleClick(event);
},
onContextMenu: event => {
if (externalOnRow.onContextMenu) externalOnRow.onContextMenu(event);
},
onMouseEnter: event => {
if (externalOnRow.onMouseEnter) externalOnRow.onMouseEnter(event);
},
onMouseLeave: event => {
if (externalOnRow.onMouseLeave) externalOnRow.onMouseLeave(event);
},
};
},
};
let columns = this.props.columns || this.inferColumns(this.state.tableData);
if (this.props.DeleteRecord && !columns.find(x => x.dataIndex === "notInUse")) columns.push({
// Add delete button on the right
dataIndex: "notInUse",
title: "",
render: (text, record) => <Popover
content={
<Button
type="primary"
danger
onClick={() => {
this.deleteRecord(record);
}}
>
Slett permanent
</Button>
}
trigger="click"
>
<Button danger style={{ float: "right", fontSize: "16px" }}>
<DeleteOutlined />
</Button>
</Popover>,
});
return <>
<Row>
<Col span={24}>
<style>
{`
.editableTable .ant-table-tbody > tr {
cursor:pointer
}
`}
</style>
<Table
className={this.props.Editable && "editableTable"}
{...tableProps}
columns={columns.map(x => ({
...x, // Add a 4th parameter to the stock render function with all of our table data
render: x.render ? (...originalParams) => x.render(...originalParams, this.state.tableData) : undefined,
}))}
dataSource={this.state.tableData}
pagination={this.state.pagination.total > 10 ? this.state.pagination : false}
onChange={this.getData.bind(this)}
/>
{this.props.AddRecord && <Button type="primary" onClick={() => this.setState({
showEditModal: true,
inserting: true,
})}>
<PlusOutlined />Add
</Button>}
</Col>
<Modal
visible={this.state.showEditModal}
onCancel={this.hideEditModal.bind(this)}
footer={null}
title={
<h2>{this.state.inserting ? "Legg til" : "Rediger"}</h2>
}
>
{this.state.showEditModal && <Form
labelCol={{
span: 6,
}}
wrapperCol={{
span: 12,
}}
layout="horizontal"
onFinish={this.onEditFinish.bind(this)}
initialValues={this.state.record}
>
{
(
(this.state.inserting && this.props.AddRecord && this.props.AddRecord.fields) // Use externally supplied form input fields
|| this.props.columns.filter && this.props.columns.filter(x => x.dataIndex !== "notInUse") // Generate input fields from external columns
|| this.inferColumns(this.state.tableData)).map(col => { // Generate input fields from autogenerated columns (only works with existing data)
console.log(this.state.record);
return <Form.Item key={col.dataIndex} label={col.title} name={col.dataIndex}>
{col.renderEdit && col.renderEdit(this.state.record[col.dataIndex], this.state.record, undefined /* Non spec compliant, is supposed to be index of record */, this.state.tableData) || <Input
placeholder={this.state.record[col.dataIndex]}
/>}
</Form.Item>;
})
}
<Form.Item>
<Button type="primary" htmlType="submit">
{this.state.inserting ? "Legg til" : "Lagre"}
</Button>
</Form.Item>
</Form>}
</Modal>
</Row>
</>;
}
inferColumns(data) {
// Look at the data and find columns and how to display them. Can be overridden by props.Columns if more customization is needed
let columns = [];
data.forEach(row => {
Object.keys(row).filter(x => x !== "key").forEach(key => {
let existing = columns.find(col => col.key === key);
if (!existing) {
columns.push({
key: key,
title: key,
dataIndex: key,
});
}
});
});
return columns;
}
}
export default DataTable;
+25
View File
@@ -0,0 +1,25 @@
@import '~antd/dist/antd.dark.css';
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
[data-theme="compact"] p, p {
margin: 0;
}
.row-links{
cursor: pointer;
}
+16
View File
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Clusterio</title>
<script>
window.web_root = "__WEB_ROOT__";
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="__WEB_ROOT__bundle.js"></script>
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
import "./index.css";
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
ReactDOM.render(<App />, document.getElementById("root"));
+166
View File
@@ -0,0 +1,166 @@
import { Layout, Menu } from "antd";
import { Route, Link, withRouter } from "react-router-dom";
import React, { Component } from "react";
import { Dashboard } from "../pages/dashboard";
import { InstancesTable } from "../pages/instances/instancesTable";
import { SlavesTable } from "../pages/slaves/slavesTable";
import { UsersTable } from "../pages/users/usersTable";
import { RolesTable } from "../pages/roles/rolesTable";
import { PermissionsTable } from "../pages/permissions/permissionsTable";
import { InstanceView } from "../pages/instances/instance";
import { UserView } from "../pages/users/user";
import { RoleView } from "../pages/roles/role";
import { Settings } from "../pages/settings/settings";
const { Content, Footer, Sider } = Layout;
const { SubMenu } = Menu;
class SiteLayout extends Component {
constructor(props) {
super(props);
const { location } = props;
this.state = {
collapsed: false,
setcollapsed: false,
tags: [],
setTags: [],
location,
siteSettings: {},
factoryModel: [],
};
}
onCollapse() {
this.setState({ collapsed: !this.state.collapsed });
}
titleCase(text) {
return text.substring(0, 1).toUpperCase() + text.substring(1);
}
replaceParametersInUrl({ endpoint, params }) {
let path = endpoint;
console.log("endpoint", params);
Object.keys(params).forEach(key => {
path = path.replace(":" + key, params[key]);
});
return path;
}
render() {
return (
<Layout style={{ minHeight: "100vh" }}>
{document.location.search !== "?hideExtras" ? (
<Sider
collapsible
collapsed={this.state.collapsed}
onCollapse={this.onCollapse.bind(this)}
width={250}
breakpoint="lg"
>
<p></p>
<div style={{ width: "100%", textAlign: "center" }}>
{" "}
{this.state.collapsed ? (
<img
alt=""
src="/smal_logo.png"
style={{ height: "48px", margin: 8 }}
/>
) : (
<img
alt=""
src="/logo.png"
style={{ width: "200px", margin: 8 }}
/>
)}
</div>
<p></p>
<p></p>
<Menu
theme="dark"
defaultSelectedKeys={[
this.state.location.pathname.split("/")[
this.state.location.pathname.split("/").length - 1
],
]}
defaultOpenKeys={(() => {
let x = this.state.location.pathname.split("/");
x.shift();
return x;
})()}
mode="inline"
>
<Menu.Item key="slaves">
<span>Slaves</span>
<Link to="/slaves" />
</Menu.Item>
<Menu.Item key="instances">
<span>Instances</span>
<Link to="/instances" />
</Menu.Item>
<Menu.Item key="users">
<span>Users</span>
<Link to="/users" />
</Menu.Item>
<Menu.Item key="roles">
<span>Roles</span>
<Link to="/roles" />
</Menu.Item>
<Menu.Item key="permissions">
<span>Permissions</span>
<Link to="/permissions" />
</Menu.Item>
<Menu.Item key="settings">
<span>Settings</span>
<Link to="/settings" />
</Menu.Item>
</Menu>
</Sider>
) : (
""
)}
<Layout style={{
// background: "#F4F4F4"
}}>
<Content
style={{
margin: "24px 16px",
padding: 24,
// background: "#fff",
minHeight: 280,
}}
>
<Route exact path="/" component={Dashboard} />
<Route exact path="/instances" component={InstancesTable} />
<Route exact path="/instances/:id/view" component={InstanceView} />
<Route exact path="/slaves" component={SlavesTable} />
<Route exact path="/slaves/:id/view" component={SlavesTable} />
<Route exact path="/users" component={UsersTable} />
<Route exact path="/users/:id/view" component={UserView} />
<Route exact path="/roles" component={RolesTable} />
<Route exact path="/roles/:id/view" component={RoleView} />
<Route exact path="/permissions" component={PermissionsTable} />
<Route exact path="/permissions/:id/view" component={PermissionsTable} />
<Route exact path="/settings" component={Settings} />
</Content>
<Footer style={{
// background: "#F4F4F4",
textAlign: "center",
}}>
Clusterio 2.0 web interface https://github.com/clusterio
</Footer>
</Layout>
</Layout>
);
}
}
export default withRouter(SiteLayout);
+7
View File
@@ -0,0 +1,7 @@
import React, {Component} from "react";
export class Dashboard extends Component {
render(){
return <h2>Dashboard</h2>;
}
}
@@ -0,0 +1,452 @@
import React, { Component } from "react";
import { Card, Button, Popover, Select, Divider, Input, Form, Col, Row, Checkbox, Space } from "antd";
import {
listSlaves,
listInstances,
startInstance,
stopInstance,
setInstanceOutputSubscriptions,
deleteInstance,
assignInstance,
createSave,
getInstanceConfig,
setInstanceConfigField,
sendRcon,
} from "../../util/wslink";
import DataTable from "../../components/data-table";
import notify from "../../util/notify";
import DeleteOutlined from "@ant-design/icons/DeleteOutlined";
import EditOutlined from "@ant-design/icons/EditOutlined";
import PlusOutlined from "@ant-design/icons/PlusOutlined";
const { Option } = Select;
export class InstanceView extends Component {
constructor(props) {
super(props);
this.state = {
instance: {},
slaves: [],
logLines: [{ message: "Logviewer. Follows the latest logs if you scroll to the bottom. Does not store logs past the lifetime of this tab" }],
};
this.messagesEndRef = React.createRef();
}
navigate(url) {
this.props.history.push(url);
}
async componentDidMount() {
let id = this.props.match.params.id;
await this.getData();
await setInstanceOutputSubscriptions({
instance_id: Number(id),
});
window.instanceOutputEventHandler = ({ instance_id, output }) => {
if (instance_id === Number(id)) {
this.setState({
logLines: [...this.state.logLines, output],
});
}
// Automatically scroll to bottom on new line.
// this.messagesEndRef.current.scrollIntoView({ behavior: 'smooth' })
};
}
async getData() {
let id = this.props.match.params.id;
let instances = await listInstances();
let slaves = await listSlaves();
this.setState({
instance: instances.find(x => x.id === Number(id)),
slaves,
});
let config = await getInstanceConfig({ instance_id: Number(id) });
console.log(config);
this.setState({
config,
});
}
async start() {
this.setState({
starting: true,
});
try {
await startInstance({
instance_id: this.state.instance.id,
save: null,
});
} catch (e) {
notify("Error starting instance", "error", e.message);
this.setState({
starting: false,
});
return;
}
notify("Instance started");
this.setState({
starting: false,
});
this.getData();
}
async stop() {
this.setState({
stopping: true,
});
try {
await stopInstance({
instance_id: this.state.instance.id,
});
} catch (e) {
notify("Error stopping instance", "error", e.message);
this.setState({
stopping: false,
});
return;
}
notify("Instance stopped");
this.setState({
stopping: false,
});
this.getData();
}
async createSave() {
this.setState({
creatingSave: true,
});
try {
await createSave({
instance_id: this.state.instance.id,
});
} catch (e) {
notify("Error creating save", "error", e.message);
this.setState({
creatingSave: false,
});
return;
}
notify("Save created", "success");
this.setState({
creatingSave: false,
});
}
async onConfigEditFinish(values) {
console.log("Completed config edit with", values);
for (let k in values) {
if (values[k] !== this.getInstanceConfigAsKvObject(this.state.config)[k]) {
let value = values[k];
if (typeof value === "boolean") value = value.toString();
console.log("Setting", k, value);
await setInstanceConfigField({
instance_id: this.state.instance.id,
field: k,
value: value,
});
}
}
notify("Set configuration fields", "success");
this.setState({
editingConfig: false,
});
this.getData();
}
render() {
console.log(this.state);
let { instance, slaves } = this.state;
let slave = slaves && slaves.find(x => x.id === instance.assigned_slave);
return <Card>
<h2>{instance.name}</h2>
<p>ID: {instance.id}</p>
<p>Slave: {slave && slave.name}</p>
{instance.status !== "running" && <Button
loading={this.state.starting}
type="primary"
onClick={this.start.bind(this)}
>
Start server
</Button>}
{instance.status !== "stopped" && <Button
loading={this.state.stopping}
type="primary"
onClick={this.stop.bind(this)}
>
Stop server
</Button>}
{instance.status !== "running" && <Button
loading={this.state.creatingSave}
type="primary"
onClick={this.createSave.bind(this)}
>
Create save
</Button>}
<Popover
content={
<Select style={{
width: "200px",
}}
onChange={async newSlaveId => {
let result = await assignInstance({ instance_id: instance.id, slave_id: newSlaveId });
notify("Assigned to slave", "success");
this.getData();
}}
>
{this.state.slaves.map(slave => <Option defaultValue={instance.assigned_slave} value={slave.id} key={slave.id}>
{slave.name}
</Option>)}
</Select>
}
trigger="click"
type={instance.status === "unassigned" ? "primary" : "default"}
>
<Button>
Assign to slave
</Button>
</Popover>
<Popover
content={
<Button
type="primary"
danger
onClick={async () => {
await deleteInstance({ instance_id: instance.id });
notify("Deleted instance " + instance.id, "success");
this.navigate("/instances");
}}
>
Delete this instance permanently
</Button>
}
trigger="click"
>
<Button danger style={{ float: "right", fontSize: "16px" }}>
<DeleteOutlined />
</Button>
</Popover>
<style>
{/* Scroll sticks to bottom of terminal window after manually scrolling to the bottom */}
{`#scroller * {
/* don't allow the children of the scrollable element to be selected as an anchor node */
overflow-anchor: none;
}
#anchor {
/* allow the final child to be selected as an anchor node */
overflow-anchor: auto;
/* anchor nodes are required to have non-zero area */
height: 1px;
}`}
</style>
<div id="scroller" style={{
height: "300px",
overflowY: "scroll",
}}>
{this.state.logLines.map(item => <p>{item.time} {item.level} {item.file}: {item.message}</p>)}
<div id="anchor" ref={this.messagesEndRef}></div>
</div>
{/* rcon input field */}
<Form
onFinish={async values => {
await sendRcon({ instance_id: instance.id, command: values.command });
}}
>
<Form.Item
label="RCON: "
name="command"
>
<Input />
</Form.Item>
</Form>
{this.state.config && this.renderConfig(this.state.config)}
</Card>;
}
getInstanceConfigAsKvObject(config) {
let object = {};
for (let group of config.serialized_config.groups) {
for (let fieldName of Object.keys(group.fields)) {
object[group.name + "." + fieldName] = group.fields[fieldName];
}
}
return object;
}
renderConfig(config) {
const layout = {
labelCol: {
span: 6,
},
wrapperCol: {
span: 14,
},
};
const tailLayout = {
wrapperCol: {
offset: 6,
span: 14,
},
};
return <Card>
<Form
{...layout}
initialValues={this.getInstanceConfigAsKvObject(config)}
onFinish={this.onConfigEditFinish.bind(this)}
>
{this.state.editingConfig ?
<Form.Item>
<Button type="primary" htmlType="submit">
Save config changes
</Button>
</Form.Item>
:
<Button onClick={() => this.setState({ editingConfig: true })}>
<EditOutlined /> Edit config
</Button>
}
<Row>
{config && config.serialized_config.groups.map(group => <>
<Col span={24} lg={12}>
<Divider
// orientation="left"
>{group.name}</Divider>
{/* {Object.keys(group.fields).map(field => <>
<span>{field}: </span>
<span>{JSON.stringify(group.fields[field])}</span>
<br />
</>)} */}
{console.log(group)}
{Object.keys(group.fields).map(field => <ConfigFormItem field={field} group={group} editingConfig={this.state.editingConfig} />)}
</Col>
</>)}
</Row>
</Form>
</Card>;
}
}
function renderDisplayField(value) {
if (typeof value === "object") {
return JSON.stringify(value);
}
if (typeof value === "boolean") {
return <Checkbox />;
}
return value.toString();
}
function renderFormField(value) {
if (typeof value === "object") {
return <ObjectInput />;
}
if (typeof value === "boolean") {
return <Checkbox />;
}
return <Input />;
}
class ObjectInput extends Component {
constructor(props) {
super(props);
console.log(this.props);
/*
value: {
auto_pause: false,
tags: ["clusterio"]
},
id: "factorio.settings",
onChange: f()
*/
this.state = {};
}
onChange(e, key) {
if (e.target.type === "checkbox") {
// if (e.target.checked !== undefined) {
// Its a checkbox, set value to true/false
let value = { ...this.props.value };
value[key] = e.target.checked || false;
this.props.onChange(value);
} else if (e.target.type === "text") {
// } else if (e.target.value !== undefined) {
// its probably a text input
let value = { ...this.props.value };
value[key] = e.target.value || "";
this.props.onChange(value);
}
}
render() {
let { value } = this.props;
let ret = [];
for (let key in value) {
if (typeof value[key] === "boolean") {
ret.push(<Checkbox
key={key}
checked={value[key]}
onChange={e => this.onChange(e, key)}
>
{key}
</Checkbox>
);
} else if (typeof value[key] === "string") {
ret.push(<div
key={key}
><span>{key}</span><Input
value={value[key]}
onChange={e => this.onChange(e, key)}
/></div>
);
}
}
ret.push(<Popover
key="addMore"
content={<>
<Input onChange={e => this.setState({ newInput: e.target.value })} />
<Button
onClick={e => {
this.onChange({ target: { value: "", type: "text" } }, this.state.newInput);
this.setState({
newInput: undefined,
addPopoverVisible: false,
});
}}
><PlusOutlined />Add property</Button>
</>}
trigger="click"
visible={this.state.addPopoverVisible}
handleVisibleChange={visible => this.setState({ addPopoverVisible: visible })}
>
<Button
onClick={e => this.setState({ addPopoverVisible: true })}
>
<PlusOutlined />Add property
</Button>
</Popover>);
return <Space direction="vertical">{ret}</Space>;
}
}
class ConfigFormItem extends Component {
render() {
let { field, group, editingConfig } = this.props;
return <Form.Item
label={field}
name={group.name + "." + field}
valuePropName={typeof group.fields[field] === "boolean" ? "checked" : "value" /* Boolean fields should be rendered as checkboxes */}
>
{editingConfig ?
renderFormField(group.fields[field])
:
renderDisplayField(group.fields[field])
}
</Form.Item>;
}
}
@@ -0,0 +1,42 @@
import React, { Component } from "react";
import { Card } from "antd";
import { listSlaves, listInstances, createInstance } from "../../util/wslink";
import DataTable from "../../components/data-table";
const config = require("@clusterio/lib/config");
export class InstancesTable extends Component {
navigate(url) {
this.props.history.push(url);
}
render() {
return <Card>
<h2>Instances</h2>
<DataTable
AddRecord={{
fields: [{
dataIndex: "name",
title: "Name",
}],
insert: async args => {
let instanceConfig = new config.InstanceConfig();
await instanceConfig.init();
console.log("Base config", instanceConfig);
instanceConfig.set("instance.name", args.name);
let serialized_config = instanceConfig.serialize();
let response = await createInstance(serialized_config);
console.log("Created instance", response);
},
}}
DataFunction={listInstances}
TableProps={{
onRow: (record, rowIndex) => ({
onClick: event => {
this.navigate(`/instances/${record.id}/view`);
},
}),
}}
/>
</Card>;
}
}
@@ -0,0 +1,26 @@
import React, { Component } from "react";
import { Card } from "antd";
import { listSlaves, listInstances, listPermissions } from "../../util/wslink";
import DataTable from "../../components/data-table";
export class PermissionsTable extends Component {
navigate(url) {
this.props.history.push(url);
}
render() {
return <Card>
<h2>Permissions</h2>
<DataTable
DataFunction={listPermissions}
TableProps={{
onRow: (record, rowIndex) => ({
onClick: event => {
this.navigate(`/permissions/${record.id}/view`);
},
}),
}}
/>
</Card>;
}
}
+118
View File
@@ -0,0 +1,118 @@
import React, { Component } from "react";
import { Card, Button, Popover, Select, Transfer } from "antd";
import DeleteOutlined from "@ant-design/icons/DeleteOutlined";
import {
listSlaves,
listInstances,
startInstance,
stopInstance,
setInstanceOutputSubscriptions,
deleteInstance,
assignInstance,
createSave,
getInstanceConfig,
setInstanceConfigField,
sendRcon,
deleteUser,
listUsers,
listRoles,
setRoles,
listPermissions,
updateRole,
} from "../../util/wslink";
import DataTable from "../../components/data-table";
import notify from "../../util/notify";
const { Option } = Select;
export class RoleView extends Component {
constructor(props) {
super(props);
this.state = {
role: {},
};
this.messagesEndRef = React.createRef();
}
navigate(url) {
this.props.history.push(url);
}
async componentDidMount() {
await this.getData();
}
async getData() {
let id = this.props.match.params.id;
let roles = await listRoles();
let permissions = await listPermissions();
this.setState({
role: roles.find(x => x.id === Number(id)),
permissions,
});
}
async handleChange(targetKeys) {
this.setState({
role: {
...this.state.role,
permissions: targetKeys,
},
});
let response = await updateRole({
...this.state.role,
permissions: targetKeys,
});
console.log("Updated user roles", response);
}
render() {
console.log(this.state);
let { role, permissions } = this.state;
return <Card>
<Popover
content={
<Button
type="primary"
danger
onClick={async () => {
await deleteUser({ role: role.name });
notify("Deleted role " + role.name, "success");
this.navigate("/roles");
}}
>
Delete this role permanently
</Button>
}
trigger="click"
>
<Button danger style={{ float: "right", fontSize: "16px" }}>
<DeleteOutlined />
</Button>
</Popover>
<h2>{role.name}</h2>
<h3>Assign roles</h3>
<style>
{`
.ant-transfer-list{
width: calc( 50% - 34px );
height: 500px;
}
`}
</style>
<Transfer
titles={["Available", "Assigned"]}
dataSource={permissions}
showSearch
filterOption={(inputValue, option) => option.name.toLowerCase().indexOf(inputValue.toLowerCase()) > -1}
targetKeys={role.permissions}
onChange={this.handleChange.bind(this)}
render={item => item.name}
/>
</Card>;
}
}
@@ -0,0 +1,39 @@
import React, { Component } from "react";
import { Card } from "antd";
import { listSlaves, listInstances, listRoles, createRole } from "../../util/wslink";
import DataTable from "../../components/data-table";
export class RolesTable extends Component {
navigate(url) {
this.props.history.push(url);
}
render() {
return <Card>
<h2>Roles</h2>
<DataTable
AddRecord={{
fields: [{
dataIndex: "name",
title: "Name",
},{
dataIndex: "description",
title: "Description",
}],
insert: async args => {
let response = await createRole({name: args.name, description: args.description});
console.log("Created role",response);
},
}}
DataFunction={listRoles}
TableProps={{
onRow: (record, rowIndex) => ({
onClick: event => {
this.navigate(`/roles/${record.id}/view`);
},
}),
}}
/>
</Card>;
}
}
@@ -0,0 +1,83 @@
import React, { Component } from "react";
import { Card, Button, Input, Form } from "antd";
const layout = {
labelCol: {
span: 4,
},
wrapperCol: {
span: 16,
},
};
const tailLayout = {
wrapperCol: {
offset: 4,
span: 16,
},
};
export class Settings extends Component {
constructor(props) {
super(props);
this.state = {
role: {},
};
}
navigate(url) {
this.props.history.push(url);
}
render() {
console.log(this.state);
return <Card>
<h2>Web interface settings</h2>
<Form
{...layout}
name="basic"
initialValues={{
master_url: localStorage.getItem("master_url"),
master_token: localStorage.getItem("master_token"),
}}
onFinish={values => {
console.log("Saving settings",values);
for (let key of Object.keys(values)){
localStorage.setItem(key, values[key].replace("https://", "wss://").replace("http://", "ws://"));
}
// Force refresh to apply changes
document.location = document.location;
}}
>
<Form.Item
label="Master URL"
name="master_url"
rules={[
{
required: true,
message: "Please input master url, ex ws://localhost:8080 or wss://localhost:8443/ . For SSL connections, ensure you have a valid certificate.",
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="User auth token"
name="master_token"
rules={[
{
required: true,
message: "Please input your user auth token from config-control.json",
},
]}
>
<Input />
</Form.Item>
<Form.Item {...tailLayout}>
<Button type="primary" htmlType="submit">
Save
</Button>
</Form.Item>
</Form>
</Card>;
}
}
@@ -0,0 +1,26 @@
import React, { Component } from "react";
import { Card } from "antd";
import { listSlaves, listInstances } from "../../util/wslink";
import DataTable from "../../components/data-table";
export class SlavesTable extends Component {
navigate(url) {
this.props.history.push(url);
}
render() {
return <Card>
<h2>Slaves</h2>
<DataTable
DataFunction={listSlaves}
TableProps={{
onRow: (record, rowIndex) => ({
onClick: event => {
this.navigate(`/slaves/${record.id}/view`);
},
}),
}}
/>
</Card>;
}
}
+95
View File
@@ -0,0 +1,95 @@
import React, { Component } from "react";
import { Card, Button, Popover, Select, Transfer } from "antd";
import {
deleteUser,
listUsers,
listRoles,
setRoles,
} from "../../util/wslink";
import DataTable from "../../components/data-table";
import notify from "../../util/notify";
import DeleteOutlined from "@ant-design/icons/DeleteOutlined";
const { Option } = Select;
export class UserView extends Component {
constructor(props) {
super(props);
this.state = {
user: {},
};
this.messagesEndRef = React.createRef();
}
navigate(url) {
this.props.history.push(url);
}
async componentDidMount() {
await this.getData();
}
async getData() {
let name = this.props.match.params.id;
let users = await listUsers();
let roles = await listRoles();
this.setState({
user: users.find(x => x.name === name),
roles,
});
}
async handleChange(targetKeys) {
this.setState({
user: {
...this.state.user,
roles: targetKeys,
},
});
let response = await setRoles({
name: this.state.user.name,
roles: targetKeys,
});
console.log("Updated user roles",response);
};
render() {
console.log(this.state);
let { user, roles } = this.state;
return <Card>
<h2>{user.name}</h2>
<h3>Assign roles</h3>
<Transfer
titles={["Available","Assigned"]}
dataSource={roles}
showSearch
filterOption={(inputValue, option) => option.name.toLowerCase().indexOf(inputValue.toLowerCase()) > -1}
targetKeys={user.roles}
onChange={this.handleChange.bind(this)}
render={item => item.name}
/>
<Popover
content={
<Button
type="primary"
danger
onClick={async () => {
await deleteUser({ name: user.name });
notify("Deleted user " + user.name, "success");
this.navigate("/users");
}}
>
Delete this user from the cluster
</Button>
}
trigger="click"
>
<Button danger style={{ float: "right", fontSize: "16px" }}>
<DeleteOutlined />
</Button>
</Popover>
</Card>;
}
}
@@ -0,0 +1,36 @@
import React, { Component } from "react";
import { Card } from "antd";
import { listSlaves, listInstances, listUsers, createUser } from "../../util/wslink";
import DataTable from "../../components/data-table";
export class UsersTable extends Component {
navigate(url) {
this.props.history.push(url);
}
render() {
return <Card>
<h2>Slaves</h2>
<DataTable
AddRecord={{
fields: [{
dataIndex: "name",
title: "Name",
}],
insert: async args => {
let response = await createUser({name: args.name});
console.log("Created user",response);
},
}}
DataFunction={listUsers}
TableProps={{
onRow: (record, rowIndex) => ({
onClick: event => {
this.navigate(`/users/${record.name}/view`);
},
}),
}}
/>
</Card>;
}
}
File renamed without changes.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

+10
View File
@@ -0,0 +1,10 @@
import { notification } from "antd";
export default function notify(message, type = "info", description = undefined) {
notification[type]({
message: typeof message === "string" ? message : "ERROR: See console",
description,
placement: "bottomRight",
});
if (typeof message !== "string") console.error(message);
}
+100
View File
@@ -0,0 +1,100 @@
const link = require("@clusterio/lib/link");
const plugin = require("@clusterio/lib/plugin");
const errors = require("@clusterio/lib/errors");
/**
* Format a parsed Factorio output message
*
* Formats a parsed Factorio output from lib/factorio back into the
* text string it was parsed from.
*
* @param {Object} output - Factorio server output.
* @returns {string} origial output text.
* @private
*/
function formatOutput(output) {
let time = "";
if (output.format === "seconds") {
time = `${output.time.padStart(8)} `;
} else if (output.format === "date") {
time = `${output.time} `;
}
let info = "";
if (output.type === "log") {
info = `${output.level} ${output.file}: `;
} else if (output.type === "action") {
info = `[${output.action}] `;
}
return `${time}${info}${output.message}`;
}
/**
* Connector for control connection to master server
* @private
*/
export class ControlConnector extends link.WebSocketClientConnector {
constructor(url, reconnectDelay, token) {
super(url, reconnectDelay);
this._token = token;
}
register() {
console.log("SOCKET | registering control");
this.sendHandshake("register_control", {
token: this._token,
agent: "web",
version: "2.0.0-alpha",
});
}
}
/**
* Handles running the control
*
* Connects to the master server over WebSocket and sends commands to it.
* @static
*/
export class Control extends link.Link {
constructor(connector, controlPlugins) {
super("control", "master", connector);
link.attachAllMessages(this);
/**
* Mapping of plugin names to their instance for loaded plugins.
* @type {Map<string, module:lib/plugin.BaseControlPlugin>}
*/
this.plugins = controlPlugins;
for (let controlPlugin of controlPlugins.values()) {
plugin.attachPluginMessages(this, controlPlugin.info, controlPlugin);
}
}
async instanceOutputEventHandler(message) {
let { instance_id, output } = message.data;
console.log(formatOutput(output));
if (window.instanceOutputEventHandler) {
window.instanceOutputEventHandler({instance_id, output});
}
}
async debugWsMessageEventHandler(message) {
console.log("WS", message.data.direction, message.data.content);
}
async shutdown() {
this.connector.setTimeout(30);
try {
await link.messages.prepareDisconnect.send(this);
} catch (err) {
if (!(err instanceof errors.SessionLost)) {
throw err;
}
}
await this.connector.close(1001, "Control Quit");
}
}
+134
View File
@@ -0,0 +1,134 @@
import { Control, ControlConnector } from "./../util/websocket";
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
localStorage.getItem("master_token"),
);
control = new Control(controlConnector, []);
try {
await controlConnector.connect();
} catch (err) {
console.log(err);
}
})();
let listSlaves = createListFunction("listSlaves");
let listInstances = createListFunction("listInstances");
let listUsers = createListFunction("listUsers", "name");
let listRoles = createListFunction("listRoles");
let listPermissions = createListFunction("listPermissions", "name");
async function startInstance({ instance_id, save = null }) {
return await link.messages.startInstance.send(control, { instance_id, save });
};
async function stopInstance({ instance_id }) {
return await link.messages.stopInstance.send(control, { instance_id });
};
async function createInstance(serialized_config) {
return await link.messages.createInstance.send(control, { serialized_config });
};
async function deleteInstance({ instance_id }) {
return await link.messages.deleteInstance.send(control, { instance_id });
};
async function assignInstance({ instance_id, slave_id }) {
return await link.messages.assignInstanceCommand.send(control, {
instance_id,
slave_id,
});
};
async function createSave({ instance_id }) {
return await link.messages.createSave.send(control, { instance_id });
};
async function getInstanceConfig({ instance_id }) {
return await link.messages.getInstanceConfig.send(control, { instance_id });
};
async function setInstanceConfigField({ instance_id, field, value }) {
return await link.messages.setInstanceConfigField.send(control, {
instance_id,
field,
value: typeof value === "object" ? JSON.stringify(value) : value,
});
};
async function setInstanceConfigProp({ instance_id, field, prop, value }) {
return await link.messages.setInstanceConfigProp.send(control, {
instance_id,
field,
prop,
value, // JSON Object
});
};
async function sendRcon({ instance_id, command }) {
return await link.messages.sendRcon.send(control, { instance_id, command });
};
async function setInstanceOutputSubscriptions({ instance_id }) {
return await link.messages.setInstanceOutputSubscriptions.send(control, { instance_ids: [instance_id] });
};
async function createUser({ name }) {
return await link.messages.createUser.send(control, { name });
};
async function deleteUser({ name }) {
return await link.messages.deleteUser.send(control, { name });
};
async function setRoles({ name, roles }) {
return await link.messages.updateUserRoles.send(control, {
name,
roles,
});
};
async function createRole({ name, description, permissions = [] }) {
return await link.messages.createRole.send(control, { name, description, permissions });
};
async function updateRole({ id, name, description, permissions }) {
return await link.messages.updateRole.send(control, {
id, name, description, permissions,
});
};
export {
listSlaves,
listInstances,
listUsers,
createUser,
deleteUser,
listRoles,
setRoles,
createRole,
updateRole,
listPermissions,
startInstance,
stopInstance,
createInstance,
deleteInstance,
assignInstance,
createSave,
getInstanceConfig,
setInstanceConfigField,
setInstanceConfigProp,
sendRcon,
setInstanceOutputSubscriptions,
};
function createListFunction(name, key) {
return async () => (
(await link.messages[name].send(control)).list.map(addKey(key || "id"))
);
}
function addKey(key) {
return function (el) {
return {
...el,
key: el[key],
};
};
}
+71
View File
@@ -0,0 +1,71 @@
"use strict";
const path = require("path");
const webpack = require("webpack");
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"),
},
performance: {
maxAssetSize: 2**21,
maxEntrypointSize: 2**21,
},
plugins: [
new CleanWebpackPlugin(),
new webpack.DefinePlugin({
"process.env.APP_ENV": JSON.stringify("browser"),
}),
// 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",
},
});
-14
View File
@@ -30,19 +30,6 @@ const masterInventoryGauge = new prometheus.Gauge(
class MasterPlugin extends plugin.BaseMasterPlugin {
async init() {
let externalAddress = this.master.config.get("master.external_address");
let root = externalAddress ? new URL(externalAddress).pathname : "/";
this.ui = {
sidebar: [
{
getHtml: () => `
<div class="nav-item mr-1">
<a class="nav-link align-middle" href="${root}subspace_storage/storage">Storage</a>
</div>`,
},
],
};
this.items = await loadDatabase(this.master.config);
this.itemsLastUpdate = new Map(this.items._items.entries());
this.autosaveId = setInterval(() => {
@@ -61,7 +48,6 @@ class MasterPlugin extends plugin.BaseMasterPlugin {
}, 1000);
routes.addApiRoutes(this.master.app, this.items, this.metrics.endpointHitCounter);
routes.addWebRoutes(this.master.app);
}
updateStorage() {
-6
View File
@@ -5,11 +5,6 @@ const path = require("path");
const prometheus = require("@clusterio/lib/prometheus");
function addWebRoutes(app) {
app.use("/subspace_storage", express.static(path.join(__dirname, "static")));
app.get("/subspace_storage/storage", (req, res) => { res.render("subspace_storage/views/storage"); });
}
function addApiRoutes(app, items, endpointHitCounter) {
/**
@@ -52,6 +47,5 @@ function addApiRoutes(app, items, endpointHitCounter) {
}
module.exports = {
addWebRoutes,
addApiRoutes,
};
@@ -1,99 +0,0 @@
// function to draw data we recieve from ajax requests
"use strict";
var _lastData = [];
function drawcontents(data) {
data = data || _lastData; //Cache data so we can drawcontents without waiting for the server, for the search box.
_lastData = data;
// allow searching for multiple criteria separated by space
var searchArgs = document.querySelector("#search").value;
searchArgs = searchArgs.trim();
searchArgs = "(" + searchArgs.replace(/ +/g,")|(") + ")";
const search = new RegExp(searchArgs, "i");
data = data.filter(function(item) {
return search.test(item.name);
});
sortByKey(data, "count");
const table = document.querySelector("#contents tbody"); //tables have tbody inserted automatically
const rows = table.children;
//update existing rows or create new ones
data.forEach(function(item, i) {
// format count to have .s in it for large number readability (1,000,000)
item.count = item.count.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
let row = rows[i];
if(!row) {
row = document.createElement("tr");
row.innerHTML = "<td><div></div></td><td class=name></td><td class=count></td>";
table.appendChild(row);
}
const itemIcon = row.querySelector("div");
const iconClass = getItemIconClass(item.name);
if (itemIcon.getAttribute("class") !== iconClass) {
itemIcon.setAttribute("class", iconClass);
}
let localeName = item.name;
let meta = factorioItemMetadata.get(item.name);
if (meta && meta.localised_name) {
// TODO implement the locale to name conversion.
localeName = factorioLocale.get(meta.localised_name[0]);
} else {
for (let section of ["item-name", "entity-name", "fluid-name", "equipment-name"]) {
let name = factorioLocale.get(`${section}.${item.name}`);
if (name) {
localeName = name;
break;
}
}
}
const name = row.querySelector(".name");
if(name.textContent !== localeName) {
name.textContent = localeName;
}
const count = row.querySelector(".count");
if(count.textContent !== String(item.count)) {
count.textContent = item.count;
}
});
//remove excess rows, for example, after filtering
while (data.length < rows.length) {
table.removeChild(rows[data.length]);
}
}
// get cluster inventory from master
function updateInventory() {
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if(xmlhttp.readyState == 4 && xmlhttp.status == 200) {
let data = JSON.parse(xmlhttp.responseText);
drawcontents(data);
}
};
xmlhttp.open("GET", `${root}api/inventory`, true);
xmlhttp.send();
}
if(JSON.parse(localStorage.settings)["Periodically update storage screen"]) {
setInterval(updateInventory, 500);
} else {
updateInventory();
}
// function to sort arrays of objects after a keys value
function sortByKey(array, key) {
array.sort(function(a, b) {
return b[key] - a[key];
});
}
document.querySelector("#search").addEventListener("input", function() {
drawcontents();
});
@@ -1,41 +0,0 @@
<!doctype HTML>
<head>
<%- include("head") %>
<script defer src="<%= locals.root %>subspace_storage/storage.js"></script>
</head>
<body>
<%- include("nav") %>
<div id="body" class="container">
<div id="storage">
<div class="page-header mt-2 row" id="banner">
<div class="row">
<div class="col-lg-8 col-md-7 col-sm-6">
<h2>Storage</h2>
</div>
</div>
</div>
<div class="form-group mt-3 mb-3 row">
<input type="text" class="form-control" id="search" placeholder="search">
</div>
<div id="contents" class="row w-100">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th scope="col">Icon</th>
<th scope="col">Resource</th>
<th scope="col">Quantity</th>
</tr>
</thead>
<tbody><!-- populated by js --></tbody>
</table>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
crossorigin="anonymous"></script>
</body>
-20
View File
@@ -2,8 +2,6 @@
const assert = require("assert").strict;
const fs = require("fs-extra");
const path = require("path");
const validateHTML = require("html5-validator");
const parallel = require("mocha.parallel");
const link = require("@clusterio/lib/link");
@@ -11,24 +9,6 @@ const { slowTest, get, execCtl, sendRcon, getControl, instancesDir } = require("
describe("Integration of Clusterio", function() {
parallel("master web interface", function() {
this.timeout(6000);
let paths = ["/", "/nodes", "/settings", "/nodeDetails"];
for (let path of paths) {
it(`sends some HTML when accessing ${path}`, async function() {
let res = await get(path);
let validation = await validateHTML(res.body.toString());
let filtered = validation.messages.filter(msg => msg.type !== "info");
assert(
filtered.length === 0,
"there are HTML errors on the page, please fix: "+JSON.stringify(validation.messages, null, 4)
);
});
}
});
describe("clusterioctl", function() {
describe("slave list", function() {
it("runs", async function() {
-5
View File
@@ -1,5 +0,0 @@
// These are libraries that does not have any units test, they are
// required here in order to track coverage for them.
"use strict";
require("@clusterio/master/npm_post_install.js");
+17
View File
@@ -18,6 +18,23 @@ class MockSocket {
this.events.set(event, fn);
}
/* eslint-disable accessor-pairs */
set onclose(fn) {
this.on("close", (code, reason) => fn({ code, reason }));
}
set onerror(fn) {
this.on("error", fn);
}
set onopen(fn) {
this.on("open", fn);
}
set onmessage(fn) {
this.on("message", (data) => fn({ data }));
}
terminate() {
this.terminateCalled = true;
}
-29
View File
@@ -1,29 +0,0 @@
<title>Clusterio</title>
<link rel="stylesheet" type="text/css" href="<%= locals.root %>style.css">
<script>root = "<%= locals.root %>";</script>
<script src="<%= locals.root %>external/jquery.js"></script>
<script src="<%= locals.root %>external/jquery-ui.js"></script>
<script src="<%= locals.root %>external/moment.min.js"></script>
<script src="<%= locals.root %>external/mousetrap.min.js"></script>
<script src="<%= locals.root %>main.js"></script>
<link href="<%= locals.root %>bootstrap.min.css" rel="stylesheet">
<link href="<%= locals.root %>external/css/Roboto.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Orbitron" rel="stylesheet">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.0/css/all.css" integrity="sha384-aOkxzJ5uQz7WBObEZcHvV5JvRW3TUc2rNPA7pe3AwnsUohiw1Vj2Rgx2KSOkF5+h" crossorigin="anonymous">
<link rel="apple-touch-icon" sizes="57x57" href="/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
<link rel="manifest" href="/favicon/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
-17
View File
@@ -1,17 +0,0 @@
<!doctype HTML>
<head>
<%- include("head") %>
</head>
<body>
<%- include("nav") %>
<div id="body" class="container">
<div id="storage">
<div class="page-header mt-2 row" id="banner">
<div class="row">
<h2>Work in progress interface</h2>
</div>
</div>
</div>
</div>
</body>
-29
View File
@@ -1,29 +0,0 @@
<nav class="navbar nav-pills nav-fill">
<div class="nav-item col-3">
<a class="navbar-brand border-0" href="/">
<img src="<%= locals.root %>pictures/logo.png" width="50" height="50" class="d-inline-block align-middle " alt=""/>
<h3 class=" ml-3 nav-title">Clusterio</h3>
</a>
</div>
<div class="nav-item mr-1">
<a class="nav-link align-middle" href="<%= locals.root %>nodes">Slaves</a>
</div><%
for (let plug of locals.masterPlugins.values()) {
if (
plug
&& plug.ui
&& typeof plug.ui.sidebar == "object"
&& Array.isArray(plug.ui.sidebar)
) {
let sidebarEntries = plug.ui.sidebar;
for (let button of sidebarEntries) {
if (button.getHtml && typeof button.getHtml == "function") { %>
<%- button.getHtml() %><%
}
}
}
} %>
<div class="nav-item">
<a class="nav-link align-middle" href="<%= locals.root %>settings">Settings</a>
</div>
</nav>
-32
View File
@@ -1,32 +0,0 @@
<!doctype HTML>
<head>
<%- include("head") %>
<script src="lib/terminal.js"></script>
<script src="nodeDetails.js"></script>
<link rel="stylesheet" type="text/css" href="nodeDetails.css">
</head>
<body>
<%- include("nav") %>
<div id="body" class="container">
<div id="hero">
<div id="display">
</div>
</div>
<div id="details">
</div>
<div id="terminal">
<div class="titleBar" style="display:none;">
<div class="title">
<p>Clusterio RCON shell</p>
</div>
<div class="titleButton close" onclick="minimizeTerminal();"></div>
<div class="titleButton maximize" onclick="maximizeTerminal();"></div>
<div class="titleButton minimize" onclick="minimizeTerminal();"></div>
</div>
<!-- JS generates a div called .Terminal that appears here with the display and typing things -->
</div>
</div>
</body>
-47
View File
@@ -1,47 +0,0 @@
<!doctype HTML>
<head>
<%- include("head") %>
</head>
<body>
<%- include("nav") %>
<div id="body" class="container">
<div class="page-header mt-2 row" id="banner">
<div class="col-lg-8 col-md-7 col-sm-6">
<h2>Connected Nodes</h2>
</div>
</div>
<div id="display"><%
for (let [id, slave] of locals.slaves) { %>
<div id="<%= id %>" class="slaveBox card mb-3">
<div class="card-body">
<h5 class="card-title"><a href='nodeDetails?instanceID=<%=id%>'>
<%= slave.name %>
</a></h5>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">ID: <%= id %></li>
</ul><%
for (let plug of locals.masterPlugins.values()) {
if (
plug
&& plug.ui
&& typeof plug.ui.slaveListing == "object"
&& Array.isArray(plug.ui.slaveListing)
) {
let slaveListingEntries = plug.ui.slaveListing;
for (let button of slaveListingEntries) {
if (button.getHtml && typeof button.getHtml == "function") { %>
<div class="plugin-link"><%- button.getHtml({slave}) %></div><%
}
}
}
} %>
</div><%
} %>
</div>
</div>
<script>
$('.plugin-link a').addClass('btn btn-primary')
</script>
</body>
-19
View File
@@ -1,19 +0,0 @@
<!doctype HTML>
<head>
<%- include("head") %>
</head>
<body>
<%- include("nav") %>
<div id="body" class="container">
<div class="page-header mt-2 row" id="banner">
<div class="row">
<div class="col-lg-8 col-md-7 col-sm-6">
<h2>Settings</h2>
</div>
</div>
</div>
</div>
<script src="settings.js"></script>
</body>
-1
View File
@@ -1 +0,0 @@
<%- include("nav") %>