Skip to content
This repository has been archived by the owner on Sep 7, 2024. It is now read-only.

Edge config gui #8

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ module.exports = {
},
},
{
"files": ["packages/web_ui/src/**/*.jsx", "plugins/*/web/**/*.jsx"],
"files": ["web/**/*.jsx"],
"env": {
"browser": true,
},
Expand All @@ -33,11 +33,17 @@ module.exports = {
"ecmaFeatures": {
"jsx": true,
},
"rules": {
"node/no-unpublished-import": "off",
},
},
},
],

"rules": {
"node/no-missing-import": ["error", {
"tryExtensions": [".js", ".jsx", ".json", ".node"],
}],
"accessor-pairs": "error",
"array-bracket-newline": "off",
"array-bracket-spacing": ["error", "never"],
Expand Down Expand Up @@ -199,7 +205,7 @@ module.exports = {
"no-spaced-func": "error",
"no-sync": "error",
"node/no-unpublished-bin": "error",
"node/no-unpublished-import": "error",
"node/no-unpublished-import": "off",
"node/no-unpublished-require": [
"error",
{ "allowModules": ["@clusterio/web_ui", "webpack", "webpack-merge", "webpack-dev-middleware"] },
Expand Down
4 changes: 4 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ module.exports = {
instanceConfigFields: {
"edge_transports.internal": {
title: "Internal",
description: "Edge transports edge configuration",
inputComponent: "edge_transports_internal",
type: "object",
initialValue: { edges: [] },
},
Expand All @@ -40,5 +42,7 @@ module.exports = {
messages: [
...Object.keys(messages).map(key => messages[key]),
],

webEntrypoint: "./web",
},
};
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"test": "spaces-to-tabs -s 4 module/**.lua && npm run lint && npm run luacheck",
"lint": "eslint \"*.js\" --fix",
"luacheck": "luacheck module/",
"prepare": "webpack --env production"
"prepare": "webpack-cli --env production"
},
"repository": {
"type": "git",
Expand All @@ -27,12 +27,15 @@
"@clusterio/lib": "^2.0.0-alpha.0"
},
"devDependencies": {
"@babel/preset-react": "^7.12.5",
"@ant-design/icons": "^5.3.7",
"@clusterio/web_ui": "^2.0.0-alpha.0",
"@sinclair/typebox": "^0.30.4",
"@swc/core": "^1.4.0",
"babel-loader": "^8.2.0",
"antd": "^5.13.0",
"eslint": "^8.4.1",
"eslint-plugin-node": "^11.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"spaces-to-tabs": "^0.0.3",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
Expand All @@ -44,4 +47,4 @@
"publishConfig": {
"access": "public"
}
}
}
239 changes: 239 additions & 0 deletions web/components/InputEdgeConfig.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import React from "react";
import { Button, Select, Modal, InputNumber, Divider, Tag, Tooltip, Space } from "antd";
import PlusOutlined from "@ant-design/icons/PlusOutlined";
import DeleteOutlined from "@ant-design/icons/DeleteOutlined";
import { useInstance, useInstanceConfig } from "@clusterio/web_ui";
import { InstanceSelector } from "./InstanceSelector";
import { direction_to_string } from "../util";

function EdgeStatusTag({ edge, instanceId = null }) {
const [instance] = useInstance(edge.target_instance);
const config = useInstanceConfig(edge.target_instance);
const target_edge = config?.["edge_transports.internal"]?.edges?.find?.(e => e.id === edge.target_edge);
Danielv123 marked this conversation as resolved.
Show resolved Hide resolved
const [targetEdgeTargetInstance] = useInstance(target_edge?.target_instance);

if (!edge.target_edge) {
return <Tag>Incomplete</Tag>;
}
if (!target_edge) {
return <Tooltip title="Target edge not found on destination instance">
<Tag color="warning">Missing</Tag>
</Tooltip>;
}
if (instanceId !== null && target_edge.target_instance !== instanceId) {
// eslint-disable-next-line max-len
return <Tooltip title={`Edge on target instance is targetting incorrect instance (${instance.name} != ${targetEdgeTargetInstance.name})`}>
<Tag color="error">Inconsistent</Tag>
</Tooltip>;
}
if (target_edge.target_edge !== edge.id) {
// eslint-disable-next-line max-len
return <Tooltip title={`Edge on target instance does not have this edge as its target (${target_edge.target_edge})`}>
<Tag color="error">Inconsistent</Tag>
</Tooltip>;
}

return <Tag color="success">OK</Tag>;
}

export function InputEdgeConfig({ value, onChange }) {
// Shim for clusterio being inconsistent with object vs stringified object handling
if (typeof value === "string") {
value = JSON.parse(value);
}

const [visible, setVisible] = React.useState(false);
const [newValue, setNewValue] = React.useState(value);

const edges = newValue.edges || [];

function displayEdge(edge, index) {
return <div key={`${index} ${edge.edge?.id}`}>
<Divider orientation="right">
<Space>
<EdgeStatusTag
edge={edge}
// Clusterio does not currently provide a way for a config input field to access the current
// instances ID so I am leaving this code disabled
// instanceId={instanceId}
/>
<Button danger onClick={() => {
setNewValue({
...newValue, edges: [
...edges.slice(0, index),
...edges.slice(index + 1),
],
});
}}>
<DeleteOutlined />
Danielv123 marked this conversation as resolved.
Show resolved Hide resolved
</Button>
</Space>
</Divider>
<EditEdge
key={index}
edge={edge}
onChange={(newEdge) => {
setNewValue({
...newValue, edges: [
...edges.slice(0, index),
newEdge,
...edges.slice(index + 1),
],
});
}} />
</div>;
}

return <>
<Button onClick={() => {
setNewValue(value);
setVisible(true);
}}>
Configure Edges
</Button>
<Modal
title="Edge Configuration"
open={visible}
onOk={() => {
onChange(JSON.stringify(newValue));
setVisible(false);
}}
onCancel={() => {
setVisible(false);
}}
>
{edges.map(displayEdge)}
{/* Button to add new edge */}
<Button onClick={() => {
setNewValue({
...newValue, edges: [
...edges,
{
surface: 1,
origin: [0, 0],
direction: 0,
length: 10,
},
],
});
}}>
<PlusOutlined /> Add Edge
</Button>
</Modal>
</>;
}

function EditEdge({ edge, onChange }) {
const leftProps = {
style: {
width: "30%",
display: "inline-block",
verticalAlign: "middle",
},
};
// Fields to edit edge properties
return <div>
<div>
<span {...leftProps}>Edge ID</span>
<InputNumber
value={edge.id}
onChange={(value) => onChange({ ...edge, id: value })}
/>
</div>
<div>
<span {...leftProps}>Origin position</span>
<InputNumber
value={edge.origin?.[0]}
formatter={(value) => `x ${value}`}
parser={value => value.replace("x ", "")}
onChange={(value) => onChange({ ...edge, origin: [value, edge.origin?.[1]] })}
Comment on lines +146 to +149
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will an edge ever not have an origin? Within onChange this would result in [value, undefined] which seans like it would only cause more errors.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It never had an origin before I defaulted it to have one. It can still not have one if defined manually using the cli for example. This pattern is required to input the array properly. If we want to avoid undefined to end up in the config like that we need a validation guard on the top level save function, not here.

/>
<InputNumber
value={edge.origin?.[1]}
formatter={(value) => `y ${value}`}
parser={value => value.replace("y ", "")}
onChange={(value) => onChange({ ...edge, origin: [edge.origin?.[0], value] })}
/>
</div>
<div>
<span {...leftProps}>Surface</span>
<InputNumber
value={edge.surface}
onChange={(value) => onChange({ ...edge, surface: value })}
/>
</div>
<div>
<span {...leftProps}>Direction</span>
<Select
value={edge.direction}
onChange={(value) => onChange({ ...edge, direction: value })}
style={{ width: "auto", minWidth: "200px" }}
>
{[0, 2, 4, 6].map(value => <Select.Option key={value} value={value}>
{direction_to_string(value)}
</Select.Option>)}
</Select>
</div>
<div>
<span {...leftProps}>Length</span>
<InputNumber
value={edge.length}
onChange={(value) => onChange({ ...edge, length: value })}
/>
</div>
<div>
<span {...leftProps}>Target Instance</span>
<InstanceSelector
selected={edge.target_instance}
onSelect={(value) => onChange({ ...edge, target_instance: value })}
/>
</div>
<div
style={{
// Prevent children from splitting into 2 lines
whiteSpace: "nowrap",
}}
>
<span {...leftProps}>Target Edge</span>
<div style={{
display: "inline-block",
verticalAlign: "middle",
}}>
<InputNumber
value={edge.target_edge}
onChange={(value) => onChange({ ...edge, target_edge: value })}
style={{
width: "75px",
}}
/>
</div>
<div style={{
display: "inline-block",
verticalAlign: "middle",
marginLeft: "10px",
}}>
<TargetEdgeInfo edge={edge} />
psihius marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
</div>;
};

function TargetEdgeInfo({ edge }) {
const [instance] = useInstance(edge.target_instance);
const config = useInstanceConfig(edge.target_instance);
const target_edge = config?.["edge_transports.internal"]?.edges?.find?.(e => e.id === edge.target_edge);

if (!target_edge) { return ""; }

let status = "Target has matching configuration";
if (target_edge.target_edge !== edge.id) {
status = `Target configured to ID ${target_edge.target_edge} instead of ${edge.id}`;
}

// Visualize some information about the target edge to make it easier to pick the right one
return <div>
<p>Surface {target_edge.surface} at x{target_edge.origin?.[0]}, y{target_edge.origin?.[1]}</p>
<p>Pointing {direction_to_string(target_edge.direction)}</p>
<p style={{ whiteSpace: "wrap" }}>{status}</p>
</div>;
}
15 changes: 15 additions & 0 deletions web/components/InstanceSelector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Select } from "antd";
import { useInstances } from "@clusterio/web_ui";

export function InstanceSelector({ selected, onSelect }) {
const [instances] = useInstances();
return <Select
value={selected}
onChange={onSelect}
style={{ width: "auto", minWidth: "200px" }}
>
{[...instances.values()].map?.(instance => <Select.Option key={instance.id} value={instance.id}>
{instance.name}
</Select.Option>)}
</Select>;
};
11 changes: 11 additions & 0 deletions web/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { BaseWebPlugin } from "@clusterio/web_ui";
import { InputEdgeConfig } from "./components/InputEdgeConfig";

export class WebPlugin extends BaseWebPlugin {
async init() {
this.pages = [];
}
inputComponents = {
edge_transports_internal: InputEdgeConfig,
};
}
20 changes: 20 additions & 0 deletions web/util/direction_to_string.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const directions = [
"East",
"South-east",
"South",
"South-west",
"West",
"North-west",
"North",
"North-east",
];

export function direction_to_string(direction) {
if (direction === undefined) {
return "";
}
if (typeof direction !== "number" || direction < 0 || direction >= 8) {
return "unknown";
}
return directions[direction % 8];
};
1 change: 1 addition & 0 deletions web/util/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./direction_to_string";
6 changes: 6 additions & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,16 @@ module.exports = (env = {}) => merge(common(env), {
exposes: {
"./": "./index.js",
"./package.json": "./package.json",
"./web": "./web/index.jsx",
},
shared: {
"@clusterio/lib": { import: false },
"@clusterio/web_ui": { import: false },
"antd": { import: false },
"react": { import: false },
"react-dom": { import: false },
"react-router": { import: false },
"react-router-dom": { import: false },
},
}),
],
Expand Down