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 // Please note that the state of the relay is driven by the last
// received stats packet sent by the relay-gateway. // received stats packet sent by the relay-gateway.
GatewayState state = 10; GatewayState state = 10;
// Region configuration ID.
string region_config_id = 11;
} }
message UpdateRelayGatewayRequest { message UpdateRelayGatewayRequest {
@ -414,4 +417,7 @@ message RelayGateway {
// This defines the expected interval in which the gateway sends its // This defines the expected interval in which the gateway sends its
// statistics. // statistics.
uint32 stats_interval = 5; 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_multiple_files = true;
option java_outer_classname = "ApplicationProto"; option java_outer_classname = "ApplicationProto";
option csharp_namespace = "Chirpstack.Api"; option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "google/api/annotations.proto"; import "google/api/annotations.proto";
import "google/protobuf/timestamp.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_multiple_files = true;
option java_outer_classname = "DeviceProto"; option java_outer_classname = "DeviceProto";
option csharp_namespace = "Chirpstack.Api"; option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "common/common.proto"; import "common/common.proto";
import "google/api/annotations.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_multiple_files = true;
option java_outer_classname = "DeviceProfileProto"; option java_outer_classname = "DeviceProfileProto";
option csharp_namespace = "Chirpstack.Api"; option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "google/api/annotations.proto"; import "google/api/annotations.proto";
import "google/protobuf/timestamp.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_multiple_files = true;
option java_outer_classname = "DeviceProfileTemplateProto"; option java_outer_classname = "DeviceProfileTemplateProto";
option csharp_namespace = "Chirpstack.Api"; option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "google/api/annotations.proto"; import "google/api/annotations.proto";
import "google/protobuf/timestamp.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_multiple_files = true;
option java_outer_classname = "GatewayProto"; option java_outer_classname = "GatewayProto";
option csharp_namespace = "Chirpstack.Api"; option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "google/api/annotations.proto"; import "google/api/annotations.proto";
import "google/protobuf/timestamp.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 // Please note that the state of the relay is driven by the last
// received stats packet sent by the relay-gateway. // received stats packet sent by the relay-gateway.
GatewayState state = 10; GatewayState state = 10;
// Region configuration ID.
string region_config_id = 11;
} }
message UpdateRelayGatewayRequest { message UpdateRelayGatewayRequest {
@ -412,4 +417,7 @@ message RelayGateway {
// This defines the expected interval in which the gateway sends its // This defines the expected interval in which the gateway sends its
// statistics. // statistics.
uint32 stats_interval = 5; 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_multiple_files = true;
option java_outer_classname = "InternalProto"; option java_outer_classname = "InternalProto";
option csharp_namespace = "Chirpstack.Api"; 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/timestamp.proto";
import "google/protobuf/empty.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_multiple_files = true;
option java_outer_classname = "MulticastGroupProto"; option java_outer_classname = "MulticastGroupProto";
option csharp_namespace = "Chirpstack.Api"; option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "google/api/annotations.proto"; import "google/api/annotations.proto";
import "google/protobuf/timestamp.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_multiple_files = true;
option java_outer_classname = "RelayProto"; option java_outer_classname = "RelayProto";
option csharp_namespace = "Chirpstack.Api"; option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "google/api/annotations.proto"; import "google/api/annotations.proto";
import "google/protobuf/timestamp.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_multiple_files = true;
option java_outer_classname = "TenantProto"; option java_outer_classname = "TenantProto";
option csharp_namespace = "Chirpstack.Api"; option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "google/api/annotations.proto"; import "google/api/annotations.proto";
import "google/protobuf/timestamp.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_multiple_files = true;
option java_outer_classname = "UserProto"; option java_outer_classname = "UserProto";
option csharp_namespace = "Chirpstack.Api"; option csharp_namespace = "Chirpstack.Api";
option php_namespace = "Chirpstack\\Api";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api";
import "google/api/annotations.proto"; import "google/api/annotations.proto";
import "google/protobuf/timestamp.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_multiple_files = true;
option java_outer_classname = "CommonProto"; option java_outer_classname = "CommonProto";
option csharp_namespace = "Chirpstack.Common"; option csharp_namespace = "Chirpstack.Common";
option php_namespace = "Chirpstack\\Common";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Common";
import "google/protobuf/timestamp.proto"; 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_multiple_files = true;
option java_outer_classname = "GatewayProto"; option java_outer_classname = "GatewayProto";
option csharp_namespace = "Chirpstack.Gateway"; option csharp_namespace = "Chirpstack.Gateway";
option php_namespace = "Chirpstack\\Gateway";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Gateway";
import "common/common.proto"; import "common/common.proto";
import "google/protobuf/timestamp.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_multiple_files = true;
option java_outer_classname = "IntegrationProto"; option java_outer_classname = "IntegrationProto";
option csharp_namespace = "Chirpstack.Integration"; option csharp_namespace = "Chirpstack.Integration";
option php_namespace = "Chirpstack\\Integration";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Integration";
import "common/common.proto"; import "common/common.proto";
import "gw/gw.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_multiple_files = true;
option java_outer_classname = "ApiRequestProto"; option java_outer_classname = "ApiRequestProto";
option csharp_namespace = "Chirpstack.Stream"; option csharp_namespace = "Chirpstack.Stream";
option php_namespace = "Chirpstack\\Stream";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Stream";
import "google/protobuf/timestamp.proto"; import "google/protobuf/timestamp.proto";
import "common/common.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_multiple_files = true;
option java_outer_classname = "BackendInterfacesProto"; option java_outer_classname = "BackendInterfacesProto";
option csharp_namespace = "Chirpstack.Stream"; option csharp_namespace = "Chirpstack.Stream";
option php_namespace = "Chirpstack\\Stream";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Stream";
import "google/protobuf/timestamp.proto"; 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_multiple_files = true;
option java_outer_classname = "FrameProto"; option java_outer_classname = "FrameProto";
option csharp_namespace = "Chirpstack.Stream"; option csharp_namespace = "Chirpstack.Stream";
option php_namespace = "Chirpstack\\Stream";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Stream";
import "google/protobuf/timestamp.proto"; import "google/protobuf/timestamp.proto";
import "common/common.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_multiple_files = true;
option java_outer_classname = "MetaProto"; option java_outer_classname = "MetaProto";
option csharp_namespace = "Chirpstack.Stream"; option csharp_namespace = "Chirpstack.Stream";
option php_namespace = "Chirpstack\\Stream";
option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Stream";
import "common/common.proto"; import "common/common.proto";
import "gw/gw.proto"; import "gw/gw.proto";

View File

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

View File

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

View File

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

View File

@ -299,6 +299,8 @@ diesel::table! {
name -> Varchar, name -> Varchar,
description -> Text, description -> Text,
stats_interval_secs -> Int4, 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.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?; gateway::update_relay_gateway(v).await?;
} }
Err(_) => { Err(_) => {

View File

@ -12,6 +12,7 @@ import {
ControlOutlined, ControlOutlined,
AppstoreOutlined, AppstoreOutlined,
CompassOutlined, CompassOutlined,
RadarChartOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { import {
@ -130,6 +131,11 @@ function SideMenu() {
setSelectedKey("tenant-gateways"); setSelectedKey("tenant-gateways");
} }
// tenant gateway-mesh
if (/\/tenants\/[\w-]{36}\/gateways\/mesh.*/g.exec(path)) {
setSelectedKey("tenant-gateways-mesh");
}
// tenant applications // tenant applications
if (/\/tenants\/[\w-]{36}\/applications.*/g.exec(path)) { if (/\/tenants\/[\w-]{36}\/applications.*/g.exec(path)) {
setSelectedKey("tenant-applications"); setSelectedKey("tenant-applications");
@ -242,6 +248,11 @@ function SideMenu() {
icon: <WifiOutlined />, icon: <WifiOutlined />,
label: <Link to={`/tenants/${tenantId}/gateways`}>Gateways</Link>, 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", key: "tenant-applications",
icon: <AppstoreOutlined />, 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, GetGatewayDutyCycleMetricsResponse,
GenerateGatewayClientCertificateRequest, GenerateGatewayClientCertificateRequest,
GenerateGatewayClientCertificateResponse, GenerateGatewayClientCertificateResponse,
GetRelayGatewayRequest,
GetRelayGatewayResponse,
ListRelayGatewaysRequest,
ListRelayGatewaysResponse,
UpdateRelayGatewayRequest,
DeleteRelayGatewayRequest,
} from "@chirpstack/chirpstack-api-grpc-web/api/gateway_pb"; } from "@chirpstack/chirpstack-api-grpc-web/api/gateway_pb";
import SessionStore from "./SessionStore"; import SessionStore from "./SessionStore";
@ -136,6 +142,60 @@ class GatewayStore extends EventEmitter {
callbackFunc(resp); 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(); 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 EditDeviceProfile from "../device-profiles/EditDeviceProfile";
import ListGateways from "../gateways/ListGateways"; import ListGateways from "../gateways/ListGateways";
import ListRelayGateways from "../gateways/mesh/ListRelayGateways";
import CreateGateway from "../gateways/CreateGateway"; import CreateGateway from "../gateways/CreateGateway";
import GatewayLayout from "../gateways/GatewayLayout"; import GatewayLayout from "../gateways/GatewayLayout";
import RelayGatewayLayout from "../gateways/mesh/RelayGatewayLayout";
import ListApplications from "../applications/ListApplications"; import ListApplications from "../applications/ListApplications";
import CreateApplication from "../applications/CreateApplication"; import CreateApplication from "../applications/CreateApplication";
@ -65,6 +67,8 @@ function TenantLoader() {
<Route path="/gateways" element={<ListGateways tenant={tenant} />} /> <Route path="/gateways" element={<ListGateways tenant={tenant} />} />
<Route path="/gateways/create" element={<CreateGateway 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="/gateways/:gatewayId/*" element={<GatewayLayout tenant={tenant} />} />
<Route path="/applications" element={<ListApplications tenant={tenant} />} /> <Route path="/applications" element={<ListApplications tenant={tenant} />} />