Reimplement the Storage page

Reimplement the functionality of the Storage page which shows which
items are stored in the shared subspace on the master server.
This commit is contained in:
Hornwitser committed 2021-03-20 03:35:05 +01:00
1 parent cebe83542f
commit c16dc74ad9
5 files changed
+234 -3

No files matched your search

+23 -2
View File
@@ -1,6 +1,7 @@
"use strict";
const libLink = require("@clusterio/lib/link");
const libConfig = require("@clusterio/lib/config");
const libUsers = require("@clusterio/lib/users");
class MasterConfigGroup extends libConfig.PluginConfigGroup {}
MasterConfigGroup.defaultAccess = ["master", "slave", "control"];
@@ -41,6 +42,13 @@ InstanceConfigGroup.define({
});
InstanceConfigGroup.finalize();
libUsers.definePermission({
name: "subspace_storage.storage.view",
title: "View Subspace Storage",
description: "View the items and fluids stored in the shared subspace.",
grantByDefault: true,
});
// JSON schema for subspace storage items
const items = {
type: "array",
@@ -65,6 +73,9 @@ module.exports = {
masterEntrypoint: "master",
MasterConfigGroup,
webEntrypoint: "./web",
routes: ["/storage"],
messages: {
// XXX this should be a request to be reliable
place: new libLink.Event({
@@ -90,7 +101,8 @@ module.exports = {
}),
getStorage: new libLink.Request({
type: "subspace_storage:get_storage",
links: ["instance-slave", "slave-master"],
links: ["instance-slave", "slave-master", "control-master"],
permission: "subspace_storage.storage.view",
forwardTo: "master",
responseProperties: {
"items": items,
@@ -98,11 +110,20 @@ module.exports = {
}),
updateStorage: new libLink.Event({
type: "subspace_storage:update_storage",
links: ["master-slave", "slave-instance"],
links: ["master-slave", "slave-instance", "master-control"],
broadcastTo: "instance",
eventProperties: {
"items": items,
},
}),
setStorageSubscription: new libLink.Request({
type: "subspace_storage:set_storage_subscription",
links: ["control-master"],
permission: "subspace_storage.storage.view",
requestProperties: {
"storage": { type: "boolean" },
},
}),
},
};
+19
View File
@@ -72,6 +72,8 @@ class MasterPlugin extends libPlugin.BaseMasterPlugin {
}
}, 1000);
this.subscribedControlLinks = new Set();
routes.addApiRoutes(this.master.app, this.items, this.metrics.endpointHitCounter);
}
@@ -90,6 +92,9 @@ class MasterPlugin extends libPlugin.BaseMasterPlugin {
itemsToUpdate = [...itemsToUpdate.entries()];
this.broadcastEventToSlaves(this.info.messages.updateStorage, { items: itemsToUpdate });
for (let link of this.subscribedControlLinks) {
this.info.messages.updateStorage.send(link, { items: itemsToUpdate });
}
this.itemsLastUpdate = new Map(this.items._items.entries());
}
@@ -181,6 +186,20 @@ class MasterPlugin extends libPlugin.BaseMasterPlugin {
};
}
async setStorageSubscriptionRequestHandler(message, request, link) {
if (message.data.storage) {
this.subscribedControlLinks.add(link);
} else {
this.subscribedControlLinks.delete(link);
}
}
onControlConnectionEvent(connection, event) {
if (event === "close") {
this.subscribedControlLinks.delete(connection);
}
}
onMetrics() {
if (this.items) {
for (let [key, count] of this.items._items) {
+5
View File
@@ -0,0 +1,5 @@
.factorio-icon {
display: inline-block;
vertical-align: middle;
margin-right: 0.5em;
}
+184
View File
@@ -0,0 +1,184 @@
import React, { useContext, useEffect, useState } from "react";
import { Input, Table, Typography } from "antd";
import libPlugin from "@clusterio/lib/plugin";
import { notifyErrorHandler, useItemMetadata, useLocale, PageLayout, ControlContext } from "@clusterio/web_ui";
import info from "../info";
import "./index.css";
const { Paragraph } = Typography;
function useStorage(control) {
let plugin = control.plugins.get("subspace_storage");
let [storage, setStorage] = useState([...plugin.storage]);
useEffect(() => {
function update() {
setStorage([...plugin.storage]);
}
plugin.onUpdate(update);
return () => {
plugin.offUpdate(update);
};
}, []);
return storage;
}
function StoragePage() {
let control = useContext(ControlContext);
let locale = useLocale();
let itemMetadata = useItemMetadata();
let storage = useStorage(control);
let [filter, setFilter] = useState(null);
function getLocaleName(itemName) {
let localeName = itemName;
let meta = itemMetadata.get(itemName);
if (meta && meta.localized_name) {
// TODO: implement the locale to name conversion.
localeName = locale.get(meta.localized_name[0]);
} else {
for (let section of ["item-name", "entity-name", "fluid-name", "equipment-name"]) {
let name = locale.get(`${section}.${itemName}`);
if (name) {
localeName = name;
break;
}
}
}
return localeName;
}
let numberFormat = new Intl.NumberFormat("en-US");
return <PageLayout nav={[{ name: "Storage" }]}>
<h2>Storage</h2>
<Paragraph>
<Input
placeholder="Search"
onChange={(event) => {
let search = event.target.value.trim();
if (!search) {
setFilter(null);
return;
}
search = search.replace(/(^| )(\w)/g, "$1\\b$2");
search = search.replace(/ +/g, ".*");
let filterExpr = new RegExp(search, "i");
setFilter(() => (item => {
let name = getLocaleName(item[0]);
return filterExpr.test(name) || filterExpr.test(item[0]);
}));
}}
/>
</Paragraph>
<Table
columns={[
{
title: "Resource",
key: "resource",
sorter: (a, b) => {
let aName = getLocaleName(a[0]);
let bName = getLocaleName(b[0]);
if (aName < bName) { return -1; }
if (aName > bName) { return 1; }
return 0;
},
render: item => {
let localeName = getLocaleName(item[0]);
let hasMeta = itemMetadata.get(item[0]);
return <>
<span className={`factorio-icon item-${hasMeta ? item[0] : "unknown-item"}`}/>
{localeName}
</>;
},
},
{
title: "Quantity",
key: "quantity",
align: "right",
defaultSortOrder: "descend",
sorter: (a, b) => a[1] - b[1],
render: item => numberFormat.format(item[1]),
},
]}
dataSource={filter ? storage.filter(filter) : storage}
rowKey={item => item[0]}
pagination={false}
/>
</PageLayout>;
}
export class WebPlugin extends libPlugin.BaseWebPlugin {
async init() {
this.pages = [
{ path: "/storage", sidebarName: "Storage", content: <StoragePage/> },
];
this.storage = new Map();
this.callbacks = [];
}
onMasterConnectionEvent(event) {
if (event === "connect") {
this.updateSubscription();
}
}
async updateStorageEventHandler(message) {
this.updateStorage(message.data.items);
}
onUpdate(callback) {
this.callbacks.push(callback);
if (this.callbacks.length) {
this.updateSubscription();
}
}
offUpdate(callback) {
let index = this.callbacks.lastIndexOf(callback);
if (index === -1) {
throw new Error("callback is not registered");
}
this.callbacks.splice(index, 1);
if (!this.callbacks.length) {
this.updateSubscription();
}
}
updateStorage(items) {
for (let [name, amount] of items) {
this.storage.set(name, amount);
}
for (let callback of this.callbacks) {
callback();
}
}
updateSubscription() {
if (!this.control.connector.connected) {
return;
}
info.messages.setStorageSubscription.send(
this.control, { storage: Boolean(this.callbacks.length) }
).catch(notifyErrorHandler("Error subscribing to storage"));
if (this.callbacks.length) {
info.messages.getStorage.send(this.control).then(
result => {
this.updateStorage(result.items);
}
).catch(notifyErrorHandler("Error updating storage"));
} else {
this.storage.clear();
}
}
}
+3 -1
View File
@@ -21,12 +21,14 @@ module.exports = (env = {}) => merge(common(env), {
exposes: {
"./info": "./info.js",
"./package.json": "./package.json",
"./web": "./web/index.jsx",
},
shared: {
"@clusterio/lib/config": { import: false },
"@clusterio/lib/link": { import: false },
"@clusterio/lib/plugin": { import: false },
"@clusterio/lib/users": { import: false },
"@clusterio/web_ui": { import: false },
"ajv": { import: false },
"antd": { import: false },
"react": { import: false },
"react-dom": { import: false },