Merge pull request #386 from Hornwitser/instances-page

Instances page rewrite
This commit is contained in:
Hornwitser authored and GitHub committed 2021-06-12 15:15:04 +02:00
commit a6cafeb5c6
9 files changed
+153 -53

No files matched your search

+1 -1
View File
@@ -374,7 +374,7 @@ let instanceProperties = {
"assigned_slave": { type: ["null", "integer"] },
"status": { enum: [
"unknown", "unassigned", "stopped", "starting", "running", "stopping",
"creating_save", "exporting_data",
"creating_save", "exporting_data", "deleted",
]},
};
+1 -1
View File
@@ -252,7 +252,7 @@ class ControlConnection extends BaseConnection {
let prev = instance.status;
instance.status = "deleted";
this.instanceUpdated(instance);
this._master.instanceUpdated(instance);
await libPlugin.invokeHook(this._master.plugins, "onInstanceStatusChanged", instance, prev);
}
@@ -43,7 +43,7 @@ export default function CreateSaveModal(props) {
}
if (values.mapSettings && values.mapSettings.trim()) {
try {
mapSettings = JSON.parse(values.mapGenSettings);
mapSettings = JSON.parse(values.mapSettings);
} catch (err) {
form.setFields([{ name: "mapSettings", errors: [err.message] }]);
return;
@@ -59,6 +59,7 @@ export default function CreateSaveModal(props) {
map_gen_settings: mapGenSettings,
map_settings: mapSettings,
}).then(() => {
form.resetFields();
setVisible(false);
}).catch(
notifyErrorHandler("Error creating save")
@@ -1,52 +1,104 @@
import React, { useContext } from "react";
import React, { useContext, useState } from "react";
import { useHistory } from "react-router-dom";
import { Button, Form, Input, Modal, PageHeader, Table } from "antd";
import libLink from "@clusterio/lib/link";
import libConfig from "@clusterio/lib/config";
import DataTable from "./data-table";
import ControlContext from "./ControlContext";
import PageLayout from "./PageLayout";
import { useInstanceList } from "../model/instance";
import { notifyErrorHandler } from "../util/notify";
import { useSlaveList } from "../model/slave";
export default function InstancesPage() {
function CreateInstanceButton(props) {
let control = useContext(ControlContext);
let history = useHistory();
let [visible, setVisible] = useState(false);
let [form] = Form.useForm();
async function listInstances() {
let result = await libLink.messages.listInstances.send(control);
return result["list"].map(item => ({
key: item["id"],
"Name": item["name"],
"Assigned Slave": item["assigned_slave"],
"Status": item["status"],
}));
async function createInstance() {
let values = form.getFieldsValue();
if (!values.instanceName) {
form.setFields([{ name: "mapGenSettings", errors: ["Name is required"] }]);
}
let instanceConfig = new libConfig.InstanceConfig("control");
await instanceConfig.init();
instanceConfig.set("instance.name", values.instanceName);
let serialized_config = instanceConfig.serialize("master");
let result = await libLink.messages.createInstance.send(control, { serialized_config });
setVisible(false);
history.push(`/instances/${instanceConfig.get("instance.id")}/view`);
}
return <>
<Button
type="primary"
onClick={() => {
setVisible(true);
}}
>Create</Button>
<Modal
title="Create Instance"
okText="Create"
visible={visible}
onOk={() => { createInstance().catch(notifyErrorHandler("Error creating instance")); }}
onCancel={() => { setVisible(false); }}
destroyOnClose
>
<Form form={form}>
<Form.Item name="instanceName" label="Name">
<Input/>
</Form.Item>
</Form>
</Modal>
</>;
}
export default function InstancesPage() {
let history = useHistory();
let [slaveList] = useSlaveList();
let [instanceList] = useInstanceList();
return <PageLayout nav={[{ name: "Instances" }]}>
<h2>Instances</h2>
<DataTable
DataFunction={listInstances}
AddRecord={{
fields: [{
dataIndex: "name",
<PageHeader
className="site-page-header"
title="Instances"
extra=<CreateInstanceButton />
/>
<Table
columns={[
{
title: "Name",
}],
insert: async args => {
let instanceConfig = new libConfig.InstanceConfig("control");
await instanceConfig.init();
instanceConfig.set("instance.name", args.name);
let serialized_config = instanceConfig.serialize("master");
let response = await libLink.messages.createInstance.send(control, { serialized_config });
dataIndex: "name",
},
}}
TableProps={{
onRow: (record, rowIndex) => ({
onClick: event => {
history.push(`/instances/${record.key}/view`);
{
title: "Assigned Slave",
key: "assigned_slave",
render: instance => {
let slave = slaveList.find(s => s.id === instance.assigned_slave);
if (slave) {
return slave.name;
}
return instance.assigned_slave;
},
}),
}}
},
{
title: "Status",
dataIndex: "status",
},
]}
dataSource={instanceList}
rowKey={instance => instance["id"]}
pagination={false}
onRow={(record, rowIndex) => ({
onClick: event => {
history.push(`/instances/${record.id}/view`);
},
})}
/>
</PageLayout>;
}
+4
View File
@@ -67,6 +67,10 @@ body {
padding-right: 64px;
}
.site-page-header {
padding: 0 0 16px;
}
.section-header {
display: flex;
align-items: center;
+46 -6
View File
@@ -10,21 +10,20 @@ export function useInstance(id) {
let [instance, setInstance] = useState({ loading: true });
function updateInstance() {
if (!Number.isInteger(id)) {
setInstance({ missing: true });
return;
}
libLink.messages.getInstance.send(control, { id }).then(result => {
setInstance({ ...result, present: true });
}).catch(err => {
logger.log(`Failed to get instance: ${err}`);
logger.error(`Failed to get instance: ${err}`);
setInstance({ missing: true });
});
}
useEffect(() => {
if (!Number.isInteger(id)) {
setInstance({ missing: true });
return undefined;
}
updateInstance();
function updateHandler(newInstance) {
@@ -39,3 +38,44 @@ export function useInstance(id) {
return [instance, updateInstance];
}
export function useInstanceList() {
let control = useContext(ControlContext);
let [instanceList, setInstanceList] = useState([]);
function updateInstanceList() {
libLink.messages.listInstances.send(control).then(result => {
setInstanceList(result.list);
}).catch(err => {
logger.error(`Failed to list instances:\n${err}`);
});
}
useEffect(() => {
updateInstanceList();
function updateHandler(newInstance) {
setInstanceList(oldList => {
let newList = oldList.concat();
let index = newList.findIndex(s => s.id === newInstance.id);
if (newInstance.status !== "deleted") {
if (index !== -1) {
newList[index] = newInstance;
} else {
newList.push(newInstance);
}
} else if (index !== -1) {
newList.splice(index, 1);
}
return newList;
});
}
control.onInstanceUpdate(null, updateHandler);
return () => {
control.offInstanceUpdate(null, updateHandler);
};
}, []);
return [instanceList];
}
+4 -5
View File
@@ -9,11 +9,6 @@ export function useSlave(id) {
let [slave, setSlave] = useState({ loading: true });
function updateSlave() {
if (!Number.isInteger(id)) {
setSlave({ missing: true });
return;
}
// XXX optimize by requesting only the slave in question
libLink.messages.listSlaves.send(control).then(result => {
let match = result.list.find(i => i.id === id);
@@ -26,6 +21,10 @@ export function useSlave(id) {
}
useEffect(() => {
if (!Number.isInteger(id)) {
setSlave({ missing: true });
return undefined;
}
updateSlave();
function updateHandler(newSlave) {
+8 -5
View File
@@ -154,14 +154,17 @@ export class Control extends libLink.Link {
}
async instanceUpdateEventHandler(message) {
let handlers = this.instanceUpdateHandlers.get(message.data.id);
for (let handler of handlers || []) {
let handlers = [].concat(
this.instanceUpdateHandlers.get(null) || [],
this.instanceUpdateHandlers.get(message.data.id) || [],
);
for (let handler of handlers) {
handler(message.data);
}
};
async onInstanceUpdate(id, handler) {
if (!Number.isInteger(id)) {
if (id !== null && !Number.isInteger(id)) {
throw new Error("Invalid instance id");
}
@@ -202,8 +205,8 @@ export class Control extends libLink.Link {
}
await libLink.messages.setInstanceSubscriptions.send(this, {
all: false,
instance_ids: [...this.instanceUpdateHandlers.keys()],
all: this.instanceUpdateHandlers.has(null),
instance_ids: [...this.instanceUpdateHandlers.keys()].filter(e => e !== null),
});
}
+4 -3
View File
@@ -501,9 +501,10 @@ describe("Integration of Clusterio", function() {
describe("instanceUpdateEventHandler()", function() {
it("should have triggered for the previous instance status updates", function() {
let statusesToCheck = new Set(
["unassigned", "stopped", "creating_save", "exporting_data", "starting", "running", "stopping"]
);
let statusesToCheck = new Set([
"unassigned", "stopped", "creating_save", "exporting_data",
"starting", "running", "stopping", "deleted",
]);
let statusesNotSeen = new Set(statusesToCheck);
for (let update of getControl().instanceUpdates) {