diff --git a/packages/master/master.js b/packages/master/master.js
index 53ac5d90..c8efaa2b 100755
--- a/packages/master/master.js
+++ b/packages/master/master.js
@@ -40,6 +40,7 @@ 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");
@@ -71,6 +72,26 @@ app.use(bodyParser.urlencoded({
app.use(fileUpload());
app.use(compression());
+
+// 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);
+ });
+ };
+}
+
// 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
@@ -1698,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);
diff --git a/packages/master/package.json b/packages/master/package.json
index a18ea824..2827404d 100644
--- a/packages/master/package.json
+++ b/packages/master/package.json
@@ -43,7 +43,6 @@
"clean-webpack-plugin": "^3.0.0",
"css-loader": "^4.3.0",
"fast-deep-equal": "^3.1.3",
- "html-webpack-plugin": "^4.5.0",
"null-loader": "^4.0.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
diff --git a/packages/master/src/routes.js b/packages/master/src/routes.js
new file mode 100644
index 00000000..7e0ca52a
--- /dev/null
+++ b/packages/master/src/routes.js
@@ -0,0 +1,16 @@
+"use strict";
+// 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",
+];
diff --git a/packages/master/web/basename.jsx b/packages/master/web/basename.jsx
new file mode 100644
index 00000000..99b8468a
--- /dev/null
+++ b/packages/master/web/basename.jsx
@@ -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;
diff --git a/packages/master/web/components/App.jsx b/packages/master/web/components/App.jsx
new file mode 100644
index 00000000..b1db4c6e
--- /dev/null
+++ b/packages/master/web/components/App.jsx
@@ -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 (
+