diff --git a/packages/lib/link/messages.js b/packages/lib/link/messages.js
index 72dbe3a8..6ba0a8a4 100644
--- a/packages/lib/link/messages.js
+++ b/packages/lib/link/messages.js
@@ -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",
]},
};
diff --git a/packages/master/src/ControlConnection.js b/packages/master/src/ControlConnection.js
index ad53cb0b..ddf42e83 100644
--- a/packages/master/src/ControlConnection.js
+++ b/packages/master/src/ControlConnection.js
@@ -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);
}
diff --git a/packages/web_ui/src/components/CreateSaveModal.jsx b/packages/web_ui/src/components/CreateSaveModal.jsx
index f067635b..10907779 100644
--- a/packages/web_ui/src/components/CreateSaveModal.jsx
+++ b/packages/web_ui/src/components/CreateSaveModal.jsx
@@ -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")
diff --git a/packages/web_ui/src/components/InstancesPage.jsx b/packages/web_ui/src/components/InstancesPage.jsx
index 0d4f9031..bab926ba 100644
--- a/packages/web_ui/src/components/InstancesPage.jsx
+++ b/packages/web_ui/src/components/InstancesPage.jsx
@@ -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 <>
+
+ { createInstance().catch(notifyErrorHandler("Error creating instance")); }}
+ onCancel={() => { setVisible(false); }}
+ destroyOnClose
+ >
+
+
+
+
+
+ >;
+}
+
+export default function InstancesPage() {
+ let history = useHistory();
+ let [slaveList] = useSlaveList();
+ let [instanceList] = useInstanceList();
+
return
- Instances
-
+ />
+
+ {
- 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`);
+ },
+ })}
/>
;
}
diff --git a/packages/web_ui/src/index.css b/packages/web_ui/src/index.css
index 97d8a4a6..1129e846 100644
--- a/packages/web_ui/src/index.css
+++ b/packages/web_ui/src/index.css
@@ -67,6 +67,10 @@ body {
padding-right: 64px;
}
+.site-page-header {
+ padding: 0 0 16px;
+}
+
.section-header {
display: flex;
align-items: center;
diff --git a/packages/web_ui/src/model/instance.jsx b/packages/web_ui/src/model/instance.jsx
index 4048a135..ef4f27a2 100644
--- a/packages/web_ui/src/model/instance.jsx
+++ b/packages/web_ui/src/model/instance.jsx
@@ -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];
+}
diff --git a/packages/web_ui/src/model/slave.jsx b/packages/web_ui/src/model/slave.jsx
index c29e7405..729e671a 100644
--- a/packages/web_ui/src/model/slave.jsx
+++ b/packages/web_ui/src/model/slave.jsx
@@ -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) {
diff --git a/packages/web_ui/src/util/websocket.jsx b/packages/web_ui/src/util/websocket.jsx
index 6b07fc65..6b3b983f 100644
--- a/packages/web_ui/src/util/websocket.jsx
+++ b/packages/web_ui/src/util/websocket.jsx
@@ -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),
});
}
diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js
index 474c86e4..990c52ec 100644
--- a/test/integration/clusterio.js
+++ b/test/integration/clusterio.js
@@ -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) {