mirror of
https://github.com/chirpstack/chirpstack.git
synced 2025-06-23 17:53:25 +00:00
Implement support for device-profile templates + TTN importer.
This commit is contained in:
@ -19,7 +19,7 @@
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"antd": "^4.20.1",
|
||||
"antd": "^4.20.6",
|
||||
"antd-mask-input": "^2.0.7",
|
||||
"buffer": "^6.0.3",
|
||||
"chart.js": "^3.7.1",
|
||||
|
@ -30,6 +30,11 @@ import ChangeUserPassword from "./views/users/ChangeUserPassword";
|
||||
import ListAdminApiKeys from "./views/api-keys/ListAdminApiKeys";
|
||||
import CreateAdminApiKey from "./views/api-keys/CreateAdminApiKey";
|
||||
|
||||
// device-profile templates
|
||||
import ListDeviceProfileTemplates from "./views/device-profile-templates/ListDeviceProfileTemplates";
|
||||
import EditDeviceProfileTemplate from "./views/device-profile-templates/EditDeviceProfileTemplate";
|
||||
import CreateDeviceProfileTemplate from "./views/device-profile-templates/CreateDeviceProfileTemplate";
|
||||
|
||||
// stores
|
||||
import SessionStore from "./stores/SessionStore";
|
||||
|
||||
@ -93,6 +98,14 @@ class App extends Component<IProps, IState> {
|
||||
|
||||
<Route exact path="/api-keys" component={ListAdminApiKeys} />
|
||||
<Route exact path="/api-keys/create" component={CreateAdminApiKey} />
|
||||
|
||||
<Route exact path="/device-profile-templates" component={ListDeviceProfileTemplates} />
|
||||
<Route exact path="/device-profile-templates/create" component={CreateDeviceProfileTemplate} />
|
||||
<Route
|
||||
exact
|
||||
path="/device-profile-templates/:deviceProfileTemplateId([\w-]+)/edit"
|
||||
component={EditDeviceProfileTemplate}
|
||||
/>
|
||||
</Switch>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
|
@ -35,6 +35,18 @@ class CodeEditor extends Component<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(oldProps: IProps) {
|
||||
if (this.props === oldProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.value) {
|
||||
this.setState({
|
||||
value: this.props.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateField = () => {
|
||||
let value = this.state.value;
|
||||
|
||||
|
@ -118,6 +118,11 @@ class SideMenu extends Component<RouteComponentProps, IState> {
|
||||
this.setState({ selectedKey: "ns-api-keys" });
|
||||
}
|
||||
|
||||
// ns device-profile templates
|
||||
if (/\/device-profile-templates(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
|
||||
this.setState({ selectedKey: "ns-device-profile-templates" });
|
||||
}
|
||||
|
||||
// tenant dashboard
|
||||
if (/\/tenants\/[\w-]{36}/g.exec(path)) {
|
||||
this.setState({ selectedKey: "tenant-dashboard" });
|
||||
@ -163,6 +168,11 @@ class SideMenu extends Component<RouteComponentProps, IState> {
|
||||
{ key: "ns-tenants", icon: <HomeOutlined />, label: <Link to="/tenants">Tenants</Link> },
|
||||
{ key: "ns-users", icon: <UserOutlined />, label: <Link to="/users">Users</Link> },
|
||||
{ key: "ns-api-keys", icon: <KeyOutlined />, label: <Link to="/api-keys">API keys</Link> },
|
||||
{
|
||||
key: "ns-device-profile-templates",
|
||||
icon: <ControlOutlined />,
|
||||
label: <Link to="/device-profile-templates">Device-profile templates</Link>,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
97
ui/src/stores/DeviceProfileTemplateStore.ts
Normal file
97
ui/src/stores/DeviceProfileTemplateStore.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { notification } from "antd";
|
||||
import { EventEmitter } from "events";
|
||||
import { DeviceProfileTemplateServiceClient } from "@chirpstack/chirpstack-api-grpc-web/api/device_profile_template_grpc_web_pb";
|
||||
import {
|
||||
CreateDeviceProfileTemplateRequest,
|
||||
GetDeviceProfileTemplateRequest,
|
||||
GetDeviceProfileTemplateResponse,
|
||||
UpdateDeviceProfileTemplateRequest,
|
||||
DeleteDeviceProfileTemplateRequest,
|
||||
ListDeviceProfileTemplatesRequest,
|
||||
ListDeviceProfileTemplatesResponse,
|
||||
} from "@chirpstack/chirpstack-api-grpc-web/api/device_profile_template_pb";
|
||||
|
||||
import SessionStore from "./SessionStore";
|
||||
import { HandleError } from "./helpers";
|
||||
|
||||
class DeviceProfileTemplateStore extends EventEmitter {
|
||||
client: DeviceProfileTemplateServiceClient;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.client = new DeviceProfileTemplateServiceClient("");
|
||||
}
|
||||
|
||||
create = (req: CreateDeviceProfileTemplateRequest, callbackFunc: () => void) => {
|
||||
this.client.create(req, SessionStore.getMetadata(), (err, resp) => {
|
||||
if (err !== null) {
|
||||
HandleError(err);
|
||||
return;
|
||||
}
|
||||
|
||||
notification.success({
|
||||
message: "Device-profile template created",
|
||||
duration: 3,
|
||||
});
|
||||
|
||||
callbackFunc();
|
||||
});
|
||||
};
|
||||
|
||||
get = (req: GetDeviceProfileTemplateRequest, callbackFunc: (resp: GetDeviceProfileTemplateResponse) => void) => {
|
||||
this.client.get(req, SessionStore.getMetadata(), (err, resp) => {
|
||||
if (err !== null) {
|
||||
HandleError(err);
|
||||
return;
|
||||
}
|
||||
|
||||
callbackFunc(resp);
|
||||
});
|
||||
};
|
||||
|
||||
update = (req: UpdateDeviceProfileTemplateRequest, callbackFunc: () => void) => {
|
||||
this.client.update(req, SessionStore.getMetadata(), err => {
|
||||
if (err !== null) {
|
||||
HandleError(err);
|
||||
return;
|
||||
}
|
||||
|
||||
notification.success({
|
||||
message: "Device-profile template updated",
|
||||
duration: 3,
|
||||
});
|
||||
|
||||
callbackFunc();
|
||||
});
|
||||
};
|
||||
|
||||
delete = (req: DeleteDeviceProfileTemplateRequest, callbackFunc: () => void) => {
|
||||
this.client.delete(req, SessionStore.getMetadata(), err => {
|
||||
if (err !== null) {
|
||||
HandleError(err);
|
||||
return;
|
||||
}
|
||||
|
||||
notification.success({
|
||||
message: "Device-profile template deleted",
|
||||
duration: 3,
|
||||
});
|
||||
|
||||
callbackFunc();
|
||||
});
|
||||
};
|
||||
|
||||
list = (req: ListDeviceProfileTemplatesRequest, callbackFunc: (resp: ListDeviceProfileTemplatesResponse) => void) => {
|
||||
this.client.list(req, SessionStore.getMetadata(), (err, resp) => {
|
||||
if (err !== null) {
|
||||
HandleError(err);
|
||||
return;
|
||||
}
|
||||
|
||||
callbackFunc(resp);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const deviceProfileTemplateStore = new DeviceProfileTemplateStore();
|
||||
export default deviceProfileTemplateStore;
|
@ -91,7 +91,7 @@ class ListAdminApiKeys extends Component<IProps, IState> {
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Network-server</span>
|
||||
<span>Network Server</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>API keys</span>
|
||||
|
@ -0,0 +1,96 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link, RouteComponentProps } from "react-router-dom";
|
||||
|
||||
import { Space, Breadcrumb, Card, PageHeader } from "antd";
|
||||
|
||||
import { MacVersion, RegParamsRevision } from "@chirpstack/chirpstack-api-grpc-web/common/common_pb";
|
||||
import {
|
||||
DeviceProfileTemplate,
|
||||
CreateDeviceProfileTemplateRequest,
|
||||
} from "@chirpstack/chirpstack-api-grpc-web/api/device_profile_template_pb";
|
||||
|
||||
import DeviceProfileTemplateForm from "./DeviceProfileTemplateForm";
|
||||
import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore";
|
||||
|
||||
class CreateDeviceProfileTemplate extends Component<RouteComponentProps> {
|
||||
onFinish = (obj: DeviceProfileTemplate) => {
|
||||
let req = new CreateDeviceProfileTemplateRequest();
|
||||
req.setDeviceProfileTemplate(obj);
|
||||
|
||||
DeviceProfileTemplateStore.create(req, () => {
|
||||
this.props.history.push(`/device-profile-templates`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const codecScript = `// Decode uplink function.
|
||||
//
|
||||
// Input is an object with the following fields:
|
||||
// - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0]
|
||||
// - fPort = Uplink fPort.
|
||||
// - variables = Object containing the configured device variables.
|
||||
//
|
||||
// Output must be an object with the following fields:
|
||||
// - data = Object representing the decoded payload.
|
||||
function decodeUplink(input) {
|
||||
return {
|
||||
object: {
|
||||
temp: 22.5
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Encode downlink function.
|
||||
//
|
||||
// Input is an object with the following fields:
|
||||
// - data = Object representing the payload that must be encoded.
|
||||
// - variables = Object containing the configured device variables.
|
||||
//
|
||||
// Output must be an object with the following fields:
|
||||
// - bytes = Byte array containing the downlink payload.
|
||||
function encodeDownlink(input) {
|
||||
return {
|
||||
data: [225, 230, 255, 0]
|
||||
};
|
||||
}
|
||||
`;
|
||||
|
||||
let deviceProfileTemplate = new DeviceProfileTemplate();
|
||||
deviceProfileTemplate.setPayloadCodecScript(codecScript);
|
||||
deviceProfileTemplate.setSupportsOtaa(true);
|
||||
deviceProfileTemplate.setUplinkInterval(3600);
|
||||
deviceProfileTemplate.setDeviceStatusReqInterval(1);
|
||||
deviceProfileTemplate.setAdrAlgorithmId("default");
|
||||
deviceProfileTemplate.setMacVersion(MacVersion.LORAWAN_1_0_3);
|
||||
deviceProfileTemplate.setRegParamsRevision(RegParamsRevision.A);
|
||||
deviceProfileTemplate.setFlushQueueOnActivate(true);
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Network Server</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/device-profile-templates`}>Device-profile templates</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Add</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Add device-profile template"
|
||||
/>
|
||||
<Card>
|
||||
<DeviceProfileTemplateForm initialValues={deviceProfileTemplate} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CreateDeviceProfileTemplate;
|
@ -0,0 +1,419 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Form, Input, Select, InputNumber, Switch, Row, Col, Button, Tabs } from "antd";
|
||||
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
|
||||
import { DeviceProfileTemplate } from "@chirpstack/chirpstack-api-grpc-web/api/device_profile_template_pb";
|
||||
import { CodecRuntime } from "@chirpstack/chirpstack-api-grpc-web/api/device_profile_pb";
|
||||
import { Region, MacVersion, RegParamsRevision } from "@chirpstack/chirpstack-api-grpc-web/common/common_pb";
|
||||
import { ListDeviceProfileAdrAlgorithmsResponse } from "@chirpstack/chirpstack-api-grpc-web/api/device_profile_pb";
|
||||
|
||||
import DeviceProfileStore from "../../stores/DeviceProfileStore";
|
||||
import CodeEditor from "../../components/CodeEditor";
|
||||
|
||||
interface IProps {
|
||||
initialValues: DeviceProfileTemplate;
|
||||
onFinish: (obj: DeviceProfileTemplate) => void;
|
||||
update?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
supportsOtaa: boolean;
|
||||
supportsClassB: boolean;
|
||||
supportsClassC: boolean;
|
||||
payloadCodecRuntime: CodecRuntime;
|
||||
adrAlgorithms: [string, string][];
|
||||
}
|
||||
|
||||
class DeviceProfileTemplateForm extends Component<IProps, IState> {
|
||||
formRef = React.createRef<any>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
supportsOtaa: false,
|
||||
supportsClassB: false,
|
||||
supportsClassC: false,
|
||||
payloadCodecRuntime: CodecRuntime.NONE,
|
||||
adrAlgorithms: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const v = this.props.initialValues;
|
||||
|
||||
this.setState({
|
||||
supportsOtaa: v.getSupportsOtaa(),
|
||||
supportsClassB: v.getSupportsClassB(),
|
||||
supportsClassC: v.getSupportsClassC(),
|
||||
payloadCodecRuntime: v.getPayloadCodecRuntime(),
|
||||
});
|
||||
|
||||
DeviceProfileStore.listAdrAlgorithms((resp: ListDeviceProfileAdrAlgorithmsResponse) => {
|
||||
let adrAlgorithms: [string, string][] = [];
|
||||
for (const a of resp.getResultList()) {
|
||||
adrAlgorithms.push([a.getId(), a.getName()]);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
adrAlgorithms: adrAlgorithms,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onFinish = (values: DeviceProfileTemplate.AsObject) => {
|
||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
||||
let dp = new DeviceProfileTemplate();
|
||||
dp.setId(v.id);
|
||||
|
||||
dp.setName(v.name);
|
||||
dp.setDescription(v.description);
|
||||
dp.setVendor(v.vendor);
|
||||
dp.setFirmware(v.firmware);
|
||||
dp.setRegion(v.region);
|
||||
dp.setMacVersion(v.macVersion);
|
||||
dp.setRegParamsRevision(v.regParamsRevision);
|
||||
dp.setAdrAlgorithmId(v.adrAlgorithmId);
|
||||
dp.setFlushQueueOnActivate(v.flushQueueOnActivate);
|
||||
dp.setUplinkInterval(v.uplinkInterval);
|
||||
dp.setDeviceStatusReqInterval(v.deviceStatusReqInterval);
|
||||
|
||||
// join otaa /abp
|
||||
dp.setSupportsOtaa(v.supportsOtaa);
|
||||
dp.setAbpRx1Delay(v.abpRx1Delay);
|
||||
dp.setAbpRx1DrOffset(v.abpRx1DrOffset);
|
||||
dp.setAbpRx2Dr(v.abpRx2Dr);
|
||||
dp.setAbpRx2Freq(v.abpRx2Freq);
|
||||
|
||||
// class-b
|
||||
dp.setSupportsClassB(v.supportsClassB);
|
||||
dp.setClassBTimeout(v.classBTimeout);
|
||||
|
||||
// class-c
|
||||
dp.setSupportsClassC(v.supportsClassC);
|
||||
dp.setClassCTimeout(v.classCTimeout);
|
||||
|
||||
// codec
|
||||
dp.setPayloadCodecRuntime(v.payloadCodecRuntime);
|
||||
dp.setPayloadCodecScript(v.payloadCodecScript);
|
||||
|
||||
// tags
|
||||
for (const elm of v.tagsMap) {
|
||||
dp.getTagsMap().set(elm[0], elm[1]);
|
||||
}
|
||||
|
||||
this.props.onFinish(dp);
|
||||
};
|
||||
|
||||
onSupportsOtaaChange = (checked: boolean) => {
|
||||
this.setState({
|
||||
supportsOtaa: checked,
|
||||
});
|
||||
};
|
||||
|
||||
onSupportsClassBChnage = (checked: boolean) => {
|
||||
this.setState({
|
||||
supportsClassB: checked,
|
||||
});
|
||||
};
|
||||
|
||||
onSupportsClassCChange = (checked: boolean) => {
|
||||
this.setState({
|
||||
supportsClassC: checked,
|
||||
});
|
||||
};
|
||||
|
||||
onPayloadCodecRuntimeChange = (value: CodecRuntime) => {
|
||||
this.setState({
|
||||
payloadCodecRuntime: value,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const adrOptions = this.state.adrAlgorithms.map(v => <Select.Option value={v[0]}>{v[1]}</Select.Option>);
|
||||
|
||||
return (
|
||||
<Form
|
||||
layout="vertical"
|
||||
initialValues={this.props.initialValues.toObject()}
|
||||
onFinish={this.onFinish}
|
||||
ref={this.formRef}
|
||||
>
|
||||
<Tabs>
|
||||
<Tabs.TabPane tab="General" key="1">
|
||||
<Form.Item
|
||||
label="ID"
|
||||
name="id"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
pattern: new RegExp(/^[\w-]*$/g),
|
||||
message: "Please enter a valid id!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input disabled={!!this.props.update} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Vendor" name="vendor" rules={[{ required: true, message: "Please enter a vendor!" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Firmware version"
|
||||
name="firmware"
|
||||
rules={[{ required: true, message: "Please enter a firmware version!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Description" name="description">
|
||||
<Input.TextArea rows={6} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Region" name="region" rules={[{ required: true, message: "Please select a region!" }]}>
|
||||
<Select>
|
||||
<Select.Option value={Region.AS923}>AS923</Select.Option>
|
||||
<Select.Option value={Region.AS923_2}>AS923-2</Select.Option>
|
||||
<Select.Option value={Region.AS923_3}>AS923-3</Select.Option>
|
||||
<Select.Option value={Region.AS923_4}>AS923-4</Select.Option>
|
||||
<Select.Option value={Region.AU915}>AU915</Select.Option>
|
||||
<Select.Option value={Region.CN779}>CN779</Select.Option>
|
||||
<Select.Option value={Region.EU433}>EU433</Select.Option>
|
||||
<Select.Option value={Region.EU868}>EU868</Select.Option>
|
||||
<Select.Option value={Region.IN865}>IN865</Select.Option>
|
||||
<Select.Option value={Region.ISM2400}>ISM2400</Select.Option>
|
||||
<Select.Option value={Region.KR920}>KR920</Select.Option>
|
||||
<Select.Option value={Region.RU864}>RU864</Select.Option>
|
||||
<Select.Option value={Region.US915}>US915</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="MAC version"
|
||||
tooltip="The LoRaWAN MAC version supported by the device."
|
||||
name="macVersion"
|
||||
rules={[{ required: true, message: "Please select a MAC version!" }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value={MacVersion.LORAWAN_1_0_0}>LoRaWAN 1.0.0</Select.Option>
|
||||
<Select.Option value={MacVersion.LORAWAN_1_0_1}>LoRaWAN 1.0.1</Select.Option>
|
||||
<Select.Option value={MacVersion.LORAWAN_1_0_2}>LoRaWAN 1.0.2</Select.Option>
|
||||
<Select.Option value={MacVersion.LORAWAN_1_0_3}>LoRaWAN 1.0.3</Select.Option>
|
||||
<Select.Option value={MacVersion.LORAWAN_1_0_4}>LoRaWAN 1.0.4</Select.Option>
|
||||
<Select.Option value={MacVersion.LORAWAN_1_1_0}>LoRaWAN 1.1.0</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Regional parameters revision"
|
||||
tooltip="Revision of the Regional Parameters specification supported by the device."
|
||||
name="regParamsRevision"
|
||||
rules={[{ required: true, message: "Please select a regional parameters revision!" }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value={RegParamsRevision.A}>A</Select.Option>
|
||||
<Select.Option value={RegParamsRevision.B}>B</Select.Option>
|
||||
<Select.Option value={RegParamsRevision.RP002_1_0_0}>RP002-1.0.0</Select.Option>
|
||||
<Select.Option value={RegParamsRevision.RP002_1_0_1}>RP002-1.0.1</Select.Option>
|
||||
<Select.Option value={RegParamsRevision.RP002_1_0_2}>RP002-1.0.2</Select.Option>
|
||||
<Select.Option value={RegParamsRevision.RP002_1_0_3}>RP002-1.0.3</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item
|
||||
label="ADR algorithm"
|
||||
tooltip="The ADR algorithm that will be used for controlling the device data-rate."
|
||||
name="adrAlgorithmId"
|
||||
rules={[{ required: true, message: "Please select an ADR algorithm!" }]}
|
||||
>
|
||||
<Select>{adrOptions}</Select>
|
||||
</Form.Item>
|
||||
<Row gutter={24}>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
label="Flush queue on activate"
|
||||
name="flushQueueOnActivate"
|
||||
valuePropName="checked"
|
||||
tooltip="If enabled, the device-queue will be flushed on ABP or OTAA activation."
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
label="Expected uplink interval (secs)"
|
||||
tooltip="The expected interval in seconds in which the device sends uplink messages. This is used to determine if a device is active or inactive."
|
||||
name="uplinkInterval"
|
||||
rules={[{ required: true, message: "Please enter an uplink interval!" }]}
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
label="Device-status request frequency (req/day)"
|
||||
tooltip="Frequency to initiate an End-Device status request (request/day). Set to 0 to disable."
|
||||
name="deviceStatusReqInterval"
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Join (OTAA / ABP)" key="2">
|
||||
<Form.Item label="Device supports OTAA" name="supportsOtaa" valuePropName="checked">
|
||||
<Switch onChange={this.onSupportsOtaaChange} />
|
||||
</Form.Item>
|
||||
{!this.state.supportsOtaa && (
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="RX1 delay"
|
||||
name="abpRx1Delay"
|
||||
rules={[{ required: true, message: "Please enter a RX1 delay!" }]}
|
||||
>
|
||||
<InputNumber min={0} max={15} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="RX1 data-rate offset"
|
||||
tooltip="Please refer the LoRaWAN Regional Parameters specification for valid values."
|
||||
name="abpRx1DrOffset"
|
||||
rules={[{ required: true, message: "Please enter a RX1 data-rate offset!" }]}
|
||||
>
|
||||
<InputNumber min={0} max={15} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
{!this.state.supportsOtaa && (
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="RX2 data-rate"
|
||||
tooltip="Please refer the LoRaWAN Regional Parameters specification for valid values."
|
||||
name="abpRx2Dr"
|
||||
rules={[{ required: true, message: "Please enter a RX2 data-rate!" }]}
|
||||
>
|
||||
<InputNumber min={0} max={15} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="RX2 frequency (Hz)"
|
||||
name="abpRx2Freq"
|
||||
rules={[{ required: true, message: "Please enter a RX2 frequency!" }]}
|
||||
>
|
||||
<InputNumber min={0} style={{ width: "200px" }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Class-B" key="3">
|
||||
<Form.Item label="Device supports Class-B" name="supportsClassB" valuePropName="checked">
|
||||
<Switch onChange={this.onSupportsClassBChnage} />
|
||||
</Form.Item>
|
||||
{this.state.supportsClassB && (
|
||||
<Form.Item
|
||||
label="Class-B confirmed downlink timeout (seconds)"
|
||||
tooltip="Class-B timeout (in seconds) for confirmed downlink transmissions."
|
||||
name="classBTimeout"
|
||||
rules={[{ required: true, message: "Please enter a Class-B confirmed downlink timeout!" }]}
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Class-C" key="4">
|
||||
<Form.Item label="Device supports Class-C" name="supportsClassC" valuePropName="checked">
|
||||
<Switch onChange={this.onSupportsClassCChange} />
|
||||
</Form.Item>
|
||||
{this.state.supportsClassC && (
|
||||
<Form.Item
|
||||
label="Class-C confirmed downlink timeout (seconds)"
|
||||
tooltip="Class-C timeout (in seconds) for confirmed downlink transmissions."
|
||||
name="classCTimeout"
|
||||
rules={[{ required: true, message: "Please enter a Class-C confirmed downlink timeout!" }]}
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Codec" key="5">
|
||||
<Form.Item
|
||||
label="Payload codec"
|
||||
name="payloadCodecRuntime"
|
||||
tooltip="By defining a payload codec, ChirpStack Application Server can encode and decode the binary device payload for you."
|
||||
>
|
||||
<Select onChange={this.onPayloadCodecRuntimeChange}>
|
||||
<Select.Option value={CodecRuntime.NONE}>None</Select.Option>
|
||||
<Select.Option value={CodecRuntime.CAYENNE_LPP}>Cayenne LPP</Select.Option>
|
||||
<Select.Option value={CodecRuntime.JS}>JavaScript functions</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{this.state.payloadCodecRuntime === CodecRuntime.JS && (
|
||||
<CodeEditor
|
||||
label="Codec functions"
|
||||
name="payloadCodecScript"
|
||||
value={this.formRef.current.getFieldValue("payloadCodecScript")}
|
||||
formRef={this.formRef}
|
||||
/>
|
||||
)}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Tags" key="6">
|
||||
<Form.List name="tagsMap">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Row gutter={24}>
|
||||
<Col span={6}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 0]}
|
||||
fieldKey={[name, 0]}
|
||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||
>
|
||||
<Input placeholder="Key" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 1]}
|
||||
fieldKey={[name, 1]}
|
||||
rules={[{ required: true, message: "Please enter a value!" }]}
|
||||
>
|
||||
<Input placeholder="Value" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
Add tag
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DeviceProfileTemplateForm;
|
@ -0,0 +1,115 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps, Link } from "react-router-dom";
|
||||
|
||||
import { Space, Breadcrumb, Card, Button, PageHeader } from "antd";
|
||||
|
||||
import {
|
||||
DeviceProfileTemplate,
|
||||
GetDeviceProfileTemplateRequest,
|
||||
GetDeviceProfileTemplateResponse,
|
||||
UpdateDeviceProfileTemplateRequest,
|
||||
DeleteDeviceProfileTemplateRequest,
|
||||
} from "@chirpstack/chirpstack-api-grpc-web/api/device_profile_template_pb";
|
||||
|
||||
import DeviceProfileTemplateForm from "./DeviceProfileTemplateForm";
|
||||
import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore";
|
||||
import DeleteConfirm from "../../components/DeleteConfirm";
|
||||
|
||||
interface IState {
|
||||
deviceProfileTemplate?: DeviceProfileTemplate;
|
||||
}
|
||||
|
||||
interface MatchParams {
|
||||
deviceProfileTemplateId: string;
|
||||
}
|
||||
|
||||
interface IProps extends RouteComponentProps<MatchParams> {}
|
||||
|
||||
class EditDeviceProfileTemplate extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getDeviceProfileTemplate();
|
||||
}
|
||||
|
||||
getDeviceProfileTemplate = () => {
|
||||
const id = this.props.match.params.deviceProfileTemplateId;
|
||||
let req = new GetDeviceProfileTemplateRequest();
|
||||
req.setId(id);
|
||||
|
||||
DeviceProfileTemplateStore.get(req, (resp: GetDeviceProfileTemplateResponse) => {
|
||||
this.setState({
|
||||
deviceProfileTemplate: resp.getDeviceProfileTemplate(),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onFinish = (obj: DeviceProfileTemplate) => {
|
||||
let req = new UpdateDeviceProfileTemplateRequest();
|
||||
req.setDeviceProfileTemplate(obj);
|
||||
|
||||
DeviceProfileTemplateStore.update(req, () => {
|
||||
this.props.history.push(`/device-profile-templates`);
|
||||
});
|
||||
};
|
||||
|
||||
deleteDeviceProfileTemplate = () => {
|
||||
let req = new DeleteDeviceProfileTemplateRequest();
|
||||
req.setId(this.props.match.params.deviceProfileTemplateId);
|
||||
|
||||
DeviceProfileTemplateStore.delete(req, () => {
|
||||
this.props.history.push(`/device-profile-templates`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const dp = this.state.deviceProfileTemplate;
|
||||
|
||||
if (!dp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Network Server</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/device-profile-templates`}>Device-profile templates</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>{dp.getName()}</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title={dp.getName()}
|
||||
subTitle={`device-profile template id: ${dp.getId()}`}
|
||||
extra={[
|
||||
<DeleteConfirm
|
||||
typ="device-profile template"
|
||||
confirm={dp.getName()}
|
||||
onConfirm={this.deleteDeviceProfileTemplate}
|
||||
>
|
||||
<Button danger type="primary">
|
||||
Delete device-profile template
|
||||
</Button>
|
||||
</DeleteConfirm>,
|
||||
]}
|
||||
/>
|
||||
<Card>
|
||||
<DeviceProfileTemplateForm initialValues={dp} update={true} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default EditDeviceProfileTemplate;
|
@ -0,0 +1,87 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Space, Breadcrumb, Button, PageHeader } from "antd";
|
||||
import { ColumnsType } from "antd/es/table";
|
||||
|
||||
import {
|
||||
ListDeviceProfileTemplatesRequest,
|
||||
ListDeviceProfileTemplatesResponse,
|
||||
DeviceProfileTemplateListItem,
|
||||
} from "@chirpstack/chirpstack-api-grpc-web/api/device_profile_template_pb";
|
||||
import { Region } from "@chirpstack/chirpstack-api-grpc-web/common/common_pb";
|
||||
|
||||
import { getEnumName } from "../helpers";
|
||||
import DataTable, { GetPageCallbackFunc } from "../../components/DataTable";
|
||||
import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore";
|
||||
|
||||
class ListDeviceProfileTemplates extends Component {
|
||||
columns = (): ColumnsType<DeviceProfileTemplateListItem.AsObject> => {
|
||||
return [
|
||||
{
|
||||
title: "Vendor",
|
||||
dataIndex: "vendor",
|
||||
key: "vendor",
|
||||
},
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
render: (text, record) => <Link to={`/device-profile-templates/${record.id}/edit`}>{text}</Link>,
|
||||
},
|
||||
{
|
||||
title: "Firmware",
|
||||
dataIndex: "firmware",
|
||||
key: "firmware",
|
||||
},
|
||||
{
|
||||
title: "Region",
|
||||
dataIndex: "region",
|
||||
key: "region",
|
||||
width: 150,
|
||||
render: (text, record) => {
|
||||
return getEnumName(Region, record.region);
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||
let req = new ListDeviceProfileTemplatesRequest();
|
||||
req.setLimit(limit);
|
||||
req.setOffset(offset);
|
||||
|
||||
DeviceProfileTemplateStore.list(req, (resp: ListDeviceProfileTemplatesResponse) => {
|
||||
const obj = resp.toObject();
|
||||
callbackFunc(obj.totalCount, obj.resultList);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Network Server</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Device-profile templates</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Device-profile templates"
|
||||
extra={[
|
||||
<Button type="primary">
|
||||
<Link to={`/device-profile-templates/create`}>Add device-profile template</Link>
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
<DataTable columns={this.columns()} getPage={this.getPage} rowKey="id" />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ListDeviceProfileTemplates;
|
@ -43,7 +43,7 @@ class CreateDeviceProfile extends Component<IProps> {
|
||||
// - data = Object representing the decoded payload.
|
||||
function decodeUplink(input) {
|
||||
return {
|
||||
object: {
|
||||
data: {
|
||||
temp: 22.5
|
||||
}
|
||||
};
|
||||
|
@ -1,14 +1,184 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Form, Input, Select, InputNumber, Switch, Row, Col, Button, Tabs } from "antd";
|
||||
import { Form, Input, Select, InputNumber, Switch, Row, Col, Button, Tabs, Modal, Spin, Cascader } from "antd";
|
||||
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
|
||||
import { DeviceProfile, CodecRuntime } from "@chirpstack/chirpstack-api-grpc-web/api/device_profile_pb";
|
||||
import { Region, MacVersion, RegParamsRevision } from "@chirpstack/chirpstack-api-grpc-web/common/common_pb";
|
||||
import { ListDeviceProfileAdrAlgorithmsResponse } from "@chirpstack/chirpstack-api-grpc-web/api/device_profile_pb";
|
||||
import {
|
||||
ListDeviceProfileTemplatesRequest,
|
||||
ListDeviceProfileTemplatesResponse,
|
||||
GetDeviceProfileTemplateRequest,
|
||||
GetDeviceProfileTemplateResponse,
|
||||
DeviceProfileTemplateListItem,
|
||||
DeviceProfileTemplate,
|
||||
} from "@chirpstack/chirpstack-api-grpc-web/api/device_profile_template_pb";
|
||||
|
||||
import { getEnumName } from "../helpers";
|
||||
import DeviceProfileStore from "../../stores/DeviceProfileStore";
|
||||
import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore";
|
||||
import CodeEditor from "../../components/CodeEditor";
|
||||
|
||||
interface ModalProps {
|
||||
onOk: (dp: DeviceProfileTemplate) => void;
|
||||
onCancel: () => void;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
interface ModalState {
|
||||
templates: DeviceProfileTemplateListItem[];
|
||||
templatesLoaded: boolean;
|
||||
templateId?: string;
|
||||
}
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
children?: Option[];
|
||||
}
|
||||
|
||||
class TemplateModal extends Component<ModalProps, ModalState> {
|
||||
constructor(props: ModalProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
templates: [],
|
||||
templatesLoaded: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: ModalProps) {
|
||||
if (prevProps === this.props) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.visible) {
|
||||
this.setState({
|
||||
templatesLoaded: false,
|
||||
});
|
||||
|
||||
let req = new ListDeviceProfileTemplatesRequest();
|
||||
req.setLimit(99999);
|
||||
|
||||
DeviceProfileTemplateStore.list(req, (resp: ListDeviceProfileTemplatesResponse) => {
|
||||
this.setState({
|
||||
templatesLoaded: true,
|
||||
templates: resp.getResultList(),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (value: (string | number)[]) => {
|
||||
this.setState({
|
||||
templateId: value.at(-1)! as string,
|
||||
});
|
||||
};
|
||||
|
||||
onOk = () => {
|
||||
if (this.state.templateId) {
|
||||
let req = new GetDeviceProfileTemplateRequest();
|
||||
req.setId(this.state.templateId);
|
||||
|
||||
DeviceProfileTemplateStore.get(req, (resp: GetDeviceProfileTemplateResponse) => {
|
||||
const dp = resp.getDeviceProfileTemplate();
|
||||
if (dp) {
|
||||
this.props.onOk(dp);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
let options: Option[] = [];
|
||||
let vendor = "";
|
||||
let device = "";
|
||||
let firmware = "";
|
||||
let region = "";
|
||||
|
||||
for (const item of this.state.templates) {
|
||||
if (vendor !== item.getVendor()) {
|
||||
options.push({
|
||||
value: item.getId(),
|
||||
label: item.getVendor(),
|
||||
children: [],
|
||||
});
|
||||
|
||||
vendor = item.getVendor();
|
||||
device = "";
|
||||
firmware = "";
|
||||
region = "";
|
||||
}
|
||||
|
||||
if (device !== item.getName()) {
|
||||
options.at(-1)!.children!.push({
|
||||
value: item.getId(),
|
||||
label: item.getName(),
|
||||
children: [],
|
||||
});
|
||||
|
||||
device = item.getName();
|
||||
firmware = "";
|
||||
region = "";
|
||||
}
|
||||
|
||||
if (firmware !== item.getFirmware()) {
|
||||
options
|
||||
.at(-1)!
|
||||
.children!.at(-1)!
|
||||
.children!.push({
|
||||
value: item.getId(),
|
||||
label: "FW version: " + item.getFirmware(),
|
||||
children: [],
|
||||
});
|
||||
|
||||
firmware = item.getFirmware();
|
||||
region = "";
|
||||
}
|
||||
|
||||
if (region !== getEnumName(Region, item.getRegion())) {
|
||||
options
|
||||
.at(-1)!
|
||||
.children!.at(-1)!
|
||||
.children!.at(-1)!
|
||||
.children!.push({
|
||||
value: item.getId(),
|
||||
label: getEnumName(Region, item.getRegion()),
|
||||
children: [],
|
||||
});
|
||||
|
||||
region = getEnumName(Region, item.getRegion());
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Select device-profile template"
|
||||
visible={this.props.visible}
|
||||
width="80%"
|
||||
bodyStyle={{ height: 300 }}
|
||||
onOk={this.onOk}
|
||||
onCancel={this.props.onCancel}
|
||||
okButtonProps={{ disabled: !!!this.state.templateId }}
|
||||
>
|
||||
{!this.state.templatesLoaded && (
|
||||
<div className="spinner">
|
||||
<Spin />
|
||||
</div>
|
||||
)}
|
||||
{this.state.templatesLoaded && (
|
||||
<Cascader
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Select a device-profile template"
|
||||
options={options}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
initialValues: DeviceProfile;
|
||||
onFinish: (obj: DeviceProfile) => void;
|
||||
@ -21,6 +191,8 @@ interface IState {
|
||||
supportsClassC: boolean;
|
||||
payloadCodecRuntime: CodecRuntime;
|
||||
adrAlgorithms: [string, string][];
|
||||
templateModalVisible: boolean;
|
||||
tabActive: string;
|
||||
}
|
||||
|
||||
class DeviceProfileForm extends Component<IProps, IState> {
|
||||
@ -34,6 +206,8 @@ class DeviceProfileForm extends Component<IProps, IState> {
|
||||
supportsClassC: false,
|
||||
payloadCodecRuntime: CodecRuntime.NONE,
|
||||
adrAlgorithms: [],
|
||||
templateModalVisible: false,
|
||||
tabActive: "1",
|
||||
};
|
||||
}
|
||||
|
||||
@ -59,13 +233,21 @@ class DeviceProfileForm extends Component<IProps, IState> {
|
||||
});
|
||||
}
|
||||
|
||||
onTabChange = (activeKey: string) => {
|
||||
this.setState({
|
||||
tabActive: activeKey,
|
||||
});
|
||||
}
|
||||
|
||||
onFinish = (values: DeviceProfile.AsObject) => {
|
||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
||||
|
||||
let dp = new DeviceProfile();
|
||||
dp.setId(v.id);
|
||||
dp.setTenantId(v.tenantId);
|
||||
|
||||
dp.setName(v.name);
|
||||
dp.setDescription(v.description);
|
||||
dp.setRegion(v.region);
|
||||
dp.setMacVersion(v.macVersion);
|
||||
dp.setRegParamsRevision(v.regParamsRevision);
|
||||
@ -98,6 +280,7 @@ class DeviceProfileForm extends Component<IProps, IState> {
|
||||
dp.getTagsMap().set(elm[0], elm[1]);
|
||||
}
|
||||
|
||||
|
||||
this.props.onFinish(dp);
|
||||
};
|
||||
|
||||
@ -125,8 +308,102 @@ class DeviceProfileForm extends Component<IProps, IState> {
|
||||
});
|
||||
};
|
||||
|
||||
showTemplateModal = () => {
|
||||
this.setState({
|
||||
templateModalVisible: true,
|
||||
});
|
||||
};
|
||||
|
||||
onTemplateModalOk = (dp: DeviceProfileTemplate) => {
|
||||
this.setState({
|
||||
templateModalVisible: false,
|
||||
});
|
||||
|
||||
this.formRef.current.setFieldsValue({
|
||||
name: dp.getName(),
|
||||
description: dp.getDescription(),
|
||||
region: dp.getRegion(),
|
||||
macVersion: dp.getMacVersion(),
|
||||
regParamsRevision: dp.getRegParamsRevision(),
|
||||
adrAlgorithmId: dp.getAdrAlgorithmId(),
|
||||
payloadCodecRuntime: dp.getPayloadCodecRuntime(),
|
||||
payloadCodecScript: dp.getPayloadCodecScript(),
|
||||
flushQueueOnActivate: dp.getFlushQueueOnActivate(),
|
||||
uplinkInterval: dp.getUplinkInterval(),
|
||||
deviceStatusReqInterval: dp.getDeviceStatusReqInterval(),
|
||||
supportsOtaa: dp.getSupportsOtaa(),
|
||||
supportsClassB: dp.getSupportsClassB(),
|
||||
supportsClassC: dp.getSupportsClassC(),
|
||||
classBTimeout: dp.getClassBTimeout(),
|
||||
abpRx1Delay: dp.getAbpRx1Delay(),
|
||||
abpRx2Dr: dp.getAbpRx2Dr(),
|
||||
abpRx2Freq: dp.getAbpRx2Freq(),
|
||||
tagsMap: [
|
||||
["firmware", dp.getFirmware()],
|
||||
["vendor", dp.getVendor()],
|
||||
["device", dp.getName()],
|
||||
["device_profile_template_id", dp.getId()],
|
||||
],
|
||||
});
|
||||
|
||||
const tabActive = this.state.tabActive;
|
||||
|
||||
this.setState({
|
||||
supportsOtaa: dp.getSupportsOtaa(),
|
||||
supportsClassB: dp.getSupportsClassB(),
|
||||
supportsClassC: dp.getSupportsClassC(),
|
||||
payloadCodecRuntime: dp.getPayloadCodecRuntime(),
|
||||
}, () => {
|
||||
// This is a workaround as without rendering the TabPane (e.g. the user
|
||||
// does not click through the different tabs), setFieldsValue does not
|
||||
// actually update the fields. For example if selecting a template with
|
||||
// a codec script and immediately click the save button, no codec script
|
||||
// is passed to the onFinish function. This seems to be with every field
|
||||
// that is not actually rendered before clicking the Save button.
|
||||
this.setState({
|
||||
tabActive: "1",
|
||||
}, () => {
|
||||
this.setState({
|
||||
tabActive: "2",
|
||||
}, () => {
|
||||
this.setState({
|
||||
tabActive: "3",
|
||||
}, () => {
|
||||
this.setState({
|
||||
tabActive: "4",
|
||||
}, () => {
|
||||
this.setState({
|
||||
tabActive: "5",
|
||||
}, () => {
|
||||
this.setState({
|
||||
tabActive: "6",
|
||||
}, () => {
|
||||
this.setState({
|
||||
tabActive: tabActive,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
onTemplateModalCancel = () => {
|
||||
this.setState({
|
||||
templateModalVisible: false,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const adrOptions = this.state.adrAlgorithms.map(v => <Select.Option value={v[0]}>{v[1]}</Select.Option>);
|
||||
const operations = (
|
||||
<Button type="primary" onClick={this.showTemplateModal}>
|
||||
Select device-profile template
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form
|
||||
@ -135,11 +412,19 @@ class DeviceProfileForm extends Component<IProps, IState> {
|
||||
onFinish={this.onFinish}
|
||||
ref={this.formRef}
|
||||
>
|
||||
<Tabs>
|
||||
<TemplateModal
|
||||
visible={this.state.templateModalVisible}
|
||||
onOk={this.onTemplateModalOk}
|
||||
onCancel={this.onTemplateModalCancel}
|
||||
/>
|
||||
<Tabs tabBarExtraContent={operations} activeKey={this.state.tabActive} onChange={this.onTabChange}>
|
||||
<Tabs.TabPane tab="General" key="1">
|
||||
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
|
||||
<Input disabled={this.props.disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Description" name="description">
|
||||
<Input.TextArea rows={6} disabled={this.props.disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Region" name="region" rules={[{ required: true, message: "Please select a region!" }]}>
|
||||
<Select disabled={this.props.disabled}>
|
||||
<Select.Option value={Region.AS923}>AS923</Select.Option>
|
||||
@ -330,7 +615,7 @@ class DeviceProfileForm extends Component<IProps, IState> {
|
||||
<CodeEditor
|
||||
label="Codec functions"
|
||||
name="payloadCodecScript"
|
||||
value={this.props.initialValues.getPayloadCodecScript()}
|
||||
value={this.formRef.current.getFieldValue("payloadCodecScript")}
|
||||
formRef={this.formRef}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
|
@ -100,7 +100,7 @@ class ListTenants extends Component {
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Network-server</span>
|
||||
<span>Network Server</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
|
@ -67,7 +67,7 @@ class ListUsers extends Component {
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Network-server</span>
|
||||
<span>Network Server</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Users</span>
|
||||
|
21
ui/yarn.lock
21
ui/yarn.lock
@ -2839,7 +2839,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
|
||||
integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
|
||||
|
||||
"@types/qs@*":
|
||||
"@types/qs@*", "@types/qs@^6.9.7":
|
||||
version "6.9.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
|
||||
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
|
||||
@ -3369,16 +3369,17 @@ antd-mask-input@^2.0.7:
|
||||
dependencies:
|
||||
imask "6.4.2"
|
||||
|
||||
antd@^4.20.1:
|
||||
version "4.20.1"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-4.20.1.tgz#6cd5a406c7172d61a5d0693ea52ee908650cf674"
|
||||
integrity sha512-asKxOV0a6AijqonbcXkO08/q+XvqS/HmGfaRIS6ZH1ALR3FS2q+kTW52rJZO9rfoOb/ldPhEBVSWiNrbiB+uCQ==
|
||||
antd@^4.20.6:
|
||||
version "4.20.6"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-4.20.6.tgz#0d46a4b6128a717b4cad7ac6902311a0210f1d6f"
|
||||
integrity sha512-JHEwCDjWTAJ1yxlC5QPb7LhRMvdhccN5lzMYDs72sp6VOiaXVGAlwols+F8nQQRaF9h/eA6yQyZ622y8b9vaoQ==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^6.0.0"
|
||||
"@ant-design/icons" "^4.7.0"
|
||||
"@ant-design/react-slick" "~0.28.1"
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@ctrl/tinycolor" "^3.4.0"
|
||||
"@types/qs" "^6.9.7"
|
||||
classnames "^2.2.6"
|
||||
copy-to-clipboard "^3.2.0"
|
||||
lodash "^4.17.21"
|
||||
@ -3403,7 +3404,7 @@ antd@^4.20.1:
|
||||
rc-progress "~3.2.1"
|
||||
rc-rate "~2.9.0"
|
||||
rc-resize-observer "^1.2.0"
|
||||
rc-segmented "~2.0.0"
|
||||
rc-segmented "~2.1.0 "
|
||||
rc-select "~14.1.1"
|
||||
rc-slider "~10.0.0"
|
||||
rc-steps "~4.1.0"
|
||||
@ -8846,10 +8847,10 @@ rc-resize-observer@^1.1.0, rc-resize-observer@^1.2.0:
|
||||
rc-util "^5.15.0"
|
||||
resize-observer-polyfill "^1.5.1"
|
||||
|
||||
rc-segmented@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-segmented/-/rc-segmented-2.0.0.tgz#209b55bec85c1a8b1821c30e62d3ebef4da04b52"
|
||||
integrity sha512-YsdS+aP7E6ZMEY35WSlewJIsrjPbBSP4X/7RvZtzLExKDZwFvXdCPCbWFVDNks4jOYY9TUPYt7qlVifEu9/zXA==
|
||||
"rc-segmented@~2.1.0 ":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-segmented/-/rc-segmented-2.1.0.tgz#0e0afe646c1a0e44a0e18785f518c42633ec8efc"
|
||||
integrity sha512-hUlonro+pYoZcwrH6Vm56B2ftLfQh046hrwif/VwLIw1j3zGt52p5mREBwmeVzXnSwgnagpOpfafspzs1asjGw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.1"
|
||||
classnames "^2.2.1"
|
||||
|
Reference in New Issue
Block a user