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

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

View File

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

View File

@ -1,18 +1,22 @@
import React, { Component } from "react"; import React, { useState } from "react";
import { Router, Route, Switch } from "react-router-dom"; import { Router, Routes, Route } from "react-router-dom";
import { Layout } from "antd"; import { Layout } from "antd";
import { User } from "@chirpstack/chirpstack-api-grpc-web/api/user_pb"; import { User } from "@chirpstack/chirpstack-api-grpc-web/api/user_pb";
import Menu from "./components/Menu";
import Header from "./components/Header"; import Header from "./components/Header";
import Menu from "./components/Menu";
// dashboard // dashboard
import Dashboard from "./views/dashboard/Dashboard"; import Dashboard from "./views/dashboard/Dashboard";
// users // users
import Login from "./views/users/Login"; 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 // tenants
import TenantRedirect from "./views/tenants/TenantRedirect"; import TenantRedirect from "./views/tenants/TenantRedirect";
@ -20,20 +24,14 @@ import ListTenants from "./views/tenants/ListTenants";
import CreateTenant from "./views/tenants/CreateTenant"; import CreateTenant from "./views/tenants/CreateTenant";
import TenantLoader from "./views/tenants/TenantLoader"; 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 // api keys
import ListAdminApiKeys from "./views/api-keys/ListAdminApiKeys"; import ListAdminApiKeys from "./views/api-keys/ListAdminApiKeys";
import CreateAdminApiKey from "./views/api-keys/CreateAdminApiKey"; import CreateAdminApiKey from "./views/api-keys/CreateAdminApiKey";
// device-profile templates // device-profile templates
import ListDeviceProfileTemplates from "./views/device-profile-templates/ListDeviceProfileTemplates"; 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 CreateDeviceProfileTemplate from "./views/device-profile-templates/CreateDeviceProfileTemplate";
import EditDeviceProfileTemplate from "./views/device-profile-templates/EditDeviceProfileTemplate";
// regions // regions
import ListRegions from "./views/regions/ListRegions"; import ListRegions from "./views/regions/ListRegions";
@ -44,85 +42,72 @@ import SessionStore from "./stores/SessionStore";
import history from "./history"; import history from "./history";
interface IProps {} const CustomRouter = ({ history, ...props }: any) => {
const [state, setState] = useState({
action: history.action,
location: history.location,
});
interface IState { React.useLayoutEffect(() => history.listen(setState), [history]);
user?: User;
}
class App extends Component<IProps, IState> { return <Router {...props} location={state.location} navigationType={state.action} navigator={history} />;
constructor(props: IProps) { };
super(props);
this.state = { function App() {
user: undefined, const [user, setUser] = useState<User | undefined>(SessionStore.getUser());
}; SessionStore.on("change", () => {
} setUser(SessionStore.getUser());
});
componentDidMount() { return (
SessionStore.on("change", () => { <Layout style={{ minHeight: "100vh" }}>
this.setState({ <CustomRouter history={history}>
user: SessionStore.getUser(), <Routes>
}); <Route path="/" element={<TenantRedirect />} />
}); <Route path="/login" element={<Login />} />
</Routes>
this.setState({ {user && (
user: SessionStore.getUser(), <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() { <Route path="/users" element={<ListUsers />} />
return ( <Route path="/users/create" element={<CreateUser />} />
<Layout style={{ minHeight: "100vh" }}> <Route path="/users/:userId" element={<EditUser />} />
<Router history={history}> <Route path="/users/:userId/password" element={<ChangeUserPassword />} />
<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 exact path="/tenants" component={ListTenants} /> <Route path="/api-keys" element={<ListAdminApiKeys />} />
<Route exact path="/tenants/create" component={CreateTenant} /> <Route path="/api-keys/create" element={<CreateAdminApiKey />} />
<Route path="/tenants/:tenantId([\w-]{36})" component={TenantLoader} />
<Route exact path="/users" component={ListUsers} /> <Route path="/device-profile-templates" element={<ListDeviceProfileTemplates />} />
<Route exact path="/users/create" component={CreateUser} /> <Route path="/device-profile-templates/create" element={<CreateDeviceProfileTemplate />} />
<Route exact path="/users/:userId([\w-]{36})" component={EditUser} /> <Route
<Route exact path="/users/:userId([\w-]{36})/password" component={ChangeUserPassword} /> path="/device-profile-templates/:deviceProfileTemplateId/edit"
element={<EditDeviceProfileTemplate />}
/>
<Route exact path="/api-keys" component={ListAdminApiKeys} /> <Route path="/regions" element={<ListRegions />} />
<Route exact path="/api-keys/create" component={CreateAdminApiKey} /> <Route path="/regions/:id" element={<RegionDetails />} />
</Routes>
<Route exact path="/device-profile-templates" component={ListDeviceProfileTemplates} /> </Layout.Content>
<Route exact path="/device-profile-templates/create" component={CreateDeviceProfileTemplate} /> </Layout>
<Route </div>
exact )}
path="/device-profile-templates/:deviceProfileTemplateId([\w-]+)/edit" </CustomRouter>
component={EditDeviceProfileTemplate} </Layout>
/> );
<Route exact path="/regions" component={ListRegions} />
<Route path="/regions/:id(.*)" component={RegionDetails} />
</Switch>
</Layout.Content>
</Layout>
</Route>
)}
</Switch>
</Router>
</Layout>
);
}
} }
export default App; 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"; import SessionStore from "../stores/SessionStore";
@ -9,73 +9,45 @@ interface IProps {
isTenantAdmin?: boolean; isTenantAdmin?: boolean;
} }
interface IState { function Admin(props: PropsWithChildren<IProps>) {
admin: boolean; const [admin, setAdmin] = useState<boolean>(false);
}
class Admin extends Component<IProps, IState> { const setIsAdmin = () => {
constructor(props: IProps) { if (!props.isDeviceAdmin && !props.isGatewayAdmin && !props.isTenantAdmin) {
super(props); setAdmin(SessionStore.isAdmin());
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(),
});
} else { } else {
if (this.props.tenantId === undefined) { if (props.tenantId === undefined) {
throw new Error("No tenantId is given"); throw new Error("No tenantId is given");
} }
if (this.props.isTenantAdmin) { if (props.isTenantAdmin) {
this.setState({ setAdmin(SessionStore.isAdmin() || SessionStore.isTenantAdmin(props.tenantId));
admin: SessionStore.isAdmin() || SessionStore.isTenantAdmin(this.props.tenantId),
});
} }
if (this.props.isDeviceAdmin) { if (props.isDeviceAdmin) {
this.setState({ setAdmin(SessionStore.isAdmin() || SessionStore.isTenantDeviceAdmin(props.tenantId));
admin: SessionStore.isAdmin() || SessionStore.isTenantDeviceAdmin(this.props.tenantId),
});
} }
if (this.props.isGatewayAdmin) { if (props.isGatewayAdmin) {
this.setState({ setAdmin(SessionStore.isAdmin() || SessionStore.isTenantGatewayAdmin(props.tenantId));
admin: SessionStore.isAdmin() || SessionStore.isTenantGatewayAdmin(this.props.tenantId),
});
} }
} }
}; };
render() { useEffect(() => {
if (this.state.admin) { SessionStore.on("change", setIsAdmin);
return this.props.children; setIsAdmin();
}
return null; return () => {
SessionStore.removeListener("change", setIsAdmin);
};
}, [props]);
if (admin) {
return <div>{props.children}</div>;
} }
return null;
} }
export default Admin; 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 { notification, Input, Select, Button, Space, Form, Dropdown, Menu } from "antd";
import { ReloadOutlined, CopyOutlined } from "@ant-design/icons"; import { ReloadOutlined, CopyOutlined } from "@ant-design/icons";
import { Buffer } from "buffer"; import { Buffer } from "buffer";
interface IProps { interface IProps {
formRef: React.RefObject<any>;
label: string; label: string;
name: string; name: string;
required?: boolean; required?: boolean;
@ -14,42 +13,29 @@ interface IProps {
tooltip?: string; tooltip?: string;
} }
interface IState { function AesKeyInput(props: IProps) {
byteOrder: string; const form = Form.useFormInstance();
value: string; const [byteOrder, setByteOrder] = useState<string>("msb");
} const [value, setValue] = useState<string>("");
class AesKeyInput extends Component<IProps, IState> { useEffect(() => {
constructor(props: IProps) { if (props.value) {
super(props); setValue(props.value);
this.state = { }
byteOrder: "msb", }, [props]);
value: "",
};
}
updateField = () => { const updateField = (v: string) => {
let value = this.state.value; if (byteOrder === "lsb") {
const bytes = v.match(/[A-Fa-f0-9]{2}/g) || [];
if (this.state.byteOrder === "lsb") { v = bytes.reverse().join("");
const bytes = value.match(/[A-Fa-f0-9]{2}/g) || [];
value = bytes.reverse().join("");
} }
this.props.formRef.current.setFieldsValue({ form.setFieldsValue({
[this.props.name]: value, [props.name]: v,
}); });
}; };
componentDidMount() { const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (this.props.value) {
this.setState({
value: this.props.value,
});
}
}
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let v = e.target.value; let v = e.target.value;
const match = v.match(/[A-Fa-f0-9]/g); const match = v.match(/[A-Fa-f0-9]/g);
@ -62,50 +48,37 @@ class AesKeyInput extends Component<IProps, IState> {
} }
} }
this.setState( setValue(value);
{ updateField(value);
value: value,
},
this.updateField,
);
}; };
onByteOrderSelect = (v: string) => { const onByteOrderSelect = (v: string) => {
if (v === this.state.byteOrder) { if (v === byteOrder) {
return; return;
} }
this.setState({ setByteOrder(v);
byteOrder: v,
});
const current = this.state.value; const current = value;
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || []; const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
const vv = bytes.reverse().join("");
this.setState( setValue(vv);
{ updateField(vv);
value: bytes.reverse().join(""),
},
this.updateField,
);
}; };
generateRandom = () => { const generateRandom = () => {
let cryptoObj = window.crypto || window.Crypto; let cryptoObj = window.crypto || window.Crypto;
let b = new Uint8Array(16); let b = new Uint8Array(16);
cryptoObj.getRandomValues(b); cryptoObj.getRandomValues(b);
let key = Buffer.from(b).toString("hex"); let key = Buffer.from(b).toString("hex");
this.setState( setValue(key);
{ updateField(key);
value: key,
},
this.updateField,
);
}; };
copyToClipboard = () => { const copyToClipboard = () => {
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g); const bytes = value.match(/[A-Fa-f0-9]{2}/g);
if (bytes !== null && navigator.clipboard !== undefined) { if (bytes !== null && navigator.clipboard !== undefined) {
navigator.clipboard navigator.clipboard
@ -126,8 +99,8 @@ class AesKeyInput extends Component<IProps, IState> {
} }
}; };
copyToClipboardHexArray = () => { const copyToClipboardHexArray = () => {
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g); const bytes = value.match(/[A-Fa-f0-9]{2}/g);
if (bytes !== null && navigator.clipboard !== undefined) { if (bytes !== null && navigator.clipboard !== undefined) {
navigator.clipboard navigator.clipboard
@ -153,72 +126,70 @@ class AesKeyInput extends Component<IProps, IState> {
} }
}; };
render() { const copyMenu = (
const copyMenu = ( <Menu
<Menu items={[
items={[ {
{ key: "1",
key: "1", label: (
label: ( <Button type="text" onClick={copyToClipboard}>
<Button type="text" onClick={this.copyToClipboard}> HEX string
HEX string </Button>
</Button> ),
), },
}, {
{ key: "2",
key: "2", label: (
label: ( <Button type="text" onClick={copyToClipboardHexArray}>
<Button type="text" onClick={this.copyToClipboardHexArray}> HEX array
HEX array </Button>
</Button> ),
), },
}, ]}
]} />
/> );
);
const addon = ( const addon = (
<Space size="large"> <Space size="large">
<Select value={this.state.byteOrder} onChange={this.onByteOrderSelect}> <Select value={byteOrder} onChange={onByteOrderSelect}>
<Select.Option value="msb">MSB</Select.Option> <Select.Option value="msb">MSB</Select.Option>
<Select.Option value="lsb">LSB</Select.Option> <Select.Option value="lsb">LSB</Select.Option>
</Select> </Select>
<Button type="text" size="small" shape="circle" onClick={this.generateRandom}> <Button type="text" size="small" onClick={generateRandom}>
<ReloadOutlined /> <ReloadOutlined />
</Button>
<Dropdown overlay={copyMenu}>
<Button type="text" size="small">
<CopyOutlined />
</Button> </Button>
<Dropdown overlay={copyMenu}> </Dropdown>
<Button type="text" size="small"> </Space>
<CopyOutlined /> );
</Button>
</Dropdown>
</Space>
);
return ( return (
<Form.Item <Form.Item
rules={[ rules={[
{ {
required: this.props.required, required: props.required,
message: `Please enter a valid ${this.props.label}`, message: `Please enter a valid ${props.label}`,
pattern: new RegExp(/[A-Fa-f0-9]{32}/g), pattern: new RegExp(/[A-Fa-f0-9]{32}/g),
}, },
]} ]}
label={this.props.label} label={props.label}
name={this.props.name} name={props.name}
tooltip={this.props.tooltip} tooltip={props.tooltip}
> >
<Input hidden /> <Input hidden />
<Input.Password <Input
id={`${this.props.name}Render`} id={`${props.name}Render`}
onChange={this.onChange} onChange={onChange}
addonAfter={!this.props.disabled && addon} addonAfter={!props.disabled && addon}
style={{ fontFamily: "monospace" }} className="input-code"
value={this.state.value} value={value}
disabled={this.props.disabled} disabled={props.disabled}
/> />
</Form.Item> </Form.Item>
); );
}
} }
export default AesKeyInput; export default AesKeyInput;

View File

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

View File

@ -1,11 +1,8 @@
import React, { Component } from "react";
import { Form } from "antd"; import { Form } from "antd";
import Autocomplete, { OptionCallbackFunc, OptionsCallbackFunc } from "./Autocomplete"; import Autocomplete, { OptionCallbackFunc, OptionsCallbackFunc } from "./Autocomplete";
interface IProps { interface IProps {
formRef: React.RefObject<any>;
label: string; label: string;
name: string; name: string;
required?: boolean; required?: boolean;
@ -14,28 +11,35 @@ interface IProps {
getOptions: (s: string, fn: OptionsCallbackFunc) => void; getOptions: (s: string, fn: OptionsCallbackFunc) => void;
} }
class AutocompleteInput extends Component<IProps> { function AutocompleteInput(props: IProps) {
render() { const form = Form.useFormInstance();
return (
<Form.Item const onSelect = (value: string) => {
rules={[ form.setFieldsValue({
{ [props.name]: value,
required: this.props.required, });
message: `Please select a ${this.props.label}`, };
},
]} return (
label={this.props.label} <Form.Item
name={this.props.name} rules={[
> {
<Autocomplete required: props.required,
placeholder={`Select a ${this.props.label}`} message: `Please select a ${props.label}`,
className="" },
getOption={this.props.getOption} ]}
getOptions={this.props.getOptions} label={props.label}
/> name={props.name}
</Form.Item> >
); <Autocomplete
} placeholder={`Select a ${props.label}`}
className=""
getOption={props.getOption}
getOptions={props.getOptions}
onSelect={onSelect}
/>
</Form.Item>
);
} }
export default AutocompleteInput; 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 { Controlled as CodeMirror } from "react-codemirror2";
import { Form } from "antd"; import { Form } from "antd";
@ -6,88 +6,49 @@ import { Form } from "antd";
import "codemirror/mode/javascript/javascript"; import "codemirror/mode/javascript/javascript";
interface IProps { interface IProps {
formRef: React.RefObject<any>;
label?: string; label?: string;
name: string; name: string;
required?: boolean; required?: boolean;
value?: string;
disabled?: boolean; disabled?: boolean;
tooltip?: string; tooltip?: string;
} }
interface IState { function CodeEditor(props: IProps) {
value: string; const form = Form.useFormInstance();
reloadKey: number; const [value, setValue] = useState<string>("");
} const [reloadKey, setReloadKey] = useState<number>(1);
class CodeEditor extends Component<IProps, IState> { useEffect(() => {
constructor(props: IProps) { setValue(form.getFieldValue(props.name));
super(props); setReloadKey(k => k + 1);
this.state = { }, [form, props]);
value: "",
reloadKey: 0,
};
}
componentDidMount() { const handleChange = (editor: any, data: any, newCode: string) => {
if (this.props.value) { setValue(newCode);
this.setState({ form.setFieldsValue({
value: this.props.value, [props.name]: newCode,
});
}
}
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,
}); });
}; };
handleChange = (editor: any, data: any, newCode: string) => { const codeMirrorOptions = {
this.setState( lineNumbers: true,
{ mode: "javascript",
value: newCode, theme: "base16-light",
}, readOnly: props.disabled,
this.updateField,
);
}; };
render() { return (
const codeMirrorOptions = { <Form.Item label={props.label} name={props.name} tooltip={props.tooltip}>
lineNumbers: true, <div style={{ border: "1px solid #cccccc" }}>
mode: "javascript", <CodeMirror
theme: "base16-light", key={`code-editor-refresh-${reloadKey}`}
readOnly: this.props.disabled, value={value}
}; options={codeMirrorOptions}
onBeforeChange={handleChange}
return ( />
<Form.Item label={this.props.label} name={this.props.name} tooltip={this.props.tooltip}> </div>
<div style={{ border: "1px solid #cccccc" }}> </Form.Item>
<CodeMirror );
key={`code-editor-refresh-${this.state.reloadKey}`}
value={this.state.value}
options={codeMirrorOptions}
onBeforeChange={this.handleChange}
/>
</div>
</Form.Item>
);
}
} }
export default CodeEditor; 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 { Table } from "antd";
import { ColumnsType } from "antd/es/table"; import { ColumnsType } from "antd/es/table";
@ -16,113 +16,81 @@ interface IProps {
noPagination?: boolean; noPagination?: boolean;
} }
interface IState { function DataTable(props: IProps) {
totalCount: number; const [totalCount, setTotalCount] = useState<number>(0);
pageSize: number; const [pageSize, setPageSize] = useState<number>(SessionStore.getRowsPerPage());
currentPage: number; const [currentPage, setCurrentPage] = useState<number>(1);
rows: object[]; const [rows, setRows] = useState<object[]>([]);
loading: boolean; const [loading, setLoading] = useState<boolean>(true);
}
class DataTable extends Component<IProps, IState> { const onChangePage = (page: number, pz?: number | void) => {
constructor(props: IProps) { setLoading(true);
super(props);
this.state = { if (!pz) {
totalCount: 0, pz = pageSize;
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;
} }
this.onChangePage(this.state.currentPage, this.state.pageSize); props.getPage(pz, (page - 1) * pz, (totalCount: number, rows: object[]) => {
} setCurrentPage(page);
setTotalCount(totalCount);
onChangePage = (page: number, pageSize?: number | void) => { setRows(rows);
this.setState( setPageSize(pz || 0);
{ setLoading(false);
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,
});
});
},
);
}; };
onShowSizeChange = (page: number, pageSize: number) => { const onShowSizeChange = (page: number, pageSize: number) => {
this.onChangePage(page, pageSize); onChangePage(page, pageSize);
SessionStore.setRowsPerPage(pageSize); SessionStore.setRowsPerPage(pageSize);
}; };
onRowsSelectChange = (ids: React.Key[]) => { const onRowsSelectChange = (ids: React.Key[]) => {
const idss = ids as string[]; const idss = ids as string[];
if (this.props.onRowsSelectChange) { if (props.onRowsSelectChange) {
this.props.onRowsSelectChange(idss); props.onRowsSelectChange(idss);
} }
}; };
render() { useEffect(() => {
const { getPage, refreshKey, ...otherProps } = this.props; onChangePage(currentPage, pageSize);
let loadingProps = undefined; }, [props, currentPage, pageSize]);
if (this.state.loading) {
loadingProps = {
delay: 300,
};
}
let pagination = undefined; const { getPage, refreshKey, ...otherProps } = props;
if (this.props.noPagination === undefined || this.props.noPagination === false) { let loadingProps = undefined;
pagination = { if (loading) {
current: this.state.currentPage, loadingProps = {
total: this.state.totalCount, delay: 300,
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}
/>
);
} }
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; 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"; import { Popover, Button, Typography, Space, Input } from "antd";
interface IProps { interface IProps {
@ -8,51 +7,32 @@ interface IProps {
onConfirm: () => void; onConfirm: () => void;
} }
interface ConfirmState { function DeleteConfirmContent(props: IProps) {
confirm: string; const [confirm, setConfirm] = useState<string>("");
}
class DeleteConfirmContent extends Component<IProps, ConfirmState> { const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
constructor(props: IProps) { setConfirm(e.target.value);
super(props);
this.state = {
confirm: "",
};
}
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
confirm: e.target.value,
});
}; };
render() { return (
return ( <Space direction="vertical">
<Space direction="vertical"> <Typography.Text>
<Typography.Text> Enter '{props.confirm}' to confirm you want to delete this {props.typ}:
Enter '{this.props.confirm}' to confirm you want to delete this {this.props.typ}: </Typography.Text>
</Typography.Text> <Input placeholder={props.confirm} onChange={onChange} />
<Input placeholder={this.props.confirm} onChange={this.onChange} /> <Button onClick={props.onConfirm} disabled={confirm !== props.confirm} style={{ float: "right" }}>
<Button Delete
onClick={this.props.onConfirm} </Button>
disabled={this.state.confirm !== this.props.confirm} </Space>
style={{ float: "right" }} );
>
Delete
</Button>
</Space>
);
}
} }
class DeleteConfirm extends Component<IProps> { function DeleteConfirm(props: PropsWithChildren<IProps>) {
render() { return (
return ( <Popover content={<DeleteConfirmContent {...props} />} trigger="click" placement="left">
<Popover content={<DeleteConfirmContent {...this.props} />} trigger="click" placement="left"> {props.children}
{this.props.children} </Popover>
</Popover> );
);
}
} }
export default DeleteConfirm; 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 { notification, Input, Select, Button, Space, Form, Dropdown, Menu } from "antd";
import { ReloadOutlined, CopyOutlined } from "@ant-design/icons"; import { ReloadOutlined, CopyOutlined } from "@ant-design/icons";
@ -8,7 +8,6 @@ import { GetRandomDevAddrRequest, GetRandomDevAddrResponse } from "@chirpstack/c
import DeviceStore from "../stores/DeviceStore"; import DeviceStore from "../stores/DeviceStore";
interface IProps { interface IProps {
formRef: React.RefObject<any>;
label: string; label: string;
name: string; name: string;
devEui: string; devEui: string;
@ -17,42 +16,29 @@ interface IProps {
disabled?: boolean; disabled?: boolean;
} }
interface IState { function DevAddrInput(props: IProps) {
byteOrder: string; const form = Form.useFormInstance();
value: string; const [byteOrder, setByteOrder] = useState<string>("msb");
} const [value, setValue] = useState<string>("");
class DevAddrInput extends Component<IProps, IState> { useEffect(() => {
constructor(props: IProps) { if (props.value) {
super(props); setValue(props.value);
this.state = { }
byteOrder: "msb", }, [props]);
value: "",
};
}
updateField = () => { const updateField = (v: string) => {
let value = this.state.value; if (byteOrder === "lsb") {
const bytes = v.match(/[A-Fa-f0-9]{2}/g) || [];
if (this.state.byteOrder === "lsb") { v = bytes.reverse().join("");
const bytes = value.match(/[A-Fa-f0-9]{2}/g) || [];
value = bytes.reverse().join("");
} }
this.props.formRef.current.setFieldsValue({ form.setFieldsValue({
[this.props.name]: value, [props.name]: v,
}); });
}; };
componentDidMount() { const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (this.props.value) {
this.setState({
value: this.props.value,
});
}
}
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let v = e.target.value; let v = e.target.value;
const match = v.match(/[A-Fa-f0-9]/g); const match = v.match(/[A-Fa-f0-9]/g);
@ -65,50 +51,37 @@ class DevAddrInput extends Component<IProps, IState> {
} }
} }
this.setState( setValue(value);
{ updateField(value);
value: value,
},
this.updateField,
);
}; };
onByteOrderSelect = (v: string) => { const onByteOrderSelect = (v: string) => {
if (v === this.state.byteOrder) { if (v === byteOrder) {
return; return;
} }
this.setState({ setByteOrder(v);
byteOrder: v,
});
const current = this.state.value; const current = value;
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || []; const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
const vv = bytes.reverse().join("");
this.setState( setValue(vv);
{ updateField(vv);
value: bytes.reverse().join(""),
},
this.updateField,
);
}; };
generateRandom = () => { const generateRandom = () => {
let req = new GetRandomDevAddrRequest(); let req = new GetRandomDevAddrRequest();
req.setDevEui(this.props.devEui); req.setDevEui(props.devEui);
DeviceStore.getRandomDevAddr(req, (resp: GetRandomDevAddrResponse) => { DeviceStore.getRandomDevAddr(req, (resp: GetRandomDevAddrResponse) => {
this.setState( setValue(resp.getDevAddr());
{ updateField(resp.getDevAddr());
value: resp.getDevAddr(),
},
this.updateField,
);
}); });
}; };
copyToClipboard = () => { const copyToClipboard = () => {
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g); const bytes = value.match(/[A-Fa-f0-9]{2}/g);
if (bytes !== null && navigator.clipboard !== undefined) { if (bytes !== null && navigator.clipboard !== undefined) {
navigator.clipboard navigator.clipboard
@ -129,8 +102,8 @@ class DevAddrInput extends Component<IProps, IState> {
} }
}; };
copyToClipboardHexArray = () => { const copyToClipboardHexArray = () => {
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g); const bytes = value.match(/[A-Fa-f0-9]{2}/g);
if (bytes !== null && navigator.clipboard !== undefined) { if (bytes !== null && navigator.clipboard !== undefined) {
navigator.clipboard navigator.clipboard
@ -156,71 +129,69 @@ class DevAddrInput extends Component<IProps, IState> {
} }
}; };
render() { const copyMenu = (
const copyMenu = ( <Menu
<Menu items={[
items={[ {
{ key: "1",
key: "1", label: (
label: ( <Button type="text" onClick={copyToClipboard}>
<Button type="text" onClick={this.copyToClipboard}> HEX string
HEX string </Button>
</Button> ),
), },
}, {
{ key: "2",
key: "2", label: (
label: ( <Button type="text" onClick={copyToClipboardHexArray}>
<Button type="text" onClick={this.copyToClipboardHexArray}> HEX array
HEX array </Button>
</Button> ),
), },
}, ]}
]} />
/> );
);
const addon = ( const addon = (
<Space size="large"> <Space size="large">
<Select value={this.state.byteOrder} onChange={this.onByteOrderSelect}> <Select value={byteOrder} onChange={onByteOrderSelect}>
<Select.Option value="msb">MSB</Select.Option> <Select.Option value="msb">MSB</Select.Option>
<Select.Option value="lsb">LSB</Select.Option> <Select.Option value="lsb">LSB</Select.Option>
</Select> </Select>
<Button type="text" size="small" shape="circle" onClick={this.generateRandom}> <Button type="text" size="small" onClick={generateRandom}>
<ReloadOutlined /> <ReloadOutlined />
</Button>
<Dropdown overlay={copyMenu}>
<Button type="text" size="small">
<CopyOutlined />
</Button> </Button>
<Dropdown overlay={copyMenu}> </Dropdown>
<Button type="text" size="small"> </Space>
<CopyOutlined /> );
</Button>
</Dropdown>
</Space>
);
return ( return (
<Form.Item <Form.Item
rules={[ rules={[
{ {
required: this.props.required, required: props.required,
message: `Please enter a valid ${this.props.label}`, message: `Please enter a valid ${props.label}`,
pattern: new RegExp(/[A-Fa-f0-9]{8}/g), pattern: new RegExp(/[A-Fa-f0-9]{8}/g),
}, },
]} ]}
label={this.props.label} label={props.label}
name={this.props.name} name={props.name}
> >
<Input hidden /> <Input hidden />
<Input <Input
id={`${this.props.name}Render`} id={`${props.name}Render`}
onChange={this.onChange} onChange={onChange}
addonAfter={!this.props.disabled && addon} addonAfter={!props.disabled && addon}
style={{ fontFamily: "monospace" }} className="input-code"
value={this.state.value} value={value}
disabled={this.props.disabled} disabled={props.disabled}
/> />
</Form.Item> </Form.Item>
); );
}
} }
export default DevAddrInput; 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 { notification, Input, Select, Button, Space, Form, Dropdown, Menu } from "antd";
import { ReloadOutlined, CopyOutlined } from "@ant-design/icons"; import { ReloadOutlined, CopyOutlined } from "@ant-design/icons";
import { Buffer } from "buffer"; import { Buffer } from "buffer";
interface IProps { interface IProps {
formRef: React.RefObject<any>;
label: string; label: string;
name: string; name: string;
required?: boolean; required?: boolean;
@ -14,42 +13,29 @@ interface IProps {
tooltip?: string; tooltip?: string;
} }
interface IState { function EuiInput(props: IProps) {
byteOrder: string; const form = Form.useFormInstance();
value: string; const [byteOrder, setByteOrder] = useState<string>("msb");
} const [value, setValue] = useState<string>("");
class EuiInput extends Component<IProps, IState> { useEffect(() => {
constructor(props: IProps) { if (props.value) {
super(props); setValue(props.value);
this.state = { }
byteOrder: "msb", }, [props]);
value: "",
};
}
updateField = () => { const updateField = (v: string) => {
let value = this.state.value; if (byteOrder === "lsb") {
const bytes = v.match(/[A-Fa-f0-9]{2}/g) || [];
if (this.state.byteOrder === "lsb") { v = bytes.reverse().join("");
const bytes = value.match(/[A-Fa-f0-9]{2}/g) || [];
value = bytes.reverse().join("");
} }
this.props.formRef.current.setFieldsValue({ form.setFieldsValue({
[this.props.name]: value, [props.name]: v,
}); });
}; };
componentDidMount() { const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (this.props.value) {
this.setState({
value: this.props.value,
});
}
}
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let v = e.target.value; let v = e.target.value;
const match = v.match(/[A-Fa-f0-9]/g); const match = v.match(/[A-Fa-f0-9]/g);
@ -62,50 +48,37 @@ class EuiInput extends Component<IProps, IState> {
} }
} }
this.setState( setValue(value);
{ updateField(value);
value: value,
},
this.updateField,
);
}; };
onByteOrderSelect = (v: string) => { const onByteOrderSelect = (v: string) => {
if (v === this.state.byteOrder) { if (v === byteOrder) {
return; return;
} }
this.setState({ setByteOrder(v);
byteOrder: v,
});
const current = this.state.value; const current = value;
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || []; const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
const vv = bytes.reverse().join("");
this.setState( setValue(vv);
{ updateField(vv);
value: bytes.reverse().join(""),
},
this.updateField,
);
}; };
generateRandom = () => { const generateRandom = () => {
let cryptoObj = window.crypto || window.Crypto; let cryptoObj = window.crypto || window.Crypto;
let b = new Uint8Array(8); let b = new Uint8Array(8);
cryptoObj.getRandomValues(b); cryptoObj.getRandomValues(b);
let key = Buffer.from(b).toString("hex"); let key = Buffer.from(b).toString("hex");
this.setState( setValue(key);
{ updateField(key);
value: key,
},
this.updateField,
);
}; };
copyToClipboard = () => { const copyToClipboard = () => {
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g); const bytes = value.match(/[A-Fa-f0-9]{2}/g);
if (bytes !== null && navigator.clipboard !== undefined) { if (bytes !== null && navigator.clipboard !== undefined) {
navigator.clipboard navigator.clipboard
@ -126,8 +99,8 @@ class EuiInput extends Component<IProps, IState> {
} }
}; };
copyToClipboardHexArray = () => { const copyToClipboardHexArray = () => {
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g); const bytes = value.match(/[A-Fa-f0-9]{2}/g);
if (bytes !== null && navigator.clipboard !== undefined) { if (bytes !== null && navigator.clipboard !== undefined) {
navigator.clipboard navigator.clipboard
@ -153,72 +126,70 @@ class EuiInput extends Component<IProps, IState> {
} }
}; };
render() { const copyMenu = (
const copyMenu = ( <Menu
<Menu items={[
items={[ {
{ key: "1",
key: "1", label: (
label: ( <Button type="text" onClick={copyToClipboard}>
<Button type="text" onClick={this.copyToClipboard}> HEX string
HEX string </Button>
</Button> ),
), },
}, {
{ key: "2",
key: "2", label: (
label: ( <Button type="text" onClick={copyToClipboardHexArray}>
<Button type="text" onClick={this.copyToClipboardHexArray}> HEX array
HEX array </Button>
</Button> ),
), },
}, ]}
]} />
/> );
);
const addon = ( const addon = (
<Space size="large"> <Space size="large">
<Select value={this.state.byteOrder} onChange={this.onByteOrderSelect}> <Select value={byteOrder} onChange={onByteOrderSelect}>
<Select.Option value="msb">MSB</Select.Option> <Select.Option value="msb">MSB</Select.Option>
<Select.Option value="lsb">LSB</Select.Option> <Select.Option value="lsb">LSB</Select.Option>
</Select> </Select>
<Button type="text" size="small" onClick={this.generateRandom}> <Button type="text" size="small" onClick={generateRandom}>
<ReloadOutlined /> <ReloadOutlined />
</Button>
<Dropdown overlay={copyMenu}>
<Button type="text" size="small">
<CopyOutlined />
</Button> </Button>
<Dropdown overlay={copyMenu}> </Dropdown>
<Button type="text" size="small"> </Space>
<CopyOutlined /> );
</Button>
</Dropdown>
</Space>
);
return ( return (
<Form.Item <Form.Item
rules={[ rules={[
{ {
required: this.props.required, required: props.required,
message: `Please enter a valid ${this.props.label}`, message: `Please enter a valid ${props.label}`,
pattern: new RegExp(/[A-Fa-f0-9]{16}/g), pattern: new RegExp(/[A-Fa-f0-9]{16}/g),
}, },
]} ]}
label={this.props.label} label={props.label}
name={this.props.name} name={props.name}
tooltip={this.props.tooltip} tooltip={props.tooltip}
> >
<Input hidden /> <Input hidden />
<Input <Input
id={`${this.props.name}Render`} id={`${props.name}Render`}
onChange={this.onChange} onChange={onChange}
addonAfter={!this.props.disabled && addon} addonAfter={!props.disabled && addon}
style={{ fontFamily: "monospace" }} className="input-code"
value={this.state.value} value={value}
disabled={this.props.disabled} disabled={props.disabled}
/> />
</Form.Item> </Form.Item>
); );
}
} }
export default EuiInput; export default EuiInput;

View File

@ -1,5 +1,5 @@
import React, { Component } from "react"; import React, { useState, useEffect } from "react";
import { Link, withRouter, RouteComponentProps } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { Button, Menu, Dropdown, Input, AutoComplete } from "antd"; import { Button, Menu, Dropdown, Input, AutoComplete } from "antd";
import { UserOutlined, DownOutlined, QuestionOutlined } from "@ant-design/icons"; import { UserOutlined, DownOutlined, QuestionOutlined } from "@ant-design/icons";
@ -14,15 +14,6 @@ import {
import InternalStore from "../stores/InternalStore"; import InternalStore from "../stores/InternalStore";
import SessionStore from "../stores/SessionStore"; 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 renderTitle = (title: string) => <span>{title}</span>;
const renderItem = (title: string, url: string) => ({ const renderItem = (title: string, url: string) => ({
@ -30,22 +21,19 @@ const renderItem = (title: string, url: string) => ({
label: <Link to={url}>{title}</Link>, label: <Link to={url}>{title}</Link>,
}); });
class Header extends Component<IProps, IState> { function Header({ user }: { user: User }) {
constructor(props: IProps) { const navigate = useNavigate();
super(props);
this.state = {}; const [settings, setSettings] = useState<SettingsResponse | undefined>(undefined);
} const [searchResult, setSearchResult] = useState<GlobalSearchResponse | undefined>(undefined);
componentDidMount() { useEffect(() => {
InternalStore.settings((resp: SettingsResponse) => { InternalStore.settings((resp: SettingsResponse) => {
this.setState({ setSettings(resp);
settings: resp,
});
}); });
} }, [user]);
onSearch = (search: string) => { const onSearch = (search: string) => {
if (search.length < 3) { if (search.length < 3) {
return; return;
} }
@ -55,14 +43,11 @@ class Header extends Component<IProps, IState> {
req.setSearch(search); req.setSearch(search);
InternalStore.globalSearch(req, (resp: GlobalSearchResponse) => { InternalStore.globalSearch(req, (resp: GlobalSearchResponse) => {
this.setState({ setSearchResult(resp);
searchResult: resp,
});
}); });
}; };
onLogout = () => { const onLogout = () => {
let settings = this.state.settings;
if (settings === undefined) { if (settings === undefined) {
return; return;
} }
@ -71,117 +56,112 @@ class Header extends Component<IProps, IState> {
if (!oidc.getEnabled() || oidc.getLogoutUrl() === "") { if (!oidc.getEnabled() || oidc.getLogoutUrl() === "") {
SessionStore.logout(true, () => { SessionStore.logout(true, () => {
this.props.history.push("/login"); navigate("/login");
}); });
} else { } else {
SessionStore.logout(false, () => { SessionStore.logout(false, () => {
window.location.assign(oidc.getLogoutUrl()); navigate(oidc.getLogoutUrl());
}); });
} }
}; };
render() { if (settings === undefined) {
if (this.state.settings === undefined) { return null;
return null; }
}
let oidcEnabled = this.state.settings!.getOpenidConnect()!.getEnabled(); let oidcEnabled = settings!.getOpenidConnect()!.getEnabled();
const menu = ( const menu = (
<Menu> <Menu>
{!oidcEnabled && ( {!oidcEnabled && (
<Menu.Item> <Menu.Item>
<Link to={`/users/${this.props.user.getId()}/password`}>Change password</Link> <Link to={`/users/${user.getId()}/password`}>Change password</Link>
</Menu.Item> </Menu.Item>
)} )}
<Menu.Item onClick={this.onLogout}>Logout</Menu.Item> <Menu.Item onClick={onLogout}>Logout</Menu.Item>
</Menu> </Menu>
); );
let options: { let options: {
label: any; label: any;
options: any[]; options: any[];
}[] = [ }[] = [
{ {
label: renderTitle("Tenants"), label: renderTitle("Tenants"),
options: [], options: [],
}, },
{ {
label: renderTitle("Gateways"), label: renderTitle("Gateways"),
options: [], options: [],
}, },
{ {
label: renderTitle("Applications"), label: renderTitle("Applications"),
options: [], options: [],
}, },
{ {
label: renderTitle("Devices"), label: renderTitle("Devices"),
options: [], options: [],
}, },
]; ];
if (this.state.searchResult !== undefined) { if (searchResult !== undefined) {
for (const res of this.state.searchResult.getResultList()) { for (const res of searchResult.getResultList()) {
if (res.getKind() === "tenant") { if (res.getKind() === "tenant") {
options[0].options.push(renderItem(res.getTenantName(), `/tenants/${res.getTenantId()}`)); options[0].options.push(renderItem(res.getTenantName(), `/tenants/${res.getTenantId()}`));
} }
if (res.getKind() === "gateway") { if (res.getKind() === "gateway") {
options[1].options.push( options[1].options.push(
renderItem(res.getGatewayName(), `/tenants/${res.getTenantId()}/gateways/${res.getGatewayId()}`), renderItem(res.getGatewayName(), `/tenants/${res.getTenantId()}/gateways/${res.getGatewayId()}`),
); );
} }
if (res.getKind() === "application") { if (res.getKind() === "application") {
options[2].options.push( options[2].options.push(
renderItem( renderItem(res.getApplicationName(), `/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}`),
res.getApplicationName(), );
`/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}`, }
),
);
}
if (res.getKind() === "device") { if (res.getKind() === "device") {
options[3].options.push( options[3].options.push(
renderItem( renderItem(
res.getDeviceName(), res.getDeviceName(),
`/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}/devices/${res.getDeviceDevEui()}`, `/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}/devices/${res.getDeviceDevEui()}`,
), ),
); );
}
} }
} }
}
return ( return (
<div> <div>
<img className="logo" alt="ChirpStack" src="/logo.png" /> <img className="logo" alt="ChirpStack" src="/logo.png" />
<div className="actions"> <div className="actions">
<div className="search"> <div className="search">
<AutoComplete <AutoComplete
dropdownClassName="search-dropdown" dropdownClassName="search-dropdown"
dropdownMatchSelectWidth={500} dropdownMatchSelectWidth={500}
options={options} options={options}
onSearch={this.onSearch} onSearch={onSearch}
> >
<Input.Search placeholder="Search..." style={{ width: 500, marginTop: -5 }} /> <Input.Search placeholder="Search..." style={{ width: 500, marginTop: -5 }} />
</AutoComplete> </AutoComplete>
</div> </div>
<div className="help"> <div className="help">
<a href="https://www.chirpstack.io" target="_blank" rel="noreferrer"> <a href="https://www.chirpstack.io" target="_blank" rel="noreferrer">
<Button icon={<QuestionOutlined />} /> <Button icon={<QuestionOutlined />} />
</a> </a>
</div> </div>
<div className="user"> <div className="user">
<Dropdown overlay={menu} placement="bottomRight" trigger={["click"]}> <Dropdown overlay={menu} placement="bottomRight" trigger={["click"]}>
<Button type="primary" icon={<UserOutlined />}> <Button type="primary" icon={<UserOutlined />}>
{this.props.user.getEmail()} <DownOutlined /> {user.getEmail()} <DownOutlined />
</Button> </Button>
</Dropdown> </Dropdown>
</div>
</div> </div>
</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 moment from "moment";
import JSONTreeOriginal from "react-json-tree"; import JSONTreeOriginal from "react-json-tree";
import fileDownload from "js-file-download"; import fileDownload from "js-file-download";
@ -12,160 +13,142 @@ interface IProps {
logs: LogItem[]; logs: LogItem[];
} }
interface IState { function LogTable(props: IProps) {
drawerOpen: boolean; const [drawerOpen, setDrawerOpen] = useState<boolean>(false);
body: any; const [body, setBody] = useState<any>(null);
drawerTitle: any; const [drawerTitle, setDrawerTitle] = useState<any>(null);
}
class LogTable extends Component<IProps, IState> { const onDrawerClose = () => {
constructor(props: IProps) { setDrawerOpen(false);
super(props);
this.state = {
drawerOpen: false,
body: null,
drawerTitle: null,
};
}
onDrawerClose = () => {
this.setState({
drawerOpen: false,
});
}; };
onDrawerOpen = (time: any, body: any) => { const onDrawerOpen = (time: any, body: any) => {
let ts = new Date(0); let ts = new Date(0);
ts.setUTCSeconds(time.seconds); ts.setUTCSeconds(time.seconds);
let drawerTitle = moment(ts).format("YYYY-MM-DD HH:mm:ss"); let drawerTitle = moment(ts).format("YYYY-MM-DD HH:mm:ss");
return () => { return () => {
this.setState({ setBody(body);
body: body, setDrawerTitle(drawerTitle);
drawerTitle: drawerTitle, setDrawerOpen(true);
drawerOpen: true,
});
}; };
}; };
downloadSingleFrame = () => { const downloadSingleFrame = () => {
fileDownload(JSON.stringify(JSON.parse(this.state.body), null, 4), "single-log.json", "application/json"); fileDownload(JSON.stringify(JSON.parse(body), null, 4), "single-log.json", "application/json");
}; };
downloadFrames = () => { const downloadFrames = () => {
let items = this.props.logs.map((l, i) => JSON.parse(l.getBody())); let items = props.logs.map((l, i) => JSON.parse(l.getBody()));
fileDownload(JSON.stringify(items, null, 4), "log.json"); fileDownload(JSON.stringify(items, null, 4), "log.json");
}; };
render() { let items = props.logs.map((l, i) => l.toObject());
let items = this.props.logs.map((l, i) => l.toObject()); let bodyJson = JSON.parse(body);
let body = JSON.parse(this.state.body);
const theme = { const theme = {
scheme: "google", scheme: "google",
author: "seth wright (http://sethawright.com)", author: "seth wright (http://sethawright.com)",
base00: "#000000", base00: "#000000",
base01: "#282a2e", base01: "#282a2e",
base02: "#373b41", base02: "#373b41",
base03: "#969896", base03: "#969896",
base04: "#b4b7b4", base04: "#b4b7b4",
base05: "#c5c8c6", base05: "#c5c8c6",
base06: "#e0e0e0", base06: "#e0e0e0",
base07: "#ffffff", base07: "#ffffff",
base08: "#CC342B", base08: "#CC342B",
base09: "#F96A38", base09: "#F96A38",
base0A: "#FBA922", base0A: "#FBA922",
base0B: "#198844", base0B: "#198844",
base0C: "#3971ED", base0C: "#3971ED",
base0D: "#3971ED", base0D: "#3971ED",
base0E: "#A36AC7", base0E: "#A36AC7",
base0F: "#3971ED", base0F: "#3971ED",
}; };
return ( return (
<Space direction="vertical" size="large" style={{ width: "100%" }}> <Space direction="vertical" size="large" style={{ width: "100%" }}>
<Drawer <Drawer
title={`Details: ${this.state.drawerTitle}`} title={`Details: ${drawerTitle}`}
placement="right" placement="right"
width={650} width={650}
onClose={this.onDrawerClose} onClose={onDrawerClose}
visible={this.state.drawerOpen} visible={drawerOpen}
extra={<Button onClick={this.downloadSingleFrame}>Download</Button>} extra={<Button onClick={downloadSingleFrame}>Download</Button>}
> >
<JSONTreeOriginal <JSONTreeOriginal
data={body} data={bodyJson}
theme={theme} theme={theme}
hideRoot={true} hideRoot={true}
shouldExpandNode={() => { shouldExpandNode={() => {
return true; 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;
}),
},
]}
/> />
</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; 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 L, { LatLngTuple, FitBoundsOptions } from "leaflet";
import "leaflet.awesome-markers"; 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"; import { MapContainer, Marker as LMarker, TileLayer } from "react-leaflet";
interface IProps { interface IProps {
@ -12,77 +12,48 @@ interface IProps {
boundsOptions?: FitBoundsOptions; boundsOptions?: FitBoundsOptions;
} }
interface IState { function MapControl(props: { center?: [number, number]; bounds?: LatLngTuple[]; boundsOptions?: FitBoundsOptions }) {
map?: L.Map; const map = useMap();
}
class Map extends Component<IProps, IState> { useEffect(() => {
constructor(props: IProps) { if (map === undefined) {
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) {
return; return;
} }
if (this.state.map) { if (props.center !== undefined) {
if (this.props.center !== undefined) { map.flyTo(props.center);
this.state.map.flyTo(this.props.center);
}
if (this.props.bounds !== undefined) {
this.state.map.flyToBounds(this.props.bounds, this.props.boundsOptions);
}
} }
}
render() { if (props.bounds !== undefined) {
const style = { map.flyToBounds(props.bounds, props.boundsOptions);
height: this.props.height, }
}; });
return ( return null;
<MapContainer }
bounds={this.props.bounds}
boundsOptions={this.props.boundsOptions} function Map(props: PropsWithChildren<IProps>) {
center={this.props.center} const style = {
zoom={13} height: props.height,
scrollWheelZoom={false} };
animate={true}
style={style} return (
whenCreated={this.setMap} <MapContainer
> bounds={props.bounds}
<TileLayer boundsOptions={props.boundsOptions}
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' center={props.center}
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" zoom={13}
/> scrollWheelZoom={false}
{this.props.children} style={style}
</MapContainer> >
); <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 = export type MarkerColor =
@ -103,22 +74,20 @@ interface MarkerProps extends LMarkerProps {
color: MarkerColor; color: MarkerColor;
} }
export class Marker extends Component<MarkerProps> { export function Marker(props: MarkerProps) {
render() { const { faIcon, color, position, ...otherProps } = props;
const { faIcon, color, position, ...otherProps } = this.props;
const iconMarker = L.AwesomeMarkers.icon({ const iconMarker = L.AwesomeMarkers.icon({
icon: faIcon, icon: faIcon,
prefix: "fa", prefix: "fa",
markerColor: color, markerColor: color,
}); });
return ( return (
<LMarker icon={iconMarker} position={position} {...otherProps}> <LMarker icon={iconMarker} position={position} {...otherProps}>
{this.props.children} {props.children}
</LMarker> </LMarker>
); );
}
} }
export default Map; export default Map;

View File

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

View File

@ -1,5 +1,3 @@
import React, { Component } from "react";
import { Card } from "antd"; import { Card } from "antd";
import { TimeUnit } from "chart.js"; import { TimeUnit } from "chart.js";
@ -14,83 +12,81 @@ interface IProps {
zeroToNull?: boolean; zeroToNull?: boolean;
} }
class MetricChart extends Component<IProps> { function MetricChart(props: IProps) {
render() { let unit: TimeUnit = "hour";
let unit: TimeUnit = "hour"; let tooltipFormat = "LT";
let tooltipFormat = "LT"; if (props.aggregation === Aggregation.DAY) {
if (this.props.aggregation === Aggregation.DAY) { unit = "day";
unit = "day"; tooltipFormat = "MMM Do";
tooltipFormat = "MMM Do"; } else if (props.aggregation === Aggregation.MONTH) {
} else if (this.props.aggregation === Aggregation.MONTH) { unit = "month";
unit = "month"; tooltipFormat = "MMM YYYY";
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>
);
} }
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; export default MetricChart;

View File

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

View File

@ -31,7 +31,7 @@
padding-top: 85px; padding-top: 85px;
overflow: auto; overflow: auto;
position: fixed; position: fixed !important;
height: 100vh; height: 100vh;
left: 0; left: 0;
} }
@ -117,5 +117,10 @@ pre {
} }
.ant-drawer-body { .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 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 { Chart, registerables } from "chart.js";
import { MatrixElement, MatrixController } from "chartjs-chart-matrix"; import { MatrixElement, MatrixController } from "chartjs-chart-matrix";
import "chartjs-adapter-moment"; import "chartjs-adapter-moment";
@ -9,7 +8,7 @@ import "chartjs-adapter-moment";
import App from "./App"; import App from "./App";
import reportWebVitals from "./reportWebVitals"; import reportWebVitals from "./reportWebVitals";
import "antd/dist/antd.min.css"; import "antd/dist/reset.css";
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css"; import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
import "@fortawesome/fontawesome-free/css/all.css"; import "@fortawesome/fontawesome-free/css/all.css";
@ -19,12 +18,8 @@ import "./index.css";
Chart.register(MatrixController, MatrixElement, ...registerables); Chart.register(MatrixController, MatrixElement, ...registerables);
ReactDOM.render( const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
<React.StrictMode> root.render(<App />);
<App />
</React.StrictMode>,
document.getElementById("root"),
);
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log)) // to log results (for example: reportWebVitals(console.log))

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Input, Typography, Button, Space } from "antd"; import { Input, Typography, Button, Space } from "antd";
@ -9,23 +8,21 @@ interface IProps {
createApiKeyResponse: CreateApiKeyResponse; createApiKeyResponse: CreateApiKeyResponse;
} }
class ApiKeyToken extends Component<IProps> { function ApiKeyToken(props: IProps) {
render() { return (
return ( <Space direction="vertical" style={{ width: "100%" }}>
<Space direction="vertical" style={{ width: "100%" }}> <Typography>
<Typography> <Typography.Paragraph>
<Typography.Paragraph> Use the following API token when making API requests. This token can be revoked at any time by deleting it.
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:
Please note that this token can only be retrieved once: </Typography.Paragraph>
</Typography.Paragraph> </Typography>
</Typography> <Input.TextArea rows={4} value={props.createApiKeyResponse.getToken()} />
<Input.TextArea rows={4} value={this.props.createApiKeyResponse.getToken()} /> <Button type="primary">
<Button type="primary"> <Link to="../api-keys">Back</Link>
<Link to="../api-keys">Back</Link> </Button>
</Button> </Space>
</Space> );
);
}
} }
export default ApiKeyToken; 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 { 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 { 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 ApiKeyToken from "./ApiKeyToken";
import InternalStore from "../../stores/InternalStore"; import InternalStore from "../../stores/InternalStore";
interface IProps {} function CreateAdminApiKey() {
const [createApiKeyResponse, setCreateApiKeyResponse] = useState<CreateApiKeyResponse | undefined>(undefined);
interface IState { const onFinish = (obj: ApiKey) => {
createApiKeyResponse?: CreateApiKeyResponse;
}
class CreateAdminApiKey extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
onFinish = (obj: ApiKey) => {
obj.setIsAdmin(true); obj.setIsAdmin(true);
let req = new CreateApiKeyRequest(); let req = new CreateApiKeyRequest();
req.setApiKey(obj); req.setApiKey(obj);
InternalStore.createApiKey(req, (resp: CreateApiKeyResponse) => { InternalStore.createApiKey(req, (resp: CreateApiKeyResponse) => {
this.setState({ setCreateApiKeyResponse(resp);
createApiKeyResponse: resp,
});
}); });
}; };
render() { const apiKey = new ApiKey();
const apiKey = new ApiKey();
return ( return (
<Space direction="vertical" style={{ width: "100%" }} size="large"> <Space direction="vertical" style={{ width: "100%" }} size="large">
<PageHeader <PageHeader
title="Add API key" title="Add API key"
breadcrumbRender={() => ( breadcrumbRender={() => (
<Breadcrumb> <Breadcrumb>
<Breadcrumb.Item> <Breadcrumb.Item>
<span>Network-server</span> <span>Network-server</span>
</Breadcrumb.Item> </Breadcrumb.Item>
<Breadcrumb.Item> <Breadcrumb.Item>
<span> <span>
<Link to="/api-keys">API keys</Link> <Link to="/api-keys">API keys</Link>
</span> </span>
</Breadcrumb.Item> </Breadcrumb.Item>
<Breadcrumb.Item> <Breadcrumb.Item>
<span>Add</span> <span>Add</span>
</Breadcrumb.Item> </Breadcrumb.Item>
</Breadcrumb> </Breadcrumb>
)} )}
/> />
<Card> <Card>
{!this.state.createApiKeyResponse && <ApiKeyForm initialValues={apiKey} onFinish={this.onFinish} />} {!createApiKeyResponse && <ApiKeyForm initialValues={apiKey} onFinish={onFinish} />}
{this.state.createApiKeyResponse && <ApiKeyToken createApiKeyResponse={this.state.createApiKeyResponse} />} {createApiKeyResponse && <ApiKeyToken createApiKeyResponse={createApiKeyResponse} />}
</Card> </Card>
</Space> </Space>
); );
}
} }
export default CreateAdminApiKey; 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 { 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 { ApiKey, CreateApiKeyRequest, CreateApiKeyResponse } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_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 ApiKeyToken from "./ApiKeyToken";
import InternalStore from "../../stores/InternalStore"; import InternalStore from "../../stores/InternalStore";
interface IState {
createApiKeyResponse?: CreateApiKeyResponse;
}
interface IProps { interface IProps {
tenant: Tenant; tenant: Tenant;
} }
class CreateTenantApiKey extends Component<IProps, IState> { function CreateTenantApiKey(props: IProps) {
constructor(props: IProps) { const [createApiKeyResponse, setCreateApiKeyResponse] = useState<CreateApiKeyResponse | undefined>(undefined);
super(props);
this.state = {};
}
onFinish = (obj: ApiKey) => { const onFinish = (obj: ApiKey) => {
obj.setTenantId(this.props.tenant.getId()); obj.setTenantId(props.tenant.getId());
let req = new CreateApiKeyRequest(); let req = new CreateApiKeyRequest();
req.setApiKey(obj); req.setApiKey(obj);
InternalStore.createApiKey(req, (resp: CreateApiKeyResponse) => { InternalStore.createApiKey(req, (resp: CreateApiKeyResponse) => {
this.setState({ setCreateApiKeyResponse(resp);
createApiKeyResponse: resp,
});
}); });
}; };
render() { const apiKey = new ApiKey();
const apiKey = new ApiKey();
return ( return (
<Space direction="vertical" style={{ width: "100%" }} size="large"> <Space direction="vertical" style={{ width: "100%" }} size="large">
<PageHeader <PageHeader
breadcrumbRender={() => ( breadcrumbRender={() => (
<Breadcrumb> <Breadcrumb>
<Breadcrumb.Item> <Breadcrumb.Item>
<span>Tenants</span> <span>Tenants</span>
</Breadcrumb.Item> </Breadcrumb.Item>
<Breadcrumb.Item> <Breadcrumb.Item>
<span> <span>
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link> <Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
</span> </span>
</Breadcrumb.Item> </Breadcrumb.Item>
<Breadcrumb.Item> <Breadcrumb.Item>
<span> <span>
<Link to={`/tenants/${this.props.tenant.getId()}/api-keys`}>API Keys</Link> <Link to={`/tenants/${props.tenant.getId()}/api-keys`}>API Keys</Link>
</span> </span>
</Breadcrumb.Item> </Breadcrumb.Item>
<Breadcrumb.Item> <Breadcrumb.Item>
<span>Add</span> <span>Add</span>
</Breadcrumb.Item> </Breadcrumb.Item>
</Breadcrumb> </Breadcrumb>
)} )}
title="Add API key" title="Add API key"
/> />
<Card> <Card>
{!this.state.createApiKeyResponse && <ApiKeyForm initialValues={apiKey} onFinish={this.onFinish} />} {!createApiKeyResponse && <ApiKeyForm initialValues={apiKey} onFinish={onFinish} />}
{this.state.createApiKeyResponse && <ApiKeyToken createApiKeyResponse={this.state.createApiKeyResponse} />} {createApiKeyResponse && <ApiKeyToken createApiKeyResponse={createApiKeyResponse} />}
</Card> </Card>
</Space> </Space>
); );
}
} }
export default CreateTenantApiKey; 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 { Link } from "react-router-dom";
import { DeleteOutlined } from "@ant-design/icons"; 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 { ColumnsType } from "antd/es/table";
import { PageHeader } from "@ant-design/pro-layout";
import { import {
ListApiKeysRequest, ListApiKeysRequest,
@ -17,62 +17,47 @@ import DataTable, { GetPageCallbackFunc } from "../../components/DataTable";
import InternalStore from "../../stores/InternalStore"; import InternalStore from "../../stores/InternalStore";
import DeleteConfirm from "../../components/DeleteConfirm"; import DeleteConfirm from "../../components/DeleteConfirm";
interface IProps {} function ListAdminApiKeys() {
const [refreshKey, setRefreshKey] = useState<number>(1);
interface IState { const columns: ColumnsType<ApiKey.AsObject> = [
refreshKey: number; {
} 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> { const deleteApiKey = (id: string): (() => void) => {
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) => {
return () => { return () => {
let req = new DeleteApiKeyRequest(); let req = new DeleteApiKeyRequest();
req.setId(id); req.setId(id);
InternalStore.deleteApiKey(req, () => { InternalStore.deleteApiKey(req, () => {
// trigger a data-table reload // trigger a data-table reload
this.setState({ setRefreshKey(refreshKey + 1);
refreshKey: this.state.refreshKey + 1,
});
}); });
}; };
}; };
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => { const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
let req = new ListApiKeysRequest(); let req = new ListApiKeysRequest();
req.setLimit(limit); req.setLimit(limit);
req.setOffset(offset); req.setOffset(offset);
@ -84,31 +69,29 @@ class ListAdminApiKeys extends Component<IProps, IState> {
}); });
}; };
render() { return (
return ( <Space direction="vertical" style={{ width: "100%" }} size="large">
<Space direction="vertical" style={{ width: "100%" }} size="large"> <PageHeader
<PageHeader breadcrumbRender={() => (
breadcrumbRender={() => ( <Breadcrumb>
<Breadcrumb> <Breadcrumb.Item>
<Breadcrumb.Item> <span>Network Server</span>
<span>Network Server</span> </Breadcrumb.Item>
</Breadcrumb.Item> <Breadcrumb.Item>
<Breadcrumb.Item> <span>API keys</span>
<span>API keys</span> </Breadcrumb.Item>
</Breadcrumb.Item> </Breadcrumb>
</Breadcrumb> )}
)} title="API keys"
title="API keys" extra={[
extra={[ <Button type="primary">
<Button type="primary"> <Link to="/api-keys/create">Add API key</Link>
<Link to="/api-keys/create">Add API key</Link> </Button>,
</Button>, ]}
]} />
/> <DataTable columns={columns} getPage={getPage} rowKey="id" refreshKey={refreshKey} />
<DataTable columns={this.columns()} getPage={this.getPage} rowKey="id" refreshKey={this.state.refreshKey} /> </Space>
</Space> );
);
}
} }
export default ListAdminApiKeys; 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 { Link } from "react-router-dom";
import { DeleteOutlined } from "@ant-design/icons"; 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 { ColumnsType } from "antd/es/table";
import { import {
@ -20,69 +21,55 @@ import Admin from "../../components/Admin";
interface IProps { interface IProps {
tenant: Tenant; tenant: Tenant;
isAdmin: boolean;
} }
interface IState { function ListTenantApiKeys(props: IProps) {
refreshKey: number; const [refreshKey, setRefreshKey] = useState<number>(1);
}
class ListTenantApiKeys extends Component<IProps, IState> { const columns: ColumnsType<ApiKey.AsObject> = [
constructor(props: IProps) { {
super(props); title: "ID",
this.state = { dataIndex: "id",
refreshKey: 1, 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> => { const deleteApiKey = (id: string): (() => void) => {
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) => {
return () => { return () => {
let req = new DeleteApiKeyRequest(); let req = new DeleteApiKeyRequest();
req.setId(id); req.setId(id);
InternalStore.deleteApiKey(req, () => { InternalStore.deleteApiKey(req, () => {
// trigger a data-table reload // trigger a data-table reload
this.setState({ setRefreshKey(refreshKey + 1);
refreshKey: this.state.refreshKey + 1,
});
}); });
}; };
}; };
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => { const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
let req = new ListApiKeysRequest(); let req = new ListApiKeysRequest();
req.setLimit(limit); req.setLimit(limit);
req.setOffset(offset); req.setOffset(offset);
req.setTenantId(this.props.tenant.getId()); req.setTenantId(props.tenant.getId());
InternalStore.listApiKeys(req, (resp: ListApiKeysResponse) => { InternalStore.listApiKeys(req, (resp: ListApiKeysResponse) => {
const obj = resp.toObject(); const obj = resp.toObject();
@ -90,38 +77,36 @@ class ListTenantApiKeys extends Component<IProps, IState> {
}); });
}; };
render() { return (
return ( <Space direction="vertical" style={{ width: "100%" }} size="large">
<Space direction="vertical" style={{ width: "100%" }} size="large"> <PageHeader
<PageHeader breadcrumbRender={() => (
breadcrumbRender={() => ( <Breadcrumb>
<Breadcrumb> <Breadcrumb.Item>
<Breadcrumb.Item> <span>Tenants</span>
<span>Tenants</span> </Breadcrumb.Item>
</Breadcrumb.Item> <Breadcrumb.Item>
<Breadcrumb.Item> <span>
<span> <Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link> </span>
</span> </Breadcrumb.Item>
</Breadcrumb.Item> <Breadcrumb.Item>
<Breadcrumb.Item> <span>API Keys</span>
<span>API Keys</span> </Breadcrumb.Item>
</Breadcrumb.Item> </Breadcrumb>
</Breadcrumb> )}
)} title="API keys"
title="API keys" extra={[
extra={[ <Admin tenantId={props.tenant.getId()} isTenantAdmin>
<Admin tenantId={this.props.tenant.getId()} isTenantAdmin> <Button type="primary">
<Button type="primary"> <Link to={`/tenants/${props.tenant.getId()}/api-keys/create`}>Add API key</Link>
<Link to={`/tenants/${this.props.tenant.getId()}/api-keys/create`}>Add API key</Link> </Button>
</Button> </Admin>,
</Admin>, ]}
]} />
/> <DataTable columns={columns} getPage={getPage} rowKey="id" refreshKey={refreshKey} />
<DataTable columns={this.columns()} getPage={this.getPage} rowKey="id" refreshKey={this.state.refreshKey} /> </Space>
</Space> );
);
}
} }
export default ListTenantApiKeys; 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 { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
import { Form, Input, Button } from "antd"; import { Form, Input, Button } from "antd";
@ -9,9 +7,9 @@ interface IProps {
disabled?: boolean; disabled?: boolean;
} }
class ApplicationForm extends Component<IProps> { function ApplicationForm(props: IProps) {
onFinish = (values: Application.AsObject) => { const onFinish = (values: Application.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values); const v = Object.assign(props.initialValues.toObject(), values);
let app = new Application(); let app = new Application();
app.setId(v.id); app.setId(v.id);
@ -19,26 +17,24 @@ class ApplicationForm extends Component<IProps> {
app.setName(v.name); app.setName(v.name);
app.setDescription(v.description); app.setDescription(v.description);
this.props.onFinish(app); props.onFinish(app);
}; };
render() { return (
return ( <Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<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!" }]}>
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}> <Input disabled={props.disabled} />
<Input disabled={this.props.disabled} /> </Form.Item>
</Form.Item> <Form.Item label="Description" name="description">
<Form.Item label="Description" name="description"> <Input.TextArea disabled={props.disabled} />
<Input.TextArea disabled={this.props.disabled} /> </Form.Item>
</Form.Item> <Form.Item>
<Form.Item> <Button type="primary" htmlType="submit" disabled={props.disabled}>
<Button type="primary" htmlType="submit" disabled={this.props.disabled}> Submit
Submit </Button>
</Button> </Form.Item>
</Form.Item> </Form>
</Form> );
);
}
} }
export default ApplicationForm; export default ApplicationForm;

View File

@ -1,7 +1,7 @@
import React, { Component } from "react"; import { Route, Routes, Link, useNavigate, useLocation } from "react-router-dom";
import { Route, Switch, RouteComponentProps, Link } 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 { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
import { Application, DeleteApplicationRequest } from "@chirpstack/chirpstack-api-grpc-web/api/application_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 CreateIftttIntegration from "./integrations/CreateIftttIntegration";
import EditIftttIntegration from "./integrations/EditIftttIntegration"; import EditIftttIntegration from "./integrations/EditIftttIntegration";
interface IProps extends RouteComponentProps { interface IProps {
tenant: Tenant; tenant: Tenant;
application: Application; application: Application;
measurementKeys: string[]; measurementKeys: string[];
} }
class ApplicationLayout extends Component<IProps> { function ApplicationLayout(props: IProps) {
deleteApplication = () => { const navigate = useNavigate();
const location = useLocation();
const deleteApplication = () => {
let req = new DeleteApplicationRequest(); let req = new DeleteApplicationRequest();
req.setId(this.props.application.getId()); req.setId(props.application.getId());
ApplicationStore.delete(req, () => { ApplicationStore.delete(req, () => {
this.props.history.push(`/tenants/${this.props.tenant.getId()}/applications`); navigate(`/tenants/${props.tenant.getId()}/applications`);
}); });
}; };
render() { const tenant = props.tenant;
const tenant = this.props.tenant; const app = props.application;
const app = this.props.application;
if (!app) { if (!app) {
return null; 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>
);
} }
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; export default ApplicationLayout;

View File

@ -1,5 +1,5 @@
import React, { Component } from "react"; import React, { useState, useEffect } from "react";
import { Route, Switch, RouteComponentProps } from "react-router-dom"; import { Route, Routes, useParams } from "react-router-dom";
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb"; import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
import { import {
@ -16,90 +16,57 @@ import MulticastGroupLayout from "../multicast-groups/MulticastGroupLayout";
import CreateMulticastGroup from "../multicast-groups/CreateMulticastGroup"; import CreateMulticastGroup from "../multicast-groups/CreateMulticastGroup";
import RelayLayout from "../relays/RelayLayout"; import RelayLayout from "../relays/RelayLayout";
interface MatchParams { interface IProps {
applicationId: string;
}
interface IProps extends RouteComponentProps<MatchParams> {
tenant: Tenant; tenant: Tenant;
} }
interface IState { function ApplicationLoader(props: IProps) {
application?: Application; const { applicationId } = useParams();
measurementKeys: string[]; const [application, setApplication] = useState<Application | undefined>(undefined);
} const [measurementKeys, setMeasurementKeys] = useState<string[]>([]);
class ApplicationLoader extends Component<IProps, IState> { useEffect(() => {
constructor(props: IProps) { ApplicationStore.on("change", loadApplication);
super(props); loadApplication();
this.state = {
measurementKeys: [], return () => {
ApplicationStore.removeAllListeners("change");
}; };
} }, [applicationId]);
componentDidMount() { const loadApplication = () => {
this.getApplication();
}
getApplication = () => {
let req = new GetApplicationRequest(); let req = new GetApplicationRequest();
req.setId(this.props.match.params.applicationId); req.setId(applicationId!);
ApplicationStore.get(req, (resp: GetApplicationResponse) => { ApplicationStore.get(req, (resp: GetApplicationResponse) => {
this.setState({ setApplication(resp.getApplication());
application: resp.getApplication(), setMeasurementKeys(resp.getMeasurementKeysList());
measurementKeys: resp.getMeasurementKeysList(),
});
}); });
}; };
render() { const app = application;
const app = this.state.application; if (!app) {
if (!app) { return null;
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 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; export default ApplicationLoader;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd"; import { Col, Card, Popconfirm } from "antd";
@ -13,47 +12,45 @@ interface IProps {
add?: boolean; add?: boolean;
} }
class AwsSns extends Component<IProps> { function AwsSns(props: IProps) {
onDelete = () => { const onDelete = () => {
let req = new DeleteAwsSnsIntegrationRequest(); let req = new DeleteAwsSnsIntegrationRequest();
req.setApplicationId(this.props.application.getId()); req.setApplicationId(props.application.getId());
ApplicationStore.deleteAwsSnsIntegration(req, () => {}); ApplicationStore.deleteAwsSnsIntegration(req, () => {});
}; };
render() { let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) { if (!!props.add) {
actions = [ actions = [
<Link to="integrations/aws-sns/create"> <Link to="aws-sns/create">
<PlusOutlined /> <PlusOutlined />
</Link>, </Link>,
]; ];
} else { } else {
actions = [ actions = [
<Link to="integrations/aws-sns/edit"> <Link to="aws-sns/edit">
<EditOutlined /> <EditOutlined />
</Link>, </Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}> <Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined /> <DeleteOutlined />
</Popconfirm>, </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>
);
} }
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; export default AwsSns;

View File

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

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd"; import { Col, Card, Popconfirm } from "antd";
@ -16,46 +15,44 @@ interface IProps {
add?: boolean; add?: boolean;
} }
class AzureServiceBusCard extends Component<IProps> { function AzureServiceBusCard(props: IProps) {
onDelete = () => { const onDelete = () => {
let req = new DeleteAzureServiceBusIntegrationRequest(); let req = new DeleteAzureServiceBusIntegrationRequest();
req.setApplicationId(this.props.application.getId()); req.setApplicationId(props.application.getId());
ApplicationStore.deleteAzureServiceBusIntegration(req, () => {}); ApplicationStore.deleteAzureServiceBusIntegration(req, () => {});
}; };
render() { let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) { if (!!props.add) {
actions = [ actions = [
<Link to="integrations/azure-service-bus/create"> <Link to="azure-service-bus/create">
<PlusOutlined /> <PlusOutlined />
</Link>, </Link>,
]; ];
} else { } else {
actions = [ actions = [
<Link to="integrations/azure-service-bus/edit"> <Link to="azure-service-bus/edit">
<EditOutlined /> <EditOutlined />
</Link>, </Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}> <Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined /> <DeleteOutlined />
</Popconfirm>, </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>
);
} }
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; export default AzureServiceBusCard;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd"; import { Col, Card, Popconfirm } from "antd";
@ -13,46 +12,44 @@ interface IProps {
add?: boolean; add?: boolean;
} }
class GcpPubSubCard extends Component<IProps> { function GcpPubSubCard(props: IProps) {
onDelete = () => { const onDelete = () => {
let req = new DeleteGcpPubSubIntegrationRequest(); let req = new DeleteGcpPubSubIntegrationRequest();
req.setApplicationId(this.props.application.getId()); req.setApplicationId(props.application.getId());
ApplicationStore.deleteGcpPubSubIntegration(req, () => {}); ApplicationStore.deleteGcpPubSubIntegration(req, () => {});
}; };
render() { let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) { if (!!props.add) {
actions = [ actions = [
<Link to="integrations/gcp-pub-sub/create"> <Link to="gcp-pub-sub/create">
<PlusOutlined /> <PlusOutlined />
</Link>, </Link>,
]; ];
} else { } else {
actions = [ actions = [
<Link to="integrations/gcp-pub-sub/edit"> <Link to="gcp-pub-sub/edit">
<EditOutlined /> <EditOutlined />
</Link>, </Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}> <Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined /> <DeleteOutlined />
</Popconfirm>, </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>
);
} }
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; export default GcpPubSubCard;

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd"; import { Col, Card, Popconfirm } from "antd";
@ -13,46 +12,44 @@ interface IProps {
add?: boolean; add?: boolean;
} }
class HttpCard extends Component<IProps> { function HttpCard(props: IProps) {
onDelete = () => { const onDelete = () => {
let req = new DeleteHttpIntegrationRequest(); let req = new DeleteHttpIntegrationRequest();
req.setApplicationId(this.props.application.getId()); req.setApplicationId(props.application.getId());
ApplicationStore.deleteHttpIntegration(req, () => {}); ApplicationStore.deleteHttpIntegration(req, () => {});
}; };
render() { let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) { if (!!props.add) {
actions = [ actions = [
<Link to="integrations/http/create"> <Link to="http/create">
<PlusOutlined /> <PlusOutlined />
</Link>, </Link>,
]; ];
} else { } else {
actions = [ actions = [
<Link to="integrations/http/edit"> <Link to="http/edit">
<EditOutlined /> <EditOutlined />
</Link>, </Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}> <Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined /> <DeleteOutlined />
</Popconfirm>, </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>
);
} }
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; 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 { Form, Input, Button, Select, Row, Col, Typography, Space } from "antd";
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"; import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
@ -10,9 +8,9 @@ interface IProps {
onFinish: (obj: HttpIntegration) => void; onFinish: (obj: HttpIntegration) => void;
} }
class HttpIntegrationForm extends Component<IProps> { function HttpIntegrationForm(props: IProps) {
onFinish = (values: HttpIntegration.AsObject) => { const onFinish = (values: HttpIntegration.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values); const v = Object.assign(props.initialValues.toObject(), values);
let i = new HttpIntegration(); let i = new HttpIntegration();
i.setApplicationId(v.applicationId); i.setApplicationId(v.applicationId);
@ -24,79 +22,77 @@ class HttpIntegrationForm extends Component<IProps> {
i.getHeadersMap().set(elm[0], elm[1]); i.getHeadersMap().set(elm[0], elm[1]);
} }
this.props.onFinish(i); props.onFinish(i);
}; };
render() { return (
return ( <Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}> <Form.Item
<Form.Item label="Payload encoding"
label="Payload encoding" name="encoding"
name="encoding" rules={[{ required: true, message: "Please select an encoding!" }]}
rules={[{ required: true, message: "Please select an encoding!" }]} >
> <Select>
<Select> <Select.Option value={Encoding.JSON}>JSON</Select.Option>
<Select.Option value={Encoding.JSON}>JSON</Select.Option> <Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option> </Select>
</Select> </Form.Item>
</Form.Item> <Form.Item
<Form.Item label="Event endpoint URL(s)"
label="Event endpoint URL(s)" name="eventEndpointUrl"
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."
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!" }]}
rules={[{ required: true, message: "Please enter an event endpoint URL!" }]} >
> <Input />
<Input /> </Form.Item>
</Form.Item> <Space direction="vertical" style={{ width: "100%" }}>
<Space direction="vertical" style={{ width: "100%" }}> <Typography.Text>Headers</Typography.Text>
<Typography.Text>Headers</Typography.Text> <Form.List name="headersMap">
<Form.List name="headersMap"> {(fields, { add, remove }) => (
{(fields, { add, remove }) => ( <>
<> {fields.map(({ key, name, ...restField }) => (
{fields.map(({ key, name, ...restField }) => ( <Row gutter={24}>
<Row gutter={24}> <Col span={6}>
<Col span={6}> <Form.Item
<Form.Item {...restField}
{...restField} name={[name, 0]}
name={[name, 0]} fieldKey={[name, 0]}
fieldKey={[name, 0]} rules={[{ required: true, message: "Please enter a key!" }]}
rules={[{ required: true, message: "Please enter a key!" }]} >
> <Input placeholder="Key" />
<Input placeholder="Key" /> </Form.Item>
</Form.Item> </Col>
</Col> <Col span={16}>
<Col span={16}> <Form.Item
<Form.Item {...restField}
{...restField} name={[name, 1]}
name={[name, 1]} fieldKey={[name, 1]}
fieldKey={[name, 1]} rules={[{ required: true, message: "Please enter a value!" }]}
rules={[{ required: true, message: "Please enter a value!" }]} >
> <Input placeholder="Value" />
<Input placeholder="Value" /> </Form.Item>
</Form.Item> </Col>
</Col> <Col span={2}>
<Col span={2}> <MinusCircleOutlined onClick={() => remove(name)} />
<MinusCircleOutlined onClick={() => remove(name)} /> </Col>
</Col> </Row>
</Row> ))}
))} <Form.Item>
<Form.Item> <Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}> Add header
Add header </Button>
</Button> </Form.Item>
</Form.Item> </>
</> )}
)} </Form.List>
</Form.List> </Space>
</Space> <Form.Item>
<Form.Item> <Button type="primary" htmlType="submit">
<Button type="primary" htmlType="submit"> Submit
Submit </Button>
</Button> </Form.Item>
</Form.Item> </Form>
</Form> );
);
}
} }
export default HttpIntegrationForm; export default HttpIntegrationForm;

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd"; import { Col, Card, Popconfirm } from "antd";
@ -13,46 +12,44 @@ interface IProps {
add?: boolean; add?: boolean;
} }
class IftttCard extends Component<IProps> { function IftttCard(props: IProps) {
onDelete = () => { const onDelete = () => {
let req = new DeleteIftttIntegrationRequest(); let req = new DeleteIftttIntegrationRequest();
req.setApplicationId(this.props.application.getId()); req.setApplicationId(props.application.getId());
ApplicationStore.deleteIftttIntegration(req, () => {}); ApplicationStore.deleteIftttIntegration(req, () => {});
}; };
render() { let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) { if (!!props.add) {
actions = [ actions = [
<Link to="integrations/ifttt/create"> <Link to="ifttt/create">
<PlusOutlined /> <PlusOutlined />
</Link>, </Link>,
]; ];
} else { } else {
actions = [ actions = [
<Link to="integrations/ifttt/edit"> <Link to="ifttt/edit">
<EditOutlined /> <EditOutlined />
</Link>, </Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}> <Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined /> <DeleteOutlined />
</Popconfirm>, </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>
);
} }
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; 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"; import { Form, Input, AutoComplete, Button, Row, Col, Switch } from "antd";
@ -10,29 +10,15 @@ interface IProps {
onFinish: (obj: IftttIntegration) => void; onFinish: (obj: IftttIntegration) => void;
} }
interface IState { function IftttIntegrationForm(props: IProps) {
arbitraryJson: boolean; const [arbitraryJson, setArbitraryJson] = useState<Boolean>(false);
}
class IftttIntegrationForm extends Component<IProps, IState> { useEffect(() => {
constructor(props: IProps) { setArbitraryJson(props.initialValues.getArbitraryJson());
super(props); }, [props]);
this.state = { const onFinish = (values: IftttIntegration.AsObject) => {
arbitraryJson: false, const v = Object.assign(props.initialValues.toObject(), values);
};
}
componentDidMount() {
const v = this.props.initialValues;
this.setState({
arbitraryJson: v.getArbitraryJson(),
});
}
onFinish = (values: IftttIntegration.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values);
let i = new IftttIntegration(); let i = new IftttIntegration();
i.setApplicationId(v.applicationId); i.setApplicationId(v.applicationId);
@ -41,55 +27,58 @@ class IftttIntegrationForm extends Component<IProps, IState> {
i.setArbitraryJson(v.arbitraryJson); i.setArbitraryJson(v.arbitraryJson);
i.setUplinkValuesList(v.uplinkValuesList); i.setUplinkValuesList(v.uplinkValuesList);
this.props.onFinish(i); props.onFinish(i);
}; };
onArbitraryJsonChange = (checked: boolean) => { const onArbitraryJsonChange = (checked: boolean) => {
this.setState({ setArbitraryJson(checked);
arbitraryJson: checked, };
});
}
render() { const options: {
const options: { value: string;
value: string; }[] = props.measurementKeys.map(v => {
}[] = this.props.measurementKeys.map(v => { return { value: v };
return { value: v }; });
});
return ( return (
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}> <Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<Form.Item <Form.Item
label="Key" label="Key"
name="key" name="key"
rules={[{ required: true, message: "Please enter a key!" }]} rules={[{ required: true, message: "Please enter a key!" }]}
tooltip="This key can be obtained from the IFTTT Webhooks integrations documentation" tooltip="This key can be obtained from the IFTTT Webhooks integrations documentation"
> >
<Input.Password /> <Input.Password />
</Form.Item> </Form.Item>
<Row gutter={24}> <Row gutter={24}>
<Col span={12}> <Col span={12}>
<Form.Item <Form.Item
label="Event prefix" label="Event prefix"
name="eventPrefix" name="eventPrefix"
rules={[{ pattern: /[A-Za-z0-9]+/, message: "Only use A-Z, a-z and 0-9 characters" }]} rules={[
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." {
> pattern: /[A-Za-z0-9]+/,
<Input /> message: "Only use A-Z, a-z and 0-9 characters",
</Form.Item> },
</Col> ]}
<Col span={12}> 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."
<Form.Item >
label="Publish as arbitrary JSON" <Input />
name="arbitraryJson" </Form.Item>
valuePropName="checked" </Col>
tooltip="If enabled, the event payload will be published as-is (arbitrary JSON payload instead of 3 JSON values format)." <Col span={12}>
> <Form.Item
<Switch onChange={this.onArbitraryJsonChange} /> label="Publish as arbitrary JSON"
</Form.Item> name="arbitraryJson"
</Col> valuePropName="checked"
</Row> tooltip="If enabled, the event payload will be published as-is (arbitrary JSON payload instead of 3 JSON values format)."
{!this.state.arbitraryJson && <Form.List name="uplinkValuesList"> >
<Switch onChange={onArbitraryJsonChange} />
</Form.Item>
</Col>
</Row>
{!arbitraryJson && (
<Form.List name="uplinkValuesList">
{fields => ( {fields => (
<Row gutter={24}> <Row gutter={24}>
{fields.map((field, i) => ( {fields.map((field, i) => (
@ -105,15 +94,15 @@ class IftttIntegrationForm extends Component<IProps, IState> {
))} ))}
</Row> </Row>
)} )}
</Form.List>} </Form.List>
<Form.Item> )}
<Button type="primary" htmlType="submit"> <Form.Item>
Submit <Button type="primary" htmlType="submit">
</Button> Submit
</Form.Item> </Button>
</Form> </Form.Item>
); </Form>
} );
} }
export default IftttIntegrationForm; 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"; import { Form, Input, Button, Select } from "antd";
@ -13,20 +13,11 @@ interface IProps {
onFinish: (obj: InfluxDbIntegration) => void; onFinish: (obj: InfluxDbIntegration) => void;
} }
interface IState { function InfluxDbIntegrationForm(props: IProps) {
selectedVersion: InfluxDbVersion; const [selectedVersion, setSelectedVersion] = useState<InfluxDbVersion>(InfluxDbVersion.INFLUXDB_1);
}
class InfluxDbIntegrationForm extends Component<IProps, IState> { const onFinish = (values: InfluxDbIntegration.AsObject) => {
constructor(props: IProps) { const v = Object.assign(props.initialValues.toObject(), values);
super(props);
this.state = {
selectedVersion: InfluxDbVersion.INFLUXDB_1,
};
}
onFinish = (values: InfluxDbIntegration.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values);
let i = new InfluxDbIntegration(); let i = new InfluxDbIntegration();
i.setApplicationId(v.applicationId); i.setApplicationId(v.applicationId);
@ -41,98 +32,90 @@ class InfluxDbIntegrationForm extends Component<IProps, IState> {
i.setBucket(v.bucket); i.setBucket(v.bucket);
i.setToken(v.token); i.setToken(v.token);
this.props.onFinish(i); props.onFinish(i);
}; };
onVersionChange = (version: InfluxDbVersion) => { const onVersionChange = (version: InfluxDbVersion) => {
this.setState({ setSelectedVersion(version);
selectedVersion: version,
});
}; };
render() { return (
return ( <Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.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 <Form.Item
label="InfluxDB version" label="Retention policy name"
name="version" name="retentionPolicyName"
rules={[{ required: true, message: "Please select an InfluxDB version!" }]} 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}> <Input />
<Select.Option value={InfluxDbVersion.INFLUXDB_1}>InfluxDB v1</Select.Option> </Form.Item>
<Select.Option value={InfluxDbVersion.INFLUXDB_2}>InfluxDB v2</Select.Option> )}
{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> </Select>
</Form.Item> </Form.Item>
<Form.Item )}
label="API endpoint (write)" {selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
name="endpoint" <Form.Item label="Organization" name="organization">
rules={[{ required: true, message: "Please enter an endpoint!" }]} <Input />
>
<Input placeholder="http://localhost:8086/api/v2/write" />
</Form.Item> </Form.Item>
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_1 && ( )}
<Form.Item label="Username" name="username"> {selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
<Input /> <Form.Item label="Bucket" name="bucket">
</Form.Item> <Input />
)}
{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>
</Form.Item> </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; export default InfluxDbIntegrationForm;

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd"; import { Col, Card, Popconfirm } from "antd";
@ -13,46 +12,44 @@ interface IProps {
add?: boolean; add?: boolean;
} }
class InfluxdbCard extends Component<IProps> { function InfluxdbCard(props: IProps) {
onDelete = () => { const onDelete = () => {
let req = new DeleteInfluxDbIntegrationRequest(); let req = new DeleteInfluxDbIntegrationRequest();
req.setApplicationId(this.props.application.getId()); req.setApplicationId(props.application.getId());
ApplicationStore.deleteInfluxDbIntegration(req, () => {}); ApplicationStore.deleteInfluxDbIntegration(req, () => {});
}; };
render() { let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) { if (!!props.add) {
actions = [ actions = [
<Link to="integrations/influxdb/create"> <Link to="influxdb/create">
<PlusOutlined /> <PlusOutlined />
</Link>, </Link>,
]; ];
} else { } else {
actions = [ actions = [
<Link to="integrations/influxdb/edit"> <Link to="influxdb/edit">
<EditOutlined /> <EditOutlined />
</Link>, </Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}> <Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined /> <DeleteOutlined />
</Popconfirm>, </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>
);
} }
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; export default InfluxdbCard;

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd"; import { Col, Card, Popconfirm } from "antd";
@ -13,46 +12,44 @@ interface IProps {
add?: boolean; add?: boolean;
} }
class LoRaCloudCard extends Component<IProps> { function LoRaCloudCard(props: IProps) {
onDelete = () => { const onDelete = () => {
let req = new DeleteLoraCloudIntegrationRequest(); let req = new DeleteLoraCloudIntegrationRequest();
req.setApplicationId(this.props.application.getId()); req.setApplicationId(props.application.getId());
ApplicationStore.deleteLoraCloudIntegration(req, () => {}); ApplicationStore.deleteLoraCloudIntegration(req, () => {});
}; };
render() { let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) { if (!!props.add) {
actions = [ actions = [
<Link to="integrations/loracloud/create"> <Link to="loracloud/create">
<PlusOutlined /> <PlusOutlined />
</Link>, </Link>,
]; ];
} else { } else {
actions = [ actions = [
<Link to="integrations/loracloud/edit"> <Link to="loracloud/edit">
<EditOutlined /> <EditOutlined />
</Link>, </Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}> <Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined /> <DeleteOutlined />
</Popconfirm>, </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>
);
} }
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; 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 { Form, Input, InputNumber, Switch, Button, Tabs, Collapse } from "antd";
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"; import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
@ -13,43 +13,28 @@ interface IProps {
onFinish: (obj: LoraCloudIntegration) => void; onFinish: (obj: LoraCloudIntegration) => void;
} }
interface IState { function LoRaCloudIntegrationForm(props: IProps) {
modemEnabled: boolean; const [modemEnabled, setModemEnabled] = useState<boolean>(false);
geolocationTdoa: boolean; const [geolocationTdoa, setGeolocationTdoa] = useState<boolean>(false);
geolocationRssi: boolean; const [geolocationRssi, setGeolocationRssi] = useState<boolean>(false);
geolocationWifi: boolean; const [geolocationWifi, setGeolocationWifi] = useState<boolean>(false);
geolocationGnss: boolean; const [geolocationGnss, setGeolocationGnss] = useState<boolean>(false);
}
class LoRaCloudIntegrationForm extends Component<IProps, IState> { useEffect(() => {
constructor(props: IProps) { const v = props.initialValues;
super(props);
this.state = {
modemEnabled: false,
geolocationTdoa: false,
geolocationRssi: false,
geolocationWifi: false,
geolocationGnss: false,
};
}
componentDidMount() {
const v = this.props.initialValues;
const mgs = v.getModemGeolocationServices(); const mgs = v.getModemGeolocationServices();
if (mgs !== undefined) { if (mgs !== undefined) {
this.setState({ setModemEnabled(mgs.getModemEnabled());
modemEnabled: mgs.getModemEnabled(), setGeolocationTdoa(mgs.getGeolocationTdoa());
geolocationTdoa: mgs.getGeolocationTdoa(), setGeolocationRssi(mgs.getGeolocationRssi());
geolocationRssi: mgs.getGeolocationRssi(), setGeolocationWifi(mgs.getGeolocationWifi());
geolocationWifi: mgs.getGeolocationWifi(), setGeolocationGnss(mgs.getGeolocationGnss());
geolocationGnss: mgs.getGeolocationGnss(),
});
} }
} }, [props]);
onFinish = (values: LoraCloudIntegration.AsObject) => { const onFinish = (values: LoraCloudIntegration.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values); const v = Object.assign(props.initialValues.toObject(), values);
const mgsv = v.modemGeolocationServices; const mgsv = v.modemGeolocationServices;
let mgs = new LoraCloudModemGeolocationServices(); let mgs = new LoraCloudModemGeolocationServices();
@ -76,202 +61,194 @@ class LoRaCloudIntegrationForm extends Component<IProps, IState> {
i.setApplicationId(v.applicationId); i.setApplicationId(v.applicationId);
i.setModemGeolocationServices(mgs); i.setModemGeolocationServices(mgs);
this.props.onFinish(i); props.onFinish(i);
}; };
onModemEnabledChange = (v: boolean) => { const onModemEnabledChange = (v: boolean) => {
this.setState({ setModemEnabled(v);
modemEnabled: v,
});
}; };
onGeolocationTdoaChange = (v: boolean) => { const onGeolocationTdoaChange = (v: boolean) => {
this.setState({ setGeolocationTdoa(v);
geolocationTdoa: v,
});
}; };
onGeolocationRssiChange = (v: boolean) => { const onGeolocationRssiChange = (v: boolean) => {
this.setState({ setGeolocationRssi(v);
geolocationRssi: v,
});
}; };
onGeolocationWifiChange = (v: boolean) => { const onGeolocationWifiChange = (v: boolean) => {
this.setState({ setGeolocationWifi(v);
geolocationWifi: v,
});
}; };
onGeolocationGnssChange = (v: boolean) => { const onGeolocationGnssChange = (v: boolean) => {
this.setState({ setGeolocationGnss(v);
geolocationGnss: v,
});
}; };
render() { return (
return ( <Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}> <Tabs>
<Tabs> <Tabs.TabPane tab="Modem & Geolocation Services" key="1">
<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 <Form.Item
label="Token" label="Use receive timestamp for GNSS geolocation"
name={["modemGeolocationServices", "token"]} name={["modemGeolocationServices", "gnssUseRxTime"]}
tooltip="This token can be obtained from loracloud.com" tooltip="If enabled, the receive timestamp of the gateway will be used as reference instead of the timestamp included in the GNSS payload."
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" valuePropName="checked"
> >
<Switch onChange={this.onModemEnabledChange} /> <Switch />
</Form.Item> </Form.Item>
{this.state.modemEnabled && ( )}
<Form.List name={["modemGeolocationServices", "forwardFPortsList"]}> {modemEnabled && (
{(fields, { add, remove }) => ( <Form.Item
<Form.Item label="Forward messages on these FPorts to LoRa Cloud"> label="Use location of receiving gateways for assistance"
{fields.map((field, index) => ( name={["modemGeolocationServices", "gnssUseGatewayLocation"]}
<Form.Item tooltip="If enabled, the gateway location will be provided to the geolocation resolver to aid the resolving process."
{...field} valuePropName="checked"
rules={[{ required: true, message: "Please a FPort value!" }]} >
style={{ display: "inline-block", width: "100px", marginRight: "24px" }} <Switch />
> </Form.Item>
<InputNumber )}
min={1} {modemEnabled && (
max={255} <Form.Item
addonAfter={<MinusCircleOutlined onClick={() => remove(index)} />} label="My device adheres to the LoRa Edge&trade; Tracker Modem-E Version Reference Design protocol"
/> name={["modemGeolocationServices", "parseTlv"]}
</Form.Item> tooltip="If enabled, ChirpStack will try to resolve the location of the device if a geolocation payload is detected."
))} valuePropName="checked"
<Button type="dashed" onClick={() => add()} icon={<PlusOutlined />} /> >
</Form.Item> <Switch />
)} </Form.Item>
</Form.List> )}
)} <Collapse style={{ marginBottom: 24 }}>
{this.state.modemEnabled && ( <Collapse.Panel header="Advanced geolocation options" key={1}>
<Form.Item <Form.Item
label="Use receive timestamp for GNSS geolocation" label="TDOA based geolocation"
name={["modemGeolocationServices", "gnssUseRxTime"]} name={["modemGeolocationServices", "geolocationTdoa"]}
tooltip="If enabled, the receive timestamp of the gateway will be used as reference instead of the timestamp included in the GNSS payload." 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" valuePropName="checked"
> >
<Switch /> <Switch onChange={onGeolocationTdoaChange} />
</Form.Item> </Form.Item>
)}
{this.state.modemEnabled && (
<Form.Item <Form.Item
label="Use location of receiving gateways for assistance" label="RSSI based geolocation"
name={["modemGeolocationServices", "gnssUseGatewayLocation"]} name={["modemGeolocationServices", "geolocationRssi"]}
tooltip="If enabled, the gateway location will be provided to the geolocation resolver to aid the resolving process." tooltip="If enabled, geolocation will be based on RSSI values reported by the receiving gateways."
valuePropName="checked" valuePropName="checked"
> >
<Switch /> <Switch onChange={onGeolocationRssiChange} />
</Form.Item> </Form.Item>
)}
{this.state.modemEnabled && (
<Form.Item <Form.Item
label="My device adheres to the LoRa Edge&trade; Tracker Modem-E Version Reference Design protocol" label="Wi-Fi based geolocation"
name={["modemGeolocationServices", "parseTlv"]} name={["modemGeolocationServices", "geolocationWifi"]}
tooltip="If enabled, ChirpStack will try to resolve the location of the device if a geolocation payload is detected." tooltip="If enabled, geolocation will be based on Wi-Fi access-point data reported by the device."
valuePropName="checked" valuePropName="checked"
> >
<Switch /> <Switch onChange={onGeolocationWifiChange} />
</Form.Item> </Form.Item>
)} <Form.Item
<Collapse style={{ marginBottom: 24 }}> label="GNSS based geolocation (LR1110)"
<Collapse.Panel header="Advanced geolocation options" key={1}> 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 <Form.Item
label="TDOA based geolocation" label="Geolocation buffer (TTL in seconds)"
name={["modemGeolocationServices", "geolocationTdoa"]} name={["modemGeolocationServices", "geolocationBufferTtl"]}
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." 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" valuePropName="checked"
> >
<Switch onChange={this.onGeolocationTdoaChange} /> <Switch />
</Form.Item> </Form.Item>
<Form.Item )}
label="RSSI based geolocation" </Collapse.Panel>
name={["modemGeolocationServices", "geolocationRssi"]} </Collapse>
tooltip="If enabled, geolocation will be based on RSSI values reported by the receiving gateways." </Tabs.TabPane>
valuePropName="checked" </Tabs>
> <Form.Item>
<Switch onChange={this.onGeolocationRssiChange} /> <Button type="primary" htmlType="submit">
</Form.Item> Submit
<Form.Item </Button>
label="Wi-Fi based geolocation" </Form.Item>
name={["modemGeolocationServices", "geolocationWifi"]} </Form>
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>
);
}
} }
export default LoRaCloudIntegrationForm; export default LoRaCloudIntegrationForm;

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Col, Card } from "antd"; import { Col, Card } from "antd";
@ -9,23 +8,21 @@ interface IProps {
application: Application; application: Application;
} }
class HttpCard extends Component<IProps> { function MqttCard(props: IProps) {
render() { let actions: any[] = [<Link to="mqtt/certificate">Get certificate</Link>];
let actions: any[] = [<Link to="integrations/mqtt/certificate">Get certificate</Link>];
return ( return (
<Col span={8}> <Col span={8}>
<Card <Card
title="MQTT" title="MQTT"
className="integration-card" className="integration-card"
cover={<img alt="MQTT" src="/integrations/mqtt.png" style={{ padding: 1 }} />} cover={<img alt="MQTT" src="/integrations/mqtt.png" style={{ padding: 1 }} />}
actions={actions} actions={actions}
> >
<Card.Meta description="The MQTT integration forwards events to a MQTT broker." /> <Card.Meta description="The MQTT integration forwards events to a MQTT broker." />
</Card> </Card>
</Col> </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 { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd"; import { Col, Card, Popconfirm } from "antd";
@ -13,46 +12,44 @@ interface IProps {
add?: boolean; add?: boolean;
} }
class MyDevicesCard extends Component<IProps> { function MyDevicesCard(props: IProps) {
onDelete = () => { const onDelete = () => {
let req = new DeleteMyDevicesIntegrationRequest(); let req = new DeleteMyDevicesIntegrationRequest();
req.setApplicationId(this.props.application.getId()); req.setApplicationId(props.application.getId());
ApplicationStore.deleteMyDevicesIntegration(req, () => {}); ApplicationStore.deleteMyDevicesIntegration(req, () => {});
}; };
render() { let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) { if (!!props.add) {
actions = [ actions = [
<Link to="integrations/mydevices/create"> <Link to="mydevices/create">
<PlusOutlined /> <PlusOutlined />
</Link>, </Link>,
]; ];
} else { } else {
actions = [ actions = [
<Link to="integrations/mydevices/edit"> <Link to="mydevices/edit">
<EditOutlined /> <EditOutlined />
</Link>, </Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}> <Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined /> <DeleteOutlined />
</Popconfirm>, </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>
);
} }
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; 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"; import { Form, Input, Button, Select } from "antd";
@ -9,79 +9,63 @@ interface IProps {
onFinish: (obj: MyDevicesIntegration) => void; onFinish: (obj: MyDevicesIntegration) => void;
} }
interface IState { function MyDevicesIntegrationForm(props: IProps) {
selectedEndpoint: string; const [selectedEndpoint, setSelectedEndpoint] = useState<string>("");
customEndpoint: string; const [customEndpoint, setCustomEndpoint] = useState<string>("");
}
class MyDevicesIntegrationForm extends Component<IProps, IState> { const onFinish = (values: MyDevicesIntegration.AsObject) => {
constructor(props: IProps) { const v = Object.assign(props.initialValues.toObject(), values);
super(props);
this.state = {
selectedEndpoint: "",
customEndpoint: "",
};
}
onFinish = (values: MyDevicesIntegration.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values);
let i = new MyDevicesIntegration(); let i = new MyDevicesIntegration();
i.setApplicationId(v.applicationId); i.setApplicationId(v.applicationId);
if (v.endpoint === "custom") { if (v.endpoint === "custom") {
i.setEndpoint(this.state.customEndpoint); i.setEndpoint(customEndpoint);
} else { } else {
i.setEndpoint(v.endpoint); i.setEndpoint(v.endpoint);
} }
this.props.onFinish(i); props.onFinish(i);
}; };
onEndpointChange = (v: string) => { const onEndpointChange = (v: string) => {
this.setState({ setSelectedEndpoint(v);
selectedEndpoint: v,
});
}; };
onCustomEndpointChange = (e: React.ChangeEvent<HTMLInputElement>) => { const onCustomEndpointChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ setCustomEndpoint(e.target.value);
customEndpoint: e.target.value,
});
}; };
render() { return (
return ( <Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.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 <Form.Item
label="Select myDevices endpoint" label="myDevices API endpoint"
name="endpoint" name="customEndpoint"
rules={[{ required: true, message: "Please select a myDevices endpoint!" }]} rules={[{ required: true, message: "Please enter an API endpoint!" }]}
> >
<Select onChange={this.onEndpointChange}> <Input onChange={onCustomEndpointChange} />
<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> </Form.Item>
{this.state.selectedEndpoint === "custom" && ( )}
<Form.Item <Form.Item>
label="myDevices API endpoint" <Button type="primary" htmlType="submit">
name="customEndpoint" Submit
rules={[{ required: true, message: "Please enter an API endpoint!" }]} </Button>
> </Form.Item>
<Input onChange={this.onCustomEndpointChange} /> </Form>
</Form.Item> );
)}
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
} }
export default MyDevicesIntegrationForm; export default MyDevicesIntegrationForm;

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd"; import { Col, Card, Popconfirm } from "antd";
@ -16,46 +15,44 @@ interface IProps {
add?: boolean; add?: boolean;
} }
class PilotThingsCard extends Component<IProps> { function PilotThingsCard(props: IProps) {
onDelete = () => { const onDelete = () => {
let req = new DeletePilotThingsIntegrationRequest(); let req = new DeletePilotThingsIntegrationRequest();
req.setApplicationId(this.props.application.getId()); req.setApplicationId(props.application.getId());
ApplicationStore.deletePilotThingsIntegration(req, () => {}); ApplicationStore.deletePilotThingsIntegration(req, () => {});
}; };
render() { let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) { if (!!props.add) {
actions = [ actions = [
<Link to="integrations/pilot-things/create"> <Link to="pilot-things/create">
<PlusOutlined /> <PlusOutlined />
</Link>, </Link>,
]; ];
} else { } else {
actions = [ actions = [
<Link to="integrations/pilot-things/edit"> <Link to="pilot-things/edit">
<EditOutlined /> <EditOutlined />
</Link>, </Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}> <Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined /> <DeleteOutlined />
</Popconfirm>, </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>
);
} }
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; export default PilotThingsCard;

View File

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

View File

@ -1,4 +1,3 @@
import React, { Component } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Col, Card, Popconfirm } from "antd"; import { Col, Card, Popconfirm } from "antd";
@ -16,46 +15,44 @@ interface IProps {
add?: boolean; add?: boolean;
} }
class ThingsBoardCard extends Component<IProps> { function ThingsBoardCard(props: IProps) {
onDelete = () => { const onDelete = () => {
let req = new DeleteThingsBoardIntegrationRequest(); let req = new DeleteThingsBoardIntegrationRequest();
req.setApplicationId(this.props.application.getId()); req.setApplicationId(props.application.getId());
ApplicationStore.deleteThingsBoardIntegration(req, () => {}); ApplicationStore.deleteThingsBoardIntegration(req, () => {});
}; };
render() { let actions: any[] = [];
let actions: any[] = [];
if (!!this.props.add) { if (!!props.add) {
actions = [ actions = [
<Link to="integrations/thingsboard/create"> <Link to="thingsboard/create">
<PlusOutlined /> <PlusOutlined />
</Link>, </Link>,
]; ];
} else { } else {
actions = [ actions = [
<Link to="integrations/thingsboard/edit"> <Link to="thingsboard/edit">
<EditOutlined /> <EditOutlined />
</Link>, </Link>,
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}> <Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
<DeleteOutlined /> <DeleteOutlined />
</Popconfirm>, </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>
);
} }
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; export default ThingsBoardCard;

View File

@ -1,5 +1,3 @@
import React, { Component } from "react";
import { Form, Input, Button, Typography } from "antd"; import { Form, Input, Button, Typography } from "antd";
import { ThingsBoardIntegration } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb"; import { ThingsBoardIntegration } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
@ -9,41 +7,44 @@ interface IProps {
onFinish: (obj: ThingsBoardIntegration) => void; onFinish: (obj: ThingsBoardIntegration) => void;
} }
class ThingsBoardIntegrationForm extends Component<IProps> { function ThingsBoardIntegrationForm(props: IProps) {
onFinish = (values: ThingsBoardIntegration.AsObject) => { const onFinish = (values: ThingsBoardIntegration.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values); const v = Object.assign(props.initialValues.toObject(), values);
let i = new ThingsBoardIntegration(); let i = new ThingsBoardIntegration();
i.setApplicationId(v.applicationId); i.setApplicationId(v.applicationId);
i.setServer(v.server); i.setServer(v.server);
this.props.onFinish(i); props.onFinish(i);
}; };
render() { return (
return ( <Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}> <Form.Item
<Form.Item label="ThingsBoard server"
label="ThingsBoard server" name="server"
name="server" rules={[
rules={[{ required: true, message: "Please enter the address to the ThingsBoard server!" }]} {
> required: true,
<Input placeholder="http://host:port" /> message: "Please enter the address to the ThingsBoard server!",
</Form.Item> },
<Form.Item> ]}
<Typography.Paragraph> >
Each device must have a 'ThingsBoardAccessToken' variable assigned. This access-token is generated by <Input placeholder="http://host:port" />
ThingsBoard. </Form.Item>
</Typography.Paragraph> <Form.Item>
</Form.Item> <Typography.Paragraph>
<Form.Item> Each device must have a 'ThingsBoardAccessToken' variable assigned. This access-token is generated by
<Button type="primary" htmlType="submit"> ThingsBoard.
Submit </Typography.Paragraph>
</Button> </Form.Item>
</Form.Item> <Form.Item>
</Form> <Button type="primary" htmlType="submit">
); Submit
} </Button>
</Form.Item>
</Form>
);
} }
export default ThingsBoardIntegrationForm; 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 { Link } from "react-router-dom";
import { presetPalettes } from "@ant-design/colors"; 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 moment from "moment";
import { LatLngTuple, PointTuple } from "leaflet"; import { LatLngTuple, PointTuple } from "leaflet";
@ -27,167 +28,125 @@ import InternalStore from "../../stores/InternalStore";
import GatewayStore from "../../stores/GatewayStore"; import GatewayStore from "../../stores/GatewayStore";
import Map, { Marker, MarkerColor } from "../../components/Map"; import Map, { Marker, MarkerColor } from "../../components/Map";
interface GatewaysMapState { function GatewaysMap() {
items: GatewayListItem[]; const [items, setItems] = useState<GatewayListItem[]>([]);
}
class GatewaysMap extends Component<{}, GatewaysMapState> { useEffect(() => {
constructor(props: {}) {
super(props);
this.state = {
items: [],
};
}
componentDidMount() {
this.loadData();
}
loadData = () => {
let req = new ListGatewaysRequest(); let req = new ListGatewaysRequest();
req.setLimit(9999); req.setLimit(9999);
GatewayStore.list(req, (resp: ListGatewaysResponse) => { GatewayStore.list(req, (resp: ListGatewaysResponse) => {
this.setState({ setItems(resp.getResultList());
items: resp.getResultList(),
});
}); });
}, []);
if (items.length === 0) {
return <Empty />;
}
const boundsOptions: {
padding: PointTuple;
} = {
padding: [50, 50],
}; };
render() { let bounds: LatLngTuple[] = [];
if (this.state.items.length === 0) { let markers: any[] = [];
return <Empty />;
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: { if (item.getLastSeenAt() !== undefined) {
padding: PointTuple; let ts = moment(item.getLastSeenAt()!.toDate());
} = { lastSeen = ts.fromNow();
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>,
);
} }
return ( markers.push(
<Map height={500} bounds={bounds} boundsOptions={boundsOptions}> <Marker position={pos} faIcon="wifi" color={color}>
{markers} <Popup>
</Map> <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 { function DevicesActiveInactive({ summary }: { summary?: GetDevicesSummaryResponse }) {
summary?: GetGatewaysSummaryResponse; if (
} summary === undefined ||
(summary.getNeverSeenCount() === 0 && summary.getInactiveCount() === 0 && summary.getActiveCount() === 0)
class GatewaysActiveInactive extends Component<GatewayProps> { ) {
render() { return <Empty />;
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" />;
} }
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 { function GatewaysActiveInactive({ summary }: { summary?: GetGatewaysSummaryResponse }) {
summary?: GetDevicesSummaryResponse; if (
} summary === undefined ||
(summary.getNeverSeenCount() === 0 && summary.getOfflineCount() === 0 && summary.getOnlineCount() === 0)
class DevicesActiveInactive extends Component<DeviceProps> { ) {
render() { return <Empty />;
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" />;
} }
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> { function DevicesDataRates({ summary }: { summary?: GetDevicesSummaryResponse }) {
getColor = (dr: number) => { const getColor = (dr: number) => {
return [ return [
"#ff5722", "#ff5722",
"#ff9800", "#ff9800",
@ -207,109 +166,92 @@ class DevicesDataRates extends Component<DeviceProps> {
][dr]; ][dr];
}; };
render() { if (summary === undefined || summary.getDrCountMap().toArray().length === 0) {
if (this.props.summary === undefined || this.props.summary.getDrCountMap().toArray().length === 0) { return <Empty />;
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 = {};
} }
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) => { InternalStore.getGatewaysSummary(new GetGatewaysSummaryRequest(), (resp: GetGatewaysSummaryResponse) => {
this.setState({ setGatewaysSummary(resp);
gatewaysSummary: resp,
});
}); });
InternalStore.getDevicesSummary(new GetDevicesSummaryRequest(), (resp: GetDevicesSummaryResponse) => { InternalStore.getDevicesSummary(new GetDevicesSummaryRequest(), (resp: GetDevicesSummaryResponse) => {
this.setState({ setDevicesSummary(resp);
devicesSummary: resp,
});
}); });
} }, []);
render() { return (
return ( <Space direction="vertical" style={{ width: "100%" }} size="large">
<Space direction="vertical" style={{ width: "100%" }} size="large"> <PageHeader
<PageHeader breadcrumbRender={() => (
breadcrumbRender={() => ( <Breadcrumb>
<Breadcrumb> <Breadcrumb.Item>
<Breadcrumb.Item> <span>Network Server</span>
<span>Network Server</span> </Breadcrumb.Item>
</Breadcrumb.Item> <Breadcrumb.Item>
<Breadcrumb.Item> <span>Dashboard</span>
<span>Dashboard</span> </Breadcrumb.Item>
</Breadcrumb.Item> </Breadcrumb>
</Breadcrumb> )}
)} title="Dashboard"
title="Dashboard" />
/> <Row gutter={24}>
<Row gutter={24}> <Col span={8}>
<Col span={8}> <Card title="Active devices">
<Card title="Active devices"> <DevicesActiveInactive summary={devicesSummary} />
<DevicesActiveInactive summary={this.state.devicesSummary} /> </Card>
</Card> </Col>
</Col> <Col span={8}>
<Col span={8}> <Card title="Active gateways">
<Card title="Active gateways"> <GatewaysActiveInactive summary={gatewaysSummary} />
<GatewaysActiveInactive summary={this.state.gatewaysSummary} /> </Card>
</Card> </Col>
</Col> <Col span={8}>
<Col span={8}> <Card title="Device data-rate usage">
<Card title="Device data-rate usage"> <DevicesDataRates summary={devicesSummary} />
<DevicesDataRates summary={this.state.devicesSummary} /> </Card>
</Card> </Col>
</Col> </Row>
</Row> <Card title="Gateway map">
<Card title="Gateway map"> <GatewaysMap />
<GatewaysMap /> </Card>
</Card> </Space>
</Space> );
);
}
} }
export default Dashboard; export default Dashboard;

View File

@ -1,7 +1,7 @@
import React, { Component } from "react"; import { Link, useNavigate } from "react-router-dom";
import { Link, RouteComponentProps } 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 { MacVersion, RegParamsRevision } from "@chirpstack/chirpstack-api-grpc-web/common/common_pb";
import { import {
@ -12,86 +12,86 @@ import {
import DeviceProfileTemplateForm from "./DeviceProfileTemplateForm"; import DeviceProfileTemplateForm from "./DeviceProfileTemplateForm";
import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore"; import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore";
class CreateDeviceProfileTemplate extends Component<RouteComponentProps> { function CreateDeviceProfileTemplate() {
onFinish = (obj: DeviceProfileTemplate) => { const navigate = useNavigate();
const onFinish = (obj: DeviceProfileTemplate) => {
let req = new CreateDeviceProfileTemplateRequest(); let req = new CreateDeviceProfileTemplateRequest();
req.setDeviceProfileTemplate(obj); req.setDeviceProfileTemplate(obj);
DeviceProfileTemplateStore.create(req, () => { DeviceProfileTemplateStore.create(req, () => {
this.props.history.push(`/device-profile-templates`); navigate(`/device-profile-templates`);
}); });
}; };
render() { const codecScript = `// Decode uplink function.
const codecScript = `// Decode uplink function. //
// // Input is an object with the following fields:
// Input is an object with the following fields: // - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0]
// - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0] // - fPort = Uplink fPort.
// - fPort = Uplink fPort. // - variables = Object containing the configured device variables.
// - variables = Object containing the configured device variables. //
// // Output must be an object with the following fields:
// Output must be an object with the following fields: // - data = Object representing the decoded payload.
// - data = Object representing the decoded payload. function decodeUplink(input) {
function decodeUplink(input) { return {
return { data: {
data: { temp: 22.5
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>
);
} }
// 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; 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 { Form, Input, Select, InputNumber, Switch, Row, Col, Button, Tabs, Card } from "antd";
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"; import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
@ -17,37 +17,20 @@ interface IProps {
update?: boolean; update?: boolean;
} }
interface IState { function DeviceProfileTemplateForm(props: IProps) {
supportsOtaa: boolean; const [form] = Form.useForm();
supportsClassB: boolean; const [supportsOtaa, setSupportsOtaa] = useState<boolean>(false);
supportsClassC: boolean; const [supportsClassB, setSupportsClassB] = useState<boolean>(false);
payloadCodecRuntime: CodecRuntime; const [supportsClassC, setSupportsClassC] = useState<boolean>(false);
adrAlgorithms: [string, string][]; const [payloadCodecRuntime, setPayloadCodecRuntime] = useState<CodecRuntime>(CodecRuntime.NONE);
} const [adrAlgorithms, setAdrAlgorithms] = useState<[string, string][]>([]);
class DeviceProfileTemplateForm extends Component<IProps, IState> { useEffect(() => {
formRef = React.createRef<any>(); const v = props.initialValues;
setSupportsOtaa(v.getSupportsOtaa());
constructor(props: IProps) { setSupportsClassB(v.getSupportsClassB());
super(props); setSupportsClassC(v.getSupportsClassC());
this.state = { setPayloadCodecRuntime(v.getPayloadCodecRuntime());
supportsOtaa: false,
supportsClassB: false,
supportsClassC: false,
payloadCodecRuntime: CodecRuntime.NONE,
adrAlgorithms: [],
};
}
componentDidMount() {
const v = this.props.initialValues;
this.setState({
supportsOtaa: v.getSupportsOtaa(),
supportsClassB: v.getSupportsClassB(),
supportsClassC: v.getSupportsClassC(),
payloadCodecRuntime: v.getPayloadCodecRuntime(),
});
DeviceProfileStore.listAdrAlgorithms((resp: ListDeviceProfileAdrAlgorithmsResponse) => { DeviceProfileStore.listAdrAlgorithms((resp: ListDeviceProfileAdrAlgorithmsResponse) => {
let adrAlgorithms: [string, string][] = []; let adrAlgorithms: [string, string][] = [];
@ -55,14 +38,12 @@ class DeviceProfileTemplateForm extends Component<IProps, IState> {
adrAlgorithms.push([a.getId(), a.getName()]); adrAlgorithms.push([a.getId(), a.getName()]);
} }
this.setState({ setAdrAlgorithms(adrAlgorithms);
adrAlgorithms: adrAlgorithms,
});
}); });
} }, [props.initialValues]);
onFinish = (values: DeviceProfileTemplate.AsObject) => { const onFinish = (values: DeviceProfileTemplate.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values); const v = Object.assign(props.initialValues.toObject(), values);
let dp = new DeviceProfileTemplate(); let dp = new DeviceProfileTemplate();
dp.setId(v.id); dp.setId(v.id);
@ -114,454 +95,487 @@ class DeviceProfileTemplateForm extends Component<IProps, IState> {
} }
dp.setAutoDetectMeasurements(v.autoDetectMeasurements); dp.setAutoDetectMeasurements(v.autoDetectMeasurements);
this.props.onFinish(dp); props.onFinish(dp);
}; };
onSupportsOtaaChange = (checked: boolean) => { const onSupportsOtaaChange = (checked: boolean) => {
this.setState({ setSupportsOtaa(checked);
supportsOtaa: checked,
});
}; };
onSupportsClassBChnage = (checked: boolean) => { const onSupportsClassBChnage = (checked: boolean) => {
this.setState({ setSupportsClassB(checked);
supportsClassB: checked,
});
}; };
onSupportsClassCChange = (checked: boolean) => { const onSupportsClassCChange = (checked: boolean) => {
this.setState({ setSupportsClassC(checked);
supportsClassC: checked,
});
}; };
onPayloadCodecRuntimeChange = (value: CodecRuntime) => { const onPayloadCodecRuntimeChange = (value: CodecRuntime) => {
this.setState({ setPayloadCodecRuntime(value);
payloadCodecRuntime: value,
});
}; };
render() { const adrOptions = adrAlgorithms.map(v => <Select.Option value={v[0]}>{v[1]}</Select.Option>);
const adrOptions = this.state.adrAlgorithms.map(v => <Select.Option value={v[0]}>{v[1]}</Select.Option>);
return ( return (
<Form <Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
layout="vertical" <Tabs>
initialValues={this.props.initialValues.toObject()} <Tabs.TabPane tab="General" key="1">
onFinish={this.onFinish} <Form.Item
ref={this.formRef} label="ID"
> name="id"
<Tabs> rules={[
<Tabs.TabPane tab="General" key="1"> {
<Form.Item required: true,
label="ID" pattern: new RegExp(/^[\w-]*$/g),
name="id" message: "Please enter a valid id!",
rules={[ },
{ ]}
required: true, >
pattern: new RegExp(/^[\w-]*$/g), <Input disabled={!!props.update} />
message: "Please enter a valid id!", </Form.Item>
}, <Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
]} <Input />
> </Form.Item>
<Input disabled={!!this.props.update} /> <Form.Item label="Vendor" name="vendor" rules={[{ required: true, message: "Please enter a vendor!" }]}>
</Form.Item> <Input />
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}> </Form.Item>
<Input /> <Form.Item
</Form.Item> label="Firmware version"
<Form.Item label="Vendor" name="vendor" rules={[{ required: true, message: "Please enter a vendor!" }]}> name="firmware"
<Input /> rules={[{ required: true, message: "Please enter a firmware version!" }]}
</Form.Item> >
<Form.Item <Input />
label="Firmware version" </Form.Item>
name="firmware" <Form.Item label="Description" name="description">
rules={[{ required: true, message: "Please enter a firmware version!" }]} <Input.TextArea rows={6} />
> </Form.Item>
<Input /> <Form.Item label="Region" name="region" rules={[{ required: true, message: "Please select a region!" }]}>
</Form.Item> <Select>
<Form.Item label="Description" name="description"> <Select.Option value={Region.AS923}>AS923</Select.Option>
<Input.TextArea rows={6} /> <Select.Option value={Region.AS923_2}>AS923-2</Select.Option>
</Form.Item> <Select.Option value={Region.AS923_3}>AS923-3</Select.Option>
<Form.Item label="Region" name="region" rules={[{ required: true, message: "Please select a region!" }]}> <Select.Option value={Region.AS923_4}>AS923-4</Select.Option>
<Select> <Select.Option value={Region.AU915}>AU915</Select.Option>
<Select.Option value={Region.AS923}>AS923</Select.Option> <Select.Option value={Region.CN779}>CN779</Select.Option>
<Select.Option value={Region.AS923_2}>AS923-2</Select.Option> <Select.Option value={Region.EU433}>EU433</Select.Option>
<Select.Option value={Region.AS923_3}>AS923-3</Select.Option> <Select.Option value={Region.EU868}>EU868</Select.Option>
<Select.Option value={Region.AS923_4}>AS923-4</Select.Option> <Select.Option value={Region.IN865}>IN865</Select.Option>
<Select.Option value={Region.AU915}>AU915</Select.Option> <Select.Option value={Region.ISM2400}>ISM2400</Select.Option>
<Select.Option value={Region.CN779}>CN779</Select.Option> <Select.Option value={Region.KR920}>KR920</Select.Option>
<Select.Option value={Region.EU433}>EU433</Select.Option> <Select.Option value={Region.RU864}>RU864</Select.Option>
<Select.Option value={Region.EU868}>EU868</Select.Option> <Select.Option value={Region.US915}>US915</Select.Option>
<Select.Option value={Region.IN865}>IN865</Select.Option> </Select>
<Select.Option value={Region.ISM2400}>ISM2400</Select.Option> </Form.Item>
<Select.Option value={Region.KR920}>KR920</Select.Option> <Row gutter={24}>
<Select.Option value={Region.RU864}>RU864</Select.Option> <Col span={12}>
<Select.Option value={Region.US915}>US915</Select.Option> <Form.Item
</Select> label="MAC version"
</Form.Item> tooltip="The LoRaWAN MAC version supported by the device."
<Row gutter={24}> 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}> <Col span={12}>
<Form.Item <Form.Item
label="MAC version" label="RX1 delay"
tooltip="The LoRaWAN MAC version supported by the device." name="abpRx1Delay"
name="macVersion" rules={[{ required: true, message: "Please enter a RX1 delay!" }]}
rules={[{ required: true, message: "Please select a MAC version!" }]}
> >
<Select> <InputNumber min={0} max={15} />
<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> </Form.Item>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Item <Form.Item
label="Regional parameters revision" label="RX1 data-rate offset"
tooltip="Revision of the Regional Parameters specification supported by the device." tooltip="Please refer the LoRaWAN Regional Parameters specification for valid values."
name="regParamsRevision" name="abpRx1DrOffset"
rules={[{ required: true, message: "Please select a regional parameters revision!" }]} rules={[
{
required: true,
message: "Please enter a RX1 data-rate offset!",
},
]}
> >
<Select> <InputNumber min={0} max={15} />
<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> </Form.Item>
</Col> </Col>
</Row> </Row>
<Form.Item )}
label="ADR algorithm" {!supportsOtaa && (
tooltip="The ADR algorithm that will be used for controlling the device data-rate." <Row>
name="adrAlgorithmId" <Col span={12}>
rules={[{ required: true, message: "Please select an ADR algorithm!" }]}
>
<Select>{adrOptions}</Select>
</Form.Item>
<Row gutter={24}>
<Col span={8}>
<Form.Item <Form.Item
label="Flush queue on activate" label="RX2 data-rate"
name="flushQueueOnActivate" tooltip="Please refer the LoRaWAN Regional Parameters specification for valid values."
valuePropName="checked" name="abpRx2Dr"
tooltip="If enabled, the device-queue will be flushed on ABP or OTAA activation." rules={[
{
required: true,
message: "Please enter a RX2 data-rate!",
},
]}
> >
<Switch /> <InputNumber min={0} max={15} />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={8}> <Col span={12}>
<Form.Item <Form.Item
label="Expected uplink interval (secs)" label="RX2 frequency (Hz)"
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="abpRx2Freq"
name="uplinkInterval" rules={[
rules={[{ required: true, message: "Please enter an uplink interval!" }]} {
required: true,
message: "Please enter a RX2 frequency!",
},
]}
> >
<InputNumber min={0} /> <InputNumber min={0} style={{ width: "200px" }} />
</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> </Form.Item>
</Col> </Col>
</Row> </Row>
</Tabs.TabPane> )}
<Tabs.TabPane tab="Join (OTAA / ABP)" key="2"> </Tabs.TabPane>
<Form.Item label="Device supports OTAA" name="supportsOtaa" valuePropName="checked"> <Tabs.TabPane tab="Class-B" key="3">
<Switch onChange={this.onSupportsOtaaChange} /> <Form.Item label="Device supports Class-B" name="supportsClassB" valuePropName="checked">
</Form.Item> <Switch onChange={onSupportsClassBChnage} />
{!this.state.supportsOtaa && ( </Form.Item>
<Row> {supportsClassB && (
<>
<Row gutter={24}>
<Col span={12}> <Col span={12}>
<Form.Item <Form.Item
label="RX1 delay" label="Class-B confirmed downlink timeout (seconds)"
name="abpRx1Delay" tooltip="Class-B timeout (in seconds) for confirmed downlink transmissions."
rules={[{ required: true, message: "Please enter a RX1 delay!" }]} name="classBTimeout"
rules={[
{
required: true,
message: "Please enter a Class-B confirmed downlink timeout!",
},
]}
> >
<InputNumber min={0} max={15} /> <InputNumber min={0} />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Item <Form.Item
label="RX1 data-rate offset" label="Class-B ping-slot periodicity"
tooltip="Please refer the LoRaWAN Regional Parameters specification for valid values." tooltip="This value must match the ping-slot periodicity of the device. Please refer to the device documentation."
name="abpRx1DrOffset" name="classBPingSlotNbK"
rules={[{ required: true, message: "Please enter a RX1 data-rate offset!" }]} 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> </Form.Item>
</Col> </Col>
</Row> </Row>
)} <Row gutter={24}>
{!this.state.supportsOtaa && (
<Row>
<Col span={12}> <Col span={12}>
<Form.Item <Form.Item
label="RX2 data-rate" label="Class-B ping-slot data-rate"
tooltip="Please refer the LoRaWAN Regional Parameters specification for valid values." tooltip="This value must match the ping-slot data-rate of the device. Please refer to the device documentation."
name="abpRx2Dr" name="classBPingSlotDr"
rules={[{ required: true, message: "Please enter a RX2 data-rate!" }]} rules={[
{
required: true,
message: "Please enter the ping-slot data-rate!",
},
]}
> >
<InputNumber min={0} max={15} /> <InputNumber min={0} />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Item <Form.Item
label="RX2 frequency (Hz)" label="Class-B ping-slot frequency (Hz)"
name="abpRx2Freq" tooltip="This value must match the ping-slot frequency of the device. Please refer to the device documentation."
rules={[{ required: true, message: "Please enter a RX2 frequency!" }]} name="classBPingSlotFreq"
rules={[
{
required: true,
message: "Please enter the ping-slot frequency!",
},
]}
> >
<InputNumber min={0} style={{ width: "200px" }} /> <InputNumber min={0} style={{ width: "200px" }} />
</Form.Item> </Form.Item>
</Col> </Col>
</Row> </Row>
)} </>
</Tabs.TabPane> )}
<Tabs.TabPane tab="Class-B" key="3"> </Tabs.TabPane>
<Form.Item label="Device supports Class-B" name="supportsClassB" valuePropName="checked"> <Tabs.TabPane tab="Class-C" key="4">
<Switch onChange={this.onSupportsClassBChnage} /> <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> </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}> {fields.map(({ key, name, ...restField }) => (
<Col span={12}> <Row gutter={24}>
<Form.Item <Col span={6}>
label="Class-B confirmed downlink timeout (seconds)" <Form.Item
tooltip="Class-B timeout (in seconds) for confirmed downlink transmissions." {...restField}
name="classBTimeout" name={[name, 0]}
rules={[{ required: true, message: "Please enter a Class-B confirmed downlink timeout!" }]} fieldKey={[name, 0]}
> rules={[{ required: true, message: "Please enter a key!" }]}
<InputNumber min={0} /> >
</Form.Item> <Input placeholder="Key" />
</Col> </Form.Item>
<Col span={12}> </Col>
<Form.Item <Col span={16}>
label="Class-B ping-slot periodicity" <Form.Item
tooltip="This value must match the ping-slot periodicity of the device. Please refer to the device documentation." {...restField}
name="classBPingSlotNbK" name={[name, 1]}
rules={[{ required: true, message: "Please select the ping-slot periodicity!" }]} fieldKey={[name, 1]}
> rules={[{ required: true, message: "Please enter a value!" }]}
<Select> >
<Select.Option value={0}>Every second</Select.Option> <Input placeholder="Value" />
<Select.Option value={1}>Every 2 seconds</Select.Option> </Form.Item>
<Select.Option value={2}>Every 4 seconds</Select.Option> </Col>
<Select.Option value={3}>Every 8 seconds</Select.Option> <Col span={2}>
<Select.Option value={4}>Every 16 seconds</Select.Option> <MinusCircleOutlined onClick={() => remove(name)} />
<Select.Option value={5}>Every 32 seconds</Select.Option> </Col>
<Select.Option value={6}>Every 64 seconds</Select.Option> </Row>
<Select.Option value={7}>Every 128 seconds</Select.Option> ))}
</Select> <Form.Item>
</Form.Item> <Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
</Col> Add tag
</Row> </Button>
<Row gutter={24}> </Form.Item>
<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>
</> </>
)} )}
</Tabs.TabPane> </Form.List>
<Tabs.TabPane tab="Class-C" key="4"> </Tabs.TabPane>
<Form.Item label="Device supports Class-C" name="supportsClassC" valuePropName="checked"> <Tabs.TabPane tab="Measurements" key="7">
<Switch onChange={this.onSupportsClassCChange} /> <Card bordered={false}>
</Form.Item> <p>
{this.state.supportsClassC && ( ChirpStack can aggregate and visualize decoded device measurements in the device dashboard. To setup the
<Form.Item aggregation of device measurements, you must configure the key, kind of measurement and name
label="Class-C confirmed downlink timeout (seconds)" (user-defined). The following measurement-kinds can be selected:
tooltip="Class-C timeout (in seconds) for confirmed downlink transmissions." </p>
name="classCTimeout" <ul>
rules={[{ required: true, message: "Please enter a Class-C confirmed downlink timeout!" }]} <li>
> <strong>Unknown / unset</strong>: Default for auto-detected keys. This disables the aggregation of this
<InputNumber min={0} /> metric.
</Form.Item> </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> </Form.List>
<Tabs.TabPane tab="Codec" key="5"> </Tabs.TabPane>
<Form.Item </Tabs>
label="Payload codec" <Form.Item>
name="payloadCodecRuntime" <Button type="primary" htmlType="submit">
tooltip="By defining a payload codec, ChirpStack Application Server can encode and decode the binary device payload for you." Submit
> </Button>
<Select onChange={this.onPayloadCodecRuntimeChange}> </Form.Item>
<Select.Option value={CodecRuntime.NONE}>None</Select.Option> </Form>
<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>
);
}
} }
export default DeviceProfileTemplateForm; export default DeviceProfileTemplateForm;

View File

@ -1,7 +1,9 @@
import React, { Component } from "react"; import React, { useState, useEffect } from "react";
import { RouteComponentProps, Link } from "react-router-dom";
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 { import {
DeviceProfileTemplate, DeviceProfileTemplate,
@ -15,101 +17,78 @@ import DeviceProfileTemplateForm from "./DeviceProfileTemplateForm";
import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore"; import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore";
import DeleteConfirm from "../../components/DeleteConfirm"; import DeleteConfirm from "../../components/DeleteConfirm";
interface IState { function EditDeviceProfileTemplate() {
deviceProfileTemplate?: DeviceProfileTemplate; const navigate = useNavigate();
} const [deviceProfileTemplate, setDeviceProfileTemplate] = useState<DeviceProfileTemplate | undefined>(undefined);
const { deviceProfileTemplateId } = useParams();
interface MatchParams { useEffect(() => {
deviceProfileTemplateId: string; const id = deviceProfileTemplateId!;
}
interface IProps extends RouteComponentProps<MatchParams> {}
class EditDeviceProfileTemplate extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
componentDidMount() {
this.getDeviceProfileTemplate();
}
getDeviceProfileTemplate = () => {
const id = this.props.match.params.deviceProfileTemplateId;
let req = new GetDeviceProfileTemplateRequest(); let req = new GetDeviceProfileTemplateRequest();
req.setId(id); req.setId(id);
DeviceProfileTemplateStore.get(req, (resp: GetDeviceProfileTemplateResponse) => { DeviceProfileTemplateStore.get(req, (resp: GetDeviceProfileTemplateResponse) => {
this.setState({ setDeviceProfileTemplate(resp.getDeviceProfileTemplate());
deviceProfileTemplate: resp.getDeviceProfileTemplate(),
});
}); });
}; }, [deviceProfileTemplateId]);
onFinish = (obj: DeviceProfileTemplate) => { const onFinish = (obj: DeviceProfileTemplate) => {
let req = new UpdateDeviceProfileTemplateRequest(); let req = new UpdateDeviceProfileTemplateRequest();
req.setDeviceProfileTemplate(obj); req.setDeviceProfileTemplate(obj);
DeviceProfileTemplateStore.update(req, () => { DeviceProfileTemplateStore.update(req, () => {
this.props.history.push(`/device-profile-templates`); navigate(`/device-profile-templates`);
}); });
}; };
deleteDeviceProfileTemplate = () => { const deleteDeviceProfileTemplate = () => {
let req = new DeleteDeviceProfileTemplateRequest(); let req = new DeleteDeviceProfileTemplateRequest();
req.setId(this.props.match.params.deviceProfileTemplateId); req.setId(deviceProfileTemplateId!);
DeviceProfileTemplateStore.delete(req, () => { DeviceProfileTemplateStore.delete(req, () => {
this.props.history.push(`/device-profile-templates`); navigate(`/device-profile-templates`);
}); });
}; };
render() { const dp = deviceProfileTemplate;
const dp = this.state.deviceProfileTemplate;
if (!dp) { if (!dp) {
return null; 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>
);
} }
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; export default EditDeviceProfileTemplate;

View File

@ -1,8 +1,8 @@
import React, { Component } from "react";
import { Link } from "react-router-dom"; 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 { ColumnsType } from "antd/es/table";
import { PageHeader } from "@ant-design/pro-layout";
import { import {
ListDeviceProfileTemplatesRequest, ListDeviceProfileTemplatesRequest,
@ -15,38 +15,36 @@ import { getEnumName } from "../helpers";
import DataTable, { GetPageCallbackFunc } from "../../components/DataTable"; import DataTable, { GetPageCallbackFunc } from "../../components/DataTable";
import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore"; import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore";
class ListDeviceProfileTemplates extends Component { function ListDeviceProfileTemplates() {
columns = (): ColumnsType<DeviceProfileTemplateListItem.AsObject> => { const columns: ColumnsType<DeviceProfileTemplateListItem.AsObject> = [
return [ {
{ title: "Vendor",
title: "Vendor", dataIndex: "vendor",
dataIndex: "vendor", key: "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(); let req = new ListDeviceProfileTemplatesRequest();
req.setLimit(limit); req.setLimit(limit);
req.setOffset(offset); req.setOffset(offset);
@ -57,31 +55,29 @@ class ListDeviceProfileTemplates extends Component {
}); });
}; };
render() { return (
return ( <Space direction="vertical" style={{ width: "100%" }} size="large">
<Space direction="vertical" style={{ width: "100%" }} size="large"> <PageHeader
<PageHeader breadcrumbRender={() => (
breadcrumbRender={() => ( <Breadcrumb>
<Breadcrumb> <Breadcrumb.Item>
<Breadcrumb.Item> <span>Network Server</span>
<span>Network Server</span> </Breadcrumb.Item>
</Breadcrumb.Item> <Breadcrumb.Item>
<Breadcrumb.Item> <span>Device-profile templates</span>
<span>Device-profile templates</span> </Breadcrumb.Item>
</Breadcrumb.Item> </Breadcrumb>
</Breadcrumb> )}
)} title="Device-profile templates"
title="Device-profile templates" extra={[
extra={[ <Button type="primary">
<Button type="primary"> <Link to={`/device-profile-templates/create`}>Add device-profile template</Link>
<Link to={`/device-profile-templates/create`}>Add device-profile template</Link> </Button>,
</Button>, ]}
]} />
/> <DataTable columns={columns} getPage={getPage} rowKey="id" />
<DataTable columns={this.columns()} getPage={this.getPage} rowKey="id" /> </Space>
</Space> );
);
}
} }
export default ListDeviceProfileTemplates; export default ListDeviceProfileTemplates;

View File

@ -1,7 +1,7 @@
import React, { Component } from "react"; import { Link, useNavigate } from "react-router-dom";
import { Link, RouteComponentProps } 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 { MacVersion, RegParamsRevision } from "@chirpstack/chirpstack-api-grpc-web/common/common_pb";
import { import {
@ -15,97 +15,97 @@ import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
import DeviceProfileForm from "./DeviceProfileForm"; import DeviceProfileForm from "./DeviceProfileForm";
import DeviceProfileStore from "../../stores/DeviceProfileStore"; import DeviceProfileStore from "../../stores/DeviceProfileStore";
interface IProps extends RouteComponentProps { interface IProps {
tenant: Tenant; tenant: Tenant;
} }
class CreateDeviceProfile extends Component<IProps> { function CreateDeviceProfile(props: IProps) {
onFinish = (obj: DeviceProfile) => { const navigate = useNavigate();
obj.setTenantId(this.props.tenant.getId());
const onFinish = (obj: DeviceProfile) => {
obj.setTenantId(props.tenant.getId());
let req = new CreateDeviceProfileRequest(); let req = new CreateDeviceProfileRequest();
req.setDeviceProfile(obj); req.setDeviceProfile(obj);
DeviceProfileStore.create(req, (_resp: CreateDeviceProfileResponse) => { 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.
const codecScript = `// Decode uplink function. //
// // Input is an object with the following fields:
// Input is an object with the following fields: // - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0]
// - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0] // - fPort = Uplink fPort.
// - fPort = Uplink fPort. // - variables = Object containing the configured device variables.
// - variables = Object containing the configured device variables. //
// // Output must be an object with the following fields:
// Output must be an object with the following fields: // - data = Object representing the decoded payload.
// - data = Object representing the decoded payload. function decodeUplink(input) {
function decodeUplink(input) { return {
return { data: {
data: { temp: 22.5
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>
);
} }
// 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; 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 React, { useState, useEffect } from "react";
import { RouteComponentProps, Link } from "react-router-dom"; 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 { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
import { import {
@ -18,112 +19,95 @@ import SessionStore from "../../stores/SessionStore";
import DeleteConfirm from "../../components/DeleteConfirm"; import DeleteConfirm from "../../components/DeleteConfirm";
import Admin from "../../components/Admin"; import Admin from "../../components/Admin";
interface IState { interface IProps {
deviceProfile?: DeviceProfile;
}
interface MatchParams {
deviceProfileId: string;
}
interface IProps extends RouteComponentProps<MatchParams> {
tenant: Tenant; tenant: Tenant;
} }
class EditDeviceProfile extends Component<IProps, IState> { function EditDeviceProfile(props: IProps) {
constructor(props: IProps) { const navigate = useNavigate();
super(props); const [deviceProfile, setDeviceProfile] = useState<DeviceProfile | undefined>(undefined);
this.state = {}; const { deviceProfileId } = useParams();
}
componentDidMount() { useEffect(() => {
this.getDeviceProfile(); const id = deviceProfileId!;
}
getDeviceProfile = () => {
const id = this.props.match.params.deviceProfileId;
let req = new GetDeviceProfileRequest(); let req = new GetDeviceProfileRequest();
req.setId(id); req.setId(id);
DeviceProfileStore.get(req, (resp: GetDeviceProfileResponse) => { DeviceProfileStore.get(req, (resp: GetDeviceProfileResponse) => {
this.setState({ setDeviceProfile(resp.getDeviceProfile());
deviceProfile: resp.getDeviceProfile(),
});
}); });
}; }, [deviceProfileId]);
onFinish = (obj: DeviceProfile) => { const onFinish = (obj: DeviceProfile) => {
let req = new UpdateDeviceProfileRequest(); let req = new UpdateDeviceProfileRequest();
req.setDeviceProfile(obj); req.setDeviceProfile(obj);
DeviceProfileStore.update(req, () => { 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(); let req = new DeleteDeviceProfileRequest();
req.setId(this.props.match.params.deviceProfileId); req.setId(deviceProfileId!);
DeviceProfileStore.delete(req, () => { DeviceProfileStore.delete(req, () => {
this.props.history.push(`/tenants/${this.props.tenant.getId()}/device-profiles`); navigate(`/tenants/${props.tenant.getId()}/device-profiles`);
}); });
}; };
render() { const dp = deviceProfile;
const dp = this.state.deviceProfile;
if (!dp) { if (!dp) {
return null; 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>
);
} }
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; export default EditDeviceProfile;

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import React, { Component } from "react"; import React, { useState, useEffect } from "react";
import { RouteComponentProps } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Space, Form, Button, Row, Col, InputNumber, Alert } from "antd"; import { Space, Form, Button, Row, Col, InputNumber, Alert } from "antd";
@ -26,11 +26,11 @@ interface FormProps {
onFinish: (obj: DeviceActivationPb) => void; onFinish: (obj: DeviceActivationPb) => void;
} }
class LW10DeviceActivationForm extends Component<FormProps> { function LW10DeviceActivationForm(props: FormProps) {
formRef = React.createRef<any>(); const [form] = Form.useForm();
onFinish = (values: DeviceActivationPb.AsObject) => { const onFinish = (values: DeviceActivationPb.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values); const v = Object.assign(props.initialValues.toObject(), values);
let da = new DeviceActivationPb(); let da = new DeviceActivationPb();
da.setDevAddr(v.devAddr); da.setDevAddr(v.devAddr);
@ -42,66 +42,56 @@ class LW10DeviceActivationForm extends Component<FormProps> {
da.setAFCntDown(v.nFCntDown); da.setAFCntDown(v.nFCntDown);
da.setNFCntDown(v.nFCntDown); da.setNFCntDown(v.nFCntDown);
this.props.onFinish(da); props.onFinish(da);
}; };
render() { return (
return ( <Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
<Form <DevAddrInput
layout="vertical" label="Device address"
initialValues={this.props.initialValues.toObject()} name="devAddr"
onFinish={this.onFinish} value={props.initialValues.getDevAddr()}
ref={this.formRef} devEui={props.device.getDevEui()}
> required
<DevAddrInput />
label="Device address" <AesKeyInput
name="devAddr" label="Network session key (LoRaWAN 1.0)"
value={this.props.initialValues.getDevAddr()} name="nwkSEncKey"
devEui={this.props.device.getDevEui()} value={props.initialValues.getNwkSEncKey()}
formRef={this.formRef} required
required />
/> <AesKeyInput
<AesKeyInput label="Application session key (LoRaWAN 1.0)"
label="Network session key (LoRaWAN 1.0)" name="appSKey"
name="nwkSEncKey" value={props.initialValues.getAppSKey()}
value={this.props.initialValues.getNwkSEncKey()} required
formRef={this.formRef} />
required <Row gutter={24}>
/> <Col span={6}>
<AesKeyInput <Form.Item label="Uplink frame-counter" name="fCntUp">
label="Application session key (LoRaWAN 1.0)" <InputNumber min={0} />
name="appSKey" </Form.Item>
value={this.props.initialValues.getAppSKey()} </Col>
formRef={this.formRef} <Col span={6}>
required <Form.Item label="Downlink frame-counter" name="nFCntDown">
/> <InputNumber min={0} />
<Row gutter={24}> </Form.Item>
<Col span={6}> </Col>
<Form.Item label="Uplink frame-counter" name="fCntUp"> </Row>
<InputNumber min={0} /> <Form.Item>
</Form.Item> <Button type="primary" htmlType="submit" disabled={props.disabled}>
</Col> (Re)activate device
<Col span={6}> </Button>
<Form.Item label="Downlink frame-counter" name="nFCntDown"> </Form.Item>
<InputNumber min={0} /> </Form>
</Form.Item> );
</Col>
</Row>
<Form.Item>
<Button type="primary" htmlType="submit" disabled={this.props.disabled}>
(Re)activate device
</Button>
</Form.Item>
</Form>
);
}
} }
class LW11DeviceActivationForm extends Component<FormProps> { function LW11DeviceActivationForm(props: FormProps) {
formRef = React.createRef<any>(); const [form] = Form.useForm();
onFinish = (values: DeviceActivationPb.AsObject) => { const onFinish = (values: DeviceActivationPb.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values); const v = Object.assign(props.initialValues.toObject(), values);
let da = new DeviceActivationPb(); let da = new DeviceActivationPb();
da.setDevAddr(v.devAddr); da.setDevAddr(v.devAddr);
@ -113,162 +103,133 @@ class LW11DeviceActivationForm extends Component<FormProps> {
da.setAFCntDown(v.aFCntDown); da.setAFCntDown(v.aFCntDown);
da.setNFCntDown(v.nFCntDown); da.setNFCntDown(v.nFCntDown);
this.props.onFinish(da); props.onFinish(da);
}; };
render() { return (
return ( <Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
<Form <DevAddrInput
layout="vertical" label="Device address"
initialValues={this.props.initialValues.toObject()} name="devAddr"
onFinish={this.onFinish} value={props.initialValues.getDevAddr()}
ref={this.formRef} devEui={props.device.getDevEui()}
> required
<DevAddrInput />
label="Device address" <AesKeyInput
name="devAddr" label="Network session encryption key"
value={this.props.initialValues.getDevAddr()} name="nwkSEncKey"
devEui={this.props.device.getDevEui()} value={props.initialValues.getNwkSEncKey()}
formRef={this.formRef} required
required />
/> <AesKeyInput
<AesKeyInput label="Serving network session integrity key"
label="Network session encryption key" name="sNwkSIntKey"
name="nwkSEncKey" value={props.initialValues.getSNwkSIntKey()}
value={this.props.initialValues.getNwkSEncKey()} required
formRef={this.formRef} />
required <AesKeyInput
/> label="Forwarding network session integrity key"
<AesKeyInput name="fNwkSIntKey"
label="Serving network session integrity key" value={props.initialValues.getFNwkSIntKey()}
name="sNwkSIntKey" required
value={this.props.initialValues.getSNwkSIntKey()} />
formRef={this.formRef} <AesKeyInput label="Application session key" name="appSKey" value={props.initialValues.getAppSKey()} required />
required <Row gutter={24}>
/> <Col span={6}>
<AesKeyInput <Form.Item label="Uplink frame-counter" name="fCntUp">
label="Forwarding network session integrity key" <InputNumber min={0} />
name="fNwkSIntKey" </Form.Item>
value={this.props.initialValues.getFNwkSIntKey()} </Col>
formRef={this.formRef} <Col span={6}>
required <Form.Item label="Downlink frame-counter (network)" name="nFCntDown">
/> <InputNumber min={0} />
<AesKeyInput </Form.Item>
label="Application session key" </Col>
name="appSKey" <Col span={6}>
value={this.props.initialValues.getAppSKey()} <Form.Item label="Downlink frame-counter (application)" name="aFCntDown">
formRef={this.formRef} <InputNumber min={0} />
required </Form.Item>
/> </Col>
<Row gutter={24}> </Row>
<Col span={6}> <Form.Item>
<Form.Item label="Uplink frame-counter" name="fCntUp"> <Button type="primary" htmlType="submit" disabled={props.disabled}>
<InputNumber min={0} /> (Re)activate device
</Form.Item> </Button>
</Col> </Form.Item>
<Col span={6}> </Form>
<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>
);
}
} }
interface IProps extends RouteComponentProps { interface IProps {
tenant: Tenant; tenant: Tenant;
application: Application; application: Application;
device: Device; device: Device;
deviceProfile: DeviceProfile; deviceProfile: DeviceProfile;
} }
interface IState { function DeviceActivation(props: IProps) {
deviceActivation?: DeviceActivationPb; const navigate = useNavigate();
deviceActivationRequested: boolean; const [deviceActivation, setDeviceActivation] = useState<DeviceActivationPb | undefined>(undefined);
} const [deviceActivationRequested, setDeviceActivationRequested] = useState<boolean>(false);
class DeviceActivation extends Component<IProps, IState> { useEffect(() => {
constructor(props: IProps) {
super(props);
this.state = {
deviceActivationRequested: false,
};
}
componentDidMount() {
let req = new GetDeviceActivationRequest(); let req = new GetDeviceActivationRequest();
req.setDevEui(this.props.device.getDevEui()); req.setDevEui(props.device.getDevEui());
DeviceStore.getActivation(req, (resp: GetDeviceActivationResponse) => { DeviceStore.getActivation(req, (resp: GetDeviceActivationResponse) => {
this.setState({ setDeviceActivation(resp.getDeviceActivation());
deviceActivation: resp.getDeviceActivation(), setDeviceActivationRequested(true);
deviceActivationRequested: true,
});
}); });
} }, [props]);
onFinish = (obj: DeviceActivationPb) => { const onFinish = (obj: DeviceActivationPb) => {
let req = new ActivateDeviceRequest(); let req = new ActivateDeviceRequest();
obj.setDevEui(this.props.device.getDevEui()); obj.setDevEui(props.device.getDevEui());
req.setDeviceActivation(obj); req.setDeviceActivation(obj);
DeviceStore.activate(req, () => { DeviceStore.activate(req, () => {
this.props.history.push( navigate(
`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}/devices/${this.props.device.getDevEui()}`, `/tenants/${props.tenant.getId()}/applications/${props.application.getId()}/devices/${props.device.getDevEui()}`,
); );
}); });
}; };
render() { if (!deviceActivationRequested) {
if (!this.state.deviceActivationRequested) { return null;
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 (!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; 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 { Link } from "react-router-dom";
import moment from "moment"; import moment from "moment";
@ -27,31 +27,18 @@ interface IProps {
lastSeenAt?: Date; lastSeenAt?: Date;
} }
interface IState { function DeviceDashboard(props: IProps) {
metricsAggregation: Aggregation; const [metricsAggregation, setMetricsAggregation] = useState<Aggregation>(Aggregation.DAY);
deviceMetrics?: GetDeviceMetricsResponse; const [deviceMetrics, setDeviceMetrics] = useState<GetDeviceMetricsResponse | undefined>(undefined);
deviceLinkMetrics?: GetDeviceLinkMetricsResponse; const [deviceLinkMetrics, setDeviceLinkMetrics] = useState<GetDeviceLinkMetricsResponse | undefined>(undefined);
deviceMetricsLoaded: boolean; const [deviceLinkMetricsLoaded, setDeviceLinkMetricsLoaded] = useState<boolean>(false);
deviceLinkMetricsLoaded: boolean;
}
class DeviceDashboard extends Component<IProps, IState> { useEffect(() => {
constructor(props: IProps) { loadMetrics();
super(props); }, [props, metricsAggregation]);
this.state = { const loadMetrics = () => {
metricsAggregation: Aggregation.DAY, const agg = metricsAggregation;
deviceMetricsLoaded: false,
deviceLinkMetricsLoaded: false,
};
}
componentDidMount() {
this.loadMetrics();
}
loadMetrics = () => {
const agg = this.state.metricsAggregation;
const end = moment(); const end = moment();
let start = moment(); let start = moment();
@ -63,19 +50,12 @@ class DeviceDashboard extends Component<IProps, IState> {
start = start.subtract(12, "months"); start = start.subtract(12, "months");
} }
this.setState( setDeviceLinkMetricsLoaded(false);
{ loadLinkMetrics(start.toDate(), end.toDate(), agg);
deviceMetricsLoaded: false, loadDeviceMetrics(start.toDate(), end.toDate(), agg);
deviceLinkMetricsLoaded: false,
},
() => {
this.loadLinkMetrics(start.toDate(), end.toDate(), agg);
this.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 startPb = new Timestamp();
let endPb = new Timestamp(); let endPb = new Timestamp();
@ -83,20 +63,17 @@ class DeviceDashboard extends Component<IProps, IState> {
endPb.fromDate(end); endPb.fromDate(end);
let req = new GetDeviceMetricsRequest(); let req = new GetDeviceMetricsRequest();
req.setDevEui(this.props.device.getDevEui()); req.setDevEui(props.device.getDevEui());
req.setStart(startPb); req.setStart(startPb);
req.setEnd(endPb); req.setEnd(endPb);
req.setAggregation(agg); req.setAggregation(agg);
DeviceStore.getMetrics(req, (resp: GetDeviceMetricsResponse) => { DeviceStore.getMetrics(req, (resp: GetDeviceMetricsResponse) => {
this.setState({ setDeviceMetrics(resp);
deviceMetrics: resp,
deviceMetricsLoaded: true,
});
}); });
}; };
loadLinkMetrics = (start: Date, end: Date, agg: Aggregation) => { const loadLinkMetrics = (start: Date, end: Date, agg: Aggregation) => {
let startPb = new Timestamp(); let startPb = new Timestamp();
let endPb = new Timestamp(); let endPb = new Timestamp();
@ -104,176 +81,153 @@ class DeviceDashboard extends Component<IProps, IState> {
endPb.fromDate(end); endPb.fromDate(end);
let req = new GetDeviceLinkMetricsRequest(); let req = new GetDeviceLinkMetricsRequest();
req.setDevEui(this.props.device.getDevEui()); req.setDevEui(props.device.getDevEui());
req.setStart(startPb); req.setStart(startPb);
req.setEnd(endPb); req.setEnd(endPb);
req.setAggregation(agg); req.setAggregation(agg);
DeviceStore.getLinkMetrics(req, (resp: GetDeviceLinkMetricsResponse) => { DeviceStore.getLinkMetrics(req, (resp: GetDeviceLinkMetricsResponse) => {
this.setState({ setDeviceLinkMetrics(resp);
deviceLinkMetrics: resp, setDeviceLinkMetricsLoaded(true);
deviceLinkMetricsLoaded: true,
});
}); });
}; };
onMetricsAggregationChange = (e: RadioChangeEvent) => { const onMetricsAggregationChange = (e: RadioChangeEvent) => {
this.setState( setMetricsAggregation(e.target.value);
{
metricsAggregation: e.target.value,
},
this.loadMetrics,
);
}; };
render() { if (deviceLinkMetrics === undefined || deviceMetrics === undefined) {
if (this.state.deviceLinkMetrics === undefined || this.state.deviceMetrics === undefined) { return null;
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>
);
} }
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; 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 { Device } from "@chirpstack/chirpstack-api-grpc-web/api/device_pb";
import { StreamDeviceEventsRequest, LogItem } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb"; import { StreamDeviceEventsRequest, LogItem } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
@ -10,55 +10,31 @@ interface IProps {
device: Device; device: Device;
} }
interface IState { function DeviceEvents(props: IProps) {
events: LogItem[]; const [events, setEvents] = useState<LogItem[]>([]);
cancelFunc?: () => void;
}
class DeviceEvents extends Component<IProps, IState> { useEffect(() => {
constructor(props: IProps) { const onMessage = (l: LogItem) => {
super(props); setEvents(e => {
if (e.length === 0 || parseInt(l.getId().replace("-", "")) > parseInt(e[0].getId().replace("-", ""))) {
e.unshift(l);
}
this.state = { return e;
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,
}); });
} };
};
render() { let req = new StreamDeviceEventsRequest();
return <LogTable logs={this.state.events} />; req.setDevEui(props.device.getDevEui());
}
let cancelFunc = InternalStore.streamDeviceEvents(req, onMessage);
return () => {
cancelFunc();
};
}, [props]);
return <LogTable logs={events} />;
} }
export default DeviceEvents; 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 { Form, Input, Row, Col, Button, Tabs, Switch } from "antd";
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"; import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
@ -24,11 +22,11 @@ interface IProps {
update?: boolean; update?: boolean;
} }
class DeviceForm extends Component<IProps> { function DeviceForm(props: IProps) {
formRef = React.createRef<any>(); const [form] = Form.useForm();
onFinish = (values: Device.AsObject) => { const onFinish = (values: Device.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values); const v = Object.assign(props.initialValues.toObject(), values);
let d = new Device(); let d = new Device();
d.setApplicationId(v.applicationId); d.setApplicationId(v.applicationId);
@ -50,12 +48,12 @@ class DeviceForm extends Component<IProps> {
d.getVariablesMap().set(elm[0], elm[1]); 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(); let req = new ListDeviceProfilesRequest();
req.setTenantId(this.props.tenant.getId()); req.setTenantId(props.tenant.getId());
req.setSearch(search); req.setSearch(search);
req.setLimit(10); req.setLimit(10);
@ -63,11 +61,12 @@ class DeviceForm extends Component<IProps> {
const options = resp.getResultList().map((o, i) => { const options = resp.getResultList().map((o, i) => {
return { label: o.getName(), value: o.getId() }; return { label: o.getName(), value: o.getId() };
}); });
fn(options); fn(options);
}); });
}; };
getDeviceProfileOption = (id: string, fn: OptionCallbackFunc) => { const getDeviceProfileOption = (id: string, fn: OptionCallbackFunc) => {
let req = new GetDeviceProfileRequest(); let req = new GetDeviceProfileRequest();
req.setId(id); req.setId(id);
@ -79,163 +78,153 @@ class DeviceForm extends Component<IProps> {
}); });
}; };
render() { return (
return ( <Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
<Form <Tabs>
layout="vertical" <Tabs.TabPane tab="Device" key="1">
initialValues={this.props.initialValues.toObject()} <Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
onFinish={this.onFinish} <Input />
ref={this.formRef} </Form.Item>
> <Form.Item label="Description" name="description">
<Tabs> <Input.TextArea />
<Tabs.TabPane tab="Device" key="1"> </Form.Item>
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}> <Row gutter={24}>
<Input /> <Col span={12}>
</Form.Item> <EuiInput
<Form.Item label="Description" name="description"> label="Device EUI (EUI64)"
<Input.TextArea /> name="devEui"
</Form.Item> value={props.initialValues.getDevEui()}
<Row gutter={24}> disabled={props.update}
<Col span={12}> required
<EuiInput />
label="Device EUI (EUI64)" </Col>
name="devEui" <Col span={12}>
value={this.props.initialValues.getDevEui()} <EuiInput
formRef={this.formRef} label="Join EUI (EUI64)"
disabled={this.props.update} name="joinEui"
required 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> />
<Col span={12}> </Col>
<EuiInput </Row>
label="Join EUI (EUI64)" <AutocompleteInput
name="joinEui" label="Device profile"
value={this.props.initialValues.getJoinEui()} name="deviceProfileId"
formRef={this.formRef} getOption={getDeviceProfileOption}
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)." getOptions={getDeviceProfileOptions}
/> required
</Col> />
</Row> <Row gutter={24}>
<AutocompleteInput <Col span={12}>
label="Device profile" <Form.Item
name="deviceProfileId" label="Device is disabled"
formRef={this.formRef} name="isDisabled"
getOption={this.getDeviceProfileOption} valuePropName="checked"
getOptions={this.getDeviceProfileOptions} tooltip="Received uplink frames and join-requests will be ignored."
required >
/> <Switch />
<Row gutter={24}> </Form.Item>
<Col span={12}> </Col>
<Form.Item <Col span={12}>
label="Device is disabled" <Form.Item
name="isDisabled" label="Disable frame-counter validation"
valuePropName="checked" name="skipFcntCheck"
tooltip="Received uplink frames and join-requests will be ignored." 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 /> >
<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> </Form.Item>
</Col> </>
<Col span={12}> )}
<Form.Item </Form.List>
label="Disable frame-counter validation" </Tabs.TabPane>
name="skipFcntCheck" <Tabs.TabPane tab="Variables" key="3">
valuePropName="checked" <Form.List name="variablesMap">
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." {(fields, { add, remove }) => (
> <>
<Switch /> {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.Item>
</Col> </>
</Row> )}
</Tabs.TabPane> </Form.List>
<Tabs.TabPane tab="Tags" key="2"> </Tabs.TabPane>
<Form.List name="tagsMap"> </Tabs>
{(fields, { add, remove }) => ( <Form.Item>
<> <Button type="primary" htmlType="submit">
{fields.map(({ key, name, ...restField }) => ( Submit
<Row gutter={24}> </Button>
<Col span={6}> </Form.Item>
<Form.Item </Form>
{...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>
);
}
} }
export default DeviceForm; 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 { Device } from "@chirpstack/chirpstack-api-grpc-web/api/device_pb";
import { StreamDeviceFramesRequest, LogItem } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb"; import { StreamDeviceFramesRequest, LogItem } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
@ -10,55 +10,31 @@ interface IProps {
device: Device; device: Device;
} }
interface IState { function DeviceFrames(props: IProps) {
frames: LogItem[]; const [frames, setFrames] = useState<LogItem[]>([]);
cancelFunc?: () => void;
}
class DeviceFrames extends Component<IProps, IState> { useEffect(() => {
constructor(props: IProps) { const onMessage = (l: LogItem) => {
super(props); setFrames(f => {
if (f.length === 0 || parseInt(l.getId().replace("-", "")) > parseInt(f[0].getId().replace("-", ""))) {
f.unshift(l);
}
this.state = { return f;
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,
}); });
} };
};
render() { let req = new StreamDeviceFramesRequest();
return <LogTable logs={this.state.frames} />; req.setDevEui(props.device.getDevEui());
}
let cancelFunc = InternalStore.streamDeviceFrames(req, onMessage);
return () => {
cancelFunc();
};
}, [props]);
return <LogTable logs={frames} />;
} }
export default DeviceFrames; export default DeviceFrames;

View File

@ -1,7 +1,8 @@
import React, { Component } from "react"; import React, { useState, useEffect } from "react";
import { Route, Switch, RouteComponentProps, Link } from "react-router-dom"; 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 { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_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 DeviceQueue from "./DeviceQueue";
import DeviceActivation from "./DeviceActivation"; import DeviceActivation from "./DeviceActivation";
interface MatchParams { interface IProps {
devEui: string;
}
interface IProps extends RouteComponentProps<MatchParams> {
tenant: Tenant; tenant: Tenant;
application: Application; application: Application;
} }
interface IState { function DeviceLayout(props: IProps) {
device?: Device; const { devEui } = useParams();
deviceProfile?: DeviceProfile; const navigate = useNavigate();
lastSeenAt?: Date; const location = useLocation();
}
class DeviceLayout extends Component<IProps, IState> { const [device, setDevice] = useState<Device | undefined>(undefined);
constructor(props: IProps) { const [deviceProfile, setDeviceProfile] = useState<DeviceProfile | undefined>(undefined);
super(props); const [lastSeenAt, setLastSeenAt] = useState<Date | undefined>(undefined);
this.state = {};
}
componentDidMount() { useEffect(() => {
this.getDevice(this.getDeviceProfile);
}
getDevice = (cb: () => void) => {
let req = new GetDeviceRequest(); let req = new GetDeviceRequest();
req.setDevEui(this.props.match.params.devEui); req.setDevEui(devEui!);
DeviceStore.get(req, (resp: GetDeviceResponse) => { DeviceStore.get(req, (resp: GetDeviceResponse) => {
this.setState( setDevice(resp.getDevice());
{
device: resp.getDevice(),
},
cb,
);
if (resp.getLastSeenAt() !== undefined) { if (resp.getLastSeenAt() !== undefined) {
this.setState({ setLastSeenAt(resp.getLastSeenAt()!.toDate());
lastSeenAt: resp.getLastSeenAt()!.toDate(),
});
} }
});
};
getDeviceProfile = () => { let req = new GetDeviceProfileRequest();
let req = new GetDeviceProfileRequest(); req.setId(resp.getDevice()!.getDeviceProfileId());
req.setId(this.state.device!.getDeviceProfileId()); DeviceProfileStore.get(req, (resp: GetDeviceProfileResponse) => {
setDeviceProfile(resp.getDeviceProfile());
DeviceProfileStore.get(req, (resp: GetDeviceProfileResponse) => {
this.setState({
deviceProfile: resp.getDeviceProfile(),
}); });
}); });
}; }, [devEui]);
deleteDevice = () => { const deleteDevice = () => {
let req = new DeleteDeviceRequest(); let req = new DeleteDeviceRequest();
req.setDevEui(this.props.match.params.devEui); req.setDevEui(devEui!);
DeviceStore.delete(req, () => { 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 dp = deviceProfile;
const device = this.state.device; if (!device || !dp) {
const dp = this.state.deviceProfile; return null;
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 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; 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"; import { Struct } from "google-protobuf/google/protobuf/struct_pb";
@ -25,87 +25,75 @@ interface IProps {
device: Device; device: Device;
} }
interface IState { function DeviceQueue(props: IProps) {
refreshCounter: number; const [refreshCounter, setRefreshCounter] = useState<number>(0);
} const [form] = Form.useForm();
class DeviceQueue extends Component<IProps, IState> { const columns: ColumnsType<DeviceQueueItem.AsObject> = [
formRef = React.createRef<any>(); {
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) { const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
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) => {
let req = new GetDeviceQueueItemsRequest(); let req = new GetDeviceQueueItemsRequest();
req.setDevEui(this.props.device.getDevEui()); req.setDevEui(props.device.getDevEui());
DeviceStore.getQueue(req, (resp: GetDeviceQueueItemsResponse) => { DeviceStore.getQueue(req, (resp: GetDeviceQueueItemsResponse) => {
const obj = resp.toObject(); const obj = resp.toObject();
@ -113,25 +101,23 @@ class DeviceQueue extends Component<IProps, IState> {
}); });
}; };
refreshQueue = () => { const refreshQueue = () => {
this.setState({ setRefreshCounter(refreshCounter + 1);
refreshCounter: this.state.refreshCounter + 1,
});
}; };
flushQueue = () => { const flushQueue = () => {
let req = new FlushDeviceQueueRequest(); let req = new FlushDeviceQueueRequest();
req.setDevEui(this.props.device.getDevEui()); req.setDevEui(props.device.getDevEui());
DeviceStore.flushQueue(req, () => { DeviceStore.flushQueue(req, () => {
this.refreshQueue(); refreshQueue();
}); });
}; };
onEnqueue = (values: any) => { const onEnqueue = (values: any) => {
let req = new EnqueueDeviceQueueItemRequest(); let req = new EnqueueDeviceQueueItemRequest();
let item = new DeviceQueueItem(); let item = new DeviceQueueItem();
item.setDevEui(this.props.device.getDevEui()); item.setDevEui(props.device.getDevEui());
item.setFPort(values.fPort); item.setFPort(values.fPort);
item.setConfirmed(values.confirmed); item.setConfirmed(values.confirmed);
@ -163,66 +149,58 @@ class DeviceQueue extends Component<IProps, IState> {
req.setQueueItem(item); req.setQueueItem(item);
DeviceStore.enqueue(req, _ => { DeviceStore.enqueue(req, _ => {
this.formRef.current.resetFields(); form.resetFields();
this.refreshQueue(); refreshQueue();
}); });
}; };
render() { return (
return ( <Space direction="vertical" style={{ width: "100%" }} size="large">
<Space direction="vertical" style={{ width: "100%" }} size="large"> <Card title="Enqueue">
<Card title="Enqueue"> <Form layout="horizontal" onFinish={onEnqueue} form={form} initialValues={{ fPort: 1 }}>
<Form layout="horizontal" onFinish={this.onEnqueue} ref={this.formRef} initialValues={{ fPort: 1 }}> <Row>
<Row> <Space direction="horizontal" style={{ width: "100%" }} size="large">
<Space direction="horizontal" style={{ width: "100%" }} size="large"> <Form.Item name="confirmed" label="Confirmed" valuePropName="checked">
<Form.Item name="confirmed" label="Confirmed" valuePropName="checked"> <Checkbox />
<Checkbox /> </Form.Item>
</Form.Item> <Form.Item name="fPort" label="FPort">
<Form.Item name="fPort" label="FPort"> <InputNumber min={1} max={254} />
<InputNumber min={1} max={254} /> </Form.Item>
</Form.Item> </Space>
</Space> </Row>
</Row> <Tabs defaultActiveKey="1">
<Tabs defaultActiveKey="1"> <Tabs.TabPane tab="HEX" key="1">
<Tabs.TabPane tab="HEX" key="1"> <Form.Item name="hex">
<Form.Item name="hex"> <Input />
<Input /> </Form.Item>
</Form.Item> </Tabs.TabPane>
</Tabs.TabPane> <Tabs.TabPane tab="BASE64" key="2">
<Tabs.TabPane tab="BASE64" key="2"> <Form.Item name="base64">
<Form.Item name="base64"> <Input />
<Input /> </Form.Item>
</Form.Item> </Tabs.TabPane>
</Tabs.TabPane> <Tabs.TabPane tab="JSON" key="3">
<Tabs.TabPane tab="JSON" key="3"> <CodeEditor name="json" />
<CodeEditor name="json" value="{}" formRef={this.formRef} /> </Tabs.TabPane>
</Tabs.TabPane> </Tabs>
</Tabs> <Button type="primary" htmlType="submit">
<Button type="primary" htmlType="submit"> Enqueue
Enqueue </Button>
</Button> </Form>
</Form> </Card>
</Card> <Row justify="end">
<Row justify="end"> <Space direction="horizontal" size="large">
<Space direction="horizontal" size="large"> <Button icon={<RedoOutlined />} onClick={refreshQueue}>
<Button icon={<RedoOutlined />} onClick={this.refreshQueue}> Reload
Reload </Button>
</Button> <Popconfirm title="Are you sure you want to flush the queue?" placement="left" onConfirm={flushQueue}>
<Popconfirm title="Are you sure you want to flush the queue?" placement="left" onConfirm={this.flushQueue}> <Button icon={<DeleteOutlined />}>Flush queue</Button>
<Button icon={<DeleteOutlined />}>Flush queue</Button> </Popconfirm>
</Popconfirm> </Space>
</Space> </Row>
</Row> <DataTable columns={columns} getPage={getPage} refreshKey={refreshCounter} rowKey="id" noPagination />
<DataTable </Space>
columns={this.columns()} );
getPage={this.getPage}
refreshKey={this.state.refreshCounter}
rowKey="id"
noPagination
/>
</Space>
);
}
} }
export default DeviceQueue; export default DeviceQueue;

View File

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

View File

@ -1,5 +1,5 @@
import React, { Component } from "react"; import React, { useEffect, useState } from "react";
import { RouteComponentProps } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Form, Button, Space, Popconfirm } from "antd"; import { Form, Button, Space, Popconfirm } from "antd";
@ -25,11 +25,11 @@ interface FormProps {
onFinish: (obj: DeviceKeys) => void; onFinish: (obj: DeviceKeys) => void;
} }
class LW10DeviceKeysForm extends Component<FormProps> { function LW10DeviceKeysForm(props: FormProps) {
formRef = React.createRef<any>(); const [form] = Form.useForm();
onFinish = (values: DeviceKeys.AsObject) => { const onFinish = (values: DeviceKeys.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values); const v = Object.assign(props.initialValues.toObject(), values);
let dk = new DeviceKeys(); let dk = new DeviceKeys();
dk.setDevEui(v.devEui); dk.setDevEui(v.devEui);
@ -37,187 +37,152 @@ class LW10DeviceKeysForm extends Component<FormProps> {
// the AppKey has been renamed to the NwkKey and a new value AppKey was added. // the AppKey has been renamed to the NwkKey and a new value AppKey was added.
dk.setNwkKey(v.nwkKey); dk.setNwkKey(v.nwkKey);
this.props.onFinish(dk); props.onFinish(dk);
}; };
render() { return (
return ( <Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
<Form <AesKeyInput
layout="vertical" label="Application key"
initialValues={this.props.initialValues.toObject()} name="nwkKey"
onFinish={this.onFinish} tooltip="For LoRaWAN 1.0 devices. In case your device supports LoRaWAN 1.1, update the device-profile first."
ref={this.formRef} value={props.initialValues.getNwkKey()}
> required
<AesKeyInput />
label="Application key" <Form.Item>
name="nwkKey" <Button type="primary" htmlType="submit">
tooltip="For LoRaWAN 1.0 devices. In case your device supports LoRaWAN 1.1, update the device-profile first." Submit
value={this.props.initialValues.getNwkKey()} </Button>
formRef={this.formRef} </Form.Item>
required </Form>
/> );
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
} }
class LW11DeviceKeysForm extends Component<FormProps> { function LW11DeviceKeysForm(props: FormProps) {
formRef = React.createRef<any>(); const [form] = Form.useForm();
onFinish = (values: DeviceKeys.AsObject) => { const onFinish = (values: DeviceKeys.AsObject) => {
const v = Object.assign(this.props.initialValues.toObject(), values); const v = Object.assign(props.initialValues.toObject(), values);
let dk = new DeviceKeys(); let dk = new DeviceKeys();
dk.setDevEui(v.devEui); dk.setDevEui(v.devEui);
dk.setAppKey(v.appKey); dk.setAppKey(v.appKey);
dk.setNwkKey(v.nwkKey); dk.setNwkKey(v.nwkKey);
this.props.onFinish(dk); props.onFinish(dk);
}; };
render() { return (
return ( <Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
<Form <AesKeyInput
layout="vertical" label="Application key"
initialValues={this.props.initialValues.toObject()} tooltip="For LoRaWAN 1.1 devices. In case your device does not support LoRaWAN 1.1, update the device-profile first."
onFinish={this.onFinish} name="appKey"
ref={this.formRef} value={props.initialValues.getAppKey()}
> required
<AesKeyInput />
label="Application key" <AesKeyInput
tooltip="For LoRaWAN 1.1 devices. In case your device does not support LoRaWAN 1.1, update the device-profile first." label="Network key"
name="appKey" tooltip="For LoRaWAN 1.1 devices. In case your device does not support LoRaWAN 1.1, update the device-profile first."
value={this.props.initialValues.getAppKey()} name="nwkKey"
formRef={this.formRef} value={props.initialValues.getNwkKey()}
required required
/> />
<AesKeyInput <Form.Item>
label="Network key" <Button type="primary" htmlType="submit">
tooltip="For LoRaWAN 1.1 devices. In case your device does not support LoRaWAN 1.1, update the device-profile first." Submit
name="nwkKey" </Button>
value={this.props.initialValues.getNwkKey()} </Form.Item>
formRef={this.formRef} </Form>
required );
/>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
} }
interface IProps extends RouteComponentProps { interface IProps {
tenant: Tenant; tenant: Tenant;
application: Application; application: Application;
device: Device; device: Device;
deviceProfile: DeviceProfile; deviceProfile: DeviceProfile;
} }
interface IState { function SetDeviceKeys(props: IProps) {
deviceKeys?: DeviceKeys; const navigate = useNavigate();
deviceKeysRequested: boolean; const [deviceKeys, setDeviceKeys] = useState<DeviceKeys | undefined>(undefined);
} const [deviceKeysRequested, setDeviceKeysRequested] = useState<boolean>(false);
class SetDeviceKeys extends Component<IProps, IState> { useEffect(() => {
constructor(props: IProps) {
super(props);
this.state = {
deviceKeysRequested: false,
};
}
componentDidMount() {
this.getDeviceKeys();
}
getDeviceKeys = () => {
let req = new GetDeviceKeysRequest(); let req = new GetDeviceKeysRequest();
req.setDevEui(this.props.device.getDevEui()); req.setDevEui(props.device.getDevEui());
DeviceStore.getKeys(req, (resp?: GetDeviceKeysResponse) => { DeviceStore.getKeys(req, (resp?: GetDeviceKeysResponse) => {
if (resp) { if (resp) {
this.setState({ setDeviceKeys(resp.getDeviceKeys());
deviceKeys: resp.getDeviceKeys(), setDeviceKeysRequested(true);
deviceKeysRequested: true,
});
} else { } else {
this.setState({ setDeviceKeysRequested(true);
deviceKeysRequested: true,
});
} }
}); });
}; }, [props]);
onFinish = (obj: DeviceKeys) => { const onFinish = (obj: DeviceKeys) => {
if (this.state.deviceKeys) { if (deviceKeys) {
// this is an update // this is an update
let req = new UpdateDeviceKeysRequest(); let req = new UpdateDeviceKeysRequest();
req.setDeviceKeys(obj); req.setDeviceKeys(obj);
DeviceStore.updateKeys(req, () => { DeviceStore.updateKeys(req, () => {
this.props.history.push( navigate(
`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}/devices/${this.props.device.getDevEui()}`, `/tenants/${props.tenant.getId()}/applications/${props.application.getId()}/devices/${props.device.getDevEui()}`,
); );
}); });
} else { } else {
// this is a create // this is a create
let req = new CreateDeviceKeysRequest(); let req = new CreateDeviceKeysRequest();
obj.setDevEui(this.props.device.getDevEui()); obj.setDevEui(props.device.getDevEui());
req.setDeviceKeys(obj); req.setDeviceKeys(obj);
DeviceStore.createKeys(req, () => { DeviceStore.createKeys(req, () => {
this.props.history.push( navigate(
`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}/devices/${this.props.device.getDevEui()}`, `/tenants/${props.tenant.getId()}/applications/${props.application.getId()}/devices/${props.device.getDevEui()}`,
); );
}); });
} }
}; };
flushDevNonces = () => { const flushDevNonces = () => {
let req = new FlushDevNoncesRequest(); let req = new FlushDevNoncesRequest();
req.setDevEui(this.props.device.getDevEui()); req.setDevEui(props.device.getDevEui());
DeviceStore.flushDevNonces(req, () => {}); DeviceStore.flushDevNonces(req, () => {});
}; };
render() { if (!deviceKeysRequested) {
if (!this.state.deviceKeysRequested) { return null;
return null;
}
const macVersion = this.props.deviceProfile.getMacVersion();
const lw11 = macVersion === MacVersion.LORAWAN_1_1_0;
let initialValues = new DeviceKeys();
if (this.state.deviceKeys) {
initialValues = this.state.deviceKeys;
}
return (
<Space direction="vertical" style={{ width: "100%" }} size="large">
{this.state.deviceKeys && (
<div style={{ float: "right" }}>
<Popconfirm
placement="left"
title="Are you sure you want to flush all device-nonces that have been used during previous OTAA activations?"
onConfirm={this.flushDevNonces}
>
<Button>Flush OTAA device nonces</Button>
</Popconfirm>
</div>
)}
{!lw11 && <LW10DeviceKeysForm initialValues={initialValues} onFinish={this.onFinish} />}
{lw11 && <LW11DeviceKeysForm initialValues={initialValues} onFinish={this.onFinish} />}
</Space>
);
} }
const macVersion = props.deviceProfile.getMacVersion();
const lw11 = macVersion === MacVersion.LORAWAN_1_1_0;
let initialValues = new DeviceKeys();
if (deviceKeys) {
initialValues = deviceKeys;
}
return (
<Space direction="vertical" style={{ width: "100%" }} size="large">
{deviceKeys && (
<div style={{ float: "right" }}>
<Popconfirm
placement="left"
title="Are you sure you want to flush all device-nonces that have been used during previous OTAA activations?"
onConfirm={flushDevNonces}
>
<Button>Flush OTAA device nonces</Button>
</Popconfirm>
</div>
)}
{!lw11 && <LW10DeviceKeysForm initialValues={initialValues} onFinish={onFinish} />}
{lw11 && <LW11DeviceKeysForm initialValues={initialValues} onFinish={onFinish} />}
</Space>
);
} }
export default SetDeviceKeys; export default SetDeviceKeys;

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