Integrate Gateway Mesh feature.

This adds a Gateway Mesh section to the web-interface (+ API endpoints)
to see the status op each Relay Gateway within the Gateway Mesh.

The Gateway Mesh (https://github.com/chirpstack/chirpstack-gateway-mesh)
is an experimental feature to extend LoRaWAN coverage throug Relay
Gateways.
This commit is contained in:
Orne Brocaar
2024-06-25 11:37:57 +01:00
parent c0b148fecb
commit 3f1a47e1e2
66 changed files with 3275 additions and 388 deletions

View File

@ -12,6 +12,7 @@ import {
ControlOutlined,
AppstoreOutlined,
CompassOutlined,
RadarChartOutlined,
} from "@ant-design/icons";
import {
@ -130,6 +131,11 @@ function SideMenu() {
setSelectedKey("tenant-gateways");
}
// tenant gateway-mesh
if (/\/tenants\/[\w-]{36}\/gateways\/mesh.*/g.exec(path)) {
setSelectedKey("tenant-gateways-mesh");
}
// tenant applications
if (/\/tenants\/[\w-]{36}\/applications.*/g.exec(path)) {
setSelectedKey("tenant-applications");
@ -242,6 +248,11 @@ function SideMenu() {
icon: <WifiOutlined />,
label: <Link to={`/tenants/${tenantId}/gateways`}>Gateways</Link>,
},
{
key: "tenant-gateways-mesh",
icon: <RadarChartOutlined />,
label: <Link to={`/tenants/${tenantId}/gateways/mesh/relays`}>Gateway Mesh</Link>,
},
{
key: "tenant-applications",
icon: <AppstoreOutlined />,

View File

@ -0,0 +1,202 @@
import React, { useState, useEffect } from "react";
import { notification, Input, Select, Button, Space, Form, Dropdown, Menu } from "antd";
import { ReloadOutlined, CopyOutlined } from "@ant-design/icons";
import { Buffer } from "buffer";
interface IProps {
label: string;
name: string;
required?: boolean;
value?: string;
disabled?: boolean;
tooltip?: string;
}
function RelayIdInput(props: IProps) {
const form = Form.useFormInstance();
const [byteOrder, setByteOrder] = useState<string>("msb");
const [value, setValue] = useState<string>("");
useEffect(() => {
if (props.value) {
setValue(props.value);
}
}, [props]);
const updateField = (v: string) => {
if (byteOrder === "lsb") {
const bytes = v.match(/[A-Fa-f0-9]{2}/g) || [];
v = bytes.reverse().join("");
}
form.setFieldsValue({
[props.name]: v,
});
};
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let v = e.target.value;
const match = v.match(/[A-Fa-f0-9]/g);
let value = "";
if (match) {
if (match.length > 8) {
value = match.slice(0, 8).join("");
} else {
value = match.join("");
}
}
setValue(value);
updateField(value);
};
const onByteOrderSelect = (v: string) => {
if (v === byteOrder) {
return;
}
setByteOrder(v);
const current = value;
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
const vv = bytes.reverse().join("");
setValue(vv);
updateField(vv);
};
const generateRandom = () => {
let cryptoObj = window.crypto || window.Crypto;
let b = new Uint8Array(4);
cryptoObj.getRandomValues(b);
let key = Buffer.from(b).toString("hex");
setValue(key);
updateField(key);
};
const copyToClipboard = () => {
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
if (bytes !== null && navigator.clipboard !== undefined) {
navigator.clipboard
.writeText(bytes.join("").toUpperCase())
.then(() => {
notification.success({
message: "Copied to clipboard",
duration: 3,
});
})
.catch(e => {
notification.error({
message: "Error",
description: e,
duration: 3,
});
});
} else {
notification.error({
message: "Error",
description: "Clipboard functionality is not available.",
duration: 3,
});
}
};
const copyToClipboardHexArray = () => {
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
if (bytes !== null && navigator.clipboard !== undefined) {
navigator.clipboard
.writeText(
bytes
.join(", ")
.toUpperCase()
.replace(/[A-Fa-f0-9]{2}/g, "0x$&"),
)
.then(() => {
notification.success({
message: "Copied to clipboard",
duration: 3,
});
})
.catch(e => {
notification.error({
message: "Error",
description: e,
duration: 3,
});
});
}
};
const copyMenu = (
<Menu
items={[
{
key: "1",
label: (
<Button type="text" onClick={copyToClipboard}>
HEX string
</Button>
),
},
{
key: "2",
label: (
<Button type="text" onClick={copyToClipboardHexArray}>
HEX array
</Button>
),
},
]}
/>
);
const addon = (
<Space size="large">
<Select value={byteOrder} onChange={onByteOrderSelect}>
<Select.Option value="msb">MSB</Select.Option>
<Select.Option value="lsb">LSB</Select.Option>
</Select>
<Button type="text" size="small" onClick={generateRandom}>
<ReloadOutlined />
</Button>
<Dropdown overlay={copyMenu}>
<Button type="text" size="small">
<CopyOutlined />
</Button>
</Dropdown>
</Space>
);
return (
<Form.Item
rules={[
{
required: props.required,
message: `Please enter a valid ${props.label}`,
pattern: new RegExp(/[A-Fa-f0-9]{8}/g),
},
]}
label={props.label}
name={props.name}
tooltip={props.tooltip}
>
<Input hidden />
<Input
id={`${props.name}Render`}
onChange={onChange}
addonAfter={!props.disabled && addon}
className="input-code"
value={value}
disabled={props.disabled}
/>
</Form.Item>
);
}
export default RelayIdInput;