mirror of
https://github.com/chirpstack/chirpstack.git
synced 2024-12-19 21:27:55 +00:00
Implement Gateway Mesh in UI.
This commit is contained in:
parent
4f5b14eeb8
commit
682d1b7b56
6
api/proto/api/gateway.proto
vendored
6
api/proto/api/gateway.proto
vendored
@ -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;
|
||||
}
|
||||
|
@ -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";
|
||||
|
2
api/rust/proto/chirpstack/api/device.proto
vendored
2
api/rust/proto/chirpstack/api/device.proto
vendored
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
8
api/rust/proto/chirpstack/api/gateway.proto
vendored
8
api/rust/proto/chirpstack/api/gateway.proto
vendored
@ -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;
|
||||
}
|
||||
|
2
api/rust/proto/chirpstack/api/internal.proto
vendored
2
api/rust/proto/chirpstack/api/internal.proto
vendored
@ -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";
|
||||
|
@ -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";
|
||||
|
2
api/rust/proto/chirpstack/api/relay.proto
vendored
2
api/rust/proto/chirpstack/api/relay.proto
vendored
@ -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";
|
||||
|
2
api/rust/proto/chirpstack/api/tenant.proto
vendored
2
api/rust/proto/chirpstack/api/tenant.proto
vendored
@ -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";
|
||||
|
2
api/rust/proto/chirpstack/api/user.proto
vendored
2
api/rust/proto/chirpstack/api/user.proto
vendored
@ -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";
|
||||
|
@ -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";
|
||||
|
||||
|
2
api/rust/proto/chirpstack/gw/gw.proto
vendored
2
api/rust/proto/chirpstack/gw/gw.proto
vendored
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
||||
|
2
api/rust/proto/chirpstack/stream/frame.proto
vendored
2
api/rust/proto/chirpstack/stream/frame.proto
vendored
@ -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";
|
||||
|
2
api/rust/proto/chirpstack/stream/meta.proto
vendored
2
api/rust/proto/chirpstack/stream/meta.proto
vendored
@ -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";
|
||||
|
@ -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)
|
||||
);
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -299,6 +299,8 @@ diesel::table! {
|
||||
name -> Varchar,
|
||||
description -> Text,
|
||||
stats_interval_secs -> Int4,
|
||||
#[max_length = 100]
|
||||
region_config_id -> Varchar,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(_) => {
|
||||
|
@ -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 />,
|
||||
|
202
ui/src/components/RelayIdInput.tsx
Normal file
202
ui/src/components/RelayIdInput.tsx
Normal 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;
|
@ -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();
|
||||
|
34
ui/src/views/gateways/mesh/EditRelayGateway.tsx
Normal file
34
ui/src/views/gateways/mesh/EditRelayGateway.tsx
Normal 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;
|
119
ui/src/views/gateways/mesh/ListRelayGateways.tsx
Normal file
119
ui/src/views/gateways/mesh/ListRelayGateways.tsx
Normal 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;
|
76
ui/src/views/gateways/mesh/RelayGatewayForm.tsx
Normal file
76
ui/src/views/gateways/mesh/RelayGatewayForm.tsx
Normal 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;
|
108
ui/src/views/gateways/mesh/RelayGatewayLayout.tsx
Normal file
108
ui/src/views/gateways/mesh/RelayGatewayLayout.tsx
Normal 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;
|
@ -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} />} />
|
||||
|
Loading…
Reference in New Issue
Block a user