Merge pull request #420 from Hornwitser/slave-page

Slave page
This commit is contained in:
Hornwitser authored and GitHub committed 2021-10-31 16:53:34 +01:00
commit 9e7a7a602f
11 files changed
+213 -74

No files matched your search

+5
View File
@@ -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"), {
@@ -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 <Table
size={props.size || "large"}
columns={columns}
dataSource={props.instances}
rowKey={instance => instance["id"]}
pagination={false}
onRow={(record, rowIndex) => ({
onClick: event => {
history.push(`/instances/${record.id}/view`);
},
})}
/>;
}
@@ -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")
&& <Title level={5} style={{ marginTop: 16 }}>Console</Title>
}
{account.hasPermission("core.log.follow") && <InstanceConsole id={instanceId} />}
{account.hasPermission("core.log.follow") && <LogConsole instances={[instanceId]} />}
{
account.hasPermission("core.instance.send_rcon")
&& <InstanceRcon id={instanceId} disabled={instance["status"] !== "running"} />
@@ -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 <PageLayout nav={[{ name: "Instances" }]}>
<PageHeader
className="site-page-header"
@@ -81,34 +69,6 @@ export default function InstancesPage() {
extra={account.hasPermission("core.instance.create") && <CreateInstanceButton />}
/>
<Table
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"]),
},
]}
dataSource={instanceList}
rowKey={instance => instance["id"]}
pagination={false}
onRow={(record, rowIndex) => ({
onClick: event => {
history.push(`/instances/${record.id}/view`);
},
})}
/>
<InstanceList instances={instanceList} />
</PageLayout>;
}
@@ -49,7 +49,7 @@ function formatLog(info, key) {
return <span key={key}>[{level}] {info.message}<br/></span>;
}
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 <>
<Paragraph code className="instance-console">
@@ -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 <PageLayout nav={[{ name: "Master" }]}>
<h2>Master</h2>
{account.hasPermission("core.log.follow") && <>
<Title level={5} style={{ marginTop: 16 }}>Console</Title>
<LogConsole master={true} />
</>}
{account.hasPermission("core.master.get_config") && <MasterConfigTree />}
</PageLayout>;
};
@@ -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 <PageLayout nav={nav}><Spin size="large" /></PageLayout>;
}
if (slave.missing) {
return <PageLayout nav={nav}>
<PageHeader
className="site-page-header"
title={slaveId}
/>
<p>Slave with id {slaveId} was not found on the master server.</p>
</PageLayout>;
}
return <PageLayout nav={nav}>
<PageHeader
className="site-page-header"
title={slave.name || slaveId}
/>
<Descriptions bordered size="small" column={{ xs: 1, sm: 2, xl: 4 }}>
<Descriptions.Item label="Name">{slave["name"]}</Descriptions.Item>
<Descriptions.Item label="Connected">{slave["connected"] ? "yes" : "no"}</Descriptions.Item>
<Descriptions.Item label="Agent">{slave["agent"]}</Descriptions.Item>
<Descriptions.Item label="Version">{slave["version"]}</Descriptions.Item>
</Descriptions>
{account.hasPermission("core.instance.list") && <>
<Title level={5} style={{ marginTop: 16 }}>Instances</Title>
<InstanceList instances={instanceList} size="small" hideAssignedSlave />
</>}
{account.hasPermission("core.log.follow") && <>
<Title level={5} style={{ marginTop: 16 }}>Console</Title>
<LogConsole slaves={[slaveId]} />
</>}
</PageLayout>;
}
@@ -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 <PageLayout nav={[{ name: "Slaves" }]}>
@@ -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`);
},
})}
/>
</PageLayout>;
};
+3 -1
View File
@@ -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";
+6
View File
@@ -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: <SlavesPage />,
},
{
path: "/slaves/:id/view",
sidebarPath: "/slaves",
content: <SlaveViewPage />,
},
{
path: "/instances",
sidebarName: "Instances",
+49 -20
View File
@@ -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) {