Refactor UI to function elements & update React + Ant.

This refactor the UI components from class based element into function
based elements. This makes it possible to use hooks that are used now
by most React components. This also updates React and Ant to the latest
versions (+ other dependencies).
This commit is contained in:
Orne Brocaar 2023-07-27 13:06:07 +01:00
parent afc196095d
commit 6f1638e87a
141 changed files with 12760 additions and 15459 deletions

View File

@ -18,4 +18,4 @@ pkgs.mkShell {
BINDGEN_EXTRA_CLANG_ARGS = "-I${pkgs.llvmPackages.libclang.lib}/lib/clang/${pkgs.llvmPackages.libclang.version}/include";
DOCKER_BUILDKIT = "1";
NIX_STORE = "/nix/store";
}
}

View File

@ -12,3 +12,6 @@ dependencies:
clean:
rm -rf build
format:
./node_modules/.bin/prettier --write .

View File

@ -1,46 +0,0 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `yarn start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

View File

@ -3,76 +3,71 @@
"version": "4.4.3",
"private": true,
"dependencies": {
"@ant-design/colors": "^6.0.0",
"@ant-design/colors": "^7.0.0",
"@ant-design/pro-layout": "^7.16.3",
"@chirpstack/chirpstack-api-grpc-web": "file:../api/grpc-web",
"@fortawesome/fontawesome-free": "^6.1.1",
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-fontawesome": "^0.1.18",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.15",
"@types/leaflet": "^1.7.5",
"@fortawesome/fontawesome-free": "^6.4.0",
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/leaflet": "^1.9.3",
"@types/leaflet.awesome-markers": "^2.0.25",
"@types/node": "^12.0.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-router-dom": "^5.3.3",
"antd": "^4.20.6",
"antd-mask-input": "^2.0.7",
"@types/node": "^16.18.38",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"antd": "^5.7.1",
"buffer": "^6.0.3",
"chart.js": "^3.7.1",
"chartjs-adapter-moment": "^1.0.0",
"chartjs-chart-matrix": "^1.1.1",
"codemirror": "^5.65.3",
"google-protobuf": "^3.21.2",
"grpc-web": "^1.4.2",
"chart.js": "^4.3.0",
"chartjs-adapter-moment": "^1.0.1",
"chartjs-chart-matrix": "^2.0.1",
"codemirror": "5.65.3",
"history": "^5.3.0",
"js-file-download": "^0.4.12",
"leaflet": "^1.7.1",
"leaflet": "^1.9.4",
"leaflet.awesome-markers": "^2.0.5",
"react": "^17.0.2",
"react-chartjs-2": "^4.1.0",
"moment": "^2.29.4",
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
"react-codemirror2": "^7.2.1",
"react-dom": "^17.0.2",
"react-json-tree": "^0.15.1",
"react-leaflet": "^3.2.1",
"react-markdown": "^8.0.3",
"react-router-dom": "^5.3.1",
"react-dom": "^18.2.0",
"react-json-tree": "0.15.1",
"react-leaflet": "^4.2.1",
"react-markdown": "^8.0.7",
"react-router-dom": "^6.14.2",
"react-scripts": "5.0.1",
"typescript": "^4.6.4",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"lint": "prettier --check .",
"format": "prettier --write ."
},
"husky": {
"hooks": {
"pre-commit": "yarn format"
}
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"ignorePatterns": [
"**/*_pb.js"
]
},
"browserslist": [
">0.2%",
"not dead",
"not op_mini all"
],
"proxy": "http://chirpstack:8080/",
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://127.0.0.1:8080/",
"devDependencies": {
"husky": "^7.0.4",
"prettier": "^2.6.2"
"prettier": "^3.0.0"
}
}

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@ -1,18 +1,22 @@
import React, { Component } from "react";
import { Router, Route, Switch } from "react-router-dom";
import React, { useState } from "react";
import { Router, Routes, Route } from "react-router-dom";
import { Layout } from "antd";
import { User } from "@chirpstack/chirpstack-api-grpc-web/api/user_pb";
import Menu from "./components/Menu";
import Header from "./components/Header";
import Menu from "./components/Menu";
// dashboard
import Dashboard from "./views/dashboard/Dashboard";
// users
import Login from "./views/users/Login";
import ListUsers from "./views/users/ListUsers";
import CreateUser from "./views/users/CreateUser";
import EditUser from "./views/users/EditUser";
import ChangeUserPassword from "./views/users/ChangeUserPassword";
// tenants
import TenantRedirect from "./views/tenants/TenantRedirect";
@ -20,20 +24,14 @@ import ListTenants from "./views/tenants/ListTenants";
import CreateTenant from "./views/tenants/CreateTenant";
import TenantLoader from "./views/tenants/TenantLoader";
// users
import ListUsers from "./views/users/ListUsers";
import CreateUser from "./views/users/CreateUser";
import EditUser from "./views/users/EditUser";
import ChangeUserPassword from "./views/users/ChangeUserPassword";
// api keys
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";
import EditDeviceProfileTemplate from "./views/device-profile-templates/EditDeviceProfileTemplate";
// regions
import ListRegions from "./views/regions/ListRegions";
@ -44,85 +42,72 @@ import SessionStore from "./stores/SessionStore";
import history from "./history";
interface IProps {}
const CustomRouter = ({ history, ...props }: any) => {
const [state, setState] = useState({
action: history.action,
location: history.location,
});
interface IState {
user?: User;
}
React.useLayoutEffect(() => history.listen(setState), [history]);
class App extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
return <Router {...props} location={state.location} navigationType={state.action} navigator={history} />;
};
this.state = {
user: undefined,
};
}
function App() {
const [user, setUser] = useState<User | undefined>(SessionStore.getUser());
SessionStore.on("change", () => {
setUser(SessionStore.getUser());
});
componentDidMount() {
SessionStore.on("change", () => {
this.setState({
user: SessionStore.getUser(),
});
});
return (
<Layout style={{ minHeight: "100vh" }}>
<CustomRouter history={history}>
<Routes>
<Route path="/" element={<TenantRedirect />} />
<Route path="/login" element={<Login />} />
</Routes>
this.setState({
user: SessionStore.getUser(),
});
}
{user && (
<div>
<Layout.Header className="layout-header">
<Header user={user} />
</Layout.Header>
<Layout className="layout">
<Layout.Sider width="300" theme="light" className="layout-menu">
<Menu />
</Layout.Sider>
<Layout.Content className="layout-content" style={{ padding: "24px 24px 24px" }}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/tenants" element={<ListTenants />} />
<Route path="/tenants/create" element={<CreateTenant />} />
<Route path="/tenants/:tenantId/*" element={<TenantLoader />} />
render() {
return (
<Layout style={{ minHeight: "100vh" }}>
<Router history={history}>
<Switch>
<Route exact path="/" component={TenantRedirect} />
<Route exact path="/login" component={Login} />
{this.state.user && (
<Route>
<Layout.Header className="layout-header">
<Header user={this.state.user} />
</Layout.Header>
<Layout className="layout">
<Layout.Sider width="300" theme="light" className="layout-menu">
<Menu />
</Layout.Sider>
<Layout.Content className="layout-content" style={{ padding: "24px 24px 24px" }}>
<Switch>
<Route exact path="/dashboard" component={Dashboard} />
<Route path="/users" element={<ListUsers />} />
<Route path="/users/create" element={<CreateUser />} />
<Route path="/users/:userId" element={<EditUser />} />
<Route path="/users/:userId/password" element={<ChangeUserPassword />} />
<Route exact path="/tenants" component={ListTenants} />
<Route exact path="/tenants/create" component={CreateTenant} />
<Route path="/tenants/:tenantId([\w-]{36})" component={TenantLoader} />
<Route path="/api-keys" element={<ListAdminApiKeys />} />
<Route path="/api-keys/create" element={<CreateAdminApiKey />} />
<Route exact path="/users" component={ListUsers} />
<Route exact path="/users/create" component={CreateUser} />
<Route exact path="/users/:userId([\w-]{36})" component={EditUser} />
<Route exact path="/users/:userId([\w-]{36})/password" component={ChangeUserPassword} />
<Route path="/device-profile-templates" element={<ListDeviceProfileTemplates />} />
<Route path="/device-profile-templates/create" element={<CreateDeviceProfileTemplate />} />
<Route
path="/device-profile-templates/:deviceProfileTemplateId/edit"
element={<EditDeviceProfileTemplate />}
/>
<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}
/>
<Route exact path="/regions" component={ListRegions} />
<Route path="/regions/:id(.*)" component={RegionDetails} />
</Switch>
</Layout.Content>
</Layout>
</Route>
)}
</Switch>
</Router>
</Layout>
);
}
<Route path="/regions" element={<ListRegions />} />
<Route path="/regions/:id" element={<RegionDetails />} />
</Routes>
</Layout.Content>
</Layout>
</div>
)}
</CustomRouter>
</Layout>
);
}
export default App;

View File

@ -1,4 +1,4 @@
import { Component } from "react";
import React, { PropsWithChildren, useState, useEffect } from "react";
import SessionStore from "../stores/SessionStore";
@ -9,73 +9,45 @@ interface IProps {
isTenantAdmin?: boolean;
}
interface IState {
admin: boolean;
}
function Admin(props: PropsWithChildren<IProps>) {
const [admin, setAdmin] = useState<boolean>(false);
class Admin extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
admin: false,
};
}
componentDidMount() {
SessionStore.on("change", this.setIsAdmin);
this.setIsAdmin();
}
componentWillUnmount() {
SessionStore.removeListener("change", this.setIsAdmin);
}
componentDidUpdate(prevProps: IProps) {
if (prevProps === this.props) {
return;
}
this.setIsAdmin();
}
setIsAdmin = () => {
if (!this.props.isDeviceAdmin && !this.props.isGatewayAdmin && !this.props.isTenantAdmin) {
this.setState({
admin: SessionStore.isAdmin(),
});
const setIsAdmin = () => {
if (!props.isDeviceAdmin && !props.isGatewayAdmin && !props.isTenantAdmin) {
setAdmin(SessionStore.isAdmin());
} else {
if (this.props.tenantId === undefined) {
if (props.tenantId === undefined) {
throw new Error("No tenantId is given");
}
if (this.props.isTenantAdmin) {
this.setState({
admin: SessionStore.isAdmin() || SessionStore.isTenantAdmin(this.props.tenantId),
});
if (props.isTenantAdmin) {
setAdmin(SessionStore.isAdmin() || SessionStore.isTenantAdmin(props.tenantId));
}
if (this.props.isDeviceAdmin) {
this.setState({
admin: SessionStore.isAdmin() || SessionStore.isTenantDeviceAdmin(this.props.tenantId),
});
if (props.isDeviceAdmin) {
setAdmin(SessionStore.isAdmin() || SessionStore.isTenantDeviceAdmin(props.tenantId));
}
if (this.props.isGatewayAdmin) {
this.setState({
admin: SessionStore.isAdmin() || SessionStore.isTenantGatewayAdmin(this.props.tenantId),
});
if (props.isGatewayAdmin) {
setAdmin(SessionStore.isAdmin() || SessionStore.isTenantGatewayAdmin(props.tenantId));
}
}
};
render() {
if (this.state.admin) {
return this.props.children;
}
useEffect(() => {
SessionStore.on("change", setIsAdmin);
setIsAdmin();
return null;
return () => {
SessionStore.removeListener("change", setIsAdmin);
};
}, [props]);
if (admin) {
return <div>{props.children}</div>;
}
return null;
}
export default Admin;

View File

@ -1,11 +1,10 @@
import React, { Component } from "react";
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 {
formRef: React.RefObject<any>;
label: string;
name: string;
required?: boolean;
@ -14,42 +13,29 @@ interface IProps {
tooltip?: string;
}
interface IState {
byteOrder: string;
value: string;
}
function AesKeyInput(props: IProps) {
const form = Form.useFormInstance();
const [byteOrder, setByteOrder] = useState<string>("msb");
const [value, setValue] = useState<string>("");
class AesKeyInput extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
byteOrder: "msb",
value: "",
};
}
useEffect(() => {
if (props.value) {
setValue(props.value);
}
}, [props]);
updateField = () => {
let value = this.state.value;
if (this.state.byteOrder === "lsb") {
const bytes = value.match(/[A-Fa-f0-9]{2}/g) || [];
value = bytes.reverse().join("");
const updateField = (v: string) => {
if (byteOrder === "lsb") {
const bytes = v.match(/[A-Fa-f0-9]{2}/g) || [];
v = bytes.reverse().join("");
}
this.props.formRef.current.setFieldsValue({
[this.props.name]: value,
form.setFieldsValue({
[props.name]: v,
});
};
componentDidMount() {
if (this.props.value) {
this.setState({
value: this.props.value,
});
}
}
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let v = e.target.value;
const match = v.match(/[A-Fa-f0-9]/g);
@ -62,50 +48,37 @@ class AesKeyInput extends Component<IProps, IState> {
}
}
this.setState(
{
value: value,
},
this.updateField,
);
setValue(value);
updateField(value);
};
onByteOrderSelect = (v: string) => {
if (v === this.state.byteOrder) {
const onByteOrderSelect = (v: string) => {
if (v === byteOrder) {
return;
}
this.setState({
byteOrder: v,
});
setByteOrder(v);
const current = this.state.value;
const current = value;
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
const vv = bytes.reverse().join("");
this.setState(
{
value: bytes.reverse().join(""),
},
this.updateField,
);
setValue(vv);
updateField(vv);
};
generateRandom = () => {
const generateRandom = () => {
let cryptoObj = window.crypto || window.Crypto;
let b = new Uint8Array(16);
cryptoObj.getRandomValues(b);
let key = Buffer.from(b).toString("hex");
this.setState(
{
value: key,
},
this.updateField,
);
setValue(key);
updateField(key);
};
copyToClipboard = () => {
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
const copyToClipboard = () => {
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
if (bytes !== null && navigator.clipboard !== undefined) {
navigator.clipboard
@ -126,8 +99,8 @@ class AesKeyInput extends Component<IProps, IState> {
}
};
copyToClipboardHexArray = () => {
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
const copyToClipboardHexArray = () => {
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
if (bytes !== null && navigator.clipboard !== undefined) {
navigator.clipboard
@ -153,72 +126,70 @@ class AesKeyInput extends Component<IProps, IState> {
}
};
render() {
const copyMenu = (
<Menu
items={[
{
key: "1",
label: (
<Button type="text" onClick={this.copyToClipboard}>
HEX string
</Button>
),
},
{
key: "2",
label: (
<Button type="text" onClick={this.copyToClipboardHexArray}>
HEX array
</Button>
),
},
]}
/>
);
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={this.state.byteOrder} onChange={this.onByteOrderSelect}>
<Select.Option value="msb">MSB</Select.Option>
<Select.Option value="lsb">LSB</Select.Option>
</Select>
<Button type="text" size="small" shape="circle" onClick={this.generateRandom}>
<ReloadOutlined />
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 overlay={copyMenu}>
<Button type="text" size="small">
<CopyOutlined />
</Button>
</Dropdown>
</Space>
);
</Dropdown>
</Space>
);
return (
<Form.Item
rules={[
{
required: this.props.required,
message: `Please enter a valid ${this.props.label}`,
pattern: new RegExp(/[A-Fa-f0-9]{32}/g),
},
]}
label={this.props.label}
name={this.props.name}
tooltip={this.props.tooltip}
>
<Input hidden />
<Input.Password
id={`${this.props.name}Render`}
onChange={this.onChange}
addonAfter={!this.props.disabled && addon}
style={{ fontFamily: "monospace" }}
value={this.state.value}
disabled={this.props.disabled}
/>
</Form.Item>
);
}
return (
<Form.Item
rules={[
{
required: props.required,
message: `Please enter a valid ${props.label}`,
pattern: new RegExp(/[A-Fa-f0-9]{32}/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 AesKeyInput;

View File

@ -1,10 +1,15 @@
import React, { Component } from "react";
import React, { useState, useEffect } from "react";
import { Select } from "antd";
export type OptionsCallbackFunc = (o: { label: string; value: string }[]) => void;
export type OptionCallbackFunc = (o: { label: string; value: string }) => void;
interface Option {
label: string;
value: string;
}
interface IProps {
placeholder: string;
className: string;
@ -14,93 +19,59 @@ interface IProps {
onSelect?: (s: string) => void;
}
interface IState {
option?: { label: string; value: string };
options: { label: string; value: string }[];
}
function AutoComplete({ placeholder, className, value, getOption, getOptions, onSelect }: IProps) {
const [option, setOption] = useState<Option | undefined>(undefined);
const [options, setOptions] = useState<Option[]>([]);
class Autocomplete extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
options: [],
};
}
componentDidMount() {
if (this.props.value && this.props.value !== "") {
this.props.getOption(this.props.value, (o: { label: string; value: string }) => {
this.setState({
options: [o],
});
useEffect(() => {
if (value && value !== "") {
getOption(value, (o: Option) => {
setOptions([o]);
});
}
}
}, [value, getOption]);
componentDidUpdate(prevProps: IProps) {
if (this.props.value === prevProps.value) {
return;
}
if (this.props.value && this.props.value !== "") {
this.props.getOption(this.props.value, (o: { label: string; value: string }) => {
this.setState({
options: [o],
});
});
}
}
onFocus = () => {
this.props.getOptions("", options => {
if (this.state.option !== undefined) {
const selected = this.state.option.value;
const onFocus = () => {
getOptions("", options => {
if (option !== undefined) {
const selected = option.value;
if (options.find(e => e.value === selected) === undefined) {
options.unshift(this.state.option);
options.unshift(option);
}
}
this.setState({
options: options,
});
setOptions(options);
});
};
onSearch = (value: string) => {
this.props.getOptions(value, options => {
this.setState({
options: options,
});
const onSearch = (value: string) => {
getOptions(value, options => {
setOptions(options);
});
};
onSelect = (value: string, option: any) => {
this.setState({
option: { label: option.label, value: option.value },
});
const onSelectFn = (value: string, option: any) => {
setOption({ label: option.label, value: option.value });
if (this.props.onSelect !== undefined) {
this.props.onSelect(value);
if (onSelect !== undefined) {
onSelect(value);
}
};
render() {
const { getOption, getOptions, ...otherProps } = this.props;
return (
<Select
showSearch
options={this.state.options}
onFocus={this.onFocus}
onSearch={this.onSearch}
onSelect={this.onSelect}
filterOption={false}
{...otherProps}
/>
);
}
return (
<Select
showSearch
options={options}
onFocus={onFocus}
onSearch={onSearch}
onSelect={onSelectFn}
filterOption={false}
placeholder={placeholder}
className={className}
value={value}
/>
);
}
export default Autocomplete;
export default AutoComplete;

View File

@ -1,11 +1,8 @@
import React, { Component } from "react";
import { Form } from "antd";
import Autocomplete, { OptionCallbackFunc, OptionsCallbackFunc } from "./Autocomplete";
interface IProps {
formRef: React.RefObject<any>;
label: string;
name: string;
required?: boolean;
@ -14,28 +11,35 @@ interface IProps {
getOptions: (s: string, fn: OptionsCallbackFunc) => void;
}
class AutocompleteInput extends Component<IProps> {
render() {
return (
<Form.Item
rules={[
{
required: this.props.required,
message: `Please select a ${this.props.label}`,
},
]}
label={this.props.label}
name={this.props.name}
>
<Autocomplete
placeholder={`Select a ${this.props.label}`}
className=""
getOption={this.props.getOption}
getOptions={this.props.getOptions}
/>
</Form.Item>
);
}
function AutocompleteInput(props: IProps) {
const form = Form.useFormInstance();
const onSelect = (value: string) => {
form.setFieldsValue({
[props.name]: value,
});
};
return (
<Form.Item
rules={[
{
required: props.required,
message: `Please select a ${props.label}`,
},
]}
label={props.label}
name={props.name}
>
<Autocomplete
placeholder={`Select a ${props.label}`}
className=""
getOption={props.getOption}
getOptions={props.getOptions}
onSelect={onSelect}
/>
</Form.Item>
);
}
export default AutocompleteInput;

View File

@ -1,4 +1,4 @@
import React, { Component } from "react";
import React, { useState, useEffect } from "react";
import { Controlled as CodeMirror } from "react-codemirror2";
import { Form } from "antd";
@ -6,88 +6,49 @@ import { Form } from "antd";
import "codemirror/mode/javascript/javascript";
interface IProps {
formRef: React.RefObject<any>;
label?: string;
name: string;
required?: boolean;
value?: string;
disabled?: boolean;
tooltip?: string;
}
interface IState {
value: string;
reloadKey: number;
}
function CodeEditor(props: IProps) {
const form = Form.useFormInstance();
const [value, setValue] = useState<string>("");
const [reloadKey, setReloadKey] = useState<number>(1);
class CodeEditor extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
value: "",
reloadKey: 0,
};
}
useEffect(() => {
setValue(form.getFieldValue(props.name));
setReloadKey(k => k + 1);
}, [form, props]);
componentDidMount() {
if (this.props.value) {
this.setState({
value: this.props.value,
});
}
}
componentDidUpdate(oldProps: IProps) {
if (this.props === oldProps) {
return;
}
if (this.props.value) {
this.setState({
value: this.props.value,
reloadKey: this.state.reloadKey + 1,
});
}
}
updateField = () => {
let value = this.state.value;
this.props.formRef.current.setFieldsValue({
[this.props.name]: value,
const handleChange = (editor: any, data: any, newCode: string) => {
setValue(newCode);
form.setFieldsValue({
[props.name]: newCode,
});
};
handleChange = (editor: any, data: any, newCode: string) => {
this.setState(
{
value: newCode,
},
this.updateField,
);
const codeMirrorOptions = {
lineNumbers: true,
mode: "javascript",
theme: "base16-light",
readOnly: props.disabled,
};
render() {
const codeMirrorOptions = {
lineNumbers: true,
mode: "javascript",
theme: "base16-light",
readOnly: this.props.disabled,
};
return (
<Form.Item label={this.props.label} name={this.props.name} tooltip={this.props.tooltip}>
<div style={{ border: "1px solid #cccccc" }}>
<CodeMirror
key={`code-editor-refresh-${this.state.reloadKey}`}
value={this.state.value}
options={codeMirrorOptions}
onBeforeChange={this.handleChange}
/>
</div>
</Form.Item>
);
}
return (
<Form.Item label={props.label} name={props.name} tooltip={props.tooltip}>
<div style={{ border: "1px solid #cccccc" }}>
<CodeMirror
key={`code-editor-refresh-${reloadKey}`}
value={value}
options={codeMirrorOptions}
onBeforeChange={handleChange}
/>
</div>
</Form.Item>
);
}
export default CodeEditor;

View File

@ -1,4 +1,4 @@
import React, { Component } from "react";
import React, { useState, useEffect } from "react";
import { Table } from "antd";
import { ColumnsType } from "antd/es/table";
@ -16,113 +16,81 @@ interface IProps {
noPagination?: boolean;
}
interface IState {
totalCount: number;
pageSize: number;
currentPage: number;
rows: object[];
loading: boolean;
}
function DataTable(props: IProps) {
const [totalCount, setTotalCount] = useState<number>(0);
const [pageSize, setPageSize] = useState<number>(SessionStore.getRowsPerPage());
const [currentPage, setCurrentPage] = useState<number>(1);
const [rows, setRows] = useState<object[]>([]);
const [loading, setLoading] = useState<boolean>(true);
class DataTable extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
const onChangePage = (page: number, pz?: number | void) => {
setLoading(true);
this.state = {
totalCount: 0,
pageSize: SessionStore.getRowsPerPage(),
currentPage: 1,
rows: [],
loading: true,
};
}
componentDidMount() {
this.onChangePage(this.state.currentPage, this.state.pageSize);
}
componentDidUpdate(prevProps: IProps) {
if (this.props === prevProps) {
return;
if (!pz) {
pz = pageSize;
}
this.onChangePage(this.state.currentPage, this.state.pageSize);
}
onChangePage = (page: number, pageSize?: number | void) => {
this.setState(
{
loading: true,
},
() => {
let pz = pageSize;
if (!pz) {
pz = this.state.pageSize;
}
this.props.getPage(pz, (page - 1) * pz, (totalCount: number, rows: object[]) => {
this.setState({
currentPage: page,
totalCount: totalCount,
rows: rows,
pageSize: pz || 0,
loading: false,
});
});
},
);
props.getPage(pz, (page - 1) * pz, (totalCount: number, rows: object[]) => {
setCurrentPage(page);
setTotalCount(totalCount);
setRows(rows);
setPageSize(pz || 0);
setLoading(false);
});
};
onShowSizeChange = (page: number, pageSize: number) => {
this.onChangePage(page, pageSize);
const onShowSizeChange = (page: number, pageSize: number) => {
onChangePage(page, pageSize);
SessionStore.setRowsPerPage(pageSize);
};
onRowsSelectChange = (ids: React.Key[]) => {
const onRowsSelectChange = (ids: React.Key[]) => {
const idss = ids as string[];
if (this.props.onRowsSelectChange) {
this.props.onRowsSelectChange(idss);
if (props.onRowsSelectChange) {
props.onRowsSelectChange(idss);
}
};
render() {
const { getPage, refreshKey, ...otherProps } = this.props;
let loadingProps = undefined;
if (this.state.loading) {
loadingProps = {
delay: 300,
};
}
useEffect(() => {
onChangePage(currentPage, pageSize);
}, [props, currentPage, pageSize]);
let pagination = undefined;
if (this.props.noPagination === undefined || this.props.noPagination === false) {
pagination = {
current: this.state.currentPage,
total: this.state.totalCount,
pageSize: this.state.pageSize,
onChange: this.onChangePage,
showSizeChanger: true,
onShowSizeChange: this.onShowSizeChange,
};
}
let rowSelection = undefined;
if (this.props.onRowsSelectChange) {
rowSelection = {
onChange: this.onRowsSelectChange,
};
}
return (
<Table
loading={loadingProps}
dataSource={this.state.rows}
pagination={pagination || false}
rowSelection={rowSelection}
{...otherProps}
/>
);
const { getPage, refreshKey, ...otherProps } = props;
let loadingProps = undefined;
if (loading) {
loadingProps = {
delay: 300,
};
}
let pagination = undefined;
if (props.noPagination === undefined || props.noPagination === false) {
pagination = {
current: currentPage,
total: totalCount,
pageSize: pageSize,
onChange: onChangePage,
showSizeChanger: true,
onShowSizeChange: onShowSizeChange,
};
}
let rowSelection = undefined;
if (props.onRowsSelectChange) {
rowSelection = {
onChange: onRowsSelectChange,
};
}
return (
<Table
loading={loadingProps}
dataSource={rows}
pagination={pagination || false}
rowSelection={rowSelection}
{...otherProps}
/>
);
}
export default DataTable;

View File

@ -1,5 +1,4 @@
import React, { Component } from "react";
import { useState, PropsWithChildren } from "react";
import { Popover, Button, Typography, Space, Input } from "antd";
interface IProps {
@ -8,51 +7,32 @@ interface IProps {
onConfirm: () => void;
}
interface ConfirmState {
confirm: string;
}
function DeleteConfirmContent(props: IProps) {
const [confirm, setConfirm] = useState<string>("");
class DeleteConfirmContent extends Component<IProps, ConfirmState> {
constructor(props: IProps) {
super(props);
this.state = {
confirm: "",
};
}
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
confirm: e.target.value,
});
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setConfirm(e.target.value);
};
render() {
return (
<Space direction="vertical">
<Typography.Text>
Enter '{this.props.confirm}' to confirm you want to delete this {this.props.typ}:
</Typography.Text>
<Input placeholder={this.props.confirm} onChange={this.onChange} />
<Button
onClick={this.props.onConfirm}
disabled={this.state.confirm !== this.props.confirm}
style={{ float: "right" }}
>
Delete
</Button>
</Space>
);
}
return (
<Space direction="vertical">
<Typography.Text>
Enter '{props.confirm}' to confirm you want to delete this {props.typ}:
</Typography.Text>
<Input placeholder={props.confirm} onChange={onChange} />
<Button onClick={props.onConfirm} disabled={confirm !== props.confirm} style={{ float: "right" }}>
Delete
</Button>
</Space>
);
}
class DeleteConfirm extends Component<IProps> {
render() {
return (
<Popover content={<DeleteConfirmContent {...this.props} />} trigger="click" placement="left">
{this.props.children}
</Popover>
);
}
function DeleteConfirm(props: PropsWithChildren<IProps>) {
return (
<Popover content={<DeleteConfirmContent {...props} />} trigger="click" placement="left">
{props.children}
</Popover>
);
}
export default DeleteConfirm;

View File

@ -1,4 +1,4 @@
import React, { Component } from "react";
import React, { useState, useEffect } from "react";
import { notification, Input, Select, Button, Space, Form, Dropdown, Menu } from "antd";
import { ReloadOutlined, CopyOutlined } from "@ant-design/icons";
@ -8,7 +8,6 @@ import { GetRandomDevAddrRequest, GetRandomDevAddrResponse } from "@chirpstack/c
import DeviceStore from "../stores/DeviceStore";
interface IProps {
formRef: React.RefObject<any>;
label: string;
name: string;
devEui: string;
@ -17,42 +16,29 @@ interface IProps {
disabled?: boolean;
}
interface IState {
byteOrder: string;
value: string;
}
function DevAddrInput(props: IProps) {
const form = Form.useFormInstance();
const [byteOrder, setByteOrder] = useState<string>("msb");
const [value, setValue] = useState<string>("");
class DevAddrInput extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
byteOrder: "msb",
value: "",
};
}
useEffect(() => {
if (props.value) {
setValue(props.value);
}
}, [props]);
updateField = () => {
let value = this.state.value;
if (this.state.byteOrder === "lsb") {
const bytes = value.match(/[A-Fa-f0-9]{2}/g) || [];
value = bytes.reverse().join("");
const updateField = (v: string) => {
if (byteOrder === "lsb") {
const bytes = v.match(/[A-Fa-f0-9]{2}/g) || [];
v = bytes.reverse().join("");
}
this.props.formRef.current.setFieldsValue({
[this.props.name]: value,
form.setFieldsValue({
[props.name]: v,
});
};
componentDidMount() {
if (this.props.value) {
this.setState({
value: this.props.value,
});
}
}
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let v = e.target.value;
const match = v.match(/[A-Fa-f0-9]/g);
@ -65,50 +51,37 @@ class DevAddrInput extends Component<IProps, IState> {
}
}
this.setState(
{
value: value,
},
this.updateField,
);
setValue(value);
updateField(value);
};
onByteOrderSelect = (v: string) => {
if (v === this.state.byteOrder) {
const onByteOrderSelect = (v: string) => {
if (v === byteOrder) {
return;
}
this.setState({
byteOrder: v,
});
setByteOrder(v);
const current = this.state.value;
const current = value;
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
const vv = bytes.reverse().join("");
this.setState(
{
value: bytes.reverse().join(""),
},
this.updateField,
);
setValue(vv);
updateField(vv);
};
generateRandom = () => {
const generateRandom = () => {
let req = new GetRandomDevAddrRequest();
req.setDevEui(this.props.devEui);
req.setDevEui(props.devEui);
DeviceStore.getRandomDevAddr(req, (resp: GetRandomDevAddrResponse) => {
this.setState(
{
value: resp.getDevAddr(),
},
this.updateField,
);
setValue(resp.getDevAddr());
updateField(resp.getDevAddr());
});
};
copyToClipboard = () => {
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
const copyToClipboard = () => {
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
if (bytes !== null && navigator.clipboard !== undefined) {
navigator.clipboard
@ -129,8 +102,8 @@ class DevAddrInput extends Component<IProps, IState> {
}
};
copyToClipboardHexArray = () => {
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
const copyToClipboardHexArray = () => {
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
if (bytes !== null && navigator.clipboard !== undefined) {
navigator.clipboard
@ -156,71 +129,69 @@ class DevAddrInput extends Component<IProps, IState> {
}
};
render() {
const copyMenu = (
<Menu
items={[
{
key: "1",
label: (
<Button type="text" onClick={this.copyToClipboard}>
HEX string
</Button>
),
},
{
key: "2",
label: (
<Button type="text" onClick={this.copyToClipboardHexArray}>
HEX array
</Button>
),
},
]}
/>
);
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={this.state.byteOrder} onChange={this.onByteOrderSelect}>
<Select.Option value="msb">MSB</Select.Option>
<Select.Option value="lsb">LSB</Select.Option>
</Select>
<Button type="text" size="small" shape="circle" onClick={this.generateRandom}>
<ReloadOutlined />
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 overlay={copyMenu}>
<Button type="text" size="small">
<CopyOutlined />
</Button>
</Dropdown>
</Space>
);
</Dropdown>
</Space>
);
return (
<Form.Item
rules={[
{
required: this.props.required,
message: `Please enter a valid ${this.props.label}`,
pattern: new RegExp(/[A-Fa-f0-9]{8}/g),
},
]}
label={this.props.label}
name={this.props.name}
>
<Input hidden />
<Input
id={`${this.props.name}Render`}
onChange={this.onChange}
addonAfter={!this.props.disabled && addon}
style={{ fontFamily: "monospace" }}
value={this.state.value}
disabled={this.props.disabled}
/>
</Form.Item>
);
}
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}
>
<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 DevAddrInput;

View File

@ -1,11 +1,10 @@
import React, { Component } from "react";
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 {
formRef: React.RefObject<any>;
label: string;
name: string;
required?: boolean;
@ -14,42 +13,29 @@ interface IProps {
tooltip?: string;
}
interface IState {
byteOrder: string;
value: string;
}
function EuiInput(props: IProps) {
const form = Form.useFormInstance();
const [byteOrder, setByteOrder] = useState<string>("msb");
const [value, setValue] = useState<string>("");
class EuiInput extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
byteOrder: "msb",
value: "",
};
}
useEffect(() => {
if (props.value) {
setValue(props.value);
}
}, [props]);
updateField = () => {
let value = this.state.value;
if (this.state.byteOrder === "lsb") {
const bytes = value.match(/[A-Fa-f0-9]{2}/g) || [];
value = bytes.reverse().join("");
const updateField = (v: string) => {
if (byteOrder === "lsb") {
const bytes = v.match(/[A-Fa-f0-9]{2}/g) || [];
v = bytes.reverse().join("");
}
this.props.formRef.current.setFieldsValue({
[this.props.name]: value,
form.setFieldsValue({
[props.name]: v,
});
};
componentDidMount() {
if (this.props.value) {
this.setState({
value: this.props.value,
});
}
}
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let v = e.target.value;
const match = v.match(/[A-Fa-f0-9]/g);
@ -62,50 +48,37 @@ class EuiInput extends Component<IProps, IState> {
}
}
this.setState(
{
value: value,
},
this.updateField,
);
setValue(value);
updateField(value);
};
onByteOrderSelect = (v: string) => {
if (v === this.state.byteOrder) {
const onByteOrderSelect = (v: string) => {
if (v === byteOrder) {
return;
}
this.setState({
byteOrder: v,
});
setByteOrder(v);
const current = this.state.value;
const current = value;
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
const vv = bytes.reverse().join("");
this.setState(
{
value: bytes.reverse().join(""),
},
this.updateField,
);
setValue(vv);
updateField(vv);
};
generateRandom = () => {
const generateRandom = () => {
let cryptoObj = window.crypto || window.Crypto;
let b = new Uint8Array(8);
cryptoObj.getRandomValues(b);
let key = Buffer.from(b).toString("hex");
this.setState(
{
value: key,
},
this.updateField,
);
setValue(key);
updateField(key);
};
copyToClipboard = () => {
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
const copyToClipboard = () => {
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
if (bytes !== null && navigator.clipboard !== undefined) {
navigator.clipboard
@ -126,8 +99,8 @@ class EuiInput extends Component<IProps, IState> {
}
};
copyToClipboardHexArray = () => {
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
const copyToClipboardHexArray = () => {
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
if (bytes !== null && navigator.clipboard !== undefined) {
navigator.clipboard
@ -153,72 +126,70 @@ class EuiInput extends Component<IProps, IState> {
}
};
render() {
const copyMenu = (
<Menu
items={[
{
key: "1",
label: (
<Button type="text" onClick={this.copyToClipboard}>
HEX string
</Button>
),
},
{
key: "2",
label: (
<Button type="text" onClick={this.copyToClipboardHexArray}>
HEX array
</Button>
),
},
]}
/>
);
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={this.state.byteOrder} onChange={this.onByteOrderSelect}>
<Select.Option value="msb">MSB</Select.Option>
<Select.Option value="lsb">LSB</Select.Option>
</Select>
<Button type="text" size="small" onClick={this.generateRandom}>
<ReloadOutlined />
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 overlay={copyMenu}>
<Button type="text" size="small">
<CopyOutlined />
</Button>
</Dropdown>
</Space>
);
</Dropdown>
</Space>
);
return (
<Form.Item
rules={[
{
required: this.props.required,
message: `Please enter a valid ${this.props.label}`,
pattern: new RegExp(/[A-Fa-f0-9]{16}/g),
},
]}
label={this.props.label}
name={this.props.name}
tooltip={this.props.tooltip}
>
<Input hidden />
<Input
id={`${this.props.name}Render`}
onChange={this.onChange}
addonAfter={!this.props.disabled && addon}
style={{ fontFamily: "monospace" }}
value={this.state.value}
disabled={this.props.disabled}
/>
</Form.Item>
);
}
return (
<Form.Item
rules={[
{
required: props.required,
message: `Please enter a valid ${props.label}`,
pattern: new RegExp(/[A-Fa-f0-9]{16}/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 EuiInput;

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { Link, withRouter, RouteComponentProps } from "react-router-dom";
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Button, Menu, Dropdown, Input, AutoComplete } from "antd";
import { UserOutlined, DownOutlined, QuestionOutlined } from "@ant-design/icons";
@ -14,15 +14,6 @@ import {
import InternalStore from "../stores/InternalStore";
import SessionStore from "../stores/SessionStore";
interface IProps extends RouteComponentProps {
user: User;
}
interface IState {
searchResult?: GlobalSearchResponse;
settings?: SettingsResponse;
}
const renderTitle = (title: string) => <span>{title}</span>;
const renderItem = (title: string, url: string) => ({
@ -30,22 +21,19 @@ const renderItem = (title: string, url: string) => ({
label: <Link to={url}>{title}</Link>,
});
class Header extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
function Header({ user }: { user: User }) {
const navigate = useNavigate();
this.state = {};
}
const [settings, setSettings] = useState<SettingsResponse | undefined>(undefined);
const [searchResult, setSearchResult] = useState<GlobalSearchResponse | undefined>(undefined);
componentDidMount() {
useEffect(() => {
InternalStore.settings((resp: SettingsResponse) => {
this.setState({
settings: resp,
});
setSettings(resp);
});
}
}, [user]);
onSearch = (search: string) => {
const onSearch = (search: string) => {
if (search.length < 3) {
return;
}
@ -55,14 +43,11 @@ class Header extends Component<IProps, IState> {
req.setSearch(search);
InternalStore.globalSearch(req, (resp: GlobalSearchResponse) => {
this.setState({
searchResult: resp,
});
setSearchResult(resp);
});
};
onLogout = () => {
let settings = this.state.settings;
const onLogout = () => {
if (settings === undefined) {
return;
}
@ -71,117 +56,112 @@ class Header extends Component<IProps, IState> {
if (!oidc.getEnabled() || oidc.getLogoutUrl() === "") {
SessionStore.logout(true, () => {
this.props.history.push("/login");
navigate("/login");
});
} else {
SessionStore.logout(false, () => {
window.location.assign(oidc.getLogoutUrl());
navigate(oidc.getLogoutUrl());
});
}
};
render() {
if (this.state.settings === undefined) {
return null;
}
if (settings === undefined) {
return null;
}
let oidcEnabled = this.state.settings!.getOpenidConnect()!.getEnabled();
let oidcEnabled = settings!.getOpenidConnect()!.getEnabled();
const menu = (
<Menu>
{!oidcEnabled && (
<Menu.Item>
<Link to={`/users/${this.props.user.getId()}/password`}>Change password</Link>
</Menu.Item>
)}
<Menu.Item onClick={this.onLogout}>Logout</Menu.Item>
</Menu>
);
const menu = (
<Menu>
{!oidcEnabled && (
<Menu.Item>
<Link to={`/users/${user.getId()}/password`}>Change password</Link>
</Menu.Item>
)}
<Menu.Item onClick={onLogout}>Logout</Menu.Item>
</Menu>
);
let options: {
label: any;
options: any[];
}[] = [
{
label: renderTitle("Tenants"),
options: [],
},
{
label: renderTitle("Gateways"),
options: [],
},
{
label: renderTitle("Applications"),
options: [],
},
{
label: renderTitle("Devices"),
options: [],
},
];
let options: {
label: any;
options: any[];
}[] = [
{
label: renderTitle("Tenants"),
options: [],
},
{
label: renderTitle("Gateways"),
options: [],
},
{
label: renderTitle("Applications"),
options: [],
},
{
label: renderTitle("Devices"),
options: [],
},
];
if (this.state.searchResult !== undefined) {
for (const res of this.state.searchResult.getResultList()) {
if (res.getKind() === "tenant") {
options[0].options.push(renderItem(res.getTenantName(), `/tenants/${res.getTenantId()}`));
}
if (searchResult !== undefined) {
for (const res of searchResult.getResultList()) {
if (res.getKind() === "tenant") {
options[0].options.push(renderItem(res.getTenantName(), `/tenants/${res.getTenantId()}`));
}
if (res.getKind() === "gateway") {
options[1].options.push(
renderItem(res.getGatewayName(), `/tenants/${res.getTenantId()}/gateways/${res.getGatewayId()}`),
);
}
if (res.getKind() === "gateway") {
options[1].options.push(
renderItem(res.getGatewayName(), `/tenants/${res.getTenantId()}/gateways/${res.getGatewayId()}`),
);
}
if (res.getKind() === "application") {
options[2].options.push(
renderItem(
res.getApplicationName(),
`/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}`,
),
);
}
if (res.getKind() === "application") {
options[2].options.push(
renderItem(res.getApplicationName(), `/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}`),
);
}
if (res.getKind() === "device") {
options[3].options.push(
renderItem(
res.getDeviceName(),
`/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}/devices/${res.getDeviceDevEui()}`,
),
);
}
if (res.getKind() === "device") {
options[3].options.push(
renderItem(
res.getDeviceName(),
`/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}/devices/${res.getDeviceDevEui()}`,
),
);
}
}
}
return (
<div>
<img className="logo" alt="ChirpStack" src="/logo.png" />
<div className="actions">
<div className="search">
<AutoComplete
dropdownClassName="search-dropdown"
dropdownMatchSelectWidth={500}
options={options}
onSearch={this.onSearch}
>
<Input.Search placeholder="Search..." style={{ width: 500, marginTop: -5 }} />
</AutoComplete>
</div>
<div className="help">
<a href="https://www.chirpstack.io" target="_blank" rel="noreferrer">
<Button icon={<QuestionOutlined />} />
</a>
</div>
<div className="user">
<Dropdown overlay={menu} placement="bottomRight" trigger={["click"]}>
<Button type="primary" icon={<UserOutlined />}>
{this.props.user.getEmail()} <DownOutlined />
</Button>
</Dropdown>
</div>
return (
<div>
<img className="logo" alt="ChirpStack" src="/logo.png" />
<div className="actions">
<div className="search">
<AutoComplete
dropdownClassName="search-dropdown"
dropdownMatchSelectWidth={500}
options={options}
onSearch={onSearch}
>
<Input.Search placeholder="Search..." style={{ width: 500, marginTop: -5 }} />
</AutoComplete>
</div>
<div className="help">
<a href="https://www.chirpstack.io" target="_blank" rel="noreferrer">
<Button icon={<QuestionOutlined />} />
</a>
</div>
<div className="user">
<Dropdown overlay={menu} placement="bottomRight" trigger={["click"]}>
<Button type="primary" icon={<UserOutlined />}>
{user.getEmail()} <DownOutlined />
</Button>
</Dropdown>
</div>
</div>
);
}
</div>
);
}
export default withRouter(Header);
export default Header;

View File

@ -1,4 +1,5 @@
import React, { Component } from "react";
import React, { useState } from "react";
import moment from "moment";
import JSONTreeOriginal from "react-json-tree";
import fileDownload from "js-file-download";
@ -12,160 +13,142 @@ interface IProps {
logs: LogItem[];
}
interface IState {
drawerOpen: boolean;
body: any;
drawerTitle: any;
}
function LogTable(props: IProps) {
const [drawerOpen, setDrawerOpen] = useState<boolean>(false);
const [body, setBody] = useState<any>(null);
const [drawerTitle, setDrawerTitle] = useState<any>(null);
class LogTable extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
drawerOpen: false,
body: null,
drawerTitle: null,
};
}
onDrawerClose = () => {
this.setState({
drawerOpen: false,
});
const onDrawerClose = () => {
setDrawerOpen(false);
};
onDrawerOpen = (time: any, body: any) => {
const onDrawerOpen = (time: any, body: any) => {
let ts = new Date(0);
ts.setUTCSeconds(time.seconds);
let drawerTitle = moment(ts).format("YYYY-MM-DD HH:mm:ss");
return () => {
this.setState({
body: body,
drawerTitle: drawerTitle,
drawerOpen: true,
});
setBody(body);
setDrawerTitle(drawerTitle);
setDrawerOpen(true);
};
};
downloadSingleFrame = () => {
fileDownload(JSON.stringify(JSON.parse(this.state.body), null, 4), "single-log.json", "application/json");
const downloadSingleFrame = () => {
fileDownload(JSON.stringify(JSON.parse(body), null, 4), "single-log.json", "application/json");
};
downloadFrames = () => {
let items = this.props.logs.map((l, i) => JSON.parse(l.getBody()));
const downloadFrames = () => {
let items = props.logs.map((l, i) => JSON.parse(l.getBody()));
fileDownload(JSON.stringify(items, null, 4), "log.json");
};
render() {
let items = this.props.logs.map((l, i) => l.toObject());
let body = JSON.parse(this.state.body);
let items = props.logs.map((l, i) => l.toObject());
let bodyJson = JSON.parse(body);
const theme = {
scheme: "google",
author: "seth wright (http://sethawright.com)",
base00: "#000000",
base01: "#282a2e",
base02: "#373b41",
base03: "#969896",
base04: "#b4b7b4",
base05: "#c5c8c6",
base06: "#e0e0e0",
base07: "#ffffff",
base08: "#CC342B",
base09: "#F96A38",
base0A: "#FBA922",
base0B: "#198844",
base0C: "#3971ED",
base0D: "#3971ED",
base0E: "#A36AC7",
base0F: "#3971ED",
};
const theme = {
scheme: "google",
author: "seth wright (http://sethawright.com)",
base00: "#000000",
base01: "#282a2e",
base02: "#373b41",
base03: "#969896",
base04: "#b4b7b4",
base05: "#c5c8c6",
base06: "#e0e0e0",
base07: "#ffffff",
base08: "#CC342B",
base09: "#F96A38",
base0A: "#FBA922",
base0B: "#198844",
base0C: "#3971ED",
base0D: "#3971ED",
base0E: "#A36AC7",
base0F: "#3971ED",
};
return (
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<Drawer
title={`Details: ${this.state.drawerTitle}`}
placement="right"
width={650}
onClose={this.onDrawerClose}
visible={this.state.drawerOpen}
extra={<Button onClick={this.downloadSingleFrame}>Download</Button>}
>
<JSONTreeOriginal
data={body}
theme={theme}
hideRoot={true}
shouldExpandNode={() => {
return true;
}}
/>
</Drawer>
{items.length !== 0 && (
<Space direction="horizontal" style={{ float: "right" }} size="large">
<Spin size="small" />
<Button onClick={this.downloadFrames}>Download</Button>
</Space>
)}
<Table
showHeader={false}
loading={items.length === 0}
dataSource={items}
pagination={false}
columns={[
{
title: "Time",
dataIndex: "time",
key: "time",
width: 200,
render: (text, obj) => {
let ts = new Date(0);
ts.setUTCSeconds(obj.time!.seconds);
return moment(ts).format("YYYY-MM-DD HH:mm:ss");
},
},
{
title: "Type",
dataIndex: "description",
key: "description",
width: 200,
render: (text, obj) => (
<Button
icon={<ZoomInOutlined />}
type="primary"
shape="round"
size="small"
onClick={this.onDrawerOpen(obj.time, obj.body)}
>
{text}
</Button>
),
},
{
title: "Properties",
dataIndex: "properties",
key: "properties",
render: (text, obj) =>
obj.propertiesMap.map((p, i) => {
if (p[1] !== "") {
return (
<Tag>
<pre>
{p[0]}: {p[1]}
</pre>
</Tag>
);
}
return null;
}),
},
]}
return (
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<Drawer
title={`Details: ${drawerTitle}`}
placement="right"
width={650}
onClose={onDrawerClose}
visible={drawerOpen}
extra={<Button onClick={downloadSingleFrame}>Download</Button>}
>
<JSONTreeOriginal
data={bodyJson}
theme={theme}
hideRoot={true}
shouldExpandNode={() => {
return true;
}}
/>
</Space>
);
}
</Drawer>
{items.length !== 0 && (
<Space direction="horizontal" style={{ float: "right" }} size="large">
<Spin size="small" />
<Button onClick={downloadFrames}>Download</Button>
</Space>
)}
<Table
showHeader={false}
loading={items.length === 0}
dataSource={items}
pagination={false}
columns={[
{
title: "Time",
dataIndex: "time",
key: "time",
width: 200,
render: (text, obj) => {
let ts = new Date(0);
ts.setUTCSeconds(obj.time!.seconds);
return moment(ts).format("YYYY-MM-DD HH:mm:ss");
},
},
{
title: "Type",
dataIndex: "description",
key: "description",
width: 200,
render: (text, obj) => (
<Button
icon={<ZoomInOutlined />}
type="primary"
shape="round"
size="small"
onClick={onDrawerOpen(obj.time, obj.body)}
>
{text}
</Button>
),
},
{
title: "Properties",
dataIndex: "properties",
key: "properties",
render: (text, obj) =>
obj.propertiesMap.map((p, i) => {
if (p[1] !== "") {
return (
<Tag>
<pre>
{p[0]}: {p[1]}
</pre>
</Tag>
);
}
return null;
}),
},
]}
/>
</Space>
);
}
export default LogTable;

View File

@ -1,8 +1,8 @@
import React, { Component } from "react";
import React, { useEffect, PropsWithChildren } from "react";
import L, { LatLngTuple, FitBoundsOptions } from "leaflet";
import "leaflet.awesome-markers";
import { MarkerProps as LMarkerProps } from "react-leaflet";
import { MarkerProps as LMarkerProps, useMap } from "react-leaflet";
import { MapContainer, Marker as LMarker, TileLayer } from "react-leaflet";
interface IProps {
@ -12,77 +12,48 @@ interface IProps {
boundsOptions?: FitBoundsOptions;
}
interface IState {
map?: L.Map;
}
function MapControl(props: { center?: [number, number]; bounds?: LatLngTuple[]; boundsOptions?: FitBoundsOptions }) {
const map = useMap();
class Map extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
setMap = (map: L.Map) => {
this.setState(
{
map: map,
},
() => {
// This is needed as setMap is called after the map has been created.
// There is a small amount of time where componentDidUpdate can't update
// the map with the new center because setMap hasn't been called yet.
// In such case, the map would never update to the new center.
if (this.props.center !== undefined) {
map.panTo(this.props.center);
}
if (this.props.bounds !== undefined) {
map.fitBounds(this.props.bounds, this.props.boundsOptions);
}
},
);
};
componentDidUpdate(oldProps: IProps) {
if (this.props === oldProps) {
useEffect(() => {
if (map === undefined) {
return;
}
if (this.state.map) {
if (this.props.center !== undefined) {
this.state.map.flyTo(this.props.center);
}
if (this.props.bounds !== undefined) {
this.state.map.flyToBounds(this.props.bounds, this.props.boundsOptions);
}
if (props.center !== undefined) {
map.flyTo(props.center);
}
}
render() {
const style = {
height: this.props.height,
};
if (props.bounds !== undefined) {
map.flyToBounds(props.bounds, props.boundsOptions);
}
});
return (
<MapContainer
bounds={this.props.bounds}
boundsOptions={this.props.boundsOptions}
center={this.props.center}
zoom={13}
scrollWheelZoom={false}
animate={true}
style={style}
whenCreated={this.setMap}
>
<TileLayer
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{this.props.children}
</MapContainer>
);
}
return null;
}
function Map(props: PropsWithChildren<IProps>) {
const style = {
height: props.height,
};
return (
<MapContainer
bounds={props.bounds}
boundsOptions={props.boundsOptions}
center={props.center}
zoom={13}
scrollWheelZoom={false}
style={style}
>
<TileLayer
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{props.children}
<MapControl bounds={props.bounds} boundsOptions={props.boundsOptions} center={props.center} />
</MapContainer>
);
}
export type MarkerColor =
@ -103,22 +74,20 @@ interface MarkerProps extends LMarkerProps {
color: MarkerColor;
}
export class Marker extends Component<MarkerProps> {
render() {
const { faIcon, color, position, ...otherProps } = this.props;
export function Marker(props: MarkerProps) {
const { faIcon, color, position, ...otherProps } = props;
const iconMarker = L.AwesomeMarkers.icon({
icon: faIcon,
prefix: "fa",
markerColor: color,
});
const iconMarker = L.AwesomeMarkers.icon({
icon: faIcon,
prefix: "fa",
markerColor: color,
});
return (
<LMarker icon={iconMarker} position={position} {...otherProps}>
{this.props.children}
</LMarker>
);
}
return (
<LMarker icon={iconMarker} position={position} {...otherProps}>
{props.children}
</LMarker>
);
}
export default Map;

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { withRouter, RouteComponentProps, Link } from "react-router-dom";
import React, { useState, useEffect } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { Menu, MenuProps } from "antd";
import {
@ -24,46 +24,18 @@ import Autocomplete, { OptionCallbackFunc, OptionsCallbackFunc } from "../compon
import TenantStore from "../stores/TenantStore";
import SessionStore from "../stores/SessionStore";
interface IState {
tenantId: string;
selectedKey: string;
}
function SideMenu() {
const [tenantId, setTenantId] = useState<string>("");
const [selectedKey, setSelectedKey] = useState<string>("");
class SideMenu extends Component<RouteComponentProps, IState> {
constructor(props: RouteComponentProps) {
super(props);
const location = useLocation();
const navigate = useNavigate();
this.state = {
tenantId: "",
selectedKey: "ns-dashboard",
};
}
componentDidMount() {
SessionStore.on("tenant.change", this.setTenant);
this.setTenant();
this.parseLocation();
}
componentWillUnmount() {
SessionStore.removeListener("tenant.change", this.setTenant);
}
componentDidUpdate(prevProps: RouteComponentProps) {
if (this.props === prevProps) {
return;
}
this.parseLocation();
}
setTenant = () => {
this.setState({
tenantId: SessionStore.getTenantId(),
});
const setTenant = () => {
setTenantId(SessionStore.getTenantId());
};
getTenantOptions = (search: string, fn: OptionsCallbackFunc) => {
const getTenantOptions = (search: string, fn: OptionsCallbackFunc) => {
let req = new ListTenantsRequest();
req.setSearch(search);
req.setLimit(10);
@ -76,7 +48,7 @@ class SideMenu extends Component<RouteComponentProps, IState> {
});
};
getTenantOption = (id: string, fn: OptionCallbackFunc) => {
const getTenantOption = (id: string, fn: OptionCallbackFunc) => {
TenantStore.get(id, (resp: GetTenantResponse) => {
const tenant = resp.getTenant();
if (tenant) {
@ -85,167 +57,208 @@ class SideMenu extends Component<RouteComponentProps, IState> {
});
};
onTenantSelect = (value: string) => {
const onTenantSelect = (value: string) => {
SessionStore.setTenantId(value);
this.props.history.push(`/tenants/${value}`);
navigate(`/tenants/${value}`);
};
parseLocation = () => {
const path = this.props.history.location.pathname;
const parseLocation = () => {
const path = location.pathname;
const tenantRe = /\/tenants\/([\w-]{36})/g;
const match = tenantRe.exec(path);
if (match !== null && this.state.tenantId !== match[1]) {
if (match !== null && tenantId !== match[1]) {
SessionStore.setTenantId(match[1]);
}
// ns dashboard
if (path === "/dashboard") {
this.setState({ selectedKey: "ns-dashboard" });
setSelectedKey("ns-dashboard");
}
// ns tenants
if (/\/tenants(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
this.setState({ selectedKey: "ns-tenants" });
setSelectedKey("ns-tenants");
}
// ns tenants
if (/\/users(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
this.setState({ selectedKey: "ns-users" });
setSelectedKey("ns-users");
}
// ns api keys
if (/\/api-keys(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
this.setState({ selectedKey: "ns-api-keys" });
setSelectedKey("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" });
setSelectedKey("ns-device-profile-templates");
}
if (/\/regions.*/g.exec(path)) {
this.setState({ selectedKey: "ns-regions" });
setSelectedKey("ns-regions");
}
// tenant dashboard
if (/\/tenants\/[\w-]{36}/g.exec(path)) {
this.setState({ selectedKey: "tenant-dashboard" });
setSelectedKey("tenant-dashboard");
}
// tenant users
if (/\/tenants\/[\w-]{36}\/users.*/g.exec(path)) {
this.setState({ selectedKey: "tenant-users" });
setSelectedKey("tenant-users");
}
// tenant api-keys
if (/\/tenants\/[\w-]{36}\/api-keys.*/g.exec(path)) {
this.setState({ selectedKey: "tenant-api-keys" });
setSelectedKey("tenant-api-keys");
}
// tenant device-profiles
if (/\/tenants\/[\w-]{36}\/device-profiles.*/g.exec(path)) {
this.setState({ selectedKey: "tenant-device-profiles" });
setSelectedKey("tenant-device-profiles");
}
// tenant gateways
if (/\/tenants\/[\w-]{36}\/gateways.*/g.exec(path)) {
this.setState({ selectedKey: "tenant-gateways" });
setSelectedKey("tenant-gateways");
}
// tenant applications
if (/\/tenants\/[\w-]{36}\/applications.*/g.exec(path)) {
this.setState({ selectedKey: "tenant-applications" });
setSelectedKey("tenant-applications");
}
};
render() {
const tenantId = this.state.tenantId;
let items: MenuProps["items"] = [];
useEffect(() => {
SessionStore.on("tenant.change", setTenant);
setTenant();
parseLocation();
if (SessionStore.isAdmin()) {
items.push({
key: "ns",
label: "Network Server",
icon: <CloudOutlined />,
children: [
{ key: "ns-dashboard", icon: <DashboardOutlined />, label: <Link to="/dashboard">Dashboard</Link> },
{ 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>,
},
{ key: "ns-regions", icon: <CompassOutlined />, label: <Link to="/regions">Regions</Link> },
],
});
} else {
items.push({
key: "ns",
label: "Network Server",
icon: <CloudOutlined />,
children: [{ key: "ns-regions", icon: <CompassOutlined />, label: <Link to="/regions">Regions</Link> }],
});
}
return () => {
SessionStore.removeListener("tenant.change", setTenant);
};
}, []);
if (tenantId !== "") {
items.push({
key: "tenant",
label: "Tenant",
icon: <HomeOutlined />,
children: [
{
key: "tenant-dashboard",
icon: <DashboardOutlined />,
label: <Link to={`/tenants/${tenantId}`}>Dashboard</Link>,
},
{ key: "tenant-users", icon: <UserOutlined />, label: <Link to={`/tenants/${tenantId}/users`}>Users</Link> },
{
key: "tenant-api-keys",
icon: <KeyOutlined />,
label: <Link to={`/tenants/${tenantId}/api-keys`}>API Keys</Link>,
},
{
key: "tenant-device-profiles",
icon: <ControlOutlined />,
label: <Link to={`/tenants/${tenantId}/device-profiles`}>Device Profiles</Link>,
},
{
key: "tenant-gateways",
icon: <WifiOutlined />,
label: <Link to={`/tenants/${tenantId}/gateways`}>Gateways</Link>,
},
{
key: "tenant-applications",
icon: <AppstoreOutlined />,
label: <Link to={`/tenants/${tenantId}/applications`}>Applications</Link>,
},
],
});
}
useEffect(() => {
parseLocation();
}, [location]);
return (
<div>
<Autocomplete
placeholder="Select tenant"
className="organiation-select"
getOption={this.getTenantOption}
getOptions={this.getTenantOptions}
onSelect={this.onTenantSelect}
value={this.state.tenantId}
/>
<Menu
mode="inline"
openKeys={["ns", "tenant"]}
selectedKeys={[this.state.selectedKey]}
expandIcon={<div></div>}
items={items}
/>
</div>
);
let items: MenuProps["items"] = [];
if (SessionStore.isAdmin()) {
items.push({
key: "ns",
label: "Network Server",
icon: <CloudOutlined />,
children: [
{
key: "ns-dashboard",
icon: <DashboardOutlined />,
label: <Link to="/dashboard">Dashboard</Link>,
},
{
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>,
},
{
key: "ns-regions",
icon: <CompassOutlined />,
label: <Link to="/regions">Regions</Link>,
},
],
});
} else {
items.push({
key: "ns",
label: "Network Server",
icon: <CloudOutlined />,
children: [
{
key: "ns-regions",
icon: <CompassOutlined />,
label: <Link to="/regions">Regions</Link>,
},
],
});
}
if (tenantId !== "") {
items.push({
key: "tenant",
label: "Tenant",
icon: <HomeOutlined />,
children: [
{
key: "tenant-dashboard",
icon: <DashboardOutlined />,
label: <Link to={`/tenants/${tenantId}`}>Dashboard</Link>,
},
{
key: "tenant-users",
icon: <UserOutlined />,
label: <Link to={`/tenants/${tenantId}/users`}>Users</Link>,
},
{
key: "tenant-api-keys",
icon: <KeyOutlined />,
label: <Link to={`/tenants/${tenantId}/api-keys`}>API Keys</Link>,
},
{
key: "tenant-device-profiles",
icon: <ControlOutlined />,
label: <Link to={`/tenants/${tenantId}/device-profiles`}>Device Profiles</Link>,
},
{
key: "tenant-gateways",
icon: <WifiOutlined />,
label: <Link to={`/tenants/${tenantId}/gateways`}>Gateways</Link>,
},
{
key: "tenant-applications",
icon: <AppstoreOutlined />,
label: <Link to={`/tenants/${tenantId}/applications`}>Applications</Link>,
},
],
});
}
return (
<div>
<Autocomplete
placeholder="Select tenant"
className="organiation-select"
getOption={getTenantOption}
getOptions={getTenantOptions}
onSelect={onTenantSelect}
value={tenantId}
/>
<Menu
mode="inline"
openKeys={["ns", "tenant"]}
selectedKeys={[selectedKey]}
expandIcon={<div></div>}
items={items}
/>
</div>
);
}
export default withRouter(SideMenu);
export default SideMenu;

View File

@ -1,5 +1,3 @@
import React, { Component } from "react";
import { Card } from "antd";
import { TimeUnit } from "chart.js";
@ -13,82 +11,80 @@ interface IProps {
aggregation: Aggregation;
}
class MetricBar extends Component<IProps> {
render() {
let unit: TimeUnit = "hour";
if (this.props.aggregation === Aggregation.DAY) {
unit = "day";
} else if (this.props.aggregation === Aggregation.MONTH) {
unit = "month";
}
let backgroundColors = [
"#8bc34a",
"#ff5722",
"#ff9800",
"#ffc107",
"#ffeb3b",
"#cddc39",
"#4caf50",
"#009688",
"#00bcd4",
"#03a9f4",
"#2196f3",
"#3f51b5",
"#673ab7",
"#9c27b0",
"#e91e63",
];
const animation: false = false;
const options = {
animation: animation,
plugins: {
legend: {
display: true,
},
},
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
},
x: {
type: "time" as const,
time: {
unit: unit,
},
},
},
};
let data: {
labels: number[];
datasets: {
label: string;
data: number[];
backgroundColor: string;
}[];
} = {
labels: this.props.metric.getTimestampsList().map(v => moment(v.toDate()).valueOf()),
datasets: [],
};
for (let ds of this.props.metric.getDatasetsList()) {
data.datasets.push({
label: ds.getLabel(),
data: ds.getDataList(),
backgroundColor: backgroundColors.shift()!,
});
}
return (
<Card title={this.props.metric.getName()} className="dashboard-chart">
<Bar data={data} options={options} />
</Card>
);
function MetricBar(props: IProps) {
let unit: TimeUnit = "hour";
if (props.aggregation === Aggregation.DAY) {
unit = "day";
} else if (props.aggregation === Aggregation.MONTH) {
unit = "month";
}
let backgroundColors = [
"#8bc34a",
"#ff5722",
"#ff9800",
"#ffc107",
"#ffeb3b",
"#cddc39",
"#4caf50",
"#009688",
"#00bcd4",
"#03a9f4",
"#2196f3",
"#3f51b5",
"#673ab7",
"#9c27b0",
"#e91e63",
];
const animation: false = false;
const options = {
animation: animation,
plugins: {
legend: {
display: true,
},
},
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
},
x: {
type: "time" as const,
time: {
unit: unit,
},
},
},
};
let data: {
labels: number[];
datasets: {
label: string;
data: number[];
backgroundColor: string;
}[];
} = {
labels: props.metric.getTimestampsList().map(v => moment(v.toDate()).valueOf()),
datasets: [],
};
for (let ds of props.metric.getDatasetsList()) {
data.datasets.push({
label: ds.getLabel(),
data: ds.getDataList(),
backgroundColor: backgroundColors.shift()!,
});
}
return (
<Card title={props.metric.getName()} className="dashboard-chart">
<Bar data={data} options={options} />
</Card>
);
}
export default MetricBar;

View File

@ -1,5 +1,3 @@
import React, { Component } from "react";
import { Card } from "antd";
import { TimeUnit } from "chart.js";
@ -14,83 +12,81 @@ interface IProps {
zeroToNull?: boolean;
}
class MetricChart extends Component<IProps> {
render() {
let unit: TimeUnit = "hour";
let tooltipFormat = "LT";
if (this.props.aggregation === Aggregation.DAY) {
unit = "day";
tooltipFormat = "MMM Do";
} else if (this.props.aggregation === Aggregation.MONTH) {
unit = "month";
tooltipFormat = "MMM YYYY";
}
const animation: false = false;
const options = {
animation: animation,
plugins: {
legend: {
display: false,
},
},
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
},
x: {
type: "time" as const,
time: {
unit: unit,
tooltipFormat: tooltipFormat,
},
},
},
};
let prevValue = 0;
let data = {
labels: this.props.metric.getTimestampsList().map(v => moment(v.toDate()).valueOf()),
datasets: this.props.metric.getDatasetsList().map(v => {
return {
label: v.getLabel(),
borderColor: "rgba(33, 150, 243, 1)",
backgroundColor: "rgba(0, 0, 0, 0)",
lineTension: 0,
pointBackgroundColor: "rgba(33, 150, 243, 1)",
data: v.getDataList().map(v => {
if (v === 0 && this.props.zeroToNull) {
return null;
} else {
if (this.props.metric.getKind() === MetricKind.COUNTER) {
let val = v - prevValue;
prevValue = v;
if (val < 0) {
return 0;
}
return val;
} else {
return v;
}
}
}),
};
}),
};
let name = this.props.metric.getName();
if (this.props.metric.getKind() === MetricKind.COUNTER) {
name = `${name} (per ${unit})`;
}
return (
<Card title={name} className="dashboard-chart">
<Line height={75} options={options} data={data} />
</Card>
);
function MetricChart(props: IProps) {
let unit: TimeUnit = "hour";
let tooltipFormat = "LT";
if (props.aggregation === Aggregation.DAY) {
unit = "day";
tooltipFormat = "MMM Do";
} else if (props.aggregation === Aggregation.MONTH) {
unit = "month";
tooltipFormat = "MMM YYYY";
}
const animation: false = false;
const options = {
animation: animation,
plugins: {
legend: {
display: false,
},
},
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
},
x: {
type: "time" as const,
time: {
unit: unit,
tooltipFormat: tooltipFormat,
},
},
},
};
let prevValue = 0;
let data = {
labels: props.metric.getTimestampsList().map(v => moment(v.toDate()).valueOf()),
datasets: props.metric.getDatasetsList().map(v => {
return {
label: v.getLabel(),
borderColor: "rgba(33, 150, 243, 1)",
backgroundColor: "rgba(0, 0, 0, 0)",
lineTension: 0,
pointBackgroundColor: "rgba(33, 150, 243, 1)",
data: v.getDataList().map(v => {
if (v === 0 && props.zeroToNull) {
return null;
} else {
if (props.metric.getKind() === MetricKind.COUNTER) {
let val = v - prevValue;
prevValue = v;
if (val < 0) {
return 0;
}
return val;
} else {
return v;
}
}
}),
};
}),
};
let name = props.metric.getName();
if (props.metric.getKind() === MetricKind.COUNTER) {
name = `${name} (per ${unit})`;
}
return (
<Card title={name} className="dashboard-chart">
<Line height={75} options={options} data={data} />
</Card>
);
}
export default MetricChart;

View File

@ -1,5 +1,3 @@
import React, { Component } from "react";
import { Card } from "antd";
import { color } from "chart.js/helpers";
@ -16,138 +14,136 @@ interface IProps {
aggregation: Aggregation;
}
class MetricHeatmap extends Component<IProps> {
render() {
let unit: TimeUnit = "hour";
if (this.props.aggregation === Aggregation.DAY) {
unit = "day";
} else if (this.props.aggregation === Aggregation.MONTH) {
unit = "month";
}
function MetricHeatmap(props: IProps) {
let unit: TimeUnit = "hour";
if (props.aggregation === Aggregation.DAY) {
unit = "day";
} else if (props.aggregation === Aggregation.MONTH) {
unit = "month";
}
const animation: false = false;
const animation: false = false;
let options = {
animation: animation,
maintainAspectRatio: false,
scales: {
y: {
type: "category" as const,
offset: true,
grid: {
display: false,
},
let options = {
animation: animation,
maintainAspectRatio: false,
scales: {
y: {
type: "category" as const,
offset: true,
grid: {
display: false,
},
x: {
type: "time" as const,
time: {
unit: unit,
},
x: {
type: "time" as const,
time: {
unit: unit,
},
offset: true,
labels: props.metric.getTimestampsList().map(v => moment(v.toDate().valueOf())),
grid: {
display: false,
},
},
},
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
title: () => {
return "";
},
offset: true,
labels: this.props.metric.getTimestampsList().map(v => moment(v.toDate().valueOf())),
grid: {
display: false,
label: (ctx: any) => {
const v = ctx.dataset.data[ctx.dataIndex].v;
return "Count: " + v;
},
},
},
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
title: () => {
return "";
},
label: (ctx: any) => {
const v = ctx.dataset.data[ctx.dataIndex].v;
return "Count: " + v;
},
},
},
};
let dataData: {
x: number;
y: string;
v: number;
}[] = [];
let data = {
labels: props.metric.getDatasetsList().map(v => v.getLabel()),
datasets: [
{
label: "Heatmap",
data: dataData,
minValue: -1,
maxValue: -1,
fromColor: props.fromColor.match(/\d+/g)!.map(Number),
toColor: props.toColor.match(/\d+/g)!.map(Number),
backgroundColor: (ctx: any): string => {
if (
ctx.dataset === undefined ||
ctx.dataset.data === undefined ||
ctx.dataset.data[ctx.dataIndex] === undefined
) {
return color("white").rgbString();
}
const value = ctx.dataset.data[ctx.dataIndex].v;
const steps = ctx.dataset.maxValue - ctx.dataset.minValue + 1;
const step = value - ctx.dataset.minValue;
const factor = (1 / steps) * step;
let result: [number, number, number] = ctx.dataset.fromColor.slice();
for (var i = 0; i < 3; i++) {
result[i] = Math.round(result[i] + factor * (ctx.dataset.toColor[i] - ctx.dataset.fromColor[i]));
}
return color(result).rgbString();
},
borderWidth: 0,
width: (ctx: any) => {
return (ctx.chart.chartArea || {}).width / props.metric.getTimestampsList().length - 1;
},
height: (ctx: any) => {
return (ctx.chart.chartArea || {}).height / props.metric.getDatasetsList().length - 1;
},
},
};
],
};
let dataData: {
x: number;
y: string;
v: number;
}[] = [];
data.labels.sort();
let data = {
labels: this.props.metric.getDatasetsList().map(v => v.getLabel()),
datasets: [
{
label: "Heatmap",
data: dataData,
minValue: -1,
maxValue: -1,
fromColor: this.props.fromColor.match(/\d+/g)!.map(Number),
toColor: this.props.toColor.match(/\d+/g)!.map(Number),
backgroundColor: (ctx: any): string => {
if (
ctx.dataset === undefined ||
ctx.dataset.data === undefined ||
ctx.dataset.data[ctx.dataIndex] === undefined
) {
return color("white").rgbString();
}
const tsList = props.metric.getTimestampsList();
const dsList = props.metric.getDatasetsList();
const value = ctx.dataset.data[ctx.dataIndex].v;
const steps = ctx.dataset.maxValue - ctx.dataset.minValue + 1;
const step = value - ctx.dataset.minValue;
const factor = (1 / steps) * step;
for (let i = 0; i < tsList.length; i++) {
for (let ds of dsList) {
let v = ds.getDataList()[i];
if (v === 0) {
continue;
}
let result: [number, number, number] = ctx.dataset.fromColor.slice();
for (var i = 0; i < 3; i++) {
result[i] = Math.round(result[i] + factor * (ctx.dataset.toColor[i] - ctx.dataset.fromColor[i]));
}
data.datasets[0].data.push({
x: moment(tsList[i].toDate()).valueOf(),
y: ds.getLabel(),
v: v,
});
return color(result).rgbString();
},
borderWidth: 0,
width: (ctx: any) => {
return (ctx.chart.chartArea || {}).width / this.props.metric.getTimestampsList().length - 1;
},
height: (ctx: any) => {
return (ctx.chart.chartArea || {}).height / this.props.metric.getDatasetsList().length - 1;
},
},
],
};
if (data.datasets[0].minValue === -1 || data.datasets[0].minValue > v) {
data.datasets[0].minValue = v;
}
data.labels.sort();
const tsList = this.props.metric.getTimestampsList();
const dsList = this.props.metric.getDatasetsList();
for (let i = 0; i < tsList.length; i++) {
for (let ds of dsList) {
let v = ds.getDataList()[i];
if (v === 0) {
continue;
}
data.datasets[0].data.push({
x: moment(tsList[i].toDate()).valueOf(),
y: ds.getLabel(),
v: v,
});
if (data.datasets[0].minValue === -1 || data.datasets[0].minValue > v) {
data.datasets[0].minValue = v;
}
if (data.datasets[0].maxValue < v) {
data.datasets[0].maxValue = v;
}
if (data.datasets[0].maxValue < v) {
data.datasets[0].maxValue = v;
}
}
return (
<Card title={this.props.metric.getName()} className="dashboard-chart">
<Chart type="matrix" data={data} options={options} />
</Card>
);
}
return (
<Card title={props.metric.getName()} className="dashboard-chart">
<Chart type="matrix" data={data} options={options} />
</Card>
);
}
export default MetricHeatmap;

View File

@ -31,7 +31,7 @@
padding-top: 85px;
overflow: auto;
position: fixed;
position: fixed !important;
height: 100vh;
left: 0;
}
@ -117,5 +117,10 @@ pre {
}
.ant-drawer-body {
padding-bottom: 88px; /* 64 + 24 */
padding-bottom: 88px;
/* 64 + 24 */
}
.input-code input {
font-family: monospace !important;
}

View File

@ -1,7 +1,6 @@
import React from "react";
import ReactDOM from "react-dom";
import ReactDOM from "react-dom/client";
// import { Chart, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, Title } from "chart.js";
import { Chart, registerables } from "chart.js";
import { MatrixElement, MatrixController } from "chartjs-chart-matrix";
import "chartjs-adapter-moment";
@ -9,7 +8,7 @@ import "chartjs-adapter-moment";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import "antd/dist/antd.min.css";
import "antd/dist/reset.css";
import "leaflet/dist/leaflet.css";
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
import "@fortawesome/fontawesome-free/css/all.css";
@ -19,12 +18,8 @@ import "./index.css";
Chart.register(MatrixController, MatrixElement, ...registerables);
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root"),
);
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
root.render(<App />);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))

View File

@ -111,6 +111,8 @@ class ApplicationStore extends EventEmitter {
return;
}
this.emit("change");
notification.success({
message: "Application updated",
duration: 3,

View File

@ -31,7 +31,7 @@ class RelayStore extends EventEmitter {
callbackFunc(resp);
});
}
};
addDevice = (req: AddRelayDeviceRequest, callbackFunc: () => void) => {
this.client.addDevice(req, SessionStore.getMetadata(), err => {
@ -47,7 +47,7 @@ class RelayStore extends EventEmitter {
callbackFunc();
});
}
};
removeDevice = (req: RemoveRelayDeviceRequest, callbackFunc: () => void) => {
this.client.removeDevice(req, SessionStore.getMetadata(), err => {
@ -63,7 +63,7 @@ class RelayStore extends EventEmitter {
callbackFunc();
});
}
};
listDevices = (req: ListRelayDevicesRequest, callbackFunc: (resp: ListRelayDevicesResponse) => void) => {
this.client.listDevices(req, SessionStore.getMetadata(), (err, resp) => {
@ -74,7 +74,7 @@ class RelayStore extends EventEmitter {
callbackFunc(resp);
});
}
};
}
const relayStore = new RelayStore();

View File

@ -1,5 +1,3 @@
import React, { Component } from "react";
import { Form, Input, Button } from "antd";
import { ApiKey } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
@ -9,29 +7,25 @@ interface IProps {
onFinish: (obj: ApiKey) => void;
}
interface IState {}
class ApiKeyForm extends Component<IProps, IState> {
onFinish = (values: ApiKey.AsObject) => {
function ApiKeyForm(props: IProps) {
const onFinish = (values: ApiKey.AsObject) => {
let apiKey = new ApiKey();
apiKey.setName(values.name);
this.props.onFinish(apiKey);
props.onFinish(apiKey);
};
render() {
return (
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
return (
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
export default ApiKeyForm;

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Input, Typography, Button, Space } from "antd";
@ -9,23 +8,21 @@ interface IProps {
createApiKeyResponse: CreateApiKeyResponse;
}
class ApiKeyToken extends Component<IProps> {
render() {
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Typography>
<Typography.Paragraph>
Use the following API token when making API requests. This token can be revoked at any time by deleting it.
Please note that this token can only be retrieved once:
</Typography.Paragraph>
</Typography>
<Input.TextArea rows={4} value={this.props.createApiKeyResponse.getToken()} />
<Button type="primary">
<Link to="../api-keys">Back</Link>
</Button>
</Space>
);
}
function ApiKeyToken(props: IProps) {
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Typography>
<Typography.Paragraph>
Use the following API token when making API requests. This token can be revoked at any time by deleting it.
Please note that this token can only be retrieved once:
</Typography.Paragraph>
</Typography>
<Input.TextArea rows={4} value={props.createApiKeyResponse.getToken()} />
<Button type="primary">
<Link to="../api-keys">Back</Link>
</Button>
</Space>
);
}
export default ApiKeyToken;

View File

@ -1,7 +1,8 @@
import React, { Component } from "react";
import React, { useState } from "react";
import { Link } from "react-router-dom";
import { Space, Breadcrumb, Card, PageHeader } from "antd";
import { Space, Breadcrumb, Card } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import { ApiKey, CreateApiKeyRequest, CreateApiKeyResponse } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
@ -9,61 +10,48 @@ import ApiKeyForm from "./ApiKeyForm";
import ApiKeyToken from "./ApiKeyToken";
import InternalStore from "../../stores/InternalStore";
interface IProps {}
function CreateAdminApiKey() {
const [createApiKeyResponse, setCreateApiKeyResponse] = useState<CreateApiKeyResponse | undefined>(undefined);
interface IState {
createApiKeyResponse?: CreateApiKeyResponse;
}
class CreateAdminApiKey extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
onFinish = (obj: ApiKey) => {
const onFinish = (obj: ApiKey) => {
obj.setIsAdmin(true);
let req = new CreateApiKeyRequest();
req.setApiKey(obj);
InternalStore.createApiKey(req, (resp: CreateApiKeyResponse) => {
this.setState({
createApiKeyResponse: resp,
});
setCreateApiKeyResponse(resp);
});
};
render() {
const apiKey = new ApiKey();
const apiKey = new ApiKey();
return (
<Space direction="vertical" style={{ width: "100%" }} size="large">
<PageHeader
title="Add API key"
breadcrumbRender={() => (
<Breadcrumb>
<Breadcrumb.Item>
<span>Network-server</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>
<Link to="/api-keys">API keys</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>Add</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
/>
<Card>
{!this.state.createApiKeyResponse && <ApiKeyForm initialValues={apiKey} onFinish={this.onFinish} />}
{this.state.createApiKeyResponse && <ApiKeyToken createApiKeyResponse={this.state.createApiKeyResponse} />}
</Card>
</Space>
);
}
return (
<Space direction="vertical" style={{ width: "100%" }} size="large">
<PageHeader
title="Add API key"
breadcrumbRender={() => (
<Breadcrumb>
<Breadcrumb.Item>
<span>Network-server</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>
<Link to="/api-keys">API keys</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>Add</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
/>
<Card>
{!createApiKeyResponse && <ApiKeyForm initialValues={apiKey} onFinish={onFinish} />}
{createApiKeyResponse && <ApiKeyToken createApiKeyResponse={createApiKeyResponse} />}
</Card>
</Space>
);
}
export default CreateAdminApiKey;

View File

@ -1,7 +1,8 @@
import React, { Component } from "react";
import React, { useState } from "react";
import { Link } from "react-router-dom";
import { Space, Breadcrumb, Card, PageHeader } from "antd";
import { Space, Breadcrumb, Card } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import { ApiKey, CreateApiKeyRequest, CreateApiKeyResponse } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
@ -10,68 +11,57 @@ import ApiKeyForm from "./ApiKeyForm";
import ApiKeyToken from "./ApiKeyToken";
import InternalStore from "../../stores/InternalStore";
interface IState {
createApiKeyResponse?: CreateApiKeyResponse;
}
interface IProps {
tenant: Tenant;
}
class CreateTenantApiKey extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
function CreateTenantApiKey(props: IProps) {
const [createApiKeyResponse, setCreateApiKeyResponse] = useState<CreateApiKeyResponse | undefined>(undefined);
onFinish = (obj: ApiKey) => {
obj.setTenantId(this.props.tenant.getId());
const onFinish = (obj: ApiKey) => {
obj.setTenantId(props.tenant.getId());
let req = new CreateApiKeyRequest();
req.setApiKey(obj);
InternalStore.createApiKey(req, (resp: CreateApiKeyResponse) => {
this.setState({
createApiKeyResponse: resp,
});
setCreateApiKeyResponse(resp);
});
};
render() {
const apiKey = new ApiKey();
const apiKey = new ApiKey();
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/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>
<Link to={`/tenants/${this.props.tenant.getId()}/api-keys`}>API Keys</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>Add</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title="Add API key"
/>
<Card>
{!this.state.createApiKeyResponse && <ApiKeyForm initialValues={apiKey} onFinish={this.onFinish} />}
{this.state.createApiKeyResponse && <ApiKeyToken createApiKeyResponse={this.state.createApiKeyResponse} />}
</Card>
</Space>
);
}
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>
<Link to={`/tenants/${props.tenant.getId()}/api-keys`}>API Keys</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>Add</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title="Add API key"
/>
<Card>
{!createApiKeyResponse && <ApiKeyForm initialValues={apiKey} onFinish={onFinish} />}
{createApiKeyResponse && <ApiKeyToken createApiKeyResponse={createApiKeyResponse} />}
</Card>
</Space>
);
}
export default CreateTenantApiKey;

View File

@ -1,10 +1,10 @@
import React, { Component } from "react";
import React, { useState } from "react";
import { Link } from "react-router-dom";
import { DeleteOutlined } from "@ant-design/icons";
import { Space, Breadcrumb, Button, PageHeader } from "antd";
import { Space, Breadcrumb, Button } from "antd";
import { ColumnsType } from "antd/es/table";
import { PageHeader } from "@ant-design/pro-layout";
import {
ListApiKeysRequest,
@ -17,62 +17,47 @@ import DataTable, { GetPageCallbackFunc } from "../../components/DataTable";
import InternalStore from "../../stores/InternalStore";
import DeleteConfirm from "../../components/DeleteConfirm";
interface IProps {}
function ListAdminApiKeys() {
const [refreshKey, setRefreshKey] = useState<number>(1);
interface IState {
refreshKey: number;
}
const columns: ColumnsType<ApiKey.AsObject> = [
{
title: "ID",
dataIndex: "id",
key: "id",
width: 400,
},
{
title: "Name",
dataIndex: "name",
key: "name",
},
{
title: "Action",
dataIndex: "id",
key: "action",
width: 100,
render: (text, record) => (
<DeleteConfirm typ="API key" confirm={record.name} onConfirm={deleteApiKey(record.id)}>
<Button shape="circle" icon={<DeleteOutlined />} />
</DeleteConfirm>
),
},
];
class ListAdminApiKeys extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
refreshKey: 1,
};
}
columns = (): ColumnsType<ApiKey.AsObject> => {
return [
{
title: "ID",
dataIndex: "id",
key: "id",
width: 400,
},
{
title: "Name",
dataIndex: "name",
key: "name",
},
{
title: "Action",
dataIndex: "id",
key: "action",
width: 100,
render: (text, record) => (
<DeleteConfirm typ="API key" confirm={record.name} onConfirm={this.deleteApiKey(record.id)}>
<Button shape="circle" icon={<DeleteOutlined />} />
</DeleteConfirm>
),
},
];
};
deleteApiKey = (id: string): (() => void) => {
const deleteApiKey = (id: string): (() => void) => {
return () => {
let req = new DeleteApiKeyRequest();
req.setId(id);
InternalStore.deleteApiKey(req, () => {
// trigger a data-table reload
this.setState({
refreshKey: this.state.refreshKey + 1,
});
setRefreshKey(refreshKey + 1);
});
};
};
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
let req = new ListApiKeysRequest();
req.setLimit(limit);
req.setOffset(offset);
@ -84,31 +69,29 @@ class ListAdminApiKeys extends Component<IProps, IState> {
});
};
render() {
return (
<Space direction="vertical" style={{ width: "100%" }} size="large">
<PageHeader
breadcrumbRender={() => (
<Breadcrumb>
<Breadcrumb.Item>
<span>Network Server</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>API keys</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title="API keys"
extra={[
<Button type="primary">
<Link to="/api-keys/create">Add API key</Link>
</Button>,
]}
/>
<DataTable columns={this.columns()} getPage={this.getPage} rowKey="id" refreshKey={this.state.refreshKey} />
</Space>
);
}
return (
<Space direction="vertical" style={{ width: "100%" }} size="large">
<PageHeader
breadcrumbRender={() => (
<Breadcrumb>
<Breadcrumb.Item>
<span>Network Server</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>API keys</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title="API keys"
extra={[
<Button type="primary">
<Link to="/api-keys/create">Add API key</Link>
</Button>,
]}
/>
<DataTable columns={columns} getPage={getPage} rowKey="id" refreshKey={refreshKey} />
</Space>
);
}
export default ListAdminApiKeys;

View File

@ -1,8 +1,9 @@
import React, { Component } from "react";
import React, { useState } from "react";
import { Link } from "react-router-dom";
import { DeleteOutlined } from "@ant-design/icons";
import { Space, Breadcrumb, Button, PageHeader } from "antd";
import { Space, Breadcrumb, Button } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import { ColumnsType } from "antd/es/table";
import {
@ -20,69 +21,55 @@ import Admin from "../../components/Admin";
interface IProps {
tenant: Tenant;
isAdmin: boolean;
}
interface IState {
refreshKey: number;
}
function ListTenantApiKeys(props: IProps) {
const [refreshKey, setRefreshKey] = useState<number>(1);
class ListTenantApiKeys extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
refreshKey: 1,
};
}
const columns: ColumnsType<ApiKey.AsObject> = [
{
title: "ID",
dataIndex: "id",
key: "id",
width: 400,
},
{
title: "Name",
dataIndex: "name",
key: "name",
},
{
title: "Action",
dataIndex: "id",
key: "action",
width: 100,
render: (text, record) => (
<Admin tenantId={props.tenant.getId()} isTenantAdmin>
<DeleteConfirm typ="API key" confirm={record.name} onConfirm={deleteApiKey(record.id)}>
<Button shape="circle" icon={<DeleteOutlined />} />
</DeleteConfirm>
</Admin>
),
},
];
columns = (): ColumnsType<ApiKey.AsObject> => {
return [
{
title: "ID",
dataIndex: "id",
key: "id",
width: 400,
},
{
title: "Name",
dataIndex: "name",
key: "name",
},
{
title: "Action",
dataIndex: "id",
key: "action",
width: 100,
render: (text, record) => (
<Admin tenantId={this.props.tenant.getId()} isTenantAdmin>
<DeleteConfirm typ="API key" confirm={record.name} onConfirm={this.deleteApiKey(record.id)}>
<Button shape="circle" icon={<DeleteOutlined />} />
</DeleteConfirm>
</Admin>
),
},
];
};
deleteApiKey = (id: string): (() => void) => {
const deleteApiKey = (id: string): (() => void) => {
return () => {
let req = new DeleteApiKeyRequest();
req.setId(id);
InternalStore.deleteApiKey(req, () => {
// trigger a data-table reload
this.setState({
refreshKey: this.state.refreshKey + 1,
});
setRefreshKey(refreshKey + 1);
});
};
};
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
let req = new ListApiKeysRequest();
req.setLimit(limit);
req.setOffset(offset);
req.setTenantId(this.props.tenant.getId());
req.setTenantId(props.tenant.getId());
InternalStore.listApiKeys(req, (resp: ListApiKeysResponse) => {
const obj = resp.toObject();
@ -90,38 +77,36 @@ class ListTenantApiKeys extends Component<IProps, IState> {
});
};
render() {
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/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>API Keys</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title="API keys"
extra={[
<Admin tenantId={this.props.tenant.getId()} isTenantAdmin>
<Button type="primary">
<Link to={`/tenants/${this.props.tenant.getId()}/api-keys/create`}>Add API key</Link>
</Button>
</Admin>,
]}
/>
<DataTable columns={this.columns()} getPage={this.getPage} rowKey="id" refreshKey={this.state.refreshKey} />
</Space>
);
}
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>API Keys</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title="API keys"
extra={[
<Admin tenantId={props.tenant.getId()} isTenantAdmin>
<Button type="primary">
<Link to={`/tenants/${props.tenant.getId()}/api-keys/create`}>Add API key</Link>
</Button>
</Admin>,
]}
/>
<DataTable columns={columns} getPage={getPage} rowKey="id" refreshKey={refreshKey} />
</Space>
);
}
export default ListTenantApiKeys;

View File

@ -1,5 +1,3 @@
import React, { Component } from "react";
import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
import { Form, Input, Button } from "antd";
@ -9,9 +7,9 @@ interface IProps {
disabled?: boolean;
}
class ApplicationForm extends Component<IProps> {
onFinish = (values: Application.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values);
function ApplicationForm(props: IProps) {
const onFinish = (values: Application.AsObject) => {
const v = Object.assign(props.initialValues.toObject(), values);
let app = new Application();
app.setId(v.id);
@ -19,26 +17,24 @@ class ApplicationForm extends Component<IProps> {
app.setName(v.name);
app.setDescription(v.description);
this.props.onFinish(app);
props.onFinish(app);
};
render() {
return (
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
<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 disabled={this.props.disabled} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" disabled={this.props.disabled}>
Submit
</Button>
</Form.Item>
</Form>
);
}
return (
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<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>
<Form.Item>
<Button type="primary" htmlType="submit" disabled={props.disabled}>
Submit
</Button>
</Form.Item>
</Form>
);
}
export default ApplicationForm;

View File

@ -1,7 +1,7 @@
import React, { Component } from "react";
import { Route, Switch, RouteComponentProps, Link } from "react-router-dom";
import { Route, Routes, Link, useNavigate, useLocation } from "react-router-dom";
import { Space, Breadcrumb, Card, Button, PageHeader, Menu } from "antd";
import { Space, Breadcrumb, Card, Button, Menu } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
import { Application, DeleteApplicationRequest } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
@ -38,247 +38,158 @@ import GenerateMqttCertificate from "./integrations/GenerateMqttCertificate";
import CreateIftttIntegration from "./integrations/CreateIftttIntegration";
import EditIftttIntegration from "./integrations/EditIftttIntegration";
interface IProps extends RouteComponentProps {
interface IProps {
tenant: Tenant;
application: Application;
measurementKeys: string[];
}
class ApplicationLayout extends Component<IProps> {
deleteApplication = () => {
function ApplicationLayout(props: IProps) {
const navigate = useNavigate();
const location = useLocation();
const deleteApplication = () => {
let req = new DeleteApplicationRequest();
req.setId(this.props.application.getId());
req.setId(props.application.getId());
ApplicationStore.delete(req, () => {
this.props.history.push(`/tenants/${this.props.tenant.getId()}/applications`);
navigate(`/tenants/${props.tenant.getId()}/applications`);
});
};
render() {
const tenant = this.props.tenant;
const app = this.props.application;
const tenant = props.tenant;
const app = props.application;
if (!app) {
return null;
}
const path = this.props.history.location.pathname;
let tab = "devices";
if (path.endsWith("/multicast-groups")) {
tab = "mg";
}
if (path.endsWith("/relays")) {
tab = "relay";
}
if (path.endsWith("/edit")) {
tab = "edit";
}
if (path.match(/.*\/integrations.*/g)) {
tab = "integrations";
}
const showIntegrations =
SessionStore.isAdmin() ||
SessionStore.isTenantAdmin(tenant.getId()) ||
SessionStore.isTenantDeviceAdmin(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/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>
<Link to={`/tenants/${this.props.tenant.getId()}/applications`}>Applications</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>{app.getName()}</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title={app.getName()}
subTitle={`application id: ${app.getId()}`}
extra={[
<Admin tenantId={this.props.tenant.getId()} isDeviceAdmin>
<DeleteConfirm confirm={app.getName()} typ="application" onConfirm={this.deleteApplication}>
<Button danger type="primary">
Delete application
</Button>
</DeleteConfirm>
</Admin>,
]}
/>
<Card>
<Menu mode="horizontal" selectedKeys={[tab]} style={{ marginBottom: 24 }}>
<Menu.Item key="devices">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}`}>Devices</Link>
</Menu.Item>
<Menu.Item key="mg">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/multicast-groups`}>
Multicast groups
</Link>
</Menu.Item>
<Menu.Item key="relay">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/relays`}>
Relays
</Link>
</Menu.Item>
<Menu.Item key="edit">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/edit`}>Application configuration</Link>
</Menu.Item>
{showIntegrations && (
<Menu.Item key="integrations">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/integrations`}>Integrations</Link>
</Menu.Item>
)}
</Menu>
<Switch>
<Route exact path={this.props.match.path} render={props => <ListDevices application={app} {...props} />} />
<Route
exact
path={`${this.props.match.path}/edit`}
render={props => <EditApplication application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations`}
render={props => <ListIntegrations application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/multicast-groups`}
render={props => <ListMulticastGroups application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/relays`}
render={props => <ListRelays application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/http/create`}
render={props => <CreateHttpIntegration application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/http/edit`}
render={props => <EditHttpIntegration application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/aws-sns/create`}
render={props => <CreateAwsSnsIntegration application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/aws-sns/edit`}
render={props => <EditAwsSnsIntegration application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/azure-service-bus/create`}
render={props => <CreateAzureServiceBusIntegration application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/azure-service-bus/edit`}
render={props => <EditAzureServiceBusIntegration application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/gcp-pub-sub/create`}
render={props => <CreateGcpPubSubIntegration application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/gcp-pub-sub/edit`}
render={props => <EditGcpPubSubIntegration application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/influxdb/create`}
render={props => <CreateInfluxDbIntegration application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/influxdb/edit`}
render={props => <EditInfluxDbIntegration application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/mydevices/create`}
render={props => <CreateMyDevicesIntegration application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/mydevices/edit`}
render={props => <EditMyDevicesIntegration application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/pilot-things/create`}
render={props => <CreatePilotThingsIntegration application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/pilot-things/edit`}
render={props => <EditPilotThingsIntegration application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/loracloud/create`}
render={props => <CreateLoRaCloudIntegration application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/loracloud/edit`}
render={props => <EditLoRaCloudIntegration application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/thingsboard/create`}
render={props => <CreateThingsBoardIntegration application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/thingsboard/edit`}
render={props => <EditThingsBoardIntegration application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/mqtt/certificate`}
render={props => <GenerateMqttCertificate application={app} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/integrations/ifttt/create`}
render={props => (
<CreateIftttIntegration application={app} measurementKeys={this.props.measurementKeys} {...props} />
)}
/>
<Route
exact
path={`${this.props.match.path}/integrations/ifttt/edit`}
render={props => (
<EditIftttIntegration application={app} measurementKeys={this.props.measurementKeys} {...props} />
)}
/>
</Switch>
</Card>
</Space>
);
if (!app) {
return null;
}
const path = location.pathname;
let tab = "devices";
if (path.endsWith("/multicast-groups")) {
tab = "mg";
}
if (path.endsWith("/relays")) {
tab = "relay";
}
if (path.endsWith("/edit")) {
tab = "edit";
}
if (path.match(/.*\/integrations.*/g)) {
tab = "integrations";
}
const showIntegrations =
SessionStore.isAdmin() ||
SessionStore.isTenantAdmin(tenant.getId()) ||
SessionStore.isTenantDeviceAdmin(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>
<Link to={`/tenants/${props.tenant.getId()}/applications`}>Applications</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>{app.getName()}</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title={app.getName()}
subTitle={`application id: ${app.getId()}`}
extra={[
<Admin tenantId={props.tenant.getId()} isDeviceAdmin>
<DeleteConfirm confirm={app.getName()} typ="application" onConfirm={deleteApplication}>
<Button danger type="primary">
Delete application
</Button>
</DeleteConfirm>
</Admin>,
]}
/>
<Card>
<Menu mode="horizontal" selectedKeys={[tab]} style={{ marginBottom: 24 }}>
<Menu.Item key="devices">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}`}>Devices</Link>
</Menu.Item>
<Menu.Item key="mg">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/multicast-groups`}>Multicast groups</Link>
</Menu.Item>
<Menu.Item key="relay">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/relays`}>Relays</Link>
</Menu.Item>
<Menu.Item key="edit">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/edit`}>Application configuration</Link>
</Menu.Item>
{showIntegrations && (
<Menu.Item key="integrations">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/integrations`}>Integrations</Link>
</Menu.Item>
)}
</Menu>
<Routes>
<Route path="/" element={<ListDevices application={app} />} />
<Route path="/edit" element={<EditApplication application={app} />} />
<Route path="/integrations" element={<ListIntegrations application={app} />} />
<Route path="/multicast-groups" element={<ListMulticastGroups application={app} />} />
<Route path="/relays" element={<ListRelays application={app} />} />
<Route path="/integrations/http/create" element={<CreateHttpIntegration application={app} />} />
<Route path="/integrations/http/edit" element={<EditHttpIntegration application={app} />} />
<Route path="/integrations/http/create" element={<CreateHttpIntegration application={app} />} />
<Route path="/integrations/http/edit" element={<EditHttpIntegration application={app} />} />
<Route path="/integrations/aws-sns/create" element={<CreateAwsSnsIntegration application={app} />} />
<Route path="/integrations/aws-sns/edit" element={<EditAwsSnsIntegration application={app} />} />
<Route
path="/integrations/azure-service-bus/create"
element={<CreateAzureServiceBusIntegration application={app} />}
/>
<Route
path="/integrations/azure-service-bus/edit"
element={<EditAzureServiceBusIntegration application={app} />}
/>
<Route path="/integrations/gcp-pub-sub/create" element={<CreateGcpPubSubIntegration application={app} />} />
<Route path="/integrations/gcp-pub-sub/edit" element={<EditGcpPubSubIntegration application={app} />} />
<Route path="/integrations/influxdb/create" element={<CreateInfluxDbIntegration application={app} />} />
<Route path="/integrations/influxdb/edit" element={<EditInfluxDbIntegration application={app} />} />
<Route path="/integrations/mydevices/create" element={<CreateMyDevicesIntegration application={app} />} />
<Route path="/integrations/mydevices/edit" element={<EditMyDevicesIntegration application={app} />} />
<Route
path="/integrations/pilot-things/create"
element={<CreatePilotThingsIntegration application={app} />}
/>
<Route path="/integrations/pilot-things/edit" element={<EditPilotThingsIntegration application={app} />} />
<Route path="/integrations/loracloud/create" element={<CreateLoRaCloudIntegration application={app} />} />
<Route path="/integrations/loracloud/edit" element={<EditLoRaCloudIntegration application={app} />} />
<Route path="/integrations/thingsboard/create" element={<CreateThingsBoardIntegration application={app} />} />
<Route path="/integrations/thingsboard/edit" element={<EditThingsBoardIntegration application={app} />} />
<Route path="/integrations/mqtt/certificate" element={<GenerateMqttCertificate application={app} />} />
<Route
path="/integrations/ifttt/create"
element={<CreateIftttIntegration application={app} measurementKeys={props.measurementKeys} />}
/>
<Route
path="/integrations/ifttt/edit"
element={<EditIftttIntegration application={app} measurementKeys={props.measurementKeys} />}
/>
</Routes>
</Card>
</Space>
);
}
export default ApplicationLayout;

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { Route, Switch, RouteComponentProps } from "react-router-dom";
import React, { useState, useEffect } from "react";
import { Route, Routes, useParams } from "react-router-dom";
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
import {
@ -16,90 +16,57 @@ import MulticastGroupLayout from "../multicast-groups/MulticastGroupLayout";
import CreateMulticastGroup from "../multicast-groups/CreateMulticastGroup";
import RelayLayout from "../relays/RelayLayout";
interface MatchParams {
applicationId: string;
}
interface IProps extends RouteComponentProps<MatchParams> {
interface IProps {
tenant: Tenant;
}
interface IState {
application?: Application;
measurementKeys: string[];
}
function ApplicationLoader(props: IProps) {
const { applicationId } = useParams();
const [application, setApplication] = useState<Application | undefined>(undefined);
const [measurementKeys, setMeasurementKeys] = useState<string[]>([]);
class ApplicationLoader extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
measurementKeys: [],
useEffect(() => {
ApplicationStore.on("change", loadApplication);
loadApplication();
return () => {
ApplicationStore.removeAllListeners("change");
};
}
}, [applicationId]);
componentDidMount() {
this.getApplication();
}
getApplication = () => {
const loadApplication = () => {
let req = new GetApplicationRequest();
req.setId(this.props.match.params.applicationId);
req.setId(applicationId!);
ApplicationStore.get(req, (resp: GetApplicationResponse) => {
this.setState({
application: resp.getApplication(),
measurementKeys: resp.getMeasurementKeysList(),
});
setApplication(resp.getApplication());
setMeasurementKeys(resp.getMeasurementKeysList());
});
};
render() {
const app = this.state.application;
if (!app) {
return null;
}
const path = this.props.match.path;
const tenant = this.props.tenant;
return (
<Switch>
<Route
exact
path={`${path}/devices/create`}
render={props => <CreateDevice tenant={tenant} application={app} {...props} />}
/>
<Route
exact
path={`${path}/multicast-groups/create`}
render={props => <CreateMulticastGroup tenant={tenant} application={app} {...props} />}
/>
<Route
path={`${path}/multicast-groups/:multicastGroupId([\\w-]{36})`}
render={(props: any) => <MulticastGroupLayout tenant={tenant} application={app} {...props} />}
/>
<Route
path={`${path}/devices/:devEui([0-9a-f]{16})`}
component={(props: any) => <DeviceLayout tenant={tenant} application={app} {...props} />}
/>
<Route
path={`${path}/relays/:relayDevEui([0-9a-f]{16})`}
component={(props: any) => <RelayLayout tenant={tenant} application={app} {...props} />}
/>
<Route
path={path}
render={props => (
<ApplicationLayout
tenant={tenant}
application={app}
measurementKeys={this.state.measurementKeys}
{...props}
/>
)}
/>
</Switch>
);
const app = application;
if (!app) {
return null;
}
const tenant = props.tenant;
return (
<Routes>
<Route path="/devices/create" element={<CreateDevice tenant={tenant} application={app} />} />
<Route path="/multicast-groups/create" element={<CreateMulticastGroup tenant={tenant} application={app} />} />
<Route
path="/multicast-groups/:multicastGroupId/*"
element={<MulticastGroupLayout tenant={tenant} application={app} />}
/>
<Route path="/devices/:devEui/*" element={<DeviceLayout tenant={tenant} application={app} />} />
<Route path="/relays/:relayDevEui/*" element={<RelayLayout tenant={tenant} application={app} />} />
<Route
path="/*"
element={<ApplicationLayout tenant={tenant} application={app} measurementKeys={measurementKeys} />}
/>
</Routes>
);
}
export default ApplicationLoader;

View File

@ -1,7 +1,7 @@
import React, { Component } from "react";
import { Link, RouteComponentProps } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom";
import { Space, Breadcrumb, Card, PageHeader } from "antd";
import { Space, Breadcrumb, Card } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
import {
@ -13,56 +13,56 @@ import {
import ApplicationForm from "./ApplicationForm";
import ApplicationStore from "../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
tenant: Tenant;
}
class CreateApplication extends Component<IProps> {
onFinish = (obj: Application) => {
obj.setTenantId(this.props.tenant.getId());
function CreateApplication(props: IProps) {
const navigate = useNavigate();
const onFinish = (obj: Application) => {
obj.setTenantId(props.tenant.getId());
let req = new CreateApplicationRequest();
req.setApplication(obj);
ApplicationStore.create(req, (resp: CreateApplicationResponse) => {
this.props.history.push(`/tenants/${this.props.tenant.getId()}/applications/${resp.getId()}`);
navigate(`/tenants/${props.tenant.getId()}/applications/${resp.getId()}`);
});
};
render() {
const app = new Application();
const app = new Application();
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/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>
<Link to={`/tenants/${this.props.tenant.getId()}/applications`}>Applications</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>Add</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title="Add application"
/>
<Card>
<ApplicationForm initialValues={app} onFinish={this.onFinish} />
</Card>
</Space>
);
}
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>
<Link to={`/tenants/${props.tenant.getId()}/applications`}>Applications</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>Add</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title="Add application"
/>
<Card>
<ApplicationForm initialValues={app} onFinish={onFinish} />
</Card>
</Space>
);
}
export default CreateApplication;

View File

@ -1,5 +1,4 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { Application, UpdateApplicationRequest } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
@ -7,31 +6,29 @@ import ApplicationStore from "../../stores/ApplicationStore";
import ApplicationForm from "./ApplicationForm";
import SessionStore from "../../stores/SessionStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
class EditApplication extends Component<IProps> {
onFinish = (obj: Application) => {
function EditApplication(props: IProps) {
const navigate = useNavigate();
const onFinish = (obj: Application) => {
let req = new UpdateApplicationRequest();
req.setApplication(obj);
ApplicationStore.update(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}`);
});
};
render() {
const disabled = !(
SessionStore.isAdmin() ||
SessionStore.isTenantAdmin(this.props.application.getTenantId()) ||
SessionStore.isTenantDeviceAdmin(this.props.application.getTenantId())
);
const disabled = !(
SessionStore.isAdmin() ||
SessionStore.isTenantAdmin(props.application.getTenantId()) ||
SessionStore.isTenantDeviceAdmin(props.application.getTenantId())
);
return <ApplicationForm initialValues={this.props.application} disabled={disabled} onFinish={this.onFinish} />;
}
return <ApplicationForm initialValues={props.application} disabled={disabled} onFinish={onFinish} />;
}
export default EditApplication;

View File

@ -1,8 +1,8 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Space, Breadcrumb, Button, PageHeader } from "antd";
import { Space, Breadcrumb, Button } from "antd";
import { ColumnsType } from "antd/es/table";
import { PageHeader } from "@ant-design/pro-layout";
import {
ListApplicationsRequest,
@ -19,29 +19,25 @@ interface IProps {
tenant: Tenant;
}
class ListApplications extends Component<IProps> {
columns = (): ColumnsType<ApplicationListItem.AsObject> => {
return [
{
title: "Name",
dataIndex: "name",
key: "name",
width: 250,
render: (text, record) => (
<Link to={`/tenants/${this.props.tenant.getId()}/applications/${record.id}`}>{text}</Link>
),
},
{
title: "Description",
dataIndex: "description",
key: "description",
},
];
};
function ListApplications(props: IProps) {
const columns: ColumnsType<ApplicationListItem.AsObject> = [
{
title: "Name",
dataIndex: "name",
key: "name",
width: 250,
render: (text, record) => <Link to={`/tenants/${props.tenant.getId()}/applications/${record.id}`}>{text}</Link>,
},
{
title: "Description",
dataIndex: "description",
key: "description",
},
];
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
let req = new ListApplicationsRequest();
req.setTenantId(this.props.tenant.getId());
req.setTenantId(props.tenant.getId());
req.setLimit(limit);
req.setOffset(offset);
@ -51,38 +47,36 @@ class ListApplications extends Component<IProps> {
});
};
render() {
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/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>Applications</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title="Applications"
extra={[
<Admin tenantId={this.props.tenant.getId()} isDeviceAdmin>
<Button type="primary">
<Link to={`/tenants/${this.props.tenant.getId()}/applications/create`}>Add application</Link>
</Button>
</Admin>,
]}
/>
<DataTable columns={this.columns()} getPage={this.getPage} rowKey="id" />
</Space>
);
}
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>Applications</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title="Applications"
extra={[
<Admin tenantId={props.tenant.getId()} isDeviceAdmin>
<Button type="primary">
<Link to={`/tenants/${props.tenant.getId()}/applications/create`}>Add application</Link>
</Button>
</Admin>,
]}
/>
<DataTable columns={columns} getPage={getPage} rowKey="id" />
</Space>
);
}
export default ListApplications;

View File

@ -1,4 +1,4 @@
import React, { Component } from "react";
import React, { useState, useEffect } from "react";
import { Row } from "antd";
@ -27,32 +27,22 @@ interface IProps {
application: Application;
}
interface IState {
configured: any[];
available: any[];
}
function ListIntegrations(props: IProps) {
const [configured, setConfigured] = useState<any[]>([]);
const [available, setAvailable] = useState<any[]>([]);
class ListIntegrations extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
configured: [],
available: [],
useEffect(() => {
ApplicationStore.on("integration.delete", loadIntegrations);
loadIntegrations();
return () => {
ApplicationStore.removeAllListeners("integration.delete");
};
}
}, []);
componentDidMount() {
ApplicationStore.on("integration.delete", this.loadIntegrations);
this.loadIntegrations();
}
componentWillUnmount() {
ApplicationStore.removeAllListeners("integration.delete");
}
loadIntegrations = () => {
const loadIntegrations = () => {
let req = new ListIntegrationsRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.listIntegrations(req, (resp: ListIntegrationsResponse) => {
let configured: any[] = [];
@ -70,94 +60,90 @@ class ListIntegrations extends Component<IProps, IState> {
// AWS SNS
if (includes(resp.getResultList(), IntegrationKind.AWS_SNS)) {
configured.push(<AwsSnsCard application={this.props.application} />);
configured.push(<AwsSnsCard application={props.application} />);
} else {
available.push(<AwsSnsCard application={this.props.application} add />);
available.push(<AwsSnsCard application={props.application} add />);
}
// Azure Service-Bus
if (includes(resp.getResultList(), IntegrationKind.AZURE_SERVICE_BUS)) {
configured.push(<AzureServiceBusCard application={this.props.application} />);
configured.push(<AzureServiceBusCard application={props.application} />);
} else {
available.push(<AzureServiceBusCard application={this.props.application} add />);
available.push(<AzureServiceBusCard application={props.application} add />);
}
// GCP Pub/Sub
if (includes(resp.getResultList(), IntegrationKind.GCP_PUB_SUB)) {
configured.push(<GcpPubSubCard application={this.props.application} />);
configured.push(<GcpPubSubCard application={props.application} />);
} else {
available.push(<GcpPubSubCard application={this.props.application} add />);
available.push(<GcpPubSubCard application={props.application} add />);
}
// HTTP
if (includes(resp.getResultList(), IntegrationKind.HTTP)) {
configured.push(<HttpCard application={this.props.application} />);
configured.push(<HttpCard application={props.application} />);
} else {
available.push(<HttpCard application={this.props.application} add />);
available.push(<HttpCard application={props.application} add />);
}
// IFTTT
if (includes(resp.getResultList(), IntegrationKind.IFTTT)) {
configured.push(<IftttCard application={this.props.application} />);
configured.push(<IftttCard application={props.application} />);
} else {
available.push(<IftttCard application={this.props.application} add />);
available.push(<IftttCard application={props.application} add />);
}
// InfluxDB
if (includes(resp.getResultList(), IntegrationKind.INFLUX_DB)) {
configured.push(<InfluxdbCard application={this.props.application} />);
configured.push(<InfluxdbCard application={props.application} />);
} else {
available.push(<InfluxdbCard application={this.props.application} add />);
available.push(<InfluxdbCard application={props.application} add />);
}
// MQTT
if (includes(resp.getResultList(), IntegrationKind.MQTT_GLOBAL)) {
configured.push(<MqttCard application={this.props.application} />);
configured.push(<MqttCard application={props.application} />);
}
// myDevices
if (includes(resp.getResultList(), IntegrationKind.MY_DEVICES)) {
configured.push(<MyDevicesCard application={this.props.application} />);
configured.push(<MyDevicesCard application={props.application} />);
} else {
available.push(<MyDevicesCard application={this.props.application} add />);
available.push(<MyDevicesCard application={props.application} add />);
}
// Pilot Things
if (includes(resp.getResultList(), IntegrationKind.PILOT_THINGS)) {
configured.push(<PilotThingsCard application={this.props.application} />);
configured.push(<PilotThingsCard application={props.application} />);
} else {
available.push(<PilotThingsCard application={this.props.application} add />);
available.push(<PilotThingsCard application={props.application} add />);
}
// Semtech LoRa Cloud
if (includes(resp.getResultList(), IntegrationKind.LORA_CLOUD)) {
configured.push(<LoRaCloudCard application={this.props.application} />);
configured.push(<LoRaCloudCard application={props.application} />);
} else {
available.push(<LoRaCloudCard application={this.props.application} add />);
available.push(<LoRaCloudCard application={props.application} add />);
}
// ThingsBoard
if (includes(resp.getResultList(), IntegrationKind.THINGS_BOARD)) {
configured.push(<ThingsBoardCard application={this.props.application} />);
configured.push(<ThingsBoardCard application={props.application} />);
} else {
available.push(<ThingsBoardCard application={this.props.application} add />);
available.push(<ThingsBoardCard application={props.application} add />);
}
this.setState({
configured: configured,
available: available,
});
setConfigured(configured);
setAvailable(available);
});
};
render() {
return (
<Row gutter={24}>
{this.state.configured}
{this.state.available}
</Row>
);
}
return (
<Row gutter={24}>
{configured}
{available}
</Row>
);
}
export default ListIntegrations;

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd";
@ -13,47 +12,45 @@ interface IProps {
add?: boolean;
}
class AwsSns extends Component<IProps> {
onDelete = () => {
function AwsSns(props: IProps) {
const onDelete = () => {
let req = new DeleteAwsSnsIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.deleteAwsSnsIntegration(req, () => {});
};
render() {
let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) {
actions = [
<Link to="integrations/aws-sns/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="integrations/aws-sns/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="AWS SNS"
className="integration-card"
cover={<img alt="AWS SNS" src="/integrations/aws_sns.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The AWS SNS integration forwards events to an AWS SNS topic." />
</Card>
</Col>
);
if (!!props.add) {
actions = [
<Link to="aws-sns/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="aws-sns/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="AWS SNS"
className="integration-card"
cover={<img alt="AWS SNS" src="/integrations/aws_sns.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The AWS SNS integration forwards events to an AWS SNS topic." />
</Card>
</Col>
);
}
export default AwsSns;

View File

@ -1,5 +1,3 @@
import React, { Component } from "react";
import { Form, Input, Button, Select } from "antd";
import { AwsSnsIntegration, Encoding } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
@ -9,9 +7,9 @@ interface IProps {
onFinish: (obj: AwsSnsIntegration) => void;
}
class AwsSnsIntegrationForm extends Component<IProps> {
onFinish = (values: AwsSnsIntegration.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values);
function AwsSnsIntegrationForm(props: IProps) {
const onFinish = (values: AwsSnsIntegration.AsObject) => {
const v = Object.assign(props.initialValues.toObject(), values);
let i = new AwsSnsIntegration();
i.setApplicationId(v.applicationId);
@ -21,54 +19,52 @@ class AwsSnsIntegrationForm extends Component<IProps> {
i.setSecretAccessKey(v.secretAccessKey);
i.setTopicArn(v.topicArn);
this.props.onFinish(i);
props.onFinish(i);
};
render() {
return (
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
<Form.Item
label="Payload encoding"
name="encoding"
rules={[{ required: true, message: "Please select an encoding!" }]}
>
<Select>
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
</Select>
</Form.Item>
<Form.Item label="AWS region" name="region" rules={[{ required: true, message: "Please enter a region!" }]}>
<Input />
</Form.Item>
<Form.Item
label="AWS Access Key ID"
name="accessKeyId"
rules={[{ required: true, message: "Please enter an Access Key ID!" }]}
>
<Input />
</Form.Item>
<Form.Item
label="AWS Secret Access Key"
name="secretAccessKey"
rules={[{ required: true, message: "Please enter a Secret Access Key!" }]}
>
<Input />
</Form.Item>
<Form.Item
label="AWS SNS topic ARN"
name="topicArn"
rules={[{ required: true, message: "Please enter a SNS topic ARN!" }]}
>
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
return (
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<Form.Item
label="Payload encoding"
name="encoding"
rules={[{ required: true, message: "Please select an encoding!" }]}
>
<Select>
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
</Select>
</Form.Item>
<Form.Item label="AWS region" name="region" rules={[{ required: true, message: "Please enter a region!" }]}>
<Input />
</Form.Item>
<Form.Item
label="AWS Access Key ID"
name="accessKeyId"
rules={[{ required: true, message: "Please enter an Access Key ID!" }]}
>
<Input />
</Form.Item>
<Form.Item
label="AWS Secret Access Key"
name="secretAccessKey"
rules={[{ required: true, message: "Please enter a Secret Access Key!" }]}
>
<Input />
</Form.Item>
<Form.Item
label="AWS SNS topic ARN"
name="topicArn"
rules={[{ required: true, message: "Please enter a SNS topic ARN!" }]}
>
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
export default AwsSnsIntegrationForm;

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd";
@ -16,46 +15,44 @@ interface IProps {
add?: boolean;
}
class AzureServiceBusCard extends Component<IProps> {
onDelete = () => {
function AzureServiceBusCard(props: IProps) {
const onDelete = () => {
let req = new DeleteAzureServiceBusIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.deleteAzureServiceBusIntegration(req, () => {});
};
render() {
let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) {
actions = [
<Link to="integrations/azure-service-bus/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="integrations/azure-service-bus/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="Azure Service-Bus"
className="integration-card"
cover={<img alt="Azure Service-Bus" src="/integrations/azure_service_bus.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The Azure Service-Bus integration forwards events to an Azure Service-Bus topic or queue." />
</Card>
</Col>
);
if (!!props.add) {
actions = [
<Link to="azure-service-bus/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="azure-service-bus/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="Azure Service-Bus"
className="integration-card"
cover={<img alt="Azure Service-Bus" src="/integrations/azure_service_bus.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The Azure Service-Bus integration forwards events to an Azure Service-Bus topic or queue." />
</Card>
</Col>
);
}
export default AzureServiceBusCard;

View File

@ -1,5 +1,3 @@
import React, { Component } from "react";
import { Form, Input, Button, Select } from "antd";
import { AzureServiceBusIntegration, Encoding } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
@ -9,9 +7,9 @@ interface IProps {
onFinish: (obj: AzureServiceBusIntegration) => void;
}
class AzureServiceBusIntegrationForm extends Component<IProps> {
onFinish = (values: AzureServiceBusIntegration.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values);
function AzureServiceBusIntegrationForm(props: IProps) {
const onFinish = (values: AzureServiceBusIntegration.AsObject) => {
const v = Object.assign(props.initialValues.toObject(), values);
let i = new AzureServiceBusIntegration();
i.setApplicationId(v.applicationId);
@ -19,45 +17,53 @@ class AzureServiceBusIntegrationForm extends Component<IProps> {
i.setConnectionString(v.connectionString);
i.setPublishName(v.publishName);
this.props.onFinish(i);
props.onFinish(i);
};
render() {
return (
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
<Form.Item
label="Payload encoding"
name="encoding"
rules={[{ required: true, message: "Please select an encoding!" }]}
>
<Select>
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="Azure Service-Bus connection string"
name="connectionString"
tooltip="This string can be obtained after creating a 'Shared access policy' with 'Send' permission."
rules={[{ required: true, message: "Please enter an Azure Service-Bus connection string!" }]}
>
<Input />
</Form.Item>
<Form.Item
label="Azure Service-Bus topic / queue name"
name="publishName"
rules={[{ required: true, message: "Please enter an Azure Service-Bus topic / queue name!" }]}
>
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
return (
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<Form.Item
label="Payload encoding"
name="encoding"
rules={[{ required: true, message: "Please select an encoding!" }]}
>
<Select>
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="Azure Service-Bus connection string"
name="connectionString"
tooltip="This string can be obtained after creating a 'Shared access policy' with 'Send' permission."
rules={[
{
required: true,
message: "Please enter an Azure Service-Bus connection string!",
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="Azure Service-Bus topic / queue name"
name="publishName"
rules={[
{
required: true,
message: "Please enter an Azure Service-Bus topic / queue name!",
},
]}
>
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
export default AzureServiceBusIntegrationForm;

View File

@ -1,5 +1,4 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -12,33 +11,31 @@ import {
import AwsSnsIntegrationForm from "./AwsSnsIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
class CreateAwsSnsIntegration extends Component<IProps> {
onFinish = (obj: AwsSnsIntegration) => {
obj.setApplicationId(this.props.application.getId());
function CreateAwsSnsIntegration(props: IProps) {
const navigate = useNavigate();
const onFinish = (obj: AwsSnsIntegration) => {
obj.setApplicationId(props.application.getId());
let req = new CreateAwsSnsIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.createAwsSnsIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
const i = new AwsSnsIntegration();
const i = new AwsSnsIntegration();
return (
<Card title="Add AWS SNS integration">
<AwsSnsIntegrationForm initialValues={i} onFinish={this.onFinish} />
</Card>
);
}
return (
<Card title="Add AWS SNS integration">
<AwsSnsIntegrationForm initialValues={i} onFinish={onFinish} />
</Card>
);
}
export default CreateAwsSnsIntegration;

View File

@ -1,5 +1,4 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -12,33 +11,31 @@ import {
import AzureServiceBusIntegrationForm from "./AzureServiceBusIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
class CreateAzureServiceBusIntegration extends Component<IProps> {
onFinish = (obj: AzureServiceBusIntegration) => {
obj.setApplicationId(this.props.application.getId());
function CreateAzureServiceBusIntegration(props: IProps) {
const navigate = useNavigate();
const onFinish = (obj: AzureServiceBusIntegration) => {
obj.setApplicationId(props.application.getId());
let req = new CreateAzureServiceBusIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.createAzureServiceBusIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
const i = new AzureServiceBusIntegration();
const i = new AzureServiceBusIntegration();
return (
<Card title="Add Azure Service-Bus integration">
<AzureServiceBusIntegrationForm initialValues={i} onFinish={this.onFinish} />
</Card>
);
}
return (
<Card title="Add Azure Service-Bus integration">
<AzureServiceBusIntegrationForm initialValues={i} onFinish={onFinish} />
</Card>
);
}
export default CreateAzureServiceBusIntegration;

View File

@ -1,5 +1,4 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -12,33 +11,31 @@ import {
import GcpPubSubIntegrationForm from "./GcpPubSubIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
class CreateGcpPubSubIntegration extends Component<IProps> {
onFinish = (obj: GcpPubSubIntegration) => {
obj.setApplicationId(this.props.application.getId());
function CreateGcpPubSubIntegration(props: IProps) {
const navigate = useNavigate();
const onFinish = (obj: GcpPubSubIntegration) => {
obj.setApplicationId(props.application.getId());
let req = new CreateGcpPubSubIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.createGcpPubSubIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
const i = new GcpPubSubIntegration();
const i = new GcpPubSubIntegration();
return (
<Card title="Add GCP Pub/Sub integration">
<GcpPubSubIntegrationForm initialValues={i} onFinish={this.onFinish} />
</Card>
);
}
return (
<Card title="Add GCP Pub/Sub integration">
<GcpPubSubIntegrationForm initialValues={i} onFinish={onFinish} />
</Card>
);
}
export default CreateGcpPubSubIntegration;

View File

@ -1,5 +1,4 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -12,33 +11,31 @@ import {
import HttpIntegrationForm from "./HttpIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
class CreateHttpIntegration extends Component<IProps> {
onFinish = (obj: HttpIntegration) => {
obj.setApplicationId(this.props.application.getId());
function CreateHttpIntegration(props: IProps) {
const navigate = useNavigate();
const onFinish = (obj: HttpIntegration) => {
obj.setApplicationId(props.application.getId());
let req = new CreateHttpIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.createHttpIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
const i = new HttpIntegration();
const i = new HttpIntegration();
return (
<Card title="Add HTTP integration">
<HttpIntegrationForm initialValues={i} onFinish={this.onFinish} />
</Card>
);
}
return (
<Card title="Add HTTP integration">
<HttpIntegrationForm initialValues={i} onFinish={onFinish} />
</Card>
);
}
export default CreateHttpIntegration;

View File

@ -1,5 +1,4 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -12,35 +11,33 @@ import {
import IftttIntegrationForm from "./IftttIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
measurementKeys: string[];
}
class CreateIftttIntegration extends Component<IProps> {
onFinish = (obj: IftttIntegration) => {
obj.setApplicationId(this.props.application.getId());
function CreateIftttIntegration(props: IProps) {
const navigate = useNavigate();
const onFinish = (obj: IftttIntegration) => {
obj.setApplicationId(props.application.getId());
let req = new CreateIftttIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.createIftttIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
const i = new IftttIntegration();
i.setUplinkValuesList(["", ""]);
const i = new IftttIntegration();
i.setUplinkValuesList(["", ""]);
return (
<Card title="Add IFTTT integration">
<IftttIntegrationForm measurementKeys={this.props.measurementKeys} initialValues={i} onFinish={this.onFinish} />
</Card>
);
}
return (
<Card title="Add IFTTT integration">
<IftttIntegrationForm measurementKeys={props.measurementKeys} initialValues={i} onFinish={onFinish} />
</Card>
);
}
export default CreateIftttIntegration;

View File

@ -1,5 +1,4 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -12,33 +11,31 @@ import {
import InfluxDbIntegrationForm from "./InfluxDbIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
class CreateInfluxDbIntegration extends Component<IProps> {
onFinish = (obj: InfluxDbIntegration) => {
obj.setApplicationId(this.props.application.getId());
function CreateInfluxDbIntegration(props: IProps) {
const navigate = useNavigate();
const onFinish = (obj: InfluxDbIntegration) => {
obj.setApplicationId(props.application.getId());
let req = new CreateInfluxDbIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.createInfluxDbIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
const i = new InfluxDbIntegration();
const i = new InfluxDbIntegration();
return (
<Card title="Add InfluxDB integration">
<InfluxDbIntegrationForm initialValues={i} onFinish={this.onFinish} />
</Card>
);
}
return (
<Card title="Add InfluxDB integration">
<InfluxDbIntegrationForm initialValues={i} onFinish={onFinish} />
</Card>
);
}
export default CreateInfluxDbIntegration;

View File

@ -1,5 +1,4 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -13,38 +12,36 @@ import {
import LoRaCloudIntegrationForm from "./LoRaCloudIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
class CreateLoRaCloudIntegration extends Component<IProps> {
onFinish = (obj: LoraCloudIntegration) => {
obj.setApplicationId(this.props.application.getId());
function CreateLoRaCloudIntegration(props: IProps) {
const navigate = useNavigate();
const onFinish = (obj: LoraCloudIntegration) => {
obj.setApplicationId(props.application.getId());
let req = new CreateLoraCloudIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.createLoraCloudIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
let i = new LoraCloudIntegration();
let mgs = new LoraCloudModemGeolocationServices();
mgs.setModemEnabled(true);
mgs.setForwardFPortsList([192, 197, 198, 199]);
let i = new LoraCloudIntegration();
let mgs = new LoraCloudModemGeolocationServices();
mgs.setModemEnabled(true);
mgs.setForwardFPortsList([192, 197, 198, 199]);
i.setModemGeolocationServices(mgs);
i.setModemGeolocationServices(mgs);
return (
<Card title="Add Semtech LoRa Cloud&trade; integration">
<LoRaCloudIntegrationForm initialValues={i} onFinish={this.onFinish} />
</Card>
);
}
return (
<Card title="Add Semtech LoRa Cloud&trade; integration">
<LoRaCloudIntegrationForm initialValues={i} onFinish={onFinish} />
</Card>
);
}
export default CreateLoRaCloudIntegration;

View File

@ -1,5 +1,4 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -12,33 +11,31 @@ import {
import MyDevicesIntegrationForm from "./MyDevicesIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
class CreateMyDevicesIntegration extends Component<IProps> {
onFinish = (obj: MyDevicesIntegration) => {
obj.setApplicationId(this.props.application.getId());
function CreateMyDevicesIntegration(props: IProps) {
const navigate = useNavigate();
const onFinish = (obj: MyDevicesIntegration) => {
obj.setApplicationId(props.application.getId());
let req = new CreateMyDevicesIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.createMyDevicesIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
const i = new MyDevicesIntegration();
const i = new MyDevicesIntegration();
return (
<Card title="Add myDevices integration">
<MyDevicesIntegrationForm initialValues={i} onFinish={this.onFinish} />
</Card>
);
}
return (
<Card title="Add myDevices integration">
<MyDevicesIntegrationForm initialValues={i} onFinish={onFinish} />
</Card>
);
}
export default CreateMyDevicesIntegration;

View File

@ -1,5 +1,4 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -12,33 +11,31 @@ import {
import PilotThingsIntegrationForm from "./PilotThingsIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
class CreatePilotThingsIntegration extends Component<IProps> {
onFinish = (obj: PilotThingsIntegration) => {
obj.setApplicationId(this.props.application.getId());
function CreatePilotThingsIntegration(props: IProps) {
const navigate = useNavigate();
const onFinish = (obj: PilotThingsIntegration) => {
obj.setApplicationId(props.application.getId());
let req = new CreatePilotThingsIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.createPilotThingsIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
const i = new PilotThingsIntegration();
const i = new PilotThingsIntegration();
return (
<Card title="Add Pilot Things integration">
<PilotThingsIntegrationForm initialValues={i} onFinish={this.onFinish} />
</Card>
);
}
return (
<Card title="Add Pilot Things integration">
<PilotThingsIntegrationForm initialValues={i} onFinish={onFinish} />
</Card>
);
}
export default CreatePilotThingsIntegration;

View File

@ -1,5 +1,4 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -12,33 +11,31 @@ import {
import ThingsBoardIntegrationForm from "./ThingsBoardIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
class CreateThingsBoardIntegration extends Component<IProps> {
onFinish = (obj: ThingsBoardIntegration) => {
obj.setApplicationId(this.props.application.getId());
function CreateThingsBoardIntegration(props: IProps) {
const navigate = useNavigate();
const onFinish = (obj: ThingsBoardIntegration) => {
obj.setApplicationId(props.application.getId());
let req = new CreateThingsBoardIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.createThingsBoardIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
const i = new ThingsBoardIntegration();
const i = new ThingsBoardIntegration();
return (
<Card title="Add ThingsBoard integration">
<ThingsBoardIntegrationForm initialValues={i} onFinish={this.onFinish} />
</Card>
);
}
return (
<Card title="Add ThingsBoard integration">
<ThingsBoardIntegrationForm initialValues={i} onFinish={onFinish} />
</Card>
);
}
export default CreateThingsBoardIntegration;

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -14,53 +14,41 @@ import {
import AwsSnsIntegrationForm from "./AwsSnsIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
interface IState {
integration?: AwsSnsIntegration;
}
function EditAwsSnsIntegration(props: IProps) {
const navigate = useNavigate();
const [integration, setIntegration] = useState<AwsSnsIntegration | undefined>(undefined);
class EditAwsSnsIntegration extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
componentDidMount() {
useEffect(() => {
let req = new GetAwsSnsIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.getAwsSnsIntegration(req, (resp: GetAwsSnsIntegrationResponse) => {
this.setState({
integration: resp.getIntegration(),
});
setIntegration(resp.getIntegration());
});
}
}, [props]);
onFinish = (obj: AwsSnsIntegration) => {
const onFinish = (obj: AwsSnsIntegration) => {
let req = new UpdateAwsSnsIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.updateAwsSnsIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
if (this.state.integration === undefined) {
return null;
}
return (
<Card title="Update AWS SNS integration">
<AwsSnsIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
</Card>
);
if (integration === undefined) {
return null;
}
return (
<Card title="Update AWS SNS integration">
<AwsSnsIntegrationForm initialValues={integration} onFinish={onFinish} />
</Card>
);
}
export default EditAwsSnsIntegration;

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -14,53 +14,41 @@ import {
import AzureServiceBusIntegrationForm from "./AzureServiceBusIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
interface IState {
integration?: AzureServiceBusIntegration;
}
function EditAzureServiceBusIntegration(props: IProps) {
const navigate = useNavigate();
const [integration, setIntegration] = useState<AzureServiceBusIntegration | undefined>(undefined);
class EditAzureServiceBusIntegration extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
componentDidMount() {
useEffect(() => {
let req = new GetAzureServiceBusIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.getAzureServiceBusIntegration(req, (resp: GetAzureServiceBusIntegrationResponse) => {
this.setState({
integration: resp.getIntegration(),
});
setIntegration(resp.getIntegration());
});
}
}, [props]);
onFinish = (obj: AzureServiceBusIntegration) => {
const onFinish = (obj: AzureServiceBusIntegration) => {
let req = new UpdateAzureServiceBusIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.updateAzureServiceBusIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
if (this.state.integration === undefined) {
return null;
}
return (
<Card title="Update Azure Service-Bus integration">
<AzureServiceBusIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
</Card>
);
if (integration === undefined) {
return null;
}
return (
<Card title="Update Azure Service-Bus integration">
<AzureServiceBusIntegrationForm initialValues={integration} onFinish={onFinish} />
</Card>
);
}
export default EditAzureServiceBusIntegration;

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -14,53 +14,41 @@ import {
import GcpPubSubIntegrationForm from "./GcpPubSubIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
interface IState {
integration?: GcpPubSubIntegration;
}
function EditGcpPubSubIntegration(props: IProps) {
const navigate = useNavigate();
const [integration, setIntegration] = useState<GcpPubSubIntegration | undefined>(undefined);
class EditGcpPubSubIntegration extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
componentDidMount() {
useEffect(() => {
let req = new GetGcpPubSubIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.getGcpPubSubIntegration(req, (resp: GetGcpPubSubIntegrationResponse) => {
this.setState({
integration: resp.getIntegration(),
});
setIntegration(resp.getIntegration());
});
}
}, [props]);
onFinish = (obj: GcpPubSubIntegration) => {
const onFinish = (obj: GcpPubSubIntegration) => {
let req = new UpdateGcpPubSubIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.updateGcpPubSubIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
if (this.state.integration === undefined) {
return null;
}
return (
<Card title="Update GCP Pub/Sub integration">
<GcpPubSubIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
</Card>
);
if (integration === undefined) {
return null;
}
return (
<Card title="Update GCP Pub/Sub integration">
<GcpPubSubIntegrationForm initialValues={integration} onFinish={onFinish} />
</Card>
);
}
export default EditGcpPubSubIntegration;

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -14,53 +14,41 @@ import {
import HttpIntegrationForm from "./HttpIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
interface IState {
integration?: HttpIntegration;
}
function EditHttpIntegration(props: IProps) {
const navigate = useNavigate();
const [integration, setIntegration] = useState<HttpIntegration | undefined>(undefined);
class EditHttpIntegration extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
componentDidMount() {
useEffect(() => {
let req = new GetHttpIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.getHttpIntegration(req, (resp: GetHttpIntegrationResponse) => {
this.setState({
integration: resp.getIntegration(),
});
setIntegration(resp.getIntegration());
});
}
}, [props]);
onFinish = (obj: HttpIntegration) => {
const onFinish = (obj: HttpIntegration) => {
let req = new UpdateHttpIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.updateHttpIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
if (this.state.integration === undefined) {
return null;
}
return (
<Card title="Update HTTP integration">
<HttpIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
</Card>
);
if (integration === undefined) {
return null;
}
return (
<Card title="Update HTTP integration">
<HttpIntegrationForm initialValues={integration} onFinish={onFinish} />
</Card>
);
}
export default EditHttpIntegration;

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -14,58 +14,42 @@ import {
import IftttIntegrationForm from "./IftttIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
measurementKeys: string[];
}
interface IState {
integration?: IftttIntegration;
}
function EditIftttIntegration(props: IProps) {
const navigate = useNavigate();
const [integration, setIntegration] = useState<IftttIntegration | undefined>(undefined);
class EditIftttIntegration extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
componentDidMount() {
useEffect(() => {
let req = new GetIftttIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.getIftttIntegration(req, (resp: GetIftttIntegrationResponse) => {
this.setState({
integration: resp.getIntegration(),
});
setIntegration(resp.getIntegration());
});
}
}, [props]);
onFinish = (obj: IftttIntegration) => {
const onFinish = (obj: IftttIntegration) => {
let req = new UpdateIftttIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.updateIftttIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
if (this.state.integration === undefined) {
return null;
}
return (
<Card title="Update IFTTT integration">
<IftttIntegrationForm
measurementKeys={this.props.measurementKeys}
initialValues={this.state.integration}
onFinish={this.onFinish}
/>
</Card>
);
if (integration === undefined) {
return null;
}
return (
<Card title="Update IFTTT integration">
<IftttIntegrationForm measurementKeys={props.measurementKeys} initialValues={integration} onFinish={onFinish} />
</Card>
);
}
export default EditIftttIntegration;

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -14,53 +14,41 @@ import {
import InfluxDbIntegrationForm from "./InfluxDbIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
interface IState {
integration?: InfluxDbIntegration;
}
function EditInfluxDbIntegration(props: IProps) {
const navigate = useNavigate();
const [integration, setIntegration] = useState<InfluxDbIntegration | undefined>(undefined);
class EditInfluxDbIntegration extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
componentDidMount() {
useEffect(() => {
let req = new GetInfluxDbIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.getInfluxDbIntegration(req, (resp: GetInfluxDbIntegrationResponse) => {
this.setState({
integration: resp.getIntegration(),
});
setIntegration(resp.getIntegration());
});
}
}, [props]);
onFinish = (obj: InfluxDbIntegration) => {
const onFinish = (obj: InfluxDbIntegration) => {
let req = new UpdateInfluxDbIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.updateInfluxDbIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
if (this.state.integration === undefined) {
return null;
}
return (
<Card title="Update InfluxDB integration">
<InfluxDbIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
</Card>
);
if (integration === undefined) {
return null;
}
return (
<Card title="Update InfluxDB integration">
<InfluxDbIntegrationForm initialValues={integration} onFinish={onFinish} />
</Card>
);
}
export default EditInfluxDbIntegration;

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -14,53 +14,41 @@ import {
import LoRaCloudIntegrationForm from "./LoRaCloudIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
interface IState {
integration?: LoraCloudIntegration;
}
function EditLoRaCloudIntegration(props: IProps) {
const navigate = useNavigate();
const [integration, setIntegration] = useState<LoraCloudIntegration | undefined>(undefined);
class EditLoRaCloudIntegration extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
componentDidMount() {
useEffect(() => {
let req = new GetLoraCloudIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.getLoraCloudIntegration(req, (resp: GetLoraCloudIntegrationResponse) => {
this.setState({
integration: resp.getIntegration(),
});
setIntegration(resp.getIntegration());
});
}
}, [props]);
onFinish = (obj: LoraCloudIntegration) => {
const onFinish = (obj: LoraCloudIntegration) => {
let req = new UpdateLoraCloudIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.updateLoraCloudIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
if (this.state.integration === undefined) {
return null;
}
return (
<Card title="Update Semtech LoRa Cloud&trade; integration">
<LoRaCloudIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
</Card>
);
if (integration === undefined) {
return null;
}
return (
<Card title="Update Semtech LoRa Cloud&trade; integration">
<LoRaCloudIntegrationForm initialValues={integration} onFinish={onFinish} />
</Card>
);
}
export default EditLoRaCloudIntegration;

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -14,53 +14,41 @@ import {
import MyDevicesIntegrationForm from "./MyDevicesIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
interface IState {
integration?: MyDevicesIntegration;
}
function EditMyDevicesIntegration(props: IProps) {
const navigate = useNavigate();
const [integration, setIntegration] = useState<MyDevicesIntegration | undefined>(undefined);
class EditMyDevicesIntegration extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
componentDidMount() {
useEffect(() => {
let req = new GetMyDevicesIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.getMyDevicesIntegration(req, (resp: GetMyDevicesIntegrationResponse) => {
this.setState({
integration: resp.getIntegration(),
});
setIntegration(resp.getIntegration());
});
}
}, [props]);
onFinish = (obj: MyDevicesIntegration) => {
const onFinish = (obj: MyDevicesIntegration) => {
let req = new UpdateMyDevicesIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.updateMyDevicesIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
if (this.state.integration === undefined) {
return null;
}
return (
<Card title="Update myDevices integration">
<MyDevicesIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
</Card>
);
if (integration === undefined) {
return null;
}
return (
<Card title="Update myDevices integration">
<MyDevicesIntegrationForm initialValues={integration} onFinish={onFinish} />
</Card>
);
}
export default EditMyDevicesIntegration;

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -14,53 +14,41 @@ import {
import PilotThingsIntegrationForm from "./PilotThingsIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
interface IState {
integration?: PilotThingsIntegration;
}
function EditPilotThingsIntegration(props: IProps) {
const navigate = useNavigate();
const [integration, setIntegration] = useState<PilotThingsIntegration | undefined>(undefined);
class EditPilotThingsIntegration extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
componentDidMount() {
useEffect(() => {
let req = new GetPilotThingsIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.getPilotThingsIntegration(req, (resp: GetPilotThingsIntegrationResponse) => {
this.setState({
integration: resp.getIntegration(),
});
setIntegration(resp.getIntegration());
});
}
}, [props]);
onFinish = (obj: PilotThingsIntegration) => {
const onFinish = (obj: PilotThingsIntegration) => {
let req = new UpdatePilotThingsIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.updatePilotThingsIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
if (this.state.integration === undefined) {
return null;
}
return (
<Card title="Update Pilot Things integration">
<PilotThingsIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
</Card>
);
if (integration === undefined) {
return null;
}
return (
<Card title="Update Pilot Things integration">
<PilotThingsIntegrationForm initialValues={integration} onFinish={onFinish} />
</Card>
);
}
export default EditPilotThingsIntegration;

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Card } from "antd";
@ -14,53 +14,41 @@ import {
import ThingsBoardIntegrationForm from "./ThingsBoardIntegrationForm";
import ApplicationStore from "../../../stores/ApplicationStore";
interface IProps extends RouteComponentProps {
interface IProps {
application: Application;
}
interface IState {
integration?: ThingsBoardIntegration;
}
function EditThingsBoardIntegration(props: IProps) {
const navigate = useNavigate();
const [integration, setIntegration] = useState<ThingsBoardIntegration | undefined>(undefined);
class EditThingsBoardIntegration extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
componentDidMount() {
useEffect(() => {
let req = new GetThingsBoardIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.getThingsBoardIntegration(req, (resp: GetThingsBoardIntegrationResponse) => {
this.setState({
integration: resp.getIntegration(),
});
setIntegration(resp.getIntegration());
});
}
}, [props]);
onFinish = (obj: ThingsBoardIntegration) => {
const onFinish = (obj: ThingsBoardIntegration) => {
let req = new UpdateThingsBoardIntegrationRequest();
req.setIntegration(obj);
ApplicationStore.updateThingsBoardIntegration(req, () => {
this.props.history.push(
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
);
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
});
};
render() {
if (this.state.integration === undefined) {
return null;
}
return (
<Card title="Update ThingsBoard integration">
<ThingsBoardIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
</Card>
);
if (integration === undefined) {
return null;
}
return (
<Card title="Update ThingsBoard integration">
<ThingsBoardIntegrationForm initialValues={integration} onFinish={onFinish} />
</Card>
);
}
export default EditThingsBoardIntegration;

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd";
@ -13,46 +12,44 @@ interface IProps {
add?: boolean;
}
class GcpPubSubCard extends Component<IProps> {
onDelete = () => {
function GcpPubSubCard(props: IProps) {
const onDelete = () => {
let req = new DeleteGcpPubSubIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.deleteGcpPubSubIntegration(req, () => {});
};
render() {
let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) {
actions = [
<Link to="integrations/gcp-pub-sub/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="integrations/gcp-pub-sub/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="GCP Pub/Sub"
className="integration-card"
cover={<img alt="GCP Pub/Sub" src="/integrations/gcp_pubsub.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The Google Cloud Pub/Sub integration forwards events to a GCP Pub/Sub topic." />
</Card>
</Col>
);
if (!!props.add) {
actions = [
<Link to="gcp-pub-sub/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="gcp-pub-sub/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="GCP Pub/Sub"
className="integration-card"
cover={<img alt="GCP Pub/Sub" src="/integrations/gcp_pubsub.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The Google Cloud Pub/Sub integration forwards events to a GCP Pub/Sub topic." />
</Card>
</Col>
);
}
export default GcpPubSubCard;

View File

@ -1,5 +1,3 @@
import React, { Component } from "react";
import { Form, Input, Button, Select } from "antd";
import { GcpPubSubIntegration, Encoding } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
@ -9,9 +7,9 @@ interface IProps {
onFinish: (obj: GcpPubSubIntegration) => void;
}
class GcpPubSubIntegrationForm extends Component<IProps> {
onFinish = (values: GcpPubSubIntegration.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values);
function GcpPubSubIntegrationForm(props: IProps) {
const onFinish = (values: GcpPubSubIntegration.AsObject) => {
const v = Object.assign(props.initialValues.toObject(), values);
let i = new GcpPubSubIntegration();
i.setApplicationId(v.applicationId);
@ -20,52 +18,55 @@ class GcpPubSubIntegrationForm extends Component<IProps> {
i.setTopicName(v.topicName);
i.setCredentialsFile(v.credentialsFile);
this.props.onFinish(i);
props.onFinish(i);
};
render() {
return (
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
<Form.Item
label="Payload encoding"
name="encoding"
rules={[{ required: true, message: "Please select an encoding!" }]}
>
<Select>
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="GCP project ID"
name="projectId"
rules={[{ required: true, message: "Please enter a GCP project ID!" }]}
>
<Input />
</Form.Item>
<Form.Item
label="GCP Pub/Sub topic name"
name="topicName"
rules={[{ required: true, message: "Please enter a GCP Pub/Sub topic name!" }]}
>
<Input />
</Form.Item>
<Form.Item
label="GCP Service account credentials file"
name="credentialsFile"
tooltip="Under IAM create a Service account with 'Pub/Sub Publisher' role, then put the content of the JSON key in this field."
rules={[{ required: true, message: "Please enter a GCP Service account credentials file!" }]}
>
<Input.TextArea rows={10} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
return (
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<Form.Item
label="Payload encoding"
name="encoding"
rules={[{ required: true, message: "Please select an encoding!" }]}
>
<Select>
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="GCP project ID"
name="projectId"
rules={[{ required: true, message: "Please enter a GCP project ID!" }]}
>
<Input />
</Form.Item>
<Form.Item
label="GCP Pub/Sub topic name"
name="topicName"
rules={[{ required: true, message: "Please enter a GCP Pub/Sub topic name!" }]}
>
<Input />
</Form.Item>
<Form.Item
label="GCP Service account credentials file"
name="credentialsFile"
tooltip="Under IAM create a Service account with 'Pub/Sub Publisher' role, then put the content of the JSON key in this field."
rules={[
{
required: true,
message: "Please enter a GCP Service account credentials file!",
},
]}
>
<Input.TextArea rows={10} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
export default GcpPubSubIntegrationForm;

View File

@ -1,4 +1,4 @@
import React, { Component } from "react";
import React, { useState } from "react";
import moment from "moment";
import { Card, Button, Form, Input } from "antd";
@ -15,39 +15,27 @@ interface IProps {
application: Application;
}
interface IState {
certificate?: GenerateMqttIntegrationClientCertificateResponse;
buttonDisabled: boolean;
}
function GenerateMqttCertificate(props: IProps) {
const [certificate, setCertificate] = useState<GenerateMqttIntegrationClientCertificateResponse | undefined>(
undefined,
);
const [buttonDisabled, setButtonDisabled] = useState<boolean>(false);
class GenerateMqttCertificate extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
certificate: undefined,
buttonDisabled: false,
};
}
requestCertificate = () => {
this.setState({
buttonDisabled: true,
});
const requestCertificate = () => {
setButtonDisabled(true);
let req = new GenerateMqttIntegrationClientCertificateRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.generateMqttIntegrationClientCertificate(
req,
(resp: GenerateMqttIntegrationClientCertificateResponse) => {
this.setState({
certificate: resp,
});
setCertificate(resp);
},
);
};
renderRequest = () => {
const renderRequest = () => {
return (
<Card>
<p>
@ -59,7 +47,7 @@ class GenerateMqttCertificate extends Component<IProps, IState> {
</strong>
</p>
<p>
<Button onClick={this.requestCertificate} disabled={this.state.buttonDisabled}>
<Button onClick={requestCertificate} disabled={buttonDisabled}>
Generate certificate
</Button>
</p>
@ -67,14 +55,14 @@ class GenerateMqttCertificate extends Component<IProps, IState> {
);
};
renderResponse = () => {
const certificate = this.state.certificate!;
const renderResponse = () => {
const cert = certificate!;
const initial = {
expiresAt: moment(certificate.getExpiresAt()!.toDate()!).format("YYYY-MM-DD HH:mm:ss"),
caCert: certificate.getCaCert(),
tlsCert: certificate.getTlsCert(),
tlsKey: certificate.getTlsKey(),
expiresAt: moment(cert.getExpiresAt()!.toDate()!).format("YYYY-MM-DD HH:mm:ss"),
caCert: cert.getCaCert(),
tlsCert: cert.getTlsCert(),
tlsKey: cert.getTlsKey(),
};
return (
@ -103,15 +91,13 @@ class GenerateMqttCertificate extends Component<IProps, IState> {
);
};
render() {
let content = this.renderRequest();
let content = renderRequest();
if (this.state.certificate !== undefined) {
content = this.renderResponse();
}
return <Card title="Generate MQTT certificate">{content}</Card>;
if (certificate !== undefined) {
content = renderResponse();
}
return <Card title="Generate MQTT certificate">{content}</Card>;
}
export default GenerateMqttCertificate;

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd";
@ -13,46 +12,44 @@ interface IProps {
add?: boolean;
}
class HttpCard extends Component<IProps> {
onDelete = () => {
function HttpCard(props: IProps) {
const onDelete = () => {
let req = new DeleteHttpIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.deleteHttpIntegration(req, () => {});
};
render() {
let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) {
actions = [
<Link to="integrations/http/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="integrations/http/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="HTTP"
className="integration-card"
cover={<img alt="HTTP" src="/integrations/http.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The HTTP integration forwards events to a user-configurable endpoint as POST requests." />
</Card>
</Col>
);
if (!!props.add) {
actions = [
<Link to="http/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="http/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="HTTP"
className="integration-card"
cover={<img alt="HTTP" src="/integrations/http.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The HTTP integration forwards events to a user-configurable endpoint as POST requests." />
</Card>
</Col>
);
}
export default HttpCard;

View File

@ -1,5 +1,3 @@
import React, { Component } from "react";
import { Form, Input, Button, Select, Row, Col, Typography, Space } from "antd";
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
@ -10,9 +8,9 @@ interface IProps {
onFinish: (obj: HttpIntegration) => void;
}
class HttpIntegrationForm extends Component<IProps> {
onFinish = (values: HttpIntegration.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values);
function HttpIntegrationForm(props: IProps) {
const onFinish = (values: HttpIntegration.AsObject) => {
const v = Object.assign(props.initialValues.toObject(), values);
let i = new HttpIntegration();
i.setApplicationId(v.applicationId);
@ -24,79 +22,77 @@ class HttpIntegrationForm extends Component<IProps> {
i.getHeadersMap().set(elm[0], elm[1]);
}
this.props.onFinish(i);
props.onFinish(i);
};
render() {
return (
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
<Form.Item
label="Payload encoding"
name="encoding"
rules={[{ required: true, message: "Please select an encoding!" }]}
>
<Select>
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="Event endpoint URL(s)"
name="eventEndpointUrl"
tooltip="ChirpStack will make a POST request to this URL(s) with 'event' as query parameter. Multiple URLs can be defined as a comma separated list. Whitespace will be automatically removed."
rules={[{ required: true, message: "Please enter an event endpoint URL!" }]}
>
<Input />
</Form.Item>
<Space direction="vertical" style={{ width: "100%" }}>
<Typography.Text>Headers</Typography.Text>
<Form.List name="headersMap">
{(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 header
</Button>
</Form.Item>
</>
)}
</Form.List>
</Space>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
return (
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<Form.Item
label="Payload encoding"
name="encoding"
rules={[{ required: true, message: "Please select an encoding!" }]}
>
<Select>
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="Event endpoint URL(s)"
name="eventEndpointUrl"
tooltip="ChirpStack will make a POST request to this URL(s) with 'event' as query parameter. Multiple URLs can be defined as a comma separated list. Whitespace will be automatically removed."
rules={[{ required: true, message: "Please enter an event endpoint URL!" }]}
>
<Input />
</Form.Item>
<Space direction="vertical" style={{ width: "100%" }}>
<Typography.Text>Headers</Typography.Text>
<Form.List name="headersMap">
{(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 header
</Button>
</Form.Item>
</>
)}
</Form.List>
</Space>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
export default HttpIntegrationForm;

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd";
@ -13,46 +12,44 @@ interface IProps {
add?: boolean;
}
class IftttCard extends Component<IProps> {
onDelete = () => {
function IftttCard(props: IProps) {
const onDelete = () => {
let req = new DeleteIftttIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.deleteIftttIntegration(req, () => {});
};
render() {
let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) {
actions = [
<Link to="integrations/ifttt/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="integrations/ifttt/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="IFTTT"
className="integration-card"
cover={<img alt="IFTTT" src="/integrations/ifttt.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The IFTTT integration forwards events to the IFTTT Webhooks integration." />
</Card>
</Col>
);
if (!!props.add) {
actions = [
<Link to="ifttt/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="ifttt/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="IFTTT"
className="integration-card"
cover={<img alt="IFTTT" src="/integrations/ifttt.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The IFTTT integration forwards events to the IFTTT Webhooks integration." />
</Card>
</Col>
);
}
export default IftttCard;

View File

@ -1,4 +1,4 @@
import React, { Component } from "react";
import React, { useState, useEffect } from "react";
import { Form, Input, AutoComplete, Button, Row, Col, Switch } from "antd";
@ -10,29 +10,15 @@ interface IProps {
onFinish: (obj: IftttIntegration) => void;
}
interface IState {
arbitraryJson: boolean;
}
function IftttIntegrationForm(props: IProps) {
const [arbitraryJson, setArbitraryJson] = useState<Boolean>(false);
class IftttIntegrationForm extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
useEffect(() => {
setArbitraryJson(props.initialValues.getArbitraryJson());
}, [props]);
this.state = {
arbitraryJson: false,
};
}
componentDidMount() {
const v = this.props.initialValues;
this.setState({
arbitraryJson: v.getArbitraryJson(),
});
}
onFinish = (values: IftttIntegration.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values);
const onFinish = (values: IftttIntegration.AsObject) => {
const v = Object.assign(props.initialValues.toObject(), values);
let i = new IftttIntegration();
i.setApplicationId(v.applicationId);
@ -41,55 +27,58 @@ class IftttIntegrationForm extends Component<IProps, IState> {
i.setArbitraryJson(v.arbitraryJson);
i.setUplinkValuesList(v.uplinkValuesList);
this.props.onFinish(i);
props.onFinish(i);
};
onArbitraryJsonChange = (checked: boolean) => {
this.setState({
arbitraryJson: checked,
});
}
const onArbitraryJsonChange = (checked: boolean) => {
setArbitraryJson(checked);
};
render() {
const options: {
value: string;
}[] = this.props.measurementKeys.map(v => {
return { value: v };
});
const options: {
value: string;
}[] = props.measurementKeys.map(v => {
return { value: v };
});
return (
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
<Form.Item
label="Key"
name="key"
rules={[{ required: true, message: "Please enter a key!" }]}
tooltip="This key can be obtained from the IFTTT Webhooks integrations documentation"
>
<Input.Password />
</Form.Item>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label="Event prefix"
name="eventPrefix"
rules={[{ pattern: /[A-Za-z0-9]+/, message: "Only use A-Z, a-z and 0-9 characters" }]}
tooltip="The prefix will be added to the Webhook event, e.g. if set an uplink will be published as PREFIX_up instead of up."
>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Publish as arbitrary JSON"
name="arbitraryJson"
valuePropName="checked"
tooltip="If enabled, the event payload will be published as-is (arbitrary JSON payload instead of 3 JSON values format)."
>
<Switch onChange={this.onArbitraryJsonChange} />
</Form.Item>
</Col>
</Row>
{!this.state.arbitraryJson && <Form.List name="uplinkValuesList">
return (
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<Form.Item
label="Key"
name="key"
rules={[{ required: true, message: "Please enter a key!" }]}
tooltip="This key can be obtained from the IFTTT Webhooks integrations documentation"
>
<Input.Password />
</Form.Item>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label="Event prefix"
name="eventPrefix"
rules={[
{
pattern: /[A-Za-z0-9]+/,
message: "Only use A-Z, a-z and 0-9 characters",
},
]}
tooltip="The prefix will be added to the Webhook event, e.g. if set an uplink will be published as PREFIX_up instead of up."
>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Publish as arbitrary JSON"
name="arbitraryJson"
valuePropName="checked"
tooltip="If enabled, the event payload will be published as-is (arbitrary JSON payload instead of 3 JSON values format)."
>
<Switch onChange={onArbitraryJsonChange} />
</Form.Item>
</Col>
</Row>
{!arbitraryJson && (
<Form.List name="uplinkValuesList">
{fields => (
<Row gutter={24}>
{fields.map((field, i) => (
@ -105,15 +94,15 @@ class IftttIntegrationForm extends Component<IProps, IState> {
))}
</Row>
)}
</Form.List>}
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
</Form.List>
)}
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
export default IftttIntegrationForm;

View File

@ -1,4 +1,4 @@
import React, { Component } from "react";
import React, { useState } from "react";
import { Form, Input, Button, Select } from "antd";
@ -13,20 +13,11 @@ interface IProps {
onFinish: (obj: InfluxDbIntegration) => void;
}
interface IState {
selectedVersion: InfluxDbVersion;
}
function InfluxDbIntegrationForm(props: IProps) {
const [selectedVersion, setSelectedVersion] = useState<InfluxDbVersion>(InfluxDbVersion.INFLUXDB_1);
class InfluxDbIntegrationForm extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
selectedVersion: InfluxDbVersion.INFLUXDB_1,
};
}
onFinish = (values: InfluxDbIntegration.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values);
const onFinish = (values: InfluxDbIntegration.AsObject) => {
const v = Object.assign(props.initialValues.toObject(), values);
let i = new InfluxDbIntegration();
i.setApplicationId(v.applicationId);
@ -41,98 +32,90 @@ class InfluxDbIntegrationForm extends Component<IProps, IState> {
i.setBucket(v.bucket);
i.setToken(v.token);
this.props.onFinish(i);
props.onFinish(i);
};
onVersionChange = (version: InfluxDbVersion) => {
this.setState({
selectedVersion: version,
});
const onVersionChange = (version: InfluxDbVersion) => {
setSelectedVersion(version);
};
render() {
return (
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
return (
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<Form.Item
label="InfluxDB version"
name="version"
rules={[{ required: true, message: "Please select an InfluxDB version!" }]}
>
<Select onChange={onVersionChange}>
<Select.Option value={InfluxDbVersion.INFLUXDB_1}>InfluxDB v1</Select.Option>
<Select.Option value={InfluxDbVersion.INFLUXDB_2}>InfluxDB v2</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="API endpoint (write)"
name="endpoint"
rules={[{ required: true, message: "Please enter an endpoint!" }]}
>
<Input placeholder="http://localhost:8086/api/v2/write" />
</Form.Item>
{selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
<Form.Item label="Username" name="username">
<Input />
</Form.Item>
)}
{selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
<Form.Item label="Password" name="password">
<Input.Password />
</Form.Item>
)}
{selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
<Form.Item label="Database name" name="db" rules={[{ required: true, message: "Please enter database name!" }]}>
<Input />
</Form.Item>
)}
{selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
<Form.Item
label="InfluxDB version"
name="version"
rules={[{ required: true, message: "Please select an InfluxDB version!" }]}
label="Retention policy name"
name="retentionPolicyName"
tooltip="Sets the target retention policy for the write. InfluxDB writes to the DEFAULT retention policy if you do not specify a retention policy."
>
<Select onChange={this.onVersionChange}>
<Select.Option value={InfluxDbVersion.INFLUXDB_1}>InfluxDB v1</Select.Option>
<Select.Option value={InfluxDbVersion.INFLUXDB_2}>InfluxDB v2</Select.Option>
<Input />
</Form.Item>
)}
{selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
<Form.Item label="Select timestamp precision" name="precision">
<Select>
<Select.Option value={InfluxDbPrecision.NS}>Nanosecond</Select.Option>
<Select.Option value={InfluxDbPrecision.U}>Microsecond</Select.Option>
<Select.Option value={InfluxDbPrecision.MS}>Millisecond</Select.Option>
<Select.Option value={InfluxDbPrecision.S}>Second</Select.Option>
<Select.Option value={InfluxDbPrecision.M}>Minute</Select.Option>
<Select.Option value={InfluxDbPrecision.H}>Hour</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="API endpoint (write)"
name="endpoint"
rules={[{ required: true, message: "Please enter an endpoint!" }]}
>
<Input placeholder="http://localhost:8086/api/v2/write" />
)}
{selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
<Form.Item label="Organization" name="organization">
<Input />
</Form.Item>
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
<Form.Item label="Username" name="username">
<Input />
</Form.Item>
)}
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
<Form.Item label="Password" name="password">
<Input.Password />
</Form.Item>
)}
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
<Form.Item
label="Database name"
name="db"
rules={[{ required: true, message: "Please enter database name!" }]}
>
<Input />
</Form.Item>
)}
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
<Form.Item
label="Retention policy name"
name="retentionPolicyName"
tooltip="Sets the target retention policy for the write. InfluxDB writes to the DEFAULT retention policy if you do not specify a retention policy."
>
<Input />
</Form.Item>
)}
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
<Form.Item label="Select timestamp precision" name="precision">
<Select>
<Select.Option value={InfluxDbPrecision.NS}>Nanosecond</Select.Option>
<Select.Option value={InfluxDbPrecision.U}>Microsecond</Select.Option>
<Select.Option value={InfluxDbPrecision.MS}>Millisecond</Select.Option>
<Select.Option value={InfluxDbPrecision.S}>Second</Select.Option>
<Select.Option value={InfluxDbPrecision.M}>Minute</Select.Option>
<Select.Option value={InfluxDbPrecision.H}>Hour</Select.Option>
</Select>
</Form.Item>
)}
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
<Form.Item label="Organization" name="organization">
<Input />
</Form.Item>
)}
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
<Form.Item label="Bucket" name="bucket">
<Input />
</Form.Item>
)}
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
<Form.Item label="Token" name="token">
<Input.Password />
</Form.Item>
)}
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
)}
{selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
<Form.Item label="Bucket" name="bucket">
<Input />
</Form.Item>
</Form>
);
}
)}
{selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
<Form.Item label="Token" name="token">
<Input.Password />
</Form.Item>
)}
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
export default InfluxDbIntegrationForm;

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd";
@ -13,46 +12,44 @@ interface IProps {
add?: boolean;
}
class InfluxdbCard extends Component<IProps> {
onDelete = () => {
function InfluxdbCard(props: IProps) {
const onDelete = () => {
let req = new DeleteInfluxDbIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.deleteInfluxDbIntegration(req, () => {});
};
render() {
let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) {
actions = [
<Link to="integrations/influxdb/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="integrations/influxdb/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="InfluxDB"
className="integration-card"
cover={<img alt="InfluxDB" src="/integrations/influxdb.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The InfluxDB integration writes events into an InfluxDB time-series database." />
</Card>
</Col>
);
if (!!props.add) {
actions = [
<Link to="influxdb/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="influxdb/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="InfluxDB"
className="integration-card"
cover={<img alt="InfluxDB" src="/integrations/influxdb.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The InfluxDB integration writes events into an InfluxDB time-series database." />
</Card>
</Col>
);
}
export default InfluxdbCard;

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd";
@ -13,46 +12,44 @@ interface IProps {
add?: boolean;
}
class LoRaCloudCard extends Component<IProps> {
onDelete = () => {
function LoRaCloudCard(props: IProps) {
const onDelete = () => {
let req = new DeleteLoraCloudIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.deleteLoraCloudIntegration(req, () => {});
};
render() {
let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) {
actions = [
<Link to="integrations/loracloud/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="integrations/loracloud/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="Semtech LoRa Cloud&trade;"
className="integration-card"
cover={<img alt="Semtech LoRa Cloud" src="/integrations/loracloud.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The Semtech LoRa Cloud integration provides Modem & Geolocation Services." />
</Card>
</Col>
);
if (!!props.add) {
actions = [
<Link to="loracloud/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="loracloud/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="Semtech LoRa Cloud&trade;"
className="integration-card"
cover={<img alt="Semtech LoRa Cloud" src="/integrations/loracloud.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The Semtech LoRa Cloud integration provides Modem & Geolocation Services." />
</Card>
</Col>
);
}
export default LoRaCloudCard;

View File

@ -1,4 +1,4 @@
import React, { Component } from "react";
import React, { useState, useEffect } from "react";
import { Form, Input, InputNumber, Switch, Button, Tabs, Collapse } from "antd";
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
@ -13,43 +13,28 @@ interface IProps {
onFinish: (obj: LoraCloudIntegration) => void;
}
interface IState {
modemEnabled: boolean;
geolocationTdoa: boolean;
geolocationRssi: boolean;
geolocationWifi: boolean;
geolocationGnss: boolean;
}
function LoRaCloudIntegrationForm(props: IProps) {
const [modemEnabled, setModemEnabled] = useState<boolean>(false);
const [geolocationTdoa, setGeolocationTdoa] = useState<boolean>(false);
const [geolocationRssi, setGeolocationRssi] = useState<boolean>(false);
const [geolocationWifi, setGeolocationWifi] = useState<boolean>(false);
const [geolocationGnss, setGeolocationGnss] = useState<boolean>(false);
class LoRaCloudIntegrationForm extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
modemEnabled: false,
geolocationTdoa: false,
geolocationRssi: false,
geolocationWifi: false,
geolocationGnss: false,
};
}
componentDidMount() {
const v = this.props.initialValues;
useEffect(() => {
const v = props.initialValues;
const mgs = v.getModemGeolocationServices();
if (mgs !== undefined) {
this.setState({
modemEnabled: mgs.getModemEnabled(),
geolocationTdoa: mgs.getGeolocationTdoa(),
geolocationRssi: mgs.getGeolocationRssi(),
geolocationWifi: mgs.getGeolocationWifi(),
geolocationGnss: mgs.getGeolocationGnss(),
});
setModemEnabled(mgs.getModemEnabled());
setGeolocationTdoa(mgs.getGeolocationTdoa());
setGeolocationRssi(mgs.getGeolocationRssi());
setGeolocationWifi(mgs.getGeolocationWifi());
setGeolocationGnss(mgs.getGeolocationGnss());
}
}
}, [props]);
onFinish = (values: LoraCloudIntegration.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values);
const onFinish = (values: LoraCloudIntegration.AsObject) => {
const v = Object.assign(props.initialValues.toObject(), values);
const mgsv = v.modemGeolocationServices;
let mgs = new LoraCloudModemGeolocationServices();
@ -76,202 +61,194 @@ class LoRaCloudIntegrationForm extends Component<IProps, IState> {
i.setApplicationId(v.applicationId);
i.setModemGeolocationServices(mgs);
this.props.onFinish(i);
props.onFinish(i);
};
onModemEnabledChange = (v: boolean) => {
this.setState({
modemEnabled: v,
});
const onModemEnabledChange = (v: boolean) => {
setModemEnabled(v);
};
onGeolocationTdoaChange = (v: boolean) => {
this.setState({
geolocationTdoa: v,
});
const onGeolocationTdoaChange = (v: boolean) => {
setGeolocationTdoa(v);
};
onGeolocationRssiChange = (v: boolean) => {
this.setState({
geolocationRssi: v,
});
const onGeolocationRssiChange = (v: boolean) => {
setGeolocationRssi(v);
};
onGeolocationWifiChange = (v: boolean) => {
this.setState({
geolocationWifi: v,
});
const onGeolocationWifiChange = (v: boolean) => {
setGeolocationWifi(v);
};
onGeolocationGnssChange = (v: boolean) => {
this.setState({
geolocationGnss: v,
});
const onGeolocationGnssChange = (v: boolean) => {
setGeolocationGnss(v);
};
render() {
return (
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
<Tabs>
<Tabs.TabPane tab="Modem & Geolocation Services" key="1">
return (
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<Tabs>
<Tabs.TabPane tab="Modem & Geolocation Services" key="1">
<Form.Item
label="Token"
name={["modemGeolocationServices", "token"]}
tooltip="This token can be obtained from loracloud.com"
rules={[{ required: true, message: "Please enter a token!" }]}
>
<Input type="password" />
</Form.Item>
<Form.Item
name={["modemGeolocationServices", "modemEnabled"]}
label="I am using LoRa Edge&trade; LR1110 or my device uses LoRa Basics™ Modem-E"
valuePropName="checked"
>
<Switch onChange={onModemEnabledChange} />
</Form.Item>
{modemEnabled && (
<Form.List name={["modemGeolocationServices", "forwardFPortsList"]}>
{(fields, { add, remove }) => (
<Form.Item label="Forward messages on these FPorts to LoRa Cloud">
{fields.map((field, index) => (
<Form.Item
{...field}
rules={[{ required: true, message: "Please a FPort value!" }]}
style={{
display: "inline-block",
width: "100px",
marginRight: "24px",
}}
>
<InputNumber
min={1}
max={255}
addonAfter={<MinusCircleOutlined onClick={() => remove(index)} />}
/>
</Form.Item>
))}
<Button type="dashed" onClick={() => add()} icon={<PlusOutlined />} />
</Form.Item>
)}
</Form.List>
)}
{modemEnabled && (
<Form.Item
label="Token"
name={["modemGeolocationServices", "token"]}
tooltip="This token can be obtained from loracloud.com"
rules={[{ required: true, message: "Please enter a token!" }]}
>
<Input type="password" />
</Form.Item>
<Form.Item
name={["modemGeolocationServices", "modemEnabled"]}
label="I am using LoRa Edge&trade; LR1110 or my device uses LoRa Basics™ Modem-E"
label="Use receive timestamp for GNSS geolocation"
name={["modemGeolocationServices", "gnssUseRxTime"]}
tooltip="If enabled, the receive timestamp of the gateway will be used as reference instead of the timestamp included in the GNSS payload."
valuePropName="checked"
>
<Switch onChange={this.onModemEnabledChange} />
<Switch />
</Form.Item>
{this.state.modemEnabled && (
<Form.List name={["modemGeolocationServices", "forwardFPortsList"]}>
{(fields, { add, remove }) => (
<Form.Item label="Forward messages on these FPorts to LoRa Cloud">
{fields.map((field, index) => (
<Form.Item
{...field}
rules={[{ required: true, message: "Please a FPort value!" }]}
style={{ display: "inline-block", width: "100px", marginRight: "24px" }}
>
<InputNumber
min={1}
max={255}
addonAfter={<MinusCircleOutlined onClick={() => remove(index)} />}
/>
</Form.Item>
))}
<Button type="dashed" onClick={() => add()} icon={<PlusOutlined />} />
</Form.Item>
)}
</Form.List>
)}
{this.state.modemEnabled && (
)}
{modemEnabled && (
<Form.Item
label="Use location of receiving gateways for assistance"
name={["modemGeolocationServices", "gnssUseGatewayLocation"]}
tooltip="If enabled, the gateway location will be provided to the geolocation resolver to aid the resolving process."
valuePropName="checked"
>
<Switch />
</Form.Item>
)}
{modemEnabled && (
<Form.Item
label="My device adheres to the LoRa Edge&trade; Tracker Modem-E Version Reference Design protocol"
name={["modemGeolocationServices", "parseTlv"]}
tooltip="If enabled, ChirpStack will try to resolve the location of the device if a geolocation payload is detected."
valuePropName="checked"
>
<Switch />
</Form.Item>
)}
<Collapse style={{ marginBottom: 24 }}>
<Collapse.Panel header="Advanced geolocation options" key={1}>
<Form.Item
label="Use receive timestamp for GNSS geolocation"
name={["modemGeolocationServices", "gnssUseRxTime"]}
tooltip="If enabled, the receive timestamp of the gateway will be used as reference instead of the timestamp included in the GNSS payload."
label="TDOA based geolocation"
name={["modemGeolocationServices", "geolocationTdoa"]}
tooltip="If enabled, geolocation will be based on time-difference of arrival (TDOA). Please note that this requires gateways that support the fine-timestamp feature."
valuePropName="checked"
>
<Switch />
<Switch onChange={onGeolocationTdoaChange} />
</Form.Item>
)}
{this.state.modemEnabled && (
<Form.Item
label="Use location of receiving gateways for assistance"
name={["modemGeolocationServices", "gnssUseGatewayLocation"]}
tooltip="If enabled, the gateway location will be provided to the geolocation resolver to aid the resolving process."
label="RSSI based geolocation"
name={["modemGeolocationServices", "geolocationRssi"]}
tooltip="If enabled, geolocation will be based on RSSI values reported by the receiving gateways."
valuePropName="checked"
>
<Switch />
<Switch onChange={onGeolocationRssiChange} />
</Form.Item>
)}
{this.state.modemEnabled && (
<Form.Item
label="My device adheres to the LoRa Edge&trade; Tracker Modem-E Version Reference Design protocol"
name={["modemGeolocationServices", "parseTlv"]}
tooltip="If enabled, ChirpStack will try to resolve the location of the device if a geolocation payload is detected."
label="Wi-Fi based geolocation"
name={["modemGeolocationServices", "geolocationWifi"]}
tooltip="If enabled, geolocation will be based on Wi-Fi access-point data reported by the device."
valuePropName="checked"
>
<Switch />
<Switch onChange={onGeolocationWifiChange} />
</Form.Item>
)}
<Collapse style={{ marginBottom: 24 }}>
<Collapse.Panel header="Advanced geolocation options" key={1}>
<Form.Item
label="GNSS based geolocation (LR1110)"
name={["modemGeolocationServices", "geolocationGnss"]}
tooltip="If enabled, geolocation will be based on GNSS data reported by the device."
valuePropName="checked"
>
<Switch onChange={onGeolocationGnssChange} />
</Form.Item>
{(geolocationTdoa || geolocationRssi) && (
<Form.Item
label="TDOA based geolocation"
name={["modemGeolocationServices", "geolocationTdoa"]}
tooltip="If enabled, geolocation will be based on time-difference of arrival (TDOA). Please note that this requires gateways that support the fine-timestamp feature."
label="Geolocation buffer (TTL in seconds)"
name={["modemGeolocationServices", "geolocationBufferTtl"]}
tooltip="The time in seconds that historical uplinks will be stored in the geolocation buffer. Used for TDOA and RSSI geolocation."
>
<InputNumber min={0} max={86400} />
</Form.Item>
)}
{(geolocationTdoa || geolocationRssi) && (
<Form.Item
label="Geolocation min buffer size"
name={["modemGeolocationServices", "geolocationMinBufferSize"]}
tooltip="The minimum buffer size required before using geolocation. Using multiple uplinks for geolocation can increase the accuracy of the geolocation results. Used for TDOA and RSSI geolocation."
>
<InputNumber min={0} />
</Form.Item>
)}
{geolocationWifi && (
<Form.Item
label="Wifi payload field"
name={["modemGeolocationServices", "geolocationWifiPayloadField"]}
tooltip="This must match the name of the field in the decoded payload which holds array of Wifi access-points. Each element in the array must contain two keys: 1) macAddress: array of 6 bytes, 2) signalStrength: RSSI of the access-point."
>
<Input />
</Form.Item>
)}
{geolocationGnss && (
<Form.Item
label="GNSS payload field"
name={["modemGeolocationServices", "geolocationGnssPayloadField"]}
tooltip="This must match the name of the field in the decoded payload which holds the LR1110 GNSS bytes."
>
<Input />
</Form.Item>
)}
{geolocationGnss && (
<Form.Item
label="Use receive timestamp for GNSS geolocation"
name={["modemGeolocationServices", "geolocationGnssUseRxTime"]}
valuePropName="checked"
>
<Switch onChange={this.onGeolocationTdoaChange} />
<Switch />
</Form.Item>
<Form.Item
label="RSSI based geolocation"
name={["modemGeolocationServices", "geolocationRssi"]}
tooltip="If enabled, geolocation will be based on RSSI values reported by the receiving gateways."
valuePropName="checked"
>
<Switch onChange={this.onGeolocationRssiChange} />
</Form.Item>
<Form.Item
label="Wi-Fi based geolocation"
name={["modemGeolocationServices", "geolocationWifi"]}
tooltip="If enabled, geolocation will be based on Wi-Fi access-point data reported by the device."
valuePropName="checked"
>
<Switch onChange={this.onGeolocationWifiChange} />
</Form.Item>
<Form.Item
label="GNSS based geolocation (LR1110)"
name={["modemGeolocationServices", "geolocationGnss"]}
tooltip="If enabled, geolocation will be based on GNSS data reported by the device."
valuePropName="checked"
>
<Switch onChange={this.onGeolocationGnssChange} />
</Form.Item>
{(this.state.geolocationTdoa || this.state.geolocationRssi) && (
<Form.Item
label="Geolocation buffer (TTL in seconds)"
name={["modemGeolocationServices", "geolocationBufferTtl"]}
tooltip="The time in seconds that historical uplinks will be stored in the geolocation buffer. Used for TDOA and RSSI geolocation."
>
<InputNumber min={0} max={86400} />
</Form.Item>
)}
{(this.state.geolocationTdoa || this.state.geolocationRssi) && (
<Form.Item
label="Geolocation min buffer size"
name={["modemGeolocationServices", "geolocationMinBufferSize"]}
tooltip="The minimum buffer size required before using geolocation. Using multiple uplinks for geolocation can increase the accuracy of the geolocation results. Used for TDOA and RSSI geolocation."
>
<InputNumber min={0} />
</Form.Item>
)}
{this.state.geolocationWifi && (
<Form.Item
label="Wifi payload field"
name={["modemGeolocationServices", "geolocationWifiPayloadField"]}
tooltip="This must match the name of the field in the decoded payload which holds array of Wifi access-points. Each element in the array must contain two keys: 1) macAddress: array of 6 bytes, 2) signalStrength: RSSI of the access-point."
>
<Input />
</Form.Item>
)}
{this.state.geolocationGnss && (
<Form.Item
label="GNSS payload field"
name={["modemGeolocationServices", "geolocationGnssPayloadField"]}
tooltip="This must match the name of the field in the decoded payload which holds the LR1110 GNSS bytes."
>
<Input />
</Form.Item>
)}
{this.state.geolocationGnss && (
<Form.Item
label="Use receive timestamp for GNSS geolocation"
name={["modemGeolocationServices", "geolocationGnssUseRxTime"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
)}
</Collapse.Panel>
</Collapse>
</Tabs.TabPane>
</Tabs>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
)}
</Collapse.Panel>
</Collapse>
</Tabs.TabPane>
</Tabs>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
export default LoRaCloudIntegrationForm;

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Col, Card } from "antd";
@ -9,23 +8,21 @@ interface IProps {
application: Application;
}
class HttpCard extends Component<IProps> {
render() {
let actions: any[] = [<Link to="integrations/mqtt/certificate">Get certificate</Link>];
function MqttCard(props: IProps) {
let actions: any[] = [<Link to="mqtt/certificate">Get certificate</Link>];
return (
<Col span={8}>
<Card
title="MQTT"
className="integration-card"
cover={<img alt="MQTT" src="/integrations/mqtt.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The MQTT integration forwards events to a MQTT broker." />
</Card>
</Col>
);
}
return (
<Col span={8}>
<Card
title="MQTT"
className="integration-card"
cover={<img alt="MQTT" src="/integrations/mqtt.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The MQTT integration forwards events to a MQTT broker." />
</Card>
</Col>
);
}
export default HttpCard;
export default MqttCard;

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd";
@ -13,46 +12,44 @@ interface IProps {
add?: boolean;
}
class MyDevicesCard extends Component<IProps> {
onDelete = () => {
function MyDevicesCard(props: IProps) {
const onDelete = () => {
let req = new DeleteMyDevicesIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.deleteMyDevicesIntegration(req, () => {});
};
render() {
let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) {
actions = [
<Link to="integrations/mydevices/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="integrations/mydevices/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="myDevices"
className="integration-card"
cover={<img alt="myDevices" src="/integrations/my_devices.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The myDevices integration forwards events to the myDevices platform." />
</Card>
</Col>
);
if (!!props.add) {
actions = [
<Link to="mydevices/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="mydevices/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="myDevices"
className="integration-card"
cover={<img alt="myDevices" src="/integrations/my_devices.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The myDevices integration forwards events to the myDevices platform." />
</Card>
</Col>
);
}
export default MyDevicesCard;

View File

@ -1,4 +1,4 @@
import React, { Component } from "react";
import React, { useState } from "react";
import { Form, Input, Button, Select } from "antd";
@ -9,79 +9,63 @@ interface IProps {
onFinish: (obj: MyDevicesIntegration) => void;
}
interface IState {
selectedEndpoint: string;
customEndpoint: string;
}
function MyDevicesIntegrationForm(props: IProps) {
const [selectedEndpoint, setSelectedEndpoint] = useState<string>("");
const [customEndpoint, setCustomEndpoint] = useState<string>("");
class MyDevicesIntegrationForm extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
selectedEndpoint: "",
customEndpoint: "",
};
}
onFinish = (values: MyDevicesIntegration.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values);
const onFinish = (values: MyDevicesIntegration.AsObject) => {
const v = Object.assign(props.initialValues.toObject(), values);
let i = new MyDevicesIntegration();
i.setApplicationId(v.applicationId);
if (v.endpoint === "custom") {
i.setEndpoint(this.state.customEndpoint);
i.setEndpoint(customEndpoint);
} else {
i.setEndpoint(v.endpoint);
}
this.props.onFinish(i);
props.onFinish(i);
};
onEndpointChange = (v: string) => {
this.setState({
selectedEndpoint: v,
});
const onEndpointChange = (v: string) => {
setSelectedEndpoint(v);
};
onCustomEndpointChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
customEndpoint: e.target.value,
});
const onCustomEndpointChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCustomEndpoint(e.target.value);
};
render() {
return (
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
return (
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<Form.Item
label="Select myDevices endpoint"
name="endpoint"
rules={[{ required: true, message: "Please select a myDevices endpoint!" }]}
>
<Select onChange={onEndpointChange}>
<Select.Option value="https://lora.mydevices.com/v1/networks/chirpstackio/uplink">Cayenne</Select.Option>
<Select.Option value="https://lora.iotinabox.com/v1/networks/iotinabox.chirpstackio/uplink">
IoT in a Box
</Select.Option>
<Select.Option value="custom">Custom endpoint URL</Select.Option>
</Select>
</Form.Item>
{selectedEndpoint === "custom" && (
<Form.Item
label="Select myDevices endpoint"
name="endpoint"
rules={[{ required: true, message: "Please select a myDevices endpoint!" }]}
label="myDevices API endpoint"
name="customEndpoint"
rules={[{ required: true, message: "Please enter an API endpoint!" }]}
>
<Select onChange={this.onEndpointChange}>
<Select.Option value="https://lora.mydevices.com/v1/networks/chirpstackio/uplink">Cayenne</Select.Option>
<Select.Option value="https://lora.iotinabox.com/v1/networks/iotinabox.chirpstackio/uplink">
IoT in a Box
</Select.Option>
<Select.Option value="custom">Custom endpoint URL</Select.Option>
</Select>
<Input onChange={onCustomEndpointChange} />
</Form.Item>
{this.state.selectedEndpoint === "custom" && (
<Form.Item
label="myDevices API endpoint"
name="customEndpoint"
rules={[{ required: true, message: "Please enter an API endpoint!" }]}
>
<Input onChange={this.onCustomEndpointChange} />
</Form.Item>
)}
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
)}
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
export default MyDevicesIntegrationForm;

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd";
@ -16,46 +15,44 @@ interface IProps {
add?: boolean;
}
class PilotThingsCard extends Component<IProps> {
onDelete = () => {
function PilotThingsCard(props: IProps) {
const onDelete = () => {
let req = new DeletePilotThingsIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.deletePilotThingsIntegration(req, () => {});
};
render() {
let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) {
actions = [
<Link to="integrations/pilot-things/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="integrations/pilot-things/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="Pilot Things"
className="integration-card"
cover={<img alt="Pilot Things" src="/integrations/pilot_things.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The Pilot Things integration forwards messages to a Pilot Things instance." />
</Card>
</Col>
);
if (!!props.add) {
actions = [
<Link to="pilot-things/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="pilot-things/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="Pilot Things"
className="integration-card"
cover={<img alt="Pilot Things" src="/integrations/pilot_things.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The Pilot Things integration forwards messages to a Pilot Things instance." />
</Card>
</Col>
);
}
export default PilotThingsCard;

View File

@ -1,5 +1,3 @@
import React, { Component } from "react";
import { Form, Input, Button } from "antd";
import { PilotThingsIntegration } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
@ -9,43 +7,41 @@ interface IProps {
onFinish: (obj: PilotThingsIntegration) => void;
}
class PilotThingsIntegrationForm extends Component<IProps> {
onFinish = (values: PilotThingsIntegration.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values);
function PilotThingsIntegrationForm(props: IProps) {
const onFinish = (values: PilotThingsIntegration.AsObject) => {
const v = Object.assign(props.initialValues.toObject(), values);
let i = new PilotThingsIntegration();
i.setApplicationId(v.applicationId);
i.setServer(v.server);
i.setToken(v.token);
this.props.onFinish(i);
props.onFinish(i);
};
render() {
return (
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
<Form.Item
label="Pilot Things server"
name="server"
rules={[{ required: true, message: "Please enter a Pilot Things server!" }]}
>
<Input placeholder="https://host:port" />
</Form.Item>
<Form.Item
label="Authentication token"
name="token"
rules={[{ required: true, message: "Please enter a Pilot Things token!" }]}
>
<Input.Password />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
return (
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<Form.Item
label="Pilot Things server"
name="server"
rules={[{ required: true, message: "Please enter a Pilot Things server!" }]}
>
<Input placeholder="https://host:port" />
</Form.Item>
<Form.Item
label="Authentication token"
name="token"
rules={[{ required: true, message: "Please enter a Pilot Things token!" }]}
>
<Input.Password />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
export default PilotThingsIntegrationForm;

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd";
@ -16,46 +15,44 @@ interface IProps {
add?: boolean;
}
class ThingsBoardCard extends Component<IProps> {
onDelete = () => {
function ThingsBoardCard(props: IProps) {
const onDelete = () => {
let req = new DeleteThingsBoardIntegrationRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
ApplicationStore.deleteThingsBoardIntegration(req, () => {});
};
render() {
let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) {
actions = [
<Link to="integrations/thingsboard/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="integrations/thingsboard/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="ThingsBoard"
className="integration-card"
cover={<img alt="ThingsBoard" src="/integrations/thingsboard.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The ThingsBoard integration forwards events to a ThingsBoard instance." />
</Card>
</Col>
);
if (!!props.add) {
actions = [
<Link to="thingsboard/create">
<PlusOutlined />
</Link>,
];
} else {
actions = [
<Link to="thingsboard/edit">
<EditOutlined />
</Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined />
</Popconfirm>,
];
}
return (
<Col span={8}>
<Card
title="ThingsBoard"
className="integration-card"
cover={<img alt="ThingsBoard" src="/integrations/thingsboard.png" style={{ padding: 1 }} />}
actions={actions}
>
<Card.Meta description="The ThingsBoard integration forwards events to a ThingsBoard instance." />
</Card>
</Col>
);
}
export default ThingsBoardCard;

View File

@ -1,5 +1,3 @@
import React, { Component } from "react";
import { Form, Input, Button, Typography } from "antd";
import { ThingsBoardIntegration } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
@ -9,41 +7,44 @@ interface IProps {
onFinish: (obj: ThingsBoardIntegration) => void;
}
class ThingsBoardIntegrationForm extends Component<IProps> {
onFinish = (values: ThingsBoardIntegration.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values);
function ThingsBoardIntegrationForm(props: IProps) {
const onFinish = (values: ThingsBoardIntegration.AsObject) => {
const v = Object.assign(props.initialValues.toObject(), values);
let i = new ThingsBoardIntegration();
i.setApplicationId(v.applicationId);
i.setServer(v.server);
this.props.onFinish(i);
props.onFinish(i);
};
render() {
return (
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
<Form.Item
label="ThingsBoard server"
name="server"
rules={[{ required: true, message: "Please enter the address to the ThingsBoard server!" }]}
>
<Input placeholder="http://host:port" />
</Form.Item>
<Form.Item>
<Typography.Paragraph>
Each device must have a 'ThingsBoardAccessToken' variable assigned. This access-token is generated by
ThingsBoard.
</Typography.Paragraph>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
return (
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<Form.Item
label="ThingsBoard server"
name="server"
rules={[
{
required: true,
message: "Please enter the address to the ThingsBoard server!",
},
]}
>
<Input placeholder="http://host:port" />
</Form.Item>
<Form.Item>
<Typography.Paragraph>
Each device must have a 'ThingsBoardAccessToken' variable assigned. This access-token is generated by
ThingsBoard.
</Typography.Paragraph>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
export default ThingsBoardIntegrationForm;

View File

@ -1,8 +1,9 @@
import React, { Component } from "react";
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { presetPalettes } from "@ant-design/colors";
import { Space, Breadcrumb, Card, Row, Col, PageHeader, Empty } from "antd";
import { Space, Breadcrumb, Card, Row, Col, Empty } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import moment from "moment";
import { LatLngTuple, PointTuple } from "leaflet";
@ -27,167 +28,125 @@ import InternalStore from "../../stores/InternalStore";
import GatewayStore from "../../stores/GatewayStore";
import Map, { Marker, MarkerColor } from "../../components/Map";
interface GatewaysMapState {
items: GatewayListItem[];
}
function GatewaysMap() {
const [items, setItems] = useState<GatewayListItem[]>([]);
class GatewaysMap extends Component<{}, GatewaysMapState> {
constructor(props: {}) {
super(props);
this.state = {
items: [],
};
}
componentDidMount() {
this.loadData();
}
loadData = () => {
useEffect(() => {
let req = new ListGatewaysRequest();
req.setLimit(9999);
GatewayStore.list(req, (resp: ListGatewaysResponse) => {
this.setState({
items: resp.getResultList(),
});
setItems(resp.getResultList());
});
}, []);
if (items.length === 0) {
return <Empty />;
}
const boundsOptions: {
padding: PointTuple;
} = {
padding: [50, 50],
};
render() {
if (this.state.items.length === 0) {
return <Empty />;
let bounds: LatLngTuple[] = [];
let markers: any[] = [];
for (const item of items) {
const pos: LatLngTuple = [item.getLocation()!.getLatitude(), item.getLocation()!.getLongitude()];
bounds.push(pos);
let color: MarkerColor = "orange";
let lastSeen: string = "Never seen online";
if (item.getState() === GatewayState.OFFLINE) {
color = "red";
} else if (item.getState() === GatewayState.ONLINE) {
color = "green";
}
const boundsOptions: {
padding: PointTuple;
} = {
padding: [50, 50],
};
let bounds: LatLngTuple[] = [];
let markers: any[] = [];
for (const item of this.state.items) {
const pos: LatLngTuple = [item.getLocation()!.getLatitude(), item.getLocation()!.getLongitude()];
bounds.push(pos);
let color: MarkerColor = "orange";
let lastSeen: string = "Never seen online";
if (item.getState() === GatewayState.OFFLINE) {
color = "red";
} else if (item.getState() === GatewayState.ONLINE) {
color = "green";
}
if (item.getLastSeenAt() !== undefined) {
let ts = moment(item.getLastSeenAt()!.toDate());
lastSeen = ts.fromNow();
}
markers.push(
<Marker position={pos} faIcon="wifi" color={color}>
<Popup>
<Link to={`/tenants/${item.getTenantId()}/gateways/${item.getGatewayId()}`}>{item.getName()}</Link>
<br />
{item.getGatewayId()}
<br />
<br />
{lastSeen}
</Popup>
</Marker>,
);
if (item.getLastSeenAt() !== undefined) {
let ts = moment(item.getLastSeenAt()!.toDate());
lastSeen = ts.fromNow();
}
return (
<Map height={500} bounds={bounds} boundsOptions={boundsOptions}>
{markers}
</Map>
markers.push(
<Marker position={pos} faIcon="wifi" color={color}>
<Popup>
<Link to={`/tenants/${item.getTenantId()}/gateways/${item.getGatewayId()}`}>{item.getName()}</Link>
<br />
{item.getGatewayId()}
<br />
<br />
{lastSeen}
</Popup>
</Marker>,
);
}
return (
<Map height={500} bounds={bounds} boundsOptions={boundsOptions}>
{markers}
</Map>
);
}
interface GatewayProps {
summary?: GetGatewaysSummaryResponse;
}
class GatewaysActiveInactive extends Component<GatewayProps> {
render() {
if (
this.props.summary === undefined ||
(this.props.summary.getNeverSeenCount() === 0 &&
this.props.summary.getOfflineCount() === 0 &&
this.props.summary.getOnlineCount() === 0)
) {
return <Empty />;
}
const data = {
labels: ["Never seen", "Offline", "Online"],
datasets: [
{
data: [
this.props.summary.getNeverSeenCount(),
this.props.summary.getOfflineCount(),
this.props.summary.getOnlineCount(),
],
backgroundColor: [presetPalettes.orange.primary, presetPalettes.red.primary, presetPalettes.green.primary],
},
],
};
const options: {
animation: false;
} = {
animation: false,
};
return <Doughnut data={data} options={options} className="chart-doughtnut" />;
function DevicesActiveInactive({ summary }: { summary?: GetDevicesSummaryResponse }) {
if (
summary === undefined ||
(summary.getNeverSeenCount() === 0 && summary.getInactiveCount() === 0 && summary.getActiveCount() === 0)
) {
return <Empty />;
}
const data = {
labels: ["Never seen", "Inactive", "Active"],
datasets: [
{
data: [summary.getNeverSeenCount(), summary.getInactiveCount(), summary.getActiveCount()],
backgroundColor: [presetPalettes.orange.primary, presetPalettes.red.primary, presetPalettes.green.primary],
},
],
};
const options: {
animation: false;
} = {
animation: false,
};
return <Doughnut data={data} options={options} className="chart-doughtnut" />;
}
interface DeviceProps {
summary?: GetDevicesSummaryResponse;
}
class DevicesActiveInactive extends Component<DeviceProps> {
render() {
if (
this.props.summary === undefined ||
(this.props.summary.getNeverSeenCount() === 0 &&
this.props.summary.getInactiveCount() === 0 &&
this.props.summary.getActiveCount() === 0)
) {
return <Empty />;
}
const data = {
labels: ["Never seen", "Inactive", "Active"],
datasets: [
{
data: [
this.props.summary.getNeverSeenCount(),
this.props.summary.getInactiveCount(),
this.props.summary.getActiveCount(),
],
backgroundColor: [presetPalettes.orange.primary, presetPalettes.red.primary, presetPalettes.green.primary],
},
],
};
const options: {
animation: false;
} = {
animation: false,
};
return <Doughnut data={data} options={options} className="chart-doughtnut" />;
function GatewaysActiveInactive({ summary }: { summary?: GetGatewaysSummaryResponse }) {
if (
summary === undefined ||
(summary.getNeverSeenCount() === 0 && summary.getOfflineCount() === 0 && summary.getOnlineCount() === 0)
) {
return <Empty />;
}
const data = {
labels: ["Never seen", "Offline", "Online"],
datasets: [
{
data: [summary.getNeverSeenCount(), summary.getOfflineCount(), summary.getOnlineCount()],
backgroundColor: [presetPalettes.orange.primary, presetPalettes.red.primary, presetPalettes.green.primary],
},
],
};
const options: {
animation: false;
} = {
animation: false,
};
return <Doughnut data={data} options={options} className="chart-doughtnut" />;
}
class DevicesDataRates extends Component<DeviceProps> {
getColor = (dr: number) => {
function DevicesDataRates({ summary }: { summary?: GetDevicesSummaryResponse }) {
const getColor = (dr: number) => {
return [
"#ff5722",
"#ff9800",
@ -207,109 +166,92 @@ class DevicesDataRates extends Component<DeviceProps> {
][dr];
};
render() {
if (this.props.summary === undefined || this.props.summary.getDrCountMap().toArray().length === 0) {
return <Empty />;
}
let data: {
labels: string[];
datasets: {
data: number[];
backgroundColor: string[];
}[];
} = {
labels: [],
datasets: [
{
data: [],
backgroundColor: [],
},
],
};
for (const elm of this.props.summary.getDrCountMap().toArray()) {
data.labels.push(`DR${elm[0]}`);
data.datasets[0].data.push(elm[1]);
data.datasets[0].backgroundColor.push(this.getColor(elm[0]));
}
const options: {
animation: false;
} = {
animation: false,
};
return <Doughnut data={data} options={options} className="chart-doughtnut" />;
}
}
interface IProps {}
interface IState {
gatewaysSummary?: GetGatewaysSummaryResponse;
devicesSummary?: GetDevicesSummaryResponse;
}
class Dashboard extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
if (summary === undefined || summary.getDrCountMap().toArray().length === 0) {
return <Empty />;
}
componentDidMount() {
let data: {
labels: string[];
datasets: {
data: number[];
backgroundColor: string[];
}[];
} = {
labels: [],
datasets: [
{
data: [],
backgroundColor: [],
},
],
};
for (const elm of summary.getDrCountMap().toArray()) {
data.labels.push(`DR${elm[0]}`);
data.datasets[0].data.push(elm[1]);
data.datasets[0].backgroundColor.push(getColor(elm[0]));
}
const options: {
animation: false;
} = {
animation: false,
};
return <Doughnut data={data} options={options} className="chart-doughtnut" />;
}
function Dashboard() {
const [gatewaysSummary, setGatewaysSummary] = useState<GetGatewaysSummaryResponse | undefined>(undefined);
const [devicesSummary, setDevicesSummary] = useState<GetDevicesSummaryResponse | undefined>(undefined);
useEffect(() => {
InternalStore.getGatewaysSummary(new GetGatewaysSummaryRequest(), (resp: GetGatewaysSummaryResponse) => {
this.setState({
gatewaysSummary: resp,
});
setGatewaysSummary(resp);
});
InternalStore.getDevicesSummary(new GetDevicesSummaryRequest(), (resp: GetDevicesSummaryResponse) => {
this.setState({
devicesSummary: resp,
});
setDevicesSummary(resp);
});
}
}, []);
render() {
return (
<Space direction="vertical" style={{ width: "100%" }} size="large">
<PageHeader
breadcrumbRender={() => (
<Breadcrumb>
<Breadcrumb.Item>
<span>Network Server</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>Dashboard</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title="Dashboard"
/>
<Row gutter={24}>
<Col span={8}>
<Card title="Active devices">
<DevicesActiveInactive summary={this.state.devicesSummary} />
</Card>
</Col>
<Col span={8}>
<Card title="Active gateways">
<GatewaysActiveInactive summary={this.state.gatewaysSummary} />
</Card>
</Col>
<Col span={8}>
<Card title="Device data-rate usage">
<DevicesDataRates summary={this.state.devicesSummary} />
</Card>
</Col>
</Row>
<Card title="Gateway map">
<GatewaysMap />
</Card>
</Space>
);
}
return (
<Space direction="vertical" style={{ width: "100%" }} size="large">
<PageHeader
breadcrumbRender={() => (
<Breadcrumb>
<Breadcrumb.Item>
<span>Network Server</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>Dashboard</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title="Dashboard"
/>
<Row gutter={24}>
<Col span={8}>
<Card title="Active devices">
<DevicesActiveInactive summary={devicesSummary} />
</Card>
</Col>
<Col span={8}>
<Card title="Active gateways">
<GatewaysActiveInactive summary={gatewaysSummary} />
</Card>
</Col>
<Col span={8}>
<Card title="Device data-rate usage">
<DevicesDataRates summary={devicesSummary} />
</Card>
</Col>
</Row>
<Card title="Gateway map">
<GatewaysMap />
</Card>
</Space>
);
}
export default Dashboard;

View File

@ -1,7 +1,7 @@
import React, { Component } from "react";
import { Link, RouteComponentProps } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom";
import { Space, Breadcrumb, Card, PageHeader } from "antd";
import { Space, Breadcrumb, Card } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import { MacVersion, RegParamsRevision } from "@chirpstack/chirpstack-api-grpc-web/common/common_pb";
import {
@ -12,86 +12,86 @@ import {
import DeviceProfileTemplateForm from "./DeviceProfileTemplateForm";
import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore";
class CreateDeviceProfileTemplate extends Component<RouteComponentProps> {
onFinish = (obj: DeviceProfileTemplate) => {
function CreateDeviceProfileTemplate() {
const navigate = useNavigate();
const onFinish = (obj: DeviceProfileTemplate) => {
let req = new CreateDeviceProfileTemplateRequest();
req.setDeviceProfileTemplate(obj);
DeviceProfileTemplateStore.create(req, () => {
this.props.history.push(`/device-profile-templates`);
navigate(`/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 {
data: {
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 {
bytes: [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);
deviceProfileTemplate.setAutoDetectMeasurements(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>
);
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 {
data: {
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 {
bytes: [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);
deviceProfileTemplate.setAutoDetectMeasurements(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={onFinish} />
</Card>
</Space>
);
}
export default CreateDeviceProfileTemplate;

View File

@ -1,4 +1,4 @@
import React, { Component } from "react";
import React, { useState, useEffect } from "react";
import { Form, Input, Select, InputNumber, Switch, Row, Col, Button, Tabs, Card } from "antd";
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
@ -17,37 +17,20 @@ interface IProps {
update?: boolean;
}
interface IState {
supportsOtaa: boolean;
supportsClassB: boolean;
supportsClassC: boolean;
payloadCodecRuntime: CodecRuntime;
adrAlgorithms: [string, string][];
}
function DeviceProfileTemplateForm(props: IProps) {
const [form] = Form.useForm();
const [supportsOtaa, setSupportsOtaa] = useState<boolean>(false);
const [supportsClassB, setSupportsClassB] = useState<boolean>(false);
const [supportsClassC, setSupportsClassC] = useState<boolean>(false);
const [payloadCodecRuntime, setPayloadCodecRuntime] = useState<CodecRuntime>(CodecRuntime.NONE);
const [adrAlgorithms, setAdrAlgorithms] = useState<[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(),
});
useEffect(() => {
const v = props.initialValues;
setSupportsOtaa(v.getSupportsOtaa());
setSupportsClassB(v.getSupportsClassB());
setSupportsClassC(v.getSupportsClassC());
setPayloadCodecRuntime(v.getPayloadCodecRuntime());
DeviceProfileStore.listAdrAlgorithms((resp: ListDeviceProfileAdrAlgorithmsResponse) => {
let adrAlgorithms: [string, string][] = [];
@ -55,14 +38,12 @@ class DeviceProfileTemplateForm extends Component<IProps, IState> {
adrAlgorithms.push([a.getId(), a.getName()]);
}
this.setState({
adrAlgorithms: adrAlgorithms,
});
setAdrAlgorithms(adrAlgorithms);
});
}
}, [props.initialValues]);
onFinish = (values: DeviceProfileTemplate.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values);
const onFinish = (values: DeviceProfileTemplate.AsObject) => {
const v = Object.assign(props.initialValues.toObject(), values);
let dp = new DeviceProfileTemplate();
dp.setId(v.id);
@ -114,454 +95,487 @@ class DeviceProfileTemplateForm extends Component<IProps, IState> {
}
dp.setAutoDetectMeasurements(v.autoDetectMeasurements);
this.props.onFinish(dp);
props.onFinish(dp);
};
onSupportsOtaaChange = (checked: boolean) => {
this.setState({
supportsOtaa: checked,
});
const onSupportsOtaaChange = (checked: boolean) => {
setSupportsOtaa(checked);
};
onSupportsClassBChnage = (checked: boolean) => {
this.setState({
supportsClassB: checked,
});
const onSupportsClassBChnage = (checked: boolean) => {
setSupportsClassB(checked);
};
onSupportsClassCChange = (checked: boolean) => {
this.setState({
supportsClassC: checked,
});
const onSupportsClassCChange = (checked: boolean) => {
setSupportsClassC(checked);
};
onPayloadCodecRuntimeChange = (value: CodecRuntime) => {
this.setState({
payloadCodecRuntime: value,
});
const onPayloadCodecRuntimeChange = (value: CodecRuntime) => {
setPayloadCodecRuntime(value);
};
render() {
const adrOptions = this.state.adrAlgorithms.map(v => <Select.Option value={v[0]}>{v[1]}</Select.Option>);
const adrOptions = 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}>
return (
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
<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={!!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={onSupportsOtaaChange} />
</Form.Item>
{!supportsOtaa && (
<Row>
<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!" }]}
label="RX1 delay"
name="abpRx1Delay"
rules={[{ required: true, message: "Please enter a RX1 delay!" }]}
>
<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>
<InputNumber min={0} max={15} />
</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!" }]}
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!",
},
]}
>
<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>
<InputNumber min={0} max={15} />
</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}>
)}
{!supportsOtaa && (
<Row>
<Col span={12}>
<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."
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!",
},
]}
>
<Switch />
<InputNumber min={0} max={15} />
</Form.Item>
</Col>
<Col span={8}>
<Col span={12}>
<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!" }]}
label="RX2 frequency (Hz)"
name="abpRx2Freq"
rules={[
{
required: true,
message: "Please enter a RX2 frequency!",
},
]}
>
<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} />
<InputNumber min={0} style={{ width: "200px" }} />
</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>
)}
</Tabs.TabPane>
<Tabs.TabPane tab="Class-B" key="3">
<Form.Item label="Device supports Class-B" name="supportsClassB" valuePropName="checked">
<Switch onChange={onSupportsClassBChnage} />
</Form.Item>
{supportsClassB && (
<>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label="RX1 delay"
name="abpRx1Delay"
rules={[{ required: true, message: "Please enter a RX1 delay!" }]}
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} max={15} />
<InputNumber min={0} />
</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!" }]}
label="Class-B ping-slot periodicity"
tooltip="This value must match the ping-slot periodicity of the device. Please refer to the device documentation."
name="classBPingSlotNbK"
rules={[
{
required: true,
message: "Please select the ping-slot periodicity!",
},
]}
>
<InputNumber min={0} max={15} />
<Select>
<Select.Option value={0}>Every second</Select.Option>
<Select.Option value={1}>Every 2 seconds</Select.Option>
<Select.Option value={2}>Every 4 seconds</Select.Option>
<Select.Option value={3}>Every 8 seconds</Select.Option>
<Select.Option value={4}>Every 16 seconds</Select.Option>
<Select.Option value={5}>Every 32 seconds</Select.Option>
<Select.Option value={6}>Every 64 seconds</Select.Option>
<Select.Option value={7}>Every 128 seconds</Select.Option>
</Select>
</Form.Item>
</Col>
</Row>
)}
{!this.state.supportsOtaa && (
<Row>
<Row gutter={24}>
<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!" }]}
label="Class-B ping-slot data-rate"
tooltip="This value must match the ping-slot data-rate of the device. Please refer to the device documentation."
name="classBPingSlotDr"
rules={[
{
required: true,
message: "Please enter the ping-slot data-rate!",
},
]}
>
<InputNumber min={0} max={15} />
<InputNumber min={0} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="RX2 frequency (Hz)"
name="abpRx2Freq"
rules={[{ required: true, message: "Please enter a RX2 frequency!" }]}
label="Class-B ping-slot frequency (Hz)"
tooltip="This value must match the ping-slot frequency of the device. Please refer to the device documentation."
name="classBPingSlotFreq"
rules={[
{
required: true,
message: "Please enter the ping-slot 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} />
</>
)}
</Tabs.TabPane>
<Tabs.TabPane tab="Class-C" key="4">
<Form.Item label="Device supports Class-C" name="supportsClassC" valuePropName="checked">
<Switch onChange={onSupportsClassCChange} />
</Form.Item>
{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>
{this.state.supportsClassB && (
)}
</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={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>
{payloadCodecRuntime === CodecRuntime.JS && <CodeEditor label="Codec functions" name="payloadCodecScript" />}
</Tabs.TabPane>
<Tabs.TabPane tab="Tags" key="6">
<Form.List name="tagsMap">
{(fields, { add, remove }) => (
<>
<Row gutter={24}>
<Col span={12}>
<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>
</Col>
<Col span={12}>
<Form.Item
label="Class-B ping-slot periodicity"
tooltip="This value must match the ping-slot periodicity of the device. Please refer to the device documentation."
name="classBPingSlotNbK"
rules={[{ required: true, message: "Please select the ping-slot periodicity!" }]}
>
<Select>
<Select.Option value={0}>Every second</Select.Option>
<Select.Option value={1}>Every 2 seconds</Select.Option>
<Select.Option value={2}>Every 4 seconds</Select.Option>
<Select.Option value={3}>Every 8 seconds</Select.Option>
<Select.Option value={4}>Every 16 seconds</Select.Option>
<Select.Option value={5}>Every 32 seconds</Select.Option>
<Select.Option value={6}>Every 64 seconds</Select.Option>
<Select.Option value={7}>Every 128 seconds</Select.Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label="Class-B ping-slot data-rate"
tooltip="This value must match the ping-slot data-rate of the device. Please refer to the device documentation."
name="classBPingSlotDr"
rules={[{ required: true, message: "Please enter the ping-slot data-rate!" }]}
>
<InputNumber min={0} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Class-B ping-slot frequency (Hz)"
tooltip="This value must match the ping-slot frequency of the device. Please refer to the device documentation."
name="classBPingSlotFreq"
rules={[{ required: true, message: "Please enter the ping-slot frequency!" }]}
>
<InputNumber min={0} style={{ width: "200px" }} />
</Form.Item>
</Col>
</Row>
{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>
</>
)}
</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>
</Form.List>
</Tabs.TabPane>
<Tabs.TabPane tab="Measurements" key="7">
<Card bordered={false}>
<p>
ChirpStack can aggregate and visualize decoded device measurements in the device dashboard. To setup the
aggregation of device measurements, you must configure the key, kind of measurement and name
(user-defined). The following measurement-kinds can be selected:
</p>
<ul>
<li>
<strong>Unknown / unset</strong>: Default for auto-detected keys. This disables the aggregation of this
metric.
</li>
<li>
<strong>Counter</strong>: For continuous incrementing counters.
</li>
<li>
<strong>Absolute</strong>: For counters which get reset upon reading / uplink.
</li>
<li>
<strong>Gauge</strong>: For temperature, humidity, pressure etc...
</li>
<li>
<strong>String</strong>: For boolean or string values.
</li>
</ul>
</Card>
<Form.Item
label="Automatically detect measurement keys"
name="autoDetectMeasurements"
valuePropName="checked"
tooltip="If enabled, measurement-keys will be automatically added based on the decoded payload keys. If the decoded payload contains random keys, you want to disable auto-detection."
>
<Switch />
</Form.Item>
<Form.List name="measurementsMap">
{(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={6}>
<Form.Item
{...restField}
name={[name, 1, "kind"]}
fieldKey={[name, 1, "kind"]}
rules={[{ required: true, message: "Please select a kind!" }]}
>
<Select>
<Select.Option value={MeasurementKind.UNKNOWN}>Unknown / unset</Select.Option>
<Select.Option value={MeasurementKind.COUNTER}>Counter</Select.Option>
<Select.Option value={MeasurementKind.ABSOLUTE}>Absolute</Select.Option>
<Select.Option value={MeasurementKind.GAUGE}>Gauge</Select.Option>
<Select.Option value={MeasurementKind.STRING}>String</Select.Option>
</Select>
</Form.Item>
</Col>
<Col span={10}>
<Form.Item
{...restField}
name={[name, 1, "name"]}
fieldKey={[name, 1, "name"]}
rules={[
{
required: true,
message: "Please enter a description!",
},
]}
>
<Input placeholder="Name" />
</Form.Item>
</Col>
<Col span={2}>
<MinusCircleOutlined onClick={() => remove(name)} />
</Col>
</Row>
))}
<Form.Item>
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
Add measurement
</Button>
</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.TabPane tab="Measurements" key="7">
<Card bordered={false}>
<p>
ChirpStack can aggregate and visualize decoded device measurements in the device dashboard. To setup the
aggregation of device measurements, you must configure the key, kind of measurement and name
(user-defined). The following measurement-kinds can be selected:
</p>
<ul>
<li>
<strong>Unknown / unset</strong>: Default for auto-detected keys. This disables the aggregation of
this metric.
</li>
<li>
<strong>Counter</strong>: For continuous incrementing counters.
</li>
<li>
<strong>Absolute</strong>: For counters which get reset upon reading / uplink.
</li>
<li>
<strong>Gauge</strong>: For temperature, humidity, pressure etc...
</li>
<li>
<strong>String</strong>: For boolean or string values.
</li>
</ul>
</Card>
<Form.Item
label="Automatically detect measurement keys"
name="autoDetectMeasurements"
valuePropName="checked"
tooltip="If enabled, measurement-keys will be automatically added based on the decoded payload keys. If the decoded payload contains random keys, you want to disable auto-detection."
>
<Switch />
</Form.Item>
<Form.List name="measurementsMap">
{(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={6}>
<Form.Item
{...restField}
name={[name, 1, "kind"]}
fieldKey={[name, 1, "kind"]}
rules={[{ required: true, message: "Please select a kind!" }]}
>
<Select>
<Select.Option value={MeasurementKind.UNKNOWN}>Unknown / unset</Select.Option>
<Select.Option value={MeasurementKind.COUNTER}>Counter</Select.Option>
<Select.Option value={MeasurementKind.ABSOLUTE}>Absolute</Select.Option>
<Select.Option value={MeasurementKind.GAUGE}>Gauge</Select.Option>
<Select.Option value={MeasurementKind.STRING}>String</Select.Option>
</Select>
</Form.Item>
</Col>
<Col span={10}>
<Form.Item
{...restField}
name={[name, 1, "name"]}
fieldKey={[name, 1, "name"]}
rules={[{ required: true, message: "Please enter a description!" }]}
>
<Input placeholder="Name" />
</Form.Item>
</Col>
<Col span={2}>
<MinusCircleOutlined onClick={() => remove(name)} />
</Col>
</Row>
))}
<Form.Item>
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
Add measurement
</Button>
</Form.Item>
</>
)}
</Form.List>
</Tabs.TabPane>
</Tabs>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
</Form.List>
</Tabs.TabPane>
</Tabs>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
export default DeviceProfileTemplateForm;

View File

@ -1,7 +1,9 @@
import React, { Component } from "react";
import { RouteComponentProps, Link } from "react-router-dom";
import React, { useState, useEffect } from "react";
import { Space, Breadcrumb, Card, Button, PageHeader } from "antd";
import { useParams, Link, useNavigate } from "react-router-dom";
import { Space, Breadcrumb, Card, Button } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import {
DeviceProfileTemplate,
@ -15,101 +17,78 @@ import DeviceProfileTemplateForm from "./DeviceProfileTemplateForm";
import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore";
import DeleteConfirm from "../../components/DeleteConfirm";
interface IState {
deviceProfileTemplate?: DeviceProfileTemplate;
}
function EditDeviceProfileTemplate() {
const navigate = useNavigate();
const [deviceProfileTemplate, setDeviceProfileTemplate] = useState<DeviceProfileTemplate | undefined>(undefined);
const { deviceProfileTemplateId } = useParams();
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;
useEffect(() => {
const id = deviceProfileTemplateId!;
let req = new GetDeviceProfileTemplateRequest();
req.setId(id);
DeviceProfileTemplateStore.get(req, (resp: GetDeviceProfileTemplateResponse) => {
this.setState({
deviceProfileTemplate: resp.getDeviceProfileTemplate(),
});
setDeviceProfileTemplate(resp.getDeviceProfileTemplate());
});
};
}, [deviceProfileTemplateId]);
onFinish = (obj: DeviceProfileTemplate) => {
const onFinish = (obj: DeviceProfileTemplate) => {
let req = new UpdateDeviceProfileTemplateRequest();
req.setDeviceProfileTemplate(obj);
DeviceProfileTemplateStore.update(req, () => {
this.props.history.push(`/device-profile-templates`);
navigate(`/device-profile-templates`);
});
};
deleteDeviceProfileTemplate = () => {
const deleteDeviceProfileTemplate = () => {
let req = new DeleteDeviceProfileTemplateRequest();
req.setId(this.props.match.params.deviceProfileTemplateId);
req.setId(deviceProfileTemplateId!);
DeviceProfileTemplateStore.delete(req, () => {
this.props.history.push(`/device-profile-templates`);
navigate(`/device-profile-templates`);
});
};
render() {
const dp = this.state.deviceProfileTemplate;
const dp = 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>
);
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={deleteDeviceProfileTemplate}>
<Button danger type="primary">
Delete device-profile template
</Button>
</DeleteConfirm>,
]}
/>
<Card>
<DeviceProfileTemplateForm initialValues={dp} update={true} onFinish={onFinish} />
</Card>
</Space>
);
}
export default EditDeviceProfileTemplate;

View File

@ -1,8 +1,8 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Space, Breadcrumb, Button, PageHeader } from "antd";
import { Space, Breadcrumb, Button } from "antd";
import { ColumnsType } from "antd/es/table";
import { PageHeader } from "@ant-design/pro-layout";
import {
ListDeviceProfileTemplatesRequest,
@ -15,38 +15,36 @@ 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",
function ListDeviceProfileTemplates() {
const columns: ColumnsType<DeviceProfileTemplateListItem.AsObject> = [
{
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);
},
{
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) => {
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
let req = new ListDeviceProfileTemplatesRequest();
req.setLimit(limit);
req.setOffset(offset);
@ -57,31 +55,29 @@ class ListDeviceProfileTemplates extends Component {
});
};
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>
);
}
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={columns} getPage={getPage} rowKey="id" />
</Space>
);
}
export default ListDeviceProfileTemplates;

View File

@ -1,7 +1,7 @@
import React, { Component } from "react";
import { Link, RouteComponentProps } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom";
import { Space, Breadcrumb, Card, PageHeader } from "antd";
import { Space, Breadcrumb, Card } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import { MacVersion, RegParamsRevision } from "@chirpstack/chirpstack-api-grpc-web/common/common_pb";
import {
@ -15,97 +15,97 @@ import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
import DeviceProfileForm from "./DeviceProfileForm";
import DeviceProfileStore from "../../stores/DeviceProfileStore";
interface IProps extends RouteComponentProps {
interface IProps {
tenant: Tenant;
}
class CreateDeviceProfile extends Component<IProps> {
onFinish = (obj: DeviceProfile) => {
obj.setTenantId(this.props.tenant.getId());
function CreateDeviceProfile(props: IProps) {
const navigate = useNavigate();
const onFinish = (obj: DeviceProfile) => {
obj.setTenantId(props.tenant.getId());
let req = new CreateDeviceProfileRequest();
req.setDeviceProfile(obj);
DeviceProfileStore.create(req, (_resp: CreateDeviceProfileResponse) => {
this.props.history.push(`/tenants/${this.props.tenant.getId()}/device-profiles`);
navigate(`/tenants/${props.tenant.getId()}/device-profiles`);
});
};
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 {
data: {
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 {
bytes: [225, 230, 255, 0]
};
}
`;
let deviceProfile = new DeviceProfile();
deviceProfile.setPayloadCodecScript(codecScript);
deviceProfile.setSupportsOtaa(true);
deviceProfile.setUplinkInterval(3600);
deviceProfile.setDeviceStatusReqInterval(1);
deviceProfile.setAdrAlgorithmId("default");
deviceProfile.setMacVersion(MacVersion.LORAWAN_1_0_3);
deviceProfile.setRegParamsRevision(RegParamsRevision.A);
deviceProfile.setFlushQueueOnActivate(true);
deviceProfile.setAutoDetectMeasurements(true);
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/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>
<Link to={`/tenants/${this.props.tenant.getId()}/device-profiles`}>Device profiles</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>Add</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title="Add device profile"
/>
<Card>
<DeviceProfileForm initialValues={deviceProfile} onFinish={this.onFinish} />
</Card>
</Space>
);
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 {
data: {
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 {
bytes: [225, 230, 255, 0]
};
}
`;
let deviceProfile = new DeviceProfile();
deviceProfile.setPayloadCodecScript(codecScript);
deviceProfile.setSupportsOtaa(true);
deviceProfile.setUplinkInterval(3600);
deviceProfile.setDeviceStatusReqInterval(1);
deviceProfile.setAdrAlgorithmId("default");
deviceProfile.setMacVersion(MacVersion.LORAWAN_1_0_3);
deviceProfile.setRegParamsRevision(RegParamsRevision.A);
deviceProfile.setFlushQueueOnActivate(true);
deviceProfile.setAutoDetectMeasurements(true);
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>
<Link to={`/tenants/${props.tenant.getId()}/device-profiles`}>Device profiles</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>Add</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title="Add device profile"
/>
<Card>
<DeviceProfileForm initialValues={deviceProfile} onFinish={onFinish} />
</Card>
</Space>
);
}
export default CreateDeviceProfile;

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,8 @@
import React, { Component } from "react";
import { RouteComponentProps, Link } from "react-router-dom";
import React, { useState, useEffect } from "react";
import { useNavigate, Link, useParams } from "react-router-dom";
import { Space, Breadcrumb, Card, Button, PageHeader } from "antd";
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 {
@ -18,112 +19,95 @@ import SessionStore from "../../stores/SessionStore";
import DeleteConfirm from "../../components/DeleteConfirm";
import Admin from "../../components/Admin";
interface IState {
deviceProfile?: DeviceProfile;
}
interface MatchParams {
deviceProfileId: string;
}
interface IProps extends RouteComponentProps<MatchParams> {
interface IProps {
tenant: Tenant;
}
class EditDeviceProfile extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
function EditDeviceProfile(props: IProps) {
const navigate = useNavigate();
const [deviceProfile, setDeviceProfile] = useState<DeviceProfile | undefined>(undefined);
const { deviceProfileId } = useParams();
componentDidMount() {
this.getDeviceProfile();
}
getDeviceProfile = () => {
const id = this.props.match.params.deviceProfileId;
useEffect(() => {
const id = deviceProfileId!;
let req = new GetDeviceProfileRequest();
req.setId(id);
DeviceProfileStore.get(req, (resp: GetDeviceProfileResponse) => {
this.setState({
deviceProfile: resp.getDeviceProfile(),
});
setDeviceProfile(resp.getDeviceProfile());
});
};
}, [deviceProfileId]);
onFinish = (obj: DeviceProfile) => {
const onFinish = (obj: DeviceProfile) => {
let req = new UpdateDeviceProfileRequest();
req.setDeviceProfile(obj);
DeviceProfileStore.update(req, () => {
this.props.history.push(`/tenants/${this.props.tenant.getId()}/device-profiles`);
navigate(`/tenants/${props.tenant.getId()}/device-profiles`);
});
};
deleteDeviceProfile = () => {
const deleteDeviceProfile = () => {
let req = new DeleteDeviceProfileRequest();
req.setId(this.props.match.params.deviceProfileId);
req.setId(deviceProfileId!);
DeviceProfileStore.delete(req, () => {
this.props.history.push(`/tenants/${this.props.tenant.getId()}/device-profiles`);
navigate(`/tenants/${props.tenant.getId()}/device-profiles`);
});
};
render() {
const dp = this.state.deviceProfile;
const dp = deviceProfile;
if (!dp) {
return null;
}
const disabled = !(
SessionStore.isAdmin() ||
SessionStore.isTenantAdmin(this.props.tenant.getId()) ||
SessionStore.isTenantDeviceAdmin(this.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/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>
<Link to={`/tenants/${this.props.tenant.getId()}/device-profiles`}>Device profiles</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>{dp.getName()}</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title={dp.getName()}
subTitle={`device profile id: ${dp.getId()}`}
extra={[
<Admin tenantId={this.props.tenant.getId()} isDeviceAdmin>
<DeleteConfirm typ="device profile" confirm={dp.getName()} onConfirm={this.deleteDeviceProfile}>
<Button danger type="primary">
Delete device profile
</Button>
</DeleteConfirm>
</Admin>,
]}
/>
<Card>
<DeviceProfileForm initialValues={dp} disabled={disabled} onFinish={this.onFinish} />
</Card>
</Space>
);
if (!dp) {
return null;
}
const disabled = !(
SessionStore.isAdmin() ||
SessionStore.isTenantAdmin(props.tenant.getId()) ||
SessionStore.isTenantDeviceAdmin(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>
<Link to={`/tenants/${props.tenant.getId()}/device-profiles`}>Device profiles</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>{dp.getName()}</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title={dp.getName()}
subTitle={`device profile id: ${dp.getId()}`}
extra={[
<Admin tenantId={props.tenant.getId()} isDeviceAdmin>
<DeleteConfirm typ="device profile" confirm={dp.getName()} onConfirm={deleteDeviceProfile}>
<Button danger type="primary">
Delete device profile
</Button>
</DeleteConfirm>
</Admin>,
]}
/>
<Card>
<DeviceProfileForm initialValues={dp} disabled={disabled} onFinish={onFinish} />
</Card>
</Space>
);
}
export default EditDeviceProfile;

View File

@ -1,8 +1,8 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Space, Breadcrumb, Button, PageHeader } from "antd";
import { Space, Breadcrumb, Button } from "antd";
import { ColumnsType } from "antd/es/table";
import { PageHeader } from "@ant-design/pro-layout";
import {
ListDeviceProfilesRequest,
@ -21,89 +21,87 @@ interface IProps {
tenant: Tenant;
}
class ListDeviceProfiles extends Component<IProps> {
columns = (): ColumnsType<DeviceProfileListItem.AsObject> => {
return [
{
title: "Name",
dataIndex: "name",
key: "name",
render: (text, record) => (
<Link to={`/tenants/${this.props.tenant.getId()}/device-profiles/${record.id}/edit`}>{text}</Link>
),
function ListDeviceProfiles(props: IProps) {
const columns: ColumnsType<DeviceProfileListItem.AsObject> = [
{
title: "Name",
dataIndex: "name",
key: "name",
render: (text, record) => (
<Link to={`/tenants/${props.tenant.getId()}/device-profiles/${record.id}/edit`}>{text}</Link>
),
},
{
title: "Region",
dataIndex: "region",
key: "region",
width: 150,
render: (text, record) => {
return getEnumName(Region, record.region);
},
{
title: "Region",
dataIndex: "region",
key: "region",
width: 150,
render: (text, record) => {
return getEnumName(Region, record.region);
},
},
{
title: "MAC version",
dataIndex: "macVersion",
key: "macVersion",
width: 150,
render: (text, record) => {
return formatMacVersion(record.macVersion);
},
{
title: "MAC version",
dataIndex: "macVersion",
key: "macVersion",
width: 150,
render: (text, record) => {
return formatMacVersion(record.macVersion);
},
},
{
title: "Revision",
dataIndex: "regParamsRevision",
key: "regParamsRevision",
width: 150,
render: (text, record) => {
return formatRegParamsRevision(record.regParamsRevision);
},
{
title: "Revision",
dataIndex: "regParamsRevision",
key: "regParamsRevision",
width: 150,
render: (text, record) => {
return formatRegParamsRevision(record.regParamsRevision);
},
},
{
title: "Supports OTAA",
dataIndex: "supportsOtaa",
key: "supportsOtaa",
width: 150,
render: (text, record) => {
if (record.supportsOtaa) {
return "yes";
} else {
return "no";
}
},
{
title: "Supports OTAA",
dataIndex: "supportsOtaa",
key: "supportsOtaa",
width: 150,
render: (text, record) => {
if (record.supportsOtaa) {
return "yes";
} else {
return "no";
}
},
},
{
title: "Supports Class-B",
dataIndex: "supportsClassB",
key: "supportsClassB",
width: 150,
render: (text, record) => {
if (record.supportsClassB) {
return "yes";
} else {
return "no";
}
},
{
title: "Supports Class-B",
dataIndex: "supportsClassB",
key: "supportsClassB",
width: 150,
render: (text, record) => {
if (record.supportsClassB) {
return "yes";
} else {
return "no";
}
},
},
{
title: "Supports Class-C",
dataIndex: "supportsClassC",
key: "supportsClassC",
width: 150,
render: (text, record) => {
if (record.supportsClassC) {
return "yes";
} else {
return "no";
}
},
{
title: "Supports Class-C",
dataIndex: "supportsClassC",
key: "supportsClassC",
width: 150,
render: (text, record) => {
if (record.supportsClassC) {
return "yes";
} else {
return "no";
}
},
},
];
};
},
];
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
let req = new ListDeviceProfilesRequest();
req.setTenantId(this.props.tenant.getId());
req.setTenantId(props.tenant.getId());
req.setLimit(limit);
req.setOffset(offset);
@ -113,38 +111,36 @@ class ListDeviceProfiles extends Component<IProps> {
});
};
render() {
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/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>Device profiles</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title="Device profiles"
extra={[
<Admin tenantId={this.props.tenant.getId()} isDeviceAdmin>
<Button type="primary">
<Link to={`/tenants/${this.props.tenant.getId()}/device-profiles/create`}>Add device profile</Link>
</Button>
</Admin>,
]}
/>
<DataTable columns={this.columns()} getPage={this.getPage} rowKey="id" />
</Space>
);
}
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>Device profiles</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title="Device profiles"
extra={[
<Admin tenantId={props.tenant.getId()} isDeviceAdmin>
<Button type="primary">
<Link to={`/tenants/${props.tenant.getId()}/device-profiles/create`}>Add device profile</Link>
</Button>
</Admin>,
]}
/>
<DataTable columns={columns} getPage={getPage} rowKey="id" />
</Space>
);
}
export default ListDeviceProfiles;

View File

@ -1,7 +1,7 @@
import React, { Component } from "react";
import { Link, RouteComponentProps } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom";
import { Space, Breadcrumb, Card, PageHeader } from "antd";
import { Space, Breadcrumb, Card } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
@ -15,14 +15,16 @@ import DeviceForm from "./DeviceForm";
import DeviceStore from "../../stores/DeviceStore";
import DeviceProfileStore from "../../stores/DeviceProfileStore";
interface IProps extends RouteComponentProps {
interface IProps {
tenant: Tenant;
application: Application;
}
class CreateDevice extends Component<IProps> {
onFinish = (obj: Device) => {
obj.setApplicationId(this.props.application.getId());
function CreateDevice(props: IProps) {
const navigate = useNavigate();
const onFinish = (obj: Device) => {
obj.setApplicationId(props.application.getId());
let req = new CreateDeviceRequest();
req.setDevice(obj);
@ -34,60 +36,58 @@ class CreateDevice extends Component<IProps> {
DeviceProfileStore.get(req, (resp: GetDeviceProfileResponse) => {
let dp = resp.getDeviceProfile()!;
if (dp.getSupportsOtaa()) {
this.props.history.push(
`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}/devices/${obj.getDevEui()}/keys`,
navigate(
`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}/devices/${obj.getDevEui()}/keys`,
);
} else {
this.props.history.push(
`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}/devices/${obj.getDevEui()}`,
navigate(
`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}/devices/${obj.getDevEui()}`,
);
}
});
});
};
render() {
let device = new Device();
device.setApplicationId(this.props.application.getId());
let device = new Device();
device.setApplicationId(props.application.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/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>
<Link to={`/tenants/${this.props.tenant.getId()}/applications`}>Applications</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>
<Link to={`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}`}>
{this.props.application.getName()}
</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>Add device</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title="Add device"
/>
<Card>
<DeviceForm tenant={this.props.tenant} initialValues={device} onFinish={this.onFinish} />
</Card>
</Space>
);
}
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>
<Link to={`/tenants/${props.tenant.getId()}/applications`}>Applications</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>
<Link to={`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}`}>
{props.application.getName()}
</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>Add device</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title="Add device"
/>
<Card>
<DeviceForm tenant={props.tenant} initialValues={device} onFinish={onFinish} />
</Card>
</Space>
);
}
export default CreateDevice;

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Space, Form, Button, Row, Col, InputNumber, Alert } from "antd";
@ -26,11 +26,11 @@ interface FormProps {
onFinish: (obj: DeviceActivationPb) => void;
}
class LW10DeviceActivationForm extends Component<FormProps> {
formRef = React.createRef<any>();
function LW10DeviceActivationForm(props: FormProps) {
const [form] = Form.useForm();
onFinish = (values: DeviceActivationPb.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values);
const onFinish = (values: DeviceActivationPb.AsObject) => {
const v = Object.assign(props.initialValues.toObject(), values);
let da = new DeviceActivationPb();
da.setDevAddr(v.devAddr);
@ -42,66 +42,56 @@ class LW10DeviceActivationForm extends Component<FormProps> {
da.setAFCntDown(v.nFCntDown);
da.setNFCntDown(v.nFCntDown);
this.props.onFinish(da);
props.onFinish(da);
};
render() {
return (
<Form
layout="vertical"
initialValues={this.props.initialValues.toObject()}
onFinish={this.onFinish}
ref={this.formRef}
>
<DevAddrInput
label="Device address"
name="devAddr"
value={this.props.initialValues.getDevAddr()}
devEui={this.props.device.getDevEui()}
formRef={this.formRef}
required
/>
<AesKeyInput
label="Network session key (LoRaWAN 1.0)"
name="nwkSEncKey"
value={this.props.initialValues.getNwkSEncKey()}
formRef={this.formRef}
required
/>
<AesKeyInput
label="Application session key (LoRaWAN 1.0)"
name="appSKey"
value={this.props.initialValues.getAppSKey()}
formRef={this.formRef}
required
/>
<Row gutter={24}>
<Col span={6}>
<Form.Item label="Uplink frame-counter" name="fCntUp">
<InputNumber min={0} />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item label="Downlink frame-counter" name="nFCntDown">
<InputNumber min={0} />
</Form.Item>
</Col>
</Row>
<Form.Item>
<Button type="primary" htmlType="submit" disabled={this.props.disabled}>
(Re)activate device
</Button>
</Form.Item>
</Form>
);
}
return (
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
<DevAddrInput
label="Device address"
name="devAddr"
value={props.initialValues.getDevAddr()}
devEui={props.device.getDevEui()}
required
/>
<AesKeyInput
label="Network session key (LoRaWAN 1.0)"
name="nwkSEncKey"
value={props.initialValues.getNwkSEncKey()}
required
/>
<AesKeyInput
label="Application session key (LoRaWAN 1.0)"
name="appSKey"
value={props.initialValues.getAppSKey()}
required
/>
<Row gutter={24}>
<Col span={6}>
<Form.Item label="Uplink frame-counter" name="fCntUp">
<InputNumber min={0} />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item label="Downlink frame-counter" name="nFCntDown">
<InputNumber min={0} />
</Form.Item>
</Col>
</Row>
<Form.Item>
<Button type="primary" htmlType="submit" disabled={props.disabled}>
(Re)activate device
</Button>
</Form.Item>
</Form>
);
}
class LW11DeviceActivationForm extends Component<FormProps> {
formRef = React.createRef<any>();
function LW11DeviceActivationForm(props: FormProps) {
const [form] = Form.useForm();
onFinish = (values: DeviceActivationPb.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values);
const onFinish = (values: DeviceActivationPb.AsObject) => {
const v = Object.assign(props.initialValues.toObject(), values);
let da = new DeviceActivationPb();
da.setDevAddr(v.devAddr);
@ -113,162 +103,133 @@ class LW11DeviceActivationForm extends Component<FormProps> {
da.setAFCntDown(v.aFCntDown);
da.setNFCntDown(v.nFCntDown);
this.props.onFinish(da);
props.onFinish(da);
};
render() {
return (
<Form
layout="vertical"
initialValues={this.props.initialValues.toObject()}
onFinish={this.onFinish}
ref={this.formRef}
>
<DevAddrInput
label="Device address"
name="devAddr"
value={this.props.initialValues.getDevAddr()}
devEui={this.props.device.getDevEui()}
formRef={this.formRef}
required
/>
<AesKeyInput
label="Network session encryption key"
name="nwkSEncKey"
value={this.props.initialValues.getNwkSEncKey()}
formRef={this.formRef}
required
/>
<AesKeyInput
label="Serving network session integrity key"
name="sNwkSIntKey"
value={this.props.initialValues.getSNwkSIntKey()}
formRef={this.formRef}
required
/>
<AesKeyInput
label="Forwarding network session integrity key"
name="fNwkSIntKey"
value={this.props.initialValues.getFNwkSIntKey()}
formRef={this.formRef}
required
/>
<AesKeyInput
label="Application session key"
name="appSKey"
value={this.props.initialValues.getAppSKey()}
formRef={this.formRef}
required
/>
<Row gutter={24}>
<Col span={6}>
<Form.Item label="Uplink frame-counter" name="fCntUp">
<InputNumber min={0} />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item label="Downlink frame-counter (network)" name="nFCntDown">
<InputNumber min={0} />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item label="Downlink frame-counter (application)" name="aFCntDown">
<InputNumber min={0} />
</Form.Item>
</Col>
</Row>
<Form.Item>
<Button type="primary" htmlType="submit" disabled={this.props.disabled}>
(Re)activate device
</Button>
</Form.Item>
</Form>
);
}
return (
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
<DevAddrInput
label="Device address"
name="devAddr"
value={props.initialValues.getDevAddr()}
devEui={props.device.getDevEui()}
required
/>
<AesKeyInput
label="Network session encryption key"
name="nwkSEncKey"
value={props.initialValues.getNwkSEncKey()}
required
/>
<AesKeyInput
label="Serving network session integrity key"
name="sNwkSIntKey"
value={props.initialValues.getSNwkSIntKey()}
required
/>
<AesKeyInput
label="Forwarding network session integrity key"
name="fNwkSIntKey"
value={props.initialValues.getFNwkSIntKey()}
required
/>
<AesKeyInput label="Application session key" name="appSKey" value={props.initialValues.getAppSKey()} required />
<Row gutter={24}>
<Col span={6}>
<Form.Item label="Uplink frame-counter" name="fCntUp">
<InputNumber min={0} />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item label="Downlink frame-counter (network)" name="nFCntDown">
<InputNumber min={0} />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item label="Downlink frame-counter (application)" name="aFCntDown">
<InputNumber min={0} />
</Form.Item>
</Col>
</Row>
<Form.Item>
<Button type="primary" htmlType="submit" disabled={props.disabled}>
(Re)activate device
</Button>
</Form.Item>
</Form>
);
}
interface IProps extends RouteComponentProps {
interface IProps {
tenant: Tenant;
application: Application;
device: Device;
deviceProfile: DeviceProfile;
}
interface IState {
deviceActivation?: DeviceActivationPb;
deviceActivationRequested: boolean;
}
function DeviceActivation(props: IProps) {
const navigate = useNavigate();
const [deviceActivation, setDeviceActivation] = useState<DeviceActivationPb | undefined>(undefined);
const [deviceActivationRequested, setDeviceActivationRequested] = useState<boolean>(false);
class DeviceActivation extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
deviceActivationRequested: false,
};
}
componentDidMount() {
useEffect(() => {
let req = new GetDeviceActivationRequest();
req.setDevEui(this.props.device.getDevEui());
req.setDevEui(props.device.getDevEui());
DeviceStore.getActivation(req, (resp: GetDeviceActivationResponse) => {
this.setState({
deviceActivation: resp.getDeviceActivation(),
deviceActivationRequested: true,
});
setDeviceActivation(resp.getDeviceActivation());
setDeviceActivationRequested(true);
});
}
}, [props]);
onFinish = (obj: DeviceActivationPb) => {
const onFinish = (obj: DeviceActivationPb) => {
let req = new ActivateDeviceRequest();
obj.setDevEui(this.props.device.getDevEui());
obj.setDevEui(props.device.getDevEui());
req.setDeviceActivation(obj);
DeviceStore.activate(req, () => {
this.props.history.push(
`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}/devices/${this.props.device.getDevEui()}`,
navigate(
`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}/devices/${props.device.getDevEui()}`,
);
});
};
render() {
if (!this.state.deviceActivationRequested) {
return null;
}
if (!this.state.deviceActivation && this.props.deviceProfile.getSupportsOtaa()) {
return <Alert type="info" showIcon message="This device has not (yet) been activated." />;
}
let macVersion = this.props.deviceProfile.getMacVersion();
const lw11 = macVersion === MacVersion.LORAWAN_1_1_0;
let initialValues = new DeviceActivationPb();
if (this.state.deviceActivation) {
initialValues = this.state.deviceActivation;
}
return (
<Space direction="vertical" style={{ width: "100%" }} size="large">
{!lw11 && (
<LW10DeviceActivationForm
initialValues={initialValues}
device={this.props.device}
onFinish={this.onFinish}
disabled={this.props.deviceProfile.getSupportsOtaa()}
/>
)}
{lw11 && (
<LW11DeviceActivationForm
initialValues={initialValues}
device={this.props.device}
onFinish={this.onFinish}
disabled={this.props.deviceProfile.getSupportsOtaa()}
/>
)}
</Space>
);
if (!deviceActivationRequested) {
return null;
}
if (!deviceActivation && props.deviceProfile.getSupportsOtaa()) {
return <Alert type="info" showIcon message="This device has not (yet) been activated." />;
}
let macVersion = props.deviceProfile.getMacVersion();
const lw11 = macVersion === MacVersion.LORAWAN_1_1_0;
let initialValues = new DeviceActivationPb();
if (deviceActivation) {
initialValues = deviceActivation;
}
return (
<Space direction="vertical" style={{ width: "100%" }} size="large">
{!lw11 && (
<LW10DeviceActivationForm
initialValues={initialValues}
device={props.device}
onFinish={onFinish}
disabled={props.deviceProfile.getSupportsOtaa()}
/>
)}
{lw11 && (
<LW11DeviceActivationForm
initialValues={initialValues}
device={props.device}
onFinish={onFinish}
disabled={props.deviceProfile.getSupportsOtaa()}
/>
)}
</Space>
);
}
export default DeviceActivation;

View File

@ -1,4 +1,4 @@
import React, { Component } from "react";
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import moment from "moment";
@ -27,31 +27,18 @@ interface IProps {
lastSeenAt?: Date;
}
interface IState {
metricsAggregation: Aggregation;
deviceMetrics?: GetDeviceMetricsResponse;
deviceLinkMetrics?: GetDeviceLinkMetricsResponse;
deviceMetricsLoaded: boolean;
deviceLinkMetricsLoaded: boolean;
}
function DeviceDashboard(props: IProps) {
const [metricsAggregation, setMetricsAggregation] = useState<Aggregation>(Aggregation.DAY);
const [deviceMetrics, setDeviceMetrics] = useState<GetDeviceMetricsResponse | undefined>(undefined);
const [deviceLinkMetrics, setDeviceLinkMetrics] = useState<GetDeviceLinkMetricsResponse | undefined>(undefined);
const [deviceLinkMetricsLoaded, setDeviceLinkMetricsLoaded] = useState<boolean>(false);
class DeviceDashboard extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
useEffect(() => {
loadMetrics();
}, [props, metricsAggregation]);
this.state = {
metricsAggregation: Aggregation.DAY,
deviceMetricsLoaded: false,
deviceLinkMetricsLoaded: false,
};
}
componentDidMount() {
this.loadMetrics();
}
loadMetrics = () => {
const agg = this.state.metricsAggregation;
const loadMetrics = () => {
const agg = metricsAggregation;
const end = moment();
let start = moment();
@ -63,19 +50,12 @@ class DeviceDashboard extends Component<IProps, IState> {
start = start.subtract(12, "months");
}
this.setState(
{
deviceMetricsLoaded: false,
deviceLinkMetricsLoaded: false,
},
() => {
this.loadLinkMetrics(start.toDate(), end.toDate(), agg);
this.loadDeviceMetrics(start.toDate(), end.toDate(), agg);
},
);
setDeviceLinkMetricsLoaded(false);
loadLinkMetrics(start.toDate(), end.toDate(), agg);
loadDeviceMetrics(start.toDate(), end.toDate(), agg);
};
loadDeviceMetrics = (start: Date, end: Date, agg: Aggregation) => {
const loadDeviceMetrics = (start: Date, end: Date, agg: Aggregation) => {
let startPb = new Timestamp();
let endPb = new Timestamp();
@ -83,20 +63,17 @@ class DeviceDashboard extends Component<IProps, IState> {
endPb.fromDate(end);
let req = new GetDeviceMetricsRequest();
req.setDevEui(this.props.device.getDevEui());
req.setDevEui(props.device.getDevEui());
req.setStart(startPb);
req.setEnd(endPb);
req.setAggregation(agg);
DeviceStore.getMetrics(req, (resp: GetDeviceMetricsResponse) => {
this.setState({
deviceMetrics: resp,
deviceMetricsLoaded: true,
});
setDeviceMetrics(resp);
});
};
loadLinkMetrics = (start: Date, end: Date, agg: Aggregation) => {
const loadLinkMetrics = (start: Date, end: Date, agg: Aggregation) => {
let startPb = new Timestamp();
let endPb = new Timestamp();
@ -104,176 +81,153 @@ class DeviceDashboard extends Component<IProps, IState> {
endPb.fromDate(end);
let req = new GetDeviceLinkMetricsRequest();
req.setDevEui(this.props.device.getDevEui());
req.setDevEui(props.device.getDevEui());
req.setStart(startPb);
req.setEnd(endPb);
req.setAggregation(agg);
DeviceStore.getLinkMetrics(req, (resp: GetDeviceLinkMetricsResponse) => {
this.setState({
deviceLinkMetrics: resp,
deviceLinkMetricsLoaded: true,
});
setDeviceLinkMetrics(resp);
setDeviceLinkMetricsLoaded(true);
});
};
onMetricsAggregationChange = (e: RadioChangeEvent) => {
this.setState(
{
metricsAggregation: e.target.value,
},
this.loadMetrics,
);
const onMetricsAggregationChange = (e: RadioChangeEvent) => {
setMetricsAggregation(e.target.value);
};
render() {
if (this.state.deviceLinkMetrics === undefined || this.state.deviceMetrics === undefined) {
return null;
}
let deviceMetrics = [];
{
let states = this.state.deviceMetrics.getStatesMap();
let keys = states.toArray().map(v => v[0]);
keys.sort();
for (let i = 0; i < keys.length; i += 3) {
let items = keys.slice(i, i + 3).map(k => {
let m = states.get(k)!;
return (
<Col span={8}>
<Card>
<Statistic title={m.getName()} value={m.getValue()} />
</Card>
</Col>
);
});
deviceMetrics.push(<Row gutter={24}>{items}</Row>);
}
}
{
let metrics = this.state.deviceMetrics.getMetricsMap();
let keys = metrics.toArray().map(v => v[0]);
keys.sort();
for (let i = 0; i < keys.length; i += 3) {
let items = keys.slice(i, i + 3).map(k => {
let m = metrics.get(k)!;
return (
<Col span={8}>
<MetricChart metric={m} aggregation={this.state.metricsAggregation} zeroToNull />
</Col>
);
});
deviceMetrics.push(<Row gutter={24}>{items}</Row>);
}
}
let lastSeenAt = "Never";
if (this.props.lastSeenAt !== undefined) {
lastSeenAt = moment(this.props.lastSeenAt).format("YYYY-MM-DD HH:mm:ss");
}
const loading = !this.state.deviceLinkMetricsLoaded || !this.state.deviceMetrics;
const aggregations = (
<Space direction="horizontal">
{loading && <Spin size="small" />}
<Radio.Group value={this.state.metricsAggregation} onChange={this.onMetricsAggregationChange} size="small">
<Radio.Button value={Aggregation.HOUR} disabled={loading}>
24h
</Radio.Button>
<Radio.Button value={Aggregation.DAY} disabled={loading}>
31d
</Radio.Button>
<Radio.Button value={Aggregation.MONTH} disabled={loading}>
1y
</Radio.Button>
</Radio.Group>
<Button type="primary" size="small" icon={<ReloadOutlined />} onClick={this.loadMetrics} disabled={loading} />
</Space>
);
return (
<Space direction="vertical" style={{ width: "100%" }} size="large">
<Card>
<Descriptions>
<Descriptions.Item label="Last seen">{lastSeenAt}</Descriptions.Item>
<Descriptions.Item label="Device profile">
<Link
to={`/tenants/${this.props.deviceProfile.getTenantId()}/device-profiles/${this.props.deviceProfile.getId()}/edit`}
>
{this.props.deviceProfile.getName()}
</Link>
</Descriptions.Item>
<Descriptions.Item label="Enabled">{this.props.device.getIsDisabled() ? "no" : "yes"}</Descriptions.Item>
<Descriptions.Item label="Description">{this.props.device.getDescription()}</Descriptions.Item>
</Descriptions>
</Card>
<Tabs tabBarExtraContent={aggregations}>
<Tabs.TabPane tab="Link metrics" key="1">
<Space direction="vertical" style={{ width: "100%" }} size="large">
<Row gutter={24}>
<Col span={8}>
<MetricChart
metric={this.state.deviceLinkMetrics.getRxPackets()!}
aggregation={this.state.metricsAggregation}
/>
</Col>
<Col span={8}>
<MetricChart
metric={this.state.deviceLinkMetrics.getGwRssi()!}
aggregation={this.state.metricsAggregation}
zeroToNull
/>
</Col>
<Col span={8}>
<MetricChart
metric={this.state.deviceLinkMetrics.getGwSnr()!}
aggregation={this.state.metricsAggregation}
zeroToNull
/>
</Col>
</Row>
<Row gutter={24}>
<Col span={8}>
<MetricHeatmap
metric={this.state.deviceLinkMetrics.getRxPacketsPerFreq()!}
aggregation={this.state.metricsAggregation}
fromColor="rgb(227, 242, 253)"
toColor="rgb(33, 150, 243, 1)"
/>
</Col>
<Col span={8}>
<MetricHeatmap
metric={this.state.deviceLinkMetrics.getRxPacketsPerDr()!}
aggregation={this.state.metricsAggregation}
fromColor="rgb(227, 242, 253)"
toColor="rgb(33, 150, 243, 1)"
/>
</Col>
<Col span={8}>
<MetricBar
metric={this.state.deviceLinkMetrics.getErrors()!}
aggregation={this.state.metricsAggregation}
/>
</Col>
</Row>
</Space>
</Tabs.TabPane>
<Tabs.TabPane tab="Device metrics" key="2">
<Space direction="vertical" style={{ width: "100%" }} size="large">
{deviceMetrics}
</Space>
</Tabs.TabPane>
</Tabs>
</Space>
);
if (deviceLinkMetrics === undefined || deviceMetrics === undefined) {
return null;
}
let dm = [];
{
let states = deviceMetrics.getStatesMap();
let keys = states.toArray().map(v => v[0]);
keys.sort();
for (let i = 0; i < keys.length; i += 3) {
let items = keys.slice(i, i + 3).map(k => {
let m = states.get(k)!;
return (
<Col span={8}>
<Card>
<Statistic title={m.getName()} value={m.getValue()} />
</Card>
</Col>
);
});
dm.push(<Row gutter={24}>{items}</Row>);
}
}
{
let metrics = deviceMetrics.getMetricsMap();
let keys = metrics.toArray().map(v => v[0]);
keys.sort();
for (let i = 0; i < keys.length; i += 3) {
let items = keys.slice(i, i + 3).map(k => {
let m = metrics.get(k)!;
return (
<Col span={8}>
<MetricChart metric={m} aggregation={metricsAggregation} zeroToNull />
</Col>
);
});
dm.push(<Row gutter={24}>{items}</Row>);
}
}
let lastSeenAt = "Never";
if (props.lastSeenAt !== undefined) {
lastSeenAt = moment(props.lastSeenAt).format("YYYY-MM-DD HH:mm:ss");
}
const loading = !deviceLinkMetricsLoaded || !deviceMetrics;
const aggregations = (
<Space direction="horizontal">
{loading && <Spin size="small" />}
<Radio.Group value={metricsAggregation} onChange={onMetricsAggregationChange} size="small">
<Radio.Button value={Aggregation.HOUR} disabled={loading}>
24h
</Radio.Button>
<Radio.Button value={Aggregation.DAY} disabled={loading}>
31d
</Radio.Button>
<Radio.Button value={Aggregation.MONTH} disabled={loading}>
1y
</Radio.Button>
</Radio.Group>
<Button type="primary" size="small" icon={<ReloadOutlined />} onClick={loadMetrics} disabled={loading} />
</Space>
);
return (
<Space direction="vertical" style={{ width: "100%" }} size="large">
<Card>
<Descriptions>
<Descriptions.Item label="Last seen">{lastSeenAt}</Descriptions.Item>
<Descriptions.Item label="Device profile">
<Link
to={`/tenants/${props.deviceProfile.getTenantId()}/device-profiles/${props.deviceProfile.getId()}/edit`}
>
{props.deviceProfile.getName()}
</Link>
</Descriptions.Item>
<Descriptions.Item label="Enabled">{props.device.getIsDisabled() ? "no" : "yes"}</Descriptions.Item>
<Descriptions.Item label="Description">{props.device.getDescription()}</Descriptions.Item>
</Descriptions>
</Card>
<Tabs tabBarExtraContent={aggregations}>
<Tabs.TabPane tab="Link metrics" key="1">
<Space direction="vertical" style={{ width: "100%" }} size="large">
<Row gutter={24}>
<Col span={8}>
<MetricChart metric={deviceLinkMetrics.getRxPackets()!} aggregation={metricsAggregation} />
</Col>
<Col span={8}>
<MetricChart metric={deviceLinkMetrics.getGwRssi()!} aggregation={metricsAggregation} zeroToNull />
</Col>
<Col span={8}>
<MetricChart metric={deviceLinkMetrics.getGwSnr()!} aggregation={metricsAggregation} zeroToNull />
</Col>
</Row>
<Row gutter={24}>
<Col span={8}>
<MetricHeatmap
metric={deviceLinkMetrics.getRxPacketsPerFreq()!}
aggregation={metricsAggregation}
fromColor="rgb(227, 242, 253)"
toColor="rgb(33, 150, 243, 1)"
/>
</Col>
<Col span={8}>
<MetricHeatmap
metric={deviceLinkMetrics.getRxPacketsPerDr()!}
aggregation={metricsAggregation}
fromColor="rgb(227, 242, 253)"
toColor="rgb(33, 150, 243, 1)"
/>
</Col>
<Col span={8}>
<MetricBar metric={deviceLinkMetrics.getErrors()!} aggregation={metricsAggregation} />
</Col>
</Row>
</Space>
</Tabs.TabPane>
<Tabs.TabPane tab="Device metrics" key="2">
<Space direction="vertical" style={{ width: "100%" }} size="large">
{dm}
</Space>
</Tabs.TabPane>
</Tabs>
</Space>
);
}
export default DeviceDashboard;

View File

@ -1,4 +1,4 @@
import React, { Component } from "react";
import React, { useEffect, useState } from "react";
import { Device } from "@chirpstack/chirpstack-api-grpc-web/api/device_pb";
import { StreamDeviceEventsRequest, LogItem } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
@ -10,55 +10,31 @@ interface IProps {
device: Device;
}
interface IState {
events: LogItem[];
cancelFunc?: () => void;
}
function DeviceEvents(props: IProps) {
const [events, setEvents] = useState<LogItem[]>([]);
class DeviceEvents extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
useEffect(() => {
const onMessage = (l: LogItem) => {
setEvents(e => {
if (e.length === 0 || parseInt(l.getId().replace("-", "")) > parseInt(e[0].getId().replace("-", ""))) {
e.unshift(l);
}
this.state = {
events: [],
cancelFunc: undefined,
};
}
componentDidMount() {
this.connectStream();
}
componentWillUnmount() {
if (this.state.cancelFunc !== undefined) {
this.state.cancelFunc();
}
}
connectStream = () => {
let req = new StreamDeviceEventsRequest();
req.setDevEui(this.props.device.getDevEui());
let cancelFunc = InternalStore.streamDeviceEvents(req, this.onMessage);
this.setState({
cancelFunc: cancelFunc,
});
};
onMessage = (l: LogItem) => {
let events = this.state.events;
if (events.length === 0 || parseInt(l.getId().replace("-", "")) > parseInt(events[0].getId().replace("-", ""))) {
events.unshift(l);
this.setState({
events: events,
return e;
});
}
};
};
render() {
return <LogTable logs={this.state.events} />;
}
let req = new StreamDeviceEventsRequest();
req.setDevEui(props.device.getDevEui());
let cancelFunc = InternalStore.streamDeviceEvents(req, onMessage);
return () => {
cancelFunc();
};
}, [props]);
return <LogTable logs={events} />;
}
export default DeviceEvents;

View File

@ -1,5 +1,3 @@
import React, { Component } from "react";
import { Form, Input, Row, Col, Button, Tabs, Switch } from "antd";
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
@ -24,11 +22,11 @@ interface IProps {
update?: boolean;
}
class DeviceForm extends Component<IProps> {
formRef = React.createRef<any>();
function DeviceForm(props: IProps) {
const [form] = Form.useForm();
onFinish = (values: Device.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values);
const onFinish = (values: Device.AsObject) => {
const v = Object.assign(props.initialValues.toObject(), values);
let d = new Device();
d.setApplicationId(v.applicationId);
@ -50,12 +48,12 @@ class DeviceForm extends Component<IProps> {
d.getVariablesMap().set(elm[0], elm[1]);
}
this.props.onFinish(d);
props.onFinish(d);
};
getDeviceProfileOptions = (search: string, fn: OptionsCallbackFunc) => {
const getDeviceProfileOptions = (search: string, fn: OptionsCallbackFunc) => {
let req = new ListDeviceProfilesRequest();
req.setTenantId(this.props.tenant.getId());
req.setTenantId(props.tenant.getId());
req.setSearch(search);
req.setLimit(10);
@ -63,11 +61,12 @@ class DeviceForm extends Component<IProps> {
const options = resp.getResultList().map((o, i) => {
return { label: o.getName(), value: o.getId() };
});
fn(options);
});
};
getDeviceProfileOption = (id: string, fn: OptionCallbackFunc) => {
const getDeviceProfileOption = (id: string, fn: OptionCallbackFunc) => {
let req = new GetDeviceProfileRequest();
req.setId(id);
@ -79,163 +78,153 @@ class DeviceForm extends Component<IProps> {
});
};
render() {
return (
<Form
layout="vertical"
initialValues={this.props.initialValues.toObject()}
onFinish={this.onFinish}
ref={this.formRef}
>
<Tabs>
<Tabs.TabPane tab="Device" key="1">
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
<Input />
</Form.Item>
<Form.Item label="Description" name="description">
<Input.TextArea />
</Form.Item>
<Row gutter={24}>
<Col span={12}>
<EuiInput
label="Device EUI (EUI64)"
name="devEui"
value={this.props.initialValues.getDevEui()}
formRef={this.formRef}
disabled={this.props.update}
required
/>
</Col>
<Col span={12}>
<EuiInput
label="Join EUI (EUI64)"
name="joinEui"
value={this.props.initialValues.getJoinEui()}
formRef={this.formRef}
tooltip="The Join EUI will be automatically set / updated on OTAA. However, in some cases this field must be configured before OTAA (e.g. OTAA using a Relay)."
/>
</Col>
</Row>
<AutocompleteInput
label="Device profile"
name="deviceProfileId"
formRef={this.formRef}
getOption={this.getDeviceProfileOption}
getOptions={this.getDeviceProfileOptions}
required
/>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label="Device is disabled"
name="isDisabled"
valuePropName="checked"
tooltip="Received uplink frames and join-requests will be ignored."
>
<Switch />
return (
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
<Tabs>
<Tabs.TabPane tab="Device" key="1">
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
<Input />
</Form.Item>
<Form.Item label="Description" name="description">
<Input.TextArea />
</Form.Item>
<Row gutter={24}>
<Col span={12}>
<EuiInput
label="Device EUI (EUI64)"
name="devEui"
value={props.initialValues.getDevEui()}
disabled={props.update}
required
/>
</Col>
<Col span={12}>
<EuiInput
label="Join EUI (EUI64)"
name="joinEui"
value={props.initialValues.getJoinEui()}
tooltip="The Join EUI will be automatically set / updated on OTAA. However, in some cases this field must be configured before OTAA (e.g. OTAA using a Relay)."
/>
</Col>
</Row>
<AutocompleteInput
label="Device profile"
name="deviceProfileId"
getOption={getDeviceProfileOption}
getOptions={getDeviceProfileOptions}
required
/>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label="Device is disabled"
name="isDisabled"
valuePropName="checked"
tooltip="Received uplink frames and join-requests will be ignored."
>
<Switch />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Disable frame-counter validation"
name="skipFcntCheck"
valuePropName="checked"
tooltip="You must re-activate your device before this setting becomes effective. Note that disabling the frame-counter validation will compromise security as it allows replay-attacks."
>
<Switch />
</Form.Item>
</Col>
</Row>
</Tabs.TabPane>
<Tabs.TabPane tab="Tags" key="2">
<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>
</Col>
<Col span={12}>
<Form.Item
label="Disable frame-counter validation"
name="skipFcntCheck"
valuePropName="checked"
tooltip="You must re-activate your device before this setting becomes effective. Note that disabling the frame-counter validation will compromise security as it allows replay-attacks."
>
<Switch />
</>
)}
</Form.List>
</Tabs.TabPane>
<Tabs.TabPane tab="Variables" key="3">
<Form.List name="variablesMap">
{(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 variable
</Button>
</Form.Item>
</Col>
</Row>
</Tabs.TabPane>
<Tabs.TabPane tab="Tags" key="2">
<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.TabPane tab="Variables" key="3">
<Form.List name="variablesMap">
{(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 variable
</Button>
</Form.Item>
</>
)}
</Form.List>
</Tabs.TabPane>
</Tabs>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
</>
)}
</Form.List>
</Tabs.TabPane>
</Tabs>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
export default DeviceForm;

View File

@ -1,4 +1,4 @@
import React, { Component } from "react";
import React, { useState, useEffect } from "react";
import { Device } from "@chirpstack/chirpstack-api-grpc-web/api/device_pb";
import { StreamDeviceFramesRequest, LogItem } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
@ -10,55 +10,31 @@ interface IProps {
device: Device;
}
interface IState {
frames: LogItem[];
cancelFunc?: () => void;
}
function DeviceFrames(props: IProps) {
const [frames, setFrames] = useState<LogItem[]>([]);
class DeviceFrames extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
useEffect(() => {
const onMessage = (l: LogItem) => {
setFrames(f => {
if (f.length === 0 || parseInt(l.getId().replace("-", "")) > parseInt(f[0].getId().replace("-", ""))) {
f.unshift(l);
}
this.state = {
frames: [],
cancelFunc: undefined,
};
}
componentDidMount() {
this.connectStream();
}
componentWillUnmount() {
if (this.state.cancelFunc !== undefined) {
this.state.cancelFunc();
}
}
connectStream = () => {
let req = new StreamDeviceFramesRequest();
req.setDevEui(this.props.device.getDevEui());
let cancelFunc = InternalStore.streamDeviceFrames(req, this.onMessage);
this.setState({
cancelFunc: cancelFunc,
});
};
onMessage = (l: LogItem) => {
let frames = this.state.frames;
if (frames.length === 0 || parseInt(l.getId().replace("-", "")) > parseInt(frames[0].getId().replace("-", ""))) {
frames.unshift(l);
this.setState({
frames: frames,
return f;
});
}
};
};
render() {
return <LogTable logs={this.state.frames} />;
}
let req = new StreamDeviceFramesRequest();
req.setDevEui(props.device.getDevEui());
let cancelFunc = InternalStore.streamDeviceFrames(req, onMessage);
return () => {
cancelFunc();
};
}, [props]);
return <LogTable logs={frames} />;
}
export default DeviceFrames;

View File

@ -1,7 +1,8 @@
import React, { Component } from "react";
import { Route, Switch, RouteComponentProps, Link } from "react-router-dom";
import React, { useState, useEffect } from "react";
import { Route, Routes, useParams, Link, useNavigate, useLocation } from "react-router-dom";
import { Space, Breadcrumb, Card, Button, PageHeader, Menu } from "antd";
import { Space, Breadcrumb, Card, Button, Menu } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
@ -30,239 +31,183 @@ import DeviceEvents from "./DeviceEvents";
import DeviceQueue from "./DeviceQueue";
import DeviceActivation from "./DeviceActivation";
interface MatchParams {
devEui: string;
}
interface IProps extends RouteComponentProps<MatchParams> {
interface IProps {
tenant: Tenant;
application: Application;
}
interface IState {
device?: Device;
deviceProfile?: DeviceProfile;
lastSeenAt?: Date;
}
function DeviceLayout(props: IProps) {
const { devEui } = useParams();
const navigate = useNavigate();
const location = useLocation();
class DeviceLayout extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
const [device, setDevice] = useState<Device | undefined>(undefined);
const [deviceProfile, setDeviceProfile] = useState<DeviceProfile | undefined>(undefined);
const [lastSeenAt, setLastSeenAt] = useState<Date | undefined>(undefined);
componentDidMount() {
this.getDevice(this.getDeviceProfile);
}
getDevice = (cb: () => void) => {
useEffect(() => {
let req = new GetDeviceRequest();
req.setDevEui(this.props.match.params.devEui);
req.setDevEui(devEui!);
DeviceStore.get(req, (resp: GetDeviceResponse) => {
this.setState(
{
device: resp.getDevice(),
},
cb,
);
setDevice(resp.getDevice());
if (resp.getLastSeenAt() !== undefined) {
this.setState({
lastSeenAt: resp.getLastSeenAt()!.toDate(),
});
setLastSeenAt(resp.getLastSeenAt()!.toDate());
}
});
};
getDeviceProfile = () => {
let req = new GetDeviceProfileRequest();
req.setId(this.state.device!.getDeviceProfileId());
DeviceProfileStore.get(req, (resp: GetDeviceProfileResponse) => {
this.setState({
deviceProfile: resp.getDeviceProfile(),
let req = new GetDeviceProfileRequest();
req.setId(resp.getDevice()!.getDeviceProfileId());
DeviceProfileStore.get(req, (resp: GetDeviceProfileResponse) => {
setDeviceProfile(resp.getDeviceProfile());
});
});
};
}, [devEui]);
deleteDevice = () => {
const deleteDevice = () => {
let req = new DeleteDeviceRequest();
req.setDevEui(this.props.match.params.devEui);
req.setDevEui(devEui!);
DeviceStore.delete(req, () => {
this.props.history.push(`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}`);
navigate(`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}`);
});
};
render() {
const device = this.state.device;
const dp = this.state.deviceProfile;
if (!device || !dp) {
return null;
}
const tenant = this.props.tenant;
const app = this.props.application;
const path = this.props.history.location.pathname;
let tab = "dashboard";
if (path.endsWith("edit")) {
tab = "edit";
}
if (path.endsWith("queue")) {
tab = "queue";
}
if (path.endsWith("keys")) {
tab = "keys";
}
if (path.endsWith("activation")) {
tab = "activation";
}
if (path.endsWith("events")) {
tab = "events";
}
if (path.endsWith("frames")) {
tab = "frames";
}
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/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>
<Link to={`/tenants/${this.props.tenant.getId()}/applications`}>Applications</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>
<Link to={`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}`}>
{this.props.application.getName()}
</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>
<Link to={`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}`}>
Devices
</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>{device.getName()}</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title={device.getName()}
subTitle={`device eui: ${device.getDevEui()}`}
extra={[
<Admin tenantId={this.props.tenant.getId()} isDeviceAdmin>
<DeleteConfirm typ="device" confirm={device.getName()} onConfirm={this.deleteDevice}>
<Button danger type="primary">
Delete device
</Button>
</DeleteConfirm>
</Admin>,
]}
/>
<Card>
<Menu mode="horizontal" selectedKeys={[tab]} style={{ marginBottom: 24 }}>
<Menu.Item key="dashboard">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}`}>
Dashboard
</Link>
</Menu.Item>
<Menu.Item key="edit">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/edit`}>
Configuration
</Link>
</Menu.Item>
<Menu.Item key="keys" disabled={!dp.getSupportsOtaa()}>
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/keys`}>
OTAA keys
</Link>
</Menu.Item>
<Menu.Item key="activation">
<Link
to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/activation`}
>
Activation
</Link>
</Menu.Item>
<Menu.Item key="queue">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/queue`}>
Queue
</Link>
</Menu.Item>
<Menu.Item key="events">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/events`}>
Events
</Link>
</Menu.Item>
<Menu.Item key="frames">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/frames`}>
LoRaWAN frames
</Link>
</Menu.Item>
</Menu>
<Switch>
<Route
exact
path={this.props.match.path}
render={props => (
<DeviceDashboard device={device} lastSeenAt={this.state.lastSeenAt} deviceProfile={dp} {...props} />
)}
/>
<Route
exact
path={`${this.props.match.path}/edit`}
render={props => <EditDevice device={device} application={app} tenant={tenant} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/keys`}
render={props => (
<SetDeviceKeys device={device} application={app} tenant={tenant} deviceProfile={dp} {...props} />
)}
/>
<Route
exact
path={`${this.props.match.path}/frames`}
render={props => <DeviceFrames device={device} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/events`}
render={props => <DeviceEvents device={device} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/queue`}
render={props => <DeviceQueue device={device} {...props} />}
/>
<Route
exact
path={`${this.props.match.path}/activation`}
render={props => (
<DeviceActivation device={device} deviceProfile={dp} tenant={tenant} application={app} {...props} />
)}
/>
</Switch>
</Card>
</Space>
);
const dp = deviceProfile;
if (!device || !dp) {
return null;
}
const tenant = props.tenant;
const app = props.application;
const path = location.pathname;
let tab = "dashboard";
if (path.endsWith("edit")) {
tab = "edit";
}
if (path.endsWith("queue")) {
tab = "queue";
}
if (path.endsWith("keys")) {
tab = "keys";
}
if (path.endsWith("activation")) {
tab = "activation";
}
if (path.endsWith("events")) {
tab = "events";
}
if (path.endsWith("frames")) {
tab = "frames";
}
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>
<Link to={`/tenants/${props.tenant.getId()}/applications`}>Applications</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>
<Link to={`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}`}>
{props.application.getName()}
</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>
<Link to={`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}`}>Devices</Link>
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>{device.getName()}</span>
</Breadcrumb.Item>
</Breadcrumb>
)}
title={device.getName()}
subTitle={`device eui: ${device.getDevEui()}`}
extra={[
<Admin tenantId={props.tenant.getId()} isDeviceAdmin>
<DeleteConfirm typ="device" confirm={device.getName()} onConfirm={deleteDevice}>
<Button danger type="primary">
Delete device
</Button>
</DeleteConfirm>
</Admin>,
]}
/>
<Card>
<Menu mode="horizontal" selectedKeys={[tab]} style={{ marginBottom: 24 }}>
<Menu.Item key="dashboard">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}`}>
Dashboard
</Link>
</Menu.Item>
<Menu.Item key="edit">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/edit`}>
Configuration
</Link>
</Menu.Item>
<Menu.Item key="keys" disabled={!dp.getSupportsOtaa()}>
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/keys`}>
OTAA keys
</Link>
</Menu.Item>
<Menu.Item key="activation">
<Link
to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/activation`}
>
Activation
</Link>
</Menu.Item>
<Menu.Item key="queue">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/queue`}>
Queue
</Link>
</Menu.Item>
<Menu.Item key="events">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/events`}>
Events
</Link>
</Menu.Item>
<Menu.Item key="frames">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/frames`}>
LoRaWAN frames
</Link>
</Menu.Item>
</Menu>
<Routes>
<Route path="/" element={<DeviceDashboard device={device} lastSeenAt={lastSeenAt} deviceProfile={dp} />} />
<Route path="/edit" element={<EditDevice device={device} application={app} tenant={tenant} />} />
<Route
path="/keys"
element={<SetDeviceKeys device={device} application={app} tenant={tenant} deviceProfile={dp} />}
/>
<Route path="/frames" element={<DeviceFrames device={device} />} />
<Route path="/events" element={<DeviceEvents device={device} />} />
<Route path="/queue" element={<DeviceQueue device={device} />} />
<Route
path="/activation"
element={<DeviceActivation device={device} deviceProfile={dp} tenant={tenant} application={app} />}
/>
</Routes>
</Card>
</Space>
);
}
export default DeviceLayout;

View File

@ -1,4 +1,4 @@
import React, { Component } from "react";
import React, { useState } from "react";
import { Struct } from "google-protobuf/google/protobuf/struct_pb";
@ -25,87 +25,75 @@ interface IProps {
device: Device;
}
interface IState {
refreshCounter: number;
}
function DeviceQueue(props: IProps) {
const [refreshCounter, setRefreshCounter] = useState<number>(0);
const [form] = Form.useForm();
class DeviceQueue extends Component<IProps, IState> {
formRef = React.createRef<any>();
const columns: ColumnsType<DeviceQueueItem.AsObject> = [
{
title: "ID",
dataIndex: "id",
key: "id",
width: 350,
},
{
title: "Is pending",
dataIndex: "isPending",
key: "isPending",
width: 100,
render: (text, record) => {
if (record.isPending) {
return "yes";
} else {
return "no";
}
},
},
{
title: "Frame-counter",
dataIndex: "fCntDown",
key: "fCntDown",
width: 200,
render: (text, record) => {
if (record.isPending === true) {
return record.fCntDown;
} else {
return "";
}
},
},
{
title: "Confirmed",
dataIndex: "confirmed",
key: "confirmed",
width: 100,
render: (text, record) => {
if (record.confirmed) {
return "yes";
} else {
return "no";
}
},
},
{
title: "FPort",
dataIndex: "fPort",
key: "fPort",
width: 100,
},
{
title: "Data (HEX)",
dataIndex: "data",
key: "data",
render: (text, record) => {
return Buffer.from(record.data as string, "base64").toString("hex");
},
},
];
constructor(props: IProps) {
super(props);
this.state = {
refreshCounter: 0,
};
}
columns = (): ColumnsType<DeviceQueueItem.AsObject> => {
return [
{
title: "ID",
dataIndex: "id",
key: "id",
width: 350,
},
{
title: "Is pending",
dataIndex: "isPending",
key: "isPending",
width: 100,
render: (text, record) => {
if (record.isPending) {
return "yes";
} else {
return "no";
}
},
},
{
title: "Frame-counter",
dataIndex: "fCntDown",
key: "fCntDown",
width: 200,
render: (text, record) => {
if (record.isPending === true) {
return record.fCntDown;
} else {
return "";
}
},
},
{
title: "Confirmed",
dataIndex: "confirmed",
key: "confirmed",
width: 100,
render: (text, record) => {
if (record.confirmed) {
return "yes";
} else {
return "no";
}
},
},
{
title: "FPort",
dataIndex: "fPort",
key: "fPort",
width: 100,
},
{
title: "Data (HEX)",
dataIndex: "data",
key: "data",
render: (text, record) => {
return Buffer.from(record.data as string, "base64").toString("hex");
},
},
];
};
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
let req = new GetDeviceQueueItemsRequest();
req.setDevEui(this.props.device.getDevEui());
req.setDevEui(props.device.getDevEui());
DeviceStore.getQueue(req, (resp: GetDeviceQueueItemsResponse) => {
const obj = resp.toObject();
@ -113,25 +101,23 @@ class DeviceQueue extends Component<IProps, IState> {
});
};
refreshQueue = () => {
this.setState({
refreshCounter: this.state.refreshCounter + 1,
});
const refreshQueue = () => {
setRefreshCounter(refreshCounter + 1);
};
flushQueue = () => {
const flushQueue = () => {
let req = new FlushDeviceQueueRequest();
req.setDevEui(this.props.device.getDevEui());
req.setDevEui(props.device.getDevEui());
DeviceStore.flushQueue(req, () => {
this.refreshQueue();
refreshQueue();
});
};
onEnqueue = (values: any) => {
const onEnqueue = (values: any) => {
let req = new EnqueueDeviceQueueItemRequest();
let item = new DeviceQueueItem();
item.setDevEui(this.props.device.getDevEui());
item.setDevEui(props.device.getDevEui());
item.setFPort(values.fPort);
item.setConfirmed(values.confirmed);
@ -163,66 +149,58 @@ class DeviceQueue extends Component<IProps, IState> {
req.setQueueItem(item);
DeviceStore.enqueue(req, _ => {
this.formRef.current.resetFields();
this.refreshQueue();
form.resetFields();
refreshQueue();
});
};
render() {
return (
<Space direction="vertical" style={{ width: "100%" }} size="large">
<Card title="Enqueue">
<Form layout="horizontal" onFinish={this.onEnqueue} ref={this.formRef} initialValues={{ fPort: 1 }}>
<Row>
<Space direction="horizontal" style={{ width: "100%" }} size="large">
<Form.Item name="confirmed" label="Confirmed" valuePropName="checked">
<Checkbox />
</Form.Item>
<Form.Item name="fPort" label="FPort">
<InputNumber min={1} max={254} />
</Form.Item>
</Space>
</Row>
<Tabs defaultActiveKey="1">
<Tabs.TabPane tab="HEX" key="1">
<Form.Item name="hex">
<Input />
</Form.Item>
</Tabs.TabPane>
<Tabs.TabPane tab="BASE64" key="2">
<Form.Item name="base64">
<Input />
</Form.Item>
</Tabs.TabPane>
<Tabs.TabPane tab="JSON" key="3">
<CodeEditor name="json" value="{}" formRef={this.formRef} />
</Tabs.TabPane>
</Tabs>
<Button type="primary" htmlType="submit">
Enqueue
</Button>
</Form>
</Card>
<Row justify="end">
<Space direction="horizontal" size="large">
<Button icon={<RedoOutlined />} onClick={this.refreshQueue}>
Reload
</Button>
<Popconfirm title="Are you sure you want to flush the queue?" placement="left" onConfirm={this.flushQueue}>
<Button icon={<DeleteOutlined />}>Flush queue</Button>
</Popconfirm>
</Space>
</Row>
<DataTable
columns={this.columns()}
getPage={this.getPage}
refreshKey={this.state.refreshCounter}
rowKey="id"
noPagination
/>
</Space>
);
}
return (
<Space direction="vertical" style={{ width: "100%" }} size="large">
<Card title="Enqueue">
<Form layout="horizontal" onFinish={onEnqueue} form={form} initialValues={{ fPort: 1 }}>
<Row>
<Space direction="horizontal" style={{ width: "100%" }} size="large">
<Form.Item name="confirmed" label="Confirmed" valuePropName="checked">
<Checkbox />
</Form.Item>
<Form.Item name="fPort" label="FPort">
<InputNumber min={1} max={254} />
</Form.Item>
</Space>
</Row>
<Tabs defaultActiveKey="1">
<Tabs.TabPane tab="HEX" key="1">
<Form.Item name="hex">
<Input />
</Form.Item>
</Tabs.TabPane>
<Tabs.TabPane tab="BASE64" key="2">
<Form.Item name="base64">
<Input />
</Form.Item>
</Tabs.TabPane>
<Tabs.TabPane tab="JSON" key="3">
<CodeEditor name="json" />
</Tabs.TabPane>
</Tabs>
<Button type="primary" htmlType="submit">
Enqueue
</Button>
</Form>
</Card>
<Row justify="end">
<Space direction="horizontal" size="large">
<Button icon={<RedoOutlined />} onClick={refreshQueue}>
Reload
</Button>
<Popconfirm title="Are you sure you want to flush the queue?" placement="left" onConfirm={flushQueue}>
<Button icon={<DeleteOutlined />}>Flush queue</Button>
</Popconfirm>
</Space>
</Row>
<DataTable columns={columns} getPage={getPage} refreshKey={refreshCounter} rowKey="id" noPagination />
</Space>
);
}
export default DeviceQueue;

View File

@ -1,5 +1,4 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
@ -8,27 +7,25 @@ import { Device, UpdateDeviceRequest } from "@chirpstack/chirpstack-api-grpc-web
import DeviceStore from "../../stores/DeviceStore";
import DeviceForm from "./DeviceForm";
interface IProps extends RouteComponentProps {
interface IProps {
tenant: Tenant;
application: Application;
device: Device;
}
class EditDevice extends Component<IProps> {
onFinish = (obj: Device) => {
function EditDevice(props: IProps) {
const navigate = useNavigate();
const onFinish = (obj: Device) => {
let req = new UpdateDeviceRequest();
req.setDevice(obj);
DeviceStore.update(req, () => {
this.props.history.push(
`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}/devices/${obj.getDevEui()}`,
);
navigate(`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}/devices/${obj.getDevEui()}`);
});
};
render() {
return <DeviceForm initialValues={this.props.device} onFinish={this.onFinish} tenant={this.props.tenant} update />;
}
return <DeviceForm initialValues={props.device} onFinish={onFinish} tenant={props.tenant} update />;
}
export default EditDevice;

View File

@ -1,4 +1,4 @@
import React, { Component } from "react";
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import moment from "moment";
@ -42,126 +42,105 @@ interface IProps {
application: Application;
}
interface IState {
selectedRowIds: string[];
multicastGroups: MulticastGroupListItem[];
relays: RelayListItem[];
mgModalVisible: boolean;
relayModalVisible: boolean;
mgSelected: string;
relaySelected: string;
}
function ListDevices(props: IProps) {
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [multicastGroups, setMulticastGroups] = useState<MulticastGroupListItem[]>([]);
const [relays, setRelays] = useState<RelayListItem[]>([]);
const [mgModalVisible, setMgModalVisible] = useState<boolean>(false);
const [relayModalVisible, setRelayModalVisible] = useState<boolean>(false);
const [mgSelected, setMgSelected] = useState<string>("");
const [relaySelected, setRelaySelected] = useState<string>("");
class ListDevices extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
selectedRowIds: [],
multicastGroups: [],
relays: [],
mgModalVisible: false,
relayModalVisible: false,
mgSelected: "",
relaySelected: "",
};
}
componentDidMount() {
useEffect(() => {
let mgReq = new ListMulticastGroupsRequest();
mgReq.setLimit(999);
mgReq.setApplicationId(this.props.application.getId());
mgReq.setApplicationId(props.application.getId());
MulticastGroupStore.list(mgReq, (resp: ListMulticastGroupsResponse) => {
this.setState({
multicastGroups: resp.getResultList(),
});
setMulticastGroups(resp.getResultList());
});
let relayReq = new ListRelaysRequest();
relayReq.setLimit(999);
relayReq.setApplicationId(this.props.application.getId());
relayReq.setApplicationId(props.application.getId());
RelayStore.list(relayReq, (resp: ListRelaysResponse) => {
this.setState({
relays: resp.getResultList(),
});
setRelays(resp.getResultList());
});
}
}, [props]);
columns = (): ColumnsType<DeviceListItem.AsObject> => {
return [
{
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");
}
return "Never";
},
const columns: ColumnsType<DeviceListItem.AsObject> = [
{
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");
}
return "Never";
},
{
title: "DevEUI",
dataIndex: "devEui",
key: "devEui",
width: 250,
render: (text, record) => (
<Link
to={`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/devices/${
record.devEui
}`}
>
{text}
</Link>
),
},
{
title: "Name",
dataIndex: "name",
key: "name",
},
{
title: "Device profile",
dataIndex: "deviceProfileName",
key: "deviceProfileName",
render: (text, record) => (
<Link to={`/tenants/${this.props.application.getTenantId()}/device-profiles/${record.deviceProfileId}/edit`}>
{text}
</Link>
),
},
{
title: "Battery",
dataIndex: "deviceStatus",
key: "deviceStatus",
render: (text, record) => {
if (record.deviceStatus === undefined) {
return;
}
},
{
title: "DevEUI",
dataIndex: "devEui",
key: "devEui",
width: 250,
render: (text, record) => (
<Link
to={`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/devices/${
record.devEui
}`}
>
{text}
</Link>
),
},
{
title: "Name",
dataIndex: "name",
key: "name",
},
{
title: "Device profile",
dataIndex: "deviceProfileName",
key: "deviceProfileName",
render: (text, record) => (
<Link to={`/tenants/${props.application.getTenantId()}/device-profiles/${record.deviceProfileId}/edit`}>
{text}
</Link>
),
},
{
title: "Battery",
dataIndex: "deviceStatus",
key: "deviceStatus",
render: (text, record) => {
if (record.deviceStatus === undefined) {
return;
}
if (record.deviceStatus.externalPowerSource === true) {
return <FontAwesomeIcon icon={faPlug} />;
} else if (record.deviceStatus.batteryLevel > 75) {
return <FontAwesomeIcon icon={faBatteryFull} />;
} else if (record.deviceStatus.batteryLevel > 50) {
return <FontAwesomeIcon icon={faBatteryThreeQuarters} />;
} else if (record.deviceStatus.batteryLevel > 25) {
return <FontAwesomeIcon icon={faBatteryHalf} />;
} else if (record.deviceStatus.batteryLevel > 0) {
return <FontAwesomeIcon icon={faBatteryQuarter} />;
}
},
if (record.deviceStatus.externalPowerSource === true) {
return <FontAwesomeIcon icon={faPlug} />;
} else if (record.deviceStatus.batteryLevel > 75) {
return <FontAwesomeIcon icon={faBatteryFull} />;
} else if (record.deviceStatus.batteryLevel > 50) {
return <FontAwesomeIcon icon={faBatteryThreeQuarters} />;
} else if (record.deviceStatus.batteryLevel > 25) {
return <FontAwesomeIcon icon={faBatteryHalf} />;
} else if (record.deviceStatus.batteryLevel > 0) {
return <FontAwesomeIcon icon={faBatteryQuarter} />;
}
},
];
};
},
];
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
let req = new ListDevicesRequest();
req.setApplicationId(this.props.application.getId());
req.setApplicationId(props.application.getId());
req.setLimit(limit);
req.setOffset(offset);
@ -171,148 +150,114 @@ class ListDevices extends Component<IProps, IState> {
});
};
onRowsSelectChange = (ids: string[]) => {
this.setState({
selectedRowIds: ids,
});
const onRowsSelectChange = (ids: string[]) => {
setSelectedRowIds(ids);
};
showMgModal = () => {
this.setState({
mgModalVisible: true,
});
const showMgModal = () => {
setMgModalVisible(true);
};
showRelayModal = () => {
this.setState({
relayModalVisible: true,
});
}
hideMgModal = () => {
this.setState({
mgModalVisible: false,
});
const showRelayModal = () => {
setRelayModalVisible(true);
};
hideRelayModal = () => {
this.setState({
relayModalVisible: false,
});
}
onMgSelected = (value: string) => {
this.setState({
mgSelected: value,
});
const hideMgModal = () => {
setMgModalVisible(false);
};
onRelaySelected = (value: string) => {
this.setState({
relaySelected: value,
});
const hideRelayModal = () => {
setRelayModalVisible(false);
};
handleMgModalOk = () => {
for (let devEui of this.state.selectedRowIds) {
const onMgSelected = (value: string) => {
setMgSelected(value);
};
const onRelaySelected = (value: string) => {
setRelaySelected(value);
};
const handleMgModalOk = () => {
for (let devEui of selectedRowIds) {
let req = new AddDeviceToMulticastGroupRequest();
req.setMulticastGroupId(this.state.mgSelected);
req.setMulticastGroupId(mgSelected);
req.setDevEui(devEui);
MulticastGroupStore.addDevice(req, () => {});
}
this.setState({
mgModalVisible: false,
});
setMgModalVisible(false);
};
handleRelayModalOk = () => {
for (let devEui of this.state.selectedRowIds) {
const handleRelayModalOk = () => {
for (let devEui of selectedRowIds) {
let req = new AddRelayDeviceRequest();
req.setRelayDevEui(this.state.relaySelected);
req.setRelayDevEui(relaySelected);
req.setDeviceDevEui(devEui);
RelayStore.addDevice(req, () => {});
}
this.setState({
relayModalVisible: false,
});
setRelayModalVisible(false);
};
render() {
const menu = (
<Menu>
<Menu.Item onClick={this.showMgModal}>Add to multicast-group</Menu.Item>
<Menu.Item onClick={this.showRelayModal}>Add to relay</Menu.Item>
</Menu>
);
const menu = (
<Menu>
<Menu.Item onClick={showMgModal}>Add to multicast-group</Menu.Item>
<Menu.Item onClick={showRelayModal}>Add to relay</Menu.Item>
</Menu>
);
const mgOptions = this.state.multicastGroups.map((mg, i) => (
<Select.Option value={mg.getId()}>{mg.getName()}</Select.Option>
));
const mgOptions = multicastGroups.map((mg, i) => <Select.Option value={mg.getId()}>{mg.getName()}</Select.Option>);
const relayOptions = this.state.relays.map((r, i) => (
<Select.Option value={r.getDevEui()}>{r.getName()}</Select.Option>
));
const relayOptions = relays.map((r, i) => <Select.Option value={r.getDevEui()}>{r.getName()}</Select.Option>);
return (
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<Modal
title="Add selected devices to multicast-group"
visible={this.state.mgModalVisible}
onOk={this.handleMgModalOk}
onCancel={this.hideMgModal}
okButtonProps={{ disabled: this.state.mgSelected === "" }}
>
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<Select style={{ width: "100%" }} onChange={this.onMgSelected} placeholder="Select Multicast-group">
{mgOptions}
</Select>
</Space>
</Modal>
<Modal
title="Add selected devices to relay"
visible={this.state.relayModalVisible}
onOk={this.handleRelayModalOk}
onCancel={this.hideRelayModal}
okButtonProps={{ disabled: this.state.relaySelected === "" }}
>
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<Select style={{ width: "100%" }} onChange={this.onRelaySelected} placeholder="Select Relay">
{relayOptions}
</Select>
</Space>
</Modal>
<Admin tenantId={this.props.application.getTenantId()} isDeviceAdmin>
<Space direction="horizontal" style={{ float: "right" }}>
<Button type="primary">
<Link
to={`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/devices/create`}
>
Add device
</Link>
</Button>
<Dropdown
placement="bottomRight"
overlay={menu}
trigger={["click"]}
disabled={this.state.selectedRowIds.length === 0}
return (
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<Modal
title="Add selected devices to multicast-group"
visible={mgModalVisible}
onOk={handleMgModalOk}
onCancel={hideMgModal}
okButtonProps={{ disabled: mgSelected === "" }}
>
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<Select style={{ width: "100%" }} onChange={onMgSelected} placeholder="Select Multicast-group">
{mgOptions}
</Select>
</Space>
</Modal>
<Modal
title="Add selected devices to relay"
visible={relayModalVisible}
onOk={handleRelayModalOk}
onCancel={hideRelayModal}
okButtonProps={{ disabled: relaySelected === "" }}
>
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<Select style={{ width: "100%" }} onChange={onRelaySelected} placeholder="Select Relay">
{relayOptions}
</Select>
</Space>
</Modal>
<Admin tenantId={props.application.getTenantId()} isDeviceAdmin>
<Space direction="horizontal" style={{ float: "right" }}>
<Button type="primary">
<Link
to={`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/devices/create`}
>
<Button>Selected devices</Button>
</Dropdown>
</Space>
</Admin>
<DataTable
columns={this.columns()}
getPage={this.getPage}
onRowsSelectChange={this.onRowsSelectChange}
rowKey="devEui"
/>
</Space>
);
}
Add device
</Link>
</Button>
<Dropdown placement="bottomRight" overlay={menu} trigger={["click"]} disabled={selectedRowIds.length === 0}>
<Button>Selected devices</Button>
</Dropdown>
</Space>
</Admin>
<DataTable columns={columns} getPage={getPage} onRowsSelectChange={onRowsSelectChange} rowKey="devEui" />
</Space>
);
}
export default ListDevices;

Some files were not shown because too many files have changed in this diff Show More