mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 23:24:52 +09:00
Merge pull request #386 from Hornwitser/instances-page
Instances page rewrite
This commit is contained in:
9 files changed
+153
-53
No files matched your search
@@ -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",
|
||||
]},
|
||||
};
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -67,6 +67,10 @@ body {
|
||||
padding-right: 64px;
|
||||
}
|
||||
|
||||
.site-page-header {
|
||||
padding: 0 0 16px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user