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) {