diff --git a/packages/slave/slave.js b/packages/slave/slave.js
index ecc492ed..a4db4b7e 100755
--- a/packages/slave/slave.js
+++ b/packages/slave/slave.js
@@ -886,6 +886,11 @@ async function discoverInstances(instancesDir) {
continue;
}
+ if (instanceInfos.has(instanceConfig.get("instance.id"))) {
+ logger.warn(`Ignoring instance with duplicate id in folder ${entry.name}`);
+ continue;
+ }
+
let instancePath = path.join(instancesDir, entry.name);
logger.verbose(`found instance ${instanceConfig.get("instance.name")} in ${instancePath}`);
instanceInfos.set(instanceConfig.get("instance.id"), {
diff --git a/packages/web_ui/src/components/InstanceList.jsx b/packages/web_ui/src/components/InstanceList.jsx
new file mode 100644
index 00000000..4a010ffa
--- /dev/null
+++ b/packages/web_ui/src/components/InstanceList.jsx
@@ -0,0 +1,58 @@
+import React from "react";
+import { useHistory } from "react-router-dom";
+import { Table } from "antd";
+
+import { useSlaveList } from "../model/slave";
+
+const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "base" }).compare;
+
+
+export default function InstanceList(props) {
+ let history = useHistory();
+ let [slaveList] = useSlaveList();
+
+ function slaveName(slaveId) {
+ let slave = slaveList.find(s => s.id === slaveId);
+ if (slave) {
+ return slave.name;
+ }
+ return String(slaveId);
+ }
+
+ let columns = [
+ {
+ title: "Name",
+ dataIndex: "name",
+ defaultSortOrder: "ascend",
+ sorter: (a, b) => strcmp(a["name"], b["name"]),
+ },
+ {
+ title: "Assigned Slave",
+ key: "assigned_slave",
+ render: instance => slaveName(instance["assigned_slave"]),
+ sorter: (a, b) => strcmp(slaveName(a["assigned_slave"]), slaveName(b["assigned_slave"])),
+ },
+ {
+ title: "Status",
+ dataIndex: "status",
+ sorter: (a, b) => strcmp(a["status"], b["status"]),
+ },
+ ];
+
+ if (props.hideAssignedSlave) {
+ columns.splice(1, 1);
+ }
+
+ return
instance["id"]}
+ pagination={false}
+ onRow={(record, rowIndex) => ({
+ onClick: event => {
+ history.push(`/instances/${record.id}/view`);
+ },
+ })}
+ />;
+}
diff --git a/packages/web_ui/src/components/InstanceViewPage.jsx b/packages/web_ui/src/components/InstanceViewPage.jsx
index e9bf72c9..c5a27cb5 100644
--- a/packages/web_ui/src/components/InstanceViewPage.jsx
+++ b/packages/web_ui/src/components/InstanceViewPage.jsx
@@ -9,7 +9,7 @@ import { useAccount } from "../model/account";
import ControlContext from "./ControlContext";
import PageLayout from "./PageLayout";
import InstanceConfigTree from "./InstanceConfigTree";
-import InstanceConsole from "./InstanceConsole";
+import LogConsole from "./LogConsole";
import InstanceRcon from "./InstanceRcon";
import AssignInstanceModal from "./AssignInstanceModal";
import StartStopInstanceButton from "./StartStopInstanceButton";
@@ -127,7 +127,7 @@ export default function InstanceViewPage(props) {
account.hasAnyPermission("core.log.follow", "core.instance.send_rcon")
&& Console
}
- {account.hasPermission("core.log.follow") && }
+ {account.hasPermission("core.log.follow") && }
{
account.hasPermission("core.instance.send_rcon")
&&
diff --git a/packages/web_ui/src/components/InstancesPage.jsx b/packages/web_ui/src/components/InstancesPage.jsx
index 85996fe2..fb6dbd71 100644
--- a/packages/web_ui/src/components/InstancesPage.jsx
+++ b/packages/web_ui/src/components/InstancesPage.jsx
@@ -1,6 +1,6 @@
import React, { useContext, useState } from "react";
import { useHistory } from "react-router-dom";
-import { Button, Form, Input, Modal, PageHeader, Table } from "antd";
+import { Button, Form, Input, Modal, PageHeader } from "antd";
import { libConfig, libLink } from "@clusterio/lib";
@@ -8,10 +8,8 @@ import { useAccount } from "../model/account";
import ControlContext from "./ControlContext";
import PageLayout from "./PageLayout";
import { useInstanceList } from "../model/instance";
+import InstanceList from "./InstanceList";
import { notifyErrorHandler } from "../util/notify";
-import { useSlaveList } from "../model/slave";
-
-const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "base" }).compare;
function CreateInstanceButton(props) {
@@ -61,19 +59,9 @@ function CreateInstanceButton(props) {
}
export default function InstancesPage() {
- let history = useHistory();
let account = useAccount();
- let [slaveList] = useSlaveList();
let [instanceList] = useInstanceList();
- function slaveName(slaveId) {
- let slave = slaveList.find(s => s.id === slaveId);
- if (slave) {
- return slave.name;
- }
- return String(slaveId);
- }
-
return
}
/>
- strcmp(a["name"], b["name"]),
- },
- {
- title: "Assigned Slave",
- key: "assigned_slave",
- render: instance => slaveName(instance["assigned_slave"]),
- sorter: (a, b) => strcmp(slaveName(a["assigned_slave"]), slaveName(b["assigned_slave"])),
- },
- {
- title: "Status",
- dataIndex: "status",
- sorter: (a, b) => strcmp(a["status"], b["status"]),
- },
- ]}
- dataSource={instanceList}
- rowKey={instance => instance["id"]}
- pagination={false}
- onRow={(record, rowIndex) => ({
- onClick: event => {
- history.push(`/instances/${record.id}/view`);
- },
- })}
- />
+
;
}
diff --git a/packages/web_ui/src/components/InstanceConsole.jsx b/packages/web_ui/src/components/LogConsole.jsx
similarity index 88%
rename from packages/web_ui/src/components/InstanceConsole.jsx
rename to packages/web_ui/src/components/LogConsole.jsx
index aa2d50ca..aa76c7a7 100644
--- a/packages/web_ui/src/components/InstanceConsole.jsx
+++ b/packages/web_ui/src/components/LogConsole.jsx
@@ -49,7 +49,7 @@ function formatLog(info, key) {
return [{level}] {info.message}
;
}
-export default function InstanceConsole(props) {
+export default function LogConsole(props) {
let account = useAccount();
let control = useContext(ControlContext);
let anchor = useRef();
@@ -57,16 +57,20 @@ export default function InstanceConsole(props) {
let [lines, setLines] = useState([]);
useEffect(() => {
+ let logFilter = {
+ all: props.all || false,
+ master: props.master || false,
+ slave_ids: props.slaves || [],
+ instance_ids: props.instances || [],
+ };
+
// Scroll view to the anchor so it sticks to the bottom
let parent = anchor.current.parentElement;
parent.scrollTop = parent.scrollHeight - parent.clientHeight;
if (account.hasPermission("core.log.query")) {
libLink.messages.queryLog.send(control, {
- all: false,
- master: false,
- slave_ids: [],
- instance_ids: [props.id],
+ ...logFilter,
max_level: null,
limit: 400,
order: "desc",
@@ -85,11 +89,11 @@ export default function InstanceConsole(props) {
));
}
- control.onInstanceLog(props.id, logHandler);
+ control.onLog(logFilter, logHandler);
return () => {
- control.offInstanceLog(props.id, logHandler);
+ control.offLog(logFilter, logHandler);
};
- }, [props.id]);
+ }, [props.all, props.master, props.slaves, props.instances]);
return <>
diff --git a/packages/web_ui/src/components/MasterPage.jsx b/packages/web_ui/src/components/MasterPage.jsx
index 53c83877..088d347e 100644
--- a/packages/web_ui/src/components/MasterPage.jsx
+++ b/packages/web_ui/src/components/MasterPage.jsx
@@ -1,15 +1,23 @@
import React, { useContext } from "react";
+import { Typography } from "antd";
+import LogConsole from "./LogConsole";
import { useAccount } from "../model/account";
import MasterConfigTree from "./MasterConfigTree";
import PageLayout from "./PageLayout";
+const { Title } = Typography;
+
export default function SlavesPage() {
let account = useAccount();
return
Master
+ {account.hasPermission("core.log.follow") && <>
+ Console
+
+ >}
{account.hasPermission("core.master.get_config") && }
;
};
diff --git a/packages/web_ui/src/components/SlaveViewPage.jsx b/packages/web_ui/src/components/SlaveViewPage.jsx
new file mode 100644
index 00000000..c5a985bd
--- /dev/null
+++ b/packages/web_ui/src/components/SlaveViewPage.jsx
@@ -0,0 +1,60 @@
+import React, { useContext, useState } from "react";
+import { Descriptions, PageHeader, Spin, Typography } from "antd";
+import { useParams } from "react-router-dom";
+
+import InstanceList from "./InstanceList";
+import LogConsole from "./LogConsole";
+import { useAccount } from "../model/account";
+import { useInstanceList } from "../model/instance";
+import { useSlave } from "../model/slave";
+import PageLayout from "./PageLayout";
+
+const { Title } = Typography;
+
+
+export default function SlaveViewPage(props) {
+ let params = useParams();
+ let slaveId = Number(params.id);
+ let account = useAccount();
+ let [instanceList] = useInstanceList();
+ let [slave] = useSlave(slaveId);
+
+ instanceList = instanceList.filter(instance => instance["assigned_slave"] === slaveId);
+
+ let nav = [{ name: "Slaves", path: "/slaves" }, { name: slave.name || slaveId }];
+ if (slave.loading) {
+ return ;
+ }
+
+ if (slave.missing) {
+ return
+
+ Slave with id {slaveId} was not found on the master server.
+ ;
+ }
+
+ return
+
+
+
+ {slave["name"]}
+ {slave["connected"] ? "yes" : "no"}
+ {slave["agent"]}
+ {slave["version"]}
+
+ {account.hasPermission("core.instance.list") && <>
+ Instances
+
+ >}
+ {account.hasPermission("core.log.follow") && <>
+ Console
+
+ >}
+ ;
+}
diff --git a/packages/web_ui/src/components/SlavesPage.jsx b/packages/web_ui/src/components/SlavesPage.jsx
index f94001a8..9333752a 100644
--- a/packages/web_ui/src/components/SlavesPage.jsx
+++ b/packages/web_ui/src/components/SlavesPage.jsx
@@ -1,4 +1,5 @@
import React, { useContext, useRef, useState } from "react";
+import { useHistory } from "react-router-dom";
import { Button, Form, Input, Modal, PageHeader, Table } from "antd";
import { libLink } from "@clusterio/lib";
@@ -75,6 +76,7 @@ function GenerateSlaveTokenButton(props) {
export default function SlavesPage() {
let account = useAccount();
+ let history = useHistory();
let [slaveList] = useSlaveList();
return
@@ -112,6 +114,11 @@ export default function SlavesPage() {
dataSource={slaveList}
rowKey={slave => slave["id"]}
pagination={false}
+ onRow={(record, rowIndex) => ({
+ onClick: event => {
+ history.push(`/slaves/${record.id}/view`);
+ },
+ })}
/>
;
};
diff --git a/packages/web_ui/src/index.jsx b/packages/web_ui/src/index.jsx
index a692e723..ae2b6bbc 100644
--- a/packages/web_ui/src/index.jsx
+++ b/packages/web_ui/src/index.jsx
@@ -13,8 +13,10 @@ export { default as BaseConfigTree } from "./components/BaseConfigTree";
export { default as InstanceConfigTree } from "./components/InstanceConfigTree";
export { default as MasterConfigTree } from "./components/MasterConfigTree";
+export { default as LogConsole } from "./components/LogConsole";
+
export { default as AssignInstanceModal } from "./components/AssignInstanceModal";
-export { default as InstanceConsole } from "./components/InstanceConsole";
+export { default as InstanceList } from "./components/InstanceList";
export { default as InstanceRcon } from "./components/InstanceRcon";
export { default as PageLayout } from "./components/PageLayout";
diff --git a/packages/web_ui/src/pages.jsx b/packages/web_ui/src/pages.jsx
index b643b0e5..cec0de30 100644
--- a/packages/web_ui/src/pages.jsx
+++ b/packages/web_ui/src/pages.jsx
@@ -2,6 +2,7 @@ import React from "react";
import MasterPage from "./components/MasterPage";
import SlavesPage from "./components/SlavesPage";
+import SlaveViewPage from "./components/SlaveViewPage";
import InstancesPage from "./components/InstancesPage";
import InstanceViewPage from "./components/InstanceViewPage";
import UsersPage from "./components/UsersPage";
@@ -24,6 +25,11 @@ export const pages = [
permission: "core.slave.list",
content: ,
},
+ {
+ path: "/slaves/:id/view",
+ sidebarPath: "/slaves",
+ content: ,
+ },
{
path: "/instances",
sidebarName: "Instances",
diff --git a/packages/web_ui/src/util/websocket.jsx b/packages/web_ui/src/util/websocket.jsx
index 18c54f1c..5473ad49 100644
--- a/packages/web_ui/src/util/websocket.jsx
+++ b/packages/web_ui/src/util/websocket.jsx
@@ -27,6 +27,22 @@ export class ControlConnector extends libLink.WebSocketClientConnector {
}
}
+function logFilter({ all, master, slave_ids, instance_ids }, info) {
+ if (all) {
+ return true;
+ }
+ if (master && info.slave_id === undefined) {
+ return true;
+ }
+ if (info.slave_id && slave_ids.includes(info.slave_id)) {
+ return true;
+ }
+ if (info.instance_id && instance_ids.includes(info.instance_id)) {
+ return true;
+ }
+ return false;
+}
+
/**
* Handles running the control
*
@@ -70,7 +86,7 @@ export class Control extends libLink.Link {
this.slaveUpdateHandlers = new Map();
this.instanceUpdateHandlers = new Map();
this.saveListUpdateHandlers = new Map();
- this.instanceLogHandlers = new Map();
+ this.logHandlers = new Map();
this.connector.on("connect", data => {
this.accountName = data.account.name;
@@ -302,23 +318,20 @@ export class Control extends libLink.Link {
async logMessageEventHandler(message) {
let info = message.data.info;
- if (info.instance_id !== undefined) {
- let instanceHandlers = this.instanceLogHandlers.get(info.instance_id);
- for (let handler of instanceHandlers || []) {
- handler(info);
+ for (let [filter, handlers] of this.logHandlers) {
+ if (logFilter(filter, info)) {
+ for (let handler of handlers) {
+ handler(info);
+ }
}
}
}
- async onInstanceLog(id, handler) {
- if (!Number.isInteger(id)) {
- throw new Error("Invalid instance id");
- }
-
- let handlers = this.instanceLogHandlers.get(id);
+ async onLog(filter, handler) {
+ let handlers = this.logHandlers.get(filter);
if (!handlers) {
handlers = [];
- this.instanceLogHandlers.set(id, handlers);
+ this.logHandlers.set(filter, handlers);
}
handlers.push(handler);
@@ -328,15 +341,15 @@ export class Control extends libLink.Link {
}
}
- async offInstanceLog(id, handler) {
- let handlers = this.instanceLogHandlers.get(id);
+ async offLog(filter, handler) {
+ let handlers = this.logHandlers.get(filter);
if (!handlers || !handlers.length) {
- throw new Error(`No handlers for instance ${id} exist`);
+ throw new Error("No handlers for the given filter exists");
}
let index = handlers.lastIndexOf(handler);
if (index === -1) {
- throw new Error(`Given handler is not registered for instance ${id}`);
+ throw new Error("Given handler is not registered for the filter");
}
handlers.splice(index, 1);
@@ -350,13 +363,29 @@ export class Control extends libLink.Link {
return;
}
- await libLink.messages.setLogSubscriptions.send(this, {
+ let combinedFilter = {
all: false,
master: false,
slave_ids: [],
- instance_ids: [...this.instanceLogHandlers.keys()],
- max_level: null,
- });
+ instance_ids: [],
+ };
+
+ for (let filter of this.logHandlers.keys()) {
+ if (filter.all) { combinedFilter.all = true; }
+ if (filter.master) { combinedFilter.master = true; }
+ for (let slaveId of filter.slave_ids || []) {
+ if (!combinedFilter.slave_ids.includes(slaveId)) {
+ combinedFilter.slave_ids.push(slaveId);
+ }
+ }
+ for (let instanceId of filter.instance_ids || []) {
+ if (!combinedFilter.instance_ids.includes(instanceId)) {
+ combinedFilter.instance_ids.push(instanceId);
+ }
+ }
+ }
+
+ await libLink.messages.setLogSubscriptions.send(this, { ...combinedFilter, max_level: null });
}
async debugWsMessageEventHandler(message) {