mirror of
https://github.com/chirpstack/chirpstack.git
synced 2025-04-28 23:19:40 +00:00
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:
parent
afc196095d
commit
6f1638e87a
@ -18,4 +18,4 @@ pkgs.mkShell {
|
||||
BINDGEN_EXTRA_CLANG_ARGS = "-I${pkgs.llvmPackages.libclang.lib}/lib/clang/${pkgs.llvmPackages.libclang.version}/include";
|
||||
DOCKER_BUILDKIT = "1";
|
||||
NIX_STORE = "/nix/store";
|
||||
}
|
||||
}
|
||||
|
@ -12,3 +12,6 @@ dependencies:
|
||||
|
||||
clean:
|
||||
rm -rf build
|
||||
|
||||
format:
|
||||
./node_modules/.bin/prettier --write .
|
||||
|
46
ui/README.md
46
ui/README.md
@ -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 can’t go back!**
|
||||
|
||||
If you aren’t 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 you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t 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/).
|
@ -3,76 +3,71 @@
|
||||
"version": "4.4.3",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^6.0.0",
|
||||
"@ant-design/colors": "^7.0.0",
|
||||
"@ant-design/pro-layout": "^7.16.3",
|
||||
"@chirpstack/chirpstack-api-grpc-web": "file:../api/grpc-web",
|
||||
"@fortawesome/fontawesome-free": "^6.1.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/leaflet": "^1.7.5",
|
||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/leaflet": "^1.9.3",
|
||||
"@types/leaflet.awesome-markers": "^2.0.25",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"antd": "^4.20.6",
|
||||
"antd-mask-input": "^2.0.7",
|
||||
"@types/node": "^16.18.38",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"antd": "^5.7.1",
|
||||
"buffer": "^6.0.3",
|
||||
"chart.js": "^3.7.1",
|
||||
"chartjs-adapter-moment": "^1.0.0",
|
||||
"chartjs-chart-matrix": "^1.1.1",
|
||||
"codemirror": "^5.65.3",
|
||||
"google-protobuf": "^3.21.2",
|
||||
"grpc-web": "^1.4.2",
|
||||
"chart.js": "^4.3.0",
|
||||
"chartjs-adapter-moment": "^1.0.1",
|
||||
"chartjs-chart-matrix": "^2.0.1",
|
||||
"codemirror": "5.65.3",
|
||||
"history": "^5.3.0",
|
||||
"js-file-download": "^0.4.12",
|
||||
"leaflet": "^1.7.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet.awesome-markers": "^2.0.5",
|
||||
"react": "^17.0.2",
|
||||
"react-chartjs-2": "^4.1.0",
|
||||
"moment": "^2.29.4",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-codemirror2": "^7.2.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-json-tree": "^0.15.1",
|
||||
"react-leaflet": "^3.2.1",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-router-dom": "^5.3.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-json-tree": "0.15.1",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-router-dom": "^6.14.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.6.4",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "prettier --check .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "yarn format"
|
||||
}
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"**/*_pb.js"
|
||||
]
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"proxy": "http://chirpstack:8080/",
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://127.0.0.1:8080/",
|
||||
"devDependencies": {
|
||||
"husky": "^7.0.4",
|
||||
"prettier": "^2.6.2"
|
||||
"prettier": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
145
ui/src/App.tsx
145
ui/src/App.tsx
@ -1,18 +1,22 @@
|
||||
import React, { Component } from "react";
|
||||
import { Router, Route, Switch } from "react-router-dom";
|
||||
import React, { useState } from "react";
|
||||
import { Router, Routes, Route } from "react-router-dom";
|
||||
|
||||
import { Layout } from "antd";
|
||||
|
||||
import { User } from "@chirpstack/chirpstack-api-grpc-web/api/user_pb";
|
||||
|
||||
import Menu from "./components/Menu";
|
||||
import Header from "./components/Header";
|
||||
import Menu from "./components/Menu";
|
||||
|
||||
// dashboard
|
||||
import Dashboard from "./views/dashboard/Dashboard";
|
||||
|
||||
// users
|
||||
import Login from "./views/users/Login";
|
||||
import ListUsers from "./views/users/ListUsers";
|
||||
import CreateUser from "./views/users/CreateUser";
|
||||
import EditUser from "./views/users/EditUser";
|
||||
import ChangeUserPassword from "./views/users/ChangeUserPassword";
|
||||
|
||||
// tenants
|
||||
import TenantRedirect from "./views/tenants/TenantRedirect";
|
||||
@ -20,20 +24,14 @@ import ListTenants from "./views/tenants/ListTenants";
|
||||
import CreateTenant from "./views/tenants/CreateTenant";
|
||||
import TenantLoader from "./views/tenants/TenantLoader";
|
||||
|
||||
// users
|
||||
import ListUsers from "./views/users/ListUsers";
|
||||
import CreateUser from "./views/users/CreateUser";
|
||||
import EditUser from "./views/users/EditUser";
|
||||
import ChangeUserPassword from "./views/users/ChangeUserPassword";
|
||||
|
||||
// api keys
|
||||
import ListAdminApiKeys from "./views/api-keys/ListAdminApiKeys";
|
||||
import CreateAdminApiKey from "./views/api-keys/CreateAdminApiKey";
|
||||
|
||||
// device-profile templates
|
||||
import ListDeviceProfileTemplates from "./views/device-profile-templates/ListDeviceProfileTemplates";
|
||||
import EditDeviceProfileTemplate from "./views/device-profile-templates/EditDeviceProfileTemplate";
|
||||
import CreateDeviceProfileTemplate from "./views/device-profile-templates/CreateDeviceProfileTemplate";
|
||||
import EditDeviceProfileTemplate from "./views/device-profile-templates/EditDeviceProfileTemplate";
|
||||
|
||||
// regions
|
||||
import ListRegions from "./views/regions/ListRegions";
|
||||
@ -44,85 +42,72 @@ import SessionStore from "./stores/SessionStore";
|
||||
|
||||
import history from "./history";
|
||||
|
||||
interface IProps {}
|
||||
const CustomRouter = ({ history, ...props }: any) => {
|
||||
const [state, setState] = useState({
|
||||
action: history.action,
|
||||
location: history.location,
|
||||
});
|
||||
|
||||
interface IState {
|
||||
user?: User;
|
||||
}
|
||||
React.useLayoutEffect(() => history.listen(setState), [history]);
|
||||
|
||||
class App extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
return <Router {...props} location={state.location} navigationType={state.action} navigator={history} />;
|
||||
};
|
||||
|
||||
this.state = {
|
||||
user: undefined,
|
||||
};
|
||||
}
|
||||
function App() {
|
||||
const [user, setUser] = useState<User | undefined>(SessionStore.getUser());
|
||||
SessionStore.on("change", () => {
|
||||
setUser(SessionStore.getUser());
|
||||
});
|
||||
|
||||
componentDidMount() {
|
||||
SessionStore.on("change", () => {
|
||||
this.setState({
|
||||
user: SessionStore.getUser(),
|
||||
});
|
||||
});
|
||||
return (
|
||||
<Layout style={{ minHeight: "100vh" }}>
|
||||
<CustomRouter history={history}>
|
||||
<Routes>
|
||||
<Route path="/" element={<TenantRedirect />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
</Routes>
|
||||
|
||||
this.setState({
|
||||
user: SessionStore.getUser(),
|
||||
});
|
||||
}
|
||||
{user && (
|
||||
<div>
|
||||
<Layout.Header className="layout-header">
|
||||
<Header user={user} />
|
||||
</Layout.Header>
|
||||
<Layout className="layout">
|
||||
<Layout.Sider width="300" theme="light" className="layout-menu">
|
||||
<Menu />
|
||||
</Layout.Sider>
|
||||
<Layout.Content className="layout-content" style={{ padding: "24px 24px 24px" }}>
|
||||
<Routes>
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/tenants" element={<ListTenants />} />
|
||||
<Route path="/tenants/create" element={<CreateTenant />} />
|
||||
<Route path="/tenants/:tenantId/*" element={<TenantLoader />} />
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Layout style={{ minHeight: "100vh" }}>
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Route exact path="/" component={TenantRedirect} />
|
||||
<Route exact path="/login" component={Login} />
|
||||
{this.state.user && (
|
||||
<Route>
|
||||
<Layout.Header className="layout-header">
|
||||
<Header user={this.state.user} />
|
||||
</Layout.Header>
|
||||
<Layout className="layout">
|
||||
<Layout.Sider width="300" theme="light" className="layout-menu">
|
||||
<Menu />
|
||||
</Layout.Sider>
|
||||
<Layout.Content className="layout-content" style={{ padding: "24px 24px 24px" }}>
|
||||
<Switch>
|
||||
<Route exact path="/dashboard" component={Dashboard} />
|
||||
<Route path="/users" element={<ListUsers />} />
|
||||
<Route path="/users/create" element={<CreateUser />} />
|
||||
<Route path="/users/:userId" element={<EditUser />} />
|
||||
<Route path="/users/:userId/password" element={<ChangeUserPassword />} />
|
||||
|
||||
<Route exact path="/tenants" component={ListTenants} />
|
||||
<Route exact path="/tenants/create" component={CreateTenant} />
|
||||
<Route path="/tenants/:tenantId([\w-]{36})" component={TenantLoader} />
|
||||
<Route path="/api-keys" element={<ListAdminApiKeys />} />
|
||||
<Route path="/api-keys/create" element={<CreateAdminApiKey />} />
|
||||
|
||||
<Route exact path="/users" component={ListUsers} />
|
||||
<Route exact path="/users/create" component={CreateUser} />
|
||||
<Route exact path="/users/:userId([\w-]{36})" component={EditUser} />
|
||||
<Route exact path="/users/:userId([\w-]{36})/password" component={ChangeUserPassword} />
|
||||
<Route path="/device-profile-templates" element={<ListDeviceProfileTemplates />} />
|
||||
<Route path="/device-profile-templates/create" element={<CreateDeviceProfileTemplate />} />
|
||||
<Route
|
||||
path="/device-profile-templates/:deviceProfileTemplateId/edit"
|
||||
element={<EditDeviceProfileTemplate />}
|
||||
/>
|
||||
|
||||
<Route exact path="/api-keys" component={ListAdminApiKeys} />
|
||||
<Route exact path="/api-keys/create" component={CreateAdminApiKey} />
|
||||
|
||||
<Route exact path="/device-profile-templates" component={ListDeviceProfileTemplates} />
|
||||
<Route exact path="/device-profile-templates/create" component={CreateDeviceProfileTemplate} />
|
||||
<Route
|
||||
exact
|
||||
path="/device-profile-templates/:deviceProfileTemplateId([\w-]+)/edit"
|
||||
component={EditDeviceProfileTemplate}
|
||||
/>
|
||||
|
||||
<Route exact path="/regions" component={ListRegions} />
|
||||
<Route path="/regions/:id(.*)" component={RegionDetails} />
|
||||
</Switch>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</Route>
|
||||
)}
|
||||
</Switch>
|
||||
</Router>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
<Route path="/regions" element={<ListRegions />} />
|
||||
<Route path="/regions/:id" element={<RegionDetails />} />
|
||||
</Routes>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</div>
|
||||
)}
|
||||
</CustomRouter>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component } from "react";
|
||||
import React, { PropsWithChildren, useState, useEffect } from "react";
|
||||
|
||||
import SessionStore from "../stores/SessionStore";
|
||||
|
||||
@ -9,73 +9,45 @@ interface IProps {
|
||||
isTenantAdmin?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
admin: boolean;
|
||||
}
|
||||
function Admin(props: PropsWithChildren<IProps>) {
|
||||
const [admin, setAdmin] = useState<boolean>(false);
|
||||
|
||||
class Admin extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
admin: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
SessionStore.on("change", this.setIsAdmin);
|
||||
this.setIsAdmin();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
SessionStore.removeListener("change", this.setIsAdmin);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: IProps) {
|
||||
if (prevProps === this.props) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setIsAdmin();
|
||||
}
|
||||
|
||||
setIsAdmin = () => {
|
||||
if (!this.props.isDeviceAdmin && !this.props.isGatewayAdmin && !this.props.isTenantAdmin) {
|
||||
this.setState({
|
||||
admin: SessionStore.isAdmin(),
|
||||
});
|
||||
const setIsAdmin = () => {
|
||||
if (!props.isDeviceAdmin && !props.isGatewayAdmin && !props.isTenantAdmin) {
|
||||
setAdmin(SessionStore.isAdmin());
|
||||
} else {
|
||||
if (this.props.tenantId === undefined) {
|
||||
if (props.tenantId === undefined) {
|
||||
throw new Error("No tenantId is given");
|
||||
}
|
||||
|
||||
if (this.props.isTenantAdmin) {
|
||||
this.setState({
|
||||
admin: SessionStore.isAdmin() || SessionStore.isTenantAdmin(this.props.tenantId),
|
||||
});
|
||||
if (props.isTenantAdmin) {
|
||||
setAdmin(SessionStore.isAdmin() || SessionStore.isTenantAdmin(props.tenantId));
|
||||
}
|
||||
|
||||
if (this.props.isDeviceAdmin) {
|
||||
this.setState({
|
||||
admin: SessionStore.isAdmin() || SessionStore.isTenantDeviceAdmin(this.props.tenantId),
|
||||
});
|
||||
if (props.isDeviceAdmin) {
|
||||
setAdmin(SessionStore.isAdmin() || SessionStore.isTenantDeviceAdmin(props.tenantId));
|
||||
}
|
||||
|
||||
if (this.props.isGatewayAdmin) {
|
||||
this.setState({
|
||||
admin: SessionStore.isAdmin() || SessionStore.isTenantGatewayAdmin(this.props.tenantId),
|
||||
});
|
||||
if (props.isGatewayAdmin) {
|
||||
setAdmin(SessionStore.isAdmin() || SessionStore.isTenantGatewayAdmin(props.tenantId));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.admin) {
|
||||
return this.props.children;
|
||||
}
|
||||
useEffect(() => {
|
||||
SessionStore.on("change", setIsAdmin);
|
||||
setIsAdmin();
|
||||
|
||||
return null;
|
||||
return () => {
|
||||
SessionStore.removeListener("change", setIsAdmin);
|
||||
};
|
||||
}, [props]);
|
||||
|
||||
if (admin) {
|
||||
return <div>{props.children}</div>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default Admin;
|
||||
|
@ -1,11 +1,10 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { notification, Input, Select, Button, Space, Form, Dropdown, Menu } from "antd";
|
||||
import { ReloadOutlined, CopyOutlined } from "@ant-design/icons";
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
interface IProps {
|
||||
formRef: React.RefObject<any>;
|
||||
label: string;
|
||||
name: string;
|
||||
required?: boolean;
|
||||
@ -14,42 +13,29 @@ interface IProps {
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
byteOrder: string;
|
||||
value: string;
|
||||
}
|
||||
function AesKeyInput(props: IProps) {
|
||||
const form = Form.useFormInstance();
|
||||
const [byteOrder, setByteOrder] = useState<string>("msb");
|
||||
const [value, setValue] = useState<string>("");
|
||||
|
||||
class AesKeyInput extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
byteOrder: "msb",
|
||||
value: "",
|
||||
};
|
||||
}
|
||||
useEffect(() => {
|
||||
if (props.value) {
|
||||
setValue(props.value);
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
updateField = () => {
|
||||
let value = this.state.value;
|
||||
|
||||
if (this.state.byteOrder === "lsb") {
|
||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||
value = bytes.reverse().join("");
|
||||
const updateField = (v: string) => {
|
||||
if (byteOrder === "lsb") {
|
||||
const bytes = v.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||
v = bytes.reverse().join("");
|
||||
}
|
||||
|
||||
this.props.formRef.current.setFieldsValue({
|
||||
[this.props.name]: value,
|
||||
form.setFieldsValue({
|
||||
[props.name]: v,
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.value) {
|
||||
this.setState({
|
||||
value: this.props.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let v = e.target.value;
|
||||
const match = v.match(/[A-Fa-f0-9]/g);
|
||||
|
||||
@ -62,50 +48,37 @@ class AesKeyInput extends Component<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
value: value,
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
setValue(value);
|
||||
updateField(value);
|
||||
};
|
||||
|
||||
onByteOrderSelect = (v: string) => {
|
||||
if (v === this.state.byteOrder) {
|
||||
const onByteOrderSelect = (v: string) => {
|
||||
if (v === byteOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
byteOrder: v,
|
||||
});
|
||||
setByteOrder(v);
|
||||
|
||||
const current = this.state.value;
|
||||
const current = value;
|
||||
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||
const vv = bytes.reverse().join("");
|
||||
|
||||
this.setState(
|
||||
{
|
||||
value: bytes.reverse().join(""),
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
setValue(vv);
|
||||
updateField(vv);
|
||||
};
|
||||
|
||||
generateRandom = () => {
|
||||
const generateRandom = () => {
|
||||
let cryptoObj = window.crypto || window.Crypto;
|
||||
let b = new Uint8Array(16);
|
||||
cryptoObj.getRandomValues(b);
|
||||
|
||||
let key = Buffer.from(b).toString("hex");
|
||||
this.setState(
|
||||
{
|
||||
value: key,
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
setValue(key);
|
||||
updateField(key);
|
||||
};
|
||||
|
||||
copyToClipboard = () => {
|
||||
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
|
||||
const copyToClipboard = () => {
|
||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
|
||||
|
||||
if (bytes !== null && navigator.clipboard !== undefined) {
|
||||
navigator.clipboard
|
||||
@ -126,8 +99,8 @@ class AesKeyInput extends Component<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
copyToClipboardHexArray = () => {
|
||||
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
|
||||
const copyToClipboardHexArray = () => {
|
||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
|
||||
|
||||
if (bytes !== null && navigator.clipboard !== undefined) {
|
||||
navigator.clipboard
|
||||
@ -153,72 +126,70 @@ class AesKeyInput extends Component<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const copyMenu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: (
|
||||
<Button type="text" onClick={this.copyToClipboard}>
|
||||
HEX string
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: (
|
||||
<Button type="text" onClick={this.copyToClipboardHexArray}>
|
||||
HEX array
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const copyMenu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: (
|
||||
<Button type="text" onClick={copyToClipboard}>
|
||||
HEX string
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: (
|
||||
<Button type="text" onClick={copyToClipboardHexArray}>
|
||||
HEX array
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const addon = (
|
||||
<Space size="large">
|
||||
<Select value={this.state.byteOrder} onChange={this.onByteOrderSelect}>
|
||||
<Select.Option value="msb">MSB</Select.Option>
|
||||
<Select.Option value="lsb">LSB</Select.Option>
|
||||
</Select>
|
||||
<Button type="text" size="small" shape="circle" onClick={this.generateRandom}>
|
||||
<ReloadOutlined />
|
||||
const addon = (
|
||||
<Space size="large">
|
||||
<Select value={byteOrder} onChange={onByteOrderSelect}>
|
||||
<Select.Option value="msb">MSB</Select.Option>
|
||||
<Select.Option value="lsb">LSB</Select.Option>
|
||||
</Select>
|
||||
<Button type="text" size="small" onClick={generateRandom}>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
<Dropdown overlay={copyMenu}>
|
||||
<Button type="text" size="small">
|
||||
<CopyOutlined />
|
||||
</Button>
|
||||
<Dropdown overlay={copyMenu}>
|
||||
<Button type="text" size="small">
|
||||
<CopyOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
);
|
||||
</Dropdown>
|
||||
</Space>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
rules={[
|
||||
{
|
||||
required: this.props.required,
|
||||
message: `Please enter a valid ${this.props.label}`,
|
||||
pattern: new RegExp(/[A-Fa-f0-9]{32}/g),
|
||||
},
|
||||
]}
|
||||
label={this.props.label}
|
||||
name={this.props.name}
|
||||
tooltip={this.props.tooltip}
|
||||
>
|
||||
<Input hidden />
|
||||
<Input.Password
|
||||
id={`${this.props.name}Render`}
|
||||
onChange={this.onChange}
|
||||
addonAfter={!this.props.disabled && addon}
|
||||
style={{ fontFamily: "monospace" }}
|
||||
value={this.state.value}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form.Item
|
||||
rules={[
|
||||
{
|
||||
required: props.required,
|
||||
message: `Please enter a valid ${props.label}`,
|
||||
pattern: new RegExp(/[A-Fa-f0-9]{32}/g),
|
||||
},
|
||||
]}
|
||||
label={props.label}
|
||||
name={props.name}
|
||||
tooltip={props.tooltip}
|
||||
>
|
||||
<Input hidden />
|
||||
<Input
|
||||
id={`${props.name}Render`}
|
||||
onChange={onChange}
|
||||
addonAfter={!props.disabled && addon}
|
||||
className="input-code"
|
||||
value={value}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export default AesKeyInput;
|
||||
|
@ -1,10 +1,15 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { Select } from "antd";
|
||||
|
||||
export type OptionsCallbackFunc = (o: { label: string; value: string }[]) => void;
|
||||
export type OptionCallbackFunc = (o: { label: string; value: string }) => void;
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
placeholder: string;
|
||||
className: string;
|
||||
@ -14,93 +19,59 @@ interface IProps {
|
||||
onSelect?: (s: string) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
option?: { label: string; value: string };
|
||||
options: { label: string; value: string }[];
|
||||
}
|
||||
function AutoComplete({ placeholder, className, value, getOption, getOptions, onSelect }: IProps) {
|
||||
const [option, setOption] = useState<Option | undefined>(undefined);
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
|
||||
class Autocomplete extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.value && this.props.value !== "") {
|
||||
this.props.getOption(this.props.value, (o: { label: string; value: string }) => {
|
||||
this.setState({
|
||||
options: [o],
|
||||
});
|
||||
useEffect(() => {
|
||||
if (value && value !== "") {
|
||||
getOption(value, (o: Option) => {
|
||||
setOptions([o]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [value, getOption]);
|
||||
|
||||
componentDidUpdate(prevProps: IProps) {
|
||||
if (this.props.value === prevProps.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.value && this.props.value !== "") {
|
||||
this.props.getOption(this.props.value, (o: { label: string; value: string }) => {
|
||||
this.setState({
|
||||
options: [o],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onFocus = () => {
|
||||
this.props.getOptions("", options => {
|
||||
if (this.state.option !== undefined) {
|
||||
const selected = this.state.option.value;
|
||||
const onFocus = () => {
|
||||
getOptions("", options => {
|
||||
if (option !== undefined) {
|
||||
const selected = option.value;
|
||||
|
||||
if (options.find(e => e.value === selected) === undefined) {
|
||||
options.unshift(this.state.option);
|
||||
options.unshift(option);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
options: options,
|
||||
});
|
||||
setOptions(options);
|
||||
});
|
||||
};
|
||||
|
||||
onSearch = (value: string) => {
|
||||
this.props.getOptions(value, options => {
|
||||
this.setState({
|
||||
options: options,
|
||||
});
|
||||
const onSearch = (value: string) => {
|
||||
getOptions(value, options => {
|
||||
setOptions(options);
|
||||
});
|
||||
};
|
||||
|
||||
onSelect = (value: string, option: any) => {
|
||||
this.setState({
|
||||
option: { label: option.label, value: option.value },
|
||||
});
|
||||
const onSelectFn = (value: string, option: any) => {
|
||||
setOption({ label: option.label, value: option.value });
|
||||
|
||||
if (this.props.onSelect !== undefined) {
|
||||
this.props.onSelect(value);
|
||||
if (onSelect !== undefined) {
|
||||
onSelect(value);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { getOption, getOptions, ...otherProps } = this.props;
|
||||
|
||||
return (
|
||||
<Select
|
||||
showSearch
|
||||
options={this.state.options}
|
||||
onFocus={this.onFocus}
|
||||
onSearch={this.onSearch}
|
||||
onSelect={this.onSelect}
|
||||
filterOption={false}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
showSearch
|
||||
options={options}
|
||||
onFocus={onFocus}
|
||||
onSearch={onSearch}
|
||||
onSelect={onSelectFn}
|
||||
filterOption={false}
|
||||
placeholder={placeholder}
|
||||
className={className}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Autocomplete;
|
||||
export default AutoComplete;
|
||||
|
@ -1,11 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Form } from "antd";
|
||||
|
||||
import Autocomplete, { OptionCallbackFunc, OptionsCallbackFunc } from "./Autocomplete";
|
||||
|
||||
interface IProps {
|
||||
formRef: React.RefObject<any>;
|
||||
label: string;
|
||||
name: string;
|
||||
required?: boolean;
|
||||
@ -14,28 +11,35 @@ interface IProps {
|
||||
getOptions: (s: string, fn: OptionsCallbackFunc) => void;
|
||||
}
|
||||
|
||||
class AutocompleteInput extends Component<IProps> {
|
||||
render() {
|
||||
return (
|
||||
<Form.Item
|
||||
rules={[
|
||||
{
|
||||
required: this.props.required,
|
||||
message: `Please select a ${this.props.label}`,
|
||||
},
|
||||
]}
|
||||
label={this.props.label}
|
||||
name={this.props.name}
|
||||
>
|
||||
<Autocomplete
|
||||
placeholder={`Select a ${this.props.label}`}
|
||||
className=""
|
||||
getOption={this.props.getOption}
|
||||
getOptions={this.props.getOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
function AutocompleteInput(props: IProps) {
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
const onSelect = (value: string) => {
|
||||
form.setFieldsValue({
|
||||
[props.name]: value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
rules={[
|
||||
{
|
||||
required: props.required,
|
||||
message: `Please select a ${props.label}`,
|
||||
},
|
||||
]}
|
||||
label={props.label}
|
||||
name={props.name}
|
||||
>
|
||||
<Autocomplete
|
||||
placeholder={`Select a ${props.label}`}
|
||||
className=""
|
||||
getOption={props.getOption}
|
||||
getOptions={props.getOptions}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export default AutocompleteInput;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Controlled as CodeMirror } from "react-codemirror2";
|
||||
|
||||
import { Form } from "antd";
|
||||
@ -6,88 +6,49 @@ import { Form } from "antd";
|
||||
import "codemirror/mode/javascript/javascript";
|
||||
|
||||
interface IProps {
|
||||
formRef: React.RefObject<any>;
|
||||
label?: string;
|
||||
name: string;
|
||||
required?: boolean;
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
value: string;
|
||||
reloadKey: number;
|
||||
}
|
||||
function CodeEditor(props: IProps) {
|
||||
const form = Form.useFormInstance();
|
||||
const [value, setValue] = useState<string>("");
|
||||
const [reloadKey, setReloadKey] = useState<number>(1);
|
||||
|
||||
class CodeEditor extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: "",
|
||||
reloadKey: 0,
|
||||
};
|
||||
}
|
||||
useEffect(() => {
|
||||
setValue(form.getFieldValue(props.name));
|
||||
setReloadKey(k => k + 1);
|
||||
}, [form, props]);
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.value) {
|
||||
this.setState({
|
||||
value: this.props.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(oldProps: IProps) {
|
||||
if (this.props === oldProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.value) {
|
||||
this.setState({
|
||||
value: this.props.value,
|
||||
reloadKey: this.state.reloadKey + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateField = () => {
|
||||
let value = this.state.value;
|
||||
|
||||
this.props.formRef.current.setFieldsValue({
|
||||
[this.props.name]: value,
|
||||
const handleChange = (editor: any, data: any, newCode: string) => {
|
||||
setValue(newCode);
|
||||
form.setFieldsValue({
|
||||
[props.name]: newCode,
|
||||
});
|
||||
};
|
||||
|
||||
handleChange = (editor: any, data: any, newCode: string) => {
|
||||
this.setState(
|
||||
{
|
||||
value: newCode,
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
const codeMirrorOptions = {
|
||||
lineNumbers: true,
|
||||
mode: "javascript",
|
||||
theme: "base16-light",
|
||||
readOnly: props.disabled,
|
||||
};
|
||||
|
||||
render() {
|
||||
const codeMirrorOptions = {
|
||||
lineNumbers: true,
|
||||
mode: "javascript",
|
||||
theme: "base16-light",
|
||||
readOnly: this.props.disabled,
|
||||
};
|
||||
|
||||
return (
|
||||
<Form.Item label={this.props.label} name={this.props.name} tooltip={this.props.tooltip}>
|
||||
<div style={{ border: "1px solid #cccccc" }}>
|
||||
<CodeMirror
|
||||
key={`code-editor-refresh-${this.state.reloadKey}`}
|
||||
value={this.state.value}
|
||||
options={codeMirrorOptions}
|
||||
onBeforeChange={this.handleChange}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form.Item label={props.label} name={props.name} tooltip={props.tooltip}>
|
||||
<div style={{ border: "1px solid #cccccc" }}>
|
||||
<CodeMirror
|
||||
key={`code-editor-refresh-${reloadKey}`}
|
||||
value={value}
|
||||
options={codeMirrorOptions}
|
||||
onBeforeChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export default CodeEditor;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { Table } from "antd";
|
||||
import { ColumnsType } from "antd/es/table";
|
||||
@ -16,113 +16,81 @@ interface IProps {
|
||||
noPagination?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
totalCount: number;
|
||||
pageSize: number;
|
||||
currentPage: number;
|
||||
rows: object[];
|
||||
loading: boolean;
|
||||
}
|
||||
function DataTable(props: IProps) {
|
||||
const [totalCount, setTotalCount] = useState<number>(0);
|
||||
const [pageSize, setPageSize] = useState<number>(SessionStore.getRowsPerPage());
|
||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||
const [rows, setRows] = useState<object[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
class DataTable extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
const onChangePage = (page: number, pz?: number | void) => {
|
||||
setLoading(true);
|
||||
|
||||
this.state = {
|
||||
totalCount: 0,
|
||||
pageSize: SessionStore.getRowsPerPage(),
|
||||
currentPage: 1,
|
||||
rows: [],
|
||||
loading: true,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.onChangePage(this.state.currentPage, this.state.pageSize);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: IProps) {
|
||||
if (this.props === prevProps) {
|
||||
return;
|
||||
if (!pz) {
|
||||
pz = pageSize;
|
||||
}
|
||||
|
||||
this.onChangePage(this.state.currentPage, this.state.pageSize);
|
||||
}
|
||||
|
||||
onChangePage = (page: number, pageSize?: number | void) => {
|
||||
this.setState(
|
||||
{
|
||||
loading: true,
|
||||
},
|
||||
() => {
|
||||
let pz = pageSize;
|
||||
if (!pz) {
|
||||
pz = this.state.pageSize;
|
||||
}
|
||||
|
||||
this.props.getPage(pz, (page - 1) * pz, (totalCount: number, rows: object[]) => {
|
||||
this.setState({
|
||||
currentPage: page,
|
||||
totalCount: totalCount,
|
||||
rows: rows,
|
||||
pageSize: pz || 0,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
props.getPage(pz, (page - 1) * pz, (totalCount: number, rows: object[]) => {
|
||||
setCurrentPage(page);
|
||||
setTotalCount(totalCount);
|
||||
setRows(rows);
|
||||
setPageSize(pz || 0);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
onShowSizeChange = (page: number, pageSize: number) => {
|
||||
this.onChangePage(page, pageSize);
|
||||
const onShowSizeChange = (page: number, pageSize: number) => {
|
||||
onChangePage(page, pageSize);
|
||||
SessionStore.setRowsPerPage(pageSize);
|
||||
};
|
||||
|
||||
onRowsSelectChange = (ids: React.Key[]) => {
|
||||
const onRowsSelectChange = (ids: React.Key[]) => {
|
||||
const idss = ids as string[];
|
||||
if (this.props.onRowsSelectChange) {
|
||||
this.props.onRowsSelectChange(idss);
|
||||
if (props.onRowsSelectChange) {
|
||||
props.onRowsSelectChange(idss);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { getPage, refreshKey, ...otherProps } = this.props;
|
||||
let loadingProps = undefined;
|
||||
if (this.state.loading) {
|
||||
loadingProps = {
|
||||
delay: 300,
|
||||
};
|
||||
}
|
||||
useEffect(() => {
|
||||
onChangePage(currentPage, pageSize);
|
||||
}, [props, currentPage, pageSize]);
|
||||
|
||||
let pagination = undefined;
|
||||
if (this.props.noPagination === undefined || this.props.noPagination === false) {
|
||||
pagination = {
|
||||
current: this.state.currentPage,
|
||||
total: this.state.totalCount,
|
||||
pageSize: this.state.pageSize,
|
||||
onChange: this.onChangePage,
|
||||
showSizeChanger: true,
|
||||
onShowSizeChange: this.onShowSizeChange,
|
||||
};
|
||||
}
|
||||
|
||||
let rowSelection = undefined;
|
||||
if (this.props.onRowsSelectChange) {
|
||||
rowSelection = {
|
||||
onChange: this.onRowsSelectChange,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
loading={loadingProps}
|
||||
dataSource={this.state.rows}
|
||||
pagination={pagination || false}
|
||||
rowSelection={rowSelection}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
const { getPage, refreshKey, ...otherProps } = props;
|
||||
let loadingProps = undefined;
|
||||
if (loading) {
|
||||
loadingProps = {
|
||||
delay: 300,
|
||||
};
|
||||
}
|
||||
|
||||
let pagination = undefined;
|
||||
if (props.noPagination === undefined || props.noPagination === false) {
|
||||
pagination = {
|
||||
current: currentPage,
|
||||
total: totalCount,
|
||||
pageSize: pageSize,
|
||||
onChange: onChangePage,
|
||||
showSizeChanger: true,
|
||||
onShowSizeChange: onShowSizeChange,
|
||||
};
|
||||
}
|
||||
|
||||
let rowSelection = undefined;
|
||||
if (props.onRowsSelectChange) {
|
||||
rowSelection = {
|
||||
onChange: onRowsSelectChange,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
loading={loadingProps}
|
||||
dataSource={rows}
|
||||
pagination={pagination || false}
|
||||
rowSelection={rowSelection}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default DataTable;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { useState, PropsWithChildren } from "react";
|
||||
import { Popover, Button, Typography, Space, Input } from "antd";
|
||||
|
||||
interface IProps {
|
||||
@ -8,51 +7,32 @@ interface IProps {
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
interface ConfirmState {
|
||||
confirm: string;
|
||||
}
|
||||
function DeleteConfirmContent(props: IProps) {
|
||||
const [confirm, setConfirm] = useState<string>("");
|
||||
|
||||
class DeleteConfirmContent extends Component<IProps, ConfirmState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
confirm: "",
|
||||
};
|
||||
}
|
||||
|
||||
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
confirm: e.target.value,
|
||||
});
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setConfirm(e.target.value);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Space direction="vertical">
|
||||
<Typography.Text>
|
||||
Enter '{this.props.confirm}' to confirm you want to delete this {this.props.typ}:
|
||||
</Typography.Text>
|
||||
<Input placeholder={this.props.confirm} onChange={this.onChange} />
|
||||
<Button
|
||||
onClick={this.props.onConfirm}
|
||||
disabled={this.state.confirm !== this.props.confirm}
|
||||
style={{ float: "right" }}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Space direction="vertical">
|
||||
<Typography.Text>
|
||||
Enter '{props.confirm}' to confirm you want to delete this {props.typ}:
|
||||
</Typography.Text>
|
||||
<Input placeholder={props.confirm} onChange={onChange} />
|
||||
<Button onClick={props.onConfirm} disabled={confirm !== props.confirm} style={{ float: "right" }}>
|
||||
Delete
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
class DeleteConfirm extends Component<IProps> {
|
||||
render() {
|
||||
return (
|
||||
<Popover content={<DeleteConfirmContent {...this.props} />} trigger="click" placement="left">
|
||||
{this.props.children}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
function DeleteConfirm(props: PropsWithChildren<IProps>) {
|
||||
return (
|
||||
<Popover content={<DeleteConfirmContent {...props} />} trigger="click" placement="left">
|
||||
{props.children}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeleteConfirm;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { notification, Input, Select, Button, Space, Form, Dropdown, Menu } from "antd";
|
||||
import { ReloadOutlined, CopyOutlined } from "@ant-design/icons";
|
||||
@ -8,7 +8,6 @@ import { GetRandomDevAddrRequest, GetRandomDevAddrResponse } from "@chirpstack/c
|
||||
import DeviceStore from "../stores/DeviceStore";
|
||||
|
||||
interface IProps {
|
||||
formRef: React.RefObject<any>;
|
||||
label: string;
|
||||
name: string;
|
||||
devEui: string;
|
||||
@ -17,42 +16,29 @@ interface IProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
byteOrder: string;
|
||||
value: string;
|
||||
}
|
||||
function DevAddrInput(props: IProps) {
|
||||
const form = Form.useFormInstance();
|
||||
const [byteOrder, setByteOrder] = useState<string>("msb");
|
||||
const [value, setValue] = useState<string>("");
|
||||
|
||||
class DevAddrInput extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
byteOrder: "msb",
|
||||
value: "",
|
||||
};
|
||||
}
|
||||
useEffect(() => {
|
||||
if (props.value) {
|
||||
setValue(props.value);
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
updateField = () => {
|
||||
let value = this.state.value;
|
||||
|
||||
if (this.state.byteOrder === "lsb") {
|
||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||
value = bytes.reverse().join("");
|
||||
const updateField = (v: string) => {
|
||||
if (byteOrder === "lsb") {
|
||||
const bytes = v.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||
v = bytes.reverse().join("");
|
||||
}
|
||||
|
||||
this.props.formRef.current.setFieldsValue({
|
||||
[this.props.name]: value,
|
||||
form.setFieldsValue({
|
||||
[props.name]: v,
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.value) {
|
||||
this.setState({
|
||||
value: this.props.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let v = e.target.value;
|
||||
const match = v.match(/[A-Fa-f0-9]/g);
|
||||
|
||||
@ -65,50 +51,37 @@ class DevAddrInput extends Component<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
value: value,
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
setValue(value);
|
||||
updateField(value);
|
||||
};
|
||||
|
||||
onByteOrderSelect = (v: string) => {
|
||||
if (v === this.state.byteOrder) {
|
||||
const onByteOrderSelect = (v: string) => {
|
||||
if (v === byteOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
byteOrder: v,
|
||||
});
|
||||
setByteOrder(v);
|
||||
|
||||
const current = this.state.value;
|
||||
const current = value;
|
||||
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||
const vv = bytes.reverse().join("");
|
||||
|
||||
this.setState(
|
||||
{
|
||||
value: bytes.reverse().join(""),
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
setValue(vv);
|
||||
updateField(vv);
|
||||
};
|
||||
|
||||
generateRandom = () => {
|
||||
const generateRandom = () => {
|
||||
let req = new GetRandomDevAddrRequest();
|
||||
req.setDevEui(this.props.devEui);
|
||||
req.setDevEui(props.devEui);
|
||||
|
||||
DeviceStore.getRandomDevAddr(req, (resp: GetRandomDevAddrResponse) => {
|
||||
this.setState(
|
||||
{
|
||||
value: resp.getDevAddr(),
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
setValue(resp.getDevAddr());
|
||||
updateField(resp.getDevAddr());
|
||||
});
|
||||
};
|
||||
|
||||
copyToClipboard = () => {
|
||||
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
|
||||
const copyToClipboard = () => {
|
||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
|
||||
|
||||
if (bytes !== null && navigator.clipboard !== undefined) {
|
||||
navigator.clipboard
|
||||
@ -129,8 +102,8 @@ class DevAddrInput extends Component<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
copyToClipboardHexArray = () => {
|
||||
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
|
||||
const copyToClipboardHexArray = () => {
|
||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
|
||||
|
||||
if (bytes !== null && navigator.clipboard !== undefined) {
|
||||
navigator.clipboard
|
||||
@ -156,71 +129,69 @@ class DevAddrInput extends Component<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const copyMenu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: (
|
||||
<Button type="text" onClick={this.copyToClipboard}>
|
||||
HEX string
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: (
|
||||
<Button type="text" onClick={this.copyToClipboardHexArray}>
|
||||
HEX array
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const copyMenu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: (
|
||||
<Button type="text" onClick={copyToClipboard}>
|
||||
HEX string
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: (
|
||||
<Button type="text" onClick={copyToClipboardHexArray}>
|
||||
HEX array
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const addon = (
|
||||
<Space size="large">
|
||||
<Select value={this.state.byteOrder} onChange={this.onByteOrderSelect}>
|
||||
<Select.Option value="msb">MSB</Select.Option>
|
||||
<Select.Option value="lsb">LSB</Select.Option>
|
||||
</Select>
|
||||
<Button type="text" size="small" shape="circle" onClick={this.generateRandom}>
|
||||
<ReloadOutlined />
|
||||
const addon = (
|
||||
<Space size="large">
|
||||
<Select value={byteOrder} onChange={onByteOrderSelect}>
|
||||
<Select.Option value="msb">MSB</Select.Option>
|
||||
<Select.Option value="lsb">LSB</Select.Option>
|
||||
</Select>
|
||||
<Button type="text" size="small" onClick={generateRandom}>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
<Dropdown overlay={copyMenu}>
|
||||
<Button type="text" size="small">
|
||||
<CopyOutlined />
|
||||
</Button>
|
||||
<Dropdown overlay={copyMenu}>
|
||||
<Button type="text" size="small">
|
||||
<CopyOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
);
|
||||
</Dropdown>
|
||||
</Space>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
rules={[
|
||||
{
|
||||
required: this.props.required,
|
||||
message: `Please enter a valid ${this.props.label}`,
|
||||
pattern: new RegExp(/[A-Fa-f0-9]{8}/g),
|
||||
},
|
||||
]}
|
||||
label={this.props.label}
|
||||
name={this.props.name}
|
||||
>
|
||||
<Input hidden />
|
||||
<Input
|
||||
id={`${this.props.name}Render`}
|
||||
onChange={this.onChange}
|
||||
addonAfter={!this.props.disabled && addon}
|
||||
style={{ fontFamily: "monospace" }}
|
||||
value={this.state.value}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form.Item
|
||||
rules={[
|
||||
{
|
||||
required: props.required,
|
||||
message: `Please enter a valid ${props.label}`,
|
||||
pattern: new RegExp(/[A-Fa-f0-9]{8}/g),
|
||||
},
|
||||
]}
|
||||
label={props.label}
|
||||
name={props.name}
|
||||
>
|
||||
<Input hidden />
|
||||
<Input
|
||||
id={`${props.name}Render`}
|
||||
onChange={onChange}
|
||||
addonAfter={!props.disabled && addon}
|
||||
className="input-code"
|
||||
value={value}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export default DevAddrInput;
|
||||
|
@ -1,11 +1,10 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { notification, Input, Select, Button, Space, Form, Dropdown, Menu } from "antd";
|
||||
import { ReloadOutlined, CopyOutlined } from "@ant-design/icons";
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
interface IProps {
|
||||
formRef: React.RefObject<any>;
|
||||
label: string;
|
||||
name: string;
|
||||
required?: boolean;
|
||||
@ -14,42 +13,29 @@ interface IProps {
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
byteOrder: string;
|
||||
value: string;
|
||||
}
|
||||
function EuiInput(props: IProps) {
|
||||
const form = Form.useFormInstance();
|
||||
const [byteOrder, setByteOrder] = useState<string>("msb");
|
||||
const [value, setValue] = useState<string>("");
|
||||
|
||||
class EuiInput extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
byteOrder: "msb",
|
||||
value: "",
|
||||
};
|
||||
}
|
||||
useEffect(() => {
|
||||
if (props.value) {
|
||||
setValue(props.value);
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
updateField = () => {
|
||||
let value = this.state.value;
|
||||
|
||||
if (this.state.byteOrder === "lsb") {
|
||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||
value = bytes.reverse().join("");
|
||||
const updateField = (v: string) => {
|
||||
if (byteOrder === "lsb") {
|
||||
const bytes = v.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||
v = bytes.reverse().join("");
|
||||
}
|
||||
|
||||
this.props.formRef.current.setFieldsValue({
|
||||
[this.props.name]: value,
|
||||
form.setFieldsValue({
|
||||
[props.name]: v,
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.value) {
|
||||
this.setState({
|
||||
value: this.props.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let v = e.target.value;
|
||||
const match = v.match(/[A-Fa-f0-9]/g);
|
||||
|
||||
@ -62,50 +48,37 @@ class EuiInput extends Component<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
value: value,
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
setValue(value);
|
||||
updateField(value);
|
||||
};
|
||||
|
||||
onByteOrderSelect = (v: string) => {
|
||||
if (v === this.state.byteOrder) {
|
||||
const onByteOrderSelect = (v: string) => {
|
||||
if (v === byteOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
byteOrder: v,
|
||||
});
|
||||
setByteOrder(v);
|
||||
|
||||
const current = this.state.value;
|
||||
const current = value;
|
||||
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||
const vv = bytes.reverse().join("");
|
||||
|
||||
this.setState(
|
||||
{
|
||||
value: bytes.reverse().join(""),
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
setValue(vv);
|
||||
updateField(vv);
|
||||
};
|
||||
|
||||
generateRandom = () => {
|
||||
const generateRandom = () => {
|
||||
let cryptoObj = window.crypto || window.Crypto;
|
||||
let b = new Uint8Array(8);
|
||||
cryptoObj.getRandomValues(b);
|
||||
|
||||
let key = Buffer.from(b).toString("hex");
|
||||
this.setState(
|
||||
{
|
||||
value: key,
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
setValue(key);
|
||||
updateField(key);
|
||||
};
|
||||
|
||||
copyToClipboard = () => {
|
||||
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
|
||||
const copyToClipboard = () => {
|
||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
|
||||
|
||||
if (bytes !== null && navigator.clipboard !== undefined) {
|
||||
navigator.clipboard
|
||||
@ -126,8 +99,8 @@ class EuiInput extends Component<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
copyToClipboardHexArray = () => {
|
||||
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
|
||||
const copyToClipboardHexArray = () => {
|
||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
|
||||
|
||||
if (bytes !== null && navigator.clipboard !== undefined) {
|
||||
navigator.clipboard
|
||||
@ -153,72 +126,70 @@ class EuiInput extends Component<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const copyMenu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: (
|
||||
<Button type="text" onClick={this.copyToClipboard}>
|
||||
HEX string
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: (
|
||||
<Button type="text" onClick={this.copyToClipboardHexArray}>
|
||||
HEX array
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const copyMenu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: (
|
||||
<Button type="text" onClick={copyToClipboard}>
|
||||
HEX string
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: (
|
||||
<Button type="text" onClick={copyToClipboardHexArray}>
|
||||
HEX array
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const addon = (
|
||||
<Space size="large">
|
||||
<Select value={this.state.byteOrder} onChange={this.onByteOrderSelect}>
|
||||
<Select.Option value="msb">MSB</Select.Option>
|
||||
<Select.Option value="lsb">LSB</Select.Option>
|
||||
</Select>
|
||||
<Button type="text" size="small" onClick={this.generateRandom}>
|
||||
<ReloadOutlined />
|
||||
const addon = (
|
||||
<Space size="large">
|
||||
<Select value={byteOrder} onChange={onByteOrderSelect}>
|
||||
<Select.Option value="msb">MSB</Select.Option>
|
||||
<Select.Option value="lsb">LSB</Select.Option>
|
||||
</Select>
|
||||
<Button type="text" size="small" onClick={generateRandom}>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
<Dropdown overlay={copyMenu}>
|
||||
<Button type="text" size="small">
|
||||
<CopyOutlined />
|
||||
</Button>
|
||||
<Dropdown overlay={copyMenu}>
|
||||
<Button type="text" size="small">
|
||||
<CopyOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
);
|
||||
</Dropdown>
|
||||
</Space>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
rules={[
|
||||
{
|
||||
required: this.props.required,
|
||||
message: `Please enter a valid ${this.props.label}`,
|
||||
pattern: new RegExp(/[A-Fa-f0-9]{16}/g),
|
||||
},
|
||||
]}
|
||||
label={this.props.label}
|
||||
name={this.props.name}
|
||||
tooltip={this.props.tooltip}
|
||||
>
|
||||
<Input hidden />
|
||||
<Input
|
||||
id={`${this.props.name}Render`}
|
||||
onChange={this.onChange}
|
||||
addonAfter={!this.props.disabled && addon}
|
||||
style={{ fontFamily: "monospace" }}
|
||||
value={this.state.value}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form.Item
|
||||
rules={[
|
||||
{
|
||||
required: props.required,
|
||||
message: `Please enter a valid ${props.label}`,
|
||||
pattern: new RegExp(/[A-Fa-f0-9]{16}/g),
|
||||
},
|
||||
]}
|
||||
label={props.label}
|
||||
name={props.name}
|
||||
tooltip={props.tooltip}
|
||||
>
|
||||
<Input hidden />
|
||||
<Input
|
||||
id={`${props.name}Render`}
|
||||
onChange={onChange}
|
||||
addonAfter={!props.disabled && addon}
|
||||
className="input-code"
|
||||
value={value}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export default EuiInput;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link, withRouter, RouteComponentProps } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
import { Button, Menu, Dropdown, Input, AutoComplete } from "antd";
|
||||
import { UserOutlined, DownOutlined, QuestionOutlined } from "@ant-design/icons";
|
||||
@ -14,15 +14,6 @@ import {
|
||||
import InternalStore from "../stores/InternalStore";
|
||||
import SessionStore from "../stores/SessionStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
user: User;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
searchResult?: GlobalSearchResponse;
|
||||
settings?: SettingsResponse;
|
||||
}
|
||||
|
||||
const renderTitle = (title: string) => <span>{title}</span>;
|
||||
|
||||
const renderItem = (title: string, url: string) => ({
|
||||
@ -30,22 +21,19 @@ const renderItem = (title: string, url: string) => ({
|
||||
label: <Link to={url}>{title}</Link>,
|
||||
});
|
||||
|
||||
class Header extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
function Header({ user }: { user: User }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
const [settings, setSettings] = useState<SettingsResponse | undefined>(undefined);
|
||||
const [searchResult, setSearchResult] = useState<GlobalSearchResponse | undefined>(undefined);
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
InternalStore.settings((resp: SettingsResponse) => {
|
||||
this.setState({
|
||||
settings: resp,
|
||||
});
|
||||
setSettings(resp);
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
onSearch = (search: string) => {
|
||||
const onSearch = (search: string) => {
|
||||
if (search.length < 3) {
|
||||
return;
|
||||
}
|
||||
@ -55,14 +43,11 @@ class Header extends Component<IProps, IState> {
|
||||
req.setSearch(search);
|
||||
|
||||
InternalStore.globalSearch(req, (resp: GlobalSearchResponse) => {
|
||||
this.setState({
|
||||
searchResult: resp,
|
||||
});
|
||||
setSearchResult(resp);
|
||||
});
|
||||
};
|
||||
|
||||
onLogout = () => {
|
||||
let settings = this.state.settings;
|
||||
const onLogout = () => {
|
||||
if (settings === undefined) {
|
||||
return;
|
||||
}
|
||||
@ -71,117 +56,112 @@ class Header extends Component<IProps, IState> {
|
||||
|
||||
if (!oidc.getEnabled() || oidc.getLogoutUrl() === "") {
|
||||
SessionStore.logout(true, () => {
|
||||
this.props.history.push("/login");
|
||||
navigate("/login");
|
||||
});
|
||||
} else {
|
||||
SessionStore.logout(false, () => {
|
||||
window.location.assign(oidc.getLogoutUrl());
|
||||
navigate(oidc.getLogoutUrl());
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.settings === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (settings === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let oidcEnabled = this.state.settings!.getOpenidConnect()!.getEnabled();
|
||||
let oidcEnabled = settings!.getOpenidConnect()!.getEnabled();
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
{!oidcEnabled && (
|
||||
<Menu.Item>
|
||||
<Link to={`/users/${this.props.user.getId()}/password`}>Change password</Link>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item onClick={this.onLogout}>Logout</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
const menu = (
|
||||
<Menu>
|
||||
{!oidcEnabled && (
|
||||
<Menu.Item>
|
||||
<Link to={`/users/${user.getId()}/password`}>Change password</Link>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item onClick={onLogout}>Logout</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
let options: {
|
||||
label: any;
|
||||
options: any[];
|
||||
}[] = [
|
||||
{
|
||||
label: renderTitle("Tenants"),
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
label: renderTitle("Gateways"),
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
label: renderTitle("Applications"),
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
label: renderTitle("Devices"),
|
||||
options: [],
|
||||
},
|
||||
];
|
||||
let options: {
|
||||
label: any;
|
||||
options: any[];
|
||||
}[] = [
|
||||
{
|
||||
label: renderTitle("Tenants"),
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
label: renderTitle("Gateways"),
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
label: renderTitle("Applications"),
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
label: renderTitle("Devices"),
|
||||
options: [],
|
||||
},
|
||||
];
|
||||
|
||||
if (this.state.searchResult !== undefined) {
|
||||
for (const res of this.state.searchResult.getResultList()) {
|
||||
if (res.getKind() === "tenant") {
|
||||
options[0].options.push(renderItem(res.getTenantName(), `/tenants/${res.getTenantId()}`));
|
||||
}
|
||||
if (searchResult !== undefined) {
|
||||
for (const res of searchResult.getResultList()) {
|
||||
if (res.getKind() === "tenant") {
|
||||
options[0].options.push(renderItem(res.getTenantName(), `/tenants/${res.getTenantId()}`));
|
||||
}
|
||||
|
||||
if (res.getKind() === "gateway") {
|
||||
options[1].options.push(
|
||||
renderItem(res.getGatewayName(), `/tenants/${res.getTenantId()}/gateways/${res.getGatewayId()}`),
|
||||
);
|
||||
}
|
||||
if (res.getKind() === "gateway") {
|
||||
options[1].options.push(
|
||||
renderItem(res.getGatewayName(), `/tenants/${res.getTenantId()}/gateways/${res.getGatewayId()}`),
|
||||
);
|
||||
}
|
||||
|
||||
if (res.getKind() === "application") {
|
||||
options[2].options.push(
|
||||
renderItem(
|
||||
res.getApplicationName(),
|
||||
`/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (res.getKind() === "application") {
|
||||
options[2].options.push(
|
||||
renderItem(res.getApplicationName(), `/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}`),
|
||||
);
|
||||
}
|
||||
|
||||
if (res.getKind() === "device") {
|
||||
options[3].options.push(
|
||||
renderItem(
|
||||
res.getDeviceName(),
|
||||
`/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}/devices/${res.getDeviceDevEui()}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (res.getKind() === "device") {
|
||||
options[3].options.push(
|
||||
renderItem(
|
||||
res.getDeviceName(),
|
||||
`/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}/devices/${res.getDeviceDevEui()}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<img className="logo" alt="ChirpStack" src="/logo.png" />
|
||||
<div className="actions">
|
||||
<div className="search">
|
||||
<AutoComplete
|
||||
dropdownClassName="search-dropdown"
|
||||
dropdownMatchSelectWidth={500}
|
||||
options={options}
|
||||
onSearch={this.onSearch}
|
||||
>
|
||||
<Input.Search placeholder="Search..." style={{ width: 500, marginTop: -5 }} />
|
||||
</AutoComplete>
|
||||
</div>
|
||||
<div className="help">
|
||||
<a href="https://www.chirpstack.io" target="_blank" rel="noreferrer">
|
||||
<Button icon={<QuestionOutlined />} />
|
||||
</a>
|
||||
</div>
|
||||
<div className="user">
|
||||
<Dropdown overlay={menu} placement="bottomRight" trigger={["click"]}>
|
||||
<Button type="primary" icon={<UserOutlined />}>
|
||||
{this.props.user.getEmail()} <DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
return (
|
||||
<div>
|
||||
<img className="logo" alt="ChirpStack" src="/logo.png" />
|
||||
<div className="actions">
|
||||
<div className="search">
|
||||
<AutoComplete
|
||||
dropdownClassName="search-dropdown"
|
||||
dropdownMatchSelectWidth={500}
|
||||
options={options}
|
||||
onSearch={onSearch}
|
||||
>
|
||||
<Input.Search placeholder="Search..." style={{ width: 500, marginTop: -5 }} />
|
||||
</AutoComplete>
|
||||
</div>
|
||||
<div className="help">
|
||||
<a href="https://www.chirpstack.io" target="_blank" rel="noreferrer">
|
||||
<Button icon={<QuestionOutlined />} />
|
||||
</a>
|
||||
</div>
|
||||
<div className="user">
|
||||
<Dropdown overlay={menu} placement="bottomRight" trigger={["click"]}>
|
||||
<Button type="primary" icon={<UserOutlined />}>
|
||||
{user.getEmail()} <DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(Header);
|
||||
export default Header;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import moment from "moment";
|
||||
import JSONTreeOriginal from "react-json-tree";
|
||||
import fileDownload from "js-file-download";
|
||||
@ -12,160 +13,142 @@ interface IProps {
|
||||
logs: LogItem[];
|
||||
}
|
||||
|
||||
interface IState {
|
||||
drawerOpen: boolean;
|
||||
body: any;
|
||||
drawerTitle: any;
|
||||
}
|
||||
function LogTable(props: IProps) {
|
||||
const [drawerOpen, setDrawerOpen] = useState<boolean>(false);
|
||||
const [body, setBody] = useState<any>(null);
|
||||
const [drawerTitle, setDrawerTitle] = useState<any>(null);
|
||||
|
||||
class LogTable extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
drawerOpen: false,
|
||||
body: null,
|
||||
drawerTitle: null,
|
||||
};
|
||||
}
|
||||
|
||||
onDrawerClose = () => {
|
||||
this.setState({
|
||||
drawerOpen: false,
|
||||
});
|
||||
const onDrawerClose = () => {
|
||||
setDrawerOpen(false);
|
||||
};
|
||||
|
||||
onDrawerOpen = (time: any, body: any) => {
|
||||
const onDrawerOpen = (time: any, body: any) => {
|
||||
let ts = new Date(0);
|
||||
ts.setUTCSeconds(time.seconds);
|
||||
let drawerTitle = moment(ts).format("YYYY-MM-DD HH:mm:ss");
|
||||
|
||||
return () => {
|
||||
this.setState({
|
||||
body: body,
|
||||
drawerTitle: drawerTitle,
|
||||
drawerOpen: true,
|
||||
});
|
||||
setBody(body);
|
||||
setDrawerTitle(drawerTitle);
|
||||
setDrawerOpen(true);
|
||||
};
|
||||
};
|
||||
|
||||
downloadSingleFrame = () => {
|
||||
fileDownload(JSON.stringify(JSON.parse(this.state.body), null, 4), "single-log.json", "application/json");
|
||||
const downloadSingleFrame = () => {
|
||||
fileDownload(JSON.stringify(JSON.parse(body), null, 4), "single-log.json", "application/json");
|
||||
};
|
||||
|
||||
downloadFrames = () => {
|
||||
let items = this.props.logs.map((l, i) => JSON.parse(l.getBody()));
|
||||
const downloadFrames = () => {
|
||||
let items = props.logs.map((l, i) => JSON.parse(l.getBody()));
|
||||
fileDownload(JSON.stringify(items, null, 4), "log.json");
|
||||
};
|
||||
|
||||
render() {
|
||||
let items = this.props.logs.map((l, i) => l.toObject());
|
||||
let body = JSON.parse(this.state.body);
|
||||
let items = props.logs.map((l, i) => l.toObject());
|
||||
let bodyJson = JSON.parse(body);
|
||||
|
||||
const theme = {
|
||||
scheme: "google",
|
||||
author: "seth wright (http://sethawright.com)",
|
||||
base00: "#000000",
|
||||
base01: "#282a2e",
|
||||
base02: "#373b41",
|
||||
base03: "#969896",
|
||||
base04: "#b4b7b4",
|
||||
base05: "#c5c8c6",
|
||||
base06: "#e0e0e0",
|
||||
base07: "#ffffff",
|
||||
base08: "#CC342B",
|
||||
base09: "#F96A38",
|
||||
base0A: "#FBA922",
|
||||
base0B: "#198844",
|
||||
base0C: "#3971ED",
|
||||
base0D: "#3971ED",
|
||||
base0E: "#A36AC7",
|
||||
base0F: "#3971ED",
|
||||
};
|
||||
const theme = {
|
||||
scheme: "google",
|
||||
author: "seth wright (http://sethawright.com)",
|
||||
base00: "#000000",
|
||||
base01: "#282a2e",
|
||||
base02: "#373b41",
|
||||
base03: "#969896",
|
||||
base04: "#b4b7b4",
|
||||
base05: "#c5c8c6",
|
||||
base06: "#e0e0e0",
|
||||
base07: "#ffffff",
|
||||
base08: "#CC342B",
|
||||
base09: "#F96A38",
|
||||
base0A: "#FBA922",
|
||||
base0B: "#198844",
|
||||
base0C: "#3971ED",
|
||||
base0D: "#3971ED",
|
||||
base0E: "#A36AC7",
|
||||
base0F: "#3971ED",
|
||||
};
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Drawer
|
||||
title={`Details: ${this.state.drawerTitle}`}
|
||||
placement="right"
|
||||
width={650}
|
||||
onClose={this.onDrawerClose}
|
||||
visible={this.state.drawerOpen}
|
||||
extra={<Button onClick={this.downloadSingleFrame}>Download</Button>}
|
||||
>
|
||||
<JSONTreeOriginal
|
||||
data={body}
|
||||
theme={theme}
|
||||
hideRoot={true}
|
||||
shouldExpandNode={() => {
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
{items.length !== 0 && (
|
||||
<Space direction="horizontal" style={{ float: "right" }} size="large">
|
||||
<Spin size="small" />
|
||||
<Button onClick={this.downloadFrames}>Download</Button>
|
||||
</Space>
|
||||
)}
|
||||
<Table
|
||||
showHeader={false}
|
||||
loading={items.length === 0}
|
||||
dataSource={items}
|
||||
pagination={false}
|
||||
columns={[
|
||||
{
|
||||
title: "Time",
|
||||
dataIndex: "time",
|
||||
key: "time",
|
||||
width: 200,
|
||||
render: (text, obj) => {
|
||||
let ts = new Date(0);
|
||||
ts.setUTCSeconds(obj.time!.seconds);
|
||||
return moment(ts).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
width: 200,
|
||||
render: (text, obj) => (
|
||||
<Button
|
||||
icon={<ZoomInOutlined />}
|
||||
type="primary"
|
||||
shape="round"
|
||||
size="small"
|
||||
onClick={this.onDrawerOpen(obj.time, obj.body)}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Properties",
|
||||
dataIndex: "properties",
|
||||
key: "properties",
|
||||
render: (text, obj) =>
|
||||
obj.propertiesMap.map((p, i) => {
|
||||
if (p[1] !== "") {
|
||||
return (
|
||||
<Tag>
|
||||
<pre>
|
||||
{p[0]}: {p[1]}
|
||||
</pre>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
]}
|
||||
return (
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Drawer
|
||||
title={`Details: ${drawerTitle}`}
|
||||
placement="right"
|
||||
width={650}
|
||||
onClose={onDrawerClose}
|
||||
visible={drawerOpen}
|
||||
extra={<Button onClick={downloadSingleFrame}>Download</Button>}
|
||||
>
|
||||
<JSONTreeOriginal
|
||||
data={bodyJson}
|
||||
theme={theme}
|
||||
hideRoot={true}
|
||||
shouldExpandNode={() => {
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
</Drawer>
|
||||
{items.length !== 0 && (
|
||||
<Space direction="horizontal" style={{ float: "right" }} size="large">
|
||||
<Spin size="small" />
|
||||
<Button onClick={downloadFrames}>Download</Button>
|
||||
</Space>
|
||||
)}
|
||||
<Table
|
||||
showHeader={false}
|
||||
loading={items.length === 0}
|
||||
dataSource={items}
|
||||
pagination={false}
|
||||
columns={[
|
||||
{
|
||||
title: "Time",
|
||||
dataIndex: "time",
|
||||
key: "time",
|
||||
width: 200,
|
||||
render: (text, obj) => {
|
||||
let ts = new Date(0);
|
||||
ts.setUTCSeconds(obj.time!.seconds);
|
||||
return moment(ts).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
width: 200,
|
||||
render: (text, obj) => (
|
||||
<Button
|
||||
icon={<ZoomInOutlined />}
|
||||
type="primary"
|
||||
shape="round"
|
||||
size="small"
|
||||
onClick={onDrawerOpen(obj.time, obj.body)}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Properties",
|
||||
dataIndex: "properties",
|
||||
key: "properties",
|
||||
render: (text, obj) =>
|
||||
obj.propertiesMap.map((p, i) => {
|
||||
if (p[1] !== "") {
|
||||
return (
|
||||
<Tag>
|
||||
<pre>
|
||||
{p[0]}: {p[1]}
|
||||
</pre>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogTable;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useEffect, PropsWithChildren } from "react";
|
||||
|
||||
import L, { LatLngTuple, FitBoundsOptions } from "leaflet";
|
||||
import "leaflet.awesome-markers";
|
||||
import { MarkerProps as LMarkerProps } from "react-leaflet";
|
||||
import { MarkerProps as LMarkerProps, useMap } from "react-leaflet";
|
||||
import { MapContainer, Marker as LMarker, TileLayer } from "react-leaflet";
|
||||
|
||||
interface IProps {
|
||||
@ -12,77 +12,48 @@ interface IProps {
|
||||
boundsOptions?: FitBoundsOptions;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
map?: L.Map;
|
||||
}
|
||||
function MapControl(props: { center?: [number, number]; bounds?: LatLngTuple[]; boundsOptions?: FitBoundsOptions }) {
|
||||
const map = useMap();
|
||||
|
||||
class Map extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
setMap = (map: L.Map) => {
|
||||
this.setState(
|
||||
{
|
||||
map: map,
|
||||
},
|
||||
() => {
|
||||
// This is needed as setMap is called after the map has been created.
|
||||
// There is a small amount of time where componentDidUpdate can't update
|
||||
// the map with the new center because setMap hasn't been called yet.
|
||||
// In such case, the map would never update to the new center.
|
||||
if (this.props.center !== undefined) {
|
||||
map.panTo(this.props.center);
|
||||
}
|
||||
|
||||
if (this.props.bounds !== undefined) {
|
||||
map.fitBounds(this.props.bounds, this.props.boundsOptions);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
componentDidUpdate(oldProps: IProps) {
|
||||
if (this.props === oldProps) {
|
||||
useEffect(() => {
|
||||
if (map === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.map) {
|
||||
if (this.props.center !== undefined) {
|
||||
this.state.map.flyTo(this.props.center);
|
||||
}
|
||||
|
||||
if (this.props.bounds !== undefined) {
|
||||
this.state.map.flyToBounds(this.props.bounds, this.props.boundsOptions);
|
||||
}
|
||||
if (props.center !== undefined) {
|
||||
map.flyTo(props.center);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = {
|
||||
height: this.props.height,
|
||||
};
|
||||
if (props.bounds !== undefined) {
|
||||
map.flyToBounds(props.bounds, props.boundsOptions);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<MapContainer
|
||||
bounds={this.props.bounds}
|
||||
boundsOptions={this.props.boundsOptions}
|
||||
center={this.props.center}
|
||||
zoom={13}
|
||||
scrollWheelZoom={false}
|
||||
animate={true}
|
||||
style={style}
|
||||
whenCreated={this.setMap}
|
||||
>
|
||||
<TileLayer
|
||||
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
{this.props.children}
|
||||
</MapContainer>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function Map(props: PropsWithChildren<IProps>) {
|
||||
const style = {
|
||||
height: props.height,
|
||||
};
|
||||
|
||||
return (
|
||||
<MapContainer
|
||||
bounds={props.bounds}
|
||||
boundsOptions={props.boundsOptions}
|
||||
center={props.center}
|
||||
zoom={13}
|
||||
scrollWheelZoom={false}
|
||||
style={style}
|
||||
>
|
||||
<TileLayer
|
||||
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
{props.children}
|
||||
<MapControl bounds={props.bounds} boundsOptions={props.boundsOptions} center={props.center} />
|
||||
</MapContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export type MarkerColor =
|
||||
@ -103,22 +74,20 @@ interface MarkerProps extends LMarkerProps {
|
||||
color: MarkerColor;
|
||||
}
|
||||
|
||||
export class Marker extends Component<MarkerProps> {
|
||||
render() {
|
||||
const { faIcon, color, position, ...otherProps } = this.props;
|
||||
export function Marker(props: MarkerProps) {
|
||||
const { faIcon, color, position, ...otherProps } = props;
|
||||
|
||||
const iconMarker = L.AwesomeMarkers.icon({
|
||||
icon: faIcon,
|
||||
prefix: "fa",
|
||||
markerColor: color,
|
||||
});
|
||||
const iconMarker = L.AwesomeMarkers.icon({
|
||||
icon: faIcon,
|
||||
prefix: "fa",
|
||||
markerColor: color,
|
||||
});
|
||||
|
||||
return (
|
||||
<LMarker icon={iconMarker} position={position} {...otherProps}>
|
||||
{this.props.children}
|
||||
</LMarker>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<LMarker icon={iconMarker} position={position} {...otherProps}>
|
||||
{props.children}
|
||||
</LMarker>
|
||||
);
|
||||
}
|
||||
|
||||
export default Map;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { withRouter, RouteComponentProps, Link } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import { Menu, MenuProps } from "antd";
|
||||
import {
|
||||
@ -24,46 +24,18 @@ import Autocomplete, { OptionCallbackFunc, OptionsCallbackFunc } from "../compon
|
||||
import TenantStore from "../stores/TenantStore";
|
||||
import SessionStore from "../stores/SessionStore";
|
||||
|
||||
interface IState {
|
||||
tenantId: string;
|
||||
selectedKey: string;
|
||||
}
|
||||
function SideMenu() {
|
||||
const [tenantId, setTenantId] = useState<string>("");
|
||||
const [selectedKey, setSelectedKey] = useState<string>("");
|
||||
|
||||
class SideMenu extends Component<RouteComponentProps, IState> {
|
||||
constructor(props: RouteComponentProps) {
|
||||
super(props);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
this.state = {
|
||||
tenantId: "",
|
||||
selectedKey: "ns-dashboard",
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
SessionStore.on("tenant.change", this.setTenant);
|
||||
this.setTenant();
|
||||
this.parseLocation();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
SessionStore.removeListener("tenant.change", this.setTenant);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: RouteComponentProps) {
|
||||
if (this.props === prevProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.parseLocation();
|
||||
}
|
||||
|
||||
setTenant = () => {
|
||||
this.setState({
|
||||
tenantId: SessionStore.getTenantId(),
|
||||
});
|
||||
const setTenant = () => {
|
||||
setTenantId(SessionStore.getTenantId());
|
||||
};
|
||||
|
||||
getTenantOptions = (search: string, fn: OptionsCallbackFunc) => {
|
||||
const getTenantOptions = (search: string, fn: OptionsCallbackFunc) => {
|
||||
let req = new ListTenantsRequest();
|
||||
req.setSearch(search);
|
||||
req.setLimit(10);
|
||||
@ -76,7 +48,7 @@ class SideMenu extends Component<RouteComponentProps, IState> {
|
||||
});
|
||||
};
|
||||
|
||||
getTenantOption = (id: string, fn: OptionCallbackFunc) => {
|
||||
const getTenantOption = (id: string, fn: OptionCallbackFunc) => {
|
||||
TenantStore.get(id, (resp: GetTenantResponse) => {
|
||||
const tenant = resp.getTenant();
|
||||
if (tenant) {
|
||||
@ -85,167 +57,208 @@ class SideMenu extends Component<RouteComponentProps, IState> {
|
||||
});
|
||||
};
|
||||
|
||||
onTenantSelect = (value: string) => {
|
||||
const onTenantSelect = (value: string) => {
|
||||
SessionStore.setTenantId(value);
|
||||
this.props.history.push(`/tenants/${value}`);
|
||||
navigate(`/tenants/${value}`);
|
||||
};
|
||||
|
||||
parseLocation = () => {
|
||||
const path = this.props.history.location.pathname;
|
||||
const parseLocation = () => {
|
||||
const path = location.pathname;
|
||||
const tenantRe = /\/tenants\/([\w-]{36})/g;
|
||||
const match = tenantRe.exec(path);
|
||||
|
||||
if (match !== null && this.state.tenantId !== match[1]) {
|
||||
if (match !== null && tenantId !== match[1]) {
|
||||
SessionStore.setTenantId(match[1]);
|
||||
}
|
||||
|
||||
// ns dashboard
|
||||
if (path === "/dashboard") {
|
||||
this.setState({ selectedKey: "ns-dashboard" });
|
||||
setSelectedKey("ns-dashboard");
|
||||
}
|
||||
|
||||
// ns tenants
|
||||
if (/\/tenants(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
|
||||
this.setState({ selectedKey: "ns-tenants" });
|
||||
setSelectedKey("ns-tenants");
|
||||
}
|
||||
|
||||
// ns tenants
|
||||
if (/\/users(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
|
||||
this.setState({ selectedKey: "ns-users" });
|
||||
setSelectedKey("ns-users");
|
||||
}
|
||||
|
||||
// ns api keys
|
||||
if (/\/api-keys(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
|
||||
this.setState({ selectedKey: "ns-api-keys" });
|
||||
setSelectedKey("ns-api-keys");
|
||||
}
|
||||
|
||||
// ns device-profile templates
|
||||
if (/\/device-profile-templates(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
|
||||
this.setState({ selectedKey: "ns-device-profile-templates" });
|
||||
setSelectedKey("ns-device-profile-templates");
|
||||
}
|
||||
|
||||
if (/\/regions.*/g.exec(path)) {
|
||||
this.setState({ selectedKey: "ns-regions" });
|
||||
setSelectedKey("ns-regions");
|
||||
}
|
||||
|
||||
// tenant dashboard
|
||||
if (/\/tenants\/[\w-]{36}/g.exec(path)) {
|
||||
this.setState({ selectedKey: "tenant-dashboard" });
|
||||
setSelectedKey("tenant-dashboard");
|
||||
}
|
||||
|
||||
// tenant users
|
||||
if (/\/tenants\/[\w-]{36}\/users.*/g.exec(path)) {
|
||||
this.setState({ selectedKey: "tenant-users" });
|
||||
setSelectedKey("tenant-users");
|
||||
}
|
||||
|
||||
// tenant api-keys
|
||||
if (/\/tenants\/[\w-]{36}\/api-keys.*/g.exec(path)) {
|
||||
this.setState({ selectedKey: "tenant-api-keys" });
|
||||
setSelectedKey("tenant-api-keys");
|
||||
}
|
||||
|
||||
// tenant device-profiles
|
||||
if (/\/tenants\/[\w-]{36}\/device-profiles.*/g.exec(path)) {
|
||||
this.setState({ selectedKey: "tenant-device-profiles" });
|
||||
setSelectedKey("tenant-device-profiles");
|
||||
}
|
||||
|
||||
// tenant gateways
|
||||
if (/\/tenants\/[\w-]{36}\/gateways.*/g.exec(path)) {
|
||||
this.setState({ selectedKey: "tenant-gateways" });
|
||||
setSelectedKey("tenant-gateways");
|
||||
}
|
||||
|
||||
// tenant applications
|
||||
if (/\/tenants\/[\w-]{36}\/applications.*/g.exec(path)) {
|
||||
this.setState({ selectedKey: "tenant-applications" });
|
||||
setSelectedKey("tenant-applications");
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const tenantId = this.state.tenantId;
|
||||
let items: MenuProps["items"] = [];
|
||||
useEffect(() => {
|
||||
SessionStore.on("tenant.change", setTenant);
|
||||
setTenant();
|
||||
parseLocation();
|
||||
|
||||
if (SessionStore.isAdmin()) {
|
||||
items.push({
|
||||
key: "ns",
|
||||
label: "Network Server",
|
||||
icon: <CloudOutlined />,
|
||||
children: [
|
||||
{ key: "ns-dashboard", icon: <DashboardOutlined />, label: <Link to="/dashboard">Dashboard</Link> },
|
||||
{ key: "ns-tenants", icon: <HomeOutlined />, label: <Link to="/tenants">Tenants</Link> },
|
||||
{ key: "ns-users", icon: <UserOutlined />, label: <Link to="/users">Users</Link> },
|
||||
{ key: "ns-api-keys", icon: <KeyOutlined />, label: <Link to="/api-keys">API Keys</Link> },
|
||||
{
|
||||
key: "ns-device-profile-templates",
|
||||
icon: <ControlOutlined />,
|
||||
label: <Link to="/device-profile-templates">Device Profile Templates</Link>,
|
||||
},
|
||||
{ key: "ns-regions", icon: <CompassOutlined />, label: <Link to="/regions">Regions</Link> },
|
||||
],
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
key: "ns",
|
||||
label: "Network Server",
|
||||
icon: <CloudOutlined />,
|
||||
children: [{ key: "ns-regions", icon: <CompassOutlined />, label: <Link to="/regions">Regions</Link> }],
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
SessionStore.removeListener("tenant.change", setTenant);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (tenantId !== "") {
|
||||
items.push({
|
||||
key: "tenant",
|
||||
label: "Tenant",
|
||||
icon: <HomeOutlined />,
|
||||
children: [
|
||||
{
|
||||
key: "tenant-dashboard",
|
||||
icon: <DashboardOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}`}>Dashboard</Link>,
|
||||
},
|
||||
{ key: "tenant-users", icon: <UserOutlined />, label: <Link to={`/tenants/${tenantId}/users`}>Users</Link> },
|
||||
{
|
||||
key: "tenant-api-keys",
|
||||
icon: <KeyOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}/api-keys`}>API Keys</Link>,
|
||||
},
|
||||
{
|
||||
key: "tenant-device-profiles",
|
||||
icon: <ControlOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}/device-profiles`}>Device Profiles</Link>,
|
||||
},
|
||||
{
|
||||
key: "tenant-gateways",
|
||||
icon: <WifiOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}/gateways`}>Gateways</Link>,
|
||||
},
|
||||
{
|
||||
key: "tenant-applications",
|
||||
icon: <AppstoreOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}/applications`}>Applications</Link>,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
useEffect(() => {
|
||||
parseLocation();
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Autocomplete
|
||||
placeholder="Select tenant"
|
||||
className="organiation-select"
|
||||
getOption={this.getTenantOption}
|
||||
getOptions={this.getTenantOptions}
|
||||
onSelect={this.onTenantSelect}
|
||||
value={this.state.tenantId}
|
||||
/>
|
||||
<Menu
|
||||
mode="inline"
|
||||
openKeys={["ns", "tenant"]}
|
||||
selectedKeys={[this.state.selectedKey]}
|
||||
expandIcon={<div></div>}
|
||||
items={items}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
let items: MenuProps["items"] = [];
|
||||
|
||||
if (SessionStore.isAdmin()) {
|
||||
items.push({
|
||||
key: "ns",
|
||||
label: "Network Server",
|
||||
icon: <CloudOutlined />,
|
||||
children: [
|
||||
{
|
||||
key: "ns-dashboard",
|
||||
icon: <DashboardOutlined />,
|
||||
label: <Link to="/dashboard">Dashboard</Link>,
|
||||
},
|
||||
{
|
||||
key: "ns-tenants",
|
||||
icon: <HomeOutlined />,
|
||||
label: <Link to="/tenants">Tenants</Link>,
|
||||
},
|
||||
{
|
||||
key: "ns-users",
|
||||
icon: <UserOutlined />,
|
||||
label: <Link to="/users">Users</Link>,
|
||||
},
|
||||
{
|
||||
key: "ns-api-keys",
|
||||
icon: <KeyOutlined />,
|
||||
label: <Link to="/api-keys">API Keys</Link>,
|
||||
},
|
||||
{
|
||||
key: "ns-device-profile-templates",
|
||||
icon: <ControlOutlined />,
|
||||
label: <Link to="/device-profile-templates">Device Profile Templates</Link>,
|
||||
},
|
||||
{
|
||||
key: "ns-regions",
|
||||
icon: <CompassOutlined />,
|
||||
label: <Link to="/regions">Regions</Link>,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
key: "ns",
|
||||
label: "Network Server",
|
||||
icon: <CloudOutlined />,
|
||||
children: [
|
||||
{
|
||||
key: "ns-regions",
|
||||
icon: <CompassOutlined />,
|
||||
label: <Link to="/regions">Regions</Link>,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (tenantId !== "") {
|
||||
items.push({
|
||||
key: "tenant",
|
||||
label: "Tenant",
|
||||
icon: <HomeOutlined />,
|
||||
children: [
|
||||
{
|
||||
key: "tenant-dashboard",
|
||||
icon: <DashboardOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}`}>Dashboard</Link>,
|
||||
},
|
||||
{
|
||||
key: "tenant-users",
|
||||
icon: <UserOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}/users`}>Users</Link>,
|
||||
},
|
||||
{
|
||||
key: "tenant-api-keys",
|
||||
icon: <KeyOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}/api-keys`}>API Keys</Link>,
|
||||
},
|
||||
{
|
||||
key: "tenant-device-profiles",
|
||||
icon: <ControlOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}/device-profiles`}>Device Profiles</Link>,
|
||||
},
|
||||
{
|
||||
key: "tenant-gateways",
|
||||
icon: <WifiOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}/gateways`}>Gateways</Link>,
|
||||
},
|
||||
{
|
||||
key: "tenant-applications",
|
||||
icon: <AppstoreOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}/applications`}>Applications</Link>,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Autocomplete
|
||||
placeholder="Select tenant"
|
||||
className="organiation-select"
|
||||
getOption={getTenantOption}
|
||||
getOptions={getTenantOptions}
|
||||
onSelect={onTenantSelect}
|
||||
value={tenantId}
|
||||
/>
|
||||
<Menu
|
||||
mode="inline"
|
||||
openKeys={["ns", "tenant"]}
|
||||
selectedKeys={[selectedKey]}
|
||||
expandIcon={<div></div>}
|
||||
items={items}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(SideMenu);
|
||||
export default SideMenu;
|
||||
|
@ -1,5 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
import { TimeUnit } from "chart.js";
|
||||
@ -13,82 +11,80 @@ interface IProps {
|
||||
aggregation: Aggregation;
|
||||
}
|
||||
|
||||
class MetricBar extends Component<IProps> {
|
||||
render() {
|
||||
let unit: TimeUnit = "hour";
|
||||
if (this.props.aggregation === Aggregation.DAY) {
|
||||
unit = "day";
|
||||
} else if (this.props.aggregation === Aggregation.MONTH) {
|
||||
unit = "month";
|
||||
}
|
||||
|
||||
let backgroundColors = [
|
||||
"#8bc34a",
|
||||
"#ff5722",
|
||||
"#ff9800",
|
||||
"#ffc107",
|
||||
"#ffeb3b",
|
||||
"#cddc39",
|
||||
"#4caf50",
|
||||
"#009688",
|
||||
"#00bcd4",
|
||||
"#03a9f4",
|
||||
"#2196f3",
|
||||
"#3f51b5",
|
||||
"#673ab7",
|
||||
"#9c27b0",
|
||||
"#e91e63",
|
||||
];
|
||||
|
||||
const animation: false = false;
|
||||
|
||||
const options = {
|
||||
animation: animation,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
},
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
x: {
|
||||
type: "time" as const,
|
||||
time: {
|
||||
unit: unit,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let data: {
|
||||
labels: number[];
|
||||
datasets: {
|
||||
label: string;
|
||||
data: number[];
|
||||
backgroundColor: string;
|
||||
}[];
|
||||
} = {
|
||||
labels: this.props.metric.getTimestampsList().map(v => moment(v.toDate()).valueOf()),
|
||||
datasets: [],
|
||||
};
|
||||
|
||||
for (let ds of this.props.metric.getDatasetsList()) {
|
||||
data.datasets.push({
|
||||
label: ds.getLabel(),
|
||||
data: ds.getDataList(),
|
||||
backgroundColor: backgroundColors.shift()!,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={this.props.metric.getName()} className="dashboard-chart">
|
||||
<Bar data={data} options={options} />
|
||||
</Card>
|
||||
);
|
||||
function MetricBar(props: IProps) {
|
||||
let unit: TimeUnit = "hour";
|
||||
if (props.aggregation === Aggregation.DAY) {
|
||||
unit = "day";
|
||||
} else if (props.aggregation === Aggregation.MONTH) {
|
||||
unit = "month";
|
||||
}
|
||||
|
||||
let backgroundColors = [
|
||||
"#8bc34a",
|
||||
"#ff5722",
|
||||
"#ff9800",
|
||||
"#ffc107",
|
||||
"#ffeb3b",
|
||||
"#cddc39",
|
||||
"#4caf50",
|
||||
"#009688",
|
||||
"#00bcd4",
|
||||
"#03a9f4",
|
||||
"#2196f3",
|
||||
"#3f51b5",
|
||||
"#673ab7",
|
||||
"#9c27b0",
|
||||
"#e91e63",
|
||||
];
|
||||
|
||||
const animation: false = false;
|
||||
|
||||
const options = {
|
||||
animation: animation,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
},
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
x: {
|
||||
type: "time" as const,
|
||||
time: {
|
||||
unit: unit,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let data: {
|
||||
labels: number[];
|
||||
datasets: {
|
||||
label: string;
|
||||
data: number[];
|
||||
backgroundColor: string;
|
||||
}[];
|
||||
} = {
|
||||
labels: props.metric.getTimestampsList().map(v => moment(v.toDate()).valueOf()),
|
||||
datasets: [],
|
||||
};
|
||||
|
||||
for (let ds of props.metric.getDatasetsList()) {
|
||||
data.datasets.push({
|
||||
label: ds.getLabel(),
|
||||
data: ds.getDataList(),
|
||||
backgroundColor: backgroundColors.shift()!,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={props.metric.getName()} className="dashboard-chart">
|
||||
<Bar data={data} options={options} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricBar;
|
||||
|
@ -1,5 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
import { TimeUnit } from "chart.js";
|
||||
@ -14,83 +12,81 @@ interface IProps {
|
||||
zeroToNull?: boolean;
|
||||
}
|
||||
|
||||
class MetricChart extends Component<IProps> {
|
||||
render() {
|
||||
let unit: TimeUnit = "hour";
|
||||
let tooltipFormat = "LT";
|
||||
if (this.props.aggregation === Aggregation.DAY) {
|
||||
unit = "day";
|
||||
tooltipFormat = "MMM Do";
|
||||
} else if (this.props.aggregation === Aggregation.MONTH) {
|
||||
unit = "month";
|
||||
tooltipFormat = "MMM YYYY";
|
||||
}
|
||||
|
||||
const animation: false = false;
|
||||
|
||||
const options = {
|
||||
animation: animation,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
x: {
|
||||
type: "time" as const,
|
||||
time: {
|
||||
unit: unit,
|
||||
tooltipFormat: tooltipFormat,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let prevValue = 0;
|
||||
let data = {
|
||||
labels: this.props.metric.getTimestampsList().map(v => moment(v.toDate()).valueOf()),
|
||||
datasets: this.props.metric.getDatasetsList().map(v => {
|
||||
return {
|
||||
label: v.getLabel(),
|
||||
borderColor: "rgba(33, 150, 243, 1)",
|
||||
backgroundColor: "rgba(0, 0, 0, 0)",
|
||||
lineTension: 0,
|
||||
pointBackgroundColor: "rgba(33, 150, 243, 1)",
|
||||
data: v.getDataList().map(v => {
|
||||
if (v === 0 && this.props.zeroToNull) {
|
||||
return null;
|
||||
} else {
|
||||
if (this.props.metric.getKind() === MetricKind.COUNTER) {
|
||||
let val = v - prevValue;
|
||||
prevValue = v;
|
||||
if (val < 0) {
|
||||
return 0;
|
||||
}
|
||||
return val;
|
||||
} else {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
let name = this.props.metric.getName();
|
||||
if (this.props.metric.getKind() === MetricKind.COUNTER) {
|
||||
name = `${name} (per ${unit})`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={name} className="dashboard-chart">
|
||||
<Line height={75} options={options} data={data} />
|
||||
</Card>
|
||||
);
|
||||
function MetricChart(props: IProps) {
|
||||
let unit: TimeUnit = "hour";
|
||||
let tooltipFormat = "LT";
|
||||
if (props.aggregation === Aggregation.DAY) {
|
||||
unit = "day";
|
||||
tooltipFormat = "MMM Do";
|
||||
} else if (props.aggregation === Aggregation.MONTH) {
|
||||
unit = "month";
|
||||
tooltipFormat = "MMM YYYY";
|
||||
}
|
||||
|
||||
const animation: false = false;
|
||||
|
||||
const options = {
|
||||
animation: animation,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
x: {
|
||||
type: "time" as const,
|
||||
time: {
|
||||
unit: unit,
|
||||
tooltipFormat: tooltipFormat,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let prevValue = 0;
|
||||
let data = {
|
||||
labels: props.metric.getTimestampsList().map(v => moment(v.toDate()).valueOf()),
|
||||
datasets: props.metric.getDatasetsList().map(v => {
|
||||
return {
|
||||
label: v.getLabel(),
|
||||
borderColor: "rgba(33, 150, 243, 1)",
|
||||
backgroundColor: "rgba(0, 0, 0, 0)",
|
||||
lineTension: 0,
|
||||
pointBackgroundColor: "rgba(33, 150, 243, 1)",
|
||||
data: v.getDataList().map(v => {
|
||||
if (v === 0 && props.zeroToNull) {
|
||||
return null;
|
||||
} else {
|
||||
if (props.metric.getKind() === MetricKind.COUNTER) {
|
||||
let val = v - prevValue;
|
||||
prevValue = v;
|
||||
if (val < 0) {
|
||||
return 0;
|
||||
}
|
||||
return val;
|
||||
} else {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
let name = props.metric.getName();
|
||||
if (props.metric.getKind() === MetricKind.COUNTER) {
|
||||
name = `${name} (per ${unit})`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={name} className="dashboard-chart">
|
||||
<Line height={75} options={options} data={data} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricChart;
|
||||
|
@ -1,5 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
import { color } from "chart.js/helpers";
|
||||
@ -16,138 +14,136 @@ interface IProps {
|
||||
aggregation: Aggregation;
|
||||
}
|
||||
|
||||
class MetricHeatmap extends Component<IProps> {
|
||||
render() {
|
||||
let unit: TimeUnit = "hour";
|
||||
if (this.props.aggregation === Aggregation.DAY) {
|
||||
unit = "day";
|
||||
} else if (this.props.aggregation === Aggregation.MONTH) {
|
||||
unit = "month";
|
||||
}
|
||||
function MetricHeatmap(props: IProps) {
|
||||
let unit: TimeUnit = "hour";
|
||||
if (props.aggregation === Aggregation.DAY) {
|
||||
unit = "day";
|
||||
} else if (props.aggregation === Aggregation.MONTH) {
|
||||
unit = "month";
|
||||
}
|
||||
|
||||
const animation: false = false;
|
||||
const animation: false = false;
|
||||
|
||||
let options = {
|
||||
animation: animation,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
type: "category" as const,
|
||||
offset: true,
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
let options = {
|
||||
animation: animation,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
type: "category" as const,
|
||||
offset: true,
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
x: {
|
||||
type: "time" as const,
|
||||
time: {
|
||||
unit: unit,
|
||||
},
|
||||
x: {
|
||||
type: "time" as const,
|
||||
time: {
|
||||
unit: unit,
|
||||
},
|
||||
offset: true,
|
||||
labels: props.metric.getTimestampsList().map(v => moment(v.toDate().valueOf())),
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title: () => {
|
||||
return "";
|
||||
},
|
||||
offset: true,
|
||||
labels: this.props.metric.getTimestampsList().map(v => moment(v.toDate().valueOf())),
|
||||
grid: {
|
||||
display: false,
|
||||
label: (ctx: any) => {
|
||||
const v = ctx.dataset.data[ctx.dataIndex].v;
|
||||
return "Count: " + v;
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title: () => {
|
||||
return "";
|
||||
},
|
||||
label: (ctx: any) => {
|
||||
const v = ctx.dataset.data[ctx.dataIndex].v;
|
||||
return "Count: " + v;
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let dataData: {
|
||||
x: number;
|
||||
y: string;
|
||||
v: number;
|
||||
}[] = [];
|
||||
|
||||
let data = {
|
||||
labels: props.metric.getDatasetsList().map(v => v.getLabel()),
|
||||
datasets: [
|
||||
{
|
||||
label: "Heatmap",
|
||||
data: dataData,
|
||||
minValue: -1,
|
||||
maxValue: -1,
|
||||
fromColor: props.fromColor.match(/\d+/g)!.map(Number),
|
||||
toColor: props.toColor.match(/\d+/g)!.map(Number),
|
||||
backgroundColor: (ctx: any): string => {
|
||||
if (
|
||||
ctx.dataset === undefined ||
|
||||
ctx.dataset.data === undefined ||
|
||||
ctx.dataset.data[ctx.dataIndex] === undefined
|
||||
) {
|
||||
return color("white").rgbString();
|
||||
}
|
||||
|
||||
const value = ctx.dataset.data[ctx.dataIndex].v;
|
||||
const steps = ctx.dataset.maxValue - ctx.dataset.minValue + 1;
|
||||
const step = value - ctx.dataset.minValue;
|
||||
const factor = (1 / steps) * step;
|
||||
|
||||
let result: [number, number, number] = ctx.dataset.fromColor.slice();
|
||||
for (var i = 0; i < 3; i++) {
|
||||
result[i] = Math.round(result[i] + factor * (ctx.dataset.toColor[i] - ctx.dataset.fromColor[i]));
|
||||
}
|
||||
|
||||
return color(result).rgbString();
|
||||
},
|
||||
borderWidth: 0,
|
||||
width: (ctx: any) => {
|
||||
return (ctx.chart.chartArea || {}).width / props.metric.getTimestampsList().length - 1;
|
||||
},
|
||||
height: (ctx: any) => {
|
||||
return (ctx.chart.chartArea || {}).height / props.metric.getDatasetsList().length - 1;
|
||||
},
|
||||
},
|
||||
};
|
||||
],
|
||||
};
|
||||
|
||||
let dataData: {
|
||||
x: number;
|
||||
y: string;
|
||||
v: number;
|
||||
}[] = [];
|
||||
data.labels.sort();
|
||||
|
||||
let data = {
|
||||
labels: this.props.metric.getDatasetsList().map(v => v.getLabel()),
|
||||
datasets: [
|
||||
{
|
||||
label: "Heatmap",
|
||||
data: dataData,
|
||||
minValue: -1,
|
||||
maxValue: -1,
|
||||
fromColor: this.props.fromColor.match(/\d+/g)!.map(Number),
|
||||
toColor: this.props.toColor.match(/\d+/g)!.map(Number),
|
||||
backgroundColor: (ctx: any): string => {
|
||||
if (
|
||||
ctx.dataset === undefined ||
|
||||
ctx.dataset.data === undefined ||
|
||||
ctx.dataset.data[ctx.dataIndex] === undefined
|
||||
) {
|
||||
return color("white").rgbString();
|
||||
}
|
||||
const tsList = props.metric.getTimestampsList();
|
||||
const dsList = props.metric.getDatasetsList();
|
||||
|
||||
const value = ctx.dataset.data[ctx.dataIndex].v;
|
||||
const steps = ctx.dataset.maxValue - ctx.dataset.minValue + 1;
|
||||
const step = value - ctx.dataset.minValue;
|
||||
const factor = (1 / steps) * step;
|
||||
for (let i = 0; i < tsList.length; i++) {
|
||||
for (let ds of dsList) {
|
||||
let v = ds.getDataList()[i];
|
||||
if (v === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let result: [number, number, number] = ctx.dataset.fromColor.slice();
|
||||
for (var i = 0; i < 3; i++) {
|
||||
result[i] = Math.round(result[i] + factor * (ctx.dataset.toColor[i] - ctx.dataset.fromColor[i]));
|
||||
}
|
||||
data.datasets[0].data.push({
|
||||
x: moment(tsList[i].toDate()).valueOf(),
|
||||
y: ds.getLabel(),
|
||||
v: v,
|
||||
});
|
||||
|
||||
return color(result).rgbString();
|
||||
},
|
||||
borderWidth: 0,
|
||||
width: (ctx: any) => {
|
||||
return (ctx.chart.chartArea || {}).width / this.props.metric.getTimestampsList().length - 1;
|
||||
},
|
||||
height: (ctx: any) => {
|
||||
return (ctx.chart.chartArea || {}).height / this.props.metric.getDatasetsList().length - 1;
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
if (data.datasets[0].minValue === -1 || data.datasets[0].minValue > v) {
|
||||
data.datasets[0].minValue = v;
|
||||
}
|
||||
|
||||
data.labels.sort();
|
||||
|
||||
const tsList = this.props.metric.getTimestampsList();
|
||||
const dsList = this.props.metric.getDatasetsList();
|
||||
|
||||
for (let i = 0; i < tsList.length; i++) {
|
||||
for (let ds of dsList) {
|
||||
let v = ds.getDataList()[i];
|
||||
if (v === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
data.datasets[0].data.push({
|
||||
x: moment(tsList[i].toDate()).valueOf(),
|
||||
y: ds.getLabel(),
|
||||
v: v,
|
||||
});
|
||||
|
||||
if (data.datasets[0].minValue === -1 || data.datasets[0].minValue > v) {
|
||||
data.datasets[0].minValue = v;
|
||||
}
|
||||
|
||||
if (data.datasets[0].maxValue < v) {
|
||||
data.datasets[0].maxValue = v;
|
||||
}
|
||||
if (data.datasets[0].maxValue < v) {
|
||||
data.datasets[0].maxValue = v;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={this.props.metric.getName()} className="dashboard-chart">
|
||||
<Chart type="matrix" data={data} options={options} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={props.metric.getName()} className="dashboard-chart">
|
||||
<Chart type="matrix" data={data} options={options} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricHeatmap;
|
||||
|
@ -31,7 +31,7 @@
|
||||
padding-top: 85px;
|
||||
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
position: fixed !important;
|
||||
height: 100vh;
|
||||
left: 0;
|
||||
}
|
||||
@ -117,5 +117,10 @@ pre {
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding-bottom: 88px; /* 64 + 24 */
|
||||
padding-bottom: 88px;
|
||||
/* 64 + 24 */
|
||||
}
|
||||
|
||||
.input-code input {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import ReactDOM from "react-dom/client";
|
||||
|
||||
// import { Chart, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, Title } from "chart.js";
|
||||
import { Chart, registerables } from "chart.js";
|
||||
import { MatrixElement, MatrixController } from "chartjs-chart-matrix";
|
||||
import "chartjs-adapter-moment";
|
||||
@ -9,7 +8,7 @@ import "chartjs-adapter-moment";
|
||||
import App from "./App";
|
||||
import reportWebVitals from "./reportWebVitals";
|
||||
|
||||
import "antd/dist/antd.min.css";
|
||||
import "antd/dist/reset.css";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||
import "@fortawesome/fontawesome-free/css/all.css";
|
||||
@ -19,12 +18,8 @@ import "./index.css";
|
||||
|
||||
Chart.register(MatrixController, MatrixElement, ...registerables);
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root"),
|
||||
);
|
||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
|
||||
root.render(<App />);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
|
@ -111,6 +111,8 @@ class ApplicationStore extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit("change");
|
||||
|
||||
notification.success({
|
||||
message: "Application updated",
|
||||
duration: 3,
|
||||
|
@ -31,7 +31,7 @@ class RelayStore extends EventEmitter {
|
||||
|
||||
callbackFunc(resp);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
addDevice = (req: AddRelayDeviceRequest, callbackFunc: () => void) => {
|
||||
this.client.addDevice(req, SessionStore.getMetadata(), err => {
|
||||
@ -47,7 +47,7 @@ class RelayStore extends EventEmitter {
|
||||
|
||||
callbackFunc();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
removeDevice = (req: RemoveRelayDeviceRequest, callbackFunc: () => void) => {
|
||||
this.client.removeDevice(req, SessionStore.getMetadata(), err => {
|
||||
@ -63,7 +63,7 @@ class RelayStore extends EventEmitter {
|
||||
|
||||
callbackFunc();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
listDevices = (req: ListRelayDevicesRequest, callbackFunc: (resp: ListRelayDevicesResponse) => void) => {
|
||||
this.client.listDevices(req, SessionStore.getMetadata(), (err, resp) => {
|
||||
@ -74,7 +74,7 @@ class RelayStore extends EventEmitter {
|
||||
|
||||
callbackFunc(resp);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const relayStore = new RelayStore();
|
||||
|
@ -1,5 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Form, Input, Button } from "antd";
|
||||
|
||||
import { ApiKey } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
|
||||
@ -9,29 +7,25 @@ interface IProps {
|
||||
onFinish: (obj: ApiKey) => void;
|
||||
}
|
||||
|
||||
interface IState {}
|
||||
|
||||
class ApiKeyForm extends Component<IProps, IState> {
|
||||
onFinish = (values: ApiKey.AsObject) => {
|
||||
function ApiKeyForm(props: IProps) {
|
||||
const onFinish = (values: ApiKey.AsObject) => {
|
||||
let apiKey = new ApiKey();
|
||||
apiKey.setName(values.name);
|
||||
this.props.onFinish(apiKey);
|
||||
props.onFinish(apiKey);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
||||
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApiKeyForm;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Input, Typography, Button, Space } from "antd";
|
||||
@ -9,23 +8,21 @@ interface IProps {
|
||||
createApiKeyResponse: CreateApiKeyResponse;
|
||||
}
|
||||
|
||||
class ApiKeyToken extends Component<IProps> {
|
||||
render() {
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Typography>
|
||||
<Typography.Paragraph>
|
||||
Use the following API token when making API requests. This token can be revoked at any time by deleting it.
|
||||
Please note that this token can only be retrieved once:
|
||||
</Typography.Paragraph>
|
||||
</Typography>
|
||||
<Input.TextArea rows={4} value={this.props.createApiKeyResponse.getToken()} />
|
||||
<Button type="primary">
|
||||
<Link to="../api-keys">Back</Link>
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
function ApiKeyToken(props: IProps) {
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Typography>
|
||||
<Typography.Paragraph>
|
||||
Use the following API token when making API requests. This token can be revoked at any time by deleting it.
|
||||
Please note that this token can only be retrieved once:
|
||||
</Typography.Paragraph>
|
||||
</Typography>
|
||||
<Input.TextArea rows={4} value={props.createApiKeyResponse.getToken()} />
|
||||
<Button type="primary">
|
||||
<Link to="../api-keys">Back</Link>
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApiKeyToken;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Space, Breadcrumb, Card, PageHeader } from "antd";
|
||||
import { Space, Breadcrumb, Card } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import { ApiKey, CreateApiKeyRequest, CreateApiKeyResponse } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
|
||||
|
||||
@ -9,61 +10,48 @@ import ApiKeyForm from "./ApiKeyForm";
|
||||
import ApiKeyToken from "./ApiKeyToken";
|
||||
import InternalStore from "../../stores/InternalStore";
|
||||
|
||||
interface IProps {}
|
||||
function CreateAdminApiKey() {
|
||||
const [createApiKeyResponse, setCreateApiKeyResponse] = useState<CreateApiKeyResponse | undefined>(undefined);
|
||||
|
||||
interface IState {
|
||||
createApiKeyResponse?: CreateApiKeyResponse;
|
||||
}
|
||||
|
||||
class CreateAdminApiKey extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
onFinish = (obj: ApiKey) => {
|
||||
const onFinish = (obj: ApiKey) => {
|
||||
obj.setIsAdmin(true);
|
||||
|
||||
let req = new CreateApiKeyRequest();
|
||||
req.setApiKey(obj);
|
||||
|
||||
InternalStore.createApiKey(req, (resp: CreateApiKeyResponse) => {
|
||||
this.setState({
|
||||
createApiKeyResponse: resp,
|
||||
});
|
||||
setCreateApiKeyResponse(resp);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const apiKey = new ApiKey();
|
||||
const apiKey = new ApiKey();
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
title="Add API key"
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Network-server</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to="/api-keys">API keys</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Add</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
/>
|
||||
<Card>
|
||||
{!this.state.createApiKeyResponse && <ApiKeyForm initialValues={apiKey} onFinish={this.onFinish} />}
|
||||
{this.state.createApiKeyResponse && <ApiKeyToken createApiKeyResponse={this.state.createApiKeyResponse} />}
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
title="Add API key"
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Network-server</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to="/api-keys">API keys</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Add</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
/>
|
||||
<Card>
|
||||
{!createApiKeyResponse && <ApiKeyForm initialValues={apiKey} onFinish={onFinish} />}
|
||||
{createApiKeyResponse && <ApiKeyToken createApiKeyResponse={createApiKeyResponse} />}
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateAdminApiKey;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Space, Breadcrumb, Card, PageHeader } from "antd";
|
||||
import { Space, Breadcrumb, Card } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import { ApiKey, CreateApiKeyRequest, CreateApiKeyResponse } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
|
||||
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
||||
@ -10,68 +11,57 @@ import ApiKeyForm from "./ApiKeyForm";
|
||||
import ApiKeyToken from "./ApiKeyToken";
|
||||
import InternalStore from "../../stores/InternalStore";
|
||||
|
||||
interface IState {
|
||||
createApiKeyResponse?: CreateApiKeyResponse;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
tenant: Tenant;
|
||||
}
|
||||
|
||||
class CreateTenantApiKey extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
function CreateTenantApiKey(props: IProps) {
|
||||
const [createApiKeyResponse, setCreateApiKeyResponse] = useState<CreateApiKeyResponse | undefined>(undefined);
|
||||
|
||||
onFinish = (obj: ApiKey) => {
|
||||
obj.setTenantId(this.props.tenant.getId());
|
||||
const onFinish = (obj: ApiKey) => {
|
||||
obj.setTenantId(props.tenant.getId());
|
||||
|
||||
let req = new CreateApiKeyRequest();
|
||||
req.setApiKey(obj);
|
||||
|
||||
InternalStore.createApiKey(req, (resp: CreateApiKeyResponse) => {
|
||||
this.setState({
|
||||
createApiKeyResponse: resp,
|
||||
});
|
||||
setCreateApiKeyResponse(resp);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const apiKey = new ApiKey();
|
||||
const apiKey = new ApiKey();
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}/api-keys`}>API Keys</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Add</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Add API key"
|
||||
/>
|
||||
<Card>
|
||||
{!this.state.createApiKeyResponse && <ApiKeyForm initialValues={apiKey} onFinish={this.onFinish} />}
|
||||
{this.state.createApiKeyResponse && <ApiKeyToken createApiKeyResponse={this.state.createApiKeyResponse} />}
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}/api-keys`}>API Keys</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Add</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Add API key"
|
||||
/>
|
||||
<Card>
|
||||
{!createApiKeyResponse && <ApiKeyForm initialValues={apiKey} onFinish={onFinish} />}
|
||||
{createApiKeyResponse && <ApiKeyToken createApiKeyResponse={createApiKeyResponse} />}
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateTenantApiKey;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { DeleteOutlined } from "@ant-design/icons";
|
||||
import { Space, Breadcrumb, Button, PageHeader } from "antd";
|
||||
import { Space, Breadcrumb, Button } from "antd";
|
||||
import { ColumnsType } from "antd/es/table";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import {
|
||||
ListApiKeysRequest,
|
||||
@ -17,62 +17,47 @@ import DataTable, { GetPageCallbackFunc } from "../../components/DataTable";
|
||||
import InternalStore from "../../stores/InternalStore";
|
||||
import DeleteConfirm from "../../components/DeleteConfirm";
|
||||
|
||||
interface IProps {}
|
||||
function ListAdminApiKeys() {
|
||||
const [refreshKey, setRefreshKey] = useState<number>(1);
|
||||
|
||||
interface IState {
|
||||
refreshKey: number;
|
||||
}
|
||||
const columns: ColumnsType<ApiKey.AsObject> = [
|
||||
{
|
||||
title: "ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
width: 400,
|
||||
},
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
title: "Action",
|
||||
dataIndex: "id",
|
||||
key: "action",
|
||||
width: 100,
|
||||
render: (text, record) => (
|
||||
<DeleteConfirm typ="API key" confirm={record.name} onConfirm={deleteApiKey(record.id)}>
|
||||
<Button shape="circle" icon={<DeleteOutlined />} />
|
||||
</DeleteConfirm>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
class ListAdminApiKeys extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
refreshKey: 1,
|
||||
};
|
||||
}
|
||||
|
||||
columns = (): ColumnsType<ApiKey.AsObject> => {
|
||||
return [
|
||||
{
|
||||
title: "ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
width: 400,
|
||||
},
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
title: "Action",
|
||||
dataIndex: "id",
|
||||
key: "action",
|
||||
width: 100,
|
||||
render: (text, record) => (
|
||||
<DeleteConfirm typ="API key" confirm={record.name} onConfirm={this.deleteApiKey(record.id)}>
|
||||
<Button shape="circle" icon={<DeleteOutlined />} />
|
||||
</DeleteConfirm>
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
deleteApiKey = (id: string): (() => void) => {
|
||||
const deleteApiKey = (id: string): (() => void) => {
|
||||
return () => {
|
||||
let req = new DeleteApiKeyRequest();
|
||||
req.setId(id);
|
||||
|
||||
InternalStore.deleteApiKey(req, () => {
|
||||
// trigger a data-table reload
|
||||
this.setState({
|
||||
refreshKey: this.state.refreshKey + 1,
|
||||
});
|
||||
setRefreshKey(refreshKey + 1);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||
let req = new ListApiKeysRequest();
|
||||
req.setLimit(limit);
|
||||
req.setOffset(offset);
|
||||
@ -84,31 +69,29 @@ class ListAdminApiKeys extends Component<IProps, IState> {
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Network Server</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>API keys</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="API keys"
|
||||
extra={[
|
||||
<Button type="primary">
|
||||
<Link to="/api-keys/create">Add API key</Link>
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
<DataTable columns={this.columns()} getPage={this.getPage} rowKey="id" refreshKey={this.state.refreshKey} />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Network Server</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>API keys</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="API keys"
|
||||
extra={[
|
||||
<Button type="primary">
|
||||
<Link to="/api-keys/create">Add API key</Link>
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
<DataTable columns={columns} getPage={getPage} rowKey="id" refreshKey={refreshKey} />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default ListAdminApiKeys;
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { DeleteOutlined } from "@ant-design/icons";
|
||||
import { Space, Breadcrumb, Button, PageHeader } from "antd";
|
||||
import { Space, Breadcrumb, Button } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { ColumnsType } from "antd/es/table";
|
||||
|
||||
import {
|
||||
@ -20,69 +21,55 @@ import Admin from "../../components/Admin";
|
||||
|
||||
interface IProps {
|
||||
tenant: Tenant;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
refreshKey: number;
|
||||
}
|
||||
function ListTenantApiKeys(props: IProps) {
|
||||
const [refreshKey, setRefreshKey] = useState<number>(1);
|
||||
|
||||
class ListTenantApiKeys extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
refreshKey: 1,
|
||||
};
|
||||
}
|
||||
const columns: ColumnsType<ApiKey.AsObject> = [
|
||||
{
|
||||
title: "ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
width: 400,
|
||||
},
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
title: "Action",
|
||||
dataIndex: "id",
|
||||
key: "action",
|
||||
width: 100,
|
||||
render: (text, record) => (
|
||||
<Admin tenantId={props.tenant.getId()} isTenantAdmin>
|
||||
<DeleteConfirm typ="API key" confirm={record.name} onConfirm={deleteApiKey(record.id)}>
|
||||
<Button shape="circle" icon={<DeleteOutlined />} />
|
||||
</DeleteConfirm>
|
||||
</Admin>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
columns = (): ColumnsType<ApiKey.AsObject> => {
|
||||
return [
|
||||
{
|
||||
title: "ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
width: 400,
|
||||
},
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
title: "Action",
|
||||
dataIndex: "id",
|
||||
key: "action",
|
||||
width: 100,
|
||||
render: (text, record) => (
|
||||
<Admin tenantId={this.props.tenant.getId()} isTenantAdmin>
|
||||
<DeleteConfirm typ="API key" confirm={record.name} onConfirm={this.deleteApiKey(record.id)}>
|
||||
<Button shape="circle" icon={<DeleteOutlined />} />
|
||||
</DeleteConfirm>
|
||||
</Admin>
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
deleteApiKey = (id: string): (() => void) => {
|
||||
const deleteApiKey = (id: string): (() => void) => {
|
||||
return () => {
|
||||
let req = new DeleteApiKeyRequest();
|
||||
req.setId(id);
|
||||
|
||||
InternalStore.deleteApiKey(req, () => {
|
||||
// trigger a data-table reload
|
||||
this.setState({
|
||||
refreshKey: this.state.refreshKey + 1,
|
||||
});
|
||||
setRefreshKey(refreshKey + 1);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||
let req = new ListApiKeysRequest();
|
||||
req.setLimit(limit);
|
||||
req.setOffset(offset);
|
||||
req.setTenantId(this.props.tenant.getId());
|
||||
req.setTenantId(props.tenant.getId());
|
||||
|
||||
InternalStore.listApiKeys(req, (resp: ListApiKeysResponse) => {
|
||||
const obj = resp.toObject();
|
||||
@ -90,38 +77,36 @@ class ListTenantApiKeys extends Component<IProps, IState> {
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>API Keys</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="API keys"
|
||||
extra={[
|
||||
<Admin tenantId={this.props.tenant.getId()} isTenantAdmin>
|
||||
<Button type="primary">
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}/api-keys/create`}>Add API key</Link>
|
||||
</Button>
|
||||
</Admin>,
|
||||
]}
|
||||
/>
|
||||
<DataTable columns={this.columns()} getPage={this.getPage} rowKey="id" refreshKey={this.state.refreshKey} />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>API Keys</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="API keys"
|
||||
extra={[
|
||||
<Admin tenantId={props.tenant.getId()} isTenantAdmin>
|
||||
<Button type="primary">
|
||||
<Link to={`/tenants/${props.tenant.getId()}/api-keys/create`}>Add API key</Link>
|
||||
</Button>
|
||||
</Admin>,
|
||||
]}
|
||||
/>
|
||||
<DataTable columns={columns} getPage={getPage} rowKey="id" refreshKey={refreshKey} />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default ListTenantApiKeys;
|
||||
|
@ -1,5 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||
import { Form, Input, Button } from "antd";
|
||||
|
||||
@ -9,9 +7,9 @@ interface IProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
class ApplicationForm extends Component<IProps> {
|
||||
onFinish = (values: Application.AsObject) => {
|
||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
||||
function ApplicationForm(props: IProps) {
|
||||
const onFinish = (values: Application.AsObject) => {
|
||||
const v = Object.assign(props.initialValues.toObject(), values);
|
||||
let app = new Application();
|
||||
|
||||
app.setId(v.id);
|
||||
@ -19,26 +17,24 @@ class ApplicationForm extends Component<IProps> {
|
||||
app.setName(v.name);
|
||||
app.setDescription(v.description);
|
||||
|
||||
this.props.onFinish(app);
|
||||
props.onFinish(app);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
||||
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
|
||||
<Input disabled={this.props.disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Description" name="description">
|
||||
<Input.TextArea disabled={this.props.disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" disabled={this.props.disabled}>
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
|
||||
<Input disabled={props.disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Description" name="description">
|
||||
<Input.TextArea disabled={props.disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" disabled={props.disabled}>
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApplicationForm;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component } from "react";
|
||||
import { Route, Switch, RouteComponentProps, Link } from "react-router-dom";
|
||||
import { Route, Routes, Link, useNavigate, useLocation } from "react-router-dom";
|
||||
|
||||
import { Space, Breadcrumb, Card, Button, PageHeader, Menu } from "antd";
|
||||
import { Space, Breadcrumb, Card, Button, Menu } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
||||
import { Application, DeleteApplicationRequest } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||
@ -38,247 +38,158 @@ import GenerateMqttCertificate from "./integrations/GenerateMqttCertificate";
|
||||
import CreateIftttIntegration from "./integrations/CreateIftttIntegration";
|
||||
import EditIftttIntegration from "./integrations/EditIftttIntegration";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
tenant: Tenant;
|
||||
application: Application;
|
||||
measurementKeys: string[];
|
||||
}
|
||||
|
||||
class ApplicationLayout extends Component<IProps> {
|
||||
deleteApplication = () => {
|
||||
function ApplicationLayout(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const deleteApplication = () => {
|
||||
let req = new DeleteApplicationRequest();
|
||||
req.setId(this.props.application.getId());
|
||||
req.setId(props.application.getId());
|
||||
|
||||
ApplicationStore.delete(req, () => {
|
||||
this.props.history.push(`/tenants/${this.props.tenant.getId()}/applications`);
|
||||
navigate(`/tenants/${props.tenant.getId()}/applications`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const tenant = this.props.tenant;
|
||||
const app = this.props.application;
|
||||
const tenant = props.tenant;
|
||||
const app = props.application;
|
||||
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const path = this.props.history.location.pathname;
|
||||
let tab = "devices";
|
||||
|
||||
if (path.endsWith("/multicast-groups")) {
|
||||
tab = "mg";
|
||||
}
|
||||
if (path.endsWith("/relays")) {
|
||||
tab = "relay";
|
||||
}
|
||||
if (path.endsWith("/edit")) {
|
||||
tab = "edit";
|
||||
}
|
||||
if (path.match(/.*\/integrations.*/g)) {
|
||||
tab = "integrations";
|
||||
}
|
||||
|
||||
const showIntegrations =
|
||||
SessionStore.isAdmin() ||
|
||||
SessionStore.isTenantAdmin(tenant.getId()) ||
|
||||
SessionStore.isTenantDeviceAdmin(tenant.getId());
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}/applications`}>Applications</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>{app.getName()}</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title={app.getName()}
|
||||
subTitle={`application id: ${app.getId()}`}
|
||||
extra={[
|
||||
<Admin tenantId={this.props.tenant.getId()} isDeviceAdmin>
|
||||
<DeleteConfirm confirm={app.getName()} typ="application" onConfirm={this.deleteApplication}>
|
||||
<Button danger type="primary">
|
||||
Delete application
|
||||
</Button>
|
||||
</DeleteConfirm>
|
||||
</Admin>,
|
||||
]}
|
||||
/>
|
||||
<Card>
|
||||
<Menu mode="horizontal" selectedKeys={[tab]} style={{ marginBottom: 24 }}>
|
||||
<Menu.Item key="devices">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}`}>Devices</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="mg">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/multicast-groups`}>
|
||||
Multicast groups
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="relay">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/relays`}>
|
||||
Relays
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="edit">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/edit`}>Application configuration</Link>
|
||||
</Menu.Item>
|
||||
{showIntegrations && (
|
||||
<Menu.Item key="integrations">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/integrations`}>Integrations</Link>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
<Switch>
|
||||
<Route exact path={this.props.match.path} render={props => <ListDevices application={app} {...props} />} />
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/edit`}
|
||||
render={props => <EditApplication application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations`}
|
||||
render={props => <ListIntegrations application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/multicast-groups`}
|
||||
render={props => <ListMulticastGroups application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/relays`}
|
||||
render={props => <ListRelays application={app} {...props} />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/http/create`}
|
||||
render={props => <CreateHttpIntegration application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/http/edit`}
|
||||
render={props => <EditHttpIntegration application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/aws-sns/create`}
|
||||
render={props => <CreateAwsSnsIntegration application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/aws-sns/edit`}
|
||||
render={props => <EditAwsSnsIntegration application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/azure-service-bus/create`}
|
||||
render={props => <CreateAzureServiceBusIntegration application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/azure-service-bus/edit`}
|
||||
render={props => <EditAzureServiceBusIntegration application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/gcp-pub-sub/create`}
|
||||
render={props => <CreateGcpPubSubIntegration application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/gcp-pub-sub/edit`}
|
||||
render={props => <EditGcpPubSubIntegration application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/influxdb/create`}
|
||||
render={props => <CreateInfluxDbIntegration application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/influxdb/edit`}
|
||||
render={props => <EditInfluxDbIntegration application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/mydevices/create`}
|
||||
render={props => <CreateMyDevicesIntegration application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/mydevices/edit`}
|
||||
render={props => <EditMyDevicesIntegration application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/pilot-things/create`}
|
||||
render={props => <CreatePilotThingsIntegration application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/pilot-things/edit`}
|
||||
render={props => <EditPilotThingsIntegration application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/loracloud/create`}
|
||||
render={props => <CreateLoRaCloudIntegration application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/loracloud/edit`}
|
||||
render={props => <EditLoRaCloudIntegration application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/thingsboard/create`}
|
||||
render={props => <CreateThingsBoardIntegration application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/thingsboard/edit`}
|
||||
render={props => <EditThingsBoardIntegration application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/mqtt/certificate`}
|
||||
render={props => <GenerateMqttCertificate application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/ifttt/create`}
|
||||
render={props => (
|
||||
<CreateIftttIntegration application={app} measurementKeys={this.props.measurementKeys} {...props} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/integrations/ifttt/edit`}
|
||||
render={props => (
|
||||
<EditIftttIntegration application={app} measurementKeys={this.props.measurementKeys} {...props} />
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const path = location.pathname;
|
||||
let tab = "devices";
|
||||
|
||||
if (path.endsWith("/multicast-groups")) {
|
||||
tab = "mg";
|
||||
}
|
||||
if (path.endsWith("/relays")) {
|
||||
tab = "relay";
|
||||
}
|
||||
if (path.endsWith("/edit")) {
|
||||
tab = "edit";
|
||||
}
|
||||
if (path.match(/.*\/integrations.*/g)) {
|
||||
tab = "integrations";
|
||||
}
|
||||
|
||||
const showIntegrations =
|
||||
SessionStore.isAdmin() ||
|
||||
SessionStore.isTenantAdmin(tenant.getId()) ||
|
||||
SessionStore.isTenantDeviceAdmin(tenant.getId());
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}/applications`}>Applications</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>{app.getName()}</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title={app.getName()}
|
||||
subTitle={`application id: ${app.getId()}`}
|
||||
extra={[
|
||||
<Admin tenantId={props.tenant.getId()} isDeviceAdmin>
|
||||
<DeleteConfirm confirm={app.getName()} typ="application" onConfirm={deleteApplication}>
|
||||
<Button danger type="primary">
|
||||
Delete application
|
||||
</Button>
|
||||
</DeleteConfirm>
|
||||
</Admin>,
|
||||
]}
|
||||
/>
|
||||
<Card>
|
||||
<Menu mode="horizontal" selectedKeys={[tab]} style={{ marginBottom: 24 }}>
|
||||
<Menu.Item key="devices">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}`}>Devices</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="mg">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/multicast-groups`}>Multicast groups</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="relay">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/relays`}>Relays</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="edit">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/edit`}>Application configuration</Link>
|
||||
</Menu.Item>
|
||||
{showIntegrations && (
|
||||
<Menu.Item key="integrations">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/integrations`}>Integrations</Link>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
<Routes>
|
||||
<Route path="/" element={<ListDevices application={app} />} />
|
||||
<Route path="/edit" element={<EditApplication application={app} />} />
|
||||
<Route path="/integrations" element={<ListIntegrations application={app} />} />
|
||||
<Route path="/multicast-groups" element={<ListMulticastGroups application={app} />} />
|
||||
<Route path="/relays" element={<ListRelays application={app} />} />
|
||||
|
||||
<Route path="/integrations/http/create" element={<CreateHttpIntegration application={app} />} />
|
||||
<Route path="/integrations/http/edit" element={<EditHttpIntegration application={app} />} />
|
||||
|
||||
<Route path="/integrations/http/create" element={<CreateHttpIntegration application={app} />} />
|
||||
<Route path="/integrations/http/edit" element={<EditHttpIntegration application={app} />} />
|
||||
<Route path="/integrations/aws-sns/create" element={<CreateAwsSnsIntegration application={app} />} />
|
||||
<Route path="/integrations/aws-sns/edit" element={<EditAwsSnsIntegration application={app} />} />
|
||||
<Route
|
||||
path="/integrations/azure-service-bus/create"
|
||||
element={<CreateAzureServiceBusIntegration application={app} />}
|
||||
/>
|
||||
<Route
|
||||
path="/integrations/azure-service-bus/edit"
|
||||
element={<EditAzureServiceBusIntegration application={app} />}
|
||||
/>
|
||||
<Route path="/integrations/gcp-pub-sub/create" element={<CreateGcpPubSubIntegration application={app} />} />
|
||||
<Route path="/integrations/gcp-pub-sub/edit" element={<EditGcpPubSubIntegration application={app} />} />
|
||||
<Route path="/integrations/influxdb/create" element={<CreateInfluxDbIntegration application={app} />} />
|
||||
<Route path="/integrations/influxdb/edit" element={<EditInfluxDbIntegration application={app} />} />
|
||||
<Route path="/integrations/mydevices/create" element={<CreateMyDevicesIntegration application={app} />} />
|
||||
<Route path="/integrations/mydevices/edit" element={<EditMyDevicesIntegration application={app} />} />
|
||||
<Route
|
||||
path="/integrations/pilot-things/create"
|
||||
element={<CreatePilotThingsIntegration application={app} />}
|
||||
/>
|
||||
<Route path="/integrations/pilot-things/edit" element={<EditPilotThingsIntegration application={app} />} />
|
||||
<Route path="/integrations/loracloud/create" element={<CreateLoRaCloudIntegration application={app} />} />
|
||||
<Route path="/integrations/loracloud/edit" element={<EditLoRaCloudIntegration application={app} />} />
|
||||
<Route path="/integrations/thingsboard/create" element={<CreateThingsBoardIntegration application={app} />} />
|
||||
<Route path="/integrations/thingsboard/edit" element={<EditThingsBoardIntegration application={app} />} />
|
||||
<Route path="/integrations/mqtt/certificate" element={<GenerateMqttCertificate application={app} />} />
|
||||
<Route
|
||||
path="/integrations/ifttt/create"
|
||||
element={<CreateIftttIntegration application={app} measurementKeys={props.measurementKeys} />}
|
||||
/>
|
||||
<Route
|
||||
path="/integrations/ifttt/edit"
|
||||
element={<EditIftttIntegration application={app} measurementKeys={props.measurementKeys} />}
|
||||
/>
|
||||
</Routes>
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApplicationLayout;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { Route, Switch, RouteComponentProps } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Route, Routes, useParams } from "react-router-dom";
|
||||
|
||||
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
||||
import {
|
||||
@ -16,90 +16,57 @@ import MulticastGroupLayout from "../multicast-groups/MulticastGroupLayout";
|
||||
import CreateMulticastGroup from "../multicast-groups/CreateMulticastGroup";
|
||||
import RelayLayout from "../relays/RelayLayout";
|
||||
|
||||
interface MatchParams {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
interface IProps extends RouteComponentProps<MatchParams> {
|
||||
interface IProps {
|
||||
tenant: Tenant;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
application?: Application;
|
||||
measurementKeys: string[];
|
||||
}
|
||||
function ApplicationLoader(props: IProps) {
|
||||
const { applicationId } = useParams();
|
||||
const [application, setApplication] = useState<Application | undefined>(undefined);
|
||||
const [measurementKeys, setMeasurementKeys] = useState<string[]>([]);
|
||||
|
||||
class ApplicationLoader extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
measurementKeys: [],
|
||||
useEffect(() => {
|
||||
ApplicationStore.on("change", loadApplication);
|
||||
loadApplication();
|
||||
|
||||
return () => {
|
||||
ApplicationStore.removeAllListeners("change");
|
||||
};
|
||||
}
|
||||
}, [applicationId]);
|
||||
|
||||
componentDidMount() {
|
||||
this.getApplication();
|
||||
}
|
||||
|
||||
getApplication = () => {
|
||||
const loadApplication = () => {
|
||||
let req = new GetApplicationRequest();
|
||||
req.setId(this.props.match.params.applicationId);
|
||||
req.setId(applicationId!);
|
||||
|
||||
ApplicationStore.get(req, (resp: GetApplicationResponse) => {
|
||||
this.setState({
|
||||
application: resp.getApplication(),
|
||||
measurementKeys: resp.getMeasurementKeysList(),
|
||||
});
|
||||
setApplication(resp.getApplication());
|
||||
setMeasurementKeys(resp.getMeasurementKeysList());
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const app = this.state.application;
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const path = this.props.match.path;
|
||||
const tenant = this.props.tenant;
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={`${path}/devices/create`}
|
||||
render={props => <CreateDevice tenant={tenant} application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${path}/multicast-groups/create`}
|
||||
render={props => <CreateMulticastGroup tenant={tenant} application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${path}/multicast-groups/:multicastGroupId([\\w-]{36})`}
|
||||
render={(props: any) => <MulticastGroupLayout tenant={tenant} application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${path}/devices/:devEui([0-9a-f]{16})`}
|
||||
component={(props: any) => <DeviceLayout tenant={tenant} application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${path}/relays/:relayDevEui([0-9a-f]{16})`}
|
||||
component={(props: any) => <RelayLayout tenant={tenant} application={app} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
path={path}
|
||||
render={props => (
|
||||
<ApplicationLayout
|
||||
tenant={tenant}
|
||||
application={app}
|
||||
measurementKeys={this.state.measurementKeys}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
const app = application;
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tenant = props.tenant;
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/devices/create" element={<CreateDevice tenant={tenant} application={app} />} />
|
||||
<Route path="/multicast-groups/create" element={<CreateMulticastGroup tenant={tenant} application={app} />} />
|
||||
<Route
|
||||
path="/multicast-groups/:multicastGroupId/*"
|
||||
element={<MulticastGroupLayout tenant={tenant} application={app} />}
|
||||
/>
|
||||
<Route path="/devices/:devEui/*" element={<DeviceLayout tenant={tenant} application={app} />} />
|
||||
<Route path="/relays/:relayDevEui/*" element={<RelayLayout tenant={tenant} application={app} />} />
|
||||
<Route
|
||||
path="/*"
|
||||
element={<ApplicationLayout tenant={tenant} application={app} measurementKeys={measurementKeys} />}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApplicationLoader;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link, RouteComponentProps } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
import { Space, Breadcrumb, Card, PageHeader } from "antd";
|
||||
import { Space, Breadcrumb, Card } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
||||
import {
|
||||
@ -13,56 +13,56 @@ import {
|
||||
import ApplicationForm from "./ApplicationForm";
|
||||
import ApplicationStore from "../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
tenant: Tenant;
|
||||
}
|
||||
|
||||
class CreateApplication extends Component<IProps> {
|
||||
onFinish = (obj: Application) => {
|
||||
obj.setTenantId(this.props.tenant.getId());
|
||||
function CreateApplication(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = (obj: Application) => {
|
||||
obj.setTenantId(props.tenant.getId());
|
||||
|
||||
let req = new CreateApplicationRequest();
|
||||
req.setApplication(obj);
|
||||
|
||||
ApplicationStore.create(req, (resp: CreateApplicationResponse) => {
|
||||
this.props.history.push(`/tenants/${this.props.tenant.getId()}/applications/${resp.getId()}`);
|
||||
navigate(`/tenants/${props.tenant.getId()}/applications/${resp.getId()}`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const app = new Application();
|
||||
const app = new Application();
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}/applications`}>Applications</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Add</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Add application"
|
||||
/>
|
||||
<Card>
|
||||
<ApplicationForm initialValues={app} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}/applications`}>Applications</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Add</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Add application"
|
||||
/>
|
||||
<Card>
|
||||
<ApplicationForm initialValues={app} onFinish={onFinish} />
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateApplication;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Application, UpdateApplicationRequest } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||
|
||||
@ -7,31 +6,29 @@ import ApplicationStore from "../../stores/ApplicationStore";
|
||||
import ApplicationForm from "./ApplicationForm";
|
||||
import SessionStore from "../../stores/SessionStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
class EditApplication extends Component<IProps> {
|
||||
onFinish = (obj: Application) => {
|
||||
function EditApplication(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = (obj: Application) => {
|
||||
let req = new UpdateApplicationRequest();
|
||||
req.setApplication(obj);
|
||||
|
||||
ApplicationStore.update(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const disabled = !(
|
||||
SessionStore.isAdmin() ||
|
||||
SessionStore.isTenantAdmin(this.props.application.getTenantId()) ||
|
||||
SessionStore.isTenantDeviceAdmin(this.props.application.getTenantId())
|
||||
);
|
||||
const disabled = !(
|
||||
SessionStore.isAdmin() ||
|
||||
SessionStore.isTenantAdmin(props.application.getTenantId()) ||
|
||||
SessionStore.isTenantDeviceAdmin(props.application.getTenantId())
|
||||
);
|
||||
|
||||
return <ApplicationForm initialValues={this.props.application} disabled={disabled} onFinish={this.onFinish} />;
|
||||
}
|
||||
return <ApplicationForm initialValues={props.application} disabled={disabled} onFinish={onFinish} />;
|
||||
}
|
||||
|
||||
export default EditApplication;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Space, Breadcrumb, Button, PageHeader } from "antd";
|
||||
import { Space, Breadcrumb, Button } from "antd";
|
||||
import { ColumnsType } from "antd/es/table";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import {
|
||||
ListApplicationsRequest,
|
||||
@ -19,29 +19,25 @@ interface IProps {
|
||||
tenant: Tenant;
|
||||
}
|
||||
|
||||
class ListApplications extends Component<IProps> {
|
||||
columns = (): ColumnsType<ApplicationListItem.AsObject> => {
|
||||
return [
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: 250,
|
||||
render: (text, record) => (
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}/applications/${record.id}`}>{text}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Description",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
},
|
||||
];
|
||||
};
|
||||
function ListApplications(props: IProps) {
|
||||
const columns: ColumnsType<ApplicationListItem.AsObject> = [
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: 250,
|
||||
render: (text, record) => <Link to={`/tenants/${props.tenant.getId()}/applications/${record.id}`}>{text}</Link>,
|
||||
},
|
||||
{
|
||||
title: "Description",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
},
|
||||
];
|
||||
|
||||
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||
let req = new ListApplicationsRequest();
|
||||
req.setTenantId(this.props.tenant.getId());
|
||||
req.setTenantId(props.tenant.getId());
|
||||
req.setLimit(limit);
|
||||
req.setOffset(offset);
|
||||
|
||||
@ -51,38 +47,36 @@ class ListApplications extends Component<IProps> {
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Applications</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Applications"
|
||||
extra={[
|
||||
<Admin tenantId={this.props.tenant.getId()} isDeviceAdmin>
|
||||
<Button type="primary">
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}/applications/create`}>Add application</Link>
|
||||
</Button>
|
||||
</Admin>,
|
||||
]}
|
||||
/>
|
||||
<DataTable columns={this.columns()} getPage={this.getPage} rowKey="id" />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Applications</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Applications"
|
||||
extra={[
|
||||
<Admin tenantId={props.tenant.getId()} isDeviceAdmin>
|
||||
<Button type="primary">
|
||||
<Link to={`/tenants/${props.tenant.getId()}/applications/create`}>Add application</Link>
|
||||
</Button>
|
||||
</Admin>,
|
||||
]}
|
||||
/>
|
||||
<DataTable columns={columns} getPage={getPage} rowKey="id" />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default ListApplications;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { Row } from "antd";
|
||||
|
||||
@ -27,32 +27,22 @@ interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
configured: any[];
|
||||
available: any[];
|
||||
}
|
||||
function ListIntegrations(props: IProps) {
|
||||
const [configured, setConfigured] = useState<any[]>([]);
|
||||
const [available, setAvailable] = useState<any[]>([]);
|
||||
|
||||
class ListIntegrations extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
configured: [],
|
||||
available: [],
|
||||
useEffect(() => {
|
||||
ApplicationStore.on("integration.delete", loadIntegrations);
|
||||
loadIntegrations();
|
||||
|
||||
return () => {
|
||||
ApplicationStore.removeAllListeners("integration.delete");
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
componentDidMount() {
|
||||
ApplicationStore.on("integration.delete", this.loadIntegrations);
|
||||
this.loadIntegrations();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ApplicationStore.removeAllListeners("integration.delete");
|
||||
}
|
||||
|
||||
loadIntegrations = () => {
|
||||
const loadIntegrations = () => {
|
||||
let req = new ListIntegrationsRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
|
||||
ApplicationStore.listIntegrations(req, (resp: ListIntegrationsResponse) => {
|
||||
let configured: any[] = [];
|
||||
@ -70,94 +60,90 @@ class ListIntegrations extends Component<IProps, IState> {
|
||||
|
||||
// AWS SNS
|
||||
if (includes(resp.getResultList(), IntegrationKind.AWS_SNS)) {
|
||||
configured.push(<AwsSnsCard application={this.props.application} />);
|
||||
configured.push(<AwsSnsCard application={props.application} />);
|
||||
} else {
|
||||
available.push(<AwsSnsCard application={this.props.application} add />);
|
||||
available.push(<AwsSnsCard application={props.application} add />);
|
||||
}
|
||||
|
||||
// Azure Service-Bus
|
||||
if (includes(resp.getResultList(), IntegrationKind.AZURE_SERVICE_BUS)) {
|
||||
configured.push(<AzureServiceBusCard application={this.props.application} />);
|
||||
configured.push(<AzureServiceBusCard application={props.application} />);
|
||||
} else {
|
||||
available.push(<AzureServiceBusCard application={this.props.application} add />);
|
||||
available.push(<AzureServiceBusCard application={props.application} add />);
|
||||
}
|
||||
|
||||
// GCP Pub/Sub
|
||||
if (includes(resp.getResultList(), IntegrationKind.GCP_PUB_SUB)) {
|
||||
configured.push(<GcpPubSubCard application={this.props.application} />);
|
||||
configured.push(<GcpPubSubCard application={props.application} />);
|
||||
} else {
|
||||
available.push(<GcpPubSubCard application={this.props.application} add />);
|
||||
available.push(<GcpPubSubCard application={props.application} add />);
|
||||
}
|
||||
|
||||
// HTTP
|
||||
if (includes(resp.getResultList(), IntegrationKind.HTTP)) {
|
||||
configured.push(<HttpCard application={this.props.application} />);
|
||||
configured.push(<HttpCard application={props.application} />);
|
||||
} else {
|
||||
available.push(<HttpCard application={this.props.application} add />);
|
||||
available.push(<HttpCard application={props.application} add />);
|
||||
}
|
||||
|
||||
// IFTTT
|
||||
if (includes(resp.getResultList(), IntegrationKind.IFTTT)) {
|
||||
configured.push(<IftttCard application={this.props.application} />);
|
||||
configured.push(<IftttCard application={props.application} />);
|
||||
} else {
|
||||
available.push(<IftttCard application={this.props.application} add />);
|
||||
available.push(<IftttCard application={props.application} add />);
|
||||
}
|
||||
|
||||
// InfluxDB
|
||||
if (includes(resp.getResultList(), IntegrationKind.INFLUX_DB)) {
|
||||
configured.push(<InfluxdbCard application={this.props.application} />);
|
||||
configured.push(<InfluxdbCard application={props.application} />);
|
||||
} else {
|
||||
available.push(<InfluxdbCard application={this.props.application} add />);
|
||||
available.push(<InfluxdbCard application={props.application} add />);
|
||||
}
|
||||
|
||||
// MQTT
|
||||
if (includes(resp.getResultList(), IntegrationKind.MQTT_GLOBAL)) {
|
||||
configured.push(<MqttCard application={this.props.application} />);
|
||||
configured.push(<MqttCard application={props.application} />);
|
||||
}
|
||||
|
||||
// myDevices
|
||||
if (includes(resp.getResultList(), IntegrationKind.MY_DEVICES)) {
|
||||
configured.push(<MyDevicesCard application={this.props.application} />);
|
||||
configured.push(<MyDevicesCard application={props.application} />);
|
||||
} else {
|
||||
available.push(<MyDevicesCard application={this.props.application} add />);
|
||||
available.push(<MyDevicesCard application={props.application} add />);
|
||||
}
|
||||
|
||||
// Pilot Things
|
||||
if (includes(resp.getResultList(), IntegrationKind.PILOT_THINGS)) {
|
||||
configured.push(<PilotThingsCard application={this.props.application} />);
|
||||
configured.push(<PilotThingsCard application={props.application} />);
|
||||
} else {
|
||||
available.push(<PilotThingsCard application={this.props.application} add />);
|
||||
available.push(<PilotThingsCard application={props.application} add />);
|
||||
}
|
||||
|
||||
// Semtech LoRa Cloud
|
||||
if (includes(resp.getResultList(), IntegrationKind.LORA_CLOUD)) {
|
||||
configured.push(<LoRaCloudCard application={this.props.application} />);
|
||||
configured.push(<LoRaCloudCard application={props.application} />);
|
||||
} else {
|
||||
available.push(<LoRaCloudCard application={this.props.application} add />);
|
||||
available.push(<LoRaCloudCard application={props.application} add />);
|
||||
}
|
||||
|
||||
// ThingsBoard
|
||||
if (includes(resp.getResultList(), IntegrationKind.THINGS_BOARD)) {
|
||||
configured.push(<ThingsBoardCard application={this.props.application} />);
|
||||
configured.push(<ThingsBoardCard application={props.application} />);
|
||||
} else {
|
||||
available.push(<ThingsBoardCard application={this.props.application} add />);
|
||||
available.push(<ThingsBoardCard application={props.application} add />);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
configured: configured,
|
||||
available: available,
|
||||
});
|
||||
setConfigured(configured);
|
||||
setAvailable(available);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Row gutter={24}>
|
||||
{this.state.configured}
|
||||
{this.state.available}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Row gutter={24}>
|
||||
{configured}
|
||||
{available}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default ListIntegrations;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Col, Card, Popconfirm } from "antd";
|
||||
@ -13,47 +12,45 @@ interface IProps {
|
||||
add?: boolean;
|
||||
}
|
||||
|
||||
class AwsSns extends Component<IProps> {
|
||||
onDelete = () => {
|
||||
function AwsSns(props: IProps) {
|
||||
const onDelete = () => {
|
||||
let req = new DeleteAwsSnsIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
|
||||
ApplicationStore.deleteAwsSnsIntegration(req, () => {});
|
||||
};
|
||||
|
||||
render() {
|
||||
let actions: any[] = [];
|
||||
let actions: any[] = [];
|
||||
|
||||
if (!!this.props.add) {
|
||||
actions = [
|
||||
<Link to="integrations/aws-sns/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="integrations/aws-sns/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="AWS SNS"
|
||||
className="integration-card"
|
||||
cover={<img alt="AWS SNS" src="/integrations/aws_sns.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The AWS SNS integration forwards events to an AWS SNS topic." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
if (!!props.add) {
|
||||
actions = [
|
||||
<Link to="aws-sns/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="aws-sns/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="AWS SNS"
|
||||
className="integration-card"
|
||||
cover={<img alt="AWS SNS" src="/integrations/aws_sns.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The AWS SNS integration forwards events to an AWS SNS topic." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default AwsSns;
|
||||
|
@ -1,5 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Form, Input, Button, Select } from "antd";
|
||||
|
||||
import { AwsSnsIntegration, Encoding } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||
@ -9,9 +7,9 @@ interface IProps {
|
||||
onFinish: (obj: AwsSnsIntegration) => void;
|
||||
}
|
||||
|
||||
class AwsSnsIntegrationForm extends Component<IProps> {
|
||||
onFinish = (values: AwsSnsIntegration.AsObject) => {
|
||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
||||
function AwsSnsIntegrationForm(props: IProps) {
|
||||
const onFinish = (values: AwsSnsIntegration.AsObject) => {
|
||||
const v = Object.assign(props.initialValues.toObject(), values);
|
||||
let i = new AwsSnsIntegration();
|
||||
|
||||
i.setApplicationId(v.applicationId);
|
||||
@ -21,54 +19,52 @@ class AwsSnsIntegrationForm extends Component<IProps> {
|
||||
i.setSecretAccessKey(v.secretAccessKey);
|
||||
i.setTopicArn(v.topicArn);
|
||||
|
||||
this.props.onFinish(i);
|
||||
props.onFinish(i);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
||||
<Form.Item
|
||||
label="Payload encoding"
|
||||
name="encoding"
|
||||
rules={[{ required: true, message: "Please select an encoding!" }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
|
||||
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="AWS region" name="region" rules={[{ required: true, message: "Please enter a region!" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="AWS Access Key ID"
|
||||
name="accessKeyId"
|
||||
rules={[{ required: true, message: "Please enter an Access Key ID!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="AWS Secret Access Key"
|
||||
name="secretAccessKey"
|
||||
rules={[{ required: true, message: "Please enter a Secret Access Key!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="AWS SNS topic ARN"
|
||||
name="topicArn"
|
||||
rules={[{ required: true, message: "Please enter a SNS topic ARN!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||
<Form.Item
|
||||
label="Payload encoding"
|
||||
name="encoding"
|
||||
rules={[{ required: true, message: "Please select an encoding!" }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
|
||||
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="AWS region" name="region" rules={[{ required: true, message: "Please enter a region!" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="AWS Access Key ID"
|
||||
name="accessKeyId"
|
||||
rules={[{ required: true, message: "Please enter an Access Key ID!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="AWS Secret Access Key"
|
||||
name="secretAccessKey"
|
||||
rules={[{ required: true, message: "Please enter a Secret Access Key!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="AWS SNS topic ARN"
|
||||
name="topicArn"
|
||||
rules={[{ required: true, message: "Please enter a SNS topic ARN!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default AwsSnsIntegrationForm;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Col, Card, Popconfirm } from "antd";
|
||||
@ -16,46 +15,44 @@ interface IProps {
|
||||
add?: boolean;
|
||||
}
|
||||
|
||||
class AzureServiceBusCard extends Component<IProps> {
|
||||
onDelete = () => {
|
||||
function AzureServiceBusCard(props: IProps) {
|
||||
const onDelete = () => {
|
||||
let req = new DeleteAzureServiceBusIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
ApplicationStore.deleteAzureServiceBusIntegration(req, () => {});
|
||||
};
|
||||
|
||||
render() {
|
||||
let actions: any[] = [];
|
||||
let actions: any[] = [];
|
||||
|
||||
if (!!this.props.add) {
|
||||
actions = [
|
||||
<Link to="integrations/azure-service-bus/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="integrations/azure-service-bus/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="Azure Service-Bus"
|
||||
className="integration-card"
|
||||
cover={<img alt="Azure Service-Bus" src="/integrations/azure_service_bus.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The Azure Service-Bus integration forwards events to an Azure Service-Bus topic or queue." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
if (!!props.add) {
|
||||
actions = [
|
||||
<Link to="azure-service-bus/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="azure-service-bus/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="Azure Service-Bus"
|
||||
className="integration-card"
|
||||
cover={<img alt="Azure Service-Bus" src="/integrations/azure_service_bus.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The Azure Service-Bus integration forwards events to an Azure Service-Bus topic or queue." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default AzureServiceBusCard;
|
||||
|
@ -1,5 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Form, Input, Button, Select } from "antd";
|
||||
|
||||
import { AzureServiceBusIntegration, Encoding } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||
@ -9,9 +7,9 @@ interface IProps {
|
||||
onFinish: (obj: AzureServiceBusIntegration) => void;
|
||||
}
|
||||
|
||||
class AzureServiceBusIntegrationForm extends Component<IProps> {
|
||||
onFinish = (values: AzureServiceBusIntegration.AsObject) => {
|
||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
||||
function AzureServiceBusIntegrationForm(props: IProps) {
|
||||
const onFinish = (values: AzureServiceBusIntegration.AsObject) => {
|
||||
const v = Object.assign(props.initialValues.toObject(), values);
|
||||
let i = new AzureServiceBusIntegration();
|
||||
|
||||
i.setApplicationId(v.applicationId);
|
||||
@ -19,45 +17,53 @@ class AzureServiceBusIntegrationForm extends Component<IProps> {
|
||||
i.setConnectionString(v.connectionString);
|
||||
i.setPublishName(v.publishName);
|
||||
|
||||
this.props.onFinish(i);
|
||||
props.onFinish(i);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
||||
<Form.Item
|
||||
label="Payload encoding"
|
||||
name="encoding"
|
||||
rules={[{ required: true, message: "Please select an encoding!" }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
|
||||
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Azure Service-Bus connection string"
|
||||
name="connectionString"
|
||||
tooltip="This string can be obtained after creating a 'Shared access policy' with 'Send' permission."
|
||||
rules={[{ required: true, message: "Please enter an Azure Service-Bus connection string!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Azure Service-Bus topic / queue name"
|
||||
name="publishName"
|
||||
rules={[{ required: true, message: "Please enter an Azure Service-Bus topic / queue name!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||
<Form.Item
|
||||
label="Payload encoding"
|
||||
name="encoding"
|
||||
rules={[{ required: true, message: "Please select an encoding!" }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
|
||||
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Azure Service-Bus connection string"
|
||||
name="connectionString"
|
||||
tooltip="This string can be obtained after creating a 'Shared access policy' with 'Send' permission."
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please enter an Azure Service-Bus connection string!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Azure Service-Bus topic / queue name"
|
||||
name="publishName"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please enter an Azure Service-Bus topic / queue name!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default AzureServiceBusIntegrationForm;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -12,33 +11,31 @@ import {
|
||||
import AwsSnsIntegrationForm from "./AwsSnsIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
class CreateAwsSnsIntegration extends Component<IProps> {
|
||||
onFinish = (obj: AwsSnsIntegration) => {
|
||||
obj.setApplicationId(this.props.application.getId());
|
||||
function CreateAwsSnsIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = (obj: AwsSnsIntegration) => {
|
||||
obj.setApplicationId(props.application.getId());
|
||||
|
||||
let req = new CreateAwsSnsIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.createAwsSnsIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const i = new AwsSnsIntegration();
|
||||
const i = new AwsSnsIntegration();
|
||||
|
||||
return (
|
||||
<Card title="Add AWS SNS integration">
|
||||
<AwsSnsIntegrationForm initialValues={i} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card title="Add AWS SNS integration">
|
||||
<AwsSnsIntegrationForm initialValues={i} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateAwsSnsIntegration;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -12,33 +11,31 @@ import {
|
||||
import AzureServiceBusIntegrationForm from "./AzureServiceBusIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
class CreateAzureServiceBusIntegration extends Component<IProps> {
|
||||
onFinish = (obj: AzureServiceBusIntegration) => {
|
||||
obj.setApplicationId(this.props.application.getId());
|
||||
function CreateAzureServiceBusIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = (obj: AzureServiceBusIntegration) => {
|
||||
obj.setApplicationId(props.application.getId());
|
||||
|
||||
let req = new CreateAzureServiceBusIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.createAzureServiceBusIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const i = new AzureServiceBusIntegration();
|
||||
const i = new AzureServiceBusIntegration();
|
||||
|
||||
return (
|
||||
<Card title="Add Azure Service-Bus integration">
|
||||
<AzureServiceBusIntegrationForm initialValues={i} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card title="Add Azure Service-Bus integration">
|
||||
<AzureServiceBusIntegrationForm initialValues={i} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateAzureServiceBusIntegration;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -12,33 +11,31 @@ import {
|
||||
import GcpPubSubIntegrationForm from "./GcpPubSubIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
class CreateGcpPubSubIntegration extends Component<IProps> {
|
||||
onFinish = (obj: GcpPubSubIntegration) => {
|
||||
obj.setApplicationId(this.props.application.getId());
|
||||
function CreateGcpPubSubIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = (obj: GcpPubSubIntegration) => {
|
||||
obj.setApplicationId(props.application.getId());
|
||||
|
||||
let req = new CreateGcpPubSubIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.createGcpPubSubIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const i = new GcpPubSubIntegration();
|
||||
const i = new GcpPubSubIntegration();
|
||||
|
||||
return (
|
||||
<Card title="Add GCP Pub/Sub integration">
|
||||
<GcpPubSubIntegrationForm initialValues={i} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card title="Add GCP Pub/Sub integration">
|
||||
<GcpPubSubIntegrationForm initialValues={i} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateGcpPubSubIntegration;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -12,33 +11,31 @@ import {
|
||||
import HttpIntegrationForm from "./HttpIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
class CreateHttpIntegration extends Component<IProps> {
|
||||
onFinish = (obj: HttpIntegration) => {
|
||||
obj.setApplicationId(this.props.application.getId());
|
||||
function CreateHttpIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = (obj: HttpIntegration) => {
|
||||
obj.setApplicationId(props.application.getId());
|
||||
|
||||
let req = new CreateHttpIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.createHttpIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const i = new HttpIntegration();
|
||||
const i = new HttpIntegration();
|
||||
|
||||
return (
|
||||
<Card title="Add HTTP integration">
|
||||
<HttpIntegrationForm initialValues={i} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card title="Add HTTP integration">
|
||||
<HttpIntegrationForm initialValues={i} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateHttpIntegration;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -12,35 +11,33 @@ import {
|
||||
import IftttIntegrationForm from "./IftttIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
measurementKeys: string[];
|
||||
}
|
||||
|
||||
class CreateIftttIntegration extends Component<IProps> {
|
||||
onFinish = (obj: IftttIntegration) => {
|
||||
obj.setApplicationId(this.props.application.getId());
|
||||
function CreateIftttIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = (obj: IftttIntegration) => {
|
||||
obj.setApplicationId(props.application.getId());
|
||||
|
||||
let req = new CreateIftttIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.createIftttIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const i = new IftttIntegration();
|
||||
i.setUplinkValuesList(["", ""]);
|
||||
const i = new IftttIntegration();
|
||||
i.setUplinkValuesList(["", ""]);
|
||||
|
||||
return (
|
||||
<Card title="Add IFTTT integration">
|
||||
<IftttIntegrationForm measurementKeys={this.props.measurementKeys} initialValues={i} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card title="Add IFTTT integration">
|
||||
<IftttIntegrationForm measurementKeys={props.measurementKeys} initialValues={i} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateIftttIntegration;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -12,33 +11,31 @@ import {
|
||||
import InfluxDbIntegrationForm from "./InfluxDbIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
class CreateInfluxDbIntegration extends Component<IProps> {
|
||||
onFinish = (obj: InfluxDbIntegration) => {
|
||||
obj.setApplicationId(this.props.application.getId());
|
||||
function CreateInfluxDbIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = (obj: InfluxDbIntegration) => {
|
||||
obj.setApplicationId(props.application.getId());
|
||||
|
||||
let req = new CreateInfluxDbIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.createInfluxDbIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const i = new InfluxDbIntegration();
|
||||
const i = new InfluxDbIntegration();
|
||||
|
||||
return (
|
||||
<Card title="Add InfluxDB integration">
|
||||
<InfluxDbIntegrationForm initialValues={i} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card title="Add InfluxDB integration">
|
||||
<InfluxDbIntegrationForm initialValues={i} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateInfluxDbIntegration;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -13,38 +12,36 @@ import {
|
||||
import LoRaCloudIntegrationForm from "./LoRaCloudIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
class CreateLoRaCloudIntegration extends Component<IProps> {
|
||||
onFinish = (obj: LoraCloudIntegration) => {
|
||||
obj.setApplicationId(this.props.application.getId());
|
||||
function CreateLoRaCloudIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = (obj: LoraCloudIntegration) => {
|
||||
obj.setApplicationId(props.application.getId());
|
||||
|
||||
let req = new CreateLoraCloudIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.createLoraCloudIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
let i = new LoraCloudIntegration();
|
||||
let mgs = new LoraCloudModemGeolocationServices();
|
||||
mgs.setModemEnabled(true);
|
||||
mgs.setForwardFPortsList([192, 197, 198, 199]);
|
||||
let i = new LoraCloudIntegration();
|
||||
let mgs = new LoraCloudModemGeolocationServices();
|
||||
mgs.setModemEnabled(true);
|
||||
mgs.setForwardFPortsList([192, 197, 198, 199]);
|
||||
|
||||
i.setModemGeolocationServices(mgs);
|
||||
i.setModemGeolocationServices(mgs);
|
||||
|
||||
return (
|
||||
<Card title="Add Semtech LoRa Cloud™ integration">
|
||||
<LoRaCloudIntegrationForm initialValues={i} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card title="Add Semtech LoRa Cloud™ integration">
|
||||
<LoRaCloudIntegrationForm initialValues={i} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateLoRaCloudIntegration;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -12,33 +11,31 @@ import {
|
||||
import MyDevicesIntegrationForm from "./MyDevicesIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
class CreateMyDevicesIntegration extends Component<IProps> {
|
||||
onFinish = (obj: MyDevicesIntegration) => {
|
||||
obj.setApplicationId(this.props.application.getId());
|
||||
function CreateMyDevicesIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = (obj: MyDevicesIntegration) => {
|
||||
obj.setApplicationId(props.application.getId());
|
||||
|
||||
let req = new CreateMyDevicesIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.createMyDevicesIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const i = new MyDevicesIntegration();
|
||||
const i = new MyDevicesIntegration();
|
||||
|
||||
return (
|
||||
<Card title="Add myDevices integration">
|
||||
<MyDevicesIntegrationForm initialValues={i} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card title="Add myDevices integration">
|
||||
<MyDevicesIntegrationForm initialValues={i} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateMyDevicesIntegration;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -12,33 +11,31 @@ import {
|
||||
import PilotThingsIntegrationForm from "./PilotThingsIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
class CreatePilotThingsIntegration extends Component<IProps> {
|
||||
onFinish = (obj: PilotThingsIntegration) => {
|
||||
obj.setApplicationId(this.props.application.getId());
|
||||
function CreatePilotThingsIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = (obj: PilotThingsIntegration) => {
|
||||
obj.setApplicationId(props.application.getId());
|
||||
|
||||
let req = new CreatePilotThingsIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.createPilotThingsIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const i = new PilotThingsIntegration();
|
||||
const i = new PilotThingsIntegration();
|
||||
|
||||
return (
|
||||
<Card title="Add Pilot Things integration">
|
||||
<PilotThingsIntegrationForm initialValues={i} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card title="Add Pilot Things integration">
|
||||
<PilotThingsIntegrationForm initialValues={i} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreatePilotThingsIntegration;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -12,33 +11,31 @@ import {
|
||||
import ThingsBoardIntegrationForm from "./ThingsBoardIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
class CreateThingsBoardIntegration extends Component<IProps> {
|
||||
onFinish = (obj: ThingsBoardIntegration) => {
|
||||
obj.setApplicationId(this.props.application.getId());
|
||||
function CreateThingsBoardIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = (obj: ThingsBoardIntegration) => {
|
||||
obj.setApplicationId(props.application.getId());
|
||||
|
||||
let req = new CreateThingsBoardIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.createThingsBoardIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const i = new ThingsBoardIntegration();
|
||||
const i = new ThingsBoardIntegration();
|
||||
|
||||
return (
|
||||
<Card title="Add ThingsBoard integration">
|
||||
<ThingsBoardIntegrationForm initialValues={i} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card title="Add ThingsBoard integration">
|
||||
<ThingsBoardIntegrationForm initialValues={i} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateThingsBoardIntegration;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -14,53 +14,41 @@ import {
|
||||
import AwsSnsIntegrationForm from "./AwsSnsIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
integration?: AwsSnsIntegration;
|
||||
}
|
||||
function EditAwsSnsIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
const [integration, setIntegration] = useState<AwsSnsIntegration | undefined>(undefined);
|
||||
|
||||
class EditAwsSnsIntegration extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
let req = new GetAwsSnsIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
|
||||
ApplicationStore.getAwsSnsIntegration(req, (resp: GetAwsSnsIntegrationResponse) => {
|
||||
this.setState({
|
||||
integration: resp.getIntegration(),
|
||||
});
|
||||
setIntegration(resp.getIntegration());
|
||||
});
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
onFinish = (obj: AwsSnsIntegration) => {
|
||||
const onFinish = (obj: AwsSnsIntegration) => {
|
||||
let req = new UpdateAwsSnsIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.updateAwsSnsIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update AWS SNS integration">
|
||||
<AwsSnsIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
if (integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update AWS SNS integration">
|
||||
<AwsSnsIntegrationForm initialValues={integration} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditAwsSnsIntegration;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -14,53 +14,41 @@ import {
|
||||
import AzureServiceBusIntegrationForm from "./AzureServiceBusIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
integration?: AzureServiceBusIntegration;
|
||||
}
|
||||
function EditAzureServiceBusIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
const [integration, setIntegration] = useState<AzureServiceBusIntegration | undefined>(undefined);
|
||||
|
||||
class EditAzureServiceBusIntegration extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
let req = new GetAzureServiceBusIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
|
||||
ApplicationStore.getAzureServiceBusIntegration(req, (resp: GetAzureServiceBusIntegrationResponse) => {
|
||||
this.setState({
|
||||
integration: resp.getIntegration(),
|
||||
});
|
||||
setIntegration(resp.getIntegration());
|
||||
});
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
onFinish = (obj: AzureServiceBusIntegration) => {
|
||||
const onFinish = (obj: AzureServiceBusIntegration) => {
|
||||
let req = new UpdateAzureServiceBusIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.updateAzureServiceBusIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update Azure Service-Bus integration">
|
||||
<AzureServiceBusIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
if (integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update Azure Service-Bus integration">
|
||||
<AzureServiceBusIntegrationForm initialValues={integration} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditAzureServiceBusIntegration;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -14,53 +14,41 @@ import {
|
||||
import GcpPubSubIntegrationForm from "./GcpPubSubIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
integration?: GcpPubSubIntegration;
|
||||
}
|
||||
function EditGcpPubSubIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
const [integration, setIntegration] = useState<GcpPubSubIntegration | undefined>(undefined);
|
||||
|
||||
class EditGcpPubSubIntegration extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
let req = new GetGcpPubSubIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
|
||||
ApplicationStore.getGcpPubSubIntegration(req, (resp: GetGcpPubSubIntegrationResponse) => {
|
||||
this.setState({
|
||||
integration: resp.getIntegration(),
|
||||
});
|
||||
setIntegration(resp.getIntegration());
|
||||
});
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
onFinish = (obj: GcpPubSubIntegration) => {
|
||||
const onFinish = (obj: GcpPubSubIntegration) => {
|
||||
let req = new UpdateGcpPubSubIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.updateGcpPubSubIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update GCP Pub/Sub integration">
|
||||
<GcpPubSubIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
if (integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update GCP Pub/Sub integration">
|
||||
<GcpPubSubIntegrationForm initialValues={integration} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditGcpPubSubIntegration;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -14,53 +14,41 @@ import {
|
||||
import HttpIntegrationForm from "./HttpIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
integration?: HttpIntegration;
|
||||
}
|
||||
function EditHttpIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
const [integration, setIntegration] = useState<HttpIntegration | undefined>(undefined);
|
||||
|
||||
class EditHttpIntegration extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
let req = new GetHttpIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
|
||||
ApplicationStore.getHttpIntegration(req, (resp: GetHttpIntegrationResponse) => {
|
||||
this.setState({
|
||||
integration: resp.getIntegration(),
|
||||
});
|
||||
setIntegration(resp.getIntegration());
|
||||
});
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
onFinish = (obj: HttpIntegration) => {
|
||||
const onFinish = (obj: HttpIntegration) => {
|
||||
let req = new UpdateHttpIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.updateHttpIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update HTTP integration">
|
||||
<HttpIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
if (integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update HTTP integration">
|
||||
<HttpIntegrationForm initialValues={integration} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditHttpIntegration;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -14,58 +14,42 @@ import {
|
||||
import IftttIntegrationForm from "./IftttIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
measurementKeys: string[];
|
||||
}
|
||||
|
||||
interface IState {
|
||||
integration?: IftttIntegration;
|
||||
}
|
||||
function EditIftttIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
const [integration, setIntegration] = useState<IftttIntegration | undefined>(undefined);
|
||||
|
||||
class EditIftttIntegration extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
let req = new GetIftttIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
|
||||
ApplicationStore.getIftttIntegration(req, (resp: GetIftttIntegrationResponse) => {
|
||||
this.setState({
|
||||
integration: resp.getIntegration(),
|
||||
});
|
||||
setIntegration(resp.getIntegration());
|
||||
});
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
onFinish = (obj: IftttIntegration) => {
|
||||
const onFinish = (obj: IftttIntegration) => {
|
||||
let req = new UpdateIftttIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.updateIftttIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update IFTTT integration">
|
||||
<IftttIntegrationForm
|
||||
measurementKeys={this.props.measurementKeys}
|
||||
initialValues={this.state.integration}
|
||||
onFinish={this.onFinish}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
if (integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update IFTTT integration">
|
||||
<IftttIntegrationForm measurementKeys={props.measurementKeys} initialValues={integration} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditIftttIntegration;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -14,53 +14,41 @@ import {
|
||||
import InfluxDbIntegrationForm from "./InfluxDbIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
integration?: InfluxDbIntegration;
|
||||
}
|
||||
function EditInfluxDbIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
const [integration, setIntegration] = useState<InfluxDbIntegration | undefined>(undefined);
|
||||
|
||||
class EditInfluxDbIntegration extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
let req = new GetInfluxDbIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
|
||||
ApplicationStore.getInfluxDbIntegration(req, (resp: GetInfluxDbIntegrationResponse) => {
|
||||
this.setState({
|
||||
integration: resp.getIntegration(),
|
||||
});
|
||||
setIntegration(resp.getIntegration());
|
||||
});
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
onFinish = (obj: InfluxDbIntegration) => {
|
||||
const onFinish = (obj: InfluxDbIntegration) => {
|
||||
let req = new UpdateInfluxDbIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.updateInfluxDbIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update InfluxDB integration">
|
||||
<InfluxDbIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
if (integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update InfluxDB integration">
|
||||
<InfluxDbIntegrationForm initialValues={integration} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditInfluxDbIntegration;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -14,53 +14,41 @@ import {
|
||||
import LoRaCloudIntegrationForm from "./LoRaCloudIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
integration?: LoraCloudIntegration;
|
||||
}
|
||||
function EditLoRaCloudIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
const [integration, setIntegration] = useState<LoraCloudIntegration | undefined>(undefined);
|
||||
|
||||
class EditLoRaCloudIntegration extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
let req = new GetLoraCloudIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
|
||||
ApplicationStore.getLoraCloudIntegration(req, (resp: GetLoraCloudIntegrationResponse) => {
|
||||
this.setState({
|
||||
integration: resp.getIntegration(),
|
||||
});
|
||||
setIntegration(resp.getIntegration());
|
||||
});
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
onFinish = (obj: LoraCloudIntegration) => {
|
||||
const onFinish = (obj: LoraCloudIntegration) => {
|
||||
let req = new UpdateLoraCloudIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.updateLoraCloudIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update Semtech LoRa Cloud™ integration">
|
||||
<LoRaCloudIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
if (integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update Semtech LoRa Cloud™ integration">
|
||||
<LoRaCloudIntegrationForm initialValues={integration} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditLoRaCloudIntegration;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -14,53 +14,41 @@ import {
|
||||
import MyDevicesIntegrationForm from "./MyDevicesIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
integration?: MyDevicesIntegration;
|
||||
}
|
||||
function EditMyDevicesIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
const [integration, setIntegration] = useState<MyDevicesIntegration | undefined>(undefined);
|
||||
|
||||
class EditMyDevicesIntegration extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
let req = new GetMyDevicesIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
|
||||
ApplicationStore.getMyDevicesIntegration(req, (resp: GetMyDevicesIntegrationResponse) => {
|
||||
this.setState({
|
||||
integration: resp.getIntegration(),
|
||||
});
|
||||
setIntegration(resp.getIntegration());
|
||||
});
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
onFinish = (obj: MyDevicesIntegration) => {
|
||||
const onFinish = (obj: MyDevicesIntegration) => {
|
||||
let req = new UpdateMyDevicesIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.updateMyDevicesIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update myDevices integration">
|
||||
<MyDevicesIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
if (integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update myDevices integration">
|
||||
<MyDevicesIntegrationForm initialValues={integration} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditMyDevicesIntegration;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -14,53 +14,41 @@ import {
|
||||
import PilotThingsIntegrationForm from "./PilotThingsIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
integration?: PilotThingsIntegration;
|
||||
}
|
||||
function EditPilotThingsIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
const [integration, setIntegration] = useState<PilotThingsIntegration | undefined>(undefined);
|
||||
|
||||
class EditPilotThingsIntegration extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
let req = new GetPilotThingsIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
|
||||
ApplicationStore.getPilotThingsIntegration(req, (resp: GetPilotThingsIntegrationResponse) => {
|
||||
this.setState({
|
||||
integration: resp.getIntegration(),
|
||||
});
|
||||
setIntegration(resp.getIntegration());
|
||||
});
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
onFinish = (obj: PilotThingsIntegration) => {
|
||||
const onFinish = (obj: PilotThingsIntegration) => {
|
||||
let req = new UpdatePilotThingsIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.updatePilotThingsIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update Pilot Things integration">
|
||||
<PilotThingsIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
if (integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update Pilot Things integration">
|
||||
<PilotThingsIntegrationForm initialValues={integration} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditPilotThingsIntegration;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
@ -14,53 +14,41 @@ import {
|
||||
import ThingsBoardIntegrationForm from "./ThingsBoardIntegrationForm";
|
||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
integration?: ThingsBoardIntegration;
|
||||
}
|
||||
function EditThingsBoardIntegration(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
const [integration, setIntegration] = useState<ThingsBoardIntegration | undefined>(undefined);
|
||||
|
||||
class EditThingsBoardIntegration extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
let req = new GetThingsBoardIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
|
||||
ApplicationStore.getThingsBoardIntegration(req, (resp: GetThingsBoardIntegrationResponse) => {
|
||||
this.setState({
|
||||
integration: resp.getIntegration(),
|
||||
});
|
||||
setIntegration(resp.getIntegration());
|
||||
});
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
onFinish = (obj: ThingsBoardIntegration) => {
|
||||
const onFinish = (obj: ThingsBoardIntegration) => {
|
||||
let req = new UpdateThingsBoardIntegrationRequest();
|
||||
req.setIntegration(obj);
|
||||
|
||||
ApplicationStore.updateThingsBoardIntegration(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
||||
);
|
||||
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update ThingsBoard integration">
|
||||
<ThingsBoardIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
);
|
||||
if (integration === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Update ThingsBoard integration">
|
||||
<ThingsBoardIntegrationForm initialValues={integration} onFinish={onFinish} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditThingsBoardIntegration;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Col, Card, Popconfirm } from "antd";
|
||||
@ -13,46 +12,44 @@ interface IProps {
|
||||
add?: boolean;
|
||||
}
|
||||
|
||||
class GcpPubSubCard extends Component<IProps> {
|
||||
onDelete = () => {
|
||||
function GcpPubSubCard(props: IProps) {
|
||||
const onDelete = () => {
|
||||
let req = new DeleteGcpPubSubIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
ApplicationStore.deleteGcpPubSubIntegration(req, () => {});
|
||||
};
|
||||
|
||||
render() {
|
||||
let actions: any[] = [];
|
||||
let actions: any[] = [];
|
||||
|
||||
if (!!this.props.add) {
|
||||
actions = [
|
||||
<Link to="integrations/gcp-pub-sub/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="integrations/gcp-pub-sub/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="GCP Pub/Sub"
|
||||
className="integration-card"
|
||||
cover={<img alt="GCP Pub/Sub" src="/integrations/gcp_pubsub.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The Google Cloud Pub/Sub integration forwards events to a GCP Pub/Sub topic." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
if (!!props.add) {
|
||||
actions = [
|
||||
<Link to="gcp-pub-sub/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="gcp-pub-sub/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="GCP Pub/Sub"
|
||||
className="integration-card"
|
||||
cover={<img alt="GCP Pub/Sub" src="/integrations/gcp_pubsub.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The Google Cloud Pub/Sub integration forwards events to a GCP Pub/Sub topic." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default GcpPubSubCard;
|
||||
|
@ -1,5 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Form, Input, Button, Select } from "antd";
|
||||
|
||||
import { GcpPubSubIntegration, Encoding } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||
@ -9,9 +7,9 @@ interface IProps {
|
||||
onFinish: (obj: GcpPubSubIntegration) => void;
|
||||
}
|
||||
|
||||
class GcpPubSubIntegrationForm extends Component<IProps> {
|
||||
onFinish = (values: GcpPubSubIntegration.AsObject) => {
|
||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
||||
function GcpPubSubIntegrationForm(props: IProps) {
|
||||
const onFinish = (values: GcpPubSubIntegration.AsObject) => {
|
||||
const v = Object.assign(props.initialValues.toObject(), values);
|
||||
let i = new GcpPubSubIntegration();
|
||||
|
||||
i.setApplicationId(v.applicationId);
|
||||
@ -20,52 +18,55 @@ class GcpPubSubIntegrationForm extends Component<IProps> {
|
||||
i.setTopicName(v.topicName);
|
||||
i.setCredentialsFile(v.credentialsFile);
|
||||
|
||||
this.props.onFinish(i);
|
||||
props.onFinish(i);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
||||
<Form.Item
|
||||
label="Payload encoding"
|
||||
name="encoding"
|
||||
rules={[{ required: true, message: "Please select an encoding!" }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
|
||||
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="GCP project ID"
|
||||
name="projectId"
|
||||
rules={[{ required: true, message: "Please enter a GCP project ID!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="GCP Pub/Sub topic name"
|
||||
name="topicName"
|
||||
rules={[{ required: true, message: "Please enter a GCP Pub/Sub topic name!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="GCP Service account credentials file"
|
||||
name="credentialsFile"
|
||||
tooltip="Under IAM create a Service account with 'Pub/Sub Publisher' role, then put the content of the JSON key in this field."
|
||||
rules={[{ required: true, message: "Please enter a GCP Service account credentials file!" }]}
|
||||
>
|
||||
<Input.TextArea rows={10} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||
<Form.Item
|
||||
label="Payload encoding"
|
||||
name="encoding"
|
||||
rules={[{ required: true, message: "Please select an encoding!" }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
|
||||
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="GCP project ID"
|
||||
name="projectId"
|
||||
rules={[{ required: true, message: "Please enter a GCP project ID!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="GCP Pub/Sub topic name"
|
||||
name="topicName"
|
||||
rules={[{ required: true, message: "Please enter a GCP Pub/Sub topic name!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="GCP Service account credentials file"
|
||||
name="credentialsFile"
|
||||
tooltip="Under IAM create a Service account with 'Pub/Sub Publisher' role, then put the content of the JSON key in this field."
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please enter a GCP Service account credentials file!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea rows={10} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default GcpPubSubIntegrationForm;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import moment from "moment";
|
||||
import { Card, Button, Form, Input } from "antd";
|
||||
@ -15,39 +15,27 @@ interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
certificate?: GenerateMqttIntegrationClientCertificateResponse;
|
||||
buttonDisabled: boolean;
|
||||
}
|
||||
function GenerateMqttCertificate(props: IProps) {
|
||||
const [certificate, setCertificate] = useState<GenerateMqttIntegrationClientCertificateResponse | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [buttonDisabled, setButtonDisabled] = useState<boolean>(false);
|
||||
|
||||
class GenerateMqttCertificate extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
certificate: undefined,
|
||||
buttonDisabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
requestCertificate = () => {
|
||||
this.setState({
|
||||
buttonDisabled: true,
|
||||
});
|
||||
const requestCertificate = () => {
|
||||
setButtonDisabled(true);
|
||||
|
||||
let req = new GenerateMqttIntegrationClientCertificateRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
|
||||
ApplicationStore.generateMqttIntegrationClientCertificate(
|
||||
req,
|
||||
(resp: GenerateMqttIntegrationClientCertificateResponse) => {
|
||||
this.setState({
|
||||
certificate: resp,
|
||||
});
|
||||
setCertificate(resp);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
renderRequest = () => {
|
||||
const renderRequest = () => {
|
||||
return (
|
||||
<Card>
|
||||
<p>
|
||||
@ -59,7 +47,7 @@ class GenerateMqttCertificate extends Component<IProps, IState> {
|
||||
</strong>
|
||||
</p>
|
||||
<p>
|
||||
<Button onClick={this.requestCertificate} disabled={this.state.buttonDisabled}>
|
||||
<Button onClick={requestCertificate} disabled={buttonDisabled}>
|
||||
Generate certificate
|
||||
</Button>
|
||||
</p>
|
||||
@ -67,14 +55,14 @@ class GenerateMqttCertificate extends Component<IProps, IState> {
|
||||
);
|
||||
};
|
||||
|
||||
renderResponse = () => {
|
||||
const certificate = this.state.certificate!;
|
||||
const renderResponse = () => {
|
||||
const cert = certificate!;
|
||||
|
||||
const initial = {
|
||||
expiresAt: moment(certificate.getExpiresAt()!.toDate()!).format("YYYY-MM-DD HH:mm:ss"),
|
||||
caCert: certificate.getCaCert(),
|
||||
tlsCert: certificate.getTlsCert(),
|
||||
tlsKey: certificate.getTlsKey(),
|
||||
expiresAt: moment(cert.getExpiresAt()!.toDate()!).format("YYYY-MM-DD HH:mm:ss"),
|
||||
caCert: cert.getCaCert(),
|
||||
tlsCert: cert.getTlsCert(),
|
||||
tlsKey: cert.getTlsKey(),
|
||||
};
|
||||
|
||||
return (
|
||||
@ -103,15 +91,13 @@ class GenerateMqttCertificate extends Component<IProps, IState> {
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
let content = this.renderRequest();
|
||||
let content = renderRequest();
|
||||
|
||||
if (this.state.certificate !== undefined) {
|
||||
content = this.renderResponse();
|
||||
}
|
||||
|
||||
return <Card title="Generate MQTT certificate">{content}</Card>;
|
||||
if (certificate !== undefined) {
|
||||
content = renderResponse();
|
||||
}
|
||||
|
||||
return <Card title="Generate MQTT certificate">{content}</Card>;
|
||||
}
|
||||
|
||||
export default GenerateMqttCertificate;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Col, Card, Popconfirm } from "antd";
|
||||
@ -13,46 +12,44 @@ interface IProps {
|
||||
add?: boolean;
|
||||
}
|
||||
|
||||
class HttpCard extends Component<IProps> {
|
||||
onDelete = () => {
|
||||
function HttpCard(props: IProps) {
|
||||
const onDelete = () => {
|
||||
let req = new DeleteHttpIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
ApplicationStore.deleteHttpIntegration(req, () => {});
|
||||
};
|
||||
|
||||
render() {
|
||||
let actions: any[] = [];
|
||||
let actions: any[] = [];
|
||||
|
||||
if (!!this.props.add) {
|
||||
actions = [
|
||||
<Link to="integrations/http/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="integrations/http/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="HTTP"
|
||||
className="integration-card"
|
||||
cover={<img alt="HTTP" src="/integrations/http.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The HTTP integration forwards events to a user-configurable endpoint as POST requests." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
if (!!props.add) {
|
||||
actions = [
|
||||
<Link to="http/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="http/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="HTTP"
|
||||
className="integration-card"
|
||||
cover={<img alt="HTTP" src="/integrations/http.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The HTTP integration forwards events to a user-configurable endpoint as POST requests." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default HttpCard;
|
||||
|
@ -1,5 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Form, Input, Button, Select, Row, Col, Typography, Space } from "antd";
|
||||
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
|
||||
@ -10,9 +8,9 @@ interface IProps {
|
||||
onFinish: (obj: HttpIntegration) => void;
|
||||
}
|
||||
|
||||
class HttpIntegrationForm extends Component<IProps> {
|
||||
onFinish = (values: HttpIntegration.AsObject) => {
|
||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
||||
function HttpIntegrationForm(props: IProps) {
|
||||
const onFinish = (values: HttpIntegration.AsObject) => {
|
||||
const v = Object.assign(props.initialValues.toObject(), values);
|
||||
let i = new HttpIntegration();
|
||||
|
||||
i.setApplicationId(v.applicationId);
|
||||
@ -24,79 +22,77 @@ class HttpIntegrationForm extends Component<IProps> {
|
||||
i.getHeadersMap().set(elm[0], elm[1]);
|
||||
}
|
||||
|
||||
this.props.onFinish(i);
|
||||
props.onFinish(i);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
||||
<Form.Item
|
||||
label="Payload encoding"
|
||||
name="encoding"
|
||||
rules={[{ required: true, message: "Please select an encoding!" }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
|
||||
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Event endpoint URL(s)"
|
||||
name="eventEndpointUrl"
|
||||
tooltip="ChirpStack will make a POST request to this URL(s) with 'event' as query parameter. Multiple URLs can be defined as a comma separated list. Whitespace will be automatically removed."
|
||||
rules={[{ required: true, message: "Please enter an event endpoint URL!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Typography.Text>Headers</Typography.Text>
|
||||
<Form.List name="headersMap">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Row gutter={24}>
|
||||
<Col span={6}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 0]}
|
||||
fieldKey={[name, 0]}
|
||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||
>
|
||||
<Input placeholder="Key" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 1]}
|
||||
fieldKey={[name, 1]}
|
||||
rules={[{ required: true, message: "Please enter a value!" }]}
|
||||
>
|
||||
<Input placeholder="Value" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
Add header
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Space>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||
<Form.Item
|
||||
label="Payload encoding"
|
||||
name="encoding"
|
||||
rules={[{ required: true, message: "Please select an encoding!" }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
|
||||
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Event endpoint URL(s)"
|
||||
name="eventEndpointUrl"
|
||||
tooltip="ChirpStack will make a POST request to this URL(s) with 'event' as query parameter. Multiple URLs can be defined as a comma separated list. Whitespace will be automatically removed."
|
||||
rules={[{ required: true, message: "Please enter an event endpoint URL!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Typography.Text>Headers</Typography.Text>
|
||||
<Form.List name="headersMap">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Row gutter={24}>
|
||||
<Col span={6}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 0]}
|
||||
fieldKey={[name, 0]}
|
||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||
>
|
||||
<Input placeholder="Key" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 1]}
|
||||
fieldKey={[name, 1]}
|
||||
rules={[{ required: true, message: "Please enter a value!" }]}
|
||||
>
|
||||
<Input placeholder="Value" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
Add header
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Space>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default HttpIntegrationForm;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Col, Card, Popconfirm } from "antd";
|
||||
@ -13,46 +12,44 @@ interface IProps {
|
||||
add?: boolean;
|
||||
}
|
||||
|
||||
class IftttCard extends Component<IProps> {
|
||||
onDelete = () => {
|
||||
function IftttCard(props: IProps) {
|
||||
const onDelete = () => {
|
||||
let req = new DeleteIftttIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
ApplicationStore.deleteIftttIntegration(req, () => {});
|
||||
};
|
||||
|
||||
render() {
|
||||
let actions: any[] = [];
|
||||
let actions: any[] = [];
|
||||
|
||||
if (!!this.props.add) {
|
||||
actions = [
|
||||
<Link to="integrations/ifttt/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="integrations/ifttt/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="IFTTT"
|
||||
className="integration-card"
|
||||
cover={<img alt="IFTTT" src="/integrations/ifttt.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The IFTTT integration forwards events to the IFTTT Webhooks integration." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
if (!!props.add) {
|
||||
actions = [
|
||||
<Link to="ifttt/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="ifttt/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="IFTTT"
|
||||
className="integration-card"
|
||||
cover={<img alt="IFTTT" src="/integrations/ifttt.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The IFTTT integration forwards events to the IFTTT Webhooks integration." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default IftttCard;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { Form, Input, AutoComplete, Button, Row, Col, Switch } from "antd";
|
||||
|
||||
@ -10,29 +10,15 @@ interface IProps {
|
||||
onFinish: (obj: IftttIntegration) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
arbitraryJson: boolean;
|
||||
}
|
||||
function IftttIntegrationForm(props: IProps) {
|
||||
const [arbitraryJson, setArbitraryJson] = useState<Boolean>(false);
|
||||
|
||||
class IftttIntegrationForm extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
useEffect(() => {
|
||||
setArbitraryJson(props.initialValues.getArbitraryJson());
|
||||
}, [props]);
|
||||
|
||||
this.state = {
|
||||
arbitraryJson: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const v = this.props.initialValues;
|
||||
|
||||
this.setState({
|
||||
arbitraryJson: v.getArbitraryJson(),
|
||||
});
|
||||
}
|
||||
|
||||
onFinish = (values: IftttIntegration.AsObject) => {
|
||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
||||
const onFinish = (values: IftttIntegration.AsObject) => {
|
||||
const v = Object.assign(props.initialValues.toObject(), values);
|
||||
let i = new IftttIntegration();
|
||||
|
||||
i.setApplicationId(v.applicationId);
|
||||
@ -41,55 +27,58 @@ class IftttIntegrationForm extends Component<IProps, IState> {
|
||||
i.setArbitraryJson(v.arbitraryJson);
|
||||
i.setUplinkValuesList(v.uplinkValuesList);
|
||||
|
||||
this.props.onFinish(i);
|
||||
props.onFinish(i);
|
||||
};
|
||||
|
||||
onArbitraryJsonChange = (checked: boolean) => {
|
||||
this.setState({
|
||||
arbitraryJson: checked,
|
||||
});
|
||||
}
|
||||
const onArbitraryJsonChange = (checked: boolean) => {
|
||||
setArbitraryJson(checked);
|
||||
};
|
||||
|
||||
render() {
|
||||
const options: {
|
||||
value: string;
|
||||
}[] = this.props.measurementKeys.map(v => {
|
||||
return { value: v };
|
||||
});
|
||||
const options: {
|
||||
value: string;
|
||||
}[] = props.measurementKeys.map(v => {
|
||||
return { value: v };
|
||||
});
|
||||
|
||||
return (
|
||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
||||
<Form.Item
|
||||
label="Key"
|
||||
name="key"
|
||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||
tooltip="This key can be obtained from the IFTTT Webhooks integrations documentation"
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Event prefix"
|
||||
name="eventPrefix"
|
||||
rules={[{ pattern: /[A-Za-z0-9]+/, message: "Only use A-Z, a-z and 0-9 characters" }]}
|
||||
tooltip="The prefix will be added to the Webhook event, e.g. if set an uplink will be published as PREFIX_up instead of up."
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Publish as arbitrary JSON"
|
||||
name="arbitraryJson"
|
||||
valuePropName="checked"
|
||||
tooltip="If enabled, the event payload will be published as-is (arbitrary JSON payload instead of 3 JSON values format)."
|
||||
>
|
||||
<Switch onChange={this.onArbitraryJsonChange} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
{!this.state.arbitraryJson && <Form.List name="uplinkValuesList">
|
||||
return (
|
||||
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||
<Form.Item
|
||||
label="Key"
|
||||
name="key"
|
||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||
tooltip="This key can be obtained from the IFTTT Webhooks integrations documentation"
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Event prefix"
|
||||
name="eventPrefix"
|
||||
rules={[
|
||||
{
|
||||
pattern: /[A-Za-z0-9]+/,
|
||||
message: "Only use A-Z, a-z and 0-9 characters",
|
||||
},
|
||||
]}
|
||||
tooltip="The prefix will be added to the Webhook event, e.g. if set an uplink will be published as PREFIX_up instead of up."
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Publish as arbitrary JSON"
|
||||
name="arbitraryJson"
|
||||
valuePropName="checked"
|
||||
tooltip="If enabled, the event payload will be published as-is (arbitrary JSON payload instead of 3 JSON values format)."
|
||||
>
|
||||
<Switch onChange={onArbitraryJsonChange} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
{!arbitraryJson && (
|
||||
<Form.List name="uplinkValuesList">
|
||||
{fields => (
|
||||
<Row gutter={24}>
|
||||
{fields.map((field, i) => (
|
||||
@ -105,15 +94,15 @@ class IftttIntegrationForm extends Component<IProps, IState> {
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
</Form.List>}
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
</Form.List>
|
||||
)}
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default IftttIntegrationForm;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { Form, Input, Button, Select } from "antd";
|
||||
|
||||
@ -13,20 +13,11 @@ interface IProps {
|
||||
onFinish: (obj: InfluxDbIntegration) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
selectedVersion: InfluxDbVersion;
|
||||
}
|
||||
function InfluxDbIntegrationForm(props: IProps) {
|
||||
const [selectedVersion, setSelectedVersion] = useState<InfluxDbVersion>(InfluxDbVersion.INFLUXDB_1);
|
||||
|
||||
class InfluxDbIntegrationForm extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedVersion: InfluxDbVersion.INFLUXDB_1,
|
||||
};
|
||||
}
|
||||
|
||||
onFinish = (values: InfluxDbIntegration.AsObject) => {
|
||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
||||
const onFinish = (values: InfluxDbIntegration.AsObject) => {
|
||||
const v = Object.assign(props.initialValues.toObject(), values);
|
||||
let i = new InfluxDbIntegration();
|
||||
|
||||
i.setApplicationId(v.applicationId);
|
||||
@ -41,98 +32,90 @@ class InfluxDbIntegrationForm extends Component<IProps, IState> {
|
||||
i.setBucket(v.bucket);
|
||||
i.setToken(v.token);
|
||||
|
||||
this.props.onFinish(i);
|
||||
props.onFinish(i);
|
||||
};
|
||||
|
||||
onVersionChange = (version: InfluxDbVersion) => {
|
||||
this.setState({
|
||||
selectedVersion: version,
|
||||
});
|
||||
const onVersionChange = (version: InfluxDbVersion) => {
|
||||
setSelectedVersion(version);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
||||
return (
|
||||
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||
<Form.Item
|
||||
label="InfluxDB version"
|
||||
name="version"
|
||||
rules={[{ required: true, message: "Please select an InfluxDB version!" }]}
|
||||
>
|
||||
<Select onChange={onVersionChange}>
|
||||
<Select.Option value={InfluxDbVersion.INFLUXDB_1}>InfluxDB v1</Select.Option>
|
||||
<Select.Option value={InfluxDbVersion.INFLUXDB_2}>InfluxDB v2</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="API endpoint (write)"
|
||||
name="endpoint"
|
||||
rules={[{ required: true, message: "Please enter an endpoint!" }]}
|
||||
>
|
||||
<Input placeholder="http://localhost:8086/api/v2/write" />
|
||||
</Form.Item>
|
||||
{selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
||||
<Form.Item label="Username" name="username">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
{selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
||||
<Form.Item label="Password" name="password">
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
)}
|
||||
{selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
||||
<Form.Item label="Database name" name="db" rules={[{ required: true, message: "Please enter database name!" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
{selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
||||
<Form.Item
|
||||
label="InfluxDB version"
|
||||
name="version"
|
||||
rules={[{ required: true, message: "Please select an InfluxDB version!" }]}
|
||||
label="Retention policy name"
|
||||
name="retentionPolicyName"
|
||||
tooltip="Sets the target retention policy for the write. InfluxDB writes to the DEFAULT retention policy if you do not specify a retention policy."
|
||||
>
|
||||
<Select onChange={this.onVersionChange}>
|
||||
<Select.Option value={InfluxDbVersion.INFLUXDB_1}>InfluxDB v1</Select.Option>
|
||||
<Select.Option value={InfluxDbVersion.INFLUXDB_2}>InfluxDB v2</Select.Option>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
{selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
||||
<Form.Item label="Select timestamp precision" name="precision">
|
||||
<Select>
|
||||
<Select.Option value={InfluxDbPrecision.NS}>Nanosecond</Select.Option>
|
||||
<Select.Option value={InfluxDbPrecision.U}>Microsecond</Select.Option>
|
||||
<Select.Option value={InfluxDbPrecision.MS}>Millisecond</Select.Option>
|
||||
<Select.Option value={InfluxDbPrecision.S}>Second</Select.Option>
|
||||
<Select.Option value={InfluxDbPrecision.M}>Minute</Select.Option>
|
||||
<Select.Option value={InfluxDbPrecision.H}>Hour</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="API endpoint (write)"
|
||||
name="endpoint"
|
||||
rules={[{ required: true, message: "Please enter an endpoint!" }]}
|
||||
>
|
||||
<Input placeholder="http://localhost:8086/api/v2/write" />
|
||||
)}
|
||||
{selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
|
||||
<Form.Item label="Organization" name="organization">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
||||
<Form.Item label="Username" name="username">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
||||
<Form.Item label="Password" name="password">
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
)}
|
||||
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
||||
<Form.Item
|
||||
label="Database name"
|
||||
name="db"
|
||||
rules={[{ required: true, message: "Please enter database name!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
||||
<Form.Item
|
||||
label="Retention policy name"
|
||||
name="retentionPolicyName"
|
||||
tooltip="Sets the target retention policy for the write. InfluxDB writes to the DEFAULT retention policy if you do not specify a retention policy."
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
||||
<Form.Item label="Select timestamp precision" name="precision">
|
||||
<Select>
|
||||
<Select.Option value={InfluxDbPrecision.NS}>Nanosecond</Select.Option>
|
||||
<Select.Option value={InfluxDbPrecision.U}>Microsecond</Select.Option>
|
||||
<Select.Option value={InfluxDbPrecision.MS}>Millisecond</Select.Option>
|
||||
<Select.Option value={InfluxDbPrecision.S}>Second</Select.Option>
|
||||
<Select.Option value={InfluxDbPrecision.M}>Minute</Select.Option>
|
||||
<Select.Option value={InfluxDbPrecision.H}>Hour</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
|
||||
<Form.Item label="Organization" name="organization">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
|
||||
<Form.Item label="Bucket" name="bucket">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
|
||||
<Form.Item label="Token" name="token">
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
)}
|
||||
{selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
|
||||
<Form.Item label="Bucket" name="bucket">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
)}
|
||||
{selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
|
||||
<Form.Item label="Token" name="token">
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default InfluxDbIntegrationForm;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Col, Card, Popconfirm } from "antd";
|
||||
@ -13,46 +12,44 @@ interface IProps {
|
||||
add?: boolean;
|
||||
}
|
||||
|
||||
class InfluxdbCard extends Component<IProps> {
|
||||
onDelete = () => {
|
||||
function InfluxdbCard(props: IProps) {
|
||||
const onDelete = () => {
|
||||
let req = new DeleteInfluxDbIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
ApplicationStore.deleteInfluxDbIntegration(req, () => {});
|
||||
};
|
||||
|
||||
render() {
|
||||
let actions: any[] = [];
|
||||
let actions: any[] = [];
|
||||
|
||||
if (!!this.props.add) {
|
||||
actions = [
|
||||
<Link to="integrations/influxdb/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="integrations/influxdb/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="InfluxDB"
|
||||
className="integration-card"
|
||||
cover={<img alt="InfluxDB" src="/integrations/influxdb.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The InfluxDB integration writes events into an InfluxDB time-series database." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
if (!!props.add) {
|
||||
actions = [
|
||||
<Link to="influxdb/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="influxdb/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="InfluxDB"
|
||||
className="integration-card"
|
||||
cover={<img alt="InfluxDB" src="/integrations/influxdb.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The InfluxDB integration writes events into an InfluxDB time-series database." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default InfluxdbCard;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Col, Card, Popconfirm } from "antd";
|
||||
@ -13,46 +12,44 @@ interface IProps {
|
||||
add?: boolean;
|
||||
}
|
||||
|
||||
class LoRaCloudCard extends Component<IProps> {
|
||||
onDelete = () => {
|
||||
function LoRaCloudCard(props: IProps) {
|
||||
const onDelete = () => {
|
||||
let req = new DeleteLoraCloudIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
ApplicationStore.deleteLoraCloudIntegration(req, () => {});
|
||||
};
|
||||
|
||||
render() {
|
||||
let actions: any[] = [];
|
||||
let actions: any[] = [];
|
||||
|
||||
if (!!this.props.add) {
|
||||
actions = [
|
||||
<Link to="integrations/loracloud/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="integrations/loracloud/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="Semtech LoRa Cloud™"
|
||||
className="integration-card"
|
||||
cover={<img alt="Semtech LoRa Cloud" src="/integrations/loracloud.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The Semtech LoRa Cloud integration provides Modem & Geolocation Services." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
if (!!props.add) {
|
||||
actions = [
|
||||
<Link to="loracloud/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="loracloud/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="Semtech LoRa Cloud™"
|
||||
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;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { Form, Input, InputNumber, Switch, Button, Tabs, Collapse } from "antd";
|
||||
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
@ -13,43 +13,28 @@ interface IProps {
|
||||
onFinish: (obj: LoraCloudIntegration) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
modemEnabled: boolean;
|
||||
geolocationTdoa: boolean;
|
||||
geolocationRssi: boolean;
|
||||
geolocationWifi: boolean;
|
||||
geolocationGnss: boolean;
|
||||
}
|
||||
function LoRaCloudIntegrationForm(props: IProps) {
|
||||
const [modemEnabled, setModemEnabled] = useState<boolean>(false);
|
||||
const [geolocationTdoa, setGeolocationTdoa] = useState<boolean>(false);
|
||||
const [geolocationRssi, setGeolocationRssi] = useState<boolean>(false);
|
||||
const [geolocationWifi, setGeolocationWifi] = useState<boolean>(false);
|
||||
const [geolocationGnss, setGeolocationGnss] = useState<boolean>(false);
|
||||
|
||||
class LoRaCloudIntegrationForm extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
modemEnabled: false,
|
||||
geolocationTdoa: false,
|
||||
geolocationRssi: false,
|
||||
geolocationWifi: false,
|
||||
geolocationGnss: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const v = this.props.initialValues;
|
||||
useEffect(() => {
|
||||
const v = props.initialValues;
|
||||
const mgs = v.getModemGeolocationServices();
|
||||
|
||||
if (mgs !== undefined) {
|
||||
this.setState({
|
||||
modemEnabled: mgs.getModemEnabled(),
|
||||
geolocationTdoa: mgs.getGeolocationTdoa(),
|
||||
geolocationRssi: mgs.getGeolocationRssi(),
|
||||
geolocationWifi: mgs.getGeolocationWifi(),
|
||||
geolocationGnss: mgs.getGeolocationGnss(),
|
||||
});
|
||||
setModemEnabled(mgs.getModemEnabled());
|
||||
setGeolocationTdoa(mgs.getGeolocationTdoa());
|
||||
setGeolocationRssi(mgs.getGeolocationRssi());
|
||||
setGeolocationWifi(mgs.getGeolocationWifi());
|
||||
setGeolocationGnss(mgs.getGeolocationGnss());
|
||||
}
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
onFinish = (values: LoraCloudIntegration.AsObject) => {
|
||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
||||
const onFinish = (values: LoraCloudIntegration.AsObject) => {
|
||||
const v = Object.assign(props.initialValues.toObject(), values);
|
||||
const mgsv = v.modemGeolocationServices;
|
||||
|
||||
let mgs = new LoraCloudModemGeolocationServices();
|
||||
@ -76,202 +61,194 @@ class LoRaCloudIntegrationForm extends Component<IProps, IState> {
|
||||
i.setApplicationId(v.applicationId);
|
||||
i.setModemGeolocationServices(mgs);
|
||||
|
||||
this.props.onFinish(i);
|
||||
props.onFinish(i);
|
||||
};
|
||||
|
||||
onModemEnabledChange = (v: boolean) => {
|
||||
this.setState({
|
||||
modemEnabled: v,
|
||||
});
|
||||
const onModemEnabledChange = (v: boolean) => {
|
||||
setModemEnabled(v);
|
||||
};
|
||||
|
||||
onGeolocationTdoaChange = (v: boolean) => {
|
||||
this.setState({
|
||||
geolocationTdoa: v,
|
||||
});
|
||||
const onGeolocationTdoaChange = (v: boolean) => {
|
||||
setGeolocationTdoa(v);
|
||||
};
|
||||
|
||||
onGeolocationRssiChange = (v: boolean) => {
|
||||
this.setState({
|
||||
geolocationRssi: v,
|
||||
});
|
||||
const onGeolocationRssiChange = (v: boolean) => {
|
||||
setGeolocationRssi(v);
|
||||
};
|
||||
|
||||
onGeolocationWifiChange = (v: boolean) => {
|
||||
this.setState({
|
||||
geolocationWifi: v,
|
||||
});
|
||||
const onGeolocationWifiChange = (v: boolean) => {
|
||||
setGeolocationWifi(v);
|
||||
};
|
||||
|
||||
onGeolocationGnssChange = (v: boolean) => {
|
||||
this.setState({
|
||||
geolocationGnss: v,
|
||||
});
|
||||
const onGeolocationGnssChange = (v: boolean) => {
|
||||
setGeolocationGnss(v);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
||||
<Tabs>
|
||||
<Tabs.TabPane tab="Modem & Geolocation Services" key="1">
|
||||
return (
|
||||
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||
<Tabs>
|
||||
<Tabs.TabPane tab="Modem & Geolocation Services" key="1">
|
||||
<Form.Item
|
||||
label="Token"
|
||||
name={["modemGeolocationServices", "token"]}
|
||||
tooltip="This token can be obtained from loracloud.com"
|
||||
rules={[{ required: true, message: "Please enter a token!" }]}
|
||||
>
|
||||
<Input type="password" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["modemGeolocationServices", "modemEnabled"]}
|
||||
label="I am using LoRa Edge™ LR1110 or my device uses LoRa Basics™ Modem-E"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch onChange={onModemEnabledChange} />
|
||||
</Form.Item>
|
||||
{modemEnabled && (
|
||||
<Form.List name={["modemGeolocationServices", "forwardFPortsList"]}>
|
||||
{(fields, { add, remove }) => (
|
||||
<Form.Item label="Forward messages on these FPorts to LoRa Cloud">
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item
|
||||
{...field}
|
||||
rules={[{ required: true, message: "Please a FPort value!" }]}
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: "100px",
|
||||
marginRight: "24px",
|
||||
}}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={255}
|
||||
addonAfter={<MinusCircleOutlined onClick={() => remove(index)} />}
|
||||
/>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Button type="dashed" onClick={() => add()} icon={<PlusOutlined />} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.List>
|
||||
)}
|
||||
{modemEnabled && (
|
||||
<Form.Item
|
||||
label="Token"
|
||||
name={["modemGeolocationServices", "token"]}
|
||||
tooltip="This token can be obtained from loracloud.com"
|
||||
rules={[{ required: true, message: "Please enter a token!" }]}
|
||||
>
|
||||
<Input type="password" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["modemGeolocationServices", "modemEnabled"]}
|
||||
label="I am using LoRa Edge™ LR1110 or my device uses LoRa Basics™ Modem-E"
|
||||
label="Use receive timestamp for GNSS geolocation"
|
||||
name={["modemGeolocationServices", "gnssUseRxTime"]}
|
||||
tooltip="If enabled, the receive timestamp of the gateway will be used as reference instead of the timestamp included in the GNSS payload."
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch onChange={this.onModemEnabledChange} />
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{this.state.modemEnabled && (
|
||||
<Form.List name={["modemGeolocationServices", "forwardFPortsList"]}>
|
||||
{(fields, { add, remove }) => (
|
||||
<Form.Item label="Forward messages on these FPorts to LoRa Cloud">
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item
|
||||
{...field}
|
||||
rules={[{ required: true, message: "Please a FPort value!" }]}
|
||||
style={{ display: "inline-block", width: "100px", marginRight: "24px" }}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={255}
|
||||
addonAfter={<MinusCircleOutlined onClick={() => remove(index)} />}
|
||||
/>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Button type="dashed" onClick={() => add()} icon={<PlusOutlined />} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.List>
|
||||
)}
|
||||
{this.state.modemEnabled && (
|
||||
)}
|
||||
{modemEnabled && (
|
||||
<Form.Item
|
||||
label="Use location of receiving gateways for assistance"
|
||||
name={["modemGeolocationServices", "gnssUseGatewayLocation"]}
|
||||
tooltip="If enabled, the gateway location will be provided to the geolocation resolver to aid the resolving process."
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
)}
|
||||
{modemEnabled && (
|
||||
<Form.Item
|
||||
label="My device adheres to the LoRa Edge™ Tracker Modem-E Version Reference Design protocol"
|
||||
name={["modemGeolocationServices", "parseTlv"]}
|
||||
tooltip="If enabled, ChirpStack will try to resolve the location of the device if a geolocation payload is detected."
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Collapse style={{ marginBottom: 24 }}>
|
||||
<Collapse.Panel header="Advanced geolocation options" key={1}>
|
||||
<Form.Item
|
||||
label="Use receive timestamp for GNSS geolocation"
|
||||
name={["modemGeolocationServices", "gnssUseRxTime"]}
|
||||
tooltip="If enabled, the receive timestamp of the gateway will be used as reference instead of the timestamp included in the GNSS payload."
|
||||
label="TDOA based geolocation"
|
||||
name={["modemGeolocationServices", "geolocationTdoa"]}
|
||||
tooltip="If enabled, geolocation will be based on time-difference of arrival (TDOA). Please note that this requires gateways that support the fine-timestamp feature."
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
<Switch onChange={onGeolocationTdoaChange} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{this.state.modemEnabled && (
|
||||
<Form.Item
|
||||
label="Use location of receiving gateways for assistance"
|
||||
name={["modemGeolocationServices", "gnssUseGatewayLocation"]}
|
||||
tooltip="If enabled, the gateway location will be provided to the geolocation resolver to aid the resolving process."
|
||||
label="RSSI based geolocation"
|
||||
name={["modemGeolocationServices", "geolocationRssi"]}
|
||||
tooltip="If enabled, geolocation will be based on RSSI values reported by the receiving gateways."
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
<Switch onChange={onGeolocationRssiChange} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{this.state.modemEnabled && (
|
||||
<Form.Item
|
||||
label="My device adheres to the LoRa Edge™ Tracker Modem-E Version Reference Design protocol"
|
||||
name={["modemGeolocationServices", "parseTlv"]}
|
||||
tooltip="If enabled, ChirpStack will try to resolve the location of the device if a geolocation payload is detected."
|
||||
label="Wi-Fi based geolocation"
|
||||
name={["modemGeolocationServices", "geolocationWifi"]}
|
||||
tooltip="If enabled, geolocation will be based on Wi-Fi access-point data reported by the device."
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
<Switch onChange={onGeolocationWifiChange} />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Collapse style={{ marginBottom: 24 }}>
|
||||
<Collapse.Panel header="Advanced geolocation options" key={1}>
|
||||
<Form.Item
|
||||
label="GNSS based geolocation (LR1110)"
|
||||
name={["modemGeolocationServices", "geolocationGnss"]}
|
||||
tooltip="If enabled, geolocation will be based on GNSS data reported by the device."
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch onChange={onGeolocationGnssChange} />
|
||||
</Form.Item>
|
||||
{(geolocationTdoa || geolocationRssi) && (
|
||||
<Form.Item
|
||||
label="TDOA based geolocation"
|
||||
name={["modemGeolocationServices", "geolocationTdoa"]}
|
||||
tooltip="If enabled, geolocation will be based on time-difference of arrival (TDOA). Please note that this requires gateways that support the fine-timestamp feature."
|
||||
label="Geolocation buffer (TTL in seconds)"
|
||||
name={["modemGeolocationServices", "geolocationBufferTtl"]}
|
||||
tooltip="The time in seconds that historical uplinks will be stored in the geolocation buffer. Used for TDOA and RSSI geolocation."
|
||||
>
|
||||
<InputNumber min={0} max={86400} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{(geolocationTdoa || geolocationRssi) && (
|
||||
<Form.Item
|
||||
label="Geolocation min buffer size"
|
||||
name={["modemGeolocationServices", "geolocationMinBufferSize"]}
|
||||
tooltip="The minimum buffer size required before using geolocation. Using multiple uplinks for geolocation can increase the accuracy of the geolocation results. Used for TDOA and RSSI geolocation."
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{geolocationWifi && (
|
||||
<Form.Item
|
||||
label="Wifi payload field"
|
||||
name={["modemGeolocationServices", "geolocationWifiPayloadField"]}
|
||||
tooltip="This must match the name of the field in the decoded payload which holds array of Wifi access-points. Each element in the array must contain two keys: 1) macAddress: array of 6 bytes, 2) signalStrength: RSSI of the access-point."
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
{geolocationGnss && (
|
||||
<Form.Item
|
||||
label="GNSS payload field"
|
||||
name={["modemGeolocationServices", "geolocationGnssPayloadField"]}
|
||||
tooltip="This must match the name of the field in the decoded payload which holds the LR1110 GNSS bytes."
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
{geolocationGnss && (
|
||||
<Form.Item
|
||||
label="Use receive timestamp for GNSS geolocation"
|
||||
name={["modemGeolocationServices", "geolocationGnssUseRxTime"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch onChange={this.onGeolocationTdoaChange} />
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="RSSI based geolocation"
|
||||
name={["modemGeolocationServices", "geolocationRssi"]}
|
||||
tooltip="If enabled, geolocation will be based on RSSI values reported by the receiving gateways."
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch onChange={this.onGeolocationRssiChange} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Wi-Fi based geolocation"
|
||||
name={["modemGeolocationServices", "geolocationWifi"]}
|
||||
tooltip="If enabled, geolocation will be based on Wi-Fi access-point data reported by the device."
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch onChange={this.onGeolocationWifiChange} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="GNSS based geolocation (LR1110)"
|
||||
name={["modemGeolocationServices", "geolocationGnss"]}
|
||||
tooltip="If enabled, geolocation will be based on GNSS data reported by the device."
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch onChange={this.onGeolocationGnssChange} />
|
||||
</Form.Item>
|
||||
{(this.state.geolocationTdoa || this.state.geolocationRssi) && (
|
||||
<Form.Item
|
||||
label="Geolocation buffer (TTL in seconds)"
|
||||
name={["modemGeolocationServices", "geolocationBufferTtl"]}
|
||||
tooltip="The time in seconds that historical uplinks will be stored in the geolocation buffer. Used for TDOA and RSSI geolocation."
|
||||
>
|
||||
<InputNumber min={0} max={86400} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{(this.state.geolocationTdoa || this.state.geolocationRssi) && (
|
||||
<Form.Item
|
||||
label="Geolocation min buffer size"
|
||||
name={["modemGeolocationServices", "geolocationMinBufferSize"]}
|
||||
tooltip="The minimum buffer size required before using geolocation. Using multiple uplinks for geolocation can increase the accuracy of the geolocation results. Used for TDOA and RSSI geolocation."
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{this.state.geolocationWifi && (
|
||||
<Form.Item
|
||||
label="Wifi payload field"
|
||||
name={["modemGeolocationServices", "geolocationWifiPayloadField"]}
|
||||
tooltip="This must match the name of the field in the decoded payload which holds array of Wifi access-points. Each element in the array must contain two keys: 1) macAddress: array of 6 bytes, 2) signalStrength: RSSI of the access-point."
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
{this.state.geolocationGnss && (
|
||||
<Form.Item
|
||||
label="GNSS payload field"
|
||||
name={["modemGeolocationServices", "geolocationGnssPayloadField"]}
|
||||
tooltip="This must match the name of the field in the decoded payload which holds the LR1110 GNSS bytes."
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
{this.state.geolocationGnss && (
|
||||
<Form.Item
|
||||
label="Use receive timestamp for GNSS geolocation"
|
||||
name={["modemGeolocationServices", "geolocationGnssUseRxTime"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoRaCloudIntegrationForm;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Col, Card } from "antd";
|
||||
@ -9,23 +8,21 @@ interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
class HttpCard extends Component<IProps> {
|
||||
render() {
|
||||
let actions: any[] = [<Link to="integrations/mqtt/certificate">Get certificate</Link>];
|
||||
function MqttCard(props: IProps) {
|
||||
let actions: any[] = [<Link to="mqtt/certificate">Get certificate</Link>];
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="MQTT"
|
||||
className="integration-card"
|
||||
cover={<img alt="MQTT" src="/integrations/mqtt.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The MQTT integration forwards events to a MQTT broker." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="MQTT"
|
||||
className="integration-card"
|
||||
cover={<img alt="MQTT" src="/integrations/mqtt.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The MQTT integration forwards events to a MQTT broker." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default HttpCard;
|
||||
export default MqttCard;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Col, Card, Popconfirm } from "antd";
|
||||
@ -13,46 +12,44 @@ interface IProps {
|
||||
add?: boolean;
|
||||
}
|
||||
|
||||
class MyDevicesCard extends Component<IProps> {
|
||||
onDelete = () => {
|
||||
function MyDevicesCard(props: IProps) {
|
||||
const onDelete = () => {
|
||||
let req = new DeleteMyDevicesIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
ApplicationStore.deleteMyDevicesIntegration(req, () => {});
|
||||
};
|
||||
|
||||
render() {
|
||||
let actions: any[] = [];
|
||||
let actions: any[] = [];
|
||||
|
||||
if (!!this.props.add) {
|
||||
actions = [
|
||||
<Link to="integrations/mydevices/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="integrations/mydevices/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="myDevices"
|
||||
className="integration-card"
|
||||
cover={<img alt="myDevices" src="/integrations/my_devices.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The myDevices integration forwards events to the myDevices platform." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
if (!!props.add) {
|
||||
actions = [
|
||||
<Link to="mydevices/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="mydevices/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="myDevices"
|
||||
className="integration-card"
|
||||
cover={<img alt="myDevices" src="/integrations/my_devices.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The myDevices integration forwards events to the myDevices platform." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyDevicesCard;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { Form, Input, Button, Select } from "antd";
|
||||
|
||||
@ -9,79 +9,63 @@ interface IProps {
|
||||
onFinish: (obj: MyDevicesIntegration) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
selectedEndpoint: string;
|
||||
customEndpoint: string;
|
||||
}
|
||||
function MyDevicesIntegrationForm(props: IProps) {
|
||||
const [selectedEndpoint, setSelectedEndpoint] = useState<string>("");
|
||||
const [customEndpoint, setCustomEndpoint] = useState<string>("");
|
||||
|
||||
class MyDevicesIntegrationForm extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedEndpoint: "",
|
||||
customEndpoint: "",
|
||||
};
|
||||
}
|
||||
|
||||
onFinish = (values: MyDevicesIntegration.AsObject) => {
|
||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
||||
const onFinish = (values: MyDevicesIntegration.AsObject) => {
|
||||
const v = Object.assign(props.initialValues.toObject(), values);
|
||||
let i = new MyDevicesIntegration();
|
||||
|
||||
i.setApplicationId(v.applicationId);
|
||||
if (v.endpoint === "custom") {
|
||||
i.setEndpoint(this.state.customEndpoint);
|
||||
i.setEndpoint(customEndpoint);
|
||||
} else {
|
||||
i.setEndpoint(v.endpoint);
|
||||
}
|
||||
|
||||
this.props.onFinish(i);
|
||||
props.onFinish(i);
|
||||
};
|
||||
|
||||
onEndpointChange = (v: string) => {
|
||||
this.setState({
|
||||
selectedEndpoint: v,
|
||||
});
|
||||
const onEndpointChange = (v: string) => {
|
||||
setSelectedEndpoint(v);
|
||||
};
|
||||
|
||||
onCustomEndpointChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
customEndpoint: e.target.value,
|
||||
});
|
||||
const onCustomEndpointChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setCustomEndpoint(e.target.value);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
||||
return (
|
||||
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||
<Form.Item
|
||||
label="Select myDevices endpoint"
|
||||
name="endpoint"
|
||||
rules={[{ required: true, message: "Please select a myDevices endpoint!" }]}
|
||||
>
|
||||
<Select onChange={onEndpointChange}>
|
||||
<Select.Option value="https://lora.mydevices.com/v1/networks/chirpstackio/uplink">Cayenne</Select.Option>
|
||||
<Select.Option value="https://lora.iotinabox.com/v1/networks/iotinabox.chirpstackio/uplink">
|
||||
IoT in a Box
|
||||
</Select.Option>
|
||||
<Select.Option value="custom">Custom endpoint URL</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{selectedEndpoint === "custom" && (
|
||||
<Form.Item
|
||||
label="Select myDevices endpoint"
|
||||
name="endpoint"
|
||||
rules={[{ required: true, message: "Please select a myDevices endpoint!" }]}
|
||||
label="myDevices API endpoint"
|
||||
name="customEndpoint"
|
||||
rules={[{ required: true, message: "Please enter an API endpoint!" }]}
|
||||
>
|
||||
<Select onChange={this.onEndpointChange}>
|
||||
<Select.Option value="https://lora.mydevices.com/v1/networks/chirpstackio/uplink">Cayenne</Select.Option>
|
||||
<Select.Option value="https://lora.iotinabox.com/v1/networks/iotinabox.chirpstackio/uplink">
|
||||
IoT in a Box
|
||||
</Select.Option>
|
||||
<Select.Option value="custom">Custom endpoint URL</Select.Option>
|
||||
</Select>
|
||||
<Input onChange={onCustomEndpointChange} />
|
||||
</Form.Item>
|
||||
{this.state.selectedEndpoint === "custom" && (
|
||||
<Form.Item
|
||||
label="myDevices API endpoint"
|
||||
name="customEndpoint"
|
||||
rules={[{ required: true, message: "Please enter an API endpoint!" }]}
|
||||
>
|
||||
<Input onChange={this.onCustomEndpointChange} />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
)}
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyDevicesIntegrationForm;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Col, Card, Popconfirm } from "antd";
|
||||
@ -16,46 +15,44 @@ interface IProps {
|
||||
add?: boolean;
|
||||
}
|
||||
|
||||
class PilotThingsCard extends Component<IProps> {
|
||||
onDelete = () => {
|
||||
function PilotThingsCard(props: IProps) {
|
||||
const onDelete = () => {
|
||||
let req = new DeletePilotThingsIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
ApplicationStore.deletePilotThingsIntegration(req, () => {});
|
||||
};
|
||||
|
||||
render() {
|
||||
let actions: any[] = [];
|
||||
let actions: any[] = [];
|
||||
|
||||
if (!!this.props.add) {
|
||||
actions = [
|
||||
<Link to="integrations/pilot-things/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="integrations/pilot-things/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="Pilot Things"
|
||||
className="integration-card"
|
||||
cover={<img alt="Pilot Things" src="/integrations/pilot_things.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The Pilot Things integration forwards messages to a Pilot Things instance." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
if (!!props.add) {
|
||||
actions = [
|
||||
<Link to="pilot-things/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="pilot-things/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="Pilot Things"
|
||||
className="integration-card"
|
||||
cover={<img alt="Pilot Things" src="/integrations/pilot_things.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The Pilot Things integration forwards messages to a Pilot Things instance." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default PilotThingsCard;
|
||||
|
@ -1,5 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Form, Input, Button } from "antd";
|
||||
|
||||
import { PilotThingsIntegration } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||
@ -9,43 +7,41 @@ interface IProps {
|
||||
onFinish: (obj: PilotThingsIntegration) => void;
|
||||
}
|
||||
|
||||
class PilotThingsIntegrationForm extends Component<IProps> {
|
||||
onFinish = (values: PilotThingsIntegration.AsObject) => {
|
||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
||||
function PilotThingsIntegrationForm(props: IProps) {
|
||||
const onFinish = (values: PilotThingsIntegration.AsObject) => {
|
||||
const v = Object.assign(props.initialValues.toObject(), values);
|
||||
let i = new PilotThingsIntegration();
|
||||
|
||||
i.setApplicationId(v.applicationId);
|
||||
i.setServer(v.server);
|
||||
i.setToken(v.token);
|
||||
|
||||
this.props.onFinish(i);
|
||||
props.onFinish(i);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
||||
<Form.Item
|
||||
label="Pilot Things server"
|
||||
name="server"
|
||||
rules={[{ required: true, message: "Please enter a Pilot Things server!" }]}
|
||||
>
|
||||
<Input placeholder="https://host:port" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Authentication token"
|
||||
name="token"
|
||||
rules={[{ required: true, message: "Please enter a Pilot Things token!" }]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||
<Form.Item
|
||||
label="Pilot Things server"
|
||||
name="server"
|
||||
rules={[{ required: true, message: "Please enter a Pilot Things server!" }]}
|
||||
>
|
||||
<Input placeholder="https://host:port" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Authentication token"
|
||||
name="token"
|
||||
rules={[{ required: true, message: "Please enter a Pilot Things token!" }]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default PilotThingsIntegrationForm;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Col, Card, Popconfirm } from "antd";
|
||||
@ -16,46 +15,44 @@ interface IProps {
|
||||
add?: boolean;
|
||||
}
|
||||
|
||||
class ThingsBoardCard extends Component<IProps> {
|
||||
onDelete = () => {
|
||||
function ThingsBoardCard(props: IProps) {
|
||||
const onDelete = () => {
|
||||
let req = new DeleteThingsBoardIntegrationRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
ApplicationStore.deleteThingsBoardIntegration(req, () => {});
|
||||
};
|
||||
|
||||
render() {
|
||||
let actions: any[] = [];
|
||||
let actions: any[] = [];
|
||||
|
||||
if (!!this.props.add) {
|
||||
actions = [
|
||||
<Link to="integrations/thingsboard/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="integrations/thingsboard/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="ThingsBoard"
|
||||
className="integration-card"
|
||||
cover={<img alt="ThingsBoard" src="/integrations/thingsboard.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The ThingsBoard integration forwards events to a ThingsBoard instance." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
if (!!props.add) {
|
||||
actions = [
|
||||
<Link to="thingsboard/create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
<Link to="thingsboard/edit">
|
||||
<EditOutlined />
|
||||
</Link>,
|
||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title="ThingsBoard"
|
||||
className="integration-card"
|
||||
cover={<img alt="ThingsBoard" src="/integrations/thingsboard.png" style={{ padding: 1 }} />}
|
||||
actions={actions}
|
||||
>
|
||||
<Card.Meta description="The ThingsBoard integration forwards events to a ThingsBoard instance." />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default ThingsBoardCard;
|
||||
|
@ -1,5 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Form, Input, Button, Typography } from "antd";
|
||||
|
||||
import { ThingsBoardIntegration } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||
@ -9,41 +7,44 @@ interface IProps {
|
||||
onFinish: (obj: ThingsBoardIntegration) => void;
|
||||
}
|
||||
|
||||
class ThingsBoardIntegrationForm extends Component<IProps> {
|
||||
onFinish = (values: ThingsBoardIntegration.AsObject) => {
|
||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
||||
function ThingsBoardIntegrationForm(props: IProps) {
|
||||
const onFinish = (values: ThingsBoardIntegration.AsObject) => {
|
||||
const v = Object.assign(props.initialValues.toObject(), values);
|
||||
let i = new ThingsBoardIntegration();
|
||||
|
||||
i.setApplicationId(v.applicationId);
|
||||
i.setServer(v.server);
|
||||
|
||||
this.props.onFinish(i);
|
||||
props.onFinish(i);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
||||
<Form.Item
|
||||
label="ThingsBoard server"
|
||||
name="server"
|
||||
rules={[{ required: true, message: "Please enter the address to the ThingsBoard server!" }]}
|
||||
>
|
||||
<Input placeholder="http://host:port" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Typography.Paragraph>
|
||||
Each device must have a 'ThingsBoardAccessToken' variable assigned. This access-token is generated by
|
||||
ThingsBoard.
|
||||
</Typography.Paragraph>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||
<Form.Item
|
||||
label="ThingsBoard server"
|
||||
name="server"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please enter the address to the ThingsBoard server!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="http://host:port" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Typography.Paragraph>
|
||||
Each device must have a 'ThingsBoardAccessToken' variable assigned. This access-token is generated by
|
||||
ThingsBoard.
|
||||
</Typography.Paragraph>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default ThingsBoardIntegrationForm;
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { presetPalettes } from "@ant-design/colors";
|
||||
import { Space, Breadcrumb, Card, Row, Col, PageHeader, Empty } from "antd";
|
||||
import { Space, Breadcrumb, Card, Row, Col, Empty } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import moment from "moment";
|
||||
import { LatLngTuple, PointTuple } from "leaflet";
|
||||
@ -27,167 +28,125 @@ import InternalStore from "../../stores/InternalStore";
|
||||
import GatewayStore from "../../stores/GatewayStore";
|
||||
import Map, { Marker, MarkerColor } from "../../components/Map";
|
||||
|
||||
interface GatewaysMapState {
|
||||
items: GatewayListItem[];
|
||||
}
|
||||
function GatewaysMap() {
|
||||
const [items, setItems] = useState<GatewayListItem[]>([]);
|
||||
|
||||
class GatewaysMap extends Component<{}, GatewaysMapState> {
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
loadData = () => {
|
||||
useEffect(() => {
|
||||
let req = new ListGatewaysRequest();
|
||||
req.setLimit(9999);
|
||||
GatewayStore.list(req, (resp: ListGatewaysResponse) => {
|
||||
this.setState({
|
||||
items: resp.getResultList(),
|
||||
});
|
||||
setItems(resp.getResultList());
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (items.length === 0) {
|
||||
return <Empty />;
|
||||
}
|
||||
|
||||
const boundsOptions: {
|
||||
padding: PointTuple;
|
||||
} = {
|
||||
padding: [50, 50],
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.items.length === 0) {
|
||||
return <Empty />;
|
||||
let bounds: LatLngTuple[] = [];
|
||||
let markers: any[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const pos: LatLngTuple = [item.getLocation()!.getLatitude(), item.getLocation()!.getLongitude()];
|
||||
bounds.push(pos);
|
||||
|
||||
let color: MarkerColor = "orange";
|
||||
let lastSeen: string = "Never seen online";
|
||||
|
||||
if (item.getState() === GatewayState.OFFLINE) {
|
||||
color = "red";
|
||||
} else if (item.getState() === GatewayState.ONLINE) {
|
||||
color = "green";
|
||||
}
|
||||
|
||||
const boundsOptions: {
|
||||
padding: PointTuple;
|
||||
} = {
|
||||
padding: [50, 50],
|
||||
};
|
||||
|
||||
let bounds: LatLngTuple[] = [];
|
||||
let markers: any[] = [];
|
||||
|
||||
for (const item of this.state.items) {
|
||||
const pos: LatLngTuple = [item.getLocation()!.getLatitude(), item.getLocation()!.getLongitude()];
|
||||
bounds.push(pos);
|
||||
|
||||
let color: MarkerColor = "orange";
|
||||
let lastSeen: string = "Never seen online";
|
||||
|
||||
if (item.getState() === GatewayState.OFFLINE) {
|
||||
color = "red";
|
||||
} else if (item.getState() === GatewayState.ONLINE) {
|
||||
color = "green";
|
||||
}
|
||||
|
||||
if (item.getLastSeenAt() !== undefined) {
|
||||
let ts = moment(item.getLastSeenAt()!.toDate());
|
||||
lastSeen = ts.fromNow();
|
||||
}
|
||||
|
||||
markers.push(
|
||||
<Marker position={pos} faIcon="wifi" color={color}>
|
||||
<Popup>
|
||||
<Link to={`/tenants/${item.getTenantId()}/gateways/${item.getGatewayId()}`}>{item.getName()}</Link>
|
||||
<br />
|
||||
{item.getGatewayId()}
|
||||
<br />
|
||||
<br />
|
||||
{lastSeen}
|
||||
</Popup>
|
||||
</Marker>,
|
||||
);
|
||||
if (item.getLastSeenAt() !== undefined) {
|
||||
let ts = moment(item.getLastSeenAt()!.toDate());
|
||||
lastSeen = ts.fromNow();
|
||||
}
|
||||
|
||||
return (
|
||||
<Map height={500} bounds={bounds} boundsOptions={boundsOptions}>
|
||||
{markers}
|
||||
</Map>
|
||||
markers.push(
|
||||
<Marker position={pos} faIcon="wifi" color={color}>
|
||||
<Popup>
|
||||
<Link to={`/tenants/${item.getTenantId()}/gateways/${item.getGatewayId()}`}>{item.getName()}</Link>
|
||||
<br />
|
||||
{item.getGatewayId()}
|
||||
<br />
|
||||
<br />
|
||||
{lastSeen}
|
||||
</Popup>
|
||||
</Marker>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Map height={500} bounds={bounds} boundsOptions={boundsOptions}>
|
||||
{markers}
|
||||
</Map>
|
||||
);
|
||||
}
|
||||
|
||||
interface GatewayProps {
|
||||
summary?: GetGatewaysSummaryResponse;
|
||||
}
|
||||
|
||||
class GatewaysActiveInactive extends Component<GatewayProps> {
|
||||
render() {
|
||||
if (
|
||||
this.props.summary === undefined ||
|
||||
(this.props.summary.getNeverSeenCount() === 0 &&
|
||||
this.props.summary.getOfflineCount() === 0 &&
|
||||
this.props.summary.getOnlineCount() === 0)
|
||||
) {
|
||||
return <Empty />;
|
||||
}
|
||||
|
||||
const data = {
|
||||
labels: ["Never seen", "Offline", "Online"],
|
||||
datasets: [
|
||||
{
|
||||
data: [
|
||||
this.props.summary.getNeverSeenCount(),
|
||||
this.props.summary.getOfflineCount(),
|
||||
this.props.summary.getOnlineCount(),
|
||||
],
|
||||
backgroundColor: [presetPalettes.orange.primary, presetPalettes.red.primary, presetPalettes.green.primary],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options: {
|
||||
animation: false;
|
||||
} = {
|
||||
animation: false,
|
||||
};
|
||||
|
||||
return <Doughnut data={data} options={options} className="chart-doughtnut" />;
|
||||
function DevicesActiveInactive({ summary }: { summary?: GetDevicesSummaryResponse }) {
|
||||
if (
|
||||
summary === undefined ||
|
||||
(summary.getNeverSeenCount() === 0 && summary.getInactiveCount() === 0 && summary.getActiveCount() === 0)
|
||||
) {
|
||||
return <Empty />;
|
||||
}
|
||||
|
||||
const data = {
|
||||
labels: ["Never seen", "Inactive", "Active"],
|
||||
datasets: [
|
||||
{
|
||||
data: [summary.getNeverSeenCount(), summary.getInactiveCount(), summary.getActiveCount()],
|
||||
backgroundColor: [presetPalettes.orange.primary, presetPalettes.red.primary, presetPalettes.green.primary],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options: {
|
||||
animation: false;
|
||||
} = {
|
||||
animation: false,
|
||||
};
|
||||
|
||||
return <Doughnut data={data} options={options} className="chart-doughtnut" />;
|
||||
}
|
||||
|
||||
interface DeviceProps {
|
||||
summary?: GetDevicesSummaryResponse;
|
||||
}
|
||||
|
||||
class DevicesActiveInactive extends Component<DeviceProps> {
|
||||
render() {
|
||||
if (
|
||||
this.props.summary === undefined ||
|
||||
(this.props.summary.getNeverSeenCount() === 0 &&
|
||||
this.props.summary.getInactiveCount() === 0 &&
|
||||
this.props.summary.getActiveCount() === 0)
|
||||
) {
|
||||
return <Empty />;
|
||||
}
|
||||
|
||||
const data = {
|
||||
labels: ["Never seen", "Inactive", "Active"],
|
||||
datasets: [
|
||||
{
|
||||
data: [
|
||||
this.props.summary.getNeverSeenCount(),
|
||||
this.props.summary.getInactiveCount(),
|
||||
this.props.summary.getActiveCount(),
|
||||
],
|
||||
backgroundColor: [presetPalettes.orange.primary, presetPalettes.red.primary, presetPalettes.green.primary],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options: {
|
||||
animation: false;
|
||||
} = {
|
||||
animation: false,
|
||||
};
|
||||
|
||||
return <Doughnut data={data} options={options} className="chart-doughtnut" />;
|
||||
function GatewaysActiveInactive({ summary }: { summary?: GetGatewaysSummaryResponse }) {
|
||||
if (
|
||||
summary === undefined ||
|
||||
(summary.getNeverSeenCount() === 0 && summary.getOfflineCount() === 0 && summary.getOnlineCount() === 0)
|
||||
) {
|
||||
return <Empty />;
|
||||
}
|
||||
|
||||
const data = {
|
||||
labels: ["Never seen", "Offline", "Online"],
|
||||
datasets: [
|
||||
{
|
||||
data: [summary.getNeverSeenCount(), summary.getOfflineCount(), summary.getOnlineCount()],
|
||||
backgroundColor: [presetPalettes.orange.primary, presetPalettes.red.primary, presetPalettes.green.primary],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options: {
|
||||
animation: false;
|
||||
} = {
|
||||
animation: false,
|
||||
};
|
||||
|
||||
return <Doughnut data={data} options={options} className="chart-doughtnut" />;
|
||||
}
|
||||
|
||||
class DevicesDataRates extends Component<DeviceProps> {
|
||||
getColor = (dr: number) => {
|
||||
function DevicesDataRates({ summary }: { summary?: GetDevicesSummaryResponse }) {
|
||||
const getColor = (dr: number) => {
|
||||
return [
|
||||
"#ff5722",
|
||||
"#ff9800",
|
||||
@ -207,109 +166,92 @@ class DevicesDataRates extends Component<DeviceProps> {
|
||||
][dr];
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.summary === undefined || this.props.summary.getDrCountMap().toArray().length === 0) {
|
||||
return <Empty />;
|
||||
}
|
||||
|
||||
let data: {
|
||||
labels: string[];
|
||||
datasets: {
|
||||
data: number[];
|
||||
backgroundColor: string[];
|
||||
}[];
|
||||
} = {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
data: [],
|
||||
backgroundColor: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
for (const elm of this.props.summary.getDrCountMap().toArray()) {
|
||||
data.labels.push(`DR${elm[0]}`);
|
||||
data.datasets[0].data.push(elm[1]);
|
||||
data.datasets[0].backgroundColor.push(this.getColor(elm[0]));
|
||||
}
|
||||
|
||||
const options: {
|
||||
animation: false;
|
||||
} = {
|
||||
animation: false,
|
||||
};
|
||||
|
||||
return <Doughnut data={data} options={options} className="chart-doughtnut" />;
|
||||
}
|
||||
}
|
||||
|
||||
interface IProps {}
|
||||
|
||||
interface IState {
|
||||
gatewaysSummary?: GetGatewaysSummaryResponse;
|
||||
devicesSummary?: GetDevicesSummaryResponse;
|
||||
}
|
||||
|
||||
class Dashboard extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
if (summary === undefined || summary.getDrCountMap().toArray().length === 0) {
|
||||
return <Empty />;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let data: {
|
||||
labels: string[];
|
||||
datasets: {
|
||||
data: number[];
|
||||
backgroundColor: string[];
|
||||
}[];
|
||||
} = {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
data: [],
|
||||
backgroundColor: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
for (const elm of summary.getDrCountMap().toArray()) {
|
||||
data.labels.push(`DR${elm[0]}`);
|
||||
data.datasets[0].data.push(elm[1]);
|
||||
data.datasets[0].backgroundColor.push(getColor(elm[0]));
|
||||
}
|
||||
|
||||
const options: {
|
||||
animation: false;
|
||||
} = {
|
||||
animation: false,
|
||||
};
|
||||
|
||||
return <Doughnut data={data} options={options} className="chart-doughtnut" />;
|
||||
}
|
||||
|
||||
function Dashboard() {
|
||||
const [gatewaysSummary, setGatewaysSummary] = useState<GetGatewaysSummaryResponse | undefined>(undefined);
|
||||
const [devicesSummary, setDevicesSummary] = useState<GetDevicesSummaryResponse | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
InternalStore.getGatewaysSummary(new GetGatewaysSummaryRequest(), (resp: GetGatewaysSummaryResponse) => {
|
||||
this.setState({
|
||||
gatewaysSummary: resp,
|
||||
});
|
||||
setGatewaysSummary(resp);
|
||||
});
|
||||
|
||||
InternalStore.getDevicesSummary(new GetDevicesSummaryRequest(), (resp: GetDevicesSummaryResponse) => {
|
||||
this.setState({
|
||||
devicesSummary: resp,
|
||||
});
|
||||
setDevicesSummary(resp);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Network Server</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Dashboard</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Dashboard"
|
||||
/>
|
||||
<Row gutter={24}>
|
||||
<Col span={8}>
|
||||
<Card title="Active devices">
|
||||
<DevicesActiveInactive summary={this.state.devicesSummary} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card title="Active gateways">
|
||||
<GatewaysActiveInactive summary={this.state.gatewaysSummary} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card title="Device data-rate usage">
|
||||
<DevicesDataRates summary={this.state.devicesSummary} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Card title="Gateway map">
|
||||
<GatewaysMap />
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Network Server</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Dashboard</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Dashboard"
|
||||
/>
|
||||
<Row gutter={24}>
|
||||
<Col span={8}>
|
||||
<Card title="Active devices">
|
||||
<DevicesActiveInactive summary={devicesSummary} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card title="Active gateways">
|
||||
<GatewaysActiveInactive summary={gatewaysSummary} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card title="Device data-rate usage">
|
||||
<DevicesDataRates summary={devicesSummary} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Card title="Gateway map">
|
||||
<GatewaysMap />
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link, RouteComponentProps } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
import { Space, Breadcrumb, Card, PageHeader } from "antd";
|
||||
import { Space, Breadcrumb, Card } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import { MacVersion, RegParamsRevision } from "@chirpstack/chirpstack-api-grpc-web/common/common_pb";
|
||||
import {
|
||||
@ -12,86 +12,86 @@ import {
|
||||
import DeviceProfileTemplateForm from "./DeviceProfileTemplateForm";
|
||||
import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore";
|
||||
|
||||
class CreateDeviceProfileTemplate extends Component<RouteComponentProps> {
|
||||
onFinish = (obj: DeviceProfileTemplate) => {
|
||||
function CreateDeviceProfileTemplate() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = (obj: DeviceProfileTemplate) => {
|
||||
let req = new CreateDeviceProfileTemplateRequest();
|
||||
req.setDeviceProfileTemplate(obj);
|
||||
|
||||
DeviceProfileTemplateStore.create(req, () => {
|
||||
this.props.history.push(`/device-profile-templates`);
|
||||
navigate(`/device-profile-templates`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const codecScript = `// Decode uplink function.
|
||||
//
|
||||
// Input is an object with the following fields:
|
||||
// - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0]
|
||||
// - fPort = Uplink fPort.
|
||||
// - variables = Object containing the configured device variables.
|
||||
//
|
||||
// Output must be an object with the following fields:
|
||||
// - data = Object representing the decoded payload.
|
||||
function decodeUplink(input) {
|
||||
return {
|
||||
data: {
|
||||
temp: 22.5
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Encode downlink function.
|
||||
//
|
||||
// Input is an object with the following fields:
|
||||
// - data = Object representing the payload that must be encoded.
|
||||
// - variables = Object containing the configured device variables.
|
||||
//
|
||||
// Output must be an object with the following fields:
|
||||
// - bytes = Byte array containing the downlink payload.
|
||||
function encodeDownlink(input) {
|
||||
return {
|
||||
bytes: [225, 230, 255, 0]
|
||||
};
|
||||
}
|
||||
`;
|
||||
|
||||
let deviceProfileTemplate = new DeviceProfileTemplate();
|
||||
deviceProfileTemplate.setPayloadCodecScript(codecScript);
|
||||
deviceProfileTemplate.setSupportsOtaa(true);
|
||||
deviceProfileTemplate.setUplinkInterval(3600);
|
||||
deviceProfileTemplate.setDeviceStatusReqInterval(1);
|
||||
deviceProfileTemplate.setAdrAlgorithmId("default");
|
||||
deviceProfileTemplate.setMacVersion(MacVersion.LORAWAN_1_0_3);
|
||||
deviceProfileTemplate.setRegParamsRevision(RegParamsRevision.A);
|
||||
deviceProfileTemplate.setFlushQueueOnActivate(true);
|
||||
deviceProfileTemplate.setAutoDetectMeasurements(true);
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Network Server</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/device-profile-templates`}>Device-profile templates</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Add</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Add device-profile template"
|
||||
/>
|
||||
<Card>
|
||||
<DeviceProfileTemplateForm initialValues={deviceProfileTemplate} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
const codecScript = `// Decode uplink function.
|
||||
//
|
||||
// Input is an object with the following fields:
|
||||
// - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0]
|
||||
// - fPort = Uplink fPort.
|
||||
// - variables = Object containing the configured device variables.
|
||||
//
|
||||
// Output must be an object with the following fields:
|
||||
// - data = Object representing the decoded payload.
|
||||
function decodeUplink(input) {
|
||||
return {
|
||||
data: {
|
||||
temp: 22.5
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Encode downlink function.
|
||||
//
|
||||
// Input is an object with the following fields:
|
||||
// - data = Object representing the payload that must be encoded.
|
||||
// - variables = Object containing the configured device variables.
|
||||
//
|
||||
// Output must be an object with the following fields:
|
||||
// - bytes = Byte array containing the downlink payload.
|
||||
function encodeDownlink(input) {
|
||||
return {
|
||||
bytes: [225, 230, 255, 0]
|
||||
};
|
||||
}
|
||||
`;
|
||||
|
||||
let deviceProfileTemplate = new DeviceProfileTemplate();
|
||||
deviceProfileTemplate.setPayloadCodecScript(codecScript);
|
||||
deviceProfileTemplate.setSupportsOtaa(true);
|
||||
deviceProfileTemplate.setUplinkInterval(3600);
|
||||
deviceProfileTemplate.setDeviceStatusReqInterval(1);
|
||||
deviceProfileTemplate.setAdrAlgorithmId("default");
|
||||
deviceProfileTemplate.setMacVersion(MacVersion.LORAWAN_1_0_3);
|
||||
deviceProfileTemplate.setRegParamsRevision(RegParamsRevision.A);
|
||||
deviceProfileTemplate.setFlushQueueOnActivate(true);
|
||||
deviceProfileTemplate.setAutoDetectMeasurements(true);
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Network Server</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/device-profile-templates`}>Device-profile templates</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Add</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Add device-profile template"
|
||||
/>
|
||||
<Card>
|
||||
<DeviceProfileTemplateForm initialValues={deviceProfileTemplate} onFinish={onFinish} />
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateDeviceProfileTemplate;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { Form, Input, Select, InputNumber, Switch, Row, Col, Button, Tabs, Card } from "antd";
|
||||
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
@ -17,37 +17,20 @@ interface IProps {
|
||||
update?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
supportsOtaa: boolean;
|
||||
supportsClassB: boolean;
|
||||
supportsClassC: boolean;
|
||||
payloadCodecRuntime: CodecRuntime;
|
||||
adrAlgorithms: [string, string][];
|
||||
}
|
||||
function DeviceProfileTemplateForm(props: IProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [supportsOtaa, setSupportsOtaa] = useState<boolean>(false);
|
||||
const [supportsClassB, setSupportsClassB] = useState<boolean>(false);
|
||||
const [supportsClassC, setSupportsClassC] = useState<boolean>(false);
|
||||
const [payloadCodecRuntime, setPayloadCodecRuntime] = useState<CodecRuntime>(CodecRuntime.NONE);
|
||||
const [adrAlgorithms, setAdrAlgorithms] = useState<[string, string][]>([]);
|
||||
|
||||
class DeviceProfileTemplateForm extends Component<IProps, IState> {
|
||||
formRef = React.createRef<any>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
supportsOtaa: false,
|
||||
supportsClassB: false,
|
||||
supportsClassC: false,
|
||||
payloadCodecRuntime: CodecRuntime.NONE,
|
||||
adrAlgorithms: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const v = this.props.initialValues;
|
||||
|
||||
this.setState({
|
||||
supportsOtaa: v.getSupportsOtaa(),
|
||||
supportsClassB: v.getSupportsClassB(),
|
||||
supportsClassC: v.getSupportsClassC(),
|
||||
payloadCodecRuntime: v.getPayloadCodecRuntime(),
|
||||
});
|
||||
useEffect(() => {
|
||||
const v = props.initialValues;
|
||||
setSupportsOtaa(v.getSupportsOtaa());
|
||||
setSupportsClassB(v.getSupportsClassB());
|
||||
setSupportsClassC(v.getSupportsClassC());
|
||||
setPayloadCodecRuntime(v.getPayloadCodecRuntime());
|
||||
|
||||
DeviceProfileStore.listAdrAlgorithms((resp: ListDeviceProfileAdrAlgorithmsResponse) => {
|
||||
let adrAlgorithms: [string, string][] = [];
|
||||
@ -55,14 +38,12 @@ class DeviceProfileTemplateForm extends Component<IProps, IState> {
|
||||
adrAlgorithms.push([a.getId(), a.getName()]);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
adrAlgorithms: adrAlgorithms,
|
||||
});
|
||||
setAdrAlgorithms(adrAlgorithms);
|
||||
});
|
||||
}
|
||||
}, [props.initialValues]);
|
||||
|
||||
onFinish = (values: DeviceProfileTemplate.AsObject) => {
|
||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
||||
const onFinish = (values: DeviceProfileTemplate.AsObject) => {
|
||||
const v = Object.assign(props.initialValues.toObject(), values);
|
||||
let dp = new DeviceProfileTemplate();
|
||||
dp.setId(v.id);
|
||||
|
||||
@ -114,454 +95,487 @@ class DeviceProfileTemplateForm extends Component<IProps, IState> {
|
||||
}
|
||||
dp.setAutoDetectMeasurements(v.autoDetectMeasurements);
|
||||
|
||||
this.props.onFinish(dp);
|
||||
props.onFinish(dp);
|
||||
};
|
||||
|
||||
onSupportsOtaaChange = (checked: boolean) => {
|
||||
this.setState({
|
||||
supportsOtaa: checked,
|
||||
});
|
||||
const onSupportsOtaaChange = (checked: boolean) => {
|
||||
setSupportsOtaa(checked);
|
||||
};
|
||||
|
||||
onSupportsClassBChnage = (checked: boolean) => {
|
||||
this.setState({
|
||||
supportsClassB: checked,
|
||||
});
|
||||
const onSupportsClassBChnage = (checked: boolean) => {
|
||||
setSupportsClassB(checked);
|
||||
};
|
||||
|
||||
onSupportsClassCChange = (checked: boolean) => {
|
||||
this.setState({
|
||||
supportsClassC: checked,
|
||||
});
|
||||
const onSupportsClassCChange = (checked: boolean) => {
|
||||
setSupportsClassC(checked);
|
||||
};
|
||||
|
||||
onPayloadCodecRuntimeChange = (value: CodecRuntime) => {
|
||||
this.setState({
|
||||
payloadCodecRuntime: value,
|
||||
});
|
||||
const onPayloadCodecRuntimeChange = (value: CodecRuntime) => {
|
||||
setPayloadCodecRuntime(value);
|
||||
};
|
||||
|
||||
render() {
|
||||
const adrOptions = this.state.adrAlgorithms.map(v => <Select.Option value={v[0]}>{v[1]}</Select.Option>);
|
||||
const adrOptions = adrAlgorithms.map(v => <Select.Option value={v[0]}>{v[1]}</Select.Option>);
|
||||
|
||||
return (
|
||||
<Form
|
||||
layout="vertical"
|
||||
initialValues={this.props.initialValues.toObject()}
|
||||
onFinish={this.onFinish}
|
||||
ref={this.formRef}
|
||||
>
|
||||
<Tabs>
|
||||
<Tabs.TabPane tab="General" key="1">
|
||||
<Form.Item
|
||||
label="ID"
|
||||
name="id"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
pattern: new RegExp(/^[\w-]*$/g),
|
||||
message: "Please enter a valid id!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input disabled={!!this.props.update} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Vendor" name="vendor" rules={[{ required: true, message: "Please enter a vendor!" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Firmware version"
|
||||
name="firmware"
|
||||
rules={[{ required: true, message: "Please enter a firmware version!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Description" name="description">
|
||||
<Input.TextArea rows={6} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Region" name="region" rules={[{ required: true, message: "Please select a region!" }]}>
|
||||
<Select>
|
||||
<Select.Option value={Region.AS923}>AS923</Select.Option>
|
||||
<Select.Option value={Region.AS923_2}>AS923-2</Select.Option>
|
||||
<Select.Option value={Region.AS923_3}>AS923-3</Select.Option>
|
||||
<Select.Option value={Region.AS923_4}>AS923-4</Select.Option>
|
||||
<Select.Option value={Region.AU915}>AU915</Select.Option>
|
||||
<Select.Option value={Region.CN779}>CN779</Select.Option>
|
||||
<Select.Option value={Region.EU433}>EU433</Select.Option>
|
||||
<Select.Option value={Region.EU868}>EU868</Select.Option>
|
||||
<Select.Option value={Region.IN865}>IN865</Select.Option>
|
||||
<Select.Option value={Region.ISM2400}>ISM2400</Select.Option>
|
||||
<Select.Option value={Region.KR920}>KR920</Select.Option>
|
||||
<Select.Option value={Region.RU864}>RU864</Select.Option>
|
||||
<Select.Option value={Region.US915}>US915</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Row gutter={24}>
|
||||
return (
|
||||
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
|
||||
<Tabs>
|
||||
<Tabs.TabPane tab="General" key="1">
|
||||
<Form.Item
|
||||
label="ID"
|
||||
name="id"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
pattern: new RegExp(/^[\w-]*$/g),
|
||||
message: "Please enter a valid id!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input disabled={!!props.update} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Vendor" name="vendor" rules={[{ required: true, message: "Please enter a vendor!" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Firmware version"
|
||||
name="firmware"
|
||||
rules={[{ required: true, message: "Please enter a firmware version!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Description" name="description">
|
||||
<Input.TextArea rows={6} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Region" name="region" rules={[{ required: true, message: "Please select a region!" }]}>
|
||||
<Select>
|
||||
<Select.Option value={Region.AS923}>AS923</Select.Option>
|
||||
<Select.Option value={Region.AS923_2}>AS923-2</Select.Option>
|
||||
<Select.Option value={Region.AS923_3}>AS923-3</Select.Option>
|
||||
<Select.Option value={Region.AS923_4}>AS923-4</Select.Option>
|
||||
<Select.Option value={Region.AU915}>AU915</Select.Option>
|
||||
<Select.Option value={Region.CN779}>CN779</Select.Option>
|
||||
<Select.Option value={Region.EU433}>EU433</Select.Option>
|
||||
<Select.Option value={Region.EU868}>EU868</Select.Option>
|
||||
<Select.Option value={Region.IN865}>IN865</Select.Option>
|
||||
<Select.Option value={Region.ISM2400}>ISM2400</Select.Option>
|
||||
<Select.Option value={Region.KR920}>KR920</Select.Option>
|
||||
<Select.Option value={Region.RU864}>RU864</Select.Option>
|
||||
<Select.Option value={Region.US915}>US915</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="MAC version"
|
||||
tooltip="The LoRaWAN MAC version supported by the device."
|
||||
name="macVersion"
|
||||
rules={[{ required: true, message: "Please select a MAC version!" }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value={MacVersion.LORAWAN_1_0_0}>LoRaWAN 1.0.0</Select.Option>
|
||||
<Select.Option value={MacVersion.LORAWAN_1_0_1}>LoRaWAN 1.0.1</Select.Option>
|
||||
<Select.Option value={MacVersion.LORAWAN_1_0_2}>LoRaWAN 1.0.2</Select.Option>
|
||||
<Select.Option value={MacVersion.LORAWAN_1_0_3}>LoRaWAN 1.0.3</Select.Option>
|
||||
<Select.Option value={MacVersion.LORAWAN_1_0_4}>LoRaWAN 1.0.4</Select.Option>
|
||||
<Select.Option value={MacVersion.LORAWAN_1_1_0}>LoRaWAN 1.1.0</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Regional parameters revision"
|
||||
tooltip="Revision of the Regional Parameters specification supported by the device."
|
||||
name="regParamsRevision"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please select a regional parameters revision!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value={RegParamsRevision.A}>A</Select.Option>
|
||||
<Select.Option value={RegParamsRevision.B}>B</Select.Option>
|
||||
<Select.Option value={RegParamsRevision.RP002_1_0_0}>RP002-1.0.0</Select.Option>
|
||||
<Select.Option value={RegParamsRevision.RP002_1_0_1}>RP002-1.0.1</Select.Option>
|
||||
<Select.Option value={RegParamsRevision.RP002_1_0_2}>RP002-1.0.2</Select.Option>
|
||||
<Select.Option value={RegParamsRevision.RP002_1_0_3}>RP002-1.0.3</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item
|
||||
label="ADR algorithm"
|
||||
tooltip="The ADR algorithm that will be used for controlling the device data-rate."
|
||||
name="adrAlgorithmId"
|
||||
rules={[{ required: true, message: "Please select an ADR algorithm!" }]}
|
||||
>
|
||||
<Select>{adrOptions}</Select>
|
||||
</Form.Item>
|
||||
<Row gutter={24}>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
label="Flush queue on activate"
|
||||
name="flushQueueOnActivate"
|
||||
valuePropName="checked"
|
||||
tooltip="If enabled, the device-queue will be flushed on ABP or OTAA activation."
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
label="Expected uplink interval (secs)"
|
||||
tooltip="The expected interval in seconds in which the device sends uplink messages. This is used to determine if a device is active or inactive."
|
||||
name="uplinkInterval"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please enter an uplink interval!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
label="Device-status request frequency (req/day)"
|
||||
tooltip="Frequency to initiate an End-Device status request (request/day). Set to 0 to disable."
|
||||
name="deviceStatusReqInterval"
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Join (OTAA / ABP)" key="2">
|
||||
<Form.Item label="Device supports OTAA" name="supportsOtaa" valuePropName="checked">
|
||||
<Switch onChange={onSupportsOtaaChange} />
|
||||
</Form.Item>
|
||||
{!supportsOtaa && (
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="MAC version"
|
||||
tooltip="The LoRaWAN MAC version supported by the device."
|
||||
name="macVersion"
|
||||
rules={[{ required: true, message: "Please select a MAC version!" }]}
|
||||
label="RX1 delay"
|
||||
name="abpRx1Delay"
|
||||
rules={[{ required: true, message: "Please enter a RX1 delay!" }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value={MacVersion.LORAWAN_1_0_0}>LoRaWAN 1.0.0</Select.Option>
|
||||
<Select.Option value={MacVersion.LORAWAN_1_0_1}>LoRaWAN 1.0.1</Select.Option>
|
||||
<Select.Option value={MacVersion.LORAWAN_1_0_2}>LoRaWAN 1.0.2</Select.Option>
|
||||
<Select.Option value={MacVersion.LORAWAN_1_0_3}>LoRaWAN 1.0.3</Select.Option>
|
||||
<Select.Option value={MacVersion.LORAWAN_1_0_4}>LoRaWAN 1.0.4</Select.Option>
|
||||
<Select.Option value={MacVersion.LORAWAN_1_1_0}>LoRaWAN 1.1.0</Select.Option>
|
||||
</Select>
|
||||
<InputNumber min={0} max={15} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Regional parameters revision"
|
||||
tooltip="Revision of the Regional Parameters specification supported by the device."
|
||||
name="regParamsRevision"
|
||||
rules={[{ required: true, message: "Please select a regional parameters revision!" }]}
|
||||
label="RX1 data-rate offset"
|
||||
tooltip="Please refer the LoRaWAN Regional Parameters specification for valid values."
|
||||
name="abpRx1DrOffset"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please enter a RX1 data-rate offset!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value={RegParamsRevision.A}>A</Select.Option>
|
||||
<Select.Option value={RegParamsRevision.B}>B</Select.Option>
|
||||
<Select.Option value={RegParamsRevision.RP002_1_0_0}>RP002-1.0.0</Select.Option>
|
||||
<Select.Option value={RegParamsRevision.RP002_1_0_1}>RP002-1.0.1</Select.Option>
|
||||
<Select.Option value={RegParamsRevision.RP002_1_0_2}>RP002-1.0.2</Select.Option>
|
||||
<Select.Option value={RegParamsRevision.RP002_1_0_3}>RP002-1.0.3</Select.Option>
|
||||
</Select>
|
||||
<InputNumber min={0} max={15} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item
|
||||
label="ADR algorithm"
|
||||
tooltip="The ADR algorithm that will be used for controlling the device data-rate."
|
||||
name="adrAlgorithmId"
|
||||
rules={[{ required: true, message: "Please select an ADR algorithm!" }]}
|
||||
>
|
||||
<Select>{adrOptions}</Select>
|
||||
</Form.Item>
|
||||
<Row gutter={24}>
|
||||
<Col span={8}>
|
||||
)}
|
||||
{!supportsOtaa && (
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Flush queue on activate"
|
||||
name="flushQueueOnActivate"
|
||||
valuePropName="checked"
|
||||
tooltip="If enabled, the device-queue will be flushed on ABP or OTAA activation."
|
||||
label="RX2 data-rate"
|
||||
tooltip="Please refer the LoRaWAN Regional Parameters specification for valid values."
|
||||
name="abpRx2Dr"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please enter a RX2 data-rate!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Switch />
|
||||
<InputNumber min={0} max={15} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Expected uplink interval (secs)"
|
||||
tooltip="The expected interval in seconds in which the device sends uplink messages. This is used to determine if a device is active or inactive."
|
||||
name="uplinkInterval"
|
||||
rules={[{ required: true, message: "Please enter an uplink interval!" }]}
|
||||
label="RX2 frequency (Hz)"
|
||||
name="abpRx2Freq"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please enter a RX2 frequency!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
label="Device-status request frequency (req/day)"
|
||||
tooltip="Frequency to initiate an End-Device status request (request/day). Set to 0 to disable."
|
||||
name="deviceStatusReqInterval"
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
<InputNumber min={0} style={{ width: "200px" }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Join (OTAA / ABP)" key="2">
|
||||
<Form.Item label="Device supports OTAA" name="supportsOtaa" valuePropName="checked">
|
||||
<Switch onChange={this.onSupportsOtaaChange} />
|
||||
</Form.Item>
|
||||
{!this.state.supportsOtaa && (
|
||||
<Row>
|
||||
)}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Class-B" key="3">
|
||||
<Form.Item label="Device supports Class-B" name="supportsClassB" valuePropName="checked">
|
||||
<Switch onChange={onSupportsClassBChnage} />
|
||||
</Form.Item>
|
||||
{supportsClassB && (
|
||||
<>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="RX1 delay"
|
||||
name="abpRx1Delay"
|
||||
rules={[{ required: true, message: "Please enter a RX1 delay!" }]}
|
||||
label="Class-B confirmed downlink timeout (seconds)"
|
||||
tooltip="Class-B timeout (in seconds) for confirmed downlink transmissions."
|
||||
name="classBTimeout"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please enter a Class-B confirmed downlink timeout!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={15} />
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="RX1 data-rate offset"
|
||||
tooltip="Please refer the LoRaWAN Regional Parameters specification for valid values."
|
||||
name="abpRx1DrOffset"
|
||||
rules={[{ required: true, message: "Please enter a RX1 data-rate offset!" }]}
|
||||
label="Class-B ping-slot periodicity"
|
||||
tooltip="This value must match the ping-slot periodicity of the device. Please refer to the device documentation."
|
||||
name="classBPingSlotNbK"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please select the ping-slot periodicity!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={15} />
|
||||
<Select>
|
||||
<Select.Option value={0}>Every second</Select.Option>
|
||||
<Select.Option value={1}>Every 2 seconds</Select.Option>
|
||||
<Select.Option value={2}>Every 4 seconds</Select.Option>
|
||||
<Select.Option value={3}>Every 8 seconds</Select.Option>
|
||||
<Select.Option value={4}>Every 16 seconds</Select.Option>
|
||||
<Select.Option value={5}>Every 32 seconds</Select.Option>
|
||||
<Select.Option value={6}>Every 64 seconds</Select.Option>
|
||||
<Select.Option value={7}>Every 128 seconds</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
{!this.state.supportsOtaa && (
|
||||
<Row>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="RX2 data-rate"
|
||||
tooltip="Please refer the LoRaWAN Regional Parameters specification for valid values."
|
||||
name="abpRx2Dr"
|
||||
rules={[{ required: true, message: "Please enter a RX2 data-rate!" }]}
|
||||
label="Class-B ping-slot data-rate"
|
||||
tooltip="This value must match the ping-slot data-rate of the device. Please refer to the device documentation."
|
||||
name="classBPingSlotDr"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please enter the ping-slot data-rate!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={15} />
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="RX2 frequency (Hz)"
|
||||
name="abpRx2Freq"
|
||||
rules={[{ required: true, message: "Please enter a RX2 frequency!" }]}
|
||||
label="Class-B ping-slot frequency (Hz)"
|
||||
tooltip="This value must match the ping-slot frequency of the device. Please refer to the device documentation."
|
||||
name="classBPingSlotFreq"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please enter the ping-slot frequency!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} style={{ width: "200px" }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Class-B" key="3">
|
||||
<Form.Item label="Device supports Class-B" name="supportsClassB" valuePropName="checked">
|
||||
<Switch onChange={this.onSupportsClassBChnage} />
|
||||
</>
|
||||
)}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Class-C" key="4">
|
||||
<Form.Item label="Device supports Class-C" name="supportsClassC" valuePropName="checked">
|
||||
<Switch onChange={onSupportsClassCChange} />
|
||||
</Form.Item>
|
||||
{supportsClassC && (
|
||||
<Form.Item
|
||||
label="Class-C confirmed downlink timeout (seconds)"
|
||||
tooltip="Class-C timeout (in seconds) for confirmed downlink transmissions."
|
||||
name="classCTimeout"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please enter a Class-C confirmed downlink timeout!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
{this.state.supportsClassB && (
|
||||
)}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Codec" key="5">
|
||||
<Form.Item
|
||||
label="Payload codec"
|
||||
name="payloadCodecRuntime"
|
||||
tooltip="By defining a payload codec, ChirpStack Application Server can encode and decode the binary device payload for you."
|
||||
>
|
||||
<Select onChange={onPayloadCodecRuntimeChange}>
|
||||
<Select.Option value={CodecRuntime.NONE}>None</Select.Option>
|
||||
<Select.Option value={CodecRuntime.CAYENNE_LPP}>Cayenne LPP</Select.Option>
|
||||
<Select.Option value={CodecRuntime.JS}>JavaScript functions</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{payloadCodecRuntime === CodecRuntime.JS && <CodeEditor label="Codec functions" name="payloadCodecScript" />}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Tags" key="6">
|
||||
<Form.List name="tagsMap">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Class-B confirmed downlink timeout (seconds)"
|
||||
tooltip="Class-B timeout (in seconds) for confirmed downlink transmissions."
|
||||
name="classBTimeout"
|
||||
rules={[{ required: true, message: "Please enter a Class-B confirmed downlink timeout!" }]}
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Class-B ping-slot periodicity"
|
||||
tooltip="This value must match the ping-slot periodicity of the device. Please refer to the device documentation."
|
||||
name="classBPingSlotNbK"
|
||||
rules={[{ required: true, message: "Please select the ping-slot periodicity!" }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value={0}>Every second</Select.Option>
|
||||
<Select.Option value={1}>Every 2 seconds</Select.Option>
|
||||
<Select.Option value={2}>Every 4 seconds</Select.Option>
|
||||
<Select.Option value={3}>Every 8 seconds</Select.Option>
|
||||
<Select.Option value={4}>Every 16 seconds</Select.Option>
|
||||
<Select.Option value={5}>Every 32 seconds</Select.Option>
|
||||
<Select.Option value={6}>Every 64 seconds</Select.Option>
|
||||
<Select.Option value={7}>Every 128 seconds</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Class-B ping-slot data-rate"
|
||||
tooltip="This value must match the ping-slot data-rate of the device. Please refer to the device documentation."
|
||||
name="classBPingSlotDr"
|
||||
rules={[{ required: true, message: "Please enter the ping-slot data-rate!" }]}
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Class-B ping-slot frequency (Hz)"
|
||||
tooltip="This value must match the ping-slot frequency of the device. Please refer to the device documentation."
|
||||
name="classBPingSlotFreq"
|
||||
rules={[{ required: true, message: "Please enter the ping-slot frequency!" }]}
|
||||
>
|
||||
<InputNumber min={0} style={{ width: "200px" }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Row gutter={24}>
|
||||
<Col span={6}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 0]}
|
||||
fieldKey={[name, 0]}
|
||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||
>
|
||||
<Input placeholder="Key" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 1]}
|
||||
fieldKey={[name, 1]}
|
||||
rules={[{ required: true, message: "Please enter a value!" }]}
|
||||
>
|
||||
<Input placeholder="Value" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
Add tag
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Class-C" key="4">
|
||||
<Form.Item label="Device supports Class-C" name="supportsClassC" valuePropName="checked">
|
||||
<Switch onChange={this.onSupportsClassCChange} />
|
||||
</Form.Item>
|
||||
{this.state.supportsClassC && (
|
||||
<Form.Item
|
||||
label="Class-C confirmed downlink timeout (seconds)"
|
||||
tooltip="Class-C timeout (in seconds) for confirmed downlink transmissions."
|
||||
name="classCTimeout"
|
||||
rules={[{ required: true, message: "Please enter a Class-C confirmed downlink timeout!" }]}
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Form.List>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Measurements" key="7">
|
||||
<Card bordered={false}>
|
||||
<p>
|
||||
ChirpStack can aggregate and visualize decoded device measurements in the device dashboard. To setup the
|
||||
aggregation of device measurements, you must configure the key, kind of measurement and name
|
||||
(user-defined). The following measurement-kinds can be selected:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Unknown / unset</strong>: Default for auto-detected keys. This disables the aggregation of this
|
||||
metric.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Counter</strong>: For continuous incrementing counters.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Absolute</strong>: For counters which get reset upon reading / uplink.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Gauge</strong>: For temperature, humidity, pressure etc...
|
||||
</li>
|
||||
<li>
|
||||
<strong>String</strong>: For boolean or string values.
|
||||
</li>
|
||||
</ul>
|
||||
</Card>
|
||||
<Form.Item
|
||||
label="Automatically detect measurement keys"
|
||||
name="autoDetectMeasurements"
|
||||
valuePropName="checked"
|
||||
tooltip="If enabled, measurement-keys will be automatically added based on the decoded payload keys. If the decoded payload contains random keys, you want to disable auto-detection."
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.List name="measurementsMap">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Row gutter={24}>
|
||||
<Col span={6}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 0]}
|
||||
fieldKey={[name, 0]}
|
||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||
>
|
||||
<Input placeholder="Key" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 1, "kind"]}
|
||||
fieldKey={[name, 1, "kind"]}
|
||||
rules={[{ required: true, message: "Please select a kind!" }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value={MeasurementKind.UNKNOWN}>Unknown / unset</Select.Option>
|
||||
<Select.Option value={MeasurementKind.COUNTER}>Counter</Select.Option>
|
||||
<Select.Option value={MeasurementKind.ABSOLUTE}>Absolute</Select.Option>
|
||||
<Select.Option value={MeasurementKind.GAUGE}>Gauge</Select.Option>
|
||||
<Select.Option value={MeasurementKind.STRING}>String</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 1, "name"]}
|
||||
fieldKey={[name, 1, "name"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please enter a description!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="Name" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
Add measurement
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Codec" key="5">
|
||||
<Form.Item
|
||||
label="Payload codec"
|
||||
name="payloadCodecRuntime"
|
||||
tooltip="By defining a payload codec, ChirpStack Application Server can encode and decode the binary device payload for you."
|
||||
>
|
||||
<Select onChange={this.onPayloadCodecRuntimeChange}>
|
||||
<Select.Option value={CodecRuntime.NONE}>None</Select.Option>
|
||||
<Select.Option value={CodecRuntime.CAYENNE_LPP}>Cayenne LPP</Select.Option>
|
||||
<Select.Option value={CodecRuntime.JS}>JavaScript functions</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{this.state.payloadCodecRuntime === CodecRuntime.JS && (
|
||||
<CodeEditor
|
||||
label="Codec functions"
|
||||
name="payloadCodecScript"
|
||||
value={this.formRef.current.getFieldValue("payloadCodecScript")}
|
||||
formRef={this.formRef}
|
||||
/>
|
||||
)}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Tags" key="6">
|
||||
<Form.List name="tagsMap">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Row gutter={24}>
|
||||
<Col span={6}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 0]}
|
||||
fieldKey={[name, 0]}
|
||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||
>
|
||||
<Input placeholder="Key" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 1]}
|
||||
fieldKey={[name, 1]}
|
||||
rules={[{ required: true, message: "Please enter a value!" }]}
|
||||
>
|
||||
<Input placeholder="Value" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
Add tag
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Measurements" key="7">
|
||||
<Card bordered={false}>
|
||||
<p>
|
||||
ChirpStack can aggregate and visualize decoded device measurements in the device dashboard. To setup the
|
||||
aggregation of device measurements, you must configure the key, kind of measurement and name
|
||||
(user-defined). The following measurement-kinds can be selected:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Unknown / unset</strong>: Default for auto-detected keys. This disables the aggregation of
|
||||
this metric.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Counter</strong>: For continuous incrementing counters.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Absolute</strong>: For counters which get reset upon reading / uplink.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Gauge</strong>: For temperature, humidity, pressure etc...
|
||||
</li>
|
||||
<li>
|
||||
<strong>String</strong>: For boolean or string values.
|
||||
</li>
|
||||
</ul>
|
||||
</Card>
|
||||
<Form.Item
|
||||
label="Automatically detect measurement keys"
|
||||
name="autoDetectMeasurements"
|
||||
valuePropName="checked"
|
||||
tooltip="If enabled, measurement-keys will be automatically added based on the decoded payload keys. If the decoded payload contains random keys, you want to disable auto-detection."
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.List name="measurementsMap">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Row gutter={24}>
|
||||
<Col span={6}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 0]}
|
||||
fieldKey={[name, 0]}
|
||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||
>
|
||||
<Input placeholder="Key" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 1, "kind"]}
|
||||
fieldKey={[name, 1, "kind"]}
|
||||
rules={[{ required: true, message: "Please select a kind!" }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value={MeasurementKind.UNKNOWN}>Unknown / unset</Select.Option>
|
||||
<Select.Option value={MeasurementKind.COUNTER}>Counter</Select.Option>
|
||||
<Select.Option value={MeasurementKind.ABSOLUTE}>Absolute</Select.Option>
|
||||
<Select.Option value={MeasurementKind.GAUGE}>Gauge</Select.Option>
|
||||
<Select.Option value={MeasurementKind.STRING}>String</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 1, "name"]}
|
||||
fieldKey={[name, 1, "name"]}
|
||||
rules={[{ required: true, message: "Please enter a description!" }]}
|
||||
>
|
||||
<Input placeholder="Name" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
Add measurement
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
</Form.List>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeviceProfileTemplateForm;
|
||||
|
@ -1,7 +1,9 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps, Link } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { Space, Breadcrumb, Card, Button, PageHeader } from "antd";
|
||||
import { useParams, Link, useNavigate } from "react-router-dom";
|
||||
|
||||
import { Space, Breadcrumb, Card, Button } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import {
|
||||
DeviceProfileTemplate,
|
||||
@ -15,101 +17,78 @@ import DeviceProfileTemplateForm from "./DeviceProfileTemplateForm";
|
||||
import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore";
|
||||
import DeleteConfirm from "../../components/DeleteConfirm";
|
||||
|
||||
interface IState {
|
||||
deviceProfileTemplate?: DeviceProfileTemplate;
|
||||
}
|
||||
function EditDeviceProfileTemplate() {
|
||||
const navigate = useNavigate();
|
||||
const [deviceProfileTemplate, setDeviceProfileTemplate] = useState<DeviceProfileTemplate | undefined>(undefined);
|
||||
const { deviceProfileTemplateId } = useParams();
|
||||
|
||||
interface MatchParams {
|
||||
deviceProfileTemplateId: string;
|
||||
}
|
||||
|
||||
interface IProps extends RouteComponentProps<MatchParams> {}
|
||||
|
||||
class EditDeviceProfileTemplate extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getDeviceProfileTemplate();
|
||||
}
|
||||
|
||||
getDeviceProfileTemplate = () => {
|
||||
const id = this.props.match.params.deviceProfileTemplateId;
|
||||
useEffect(() => {
|
||||
const id = deviceProfileTemplateId!;
|
||||
let req = new GetDeviceProfileTemplateRequest();
|
||||
req.setId(id);
|
||||
|
||||
DeviceProfileTemplateStore.get(req, (resp: GetDeviceProfileTemplateResponse) => {
|
||||
this.setState({
|
||||
deviceProfileTemplate: resp.getDeviceProfileTemplate(),
|
||||
});
|
||||
setDeviceProfileTemplate(resp.getDeviceProfileTemplate());
|
||||
});
|
||||
};
|
||||
}, [deviceProfileTemplateId]);
|
||||
|
||||
onFinish = (obj: DeviceProfileTemplate) => {
|
||||
const onFinish = (obj: DeviceProfileTemplate) => {
|
||||
let req = new UpdateDeviceProfileTemplateRequest();
|
||||
req.setDeviceProfileTemplate(obj);
|
||||
|
||||
DeviceProfileTemplateStore.update(req, () => {
|
||||
this.props.history.push(`/device-profile-templates`);
|
||||
navigate(`/device-profile-templates`);
|
||||
});
|
||||
};
|
||||
|
||||
deleteDeviceProfileTemplate = () => {
|
||||
const deleteDeviceProfileTemplate = () => {
|
||||
let req = new DeleteDeviceProfileTemplateRequest();
|
||||
req.setId(this.props.match.params.deviceProfileTemplateId);
|
||||
req.setId(deviceProfileTemplateId!);
|
||||
|
||||
DeviceProfileTemplateStore.delete(req, () => {
|
||||
this.props.history.push(`/device-profile-templates`);
|
||||
navigate(`/device-profile-templates`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const dp = this.state.deviceProfileTemplate;
|
||||
const dp = deviceProfileTemplate;
|
||||
|
||||
if (!dp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Network Server</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/device-profile-templates`}>Device-profile templates</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>{dp.getName()}</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title={dp.getName()}
|
||||
subTitle={`device-profile template id: ${dp.getId()}`}
|
||||
extra={[
|
||||
<DeleteConfirm
|
||||
typ="device-profile template"
|
||||
confirm={dp.getName()}
|
||||
onConfirm={this.deleteDeviceProfileTemplate}
|
||||
>
|
||||
<Button danger type="primary">
|
||||
Delete device-profile template
|
||||
</Button>
|
||||
</DeleteConfirm>,
|
||||
]}
|
||||
/>
|
||||
<Card>
|
||||
<DeviceProfileTemplateForm initialValues={dp} update={true} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
if (!dp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Network Server</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/device-profile-templates`}>Device-profile templates</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>{dp.getName()}</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title={dp.getName()}
|
||||
subTitle={`device-profile template id: ${dp.getId()}`}
|
||||
extra={[
|
||||
<DeleteConfirm typ="device-profile template" confirm={dp.getName()} onConfirm={deleteDeviceProfileTemplate}>
|
||||
<Button danger type="primary">
|
||||
Delete device-profile template
|
||||
</Button>
|
||||
</DeleteConfirm>,
|
||||
]}
|
||||
/>
|
||||
<Card>
|
||||
<DeviceProfileTemplateForm initialValues={dp} update={true} onFinish={onFinish} />
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditDeviceProfileTemplate;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Space, Breadcrumb, Button, PageHeader } from "antd";
|
||||
import { Space, Breadcrumb, Button } from "antd";
|
||||
import { ColumnsType } from "antd/es/table";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import {
|
||||
ListDeviceProfileTemplatesRequest,
|
||||
@ -15,38 +15,36 @@ import { getEnumName } from "../helpers";
|
||||
import DataTable, { GetPageCallbackFunc } from "../../components/DataTable";
|
||||
import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore";
|
||||
|
||||
class ListDeviceProfileTemplates extends Component {
|
||||
columns = (): ColumnsType<DeviceProfileTemplateListItem.AsObject> => {
|
||||
return [
|
||||
{
|
||||
title: "Vendor",
|
||||
dataIndex: "vendor",
|
||||
key: "vendor",
|
||||
function ListDeviceProfileTemplates() {
|
||||
const columns: ColumnsType<DeviceProfileTemplateListItem.AsObject> = [
|
||||
{
|
||||
title: "Vendor",
|
||||
dataIndex: "vendor",
|
||||
key: "vendor",
|
||||
},
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
render: (text, record) => <Link to={`/device-profile-templates/${record.id}/edit`}>{text}</Link>,
|
||||
},
|
||||
{
|
||||
title: "Firmware",
|
||||
dataIndex: "firmware",
|
||||
key: "firmware",
|
||||
},
|
||||
{
|
||||
title: "Region",
|
||||
dataIndex: "region",
|
||||
key: "region",
|
||||
width: 150,
|
||||
render: (text, record) => {
|
||||
return getEnumName(Region, record.region);
|
||||
},
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
render: (text, record) => <Link to={`/device-profile-templates/${record.id}/edit`}>{text}</Link>,
|
||||
},
|
||||
{
|
||||
title: "Firmware",
|
||||
dataIndex: "firmware",
|
||||
key: "firmware",
|
||||
},
|
||||
{
|
||||
title: "Region",
|
||||
dataIndex: "region",
|
||||
key: "region",
|
||||
width: 150,
|
||||
render: (text, record) => {
|
||||
return getEnumName(Region, record.region);
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||
let req = new ListDeviceProfileTemplatesRequest();
|
||||
req.setLimit(limit);
|
||||
req.setOffset(offset);
|
||||
@ -57,31 +55,29 @@ class ListDeviceProfileTemplates extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Network Server</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Device-profile templates</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Device-profile templates"
|
||||
extra={[
|
||||
<Button type="primary">
|
||||
<Link to={`/device-profile-templates/create`}>Add device-profile template</Link>
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
<DataTable columns={this.columns()} getPage={this.getPage} rowKey="id" />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Network Server</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Device-profile templates</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Device-profile templates"
|
||||
extra={[
|
||||
<Button type="primary">
|
||||
<Link to={`/device-profile-templates/create`}>Add device-profile template</Link>
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
<DataTable columns={columns} getPage={getPage} rowKey="id" />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default ListDeviceProfileTemplates;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link, RouteComponentProps } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
import { Space, Breadcrumb, Card, PageHeader } from "antd";
|
||||
import { Space, Breadcrumb, Card } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import { MacVersion, RegParamsRevision } from "@chirpstack/chirpstack-api-grpc-web/common/common_pb";
|
||||
import {
|
||||
@ -15,97 +15,97 @@ import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
||||
import DeviceProfileForm from "./DeviceProfileForm";
|
||||
import DeviceProfileStore from "../../stores/DeviceProfileStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
tenant: Tenant;
|
||||
}
|
||||
|
||||
class CreateDeviceProfile extends Component<IProps> {
|
||||
onFinish = (obj: DeviceProfile) => {
|
||||
obj.setTenantId(this.props.tenant.getId());
|
||||
function CreateDeviceProfile(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = (obj: DeviceProfile) => {
|
||||
obj.setTenantId(props.tenant.getId());
|
||||
|
||||
let req = new CreateDeviceProfileRequest();
|
||||
req.setDeviceProfile(obj);
|
||||
|
||||
DeviceProfileStore.create(req, (_resp: CreateDeviceProfileResponse) => {
|
||||
this.props.history.push(`/tenants/${this.props.tenant.getId()}/device-profiles`);
|
||||
navigate(`/tenants/${props.tenant.getId()}/device-profiles`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const codecScript = `// Decode uplink function.
|
||||
//
|
||||
// Input is an object with the following fields:
|
||||
// - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0]
|
||||
// - fPort = Uplink fPort.
|
||||
// - variables = Object containing the configured device variables.
|
||||
//
|
||||
// Output must be an object with the following fields:
|
||||
// - data = Object representing the decoded payload.
|
||||
function decodeUplink(input) {
|
||||
return {
|
||||
data: {
|
||||
temp: 22.5
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Encode downlink function.
|
||||
//
|
||||
// Input is an object with the following fields:
|
||||
// - data = Object representing the payload that must be encoded.
|
||||
// - variables = Object containing the configured device variables.
|
||||
//
|
||||
// Output must be an object with the following fields:
|
||||
// - bytes = Byte array containing the downlink payload.
|
||||
function encodeDownlink(input) {
|
||||
return {
|
||||
bytes: [225, 230, 255, 0]
|
||||
};
|
||||
}
|
||||
`;
|
||||
|
||||
let deviceProfile = new DeviceProfile();
|
||||
deviceProfile.setPayloadCodecScript(codecScript);
|
||||
deviceProfile.setSupportsOtaa(true);
|
||||
deviceProfile.setUplinkInterval(3600);
|
||||
deviceProfile.setDeviceStatusReqInterval(1);
|
||||
deviceProfile.setAdrAlgorithmId("default");
|
||||
deviceProfile.setMacVersion(MacVersion.LORAWAN_1_0_3);
|
||||
deviceProfile.setRegParamsRevision(RegParamsRevision.A);
|
||||
deviceProfile.setFlushQueueOnActivate(true);
|
||||
deviceProfile.setAutoDetectMeasurements(true);
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}/device-profiles`}>Device profiles</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Add</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Add device profile"
|
||||
/>
|
||||
<Card>
|
||||
<DeviceProfileForm initialValues={deviceProfile} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
const codecScript = `// Decode uplink function.
|
||||
//
|
||||
// Input is an object with the following fields:
|
||||
// - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0]
|
||||
// - fPort = Uplink fPort.
|
||||
// - variables = Object containing the configured device variables.
|
||||
//
|
||||
// Output must be an object with the following fields:
|
||||
// - data = Object representing the decoded payload.
|
||||
function decodeUplink(input) {
|
||||
return {
|
||||
data: {
|
||||
temp: 22.5
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Encode downlink function.
|
||||
//
|
||||
// Input is an object with the following fields:
|
||||
// - data = Object representing the payload that must be encoded.
|
||||
// - variables = Object containing the configured device variables.
|
||||
//
|
||||
// Output must be an object with the following fields:
|
||||
// - bytes = Byte array containing the downlink payload.
|
||||
function encodeDownlink(input) {
|
||||
return {
|
||||
bytes: [225, 230, 255, 0]
|
||||
};
|
||||
}
|
||||
`;
|
||||
|
||||
let deviceProfile = new DeviceProfile();
|
||||
deviceProfile.setPayloadCodecScript(codecScript);
|
||||
deviceProfile.setSupportsOtaa(true);
|
||||
deviceProfile.setUplinkInterval(3600);
|
||||
deviceProfile.setDeviceStatusReqInterval(1);
|
||||
deviceProfile.setAdrAlgorithmId("default");
|
||||
deviceProfile.setMacVersion(MacVersion.LORAWAN_1_0_3);
|
||||
deviceProfile.setRegParamsRevision(RegParamsRevision.A);
|
||||
deviceProfile.setFlushQueueOnActivate(true);
|
||||
deviceProfile.setAutoDetectMeasurements(true);
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}/device-profiles`}>Device profiles</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Add</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Add device profile"
|
||||
/>
|
||||
<Card>
|
||||
<DeviceProfileForm initialValues={deviceProfile} onFinish={onFinish} />
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateDeviceProfile;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps, Link } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
|
||||
import { Space, Breadcrumb, Card, Button, PageHeader } from "antd";
|
||||
import { Space, Breadcrumb, Card, Button } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
||||
import {
|
||||
@ -18,112 +19,95 @@ import SessionStore from "../../stores/SessionStore";
|
||||
import DeleteConfirm from "../../components/DeleteConfirm";
|
||||
import Admin from "../../components/Admin";
|
||||
|
||||
interface IState {
|
||||
deviceProfile?: DeviceProfile;
|
||||
}
|
||||
|
||||
interface MatchParams {
|
||||
deviceProfileId: string;
|
||||
}
|
||||
|
||||
interface IProps extends RouteComponentProps<MatchParams> {
|
||||
interface IProps {
|
||||
tenant: Tenant;
|
||||
}
|
||||
|
||||
class EditDeviceProfile extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
function EditDeviceProfile(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
const [deviceProfile, setDeviceProfile] = useState<DeviceProfile | undefined>(undefined);
|
||||
const { deviceProfileId } = useParams();
|
||||
|
||||
componentDidMount() {
|
||||
this.getDeviceProfile();
|
||||
}
|
||||
|
||||
getDeviceProfile = () => {
|
||||
const id = this.props.match.params.deviceProfileId;
|
||||
useEffect(() => {
|
||||
const id = deviceProfileId!;
|
||||
let req = new GetDeviceProfileRequest();
|
||||
req.setId(id);
|
||||
|
||||
DeviceProfileStore.get(req, (resp: GetDeviceProfileResponse) => {
|
||||
this.setState({
|
||||
deviceProfile: resp.getDeviceProfile(),
|
||||
});
|
||||
setDeviceProfile(resp.getDeviceProfile());
|
||||
});
|
||||
};
|
||||
}, [deviceProfileId]);
|
||||
|
||||
onFinish = (obj: DeviceProfile) => {
|
||||
const onFinish = (obj: DeviceProfile) => {
|
||||
let req = new UpdateDeviceProfileRequest();
|
||||
req.setDeviceProfile(obj);
|
||||
|
||||
DeviceProfileStore.update(req, () => {
|
||||
this.props.history.push(`/tenants/${this.props.tenant.getId()}/device-profiles`);
|
||||
navigate(`/tenants/${props.tenant.getId()}/device-profiles`);
|
||||
});
|
||||
};
|
||||
|
||||
deleteDeviceProfile = () => {
|
||||
const deleteDeviceProfile = () => {
|
||||
let req = new DeleteDeviceProfileRequest();
|
||||
req.setId(this.props.match.params.deviceProfileId);
|
||||
req.setId(deviceProfileId!);
|
||||
|
||||
DeviceProfileStore.delete(req, () => {
|
||||
this.props.history.push(`/tenants/${this.props.tenant.getId()}/device-profiles`);
|
||||
navigate(`/tenants/${props.tenant.getId()}/device-profiles`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const dp = this.state.deviceProfile;
|
||||
const dp = deviceProfile;
|
||||
|
||||
if (!dp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const disabled = !(
|
||||
SessionStore.isAdmin() ||
|
||||
SessionStore.isTenantAdmin(this.props.tenant.getId()) ||
|
||||
SessionStore.isTenantDeviceAdmin(this.props.tenant.getId())
|
||||
);
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}/device-profiles`}>Device profiles</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>{dp.getName()}</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title={dp.getName()}
|
||||
subTitle={`device profile id: ${dp.getId()}`}
|
||||
extra={[
|
||||
<Admin tenantId={this.props.tenant.getId()} isDeviceAdmin>
|
||||
<DeleteConfirm typ="device profile" confirm={dp.getName()} onConfirm={this.deleteDeviceProfile}>
|
||||
<Button danger type="primary">
|
||||
Delete device profile
|
||||
</Button>
|
||||
</DeleteConfirm>
|
||||
</Admin>,
|
||||
]}
|
||||
/>
|
||||
<Card>
|
||||
<DeviceProfileForm initialValues={dp} disabled={disabled} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
if (!dp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const disabled = !(
|
||||
SessionStore.isAdmin() ||
|
||||
SessionStore.isTenantAdmin(props.tenant.getId()) ||
|
||||
SessionStore.isTenantDeviceAdmin(props.tenant.getId())
|
||||
);
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}/device-profiles`}>Device profiles</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>{dp.getName()}</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title={dp.getName()}
|
||||
subTitle={`device profile id: ${dp.getId()}`}
|
||||
extra={[
|
||||
<Admin tenantId={props.tenant.getId()} isDeviceAdmin>
|
||||
<DeleteConfirm typ="device profile" confirm={dp.getName()} onConfirm={deleteDeviceProfile}>
|
||||
<Button danger type="primary">
|
||||
Delete device profile
|
||||
</Button>
|
||||
</DeleteConfirm>
|
||||
</Admin>,
|
||||
]}
|
||||
/>
|
||||
<Card>
|
||||
<DeviceProfileForm initialValues={dp} disabled={disabled} onFinish={onFinish} />
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditDeviceProfile;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Space, Breadcrumb, Button, PageHeader } from "antd";
|
||||
import { Space, Breadcrumb, Button } from "antd";
|
||||
import { ColumnsType } from "antd/es/table";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import {
|
||||
ListDeviceProfilesRequest,
|
||||
@ -21,89 +21,87 @@ interface IProps {
|
||||
tenant: Tenant;
|
||||
}
|
||||
|
||||
class ListDeviceProfiles extends Component<IProps> {
|
||||
columns = (): ColumnsType<DeviceProfileListItem.AsObject> => {
|
||||
return [
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
render: (text, record) => (
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}/device-profiles/${record.id}/edit`}>{text}</Link>
|
||||
),
|
||||
function ListDeviceProfiles(props: IProps) {
|
||||
const columns: ColumnsType<DeviceProfileListItem.AsObject> = [
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
render: (text, record) => (
|
||||
<Link to={`/tenants/${props.tenant.getId()}/device-profiles/${record.id}/edit`}>{text}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Region",
|
||||
dataIndex: "region",
|
||||
key: "region",
|
||||
width: 150,
|
||||
render: (text, record) => {
|
||||
return getEnumName(Region, record.region);
|
||||
},
|
||||
{
|
||||
title: "Region",
|
||||
dataIndex: "region",
|
||||
key: "region",
|
||||
width: 150,
|
||||
render: (text, record) => {
|
||||
return getEnumName(Region, record.region);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "MAC version",
|
||||
dataIndex: "macVersion",
|
||||
key: "macVersion",
|
||||
width: 150,
|
||||
render: (text, record) => {
|
||||
return formatMacVersion(record.macVersion);
|
||||
},
|
||||
{
|
||||
title: "MAC version",
|
||||
dataIndex: "macVersion",
|
||||
key: "macVersion",
|
||||
width: 150,
|
||||
render: (text, record) => {
|
||||
return formatMacVersion(record.macVersion);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Revision",
|
||||
dataIndex: "regParamsRevision",
|
||||
key: "regParamsRevision",
|
||||
width: 150,
|
||||
render: (text, record) => {
|
||||
return formatRegParamsRevision(record.regParamsRevision);
|
||||
},
|
||||
{
|
||||
title: "Revision",
|
||||
dataIndex: "regParamsRevision",
|
||||
key: "regParamsRevision",
|
||||
width: 150,
|
||||
render: (text, record) => {
|
||||
return formatRegParamsRevision(record.regParamsRevision);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Supports OTAA",
|
||||
dataIndex: "supportsOtaa",
|
||||
key: "supportsOtaa",
|
||||
width: 150,
|
||||
render: (text, record) => {
|
||||
if (record.supportsOtaa) {
|
||||
return "yes";
|
||||
} else {
|
||||
return "no";
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Supports OTAA",
|
||||
dataIndex: "supportsOtaa",
|
||||
key: "supportsOtaa",
|
||||
width: 150,
|
||||
render: (text, record) => {
|
||||
if (record.supportsOtaa) {
|
||||
return "yes";
|
||||
} else {
|
||||
return "no";
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Supports Class-B",
|
||||
dataIndex: "supportsClassB",
|
||||
key: "supportsClassB",
|
||||
width: 150,
|
||||
render: (text, record) => {
|
||||
if (record.supportsClassB) {
|
||||
return "yes";
|
||||
} else {
|
||||
return "no";
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Supports Class-B",
|
||||
dataIndex: "supportsClassB",
|
||||
key: "supportsClassB",
|
||||
width: 150,
|
||||
render: (text, record) => {
|
||||
if (record.supportsClassB) {
|
||||
return "yes";
|
||||
} else {
|
||||
return "no";
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Supports Class-C",
|
||||
dataIndex: "supportsClassC",
|
||||
key: "supportsClassC",
|
||||
width: 150,
|
||||
render: (text, record) => {
|
||||
if (record.supportsClassC) {
|
||||
return "yes";
|
||||
} else {
|
||||
return "no";
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Supports Class-C",
|
||||
dataIndex: "supportsClassC",
|
||||
key: "supportsClassC",
|
||||
width: 150,
|
||||
render: (text, record) => {
|
||||
if (record.supportsClassC) {
|
||||
return "yes";
|
||||
} else {
|
||||
return "no";
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||
let req = new ListDeviceProfilesRequest();
|
||||
req.setTenantId(this.props.tenant.getId());
|
||||
req.setTenantId(props.tenant.getId());
|
||||
req.setLimit(limit);
|
||||
req.setOffset(offset);
|
||||
|
||||
@ -113,38 +111,36 @@ class ListDeviceProfiles extends Component<IProps> {
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Device profiles</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Device profiles"
|
||||
extra={[
|
||||
<Admin tenantId={this.props.tenant.getId()} isDeviceAdmin>
|
||||
<Button type="primary">
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}/device-profiles/create`}>Add device profile</Link>
|
||||
</Button>
|
||||
</Admin>,
|
||||
]}
|
||||
/>
|
||||
<DataTable columns={this.columns()} getPage={this.getPage} rowKey="id" />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Device profiles</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Device profiles"
|
||||
extra={[
|
||||
<Admin tenantId={props.tenant.getId()} isDeviceAdmin>
|
||||
<Button type="primary">
|
||||
<Link to={`/tenants/${props.tenant.getId()}/device-profiles/create`}>Add device profile</Link>
|
||||
</Button>
|
||||
</Admin>,
|
||||
]}
|
||||
/>
|
||||
<DataTable columns={columns} getPage={getPage} rowKey="id" />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default ListDeviceProfiles;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link, RouteComponentProps } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
import { Space, Breadcrumb, Card, PageHeader } from "antd";
|
||||
import { Space, Breadcrumb, Card } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
||||
import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||
@ -15,14 +15,16 @@ import DeviceForm from "./DeviceForm";
|
||||
import DeviceStore from "../../stores/DeviceStore";
|
||||
import DeviceProfileStore from "../../stores/DeviceProfileStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
tenant: Tenant;
|
||||
application: Application;
|
||||
}
|
||||
|
||||
class CreateDevice extends Component<IProps> {
|
||||
onFinish = (obj: Device) => {
|
||||
obj.setApplicationId(this.props.application.getId());
|
||||
function CreateDevice(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = (obj: Device) => {
|
||||
obj.setApplicationId(props.application.getId());
|
||||
|
||||
let req = new CreateDeviceRequest();
|
||||
req.setDevice(obj);
|
||||
@ -34,60 +36,58 @@ class CreateDevice extends Component<IProps> {
|
||||
DeviceProfileStore.get(req, (resp: GetDeviceProfileResponse) => {
|
||||
let dp = resp.getDeviceProfile()!;
|
||||
if (dp.getSupportsOtaa()) {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}/devices/${obj.getDevEui()}/keys`,
|
||||
navigate(
|
||||
`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}/devices/${obj.getDevEui()}/keys`,
|
||||
);
|
||||
} else {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}/devices/${obj.getDevEui()}`,
|
||||
navigate(
|
||||
`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}/devices/${obj.getDevEui()}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
let device = new Device();
|
||||
device.setApplicationId(this.props.application.getId());
|
||||
let device = new Device();
|
||||
device.setApplicationId(props.application.getId());
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}/applications`}>Applications</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}`}>
|
||||
{this.props.application.getName()}
|
||||
</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Add device</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Add device"
|
||||
/>
|
||||
<Card>
|
||||
<DeviceForm tenant={this.props.tenant} initialValues={device} onFinish={this.onFinish} />
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}/applications`}>Applications</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}`}>
|
||||
{props.application.getName()}
|
||||
</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>Add device</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title="Add device"
|
||||
/>
|
||||
<Card>
|
||||
<DeviceForm tenant={props.tenant} initialValues={device} onFinish={onFinish} />
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateDevice;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Space, Form, Button, Row, Col, InputNumber, Alert } from "antd";
|
||||
|
||||
@ -26,11 +26,11 @@ interface FormProps {
|
||||
onFinish: (obj: DeviceActivationPb) => void;
|
||||
}
|
||||
|
||||
class LW10DeviceActivationForm extends Component<FormProps> {
|
||||
formRef = React.createRef<any>();
|
||||
function LW10DeviceActivationForm(props: FormProps) {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
onFinish = (values: DeviceActivationPb.AsObject) => {
|
||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
||||
const onFinish = (values: DeviceActivationPb.AsObject) => {
|
||||
const v = Object.assign(props.initialValues.toObject(), values);
|
||||
let da = new DeviceActivationPb();
|
||||
|
||||
da.setDevAddr(v.devAddr);
|
||||
@ -42,66 +42,56 @@ class LW10DeviceActivationForm extends Component<FormProps> {
|
||||
da.setAFCntDown(v.nFCntDown);
|
||||
da.setNFCntDown(v.nFCntDown);
|
||||
|
||||
this.props.onFinish(da);
|
||||
props.onFinish(da);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Form
|
||||
layout="vertical"
|
||||
initialValues={this.props.initialValues.toObject()}
|
||||
onFinish={this.onFinish}
|
||||
ref={this.formRef}
|
||||
>
|
||||
<DevAddrInput
|
||||
label="Device address"
|
||||
name="devAddr"
|
||||
value={this.props.initialValues.getDevAddr()}
|
||||
devEui={this.props.device.getDevEui()}
|
||||
formRef={this.formRef}
|
||||
required
|
||||
/>
|
||||
<AesKeyInput
|
||||
label="Network session key (LoRaWAN 1.0)"
|
||||
name="nwkSEncKey"
|
||||
value={this.props.initialValues.getNwkSEncKey()}
|
||||
formRef={this.formRef}
|
||||
required
|
||||
/>
|
||||
<AesKeyInput
|
||||
label="Application session key (LoRaWAN 1.0)"
|
||||
name="appSKey"
|
||||
value={this.props.initialValues.getAppSKey()}
|
||||
formRef={this.formRef}
|
||||
required
|
||||
/>
|
||||
<Row gutter={24}>
|
||||
<Col span={6}>
|
||||
<Form.Item label="Uplink frame-counter" name="fCntUp">
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item label="Downlink frame-counter" name="nFCntDown">
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" disabled={this.props.disabled}>
|
||||
(Re)activate device
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
|
||||
<DevAddrInput
|
||||
label="Device address"
|
||||
name="devAddr"
|
||||
value={props.initialValues.getDevAddr()}
|
||||
devEui={props.device.getDevEui()}
|
||||
required
|
||||
/>
|
||||
<AesKeyInput
|
||||
label="Network session key (LoRaWAN 1.0)"
|
||||
name="nwkSEncKey"
|
||||
value={props.initialValues.getNwkSEncKey()}
|
||||
required
|
||||
/>
|
||||
<AesKeyInput
|
||||
label="Application session key (LoRaWAN 1.0)"
|
||||
name="appSKey"
|
||||
value={props.initialValues.getAppSKey()}
|
||||
required
|
||||
/>
|
||||
<Row gutter={24}>
|
||||
<Col span={6}>
|
||||
<Form.Item label="Uplink frame-counter" name="fCntUp">
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item label="Downlink frame-counter" name="nFCntDown">
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" disabled={props.disabled}>
|
||||
(Re)activate device
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
class LW11DeviceActivationForm extends Component<FormProps> {
|
||||
formRef = React.createRef<any>();
|
||||
function LW11DeviceActivationForm(props: FormProps) {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
onFinish = (values: DeviceActivationPb.AsObject) => {
|
||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
||||
const onFinish = (values: DeviceActivationPb.AsObject) => {
|
||||
const v = Object.assign(props.initialValues.toObject(), values);
|
||||
let da = new DeviceActivationPb();
|
||||
|
||||
da.setDevAddr(v.devAddr);
|
||||
@ -113,162 +103,133 @@ class LW11DeviceActivationForm extends Component<FormProps> {
|
||||
da.setAFCntDown(v.aFCntDown);
|
||||
da.setNFCntDown(v.nFCntDown);
|
||||
|
||||
this.props.onFinish(da);
|
||||
props.onFinish(da);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Form
|
||||
layout="vertical"
|
||||
initialValues={this.props.initialValues.toObject()}
|
||||
onFinish={this.onFinish}
|
||||
ref={this.formRef}
|
||||
>
|
||||
<DevAddrInput
|
||||
label="Device address"
|
||||
name="devAddr"
|
||||
value={this.props.initialValues.getDevAddr()}
|
||||
devEui={this.props.device.getDevEui()}
|
||||
formRef={this.formRef}
|
||||
required
|
||||
/>
|
||||
<AesKeyInput
|
||||
label="Network session encryption key"
|
||||
name="nwkSEncKey"
|
||||
value={this.props.initialValues.getNwkSEncKey()}
|
||||
formRef={this.formRef}
|
||||
required
|
||||
/>
|
||||
<AesKeyInput
|
||||
label="Serving network session integrity key"
|
||||
name="sNwkSIntKey"
|
||||
value={this.props.initialValues.getSNwkSIntKey()}
|
||||
formRef={this.formRef}
|
||||
required
|
||||
/>
|
||||
<AesKeyInput
|
||||
label="Forwarding network session integrity key"
|
||||
name="fNwkSIntKey"
|
||||
value={this.props.initialValues.getFNwkSIntKey()}
|
||||
formRef={this.formRef}
|
||||
required
|
||||
/>
|
||||
<AesKeyInput
|
||||
label="Application session key"
|
||||
name="appSKey"
|
||||
value={this.props.initialValues.getAppSKey()}
|
||||
formRef={this.formRef}
|
||||
required
|
||||
/>
|
||||
<Row gutter={24}>
|
||||
<Col span={6}>
|
||||
<Form.Item label="Uplink frame-counter" name="fCntUp">
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item label="Downlink frame-counter (network)" name="nFCntDown">
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item label="Downlink frame-counter (application)" name="aFCntDown">
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" disabled={this.props.disabled}>
|
||||
(Re)activate device
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
|
||||
<DevAddrInput
|
||||
label="Device address"
|
||||
name="devAddr"
|
||||
value={props.initialValues.getDevAddr()}
|
||||
devEui={props.device.getDevEui()}
|
||||
required
|
||||
/>
|
||||
<AesKeyInput
|
||||
label="Network session encryption key"
|
||||
name="nwkSEncKey"
|
||||
value={props.initialValues.getNwkSEncKey()}
|
||||
required
|
||||
/>
|
||||
<AesKeyInput
|
||||
label="Serving network session integrity key"
|
||||
name="sNwkSIntKey"
|
||||
value={props.initialValues.getSNwkSIntKey()}
|
||||
required
|
||||
/>
|
||||
<AesKeyInput
|
||||
label="Forwarding network session integrity key"
|
||||
name="fNwkSIntKey"
|
||||
value={props.initialValues.getFNwkSIntKey()}
|
||||
required
|
||||
/>
|
||||
<AesKeyInput label="Application session key" name="appSKey" value={props.initialValues.getAppSKey()} required />
|
||||
<Row gutter={24}>
|
||||
<Col span={6}>
|
||||
<Form.Item label="Uplink frame-counter" name="fCntUp">
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item label="Downlink frame-counter (network)" name="nFCntDown">
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item label="Downlink frame-counter (application)" name="aFCntDown">
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" disabled={props.disabled}>
|
||||
(Re)activate device
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
tenant: Tenant;
|
||||
application: Application;
|
||||
device: Device;
|
||||
deviceProfile: DeviceProfile;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
deviceActivation?: DeviceActivationPb;
|
||||
deviceActivationRequested: boolean;
|
||||
}
|
||||
function DeviceActivation(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
const [deviceActivation, setDeviceActivation] = useState<DeviceActivationPb | undefined>(undefined);
|
||||
const [deviceActivationRequested, setDeviceActivationRequested] = useState<boolean>(false);
|
||||
|
||||
class DeviceActivation extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
deviceActivationRequested: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
let req = new GetDeviceActivationRequest();
|
||||
req.setDevEui(this.props.device.getDevEui());
|
||||
req.setDevEui(props.device.getDevEui());
|
||||
|
||||
DeviceStore.getActivation(req, (resp: GetDeviceActivationResponse) => {
|
||||
this.setState({
|
||||
deviceActivation: resp.getDeviceActivation(),
|
||||
deviceActivationRequested: true,
|
||||
});
|
||||
setDeviceActivation(resp.getDeviceActivation());
|
||||
setDeviceActivationRequested(true);
|
||||
});
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
onFinish = (obj: DeviceActivationPb) => {
|
||||
const onFinish = (obj: DeviceActivationPb) => {
|
||||
let req = new ActivateDeviceRequest();
|
||||
obj.setDevEui(this.props.device.getDevEui());
|
||||
obj.setDevEui(props.device.getDevEui());
|
||||
req.setDeviceActivation(obj);
|
||||
|
||||
DeviceStore.activate(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}/devices/${this.props.device.getDevEui()}`,
|
||||
navigate(
|
||||
`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}/devices/${props.device.getDevEui()}`,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.state.deviceActivationRequested) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.state.deviceActivation && this.props.deviceProfile.getSupportsOtaa()) {
|
||||
return <Alert type="info" showIcon message="This device has not (yet) been activated." />;
|
||||
}
|
||||
|
||||
let macVersion = this.props.deviceProfile.getMacVersion();
|
||||
const lw11 = macVersion === MacVersion.LORAWAN_1_1_0;
|
||||
|
||||
let initialValues = new DeviceActivationPb();
|
||||
if (this.state.deviceActivation) {
|
||||
initialValues = this.state.deviceActivation;
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
{!lw11 && (
|
||||
<LW10DeviceActivationForm
|
||||
initialValues={initialValues}
|
||||
device={this.props.device}
|
||||
onFinish={this.onFinish}
|
||||
disabled={this.props.deviceProfile.getSupportsOtaa()}
|
||||
/>
|
||||
)}
|
||||
{lw11 && (
|
||||
<LW11DeviceActivationForm
|
||||
initialValues={initialValues}
|
||||
device={this.props.device}
|
||||
onFinish={this.onFinish}
|
||||
disabled={this.props.deviceProfile.getSupportsOtaa()}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
if (!deviceActivationRequested) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!deviceActivation && props.deviceProfile.getSupportsOtaa()) {
|
||||
return <Alert type="info" showIcon message="This device has not (yet) been activated." />;
|
||||
}
|
||||
|
||||
let macVersion = props.deviceProfile.getMacVersion();
|
||||
const lw11 = macVersion === MacVersion.LORAWAN_1_1_0;
|
||||
|
||||
let initialValues = new DeviceActivationPb();
|
||||
if (deviceActivation) {
|
||||
initialValues = deviceActivation;
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
{!lw11 && (
|
||||
<LW10DeviceActivationForm
|
||||
initialValues={initialValues}
|
||||
device={props.device}
|
||||
onFinish={onFinish}
|
||||
disabled={props.deviceProfile.getSupportsOtaa()}
|
||||
/>
|
||||
)}
|
||||
{lw11 && (
|
||||
<LW11DeviceActivationForm
|
||||
initialValues={initialValues}
|
||||
device={props.device}
|
||||
onFinish={onFinish}
|
||||
disabled={props.deviceProfile.getSupportsOtaa()}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeviceActivation;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import moment from "moment";
|
||||
@ -27,31 +27,18 @@ interface IProps {
|
||||
lastSeenAt?: Date;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
metricsAggregation: Aggregation;
|
||||
deviceMetrics?: GetDeviceMetricsResponse;
|
||||
deviceLinkMetrics?: GetDeviceLinkMetricsResponse;
|
||||
deviceMetricsLoaded: boolean;
|
||||
deviceLinkMetricsLoaded: boolean;
|
||||
}
|
||||
function DeviceDashboard(props: IProps) {
|
||||
const [metricsAggregation, setMetricsAggregation] = useState<Aggregation>(Aggregation.DAY);
|
||||
const [deviceMetrics, setDeviceMetrics] = useState<GetDeviceMetricsResponse | undefined>(undefined);
|
||||
const [deviceLinkMetrics, setDeviceLinkMetrics] = useState<GetDeviceLinkMetricsResponse | undefined>(undefined);
|
||||
const [deviceLinkMetricsLoaded, setDeviceLinkMetricsLoaded] = useState<boolean>(false);
|
||||
|
||||
class DeviceDashboard extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
useEffect(() => {
|
||||
loadMetrics();
|
||||
}, [props, metricsAggregation]);
|
||||
|
||||
this.state = {
|
||||
metricsAggregation: Aggregation.DAY,
|
||||
deviceMetricsLoaded: false,
|
||||
deviceLinkMetricsLoaded: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadMetrics();
|
||||
}
|
||||
|
||||
loadMetrics = () => {
|
||||
const agg = this.state.metricsAggregation;
|
||||
const loadMetrics = () => {
|
||||
const agg = metricsAggregation;
|
||||
const end = moment();
|
||||
let start = moment();
|
||||
|
||||
@ -63,19 +50,12 @@ class DeviceDashboard extends Component<IProps, IState> {
|
||||
start = start.subtract(12, "months");
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
deviceMetricsLoaded: false,
|
||||
deviceLinkMetricsLoaded: false,
|
||||
},
|
||||
() => {
|
||||
this.loadLinkMetrics(start.toDate(), end.toDate(), agg);
|
||||
this.loadDeviceMetrics(start.toDate(), end.toDate(), agg);
|
||||
},
|
||||
);
|
||||
setDeviceLinkMetricsLoaded(false);
|
||||
loadLinkMetrics(start.toDate(), end.toDate(), agg);
|
||||
loadDeviceMetrics(start.toDate(), end.toDate(), agg);
|
||||
};
|
||||
|
||||
loadDeviceMetrics = (start: Date, end: Date, agg: Aggregation) => {
|
||||
const loadDeviceMetrics = (start: Date, end: Date, agg: Aggregation) => {
|
||||
let startPb = new Timestamp();
|
||||
let endPb = new Timestamp();
|
||||
|
||||
@ -83,20 +63,17 @@ class DeviceDashboard extends Component<IProps, IState> {
|
||||
endPb.fromDate(end);
|
||||
|
||||
let req = new GetDeviceMetricsRequest();
|
||||
req.setDevEui(this.props.device.getDevEui());
|
||||
req.setDevEui(props.device.getDevEui());
|
||||
req.setStart(startPb);
|
||||
req.setEnd(endPb);
|
||||
req.setAggregation(agg);
|
||||
|
||||
DeviceStore.getMetrics(req, (resp: GetDeviceMetricsResponse) => {
|
||||
this.setState({
|
||||
deviceMetrics: resp,
|
||||
deviceMetricsLoaded: true,
|
||||
});
|
||||
setDeviceMetrics(resp);
|
||||
});
|
||||
};
|
||||
|
||||
loadLinkMetrics = (start: Date, end: Date, agg: Aggregation) => {
|
||||
const loadLinkMetrics = (start: Date, end: Date, agg: Aggregation) => {
|
||||
let startPb = new Timestamp();
|
||||
let endPb = new Timestamp();
|
||||
|
||||
@ -104,176 +81,153 @@ class DeviceDashboard extends Component<IProps, IState> {
|
||||
endPb.fromDate(end);
|
||||
|
||||
let req = new GetDeviceLinkMetricsRequest();
|
||||
req.setDevEui(this.props.device.getDevEui());
|
||||
req.setDevEui(props.device.getDevEui());
|
||||
req.setStart(startPb);
|
||||
req.setEnd(endPb);
|
||||
req.setAggregation(agg);
|
||||
|
||||
DeviceStore.getLinkMetrics(req, (resp: GetDeviceLinkMetricsResponse) => {
|
||||
this.setState({
|
||||
deviceLinkMetrics: resp,
|
||||
deviceLinkMetricsLoaded: true,
|
||||
});
|
||||
setDeviceLinkMetrics(resp);
|
||||
setDeviceLinkMetricsLoaded(true);
|
||||
});
|
||||
};
|
||||
|
||||
onMetricsAggregationChange = (e: RadioChangeEvent) => {
|
||||
this.setState(
|
||||
{
|
||||
metricsAggregation: e.target.value,
|
||||
},
|
||||
this.loadMetrics,
|
||||
);
|
||||
const onMetricsAggregationChange = (e: RadioChangeEvent) => {
|
||||
setMetricsAggregation(e.target.value);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.deviceLinkMetrics === undefined || this.state.deviceMetrics === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let deviceMetrics = [];
|
||||
|
||||
{
|
||||
let states = this.state.deviceMetrics.getStatesMap();
|
||||
let keys = states.toArray().map(v => v[0]);
|
||||
keys.sort();
|
||||
|
||||
for (let i = 0; i < keys.length; i += 3) {
|
||||
let items = keys.slice(i, i + 3).map(k => {
|
||||
let m = states.get(k)!;
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic title={m.getName()} value={m.getValue()} />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
});
|
||||
|
||||
deviceMetrics.push(<Row gutter={24}>{items}</Row>);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let metrics = this.state.deviceMetrics.getMetricsMap();
|
||||
let keys = metrics.toArray().map(v => v[0]);
|
||||
keys.sort();
|
||||
|
||||
for (let i = 0; i < keys.length; i += 3) {
|
||||
let items = keys.slice(i, i + 3).map(k => {
|
||||
let m = metrics.get(k)!;
|
||||
return (
|
||||
<Col span={8}>
|
||||
<MetricChart metric={m} aggregation={this.state.metricsAggregation} zeroToNull />
|
||||
</Col>
|
||||
);
|
||||
});
|
||||
|
||||
deviceMetrics.push(<Row gutter={24}>{items}</Row>);
|
||||
}
|
||||
}
|
||||
|
||||
let lastSeenAt = "Never";
|
||||
if (this.props.lastSeenAt !== undefined) {
|
||||
lastSeenAt = moment(this.props.lastSeenAt).format("YYYY-MM-DD HH:mm:ss");
|
||||
}
|
||||
|
||||
const loading = !this.state.deviceLinkMetricsLoaded || !this.state.deviceMetrics;
|
||||
|
||||
const aggregations = (
|
||||
<Space direction="horizontal">
|
||||
{loading && <Spin size="small" />}
|
||||
<Radio.Group value={this.state.metricsAggregation} onChange={this.onMetricsAggregationChange} size="small">
|
||||
<Radio.Button value={Aggregation.HOUR} disabled={loading}>
|
||||
24h
|
||||
</Radio.Button>
|
||||
<Radio.Button value={Aggregation.DAY} disabled={loading}>
|
||||
31d
|
||||
</Radio.Button>
|
||||
<Radio.Button value={Aggregation.MONTH} disabled={loading}>
|
||||
1y
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
<Button type="primary" size="small" icon={<ReloadOutlined />} onClick={this.loadMetrics} disabled={loading} />
|
||||
</Space>
|
||||
);
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<Card>
|
||||
<Descriptions>
|
||||
<Descriptions.Item label="Last seen">{lastSeenAt}</Descriptions.Item>
|
||||
<Descriptions.Item label="Device profile">
|
||||
<Link
|
||||
to={`/tenants/${this.props.deviceProfile.getTenantId()}/device-profiles/${this.props.deviceProfile.getId()}/edit`}
|
||||
>
|
||||
{this.props.deviceProfile.getName()}
|
||||
</Link>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Enabled">{this.props.device.getIsDisabled() ? "no" : "yes"}</Descriptions.Item>
|
||||
<Descriptions.Item label="Description">{this.props.device.getDescription()}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
<Tabs tabBarExtraContent={aggregations}>
|
||||
<Tabs.TabPane tab="Link metrics" key="1">
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<Row gutter={24}>
|
||||
<Col span={8}>
|
||||
<MetricChart
|
||||
metric={this.state.deviceLinkMetrics.getRxPackets()!}
|
||||
aggregation={this.state.metricsAggregation}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<MetricChart
|
||||
metric={this.state.deviceLinkMetrics.getGwRssi()!}
|
||||
aggregation={this.state.metricsAggregation}
|
||||
zeroToNull
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<MetricChart
|
||||
metric={this.state.deviceLinkMetrics.getGwSnr()!}
|
||||
aggregation={this.state.metricsAggregation}
|
||||
zeroToNull
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={24}>
|
||||
<Col span={8}>
|
||||
<MetricHeatmap
|
||||
metric={this.state.deviceLinkMetrics.getRxPacketsPerFreq()!}
|
||||
aggregation={this.state.metricsAggregation}
|
||||
fromColor="rgb(227, 242, 253)"
|
||||
toColor="rgb(33, 150, 243, 1)"
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<MetricHeatmap
|
||||
metric={this.state.deviceLinkMetrics.getRxPacketsPerDr()!}
|
||||
aggregation={this.state.metricsAggregation}
|
||||
fromColor="rgb(227, 242, 253)"
|
||||
toColor="rgb(33, 150, 243, 1)"
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<MetricBar
|
||||
metric={this.state.deviceLinkMetrics.getErrors()!}
|
||||
aggregation={this.state.metricsAggregation}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Device metrics" key="2">
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
{deviceMetrics}
|
||||
</Space>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Space>
|
||||
);
|
||||
if (deviceLinkMetrics === undefined || deviceMetrics === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let dm = [];
|
||||
|
||||
{
|
||||
let states = deviceMetrics.getStatesMap();
|
||||
let keys = states.toArray().map(v => v[0]);
|
||||
keys.sort();
|
||||
|
||||
for (let i = 0; i < keys.length; i += 3) {
|
||||
let items = keys.slice(i, i + 3).map(k => {
|
||||
let m = states.get(k)!;
|
||||
return (
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic title={m.getName()} value={m.getValue()} />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
});
|
||||
|
||||
dm.push(<Row gutter={24}>{items}</Row>);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let metrics = deviceMetrics.getMetricsMap();
|
||||
let keys = metrics.toArray().map(v => v[0]);
|
||||
keys.sort();
|
||||
|
||||
for (let i = 0; i < keys.length; i += 3) {
|
||||
let items = keys.slice(i, i + 3).map(k => {
|
||||
let m = metrics.get(k)!;
|
||||
return (
|
||||
<Col span={8}>
|
||||
<MetricChart metric={m} aggregation={metricsAggregation} zeroToNull />
|
||||
</Col>
|
||||
);
|
||||
});
|
||||
|
||||
dm.push(<Row gutter={24}>{items}</Row>);
|
||||
}
|
||||
}
|
||||
|
||||
let lastSeenAt = "Never";
|
||||
if (props.lastSeenAt !== undefined) {
|
||||
lastSeenAt = moment(props.lastSeenAt).format("YYYY-MM-DD HH:mm:ss");
|
||||
}
|
||||
|
||||
const loading = !deviceLinkMetricsLoaded || !deviceMetrics;
|
||||
|
||||
const aggregations = (
|
||||
<Space direction="horizontal">
|
||||
{loading && <Spin size="small" />}
|
||||
<Radio.Group value={metricsAggregation} onChange={onMetricsAggregationChange} size="small">
|
||||
<Radio.Button value={Aggregation.HOUR} disabled={loading}>
|
||||
24h
|
||||
</Radio.Button>
|
||||
<Radio.Button value={Aggregation.DAY} disabled={loading}>
|
||||
31d
|
||||
</Radio.Button>
|
||||
<Radio.Button value={Aggregation.MONTH} disabled={loading}>
|
||||
1y
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
<Button type="primary" size="small" icon={<ReloadOutlined />} onClick={loadMetrics} disabled={loading} />
|
||||
</Space>
|
||||
);
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<Card>
|
||||
<Descriptions>
|
||||
<Descriptions.Item label="Last seen">{lastSeenAt}</Descriptions.Item>
|
||||
<Descriptions.Item label="Device profile">
|
||||
<Link
|
||||
to={`/tenants/${props.deviceProfile.getTenantId()}/device-profiles/${props.deviceProfile.getId()}/edit`}
|
||||
>
|
||||
{props.deviceProfile.getName()}
|
||||
</Link>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Enabled">{props.device.getIsDisabled() ? "no" : "yes"}</Descriptions.Item>
|
||||
<Descriptions.Item label="Description">{props.device.getDescription()}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
<Tabs tabBarExtraContent={aggregations}>
|
||||
<Tabs.TabPane tab="Link metrics" key="1">
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<Row gutter={24}>
|
||||
<Col span={8}>
|
||||
<MetricChart metric={deviceLinkMetrics.getRxPackets()!} aggregation={metricsAggregation} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<MetricChart metric={deviceLinkMetrics.getGwRssi()!} aggregation={metricsAggregation} zeroToNull />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<MetricChart metric={deviceLinkMetrics.getGwSnr()!} aggregation={metricsAggregation} zeroToNull />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={24}>
|
||||
<Col span={8}>
|
||||
<MetricHeatmap
|
||||
metric={deviceLinkMetrics.getRxPacketsPerFreq()!}
|
||||
aggregation={metricsAggregation}
|
||||
fromColor="rgb(227, 242, 253)"
|
||||
toColor="rgb(33, 150, 243, 1)"
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<MetricHeatmap
|
||||
metric={deviceLinkMetrics.getRxPacketsPerDr()!}
|
||||
aggregation={metricsAggregation}
|
||||
fromColor="rgb(227, 242, 253)"
|
||||
toColor="rgb(33, 150, 243, 1)"
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<MetricBar metric={deviceLinkMetrics.getErrors()!} aggregation={metricsAggregation} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Device metrics" key="2">
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
{dm}
|
||||
</Space>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeviceDashboard;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { Device } from "@chirpstack/chirpstack-api-grpc-web/api/device_pb";
|
||||
import { StreamDeviceEventsRequest, LogItem } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
|
||||
@ -10,55 +10,31 @@ interface IProps {
|
||||
device: Device;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
events: LogItem[];
|
||||
cancelFunc?: () => void;
|
||||
}
|
||||
function DeviceEvents(props: IProps) {
|
||||
const [events, setEvents] = useState<LogItem[]>([]);
|
||||
|
||||
class DeviceEvents extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
useEffect(() => {
|
||||
const onMessage = (l: LogItem) => {
|
||||
setEvents(e => {
|
||||
if (e.length === 0 || parseInt(l.getId().replace("-", "")) > parseInt(e[0].getId().replace("-", ""))) {
|
||||
e.unshift(l);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
events: [],
|
||||
cancelFunc: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.connectStream();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.state.cancelFunc !== undefined) {
|
||||
this.state.cancelFunc();
|
||||
}
|
||||
}
|
||||
|
||||
connectStream = () => {
|
||||
let req = new StreamDeviceEventsRequest();
|
||||
req.setDevEui(this.props.device.getDevEui());
|
||||
|
||||
let cancelFunc = InternalStore.streamDeviceEvents(req, this.onMessage);
|
||||
this.setState({
|
||||
cancelFunc: cancelFunc,
|
||||
});
|
||||
};
|
||||
|
||||
onMessage = (l: LogItem) => {
|
||||
let events = this.state.events;
|
||||
|
||||
if (events.length === 0 || parseInt(l.getId().replace("-", "")) > parseInt(events[0].getId().replace("-", ""))) {
|
||||
events.unshift(l);
|
||||
this.setState({
|
||||
events: events,
|
||||
return e;
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
render() {
|
||||
return <LogTable logs={this.state.events} />;
|
||||
}
|
||||
let req = new StreamDeviceEventsRequest();
|
||||
req.setDevEui(props.device.getDevEui());
|
||||
|
||||
let cancelFunc = InternalStore.streamDeviceEvents(req, onMessage);
|
||||
|
||||
return () => {
|
||||
cancelFunc();
|
||||
};
|
||||
}, [props]);
|
||||
|
||||
return <LogTable logs={events} />;
|
||||
}
|
||||
|
||||
export default DeviceEvents;
|
||||
|
@ -1,5 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Form, Input, Row, Col, Button, Tabs, Switch } from "antd";
|
||||
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
|
||||
@ -24,11 +22,11 @@ interface IProps {
|
||||
update?: boolean;
|
||||
}
|
||||
|
||||
class DeviceForm extends Component<IProps> {
|
||||
formRef = React.createRef<any>();
|
||||
function DeviceForm(props: IProps) {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
onFinish = (values: Device.AsObject) => {
|
||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
||||
const onFinish = (values: Device.AsObject) => {
|
||||
const v = Object.assign(props.initialValues.toObject(), values);
|
||||
let d = new Device();
|
||||
|
||||
d.setApplicationId(v.applicationId);
|
||||
@ -50,12 +48,12 @@ class DeviceForm extends Component<IProps> {
|
||||
d.getVariablesMap().set(elm[0], elm[1]);
|
||||
}
|
||||
|
||||
this.props.onFinish(d);
|
||||
props.onFinish(d);
|
||||
};
|
||||
|
||||
getDeviceProfileOptions = (search: string, fn: OptionsCallbackFunc) => {
|
||||
const getDeviceProfileOptions = (search: string, fn: OptionsCallbackFunc) => {
|
||||
let req = new ListDeviceProfilesRequest();
|
||||
req.setTenantId(this.props.tenant.getId());
|
||||
req.setTenantId(props.tenant.getId());
|
||||
req.setSearch(search);
|
||||
req.setLimit(10);
|
||||
|
||||
@ -63,11 +61,12 @@ class DeviceForm extends Component<IProps> {
|
||||
const options = resp.getResultList().map((o, i) => {
|
||||
return { label: o.getName(), value: o.getId() };
|
||||
});
|
||||
|
||||
fn(options);
|
||||
});
|
||||
};
|
||||
|
||||
getDeviceProfileOption = (id: string, fn: OptionCallbackFunc) => {
|
||||
const getDeviceProfileOption = (id: string, fn: OptionCallbackFunc) => {
|
||||
let req = new GetDeviceProfileRequest();
|
||||
req.setId(id);
|
||||
|
||||
@ -79,163 +78,153 @@ class DeviceForm extends Component<IProps> {
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Form
|
||||
layout="vertical"
|
||||
initialValues={this.props.initialValues.toObject()}
|
||||
onFinish={this.onFinish}
|
||||
ref={this.formRef}
|
||||
>
|
||||
<Tabs>
|
||||
<Tabs.TabPane tab="Device" key="1">
|
||||
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Description" name="description">
|
||||
<Input.TextArea />
|
||||
</Form.Item>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<EuiInput
|
||||
label="Device EUI (EUI64)"
|
||||
name="devEui"
|
||||
value={this.props.initialValues.getDevEui()}
|
||||
formRef={this.formRef}
|
||||
disabled={this.props.update}
|
||||
required
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<EuiInput
|
||||
label="Join EUI (EUI64)"
|
||||
name="joinEui"
|
||||
value={this.props.initialValues.getJoinEui()}
|
||||
formRef={this.formRef}
|
||||
tooltip="The Join EUI will be automatically set / updated on OTAA. However, in some cases this field must be configured before OTAA (e.g. OTAA using a Relay)."
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<AutocompleteInput
|
||||
label="Device profile"
|
||||
name="deviceProfileId"
|
||||
formRef={this.formRef}
|
||||
getOption={this.getDeviceProfileOption}
|
||||
getOptions={this.getDeviceProfileOptions}
|
||||
required
|
||||
/>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Device is disabled"
|
||||
name="isDisabled"
|
||||
valuePropName="checked"
|
||||
tooltip="Received uplink frames and join-requests will be ignored."
|
||||
>
|
||||
<Switch />
|
||||
return (
|
||||
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
|
||||
<Tabs>
|
||||
<Tabs.TabPane tab="Device" key="1">
|
||||
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Description" name="description">
|
||||
<Input.TextArea />
|
||||
</Form.Item>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<EuiInput
|
||||
label="Device EUI (EUI64)"
|
||||
name="devEui"
|
||||
value={props.initialValues.getDevEui()}
|
||||
disabled={props.update}
|
||||
required
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<EuiInput
|
||||
label="Join EUI (EUI64)"
|
||||
name="joinEui"
|
||||
value={props.initialValues.getJoinEui()}
|
||||
tooltip="The Join EUI will be automatically set / updated on OTAA. However, in some cases this field must be configured before OTAA (e.g. OTAA using a Relay)."
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<AutocompleteInput
|
||||
label="Device profile"
|
||||
name="deviceProfileId"
|
||||
getOption={getDeviceProfileOption}
|
||||
getOptions={getDeviceProfileOptions}
|
||||
required
|
||||
/>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Device is disabled"
|
||||
name="isDisabled"
|
||||
valuePropName="checked"
|
||||
tooltip="Received uplink frames and join-requests will be ignored."
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Disable frame-counter validation"
|
||||
name="skipFcntCheck"
|
||||
valuePropName="checked"
|
||||
tooltip="You must re-activate your device before this setting becomes effective. Note that disabling the frame-counter validation will compromise security as it allows replay-attacks."
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Tags" key="2">
|
||||
<Form.List name="tagsMap">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Row gutter={24}>
|
||||
<Col span={6}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 0]}
|
||||
fieldKey={[name, 0]}
|
||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||
>
|
||||
<Input placeholder="Key" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 1]}
|
||||
fieldKey={[name, 1]}
|
||||
rules={[{ required: true, message: "Please enter a value!" }]}
|
||||
>
|
||||
<Input placeholder="Value" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
Add tag
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Disable frame-counter validation"
|
||||
name="skipFcntCheck"
|
||||
valuePropName="checked"
|
||||
tooltip="You must re-activate your device before this setting becomes effective. Note that disabling the frame-counter validation will compromise security as it allows replay-attacks."
|
||||
>
|
||||
<Switch />
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Variables" key="3">
|
||||
<Form.List name="variablesMap">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Row gutter={24}>
|
||||
<Col span={6}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 0]}
|
||||
fieldKey={[name, 0]}
|
||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||
>
|
||||
<Input placeholder="Key" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 1]}
|
||||
fieldKey={[name, 1]}
|
||||
rules={[{ required: true, message: "Please enter a value!" }]}
|
||||
>
|
||||
<Input placeholder="Value" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
Add variable
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Tags" key="2">
|
||||
<Form.List name="tagsMap">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Row gutter={24}>
|
||||
<Col span={6}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 0]}
|
||||
fieldKey={[name, 0]}
|
||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||
>
|
||||
<Input placeholder="Key" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 1]}
|
||||
fieldKey={[name, 1]}
|
||||
rules={[{ required: true, message: "Please enter a value!" }]}
|
||||
>
|
||||
<Input placeholder="Value" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
Add tag
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Variables" key="3">
|
||||
<Form.List name="variablesMap">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Row gutter={24}>
|
||||
<Col span={6}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 0]}
|
||||
fieldKey={[name, 0]}
|
||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||
>
|
||||
<Input placeholder="Key" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 1]}
|
||||
fieldKey={[name, 1]}
|
||||
rules={[{ required: true, message: "Please enter a value!" }]}
|
||||
>
|
||||
<Input placeholder="Value" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
Add variable
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeviceForm;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { Device } from "@chirpstack/chirpstack-api-grpc-web/api/device_pb";
|
||||
import { StreamDeviceFramesRequest, LogItem } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
|
||||
@ -10,55 +10,31 @@ interface IProps {
|
||||
device: Device;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
frames: LogItem[];
|
||||
cancelFunc?: () => void;
|
||||
}
|
||||
function DeviceFrames(props: IProps) {
|
||||
const [frames, setFrames] = useState<LogItem[]>([]);
|
||||
|
||||
class DeviceFrames extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
useEffect(() => {
|
||||
const onMessage = (l: LogItem) => {
|
||||
setFrames(f => {
|
||||
if (f.length === 0 || parseInt(l.getId().replace("-", "")) > parseInt(f[0].getId().replace("-", ""))) {
|
||||
f.unshift(l);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
frames: [],
|
||||
cancelFunc: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.connectStream();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.state.cancelFunc !== undefined) {
|
||||
this.state.cancelFunc();
|
||||
}
|
||||
}
|
||||
|
||||
connectStream = () => {
|
||||
let req = new StreamDeviceFramesRequest();
|
||||
req.setDevEui(this.props.device.getDevEui());
|
||||
|
||||
let cancelFunc = InternalStore.streamDeviceFrames(req, this.onMessage);
|
||||
this.setState({
|
||||
cancelFunc: cancelFunc,
|
||||
});
|
||||
};
|
||||
|
||||
onMessage = (l: LogItem) => {
|
||||
let frames = this.state.frames;
|
||||
|
||||
if (frames.length === 0 || parseInt(l.getId().replace("-", "")) > parseInt(frames[0].getId().replace("-", ""))) {
|
||||
frames.unshift(l);
|
||||
this.setState({
|
||||
frames: frames,
|
||||
return f;
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
render() {
|
||||
return <LogTable logs={this.state.frames} />;
|
||||
}
|
||||
let req = new StreamDeviceFramesRequest();
|
||||
req.setDevEui(props.device.getDevEui());
|
||||
|
||||
let cancelFunc = InternalStore.streamDeviceFrames(req, onMessage);
|
||||
|
||||
return () => {
|
||||
cancelFunc();
|
||||
};
|
||||
}, [props]);
|
||||
|
||||
return <LogTable logs={frames} />;
|
||||
}
|
||||
|
||||
export default DeviceFrames;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
import { Route, Switch, RouteComponentProps, Link } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Route, Routes, useParams, Link, useNavigate, useLocation } from "react-router-dom";
|
||||
|
||||
import { Space, Breadcrumb, Card, Button, PageHeader, Menu } from "antd";
|
||||
import { Space, Breadcrumb, Card, Button, Menu } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
||||
import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||
@ -30,239 +31,183 @@ import DeviceEvents from "./DeviceEvents";
|
||||
import DeviceQueue from "./DeviceQueue";
|
||||
import DeviceActivation from "./DeviceActivation";
|
||||
|
||||
interface MatchParams {
|
||||
devEui: string;
|
||||
}
|
||||
|
||||
interface IProps extends RouteComponentProps<MatchParams> {
|
||||
interface IProps {
|
||||
tenant: Tenant;
|
||||
application: Application;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
device?: Device;
|
||||
deviceProfile?: DeviceProfile;
|
||||
lastSeenAt?: Date;
|
||||
}
|
||||
function DeviceLayout(props: IProps) {
|
||||
const { devEui } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
class DeviceLayout extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
const [device, setDevice] = useState<Device | undefined>(undefined);
|
||||
const [deviceProfile, setDeviceProfile] = useState<DeviceProfile | undefined>(undefined);
|
||||
const [lastSeenAt, setLastSeenAt] = useState<Date | undefined>(undefined);
|
||||
|
||||
componentDidMount() {
|
||||
this.getDevice(this.getDeviceProfile);
|
||||
}
|
||||
|
||||
getDevice = (cb: () => void) => {
|
||||
useEffect(() => {
|
||||
let req = new GetDeviceRequest();
|
||||
req.setDevEui(this.props.match.params.devEui);
|
||||
req.setDevEui(devEui!);
|
||||
|
||||
DeviceStore.get(req, (resp: GetDeviceResponse) => {
|
||||
this.setState(
|
||||
{
|
||||
device: resp.getDevice(),
|
||||
},
|
||||
cb,
|
||||
);
|
||||
setDevice(resp.getDevice());
|
||||
|
||||
if (resp.getLastSeenAt() !== undefined) {
|
||||
this.setState({
|
||||
lastSeenAt: resp.getLastSeenAt()!.toDate(),
|
||||
});
|
||||
setLastSeenAt(resp.getLastSeenAt()!.toDate());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
getDeviceProfile = () => {
|
||||
let req = new GetDeviceProfileRequest();
|
||||
req.setId(this.state.device!.getDeviceProfileId());
|
||||
|
||||
DeviceProfileStore.get(req, (resp: GetDeviceProfileResponse) => {
|
||||
this.setState({
|
||||
deviceProfile: resp.getDeviceProfile(),
|
||||
let req = new GetDeviceProfileRequest();
|
||||
req.setId(resp.getDevice()!.getDeviceProfileId());
|
||||
DeviceProfileStore.get(req, (resp: GetDeviceProfileResponse) => {
|
||||
setDeviceProfile(resp.getDeviceProfile());
|
||||
});
|
||||
});
|
||||
};
|
||||
}, [devEui]);
|
||||
|
||||
deleteDevice = () => {
|
||||
const deleteDevice = () => {
|
||||
let req = new DeleteDeviceRequest();
|
||||
req.setDevEui(this.props.match.params.devEui);
|
||||
req.setDevEui(devEui!);
|
||||
|
||||
DeviceStore.delete(req, () => {
|
||||
this.props.history.push(`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}`);
|
||||
navigate(`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const device = this.state.device;
|
||||
const dp = this.state.deviceProfile;
|
||||
if (!device || !dp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tenant = this.props.tenant;
|
||||
const app = this.props.application;
|
||||
|
||||
const path = this.props.history.location.pathname;
|
||||
let tab = "dashboard";
|
||||
|
||||
if (path.endsWith("edit")) {
|
||||
tab = "edit";
|
||||
}
|
||||
if (path.endsWith("queue")) {
|
||||
tab = "queue";
|
||||
}
|
||||
if (path.endsWith("keys")) {
|
||||
tab = "keys";
|
||||
}
|
||||
if (path.endsWith("activation")) {
|
||||
tab = "activation";
|
||||
}
|
||||
if (path.endsWith("events")) {
|
||||
tab = "events";
|
||||
}
|
||||
if (path.endsWith("frames")) {
|
||||
tab = "frames";
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}/applications`}>Applications</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}`}>
|
||||
{this.props.application.getName()}
|
||||
</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}`}>
|
||||
Devices
|
||||
</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>{device.getName()}</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title={device.getName()}
|
||||
subTitle={`device eui: ${device.getDevEui()}`}
|
||||
extra={[
|
||||
<Admin tenantId={this.props.tenant.getId()} isDeviceAdmin>
|
||||
<DeleteConfirm typ="device" confirm={device.getName()} onConfirm={this.deleteDevice}>
|
||||
<Button danger type="primary">
|
||||
Delete device
|
||||
</Button>
|
||||
</DeleteConfirm>
|
||||
</Admin>,
|
||||
]}
|
||||
/>
|
||||
<Card>
|
||||
<Menu mode="horizontal" selectedKeys={[tab]} style={{ marginBottom: 24 }}>
|
||||
<Menu.Item key="dashboard">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}`}>
|
||||
Dashboard
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="edit">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/edit`}>
|
||||
Configuration
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="keys" disabled={!dp.getSupportsOtaa()}>
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/keys`}>
|
||||
OTAA keys
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="activation">
|
||||
<Link
|
||||
to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/activation`}
|
||||
>
|
||||
Activation
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="queue">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/queue`}>
|
||||
Queue
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="events">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/events`}>
|
||||
Events
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="frames">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/frames`}>
|
||||
LoRaWAN frames
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={this.props.match.path}
|
||||
render={props => (
|
||||
<DeviceDashboard device={device} lastSeenAt={this.state.lastSeenAt} deviceProfile={dp} {...props} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/edit`}
|
||||
render={props => <EditDevice device={device} application={app} tenant={tenant} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/keys`}
|
||||
render={props => (
|
||||
<SetDeviceKeys device={device} application={app} tenant={tenant} deviceProfile={dp} {...props} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/frames`}
|
||||
render={props => <DeviceFrames device={device} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/events`}
|
||||
render={props => <DeviceEvents device={device} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/queue`}
|
||||
render={props => <DeviceQueue device={device} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${this.props.match.path}/activation`}
|
||||
render={props => (
|
||||
<DeviceActivation device={device} deviceProfile={dp} tenant={tenant} application={app} {...props} />
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
const dp = deviceProfile;
|
||||
if (!device || !dp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tenant = props.tenant;
|
||||
const app = props.application;
|
||||
|
||||
const path = location.pathname;
|
||||
let tab = "dashboard";
|
||||
|
||||
if (path.endsWith("edit")) {
|
||||
tab = "edit";
|
||||
}
|
||||
if (path.endsWith("queue")) {
|
||||
tab = "queue";
|
||||
}
|
||||
if (path.endsWith("keys")) {
|
||||
tab = "keys";
|
||||
}
|
||||
if (path.endsWith("activation")) {
|
||||
tab = "activation";
|
||||
}
|
||||
if (path.endsWith("events")) {
|
||||
tab = "events";
|
||||
}
|
||||
if (path.endsWith("frames")) {
|
||||
tab = "frames";
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<PageHeader
|
||||
breadcrumbRender={() => (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<span>Tenants</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}/applications`}>Applications</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}`}>
|
||||
{props.application.getName()}
|
||||
</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>
|
||||
<Link to={`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}`}>Devices</Link>
|
||||
</span>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<span>{device.getName()}</span>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
title={device.getName()}
|
||||
subTitle={`device eui: ${device.getDevEui()}`}
|
||||
extra={[
|
||||
<Admin tenantId={props.tenant.getId()} isDeviceAdmin>
|
||||
<DeleteConfirm typ="device" confirm={device.getName()} onConfirm={deleteDevice}>
|
||||
<Button danger type="primary">
|
||||
Delete device
|
||||
</Button>
|
||||
</DeleteConfirm>
|
||||
</Admin>,
|
||||
]}
|
||||
/>
|
||||
<Card>
|
||||
<Menu mode="horizontal" selectedKeys={[tab]} style={{ marginBottom: 24 }}>
|
||||
<Menu.Item key="dashboard">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}`}>
|
||||
Dashboard
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="edit">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/edit`}>
|
||||
Configuration
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="keys" disabled={!dp.getSupportsOtaa()}>
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/keys`}>
|
||||
OTAA keys
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="activation">
|
||||
<Link
|
||||
to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/activation`}
|
||||
>
|
||||
Activation
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="queue">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/queue`}>
|
||||
Queue
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="events">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/events`}>
|
||||
Events
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="frames">
|
||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/frames`}>
|
||||
LoRaWAN frames
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
<Routes>
|
||||
<Route path="/" element={<DeviceDashboard device={device} lastSeenAt={lastSeenAt} deviceProfile={dp} />} />
|
||||
<Route path="/edit" element={<EditDevice device={device} application={app} tenant={tenant} />} />
|
||||
<Route
|
||||
path="/keys"
|
||||
element={<SetDeviceKeys device={device} application={app} tenant={tenant} deviceProfile={dp} />}
|
||||
/>
|
||||
<Route path="/frames" element={<DeviceFrames device={device} />} />
|
||||
<Route path="/events" element={<DeviceEvents device={device} />} />
|
||||
<Route path="/queue" element={<DeviceQueue device={device} />} />
|
||||
<Route
|
||||
path="/activation"
|
||||
element={<DeviceActivation device={device} deviceProfile={dp} tenant={tenant} application={app} />}
|
||||
/>
|
||||
</Routes>
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeviceLayout;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { Struct } from "google-protobuf/google/protobuf/struct_pb";
|
||||
|
||||
@ -25,87 +25,75 @@ interface IProps {
|
||||
device: Device;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
refreshCounter: number;
|
||||
}
|
||||
function DeviceQueue(props: IProps) {
|
||||
const [refreshCounter, setRefreshCounter] = useState<number>(0);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
class DeviceQueue extends Component<IProps, IState> {
|
||||
formRef = React.createRef<any>();
|
||||
const columns: ColumnsType<DeviceQueueItem.AsObject> = [
|
||||
{
|
||||
title: "ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
width: 350,
|
||||
},
|
||||
{
|
||||
title: "Is pending",
|
||||
dataIndex: "isPending",
|
||||
key: "isPending",
|
||||
width: 100,
|
||||
render: (text, record) => {
|
||||
if (record.isPending) {
|
||||
return "yes";
|
||||
} else {
|
||||
return "no";
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Frame-counter",
|
||||
dataIndex: "fCntDown",
|
||||
key: "fCntDown",
|
||||
width: 200,
|
||||
render: (text, record) => {
|
||||
if (record.isPending === true) {
|
||||
return record.fCntDown;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Confirmed",
|
||||
dataIndex: "confirmed",
|
||||
key: "confirmed",
|
||||
width: 100,
|
||||
render: (text, record) => {
|
||||
if (record.confirmed) {
|
||||
return "yes";
|
||||
} else {
|
||||
return "no";
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "FPort",
|
||||
dataIndex: "fPort",
|
||||
key: "fPort",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "Data (HEX)",
|
||||
dataIndex: "data",
|
||||
key: "data",
|
||||
render: (text, record) => {
|
||||
return Buffer.from(record.data as string, "base64").toString("hex");
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
refreshCounter: 0,
|
||||
};
|
||||
}
|
||||
|
||||
columns = (): ColumnsType<DeviceQueueItem.AsObject> => {
|
||||
return [
|
||||
{
|
||||
title: "ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
width: 350,
|
||||
},
|
||||
{
|
||||
title: "Is pending",
|
||||
dataIndex: "isPending",
|
||||
key: "isPending",
|
||||
width: 100,
|
||||
render: (text, record) => {
|
||||
if (record.isPending) {
|
||||
return "yes";
|
||||
} else {
|
||||
return "no";
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Frame-counter",
|
||||
dataIndex: "fCntDown",
|
||||
key: "fCntDown",
|
||||
width: 200,
|
||||
render: (text, record) => {
|
||||
if (record.isPending === true) {
|
||||
return record.fCntDown;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Confirmed",
|
||||
dataIndex: "confirmed",
|
||||
key: "confirmed",
|
||||
width: 100,
|
||||
render: (text, record) => {
|
||||
if (record.confirmed) {
|
||||
return "yes";
|
||||
} else {
|
||||
return "no";
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "FPort",
|
||||
dataIndex: "fPort",
|
||||
key: "fPort",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "Data (HEX)",
|
||||
dataIndex: "data",
|
||||
key: "data",
|
||||
render: (text, record) => {
|
||||
return Buffer.from(record.data as string, "base64").toString("hex");
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||
let req = new GetDeviceQueueItemsRequest();
|
||||
req.setDevEui(this.props.device.getDevEui());
|
||||
req.setDevEui(props.device.getDevEui());
|
||||
|
||||
DeviceStore.getQueue(req, (resp: GetDeviceQueueItemsResponse) => {
|
||||
const obj = resp.toObject();
|
||||
@ -113,25 +101,23 @@ class DeviceQueue extends Component<IProps, IState> {
|
||||
});
|
||||
};
|
||||
|
||||
refreshQueue = () => {
|
||||
this.setState({
|
||||
refreshCounter: this.state.refreshCounter + 1,
|
||||
});
|
||||
const refreshQueue = () => {
|
||||
setRefreshCounter(refreshCounter + 1);
|
||||
};
|
||||
|
||||
flushQueue = () => {
|
||||
const flushQueue = () => {
|
||||
let req = new FlushDeviceQueueRequest();
|
||||
req.setDevEui(this.props.device.getDevEui());
|
||||
req.setDevEui(props.device.getDevEui());
|
||||
DeviceStore.flushQueue(req, () => {
|
||||
this.refreshQueue();
|
||||
refreshQueue();
|
||||
});
|
||||
};
|
||||
|
||||
onEnqueue = (values: any) => {
|
||||
const onEnqueue = (values: any) => {
|
||||
let req = new EnqueueDeviceQueueItemRequest();
|
||||
let item = new DeviceQueueItem();
|
||||
|
||||
item.setDevEui(this.props.device.getDevEui());
|
||||
item.setDevEui(props.device.getDevEui());
|
||||
item.setFPort(values.fPort);
|
||||
item.setConfirmed(values.confirmed);
|
||||
|
||||
@ -163,66 +149,58 @@ class DeviceQueue extends Component<IProps, IState> {
|
||||
req.setQueueItem(item);
|
||||
|
||||
DeviceStore.enqueue(req, _ => {
|
||||
this.formRef.current.resetFields();
|
||||
this.refreshQueue();
|
||||
form.resetFields();
|
||||
refreshQueue();
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<Card title="Enqueue">
|
||||
<Form layout="horizontal" onFinish={this.onEnqueue} ref={this.formRef} initialValues={{ fPort: 1 }}>
|
||||
<Row>
|
||||
<Space direction="horizontal" style={{ width: "100%" }} size="large">
|
||||
<Form.Item name="confirmed" label="Confirmed" valuePropName="checked">
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
<Form.Item name="fPort" label="FPort">
|
||||
<InputNumber min={1} max={254} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Row>
|
||||
<Tabs defaultActiveKey="1">
|
||||
<Tabs.TabPane tab="HEX" key="1">
|
||||
<Form.Item name="hex">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="BASE64" key="2">
|
||||
<Form.Item name="base64">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="JSON" key="3">
|
||||
<CodeEditor name="json" value="{}" formRef={this.formRef} />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Enqueue
|
||||
</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
<Row justify="end">
|
||||
<Space direction="horizontal" size="large">
|
||||
<Button icon={<RedoOutlined />} onClick={this.refreshQueue}>
|
||||
Reload
|
||||
</Button>
|
||||
<Popconfirm title="Are you sure you want to flush the queue?" placement="left" onConfirm={this.flushQueue}>
|
||||
<Button icon={<DeleteOutlined />}>Flush queue</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</Row>
|
||||
<DataTable
|
||||
columns={this.columns()}
|
||||
getPage={this.getPage}
|
||||
refreshKey={this.state.refreshCounter}
|
||||
rowKey="id"
|
||||
noPagination
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<Card title="Enqueue">
|
||||
<Form layout="horizontal" onFinish={onEnqueue} form={form} initialValues={{ fPort: 1 }}>
|
||||
<Row>
|
||||
<Space direction="horizontal" style={{ width: "100%" }} size="large">
|
||||
<Form.Item name="confirmed" label="Confirmed" valuePropName="checked">
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
<Form.Item name="fPort" label="FPort">
|
||||
<InputNumber min={1} max={254} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Row>
|
||||
<Tabs defaultActiveKey="1">
|
||||
<Tabs.TabPane tab="HEX" key="1">
|
||||
<Form.Item name="hex">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="BASE64" key="2">
|
||||
<Form.Item name="base64">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="JSON" key="3">
|
||||
<CodeEditor name="json" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Enqueue
|
||||
</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
<Row justify="end">
|
||||
<Space direction="horizontal" size="large">
|
||||
<Button icon={<RedoOutlined />} onClick={refreshQueue}>
|
||||
Reload
|
||||
</Button>
|
||||
<Popconfirm title="Are you sure you want to flush the queue?" placement="left" onConfirm={flushQueue}>
|
||||
<Button icon={<DeleteOutlined />}>Flush queue</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</Row>
|
||||
<DataTable columns={columns} getPage={getPage} refreshKey={refreshCounter} rowKey="id" noPagination />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeviceQueue;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
||||
import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||
@ -8,27 +7,25 @@ import { Device, UpdateDeviceRequest } from "@chirpstack/chirpstack-api-grpc-web
|
||||
import DeviceStore from "../../stores/DeviceStore";
|
||||
import DeviceForm from "./DeviceForm";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
interface IProps {
|
||||
tenant: Tenant;
|
||||
application: Application;
|
||||
device: Device;
|
||||
}
|
||||
|
||||
class EditDevice extends Component<IProps> {
|
||||
onFinish = (obj: Device) => {
|
||||
function EditDevice(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = (obj: Device) => {
|
||||
let req = new UpdateDeviceRequest();
|
||||
req.setDevice(obj);
|
||||
|
||||
DeviceStore.update(req, () => {
|
||||
this.props.history.push(
|
||||
`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}/devices/${obj.getDevEui()}`,
|
||||
);
|
||||
navigate(`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}/devices/${obj.getDevEui()}`);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return <DeviceForm initialValues={this.props.device} onFinish={this.onFinish} tenant={this.props.tenant} update />;
|
||||
}
|
||||
return <DeviceForm initialValues={props.device} onFinish={onFinish} tenant={props.tenant} update />;
|
||||
}
|
||||
|
||||
export default EditDevice;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import moment from "moment";
|
||||
@ -42,126 +42,105 @@ interface IProps {
|
||||
application: Application;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
selectedRowIds: string[];
|
||||
multicastGroups: MulticastGroupListItem[];
|
||||
relays: RelayListItem[];
|
||||
mgModalVisible: boolean;
|
||||
relayModalVisible: boolean;
|
||||
mgSelected: string;
|
||||
relaySelected: string;
|
||||
}
|
||||
function ListDevices(props: IProps) {
|
||||
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
|
||||
const [multicastGroups, setMulticastGroups] = useState<MulticastGroupListItem[]>([]);
|
||||
const [relays, setRelays] = useState<RelayListItem[]>([]);
|
||||
const [mgModalVisible, setMgModalVisible] = useState<boolean>(false);
|
||||
const [relayModalVisible, setRelayModalVisible] = useState<boolean>(false);
|
||||
const [mgSelected, setMgSelected] = useState<string>("");
|
||||
const [relaySelected, setRelaySelected] = useState<string>("");
|
||||
|
||||
class ListDevices extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedRowIds: [],
|
||||
multicastGroups: [],
|
||||
relays: [],
|
||||
mgModalVisible: false,
|
||||
relayModalVisible: false,
|
||||
mgSelected: "",
|
||||
relaySelected: "",
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
let mgReq = new ListMulticastGroupsRequest();
|
||||
mgReq.setLimit(999);
|
||||
mgReq.setApplicationId(this.props.application.getId());
|
||||
mgReq.setApplicationId(props.application.getId());
|
||||
|
||||
MulticastGroupStore.list(mgReq, (resp: ListMulticastGroupsResponse) => {
|
||||
this.setState({
|
||||
multicastGroups: resp.getResultList(),
|
||||
});
|
||||
setMulticastGroups(resp.getResultList());
|
||||
});
|
||||
|
||||
let relayReq = new ListRelaysRequest();
|
||||
relayReq.setLimit(999);
|
||||
relayReq.setApplicationId(this.props.application.getId());
|
||||
relayReq.setApplicationId(props.application.getId());
|
||||
|
||||
RelayStore.list(relayReq, (resp: ListRelaysResponse) => {
|
||||
this.setState({
|
||||
relays: resp.getResultList(),
|
||||
});
|
||||
setRelays(resp.getResultList());
|
||||
});
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
columns = (): ColumnsType<DeviceListItem.AsObject> => {
|
||||
return [
|
||||
{
|
||||
title: "Last seen",
|
||||
dataIndex: "lastSeenAt",
|
||||
key: "lastSeenAt",
|
||||
width: 250,
|
||||
render: (text, record) => {
|
||||
if (record.lastSeenAt !== undefined) {
|
||||
let ts = new Date(0);
|
||||
ts.setUTCSeconds(record.lastSeenAt.seconds);
|
||||
return moment(ts).format("YYYY-MM-DD HH:mm:ss");
|
||||
}
|
||||
return "Never";
|
||||
},
|
||||
const columns: ColumnsType<DeviceListItem.AsObject> = [
|
||||
{
|
||||
title: "Last seen",
|
||||
dataIndex: "lastSeenAt",
|
||||
key: "lastSeenAt",
|
||||
width: 250,
|
||||
render: (text, record) => {
|
||||
if (record.lastSeenAt !== undefined) {
|
||||
let ts = new Date(0);
|
||||
ts.setUTCSeconds(record.lastSeenAt.seconds);
|
||||
return moment(ts).format("YYYY-MM-DD HH:mm:ss");
|
||||
}
|
||||
return "Never";
|
||||
},
|
||||
{
|
||||
title: "DevEUI",
|
||||
dataIndex: "devEui",
|
||||
key: "devEui",
|
||||
width: 250,
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/devices/${
|
||||
record.devEui
|
||||
}`}
|
||||
>
|
||||
{text}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
title: "Device profile",
|
||||
dataIndex: "deviceProfileName",
|
||||
key: "deviceProfileName",
|
||||
render: (text, record) => (
|
||||
<Link to={`/tenants/${this.props.application.getTenantId()}/device-profiles/${record.deviceProfileId}/edit`}>
|
||||
{text}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Battery",
|
||||
dataIndex: "deviceStatus",
|
||||
key: "deviceStatus",
|
||||
render: (text, record) => {
|
||||
if (record.deviceStatus === undefined) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "DevEUI",
|
||||
dataIndex: "devEui",
|
||||
key: "devEui",
|
||||
width: 250,
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/devices/${
|
||||
record.devEui
|
||||
}`}
|
||||
>
|
||||
{text}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
title: "Device profile",
|
||||
dataIndex: "deviceProfileName",
|
||||
key: "deviceProfileName",
|
||||
render: (text, record) => (
|
||||
<Link to={`/tenants/${props.application.getTenantId()}/device-profiles/${record.deviceProfileId}/edit`}>
|
||||
{text}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Battery",
|
||||
dataIndex: "deviceStatus",
|
||||
key: "deviceStatus",
|
||||
render: (text, record) => {
|
||||
if (record.deviceStatus === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (record.deviceStatus.externalPowerSource === true) {
|
||||
return <FontAwesomeIcon icon={faPlug} />;
|
||||
} else if (record.deviceStatus.batteryLevel > 75) {
|
||||
return <FontAwesomeIcon icon={faBatteryFull} />;
|
||||
} else if (record.deviceStatus.batteryLevel > 50) {
|
||||
return <FontAwesomeIcon icon={faBatteryThreeQuarters} />;
|
||||
} else if (record.deviceStatus.batteryLevel > 25) {
|
||||
return <FontAwesomeIcon icon={faBatteryHalf} />;
|
||||
} else if (record.deviceStatus.batteryLevel > 0) {
|
||||
return <FontAwesomeIcon icon={faBatteryQuarter} />;
|
||||
}
|
||||
},
|
||||
if (record.deviceStatus.externalPowerSource === true) {
|
||||
return <FontAwesomeIcon icon={faPlug} />;
|
||||
} else if (record.deviceStatus.batteryLevel > 75) {
|
||||
return <FontAwesomeIcon icon={faBatteryFull} />;
|
||||
} else if (record.deviceStatus.batteryLevel > 50) {
|
||||
return <FontAwesomeIcon icon={faBatteryThreeQuarters} />;
|
||||
} else if (record.deviceStatus.batteryLevel > 25) {
|
||||
return <FontAwesomeIcon icon={faBatteryHalf} />;
|
||||
} else if (record.deviceStatus.batteryLevel > 0) {
|
||||
return <FontAwesomeIcon icon={faBatteryQuarter} />;
|
||||
}
|
||||
},
|
||||
];
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||
let req = new ListDevicesRequest();
|
||||
req.setApplicationId(this.props.application.getId());
|
||||
req.setApplicationId(props.application.getId());
|
||||
req.setLimit(limit);
|
||||
req.setOffset(offset);
|
||||
|
||||
@ -171,148 +150,114 @@ class ListDevices extends Component<IProps, IState> {
|
||||
});
|
||||
};
|
||||
|
||||
onRowsSelectChange = (ids: string[]) => {
|
||||
this.setState({
|
||||
selectedRowIds: ids,
|
||||
});
|
||||
const onRowsSelectChange = (ids: string[]) => {
|
||||
setSelectedRowIds(ids);
|
||||
};
|
||||
|
||||
showMgModal = () => {
|
||||
this.setState({
|
||||
mgModalVisible: true,
|
||||
});
|
||||
const showMgModal = () => {
|
||||
setMgModalVisible(true);
|
||||
};
|
||||
|
||||
showRelayModal = () => {
|
||||
this.setState({
|
||||
relayModalVisible: true,
|
||||
});
|
||||
}
|
||||
|
||||
hideMgModal = () => {
|
||||
this.setState({
|
||||
mgModalVisible: false,
|
||||
});
|
||||
const showRelayModal = () => {
|
||||
setRelayModalVisible(true);
|
||||
};
|
||||
|
||||
hideRelayModal = () => {
|
||||
this.setState({
|
||||
relayModalVisible: false,
|
||||
});
|
||||
}
|
||||
|
||||
onMgSelected = (value: string) => {
|
||||
this.setState({
|
||||
mgSelected: value,
|
||||
});
|
||||
const hideMgModal = () => {
|
||||
setMgModalVisible(false);
|
||||
};
|
||||
|
||||
onRelaySelected = (value: string) => {
|
||||
this.setState({
|
||||
relaySelected: value,
|
||||
});
|
||||
const hideRelayModal = () => {
|
||||
setRelayModalVisible(false);
|
||||
};
|
||||
|
||||
handleMgModalOk = () => {
|
||||
for (let devEui of this.state.selectedRowIds) {
|
||||
const onMgSelected = (value: string) => {
|
||||
setMgSelected(value);
|
||||
};
|
||||
|
||||
const onRelaySelected = (value: string) => {
|
||||
setRelaySelected(value);
|
||||
};
|
||||
|
||||
const handleMgModalOk = () => {
|
||||
for (let devEui of selectedRowIds) {
|
||||
let req = new AddDeviceToMulticastGroupRequest();
|
||||
req.setMulticastGroupId(this.state.mgSelected);
|
||||
req.setMulticastGroupId(mgSelected);
|
||||
req.setDevEui(devEui);
|
||||
|
||||
MulticastGroupStore.addDevice(req, () => {});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
mgModalVisible: false,
|
||||
});
|
||||
setMgModalVisible(false);
|
||||
};
|
||||
|
||||
handleRelayModalOk = () => {
|
||||
for (let devEui of this.state.selectedRowIds) {
|
||||
const handleRelayModalOk = () => {
|
||||
for (let devEui of selectedRowIds) {
|
||||
let req = new AddRelayDeviceRequest();
|
||||
req.setRelayDevEui(this.state.relaySelected);
|
||||
req.setRelayDevEui(relaySelected);
|
||||
req.setDeviceDevEui(devEui);
|
||||
|
||||
RelayStore.addDevice(req, () => {});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
relayModalVisible: false,
|
||||
});
|
||||
setRelayModalVisible(false);
|
||||
};
|
||||
|
||||
render() {
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item onClick={this.showMgModal}>Add to multicast-group</Menu.Item>
|
||||
<Menu.Item onClick={this.showRelayModal}>Add to relay</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item onClick={showMgModal}>Add to multicast-group</Menu.Item>
|
||||
<Menu.Item onClick={showRelayModal}>Add to relay</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const mgOptions = this.state.multicastGroups.map((mg, i) => (
|
||||
<Select.Option value={mg.getId()}>{mg.getName()}</Select.Option>
|
||||
));
|
||||
const mgOptions = multicastGroups.map((mg, i) => <Select.Option value={mg.getId()}>{mg.getName()}</Select.Option>);
|
||||
|
||||
const relayOptions = this.state.relays.map((r, i) => (
|
||||
<Select.Option value={r.getDevEui()}>{r.getName()}</Select.Option>
|
||||
));
|
||||
const relayOptions = relays.map((r, i) => <Select.Option value={r.getDevEui()}>{r.getName()}</Select.Option>);
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Modal
|
||||
title="Add selected devices to multicast-group"
|
||||
visible={this.state.mgModalVisible}
|
||||
onOk={this.handleMgModalOk}
|
||||
onCancel={this.hideMgModal}
|
||||
okButtonProps={{ disabled: this.state.mgSelected === "" }}
|
||||
>
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Select style={{ width: "100%" }} onChange={this.onMgSelected} placeholder="Select Multicast-group">
|
||||
{mgOptions}
|
||||
</Select>
|
||||
</Space>
|
||||
</Modal>
|
||||
<Modal
|
||||
title="Add selected devices to relay"
|
||||
visible={this.state.relayModalVisible}
|
||||
onOk={this.handleRelayModalOk}
|
||||
onCancel={this.hideRelayModal}
|
||||
okButtonProps={{ disabled: this.state.relaySelected === "" }}
|
||||
>
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Select style={{ width: "100%" }} onChange={this.onRelaySelected} placeholder="Select Relay">
|
||||
{relayOptions}
|
||||
</Select>
|
||||
</Space>
|
||||
</Modal>
|
||||
<Admin tenantId={this.props.application.getTenantId()} isDeviceAdmin>
|
||||
<Space direction="horizontal" style={{ float: "right" }}>
|
||||
<Button type="primary">
|
||||
<Link
|
||||
to={`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/devices/create`}
|
||||
>
|
||||
Add device
|
||||
</Link>
|
||||
</Button>
|
||||
<Dropdown
|
||||
placement="bottomRight"
|
||||
overlay={menu}
|
||||
trigger={["click"]}
|
||||
disabled={this.state.selectedRowIds.length === 0}
|
||||
return (
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Modal
|
||||
title="Add selected devices to multicast-group"
|
||||
visible={mgModalVisible}
|
||||
onOk={handleMgModalOk}
|
||||
onCancel={hideMgModal}
|
||||
okButtonProps={{ disabled: mgSelected === "" }}
|
||||
>
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Select style={{ width: "100%" }} onChange={onMgSelected} placeholder="Select Multicast-group">
|
||||
{mgOptions}
|
||||
</Select>
|
||||
</Space>
|
||||
</Modal>
|
||||
<Modal
|
||||
title="Add selected devices to relay"
|
||||
visible={relayModalVisible}
|
||||
onOk={handleRelayModalOk}
|
||||
onCancel={hideRelayModal}
|
||||
okButtonProps={{ disabled: relaySelected === "" }}
|
||||
>
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Select style={{ width: "100%" }} onChange={onRelaySelected} placeholder="Select Relay">
|
||||
{relayOptions}
|
||||
</Select>
|
||||
</Space>
|
||||
</Modal>
|
||||
<Admin tenantId={props.application.getTenantId()} isDeviceAdmin>
|
||||
<Space direction="horizontal" style={{ float: "right" }}>
|
||||
<Button type="primary">
|
||||
<Link
|
||||
to={`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/devices/create`}
|
||||
>
|
||||
<Button>Selected devices</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</Admin>
|
||||
<DataTable
|
||||
columns={this.columns()}
|
||||
getPage={this.getPage}
|
||||
onRowsSelectChange={this.onRowsSelectChange}
|
||||
rowKey="devEui"
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
Add device
|
||||
</Link>
|
||||
</Button>
|
||||
<Dropdown placement="bottomRight" overlay={menu} trigger={["click"]} disabled={selectedRowIds.length === 0}>
|
||||
<Button>Selected devices</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</Admin>
|
||||
<DataTable columns={columns} getPage={getPage} onRowsSelectChange={onRowsSelectChange} rowKey="devEui" />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default ListDevices;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user