Implement Gateway Mesh in UI.

This commit is contained in:
Orne Brocaar 2024-06-20 14:31:02 +01:00
parent 4f5b14eeb8
commit 682d1b7b56
31 changed files with 682 additions and 0 deletions

View File

@ -382,6 +382,9 @@ message RelayGatewayListItem {
// Please note that the state of the relay is driven by the last
// received stats packet sent by the relay-gateway.
GatewayState state = 10;
// Region configuration ID.
string region_config_id = 11;
}
message UpdateRelayGatewayRequest {
@ -414,4 +417,7 @@ message RelayGateway {
// This defines the expected interval in which the gateway sends its
// statistics.
uint32 stats_interval = 5;
// Region configuration ID.
string region_config_id = 6;
}

View File

@ -7,6 +7,8 @@ option java_package = "io.chirpstack.api";
option java_multiple_files = true;
option java_outer_classname = "ApplicationProto";
option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";

View File

@ -7,6 +7,8 @@ option java_package = "io.chirpstack.api";
option java_multiple_files = true;
option java_outer_classname = "DeviceProto";
option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "common/common.proto";
import "google/api/annotations.proto";

View File

@ -7,6 +7,8 @@ option java_package = "io.chirpstack.api";
option java_multiple_files = true;
option java_outer_classname = "DeviceProfileProto";
option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";

View File

@ -7,6 +7,8 @@ option java_package = "io.chirpstack.api";
option java_multiple_files = true;
option java_outer_classname = "DeviceProfileTemplateProto";
option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";

View File

@ -7,6 +7,8 @@ option java_package = "io.chirpstack.api";
option java_multiple_files = true;
option java_outer_classname = "GatewayProto";
option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
@ -380,6 +382,9 @@ message RelayGatewayListItem {
// Please note that the state of the relay is driven by the last
// received stats packet sent by the relay-gateway.
GatewayState state = 10;
// Region configuration ID.
string region_config_id = 11;
}
message UpdateRelayGatewayRequest {
@ -412,4 +417,7 @@ message RelayGateway {
// This defines the expected interval in which the gateway sends its
// statistics.
uint32 stats_interval = 5;
// Region configuration ID.
string region_config_id = 6;
}

View File

@ -7,6 +7,8 @@ option java_package = "io.chirpstack.api";
option java_multiple_files = true;
option java_outer_classname = "InternalProto";
option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";

View File

@ -7,6 +7,8 @@ option java_package = "io.chirpstack.api";
option java_multiple_files = true;
option java_outer_classname = "MulticastGroupProto";
option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";

View File

@ -7,6 +7,8 @@ option java_package = "io.chirpstack.api";
option java_multiple_files = true;
option java_outer_classname = "RelayProto";
option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";

View File

@ -7,6 +7,8 @@ option java_package = "io.chirpstack.api";
option java_multiple_files = true;
option java_outer_classname = "TenantProto";
option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";

View File

@ -7,6 +7,8 @@ option java_package = "io.chirpstack.api";
option java_multiple_files = true;
option java_outer_classname = "UserProto";
option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";

View File

@ -7,6 +7,8 @@ option java_package = "io.chirpstack.api";
option java_multiple_files = true;
option java_outer_classname = "CommonProto";
option csharp_namespace = "Chirpstack.Common";
option php_namespace = "Chirpstack\\Common";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Common";
import "google/protobuf/timestamp.proto";

View File

@ -7,6 +7,8 @@ option java_package = "io.chirpstack.api.gw";
option java_multiple_files = true;
option java_outer_classname = "GatewayProto";
option csharp_namespace = "Chirpstack.Gateway";
option php_namespace = "Chirpstack\\Gateway";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Gateway";
import "common/common.proto";
import "google/protobuf/timestamp.proto";

View File

@ -7,6 +7,8 @@ option java_package = "io.chirpstack.api.integration";
option java_multiple_files = true;
option java_outer_classname = "IntegrationProto";
option csharp_namespace = "Chirpstack.Integration";
option php_namespace = "Chirpstack\\Integration";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Integration";
import "common/common.proto";
import "gw/gw.proto";

View File

@ -7,6 +7,8 @@ option java_package = "io.chirpstack.api.stream";
option java_multiple_files = true;
option java_outer_classname = "ApiRequestProto";
option csharp_namespace = "Chirpstack.Stream";
option php_namespace = "Chirpstack\\Stream";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Stream";
import "google/protobuf/timestamp.proto";
import "common/common.proto";

View File

@ -7,6 +7,8 @@ option java_package = "io.chirpstack.api.stream";
option java_multiple_files = true;
option java_outer_classname = "BackendInterfacesProto";
option csharp_namespace = "Chirpstack.Stream";
option php_namespace = "Chirpstack\\Stream";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Stream";
import "google/protobuf/timestamp.proto";

View File

@ -7,6 +7,8 @@ option java_package = "io.chirpstack.api.stream";
option java_multiple_files = true;
option java_outer_classname = "FrameProto";
option csharp_namespace = "Chirpstack.Stream";
option php_namespace = "Chirpstack\\Stream";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Stream";
import "google/protobuf/timestamp.proto";
import "common/common.proto";

View File

@ -7,6 +7,8 @@ option java_package = "io.chirpstack.api.stream";
option java_multiple_files = true;
option java_outer_classname = "MetaProto";
option csharp_namespace = "Chirpstack.Stream";
option php_namespace = "Chirpstack\\Stream";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Stream";
import "common/common.proto";
import "gw/gw.proto";

View File

@ -7,6 +7,7 @@ create table relay_gateway (
name varchar(100) not null,
description text not null,
stats_interval_secs integer not null,
region_config_id varchar(100) not null,
primary key (tenant_id, relay_id)
);

View File

@ -815,6 +815,7 @@ impl GatewayService for Gateway {
name: relay.name,
description: relay.description,
stats_interval: relay.stats_interval_secs as u32,
region_config_id: relay.region_config_id.to_string(),
}),
created_at: Some(helpers::datetime_to_prost_timestamp(&relay.created_at)),
updated_at: Some(helpers::datetime_to_prost_timestamp(&relay.updated_at)),
@ -859,6 +860,7 @@ impl GatewayService for Gateway {
name: req_relay.name.clone(),
description: req_relay.description.clone(),
stats_interval_secs: req_relay.stats_interval as i32,
region_config_id: req_relay.region_config_id.clone(),
..Default::default()
})
.await
@ -962,6 +964,7 @@ impl GatewayService for Gateway {
}
}
.into(),
region_config_id: r.region_config_id.to_string(),
})
.collect(),
});
@ -1358,6 +1361,7 @@ pub mod test {
relay_id: gateway::RelayId::from_be_bytes([1, 2, 3, 4]),
name: "test-relay".into(),
description: "test relay".into(),
region_config_id: "eu868".into(),
..Default::default()
})
.await
@ -1378,6 +1382,7 @@ pub mod test {
name: "test-relay".into(),
description: "test relay".into(),
stats_interval: 900,
region_config_id: "eu868".into(),
}),
get_relay_resp.get_ref().relay_gateway
);
@ -1390,6 +1395,7 @@ pub mod test {
name: "updated-relay".into(),
description: "updated relay".into(),
stats_interval: 600,
region_config_id: "us915_0".into(),
}),
};
let mut up_relay_req = Request::new(up_relay_req);
@ -1411,6 +1417,7 @@ pub mod test {
name: "updated-relay".into(),
description: "updated relay".into(),
stats_interval: 600,
region_config_id: "us915_0".into(),
}),
get_relay_resp.get_ref().relay_gateway
);

View File

@ -131,6 +131,7 @@ pub struct RelayGateway {
pub name: String,
pub description: String,
pub stats_interval_secs: i32,
pub region_config_id: String,
}
impl Default for RelayGateway {
@ -146,6 +147,7 @@ impl Default for RelayGateway {
name: "".into(),
description: "".into(),
stats_interval_secs: 900,
region_config_id: "".into(),
}
}
}
@ -165,6 +167,7 @@ pub struct RelayGatewayListItem {
pub name: String,
pub description: String,
pub stats_interval_secs: i32,
pub region_config_id: String,
}
pub async fn create(gw: Gateway) -> Result<Gateway, Error> {
@ -400,6 +403,7 @@ pub async fn update_relay_gateway(relay: RelayGateway) -> Result<RelayGateway, E
relay_gateway::name.eq(&relay.name),
relay_gateway::description.eq(&relay.description),
relay_gateway::stats_interval_secs.eq(&relay.stats_interval_secs),
relay_gateway::region_config_id.eq(&relay.region_config_id),
))
.get_result(&mut get_async_db_conn().await?)
.await
@ -450,6 +454,7 @@ pub async fn list_relay_gateways(
relay_gateway::name,
relay_gateway::description,
relay_gateway::stats_interval_secs,
relay_gateway::region_config_id,
))
.into_boxed();
@ -662,6 +667,7 @@ pub mod test {
tenant_id: gw.tenant_id,
name: "test-relay".into(),
description: "test relay".into(),
region_config_id: "eu868".into(),
..Default::default()
})
.await
@ -675,6 +681,7 @@ pub mod test {
// update
relay.name = "updated-relay".into();
relay.region_config_id = "us915_0".into();
relay = update_relay_gateway(relay).await.unwrap();
let relay_get = get_relay_gateway(relay.tenant_id, relay.relay_id)
.await

View File

@ -299,6 +299,8 @@ diesel::table! {
name -> Varchar,
description -> Text,
stats_interval_secs -> Int4,
#[max_length = 100]
region_config_id -> Varchar,
}
}

View File

@ -95,6 +95,11 @@ impl MeshStats {
}
v.last_seen_at = Some(ts);
v.region_config_id = border_gw
.properties
.get("region_config_id")
.cloned()
.unwrap_or_default();
gateway::update_relay_gateway(v).await?;
}
Err(_) => {

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;

View File

@ -15,6 +15,12 @@ import {
GetGatewayDutyCycleMetricsResponse,
GenerateGatewayClientCertificateRequest,
GenerateGatewayClientCertificateResponse,
GetRelayGatewayRequest,
GetRelayGatewayResponse,
ListRelayGatewaysRequest,
ListRelayGatewaysResponse,
UpdateRelayGatewayRequest,
DeleteRelayGatewayRequest,
} from "@chirpstack/chirpstack-api-grpc-web/api/gateway_pb";
import SessionStore from "./SessionStore";
@ -136,6 +142,60 @@ class GatewayStore extends EventEmitter {
callbackFunc(resp);
});
};
getRelayGateway = (req: GetRelayGatewayRequest, callbackFunc: (resp: GetRelayGatewayResponse) => void) => {
this.client.getRelayGateway(req, SessionStore.getMetadata(), (err, resp) => {
if (err !== null) {
HandleError(err);
return;
}
callbackFunc(resp);
})
}
listRelayGateways = (req: ListRelayGatewaysRequest, callbackFunc: (resp: ListRelayGatewaysResponse) => void) => {
this.client.listRelayGateways(req, SessionStore.getMetadata(), (err, resp) => {
if (err !== null) {
HandleError(err);
return;
}
callbackFunc(resp);
})
}
updateRelayGateway = (req: UpdateRelayGatewayRequest, callbackFunc: () => void) => {
this.client.updateRelayGateway(req, SessionStore.getMetadata(), (err) => {
if (err !== null) {
HandleError(err);
return;
}
notification.success({
message: "Relay Gateway updated",
duration: 3,
});
callbackFunc();
})
}
deleteRelayGateway = (req: DeleteRelayGatewayRequest, callbackFunc: () => void) => {
this.client.deleteRelayGateway(req, SessionStore.getMetadata(), (err) => {
if (err !== null) {
HandleError(err);
return;
}
notification.success({
message: "Relay Gateway deleted",
duration: 3,
});
callbackFunc();
})
}
}
const gatewayStore = new GatewayStore();

View File

@ -0,0 +1,34 @@
import { useNavigate } from "react-router-dom";
import { RelayGateway, UpdateRelayGatewayRequest } from "@chirpstack/chirpstack-api-grpc-web/api/gateway_pb";
import RelayGatewayForm from "./RelayGatewayForm";
import GatewayStore from "../../../stores/GatewayStore";
import SessionStore from "../../../stores/SessionStore";
interface IProps {
relayGateway: RelayGateway;
}
function EditRelayGateway(props: IProps) {
const navigate = useNavigate();
const onFinish = (obj: RelayGateway) => {
let req = new UpdateRelayGatewayRequest();
req.setRelayGateway(obj);
GatewayStore.updateRelayGateway(req, () => {
navigate(`/tenants/${obj.getTenantId()}/gateways/mesh/relays`);
});
};
const disabled = !(
SessionStore.isAdmin() ||
SessionStore.isTenantAdmin(props.relayGateway.getTenantId()) ||
SessionStore.isTenantGatewayAdmin(props.relayGateway.getTenantId())
);
return <RelayGatewayForm initialValues={props.relayGateway} onFinish={onFinish} disabled={disabled} update />;
}
export default EditRelayGateway;

View File

@ -0,0 +1,119 @@
import React from "react";
import { Link } from "react-router-dom";
import moment from "moment";
import { Space, Breadcrumb, Badge } from "antd";
import { ColumnsType } from "antd/es/table";
import { PageHeader } from "@ant-design/pro-layout";
import {
ListRelayGatewaysRequest,
ListRelayGatewaysResponse,
RelayGatewayListItem,
GatewayState,
} from "@chirpstack/chirpstack-api-grpc-web/api/gateway_pb";
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
import DataTable, { GetPageCallbackFunc } from "../../../components/DataTable";
import GatewayStore from "../../../stores/GatewayStore";
interface IProps {
tenant: Tenant;
}
function ListRelayGateways(props: IProps) {
const columns: ColumnsType<RelayGatewayListItem.AsObject> = [
{
title: "",
dataIndex: "state",
key: "state",
width: 150,
render: (text, record) => {
if (record.state === GatewayState.NEVER_SEEN) {
return <Badge status="warning" text="Never seen" />;
} else if (record.state === GatewayState.OFFLINE) {
return <Badge status="error" text="Offline" />;
} else if (record.state === GatewayState.ONLINE) {
return <Badge status="success" text="Online" />;
}
},
},
{
title: "Last seen",
dataIndex: "lastSeenAt",
key: "lastSeenAt",
width: 250,
render: (text, record) => {
if (record.lastSeenAt !== undefined) {
let ts = new Date(0);
ts.setUTCSeconds(record.lastSeenAt.seconds);
return moment(ts).format("YYYY-MM-DD HH:mm:ss");
}
},
},
{
title: "Relay ID",
dataIndex: "relayId",
key: "relayId",
width: 250,
render: (text, record) => (
<Link to={`/tenants/${props.tenant.getId()}/gateways/mesh/relays/${record.relayId}/edit`}>{text}</Link>
),
},
{
title: "Name",
dataIndex: "name",
key: "name",
},
{
title: "Region ID",
dataIndex: "regionConfigId",
key: "regionConfigId",
width: 150,
render: (text) => {
return <Link to={`/regions/${text}`}>{text}</Link>;
}
},
];
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
let req = new ListRelayGatewaysRequest();
req.setTenantId(props.tenant.getId());
req.setLimit(limit);
req.setOffset(offset);
GatewayStore.listRelayGateways(req, (resp: ListRelayGatewaysResponse) => {
const obj = resp.toObject();
callbackFunc(obj.totalCount, obj.resultList);
});
};
return (
<Space direction="vertical" style={{ width: "100%" }} size="large">
<PageHeader
title="Relay Gateways"
breadcrumbRender={() => (
<Breadcrumb>
<Breadcrumb.Item>
<span>Tenants</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>Gateway Mesh</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>Relay Gateways</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
/>
<DataTable columns={columns} getPage={getPage} rowKey="relayId" />
</Space>
);
}
export default ListRelayGateways;

View File

@ -0,0 +1,76 @@
import React from "react";
import { Form, Input, InputNumber, Row, Col, Button } from "antd";
import { RelayGateway } from "@chirpstack/chirpstack-api-grpc-web/api/gateway_pb";
import { onFinishFailed } from "../../helpers";
import RelayIdInput from "../../../components/RelayIdInput";
interface IProps {
initialValues: RelayGateway;
onFinish: (obj: RelayGateway) => void;
update?: boolean;
disabled?: boolean;
}
function RelayGatewayForm(props: IProps) {
const onFinish = (values: RelayGateway.AsObject) => {
const v = Object.assign(props.initialValues.toObject(), values);
let relay = new RelayGateway();
relay.setTenantId(v.tenantId);
relay.setRelayId(v.relayId);
relay.setName(v.name);
relay.setDescription(v.description);
relay.setStatsInterval(v.statsInterval);
relay.setRegionConfigId(v.regionConfigId);
props.onFinish(relay);
};
return (
<Form
layout="vertical"
initialValues={props.initialValues.toObject()}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
<Input disabled={props.disabled} />
</Form.Item>
<Form.Item label="Description" name="description">
<Input.TextArea disabled={props.disabled} />
</Form.Item>
<Row gutter={24}>
<Col span={12}>
<RelayIdInput
label="Relay ID (4 bytes)"
name="relayId"
value={props.initialValues.getRelayId()}
disabled={props.update || props.disabled}
required
/>
</Col>
<Col span={12}>
<Form.Item
label="Stats interval (secs)"
tooltip="The expected interval in seconds in which the relay gateway sends its statistics"
name="statsInterval"
rules={[{ required: true, message: "Please enter a stats interval!" }]}
>
<InputNumber min={0} disabled={props.disabled} />
</Form.Item>
</Col>
</Row>
<Form.Item>
<Button type="primary" htmlType="submit" disabled={props.disabled}>
Submit
</Button>
</Form.Item>
</Form>
);
}
export default RelayGatewayForm;

View File

@ -0,0 +1,108 @@
import React, { useState, useEffect } from "react";
import { Route, Routes, Link, useParams, useNavigate } from "react-router-dom";
import { Space, Breadcrumb, Card, Button } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
import {
RelayGateway,
GetRelayGatewayRequest,
GetRelayGatewayResponse,
DeleteRelayGatewayRequest,
} from "@chirpstack/chirpstack-api-grpc-web/api/gateway_pb";
import Admin from "../../../components/Admin";
import SessionStore from "../../../stores/SessionStore";
import GatewayStore from "../../../stores/GatewayStore";
import DeleteConfirm from "../../../components/DeleteConfirm";
import EditRelayGateway from "./EditRelayGateway";
interface IProps {
tenant: Tenant;
}
function RelayGatewayLayout(props: IProps) {
const { relayId } = useParams();
const navigate = useNavigate();
const [relayGateway, setRelayGateway] = useState<RelayGateway | undefined>(undefined);
useEffect(() => {
let req = new GetRelayGatewayRequest();
req.setTenantId(props.tenant.getId());
req.setRelayId(relayId!);
GatewayStore.getRelayGateway(req, (resp: GetRelayGatewayResponse) => {
setRelayGateway(resp.getRelayGateway());
});
}, [props, relayId]);
const deleteRelayGateway = () => {
let req = new DeleteRelayGatewayRequest();
req.setTenantId(props.tenant.getId());
req.setRelayId(relayId!);
GatewayStore.deleteRelayGateway(req, () => {
navigate(`/tenants/${props.tenant.getId()}/gateways/mesh/relays`);
});
}
if (!relayGateway) {
return null;
}
let isGatewayAdmin =
SessionStore.isAdmin() ||
SessionStore.isTenantAdmin(props.tenant.getId()) ||
SessionStore.isTenantGatewayAdmin(props.tenant.getId());
return (
<Space direction="vertical" style={{ width: "100%" }} size="large">
<PageHeader
breadcrumbRender={() => (
<Breadcrumb>
<Breadcrumb.Item>
<span>Tenants</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>Gateway Mesh</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>
<Link to={`/tenants/${props.tenant.getId()}/gateways/mesh/relays`}>Relay Gateways</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>{relayGateway.getName()}</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title={relayGateway.getName()}
subTitle={`relay id: ${relayGateway.getRelayId()}`}
extra={[
<Admin tenantId={props.tenant.getId()} isGatewayAdmin>
<DeleteConfirm confirm={relayGateway.getName()} typ="relay gateway" onConfirm={deleteRelayGateway}>
<Button danger type="primary">
Delete Relay Gateway
</Button>
</DeleteConfirm>
</Admin>,
]}
/>
<Card>
<Routes>
<Route path="/edit" element={<EditRelayGateway relayGateway={relayGateway} />} />
</Routes>
</Card>
</Space>
);
}
export default RelayGatewayLayout;

View File

@ -19,8 +19,10 @@ import CreateDeviceProfile from "../device-profiles/CreateDeviceProfile";
import EditDeviceProfile from "../device-profiles/EditDeviceProfile";
import ListGateways from "../gateways/ListGateways";
import ListRelayGateways from "../gateways/mesh/ListRelayGateways";
import CreateGateway from "../gateways/CreateGateway";
import GatewayLayout from "../gateways/GatewayLayout";
import RelayGatewayLayout from "../gateways/mesh/RelayGatewayLayout";
import ListApplications from "../applications/ListApplications";
import CreateApplication from "../applications/CreateApplication";
@ -65,6 +67,8 @@ function TenantLoader() {
<Route path="/gateways" element={<ListGateways tenant={tenant} />} />
<Route path="/gateways/create" element={<CreateGateway tenant={tenant} />} />
<Route path="/gateways/mesh/relays" element={<ListRelayGateways tenant={tenant} />} />
<Route path="/gateways/mesh/relays/:relayId/*" element={<RelayGatewayLayout tenant={tenant} />} />
<Route path="/gateways/:gatewayId/*" element={<GatewayLayout tenant={tenant} />} />
<Route path="/applications" element={<ListApplications tenant={tenant} />} />