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
@ -12,3 +12,6 @@ dependencies:
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf build
|
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",
|
"version": "4.4.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/colors": "^6.0.0",
|
"@ant-design/colors": "^7.0.0",
|
||||||
|
"@ant-design/pro-layout": "^7.16.3",
|
||||||
"@chirpstack/chirpstack-api-grpc-web": "file:../api/grpc-web",
|
"@chirpstack/chirpstack-api-grpc-web": "file:../api/grpc-web",
|
||||||
"@fortawesome/fontawesome-free": "^6.1.1",
|
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/jest": "^26.0.15",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/leaflet": "^1.7.5",
|
"@types/leaflet": "^1.9.3",
|
||||||
"@types/leaflet.awesome-markers": "^2.0.25",
|
"@types/leaflet.awesome-markers": "^2.0.25",
|
||||||
"@types/node": "^12.0.0",
|
"@types/node": "^16.18.38",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^18.2.15",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"antd": "^5.7.1",
|
||||||
"antd": "^4.20.6",
|
|
||||||
"antd-mask-input": "^2.0.7",
|
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"chart.js": "^3.7.1",
|
"chart.js": "^4.3.0",
|
||||||
"chartjs-adapter-moment": "^1.0.0",
|
"chartjs-adapter-moment": "^1.0.1",
|
||||||
"chartjs-chart-matrix": "^1.1.1",
|
"chartjs-chart-matrix": "^2.0.1",
|
||||||
"codemirror": "^5.65.3",
|
"codemirror": "5.65.3",
|
||||||
"google-protobuf": "^3.21.2",
|
"history": "^5.3.0",
|
||||||
"grpc-web": "^1.4.2",
|
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
"leaflet": "^1.7.1",
|
"leaflet": "^1.9.4",
|
||||||
"leaflet.awesome-markers": "^2.0.5",
|
"leaflet.awesome-markers": "^2.0.5",
|
||||||
"react": "^17.0.2",
|
"moment": "^2.29.4",
|
||||||
"react-chartjs-2": "^4.1.0",
|
"react": "^18.2.0",
|
||||||
|
"react-chartjs-2": "^5.2.0",
|
||||||
"react-codemirror2": "^7.2.1",
|
"react-codemirror2": "^7.2.1",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^18.2.0",
|
||||||
"react-json-tree": "^0.15.1",
|
"react-json-tree": "0.15.1",
|
||||||
"react-leaflet": "^3.2.1",
|
"react-leaflet": "^4.2.1",
|
||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.7",
|
||||||
"react-router-dom": "^5.3.1",
|
"react-router-dom": "^6.14.2",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"typescript": "^4.6.4",
|
"typescript": "^4.9.5",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject"
|
||||||
"lint": "prettier --check .",
|
|
||||||
"format": "prettier --write ."
|
|
||||||
},
|
|
||||||
"husky": {
|
|
||||||
"hooks": {
|
|
||||||
"pre-commit": "yarn format"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
"react-app",
|
"react-app",
|
||||||
"react-app/jest"
|
"react-app/jest"
|
||||||
],
|
|
||||||
"ignorePatterns": [
|
|
||||||
"**/*_pb.js"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": {
|
||||||
">0.2%",
|
"production": [
|
||||||
"not dead",
|
">0.2%",
|
||||||
"not op_mini all"
|
"not dead",
|
||||||
],
|
"not op_mini all"
|
||||||
"proxy": "http://chirpstack:8080/",
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"proxy": "http://127.0.0.1:8080/",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"husky": "^7.0.4",
|
"prettier": "^3.0.0"
|
||||||
"prettier": "^2.6.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
145
ui/src/App.tsx
145
ui/src/App.tsx
@ -1,18 +1,22 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState } from "react";
|
||||||
import { Router, Route, Switch } from "react-router-dom";
|
import { Router, Routes, Route } from "react-router-dom";
|
||||||
|
|
||||||
import { Layout } from "antd";
|
import { Layout } from "antd";
|
||||||
|
|
||||||
import { User } from "@chirpstack/chirpstack-api-grpc-web/api/user_pb";
|
import { User } from "@chirpstack/chirpstack-api-grpc-web/api/user_pb";
|
||||||
|
|
||||||
import Menu from "./components/Menu";
|
|
||||||
import Header from "./components/Header";
|
import Header from "./components/Header";
|
||||||
|
import Menu from "./components/Menu";
|
||||||
|
|
||||||
// dashboard
|
// dashboard
|
||||||
import Dashboard from "./views/dashboard/Dashboard";
|
import Dashboard from "./views/dashboard/Dashboard";
|
||||||
|
|
||||||
// users
|
// users
|
||||||
import Login from "./views/users/Login";
|
import Login from "./views/users/Login";
|
||||||
|
import ListUsers from "./views/users/ListUsers";
|
||||||
|
import CreateUser from "./views/users/CreateUser";
|
||||||
|
import EditUser from "./views/users/EditUser";
|
||||||
|
import ChangeUserPassword from "./views/users/ChangeUserPassword";
|
||||||
|
|
||||||
// tenants
|
// tenants
|
||||||
import TenantRedirect from "./views/tenants/TenantRedirect";
|
import TenantRedirect from "./views/tenants/TenantRedirect";
|
||||||
@ -20,20 +24,14 @@ import ListTenants from "./views/tenants/ListTenants";
|
|||||||
import CreateTenant from "./views/tenants/CreateTenant";
|
import CreateTenant from "./views/tenants/CreateTenant";
|
||||||
import TenantLoader from "./views/tenants/TenantLoader";
|
import TenantLoader from "./views/tenants/TenantLoader";
|
||||||
|
|
||||||
// users
|
|
||||||
import ListUsers from "./views/users/ListUsers";
|
|
||||||
import CreateUser from "./views/users/CreateUser";
|
|
||||||
import EditUser from "./views/users/EditUser";
|
|
||||||
import ChangeUserPassword from "./views/users/ChangeUserPassword";
|
|
||||||
|
|
||||||
// api keys
|
// api keys
|
||||||
import ListAdminApiKeys from "./views/api-keys/ListAdminApiKeys";
|
import ListAdminApiKeys from "./views/api-keys/ListAdminApiKeys";
|
||||||
import CreateAdminApiKey from "./views/api-keys/CreateAdminApiKey";
|
import CreateAdminApiKey from "./views/api-keys/CreateAdminApiKey";
|
||||||
|
|
||||||
// device-profile templates
|
// device-profile templates
|
||||||
import ListDeviceProfileTemplates from "./views/device-profile-templates/ListDeviceProfileTemplates";
|
import ListDeviceProfileTemplates from "./views/device-profile-templates/ListDeviceProfileTemplates";
|
||||||
import EditDeviceProfileTemplate from "./views/device-profile-templates/EditDeviceProfileTemplate";
|
|
||||||
import CreateDeviceProfileTemplate from "./views/device-profile-templates/CreateDeviceProfileTemplate";
|
import CreateDeviceProfileTemplate from "./views/device-profile-templates/CreateDeviceProfileTemplate";
|
||||||
|
import EditDeviceProfileTemplate from "./views/device-profile-templates/EditDeviceProfileTemplate";
|
||||||
|
|
||||||
// regions
|
// regions
|
||||||
import ListRegions from "./views/regions/ListRegions";
|
import ListRegions from "./views/regions/ListRegions";
|
||||||
@ -44,85 +42,72 @@ import SessionStore from "./stores/SessionStore";
|
|||||||
|
|
||||||
import history from "./history";
|
import history from "./history";
|
||||||
|
|
||||||
interface IProps {}
|
const CustomRouter = ({ history, ...props }: any) => {
|
||||||
|
const [state, setState] = useState({
|
||||||
|
action: history.action,
|
||||||
|
location: history.location,
|
||||||
|
});
|
||||||
|
|
||||||
interface IState {
|
React.useLayoutEffect(() => history.listen(setState), [history]);
|
||||||
user?: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
class App extends Component<IProps, IState> {
|
return <Router {...props} location={state.location} navigationType={state.action} navigator={history} />;
|
||||||
constructor(props: IProps) {
|
};
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
function App() {
|
||||||
user: undefined,
|
const [user, setUser] = useState<User | undefined>(SessionStore.getUser());
|
||||||
};
|
SessionStore.on("change", () => {
|
||||||
}
|
setUser(SessionStore.getUser());
|
||||||
|
});
|
||||||
|
|
||||||
componentDidMount() {
|
return (
|
||||||
SessionStore.on("change", () => {
|
<Layout style={{ minHeight: "100vh" }}>
|
||||||
this.setState({
|
<CustomRouter history={history}>
|
||||||
user: SessionStore.getUser(),
|
<Routes>
|
||||||
});
|
<Route path="/" element={<TenantRedirect />} />
|
||||||
});
|
<Route path="/login" element={<Login />} />
|
||||||
|
</Routes>
|
||||||
|
|
||||||
this.setState({
|
{user && (
|
||||||
user: SessionStore.getUser(),
|
<div>
|
||||||
});
|
<Layout.Header className="layout-header">
|
||||||
}
|
<Header user={user} />
|
||||||
|
</Layout.Header>
|
||||||
|
<Layout className="layout">
|
||||||
|
<Layout.Sider width="300" theme="light" className="layout-menu">
|
||||||
|
<Menu />
|
||||||
|
</Layout.Sider>
|
||||||
|
<Layout.Content className="layout-content" style={{ padding: "24px 24px 24px" }}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
|
<Route path="/tenants" element={<ListTenants />} />
|
||||||
|
<Route path="/tenants/create" element={<CreateTenant />} />
|
||||||
|
<Route path="/tenants/:tenantId/*" element={<TenantLoader />} />
|
||||||
|
|
||||||
render() {
|
<Route path="/users" element={<ListUsers />} />
|
||||||
return (
|
<Route path="/users/create" element={<CreateUser />} />
|
||||||
<Layout style={{ minHeight: "100vh" }}>
|
<Route path="/users/:userId" element={<EditUser />} />
|
||||||
<Router history={history}>
|
<Route path="/users/:userId/password" element={<ChangeUserPassword />} />
|
||||||
<Switch>
|
|
||||||
<Route exact path="/" component={TenantRedirect} />
|
|
||||||
<Route exact path="/login" component={Login} />
|
|
||||||
{this.state.user && (
|
|
||||||
<Route>
|
|
||||||
<Layout.Header className="layout-header">
|
|
||||||
<Header user={this.state.user} />
|
|
||||||
</Layout.Header>
|
|
||||||
<Layout className="layout">
|
|
||||||
<Layout.Sider width="300" theme="light" className="layout-menu">
|
|
||||||
<Menu />
|
|
||||||
</Layout.Sider>
|
|
||||||
<Layout.Content className="layout-content" style={{ padding: "24px 24px 24px" }}>
|
|
||||||
<Switch>
|
|
||||||
<Route exact path="/dashboard" component={Dashboard} />
|
|
||||||
|
|
||||||
<Route exact path="/tenants" component={ListTenants} />
|
<Route path="/api-keys" element={<ListAdminApiKeys />} />
|
||||||
<Route exact path="/tenants/create" component={CreateTenant} />
|
<Route path="/api-keys/create" element={<CreateAdminApiKey />} />
|
||||||
<Route path="/tenants/:tenantId([\w-]{36})" component={TenantLoader} />
|
|
||||||
|
|
||||||
<Route exact path="/users" component={ListUsers} />
|
<Route path="/device-profile-templates" element={<ListDeviceProfileTemplates />} />
|
||||||
<Route exact path="/users/create" component={CreateUser} />
|
<Route path="/device-profile-templates/create" element={<CreateDeviceProfileTemplate />} />
|
||||||
<Route exact path="/users/:userId([\w-]{36})" component={EditUser} />
|
<Route
|
||||||
<Route exact path="/users/:userId([\w-]{36})/password" component={ChangeUserPassword} />
|
path="/device-profile-templates/:deviceProfileTemplateId/edit"
|
||||||
|
element={<EditDeviceProfileTemplate />}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route exact path="/api-keys" component={ListAdminApiKeys} />
|
<Route path="/regions" element={<ListRegions />} />
|
||||||
<Route exact path="/api-keys/create" component={CreateAdminApiKey} />
|
<Route path="/regions/:id" element={<RegionDetails />} />
|
||||||
|
</Routes>
|
||||||
<Route exact path="/device-profile-templates" component={ListDeviceProfileTemplates} />
|
</Layout.Content>
|
||||||
<Route exact path="/device-profile-templates/create" component={CreateDeviceProfileTemplate} />
|
</Layout>
|
||||||
<Route
|
</div>
|
||||||
exact
|
)}
|
||||||
path="/device-profile-templates/:deviceProfileTemplateId([\w-]+)/edit"
|
</CustomRouter>
|
||||||
component={EditDeviceProfileTemplate}
|
</Layout>
|
||||||
/>
|
);
|
||||||
|
|
||||||
<Route exact path="/regions" component={ListRegions} />
|
|
||||||
<Route path="/regions/:id(.*)" component={RegionDetails} />
|
|
||||||
</Switch>
|
|
||||||
</Layout.Content>
|
|
||||||
</Layout>
|
|
||||||
</Route>
|
|
||||||
)}
|
|
||||||
</Switch>
|
|
||||||
</Router>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component } from "react";
|
import React, { PropsWithChildren, useState, useEffect } from "react";
|
||||||
|
|
||||||
import SessionStore from "../stores/SessionStore";
|
import SessionStore from "../stores/SessionStore";
|
||||||
|
|
||||||
@ -9,73 +9,45 @@ interface IProps {
|
|||||||
isTenantAdmin?: boolean;
|
isTenantAdmin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function Admin(props: PropsWithChildren<IProps>) {
|
||||||
admin: boolean;
|
const [admin, setAdmin] = useState<boolean>(false);
|
||||||
}
|
|
||||||
|
|
||||||
class Admin extends Component<IProps, IState> {
|
const setIsAdmin = () => {
|
||||||
constructor(props: IProps) {
|
if (!props.isDeviceAdmin && !props.isGatewayAdmin && !props.isTenantAdmin) {
|
||||||
super(props);
|
setAdmin(SessionStore.isAdmin());
|
||||||
|
|
||||||
this.state = {
|
|
||||||
admin: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
SessionStore.on("change", this.setIsAdmin);
|
|
||||||
this.setIsAdmin();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
SessionStore.removeListener("change", this.setIsAdmin);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: IProps) {
|
|
||||||
if (prevProps === this.props) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setIsAdmin();
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsAdmin = () => {
|
|
||||||
if (!this.props.isDeviceAdmin && !this.props.isGatewayAdmin && !this.props.isTenantAdmin) {
|
|
||||||
this.setState({
|
|
||||||
admin: SessionStore.isAdmin(),
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
if (this.props.tenantId === undefined) {
|
if (props.tenantId === undefined) {
|
||||||
throw new Error("No tenantId is given");
|
throw new Error("No tenantId is given");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.isTenantAdmin) {
|
if (props.isTenantAdmin) {
|
||||||
this.setState({
|
setAdmin(SessionStore.isAdmin() || SessionStore.isTenantAdmin(props.tenantId));
|
||||||
admin: SessionStore.isAdmin() || SessionStore.isTenantAdmin(this.props.tenantId),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.isDeviceAdmin) {
|
if (props.isDeviceAdmin) {
|
||||||
this.setState({
|
setAdmin(SessionStore.isAdmin() || SessionStore.isTenantDeviceAdmin(props.tenantId));
|
||||||
admin: SessionStore.isAdmin() || SessionStore.isTenantDeviceAdmin(this.props.tenantId),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.isGatewayAdmin) {
|
if (props.isGatewayAdmin) {
|
||||||
this.setState({
|
setAdmin(SessionStore.isAdmin() || SessionStore.isTenantGatewayAdmin(props.tenantId));
|
||||||
admin: SessionStore.isAdmin() || SessionStore.isTenantGatewayAdmin(this.props.tenantId),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
useEffect(() => {
|
||||||
if (this.state.admin) {
|
SessionStore.on("change", setIsAdmin);
|
||||||
return this.props.children;
|
setIsAdmin();
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return () => {
|
||||||
|
SessionStore.removeListener("change", setIsAdmin);
|
||||||
|
};
|
||||||
|
}, [props]);
|
||||||
|
|
||||||
|
if (admin) {
|
||||||
|
return <div>{props.children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Admin;
|
export default Admin;
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { notification, Input, Select, Button, Space, Form, Dropdown, Menu } from "antd";
|
import { notification, Input, Select, Button, Space, Form, Dropdown, Menu } from "antd";
|
||||||
import { ReloadOutlined, CopyOutlined } from "@ant-design/icons";
|
import { ReloadOutlined, CopyOutlined } from "@ant-design/icons";
|
||||||
import { Buffer } from "buffer";
|
import { Buffer } from "buffer";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
formRef: React.RefObject<any>;
|
|
||||||
label: string;
|
label: string;
|
||||||
name: string;
|
name: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
@ -14,42 +13,29 @@ interface IProps {
|
|||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function AesKeyInput(props: IProps) {
|
||||||
byteOrder: string;
|
const form = Form.useFormInstance();
|
||||||
value: string;
|
const [byteOrder, setByteOrder] = useState<string>("msb");
|
||||||
}
|
const [value, setValue] = useState<string>("");
|
||||||
|
|
||||||
class AesKeyInput extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
if (props.value) {
|
||||||
super(props);
|
setValue(props.value);
|
||||||
this.state = {
|
}
|
||||||
byteOrder: "msb",
|
}, [props]);
|
||||||
value: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
updateField = () => {
|
const updateField = (v: string) => {
|
||||||
let value = this.state.value;
|
if (byteOrder === "lsb") {
|
||||||
|
const bytes = v.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||||
if (this.state.byteOrder === "lsb") {
|
v = bytes.reverse().join("");
|
||||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g) || [];
|
|
||||||
value = bytes.reverse().join("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.formRef.current.setFieldsValue({
|
form.setFieldsValue({
|
||||||
[this.props.name]: value,
|
[props.name]: v,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (this.props.value) {
|
|
||||||
this.setState({
|
|
||||||
value: this.props.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
let v = e.target.value;
|
let v = e.target.value;
|
||||||
const match = v.match(/[A-Fa-f0-9]/g);
|
const match = v.match(/[A-Fa-f0-9]/g);
|
||||||
|
|
||||||
@ -62,50 +48,37 @@ class AesKeyInput extends Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(
|
setValue(value);
|
||||||
{
|
updateField(value);
|
||||||
value: value,
|
|
||||||
},
|
|
||||||
this.updateField,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onByteOrderSelect = (v: string) => {
|
const onByteOrderSelect = (v: string) => {
|
||||||
if (v === this.state.byteOrder) {
|
if (v === byteOrder) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
setByteOrder(v);
|
||||||
byteOrder: v,
|
|
||||||
});
|
|
||||||
|
|
||||||
const current = this.state.value;
|
const current = value;
|
||||||
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
|
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||||
|
const vv = bytes.reverse().join("");
|
||||||
|
|
||||||
this.setState(
|
setValue(vv);
|
||||||
{
|
updateField(vv);
|
||||||
value: bytes.reverse().join(""),
|
|
||||||
},
|
|
||||||
this.updateField,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
generateRandom = () => {
|
const generateRandom = () => {
|
||||||
let cryptoObj = window.crypto || window.Crypto;
|
let cryptoObj = window.crypto || window.Crypto;
|
||||||
let b = new Uint8Array(16);
|
let b = new Uint8Array(16);
|
||||||
cryptoObj.getRandomValues(b);
|
cryptoObj.getRandomValues(b);
|
||||||
|
|
||||||
let key = Buffer.from(b).toString("hex");
|
let key = Buffer.from(b).toString("hex");
|
||||||
this.setState(
|
setValue(key);
|
||||||
{
|
updateField(key);
|
||||||
value: key,
|
|
||||||
},
|
|
||||||
this.updateField,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
copyToClipboard = () => {
|
const copyToClipboard = () => {
|
||||||
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
|
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
|
||||||
|
|
||||||
if (bytes !== null && navigator.clipboard !== undefined) {
|
if (bytes !== null && navigator.clipboard !== undefined) {
|
||||||
navigator.clipboard
|
navigator.clipboard
|
||||||
@ -126,8 +99,8 @@ class AesKeyInput extends Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
copyToClipboardHexArray = () => {
|
const copyToClipboardHexArray = () => {
|
||||||
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
|
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
|
||||||
|
|
||||||
if (bytes !== null && navigator.clipboard !== undefined) {
|
if (bytes !== null && navigator.clipboard !== undefined) {
|
||||||
navigator.clipboard
|
navigator.clipboard
|
||||||
@ -153,72 +126,70 @@ class AesKeyInput extends Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const copyMenu = (
|
||||||
const copyMenu = (
|
<Menu
|
||||||
<Menu
|
items={[
|
||||||
items={[
|
{
|
||||||
{
|
key: "1",
|
||||||
key: "1",
|
label: (
|
||||||
label: (
|
<Button type="text" onClick={copyToClipboard}>
|
||||||
<Button type="text" onClick={this.copyToClipboard}>
|
HEX string
|
||||||
HEX string
|
</Button>
|
||||||
</Button>
|
),
|
||||||
),
|
},
|
||||||
},
|
{
|
||||||
{
|
key: "2",
|
||||||
key: "2",
|
label: (
|
||||||
label: (
|
<Button type="text" onClick={copyToClipboardHexArray}>
|
||||||
<Button type="text" onClick={this.copyToClipboardHexArray}>
|
HEX array
|
||||||
HEX array
|
</Button>
|
||||||
</Button>
|
),
|
||||||
),
|
},
|
||||||
},
|
]}
|
||||||
]}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const addon = (
|
const addon = (
|
||||||
<Space size="large">
|
<Space size="large">
|
||||||
<Select value={this.state.byteOrder} onChange={this.onByteOrderSelect}>
|
<Select value={byteOrder} onChange={onByteOrderSelect}>
|
||||||
<Select.Option value="msb">MSB</Select.Option>
|
<Select.Option value="msb">MSB</Select.Option>
|
||||||
<Select.Option value="lsb">LSB</Select.Option>
|
<Select.Option value="lsb">LSB</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
<Button type="text" size="small" shape="circle" onClick={this.generateRandom}>
|
<Button type="text" size="small" onClick={generateRandom}>
|
||||||
<ReloadOutlined />
|
<ReloadOutlined />
|
||||||
|
</Button>
|
||||||
|
<Dropdown overlay={copyMenu}>
|
||||||
|
<Button type="text" size="small">
|
||||||
|
<CopyOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
<Dropdown overlay={copyMenu}>
|
</Dropdown>
|
||||||
<Button type="text" size="small">
|
</Space>
|
||||||
<CopyOutlined />
|
);
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: this.props.required,
|
required: props.required,
|
||||||
message: `Please enter a valid ${this.props.label}`,
|
message: `Please enter a valid ${props.label}`,
|
||||||
pattern: new RegExp(/[A-Fa-f0-9]{32}/g),
|
pattern: new RegExp(/[A-Fa-f0-9]{32}/g),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
label={this.props.label}
|
label={props.label}
|
||||||
name={this.props.name}
|
name={props.name}
|
||||||
tooltip={this.props.tooltip}
|
tooltip={props.tooltip}
|
||||||
>
|
>
|
||||||
<Input hidden />
|
<Input hidden />
|
||||||
<Input.Password
|
<Input
|
||||||
id={`${this.props.name}Render`}
|
id={`${props.name}Render`}
|
||||||
onChange={this.onChange}
|
onChange={onChange}
|
||||||
addonAfter={!this.props.disabled && addon}
|
addonAfter={!props.disabled && addon}
|
||||||
style={{ fontFamily: "monospace" }}
|
className="input-code"
|
||||||
value={this.state.value}
|
value={value}
|
||||||
disabled={this.props.disabled}
|
disabled={props.disabled}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AesKeyInput;
|
export default AesKeyInput;
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { Select } from "antd";
|
import { Select } from "antd";
|
||||||
|
|
||||||
export type OptionsCallbackFunc = (o: { label: string; value: string }[]) => void;
|
export type OptionsCallbackFunc = (o: { label: string; value: string }[]) => void;
|
||||||
export type OptionCallbackFunc = (o: { label: string; value: string }) => void;
|
export type OptionCallbackFunc = (o: { label: string; value: string }) => void;
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
className: string;
|
className: string;
|
||||||
@ -14,93 +19,59 @@ interface IProps {
|
|||||||
onSelect?: (s: string) => void;
|
onSelect?: (s: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function AutoComplete({ placeholder, className, value, getOption, getOptions, onSelect }: IProps) {
|
||||||
option?: { label: string; value: string };
|
const [option, setOption] = useState<Option | undefined>(undefined);
|
||||||
options: { label: string; value: string }[];
|
const [options, setOptions] = useState<Option[]>([]);
|
||||||
}
|
|
||||||
|
|
||||||
class Autocomplete extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
if (value && value !== "") {
|
||||||
super(props);
|
getOption(value, (o: Option) => {
|
||||||
|
setOptions([o]);
|
||||||
this.state = {
|
|
||||||
options: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (this.props.value && this.props.value !== "") {
|
|
||||||
this.props.getOption(this.props.value, (o: { label: string; value: string }) => {
|
|
||||||
this.setState({
|
|
||||||
options: [o],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}, [value, getOption]);
|
||||||
|
|
||||||
componentDidUpdate(prevProps: IProps) {
|
const onFocus = () => {
|
||||||
if (this.props.value === prevProps.value) {
|
getOptions("", options => {
|
||||||
return;
|
if (option !== undefined) {
|
||||||
}
|
const selected = option.value;
|
||||||
|
|
||||||
if (this.props.value && this.props.value !== "") {
|
|
||||||
this.props.getOption(this.props.value, (o: { label: string; value: string }) => {
|
|
||||||
this.setState({
|
|
||||||
options: [o],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onFocus = () => {
|
|
||||||
this.props.getOptions("", options => {
|
|
||||||
if (this.state.option !== undefined) {
|
|
||||||
const selected = this.state.option.value;
|
|
||||||
|
|
||||||
if (options.find(e => e.value === selected) === undefined) {
|
if (options.find(e => e.value === selected) === undefined) {
|
||||||
options.unshift(this.state.option);
|
options.unshift(option);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
setOptions(options);
|
||||||
options: options,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearch = (value: string) => {
|
const onSearch = (value: string) => {
|
||||||
this.props.getOptions(value, options => {
|
getOptions(value, options => {
|
||||||
this.setState({
|
setOptions(options);
|
||||||
options: options,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onSelect = (value: string, option: any) => {
|
const onSelectFn = (value: string, option: any) => {
|
||||||
this.setState({
|
setOption({ label: option.label, value: option.value });
|
||||||
option: { label: option.label, value: option.value },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.props.onSelect !== undefined) {
|
if (onSelect !== undefined) {
|
||||||
this.props.onSelect(value);
|
onSelect(value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const { getOption, getOptions, ...otherProps } = this.props;
|
<Select
|
||||||
|
showSearch
|
||||||
return (
|
options={options}
|
||||||
<Select
|
onFocus={onFocus}
|
||||||
showSearch
|
onSearch={onSearch}
|
||||||
options={this.state.options}
|
onSelect={onSelectFn}
|
||||||
onFocus={this.onFocus}
|
filterOption={false}
|
||||||
onSearch={this.onSearch}
|
placeholder={placeholder}
|
||||||
onSelect={this.onSelect}
|
className={className}
|
||||||
filterOption={false}
|
value={value}
|
||||||
{...otherProps}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Autocomplete;
|
export default AutoComplete;
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
|
|
||||||
import { Form } from "antd";
|
import { Form } from "antd";
|
||||||
|
|
||||||
import Autocomplete, { OptionCallbackFunc, OptionsCallbackFunc } from "./Autocomplete";
|
import Autocomplete, { OptionCallbackFunc, OptionsCallbackFunc } from "./Autocomplete";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
formRef: React.RefObject<any>;
|
|
||||||
label: string;
|
label: string;
|
||||||
name: string;
|
name: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
@ -14,28 +11,35 @@ interface IProps {
|
|||||||
getOptions: (s: string, fn: OptionsCallbackFunc) => void;
|
getOptions: (s: string, fn: OptionsCallbackFunc) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AutocompleteInput extends Component<IProps> {
|
function AutocompleteInput(props: IProps) {
|
||||||
render() {
|
const form = Form.useFormInstance();
|
||||||
return (
|
|
||||||
<Form.Item
|
const onSelect = (value: string) => {
|
||||||
rules={[
|
form.setFieldsValue({
|
||||||
{
|
[props.name]: value,
|
||||||
required: this.props.required,
|
});
|
||||||
message: `Please select a ${this.props.label}`,
|
};
|
||||||
},
|
|
||||||
]}
|
return (
|
||||||
label={this.props.label}
|
<Form.Item
|
||||||
name={this.props.name}
|
rules={[
|
||||||
>
|
{
|
||||||
<Autocomplete
|
required: props.required,
|
||||||
placeholder={`Select a ${this.props.label}`}
|
message: `Please select a ${props.label}`,
|
||||||
className=""
|
},
|
||||||
getOption={this.props.getOption}
|
]}
|
||||||
getOptions={this.props.getOptions}
|
label={props.label}
|
||||||
/>
|
name={props.name}
|
||||||
</Form.Item>
|
>
|
||||||
);
|
<Autocomplete
|
||||||
}
|
placeholder={`Select a ${props.label}`}
|
||||||
|
className=""
|
||||||
|
getOption={props.getOption}
|
||||||
|
getOptions={props.getOptions}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AutocompleteInput;
|
export default AutocompleteInput;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Controlled as CodeMirror } from "react-codemirror2";
|
import { Controlled as CodeMirror } from "react-codemirror2";
|
||||||
|
|
||||||
import { Form } from "antd";
|
import { Form } from "antd";
|
||||||
@ -6,88 +6,49 @@ import { Form } from "antd";
|
|||||||
import "codemirror/mode/javascript/javascript";
|
import "codemirror/mode/javascript/javascript";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
formRef: React.RefObject<any>;
|
|
||||||
label?: string;
|
label?: string;
|
||||||
name: string;
|
name: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
value?: string;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function CodeEditor(props: IProps) {
|
||||||
value: string;
|
const form = Form.useFormInstance();
|
||||||
reloadKey: number;
|
const [value, setValue] = useState<string>("");
|
||||||
}
|
const [reloadKey, setReloadKey] = useState<number>(1);
|
||||||
|
|
||||||
class CodeEditor extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
setValue(form.getFieldValue(props.name));
|
||||||
super(props);
|
setReloadKey(k => k + 1);
|
||||||
this.state = {
|
}, [form, props]);
|
||||||
value: "",
|
|
||||||
reloadKey: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
const handleChange = (editor: any, data: any, newCode: string) => {
|
||||||
if (this.props.value) {
|
setValue(newCode);
|
||||||
this.setState({
|
form.setFieldsValue({
|
||||||
value: this.props.value,
|
[props.name]: newCode,
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(oldProps: IProps) {
|
|
||||||
if (this.props === oldProps) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.value) {
|
|
||||||
this.setState({
|
|
||||||
value: this.props.value,
|
|
||||||
reloadKey: this.state.reloadKey + 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateField = () => {
|
|
||||||
let value = this.state.value;
|
|
||||||
|
|
||||||
this.props.formRef.current.setFieldsValue({
|
|
||||||
[this.props.name]: value,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleChange = (editor: any, data: any, newCode: string) => {
|
const codeMirrorOptions = {
|
||||||
this.setState(
|
lineNumbers: true,
|
||||||
{
|
mode: "javascript",
|
||||||
value: newCode,
|
theme: "base16-light",
|
||||||
},
|
readOnly: props.disabled,
|
||||||
this.updateField,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const codeMirrorOptions = {
|
<Form.Item label={props.label} name={props.name} tooltip={props.tooltip}>
|
||||||
lineNumbers: true,
|
<div style={{ border: "1px solid #cccccc" }}>
|
||||||
mode: "javascript",
|
<CodeMirror
|
||||||
theme: "base16-light",
|
key={`code-editor-refresh-${reloadKey}`}
|
||||||
readOnly: this.props.disabled,
|
value={value}
|
||||||
};
|
options={codeMirrorOptions}
|
||||||
|
onBeforeChange={handleChange}
|
||||||
return (
|
/>
|
||||||
<Form.Item label={this.props.label} name={this.props.name} tooltip={this.props.tooltip}>
|
</div>
|
||||||
<div style={{ border: "1px solid #cccccc" }}>
|
</Form.Item>
|
||||||
<CodeMirror
|
);
|
||||||
key={`code-editor-refresh-${this.state.reloadKey}`}
|
|
||||||
value={this.state.value}
|
|
||||||
options={codeMirrorOptions}
|
|
||||||
onBeforeChange={this.handleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CodeEditor;
|
export default CodeEditor;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { Table } from "antd";
|
import { Table } from "antd";
|
||||||
import { ColumnsType } from "antd/es/table";
|
import { ColumnsType } from "antd/es/table";
|
||||||
@ -16,113 +16,81 @@ interface IProps {
|
|||||||
noPagination?: boolean;
|
noPagination?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function DataTable(props: IProps) {
|
||||||
totalCount: number;
|
const [totalCount, setTotalCount] = useState<number>(0);
|
||||||
pageSize: number;
|
const [pageSize, setPageSize] = useState<number>(SessionStore.getRowsPerPage());
|
||||||
currentPage: number;
|
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||||
rows: object[];
|
const [rows, setRows] = useState<object[]>([]);
|
||||||
loading: boolean;
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
}
|
|
||||||
|
|
||||||
class DataTable extends Component<IProps, IState> {
|
const onChangePage = (page: number, pz?: number | void) => {
|
||||||
constructor(props: IProps) {
|
setLoading(true);
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
if (!pz) {
|
||||||
totalCount: 0,
|
pz = pageSize;
|
||||||
pageSize: SessionStore.getRowsPerPage(),
|
|
||||||
currentPage: 1,
|
|
||||||
rows: [],
|
|
||||||
loading: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.onChangePage(this.state.currentPage, this.state.pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: IProps) {
|
|
||||||
if (this.props === prevProps) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onChangePage(this.state.currentPage, this.state.pageSize);
|
props.getPage(pz, (page - 1) * pz, (totalCount: number, rows: object[]) => {
|
||||||
}
|
setCurrentPage(page);
|
||||||
|
setTotalCount(totalCount);
|
||||||
onChangePage = (page: number, pageSize?: number | void) => {
|
setRows(rows);
|
||||||
this.setState(
|
setPageSize(pz || 0);
|
||||||
{
|
setLoading(false);
|
||||||
loading: true,
|
});
|
||||||
},
|
|
||||||
() => {
|
|
||||||
let pz = pageSize;
|
|
||||||
if (!pz) {
|
|
||||||
pz = this.state.pageSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.getPage(pz, (page - 1) * pz, (totalCount: number, rows: object[]) => {
|
|
||||||
this.setState({
|
|
||||||
currentPage: page,
|
|
||||||
totalCount: totalCount,
|
|
||||||
rows: rows,
|
|
||||||
pageSize: pz || 0,
|
|
||||||
loading: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onShowSizeChange = (page: number, pageSize: number) => {
|
const onShowSizeChange = (page: number, pageSize: number) => {
|
||||||
this.onChangePage(page, pageSize);
|
onChangePage(page, pageSize);
|
||||||
SessionStore.setRowsPerPage(pageSize);
|
SessionStore.setRowsPerPage(pageSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
onRowsSelectChange = (ids: React.Key[]) => {
|
const onRowsSelectChange = (ids: React.Key[]) => {
|
||||||
const idss = ids as string[];
|
const idss = ids as string[];
|
||||||
if (this.props.onRowsSelectChange) {
|
if (props.onRowsSelectChange) {
|
||||||
this.props.onRowsSelectChange(idss);
|
props.onRowsSelectChange(idss);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
useEffect(() => {
|
||||||
const { getPage, refreshKey, ...otherProps } = this.props;
|
onChangePage(currentPage, pageSize);
|
||||||
let loadingProps = undefined;
|
}, [props, currentPage, pageSize]);
|
||||||
if (this.state.loading) {
|
|
||||||
loadingProps = {
|
|
||||||
delay: 300,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let pagination = undefined;
|
const { getPage, refreshKey, ...otherProps } = props;
|
||||||
if (this.props.noPagination === undefined || this.props.noPagination === false) {
|
let loadingProps = undefined;
|
||||||
pagination = {
|
if (loading) {
|
||||||
current: this.state.currentPage,
|
loadingProps = {
|
||||||
total: this.state.totalCount,
|
delay: 300,
|
||||||
pageSize: this.state.pageSize,
|
};
|
||||||
onChange: this.onChangePage,
|
|
||||||
showSizeChanger: true,
|
|
||||||
onShowSizeChange: this.onShowSizeChange,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let rowSelection = undefined;
|
|
||||||
if (this.props.onRowsSelectChange) {
|
|
||||||
rowSelection = {
|
|
||||||
onChange: this.onRowsSelectChange,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table
|
|
||||||
loading={loadingProps}
|
|
||||||
dataSource={this.state.rows}
|
|
||||||
pagination={pagination || false}
|
|
||||||
rowSelection={rowSelection}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pagination = undefined;
|
||||||
|
if (props.noPagination === undefined || props.noPagination === false) {
|
||||||
|
pagination = {
|
||||||
|
current: currentPage,
|
||||||
|
total: totalCount,
|
||||||
|
pageSize: pageSize,
|
||||||
|
onChange: onChangePage,
|
||||||
|
showSizeChanger: true,
|
||||||
|
onShowSizeChange: onShowSizeChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let rowSelection = undefined;
|
||||||
|
if (props.onRowsSelectChange) {
|
||||||
|
rowSelection = {
|
||||||
|
onChange: onRowsSelectChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
loading={loadingProps}
|
||||||
|
dataSource={rows}
|
||||||
|
pagination={pagination || false}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DataTable;
|
export default DataTable;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import { useState, PropsWithChildren } from "react";
|
||||||
|
|
||||||
import { Popover, Button, Typography, Space, Input } from "antd";
|
import { Popover, Button, Typography, Space, Input } from "antd";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
@ -8,51 +7,32 @@ interface IProps {
|
|||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConfirmState {
|
function DeleteConfirmContent(props: IProps) {
|
||||||
confirm: string;
|
const [confirm, setConfirm] = useState<string>("");
|
||||||
}
|
|
||||||
|
|
||||||
class DeleteConfirmContent extends Component<IProps, ConfirmState> {
|
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
constructor(props: IProps) {
|
setConfirm(e.target.value);
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
confirm: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
this.setState({
|
|
||||||
confirm: e.target.value,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Space direction="vertical">
|
||||||
<Space direction="vertical">
|
<Typography.Text>
|
||||||
<Typography.Text>
|
Enter '{props.confirm}' to confirm you want to delete this {props.typ}:
|
||||||
Enter '{this.props.confirm}' to confirm you want to delete this {this.props.typ}:
|
</Typography.Text>
|
||||||
</Typography.Text>
|
<Input placeholder={props.confirm} onChange={onChange} />
|
||||||
<Input placeholder={this.props.confirm} onChange={this.onChange} />
|
<Button onClick={props.onConfirm} disabled={confirm !== props.confirm} style={{ float: "right" }}>
|
||||||
<Button
|
Delete
|
||||||
onClick={this.props.onConfirm}
|
</Button>
|
||||||
disabled={this.state.confirm !== this.props.confirm}
|
</Space>
|
||||||
style={{ float: "right" }}
|
);
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeleteConfirm extends Component<IProps> {
|
function DeleteConfirm(props: PropsWithChildren<IProps>) {
|
||||||
render() {
|
return (
|
||||||
return (
|
<Popover content={<DeleteConfirmContent {...props} />} trigger="click" placement="left">
|
||||||
<Popover content={<DeleteConfirmContent {...this.props} />} trigger="click" placement="left">
|
{props.children}
|
||||||
{this.props.children}
|
</Popover>
|
||||||
</Popover>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DeleteConfirm;
|
export default DeleteConfirm;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { notification, Input, Select, Button, Space, Form, Dropdown, Menu } from "antd";
|
import { notification, Input, Select, Button, Space, Form, Dropdown, Menu } from "antd";
|
||||||
import { ReloadOutlined, CopyOutlined } from "@ant-design/icons";
|
import { ReloadOutlined, CopyOutlined } from "@ant-design/icons";
|
||||||
@ -8,7 +8,6 @@ import { GetRandomDevAddrRequest, GetRandomDevAddrResponse } from "@chirpstack/c
|
|||||||
import DeviceStore from "../stores/DeviceStore";
|
import DeviceStore from "../stores/DeviceStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
formRef: React.RefObject<any>;
|
|
||||||
label: string;
|
label: string;
|
||||||
name: string;
|
name: string;
|
||||||
devEui: string;
|
devEui: string;
|
||||||
@ -17,42 +16,29 @@ interface IProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function DevAddrInput(props: IProps) {
|
||||||
byteOrder: string;
|
const form = Form.useFormInstance();
|
||||||
value: string;
|
const [byteOrder, setByteOrder] = useState<string>("msb");
|
||||||
}
|
const [value, setValue] = useState<string>("");
|
||||||
|
|
||||||
class DevAddrInput extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
if (props.value) {
|
||||||
super(props);
|
setValue(props.value);
|
||||||
this.state = {
|
}
|
||||||
byteOrder: "msb",
|
}, [props]);
|
||||||
value: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
updateField = () => {
|
const updateField = (v: string) => {
|
||||||
let value = this.state.value;
|
if (byteOrder === "lsb") {
|
||||||
|
const bytes = v.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||||
if (this.state.byteOrder === "lsb") {
|
v = bytes.reverse().join("");
|
||||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g) || [];
|
|
||||||
value = bytes.reverse().join("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.formRef.current.setFieldsValue({
|
form.setFieldsValue({
|
||||||
[this.props.name]: value,
|
[props.name]: v,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (this.props.value) {
|
|
||||||
this.setState({
|
|
||||||
value: this.props.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
let v = e.target.value;
|
let v = e.target.value;
|
||||||
const match = v.match(/[A-Fa-f0-9]/g);
|
const match = v.match(/[A-Fa-f0-9]/g);
|
||||||
|
|
||||||
@ -65,50 +51,37 @@ class DevAddrInput extends Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(
|
setValue(value);
|
||||||
{
|
updateField(value);
|
||||||
value: value,
|
|
||||||
},
|
|
||||||
this.updateField,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onByteOrderSelect = (v: string) => {
|
const onByteOrderSelect = (v: string) => {
|
||||||
if (v === this.state.byteOrder) {
|
if (v === byteOrder) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
setByteOrder(v);
|
||||||
byteOrder: v,
|
|
||||||
});
|
|
||||||
|
|
||||||
const current = this.state.value;
|
const current = value;
|
||||||
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
|
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||||
|
const vv = bytes.reverse().join("");
|
||||||
|
|
||||||
this.setState(
|
setValue(vv);
|
||||||
{
|
updateField(vv);
|
||||||
value: bytes.reverse().join(""),
|
|
||||||
},
|
|
||||||
this.updateField,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
generateRandom = () => {
|
const generateRandom = () => {
|
||||||
let req = new GetRandomDevAddrRequest();
|
let req = new GetRandomDevAddrRequest();
|
||||||
req.setDevEui(this.props.devEui);
|
req.setDevEui(props.devEui);
|
||||||
|
|
||||||
DeviceStore.getRandomDevAddr(req, (resp: GetRandomDevAddrResponse) => {
|
DeviceStore.getRandomDevAddr(req, (resp: GetRandomDevAddrResponse) => {
|
||||||
this.setState(
|
setValue(resp.getDevAddr());
|
||||||
{
|
updateField(resp.getDevAddr());
|
||||||
value: resp.getDevAddr(),
|
|
||||||
},
|
|
||||||
this.updateField,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
copyToClipboard = () => {
|
const copyToClipboard = () => {
|
||||||
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
|
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
|
||||||
|
|
||||||
if (bytes !== null && navigator.clipboard !== undefined) {
|
if (bytes !== null && navigator.clipboard !== undefined) {
|
||||||
navigator.clipboard
|
navigator.clipboard
|
||||||
@ -129,8 +102,8 @@ class DevAddrInput extends Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
copyToClipboardHexArray = () => {
|
const copyToClipboardHexArray = () => {
|
||||||
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
|
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
|
||||||
|
|
||||||
if (bytes !== null && navigator.clipboard !== undefined) {
|
if (bytes !== null && navigator.clipboard !== undefined) {
|
||||||
navigator.clipboard
|
navigator.clipboard
|
||||||
@ -156,71 +129,69 @@ class DevAddrInput extends Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const copyMenu = (
|
||||||
const copyMenu = (
|
<Menu
|
||||||
<Menu
|
items={[
|
||||||
items={[
|
{
|
||||||
{
|
key: "1",
|
||||||
key: "1",
|
label: (
|
||||||
label: (
|
<Button type="text" onClick={copyToClipboard}>
|
||||||
<Button type="text" onClick={this.copyToClipboard}>
|
HEX string
|
||||||
HEX string
|
</Button>
|
||||||
</Button>
|
),
|
||||||
),
|
},
|
||||||
},
|
{
|
||||||
{
|
key: "2",
|
||||||
key: "2",
|
label: (
|
||||||
label: (
|
<Button type="text" onClick={copyToClipboardHexArray}>
|
||||||
<Button type="text" onClick={this.copyToClipboardHexArray}>
|
HEX array
|
||||||
HEX array
|
</Button>
|
||||||
</Button>
|
),
|
||||||
),
|
},
|
||||||
},
|
]}
|
||||||
]}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const addon = (
|
const addon = (
|
||||||
<Space size="large">
|
<Space size="large">
|
||||||
<Select value={this.state.byteOrder} onChange={this.onByteOrderSelect}>
|
<Select value={byteOrder} onChange={onByteOrderSelect}>
|
||||||
<Select.Option value="msb">MSB</Select.Option>
|
<Select.Option value="msb">MSB</Select.Option>
|
||||||
<Select.Option value="lsb">LSB</Select.Option>
|
<Select.Option value="lsb">LSB</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
<Button type="text" size="small" shape="circle" onClick={this.generateRandom}>
|
<Button type="text" size="small" onClick={generateRandom}>
|
||||||
<ReloadOutlined />
|
<ReloadOutlined />
|
||||||
|
</Button>
|
||||||
|
<Dropdown overlay={copyMenu}>
|
||||||
|
<Button type="text" size="small">
|
||||||
|
<CopyOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
<Dropdown overlay={copyMenu}>
|
</Dropdown>
|
||||||
<Button type="text" size="small">
|
</Space>
|
||||||
<CopyOutlined />
|
);
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: this.props.required,
|
required: props.required,
|
||||||
message: `Please enter a valid ${this.props.label}`,
|
message: `Please enter a valid ${props.label}`,
|
||||||
pattern: new RegExp(/[A-Fa-f0-9]{8}/g),
|
pattern: new RegExp(/[A-Fa-f0-9]{8}/g),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
label={this.props.label}
|
label={props.label}
|
||||||
name={this.props.name}
|
name={props.name}
|
||||||
>
|
>
|
||||||
<Input hidden />
|
<Input hidden />
|
||||||
<Input
|
<Input
|
||||||
id={`${this.props.name}Render`}
|
id={`${props.name}Render`}
|
||||||
onChange={this.onChange}
|
onChange={onChange}
|
||||||
addonAfter={!this.props.disabled && addon}
|
addonAfter={!props.disabled && addon}
|
||||||
style={{ fontFamily: "monospace" }}
|
className="input-code"
|
||||||
value={this.state.value}
|
value={value}
|
||||||
disabled={this.props.disabled}
|
disabled={props.disabled}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DevAddrInput;
|
export default DevAddrInput;
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { notification, Input, Select, Button, Space, Form, Dropdown, Menu } from "antd";
|
import { notification, Input, Select, Button, Space, Form, Dropdown, Menu } from "antd";
|
||||||
import { ReloadOutlined, CopyOutlined } from "@ant-design/icons";
|
import { ReloadOutlined, CopyOutlined } from "@ant-design/icons";
|
||||||
import { Buffer } from "buffer";
|
import { Buffer } from "buffer";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
formRef: React.RefObject<any>;
|
|
||||||
label: string;
|
label: string;
|
||||||
name: string;
|
name: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
@ -14,42 +13,29 @@ interface IProps {
|
|||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function EuiInput(props: IProps) {
|
||||||
byteOrder: string;
|
const form = Form.useFormInstance();
|
||||||
value: string;
|
const [byteOrder, setByteOrder] = useState<string>("msb");
|
||||||
}
|
const [value, setValue] = useState<string>("");
|
||||||
|
|
||||||
class EuiInput extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
if (props.value) {
|
||||||
super(props);
|
setValue(props.value);
|
||||||
this.state = {
|
}
|
||||||
byteOrder: "msb",
|
}, [props]);
|
||||||
value: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
updateField = () => {
|
const updateField = (v: string) => {
|
||||||
let value = this.state.value;
|
if (byteOrder === "lsb") {
|
||||||
|
const bytes = v.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||||
if (this.state.byteOrder === "lsb") {
|
v = bytes.reverse().join("");
|
||||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g) || [];
|
|
||||||
value = bytes.reverse().join("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.formRef.current.setFieldsValue({
|
form.setFieldsValue({
|
||||||
[this.props.name]: value,
|
[props.name]: v,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (this.props.value) {
|
|
||||||
this.setState({
|
|
||||||
value: this.props.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
let v = e.target.value;
|
let v = e.target.value;
|
||||||
const match = v.match(/[A-Fa-f0-9]/g);
|
const match = v.match(/[A-Fa-f0-9]/g);
|
||||||
|
|
||||||
@ -62,50 +48,37 @@ class EuiInput extends Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(
|
setValue(value);
|
||||||
{
|
updateField(value);
|
||||||
value: value,
|
|
||||||
},
|
|
||||||
this.updateField,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onByteOrderSelect = (v: string) => {
|
const onByteOrderSelect = (v: string) => {
|
||||||
if (v === this.state.byteOrder) {
|
if (v === byteOrder) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
setByteOrder(v);
|
||||||
byteOrder: v,
|
|
||||||
});
|
|
||||||
|
|
||||||
const current = this.state.value;
|
const current = value;
|
||||||
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
|
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||||
|
const vv = bytes.reverse().join("");
|
||||||
|
|
||||||
this.setState(
|
setValue(vv);
|
||||||
{
|
updateField(vv);
|
||||||
value: bytes.reverse().join(""),
|
|
||||||
},
|
|
||||||
this.updateField,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
generateRandom = () => {
|
const generateRandom = () => {
|
||||||
let cryptoObj = window.crypto || window.Crypto;
|
let cryptoObj = window.crypto || window.Crypto;
|
||||||
let b = new Uint8Array(8);
|
let b = new Uint8Array(8);
|
||||||
cryptoObj.getRandomValues(b);
|
cryptoObj.getRandomValues(b);
|
||||||
|
|
||||||
let key = Buffer.from(b).toString("hex");
|
let key = Buffer.from(b).toString("hex");
|
||||||
this.setState(
|
setValue(key);
|
||||||
{
|
updateField(key);
|
||||||
value: key,
|
|
||||||
},
|
|
||||||
this.updateField,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
copyToClipboard = () => {
|
const copyToClipboard = () => {
|
||||||
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
|
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
|
||||||
|
|
||||||
if (bytes !== null && navigator.clipboard !== undefined) {
|
if (bytes !== null && navigator.clipboard !== undefined) {
|
||||||
navigator.clipboard
|
navigator.clipboard
|
||||||
@ -126,8 +99,8 @@ class EuiInput extends Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
copyToClipboardHexArray = () => {
|
const copyToClipboardHexArray = () => {
|
||||||
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
|
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
|
||||||
|
|
||||||
if (bytes !== null && navigator.clipboard !== undefined) {
|
if (bytes !== null && navigator.clipboard !== undefined) {
|
||||||
navigator.clipboard
|
navigator.clipboard
|
||||||
@ -153,72 +126,70 @@ class EuiInput extends Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const copyMenu = (
|
||||||
const copyMenu = (
|
<Menu
|
||||||
<Menu
|
items={[
|
||||||
items={[
|
{
|
||||||
{
|
key: "1",
|
||||||
key: "1",
|
label: (
|
||||||
label: (
|
<Button type="text" onClick={copyToClipboard}>
|
||||||
<Button type="text" onClick={this.copyToClipboard}>
|
HEX string
|
||||||
HEX string
|
</Button>
|
||||||
</Button>
|
),
|
||||||
),
|
},
|
||||||
},
|
{
|
||||||
{
|
key: "2",
|
||||||
key: "2",
|
label: (
|
||||||
label: (
|
<Button type="text" onClick={copyToClipboardHexArray}>
|
||||||
<Button type="text" onClick={this.copyToClipboardHexArray}>
|
HEX array
|
||||||
HEX array
|
</Button>
|
||||||
</Button>
|
),
|
||||||
),
|
},
|
||||||
},
|
]}
|
||||||
]}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const addon = (
|
const addon = (
|
||||||
<Space size="large">
|
<Space size="large">
|
||||||
<Select value={this.state.byteOrder} onChange={this.onByteOrderSelect}>
|
<Select value={byteOrder} onChange={onByteOrderSelect}>
|
||||||
<Select.Option value="msb">MSB</Select.Option>
|
<Select.Option value="msb">MSB</Select.Option>
|
||||||
<Select.Option value="lsb">LSB</Select.Option>
|
<Select.Option value="lsb">LSB</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
<Button type="text" size="small" onClick={this.generateRandom}>
|
<Button type="text" size="small" onClick={generateRandom}>
|
||||||
<ReloadOutlined />
|
<ReloadOutlined />
|
||||||
|
</Button>
|
||||||
|
<Dropdown overlay={copyMenu}>
|
||||||
|
<Button type="text" size="small">
|
||||||
|
<CopyOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
<Dropdown overlay={copyMenu}>
|
</Dropdown>
|
||||||
<Button type="text" size="small">
|
</Space>
|
||||||
<CopyOutlined />
|
);
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: this.props.required,
|
required: props.required,
|
||||||
message: `Please enter a valid ${this.props.label}`,
|
message: `Please enter a valid ${props.label}`,
|
||||||
pattern: new RegExp(/[A-Fa-f0-9]{16}/g),
|
pattern: new RegExp(/[A-Fa-f0-9]{16}/g),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
label={this.props.label}
|
label={props.label}
|
||||||
name={this.props.name}
|
name={props.name}
|
||||||
tooltip={this.props.tooltip}
|
tooltip={props.tooltip}
|
||||||
>
|
>
|
||||||
<Input hidden />
|
<Input hidden />
|
||||||
<Input
|
<Input
|
||||||
id={`${this.props.name}Render`}
|
id={`${props.name}Render`}
|
||||||
onChange={this.onChange}
|
onChange={onChange}
|
||||||
addonAfter={!this.props.disabled && addon}
|
addonAfter={!props.disabled && addon}
|
||||||
style={{ fontFamily: "monospace" }}
|
className="input-code"
|
||||||
value={this.state.value}
|
value={value}
|
||||||
disabled={this.props.disabled}
|
disabled={props.disabled}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EuiInput;
|
export default EuiInput;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Link, withRouter, RouteComponentProps } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Button, Menu, Dropdown, Input, AutoComplete } from "antd";
|
import { Button, Menu, Dropdown, Input, AutoComplete } from "antd";
|
||||||
import { UserOutlined, DownOutlined, QuestionOutlined } from "@ant-design/icons";
|
import { UserOutlined, DownOutlined, QuestionOutlined } from "@ant-design/icons";
|
||||||
@ -14,15 +14,6 @@ import {
|
|||||||
import InternalStore from "../stores/InternalStore";
|
import InternalStore from "../stores/InternalStore";
|
||||||
import SessionStore from "../stores/SessionStore";
|
import SessionStore from "../stores/SessionStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
|
||||||
user: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
searchResult?: GlobalSearchResponse;
|
|
||||||
settings?: SettingsResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderTitle = (title: string) => <span>{title}</span>;
|
const renderTitle = (title: string) => <span>{title}</span>;
|
||||||
|
|
||||||
const renderItem = (title: string, url: string) => ({
|
const renderItem = (title: string, url: string) => ({
|
||||||
@ -30,22 +21,19 @@ const renderItem = (title: string, url: string) => ({
|
|||||||
label: <Link to={url}>{title}</Link>,
|
label: <Link to={url}>{title}</Link>,
|
||||||
});
|
});
|
||||||
|
|
||||||
class Header extends Component<IProps, IState> {
|
function Header({ user }: { user: User }) {
|
||||||
constructor(props: IProps) {
|
const navigate = useNavigate();
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {};
|
const [settings, setSettings] = useState<SettingsResponse | undefined>(undefined);
|
||||||
}
|
const [searchResult, setSearchResult] = useState<GlobalSearchResponse | undefined>(undefined);
|
||||||
|
|
||||||
componentDidMount() {
|
useEffect(() => {
|
||||||
InternalStore.settings((resp: SettingsResponse) => {
|
InternalStore.settings((resp: SettingsResponse) => {
|
||||||
this.setState({
|
setSettings(resp);
|
||||||
settings: resp,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}, [user]);
|
||||||
|
|
||||||
onSearch = (search: string) => {
|
const onSearch = (search: string) => {
|
||||||
if (search.length < 3) {
|
if (search.length < 3) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -55,14 +43,11 @@ class Header extends Component<IProps, IState> {
|
|||||||
req.setSearch(search);
|
req.setSearch(search);
|
||||||
|
|
||||||
InternalStore.globalSearch(req, (resp: GlobalSearchResponse) => {
|
InternalStore.globalSearch(req, (resp: GlobalSearchResponse) => {
|
||||||
this.setState({
|
setSearchResult(resp);
|
||||||
searchResult: resp,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onLogout = () => {
|
const onLogout = () => {
|
||||||
let settings = this.state.settings;
|
|
||||||
if (settings === undefined) {
|
if (settings === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -71,117 +56,112 @@ class Header extends Component<IProps, IState> {
|
|||||||
|
|
||||||
if (!oidc.getEnabled() || oidc.getLogoutUrl() === "") {
|
if (!oidc.getEnabled() || oidc.getLogoutUrl() === "") {
|
||||||
SessionStore.logout(true, () => {
|
SessionStore.logout(true, () => {
|
||||||
this.props.history.push("/login");
|
navigate("/login");
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
SessionStore.logout(false, () => {
|
SessionStore.logout(false, () => {
|
||||||
window.location.assign(oidc.getLogoutUrl());
|
navigate(oidc.getLogoutUrl());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
if (settings === undefined) {
|
||||||
if (this.state.settings === undefined) {
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let oidcEnabled = this.state.settings!.getOpenidConnect()!.getEnabled();
|
let oidcEnabled = settings!.getOpenidConnect()!.getEnabled();
|
||||||
|
|
||||||
const menu = (
|
const menu = (
|
||||||
<Menu>
|
<Menu>
|
||||||
{!oidcEnabled && (
|
{!oidcEnabled && (
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<Link to={`/users/${this.props.user.getId()}/password`}>Change password</Link>
|
<Link to={`/users/${user.getId()}/password`}>Change password</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
)}
|
)}
|
||||||
<Menu.Item onClick={this.onLogout}>Logout</Menu.Item>
|
<Menu.Item onClick={onLogout}>Logout</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|
||||||
let options: {
|
let options: {
|
||||||
label: any;
|
label: any;
|
||||||
options: any[];
|
options: any[];
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
label: renderTitle("Tenants"),
|
label: renderTitle("Tenants"),
|
||||||
options: [],
|
options: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: renderTitle("Gateways"),
|
label: renderTitle("Gateways"),
|
||||||
options: [],
|
options: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: renderTitle("Applications"),
|
label: renderTitle("Applications"),
|
||||||
options: [],
|
options: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: renderTitle("Devices"),
|
label: renderTitle("Devices"),
|
||||||
options: [],
|
options: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (this.state.searchResult !== undefined) {
|
if (searchResult !== undefined) {
|
||||||
for (const res of this.state.searchResult.getResultList()) {
|
for (const res of searchResult.getResultList()) {
|
||||||
if (res.getKind() === "tenant") {
|
if (res.getKind() === "tenant") {
|
||||||
options[0].options.push(renderItem(res.getTenantName(), `/tenants/${res.getTenantId()}`));
|
options[0].options.push(renderItem(res.getTenantName(), `/tenants/${res.getTenantId()}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.getKind() === "gateway") {
|
if (res.getKind() === "gateway") {
|
||||||
options[1].options.push(
|
options[1].options.push(
|
||||||
renderItem(res.getGatewayName(), `/tenants/${res.getTenantId()}/gateways/${res.getGatewayId()}`),
|
renderItem(res.getGatewayName(), `/tenants/${res.getTenantId()}/gateways/${res.getGatewayId()}`),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.getKind() === "application") {
|
if (res.getKind() === "application") {
|
||||||
options[2].options.push(
|
options[2].options.push(
|
||||||
renderItem(
|
renderItem(res.getApplicationName(), `/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}`),
|
||||||
res.getApplicationName(),
|
);
|
||||||
`/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}`,
|
}
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.getKind() === "device") {
|
if (res.getKind() === "device") {
|
||||||
options[3].options.push(
|
options[3].options.push(
|
||||||
renderItem(
|
renderItem(
|
||||||
res.getDeviceName(),
|
res.getDeviceName(),
|
||||||
`/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}/devices/${res.getDeviceDevEui()}`,
|
`/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}/devices/${res.getDeviceDevEui()}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<img className="logo" alt="ChirpStack" src="/logo.png" />
|
<img className="logo" alt="ChirpStack" src="/logo.png" />
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<div className="search">
|
<div className="search">
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
dropdownClassName="search-dropdown"
|
dropdownClassName="search-dropdown"
|
||||||
dropdownMatchSelectWidth={500}
|
dropdownMatchSelectWidth={500}
|
||||||
options={options}
|
options={options}
|
||||||
onSearch={this.onSearch}
|
onSearch={onSearch}
|
||||||
>
|
>
|
||||||
<Input.Search placeholder="Search..." style={{ width: 500, marginTop: -5 }} />
|
<Input.Search placeholder="Search..." style={{ width: 500, marginTop: -5 }} />
|
||||||
</AutoComplete>
|
</AutoComplete>
|
||||||
</div>
|
</div>
|
||||||
<div className="help">
|
<div className="help">
|
||||||
<a href="https://www.chirpstack.io" target="_blank" rel="noreferrer">
|
<a href="https://www.chirpstack.io" target="_blank" rel="noreferrer">
|
||||||
<Button icon={<QuestionOutlined />} />
|
<Button icon={<QuestionOutlined />} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="user">
|
<div className="user">
|
||||||
<Dropdown overlay={menu} placement="bottomRight" trigger={["click"]}>
|
<Dropdown overlay={menu} placement="bottomRight" trigger={["click"]}>
|
||||||
<Button type="primary" icon={<UserOutlined />}>
|
<Button type="primary" icon={<UserOutlined />}>
|
||||||
{this.props.user.getEmail()} <DownOutlined />
|
{user.getEmail()} <DownOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(Header);
|
export default Header;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import JSONTreeOriginal from "react-json-tree";
|
import JSONTreeOriginal from "react-json-tree";
|
||||||
import fileDownload from "js-file-download";
|
import fileDownload from "js-file-download";
|
||||||
@ -12,160 +13,142 @@ interface IProps {
|
|||||||
logs: LogItem[];
|
logs: LogItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function LogTable(props: IProps) {
|
||||||
drawerOpen: boolean;
|
const [drawerOpen, setDrawerOpen] = useState<boolean>(false);
|
||||||
body: any;
|
const [body, setBody] = useState<any>(null);
|
||||||
drawerTitle: any;
|
const [drawerTitle, setDrawerTitle] = useState<any>(null);
|
||||||
}
|
|
||||||
|
|
||||||
class LogTable extends Component<IProps, IState> {
|
const onDrawerClose = () => {
|
||||||
constructor(props: IProps) {
|
setDrawerOpen(false);
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
drawerOpen: false,
|
|
||||||
body: null,
|
|
||||||
drawerTitle: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onDrawerClose = () => {
|
|
||||||
this.setState({
|
|
||||||
drawerOpen: false,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onDrawerOpen = (time: any, body: any) => {
|
const onDrawerOpen = (time: any, body: any) => {
|
||||||
let ts = new Date(0);
|
let ts = new Date(0);
|
||||||
ts.setUTCSeconds(time.seconds);
|
ts.setUTCSeconds(time.seconds);
|
||||||
let drawerTitle = moment(ts).format("YYYY-MM-DD HH:mm:ss");
|
let drawerTitle = moment(ts).format("YYYY-MM-DD HH:mm:ss");
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
this.setState({
|
setBody(body);
|
||||||
body: body,
|
setDrawerTitle(drawerTitle);
|
||||||
drawerTitle: drawerTitle,
|
setDrawerOpen(true);
|
||||||
drawerOpen: true,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
downloadSingleFrame = () => {
|
const downloadSingleFrame = () => {
|
||||||
fileDownload(JSON.stringify(JSON.parse(this.state.body), null, 4), "single-log.json", "application/json");
|
fileDownload(JSON.stringify(JSON.parse(body), null, 4), "single-log.json", "application/json");
|
||||||
};
|
};
|
||||||
|
|
||||||
downloadFrames = () => {
|
const downloadFrames = () => {
|
||||||
let items = this.props.logs.map((l, i) => JSON.parse(l.getBody()));
|
let items = props.logs.map((l, i) => JSON.parse(l.getBody()));
|
||||||
fileDownload(JSON.stringify(items, null, 4), "log.json");
|
fileDownload(JSON.stringify(items, null, 4), "log.json");
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
let items = props.logs.map((l, i) => l.toObject());
|
||||||
let items = this.props.logs.map((l, i) => l.toObject());
|
let bodyJson = JSON.parse(body);
|
||||||
let body = JSON.parse(this.state.body);
|
|
||||||
|
|
||||||
const theme = {
|
const theme = {
|
||||||
scheme: "google",
|
scheme: "google",
|
||||||
author: "seth wright (http://sethawright.com)",
|
author: "seth wright (http://sethawright.com)",
|
||||||
base00: "#000000",
|
base00: "#000000",
|
||||||
base01: "#282a2e",
|
base01: "#282a2e",
|
||||||
base02: "#373b41",
|
base02: "#373b41",
|
||||||
base03: "#969896",
|
base03: "#969896",
|
||||||
base04: "#b4b7b4",
|
base04: "#b4b7b4",
|
||||||
base05: "#c5c8c6",
|
base05: "#c5c8c6",
|
||||||
base06: "#e0e0e0",
|
base06: "#e0e0e0",
|
||||||
base07: "#ffffff",
|
base07: "#ffffff",
|
||||||
base08: "#CC342B",
|
base08: "#CC342B",
|
||||||
base09: "#F96A38",
|
base09: "#F96A38",
|
||||||
base0A: "#FBA922",
|
base0A: "#FBA922",
|
||||||
base0B: "#198844",
|
base0B: "#198844",
|
||||||
base0C: "#3971ED",
|
base0C: "#3971ED",
|
||||||
base0D: "#3971ED",
|
base0D: "#3971ED",
|
||||||
base0E: "#A36AC7",
|
base0E: "#A36AC7",
|
||||||
base0F: "#3971ED",
|
base0F: "#3971ED",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||||
<Drawer
|
<Drawer
|
||||||
title={`Details: ${this.state.drawerTitle}`}
|
title={`Details: ${drawerTitle}`}
|
||||||
placement="right"
|
placement="right"
|
||||||
width={650}
|
width={650}
|
||||||
onClose={this.onDrawerClose}
|
onClose={onDrawerClose}
|
||||||
visible={this.state.drawerOpen}
|
visible={drawerOpen}
|
||||||
extra={<Button onClick={this.downloadSingleFrame}>Download</Button>}
|
extra={<Button onClick={downloadSingleFrame}>Download</Button>}
|
||||||
>
|
>
|
||||||
<JSONTreeOriginal
|
<JSONTreeOriginal
|
||||||
data={body}
|
data={bodyJson}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
hideRoot={true}
|
hideRoot={true}
|
||||||
shouldExpandNode={() => {
|
shouldExpandNode={() => {
|
||||||
return true;
|
return true;
|
||||||
}}
|
}}
|
||||||
/>
|
|
||||||
</Drawer>
|
|
||||||
{items.length !== 0 && (
|
|
||||||
<Space direction="horizontal" style={{ float: "right" }} size="large">
|
|
||||||
<Spin size="small" />
|
|
||||||
<Button onClick={this.downloadFrames}>Download</Button>
|
|
||||||
</Space>
|
|
||||||
)}
|
|
||||||
<Table
|
|
||||||
showHeader={false}
|
|
||||||
loading={items.length === 0}
|
|
||||||
dataSource={items}
|
|
||||||
pagination={false}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
title: "Time",
|
|
||||||
dataIndex: "time",
|
|
||||||
key: "time",
|
|
||||||
width: 200,
|
|
||||||
render: (text, obj) => {
|
|
||||||
let ts = new Date(0);
|
|
||||||
ts.setUTCSeconds(obj.time!.seconds);
|
|
||||||
return moment(ts).format("YYYY-MM-DD HH:mm:ss");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Type",
|
|
||||||
dataIndex: "description",
|
|
||||||
key: "description",
|
|
||||||
width: 200,
|
|
||||||
render: (text, obj) => (
|
|
||||||
<Button
|
|
||||||
icon={<ZoomInOutlined />}
|
|
||||||
type="primary"
|
|
||||||
shape="round"
|
|
||||||
size="small"
|
|
||||||
onClick={this.onDrawerOpen(obj.time, obj.body)}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Properties",
|
|
||||||
dataIndex: "properties",
|
|
||||||
key: "properties",
|
|
||||||
render: (text, obj) =>
|
|
||||||
obj.propertiesMap.map((p, i) => {
|
|
||||||
if (p[1] !== "") {
|
|
||||||
return (
|
|
||||||
<Tag>
|
|
||||||
<pre>
|
|
||||||
{p[0]}: {p[1]}
|
|
||||||
</pre>
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Drawer>
|
||||||
);
|
{items.length !== 0 && (
|
||||||
}
|
<Space direction="horizontal" style={{ float: "right" }} size="large">
|
||||||
|
<Spin size="small" />
|
||||||
|
<Button onClick={downloadFrames}>Download</Button>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
<Table
|
||||||
|
showHeader={false}
|
||||||
|
loading={items.length === 0}
|
||||||
|
dataSource={items}
|
||||||
|
pagination={false}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: "Time",
|
||||||
|
dataIndex: "time",
|
||||||
|
key: "time",
|
||||||
|
width: 200,
|
||||||
|
render: (text, obj) => {
|
||||||
|
let ts = new Date(0);
|
||||||
|
ts.setUTCSeconds(obj.time!.seconds);
|
||||||
|
return moment(ts).format("YYYY-MM-DD HH:mm:ss");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Type",
|
||||||
|
dataIndex: "description",
|
||||||
|
key: "description",
|
||||||
|
width: 200,
|
||||||
|
render: (text, obj) => (
|
||||||
|
<Button
|
||||||
|
icon={<ZoomInOutlined />}
|
||||||
|
type="primary"
|
||||||
|
shape="round"
|
||||||
|
size="small"
|
||||||
|
onClick={onDrawerOpen(obj.time, obj.body)}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Properties",
|
||||||
|
dataIndex: "properties",
|
||||||
|
key: "properties",
|
||||||
|
render: (text, obj) =>
|
||||||
|
obj.propertiesMap.map((p, i) => {
|
||||||
|
if (p[1] !== "") {
|
||||||
|
return (
|
||||||
|
<Tag>
|
||||||
|
<pre>
|
||||||
|
{p[0]}: {p[1]}
|
||||||
|
</pre>
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LogTable;
|
export default LogTable;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useEffect, PropsWithChildren } from "react";
|
||||||
|
|
||||||
import L, { LatLngTuple, FitBoundsOptions } from "leaflet";
|
import L, { LatLngTuple, FitBoundsOptions } from "leaflet";
|
||||||
import "leaflet.awesome-markers";
|
import "leaflet.awesome-markers";
|
||||||
import { MarkerProps as LMarkerProps } from "react-leaflet";
|
import { MarkerProps as LMarkerProps, useMap } from "react-leaflet";
|
||||||
import { MapContainer, Marker as LMarker, TileLayer } from "react-leaflet";
|
import { MapContainer, Marker as LMarker, TileLayer } from "react-leaflet";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
@ -12,77 +12,48 @@ interface IProps {
|
|||||||
boundsOptions?: FitBoundsOptions;
|
boundsOptions?: FitBoundsOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function MapControl(props: { center?: [number, number]; bounds?: LatLngTuple[]; boundsOptions?: FitBoundsOptions }) {
|
||||||
map?: L.Map;
|
const map = useMap();
|
||||||
}
|
|
||||||
|
|
||||||
class Map extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
if (map === undefined) {
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
setMap = (map: L.Map) => {
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
map: map,
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
// This is needed as setMap is called after the map has been created.
|
|
||||||
// There is a small amount of time where componentDidUpdate can't update
|
|
||||||
// the map with the new center because setMap hasn't been called yet.
|
|
||||||
// In such case, the map would never update to the new center.
|
|
||||||
if (this.props.center !== undefined) {
|
|
||||||
map.panTo(this.props.center);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.bounds !== undefined) {
|
|
||||||
map.fitBounds(this.props.bounds, this.props.boundsOptions);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidUpdate(oldProps: IProps) {
|
|
||||||
if (this.props === oldProps) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.map) {
|
if (props.center !== undefined) {
|
||||||
if (this.props.center !== undefined) {
|
map.flyTo(props.center);
|
||||||
this.state.map.flyTo(this.props.center);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.bounds !== undefined) {
|
|
||||||
this.state.map.flyToBounds(this.props.bounds, this.props.boundsOptions);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
if (props.bounds !== undefined) {
|
||||||
const style = {
|
map.flyToBounds(props.bounds, props.boundsOptions);
|
||||||
height: this.props.height,
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
return (
|
return null;
|
||||||
<MapContainer
|
}
|
||||||
bounds={this.props.bounds}
|
|
||||||
boundsOptions={this.props.boundsOptions}
|
function Map(props: PropsWithChildren<IProps>) {
|
||||||
center={this.props.center}
|
const style = {
|
||||||
zoom={13}
|
height: props.height,
|
||||||
scrollWheelZoom={false}
|
};
|
||||||
animate={true}
|
|
||||||
style={style}
|
return (
|
||||||
whenCreated={this.setMap}
|
<MapContainer
|
||||||
>
|
bounds={props.bounds}
|
||||||
<TileLayer
|
boundsOptions={props.boundsOptions}
|
||||||
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
center={props.center}
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
zoom={13}
|
||||||
/>
|
scrollWheelZoom={false}
|
||||||
{this.props.children}
|
style={style}
|
||||||
</MapContainer>
|
>
|
||||||
);
|
<TileLayer
|
||||||
}
|
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
/>
|
||||||
|
{props.children}
|
||||||
|
<MapControl bounds={props.bounds} boundsOptions={props.boundsOptions} center={props.center} />
|
||||||
|
</MapContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MarkerColor =
|
export type MarkerColor =
|
||||||
@ -103,22 +74,20 @@ interface MarkerProps extends LMarkerProps {
|
|||||||
color: MarkerColor;
|
color: MarkerColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Marker extends Component<MarkerProps> {
|
export function Marker(props: MarkerProps) {
|
||||||
render() {
|
const { faIcon, color, position, ...otherProps } = props;
|
||||||
const { faIcon, color, position, ...otherProps } = this.props;
|
|
||||||
|
|
||||||
const iconMarker = L.AwesomeMarkers.icon({
|
const iconMarker = L.AwesomeMarkers.icon({
|
||||||
icon: faIcon,
|
icon: faIcon,
|
||||||
prefix: "fa",
|
prefix: "fa",
|
||||||
markerColor: color,
|
markerColor: color,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LMarker icon={iconMarker} position={position} {...otherProps}>
|
<LMarker icon={iconMarker} position={position} {...otherProps}>
|
||||||
{this.props.children}
|
{props.children}
|
||||||
</LMarker>
|
</LMarker>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Map;
|
export default Map;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { withRouter, RouteComponentProps, Link } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Menu, MenuProps } from "antd";
|
import { Menu, MenuProps } from "antd";
|
||||||
import {
|
import {
|
||||||
@ -24,46 +24,18 @@ import Autocomplete, { OptionCallbackFunc, OptionsCallbackFunc } from "../compon
|
|||||||
import TenantStore from "../stores/TenantStore";
|
import TenantStore from "../stores/TenantStore";
|
||||||
import SessionStore from "../stores/SessionStore";
|
import SessionStore from "../stores/SessionStore";
|
||||||
|
|
||||||
interface IState {
|
function SideMenu() {
|
||||||
tenantId: string;
|
const [tenantId, setTenantId] = useState<string>("");
|
||||||
selectedKey: string;
|
const [selectedKey, setSelectedKey] = useState<string>("");
|
||||||
}
|
|
||||||
|
|
||||||
class SideMenu extends Component<RouteComponentProps, IState> {
|
const location = useLocation();
|
||||||
constructor(props: RouteComponentProps) {
|
const navigate = useNavigate();
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
const setTenant = () => {
|
||||||
tenantId: "",
|
setTenantId(SessionStore.getTenantId());
|
||||||
selectedKey: "ns-dashboard",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
SessionStore.on("tenant.change", this.setTenant);
|
|
||||||
this.setTenant();
|
|
||||||
this.parseLocation();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
SessionStore.removeListener("tenant.change", this.setTenant);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: RouteComponentProps) {
|
|
||||||
if (this.props === prevProps) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.parseLocation();
|
|
||||||
}
|
|
||||||
|
|
||||||
setTenant = () => {
|
|
||||||
this.setState({
|
|
||||||
tenantId: SessionStore.getTenantId(),
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
getTenantOptions = (search: string, fn: OptionsCallbackFunc) => {
|
const getTenantOptions = (search: string, fn: OptionsCallbackFunc) => {
|
||||||
let req = new ListTenantsRequest();
|
let req = new ListTenantsRequest();
|
||||||
req.setSearch(search);
|
req.setSearch(search);
|
||||||
req.setLimit(10);
|
req.setLimit(10);
|
||||||
@ -76,7 +48,7 @@ class SideMenu extends Component<RouteComponentProps, IState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
getTenantOption = (id: string, fn: OptionCallbackFunc) => {
|
const getTenantOption = (id: string, fn: OptionCallbackFunc) => {
|
||||||
TenantStore.get(id, (resp: GetTenantResponse) => {
|
TenantStore.get(id, (resp: GetTenantResponse) => {
|
||||||
const tenant = resp.getTenant();
|
const tenant = resp.getTenant();
|
||||||
if (tenant) {
|
if (tenant) {
|
||||||
@ -85,167 +57,208 @@ class SideMenu extends Component<RouteComponentProps, IState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onTenantSelect = (value: string) => {
|
const onTenantSelect = (value: string) => {
|
||||||
SessionStore.setTenantId(value);
|
SessionStore.setTenantId(value);
|
||||||
this.props.history.push(`/tenants/${value}`);
|
navigate(`/tenants/${value}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
parseLocation = () => {
|
const parseLocation = () => {
|
||||||
const path = this.props.history.location.pathname;
|
const path = location.pathname;
|
||||||
const tenantRe = /\/tenants\/([\w-]{36})/g;
|
const tenantRe = /\/tenants\/([\w-]{36})/g;
|
||||||
const match = tenantRe.exec(path);
|
const match = tenantRe.exec(path);
|
||||||
|
|
||||||
if (match !== null && this.state.tenantId !== match[1]) {
|
if (match !== null && tenantId !== match[1]) {
|
||||||
SessionStore.setTenantId(match[1]);
|
SessionStore.setTenantId(match[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ns dashboard
|
// ns dashboard
|
||||||
if (path === "/dashboard") {
|
if (path === "/dashboard") {
|
||||||
this.setState({ selectedKey: "ns-dashboard" });
|
setSelectedKey("ns-dashboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ns tenants
|
// ns tenants
|
||||||
if (/\/tenants(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
|
if (/\/tenants(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
|
||||||
this.setState({ selectedKey: "ns-tenants" });
|
setSelectedKey("ns-tenants");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ns tenants
|
// ns tenants
|
||||||
if (/\/users(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
|
if (/\/users(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
|
||||||
this.setState({ selectedKey: "ns-users" });
|
setSelectedKey("ns-users");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ns api keys
|
// ns api keys
|
||||||
if (/\/api-keys(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
|
if (/\/api-keys(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
|
||||||
this.setState({ selectedKey: "ns-api-keys" });
|
setSelectedKey("ns-api-keys");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ns device-profile templates
|
// ns device-profile templates
|
||||||
if (/\/device-profile-templates(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
|
if (/\/device-profile-templates(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
|
||||||
this.setState({ selectedKey: "ns-device-profile-templates" });
|
setSelectedKey("ns-device-profile-templates");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\/regions.*/g.exec(path)) {
|
if (/\/regions.*/g.exec(path)) {
|
||||||
this.setState({ selectedKey: "ns-regions" });
|
setSelectedKey("ns-regions");
|
||||||
}
|
}
|
||||||
|
|
||||||
// tenant dashboard
|
// tenant dashboard
|
||||||
if (/\/tenants\/[\w-]{36}/g.exec(path)) {
|
if (/\/tenants\/[\w-]{36}/g.exec(path)) {
|
||||||
this.setState({ selectedKey: "tenant-dashboard" });
|
setSelectedKey("tenant-dashboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
// tenant users
|
// tenant users
|
||||||
if (/\/tenants\/[\w-]{36}\/users.*/g.exec(path)) {
|
if (/\/tenants\/[\w-]{36}\/users.*/g.exec(path)) {
|
||||||
this.setState({ selectedKey: "tenant-users" });
|
setSelectedKey("tenant-users");
|
||||||
}
|
}
|
||||||
|
|
||||||
// tenant api-keys
|
// tenant api-keys
|
||||||
if (/\/tenants\/[\w-]{36}\/api-keys.*/g.exec(path)) {
|
if (/\/tenants\/[\w-]{36}\/api-keys.*/g.exec(path)) {
|
||||||
this.setState({ selectedKey: "tenant-api-keys" });
|
setSelectedKey("tenant-api-keys");
|
||||||
}
|
}
|
||||||
|
|
||||||
// tenant device-profiles
|
// tenant device-profiles
|
||||||
if (/\/tenants\/[\w-]{36}\/device-profiles.*/g.exec(path)) {
|
if (/\/tenants\/[\w-]{36}\/device-profiles.*/g.exec(path)) {
|
||||||
this.setState({ selectedKey: "tenant-device-profiles" });
|
setSelectedKey("tenant-device-profiles");
|
||||||
}
|
}
|
||||||
|
|
||||||
// tenant gateways
|
// tenant gateways
|
||||||
if (/\/tenants\/[\w-]{36}\/gateways.*/g.exec(path)) {
|
if (/\/tenants\/[\w-]{36}\/gateways.*/g.exec(path)) {
|
||||||
this.setState({ selectedKey: "tenant-gateways" });
|
setSelectedKey("tenant-gateways");
|
||||||
}
|
}
|
||||||
|
|
||||||
// tenant applications
|
// tenant applications
|
||||||
if (/\/tenants\/[\w-]{36}\/applications.*/g.exec(path)) {
|
if (/\/tenants\/[\w-]{36}\/applications.*/g.exec(path)) {
|
||||||
this.setState({ selectedKey: "tenant-applications" });
|
setSelectedKey("tenant-applications");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
useEffect(() => {
|
||||||
const tenantId = this.state.tenantId;
|
SessionStore.on("tenant.change", setTenant);
|
||||||
let items: MenuProps["items"] = [];
|
setTenant();
|
||||||
|
parseLocation();
|
||||||
|
|
||||||
if (SessionStore.isAdmin()) {
|
return () => {
|
||||||
items.push({
|
SessionStore.removeListener("tenant.change", setTenant);
|
||||||
key: "ns",
|
};
|
||||||
label: "Network Server",
|
}, []);
|
||||||
icon: <CloudOutlined />,
|
|
||||||
children: [
|
|
||||||
{ key: "ns-dashboard", icon: <DashboardOutlined />, label: <Link to="/dashboard">Dashboard</Link> },
|
|
||||||
{ key: "ns-tenants", icon: <HomeOutlined />, label: <Link to="/tenants">Tenants</Link> },
|
|
||||||
{ key: "ns-users", icon: <UserOutlined />, label: <Link to="/users">Users</Link> },
|
|
||||||
{ key: "ns-api-keys", icon: <KeyOutlined />, label: <Link to="/api-keys">API Keys</Link> },
|
|
||||||
{
|
|
||||||
key: "ns-device-profile-templates",
|
|
||||||
icon: <ControlOutlined />,
|
|
||||||
label: <Link to="/device-profile-templates">Device Profile Templates</Link>,
|
|
||||||
},
|
|
||||||
{ key: "ns-regions", icon: <CompassOutlined />, label: <Link to="/regions">Regions</Link> },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
items.push({
|
|
||||||
key: "ns",
|
|
||||||
label: "Network Server",
|
|
||||||
icon: <CloudOutlined />,
|
|
||||||
children: [{ key: "ns-regions", icon: <CompassOutlined />, label: <Link to="/regions">Regions</Link> }],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tenantId !== "") {
|
useEffect(() => {
|
||||||
items.push({
|
parseLocation();
|
||||||
key: "tenant",
|
}, [location]);
|
||||||
label: "Tenant",
|
|
||||||
icon: <HomeOutlined />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
key: "tenant-dashboard",
|
|
||||||
icon: <DashboardOutlined />,
|
|
||||||
label: <Link to={`/tenants/${tenantId}`}>Dashboard</Link>,
|
|
||||||
},
|
|
||||||
{ key: "tenant-users", icon: <UserOutlined />, label: <Link to={`/tenants/${tenantId}/users`}>Users</Link> },
|
|
||||||
{
|
|
||||||
key: "tenant-api-keys",
|
|
||||||
icon: <KeyOutlined />,
|
|
||||||
label: <Link to={`/tenants/${tenantId}/api-keys`}>API Keys</Link>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "tenant-device-profiles",
|
|
||||||
icon: <ControlOutlined />,
|
|
||||||
label: <Link to={`/tenants/${tenantId}/device-profiles`}>Device Profiles</Link>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "tenant-gateways",
|
|
||||||
icon: <WifiOutlined />,
|
|
||||||
label: <Link to={`/tenants/${tenantId}/gateways`}>Gateways</Link>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "tenant-applications",
|
|
||||||
icon: <AppstoreOutlined />,
|
|
||||||
label: <Link to={`/tenants/${tenantId}/applications`}>Applications</Link>,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
let items: MenuProps["items"] = [];
|
||||||
<div>
|
|
||||||
<Autocomplete
|
if (SessionStore.isAdmin()) {
|
||||||
placeholder="Select tenant"
|
items.push({
|
||||||
className="organiation-select"
|
key: "ns",
|
||||||
getOption={this.getTenantOption}
|
label: "Network Server",
|
||||||
getOptions={this.getTenantOptions}
|
icon: <CloudOutlined />,
|
||||||
onSelect={this.onTenantSelect}
|
children: [
|
||||||
value={this.state.tenantId}
|
{
|
||||||
/>
|
key: "ns-dashboard",
|
||||||
<Menu
|
icon: <DashboardOutlined />,
|
||||||
mode="inline"
|
label: <Link to="/dashboard">Dashboard</Link>,
|
||||||
openKeys={["ns", "tenant"]}
|
},
|
||||||
selectedKeys={[this.state.selectedKey]}
|
{
|
||||||
expandIcon={<div></div>}
|
key: "ns-tenants",
|
||||||
items={items}
|
icon: <HomeOutlined />,
|
||||||
/>
|
label: <Link to="/tenants">Tenants</Link>,
|
||||||
</div>
|
},
|
||||||
);
|
{
|
||||||
|
key: "ns-users",
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
label: <Link to="/users">Users</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ns-api-keys",
|
||||||
|
icon: <KeyOutlined />,
|
||||||
|
label: <Link to="/api-keys">API Keys</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ns-device-profile-templates",
|
||||||
|
icon: <ControlOutlined />,
|
||||||
|
label: <Link to="/device-profile-templates">Device Profile Templates</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ns-regions",
|
||||||
|
icon: <CompassOutlined />,
|
||||||
|
label: <Link to="/regions">Regions</Link>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
items.push({
|
||||||
|
key: "ns",
|
||||||
|
label: "Network Server",
|
||||||
|
icon: <CloudOutlined />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "ns-regions",
|
||||||
|
icon: <CompassOutlined />,
|
||||||
|
label: <Link to="/regions">Regions</Link>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tenantId !== "") {
|
||||||
|
items.push({
|
||||||
|
key: "tenant",
|
||||||
|
label: "Tenant",
|
||||||
|
icon: <HomeOutlined />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "tenant-dashboard",
|
||||||
|
icon: <DashboardOutlined />,
|
||||||
|
label: <Link to={`/tenants/${tenantId}`}>Dashboard</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "tenant-users",
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
label: <Link to={`/tenants/${tenantId}/users`}>Users</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "tenant-api-keys",
|
||||||
|
icon: <KeyOutlined />,
|
||||||
|
label: <Link to={`/tenants/${tenantId}/api-keys`}>API Keys</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "tenant-device-profiles",
|
||||||
|
icon: <ControlOutlined />,
|
||||||
|
label: <Link to={`/tenants/${tenantId}/device-profiles`}>Device Profiles</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "tenant-gateways",
|
||||||
|
icon: <WifiOutlined />,
|
||||||
|
label: <Link to={`/tenants/${tenantId}/gateways`}>Gateways</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "tenant-applications",
|
||||||
|
icon: <AppstoreOutlined />,
|
||||||
|
label: <Link to={`/tenants/${tenantId}/applications`}>Applications</Link>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Autocomplete
|
||||||
|
placeholder="Select tenant"
|
||||||
|
className="organiation-select"
|
||||||
|
getOption={getTenantOption}
|
||||||
|
getOptions={getTenantOptions}
|
||||||
|
onSelect={onTenantSelect}
|
||||||
|
value={tenantId}
|
||||||
|
/>
|
||||||
|
<Menu
|
||||||
|
mode="inline"
|
||||||
|
openKeys={["ns", "tenant"]}
|
||||||
|
selectedKeys={[selectedKey]}
|
||||||
|
expandIcon={<div></div>}
|
||||||
|
items={items}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(SideMenu);
|
export default SideMenu;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
import { TimeUnit } from "chart.js";
|
import { TimeUnit } from "chart.js";
|
||||||
@ -13,82 +11,80 @@ interface IProps {
|
|||||||
aggregation: Aggregation;
|
aggregation: Aggregation;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MetricBar extends Component<IProps> {
|
function MetricBar(props: IProps) {
|
||||||
render() {
|
let unit: TimeUnit = "hour";
|
||||||
let unit: TimeUnit = "hour";
|
if (props.aggregation === Aggregation.DAY) {
|
||||||
if (this.props.aggregation === Aggregation.DAY) {
|
unit = "day";
|
||||||
unit = "day";
|
} else if (props.aggregation === Aggregation.MONTH) {
|
||||||
} else if (this.props.aggregation === Aggregation.MONTH) {
|
unit = "month";
|
||||||
unit = "month";
|
|
||||||
}
|
|
||||||
|
|
||||||
let backgroundColors = [
|
|
||||||
"#8bc34a",
|
|
||||||
"#ff5722",
|
|
||||||
"#ff9800",
|
|
||||||
"#ffc107",
|
|
||||||
"#ffeb3b",
|
|
||||||
"#cddc39",
|
|
||||||
"#4caf50",
|
|
||||||
"#009688",
|
|
||||||
"#00bcd4",
|
|
||||||
"#03a9f4",
|
|
||||||
"#2196f3",
|
|
||||||
"#3f51b5",
|
|
||||||
"#673ab7",
|
|
||||||
"#9c27b0",
|
|
||||||
"#e91e63",
|
|
||||||
];
|
|
||||||
|
|
||||||
const animation: false = false;
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
animation: animation,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
beginAtZero: true,
|
|
||||||
},
|
|
||||||
x: {
|
|
||||||
type: "time" as const,
|
|
||||||
time: {
|
|
||||||
unit: unit,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let data: {
|
|
||||||
labels: number[];
|
|
||||||
datasets: {
|
|
||||||
label: string;
|
|
||||||
data: number[];
|
|
||||||
backgroundColor: string;
|
|
||||||
}[];
|
|
||||||
} = {
|
|
||||||
labels: this.props.metric.getTimestampsList().map(v => moment(v.toDate()).valueOf()),
|
|
||||||
datasets: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let ds of this.props.metric.getDatasetsList()) {
|
|
||||||
data.datasets.push({
|
|
||||||
label: ds.getLabel(),
|
|
||||||
data: ds.getDataList(),
|
|
||||||
backgroundColor: backgroundColors.shift()!,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title={this.props.metric.getName()} className="dashboard-chart">
|
|
||||||
<Bar data={data} options={options} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let backgroundColors = [
|
||||||
|
"#8bc34a",
|
||||||
|
"#ff5722",
|
||||||
|
"#ff9800",
|
||||||
|
"#ffc107",
|
||||||
|
"#ffeb3b",
|
||||||
|
"#cddc39",
|
||||||
|
"#4caf50",
|
||||||
|
"#009688",
|
||||||
|
"#00bcd4",
|
||||||
|
"#03a9f4",
|
||||||
|
"#2196f3",
|
||||||
|
"#3f51b5",
|
||||||
|
"#673ab7",
|
||||||
|
"#9c27b0",
|
||||||
|
"#e91e63",
|
||||||
|
];
|
||||||
|
|
||||||
|
const animation: false = false;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
animation: animation,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
type: "time" as const,
|
||||||
|
time: {
|
||||||
|
unit: unit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let data: {
|
||||||
|
labels: number[];
|
||||||
|
datasets: {
|
||||||
|
label: string;
|
||||||
|
data: number[];
|
||||||
|
backgroundColor: string;
|
||||||
|
}[];
|
||||||
|
} = {
|
||||||
|
labels: props.metric.getTimestampsList().map(v => moment(v.toDate()).valueOf()),
|
||||||
|
datasets: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let ds of props.metric.getDatasetsList()) {
|
||||||
|
data.datasets.push({
|
||||||
|
label: ds.getLabel(),
|
||||||
|
data: ds.getDataList(),
|
||||||
|
backgroundColor: backgroundColors.shift()!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title={props.metric.getName()} className="dashboard-chart">
|
||||||
|
<Bar data={data} options={options} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MetricBar;
|
export default MetricBar;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
import { TimeUnit } from "chart.js";
|
import { TimeUnit } from "chart.js";
|
||||||
@ -14,83 +12,81 @@ interface IProps {
|
|||||||
zeroToNull?: boolean;
|
zeroToNull?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MetricChart extends Component<IProps> {
|
function MetricChart(props: IProps) {
|
||||||
render() {
|
let unit: TimeUnit = "hour";
|
||||||
let unit: TimeUnit = "hour";
|
let tooltipFormat = "LT";
|
||||||
let tooltipFormat = "LT";
|
if (props.aggregation === Aggregation.DAY) {
|
||||||
if (this.props.aggregation === Aggregation.DAY) {
|
unit = "day";
|
||||||
unit = "day";
|
tooltipFormat = "MMM Do";
|
||||||
tooltipFormat = "MMM Do";
|
} else if (props.aggregation === Aggregation.MONTH) {
|
||||||
} else if (this.props.aggregation === Aggregation.MONTH) {
|
unit = "month";
|
||||||
unit = "month";
|
tooltipFormat = "MMM YYYY";
|
||||||
tooltipFormat = "MMM YYYY";
|
|
||||||
}
|
|
||||||
|
|
||||||
const animation: false = false;
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
animation: animation,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
beginAtZero: true,
|
|
||||||
},
|
|
||||||
x: {
|
|
||||||
type: "time" as const,
|
|
||||||
time: {
|
|
||||||
unit: unit,
|
|
||||||
tooltipFormat: tooltipFormat,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let prevValue = 0;
|
|
||||||
let data = {
|
|
||||||
labels: this.props.metric.getTimestampsList().map(v => moment(v.toDate()).valueOf()),
|
|
||||||
datasets: this.props.metric.getDatasetsList().map(v => {
|
|
||||||
return {
|
|
||||||
label: v.getLabel(),
|
|
||||||
borderColor: "rgba(33, 150, 243, 1)",
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0)",
|
|
||||||
lineTension: 0,
|
|
||||||
pointBackgroundColor: "rgba(33, 150, 243, 1)",
|
|
||||||
data: v.getDataList().map(v => {
|
|
||||||
if (v === 0 && this.props.zeroToNull) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
if (this.props.metric.getKind() === MetricKind.COUNTER) {
|
|
||||||
let val = v - prevValue;
|
|
||||||
prevValue = v;
|
|
||||||
if (val < 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return val;
|
|
||||||
} else {
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
let name = this.props.metric.getName();
|
|
||||||
if (this.props.metric.getKind() === MetricKind.COUNTER) {
|
|
||||||
name = `${name} (per ${unit})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title={name} className="dashboard-chart">
|
|
||||||
<Line height={75} options={options} data={data} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const animation: false = false;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
animation: animation,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
type: "time" as const,
|
||||||
|
time: {
|
||||||
|
unit: unit,
|
||||||
|
tooltipFormat: tooltipFormat,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let prevValue = 0;
|
||||||
|
let data = {
|
||||||
|
labels: props.metric.getTimestampsList().map(v => moment(v.toDate()).valueOf()),
|
||||||
|
datasets: props.metric.getDatasetsList().map(v => {
|
||||||
|
return {
|
||||||
|
label: v.getLabel(),
|
||||||
|
borderColor: "rgba(33, 150, 243, 1)",
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0)",
|
||||||
|
lineTension: 0,
|
||||||
|
pointBackgroundColor: "rgba(33, 150, 243, 1)",
|
||||||
|
data: v.getDataList().map(v => {
|
||||||
|
if (v === 0 && props.zeroToNull) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
if (props.metric.getKind() === MetricKind.COUNTER) {
|
||||||
|
let val = v - prevValue;
|
||||||
|
prevValue = v;
|
||||||
|
if (val < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
} else {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = props.metric.getName();
|
||||||
|
if (props.metric.getKind() === MetricKind.COUNTER) {
|
||||||
|
name = `${name} (per ${unit})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title={name} className="dashboard-chart">
|
||||||
|
<Line height={75} options={options} data={data} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MetricChart;
|
export default MetricChart;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
import { color } from "chart.js/helpers";
|
import { color } from "chart.js/helpers";
|
||||||
@ -16,138 +14,136 @@ interface IProps {
|
|||||||
aggregation: Aggregation;
|
aggregation: Aggregation;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MetricHeatmap extends Component<IProps> {
|
function MetricHeatmap(props: IProps) {
|
||||||
render() {
|
let unit: TimeUnit = "hour";
|
||||||
let unit: TimeUnit = "hour";
|
if (props.aggregation === Aggregation.DAY) {
|
||||||
if (this.props.aggregation === Aggregation.DAY) {
|
unit = "day";
|
||||||
unit = "day";
|
} else if (props.aggregation === Aggregation.MONTH) {
|
||||||
} else if (this.props.aggregation === Aggregation.MONTH) {
|
unit = "month";
|
||||||
unit = "month";
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const animation: false = false;
|
const animation: false = false;
|
||||||
|
|
||||||
let options = {
|
let options = {
|
||||||
animation: animation,
|
animation: animation,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
type: "category" as const,
|
type: "category" as const,
|
||||||
offset: true,
|
offset: true,
|
||||||
grid: {
|
grid: {
|
||||||
display: false,
|
display: false,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
x: {
|
},
|
||||||
type: "time" as const,
|
x: {
|
||||||
time: {
|
type: "time" as const,
|
||||||
unit: unit,
|
time: {
|
||||||
|
unit: unit,
|
||||||
|
},
|
||||||
|
offset: true,
|
||||||
|
labels: props.metric.getTimestampsList().map(v => moment(v.toDate().valueOf())),
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
title: () => {
|
||||||
|
return "";
|
||||||
},
|
},
|
||||||
offset: true,
|
label: (ctx: any) => {
|
||||||
labels: this.props.metric.getTimestampsList().map(v => moment(v.toDate().valueOf())),
|
const v = ctx.dataset.data[ctx.dataIndex].v;
|
||||||
grid: {
|
return "Count: " + v;
|
||||||
display: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: {
|
},
|
||||||
legend: { display: false },
|
};
|
||||||
tooltip: {
|
|
||||||
callbacks: {
|
let dataData: {
|
||||||
title: () => {
|
x: number;
|
||||||
return "";
|
y: string;
|
||||||
},
|
v: number;
|
||||||
label: (ctx: any) => {
|
}[] = [];
|
||||||
const v = ctx.dataset.data[ctx.dataIndex].v;
|
|
||||||
return "Count: " + v;
|
let data = {
|
||||||
},
|
labels: props.metric.getDatasetsList().map(v => v.getLabel()),
|
||||||
},
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Heatmap",
|
||||||
|
data: dataData,
|
||||||
|
minValue: -1,
|
||||||
|
maxValue: -1,
|
||||||
|
fromColor: props.fromColor.match(/\d+/g)!.map(Number),
|
||||||
|
toColor: props.toColor.match(/\d+/g)!.map(Number),
|
||||||
|
backgroundColor: (ctx: any): string => {
|
||||||
|
if (
|
||||||
|
ctx.dataset === undefined ||
|
||||||
|
ctx.dataset.data === undefined ||
|
||||||
|
ctx.dataset.data[ctx.dataIndex] === undefined
|
||||||
|
) {
|
||||||
|
return color("white").rgbString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = ctx.dataset.data[ctx.dataIndex].v;
|
||||||
|
const steps = ctx.dataset.maxValue - ctx.dataset.minValue + 1;
|
||||||
|
const step = value - ctx.dataset.minValue;
|
||||||
|
const factor = (1 / steps) * step;
|
||||||
|
|
||||||
|
let result: [number, number, number] = ctx.dataset.fromColor.slice();
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
result[i] = Math.round(result[i] + factor * (ctx.dataset.toColor[i] - ctx.dataset.fromColor[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return color(result).rgbString();
|
||||||
|
},
|
||||||
|
borderWidth: 0,
|
||||||
|
width: (ctx: any) => {
|
||||||
|
return (ctx.chart.chartArea || {}).width / props.metric.getTimestampsList().length - 1;
|
||||||
|
},
|
||||||
|
height: (ctx: any) => {
|
||||||
|
return (ctx.chart.chartArea || {}).height / props.metric.getDatasetsList().length - 1;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
],
|
||||||
|
};
|
||||||
|
|
||||||
let dataData: {
|
data.labels.sort();
|
||||||
x: number;
|
|
||||||
y: string;
|
|
||||||
v: number;
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
let data = {
|
const tsList = props.metric.getTimestampsList();
|
||||||
labels: this.props.metric.getDatasetsList().map(v => v.getLabel()),
|
const dsList = props.metric.getDatasetsList();
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "Heatmap",
|
|
||||||
data: dataData,
|
|
||||||
minValue: -1,
|
|
||||||
maxValue: -1,
|
|
||||||
fromColor: this.props.fromColor.match(/\d+/g)!.map(Number),
|
|
||||||
toColor: this.props.toColor.match(/\d+/g)!.map(Number),
|
|
||||||
backgroundColor: (ctx: any): string => {
|
|
||||||
if (
|
|
||||||
ctx.dataset === undefined ||
|
|
||||||
ctx.dataset.data === undefined ||
|
|
||||||
ctx.dataset.data[ctx.dataIndex] === undefined
|
|
||||||
) {
|
|
||||||
return color("white").rgbString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = ctx.dataset.data[ctx.dataIndex].v;
|
for (let i = 0; i < tsList.length; i++) {
|
||||||
const steps = ctx.dataset.maxValue - ctx.dataset.minValue + 1;
|
for (let ds of dsList) {
|
||||||
const step = value - ctx.dataset.minValue;
|
let v = ds.getDataList()[i];
|
||||||
const factor = (1 / steps) * step;
|
if (v === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let result: [number, number, number] = ctx.dataset.fromColor.slice();
|
data.datasets[0].data.push({
|
||||||
for (var i = 0; i < 3; i++) {
|
x: moment(tsList[i].toDate()).valueOf(),
|
||||||
result[i] = Math.round(result[i] + factor * (ctx.dataset.toColor[i] - ctx.dataset.fromColor[i]));
|
y: ds.getLabel(),
|
||||||
}
|
v: v,
|
||||||
|
});
|
||||||
|
|
||||||
return color(result).rgbString();
|
if (data.datasets[0].minValue === -1 || data.datasets[0].minValue > v) {
|
||||||
},
|
data.datasets[0].minValue = v;
|
||||||
borderWidth: 0,
|
}
|
||||||
width: (ctx: any) => {
|
|
||||||
return (ctx.chart.chartArea || {}).width / this.props.metric.getTimestampsList().length - 1;
|
|
||||||
},
|
|
||||||
height: (ctx: any) => {
|
|
||||||
return (ctx.chart.chartArea || {}).height / this.props.metric.getDatasetsList().length - 1;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
data.labels.sort();
|
if (data.datasets[0].maxValue < v) {
|
||||||
|
data.datasets[0].maxValue = v;
|
||||||
const tsList = this.props.metric.getTimestampsList();
|
|
||||||
const dsList = this.props.metric.getDatasetsList();
|
|
||||||
|
|
||||||
for (let i = 0; i < tsList.length; i++) {
|
|
||||||
for (let ds of dsList) {
|
|
||||||
let v = ds.getDataList()[i];
|
|
||||||
if (v === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.datasets[0].data.push({
|
|
||||||
x: moment(tsList[i].toDate()).valueOf(),
|
|
||||||
y: ds.getLabel(),
|
|
||||||
v: v,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data.datasets[0].minValue === -1 || data.datasets[0].minValue > v) {
|
|
||||||
data.datasets[0].minValue = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.datasets[0].maxValue < v) {
|
|
||||||
data.datasets[0].maxValue = v;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title={this.props.metric.getName()} className="dashboard-chart">
|
|
||||||
<Chart type="matrix" data={data} options={options} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title={props.metric.getName()} className="dashboard-chart">
|
||||||
|
<Chart type="matrix" data={data} options={options} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MetricHeatmap;
|
export default MetricHeatmap;
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
padding-top: 85px;
|
padding-top: 85px;
|
||||||
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
position: fixed;
|
position: fixed !important;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
@ -117,5 +117,10 @@ pre {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-drawer-body {
|
.ant-drawer-body {
|
||||||
padding-bottom: 88px; /* 64 + 24 */
|
padding-bottom: 88px;
|
||||||
|
/* 64 + 24 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-code input {
|
||||||
|
font-family: monospace !important;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom/client";
|
||||||
|
|
||||||
// import { Chart, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, Title } from "chart.js";
|
|
||||||
import { Chart, registerables } from "chart.js";
|
import { Chart, registerables } from "chart.js";
|
||||||
import { MatrixElement, MatrixController } from "chartjs-chart-matrix";
|
import { MatrixElement, MatrixController } from "chartjs-chart-matrix";
|
||||||
import "chartjs-adapter-moment";
|
import "chartjs-adapter-moment";
|
||||||
@ -9,7 +8,7 @@ import "chartjs-adapter-moment";
|
|||||||
import App from "./App";
|
import App from "./App";
|
||||||
import reportWebVitals from "./reportWebVitals";
|
import reportWebVitals from "./reportWebVitals";
|
||||||
|
|
||||||
import "antd/dist/antd.min.css";
|
import "antd/dist/reset.css";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||||
import "@fortawesome/fontawesome-free/css/all.css";
|
import "@fortawesome/fontawesome-free/css/all.css";
|
||||||
@ -19,12 +18,8 @@ import "./index.css";
|
|||||||
|
|
||||||
Chart.register(MatrixController, MatrixElement, ...registerables);
|
Chart.register(MatrixController, MatrixElement, ...registerables);
|
||||||
|
|
||||||
ReactDOM.render(
|
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
|
||||||
<React.StrictMode>
|
root.render(<App />);
|
||||||
<App />
|
|
||||||
</React.StrictMode>,
|
|
||||||
document.getElementById("root"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
// If you want to start measuring performance in your app, pass a function
|
||||||
// to log results (for example: reportWebVitals(console.log))
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
|
@ -111,6 +111,8 @@ class ApplicationStore extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.emit("change");
|
||||||
|
|
||||||
notification.success({
|
notification.success({
|
||||||
message: "Application updated",
|
message: "Application updated",
|
||||||
duration: 3,
|
duration: 3,
|
||||||
|
@ -31,7 +31,7 @@ class RelayStore extends EventEmitter {
|
|||||||
|
|
||||||
callbackFunc(resp);
|
callbackFunc(resp);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
addDevice = (req: AddRelayDeviceRequest, callbackFunc: () => void) => {
|
addDevice = (req: AddRelayDeviceRequest, callbackFunc: () => void) => {
|
||||||
this.client.addDevice(req, SessionStore.getMetadata(), err => {
|
this.client.addDevice(req, SessionStore.getMetadata(), err => {
|
||||||
@ -47,7 +47,7 @@ class RelayStore extends EventEmitter {
|
|||||||
|
|
||||||
callbackFunc();
|
callbackFunc();
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
removeDevice = (req: RemoveRelayDeviceRequest, callbackFunc: () => void) => {
|
removeDevice = (req: RemoveRelayDeviceRequest, callbackFunc: () => void) => {
|
||||||
this.client.removeDevice(req, SessionStore.getMetadata(), err => {
|
this.client.removeDevice(req, SessionStore.getMetadata(), err => {
|
||||||
@ -63,7 +63,7 @@ class RelayStore extends EventEmitter {
|
|||||||
|
|
||||||
callbackFunc();
|
callbackFunc();
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
listDevices = (req: ListRelayDevicesRequest, callbackFunc: (resp: ListRelayDevicesResponse) => void) => {
|
listDevices = (req: ListRelayDevicesRequest, callbackFunc: (resp: ListRelayDevicesResponse) => void) => {
|
||||||
this.client.listDevices(req, SessionStore.getMetadata(), (err, resp) => {
|
this.client.listDevices(req, SessionStore.getMetadata(), (err, resp) => {
|
||||||
@ -74,7 +74,7 @@ class RelayStore extends EventEmitter {
|
|||||||
|
|
||||||
callbackFunc(resp);
|
callbackFunc(resp);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const relayStore = new RelayStore();
|
const relayStore = new RelayStore();
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
|
|
||||||
import { Form, Input, Button } from "antd";
|
import { Form, Input, Button } from "antd";
|
||||||
|
|
||||||
import { ApiKey } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
|
import { ApiKey } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
|
||||||
@ -9,29 +7,25 @@ interface IProps {
|
|||||||
onFinish: (obj: ApiKey) => void;
|
onFinish: (obj: ApiKey) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {}
|
function ApiKeyForm(props: IProps) {
|
||||||
|
const onFinish = (values: ApiKey.AsObject) => {
|
||||||
class ApiKeyForm extends Component<IProps, IState> {
|
|
||||||
onFinish = (values: ApiKey.AsObject) => {
|
|
||||||
let apiKey = new ApiKey();
|
let apiKey = new ApiKey();
|
||||||
apiKey.setName(values.name);
|
apiKey.setName(values.name);
|
||||||
this.props.onFinish(apiKey);
|
props.onFinish(apiKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
|
||||||
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
|
<Input />
|
||||||
<Input />
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item>
|
||||||
<Form.Item>
|
<Button type="primary" htmlType="submit">
|
||||||
<Button type="primary" htmlType="submit">
|
Submit
|
||||||
Submit
|
</Button>
|
||||||
</Button>
|
</Form.Item>
|
||||||
</Form.Item>
|
</Form>
|
||||||
</Form>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ApiKeyForm;
|
export default ApiKeyForm;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Input, Typography, Button, Space } from "antd";
|
import { Input, Typography, Button, Space } from "antd";
|
||||||
@ -9,23 +8,21 @@ interface IProps {
|
|||||||
createApiKeyResponse: CreateApiKeyResponse;
|
createApiKeyResponse: CreateApiKeyResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ApiKeyToken extends Component<IProps> {
|
function ApiKeyToken(props: IProps) {
|
||||||
render() {
|
return (
|
||||||
return (
|
<Space direction="vertical" style={{ width: "100%" }}>
|
||||||
<Space direction="vertical" style={{ width: "100%" }}>
|
<Typography>
|
||||||
<Typography>
|
<Typography.Paragraph>
|
||||||
<Typography.Paragraph>
|
Use the following API token when making API requests. This token can be revoked at any time by deleting it.
|
||||||
Use the following API token when making API requests. This token can be revoked at any time by deleting it.
|
Please note that this token can only be retrieved once:
|
||||||
Please note that this token can only be retrieved once:
|
</Typography.Paragraph>
|
||||||
</Typography.Paragraph>
|
</Typography>
|
||||||
</Typography>
|
<Input.TextArea rows={4} value={props.createApiKeyResponse.getToken()} />
|
||||||
<Input.TextArea rows={4} value={this.props.createApiKeyResponse.getToken()} />
|
<Button type="primary">
|
||||||
<Button type="primary">
|
<Link to="../api-keys">Back</Link>
|
||||||
<Link to="../api-keys">Back</Link>
|
</Button>
|
||||||
</Button>
|
</Space>
|
||||||
</Space>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ApiKeyToken;
|
export default ApiKeyToken;
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Space, Breadcrumb, Card, PageHeader } from "antd";
|
import { Space, Breadcrumb, Card } from "antd";
|
||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
|
||||||
import { ApiKey, CreateApiKeyRequest, CreateApiKeyResponse } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
|
import { ApiKey, CreateApiKeyRequest, CreateApiKeyResponse } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
|
||||||
|
|
||||||
@ -9,61 +10,48 @@ import ApiKeyForm from "./ApiKeyForm";
|
|||||||
import ApiKeyToken from "./ApiKeyToken";
|
import ApiKeyToken from "./ApiKeyToken";
|
||||||
import InternalStore from "../../stores/InternalStore";
|
import InternalStore from "../../stores/InternalStore";
|
||||||
|
|
||||||
interface IProps {}
|
function CreateAdminApiKey() {
|
||||||
|
const [createApiKeyResponse, setCreateApiKeyResponse] = useState<CreateApiKeyResponse | undefined>(undefined);
|
||||||
|
|
||||||
interface IState {
|
const onFinish = (obj: ApiKey) => {
|
||||||
createApiKeyResponse?: CreateApiKeyResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
class CreateAdminApiKey extends Component<IProps, IState> {
|
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
onFinish = (obj: ApiKey) => {
|
|
||||||
obj.setIsAdmin(true);
|
obj.setIsAdmin(true);
|
||||||
|
|
||||||
let req = new CreateApiKeyRequest();
|
let req = new CreateApiKeyRequest();
|
||||||
req.setApiKey(obj);
|
req.setApiKey(obj);
|
||||||
|
|
||||||
InternalStore.createApiKey(req, (resp: CreateApiKeyResponse) => {
|
InternalStore.createApiKey(req, (resp: CreateApiKeyResponse) => {
|
||||||
this.setState({
|
setCreateApiKeyResponse(resp);
|
||||||
createApiKeyResponse: resp,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const apiKey = new ApiKey();
|
||||||
const apiKey = new ApiKey();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Add API key"
|
title="Add API key"
|
||||||
breadcrumbRender={() => (
|
breadcrumbRender={() => (
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<span>Network-server</span>
|
<span>Network-server</span>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<span>
|
<span>
|
||||||
<Link to="/api-keys">API keys</Link>
|
<Link to="/api-keys">API keys</Link>
|
||||||
</span>
|
</span>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<span>Add</span>
|
<span>Add</span>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Card>
|
<Card>
|
||||||
{!this.state.createApiKeyResponse && <ApiKeyForm initialValues={apiKey} onFinish={this.onFinish} />}
|
{!createApiKeyResponse && <ApiKeyForm initialValues={apiKey} onFinish={onFinish} />}
|
||||||
{this.state.createApiKeyResponse && <ApiKeyToken createApiKeyResponse={this.state.createApiKeyResponse} />}
|
{createApiKeyResponse && <ApiKeyToken createApiKeyResponse={createApiKeyResponse} />}
|
||||||
</Card>
|
</Card>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateAdminApiKey;
|
export default CreateAdminApiKey;
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Space, Breadcrumb, Card, PageHeader } from "antd";
|
import { Space, Breadcrumb, Card } from "antd";
|
||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
|
||||||
import { ApiKey, CreateApiKeyRequest, CreateApiKeyResponse } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
|
import { ApiKey, CreateApiKeyRequest, CreateApiKeyResponse } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
|
||||||
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
||||||
@ -10,68 +11,57 @@ import ApiKeyForm from "./ApiKeyForm";
|
|||||||
import ApiKeyToken from "./ApiKeyToken";
|
import ApiKeyToken from "./ApiKeyToken";
|
||||||
import InternalStore from "../../stores/InternalStore";
|
import InternalStore from "../../stores/InternalStore";
|
||||||
|
|
||||||
interface IState {
|
|
||||||
createApiKeyResponse?: CreateApiKeyResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
tenant: Tenant;
|
tenant: Tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateTenantApiKey extends Component<IProps, IState> {
|
function CreateTenantApiKey(props: IProps) {
|
||||||
constructor(props: IProps) {
|
const [createApiKeyResponse, setCreateApiKeyResponse] = useState<CreateApiKeyResponse | undefined>(undefined);
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
onFinish = (obj: ApiKey) => {
|
const onFinish = (obj: ApiKey) => {
|
||||||
obj.setTenantId(this.props.tenant.getId());
|
obj.setTenantId(props.tenant.getId());
|
||||||
|
|
||||||
let req = new CreateApiKeyRequest();
|
let req = new CreateApiKeyRequest();
|
||||||
req.setApiKey(obj);
|
req.setApiKey(obj);
|
||||||
|
|
||||||
InternalStore.createApiKey(req, (resp: CreateApiKeyResponse) => {
|
InternalStore.createApiKey(req, (resp: CreateApiKeyResponse) => {
|
||||||
this.setState({
|
setCreateApiKeyResponse(resp);
|
||||||
createApiKeyResponse: resp,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const apiKey = new ApiKey();
|
||||||
const apiKey = new ApiKey();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
breadcrumbRender={() => (
|
breadcrumbRender={() => (
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<span>Tenants</span>
|
<span>Tenants</span>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<span>
|
<span>
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||||
</span>
|
</span>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<span>
|
<span>
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}/api-keys`}>API Keys</Link>
|
<Link to={`/tenants/${props.tenant.getId()}/api-keys`}>API Keys</Link>
|
||||||
</span>
|
</span>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<span>Add</span>
|
<span>Add</span>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
)}
|
)}
|
||||||
title="Add API key"
|
title="Add API key"
|
||||||
/>
|
/>
|
||||||
<Card>
|
<Card>
|
||||||
{!this.state.createApiKeyResponse && <ApiKeyForm initialValues={apiKey} onFinish={this.onFinish} />}
|
{!createApiKeyResponse && <ApiKeyForm initialValues={apiKey} onFinish={onFinish} />}
|
||||||
{this.state.createApiKeyResponse && <ApiKeyToken createApiKeyResponse={this.state.createApiKeyResponse} />}
|
{createApiKeyResponse && <ApiKeyToken createApiKeyResponse={createApiKeyResponse} />}
|
||||||
</Card>
|
</Card>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateTenantApiKey;
|
export default CreateTenantApiKey;
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { DeleteOutlined } from "@ant-design/icons";
|
import { DeleteOutlined } from "@ant-design/icons";
|
||||||
import { Space, Breadcrumb, Button, PageHeader } from "antd";
|
import { Space, Breadcrumb, Button } from "antd";
|
||||||
import { ColumnsType } from "antd/es/table";
|
import { ColumnsType } from "antd/es/table";
|
||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ListApiKeysRequest,
|
ListApiKeysRequest,
|
||||||
@ -17,62 +17,47 @@ import DataTable, { GetPageCallbackFunc } from "../../components/DataTable";
|
|||||||
import InternalStore from "../../stores/InternalStore";
|
import InternalStore from "../../stores/InternalStore";
|
||||||
import DeleteConfirm from "../../components/DeleteConfirm";
|
import DeleteConfirm from "../../components/DeleteConfirm";
|
||||||
|
|
||||||
interface IProps {}
|
function ListAdminApiKeys() {
|
||||||
|
const [refreshKey, setRefreshKey] = useState<number>(1);
|
||||||
|
|
||||||
interface IState {
|
const columns: ColumnsType<ApiKey.AsObject> = [
|
||||||
refreshKey: number;
|
{
|
||||||
}
|
title: "ID",
|
||||||
|
dataIndex: "id",
|
||||||
|
key: "id",
|
||||||
|
width: 400,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Name",
|
||||||
|
dataIndex: "name",
|
||||||
|
key: "name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Action",
|
||||||
|
dataIndex: "id",
|
||||||
|
key: "action",
|
||||||
|
width: 100,
|
||||||
|
render: (text, record) => (
|
||||||
|
<DeleteConfirm typ="API key" confirm={record.name} onConfirm={deleteApiKey(record.id)}>
|
||||||
|
<Button shape="circle" icon={<DeleteOutlined />} />
|
||||||
|
</DeleteConfirm>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
class ListAdminApiKeys extends Component<IProps, IState> {
|
const deleteApiKey = (id: string): (() => void) => {
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
refreshKey: 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
columns = (): ColumnsType<ApiKey.AsObject> => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
title: "ID",
|
|
||||||
dataIndex: "id",
|
|
||||||
key: "id",
|
|
||||||
width: 400,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Name",
|
|
||||||
dataIndex: "name",
|
|
||||||
key: "name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Action",
|
|
||||||
dataIndex: "id",
|
|
||||||
key: "action",
|
|
||||||
width: 100,
|
|
||||||
render: (text, record) => (
|
|
||||||
<DeleteConfirm typ="API key" confirm={record.name} onConfirm={this.deleteApiKey(record.id)}>
|
|
||||||
<Button shape="circle" icon={<DeleteOutlined />} />
|
|
||||||
</DeleteConfirm>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteApiKey = (id: string): (() => void) => {
|
|
||||||
return () => {
|
return () => {
|
||||||
let req = new DeleteApiKeyRequest();
|
let req = new DeleteApiKeyRequest();
|
||||||
req.setId(id);
|
req.setId(id);
|
||||||
|
|
||||||
InternalStore.deleteApiKey(req, () => {
|
InternalStore.deleteApiKey(req, () => {
|
||||||
// trigger a data-table reload
|
// trigger a data-table reload
|
||||||
this.setState({
|
setRefreshKey(refreshKey + 1);
|
||||||
refreshKey: this.state.refreshKey + 1,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||||
let req = new ListApiKeysRequest();
|
let req = new ListApiKeysRequest();
|
||||||
req.setLimit(limit);
|
req.setLimit(limit);
|
||||||
req.setOffset(offset);
|
req.setOffset(offset);
|
||||||
@ -84,31 +69,29 @@ class ListAdminApiKeys extends Component<IProps, IState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
<PageHeader
|
||||||
<PageHeader
|
breadcrumbRender={() => (
|
||||||
breadcrumbRender={() => (
|
<Breadcrumb>
|
||||||
<Breadcrumb>
|
<Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<span>Network Server</span>
|
||||||
<span>Network Server</span>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<span>API keys</span>
|
||||||
<span>API keys</span>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb>
|
||||||
</Breadcrumb>
|
)}
|
||||||
)}
|
title="API keys"
|
||||||
title="API keys"
|
extra={[
|
||||||
extra={[
|
<Button type="primary">
|
||||||
<Button type="primary">
|
<Link to="/api-keys/create">Add API key</Link>
|
||||||
<Link to="/api-keys/create">Add API key</Link>
|
</Button>,
|
||||||
</Button>,
|
]}
|
||||||
]}
|
/>
|
||||||
/>
|
<DataTable columns={columns} getPage={getPage} rowKey="id" refreshKey={refreshKey} />
|
||||||
<DataTable columns={this.columns()} getPage={this.getPage} rowKey="id" refreshKey={this.state.refreshKey} />
|
</Space>
|
||||||
</Space>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ListAdminApiKeys;
|
export default ListAdminApiKeys;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { DeleteOutlined } from "@ant-design/icons";
|
import { DeleteOutlined } from "@ant-design/icons";
|
||||||
import { Space, Breadcrumb, Button, PageHeader } from "antd";
|
import { Space, Breadcrumb, Button } from "antd";
|
||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
import { ColumnsType } from "antd/es/table";
|
import { ColumnsType } from "antd/es/table";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -20,69 +21,55 @@ import Admin from "../../components/Admin";
|
|||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
tenant: Tenant;
|
tenant: Tenant;
|
||||||
isAdmin: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function ListTenantApiKeys(props: IProps) {
|
||||||
refreshKey: number;
|
const [refreshKey, setRefreshKey] = useState<number>(1);
|
||||||
}
|
|
||||||
|
|
||||||
class ListTenantApiKeys extends Component<IProps, IState> {
|
const columns: ColumnsType<ApiKey.AsObject> = [
|
||||||
constructor(props: IProps) {
|
{
|
||||||
super(props);
|
title: "ID",
|
||||||
this.state = {
|
dataIndex: "id",
|
||||||
refreshKey: 1,
|
key: "id",
|
||||||
};
|
width: 400,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
title: "Name",
|
||||||
|
dataIndex: "name",
|
||||||
|
key: "name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Action",
|
||||||
|
dataIndex: "id",
|
||||||
|
key: "action",
|
||||||
|
width: 100,
|
||||||
|
render: (text, record) => (
|
||||||
|
<Admin tenantId={props.tenant.getId()} isTenantAdmin>
|
||||||
|
<DeleteConfirm typ="API key" confirm={record.name} onConfirm={deleteApiKey(record.id)}>
|
||||||
|
<Button shape="circle" icon={<DeleteOutlined />} />
|
||||||
|
</DeleteConfirm>
|
||||||
|
</Admin>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
columns = (): ColumnsType<ApiKey.AsObject> => {
|
const deleteApiKey = (id: string): (() => void) => {
|
||||||
return [
|
|
||||||
{
|
|
||||||
title: "ID",
|
|
||||||
dataIndex: "id",
|
|
||||||
key: "id",
|
|
||||||
width: 400,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Name",
|
|
||||||
dataIndex: "name",
|
|
||||||
key: "name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Action",
|
|
||||||
dataIndex: "id",
|
|
||||||
key: "action",
|
|
||||||
width: 100,
|
|
||||||
render: (text, record) => (
|
|
||||||
<Admin tenantId={this.props.tenant.getId()} isTenantAdmin>
|
|
||||||
<DeleteConfirm typ="API key" confirm={record.name} onConfirm={this.deleteApiKey(record.id)}>
|
|
||||||
<Button shape="circle" icon={<DeleteOutlined />} />
|
|
||||||
</DeleteConfirm>
|
|
||||||
</Admin>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteApiKey = (id: string): (() => void) => {
|
|
||||||
return () => {
|
return () => {
|
||||||
let req = new DeleteApiKeyRequest();
|
let req = new DeleteApiKeyRequest();
|
||||||
req.setId(id);
|
req.setId(id);
|
||||||
|
|
||||||
InternalStore.deleteApiKey(req, () => {
|
InternalStore.deleteApiKey(req, () => {
|
||||||
// trigger a data-table reload
|
// trigger a data-table reload
|
||||||
this.setState({
|
setRefreshKey(refreshKey + 1);
|
||||||
refreshKey: this.state.refreshKey + 1,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||||
let req = new ListApiKeysRequest();
|
let req = new ListApiKeysRequest();
|
||||||
req.setLimit(limit);
|
req.setLimit(limit);
|
||||||
req.setOffset(offset);
|
req.setOffset(offset);
|
||||||
req.setTenantId(this.props.tenant.getId());
|
req.setTenantId(props.tenant.getId());
|
||||||
|
|
||||||
InternalStore.listApiKeys(req, (resp: ListApiKeysResponse) => {
|
InternalStore.listApiKeys(req, (resp: ListApiKeysResponse) => {
|
||||||
const obj = resp.toObject();
|
const obj = resp.toObject();
|
||||||
@ -90,38 +77,36 @@ class ListTenantApiKeys extends Component<IProps, IState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
<PageHeader
|
||||||
<PageHeader
|
breadcrumbRender={() => (
|
||||||
breadcrumbRender={() => (
|
<Breadcrumb>
|
||||||
<Breadcrumb>
|
<Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<span>Tenants</span>
|
||||||
<span>Tenants</span>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<span>
|
||||||
<span>
|
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
</span>
|
||||||
</span>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<span>API Keys</span>
|
||||||
<span>API Keys</span>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb>
|
||||||
</Breadcrumb>
|
)}
|
||||||
)}
|
title="API keys"
|
||||||
title="API keys"
|
extra={[
|
||||||
extra={[
|
<Admin tenantId={props.tenant.getId()} isTenantAdmin>
|
||||||
<Admin tenantId={this.props.tenant.getId()} isTenantAdmin>
|
<Button type="primary">
|
||||||
<Button type="primary">
|
<Link to={`/tenants/${props.tenant.getId()}/api-keys/create`}>Add API key</Link>
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}/api-keys/create`}>Add API key</Link>
|
</Button>
|
||||||
</Button>
|
</Admin>,
|
||||||
</Admin>,
|
]}
|
||||||
]}
|
/>
|
||||||
/>
|
<DataTable columns={columns} getPage={getPage} rowKey="id" refreshKey={refreshKey} />
|
||||||
<DataTable columns={this.columns()} getPage={this.getPage} rowKey="id" refreshKey={this.state.refreshKey} />
|
</Space>
|
||||||
</Space>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ListTenantApiKeys;
|
export default ListTenantApiKeys;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
|
|
||||||
import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||||
import { Form, Input, Button } from "antd";
|
import { Form, Input, Button } from "antd";
|
||||||
|
|
||||||
@ -9,9 +7,9 @@ interface IProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ApplicationForm extends Component<IProps> {
|
function ApplicationForm(props: IProps) {
|
||||||
onFinish = (values: Application.AsObject) => {
|
const onFinish = (values: Application.AsObject) => {
|
||||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
const v = Object.assign(props.initialValues.toObject(), values);
|
||||||
let app = new Application();
|
let app = new Application();
|
||||||
|
|
||||||
app.setId(v.id);
|
app.setId(v.id);
|
||||||
@ -19,26 +17,24 @@ class ApplicationForm extends Component<IProps> {
|
|||||||
app.setName(v.name);
|
app.setName(v.name);
|
||||||
app.setDescription(v.description);
|
app.setDescription(v.description);
|
||||||
|
|
||||||
this.props.onFinish(app);
|
props.onFinish(app);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
|
||||||
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
|
<Input disabled={props.disabled} />
|
||||||
<Input disabled={this.props.disabled} />
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item label="Description" name="description">
|
||||||
<Form.Item label="Description" name="description">
|
<Input.TextArea disabled={props.disabled} />
|
||||||
<Input.TextArea disabled={this.props.disabled} />
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item>
|
||||||
<Form.Item>
|
<Button type="primary" htmlType="submit" disabled={props.disabled}>
|
||||||
<Button type="primary" htmlType="submit" disabled={this.props.disabled}>
|
Submit
|
||||||
Submit
|
</Button>
|
||||||
</Button>
|
</Form.Item>
|
||||||
</Form.Item>
|
</Form>
|
||||||
</Form>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ApplicationForm;
|
export default ApplicationForm;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { Component } from "react";
|
import { Route, Routes, Link, useNavigate, useLocation } from "react-router-dom";
|
||||||
import { Route, Switch, RouteComponentProps, Link } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Space, Breadcrumb, Card, Button, PageHeader, Menu } from "antd";
|
import { Space, Breadcrumb, Card, Button, Menu } from "antd";
|
||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
|
||||||
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
||||||
import { Application, DeleteApplicationRequest } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
import { Application, DeleteApplicationRequest } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||||
@ -38,247 +38,158 @@ import GenerateMqttCertificate from "./integrations/GenerateMqttCertificate";
|
|||||||
import CreateIftttIntegration from "./integrations/CreateIftttIntegration";
|
import CreateIftttIntegration from "./integrations/CreateIftttIntegration";
|
||||||
import EditIftttIntegration from "./integrations/EditIftttIntegration";
|
import EditIftttIntegration from "./integrations/EditIftttIntegration";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
tenant: Tenant;
|
tenant: Tenant;
|
||||||
application: Application;
|
application: Application;
|
||||||
measurementKeys: string[];
|
measurementKeys: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class ApplicationLayout extends Component<IProps> {
|
function ApplicationLayout(props: IProps) {
|
||||||
deleteApplication = () => {
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const deleteApplication = () => {
|
||||||
let req = new DeleteApplicationRequest();
|
let req = new DeleteApplicationRequest();
|
||||||
req.setId(this.props.application.getId());
|
req.setId(props.application.getId());
|
||||||
|
|
||||||
ApplicationStore.delete(req, () => {
|
ApplicationStore.delete(req, () => {
|
||||||
this.props.history.push(`/tenants/${this.props.tenant.getId()}/applications`);
|
navigate(`/tenants/${props.tenant.getId()}/applications`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const tenant = props.tenant;
|
||||||
const tenant = this.props.tenant;
|
const app = props.application;
|
||||||
const app = this.props.application;
|
|
||||||
|
|
||||||
if (!app) {
|
if (!app) {
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
const path = this.props.history.location.pathname;
|
|
||||||
let tab = "devices";
|
|
||||||
|
|
||||||
if (path.endsWith("/multicast-groups")) {
|
|
||||||
tab = "mg";
|
|
||||||
}
|
|
||||||
if (path.endsWith("/relays")) {
|
|
||||||
tab = "relay";
|
|
||||||
}
|
|
||||||
if (path.endsWith("/edit")) {
|
|
||||||
tab = "edit";
|
|
||||||
}
|
|
||||||
if (path.match(/.*\/integrations.*/g)) {
|
|
||||||
tab = "integrations";
|
|
||||||
}
|
|
||||||
|
|
||||||
const showIntegrations =
|
|
||||||
SessionStore.isAdmin() ||
|
|
||||||
SessionStore.isTenantAdmin(tenant.getId()) ||
|
|
||||||
SessionStore.isTenantDeviceAdmin(tenant.getId());
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
|
||||||
<PageHeader
|
|
||||||
breadcrumbRender={() => (
|
|
||||||
<Breadcrumb>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>Tenants</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>
|
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
|
||||||
</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>
|
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}/applications`}>Applications</Link>
|
|
||||||
</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>{app.getName()}</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
</Breadcrumb>
|
|
||||||
)}
|
|
||||||
title={app.getName()}
|
|
||||||
subTitle={`application id: ${app.getId()}`}
|
|
||||||
extra={[
|
|
||||||
<Admin tenantId={this.props.tenant.getId()} isDeviceAdmin>
|
|
||||||
<DeleteConfirm confirm={app.getName()} typ="application" onConfirm={this.deleteApplication}>
|
|
||||||
<Button danger type="primary">
|
|
||||||
Delete application
|
|
||||||
</Button>
|
|
||||||
</DeleteConfirm>
|
|
||||||
</Admin>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Card>
|
|
||||||
<Menu mode="horizontal" selectedKeys={[tab]} style={{ marginBottom: 24 }}>
|
|
||||||
<Menu.Item key="devices">
|
|
||||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}`}>Devices</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="mg">
|
|
||||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/multicast-groups`}>
|
|
||||||
Multicast groups
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="relay">
|
|
||||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/relays`}>
|
|
||||||
Relays
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="edit">
|
|
||||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/edit`}>Application configuration</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
{showIntegrations && (
|
|
||||||
<Menu.Item key="integrations">
|
|
||||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/integrations`}>Integrations</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
<Switch>
|
|
||||||
<Route exact path={this.props.match.path} render={props => <ListDevices application={app} {...props} />} />
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/edit`}
|
|
||||||
render={props => <EditApplication application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations`}
|
|
||||||
render={props => <ListIntegrations application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/multicast-groups`}
|
|
||||||
render={props => <ListMulticastGroups application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/relays`}
|
|
||||||
render={props => <ListRelays application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/http/create`}
|
|
||||||
render={props => <CreateHttpIntegration application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/http/edit`}
|
|
||||||
render={props => <EditHttpIntegration application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/aws-sns/create`}
|
|
||||||
render={props => <CreateAwsSnsIntegration application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/aws-sns/edit`}
|
|
||||||
render={props => <EditAwsSnsIntegration application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/azure-service-bus/create`}
|
|
||||||
render={props => <CreateAzureServiceBusIntegration application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/azure-service-bus/edit`}
|
|
||||||
render={props => <EditAzureServiceBusIntegration application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/gcp-pub-sub/create`}
|
|
||||||
render={props => <CreateGcpPubSubIntegration application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/gcp-pub-sub/edit`}
|
|
||||||
render={props => <EditGcpPubSubIntegration application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/influxdb/create`}
|
|
||||||
render={props => <CreateInfluxDbIntegration application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/influxdb/edit`}
|
|
||||||
render={props => <EditInfluxDbIntegration application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/mydevices/create`}
|
|
||||||
render={props => <CreateMyDevicesIntegration application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/mydevices/edit`}
|
|
||||||
render={props => <EditMyDevicesIntegration application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/pilot-things/create`}
|
|
||||||
render={props => <CreatePilotThingsIntegration application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/pilot-things/edit`}
|
|
||||||
render={props => <EditPilotThingsIntegration application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/loracloud/create`}
|
|
||||||
render={props => <CreateLoRaCloudIntegration application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/loracloud/edit`}
|
|
||||||
render={props => <EditLoRaCloudIntegration application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/thingsboard/create`}
|
|
||||||
render={props => <CreateThingsBoardIntegration application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/thingsboard/edit`}
|
|
||||||
render={props => <EditThingsBoardIntegration application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/mqtt/certificate`}
|
|
||||||
render={props => <GenerateMqttCertificate application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/ifttt/create`}
|
|
||||||
render={props => (
|
|
||||||
<CreateIftttIntegration application={app} measurementKeys={this.props.measurementKeys} {...props} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/integrations/ifttt/edit`}
|
|
||||||
render={props => (
|
|
||||||
<EditIftttIntegration application={app} measurementKeys={this.props.measurementKeys} {...props} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
</Card>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const path = location.pathname;
|
||||||
|
let tab = "devices";
|
||||||
|
|
||||||
|
if (path.endsWith("/multicast-groups")) {
|
||||||
|
tab = "mg";
|
||||||
|
}
|
||||||
|
if (path.endsWith("/relays")) {
|
||||||
|
tab = "relay";
|
||||||
|
}
|
||||||
|
if (path.endsWith("/edit")) {
|
||||||
|
tab = "edit";
|
||||||
|
}
|
||||||
|
if (path.match(/.*\/integrations.*/g)) {
|
||||||
|
tab = "integrations";
|
||||||
|
}
|
||||||
|
|
||||||
|
const showIntegrations =
|
||||||
|
SessionStore.isAdmin() ||
|
||||||
|
SessionStore.isTenantAdmin(tenant.getId()) ||
|
||||||
|
SessionStore.isTenantDeviceAdmin(tenant.getId());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
|
<PageHeader
|
||||||
|
breadcrumbRender={() => (
|
||||||
|
<Breadcrumb>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>Tenants</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>
|
||||||
|
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||||
|
</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>
|
||||||
|
<Link to={`/tenants/${props.tenant.getId()}/applications`}>Applications</Link>
|
||||||
|
</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>{app.getName()}</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
</Breadcrumb>
|
||||||
|
)}
|
||||||
|
title={app.getName()}
|
||||||
|
subTitle={`application id: ${app.getId()}`}
|
||||||
|
extra={[
|
||||||
|
<Admin tenantId={props.tenant.getId()} isDeviceAdmin>
|
||||||
|
<DeleteConfirm confirm={app.getName()} typ="application" onConfirm={deleteApplication}>
|
||||||
|
<Button danger type="primary">
|
||||||
|
Delete application
|
||||||
|
</Button>
|
||||||
|
</DeleteConfirm>
|
||||||
|
</Admin>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Card>
|
||||||
|
<Menu mode="horizontal" selectedKeys={[tab]} style={{ marginBottom: 24 }}>
|
||||||
|
<Menu.Item key="devices">
|
||||||
|
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}`}>Devices</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="mg">
|
||||||
|
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/multicast-groups`}>Multicast groups</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="relay">
|
||||||
|
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/relays`}>Relays</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="edit">
|
||||||
|
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/edit`}>Application configuration</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
{showIntegrations && (
|
||||||
|
<Menu.Item key="integrations">
|
||||||
|
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/integrations`}>Integrations</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<ListDevices application={app} />} />
|
||||||
|
<Route path="/edit" element={<EditApplication application={app} />} />
|
||||||
|
<Route path="/integrations" element={<ListIntegrations application={app} />} />
|
||||||
|
<Route path="/multicast-groups" element={<ListMulticastGroups application={app} />} />
|
||||||
|
<Route path="/relays" element={<ListRelays application={app} />} />
|
||||||
|
|
||||||
|
<Route path="/integrations/http/create" element={<CreateHttpIntegration application={app} />} />
|
||||||
|
<Route path="/integrations/http/edit" element={<EditHttpIntegration application={app} />} />
|
||||||
|
|
||||||
|
<Route path="/integrations/http/create" element={<CreateHttpIntegration application={app} />} />
|
||||||
|
<Route path="/integrations/http/edit" element={<EditHttpIntegration application={app} />} />
|
||||||
|
<Route path="/integrations/aws-sns/create" element={<CreateAwsSnsIntegration application={app} />} />
|
||||||
|
<Route path="/integrations/aws-sns/edit" element={<EditAwsSnsIntegration application={app} />} />
|
||||||
|
<Route
|
||||||
|
path="/integrations/azure-service-bus/create"
|
||||||
|
element={<CreateAzureServiceBusIntegration application={app} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/integrations/azure-service-bus/edit"
|
||||||
|
element={<EditAzureServiceBusIntegration application={app} />}
|
||||||
|
/>
|
||||||
|
<Route path="/integrations/gcp-pub-sub/create" element={<CreateGcpPubSubIntegration application={app} />} />
|
||||||
|
<Route path="/integrations/gcp-pub-sub/edit" element={<EditGcpPubSubIntegration application={app} />} />
|
||||||
|
<Route path="/integrations/influxdb/create" element={<CreateInfluxDbIntegration application={app} />} />
|
||||||
|
<Route path="/integrations/influxdb/edit" element={<EditInfluxDbIntegration application={app} />} />
|
||||||
|
<Route path="/integrations/mydevices/create" element={<CreateMyDevicesIntegration application={app} />} />
|
||||||
|
<Route path="/integrations/mydevices/edit" element={<EditMyDevicesIntegration application={app} />} />
|
||||||
|
<Route
|
||||||
|
path="/integrations/pilot-things/create"
|
||||||
|
element={<CreatePilotThingsIntegration application={app} />}
|
||||||
|
/>
|
||||||
|
<Route path="/integrations/pilot-things/edit" element={<EditPilotThingsIntegration application={app} />} />
|
||||||
|
<Route path="/integrations/loracloud/create" element={<CreateLoRaCloudIntegration application={app} />} />
|
||||||
|
<Route path="/integrations/loracloud/edit" element={<EditLoRaCloudIntegration application={app} />} />
|
||||||
|
<Route path="/integrations/thingsboard/create" element={<CreateThingsBoardIntegration application={app} />} />
|
||||||
|
<Route path="/integrations/thingsboard/edit" element={<EditThingsBoardIntegration application={app} />} />
|
||||||
|
<Route path="/integrations/mqtt/certificate" element={<GenerateMqttCertificate application={app} />} />
|
||||||
|
<Route
|
||||||
|
path="/integrations/ifttt/create"
|
||||||
|
element={<CreateIftttIntegration application={app} measurementKeys={props.measurementKeys} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/integrations/ifttt/edit"
|
||||||
|
element={<EditIftttIntegration application={app} measurementKeys={props.measurementKeys} />}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ApplicationLayout;
|
export default ApplicationLayout;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Route, Switch, RouteComponentProps } from "react-router-dom";
|
import { Route, Routes, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
||||||
import {
|
import {
|
||||||
@ -16,90 +16,57 @@ import MulticastGroupLayout from "../multicast-groups/MulticastGroupLayout";
|
|||||||
import CreateMulticastGroup from "../multicast-groups/CreateMulticastGroup";
|
import CreateMulticastGroup from "../multicast-groups/CreateMulticastGroup";
|
||||||
import RelayLayout from "../relays/RelayLayout";
|
import RelayLayout from "../relays/RelayLayout";
|
||||||
|
|
||||||
interface MatchParams {
|
interface IProps {
|
||||||
applicationId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps<MatchParams> {
|
|
||||||
tenant: Tenant;
|
tenant: Tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function ApplicationLoader(props: IProps) {
|
||||||
application?: Application;
|
const { applicationId } = useParams();
|
||||||
measurementKeys: string[];
|
const [application, setApplication] = useState<Application | undefined>(undefined);
|
||||||
}
|
const [measurementKeys, setMeasurementKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
class ApplicationLoader extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
ApplicationStore.on("change", loadApplication);
|
||||||
super(props);
|
loadApplication();
|
||||||
this.state = {
|
|
||||||
measurementKeys: [],
|
return () => {
|
||||||
|
ApplicationStore.removeAllListeners("change");
|
||||||
};
|
};
|
||||||
}
|
}, [applicationId]);
|
||||||
|
|
||||||
componentDidMount() {
|
const loadApplication = () => {
|
||||||
this.getApplication();
|
|
||||||
}
|
|
||||||
|
|
||||||
getApplication = () => {
|
|
||||||
let req = new GetApplicationRequest();
|
let req = new GetApplicationRequest();
|
||||||
req.setId(this.props.match.params.applicationId);
|
req.setId(applicationId!);
|
||||||
|
|
||||||
ApplicationStore.get(req, (resp: GetApplicationResponse) => {
|
ApplicationStore.get(req, (resp: GetApplicationResponse) => {
|
||||||
this.setState({
|
setApplication(resp.getApplication());
|
||||||
application: resp.getApplication(),
|
setMeasurementKeys(resp.getMeasurementKeysList());
|
||||||
measurementKeys: resp.getMeasurementKeysList(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const app = application;
|
||||||
const app = this.state.application;
|
if (!app) {
|
||||||
if (!app) {
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = this.props.match.path;
|
|
||||||
const tenant = this.props.tenant;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${path}/devices/create`}
|
|
||||||
render={props => <CreateDevice tenant={tenant} application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${path}/multicast-groups/create`}
|
|
||||||
render={props => <CreateMulticastGroup tenant={tenant} application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path={`${path}/multicast-groups/:multicastGroupId([\\w-]{36})`}
|
|
||||||
render={(props: any) => <MulticastGroupLayout tenant={tenant} application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path={`${path}/devices/:devEui([0-9a-f]{16})`}
|
|
||||||
component={(props: any) => <DeviceLayout tenant={tenant} application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path={`${path}/relays/:relayDevEui([0-9a-f]{16})`}
|
|
||||||
component={(props: any) => <RelayLayout tenant={tenant} application={app} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path={path}
|
|
||||||
render={props => (
|
|
||||||
<ApplicationLayout
|
|
||||||
tenant={tenant}
|
|
||||||
application={app}
|
|
||||||
measurementKeys={this.state.measurementKeys}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tenant = props.tenant;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/devices/create" element={<CreateDevice tenant={tenant} application={app} />} />
|
||||||
|
<Route path="/multicast-groups/create" element={<CreateMulticastGroup tenant={tenant} application={app} />} />
|
||||||
|
<Route
|
||||||
|
path="/multicast-groups/:multicastGroupId/*"
|
||||||
|
element={<MulticastGroupLayout tenant={tenant} application={app} />}
|
||||||
|
/>
|
||||||
|
<Route path="/devices/:devEui/*" element={<DeviceLayout tenant={tenant} application={app} />} />
|
||||||
|
<Route path="/relays/:relayDevEui/*" element={<RelayLayout tenant={tenant} application={app} />} />
|
||||||
|
<Route
|
||||||
|
path="/*"
|
||||||
|
element={<ApplicationLayout tenant={tenant} application={app} measurementKeys={measurementKeys} />}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ApplicationLoader;
|
export default ApplicationLoader;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { Component } from "react";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { Link, RouteComponentProps } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Space, Breadcrumb, Card, PageHeader } from "antd";
|
import { Space, Breadcrumb, Card } from "antd";
|
||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
|
||||||
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
||||||
import {
|
import {
|
||||||
@ -13,56 +13,56 @@ import {
|
|||||||
import ApplicationForm from "./ApplicationForm";
|
import ApplicationForm from "./ApplicationForm";
|
||||||
import ApplicationStore from "../../stores/ApplicationStore";
|
import ApplicationStore from "../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
tenant: Tenant;
|
tenant: Tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateApplication extends Component<IProps> {
|
function CreateApplication(props: IProps) {
|
||||||
onFinish = (obj: Application) => {
|
const navigate = useNavigate();
|
||||||
obj.setTenantId(this.props.tenant.getId());
|
|
||||||
|
const onFinish = (obj: Application) => {
|
||||||
|
obj.setTenantId(props.tenant.getId());
|
||||||
|
|
||||||
let req = new CreateApplicationRequest();
|
let req = new CreateApplicationRequest();
|
||||||
req.setApplication(obj);
|
req.setApplication(obj);
|
||||||
|
|
||||||
ApplicationStore.create(req, (resp: CreateApplicationResponse) => {
|
ApplicationStore.create(req, (resp: CreateApplicationResponse) => {
|
||||||
this.props.history.push(`/tenants/${this.props.tenant.getId()}/applications/${resp.getId()}`);
|
navigate(`/tenants/${props.tenant.getId()}/applications/${resp.getId()}`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const app = new Application();
|
||||||
const app = new Application();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
breadcrumbRender={() => (
|
breadcrumbRender={() => (
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<span>Tenants</span>
|
<span>Tenants</span>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<span>
|
<span>
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||||
</span>
|
</span>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<span>
|
<span>
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}/applications`}>Applications</Link>
|
<Link to={`/tenants/${props.tenant.getId()}/applications`}>Applications</Link>
|
||||||
</span>
|
</span>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<span>Add</span>
|
<span>Add</span>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
)}
|
)}
|
||||||
title="Add application"
|
title="Add application"
|
||||||
/>
|
/>
|
||||||
<Card>
|
<Card>
|
||||||
<ApplicationForm initialValues={app} onFinish={this.onFinish} />
|
<ApplicationForm initialValues={app} onFinish={onFinish} />
|
||||||
</Card>
|
</Card>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateApplication;
|
export default CreateApplication;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Application, UpdateApplicationRequest } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
import { Application, UpdateApplicationRequest } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||||
|
|
||||||
@ -7,31 +6,29 @@ import ApplicationStore from "../../stores/ApplicationStore";
|
|||||||
import ApplicationForm from "./ApplicationForm";
|
import ApplicationForm from "./ApplicationForm";
|
||||||
import SessionStore from "../../stores/SessionStore";
|
import SessionStore from "../../stores/SessionStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
class EditApplication extends Component<IProps> {
|
function EditApplication(props: IProps) {
|
||||||
onFinish = (obj: Application) => {
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const onFinish = (obj: Application) => {
|
||||||
let req = new UpdateApplicationRequest();
|
let req = new UpdateApplicationRequest();
|
||||||
req.setApplication(obj);
|
req.setApplication(obj);
|
||||||
|
|
||||||
ApplicationStore.update(req, () => {
|
ApplicationStore.update(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const disabled = !(
|
||||||
const disabled = !(
|
SessionStore.isAdmin() ||
|
||||||
SessionStore.isAdmin() ||
|
SessionStore.isTenantAdmin(props.application.getTenantId()) ||
|
||||||
SessionStore.isTenantAdmin(this.props.application.getTenantId()) ||
|
SessionStore.isTenantDeviceAdmin(props.application.getTenantId())
|
||||||
SessionStore.isTenantDeviceAdmin(this.props.application.getTenantId())
|
);
|
||||||
);
|
|
||||||
|
|
||||||
return <ApplicationForm initialValues={this.props.application} disabled={disabled} onFinish={this.onFinish} />;
|
return <ApplicationForm initialValues={props.application} disabled={disabled} onFinish={onFinish} />;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditApplication;
|
export default EditApplication;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Space, Breadcrumb, Button, PageHeader } from "antd";
|
import { Space, Breadcrumb, Button } from "antd";
|
||||||
import { ColumnsType } from "antd/es/table";
|
import { ColumnsType } from "antd/es/table";
|
||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ListApplicationsRequest,
|
ListApplicationsRequest,
|
||||||
@ -19,29 +19,25 @@ interface IProps {
|
|||||||
tenant: Tenant;
|
tenant: Tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ListApplications extends Component<IProps> {
|
function ListApplications(props: IProps) {
|
||||||
columns = (): ColumnsType<ApplicationListItem.AsObject> => {
|
const columns: ColumnsType<ApplicationListItem.AsObject> = [
|
||||||
return [
|
{
|
||||||
{
|
title: "Name",
|
||||||
title: "Name",
|
dataIndex: "name",
|
||||||
dataIndex: "name",
|
key: "name",
|
||||||
key: "name",
|
width: 250,
|
||||||
width: 250,
|
render: (text, record) => <Link to={`/tenants/${props.tenant.getId()}/applications/${record.id}`}>{text}</Link>,
|
||||||
render: (text, record) => (
|
},
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}/applications/${record.id}`}>{text}</Link>
|
{
|
||||||
),
|
title: "Description",
|
||||||
},
|
dataIndex: "description",
|
||||||
{
|
key: "description",
|
||||||
title: "Description",
|
},
|
||||||
dataIndex: "description",
|
];
|
||||||
key: "description",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||||
let req = new ListApplicationsRequest();
|
let req = new ListApplicationsRequest();
|
||||||
req.setTenantId(this.props.tenant.getId());
|
req.setTenantId(props.tenant.getId());
|
||||||
req.setLimit(limit);
|
req.setLimit(limit);
|
||||||
req.setOffset(offset);
|
req.setOffset(offset);
|
||||||
|
|
||||||
@ -51,38 +47,36 @@ class ListApplications extends Component<IProps> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
<PageHeader
|
||||||
<PageHeader
|
breadcrumbRender={() => (
|
||||||
breadcrumbRender={() => (
|
<Breadcrumb>
|
||||||
<Breadcrumb>
|
<Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<span>Tenants</span>
|
||||||
<span>Tenants</span>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<span>
|
||||||
<span>
|
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
</span>
|
||||||
</span>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<span>Applications</span>
|
||||||
<span>Applications</span>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb>
|
||||||
</Breadcrumb>
|
)}
|
||||||
)}
|
title="Applications"
|
||||||
title="Applications"
|
extra={[
|
||||||
extra={[
|
<Admin tenantId={props.tenant.getId()} isDeviceAdmin>
|
||||||
<Admin tenantId={this.props.tenant.getId()} isDeviceAdmin>
|
<Button type="primary">
|
||||||
<Button type="primary">
|
<Link to={`/tenants/${props.tenant.getId()}/applications/create`}>Add application</Link>
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}/applications/create`}>Add application</Link>
|
</Button>
|
||||||
</Button>
|
</Admin>,
|
||||||
</Admin>,
|
]}
|
||||||
]}
|
/>
|
||||||
/>
|
<DataTable columns={columns} getPage={getPage} rowKey="id" />
|
||||||
<DataTable columns={this.columns()} getPage={this.getPage} rowKey="id" />
|
</Space>
|
||||||
</Space>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ListApplications;
|
export default ListApplications;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { Row } from "antd";
|
import { Row } from "antd";
|
||||||
|
|
||||||
@ -27,32 +27,22 @@ interface IProps {
|
|||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function ListIntegrations(props: IProps) {
|
||||||
configured: any[];
|
const [configured, setConfigured] = useState<any[]>([]);
|
||||||
available: any[];
|
const [available, setAvailable] = useState<any[]>([]);
|
||||||
}
|
|
||||||
|
|
||||||
class ListIntegrations extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
ApplicationStore.on("integration.delete", loadIntegrations);
|
||||||
super(props);
|
loadIntegrations();
|
||||||
this.state = {
|
|
||||||
configured: [],
|
return () => {
|
||||||
available: [],
|
ApplicationStore.removeAllListeners("integration.delete");
|
||||||
};
|
};
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
componentDidMount() {
|
const loadIntegrations = () => {
|
||||||
ApplicationStore.on("integration.delete", this.loadIntegrations);
|
|
||||||
this.loadIntegrations();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
ApplicationStore.removeAllListeners("integration.delete");
|
|
||||||
}
|
|
||||||
|
|
||||||
loadIntegrations = () => {
|
|
||||||
let req = new ListIntegrationsRequest();
|
let req = new ListIntegrationsRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
ApplicationStore.listIntegrations(req, (resp: ListIntegrationsResponse) => {
|
ApplicationStore.listIntegrations(req, (resp: ListIntegrationsResponse) => {
|
||||||
let configured: any[] = [];
|
let configured: any[] = [];
|
||||||
@ -70,94 +60,90 @@ class ListIntegrations extends Component<IProps, IState> {
|
|||||||
|
|
||||||
// AWS SNS
|
// AWS SNS
|
||||||
if (includes(resp.getResultList(), IntegrationKind.AWS_SNS)) {
|
if (includes(resp.getResultList(), IntegrationKind.AWS_SNS)) {
|
||||||
configured.push(<AwsSnsCard application={this.props.application} />);
|
configured.push(<AwsSnsCard application={props.application} />);
|
||||||
} else {
|
} else {
|
||||||
available.push(<AwsSnsCard application={this.props.application} add />);
|
available.push(<AwsSnsCard application={props.application} add />);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Azure Service-Bus
|
// Azure Service-Bus
|
||||||
if (includes(resp.getResultList(), IntegrationKind.AZURE_SERVICE_BUS)) {
|
if (includes(resp.getResultList(), IntegrationKind.AZURE_SERVICE_BUS)) {
|
||||||
configured.push(<AzureServiceBusCard application={this.props.application} />);
|
configured.push(<AzureServiceBusCard application={props.application} />);
|
||||||
} else {
|
} else {
|
||||||
available.push(<AzureServiceBusCard application={this.props.application} add />);
|
available.push(<AzureServiceBusCard application={props.application} add />);
|
||||||
}
|
}
|
||||||
|
|
||||||
// GCP Pub/Sub
|
// GCP Pub/Sub
|
||||||
if (includes(resp.getResultList(), IntegrationKind.GCP_PUB_SUB)) {
|
if (includes(resp.getResultList(), IntegrationKind.GCP_PUB_SUB)) {
|
||||||
configured.push(<GcpPubSubCard application={this.props.application} />);
|
configured.push(<GcpPubSubCard application={props.application} />);
|
||||||
} else {
|
} else {
|
||||||
available.push(<GcpPubSubCard application={this.props.application} add />);
|
available.push(<GcpPubSubCard application={props.application} add />);
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP
|
// HTTP
|
||||||
if (includes(resp.getResultList(), IntegrationKind.HTTP)) {
|
if (includes(resp.getResultList(), IntegrationKind.HTTP)) {
|
||||||
configured.push(<HttpCard application={this.props.application} />);
|
configured.push(<HttpCard application={props.application} />);
|
||||||
} else {
|
} else {
|
||||||
available.push(<HttpCard application={this.props.application} add />);
|
available.push(<HttpCard application={props.application} add />);
|
||||||
}
|
}
|
||||||
|
|
||||||
// IFTTT
|
// IFTTT
|
||||||
if (includes(resp.getResultList(), IntegrationKind.IFTTT)) {
|
if (includes(resp.getResultList(), IntegrationKind.IFTTT)) {
|
||||||
configured.push(<IftttCard application={this.props.application} />);
|
configured.push(<IftttCard application={props.application} />);
|
||||||
} else {
|
} else {
|
||||||
available.push(<IftttCard application={this.props.application} add />);
|
available.push(<IftttCard application={props.application} add />);
|
||||||
}
|
}
|
||||||
|
|
||||||
// InfluxDB
|
// InfluxDB
|
||||||
if (includes(resp.getResultList(), IntegrationKind.INFLUX_DB)) {
|
if (includes(resp.getResultList(), IntegrationKind.INFLUX_DB)) {
|
||||||
configured.push(<InfluxdbCard application={this.props.application} />);
|
configured.push(<InfluxdbCard application={props.application} />);
|
||||||
} else {
|
} else {
|
||||||
available.push(<InfluxdbCard application={this.props.application} add />);
|
available.push(<InfluxdbCard application={props.application} add />);
|
||||||
}
|
}
|
||||||
|
|
||||||
// MQTT
|
// MQTT
|
||||||
if (includes(resp.getResultList(), IntegrationKind.MQTT_GLOBAL)) {
|
if (includes(resp.getResultList(), IntegrationKind.MQTT_GLOBAL)) {
|
||||||
configured.push(<MqttCard application={this.props.application} />);
|
configured.push(<MqttCard application={props.application} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
// myDevices
|
// myDevices
|
||||||
if (includes(resp.getResultList(), IntegrationKind.MY_DEVICES)) {
|
if (includes(resp.getResultList(), IntegrationKind.MY_DEVICES)) {
|
||||||
configured.push(<MyDevicesCard application={this.props.application} />);
|
configured.push(<MyDevicesCard application={props.application} />);
|
||||||
} else {
|
} else {
|
||||||
available.push(<MyDevicesCard application={this.props.application} add />);
|
available.push(<MyDevicesCard application={props.application} add />);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pilot Things
|
// Pilot Things
|
||||||
if (includes(resp.getResultList(), IntegrationKind.PILOT_THINGS)) {
|
if (includes(resp.getResultList(), IntegrationKind.PILOT_THINGS)) {
|
||||||
configured.push(<PilotThingsCard application={this.props.application} />);
|
configured.push(<PilotThingsCard application={props.application} />);
|
||||||
} else {
|
} else {
|
||||||
available.push(<PilotThingsCard application={this.props.application} add />);
|
available.push(<PilotThingsCard application={props.application} add />);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Semtech LoRa Cloud
|
// Semtech LoRa Cloud
|
||||||
if (includes(resp.getResultList(), IntegrationKind.LORA_CLOUD)) {
|
if (includes(resp.getResultList(), IntegrationKind.LORA_CLOUD)) {
|
||||||
configured.push(<LoRaCloudCard application={this.props.application} />);
|
configured.push(<LoRaCloudCard application={props.application} />);
|
||||||
} else {
|
} else {
|
||||||
available.push(<LoRaCloudCard application={this.props.application} add />);
|
available.push(<LoRaCloudCard application={props.application} add />);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ThingsBoard
|
// ThingsBoard
|
||||||
if (includes(resp.getResultList(), IntegrationKind.THINGS_BOARD)) {
|
if (includes(resp.getResultList(), IntegrationKind.THINGS_BOARD)) {
|
||||||
configured.push(<ThingsBoardCard application={this.props.application} />);
|
configured.push(<ThingsBoardCard application={props.application} />);
|
||||||
} else {
|
} else {
|
||||||
available.push(<ThingsBoardCard application={this.props.application} add />);
|
available.push(<ThingsBoardCard application={props.application} add />);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
setConfigured(configured);
|
||||||
configured: configured,
|
setAvailable(available);
|
||||||
available: available,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Row gutter={24}>
|
||||||
<Row gutter={24}>
|
{configured}
|
||||||
{this.state.configured}
|
{available}
|
||||||
{this.state.available}
|
</Row>
|
||||||
</Row>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ListIntegrations;
|
export default ListIntegrations;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Col, Card, Popconfirm } from "antd";
|
import { Col, Card, Popconfirm } from "antd";
|
||||||
@ -13,47 +12,45 @@ interface IProps {
|
|||||||
add?: boolean;
|
add?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AwsSns extends Component<IProps> {
|
function AwsSns(props: IProps) {
|
||||||
onDelete = () => {
|
const onDelete = () => {
|
||||||
let req = new DeleteAwsSnsIntegrationRequest();
|
let req = new DeleteAwsSnsIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
ApplicationStore.deleteAwsSnsIntegration(req, () => {});
|
ApplicationStore.deleteAwsSnsIntegration(req, () => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
let actions: any[] = [];
|
||||||
let actions: any[] = [];
|
|
||||||
|
|
||||||
if (!!this.props.add) {
|
if (!!props.add) {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/aws-sns/create">
|
<Link to="aws-sns/create">
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/aws-sns/edit">
|
<Link to="aws-sns/edit">
|
||||||
<EditOutlined />
|
<EditOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
</Popconfirm>,
|
</Popconfirm>,
|
||||||
];
|
];
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col span={8}>
|
|
||||||
<Card
|
|
||||||
title="AWS SNS"
|
|
||||||
className="integration-card"
|
|
||||||
cover={<img alt="AWS SNS" src="/integrations/aws_sns.png" style={{ padding: 1 }} />}
|
|
||||||
actions={actions}
|
|
||||||
>
|
|
||||||
<Card.Meta description="The AWS SNS integration forwards events to an AWS SNS topic." />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col span={8}>
|
||||||
|
<Card
|
||||||
|
title="AWS SNS"
|
||||||
|
className="integration-card"
|
||||||
|
cover={<img alt="AWS SNS" src="/integrations/aws_sns.png" style={{ padding: 1 }} />}
|
||||||
|
actions={actions}
|
||||||
|
>
|
||||||
|
<Card.Meta description="The AWS SNS integration forwards events to an AWS SNS topic." />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AwsSns;
|
export default AwsSns;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
|
|
||||||
import { Form, Input, Button, Select } from "antd";
|
import { Form, Input, Button, Select } from "antd";
|
||||||
|
|
||||||
import { AwsSnsIntegration, Encoding } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
import { AwsSnsIntegration, Encoding } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||||
@ -9,9 +7,9 @@ interface IProps {
|
|||||||
onFinish: (obj: AwsSnsIntegration) => void;
|
onFinish: (obj: AwsSnsIntegration) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AwsSnsIntegrationForm extends Component<IProps> {
|
function AwsSnsIntegrationForm(props: IProps) {
|
||||||
onFinish = (values: AwsSnsIntegration.AsObject) => {
|
const onFinish = (values: AwsSnsIntegration.AsObject) => {
|
||||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
const v = Object.assign(props.initialValues.toObject(), values);
|
||||||
let i = new AwsSnsIntegration();
|
let i = new AwsSnsIntegration();
|
||||||
|
|
||||||
i.setApplicationId(v.applicationId);
|
i.setApplicationId(v.applicationId);
|
||||||
@ -21,54 +19,52 @@ class AwsSnsIntegrationForm extends Component<IProps> {
|
|||||||
i.setSecretAccessKey(v.secretAccessKey);
|
i.setSecretAccessKey(v.secretAccessKey);
|
||||||
i.setTopicArn(v.topicArn);
|
i.setTopicArn(v.topicArn);
|
||||||
|
|
||||||
this.props.onFinish(i);
|
props.onFinish(i);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
<Form.Item
|
||||||
<Form.Item
|
label="Payload encoding"
|
||||||
label="Payload encoding"
|
name="encoding"
|
||||||
name="encoding"
|
rules={[{ required: true, message: "Please select an encoding!" }]}
|
||||||
rules={[{ required: true, message: "Please select an encoding!" }]}
|
>
|
||||||
>
|
<Select>
|
||||||
<Select>
|
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
|
||||||
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
|
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
|
||||||
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
|
</Select>
|
||||||
</Select>
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item label="AWS region" name="region" rules={[{ required: true, message: "Please enter a region!" }]}>
|
||||||
<Form.Item label="AWS region" name="region" rules={[{ required: true, message: "Please enter a region!" }]}>
|
<Input />
|
||||||
<Input />
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
<Form.Item
|
label="AWS Access Key ID"
|
||||||
label="AWS Access Key ID"
|
name="accessKeyId"
|
||||||
name="accessKeyId"
|
rules={[{ required: true, message: "Please enter an Access Key ID!" }]}
|
||||||
rules={[{ required: true, message: "Please enter an Access Key ID!" }]}
|
>
|
||||||
>
|
<Input />
|
||||||
<Input />
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
<Form.Item
|
label="AWS Secret Access Key"
|
||||||
label="AWS Secret Access Key"
|
name="secretAccessKey"
|
||||||
name="secretAccessKey"
|
rules={[{ required: true, message: "Please enter a Secret Access Key!" }]}
|
||||||
rules={[{ required: true, message: "Please enter a Secret Access Key!" }]}
|
>
|
||||||
>
|
<Input />
|
||||||
<Input />
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
<Form.Item
|
label="AWS SNS topic ARN"
|
||||||
label="AWS SNS topic ARN"
|
name="topicArn"
|
||||||
name="topicArn"
|
rules={[{ required: true, message: "Please enter a SNS topic ARN!" }]}
|
||||||
rules={[{ required: true, message: "Please enter a SNS topic ARN!" }]}
|
>
|
||||||
>
|
<Input />
|
||||||
<Input />
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item>
|
||||||
<Form.Item>
|
<Button type="primary" htmlType="submit">
|
||||||
<Button type="primary" htmlType="submit">
|
Submit
|
||||||
Submit
|
</Button>
|
||||||
</Button>
|
</Form.Item>
|
||||||
</Form.Item>
|
</Form>
|
||||||
</Form>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AwsSnsIntegrationForm;
|
export default AwsSnsIntegrationForm;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Col, Card, Popconfirm } from "antd";
|
import { Col, Card, Popconfirm } from "antd";
|
||||||
@ -16,46 +15,44 @@ interface IProps {
|
|||||||
add?: boolean;
|
add?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AzureServiceBusCard extends Component<IProps> {
|
function AzureServiceBusCard(props: IProps) {
|
||||||
onDelete = () => {
|
const onDelete = () => {
|
||||||
let req = new DeleteAzureServiceBusIntegrationRequest();
|
let req = new DeleteAzureServiceBusIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
ApplicationStore.deleteAzureServiceBusIntegration(req, () => {});
|
ApplicationStore.deleteAzureServiceBusIntegration(req, () => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
let actions: any[] = [];
|
||||||
let actions: any[] = [];
|
|
||||||
|
|
||||||
if (!!this.props.add) {
|
if (!!props.add) {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/azure-service-bus/create">
|
<Link to="azure-service-bus/create">
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/azure-service-bus/edit">
|
<Link to="azure-service-bus/edit">
|
||||||
<EditOutlined />
|
<EditOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
</Popconfirm>,
|
</Popconfirm>,
|
||||||
];
|
];
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col span={8}>
|
|
||||||
<Card
|
|
||||||
title="Azure Service-Bus"
|
|
||||||
className="integration-card"
|
|
||||||
cover={<img alt="Azure Service-Bus" src="/integrations/azure_service_bus.png" style={{ padding: 1 }} />}
|
|
||||||
actions={actions}
|
|
||||||
>
|
|
||||||
<Card.Meta description="The Azure Service-Bus integration forwards events to an Azure Service-Bus topic or queue." />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col span={8}>
|
||||||
|
<Card
|
||||||
|
title="Azure Service-Bus"
|
||||||
|
className="integration-card"
|
||||||
|
cover={<img alt="Azure Service-Bus" src="/integrations/azure_service_bus.png" style={{ padding: 1 }} />}
|
||||||
|
actions={actions}
|
||||||
|
>
|
||||||
|
<Card.Meta description="The Azure Service-Bus integration forwards events to an Azure Service-Bus topic or queue." />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AzureServiceBusCard;
|
export default AzureServiceBusCard;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
|
|
||||||
import { Form, Input, Button, Select } from "antd";
|
import { Form, Input, Button, Select } from "antd";
|
||||||
|
|
||||||
import { AzureServiceBusIntegration, Encoding } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
import { AzureServiceBusIntegration, Encoding } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||||
@ -9,9 +7,9 @@ interface IProps {
|
|||||||
onFinish: (obj: AzureServiceBusIntegration) => void;
|
onFinish: (obj: AzureServiceBusIntegration) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AzureServiceBusIntegrationForm extends Component<IProps> {
|
function AzureServiceBusIntegrationForm(props: IProps) {
|
||||||
onFinish = (values: AzureServiceBusIntegration.AsObject) => {
|
const onFinish = (values: AzureServiceBusIntegration.AsObject) => {
|
||||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
const v = Object.assign(props.initialValues.toObject(), values);
|
||||||
let i = new AzureServiceBusIntegration();
|
let i = new AzureServiceBusIntegration();
|
||||||
|
|
||||||
i.setApplicationId(v.applicationId);
|
i.setApplicationId(v.applicationId);
|
||||||
@ -19,45 +17,53 @@ class AzureServiceBusIntegrationForm extends Component<IProps> {
|
|||||||
i.setConnectionString(v.connectionString);
|
i.setConnectionString(v.connectionString);
|
||||||
i.setPublishName(v.publishName);
|
i.setPublishName(v.publishName);
|
||||||
|
|
||||||
this.props.onFinish(i);
|
props.onFinish(i);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
<Form.Item
|
||||||
<Form.Item
|
label="Payload encoding"
|
||||||
label="Payload encoding"
|
name="encoding"
|
||||||
name="encoding"
|
rules={[{ required: true, message: "Please select an encoding!" }]}
|
||||||
rules={[{ required: true, message: "Please select an encoding!" }]}
|
>
|
||||||
>
|
<Select>
|
||||||
<Select>
|
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
|
||||||
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
|
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
|
||||||
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
|
</Select>
|
||||||
</Select>
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
<Form.Item
|
label="Azure Service-Bus connection string"
|
||||||
label="Azure Service-Bus connection string"
|
name="connectionString"
|
||||||
name="connectionString"
|
tooltip="This string can be obtained after creating a 'Shared access policy' with 'Send' permission."
|
||||||
tooltip="This string can be obtained after creating a 'Shared access policy' with 'Send' permission."
|
rules={[
|
||||||
rules={[{ required: true, message: "Please enter an Azure Service-Bus connection string!" }]}
|
{
|
||||||
>
|
required: true,
|
||||||
<Input />
|
message: "Please enter an Azure Service-Bus connection string!",
|
||||||
</Form.Item>
|
},
|
||||||
<Form.Item
|
]}
|
||||||
label="Azure Service-Bus topic / queue name"
|
>
|
||||||
name="publishName"
|
<Input />
|
||||||
rules={[{ required: true, message: "Please enter an Azure Service-Bus topic / queue name!" }]}
|
</Form.Item>
|
||||||
>
|
<Form.Item
|
||||||
<Input />
|
label="Azure Service-Bus topic / queue name"
|
||||||
</Form.Item>
|
name="publishName"
|
||||||
<Form.Item>
|
rules={[
|
||||||
<Button type="primary" htmlType="submit">
|
{
|
||||||
Submit
|
required: true,
|
||||||
</Button>
|
message: "Please enter an Azure Service-Bus topic / queue name!",
|
||||||
</Form.Item>
|
},
|
||||||
</Form>
|
]}
|
||||||
);
|
>
|
||||||
}
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AzureServiceBusIntegrationForm;
|
export default AzureServiceBusIntegrationForm;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -12,33 +11,31 @@ import {
|
|||||||
import AwsSnsIntegrationForm from "./AwsSnsIntegrationForm";
|
import AwsSnsIntegrationForm from "./AwsSnsIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateAwsSnsIntegration extends Component<IProps> {
|
function CreateAwsSnsIntegration(props: IProps) {
|
||||||
onFinish = (obj: AwsSnsIntegration) => {
|
const navigate = useNavigate();
|
||||||
obj.setApplicationId(this.props.application.getId());
|
|
||||||
|
const onFinish = (obj: AwsSnsIntegration) => {
|
||||||
|
obj.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
let req = new CreateAwsSnsIntegrationRequest();
|
let req = new CreateAwsSnsIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.createAwsSnsIntegration(req, () => {
|
ApplicationStore.createAwsSnsIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const i = new AwsSnsIntegration();
|
||||||
const i = new AwsSnsIntegration();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title="Add AWS SNS integration">
|
<Card title="Add AWS SNS integration">
|
||||||
<AwsSnsIntegrationForm initialValues={i} onFinish={this.onFinish} />
|
<AwsSnsIntegrationForm initialValues={i} onFinish={onFinish} />
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateAwsSnsIntegration;
|
export default CreateAwsSnsIntegration;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -12,33 +11,31 @@ import {
|
|||||||
import AzureServiceBusIntegrationForm from "./AzureServiceBusIntegrationForm";
|
import AzureServiceBusIntegrationForm from "./AzureServiceBusIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateAzureServiceBusIntegration extends Component<IProps> {
|
function CreateAzureServiceBusIntegration(props: IProps) {
|
||||||
onFinish = (obj: AzureServiceBusIntegration) => {
|
const navigate = useNavigate();
|
||||||
obj.setApplicationId(this.props.application.getId());
|
|
||||||
|
const onFinish = (obj: AzureServiceBusIntegration) => {
|
||||||
|
obj.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
let req = new CreateAzureServiceBusIntegrationRequest();
|
let req = new CreateAzureServiceBusIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.createAzureServiceBusIntegration(req, () => {
|
ApplicationStore.createAzureServiceBusIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const i = new AzureServiceBusIntegration();
|
||||||
const i = new AzureServiceBusIntegration();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title="Add Azure Service-Bus integration">
|
<Card title="Add Azure Service-Bus integration">
|
||||||
<AzureServiceBusIntegrationForm initialValues={i} onFinish={this.onFinish} />
|
<AzureServiceBusIntegrationForm initialValues={i} onFinish={onFinish} />
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateAzureServiceBusIntegration;
|
export default CreateAzureServiceBusIntegration;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -12,33 +11,31 @@ import {
|
|||||||
import GcpPubSubIntegrationForm from "./GcpPubSubIntegrationForm";
|
import GcpPubSubIntegrationForm from "./GcpPubSubIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateGcpPubSubIntegration extends Component<IProps> {
|
function CreateGcpPubSubIntegration(props: IProps) {
|
||||||
onFinish = (obj: GcpPubSubIntegration) => {
|
const navigate = useNavigate();
|
||||||
obj.setApplicationId(this.props.application.getId());
|
|
||||||
|
const onFinish = (obj: GcpPubSubIntegration) => {
|
||||||
|
obj.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
let req = new CreateGcpPubSubIntegrationRequest();
|
let req = new CreateGcpPubSubIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.createGcpPubSubIntegration(req, () => {
|
ApplicationStore.createGcpPubSubIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const i = new GcpPubSubIntegration();
|
||||||
const i = new GcpPubSubIntegration();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title="Add GCP Pub/Sub integration">
|
<Card title="Add GCP Pub/Sub integration">
|
||||||
<GcpPubSubIntegrationForm initialValues={i} onFinish={this.onFinish} />
|
<GcpPubSubIntegrationForm initialValues={i} onFinish={onFinish} />
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateGcpPubSubIntegration;
|
export default CreateGcpPubSubIntegration;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -12,33 +11,31 @@ import {
|
|||||||
import HttpIntegrationForm from "./HttpIntegrationForm";
|
import HttpIntegrationForm from "./HttpIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateHttpIntegration extends Component<IProps> {
|
function CreateHttpIntegration(props: IProps) {
|
||||||
onFinish = (obj: HttpIntegration) => {
|
const navigate = useNavigate();
|
||||||
obj.setApplicationId(this.props.application.getId());
|
|
||||||
|
const onFinish = (obj: HttpIntegration) => {
|
||||||
|
obj.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
let req = new CreateHttpIntegrationRequest();
|
let req = new CreateHttpIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.createHttpIntegration(req, () => {
|
ApplicationStore.createHttpIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const i = new HttpIntegration();
|
||||||
const i = new HttpIntegration();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title="Add HTTP integration">
|
<Card title="Add HTTP integration">
|
||||||
<HttpIntegrationForm initialValues={i} onFinish={this.onFinish} />
|
<HttpIntegrationForm initialValues={i} onFinish={onFinish} />
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateHttpIntegration;
|
export default CreateHttpIntegration;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -12,35 +11,33 @@ import {
|
|||||||
import IftttIntegrationForm from "./IftttIntegrationForm";
|
import IftttIntegrationForm from "./IftttIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
measurementKeys: string[];
|
measurementKeys: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateIftttIntegration extends Component<IProps> {
|
function CreateIftttIntegration(props: IProps) {
|
||||||
onFinish = (obj: IftttIntegration) => {
|
const navigate = useNavigate();
|
||||||
obj.setApplicationId(this.props.application.getId());
|
|
||||||
|
const onFinish = (obj: IftttIntegration) => {
|
||||||
|
obj.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
let req = new CreateIftttIntegrationRequest();
|
let req = new CreateIftttIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.createIftttIntegration(req, () => {
|
ApplicationStore.createIftttIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const i = new IftttIntegration();
|
||||||
const i = new IftttIntegration();
|
i.setUplinkValuesList(["", ""]);
|
||||||
i.setUplinkValuesList(["", ""]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title="Add IFTTT integration">
|
<Card title="Add IFTTT integration">
|
||||||
<IftttIntegrationForm measurementKeys={this.props.measurementKeys} initialValues={i} onFinish={this.onFinish} />
|
<IftttIntegrationForm measurementKeys={props.measurementKeys} initialValues={i} onFinish={onFinish} />
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateIftttIntegration;
|
export default CreateIftttIntegration;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -12,33 +11,31 @@ import {
|
|||||||
import InfluxDbIntegrationForm from "./InfluxDbIntegrationForm";
|
import InfluxDbIntegrationForm from "./InfluxDbIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateInfluxDbIntegration extends Component<IProps> {
|
function CreateInfluxDbIntegration(props: IProps) {
|
||||||
onFinish = (obj: InfluxDbIntegration) => {
|
const navigate = useNavigate();
|
||||||
obj.setApplicationId(this.props.application.getId());
|
|
||||||
|
const onFinish = (obj: InfluxDbIntegration) => {
|
||||||
|
obj.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
let req = new CreateInfluxDbIntegrationRequest();
|
let req = new CreateInfluxDbIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.createInfluxDbIntegration(req, () => {
|
ApplicationStore.createInfluxDbIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const i = new InfluxDbIntegration();
|
||||||
const i = new InfluxDbIntegration();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title="Add InfluxDB integration">
|
<Card title="Add InfluxDB integration">
|
||||||
<InfluxDbIntegrationForm initialValues={i} onFinish={this.onFinish} />
|
<InfluxDbIntegrationForm initialValues={i} onFinish={onFinish} />
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateInfluxDbIntegration;
|
export default CreateInfluxDbIntegration;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -13,38 +12,36 @@ import {
|
|||||||
import LoRaCloudIntegrationForm from "./LoRaCloudIntegrationForm";
|
import LoRaCloudIntegrationForm from "./LoRaCloudIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateLoRaCloudIntegration extends Component<IProps> {
|
function CreateLoRaCloudIntegration(props: IProps) {
|
||||||
onFinish = (obj: LoraCloudIntegration) => {
|
const navigate = useNavigate();
|
||||||
obj.setApplicationId(this.props.application.getId());
|
|
||||||
|
const onFinish = (obj: LoraCloudIntegration) => {
|
||||||
|
obj.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
let req = new CreateLoraCloudIntegrationRequest();
|
let req = new CreateLoraCloudIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.createLoraCloudIntegration(req, () => {
|
ApplicationStore.createLoraCloudIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
let i = new LoraCloudIntegration();
|
||||||
let i = new LoraCloudIntegration();
|
let mgs = new LoraCloudModemGeolocationServices();
|
||||||
let mgs = new LoraCloudModemGeolocationServices();
|
mgs.setModemEnabled(true);
|
||||||
mgs.setModemEnabled(true);
|
mgs.setForwardFPortsList([192, 197, 198, 199]);
|
||||||
mgs.setForwardFPortsList([192, 197, 198, 199]);
|
|
||||||
|
|
||||||
i.setModemGeolocationServices(mgs);
|
i.setModemGeolocationServices(mgs);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title="Add Semtech LoRa Cloud™ integration">
|
<Card title="Add Semtech LoRa Cloud™ integration">
|
||||||
<LoRaCloudIntegrationForm initialValues={i} onFinish={this.onFinish} />
|
<LoRaCloudIntegrationForm initialValues={i} onFinish={onFinish} />
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateLoRaCloudIntegration;
|
export default CreateLoRaCloudIntegration;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -12,33 +11,31 @@ import {
|
|||||||
import MyDevicesIntegrationForm from "./MyDevicesIntegrationForm";
|
import MyDevicesIntegrationForm from "./MyDevicesIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateMyDevicesIntegration extends Component<IProps> {
|
function CreateMyDevicesIntegration(props: IProps) {
|
||||||
onFinish = (obj: MyDevicesIntegration) => {
|
const navigate = useNavigate();
|
||||||
obj.setApplicationId(this.props.application.getId());
|
|
||||||
|
const onFinish = (obj: MyDevicesIntegration) => {
|
||||||
|
obj.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
let req = new CreateMyDevicesIntegrationRequest();
|
let req = new CreateMyDevicesIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.createMyDevicesIntegration(req, () => {
|
ApplicationStore.createMyDevicesIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const i = new MyDevicesIntegration();
|
||||||
const i = new MyDevicesIntegration();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title="Add myDevices integration">
|
<Card title="Add myDevices integration">
|
||||||
<MyDevicesIntegrationForm initialValues={i} onFinish={this.onFinish} />
|
<MyDevicesIntegrationForm initialValues={i} onFinish={onFinish} />
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateMyDevicesIntegration;
|
export default CreateMyDevicesIntegration;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -12,33 +11,31 @@ import {
|
|||||||
import PilotThingsIntegrationForm from "./PilotThingsIntegrationForm";
|
import PilotThingsIntegrationForm from "./PilotThingsIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreatePilotThingsIntegration extends Component<IProps> {
|
function CreatePilotThingsIntegration(props: IProps) {
|
||||||
onFinish = (obj: PilotThingsIntegration) => {
|
const navigate = useNavigate();
|
||||||
obj.setApplicationId(this.props.application.getId());
|
|
||||||
|
const onFinish = (obj: PilotThingsIntegration) => {
|
||||||
|
obj.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
let req = new CreatePilotThingsIntegrationRequest();
|
let req = new CreatePilotThingsIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.createPilotThingsIntegration(req, () => {
|
ApplicationStore.createPilotThingsIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const i = new PilotThingsIntegration();
|
||||||
const i = new PilotThingsIntegration();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title="Add Pilot Things integration">
|
<Card title="Add Pilot Things integration">
|
||||||
<PilotThingsIntegrationForm initialValues={i} onFinish={this.onFinish} />
|
<PilotThingsIntegrationForm initialValues={i} onFinish={onFinish} />
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreatePilotThingsIntegration;
|
export default CreatePilotThingsIntegration;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -12,33 +11,31 @@ import {
|
|||||||
import ThingsBoardIntegrationForm from "./ThingsBoardIntegrationForm";
|
import ThingsBoardIntegrationForm from "./ThingsBoardIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateThingsBoardIntegration extends Component<IProps> {
|
function CreateThingsBoardIntegration(props: IProps) {
|
||||||
onFinish = (obj: ThingsBoardIntegration) => {
|
const navigate = useNavigate();
|
||||||
obj.setApplicationId(this.props.application.getId());
|
|
||||||
|
const onFinish = (obj: ThingsBoardIntegration) => {
|
||||||
|
obj.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
let req = new CreateThingsBoardIntegrationRequest();
|
let req = new CreateThingsBoardIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.createThingsBoardIntegration(req, () => {
|
ApplicationStore.createThingsBoardIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const i = new ThingsBoardIntegration();
|
||||||
const i = new ThingsBoardIntegration();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title="Add ThingsBoard integration">
|
<Card title="Add ThingsBoard integration">
|
||||||
<ThingsBoardIntegrationForm initialValues={i} onFinish={this.onFinish} />
|
<ThingsBoardIntegrationForm initialValues={i} onFinish={onFinish} />
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateThingsBoardIntegration;
|
export default CreateThingsBoardIntegration;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -14,53 +14,41 @@ import {
|
|||||||
import AwsSnsIntegrationForm from "./AwsSnsIntegrationForm";
|
import AwsSnsIntegrationForm from "./AwsSnsIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function EditAwsSnsIntegration(props: IProps) {
|
||||||
integration?: AwsSnsIntegration;
|
const navigate = useNavigate();
|
||||||
}
|
const [integration, setIntegration] = useState<AwsSnsIntegration | undefined>(undefined);
|
||||||
|
|
||||||
class EditAwsSnsIntegration extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
let req = new GetAwsSnsIntegrationRequest();
|
let req = new GetAwsSnsIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
ApplicationStore.getAwsSnsIntegration(req, (resp: GetAwsSnsIntegrationResponse) => {
|
ApplicationStore.getAwsSnsIntegration(req, (resp: GetAwsSnsIntegrationResponse) => {
|
||||||
this.setState({
|
setIntegration(resp.getIntegration());
|
||||||
integration: resp.getIntegration(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}, [props]);
|
||||||
|
|
||||||
onFinish = (obj: AwsSnsIntegration) => {
|
const onFinish = (obj: AwsSnsIntegration) => {
|
||||||
let req = new UpdateAwsSnsIntegrationRequest();
|
let req = new UpdateAwsSnsIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.updateAwsSnsIntegration(req, () => {
|
ApplicationStore.updateAwsSnsIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
if (integration === undefined) {
|
||||||
if (this.state.integration === undefined) {
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title="Update AWS SNS integration">
|
|
||||||
<AwsSnsIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="Update AWS SNS integration">
|
||||||
|
<AwsSnsIntegrationForm initialValues={integration} onFinish={onFinish} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditAwsSnsIntegration;
|
export default EditAwsSnsIntegration;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -14,53 +14,41 @@ import {
|
|||||||
import AzureServiceBusIntegrationForm from "./AzureServiceBusIntegrationForm";
|
import AzureServiceBusIntegrationForm from "./AzureServiceBusIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function EditAzureServiceBusIntegration(props: IProps) {
|
||||||
integration?: AzureServiceBusIntegration;
|
const navigate = useNavigate();
|
||||||
}
|
const [integration, setIntegration] = useState<AzureServiceBusIntegration | undefined>(undefined);
|
||||||
|
|
||||||
class EditAzureServiceBusIntegration extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
let req = new GetAzureServiceBusIntegrationRequest();
|
let req = new GetAzureServiceBusIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
ApplicationStore.getAzureServiceBusIntegration(req, (resp: GetAzureServiceBusIntegrationResponse) => {
|
ApplicationStore.getAzureServiceBusIntegration(req, (resp: GetAzureServiceBusIntegrationResponse) => {
|
||||||
this.setState({
|
setIntegration(resp.getIntegration());
|
||||||
integration: resp.getIntegration(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}, [props]);
|
||||||
|
|
||||||
onFinish = (obj: AzureServiceBusIntegration) => {
|
const onFinish = (obj: AzureServiceBusIntegration) => {
|
||||||
let req = new UpdateAzureServiceBusIntegrationRequest();
|
let req = new UpdateAzureServiceBusIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.updateAzureServiceBusIntegration(req, () => {
|
ApplicationStore.updateAzureServiceBusIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
if (integration === undefined) {
|
||||||
if (this.state.integration === undefined) {
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title="Update Azure Service-Bus integration">
|
|
||||||
<AzureServiceBusIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="Update Azure Service-Bus integration">
|
||||||
|
<AzureServiceBusIntegrationForm initialValues={integration} onFinish={onFinish} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditAzureServiceBusIntegration;
|
export default EditAzureServiceBusIntegration;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -14,53 +14,41 @@ import {
|
|||||||
import GcpPubSubIntegrationForm from "./GcpPubSubIntegrationForm";
|
import GcpPubSubIntegrationForm from "./GcpPubSubIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function EditGcpPubSubIntegration(props: IProps) {
|
||||||
integration?: GcpPubSubIntegration;
|
const navigate = useNavigate();
|
||||||
}
|
const [integration, setIntegration] = useState<GcpPubSubIntegration | undefined>(undefined);
|
||||||
|
|
||||||
class EditGcpPubSubIntegration extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
let req = new GetGcpPubSubIntegrationRequest();
|
let req = new GetGcpPubSubIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
ApplicationStore.getGcpPubSubIntegration(req, (resp: GetGcpPubSubIntegrationResponse) => {
|
ApplicationStore.getGcpPubSubIntegration(req, (resp: GetGcpPubSubIntegrationResponse) => {
|
||||||
this.setState({
|
setIntegration(resp.getIntegration());
|
||||||
integration: resp.getIntegration(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}, [props]);
|
||||||
|
|
||||||
onFinish = (obj: GcpPubSubIntegration) => {
|
const onFinish = (obj: GcpPubSubIntegration) => {
|
||||||
let req = new UpdateGcpPubSubIntegrationRequest();
|
let req = new UpdateGcpPubSubIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.updateGcpPubSubIntegration(req, () => {
|
ApplicationStore.updateGcpPubSubIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
if (integration === undefined) {
|
||||||
if (this.state.integration === undefined) {
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title="Update GCP Pub/Sub integration">
|
|
||||||
<GcpPubSubIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="Update GCP Pub/Sub integration">
|
||||||
|
<GcpPubSubIntegrationForm initialValues={integration} onFinish={onFinish} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditGcpPubSubIntegration;
|
export default EditGcpPubSubIntegration;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -14,53 +14,41 @@ import {
|
|||||||
import HttpIntegrationForm from "./HttpIntegrationForm";
|
import HttpIntegrationForm from "./HttpIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function EditHttpIntegration(props: IProps) {
|
||||||
integration?: HttpIntegration;
|
const navigate = useNavigate();
|
||||||
}
|
const [integration, setIntegration] = useState<HttpIntegration | undefined>(undefined);
|
||||||
|
|
||||||
class EditHttpIntegration extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
let req = new GetHttpIntegrationRequest();
|
let req = new GetHttpIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
ApplicationStore.getHttpIntegration(req, (resp: GetHttpIntegrationResponse) => {
|
ApplicationStore.getHttpIntegration(req, (resp: GetHttpIntegrationResponse) => {
|
||||||
this.setState({
|
setIntegration(resp.getIntegration());
|
||||||
integration: resp.getIntegration(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}, [props]);
|
||||||
|
|
||||||
onFinish = (obj: HttpIntegration) => {
|
const onFinish = (obj: HttpIntegration) => {
|
||||||
let req = new UpdateHttpIntegrationRequest();
|
let req = new UpdateHttpIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.updateHttpIntegration(req, () => {
|
ApplicationStore.updateHttpIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
if (integration === undefined) {
|
||||||
if (this.state.integration === undefined) {
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title="Update HTTP integration">
|
|
||||||
<HttpIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="Update HTTP integration">
|
||||||
|
<HttpIntegrationForm initialValues={integration} onFinish={onFinish} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditHttpIntegration;
|
export default EditHttpIntegration;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -14,58 +14,42 @@ import {
|
|||||||
import IftttIntegrationForm from "./IftttIntegrationForm";
|
import IftttIntegrationForm from "./IftttIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
measurementKeys: string[];
|
measurementKeys: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function EditIftttIntegration(props: IProps) {
|
||||||
integration?: IftttIntegration;
|
const navigate = useNavigate();
|
||||||
}
|
const [integration, setIntegration] = useState<IftttIntegration | undefined>(undefined);
|
||||||
|
|
||||||
class EditIftttIntegration extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
let req = new GetIftttIntegrationRequest();
|
let req = new GetIftttIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
ApplicationStore.getIftttIntegration(req, (resp: GetIftttIntegrationResponse) => {
|
ApplicationStore.getIftttIntegration(req, (resp: GetIftttIntegrationResponse) => {
|
||||||
this.setState({
|
setIntegration(resp.getIntegration());
|
||||||
integration: resp.getIntegration(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}, [props]);
|
||||||
|
|
||||||
onFinish = (obj: IftttIntegration) => {
|
const onFinish = (obj: IftttIntegration) => {
|
||||||
let req = new UpdateIftttIntegrationRequest();
|
let req = new UpdateIftttIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.updateIftttIntegration(req, () => {
|
ApplicationStore.updateIftttIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
if (integration === undefined) {
|
||||||
if (this.state.integration === undefined) {
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title="Update IFTTT integration">
|
|
||||||
<IftttIntegrationForm
|
|
||||||
measurementKeys={this.props.measurementKeys}
|
|
||||||
initialValues={this.state.integration}
|
|
||||||
onFinish={this.onFinish}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="Update IFTTT integration">
|
||||||
|
<IftttIntegrationForm measurementKeys={props.measurementKeys} initialValues={integration} onFinish={onFinish} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditIftttIntegration;
|
export default EditIftttIntegration;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -14,53 +14,41 @@ import {
|
|||||||
import InfluxDbIntegrationForm from "./InfluxDbIntegrationForm";
|
import InfluxDbIntegrationForm from "./InfluxDbIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function EditInfluxDbIntegration(props: IProps) {
|
||||||
integration?: InfluxDbIntegration;
|
const navigate = useNavigate();
|
||||||
}
|
const [integration, setIntegration] = useState<InfluxDbIntegration | undefined>(undefined);
|
||||||
|
|
||||||
class EditInfluxDbIntegration extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
let req = new GetInfluxDbIntegrationRequest();
|
let req = new GetInfluxDbIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
ApplicationStore.getInfluxDbIntegration(req, (resp: GetInfluxDbIntegrationResponse) => {
|
ApplicationStore.getInfluxDbIntegration(req, (resp: GetInfluxDbIntegrationResponse) => {
|
||||||
this.setState({
|
setIntegration(resp.getIntegration());
|
||||||
integration: resp.getIntegration(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}, [props]);
|
||||||
|
|
||||||
onFinish = (obj: InfluxDbIntegration) => {
|
const onFinish = (obj: InfluxDbIntegration) => {
|
||||||
let req = new UpdateInfluxDbIntegrationRequest();
|
let req = new UpdateInfluxDbIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.updateInfluxDbIntegration(req, () => {
|
ApplicationStore.updateInfluxDbIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
if (integration === undefined) {
|
||||||
if (this.state.integration === undefined) {
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title="Update InfluxDB integration">
|
|
||||||
<InfluxDbIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="Update InfluxDB integration">
|
||||||
|
<InfluxDbIntegrationForm initialValues={integration} onFinish={onFinish} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditInfluxDbIntegration;
|
export default EditInfluxDbIntegration;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -14,53 +14,41 @@ import {
|
|||||||
import LoRaCloudIntegrationForm from "./LoRaCloudIntegrationForm";
|
import LoRaCloudIntegrationForm from "./LoRaCloudIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function EditLoRaCloudIntegration(props: IProps) {
|
||||||
integration?: LoraCloudIntegration;
|
const navigate = useNavigate();
|
||||||
}
|
const [integration, setIntegration] = useState<LoraCloudIntegration | undefined>(undefined);
|
||||||
|
|
||||||
class EditLoRaCloudIntegration extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
let req = new GetLoraCloudIntegrationRequest();
|
let req = new GetLoraCloudIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
ApplicationStore.getLoraCloudIntegration(req, (resp: GetLoraCloudIntegrationResponse) => {
|
ApplicationStore.getLoraCloudIntegration(req, (resp: GetLoraCloudIntegrationResponse) => {
|
||||||
this.setState({
|
setIntegration(resp.getIntegration());
|
||||||
integration: resp.getIntegration(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}, [props]);
|
||||||
|
|
||||||
onFinish = (obj: LoraCloudIntegration) => {
|
const onFinish = (obj: LoraCloudIntegration) => {
|
||||||
let req = new UpdateLoraCloudIntegrationRequest();
|
let req = new UpdateLoraCloudIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.updateLoraCloudIntegration(req, () => {
|
ApplicationStore.updateLoraCloudIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
if (integration === undefined) {
|
||||||
if (this.state.integration === undefined) {
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title="Update Semtech LoRa Cloud™ integration">
|
|
||||||
<LoRaCloudIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="Update Semtech LoRa Cloud™ integration">
|
||||||
|
<LoRaCloudIntegrationForm initialValues={integration} onFinish={onFinish} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditLoRaCloudIntegration;
|
export default EditLoRaCloudIntegration;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -14,53 +14,41 @@ import {
|
|||||||
import MyDevicesIntegrationForm from "./MyDevicesIntegrationForm";
|
import MyDevicesIntegrationForm from "./MyDevicesIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function EditMyDevicesIntegration(props: IProps) {
|
||||||
integration?: MyDevicesIntegration;
|
const navigate = useNavigate();
|
||||||
}
|
const [integration, setIntegration] = useState<MyDevicesIntegration | undefined>(undefined);
|
||||||
|
|
||||||
class EditMyDevicesIntegration extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
let req = new GetMyDevicesIntegrationRequest();
|
let req = new GetMyDevicesIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
ApplicationStore.getMyDevicesIntegration(req, (resp: GetMyDevicesIntegrationResponse) => {
|
ApplicationStore.getMyDevicesIntegration(req, (resp: GetMyDevicesIntegrationResponse) => {
|
||||||
this.setState({
|
setIntegration(resp.getIntegration());
|
||||||
integration: resp.getIntegration(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}, [props]);
|
||||||
|
|
||||||
onFinish = (obj: MyDevicesIntegration) => {
|
const onFinish = (obj: MyDevicesIntegration) => {
|
||||||
let req = new UpdateMyDevicesIntegrationRequest();
|
let req = new UpdateMyDevicesIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.updateMyDevicesIntegration(req, () => {
|
ApplicationStore.updateMyDevicesIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
if (integration === undefined) {
|
||||||
if (this.state.integration === undefined) {
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title="Update myDevices integration">
|
|
||||||
<MyDevicesIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="Update myDevices integration">
|
||||||
|
<MyDevicesIntegrationForm initialValues={integration} onFinish={onFinish} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditMyDevicesIntegration;
|
export default EditMyDevicesIntegration;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -14,53 +14,41 @@ import {
|
|||||||
import PilotThingsIntegrationForm from "./PilotThingsIntegrationForm";
|
import PilotThingsIntegrationForm from "./PilotThingsIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function EditPilotThingsIntegration(props: IProps) {
|
||||||
integration?: PilotThingsIntegration;
|
const navigate = useNavigate();
|
||||||
}
|
const [integration, setIntegration] = useState<PilotThingsIntegration | undefined>(undefined);
|
||||||
|
|
||||||
class EditPilotThingsIntegration extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
let req = new GetPilotThingsIntegrationRequest();
|
let req = new GetPilotThingsIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
ApplicationStore.getPilotThingsIntegration(req, (resp: GetPilotThingsIntegrationResponse) => {
|
ApplicationStore.getPilotThingsIntegration(req, (resp: GetPilotThingsIntegrationResponse) => {
|
||||||
this.setState({
|
setIntegration(resp.getIntegration());
|
||||||
integration: resp.getIntegration(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}, [props]);
|
||||||
|
|
||||||
onFinish = (obj: PilotThingsIntegration) => {
|
const onFinish = (obj: PilotThingsIntegration) => {
|
||||||
let req = new UpdatePilotThingsIntegrationRequest();
|
let req = new UpdatePilotThingsIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.updatePilotThingsIntegration(req, () => {
|
ApplicationStore.updatePilotThingsIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
if (integration === undefined) {
|
||||||
if (this.state.integration === undefined) {
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title="Update Pilot Things integration">
|
|
||||||
<PilotThingsIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="Update Pilot Things integration">
|
||||||
|
<PilotThingsIntegrationForm initialValues={integration} onFinish={onFinish} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditPilotThingsIntegration;
|
export default EditPilotThingsIntegration;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
|
|
||||||
@ -14,53 +14,41 @@ import {
|
|||||||
import ThingsBoardIntegrationForm from "./ThingsBoardIntegrationForm";
|
import ThingsBoardIntegrationForm from "./ThingsBoardIntegrationForm";
|
||||||
import ApplicationStore from "../../../stores/ApplicationStore";
|
import ApplicationStore from "../../../stores/ApplicationStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function EditThingsBoardIntegration(props: IProps) {
|
||||||
integration?: ThingsBoardIntegration;
|
const navigate = useNavigate();
|
||||||
}
|
const [integration, setIntegration] = useState<ThingsBoardIntegration | undefined>(undefined);
|
||||||
|
|
||||||
class EditThingsBoardIntegration extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
let req = new GetThingsBoardIntegrationRequest();
|
let req = new GetThingsBoardIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
ApplicationStore.getThingsBoardIntegration(req, (resp: GetThingsBoardIntegrationResponse) => {
|
ApplicationStore.getThingsBoardIntegration(req, (resp: GetThingsBoardIntegrationResponse) => {
|
||||||
this.setState({
|
setIntegration(resp.getIntegration());
|
||||||
integration: resp.getIntegration(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}, [props]);
|
||||||
|
|
||||||
onFinish = (obj: ThingsBoardIntegration) => {
|
const onFinish = (obj: ThingsBoardIntegration) => {
|
||||||
let req = new UpdateThingsBoardIntegrationRequest();
|
let req = new UpdateThingsBoardIntegrationRequest();
|
||||||
req.setIntegration(obj);
|
req.setIntegration(obj);
|
||||||
|
|
||||||
ApplicationStore.updateThingsBoardIntegration(req, () => {
|
ApplicationStore.updateThingsBoardIntegration(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/integrations`);
|
||||||
`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/integrations`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
if (integration === undefined) {
|
||||||
if (this.state.integration === undefined) {
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title="Update ThingsBoard integration">
|
|
||||||
<ThingsBoardIntegrationForm initialValues={this.state.integration} onFinish={this.onFinish} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="Update ThingsBoard integration">
|
||||||
|
<ThingsBoardIntegrationForm initialValues={integration} onFinish={onFinish} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditThingsBoardIntegration;
|
export default EditThingsBoardIntegration;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Col, Card, Popconfirm } from "antd";
|
import { Col, Card, Popconfirm } from "antd";
|
||||||
@ -13,46 +12,44 @@ interface IProps {
|
|||||||
add?: boolean;
|
add?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class GcpPubSubCard extends Component<IProps> {
|
function GcpPubSubCard(props: IProps) {
|
||||||
onDelete = () => {
|
const onDelete = () => {
|
||||||
let req = new DeleteGcpPubSubIntegrationRequest();
|
let req = new DeleteGcpPubSubIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
ApplicationStore.deleteGcpPubSubIntegration(req, () => {});
|
ApplicationStore.deleteGcpPubSubIntegration(req, () => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
let actions: any[] = [];
|
||||||
let actions: any[] = [];
|
|
||||||
|
|
||||||
if (!!this.props.add) {
|
if (!!props.add) {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/gcp-pub-sub/create">
|
<Link to="gcp-pub-sub/create">
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/gcp-pub-sub/edit">
|
<Link to="gcp-pub-sub/edit">
|
||||||
<EditOutlined />
|
<EditOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
</Popconfirm>,
|
</Popconfirm>,
|
||||||
];
|
];
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col span={8}>
|
|
||||||
<Card
|
|
||||||
title="GCP Pub/Sub"
|
|
||||||
className="integration-card"
|
|
||||||
cover={<img alt="GCP Pub/Sub" src="/integrations/gcp_pubsub.png" style={{ padding: 1 }} />}
|
|
||||||
actions={actions}
|
|
||||||
>
|
|
||||||
<Card.Meta description="The Google Cloud Pub/Sub integration forwards events to a GCP Pub/Sub topic." />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col span={8}>
|
||||||
|
<Card
|
||||||
|
title="GCP Pub/Sub"
|
||||||
|
className="integration-card"
|
||||||
|
cover={<img alt="GCP Pub/Sub" src="/integrations/gcp_pubsub.png" style={{ padding: 1 }} />}
|
||||||
|
actions={actions}
|
||||||
|
>
|
||||||
|
<Card.Meta description="The Google Cloud Pub/Sub integration forwards events to a GCP Pub/Sub topic." />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GcpPubSubCard;
|
export default GcpPubSubCard;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
|
|
||||||
import { Form, Input, Button, Select } from "antd";
|
import { Form, Input, Button, Select } from "antd";
|
||||||
|
|
||||||
import { GcpPubSubIntegration, Encoding } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
import { GcpPubSubIntegration, Encoding } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||||
@ -9,9 +7,9 @@ interface IProps {
|
|||||||
onFinish: (obj: GcpPubSubIntegration) => void;
|
onFinish: (obj: GcpPubSubIntegration) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class GcpPubSubIntegrationForm extends Component<IProps> {
|
function GcpPubSubIntegrationForm(props: IProps) {
|
||||||
onFinish = (values: GcpPubSubIntegration.AsObject) => {
|
const onFinish = (values: GcpPubSubIntegration.AsObject) => {
|
||||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
const v = Object.assign(props.initialValues.toObject(), values);
|
||||||
let i = new GcpPubSubIntegration();
|
let i = new GcpPubSubIntegration();
|
||||||
|
|
||||||
i.setApplicationId(v.applicationId);
|
i.setApplicationId(v.applicationId);
|
||||||
@ -20,52 +18,55 @@ class GcpPubSubIntegrationForm extends Component<IProps> {
|
|||||||
i.setTopicName(v.topicName);
|
i.setTopicName(v.topicName);
|
||||||
i.setCredentialsFile(v.credentialsFile);
|
i.setCredentialsFile(v.credentialsFile);
|
||||||
|
|
||||||
this.props.onFinish(i);
|
props.onFinish(i);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
<Form.Item
|
||||||
<Form.Item
|
label="Payload encoding"
|
||||||
label="Payload encoding"
|
name="encoding"
|
||||||
name="encoding"
|
rules={[{ required: true, message: "Please select an encoding!" }]}
|
||||||
rules={[{ required: true, message: "Please select an encoding!" }]}
|
>
|
||||||
>
|
<Select>
|
||||||
<Select>
|
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
|
||||||
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
|
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
|
||||||
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
|
</Select>
|
||||||
</Select>
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
<Form.Item
|
label="GCP project ID"
|
||||||
label="GCP project ID"
|
name="projectId"
|
||||||
name="projectId"
|
rules={[{ required: true, message: "Please enter a GCP project ID!" }]}
|
||||||
rules={[{ required: true, message: "Please enter a GCP project ID!" }]}
|
>
|
||||||
>
|
<Input />
|
||||||
<Input />
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
<Form.Item
|
label="GCP Pub/Sub topic name"
|
||||||
label="GCP Pub/Sub topic name"
|
name="topicName"
|
||||||
name="topicName"
|
rules={[{ required: true, message: "Please enter a GCP Pub/Sub topic name!" }]}
|
||||||
rules={[{ required: true, message: "Please enter a GCP Pub/Sub topic name!" }]}
|
>
|
||||||
>
|
<Input />
|
||||||
<Input />
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
<Form.Item
|
label="GCP Service account credentials file"
|
||||||
label="GCP Service account credentials file"
|
name="credentialsFile"
|
||||||
name="credentialsFile"
|
tooltip="Under IAM create a Service account with 'Pub/Sub Publisher' role, then put the content of the JSON key in this field."
|
||||||
tooltip="Under IAM create a Service account with 'Pub/Sub Publisher' role, then put the content of the JSON key in this field."
|
rules={[
|
||||||
rules={[{ required: true, message: "Please enter a GCP Service account credentials file!" }]}
|
{
|
||||||
>
|
required: true,
|
||||||
<Input.TextArea rows={10} />
|
message: "Please enter a GCP Service account credentials file!",
|
||||||
</Form.Item>
|
},
|
||||||
<Form.Item>
|
]}
|
||||||
<Button type="primary" htmlType="submit">
|
>
|
||||||
Submit
|
<Input.TextArea rows={10} />
|
||||||
</Button>
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item>
|
||||||
</Form>
|
<Button type="primary" htmlType="submit">
|
||||||
);
|
Submit
|
||||||
}
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GcpPubSubIntegrationForm;
|
export default GcpPubSubIntegrationForm;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { Card, Button, Form, Input } from "antd";
|
import { Card, Button, Form, Input } from "antd";
|
||||||
@ -15,39 +15,27 @@ interface IProps {
|
|||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function GenerateMqttCertificate(props: IProps) {
|
||||||
certificate?: GenerateMqttIntegrationClientCertificateResponse;
|
const [certificate, setCertificate] = useState<GenerateMqttIntegrationClientCertificateResponse | undefined>(
|
||||||
buttonDisabled: boolean;
|
undefined,
|
||||||
}
|
);
|
||||||
|
const [buttonDisabled, setButtonDisabled] = useState<boolean>(false);
|
||||||
|
|
||||||
class GenerateMqttCertificate extends Component<IProps, IState> {
|
const requestCertificate = () => {
|
||||||
constructor(props: IProps) {
|
setButtonDisabled(true);
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
certificate: undefined,
|
|
||||||
buttonDisabled: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
requestCertificate = () => {
|
|
||||||
this.setState({
|
|
||||||
buttonDisabled: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
let req = new GenerateMqttIntegrationClientCertificateRequest();
|
let req = new GenerateMqttIntegrationClientCertificateRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
ApplicationStore.generateMqttIntegrationClientCertificate(
|
ApplicationStore.generateMqttIntegrationClientCertificate(
|
||||||
req,
|
req,
|
||||||
(resp: GenerateMqttIntegrationClientCertificateResponse) => {
|
(resp: GenerateMqttIntegrationClientCertificateResponse) => {
|
||||||
this.setState({
|
setCertificate(resp);
|
||||||
certificate: resp,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
renderRequest = () => {
|
const renderRequest = () => {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<p>
|
<p>
|
||||||
@ -59,7 +47,7 @@ class GenerateMqttCertificate extends Component<IProps, IState> {
|
|||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<Button onClick={this.requestCertificate} disabled={this.state.buttonDisabled}>
|
<Button onClick={requestCertificate} disabled={buttonDisabled}>
|
||||||
Generate certificate
|
Generate certificate
|
||||||
</Button>
|
</Button>
|
||||||
</p>
|
</p>
|
||||||
@ -67,14 +55,14 @@ class GenerateMqttCertificate extends Component<IProps, IState> {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
renderResponse = () => {
|
const renderResponse = () => {
|
||||||
const certificate = this.state.certificate!;
|
const cert = certificate!;
|
||||||
|
|
||||||
const initial = {
|
const initial = {
|
||||||
expiresAt: moment(certificate.getExpiresAt()!.toDate()!).format("YYYY-MM-DD HH:mm:ss"),
|
expiresAt: moment(cert.getExpiresAt()!.toDate()!).format("YYYY-MM-DD HH:mm:ss"),
|
||||||
caCert: certificate.getCaCert(),
|
caCert: cert.getCaCert(),
|
||||||
tlsCert: certificate.getTlsCert(),
|
tlsCert: cert.getTlsCert(),
|
||||||
tlsKey: certificate.getTlsKey(),
|
tlsKey: cert.getTlsKey(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -103,15 +91,13 @@ class GenerateMqttCertificate extends Component<IProps, IState> {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
let content = renderRequest();
|
||||||
let content = this.renderRequest();
|
|
||||||
|
|
||||||
if (this.state.certificate !== undefined) {
|
if (certificate !== undefined) {
|
||||||
content = this.renderResponse();
|
content = renderResponse();
|
||||||
}
|
|
||||||
|
|
||||||
return <Card title="Generate MQTT certificate">{content}</Card>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return <Card title="Generate MQTT certificate">{content}</Card>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GenerateMqttCertificate;
|
export default GenerateMqttCertificate;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Col, Card, Popconfirm } from "antd";
|
import { Col, Card, Popconfirm } from "antd";
|
||||||
@ -13,46 +12,44 @@ interface IProps {
|
|||||||
add?: boolean;
|
add?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class HttpCard extends Component<IProps> {
|
function HttpCard(props: IProps) {
|
||||||
onDelete = () => {
|
const onDelete = () => {
|
||||||
let req = new DeleteHttpIntegrationRequest();
|
let req = new DeleteHttpIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
ApplicationStore.deleteHttpIntegration(req, () => {});
|
ApplicationStore.deleteHttpIntegration(req, () => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
let actions: any[] = [];
|
||||||
let actions: any[] = [];
|
|
||||||
|
|
||||||
if (!!this.props.add) {
|
if (!!props.add) {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/http/create">
|
<Link to="http/create">
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/http/edit">
|
<Link to="http/edit">
|
||||||
<EditOutlined />
|
<EditOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
</Popconfirm>,
|
</Popconfirm>,
|
||||||
];
|
];
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col span={8}>
|
|
||||||
<Card
|
|
||||||
title="HTTP"
|
|
||||||
className="integration-card"
|
|
||||||
cover={<img alt="HTTP" src="/integrations/http.png" style={{ padding: 1 }} />}
|
|
||||||
actions={actions}
|
|
||||||
>
|
|
||||||
<Card.Meta description="The HTTP integration forwards events to a user-configurable endpoint as POST requests." />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col span={8}>
|
||||||
|
<Card
|
||||||
|
title="HTTP"
|
||||||
|
className="integration-card"
|
||||||
|
cover={<img alt="HTTP" src="/integrations/http.png" style={{ padding: 1 }} />}
|
||||||
|
actions={actions}
|
||||||
|
>
|
||||||
|
<Card.Meta description="The HTTP integration forwards events to a user-configurable endpoint as POST requests." />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HttpCard;
|
export default HttpCard;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
|
|
||||||
import { Form, Input, Button, Select, Row, Col, Typography, Space } from "antd";
|
import { Form, Input, Button, Select, Row, Col, Typography, Space } from "antd";
|
||||||
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
@ -10,9 +8,9 @@ interface IProps {
|
|||||||
onFinish: (obj: HttpIntegration) => void;
|
onFinish: (obj: HttpIntegration) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class HttpIntegrationForm extends Component<IProps> {
|
function HttpIntegrationForm(props: IProps) {
|
||||||
onFinish = (values: HttpIntegration.AsObject) => {
|
const onFinish = (values: HttpIntegration.AsObject) => {
|
||||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
const v = Object.assign(props.initialValues.toObject(), values);
|
||||||
let i = new HttpIntegration();
|
let i = new HttpIntegration();
|
||||||
|
|
||||||
i.setApplicationId(v.applicationId);
|
i.setApplicationId(v.applicationId);
|
||||||
@ -24,79 +22,77 @@ class HttpIntegrationForm extends Component<IProps> {
|
|||||||
i.getHeadersMap().set(elm[0], elm[1]);
|
i.getHeadersMap().set(elm[0], elm[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onFinish(i);
|
props.onFinish(i);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
<Form.Item
|
||||||
<Form.Item
|
label="Payload encoding"
|
||||||
label="Payload encoding"
|
name="encoding"
|
||||||
name="encoding"
|
rules={[{ required: true, message: "Please select an encoding!" }]}
|
||||||
rules={[{ required: true, message: "Please select an encoding!" }]}
|
>
|
||||||
>
|
<Select>
|
||||||
<Select>
|
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
|
||||||
<Select.Option value={Encoding.JSON}>JSON</Select.Option>
|
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
|
||||||
<Select.Option value={Encoding.PROTOBUF}>Protobuf (binary)</Select.Option>
|
</Select>
|
||||||
</Select>
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
<Form.Item
|
label="Event endpoint URL(s)"
|
||||||
label="Event endpoint URL(s)"
|
name="eventEndpointUrl"
|
||||||
name="eventEndpointUrl"
|
tooltip="ChirpStack will make a POST request to this URL(s) with 'event' as query parameter. Multiple URLs can be defined as a comma separated list. Whitespace will be automatically removed."
|
||||||
tooltip="ChirpStack will make a POST request to this URL(s) with 'event' as query parameter. Multiple URLs can be defined as a comma separated list. Whitespace will be automatically removed."
|
rules={[{ required: true, message: "Please enter an event endpoint URL!" }]}
|
||||||
rules={[{ required: true, message: "Please enter an event endpoint URL!" }]}
|
>
|
||||||
>
|
<Input />
|
||||||
<Input />
|
</Form.Item>
|
||||||
</Form.Item>
|
<Space direction="vertical" style={{ width: "100%" }}>
|
||||||
<Space direction="vertical" style={{ width: "100%" }}>
|
<Typography.Text>Headers</Typography.Text>
|
||||||
<Typography.Text>Headers</Typography.Text>
|
<Form.List name="headersMap">
|
||||||
<Form.List name="headersMap">
|
{(fields, { add, remove }) => (
|
||||||
{(fields, { add, remove }) => (
|
<>
|
||||||
<>
|
{fields.map(({ key, name, ...restField }) => (
|
||||||
{fields.map(({ key, name, ...restField }) => (
|
<Row gutter={24}>
|
||||||
<Row gutter={24}>
|
<Col span={6}>
|
||||||
<Col span={6}>
|
<Form.Item
|
||||||
<Form.Item
|
{...restField}
|
||||||
{...restField}
|
name={[name, 0]}
|
||||||
name={[name, 0]}
|
fieldKey={[name, 0]}
|
||||||
fieldKey={[name, 0]}
|
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
>
|
||||||
>
|
<Input placeholder="Key" />
|
||||||
<Input placeholder="Key" />
|
</Form.Item>
|
||||||
</Form.Item>
|
</Col>
|
||||||
</Col>
|
<Col span={16}>
|
||||||
<Col span={16}>
|
<Form.Item
|
||||||
<Form.Item
|
{...restField}
|
||||||
{...restField}
|
name={[name, 1]}
|
||||||
name={[name, 1]}
|
fieldKey={[name, 1]}
|
||||||
fieldKey={[name, 1]}
|
rules={[{ required: true, message: "Please enter a value!" }]}
|
||||||
rules={[{ required: true, message: "Please enter a value!" }]}
|
>
|
||||||
>
|
<Input placeholder="Value" />
|
||||||
<Input placeholder="Value" />
|
</Form.Item>
|
||||||
</Form.Item>
|
</Col>
|
||||||
</Col>
|
<Col span={2}>
|
||||||
<Col span={2}>
|
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
</Col>
|
||||||
</Col>
|
</Row>
|
||||||
</Row>
|
))}
|
||||||
))}
|
<Form.Item>
|
||||||
<Form.Item>
|
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
Add header
|
||||||
Add header
|
</Button>
|
||||||
</Button>
|
</Form.Item>
|
||||||
</Form.Item>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</Form.List>
|
||||||
</Form.List>
|
</Space>
|
||||||
</Space>
|
<Form.Item>
|
||||||
<Form.Item>
|
<Button type="primary" htmlType="submit">
|
||||||
<Button type="primary" htmlType="submit">
|
Submit
|
||||||
Submit
|
</Button>
|
||||||
</Button>
|
</Form.Item>
|
||||||
</Form.Item>
|
</Form>
|
||||||
</Form>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HttpIntegrationForm;
|
export default HttpIntegrationForm;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Col, Card, Popconfirm } from "antd";
|
import { Col, Card, Popconfirm } from "antd";
|
||||||
@ -13,46 +12,44 @@ interface IProps {
|
|||||||
add?: boolean;
|
add?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class IftttCard extends Component<IProps> {
|
function IftttCard(props: IProps) {
|
||||||
onDelete = () => {
|
const onDelete = () => {
|
||||||
let req = new DeleteIftttIntegrationRequest();
|
let req = new DeleteIftttIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
ApplicationStore.deleteIftttIntegration(req, () => {});
|
ApplicationStore.deleteIftttIntegration(req, () => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
let actions: any[] = [];
|
||||||
let actions: any[] = [];
|
|
||||||
|
|
||||||
if (!!this.props.add) {
|
if (!!props.add) {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/ifttt/create">
|
<Link to="ifttt/create">
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/ifttt/edit">
|
<Link to="ifttt/edit">
|
||||||
<EditOutlined />
|
<EditOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
</Popconfirm>,
|
</Popconfirm>,
|
||||||
];
|
];
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col span={8}>
|
|
||||||
<Card
|
|
||||||
title="IFTTT"
|
|
||||||
className="integration-card"
|
|
||||||
cover={<img alt="IFTTT" src="/integrations/ifttt.png" style={{ padding: 1 }} />}
|
|
||||||
actions={actions}
|
|
||||||
>
|
|
||||||
<Card.Meta description="The IFTTT integration forwards events to the IFTTT Webhooks integration." />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col span={8}>
|
||||||
|
<Card
|
||||||
|
title="IFTTT"
|
||||||
|
className="integration-card"
|
||||||
|
cover={<img alt="IFTTT" src="/integrations/ifttt.png" style={{ padding: 1 }} />}
|
||||||
|
actions={actions}
|
||||||
|
>
|
||||||
|
<Card.Meta description="The IFTTT integration forwards events to the IFTTT Webhooks integration." />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IftttCard;
|
export default IftttCard;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { Form, Input, AutoComplete, Button, Row, Col, Switch } from "antd";
|
import { Form, Input, AutoComplete, Button, Row, Col, Switch } from "antd";
|
||||||
|
|
||||||
@ -10,29 +10,15 @@ interface IProps {
|
|||||||
onFinish: (obj: IftttIntegration) => void;
|
onFinish: (obj: IftttIntegration) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function IftttIntegrationForm(props: IProps) {
|
||||||
arbitraryJson: boolean;
|
const [arbitraryJson, setArbitraryJson] = useState<Boolean>(false);
|
||||||
}
|
|
||||||
|
|
||||||
class IftttIntegrationForm extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
setArbitraryJson(props.initialValues.getArbitraryJson());
|
||||||
super(props);
|
}, [props]);
|
||||||
|
|
||||||
this.state = {
|
const onFinish = (values: IftttIntegration.AsObject) => {
|
||||||
arbitraryJson: false,
|
const v = Object.assign(props.initialValues.toObject(), values);
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const v = this.props.initialValues;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
arbitraryJson: v.getArbitraryJson(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onFinish = (values: IftttIntegration.AsObject) => {
|
|
||||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
|
||||||
let i = new IftttIntegration();
|
let i = new IftttIntegration();
|
||||||
|
|
||||||
i.setApplicationId(v.applicationId);
|
i.setApplicationId(v.applicationId);
|
||||||
@ -41,55 +27,58 @@ class IftttIntegrationForm extends Component<IProps, IState> {
|
|||||||
i.setArbitraryJson(v.arbitraryJson);
|
i.setArbitraryJson(v.arbitraryJson);
|
||||||
i.setUplinkValuesList(v.uplinkValuesList);
|
i.setUplinkValuesList(v.uplinkValuesList);
|
||||||
|
|
||||||
this.props.onFinish(i);
|
props.onFinish(i);
|
||||||
};
|
};
|
||||||
|
|
||||||
onArbitraryJsonChange = (checked: boolean) => {
|
const onArbitraryJsonChange = (checked: boolean) => {
|
||||||
this.setState({
|
setArbitraryJson(checked);
|
||||||
arbitraryJson: checked,
|
};
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
const options: {
|
||||||
const options: {
|
value: string;
|
||||||
value: string;
|
}[] = props.measurementKeys.map(v => {
|
||||||
}[] = this.props.measurementKeys.map(v => {
|
return { value: v };
|
||||||
return { value: v };
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Key"
|
label="Key"
|
||||||
name="key"
|
name="key"
|
||||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||||
tooltip="This key can be obtained from the IFTTT Webhooks integrations documentation"
|
tooltip="This key can be obtained from the IFTTT Webhooks integrations documentation"
|
||||||
>
|
>
|
||||||
<Input.Password />
|
<Input.Password />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Row gutter={24}>
|
<Row gutter={24}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Event prefix"
|
label="Event prefix"
|
||||||
name="eventPrefix"
|
name="eventPrefix"
|
||||||
rules={[{ pattern: /[A-Za-z0-9]+/, message: "Only use A-Z, a-z and 0-9 characters" }]}
|
rules={[
|
||||||
tooltip="The prefix will be added to the Webhook event, e.g. if set an uplink will be published as PREFIX_up instead of up."
|
{
|
||||||
>
|
pattern: /[A-Za-z0-9]+/,
|
||||||
<Input />
|
message: "Only use A-Z, a-z and 0-9 characters",
|
||||||
</Form.Item>
|
},
|
||||||
</Col>
|
]}
|
||||||
<Col span={12}>
|
tooltip="The prefix will be added to the Webhook event, e.g. if set an uplink will be published as PREFIX_up instead of up."
|
||||||
<Form.Item
|
>
|
||||||
label="Publish as arbitrary JSON"
|
<Input />
|
||||||
name="arbitraryJson"
|
</Form.Item>
|
||||||
valuePropName="checked"
|
</Col>
|
||||||
tooltip="If enabled, the event payload will be published as-is (arbitrary JSON payload instead of 3 JSON values format)."
|
<Col span={12}>
|
||||||
>
|
<Form.Item
|
||||||
<Switch onChange={this.onArbitraryJsonChange} />
|
label="Publish as arbitrary JSON"
|
||||||
</Form.Item>
|
name="arbitraryJson"
|
||||||
</Col>
|
valuePropName="checked"
|
||||||
</Row>
|
tooltip="If enabled, the event payload will be published as-is (arbitrary JSON payload instead of 3 JSON values format)."
|
||||||
{!this.state.arbitraryJson && <Form.List name="uplinkValuesList">
|
>
|
||||||
|
<Switch onChange={onArbitraryJsonChange} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{!arbitraryJson && (
|
||||||
|
<Form.List name="uplinkValuesList">
|
||||||
{fields => (
|
{fields => (
|
||||||
<Row gutter={24}>
|
<Row gutter={24}>
|
||||||
{fields.map((field, i) => (
|
{fields.map((field, i) => (
|
||||||
@ -105,15 +94,15 @@ class IftttIntegrationForm extends Component<IProps, IState> {
|
|||||||
))}
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
</Form.List>}
|
</Form.List>
|
||||||
<Form.Item>
|
)}
|
||||||
<Button type="primary" htmlType="submit">
|
<Form.Item>
|
||||||
Submit
|
<Button type="primary" htmlType="submit">
|
||||||
</Button>
|
Submit
|
||||||
</Form.Item>
|
</Button>
|
||||||
</Form>
|
</Form.Item>
|
||||||
);
|
</Form>
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IftttIntegrationForm;
|
export default IftttIntegrationForm;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { Form, Input, Button, Select } from "antd";
|
import { Form, Input, Button, Select } from "antd";
|
||||||
|
|
||||||
@ -13,20 +13,11 @@ interface IProps {
|
|||||||
onFinish: (obj: InfluxDbIntegration) => void;
|
onFinish: (obj: InfluxDbIntegration) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function InfluxDbIntegrationForm(props: IProps) {
|
||||||
selectedVersion: InfluxDbVersion;
|
const [selectedVersion, setSelectedVersion] = useState<InfluxDbVersion>(InfluxDbVersion.INFLUXDB_1);
|
||||||
}
|
|
||||||
|
|
||||||
class InfluxDbIntegrationForm extends Component<IProps, IState> {
|
const onFinish = (values: InfluxDbIntegration.AsObject) => {
|
||||||
constructor(props: IProps) {
|
const v = Object.assign(props.initialValues.toObject(), values);
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
selectedVersion: InfluxDbVersion.INFLUXDB_1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onFinish = (values: InfluxDbIntegration.AsObject) => {
|
|
||||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
|
||||||
let i = new InfluxDbIntegration();
|
let i = new InfluxDbIntegration();
|
||||||
|
|
||||||
i.setApplicationId(v.applicationId);
|
i.setApplicationId(v.applicationId);
|
||||||
@ -41,98 +32,90 @@ class InfluxDbIntegrationForm extends Component<IProps, IState> {
|
|||||||
i.setBucket(v.bucket);
|
i.setBucket(v.bucket);
|
||||||
i.setToken(v.token);
|
i.setToken(v.token);
|
||||||
|
|
||||||
this.props.onFinish(i);
|
props.onFinish(i);
|
||||||
};
|
};
|
||||||
|
|
||||||
onVersionChange = (version: InfluxDbVersion) => {
|
const onVersionChange = (version: InfluxDbVersion) => {
|
||||||
this.setState({
|
setSelectedVersion(version);
|
||||||
selectedVersion: version,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
<Form.Item
|
||||||
|
label="InfluxDB version"
|
||||||
|
name="version"
|
||||||
|
rules={[{ required: true, message: "Please select an InfluxDB version!" }]}
|
||||||
|
>
|
||||||
|
<Select onChange={onVersionChange}>
|
||||||
|
<Select.Option value={InfluxDbVersion.INFLUXDB_1}>InfluxDB v1</Select.Option>
|
||||||
|
<Select.Option value={InfluxDbVersion.INFLUXDB_2}>InfluxDB v2</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="API endpoint (write)"
|
||||||
|
name="endpoint"
|
||||||
|
rules={[{ required: true, message: "Please enter an endpoint!" }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="http://localhost:8086/api/v2/write" />
|
||||||
|
</Form.Item>
|
||||||
|
{selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
||||||
|
<Form.Item label="Username" name="username">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
||||||
|
<Form.Item label="Password" name="password">
|
||||||
|
<Input.Password />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
||||||
|
<Form.Item label="Database name" name="db" rules={[{ required: true, message: "Please enter database name!" }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="InfluxDB version"
|
label="Retention policy name"
|
||||||
name="version"
|
name="retentionPolicyName"
|
||||||
rules={[{ required: true, message: "Please select an InfluxDB version!" }]}
|
tooltip="Sets the target retention policy for the write. InfluxDB writes to the DEFAULT retention policy if you do not specify a retention policy."
|
||||||
>
|
>
|
||||||
<Select onChange={this.onVersionChange}>
|
<Input />
|
||||||
<Select.Option value={InfluxDbVersion.INFLUXDB_1}>InfluxDB v1</Select.Option>
|
</Form.Item>
|
||||||
<Select.Option value={InfluxDbVersion.INFLUXDB_2}>InfluxDB v2</Select.Option>
|
)}
|
||||||
|
{selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
||||||
|
<Form.Item label="Select timestamp precision" name="precision">
|
||||||
|
<Select>
|
||||||
|
<Select.Option value={InfluxDbPrecision.NS}>Nanosecond</Select.Option>
|
||||||
|
<Select.Option value={InfluxDbPrecision.U}>Microsecond</Select.Option>
|
||||||
|
<Select.Option value={InfluxDbPrecision.MS}>Millisecond</Select.Option>
|
||||||
|
<Select.Option value={InfluxDbPrecision.S}>Second</Select.Option>
|
||||||
|
<Select.Option value={InfluxDbPrecision.M}>Minute</Select.Option>
|
||||||
|
<Select.Option value={InfluxDbPrecision.H}>Hour</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
)}
|
||||||
label="API endpoint (write)"
|
{selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
|
||||||
name="endpoint"
|
<Form.Item label="Organization" name="organization">
|
||||||
rules={[{ required: true, message: "Please enter an endpoint!" }]}
|
<Input />
|
||||||
>
|
|
||||||
<Input placeholder="http://localhost:8086/api/v2/write" />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
)}
|
||||||
<Form.Item label="Username" name="username">
|
{selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
|
||||||
<Input />
|
<Form.Item label="Bucket" name="bucket">
|
||||||
</Form.Item>
|
<Input />
|
||||||
)}
|
|
||||||
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
|
||||||
<Form.Item label="Password" name="password">
|
|
||||||
<Input.Password />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
|
||||||
<Form.Item
|
|
||||||
label="Database name"
|
|
||||||
name="db"
|
|
||||||
rules={[{ required: true, message: "Please enter database name!" }]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
|
||||||
<Form.Item
|
|
||||||
label="Retention policy name"
|
|
||||||
name="retentionPolicyName"
|
|
||||||
tooltip="Sets the target retention policy for the write. InfluxDB writes to the DEFAULT retention policy if you do not specify a retention policy."
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_1 && (
|
|
||||||
<Form.Item label="Select timestamp precision" name="precision">
|
|
||||||
<Select>
|
|
||||||
<Select.Option value={InfluxDbPrecision.NS}>Nanosecond</Select.Option>
|
|
||||||
<Select.Option value={InfluxDbPrecision.U}>Microsecond</Select.Option>
|
|
||||||
<Select.Option value={InfluxDbPrecision.MS}>Millisecond</Select.Option>
|
|
||||||
<Select.Option value={InfluxDbPrecision.S}>Second</Select.Option>
|
|
||||||
<Select.Option value={InfluxDbPrecision.M}>Minute</Select.Option>
|
|
||||||
<Select.Option value={InfluxDbPrecision.H}>Hour</Select.Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
|
|
||||||
<Form.Item label="Organization" name="organization">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
|
|
||||||
<Form.Item label="Bucket" name="bucket">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
{this.state.selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
|
|
||||||
<Form.Item label="Token" name="token">
|
|
||||||
<Input.Password />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="primary" htmlType="submit">
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
)}
|
||||||
);
|
{selectedVersion === InfluxDbVersion.INFLUXDB_2 && (
|
||||||
}
|
<Form.Item label="Token" name="token">
|
||||||
|
<Input.Password />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InfluxDbIntegrationForm;
|
export default InfluxDbIntegrationForm;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Col, Card, Popconfirm } from "antd";
|
import { Col, Card, Popconfirm } from "antd";
|
||||||
@ -13,46 +12,44 @@ interface IProps {
|
|||||||
add?: boolean;
|
add?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class InfluxdbCard extends Component<IProps> {
|
function InfluxdbCard(props: IProps) {
|
||||||
onDelete = () => {
|
const onDelete = () => {
|
||||||
let req = new DeleteInfluxDbIntegrationRequest();
|
let req = new DeleteInfluxDbIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
ApplicationStore.deleteInfluxDbIntegration(req, () => {});
|
ApplicationStore.deleteInfluxDbIntegration(req, () => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
let actions: any[] = [];
|
||||||
let actions: any[] = [];
|
|
||||||
|
|
||||||
if (!!this.props.add) {
|
if (!!props.add) {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/influxdb/create">
|
<Link to="influxdb/create">
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/influxdb/edit">
|
<Link to="influxdb/edit">
|
||||||
<EditOutlined />
|
<EditOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
</Popconfirm>,
|
</Popconfirm>,
|
||||||
];
|
];
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col span={8}>
|
|
||||||
<Card
|
|
||||||
title="InfluxDB"
|
|
||||||
className="integration-card"
|
|
||||||
cover={<img alt="InfluxDB" src="/integrations/influxdb.png" style={{ padding: 1 }} />}
|
|
||||||
actions={actions}
|
|
||||||
>
|
|
||||||
<Card.Meta description="The InfluxDB integration writes events into an InfluxDB time-series database." />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col span={8}>
|
||||||
|
<Card
|
||||||
|
title="InfluxDB"
|
||||||
|
className="integration-card"
|
||||||
|
cover={<img alt="InfluxDB" src="/integrations/influxdb.png" style={{ padding: 1 }} />}
|
||||||
|
actions={actions}
|
||||||
|
>
|
||||||
|
<Card.Meta description="The InfluxDB integration writes events into an InfluxDB time-series database." />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InfluxdbCard;
|
export default InfluxdbCard;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Col, Card, Popconfirm } from "antd";
|
import { Col, Card, Popconfirm } from "antd";
|
||||||
@ -13,46 +12,44 @@ interface IProps {
|
|||||||
add?: boolean;
|
add?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoRaCloudCard extends Component<IProps> {
|
function LoRaCloudCard(props: IProps) {
|
||||||
onDelete = () => {
|
const onDelete = () => {
|
||||||
let req = new DeleteLoraCloudIntegrationRequest();
|
let req = new DeleteLoraCloudIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
ApplicationStore.deleteLoraCloudIntegration(req, () => {});
|
ApplicationStore.deleteLoraCloudIntegration(req, () => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
let actions: any[] = [];
|
||||||
let actions: any[] = [];
|
|
||||||
|
|
||||||
if (!!this.props.add) {
|
if (!!props.add) {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/loracloud/create">
|
<Link to="loracloud/create">
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/loracloud/edit">
|
<Link to="loracloud/edit">
|
||||||
<EditOutlined />
|
<EditOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
</Popconfirm>,
|
</Popconfirm>,
|
||||||
];
|
];
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col span={8}>
|
|
||||||
<Card
|
|
||||||
title="Semtech LoRa Cloud™"
|
|
||||||
className="integration-card"
|
|
||||||
cover={<img alt="Semtech LoRa Cloud" src="/integrations/loracloud.png" style={{ padding: 1 }} />}
|
|
||||||
actions={actions}
|
|
||||||
>
|
|
||||||
<Card.Meta description="The Semtech LoRa Cloud integration provides Modem & Geolocation Services." />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col span={8}>
|
||||||
|
<Card
|
||||||
|
title="Semtech LoRa Cloud™"
|
||||||
|
className="integration-card"
|
||||||
|
cover={<img alt="Semtech LoRa Cloud" src="/integrations/loracloud.png" style={{ padding: 1 }} />}
|
||||||
|
actions={actions}
|
||||||
|
>
|
||||||
|
<Card.Meta description="The Semtech LoRa Cloud integration provides Modem & Geolocation Services." />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LoRaCloudCard;
|
export default LoRaCloudCard;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { Form, Input, InputNumber, Switch, Button, Tabs, Collapse } from "antd";
|
import { Form, Input, InputNumber, Switch, Button, Tabs, Collapse } from "antd";
|
||||||
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
||||||
@ -13,43 +13,28 @@ interface IProps {
|
|||||||
onFinish: (obj: LoraCloudIntegration) => void;
|
onFinish: (obj: LoraCloudIntegration) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function LoRaCloudIntegrationForm(props: IProps) {
|
||||||
modemEnabled: boolean;
|
const [modemEnabled, setModemEnabled] = useState<boolean>(false);
|
||||||
geolocationTdoa: boolean;
|
const [geolocationTdoa, setGeolocationTdoa] = useState<boolean>(false);
|
||||||
geolocationRssi: boolean;
|
const [geolocationRssi, setGeolocationRssi] = useState<boolean>(false);
|
||||||
geolocationWifi: boolean;
|
const [geolocationWifi, setGeolocationWifi] = useState<boolean>(false);
|
||||||
geolocationGnss: boolean;
|
const [geolocationGnss, setGeolocationGnss] = useState<boolean>(false);
|
||||||
}
|
|
||||||
|
|
||||||
class LoRaCloudIntegrationForm extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
const v = props.initialValues;
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
modemEnabled: false,
|
|
||||||
geolocationTdoa: false,
|
|
||||||
geolocationRssi: false,
|
|
||||||
geolocationWifi: false,
|
|
||||||
geolocationGnss: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const v = this.props.initialValues;
|
|
||||||
const mgs = v.getModemGeolocationServices();
|
const mgs = v.getModemGeolocationServices();
|
||||||
|
|
||||||
if (mgs !== undefined) {
|
if (mgs !== undefined) {
|
||||||
this.setState({
|
setModemEnabled(mgs.getModemEnabled());
|
||||||
modemEnabled: mgs.getModemEnabled(),
|
setGeolocationTdoa(mgs.getGeolocationTdoa());
|
||||||
geolocationTdoa: mgs.getGeolocationTdoa(),
|
setGeolocationRssi(mgs.getGeolocationRssi());
|
||||||
geolocationRssi: mgs.getGeolocationRssi(),
|
setGeolocationWifi(mgs.getGeolocationWifi());
|
||||||
geolocationWifi: mgs.getGeolocationWifi(),
|
setGeolocationGnss(mgs.getGeolocationGnss());
|
||||||
geolocationGnss: mgs.getGeolocationGnss(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}, [props]);
|
||||||
|
|
||||||
onFinish = (values: LoraCloudIntegration.AsObject) => {
|
const onFinish = (values: LoraCloudIntegration.AsObject) => {
|
||||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
const v = Object.assign(props.initialValues.toObject(), values);
|
||||||
const mgsv = v.modemGeolocationServices;
|
const mgsv = v.modemGeolocationServices;
|
||||||
|
|
||||||
let mgs = new LoraCloudModemGeolocationServices();
|
let mgs = new LoraCloudModemGeolocationServices();
|
||||||
@ -76,202 +61,194 @@ class LoRaCloudIntegrationForm extends Component<IProps, IState> {
|
|||||||
i.setApplicationId(v.applicationId);
|
i.setApplicationId(v.applicationId);
|
||||||
i.setModemGeolocationServices(mgs);
|
i.setModemGeolocationServices(mgs);
|
||||||
|
|
||||||
this.props.onFinish(i);
|
props.onFinish(i);
|
||||||
};
|
};
|
||||||
|
|
||||||
onModemEnabledChange = (v: boolean) => {
|
const onModemEnabledChange = (v: boolean) => {
|
||||||
this.setState({
|
setModemEnabled(v);
|
||||||
modemEnabled: v,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onGeolocationTdoaChange = (v: boolean) => {
|
const onGeolocationTdoaChange = (v: boolean) => {
|
||||||
this.setState({
|
setGeolocationTdoa(v);
|
||||||
geolocationTdoa: v,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onGeolocationRssiChange = (v: boolean) => {
|
const onGeolocationRssiChange = (v: boolean) => {
|
||||||
this.setState({
|
setGeolocationRssi(v);
|
||||||
geolocationRssi: v,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onGeolocationWifiChange = (v: boolean) => {
|
const onGeolocationWifiChange = (v: boolean) => {
|
||||||
this.setState({
|
setGeolocationWifi(v);
|
||||||
geolocationWifi: v,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onGeolocationGnssChange = (v: boolean) => {
|
const onGeolocationGnssChange = (v: boolean) => {
|
||||||
this.setState({
|
setGeolocationGnss(v);
|
||||||
geolocationGnss: v,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
<Tabs>
|
||||||
<Tabs>
|
<Tabs.TabPane tab="Modem & Geolocation Services" key="1">
|
||||||
<Tabs.TabPane tab="Modem & Geolocation Services" key="1">
|
<Form.Item
|
||||||
|
label="Token"
|
||||||
|
name={["modemGeolocationServices", "token"]}
|
||||||
|
tooltip="This token can be obtained from loracloud.com"
|
||||||
|
rules={[{ required: true, message: "Please enter a token!" }]}
|
||||||
|
>
|
||||||
|
<Input type="password" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={["modemGeolocationServices", "modemEnabled"]}
|
||||||
|
label="I am using LoRa Edge™ LR1110 or my device uses LoRa Basics™ Modem-E"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch onChange={onModemEnabledChange} />
|
||||||
|
</Form.Item>
|
||||||
|
{modemEnabled && (
|
||||||
|
<Form.List name={["modemGeolocationServices", "forwardFPortsList"]}>
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<Form.Item label="Forward messages on these FPorts to LoRa Cloud">
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<Form.Item
|
||||||
|
{...field}
|
||||||
|
rules={[{ required: true, message: "Please a FPort value!" }]}
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
width: "100px",
|
||||||
|
marginRight: "24px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
max={255}
|
||||||
|
addonAfter={<MinusCircleOutlined onClick={() => remove(index)} />}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
<Button type="dashed" onClick={() => add()} icon={<PlusOutlined />} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
)}
|
||||||
|
{modemEnabled && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Token"
|
label="Use receive timestamp for GNSS geolocation"
|
||||||
name={["modemGeolocationServices", "token"]}
|
name={["modemGeolocationServices", "gnssUseRxTime"]}
|
||||||
tooltip="This token can be obtained from loracloud.com"
|
tooltip="If enabled, the receive timestamp of the gateway will be used as reference instead of the timestamp included in the GNSS payload."
|
||||||
rules={[{ required: true, message: "Please enter a token!" }]}
|
|
||||||
>
|
|
||||||
<Input type="password" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={["modemGeolocationServices", "modemEnabled"]}
|
|
||||||
label="I am using LoRa Edge™ LR1110 or my device uses LoRa Basics™ Modem-E"
|
|
||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
>
|
>
|
||||||
<Switch onChange={this.onModemEnabledChange} />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{this.state.modemEnabled && (
|
)}
|
||||||
<Form.List name={["modemGeolocationServices", "forwardFPortsList"]}>
|
{modemEnabled && (
|
||||||
{(fields, { add, remove }) => (
|
<Form.Item
|
||||||
<Form.Item label="Forward messages on these FPorts to LoRa Cloud">
|
label="Use location of receiving gateways for assistance"
|
||||||
{fields.map((field, index) => (
|
name={["modemGeolocationServices", "gnssUseGatewayLocation"]}
|
||||||
<Form.Item
|
tooltip="If enabled, the gateway location will be provided to the geolocation resolver to aid the resolving process."
|
||||||
{...field}
|
valuePropName="checked"
|
||||||
rules={[{ required: true, message: "Please a FPort value!" }]}
|
>
|
||||||
style={{ display: "inline-block", width: "100px", marginRight: "24px" }}
|
<Switch />
|
||||||
>
|
</Form.Item>
|
||||||
<InputNumber
|
)}
|
||||||
min={1}
|
{modemEnabled && (
|
||||||
max={255}
|
<Form.Item
|
||||||
addonAfter={<MinusCircleOutlined onClick={() => remove(index)} />}
|
label="My device adheres to the LoRa Edge™ Tracker Modem-E Version Reference Design protocol"
|
||||||
/>
|
name={["modemGeolocationServices", "parseTlv"]}
|
||||||
</Form.Item>
|
tooltip="If enabled, ChirpStack will try to resolve the location of the device if a geolocation payload is detected."
|
||||||
))}
|
valuePropName="checked"
|
||||||
<Button type="dashed" onClick={() => add()} icon={<PlusOutlined />} />
|
>
|
||||||
</Form.Item>
|
<Switch />
|
||||||
)}
|
</Form.Item>
|
||||||
</Form.List>
|
)}
|
||||||
)}
|
<Collapse style={{ marginBottom: 24 }}>
|
||||||
{this.state.modemEnabled && (
|
<Collapse.Panel header="Advanced geolocation options" key={1}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Use receive timestamp for GNSS geolocation"
|
label="TDOA based geolocation"
|
||||||
name={["modemGeolocationServices", "gnssUseRxTime"]}
|
name={["modemGeolocationServices", "geolocationTdoa"]}
|
||||||
tooltip="If enabled, the receive timestamp of the gateway will be used as reference instead of the timestamp included in the GNSS payload."
|
tooltip="If enabled, geolocation will be based on time-difference of arrival (TDOA). Please note that this requires gateways that support the fine-timestamp feature."
|
||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
>
|
>
|
||||||
<Switch />
|
<Switch onChange={onGeolocationTdoaChange} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
|
||||||
{this.state.modemEnabled && (
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Use location of receiving gateways for assistance"
|
label="RSSI based geolocation"
|
||||||
name={["modemGeolocationServices", "gnssUseGatewayLocation"]}
|
name={["modemGeolocationServices", "geolocationRssi"]}
|
||||||
tooltip="If enabled, the gateway location will be provided to the geolocation resolver to aid the resolving process."
|
tooltip="If enabled, geolocation will be based on RSSI values reported by the receiving gateways."
|
||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
>
|
>
|
||||||
<Switch />
|
<Switch onChange={onGeolocationRssiChange} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
|
||||||
{this.state.modemEnabled && (
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="My device adheres to the LoRa Edge™ Tracker Modem-E Version Reference Design protocol"
|
label="Wi-Fi based geolocation"
|
||||||
name={["modemGeolocationServices", "parseTlv"]}
|
name={["modemGeolocationServices", "geolocationWifi"]}
|
||||||
tooltip="If enabled, ChirpStack will try to resolve the location of the device if a geolocation payload is detected."
|
tooltip="If enabled, geolocation will be based on Wi-Fi access-point data reported by the device."
|
||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
>
|
>
|
||||||
<Switch />
|
<Switch onChange={onGeolocationWifiChange} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
<Form.Item
|
||||||
<Collapse style={{ marginBottom: 24 }}>
|
label="GNSS based geolocation (LR1110)"
|
||||||
<Collapse.Panel header="Advanced geolocation options" key={1}>
|
name={["modemGeolocationServices", "geolocationGnss"]}
|
||||||
|
tooltip="If enabled, geolocation will be based on GNSS data reported by the device."
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch onChange={onGeolocationGnssChange} />
|
||||||
|
</Form.Item>
|
||||||
|
{(geolocationTdoa || geolocationRssi) && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="TDOA based geolocation"
|
label="Geolocation buffer (TTL in seconds)"
|
||||||
name={["modemGeolocationServices", "geolocationTdoa"]}
|
name={["modemGeolocationServices", "geolocationBufferTtl"]}
|
||||||
tooltip="If enabled, geolocation will be based on time-difference of arrival (TDOA). Please note that this requires gateways that support the fine-timestamp feature."
|
tooltip="The time in seconds that historical uplinks will be stored in the geolocation buffer. Used for TDOA and RSSI geolocation."
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={86400} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{(geolocationTdoa || geolocationRssi) && (
|
||||||
|
<Form.Item
|
||||||
|
label="Geolocation min buffer size"
|
||||||
|
name={["modemGeolocationServices", "geolocationMinBufferSize"]}
|
||||||
|
tooltip="The minimum buffer size required before using geolocation. Using multiple uplinks for geolocation can increase the accuracy of the geolocation results. Used for TDOA and RSSI geolocation."
|
||||||
|
>
|
||||||
|
<InputNumber min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{geolocationWifi && (
|
||||||
|
<Form.Item
|
||||||
|
label="Wifi payload field"
|
||||||
|
name={["modemGeolocationServices", "geolocationWifiPayloadField"]}
|
||||||
|
tooltip="This must match the name of the field in the decoded payload which holds array of Wifi access-points. Each element in the array must contain two keys: 1) macAddress: array of 6 bytes, 2) signalStrength: RSSI of the access-point."
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{geolocationGnss && (
|
||||||
|
<Form.Item
|
||||||
|
label="GNSS payload field"
|
||||||
|
name={["modemGeolocationServices", "geolocationGnssPayloadField"]}
|
||||||
|
tooltip="This must match the name of the field in the decoded payload which holds the LR1110 GNSS bytes."
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{geolocationGnss && (
|
||||||
|
<Form.Item
|
||||||
|
label="Use receive timestamp for GNSS geolocation"
|
||||||
|
name={["modemGeolocationServices", "geolocationGnssUseRxTime"]}
|
||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
>
|
>
|
||||||
<Switch onChange={this.onGeolocationTdoaChange} />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
)}
|
||||||
label="RSSI based geolocation"
|
</Collapse.Panel>
|
||||||
name={["modemGeolocationServices", "geolocationRssi"]}
|
</Collapse>
|
||||||
tooltip="If enabled, geolocation will be based on RSSI values reported by the receiving gateways."
|
</Tabs.TabPane>
|
||||||
valuePropName="checked"
|
</Tabs>
|
||||||
>
|
<Form.Item>
|
||||||
<Switch onChange={this.onGeolocationRssiChange} />
|
<Button type="primary" htmlType="submit">
|
||||||
</Form.Item>
|
Submit
|
||||||
<Form.Item
|
</Button>
|
||||||
label="Wi-Fi based geolocation"
|
</Form.Item>
|
||||||
name={["modemGeolocationServices", "geolocationWifi"]}
|
</Form>
|
||||||
tooltip="If enabled, geolocation will be based on Wi-Fi access-point data reported by the device."
|
);
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch onChange={this.onGeolocationWifiChange} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label="GNSS based geolocation (LR1110)"
|
|
||||||
name={["modemGeolocationServices", "geolocationGnss"]}
|
|
||||||
tooltip="If enabled, geolocation will be based on GNSS data reported by the device."
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch onChange={this.onGeolocationGnssChange} />
|
|
||||||
</Form.Item>
|
|
||||||
{(this.state.geolocationTdoa || this.state.geolocationRssi) && (
|
|
||||||
<Form.Item
|
|
||||||
label="Geolocation buffer (TTL in seconds)"
|
|
||||||
name={["modemGeolocationServices", "geolocationBufferTtl"]}
|
|
||||||
tooltip="The time in seconds that historical uplinks will be stored in the geolocation buffer. Used for TDOA and RSSI geolocation."
|
|
||||||
>
|
|
||||||
<InputNumber min={0} max={86400} />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
{(this.state.geolocationTdoa || this.state.geolocationRssi) && (
|
|
||||||
<Form.Item
|
|
||||||
label="Geolocation min buffer size"
|
|
||||||
name={["modemGeolocationServices", "geolocationMinBufferSize"]}
|
|
||||||
tooltip="The minimum buffer size required before using geolocation. Using multiple uplinks for geolocation can increase the accuracy of the geolocation results. Used for TDOA and RSSI geolocation."
|
|
||||||
>
|
|
||||||
<InputNumber min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
{this.state.geolocationWifi && (
|
|
||||||
<Form.Item
|
|
||||||
label="Wifi payload field"
|
|
||||||
name={["modemGeolocationServices", "geolocationWifiPayloadField"]}
|
|
||||||
tooltip="This must match the name of the field in the decoded payload which holds array of Wifi access-points. Each element in the array must contain two keys: 1) macAddress: array of 6 bytes, 2) signalStrength: RSSI of the access-point."
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
{this.state.geolocationGnss && (
|
|
||||||
<Form.Item
|
|
||||||
label="GNSS payload field"
|
|
||||||
name={["modemGeolocationServices", "geolocationGnssPayloadField"]}
|
|
||||||
tooltip="This must match the name of the field in the decoded payload which holds the LR1110 GNSS bytes."
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
{this.state.geolocationGnss && (
|
|
||||||
<Form.Item
|
|
||||||
label="Use receive timestamp for GNSS geolocation"
|
|
||||||
name={["modemGeolocationServices", "geolocationGnssUseRxTime"]}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</Collapse.Panel>
|
|
||||||
</Collapse>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
</Tabs>
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="primary" htmlType="submit">
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LoRaCloudIntegrationForm;
|
export default LoRaCloudIntegrationForm;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Col, Card } from "antd";
|
import { Col, Card } from "antd";
|
||||||
@ -9,23 +8,21 @@ interface IProps {
|
|||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
class HttpCard extends Component<IProps> {
|
function MqttCard(props: IProps) {
|
||||||
render() {
|
let actions: any[] = [<Link to="mqtt/certificate">Get certificate</Link>];
|
||||||
let actions: any[] = [<Link to="integrations/mqtt/certificate">Get certificate</Link>];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Card
|
<Card
|
||||||
title="MQTT"
|
title="MQTT"
|
||||||
className="integration-card"
|
className="integration-card"
|
||||||
cover={<img alt="MQTT" src="/integrations/mqtt.png" style={{ padding: 1 }} />}
|
cover={<img alt="MQTT" src="/integrations/mqtt.png" style={{ padding: 1 }} />}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
>
|
>
|
||||||
<Card.Meta description="The MQTT integration forwards events to a MQTT broker." />
|
<Card.Meta description="The MQTT integration forwards events to a MQTT broker." />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HttpCard;
|
export default MqttCard;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Col, Card, Popconfirm } from "antd";
|
import { Col, Card, Popconfirm } from "antd";
|
||||||
@ -13,46 +12,44 @@ interface IProps {
|
|||||||
add?: boolean;
|
add?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyDevicesCard extends Component<IProps> {
|
function MyDevicesCard(props: IProps) {
|
||||||
onDelete = () => {
|
const onDelete = () => {
|
||||||
let req = new DeleteMyDevicesIntegrationRequest();
|
let req = new DeleteMyDevicesIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
ApplicationStore.deleteMyDevicesIntegration(req, () => {});
|
ApplicationStore.deleteMyDevicesIntegration(req, () => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
let actions: any[] = [];
|
||||||
let actions: any[] = [];
|
|
||||||
|
|
||||||
if (!!this.props.add) {
|
if (!!props.add) {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/mydevices/create">
|
<Link to="mydevices/create">
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/mydevices/edit">
|
<Link to="mydevices/edit">
|
||||||
<EditOutlined />
|
<EditOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
</Popconfirm>,
|
</Popconfirm>,
|
||||||
];
|
];
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col span={8}>
|
|
||||||
<Card
|
|
||||||
title="myDevices"
|
|
||||||
className="integration-card"
|
|
||||||
cover={<img alt="myDevices" src="/integrations/my_devices.png" style={{ padding: 1 }} />}
|
|
||||||
actions={actions}
|
|
||||||
>
|
|
||||||
<Card.Meta description="The myDevices integration forwards events to the myDevices platform." />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col span={8}>
|
||||||
|
<Card
|
||||||
|
title="myDevices"
|
||||||
|
className="integration-card"
|
||||||
|
cover={<img alt="myDevices" src="/integrations/my_devices.png" style={{ padding: 1 }} />}
|
||||||
|
actions={actions}
|
||||||
|
>
|
||||||
|
<Card.Meta description="The myDevices integration forwards events to the myDevices platform." />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MyDevicesCard;
|
export default MyDevicesCard;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { Form, Input, Button, Select } from "antd";
|
import { Form, Input, Button, Select } from "antd";
|
||||||
|
|
||||||
@ -9,79 +9,63 @@ interface IProps {
|
|||||||
onFinish: (obj: MyDevicesIntegration) => void;
|
onFinish: (obj: MyDevicesIntegration) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function MyDevicesIntegrationForm(props: IProps) {
|
||||||
selectedEndpoint: string;
|
const [selectedEndpoint, setSelectedEndpoint] = useState<string>("");
|
||||||
customEndpoint: string;
|
const [customEndpoint, setCustomEndpoint] = useState<string>("");
|
||||||
}
|
|
||||||
|
|
||||||
class MyDevicesIntegrationForm extends Component<IProps, IState> {
|
const onFinish = (values: MyDevicesIntegration.AsObject) => {
|
||||||
constructor(props: IProps) {
|
const v = Object.assign(props.initialValues.toObject(), values);
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
selectedEndpoint: "",
|
|
||||||
customEndpoint: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onFinish = (values: MyDevicesIntegration.AsObject) => {
|
|
||||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
|
||||||
let i = new MyDevicesIntegration();
|
let i = new MyDevicesIntegration();
|
||||||
|
|
||||||
i.setApplicationId(v.applicationId);
|
i.setApplicationId(v.applicationId);
|
||||||
if (v.endpoint === "custom") {
|
if (v.endpoint === "custom") {
|
||||||
i.setEndpoint(this.state.customEndpoint);
|
i.setEndpoint(customEndpoint);
|
||||||
} else {
|
} else {
|
||||||
i.setEndpoint(v.endpoint);
|
i.setEndpoint(v.endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onFinish(i);
|
props.onFinish(i);
|
||||||
};
|
};
|
||||||
|
|
||||||
onEndpointChange = (v: string) => {
|
const onEndpointChange = (v: string) => {
|
||||||
this.setState({
|
setSelectedEndpoint(v);
|
||||||
selectedEndpoint: v,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onCustomEndpointChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const onCustomEndpointChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({
|
setCustomEndpoint(e.target.value);
|
||||||
customEndpoint: e.target.value,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
<Form.Item
|
||||||
|
label="Select myDevices endpoint"
|
||||||
|
name="endpoint"
|
||||||
|
rules={[{ required: true, message: "Please select a myDevices endpoint!" }]}
|
||||||
|
>
|
||||||
|
<Select onChange={onEndpointChange}>
|
||||||
|
<Select.Option value="https://lora.mydevices.com/v1/networks/chirpstackio/uplink">Cayenne</Select.Option>
|
||||||
|
<Select.Option value="https://lora.iotinabox.com/v1/networks/iotinabox.chirpstackio/uplink">
|
||||||
|
IoT in a Box
|
||||||
|
</Select.Option>
|
||||||
|
<Select.Option value="custom">Custom endpoint URL</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
{selectedEndpoint === "custom" && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Select myDevices endpoint"
|
label="myDevices API endpoint"
|
||||||
name="endpoint"
|
name="customEndpoint"
|
||||||
rules={[{ required: true, message: "Please select a myDevices endpoint!" }]}
|
rules={[{ required: true, message: "Please enter an API endpoint!" }]}
|
||||||
>
|
>
|
||||||
<Select onChange={this.onEndpointChange}>
|
<Input onChange={onCustomEndpointChange} />
|
||||||
<Select.Option value="https://lora.mydevices.com/v1/networks/chirpstackio/uplink">Cayenne</Select.Option>
|
|
||||||
<Select.Option value="https://lora.iotinabox.com/v1/networks/iotinabox.chirpstackio/uplink">
|
|
||||||
IoT in a Box
|
|
||||||
</Select.Option>
|
|
||||||
<Select.Option value="custom">Custom endpoint URL</Select.Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{this.state.selectedEndpoint === "custom" && (
|
)}
|
||||||
<Form.Item
|
<Form.Item>
|
||||||
label="myDevices API endpoint"
|
<Button type="primary" htmlType="submit">
|
||||||
name="customEndpoint"
|
Submit
|
||||||
rules={[{ required: true, message: "Please enter an API endpoint!" }]}
|
</Button>
|
||||||
>
|
</Form.Item>
|
||||||
<Input onChange={this.onCustomEndpointChange} />
|
</Form>
|
||||||
</Form.Item>
|
);
|
||||||
)}
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="primary" htmlType="submit">
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MyDevicesIntegrationForm;
|
export default MyDevicesIntegrationForm;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Col, Card, Popconfirm } from "antd";
|
import { Col, Card, Popconfirm } from "antd";
|
||||||
@ -16,46 +15,44 @@ interface IProps {
|
|||||||
add?: boolean;
|
add?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PilotThingsCard extends Component<IProps> {
|
function PilotThingsCard(props: IProps) {
|
||||||
onDelete = () => {
|
const onDelete = () => {
|
||||||
let req = new DeletePilotThingsIntegrationRequest();
|
let req = new DeletePilotThingsIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
ApplicationStore.deletePilotThingsIntegration(req, () => {});
|
ApplicationStore.deletePilotThingsIntegration(req, () => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
let actions: any[] = [];
|
||||||
let actions: any[] = [];
|
|
||||||
|
|
||||||
if (!!this.props.add) {
|
if (!!props.add) {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/pilot-things/create">
|
<Link to="pilot-things/create">
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/pilot-things/edit">
|
<Link to="pilot-things/edit">
|
||||||
<EditOutlined />
|
<EditOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
</Popconfirm>,
|
</Popconfirm>,
|
||||||
];
|
];
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col span={8}>
|
|
||||||
<Card
|
|
||||||
title="Pilot Things"
|
|
||||||
className="integration-card"
|
|
||||||
cover={<img alt="Pilot Things" src="/integrations/pilot_things.png" style={{ padding: 1 }} />}
|
|
||||||
actions={actions}
|
|
||||||
>
|
|
||||||
<Card.Meta description="The Pilot Things integration forwards messages to a Pilot Things instance." />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col span={8}>
|
||||||
|
<Card
|
||||||
|
title="Pilot Things"
|
||||||
|
className="integration-card"
|
||||||
|
cover={<img alt="Pilot Things" src="/integrations/pilot_things.png" style={{ padding: 1 }} />}
|
||||||
|
actions={actions}
|
||||||
|
>
|
||||||
|
<Card.Meta description="The Pilot Things integration forwards messages to a Pilot Things instance." />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PilotThingsCard;
|
export default PilotThingsCard;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
|
|
||||||
import { Form, Input, Button } from "antd";
|
import { Form, Input, Button } from "antd";
|
||||||
|
|
||||||
import { PilotThingsIntegration } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
import { PilotThingsIntegration } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||||
@ -9,43 +7,41 @@ interface IProps {
|
|||||||
onFinish: (obj: PilotThingsIntegration) => void;
|
onFinish: (obj: PilotThingsIntegration) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PilotThingsIntegrationForm extends Component<IProps> {
|
function PilotThingsIntegrationForm(props: IProps) {
|
||||||
onFinish = (values: PilotThingsIntegration.AsObject) => {
|
const onFinish = (values: PilotThingsIntegration.AsObject) => {
|
||||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
const v = Object.assign(props.initialValues.toObject(), values);
|
||||||
let i = new PilotThingsIntegration();
|
let i = new PilotThingsIntegration();
|
||||||
|
|
||||||
i.setApplicationId(v.applicationId);
|
i.setApplicationId(v.applicationId);
|
||||||
i.setServer(v.server);
|
i.setServer(v.server);
|
||||||
i.setToken(v.token);
|
i.setToken(v.token);
|
||||||
|
|
||||||
this.props.onFinish(i);
|
props.onFinish(i);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
<Form.Item
|
||||||
<Form.Item
|
label="Pilot Things server"
|
||||||
label="Pilot Things server"
|
name="server"
|
||||||
name="server"
|
rules={[{ required: true, message: "Please enter a Pilot Things server!" }]}
|
||||||
rules={[{ required: true, message: "Please enter a Pilot Things server!" }]}
|
>
|
||||||
>
|
<Input placeholder="https://host:port" />
|
||||||
<Input placeholder="https://host:port" />
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
<Form.Item
|
label="Authentication token"
|
||||||
label="Authentication token"
|
name="token"
|
||||||
name="token"
|
rules={[{ required: true, message: "Please enter a Pilot Things token!" }]}
|
||||||
rules={[{ required: true, message: "Please enter a Pilot Things token!" }]}
|
>
|
||||||
>
|
<Input.Password />
|
||||||
<Input.Password />
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item>
|
||||||
<Form.Item>
|
<Button type="primary" htmlType="submit">
|
||||||
<Button type="primary" htmlType="submit">
|
Submit
|
||||||
Submit
|
</Button>
|
||||||
</Button>
|
</Form.Item>
|
||||||
</Form.Item>
|
</Form>
|
||||||
</Form>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PilotThingsIntegrationForm;
|
export default PilotThingsIntegrationForm;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Col, Card, Popconfirm } from "antd";
|
import { Col, Card, Popconfirm } from "antd";
|
||||||
@ -16,46 +15,44 @@ interface IProps {
|
|||||||
add?: boolean;
|
add?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ThingsBoardCard extends Component<IProps> {
|
function ThingsBoardCard(props: IProps) {
|
||||||
onDelete = () => {
|
const onDelete = () => {
|
||||||
let req = new DeleteThingsBoardIntegrationRequest();
|
let req = new DeleteThingsBoardIntegrationRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
ApplicationStore.deleteThingsBoardIntegration(req, () => {});
|
ApplicationStore.deleteThingsBoardIntegration(req, () => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
let actions: any[] = [];
|
||||||
let actions: any[] = [];
|
|
||||||
|
|
||||||
if (!!this.props.add) {
|
if (!!props.add) {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/thingsboard/create">
|
<Link to="thingsboard/create">
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
actions = [
|
actions = [
|
||||||
<Link to="integrations/thingsboard/edit">
|
<Link to="thingsboard/edit">
|
||||||
<EditOutlined />
|
<EditOutlined />
|
||||||
</Link>,
|
</Link>,
|
||||||
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={this.onDelete}>
|
<Popconfirm title="Are you sure you want to delete this integration?" onConfirm={onDelete}>
|
||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
</Popconfirm>,
|
</Popconfirm>,
|
||||||
];
|
];
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col span={8}>
|
|
||||||
<Card
|
|
||||||
title="ThingsBoard"
|
|
||||||
className="integration-card"
|
|
||||||
cover={<img alt="ThingsBoard" src="/integrations/thingsboard.png" style={{ padding: 1 }} />}
|
|
||||||
actions={actions}
|
|
||||||
>
|
|
||||||
<Card.Meta description="The ThingsBoard integration forwards events to a ThingsBoard instance." />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col span={8}>
|
||||||
|
<Card
|
||||||
|
title="ThingsBoard"
|
||||||
|
className="integration-card"
|
||||||
|
cover={<img alt="ThingsBoard" src="/integrations/thingsboard.png" style={{ padding: 1 }} />}
|
||||||
|
actions={actions}
|
||||||
|
>
|
||||||
|
<Card.Meta description="The ThingsBoard integration forwards events to a ThingsBoard instance." />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ThingsBoardCard;
|
export default ThingsBoardCard;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
|
|
||||||
import { Form, Input, Button, Typography } from "antd";
|
import { Form, Input, Button, Typography } from "antd";
|
||||||
|
|
||||||
import { ThingsBoardIntegration } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
import { ThingsBoardIntegration } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||||
@ -9,41 +7,44 @@ interface IProps {
|
|||||||
onFinish: (obj: ThingsBoardIntegration) => void;
|
onFinish: (obj: ThingsBoardIntegration) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ThingsBoardIntegrationForm extends Component<IProps> {
|
function ThingsBoardIntegrationForm(props: IProps) {
|
||||||
onFinish = (values: ThingsBoardIntegration.AsObject) => {
|
const onFinish = (values: ThingsBoardIntegration.AsObject) => {
|
||||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
const v = Object.assign(props.initialValues.toObject(), values);
|
||||||
let i = new ThingsBoardIntegration();
|
let i = new ThingsBoardIntegration();
|
||||||
|
|
||||||
i.setApplicationId(v.applicationId);
|
i.setApplicationId(v.applicationId);
|
||||||
i.setServer(v.server);
|
i.setServer(v.server);
|
||||||
|
|
||||||
this.props.onFinish(i);
|
props.onFinish(i);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish}>
|
||||||
<Form layout="vertical" initialValues={this.props.initialValues.toObject()} onFinish={this.onFinish}>
|
<Form.Item
|
||||||
<Form.Item
|
label="ThingsBoard server"
|
||||||
label="ThingsBoard server"
|
name="server"
|
||||||
name="server"
|
rules={[
|
||||||
rules={[{ required: true, message: "Please enter the address to the ThingsBoard server!" }]}
|
{
|
||||||
>
|
required: true,
|
||||||
<Input placeholder="http://host:port" />
|
message: "Please enter the address to the ThingsBoard server!",
|
||||||
</Form.Item>
|
},
|
||||||
<Form.Item>
|
]}
|
||||||
<Typography.Paragraph>
|
>
|
||||||
Each device must have a 'ThingsBoardAccessToken' variable assigned. This access-token is generated by
|
<Input placeholder="http://host:port" />
|
||||||
ThingsBoard.
|
</Form.Item>
|
||||||
</Typography.Paragraph>
|
<Form.Item>
|
||||||
</Form.Item>
|
<Typography.Paragraph>
|
||||||
<Form.Item>
|
Each device must have a 'ThingsBoardAccessToken' variable assigned. This access-token is generated by
|
||||||
<Button type="primary" htmlType="submit">
|
ThingsBoard.
|
||||||
Submit
|
</Typography.Paragraph>
|
||||||
</Button>
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item>
|
||||||
</Form>
|
<Button type="primary" htmlType="submit">
|
||||||
);
|
Submit
|
||||||
}
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ThingsBoardIntegrationForm;
|
export default ThingsBoardIntegrationForm;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { presetPalettes } from "@ant-design/colors";
|
import { presetPalettes } from "@ant-design/colors";
|
||||||
import { Space, Breadcrumb, Card, Row, Col, PageHeader, Empty } from "antd";
|
import { Space, Breadcrumb, Card, Row, Col, Empty } from "antd";
|
||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { LatLngTuple, PointTuple } from "leaflet";
|
import { LatLngTuple, PointTuple } from "leaflet";
|
||||||
@ -27,167 +28,125 @@ import InternalStore from "../../stores/InternalStore";
|
|||||||
import GatewayStore from "../../stores/GatewayStore";
|
import GatewayStore from "../../stores/GatewayStore";
|
||||||
import Map, { Marker, MarkerColor } from "../../components/Map";
|
import Map, { Marker, MarkerColor } from "../../components/Map";
|
||||||
|
|
||||||
interface GatewaysMapState {
|
function GatewaysMap() {
|
||||||
items: GatewayListItem[];
|
const [items, setItems] = useState<GatewayListItem[]>([]);
|
||||||
}
|
|
||||||
|
|
||||||
class GatewaysMap extends Component<{}, GatewaysMapState> {
|
useEffect(() => {
|
||||||
constructor(props: {}) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
items: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadData = () => {
|
|
||||||
let req = new ListGatewaysRequest();
|
let req = new ListGatewaysRequest();
|
||||||
req.setLimit(9999);
|
req.setLimit(9999);
|
||||||
GatewayStore.list(req, (resp: ListGatewaysResponse) => {
|
GatewayStore.list(req, (resp: ListGatewaysResponse) => {
|
||||||
this.setState({
|
setItems(resp.getResultList());
|
||||||
items: resp.getResultList(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
return <Empty />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const boundsOptions: {
|
||||||
|
padding: PointTuple;
|
||||||
|
} = {
|
||||||
|
padding: [50, 50],
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
let bounds: LatLngTuple[] = [];
|
||||||
if (this.state.items.length === 0) {
|
let markers: any[] = [];
|
||||||
return <Empty />;
|
|
||||||
|
for (const item of items) {
|
||||||
|
const pos: LatLngTuple = [item.getLocation()!.getLatitude(), item.getLocation()!.getLongitude()];
|
||||||
|
bounds.push(pos);
|
||||||
|
|
||||||
|
let color: MarkerColor = "orange";
|
||||||
|
let lastSeen: string = "Never seen online";
|
||||||
|
|
||||||
|
if (item.getState() === GatewayState.OFFLINE) {
|
||||||
|
color = "red";
|
||||||
|
} else if (item.getState() === GatewayState.ONLINE) {
|
||||||
|
color = "green";
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundsOptions: {
|
if (item.getLastSeenAt() !== undefined) {
|
||||||
padding: PointTuple;
|
let ts = moment(item.getLastSeenAt()!.toDate());
|
||||||
} = {
|
lastSeen = ts.fromNow();
|
||||||
padding: [50, 50],
|
|
||||||
};
|
|
||||||
|
|
||||||
let bounds: LatLngTuple[] = [];
|
|
||||||
let markers: any[] = [];
|
|
||||||
|
|
||||||
for (const item of this.state.items) {
|
|
||||||
const pos: LatLngTuple = [item.getLocation()!.getLatitude(), item.getLocation()!.getLongitude()];
|
|
||||||
bounds.push(pos);
|
|
||||||
|
|
||||||
let color: MarkerColor = "orange";
|
|
||||||
let lastSeen: string = "Never seen online";
|
|
||||||
|
|
||||||
if (item.getState() === GatewayState.OFFLINE) {
|
|
||||||
color = "red";
|
|
||||||
} else if (item.getState() === GatewayState.ONLINE) {
|
|
||||||
color = "green";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.getLastSeenAt() !== undefined) {
|
|
||||||
let ts = moment(item.getLastSeenAt()!.toDate());
|
|
||||||
lastSeen = ts.fromNow();
|
|
||||||
}
|
|
||||||
|
|
||||||
markers.push(
|
|
||||||
<Marker position={pos} faIcon="wifi" color={color}>
|
|
||||||
<Popup>
|
|
||||||
<Link to={`/tenants/${item.getTenantId()}/gateways/${item.getGatewayId()}`}>{item.getName()}</Link>
|
|
||||||
<br />
|
|
||||||
{item.getGatewayId()}
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
{lastSeen}
|
|
||||||
</Popup>
|
|
||||||
</Marker>,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
markers.push(
|
||||||
<Map height={500} bounds={bounds} boundsOptions={boundsOptions}>
|
<Marker position={pos} faIcon="wifi" color={color}>
|
||||||
{markers}
|
<Popup>
|
||||||
</Map>
|
<Link to={`/tenants/${item.getTenantId()}/gateways/${item.getGatewayId()}`}>{item.getName()}</Link>
|
||||||
|
<br />
|
||||||
|
{item.getGatewayId()}
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
{lastSeen}
|
||||||
|
</Popup>
|
||||||
|
</Marker>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Map height={500} bounds={bounds} boundsOptions={boundsOptions}>
|
||||||
|
{markers}
|
||||||
|
</Map>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GatewayProps {
|
function DevicesActiveInactive({ summary }: { summary?: GetDevicesSummaryResponse }) {
|
||||||
summary?: GetGatewaysSummaryResponse;
|
if (
|
||||||
}
|
summary === undefined ||
|
||||||
|
(summary.getNeverSeenCount() === 0 && summary.getInactiveCount() === 0 && summary.getActiveCount() === 0)
|
||||||
class GatewaysActiveInactive extends Component<GatewayProps> {
|
) {
|
||||||
render() {
|
return <Empty />;
|
||||||
if (
|
|
||||||
this.props.summary === undefined ||
|
|
||||||
(this.props.summary.getNeverSeenCount() === 0 &&
|
|
||||||
this.props.summary.getOfflineCount() === 0 &&
|
|
||||||
this.props.summary.getOnlineCount() === 0)
|
|
||||||
) {
|
|
||||||
return <Empty />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
labels: ["Never seen", "Offline", "Online"],
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
data: [
|
|
||||||
this.props.summary.getNeverSeenCount(),
|
|
||||||
this.props.summary.getOfflineCount(),
|
|
||||||
this.props.summary.getOnlineCount(),
|
|
||||||
],
|
|
||||||
backgroundColor: [presetPalettes.orange.primary, presetPalettes.red.primary, presetPalettes.green.primary],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const options: {
|
|
||||||
animation: false;
|
|
||||||
} = {
|
|
||||||
animation: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
return <Doughnut data={data} options={options} className="chart-doughtnut" />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
labels: ["Never seen", "Inactive", "Active"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: [summary.getNeverSeenCount(), summary.getInactiveCount(), summary.getActiveCount()],
|
||||||
|
backgroundColor: [presetPalettes.orange.primary, presetPalettes.red.primary, presetPalettes.green.primary],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const options: {
|
||||||
|
animation: false;
|
||||||
|
} = {
|
||||||
|
animation: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Doughnut data={data} options={options} className="chart-doughtnut" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeviceProps {
|
function GatewaysActiveInactive({ summary }: { summary?: GetGatewaysSummaryResponse }) {
|
||||||
summary?: GetDevicesSummaryResponse;
|
if (
|
||||||
}
|
summary === undefined ||
|
||||||
|
(summary.getNeverSeenCount() === 0 && summary.getOfflineCount() === 0 && summary.getOnlineCount() === 0)
|
||||||
class DevicesActiveInactive extends Component<DeviceProps> {
|
) {
|
||||||
render() {
|
return <Empty />;
|
||||||
if (
|
|
||||||
this.props.summary === undefined ||
|
|
||||||
(this.props.summary.getNeverSeenCount() === 0 &&
|
|
||||||
this.props.summary.getInactiveCount() === 0 &&
|
|
||||||
this.props.summary.getActiveCount() === 0)
|
|
||||||
) {
|
|
||||||
return <Empty />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
labels: ["Never seen", "Inactive", "Active"],
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
data: [
|
|
||||||
this.props.summary.getNeverSeenCount(),
|
|
||||||
this.props.summary.getInactiveCount(),
|
|
||||||
this.props.summary.getActiveCount(),
|
|
||||||
],
|
|
||||||
backgroundColor: [presetPalettes.orange.primary, presetPalettes.red.primary, presetPalettes.green.primary],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const options: {
|
|
||||||
animation: false;
|
|
||||||
} = {
|
|
||||||
animation: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
return <Doughnut data={data} options={options} className="chart-doughtnut" />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
labels: ["Never seen", "Offline", "Online"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: [summary.getNeverSeenCount(), summary.getOfflineCount(), summary.getOnlineCount()],
|
||||||
|
backgroundColor: [presetPalettes.orange.primary, presetPalettes.red.primary, presetPalettes.green.primary],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const options: {
|
||||||
|
animation: false;
|
||||||
|
} = {
|
||||||
|
animation: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Doughnut data={data} options={options} className="chart-doughtnut" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DevicesDataRates extends Component<DeviceProps> {
|
function DevicesDataRates({ summary }: { summary?: GetDevicesSummaryResponse }) {
|
||||||
getColor = (dr: number) => {
|
const getColor = (dr: number) => {
|
||||||
return [
|
return [
|
||||||
"#ff5722",
|
"#ff5722",
|
||||||
"#ff9800",
|
"#ff9800",
|
||||||
@ -207,109 +166,92 @@ class DevicesDataRates extends Component<DeviceProps> {
|
|||||||
][dr];
|
][dr];
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
if (summary === undefined || summary.getDrCountMap().toArray().length === 0) {
|
||||||
if (this.props.summary === undefined || this.props.summary.getDrCountMap().toArray().length === 0) {
|
return <Empty />;
|
||||||
return <Empty />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let data: {
|
|
||||||
labels: string[];
|
|
||||||
datasets: {
|
|
||||||
data: number[];
|
|
||||||
backgroundColor: string[];
|
|
||||||
}[];
|
|
||||||
} = {
|
|
||||||
labels: [],
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
data: [],
|
|
||||||
backgroundColor: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const elm of this.props.summary.getDrCountMap().toArray()) {
|
|
||||||
data.labels.push(`DR${elm[0]}`);
|
|
||||||
data.datasets[0].data.push(elm[1]);
|
|
||||||
data.datasets[0].backgroundColor.push(this.getColor(elm[0]));
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: {
|
|
||||||
animation: false;
|
|
||||||
} = {
|
|
||||||
animation: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
return <Doughnut data={data} options={options} className="chart-doughtnut" />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IProps {}
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
gatewaysSummary?: GetGatewaysSummaryResponse;
|
|
||||||
devicesSummary?: GetDevicesSummaryResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Dashboard extends Component<IProps, IState> {
|
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
let data: {
|
||||||
|
labels: string[];
|
||||||
|
datasets: {
|
||||||
|
data: number[];
|
||||||
|
backgroundColor: string[];
|
||||||
|
}[];
|
||||||
|
} = {
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: [],
|
||||||
|
backgroundColor: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const elm of summary.getDrCountMap().toArray()) {
|
||||||
|
data.labels.push(`DR${elm[0]}`);
|
||||||
|
data.datasets[0].data.push(elm[1]);
|
||||||
|
data.datasets[0].backgroundColor.push(getColor(elm[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: {
|
||||||
|
animation: false;
|
||||||
|
} = {
|
||||||
|
animation: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Doughnut data={data} options={options} className="chart-doughtnut" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Dashboard() {
|
||||||
|
const [gatewaysSummary, setGatewaysSummary] = useState<GetGatewaysSummaryResponse | undefined>(undefined);
|
||||||
|
const [devicesSummary, setDevicesSummary] = useState<GetDevicesSummaryResponse | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
InternalStore.getGatewaysSummary(new GetGatewaysSummaryRequest(), (resp: GetGatewaysSummaryResponse) => {
|
InternalStore.getGatewaysSummary(new GetGatewaysSummaryRequest(), (resp: GetGatewaysSummaryResponse) => {
|
||||||
this.setState({
|
setGatewaysSummary(resp);
|
||||||
gatewaysSummary: resp,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
InternalStore.getDevicesSummary(new GetDevicesSummaryRequest(), (resp: GetDevicesSummaryResponse) => {
|
InternalStore.getDevicesSummary(new GetDevicesSummaryRequest(), (resp: GetDevicesSummaryResponse) => {
|
||||||
this.setState({
|
setDevicesSummary(resp);
|
||||||
devicesSummary: resp,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
<PageHeader
|
||||||
<PageHeader
|
breadcrumbRender={() => (
|
||||||
breadcrumbRender={() => (
|
<Breadcrumb>
|
||||||
<Breadcrumb>
|
<Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<span>Network Server</span>
|
||||||
<span>Network Server</span>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<span>Dashboard</span>
|
||||||
<span>Dashboard</span>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb>
|
||||||
</Breadcrumb>
|
)}
|
||||||
)}
|
title="Dashboard"
|
||||||
title="Dashboard"
|
/>
|
||||||
/>
|
<Row gutter={24}>
|
||||||
<Row gutter={24}>
|
<Col span={8}>
|
||||||
<Col span={8}>
|
<Card title="Active devices">
|
||||||
<Card title="Active devices">
|
<DevicesActiveInactive summary={devicesSummary} />
|
||||||
<DevicesActiveInactive summary={this.state.devicesSummary} />
|
</Card>
|
||||||
</Card>
|
</Col>
|
||||||
</Col>
|
<Col span={8}>
|
||||||
<Col span={8}>
|
<Card title="Active gateways">
|
||||||
<Card title="Active gateways">
|
<GatewaysActiveInactive summary={gatewaysSummary} />
|
||||||
<GatewaysActiveInactive summary={this.state.gatewaysSummary} />
|
</Card>
|
||||||
</Card>
|
</Col>
|
||||||
</Col>
|
<Col span={8}>
|
||||||
<Col span={8}>
|
<Card title="Device data-rate usage">
|
||||||
<Card title="Device data-rate usage">
|
<DevicesDataRates summary={devicesSummary} />
|
||||||
<DevicesDataRates summary={this.state.devicesSummary} />
|
</Card>
|
||||||
</Card>
|
</Col>
|
||||||
</Col>
|
</Row>
|
||||||
</Row>
|
<Card title="Gateway map">
|
||||||
<Card title="Gateway map">
|
<GatewaysMap />
|
||||||
<GatewaysMap />
|
</Card>
|
||||||
</Card>
|
</Space>
|
||||||
</Space>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Dashboard;
|
export default Dashboard;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { Component } from "react";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { Link, RouteComponentProps } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Space, Breadcrumb, Card, PageHeader } from "antd";
|
import { Space, Breadcrumb, Card } from "antd";
|
||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
|
||||||
import { MacVersion, RegParamsRevision } from "@chirpstack/chirpstack-api-grpc-web/common/common_pb";
|
import { MacVersion, RegParamsRevision } from "@chirpstack/chirpstack-api-grpc-web/common/common_pb";
|
||||||
import {
|
import {
|
||||||
@ -12,86 +12,86 @@ import {
|
|||||||
import DeviceProfileTemplateForm from "./DeviceProfileTemplateForm";
|
import DeviceProfileTemplateForm from "./DeviceProfileTemplateForm";
|
||||||
import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore";
|
import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore";
|
||||||
|
|
||||||
class CreateDeviceProfileTemplate extends Component<RouteComponentProps> {
|
function CreateDeviceProfileTemplate() {
|
||||||
onFinish = (obj: DeviceProfileTemplate) => {
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const onFinish = (obj: DeviceProfileTemplate) => {
|
||||||
let req = new CreateDeviceProfileTemplateRequest();
|
let req = new CreateDeviceProfileTemplateRequest();
|
||||||
req.setDeviceProfileTemplate(obj);
|
req.setDeviceProfileTemplate(obj);
|
||||||
|
|
||||||
DeviceProfileTemplateStore.create(req, () => {
|
DeviceProfileTemplateStore.create(req, () => {
|
||||||
this.props.history.push(`/device-profile-templates`);
|
navigate(`/device-profile-templates`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const codecScript = `// Decode uplink function.
|
||||||
const codecScript = `// Decode uplink function.
|
//
|
||||||
//
|
// Input is an object with the following fields:
|
||||||
// Input is an object with the following fields:
|
// - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0]
|
||||||
// - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0]
|
// - fPort = Uplink fPort.
|
||||||
// - fPort = Uplink fPort.
|
// - variables = Object containing the configured device variables.
|
||||||
// - variables = Object containing the configured device variables.
|
//
|
||||||
//
|
// Output must be an object with the following fields:
|
||||||
// Output must be an object with the following fields:
|
// - data = Object representing the decoded payload.
|
||||||
// - data = Object representing the decoded payload.
|
function decodeUplink(input) {
|
||||||
function decodeUplink(input) {
|
return {
|
||||||
return {
|
data: {
|
||||||
data: {
|
temp: 22.5
|
||||||
temp: 22.5
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode downlink function.
|
|
||||||
//
|
|
||||||
// Input is an object with the following fields:
|
|
||||||
// - data = Object representing the payload that must be encoded.
|
|
||||||
// - variables = Object containing the configured device variables.
|
|
||||||
//
|
|
||||||
// Output must be an object with the following fields:
|
|
||||||
// - bytes = Byte array containing the downlink payload.
|
|
||||||
function encodeDownlink(input) {
|
|
||||||
return {
|
|
||||||
bytes: [225, 230, 255, 0]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
let deviceProfileTemplate = new DeviceProfileTemplate();
|
|
||||||
deviceProfileTemplate.setPayloadCodecScript(codecScript);
|
|
||||||
deviceProfileTemplate.setSupportsOtaa(true);
|
|
||||||
deviceProfileTemplate.setUplinkInterval(3600);
|
|
||||||
deviceProfileTemplate.setDeviceStatusReqInterval(1);
|
|
||||||
deviceProfileTemplate.setAdrAlgorithmId("default");
|
|
||||||
deviceProfileTemplate.setMacVersion(MacVersion.LORAWAN_1_0_3);
|
|
||||||
deviceProfileTemplate.setRegParamsRevision(RegParamsRevision.A);
|
|
||||||
deviceProfileTemplate.setFlushQueueOnActivate(true);
|
|
||||||
deviceProfileTemplate.setAutoDetectMeasurements(true);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
|
||||||
<PageHeader
|
|
||||||
breadcrumbRender={() => (
|
|
||||||
<Breadcrumb>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>Network Server</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>
|
|
||||||
<Link to={`/device-profile-templates`}>Device-profile templates</Link>
|
|
||||||
</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>Add</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
</Breadcrumb>
|
|
||||||
)}
|
|
||||||
title="Add device-profile template"
|
|
||||||
/>
|
|
||||||
<Card>
|
|
||||||
<DeviceProfileTemplateForm initialValues={deviceProfileTemplate} onFinish={this.onFinish} />
|
|
||||||
</Card>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encode downlink function.
|
||||||
|
//
|
||||||
|
// Input is an object with the following fields:
|
||||||
|
// - data = Object representing the payload that must be encoded.
|
||||||
|
// - variables = Object containing the configured device variables.
|
||||||
|
//
|
||||||
|
// Output must be an object with the following fields:
|
||||||
|
// - bytes = Byte array containing the downlink payload.
|
||||||
|
function encodeDownlink(input) {
|
||||||
|
return {
|
||||||
|
bytes: [225, 230, 255, 0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
let deviceProfileTemplate = new DeviceProfileTemplate();
|
||||||
|
deviceProfileTemplate.setPayloadCodecScript(codecScript);
|
||||||
|
deviceProfileTemplate.setSupportsOtaa(true);
|
||||||
|
deviceProfileTemplate.setUplinkInterval(3600);
|
||||||
|
deviceProfileTemplate.setDeviceStatusReqInterval(1);
|
||||||
|
deviceProfileTemplate.setAdrAlgorithmId("default");
|
||||||
|
deviceProfileTemplate.setMacVersion(MacVersion.LORAWAN_1_0_3);
|
||||||
|
deviceProfileTemplate.setRegParamsRevision(RegParamsRevision.A);
|
||||||
|
deviceProfileTemplate.setFlushQueueOnActivate(true);
|
||||||
|
deviceProfileTemplate.setAutoDetectMeasurements(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
|
<PageHeader
|
||||||
|
breadcrumbRender={() => (
|
||||||
|
<Breadcrumb>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>Network Server</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>
|
||||||
|
<Link to={`/device-profile-templates`}>Device-profile templates</Link>
|
||||||
|
</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>Add</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
</Breadcrumb>
|
||||||
|
)}
|
||||||
|
title="Add device-profile template"
|
||||||
|
/>
|
||||||
|
<Card>
|
||||||
|
<DeviceProfileTemplateForm initialValues={deviceProfileTemplate} onFinish={onFinish} />
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateDeviceProfileTemplate;
|
export default CreateDeviceProfileTemplate;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { Form, Input, Select, InputNumber, Switch, Row, Col, Button, Tabs, Card } from "antd";
|
import { Form, Input, Select, InputNumber, Switch, Row, Col, Button, Tabs, Card } from "antd";
|
||||||
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
||||||
@ -17,37 +17,20 @@ interface IProps {
|
|||||||
update?: boolean;
|
update?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function DeviceProfileTemplateForm(props: IProps) {
|
||||||
supportsOtaa: boolean;
|
const [form] = Form.useForm();
|
||||||
supportsClassB: boolean;
|
const [supportsOtaa, setSupportsOtaa] = useState<boolean>(false);
|
||||||
supportsClassC: boolean;
|
const [supportsClassB, setSupportsClassB] = useState<boolean>(false);
|
||||||
payloadCodecRuntime: CodecRuntime;
|
const [supportsClassC, setSupportsClassC] = useState<boolean>(false);
|
||||||
adrAlgorithms: [string, string][];
|
const [payloadCodecRuntime, setPayloadCodecRuntime] = useState<CodecRuntime>(CodecRuntime.NONE);
|
||||||
}
|
const [adrAlgorithms, setAdrAlgorithms] = useState<[string, string][]>([]);
|
||||||
|
|
||||||
class DeviceProfileTemplateForm extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
formRef = React.createRef<any>();
|
const v = props.initialValues;
|
||||||
|
setSupportsOtaa(v.getSupportsOtaa());
|
||||||
constructor(props: IProps) {
|
setSupportsClassB(v.getSupportsClassB());
|
||||||
super(props);
|
setSupportsClassC(v.getSupportsClassC());
|
||||||
this.state = {
|
setPayloadCodecRuntime(v.getPayloadCodecRuntime());
|
||||||
supportsOtaa: false,
|
|
||||||
supportsClassB: false,
|
|
||||||
supportsClassC: false,
|
|
||||||
payloadCodecRuntime: CodecRuntime.NONE,
|
|
||||||
adrAlgorithms: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const v = this.props.initialValues;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
supportsOtaa: v.getSupportsOtaa(),
|
|
||||||
supportsClassB: v.getSupportsClassB(),
|
|
||||||
supportsClassC: v.getSupportsClassC(),
|
|
||||||
payloadCodecRuntime: v.getPayloadCodecRuntime(),
|
|
||||||
});
|
|
||||||
|
|
||||||
DeviceProfileStore.listAdrAlgorithms((resp: ListDeviceProfileAdrAlgorithmsResponse) => {
|
DeviceProfileStore.listAdrAlgorithms((resp: ListDeviceProfileAdrAlgorithmsResponse) => {
|
||||||
let adrAlgorithms: [string, string][] = [];
|
let adrAlgorithms: [string, string][] = [];
|
||||||
@ -55,14 +38,12 @@ class DeviceProfileTemplateForm extends Component<IProps, IState> {
|
|||||||
adrAlgorithms.push([a.getId(), a.getName()]);
|
adrAlgorithms.push([a.getId(), a.getName()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
setAdrAlgorithms(adrAlgorithms);
|
||||||
adrAlgorithms: adrAlgorithms,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}, [props.initialValues]);
|
||||||
|
|
||||||
onFinish = (values: DeviceProfileTemplate.AsObject) => {
|
const onFinish = (values: DeviceProfileTemplate.AsObject) => {
|
||||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
const v = Object.assign(props.initialValues.toObject(), values);
|
||||||
let dp = new DeviceProfileTemplate();
|
let dp = new DeviceProfileTemplate();
|
||||||
dp.setId(v.id);
|
dp.setId(v.id);
|
||||||
|
|
||||||
@ -114,454 +95,487 @@ class DeviceProfileTemplateForm extends Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
dp.setAutoDetectMeasurements(v.autoDetectMeasurements);
|
dp.setAutoDetectMeasurements(v.autoDetectMeasurements);
|
||||||
|
|
||||||
this.props.onFinish(dp);
|
props.onFinish(dp);
|
||||||
};
|
};
|
||||||
|
|
||||||
onSupportsOtaaChange = (checked: boolean) => {
|
const onSupportsOtaaChange = (checked: boolean) => {
|
||||||
this.setState({
|
setSupportsOtaa(checked);
|
||||||
supportsOtaa: checked,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onSupportsClassBChnage = (checked: boolean) => {
|
const onSupportsClassBChnage = (checked: boolean) => {
|
||||||
this.setState({
|
setSupportsClassB(checked);
|
||||||
supportsClassB: checked,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onSupportsClassCChange = (checked: boolean) => {
|
const onSupportsClassCChange = (checked: boolean) => {
|
||||||
this.setState({
|
setSupportsClassC(checked);
|
||||||
supportsClassC: checked,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onPayloadCodecRuntimeChange = (value: CodecRuntime) => {
|
const onPayloadCodecRuntimeChange = (value: CodecRuntime) => {
|
||||||
this.setState({
|
setPayloadCodecRuntime(value);
|
||||||
payloadCodecRuntime: value,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const adrOptions = adrAlgorithms.map(v => <Select.Option value={v[0]}>{v[1]}</Select.Option>);
|
||||||
const adrOptions = this.state.adrAlgorithms.map(v => <Select.Option value={v[0]}>{v[1]}</Select.Option>);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
|
||||||
layout="vertical"
|
<Tabs>
|
||||||
initialValues={this.props.initialValues.toObject()}
|
<Tabs.TabPane tab="General" key="1">
|
||||||
onFinish={this.onFinish}
|
<Form.Item
|
||||||
ref={this.formRef}
|
label="ID"
|
||||||
>
|
name="id"
|
||||||
<Tabs>
|
rules={[
|
||||||
<Tabs.TabPane tab="General" key="1">
|
{
|
||||||
<Form.Item
|
required: true,
|
||||||
label="ID"
|
pattern: new RegExp(/^[\w-]*$/g),
|
||||||
name="id"
|
message: "Please enter a valid id!",
|
||||||
rules={[
|
},
|
||||||
{
|
]}
|
||||||
required: true,
|
>
|
||||||
pattern: new RegExp(/^[\w-]*$/g),
|
<Input disabled={!!props.update} />
|
||||||
message: "Please enter a valid id!",
|
</Form.Item>
|
||||||
},
|
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
|
||||||
]}
|
<Input />
|
||||||
>
|
</Form.Item>
|
||||||
<Input disabled={!!this.props.update} />
|
<Form.Item label="Vendor" name="vendor" rules={[{ required: true, message: "Please enter a vendor!" }]}>
|
||||||
</Form.Item>
|
<Input />
|
||||||
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
|
</Form.Item>
|
||||||
<Input />
|
<Form.Item
|
||||||
</Form.Item>
|
label="Firmware version"
|
||||||
<Form.Item label="Vendor" name="vendor" rules={[{ required: true, message: "Please enter a vendor!" }]}>
|
name="firmware"
|
||||||
<Input />
|
rules={[{ required: true, message: "Please enter a firmware version!" }]}
|
||||||
</Form.Item>
|
>
|
||||||
<Form.Item
|
<Input />
|
||||||
label="Firmware version"
|
</Form.Item>
|
||||||
name="firmware"
|
<Form.Item label="Description" name="description">
|
||||||
rules={[{ required: true, message: "Please enter a firmware version!" }]}
|
<Input.TextArea rows={6} />
|
||||||
>
|
</Form.Item>
|
||||||
<Input />
|
<Form.Item label="Region" name="region" rules={[{ required: true, message: "Please select a region!" }]}>
|
||||||
</Form.Item>
|
<Select>
|
||||||
<Form.Item label="Description" name="description">
|
<Select.Option value={Region.AS923}>AS923</Select.Option>
|
||||||
<Input.TextArea rows={6} />
|
<Select.Option value={Region.AS923_2}>AS923-2</Select.Option>
|
||||||
</Form.Item>
|
<Select.Option value={Region.AS923_3}>AS923-3</Select.Option>
|
||||||
<Form.Item label="Region" name="region" rules={[{ required: true, message: "Please select a region!" }]}>
|
<Select.Option value={Region.AS923_4}>AS923-4</Select.Option>
|
||||||
<Select>
|
<Select.Option value={Region.AU915}>AU915</Select.Option>
|
||||||
<Select.Option value={Region.AS923}>AS923</Select.Option>
|
<Select.Option value={Region.CN779}>CN779</Select.Option>
|
||||||
<Select.Option value={Region.AS923_2}>AS923-2</Select.Option>
|
<Select.Option value={Region.EU433}>EU433</Select.Option>
|
||||||
<Select.Option value={Region.AS923_3}>AS923-3</Select.Option>
|
<Select.Option value={Region.EU868}>EU868</Select.Option>
|
||||||
<Select.Option value={Region.AS923_4}>AS923-4</Select.Option>
|
<Select.Option value={Region.IN865}>IN865</Select.Option>
|
||||||
<Select.Option value={Region.AU915}>AU915</Select.Option>
|
<Select.Option value={Region.ISM2400}>ISM2400</Select.Option>
|
||||||
<Select.Option value={Region.CN779}>CN779</Select.Option>
|
<Select.Option value={Region.KR920}>KR920</Select.Option>
|
||||||
<Select.Option value={Region.EU433}>EU433</Select.Option>
|
<Select.Option value={Region.RU864}>RU864</Select.Option>
|
||||||
<Select.Option value={Region.EU868}>EU868</Select.Option>
|
<Select.Option value={Region.US915}>US915</Select.Option>
|
||||||
<Select.Option value={Region.IN865}>IN865</Select.Option>
|
</Select>
|
||||||
<Select.Option value={Region.ISM2400}>ISM2400</Select.Option>
|
</Form.Item>
|
||||||
<Select.Option value={Region.KR920}>KR920</Select.Option>
|
<Row gutter={24}>
|
||||||
<Select.Option value={Region.RU864}>RU864</Select.Option>
|
<Col span={12}>
|
||||||
<Select.Option value={Region.US915}>US915</Select.Option>
|
<Form.Item
|
||||||
</Select>
|
label="MAC version"
|
||||||
</Form.Item>
|
tooltip="The LoRaWAN MAC version supported by the device."
|
||||||
<Row gutter={24}>
|
name="macVersion"
|
||||||
|
rules={[{ required: true, message: "Please select a MAC version!" }]}
|
||||||
|
>
|
||||||
|
<Select>
|
||||||
|
<Select.Option value={MacVersion.LORAWAN_1_0_0}>LoRaWAN 1.0.0</Select.Option>
|
||||||
|
<Select.Option value={MacVersion.LORAWAN_1_0_1}>LoRaWAN 1.0.1</Select.Option>
|
||||||
|
<Select.Option value={MacVersion.LORAWAN_1_0_2}>LoRaWAN 1.0.2</Select.Option>
|
||||||
|
<Select.Option value={MacVersion.LORAWAN_1_0_3}>LoRaWAN 1.0.3</Select.Option>
|
||||||
|
<Select.Option value={MacVersion.LORAWAN_1_0_4}>LoRaWAN 1.0.4</Select.Option>
|
||||||
|
<Select.Option value={MacVersion.LORAWAN_1_1_0}>LoRaWAN 1.1.0</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="Regional parameters revision"
|
||||||
|
tooltip="Revision of the Regional Parameters specification supported by the device."
|
||||||
|
name="regParamsRevision"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "Please select a regional parameters revision!",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select>
|
||||||
|
<Select.Option value={RegParamsRevision.A}>A</Select.Option>
|
||||||
|
<Select.Option value={RegParamsRevision.B}>B</Select.Option>
|
||||||
|
<Select.Option value={RegParamsRevision.RP002_1_0_0}>RP002-1.0.0</Select.Option>
|
||||||
|
<Select.Option value={RegParamsRevision.RP002_1_0_1}>RP002-1.0.1</Select.Option>
|
||||||
|
<Select.Option value={RegParamsRevision.RP002_1_0_2}>RP002-1.0.2</Select.Option>
|
||||||
|
<Select.Option value={RegParamsRevision.RP002_1_0_3}>RP002-1.0.3</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Form.Item
|
||||||
|
label="ADR algorithm"
|
||||||
|
tooltip="The ADR algorithm that will be used for controlling the device data-rate."
|
||||||
|
name="adrAlgorithmId"
|
||||||
|
rules={[{ required: true, message: "Please select an ADR algorithm!" }]}
|
||||||
|
>
|
||||||
|
<Select>{adrOptions}</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Row gutter={24}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Item
|
||||||
|
label="Flush queue on activate"
|
||||||
|
name="flushQueueOnActivate"
|
||||||
|
valuePropName="checked"
|
||||||
|
tooltip="If enabled, the device-queue will be flushed on ABP or OTAA activation."
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Item
|
||||||
|
label="Expected uplink interval (secs)"
|
||||||
|
tooltip="The expected interval in seconds in which the device sends uplink messages. This is used to determine if a device is active or inactive."
|
||||||
|
name="uplinkInterval"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "Please enter an uplink interval!",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Item
|
||||||
|
label="Device-status request frequency (req/day)"
|
||||||
|
tooltip="Frequency to initiate an End-Device status request (request/day). Set to 0 to disable."
|
||||||
|
name="deviceStatusReqInterval"
|
||||||
|
>
|
||||||
|
<InputNumber min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab="Join (OTAA / ABP)" key="2">
|
||||||
|
<Form.Item label="Device supports OTAA" name="supportsOtaa" valuePropName="checked">
|
||||||
|
<Switch onChange={onSupportsOtaaChange} />
|
||||||
|
</Form.Item>
|
||||||
|
{!supportsOtaa && (
|
||||||
|
<Row>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="MAC version"
|
label="RX1 delay"
|
||||||
tooltip="The LoRaWAN MAC version supported by the device."
|
name="abpRx1Delay"
|
||||||
name="macVersion"
|
rules={[{ required: true, message: "Please enter a RX1 delay!" }]}
|
||||||
rules={[{ required: true, message: "Please select a MAC version!" }]}
|
|
||||||
>
|
>
|
||||||
<Select>
|
<InputNumber min={0} max={15} />
|
||||||
<Select.Option value={MacVersion.LORAWAN_1_0_0}>LoRaWAN 1.0.0</Select.Option>
|
|
||||||
<Select.Option value={MacVersion.LORAWAN_1_0_1}>LoRaWAN 1.0.1</Select.Option>
|
|
||||||
<Select.Option value={MacVersion.LORAWAN_1_0_2}>LoRaWAN 1.0.2</Select.Option>
|
|
||||||
<Select.Option value={MacVersion.LORAWAN_1_0_3}>LoRaWAN 1.0.3</Select.Option>
|
|
||||||
<Select.Option value={MacVersion.LORAWAN_1_0_4}>LoRaWAN 1.0.4</Select.Option>
|
|
||||||
<Select.Option value={MacVersion.LORAWAN_1_1_0}>LoRaWAN 1.1.0</Select.Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Regional parameters revision"
|
label="RX1 data-rate offset"
|
||||||
tooltip="Revision of the Regional Parameters specification supported by the device."
|
tooltip="Please refer the LoRaWAN Regional Parameters specification for valid values."
|
||||||
name="regParamsRevision"
|
name="abpRx1DrOffset"
|
||||||
rules={[{ required: true, message: "Please select a regional parameters revision!" }]}
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "Please enter a RX1 data-rate offset!",
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Select>
|
<InputNumber min={0} max={15} />
|
||||||
<Select.Option value={RegParamsRevision.A}>A</Select.Option>
|
|
||||||
<Select.Option value={RegParamsRevision.B}>B</Select.Option>
|
|
||||||
<Select.Option value={RegParamsRevision.RP002_1_0_0}>RP002-1.0.0</Select.Option>
|
|
||||||
<Select.Option value={RegParamsRevision.RP002_1_0_1}>RP002-1.0.1</Select.Option>
|
|
||||||
<Select.Option value={RegParamsRevision.RP002_1_0_2}>RP002-1.0.2</Select.Option>
|
|
||||||
<Select.Option value={RegParamsRevision.RP002_1_0_3}>RP002-1.0.3</Select.Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Form.Item
|
)}
|
||||||
label="ADR algorithm"
|
{!supportsOtaa && (
|
||||||
tooltip="The ADR algorithm that will be used for controlling the device data-rate."
|
<Row>
|
||||||
name="adrAlgorithmId"
|
<Col span={12}>
|
||||||
rules={[{ required: true, message: "Please select an ADR algorithm!" }]}
|
|
||||||
>
|
|
||||||
<Select>{adrOptions}</Select>
|
|
||||||
</Form.Item>
|
|
||||||
<Row gutter={24}>
|
|
||||||
<Col span={8}>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Flush queue on activate"
|
label="RX2 data-rate"
|
||||||
name="flushQueueOnActivate"
|
tooltip="Please refer the LoRaWAN Regional Parameters specification for valid values."
|
||||||
valuePropName="checked"
|
name="abpRx2Dr"
|
||||||
tooltip="If enabled, the device-queue will be flushed on ABP or OTAA activation."
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "Please enter a RX2 data-rate!",
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Switch />
|
<InputNumber min={0} max={15} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Expected uplink interval (secs)"
|
label="RX2 frequency (Hz)"
|
||||||
tooltip="The expected interval in seconds in which the device sends uplink messages. This is used to determine if a device is active or inactive."
|
name="abpRx2Freq"
|
||||||
name="uplinkInterval"
|
rules={[
|
||||||
rules={[{ required: true, message: "Please enter an uplink interval!" }]}
|
{
|
||||||
|
required: true,
|
||||||
|
message: "Please enter a RX2 frequency!",
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} />
|
<InputNumber min={0} style={{ width: "200px" }} />
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={8}>
|
|
||||||
<Form.Item
|
|
||||||
label="Device-status request frequency (req/day)"
|
|
||||||
tooltip="Frequency to initiate an End-Device status request (request/day). Set to 0 to disable."
|
|
||||||
name="deviceStatusReqInterval"
|
|
||||||
>
|
|
||||||
<InputNumber min={0} />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Tabs.TabPane>
|
)}
|
||||||
<Tabs.TabPane tab="Join (OTAA / ABP)" key="2">
|
</Tabs.TabPane>
|
||||||
<Form.Item label="Device supports OTAA" name="supportsOtaa" valuePropName="checked">
|
<Tabs.TabPane tab="Class-B" key="3">
|
||||||
<Switch onChange={this.onSupportsOtaaChange} />
|
<Form.Item label="Device supports Class-B" name="supportsClassB" valuePropName="checked">
|
||||||
</Form.Item>
|
<Switch onChange={onSupportsClassBChnage} />
|
||||||
{!this.state.supportsOtaa && (
|
</Form.Item>
|
||||||
<Row>
|
{supportsClassB && (
|
||||||
|
<>
|
||||||
|
<Row gutter={24}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="RX1 delay"
|
label="Class-B confirmed downlink timeout (seconds)"
|
||||||
name="abpRx1Delay"
|
tooltip="Class-B timeout (in seconds) for confirmed downlink transmissions."
|
||||||
rules={[{ required: true, message: "Please enter a RX1 delay!" }]}
|
name="classBTimeout"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "Please enter a Class-B confirmed downlink timeout!",
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={15} />
|
<InputNumber min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="RX1 data-rate offset"
|
label="Class-B ping-slot periodicity"
|
||||||
tooltip="Please refer the LoRaWAN Regional Parameters specification for valid values."
|
tooltip="This value must match the ping-slot periodicity of the device. Please refer to the device documentation."
|
||||||
name="abpRx1DrOffset"
|
name="classBPingSlotNbK"
|
||||||
rules={[{ required: true, message: "Please enter a RX1 data-rate offset!" }]}
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "Please select the ping-slot periodicity!",
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={15} />
|
<Select>
|
||||||
|
<Select.Option value={0}>Every second</Select.Option>
|
||||||
|
<Select.Option value={1}>Every 2 seconds</Select.Option>
|
||||||
|
<Select.Option value={2}>Every 4 seconds</Select.Option>
|
||||||
|
<Select.Option value={3}>Every 8 seconds</Select.Option>
|
||||||
|
<Select.Option value={4}>Every 16 seconds</Select.Option>
|
||||||
|
<Select.Option value={5}>Every 32 seconds</Select.Option>
|
||||||
|
<Select.Option value={6}>Every 64 seconds</Select.Option>
|
||||||
|
<Select.Option value={7}>Every 128 seconds</Select.Option>
|
||||||
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
<Row gutter={24}>
|
||||||
{!this.state.supportsOtaa && (
|
|
||||||
<Row>
|
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="RX2 data-rate"
|
label="Class-B ping-slot data-rate"
|
||||||
tooltip="Please refer the LoRaWAN Regional Parameters specification for valid values."
|
tooltip="This value must match the ping-slot data-rate of the device. Please refer to the device documentation."
|
||||||
name="abpRx2Dr"
|
name="classBPingSlotDr"
|
||||||
rules={[{ required: true, message: "Please enter a RX2 data-rate!" }]}
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "Please enter the ping-slot data-rate!",
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={15} />
|
<InputNumber min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="RX2 frequency (Hz)"
|
label="Class-B ping-slot frequency (Hz)"
|
||||||
name="abpRx2Freq"
|
tooltip="This value must match the ping-slot frequency of the device. Please refer to the device documentation."
|
||||||
rules={[{ required: true, message: "Please enter a RX2 frequency!" }]}
|
name="classBPingSlotFreq"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "Please enter the ping-slot frequency!",
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} style={{ width: "200px" }} />
|
<InputNumber min={0} style={{ width: "200px" }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
</>
|
||||||
</Tabs.TabPane>
|
)}
|
||||||
<Tabs.TabPane tab="Class-B" key="3">
|
</Tabs.TabPane>
|
||||||
<Form.Item label="Device supports Class-B" name="supportsClassB" valuePropName="checked">
|
<Tabs.TabPane tab="Class-C" key="4">
|
||||||
<Switch onChange={this.onSupportsClassBChnage} />
|
<Form.Item label="Device supports Class-C" name="supportsClassC" valuePropName="checked">
|
||||||
|
<Switch onChange={onSupportsClassCChange} />
|
||||||
|
</Form.Item>
|
||||||
|
{supportsClassC && (
|
||||||
|
<Form.Item
|
||||||
|
label="Class-C confirmed downlink timeout (seconds)"
|
||||||
|
tooltip="Class-C timeout (in seconds) for confirmed downlink transmissions."
|
||||||
|
name="classCTimeout"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "Please enter a Class-C confirmed downlink timeout!",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{this.state.supportsClassB && (
|
)}
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab="Codec" key="5">
|
||||||
|
<Form.Item
|
||||||
|
label="Payload codec"
|
||||||
|
name="payloadCodecRuntime"
|
||||||
|
tooltip="By defining a payload codec, ChirpStack Application Server can encode and decode the binary device payload for you."
|
||||||
|
>
|
||||||
|
<Select onChange={onPayloadCodecRuntimeChange}>
|
||||||
|
<Select.Option value={CodecRuntime.NONE}>None</Select.Option>
|
||||||
|
<Select.Option value={CodecRuntime.CAYENNE_LPP}>Cayenne LPP</Select.Option>
|
||||||
|
<Select.Option value={CodecRuntime.JS}>JavaScript functions</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
{payloadCodecRuntime === CodecRuntime.JS && <CodeEditor label="Codec functions" name="payloadCodecScript" />}
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab="Tags" key="6">
|
||||||
|
<Form.List name="tagsMap">
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
<>
|
<>
|
||||||
<Row gutter={24}>
|
{fields.map(({ key, name, ...restField }) => (
|
||||||
<Col span={12}>
|
<Row gutter={24}>
|
||||||
<Form.Item
|
<Col span={6}>
|
||||||
label="Class-B confirmed downlink timeout (seconds)"
|
<Form.Item
|
||||||
tooltip="Class-B timeout (in seconds) for confirmed downlink transmissions."
|
{...restField}
|
||||||
name="classBTimeout"
|
name={[name, 0]}
|
||||||
rules={[{ required: true, message: "Please enter a Class-B confirmed downlink timeout!" }]}
|
fieldKey={[name, 0]}
|
||||||
>
|
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||||
<InputNumber min={0} />
|
>
|
||||||
</Form.Item>
|
<Input placeholder="Key" />
|
||||||
</Col>
|
</Form.Item>
|
||||||
<Col span={12}>
|
</Col>
|
||||||
<Form.Item
|
<Col span={16}>
|
||||||
label="Class-B ping-slot periodicity"
|
<Form.Item
|
||||||
tooltip="This value must match the ping-slot periodicity of the device. Please refer to the device documentation."
|
{...restField}
|
||||||
name="classBPingSlotNbK"
|
name={[name, 1]}
|
||||||
rules={[{ required: true, message: "Please select the ping-slot periodicity!" }]}
|
fieldKey={[name, 1]}
|
||||||
>
|
rules={[{ required: true, message: "Please enter a value!" }]}
|
||||||
<Select>
|
>
|
||||||
<Select.Option value={0}>Every second</Select.Option>
|
<Input placeholder="Value" />
|
||||||
<Select.Option value={1}>Every 2 seconds</Select.Option>
|
</Form.Item>
|
||||||
<Select.Option value={2}>Every 4 seconds</Select.Option>
|
</Col>
|
||||||
<Select.Option value={3}>Every 8 seconds</Select.Option>
|
<Col span={2}>
|
||||||
<Select.Option value={4}>Every 16 seconds</Select.Option>
|
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||||
<Select.Option value={5}>Every 32 seconds</Select.Option>
|
</Col>
|
||||||
<Select.Option value={6}>Every 64 seconds</Select.Option>
|
</Row>
|
||||||
<Select.Option value={7}>Every 128 seconds</Select.Option>
|
))}
|
||||||
</Select>
|
<Form.Item>
|
||||||
</Form.Item>
|
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||||
</Col>
|
Add tag
|
||||||
</Row>
|
</Button>
|
||||||
<Row gutter={24}>
|
</Form.Item>
|
||||||
<Col span={12}>
|
|
||||||
<Form.Item
|
|
||||||
label="Class-B ping-slot data-rate"
|
|
||||||
tooltip="This value must match the ping-slot data-rate of the device. Please refer to the device documentation."
|
|
||||||
name="classBPingSlotDr"
|
|
||||||
rules={[{ required: true, message: "Please enter the ping-slot data-rate!" }]}
|
|
||||||
>
|
|
||||||
<InputNumber min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<Form.Item
|
|
||||||
label="Class-B ping-slot frequency (Hz)"
|
|
||||||
tooltip="This value must match the ping-slot frequency of the device. Please refer to the device documentation."
|
|
||||||
name="classBPingSlotFreq"
|
|
||||||
rules={[{ required: true, message: "Please enter the ping-slot frequency!" }]}
|
|
||||||
>
|
|
||||||
<InputNumber min={0} style={{ width: "200px" }} />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Tabs.TabPane>
|
</Form.List>
|
||||||
<Tabs.TabPane tab="Class-C" key="4">
|
</Tabs.TabPane>
|
||||||
<Form.Item label="Device supports Class-C" name="supportsClassC" valuePropName="checked">
|
<Tabs.TabPane tab="Measurements" key="7">
|
||||||
<Switch onChange={this.onSupportsClassCChange} />
|
<Card bordered={false}>
|
||||||
</Form.Item>
|
<p>
|
||||||
{this.state.supportsClassC && (
|
ChirpStack can aggregate and visualize decoded device measurements in the device dashboard. To setup the
|
||||||
<Form.Item
|
aggregation of device measurements, you must configure the key, kind of measurement and name
|
||||||
label="Class-C confirmed downlink timeout (seconds)"
|
(user-defined). The following measurement-kinds can be selected:
|
||||||
tooltip="Class-C timeout (in seconds) for confirmed downlink transmissions."
|
</p>
|
||||||
name="classCTimeout"
|
<ul>
|
||||||
rules={[{ required: true, message: "Please enter a Class-C confirmed downlink timeout!" }]}
|
<li>
|
||||||
>
|
<strong>Unknown / unset</strong>: Default for auto-detected keys. This disables the aggregation of this
|
||||||
<InputNumber min={0} />
|
metric.
|
||||||
</Form.Item>
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Counter</strong>: For continuous incrementing counters.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Absolute</strong>: For counters which get reset upon reading / uplink.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Gauge</strong>: For temperature, humidity, pressure etc...
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>String</strong>: For boolean or string values.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Card>
|
||||||
|
<Form.Item
|
||||||
|
label="Automatically detect measurement keys"
|
||||||
|
name="autoDetectMeasurements"
|
||||||
|
valuePropName="checked"
|
||||||
|
tooltip="If enabled, measurement-keys will be automatically added based on the decoded payload keys. If the decoded payload contains random keys, you want to disable auto-detection."
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.List name="measurementsMap">
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
{fields.map(({ key, name, ...restField }) => (
|
||||||
|
<Row gutter={24}>
|
||||||
|
<Col span={6}>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, 0]}
|
||||||
|
fieldKey={[name, 0]}
|
||||||
|
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="Key" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, 1, "kind"]}
|
||||||
|
fieldKey={[name, 1, "kind"]}
|
||||||
|
rules={[{ required: true, message: "Please select a kind!" }]}
|
||||||
|
>
|
||||||
|
<Select>
|
||||||
|
<Select.Option value={MeasurementKind.UNKNOWN}>Unknown / unset</Select.Option>
|
||||||
|
<Select.Option value={MeasurementKind.COUNTER}>Counter</Select.Option>
|
||||||
|
<Select.Option value={MeasurementKind.ABSOLUTE}>Absolute</Select.Option>
|
||||||
|
<Select.Option value={MeasurementKind.GAUGE}>Gauge</Select.Option>
|
||||||
|
<Select.Option value={MeasurementKind.STRING}>String</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={10}>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, 1, "name"]}
|
||||||
|
fieldKey={[name, 1, "name"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "Please enter a description!",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="Name" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={2}>
|
||||||
|
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||||
|
Add measurement
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Tabs.TabPane>
|
</Form.List>
|
||||||
<Tabs.TabPane tab="Codec" key="5">
|
</Tabs.TabPane>
|
||||||
<Form.Item
|
</Tabs>
|
||||||
label="Payload codec"
|
<Form.Item>
|
||||||
name="payloadCodecRuntime"
|
<Button type="primary" htmlType="submit">
|
||||||
tooltip="By defining a payload codec, ChirpStack Application Server can encode and decode the binary device payload for you."
|
Submit
|
||||||
>
|
</Button>
|
||||||
<Select onChange={this.onPayloadCodecRuntimeChange}>
|
</Form.Item>
|
||||||
<Select.Option value={CodecRuntime.NONE}>None</Select.Option>
|
</Form>
|
||||||
<Select.Option value={CodecRuntime.CAYENNE_LPP}>Cayenne LPP</Select.Option>
|
);
|
||||||
<Select.Option value={CodecRuntime.JS}>JavaScript functions</Select.Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
{this.state.payloadCodecRuntime === CodecRuntime.JS && (
|
|
||||||
<CodeEditor
|
|
||||||
label="Codec functions"
|
|
||||||
name="payloadCodecScript"
|
|
||||||
value={this.formRef.current.getFieldValue("payloadCodecScript")}
|
|
||||||
formRef={this.formRef}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Tabs.TabPane>
|
|
||||||
<Tabs.TabPane tab="Tags" key="6">
|
|
||||||
<Form.List name="tagsMap">
|
|
||||||
{(fields, { add, remove }) => (
|
|
||||||
<>
|
|
||||||
{fields.map(({ key, name, ...restField }) => (
|
|
||||||
<Row gutter={24}>
|
|
||||||
<Col span={6}>
|
|
||||||
<Form.Item
|
|
||||||
{...restField}
|
|
||||||
name={[name, 0]}
|
|
||||||
fieldKey={[name, 0]}
|
|
||||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="Key" />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={16}>
|
|
||||||
<Form.Item
|
|
||||||
{...restField}
|
|
||||||
name={[name, 1]}
|
|
||||||
fieldKey={[name, 1]}
|
|
||||||
rules={[{ required: true, message: "Please enter a value!" }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="Value" />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={2}>
|
|
||||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
))}
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
|
||||||
Add tag
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Form.List>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
<Tabs.TabPane tab="Measurements" key="7">
|
|
||||||
<Card bordered={false}>
|
|
||||||
<p>
|
|
||||||
ChirpStack can aggregate and visualize decoded device measurements in the device dashboard. To setup the
|
|
||||||
aggregation of device measurements, you must configure the key, kind of measurement and name
|
|
||||||
(user-defined). The following measurement-kinds can be selected:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<strong>Unknown / unset</strong>: Default for auto-detected keys. This disables the aggregation of
|
|
||||||
this metric.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Counter</strong>: For continuous incrementing counters.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Absolute</strong>: For counters which get reset upon reading / uplink.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Gauge</strong>: For temperature, humidity, pressure etc...
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>String</strong>: For boolean or string values.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Card>
|
|
||||||
<Form.Item
|
|
||||||
label="Automatically detect measurement keys"
|
|
||||||
name="autoDetectMeasurements"
|
|
||||||
valuePropName="checked"
|
|
||||||
tooltip="If enabled, measurement-keys will be automatically added based on the decoded payload keys. If the decoded payload contains random keys, you want to disable auto-detection."
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.List name="measurementsMap">
|
|
||||||
{(fields, { add, remove }) => (
|
|
||||||
<>
|
|
||||||
{fields.map(({ key, name, ...restField }) => (
|
|
||||||
<Row gutter={24}>
|
|
||||||
<Col span={6}>
|
|
||||||
<Form.Item
|
|
||||||
{...restField}
|
|
||||||
name={[name, 0]}
|
|
||||||
fieldKey={[name, 0]}
|
|
||||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="Key" />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={6}>
|
|
||||||
<Form.Item
|
|
||||||
{...restField}
|
|
||||||
name={[name, 1, "kind"]}
|
|
||||||
fieldKey={[name, 1, "kind"]}
|
|
||||||
rules={[{ required: true, message: "Please select a kind!" }]}
|
|
||||||
>
|
|
||||||
<Select>
|
|
||||||
<Select.Option value={MeasurementKind.UNKNOWN}>Unknown / unset</Select.Option>
|
|
||||||
<Select.Option value={MeasurementKind.COUNTER}>Counter</Select.Option>
|
|
||||||
<Select.Option value={MeasurementKind.ABSOLUTE}>Absolute</Select.Option>
|
|
||||||
<Select.Option value={MeasurementKind.GAUGE}>Gauge</Select.Option>
|
|
||||||
<Select.Option value={MeasurementKind.STRING}>String</Select.Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={10}>
|
|
||||||
<Form.Item
|
|
||||||
{...restField}
|
|
||||||
name={[name, 1, "name"]}
|
|
||||||
fieldKey={[name, 1, "name"]}
|
|
||||||
rules={[{ required: true, message: "Please enter a description!" }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="Name" />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={2}>
|
|
||||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
))}
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
|
||||||
Add measurement
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Form.List>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
</Tabs>
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="primary" htmlType="submit">
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DeviceProfileTemplateForm;
|
export default DeviceProfileTemplateForm;
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { RouteComponentProps, Link } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Space, Breadcrumb, Card, Button, PageHeader } from "antd";
|
import { useParams, Link, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { Space, Breadcrumb, Card, Button } from "antd";
|
||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeviceProfileTemplate,
|
DeviceProfileTemplate,
|
||||||
@ -15,101 +17,78 @@ import DeviceProfileTemplateForm from "./DeviceProfileTemplateForm";
|
|||||||
import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore";
|
import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore";
|
||||||
import DeleteConfirm from "../../components/DeleteConfirm";
|
import DeleteConfirm from "../../components/DeleteConfirm";
|
||||||
|
|
||||||
interface IState {
|
function EditDeviceProfileTemplate() {
|
||||||
deviceProfileTemplate?: DeviceProfileTemplate;
|
const navigate = useNavigate();
|
||||||
}
|
const [deviceProfileTemplate, setDeviceProfileTemplate] = useState<DeviceProfileTemplate | undefined>(undefined);
|
||||||
|
const { deviceProfileTemplateId } = useParams();
|
||||||
|
|
||||||
interface MatchParams {
|
useEffect(() => {
|
||||||
deviceProfileTemplateId: string;
|
const id = deviceProfileTemplateId!;
|
||||||
}
|
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps<MatchParams> {}
|
|
||||||
|
|
||||||
class EditDeviceProfileTemplate extends Component<IProps, IState> {
|
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.getDeviceProfileTemplate();
|
|
||||||
}
|
|
||||||
|
|
||||||
getDeviceProfileTemplate = () => {
|
|
||||||
const id = this.props.match.params.deviceProfileTemplateId;
|
|
||||||
let req = new GetDeviceProfileTemplateRequest();
|
let req = new GetDeviceProfileTemplateRequest();
|
||||||
req.setId(id);
|
req.setId(id);
|
||||||
|
|
||||||
DeviceProfileTemplateStore.get(req, (resp: GetDeviceProfileTemplateResponse) => {
|
DeviceProfileTemplateStore.get(req, (resp: GetDeviceProfileTemplateResponse) => {
|
||||||
this.setState({
|
setDeviceProfileTemplate(resp.getDeviceProfileTemplate());
|
||||||
deviceProfileTemplate: resp.getDeviceProfileTemplate(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
}, [deviceProfileTemplateId]);
|
||||||
|
|
||||||
onFinish = (obj: DeviceProfileTemplate) => {
|
const onFinish = (obj: DeviceProfileTemplate) => {
|
||||||
let req = new UpdateDeviceProfileTemplateRequest();
|
let req = new UpdateDeviceProfileTemplateRequest();
|
||||||
req.setDeviceProfileTemplate(obj);
|
req.setDeviceProfileTemplate(obj);
|
||||||
|
|
||||||
DeviceProfileTemplateStore.update(req, () => {
|
DeviceProfileTemplateStore.update(req, () => {
|
||||||
this.props.history.push(`/device-profile-templates`);
|
navigate(`/device-profile-templates`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteDeviceProfileTemplate = () => {
|
const deleteDeviceProfileTemplate = () => {
|
||||||
let req = new DeleteDeviceProfileTemplateRequest();
|
let req = new DeleteDeviceProfileTemplateRequest();
|
||||||
req.setId(this.props.match.params.deviceProfileTemplateId);
|
req.setId(deviceProfileTemplateId!);
|
||||||
|
|
||||||
DeviceProfileTemplateStore.delete(req, () => {
|
DeviceProfileTemplateStore.delete(req, () => {
|
||||||
this.props.history.push(`/device-profile-templates`);
|
navigate(`/device-profile-templates`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const dp = deviceProfileTemplate;
|
||||||
const dp = this.state.deviceProfileTemplate;
|
|
||||||
|
|
||||||
if (!dp) {
|
if (!dp) {
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
|
||||||
<PageHeader
|
|
||||||
breadcrumbRender={() => (
|
|
||||||
<Breadcrumb>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>Network Server</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>
|
|
||||||
<Link to={`/device-profile-templates`}>Device-profile templates</Link>
|
|
||||||
</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>{dp.getName()}</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
</Breadcrumb>
|
|
||||||
)}
|
|
||||||
title={dp.getName()}
|
|
||||||
subTitle={`device-profile template id: ${dp.getId()}`}
|
|
||||||
extra={[
|
|
||||||
<DeleteConfirm
|
|
||||||
typ="device-profile template"
|
|
||||||
confirm={dp.getName()}
|
|
||||||
onConfirm={this.deleteDeviceProfileTemplate}
|
|
||||||
>
|
|
||||||
<Button danger type="primary">
|
|
||||||
Delete device-profile template
|
|
||||||
</Button>
|
|
||||||
</DeleteConfirm>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Card>
|
|
||||||
<DeviceProfileTemplateForm initialValues={dp} update={true} onFinish={this.onFinish} />
|
|
||||||
</Card>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
|
<PageHeader
|
||||||
|
breadcrumbRender={() => (
|
||||||
|
<Breadcrumb>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>Network Server</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>
|
||||||
|
<Link to={`/device-profile-templates`}>Device-profile templates</Link>
|
||||||
|
</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>{dp.getName()}</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
</Breadcrumb>
|
||||||
|
)}
|
||||||
|
title={dp.getName()}
|
||||||
|
subTitle={`device-profile template id: ${dp.getId()}`}
|
||||||
|
extra={[
|
||||||
|
<DeleteConfirm typ="device-profile template" confirm={dp.getName()} onConfirm={deleteDeviceProfileTemplate}>
|
||||||
|
<Button danger type="primary">
|
||||||
|
Delete device-profile template
|
||||||
|
</Button>
|
||||||
|
</DeleteConfirm>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Card>
|
||||||
|
<DeviceProfileTemplateForm initialValues={dp} update={true} onFinish={onFinish} />
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditDeviceProfileTemplate;
|
export default EditDeviceProfileTemplate;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Space, Breadcrumb, Button, PageHeader } from "antd";
|
import { Space, Breadcrumb, Button } from "antd";
|
||||||
import { ColumnsType } from "antd/es/table";
|
import { ColumnsType } from "antd/es/table";
|
||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ListDeviceProfileTemplatesRequest,
|
ListDeviceProfileTemplatesRequest,
|
||||||
@ -15,38 +15,36 @@ import { getEnumName } from "../helpers";
|
|||||||
import DataTable, { GetPageCallbackFunc } from "../../components/DataTable";
|
import DataTable, { GetPageCallbackFunc } from "../../components/DataTable";
|
||||||
import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore";
|
import DeviceProfileTemplateStore from "../../stores/DeviceProfileTemplateStore";
|
||||||
|
|
||||||
class ListDeviceProfileTemplates extends Component {
|
function ListDeviceProfileTemplates() {
|
||||||
columns = (): ColumnsType<DeviceProfileTemplateListItem.AsObject> => {
|
const columns: ColumnsType<DeviceProfileTemplateListItem.AsObject> = [
|
||||||
return [
|
{
|
||||||
{
|
title: "Vendor",
|
||||||
title: "Vendor",
|
dataIndex: "vendor",
|
||||||
dataIndex: "vendor",
|
key: "vendor",
|
||||||
key: "vendor",
|
},
|
||||||
|
{
|
||||||
|
title: "Name",
|
||||||
|
dataIndex: "name",
|
||||||
|
key: "name",
|
||||||
|
render: (text, record) => <Link to={`/device-profile-templates/${record.id}/edit`}>{text}</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Firmware",
|
||||||
|
dataIndex: "firmware",
|
||||||
|
key: "firmware",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Region",
|
||||||
|
dataIndex: "region",
|
||||||
|
key: "region",
|
||||||
|
width: 150,
|
||||||
|
render: (text, record) => {
|
||||||
|
return getEnumName(Region, record.region);
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
title: "Name",
|
];
|
||||||
dataIndex: "name",
|
|
||||||
key: "name",
|
|
||||||
render: (text, record) => <Link to={`/device-profile-templates/${record.id}/edit`}>{text}</Link>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Firmware",
|
|
||||||
dataIndex: "firmware",
|
|
||||||
key: "firmware",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Region",
|
|
||||||
dataIndex: "region",
|
|
||||||
key: "region",
|
|
||||||
width: 150,
|
|
||||||
render: (text, record) => {
|
|
||||||
return getEnumName(Region, record.region);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||||
let req = new ListDeviceProfileTemplatesRequest();
|
let req = new ListDeviceProfileTemplatesRequest();
|
||||||
req.setLimit(limit);
|
req.setLimit(limit);
|
||||||
req.setOffset(offset);
|
req.setOffset(offset);
|
||||||
@ -57,31 +55,29 @@ class ListDeviceProfileTemplates extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
<PageHeader
|
||||||
<PageHeader
|
breadcrumbRender={() => (
|
||||||
breadcrumbRender={() => (
|
<Breadcrumb>
|
||||||
<Breadcrumb>
|
<Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<span>Network Server</span>
|
||||||
<span>Network Server</span>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<span>Device-profile templates</span>
|
||||||
<span>Device-profile templates</span>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb>
|
||||||
</Breadcrumb>
|
)}
|
||||||
)}
|
title="Device-profile templates"
|
||||||
title="Device-profile templates"
|
extra={[
|
||||||
extra={[
|
<Button type="primary">
|
||||||
<Button type="primary">
|
<Link to={`/device-profile-templates/create`}>Add device-profile template</Link>
|
||||||
<Link to={`/device-profile-templates/create`}>Add device-profile template</Link>
|
</Button>,
|
||||||
</Button>,
|
]}
|
||||||
]}
|
/>
|
||||||
/>
|
<DataTable columns={columns} getPage={getPage} rowKey="id" />
|
||||||
<DataTable columns={this.columns()} getPage={this.getPage} rowKey="id" />
|
</Space>
|
||||||
</Space>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ListDeviceProfileTemplates;
|
export default ListDeviceProfileTemplates;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { Component } from "react";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { Link, RouteComponentProps } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Space, Breadcrumb, Card, PageHeader } from "antd";
|
import { Space, Breadcrumb, Card } from "antd";
|
||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
|
||||||
import { MacVersion, RegParamsRevision } from "@chirpstack/chirpstack-api-grpc-web/common/common_pb";
|
import { MacVersion, RegParamsRevision } from "@chirpstack/chirpstack-api-grpc-web/common/common_pb";
|
||||||
import {
|
import {
|
||||||
@ -15,97 +15,97 @@ import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
|||||||
import DeviceProfileForm from "./DeviceProfileForm";
|
import DeviceProfileForm from "./DeviceProfileForm";
|
||||||
import DeviceProfileStore from "../../stores/DeviceProfileStore";
|
import DeviceProfileStore from "../../stores/DeviceProfileStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
tenant: Tenant;
|
tenant: Tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateDeviceProfile extends Component<IProps> {
|
function CreateDeviceProfile(props: IProps) {
|
||||||
onFinish = (obj: DeviceProfile) => {
|
const navigate = useNavigate();
|
||||||
obj.setTenantId(this.props.tenant.getId());
|
|
||||||
|
const onFinish = (obj: DeviceProfile) => {
|
||||||
|
obj.setTenantId(props.tenant.getId());
|
||||||
|
|
||||||
let req = new CreateDeviceProfileRequest();
|
let req = new CreateDeviceProfileRequest();
|
||||||
req.setDeviceProfile(obj);
|
req.setDeviceProfile(obj);
|
||||||
|
|
||||||
DeviceProfileStore.create(req, (_resp: CreateDeviceProfileResponse) => {
|
DeviceProfileStore.create(req, (_resp: CreateDeviceProfileResponse) => {
|
||||||
this.props.history.push(`/tenants/${this.props.tenant.getId()}/device-profiles`);
|
navigate(`/tenants/${props.tenant.getId()}/device-profiles`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const codecScript = `// Decode uplink function.
|
||||||
const codecScript = `// Decode uplink function.
|
//
|
||||||
//
|
// Input is an object with the following fields:
|
||||||
// Input is an object with the following fields:
|
// - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0]
|
||||||
// - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0]
|
// - fPort = Uplink fPort.
|
||||||
// - fPort = Uplink fPort.
|
// - variables = Object containing the configured device variables.
|
||||||
// - variables = Object containing the configured device variables.
|
//
|
||||||
//
|
// Output must be an object with the following fields:
|
||||||
// Output must be an object with the following fields:
|
// - data = Object representing the decoded payload.
|
||||||
// - data = Object representing the decoded payload.
|
function decodeUplink(input) {
|
||||||
function decodeUplink(input) {
|
return {
|
||||||
return {
|
data: {
|
||||||
data: {
|
temp: 22.5
|
||||||
temp: 22.5
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode downlink function.
|
|
||||||
//
|
|
||||||
// Input is an object with the following fields:
|
|
||||||
// - data = Object representing the payload that must be encoded.
|
|
||||||
// - variables = Object containing the configured device variables.
|
|
||||||
//
|
|
||||||
// Output must be an object with the following fields:
|
|
||||||
// - bytes = Byte array containing the downlink payload.
|
|
||||||
function encodeDownlink(input) {
|
|
||||||
return {
|
|
||||||
bytes: [225, 230, 255, 0]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
let deviceProfile = new DeviceProfile();
|
|
||||||
deviceProfile.setPayloadCodecScript(codecScript);
|
|
||||||
deviceProfile.setSupportsOtaa(true);
|
|
||||||
deviceProfile.setUplinkInterval(3600);
|
|
||||||
deviceProfile.setDeviceStatusReqInterval(1);
|
|
||||||
deviceProfile.setAdrAlgorithmId("default");
|
|
||||||
deviceProfile.setMacVersion(MacVersion.LORAWAN_1_0_3);
|
|
||||||
deviceProfile.setRegParamsRevision(RegParamsRevision.A);
|
|
||||||
deviceProfile.setFlushQueueOnActivate(true);
|
|
||||||
deviceProfile.setAutoDetectMeasurements(true);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
|
||||||
<PageHeader
|
|
||||||
breadcrumbRender={() => (
|
|
||||||
<Breadcrumb>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>Tenants</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>
|
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
|
||||||
</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>
|
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}/device-profiles`}>Device profiles</Link>
|
|
||||||
</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>Add</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
</Breadcrumb>
|
|
||||||
)}
|
|
||||||
title="Add device profile"
|
|
||||||
/>
|
|
||||||
<Card>
|
|
||||||
<DeviceProfileForm initialValues={deviceProfile} onFinish={this.onFinish} />
|
|
||||||
</Card>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encode downlink function.
|
||||||
|
//
|
||||||
|
// Input is an object with the following fields:
|
||||||
|
// - data = Object representing the payload that must be encoded.
|
||||||
|
// - variables = Object containing the configured device variables.
|
||||||
|
//
|
||||||
|
// Output must be an object with the following fields:
|
||||||
|
// - bytes = Byte array containing the downlink payload.
|
||||||
|
function encodeDownlink(input) {
|
||||||
|
return {
|
||||||
|
bytes: [225, 230, 255, 0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
let deviceProfile = new DeviceProfile();
|
||||||
|
deviceProfile.setPayloadCodecScript(codecScript);
|
||||||
|
deviceProfile.setSupportsOtaa(true);
|
||||||
|
deviceProfile.setUplinkInterval(3600);
|
||||||
|
deviceProfile.setDeviceStatusReqInterval(1);
|
||||||
|
deviceProfile.setAdrAlgorithmId("default");
|
||||||
|
deviceProfile.setMacVersion(MacVersion.LORAWAN_1_0_3);
|
||||||
|
deviceProfile.setRegParamsRevision(RegParamsRevision.A);
|
||||||
|
deviceProfile.setFlushQueueOnActivate(true);
|
||||||
|
deviceProfile.setAutoDetectMeasurements(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
|
<PageHeader
|
||||||
|
breadcrumbRender={() => (
|
||||||
|
<Breadcrumb>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>Tenants</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>
|
||||||
|
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||||
|
</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>
|
||||||
|
<Link to={`/tenants/${props.tenant.getId()}/device-profiles`}>Device profiles</Link>
|
||||||
|
</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>Add</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
</Breadcrumb>
|
||||||
|
)}
|
||||||
|
title="Add device profile"
|
||||||
|
/>
|
||||||
|
<Card>
|
||||||
|
<DeviceProfileForm initialValues={deviceProfile} onFinish={onFinish} />
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateDeviceProfile;
|
export default CreateDeviceProfile;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,8 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { RouteComponentProps, Link } from "react-router-dom";
|
import { useNavigate, Link, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import { Space, Breadcrumb, Card, Button, PageHeader } from "antd";
|
import { Space, Breadcrumb, Card, Button } from "antd";
|
||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
|
||||||
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
||||||
import {
|
import {
|
||||||
@ -18,112 +19,95 @@ import SessionStore from "../../stores/SessionStore";
|
|||||||
import DeleteConfirm from "../../components/DeleteConfirm";
|
import DeleteConfirm from "../../components/DeleteConfirm";
|
||||||
import Admin from "../../components/Admin";
|
import Admin from "../../components/Admin";
|
||||||
|
|
||||||
interface IState {
|
interface IProps {
|
||||||
deviceProfile?: DeviceProfile;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MatchParams {
|
|
||||||
deviceProfileId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps<MatchParams> {
|
|
||||||
tenant: Tenant;
|
tenant: Tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
class EditDeviceProfile extends Component<IProps, IState> {
|
function EditDeviceProfile(props: IProps) {
|
||||||
constructor(props: IProps) {
|
const navigate = useNavigate();
|
||||||
super(props);
|
const [deviceProfile, setDeviceProfile] = useState<DeviceProfile | undefined>(undefined);
|
||||||
this.state = {};
|
const { deviceProfileId } = useParams();
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
useEffect(() => {
|
||||||
this.getDeviceProfile();
|
const id = deviceProfileId!;
|
||||||
}
|
|
||||||
|
|
||||||
getDeviceProfile = () => {
|
|
||||||
const id = this.props.match.params.deviceProfileId;
|
|
||||||
let req = new GetDeviceProfileRequest();
|
let req = new GetDeviceProfileRequest();
|
||||||
req.setId(id);
|
req.setId(id);
|
||||||
|
|
||||||
DeviceProfileStore.get(req, (resp: GetDeviceProfileResponse) => {
|
DeviceProfileStore.get(req, (resp: GetDeviceProfileResponse) => {
|
||||||
this.setState({
|
setDeviceProfile(resp.getDeviceProfile());
|
||||||
deviceProfile: resp.getDeviceProfile(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
}, [deviceProfileId]);
|
||||||
|
|
||||||
onFinish = (obj: DeviceProfile) => {
|
const onFinish = (obj: DeviceProfile) => {
|
||||||
let req = new UpdateDeviceProfileRequest();
|
let req = new UpdateDeviceProfileRequest();
|
||||||
req.setDeviceProfile(obj);
|
req.setDeviceProfile(obj);
|
||||||
|
|
||||||
DeviceProfileStore.update(req, () => {
|
DeviceProfileStore.update(req, () => {
|
||||||
this.props.history.push(`/tenants/${this.props.tenant.getId()}/device-profiles`);
|
navigate(`/tenants/${props.tenant.getId()}/device-profiles`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteDeviceProfile = () => {
|
const deleteDeviceProfile = () => {
|
||||||
let req = new DeleteDeviceProfileRequest();
|
let req = new DeleteDeviceProfileRequest();
|
||||||
req.setId(this.props.match.params.deviceProfileId);
|
req.setId(deviceProfileId!);
|
||||||
|
|
||||||
DeviceProfileStore.delete(req, () => {
|
DeviceProfileStore.delete(req, () => {
|
||||||
this.props.history.push(`/tenants/${this.props.tenant.getId()}/device-profiles`);
|
navigate(`/tenants/${props.tenant.getId()}/device-profiles`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const dp = deviceProfile;
|
||||||
const dp = this.state.deviceProfile;
|
|
||||||
|
|
||||||
if (!dp) {
|
if (!dp) {
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
const disabled = !(
|
|
||||||
SessionStore.isAdmin() ||
|
|
||||||
SessionStore.isTenantAdmin(this.props.tenant.getId()) ||
|
|
||||||
SessionStore.isTenantDeviceAdmin(this.props.tenant.getId())
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
|
||||||
<PageHeader
|
|
||||||
breadcrumbRender={() => (
|
|
||||||
<Breadcrumb>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>Tenants</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>
|
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
|
||||||
</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>
|
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}/device-profiles`}>Device profiles</Link>
|
|
||||||
</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>{dp.getName()}</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
</Breadcrumb>
|
|
||||||
)}
|
|
||||||
title={dp.getName()}
|
|
||||||
subTitle={`device profile id: ${dp.getId()}`}
|
|
||||||
extra={[
|
|
||||||
<Admin tenantId={this.props.tenant.getId()} isDeviceAdmin>
|
|
||||||
<DeleteConfirm typ="device profile" confirm={dp.getName()} onConfirm={this.deleteDeviceProfile}>
|
|
||||||
<Button danger type="primary">
|
|
||||||
Delete device profile
|
|
||||||
</Button>
|
|
||||||
</DeleteConfirm>
|
|
||||||
</Admin>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Card>
|
|
||||||
<DeviceProfileForm initialValues={dp} disabled={disabled} onFinish={this.onFinish} />
|
|
||||||
</Card>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const disabled = !(
|
||||||
|
SessionStore.isAdmin() ||
|
||||||
|
SessionStore.isTenantAdmin(props.tenant.getId()) ||
|
||||||
|
SessionStore.isTenantDeviceAdmin(props.tenant.getId())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
|
<PageHeader
|
||||||
|
breadcrumbRender={() => (
|
||||||
|
<Breadcrumb>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>Tenants</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>
|
||||||
|
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||||
|
</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>
|
||||||
|
<Link to={`/tenants/${props.tenant.getId()}/device-profiles`}>Device profiles</Link>
|
||||||
|
</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>{dp.getName()}</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
</Breadcrumb>
|
||||||
|
)}
|
||||||
|
title={dp.getName()}
|
||||||
|
subTitle={`device profile id: ${dp.getId()}`}
|
||||||
|
extra={[
|
||||||
|
<Admin tenantId={props.tenant.getId()} isDeviceAdmin>
|
||||||
|
<DeleteConfirm typ="device profile" confirm={dp.getName()} onConfirm={deleteDeviceProfile}>
|
||||||
|
<Button danger type="primary">
|
||||||
|
Delete device profile
|
||||||
|
</Button>
|
||||||
|
</DeleteConfirm>
|
||||||
|
</Admin>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Card>
|
||||||
|
<DeviceProfileForm initialValues={dp} disabled={disabled} onFinish={onFinish} />
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditDeviceProfile;
|
export default EditDeviceProfile;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Space, Breadcrumb, Button, PageHeader } from "antd";
|
import { Space, Breadcrumb, Button } from "antd";
|
||||||
import { ColumnsType } from "antd/es/table";
|
import { ColumnsType } from "antd/es/table";
|
||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ListDeviceProfilesRequest,
|
ListDeviceProfilesRequest,
|
||||||
@ -21,89 +21,87 @@ interface IProps {
|
|||||||
tenant: Tenant;
|
tenant: Tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ListDeviceProfiles extends Component<IProps> {
|
function ListDeviceProfiles(props: IProps) {
|
||||||
columns = (): ColumnsType<DeviceProfileListItem.AsObject> => {
|
const columns: ColumnsType<DeviceProfileListItem.AsObject> = [
|
||||||
return [
|
{
|
||||||
{
|
title: "Name",
|
||||||
title: "Name",
|
dataIndex: "name",
|
||||||
dataIndex: "name",
|
key: "name",
|
||||||
key: "name",
|
render: (text, record) => (
|
||||||
render: (text, record) => (
|
<Link to={`/tenants/${props.tenant.getId()}/device-profiles/${record.id}/edit`}>{text}</Link>
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}/device-profiles/${record.id}/edit`}>{text}</Link>
|
),
|
||||||
),
|
},
|
||||||
|
{
|
||||||
|
title: "Region",
|
||||||
|
dataIndex: "region",
|
||||||
|
key: "region",
|
||||||
|
width: 150,
|
||||||
|
render: (text, record) => {
|
||||||
|
return getEnumName(Region, record.region);
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
title: "Region",
|
{
|
||||||
dataIndex: "region",
|
title: "MAC version",
|
||||||
key: "region",
|
dataIndex: "macVersion",
|
||||||
width: 150,
|
key: "macVersion",
|
||||||
render: (text, record) => {
|
width: 150,
|
||||||
return getEnumName(Region, record.region);
|
render: (text, record) => {
|
||||||
},
|
return formatMacVersion(record.macVersion);
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
title: "MAC version",
|
{
|
||||||
dataIndex: "macVersion",
|
title: "Revision",
|
||||||
key: "macVersion",
|
dataIndex: "regParamsRevision",
|
||||||
width: 150,
|
key: "regParamsRevision",
|
||||||
render: (text, record) => {
|
width: 150,
|
||||||
return formatMacVersion(record.macVersion);
|
render: (text, record) => {
|
||||||
},
|
return formatRegParamsRevision(record.regParamsRevision);
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
title: "Revision",
|
{
|
||||||
dataIndex: "regParamsRevision",
|
title: "Supports OTAA",
|
||||||
key: "regParamsRevision",
|
dataIndex: "supportsOtaa",
|
||||||
width: 150,
|
key: "supportsOtaa",
|
||||||
render: (text, record) => {
|
width: 150,
|
||||||
return formatRegParamsRevision(record.regParamsRevision);
|
render: (text, record) => {
|
||||||
},
|
if (record.supportsOtaa) {
|
||||||
|
return "yes";
|
||||||
|
} else {
|
||||||
|
return "no";
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
title: "Supports OTAA",
|
{
|
||||||
dataIndex: "supportsOtaa",
|
title: "Supports Class-B",
|
||||||
key: "supportsOtaa",
|
dataIndex: "supportsClassB",
|
||||||
width: 150,
|
key: "supportsClassB",
|
||||||
render: (text, record) => {
|
width: 150,
|
||||||
if (record.supportsOtaa) {
|
render: (text, record) => {
|
||||||
return "yes";
|
if (record.supportsClassB) {
|
||||||
} else {
|
return "yes";
|
||||||
return "no";
|
} else {
|
||||||
}
|
return "no";
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
title: "Supports Class-B",
|
{
|
||||||
dataIndex: "supportsClassB",
|
title: "Supports Class-C",
|
||||||
key: "supportsClassB",
|
dataIndex: "supportsClassC",
|
||||||
width: 150,
|
key: "supportsClassC",
|
||||||
render: (text, record) => {
|
width: 150,
|
||||||
if (record.supportsClassB) {
|
render: (text, record) => {
|
||||||
return "yes";
|
if (record.supportsClassC) {
|
||||||
} else {
|
return "yes";
|
||||||
return "no";
|
} else {
|
||||||
}
|
return "no";
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
title: "Supports Class-C",
|
];
|
||||||
dataIndex: "supportsClassC",
|
|
||||||
key: "supportsClassC",
|
|
||||||
width: 150,
|
|
||||||
render: (text, record) => {
|
|
||||||
if (record.supportsClassC) {
|
|
||||||
return "yes";
|
|
||||||
} else {
|
|
||||||
return "no";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||||
let req = new ListDeviceProfilesRequest();
|
let req = new ListDeviceProfilesRequest();
|
||||||
req.setTenantId(this.props.tenant.getId());
|
req.setTenantId(props.tenant.getId());
|
||||||
req.setLimit(limit);
|
req.setLimit(limit);
|
||||||
req.setOffset(offset);
|
req.setOffset(offset);
|
||||||
|
|
||||||
@ -113,38 +111,36 @@ class ListDeviceProfiles extends Component<IProps> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
<PageHeader
|
||||||
<PageHeader
|
breadcrumbRender={() => (
|
||||||
breadcrumbRender={() => (
|
<Breadcrumb>
|
||||||
<Breadcrumb>
|
<Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<span>Tenants</span>
|
||||||
<span>Tenants</span>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<span>
|
||||||
<span>
|
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
</span>
|
||||||
</span>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<span>Device profiles</span>
|
||||||
<span>Device profiles</span>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb>
|
||||||
</Breadcrumb>
|
)}
|
||||||
)}
|
title="Device profiles"
|
||||||
title="Device profiles"
|
extra={[
|
||||||
extra={[
|
<Admin tenantId={props.tenant.getId()} isDeviceAdmin>
|
||||||
<Admin tenantId={this.props.tenant.getId()} isDeviceAdmin>
|
<Button type="primary">
|
||||||
<Button type="primary">
|
<Link to={`/tenants/${props.tenant.getId()}/device-profiles/create`}>Add device profile</Link>
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}/device-profiles/create`}>Add device profile</Link>
|
</Button>
|
||||||
</Button>
|
</Admin>,
|
||||||
</Admin>,
|
]}
|
||||||
]}
|
/>
|
||||||
/>
|
<DataTable columns={columns} getPage={getPage} rowKey="id" />
|
||||||
<DataTable columns={this.columns()} getPage={this.getPage} rowKey="id" />
|
</Space>
|
||||||
</Space>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ListDeviceProfiles;
|
export default ListDeviceProfiles;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { Component } from "react";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { Link, RouteComponentProps } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Space, Breadcrumb, Card, PageHeader } from "antd";
|
import { Space, Breadcrumb, Card } from "antd";
|
||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
|
||||||
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
||||||
import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||||
@ -15,14 +15,16 @@ import DeviceForm from "./DeviceForm";
|
|||||||
import DeviceStore from "../../stores/DeviceStore";
|
import DeviceStore from "../../stores/DeviceStore";
|
||||||
import DeviceProfileStore from "../../stores/DeviceProfileStore";
|
import DeviceProfileStore from "../../stores/DeviceProfileStore";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
tenant: Tenant;
|
tenant: Tenant;
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateDevice extends Component<IProps> {
|
function CreateDevice(props: IProps) {
|
||||||
onFinish = (obj: Device) => {
|
const navigate = useNavigate();
|
||||||
obj.setApplicationId(this.props.application.getId());
|
|
||||||
|
const onFinish = (obj: Device) => {
|
||||||
|
obj.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
let req = new CreateDeviceRequest();
|
let req = new CreateDeviceRequest();
|
||||||
req.setDevice(obj);
|
req.setDevice(obj);
|
||||||
@ -34,60 +36,58 @@ class CreateDevice extends Component<IProps> {
|
|||||||
DeviceProfileStore.get(req, (resp: GetDeviceProfileResponse) => {
|
DeviceProfileStore.get(req, (resp: GetDeviceProfileResponse) => {
|
||||||
let dp = resp.getDeviceProfile()!;
|
let dp = resp.getDeviceProfile()!;
|
||||||
if (dp.getSupportsOtaa()) {
|
if (dp.getSupportsOtaa()) {
|
||||||
this.props.history.push(
|
navigate(
|
||||||
`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}/devices/${obj.getDevEui()}/keys`,
|
`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}/devices/${obj.getDevEui()}/keys`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.props.history.push(
|
navigate(
|
||||||
`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}/devices/${obj.getDevEui()}`,
|
`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}/devices/${obj.getDevEui()}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
let device = new Device();
|
||||||
let device = new Device();
|
device.setApplicationId(props.application.getId());
|
||||||
device.setApplicationId(this.props.application.getId());
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
breadcrumbRender={() => (
|
breadcrumbRender={() => (
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<span>Tenants</span>
|
<span>Tenants</span>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<span>
|
<span>
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||||
</span>
|
</span>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<span>
|
<span>
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}/applications`}>Applications</Link>
|
<Link to={`/tenants/${props.tenant.getId()}/applications`}>Applications</Link>
|
||||||
</span>
|
</span>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<span>
|
<span>
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}`}>
|
<Link to={`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}`}>
|
||||||
{this.props.application.getName()}
|
{props.application.getName()}
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<span>Add device</span>
|
<span>Add device</span>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
)}
|
)}
|
||||||
title="Add device"
|
title="Add device"
|
||||||
/>
|
/>
|
||||||
<Card>
|
<Card>
|
||||||
<DeviceForm tenant={this.props.tenant} initialValues={device} onFinish={this.onFinish} />
|
<DeviceForm tenant={props.tenant} initialValues={device} onFinish={onFinish} />
|
||||||
</Card>
|
</Card>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateDevice;
|
export default CreateDevice;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Space, Form, Button, Row, Col, InputNumber, Alert } from "antd";
|
import { Space, Form, Button, Row, Col, InputNumber, Alert } from "antd";
|
||||||
|
|
||||||
@ -26,11 +26,11 @@ interface FormProps {
|
|||||||
onFinish: (obj: DeviceActivationPb) => void;
|
onFinish: (obj: DeviceActivationPb) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class LW10DeviceActivationForm extends Component<FormProps> {
|
function LW10DeviceActivationForm(props: FormProps) {
|
||||||
formRef = React.createRef<any>();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
onFinish = (values: DeviceActivationPb.AsObject) => {
|
const onFinish = (values: DeviceActivationPb.AsObject) => {
|
||||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
const v = Object.assign(props.initialValues.toObject(), values);
|
||||||
let da = new DeviceActivationPb();
|
let da = new DeviceActivationPb();
|
||||||
|
|
||||||
da.setDevAddr(v.devAddr);
|
da.setDevAddr(v.devAddr);
|
||||||
@ -42,66 +42,56 @@ class LW10DeviceActivationForm extends Component<FormProps> {
|
|||||||
da.setAFCntDown(v.nFCntDown);
|
da.setAFCntDown(v.nFCntDown);
|
||||||
da.setNFCntDown(v.nFCntDown);
|
da.setNFCntDown(v.nFCntDown);
|
||||||
|
|
||||||
this.props.onFinish(da);
|
props.onFinish(da);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
|
||||||
<Form
|
<DevAddrInput
|
||||||
layout="vertical"
|
label="Device address"
|
||||||
initialValues={this.props.initialValues.toObject()}
|
name="devAddr"
|
||||||
onFinish={this.onFinish}
|
value={props.initialValues.getDevAddr()}
|
||||||
ref={this.formRef}
|
devEui={props.device.getDevEui()}
|
||||||
>
|
required
|
||||||
<DevAddrInput
|
/>
|
||||||
label="Device address"
|
<AesKeyInput
|
||||||
name="devAddr"
|
label="Network session key (LoRaWAN 1.0)"
|
||||||
value={this.props.initialValues.getDevAddr()}
|
name="nwkSEncKey"
|
||||||
devEui={this.props.device.getDevEui()}
|
value={props.initialValues.getNwkSEncKey()}
|
||||||
formRef={this.formRef}
|
required
|
||||||
required
|
/>
|
||||||
/>
|
<AesKeyInput
|
||||||
<AesKeyInput
|
label="Application session key (LoRaWAN 1.0)"
|
||||||
label="Network session key (LoRaWAN 1.0)"
|
name="appSKey"
|
||||||
name="nwkSEncKey"
|
value={props.initialValues.getAppSKey()}
|
||||||
value={this.props.initialValues.getNwkSEncKey()}
|
required
|
||||||
formRef={this.formRef}
|
/>
|
||||||
required
|
<Row gutter={24}>
|
||||||
/>
|
<Col span={6}>
|
||||||
<AesKeyInput
|
<Form.Item label="Uplink frame-counter" name="fCntUp">
|
||||||
label="Application session key (LoRaWAN 1.0)"
|
<InputNumber min={0} />
|
||||||
name="appSKey"
|
</Form.Item>
|
||||||
value={this.props.initialValues.getAppSKey()}
|
</Col>
|
||||||
formRef={this.formRef}
|
<Col span={6}>
|
||||||
required
|
<Form.Item label="Downlink frame-counter" name="nFCntDown">
|
||||||
/>
|
<InputNumber min={0} />
|
||||||
<Row gutter={24}>
|
</Form.Item>
|
||||||
<Col span={6}>
|
</Col>
|
||||||
<Form.Item label="Uplink frame-counter" name="fCntUp">
|
</Row>
|
||||||
<InputNumber min={0} />
|
<Form.Item>
|
||||||
</Form.Item>
|
<Button type="primary" htmlType="submit" disabled={props.disabled}>
|
||||||
</Col>
|
(Re)activate device
|
||||||
<Col span={6}>
|
</Button>
|
||||||
<Form.Item label="Downlink frame-counter" name="nFCntDown">
|
</Form.Item>
|
||||||
<InputNumber min={0} />
|
</Form>
|
||||||
</Form.Item>
|
);
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="primary" htmlType="submit" disabled={this.props.disabled}>
|
|
||||||
(Re)activate device
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class LW11DeviceActivationForm extends Component<FormProps> {
|
function LW11DeviceActivationForm(props: FormProps) {
|
||||||
formRef = React.createRef<any>();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
onFinish = (values: DeviceActivationPb.AsObject) => {
|
const onFinish = (values: DeviceActivationPb.AsObject) => {
|
||||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
const v = Object.assign(props.initialValues.toObject(), values);
|
||||||
let da = new DeviceActivationPb();
|
let da = new DeviceActivationPb();
|
||||||
|
|
||||||
da.setDevAddr(v.devAddr);
|
da.setDevAddr(v.devAddr);
|
||||||
@ -113,162 +103,133 @@ class LW11DeviceActivationForm extends Component<FormProps> {
|
|||||||
da.setAFCntDown(v.aFCntDown);
|
da.setAFCntDown(v.aFCntDown);
|
||||||
da.setNFCntDown(v.nFCntDown);
|
da.setNFCntDown(v.nFCntDown);
|
||||||
|
|
||||||
this.props.onFinish(da);
|
props.onFinish(da);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
|
||||||
<Form
|
<DevAddrInput
|
||||||
layout="vertical"
|
label="Device address"
|
||||||
initialValues={this.props.initialValues.toObject()}
|
name="devAddr"
|
||||||
onFinish={this.onFinish}
|
value={props.initialValues.getDevAddr()}
|
||||||
ref={this.formRef}
|
devEui={props.device.getDevEui()}
|
||||||
>
|
required
|
||||||
<DevAddrInput
|
/>
|
||||||
label="Device address"
|
<AesKeyInput
|
||||||
name="devAddr"
|
label="Network session encryption key"
|
||||||
value={this.props.initialValues.getDevAddr()}
|
name="nwkSEncKey"
|
||||||
devEui={this.props.device.getDevEui()}
|
value={props.initialValues.getNwkSEncKey()}
|
||||||
formRef={this.formRef}
|
required
|
||||||
required
|
/>
|
||||||
/>
|
<AesKeyInput
|
||||||
<AesKeyInput
|
label="Serving network session integrity key"
|
||||||
label="Network session encryption key"
|
name="sNwkSIntKey"
|
||||||
name="nwkSEncKey"
|
value={props.initialValues.getSNwkSIntKey()}
|
||||||
value={this.props.initialValues.getNwkSEncKey()}
|
required
|
||||||
formRef={this.formRef}
|
/>
|
||||||
required
|
<AesKeyInput
|
||||||
/>
|
label="Forwarding network session integrity key"
|
||||||
<AesKeyInput
|
name="fNwkSIntKey"
|
||||||
label="Serving network session integrity key"
|
value={props.initialValues.getFNwkSIntKey()}
|
||||||
name="sNwkSIntKey"
|
required
|
||||||
value={this.props.initialValues.getSNwkSIntKey()}
|
/>
|
||||||
formRef={this.formRef}
|
<AesKeyInput label="Application session key" name="appSKey" value={props.initialValues.getAppSKey()} required />
|
||||||
required
|
<Row gutter={24}>
|
||||||
/>
|
<Col span={6}>
|
||||||
<AesKeyInput
|
<Form.Item label="Uplink frame-counter" name="fCntUp">
|
||||||
label="Forwarding network session integrity key"
|
<InputNumber min={0} />
|
||||||
name="fNwkSIntKey"
|
</Form.Item>
|
||||||
value={this.props.initialValues.getFNwkSIntKey()}
|
</Col>
|
||||||
formRef={this.formRef}
|
<Col span={6}>
|
||||||
required
|
<Form.Item label="Downlink frame-counter (network)" name="nFCntDown">
|
||||||
/>
|
<InputNumber min={0} />
|
||||||
<AesKeyInput
|
</Form.Item>
|
||||||
label="Application session key"
|
</Col>
|
||||||
name="appSKey"
|
<Col span={6}>
|
||||||
value={this.props.initialValues.getAppSKey()}
|
<Form.Item label="Downlink frame-counter (application)" name="aFCntDown">
|
||||||
formRef={this.formRef}
|
<InputNumber min={0} />
|
||||||
required
|
</Form.Item>
|
||||||
/>
|
</Col>
|
||||||
<Row gutter={24}>
|
</Row>
|
||||||
<Col span={6}>
|
<Form.Item>
|
||||||
<Form.Item label="Uplink frame-counter" name="fCntUp">
|
<Button type="primary" htmlType="submit" disabled={props.disabled}>
|
||||||
<InputNumber min={0} />
|
(Re)activate device
|
||||||
</Form.Item>
|
</Button>
|
||||||
</Col>
|
</Form.Item>
|
||||||
<Col span={6}>
|
</Form>
|
||||||
<Form.Item label="Downlink frame-counter (network)" name="nFCntDown">
|
);
|
||||||
<InputNumber min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={6}>
|
|
||||||
<Form.Item label="Downlink frame-counter (application)" name="aFCntDown">
|
|
||||||
<InputNumber min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="primary" htmlType="submit" disabled={this.props.disabled}>
|
|
||||||
(Re)activate device
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
tenant: Tenant;
|
tenant: Tenant;
|
||||||
application: Application;
|
application: Application;
|
||||||
device: Device;
|
device: Device;
|
||||||
deviceProfile: DeviceProfile;
|
deviceProfile: DeviceProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function DeviceActivation(props: IProps) {
|
||||||
deviceActivation?: DeviceActivationPb;
|
const navigate = useNavigate();
|
||||||
deviceActivationRequested: boolean;
|
const [deviceActivation, setDeviceActivation] = useState<DeviceActivationPb | undefined>(undefined);
|
||||||
}
|
const [deviceActivationRequested, setDeviceActivationRequested] = useState<boolean>(false);
|
||||||
|
|
||||||
class DeviceActivation extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
deviceActivationRequested: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
let req = new GetDeviceActivationRequest();
|
let req = new GetDeviceActivationRequest();
|
||||||
req.setDevEui(this.props.device.getDevEui());
|
req.setDevEui(props.device.getDevEui());
|
||||||
|
|
||||||
DeviceStore.getActivation(req, (resp: GetDeviceActivationResponse) => {
|
DeviceStore.getActivation(req, (resp: GetDeviceActivationResponse) => {
|
||||||
this.setState({
|
setDeviceActivation(resp.getDeviceActivation());
|
||||||
deviceActivation: resp.getDeviceActivation(),
|
setDeviceActivationRequested(true);
|
||||||
deviceActivationRequested: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}, [props]);
|
||||||
|
|
||||||
onFinish = (obj: DeviceActivationPb) => {
|
const onFinish = (obj: DeviceActivationPb) => {
|
||||||
let req = new ActivateDeviceRequest();
|
let req = new ActivateDeviceRequest();
|
||||||
obj.setDevEui(this.props.device.getDevEui());
|
obj.setDevEui(props.device.getDevEui());
|
||||||
req.setDeviceActivation(obj);
|
req.setDeviceActivation(obj);
|
||||||
|
|
||||||
DeviceStore.activate(req, () => {
|
DeviceStore.activate(req, () => {
|
||||||
this.props.history.push(
|
navigate(
|
||||||
`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}/devices/${this.props.device.getDevEui()}`,
|
`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}/devices/${props.device.getDevEui()}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
if (!deviceActivationRequested) {
|
||||||
if (!this.state.deviceActivationRequested) {
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.state.deviceActivation && this.props.deviceProfile.getSupportsOtaa()) {
|
|
||||||
return <Alert type="info" showIcon message="This device has not (yet) been activated." />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let macVersion = this.props.deviceProfile.getMacVersion();
|
|
||||||
const lw11 = macVersion === MacVersion.LORAWAN_1_1_0;
|
|
||||||
|
|
||||||
let initialValues = new DeviceActivationPb();
|
|
||||||
if (this.state.deviceActivation) {
|
|
||||||
initialValues = this.state.deviceActivation;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
|
||||||
{!lw11 && (
|
|
||||||
<LW10DeviceActivationForm
|
|
||||||
initialValues={initialValues}
|
|
||||||
device={this.props.device}
|
|
||||||
onFinish={this.onFinish}
|
|
||||||
disabled={this.props.deviceProfile.getSupportsOtaa()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{lw11 && (
|
|
||||||
<LW11DeviceActivationForm
|
|
||||||
initialValues={initialValues}
|
|
||||||
device={this.props.device}
|
|
||||||
onFinish={this.onFinish}
|
|
||||||
disabled={this.props.deviceProfile.getSupportsOtaa()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!deviceActivation && props.deviceProfile.getSupportsOtaa()) {
|
||||||
|
return <Alert type="info" showIcon message="This device has not (yet) been activated." />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let macVersion = props.deviceProfile.getMacVersion();
|
||||||
|
const lw11 = macVersion === MacVersion.LORAWAN_1_1_0;
|
||||||
|
|
||||||
|
let initialValues = new DeviceActivationPb();
|
||||||
|
if (deviceActivation) {
|
||||||
|
initialValues = deviceActivation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
|
{!lw11 && (
|
||||||
|
<LW10DeviceActivationForm
|
||||||
|
initialValues={initialValues}
|
||||||
|
device={props.device}
|
||||||
|
onFinish={onFinish}
|
||||||
|
disabled={props.deviceProfile.getSupportsOtaa()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{lw11 && (
|
||||||
|
<LW11DeviceActivationForm
|
||||||
|
initialValues={initialValues}
|
||||||
|
device={props.device}
|
||||||
|
onFinish={onFinish}
|
||||||
|
disabled={props.deviceProfile.getSupportsOtaa()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DeviceActivation;
|
export default DeviceActivation;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
@ -27,31 +27,18 @@ interface IProps {
|
|||||||
lastSeenAt?: Date;
|
lastSeenAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function DeviceDashboard(props: IProps) {
|
||||||
metricsAggregation: Aggregation;
|
const [metricsAggregation, setMetricsAggregation] = useState<Aggregation>(Aggregation.DAY);
|
||||||
deviceMetrics?: GetDeviceMetricsResponse;
|
const [deviceMetrics, setDeviceMetrics] = useState<GetDeviceMetricsResponse | undefined>(undefined);
|
||||||
deviceLinkMetrics?: GetDeviceLinkMetricsResponse;
|
const [deviceLinkMetrics, setDeviceLinkMetrics] = useState<GetDeviceLinkMetricsResponse | undefined>(undefined);
|
||||||
deviceMetricsLoaded: boolean;
|
const [deviceLinkMetricsLoaded, setDeviceLinkMetricsLoaded] = useState<boolean>(false);
|
||||||
deviceLinkMetricsLoaded: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
class DeviceDashboard extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
loadMetrics();
|
||||||
super(props);
|
}, [props, metricsAggregation]);
|
||||||
|
|
||||||
this.state = {
|
const loadMetrics = () => {
|
||||||
metricsAggregation: Aggregation.DAY,
|
const agg = metricsAggregation;
|
||||||
deviceMetricsLoaded: false,
|
|
||||||
deviceLinkMetricsLoaded: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.loadMetrics();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMetrics = () => {
|
|
||||||
const agg = this.state.metricsAggregation;
|
|
||||||
const end = moment();
|
const end = moment();
|
||||||
let start = moment();
|
let start = moment();
|
||||||
|
|
||||||
@ -63,19 +50,12 @@ class DeviceDashboard extends Component<IProps, IState> {
|
|||||||
start = start.subtract(12, "months");
|
start = start.subtract(12, "months");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(
|
setDeviceLinkMetricsLoaded(false);
|
||||||
{
|
loadLinkMetrics(start.toDate(), end.toDate(), agg);
|
||||||
deviceMetricsLoaded: false,
|
loadDeviceMetrics(start.toDate(), end.toDate(), agg);
|
||||||
deviceLinkMetricsLoaded: false,
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.loadLinkMetrics(start.toDate(), end.toDate(), agg);
|
|
||||||
this.loadDeviceMetrics(start.toDate(), end.toDate(), agg);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
loadDeviceMetrics = (start: Date, end: Date, agg: Aggregation) => {
|
const loadDeviceMetrics = (start: Date, end: Date, agg: Aggregation) => {
|
||||||
let startPb = new Timestamp();
|
let startPb = new Timestamp();
|
||||||
let endPb = new Timestamp();
|
let endPb = new Timestamp();
|
||||||
|
|
||||||
@ -83,20 +63,17 @@ class DeviceDashboard extends Component<IProps, IState> {
|
|||||||
endPb.fromDate(end);
|
endPb.fromDate(end);
|
||||||
|
|
||||||
let req = new GetDeviceMetricsRequest();
|
let req = new GetDeviceMetricsRequest();
|
||||||
req.setDevEui(this.props.device.getDevEui());
|
req.setDevEui(props.device.getDevEui());
|
||||||
req.setStart(startPb);
|
req.setStart(startPb);
|
||||||
req.setEnd(endPb);
|
req.setEnd(endPb);
|
||||||
req.setAggregation(agg);
|
req.setAggregation(agg);
|
||||||
|
|
||||||
DeviceStore.getMetrics(req, (resp: GetDeviceMetricsResponse) => {
|
DeviceStore.getMetrics(req, (resp: GetDeviceMetricsResponse) => {
|
||||||
this.setState({
|
setDeviceMetrics(resp);
|
||||||
deviceMetrics: resp,
|
|
||||||
deviceMetricsLoaded: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
loadLinkMetrics = (start: Date, end: Date, agg: Aggregation) => {
|
const loadLinkMetrics = (start: Date, end: Date, agg: Aggregation) => {
|
||||||
let startPb = new Timestamp();
|
let startPb = new Timestamp();
|
||||||
let endPb = new Timestamp();
|
let endPb = new Timestamp();
|
||||||
|
|
||||||
@ -104,176 +81,153 @@ class DeviceDashboard extends Component<IProps, IState> {
|
|||||||
endPb.fromDate(end);
|
endPb.fromDate(end);
|
||||||
|
|
||||||
let req = new GetDeviceLinkMetricsRequest();
|
let req = new GetDeviceLinkMetricsRequest();
|
||||||
req.setDevEui(this.props.device.getDevEui());
|
req.setDevEui(props.device.getDevEui());
|
||||||
req.setStart(startPb);
|
req.setStart(startPb);
|
||||||
req.setEnd(endPb);
|
req.setEnd(endPb);
|
||||||
req.setAggregation(agg);
|
req.setAggregation(agg);
|
||||||
|
|
||||||
DeviceStore.getLinkMetrics(req, (resp: GetDeviceLinkMetricsResponse) => {
|
DeviceStore.getLinkMetrics(req, (resp: GetDeviceLinkMetricsResponse) => {
|
||||||
this.setState({
|
setDeviceLinkMetrics(resp);
|
||||||
deviceLinkMetrics: resp,
|
setDeviceLinkMetricsLoaded(true);
|
||||||
deviceLinkMetricsLoaded: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onMetricsAggregationChange = (e: RadioChangeEvent) => {
|
const onMetricsAggregationChange = (e: RadioChangeEvent) => {
|
||||||
this.setState(
|
setMetricsAggregation(e.target.value);
|
||||||
{
|
|
||||||
metricsAggregation: e.target.value,
|
|
||||||
},
|
|
||||||
this.loadMetrics,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
if (deviceLinkMetrics === undefined || deviceMetrics === undefined) {
|
||||||
if (this.state.deviceLinkMetrics === undefined || this.state.deviceMetrics === undefined) {
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let deviceMetrics = [];
|
|
||||||
|
|
||||||
{
|
|
||||||
let states = this.state.deviceMetrics.getStatesMap();
|
|
||||||
let keys = states.toArray().map(v => v[0]);
|
|
||||||
keys.sort();
|
|
||||||
|
|
||||||
for (let i = 0; i < keys.length; i += 3) {
|
|
||||||
let items = keys.slice(i, i + 3).map(k => {
|
|
||||||
let m = states.get(k)!;
|
|
||||||
return (
|
|
||||||
<Col span={8}>
|
|
||||||
<Card>
|
|
||||||
<Statistic title={m.getName()} value={m.getValue()} />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
deviceMetrics.push(<Row gutter={24}>{items}</Row>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let metrics = this.state.deviceMetrics.getMetricsMap();
|
|
||||||
let keys = metrics.toArray().map(v => v[0]);
|
|
||||||
keys.sort();
|
|
||||||
|
|
||||||
for (let i = 0; i < keys.length; i += 3) {
|
|
||||||
let items = keys.slice(i, i + 3).map(k => {
|
|
||||||
let m = metrics.get(k)!;
|
|
||||||
return (
|
|
||||||
<Col span={8}>
|
|
||||||
<MetricChart metric={m} aggregation={this.state.metricsAggregation} zeroToNull />
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
deviceMetrics.push(<Row gutter={24}>{items}</Row>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastSeenAt = "Never";
|
|
||||||
if (this.props.lastSeenAt !== undefined) {
|
|
||||||
lastSeenAt = moment(this.props.lastSeenAt).format("YYYY-MM-DD HH:mm:ss");
|
|
||||||
}
|
|
||||||
|
|
||||||
const loading = !this.state.deviceLinkMetricsLoaded || !this.state.deviceMetrics;
|
|
||||||
|
|
||||||
const aggregations = (
|
|
||||||
<Space direction="horizontal">
|
|
||||||
{loading && <Spin size="small" />}
|
|
||||||
<Radio.Group value={this.state.metricsAggregation} onChange={this.onMetricsAggregationChange} size="small">
|
|
||||||
<Radio.Button value={Aggregation.HOUR} disabled={loading}>
|
|
||||||
24h
|
|
||||||
</Radio.Button>
|
|
||||||
<Radio.Button value={Aggregation.DAY} disabled={loading}>
|
|
||||||
31d
|
|
||||||
</Radio.Button>
|
|
||||||
<Radio.Button value={Aggregation.MONTH} disabled={loading}>
|
|
||||||
1y
|
|
||||||
</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
<Button type="primary" size="small" icon={<ReloadOutlined />} onClick={this.loadMetrics} disabled={loading} />
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
|
||||||
<Card>
|
|
||||||
<Descriptions>
|
|
||||||
<Descriptions.Item label="Last seen">{lastSeenAt}</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="Device profile">
|
|
||||||
<Link
|
|
||||||
to={`/tenants/${this.props.deviceProfile.getTenantId()}/device-profiles/${this.props.deviceProfile.getId()}/edit`}
|
|
||||||
>
|
|
||||||
{this.props.deviceProfile.getName()}
|
|
||||||
</Link>
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="Enabled">{this.props.device.getIsDisabled() ? "no" : "yes"}</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="Description">{this.props.device.getDescription()}</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
</Card>
|
|
||||||
<Tabs tabBarExtraContent={aggregations}>
|
|
||||||
<Tabs.TabPane tab="Link metrics" key="1">
|
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
|
||||||
<Row gutter={24}>
|
|
||||||
<Col span={8}>
|
|
||||||
<MetricChart
|
|
||||||
metric={this.state.deviceLinkMetrics.getRxPackets()!}
|
|
||||||
aggregation={this.state.metricsAggregation}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col span={8}>
|
|
||||||
<MetricChart
|
|
||||||
metric={this.state.deviceLinkMetrics.getGwRssi()!}
|
|
||||||
aggregation={this.state.metricsAggregation}
|
|
||||||
zeroToNull
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col span={8}>
|
|
||||||
<MetricChart
|
|
||||||
metric={this.state.deviceLinkMetrics.getGwSnr()!}
|
|
||||||
aggregation={this.state.metricsAggregation}
|
|
||||||
zeroToNull
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row gutter={24}>
|
|
||||||
<Col span={8}>
|
|
||||||
<MetricHeatmap
|
|
||||||
metric={this.state.deviceLinkMetrics.getRxPacketsPerFreq()!}
|
|
||||||
aggregation={this.state.metricsAggregation}
|
|
||||||
fromColor="rgb(227, 242, 253)"
|
|
||||||
toColor="rgb(33, 150, 243, 1)"
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col span={8}>
|
|
||||||
<MetricHeatmap
|
|
||||||
metric={this.state.deviceLinkMetrics.getRxPacketsPerDr()!}
|
|
||||||
aggregation={this.state.metricsAggregation}
|
|
||||||
fromColor="rgb(227, 242, 253)"
|
|
||||||
toColor="rgb(33, 150, 243, 1)"
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col span={8}>
|
|
||||||
<MetricBar
|
|
||||||
metric={this.state.deviceLinkMetrics.getErrors()!}
|
|
||||||
aggregation={this.state.metricsAggregation}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Space>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
<Tabs.TabPane tab="Device metrics" key="2">
|
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
|
||||||
{deviceMetrics}
|
|
||||||
</Space>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
</Tabs>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dm = [];
|
||||||
|
|
||||||
|
{
|
||||||
|
let states = deviceMetrics.getStatesMap();
|
||||||
|
let keys = states.toArray().map(v => v[0]);
|
||||||
|
keys.sort();
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i += 3) {
|
||||||
|
let items = keys.slice(i, i + 3).map(k => {
|
||||||
|
let m = states.get(k)!;
|
||||||
|
return (
|
||||||
|
<Col span={8}>
|
||||||
|
<Card>
|
||||||
|
<Statistic title={m.getName()} value={m.getValue()} />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
dm.push(<Row gutter={24}>{items}</Row>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let metrics = deviceMetrics.getMetricsMap();
|
||||||
|
let keys = metrics.toArray().map(v => v[0]);
|
||||||
|
keys.sort();
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i += 3) {
|
||||||
|
let items = keys.slice(i, i + 3).map(k => {
|
||||||
|
let m = metrics.get(k)!;
|
||||||
|
return (
|
||||||
|
<Col span={8}>
|
||||||
|
<MetricChart metric={m} aggregation={metricsAggregation} zeroToNull />
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
dm.push(<Row gutter={24}>{items}</Row>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastSeenAt = "Never";
|
||||||
|
if (props.lastSeenAt !== undefined) {
|
||||||
|
lastSeenAt = moment(props.lastSeenAt).format("YYYY-MM-DD HH:mm:ss");
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = !deviceLinkMetricsLoaded || !deviceMetrics;
|
||||||
|
|
||||||
|
const aggregations = (
|
||||||
|
<Space direction="horizontal">
|
||||||
|
{loading && <Spin size="small" />}
|
||||||
|
<Radio.Group value={metricsAggregation} onChange={onMetricsAggregationChange} size="small">
|
||||||
|
<Radio.Button value={Aggregation.HOUR} disabled={loading}>
|
||||||
|
24h
|
||||||
|
</Radio.Button>
|
||||||
|
<Radio.Button value={Aggregation.DAY} disabled={loading}>
|
||||||
|
31d
|
||||||
|
</Radio.Button>
|
||||||
|
<Radio.Button value={Aggregation.MONTH} disabled={loading}>
|
||||||
|
1y
|
||||||
|
</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
<Button type="primary" size="small" icon={<ReloadOutlined />} onClick={loadMetrics} disabled={loading} />
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
|
<Card>
|
||||||
|
<Descriptions>
|
||||||
|
<Descriptions.Item label="Last seen">{lastSeenAt}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Device profile">
|
||||||
|
<Link
|
||||||
|
to={`/tenants/${props.deviceProfile.getTenantId()}/device-profiles/${props.deviceProfile.getId()}/edit`}
|
||||||
|
>
|
||||||
|
{props.deviceProfile.getName()}
|
||||||
|
</Link>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Enabled">{props.device.getIsDisabled() ? "no" : "yes"}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Description">{props.device.getDescription()}</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
<Tabs tabBarExtraContent={aggregations}>
|
||||||
|
<Tabs.TabPane tab="Link metrics" key="1">
|
||||||
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
|
<Row gutter={24}>
|
||||||
|
<Col span={8}>
|
||||||
|
<MetricChart metric={deviceLinkMetrics.getRxPackets()!} aggregation={metricsAggregation} />
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<MetricChart metric={deviceLinkMetrics.getGwRssi()!} aggregation={metricsAggregation} zeroToNull />
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<MetricChart metric={deviceLinkMetrics.getGwSnr()!} aggregation={metricsAggregation} zeroToNull />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row gutter={24}>
|
||||||
|
<Col span={8}>
|
||||||
|
<MetricHeatmap
|
||||||
|
metric={deviceLinkMetrics.getRxPacketsPerFreq()!}
|
||||||
|
aggregation={metricsAggregation}
|
||||||
|
fromColor="rgb(227, 242, 253)"
|
||||||
|
toColor="rgb(33, 150, 243, 1)"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<MetricHeatmap
|
||||||
|
metric={deviceLinkMetrics.getRxPacketsPerDr()!}
|
||||||
|
aggregation={metricsAggregation}
|
||||||
|
fromColor="rgb(227, 242, 253)"
|
||||||
|
toColor="rgb(33, 150, 243, 1)"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<MetricBar metric={deviceLinkMetrics.getErrors()!} aggregation={metricsAggregation} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Space>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab="Device metrics" key="2">
|
||||||
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
|
{dm}
|
||||||
|
</Space>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DeviceDashboard;
|
export default DeviceDashboard;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { Device } from "@chirpstack/chirpstack-api-grpc-web/api/device_pb";
|
import { Device } from "@chirpstack/chirpstack-api-grpc-web/api/device_pb";
|
||||||
import { StreamDeviceEventsRequest, LogItem } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
|
import { StreamDeviceEventsRequest, LogItem } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
|
||||||
@ -10,55 +10,31 @@ interface IProps {
|
|||||||
device: Device;
|
device: Device;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function DeviceEvents(props: IProps) {
|
||||||
events: LogItem[];
|
const [events, setEvents] = useState<LogItem[]>([]);
|
||||||
cancelFunc?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
class DeviceEvents extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
const onMessage = (l: LogItem) => {
|
||||||
super(props);
|
setEvents(e => {
|
||||||
|
if (e.length === 0 || parseInt(l.getId().replace("-", "")) > parseInt(e[0].getId().replace("-", ""))) {
|
||||||
|
e.unshift(l);
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
return e;
|
||||||
events: [],
|
|
||||||
cancelFunc: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.connectStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.state.cancelFunc !== undefined) {
|
|
||||||
this.state.cancelFunc();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connectStream = () => {
|
|
||||||
let req = new StreamDeviceEventsRequest();
|
|
||||||
req.setDevEui(this.props.device.getDevEui());
|
|
||||||
|
|
||||||
let cancelFunc = InternalStore.streamDeviceEvents(req, this.onMessage);
|
|
||||||
this.setState({
|
|
||||||
cancelFunc: cancelFunc,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onMessage = (l: LogItem) => {
|
|
||||||
let events = this.state.events;
|
|
||||||
|
|
||||||
if (events.length === 0 || parseInt(l.getId().replace("-", "")) > parseInt(events[0].getId().replace("-", ""))) {
|
|
||||||
events.unshift(l);
|
|
||||||
this.setState({
|
|
||||||
events: events,
|
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
let req = new StreamDeviceEventsRequest();
|
||||||
return <LogTable logs={this.state.events} />;
|
req.setDevEui(props.device.getDevEui());
|
||||||
}
|
|
||||||
|
let cancelFunc = InternalStore.streamDeviceEvents(req, onMessage);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelFunc();
|
||||||
|
};
|
||||||
|
}, [props]);
|
||||||
|
|
||||||
|
return <LogTable logs={events} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DeviceEvents;
|
export default DeviceEvents;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import React, { Component } from "react";
|
|
||||||
|
|
||||||
import { Form, Input, Row, Col, Button, Tabs, Switch } from "antd";
|
import { Form, Input, Row, Col, Button, Tabs, Switch } from "antd";
|
||||||
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
@ -24,11 +22,11 @@ interface IProps {
|
|||||||
update?: boolean;
|
update?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeviceForm extends Component<IProps> {
|
function DeviceForm(props: IProps) {
|
||||||
formRef = React.createRef<any>();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
onFinish = (values: Device.AsObject) => {
|
const onFinish = (values: Device.AsObject) => {
|
||||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
const v = Object.assign(props.initialValues.toObject(), values);
|
||||||
let d = new Device();
|
let d = new Device();
|
||||||
|
|
||||||
d.setApplicationId(v.applicationId);
|
d.setApplicationId(v.applicationId);
|
||||||
@ -50,12 +48,12 @@ class DeviceForm extends Component<IProps> {
|
|||||||
d.getVariablesMap().set(elm[0], elm[1]);
|
d.getVariablesMap().set(elm[0], elm[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onFinish(d);
|
props.onFinish(d);
|
||||||
};
|
};
|
||||||
|
|
||||||
getDeviceProfileOptions = (search: string, fn: OptionsCallbackFunc) => {
|
const getDeviceProfileOptions = (search: string, fn: OptionsCallbackFunc) => {
|
||||||
let req = new ListDeviceProfilesRequest();
|
let req = new ListDeviceProfilesRequest();
|
||||||
req.setTenantId(this.props.tenant.getId());
|
req.setTenantId(props.tenant.getId());
|
||||||
req.setSearch(search);
|
req.setSearch(search);
|
||||||
req.setLimit(10);
|
req.setLimit(10);
|
||||||
|
|
||||||
@ -63,11 +61,12 @@ class DeviceForm extends Component<IProps> {
|
|||||||
const options = resp.getResultList().map((o, i) => {
|
const options = resp.getResultList().map((o, i) => {
|
||||||
return { label: o.getName(), value: o.getId() };
|
return { label: o.getName(), value: o.getId() };
|
||||||
});
|
});
|
||||||
|
|
||||||
fn(options);
|
fn(options);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
getDeviceProfileOption = (id: string, fn: OptionCallbackFunc) => {
|
const getDeviceProfileOption = (id: string, fn: OptionCallbackFunc) => {
|
||||||
let req = new GetDeviceProfileRequest();
|
let req = new GetDeviceProfileRequest();
|
||||||
req.setId(id);
|
req.setId(id);
|
||||||
|
|
||||||
@ -79,163 +78,153 @@ class DeviceForm extends Component<IProps> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
|
||||||
<Form
|
<Tabs>
|
||||||
layout="vertical"
|
<Tabs.TabPane tab="Device" key="1">
|
||||||
initialValues={this.props.initialValues.toObject()}
|
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
|
||||||
onFinish={this.onFinish}
|
<Input />
|
||||||
ref={this.formRef}
|
</Form.Item>
|
||||||
>
|
<Form.Item label="Description" name="description">
|
||||||
<Tabs>
|
<Input.TextArea />
|
||||||
<Tabs.TabPane tab="Device" key="1">
|
</Form.Item>
|
||||||
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
|
<Row gutter={24}>
|
||||||
<Input />
|
<Col span={12}>
|
||||||
</Form.Item>
|
<EuiInput
|
||||||
<Form.Item label="Description" name="description">
|
label="Device EUI (EUI64)"
|
||||||
<Input.TextArea />
|
name="devEui"
|
||||||
</Form.Item>
|
value={props.initialValues.getDevEui()}
|
||||||
<Row gutter={24}>
|
disabled={props.update}
|
||||||
<Col span={12}>
|
required
|
||||||
<EuiInput
|
/>
|
||||||
label="Device EUI (EUI64)"
|
</Col>
|
||||||
name="devEui"
|
<Col span={12}>
|
||||||
value={this.props.initialValues.getDevEui()}
|
<EuiInput
|
||||||
formRef={this.formRef}
|
label="Join EUI (EUI64)"
|
||||||
disabled={this.props.update}
|
name="joinEui"
|
||||||
required
|
value={props.initialValues.getJoinEui()}
|
||||||
/>
|
tooltip="The Join EUI will be automatically set / updated on OTAA. However, in some cases this field must be configured before OTAA (e.g. OTAA using a Relay)."
|
||||||
</Col>
|
/>
|
||||||
<Col span={12}>
|
</Col>
|
||||||
<EuiInput
|
</Row>
|
||||||
label="Join EUI (EUI64)"
|
<AutocompleteInput
|
||||||
name="joinEui"
|
label="Device profile"
|
||||||
value={this.props.initialValues.getJoinEui()}
|
name="deviceProfileId"
|
||||||
formRef={this.formRef}
|
getOption={getDeviceProfileOption}
|
||||||
tooltip="The Join EUI will be automatically set / updated on OTAA. However, in some cases this field must be configured before OTAA (e.g. OTAA using a Relay)."
|
getOptions={getDeviceProfileOptions}
|
||||||
/>
|
required
|
||||||
</Col>
|
/>
|
||||||
</Row>
|
<Row gutter={24}>
|
||||||
<AutocompleteInput
|
<Col span={12}>
|
||||||
label="Device profile"
|
<Form.Item
|
||||||
name="deviceProfileId"
|
label="Device is disabled"
|
||||||
formRef={this.formRef}
|
name="isDisabled"
|
||||||
getOption={this.getDeviceProfileOption}
|
valuePropName="checked"
|
||||||
getOptions={this.getDeviceProfileOptions}
|
tooltip="Received uplink frames and join-requests will be ignored."
|
||||||
required
|
>
|
||||||
/>
|
<Switch />
|
||||||
<Row gutter={24}>
|
</Form.Item>
|
||||||
<Col span={12}>
|
</Col>
|
||||||
<Form.Item
|
<Col span={12}>
|
||||||
label="Device is disabled"
|
<Form.Item
|
||||||
name="isDisabled"
|
label="Disable frame-counter validation"
|
||||||
valuePropName="checked"
|
name="skipFcntCheck"
|
||||||
tooltip="Received uplink frames and join-requests will be ignored."
|
valuePropName="checked"
|
||||||
>
|
tooltip="You must re-activate your device before this setting becomes effective. Note that disabling the frame-counter validation will compromise security as it allows replay-attacks."
|
||||||
<Switch />
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab="Tags" key="2">
|
||||||
|
<Form.List name="tagsMap">
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
{fields.map(({ key, name, ...restField }) => (
|
||||||
|
<Row gutter={24}>
|
||||||
|
<Col span={6}>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, 0]}
|
||||||
|
fieldKey={[name, 0]}
|
||||||
|
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="Key" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={16}>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, 1]}
|
||||||
|
fieldKey={[name, 1]}
|
||||||
|
rules={[{ required: true, message: "Please enter a value!" }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="Value" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={2}>
|
||||||
|
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||||
|
Add tag
|
||||||
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</>
|
||||||
<Col span={12}>
|
)}
|
||||||
<Form.Item
|
</Form.List>
|
||||||
label="Disable frame-counter validation"
|
</Tabs.TabPane>
|
||||||
name="skipFcntCheck"
|
<Tabs.TabPane tab="Variables" key="3">
|
||||||
valuePropName="checked"
|
<Form.List name="variablesMap">
|
||||||
tooltip="You must re-activate your device before this setting becomes effective. Note that disabling the frame-counter validation will compromise security as it allows replay-attacks."
|
{(fields, { add, remove }) => (
|
||||||
>
|
<>
|
||||||
<Switch />
|
{fields.map(({ key, name, ...restField }) => (
|
||||||
|
<Row gutter={24}>
|
||||||
|
<Col span={6}>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, 0]}
|
||||||
|
fieldKey={[name, 0]}
|
||||||
|
rules={[{ required: true, message: "Please enter a key!" }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="Key" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={16}>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, 1]}
|
||||||
|
fieldKey={[name, 1]}
|
||||||
|
rules={[{ required: true, message: "Please enter a value!" }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="Value" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={2}>
|
||||||
|
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||||
|
Add variable
|
||||||
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</>
|
||||||
</Row>
|
)}
|
||||||
</Tabs.TabPane>
|
</Form.List>
|
||||||
<Tabs.TabPane tab="Tags" key="2">
|
</Tabs.TabPane>
|
||||||
<Form.List name="tagsMap">
|
</Tabs>
|
||||||
{(fields, { add, remove }) => (
|
<Form.Item>
|
||||||
<>
|
<Button type="primary" htmlType="submit">
|
||||||
{fields.map(({ key, name, ...restField }) => (
|
Submit
|
||||||
<Row gutter={24}>
|
</Button>
|
||||||
<Col span={6}>
|
</Form.Item>
|
||||||
<Form.Item
|
</Form>
|
||||||
{...restField}
|
);
|
||||||
name={[name, 0]}
|
|
||||||
fieldKey={[name, 0]}
|
|
||||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="Key" />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={16}>
|
|
||||||
<Form.Item
|
|
||||||
{...restField}
|
|
||||||
name={[name, 1]}
|
|
||||||
fieldKey={[name, 1]}
|
|
||||||
rules={[{ required: true, message: "Please enter a value!" }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="Value" />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={2}>
|
|
||||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
))}
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
|
||||||
Add tag
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Form.List>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
<Tabs.TabPane tab="Variables" key="3">
|
|
||||||
<Form.List name="variablesMap">
|
|
||||||
{(fields, { add, remove }) => (
|
|
||||||
<>
|
|
||||||
{fields.map(({ key, name, ...restField }) => (
|
|
||||||
<Row gutter={24}>
|
|
||||||
<Col span={6}>
|
|
||||||
<Form.Item
|
|
||||||
{...restField}
|
|
||||||
name={[name, 0]}
|
|
||||||
fieldKey={[name, 0]}
|
|
||||||
rules={[{ required: true, message: "Please enter a key!" }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="Key" />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={16}>
|
|
||||||
<Form.Item
|
|
||||||
{...restField}
|
|
||||||
name={[name, 1]}
|
|
||||||
fieldKey={[name, 1]}
|
|
||||||
rules={[{ required: true, message: "Please enter a value!" }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="Value" />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={2}>
|
|
||||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
))}
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
|
||||||
Add variable
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Form.List>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
</Tabs>
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="primary" htmlType="submit">
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DeviceForm;
|
export default DeviceForm;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { Device } from "@chirpstack/chirpstack-api-grpc-web/api/device_pb";
|
import { Device } from "@chirpstack/chirpstack-api-grpc-web/api/device_pb";
|
||||||
import { StreamDeviceFramesRequest, LogItem } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
|
import { StreamDeviceFramesRequest, LogItem } from "@chirpstack/chirpstack-api-grpc-web/api/internal_pb";
|
||||||
@ -10,55 +10,31 @@ interface IProps {
|
|||||||
device: Device;
|
device: Device;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function DeviceFrames(props: IProps) {
|
||||||
frames: LogItem[];
|
const [frames, setFrames] = useState<LogItem[]>([]);
|
||||||
cancelFunc?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
class DeviceFrames extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
const onMessage = (l: LogItem) => {
|
||||||
super(props);
|
setFrames(f => {
|
||||||
|
if (f.length === 0 || parseInt(l.getId().replace("-", "")) > parseInt(f[0].getId().replace("-", ""))) {
|
||||||
|
f.unshift(l);
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
return f;
|
||||||
frames: [],
|
|
||||||
cancelFunc: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.connectStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.state.cancelFunc !== undefined) {
|
|
||||||
this.state.cancelFunc();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connectStream = () => {
|
|
||||||
let req = new StreamDeviceFramesRequest();
|
|
||||||
req.setDevEui(this.props.device.getDevEui());
|
|
||||||
|
|
||||||
let cancelFunc = InternalStore.streamDeviceFrames(req, this.onMessage);
|
|
||||||
this.setState({
|
|
||||||
cancelFunc: cancelFunc,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onMessage = (l: LogItem) => {
|
|
||||||
let frames = this.state.frames;
|
|
||||||
|
|
||||||
if (frames.length === 0 || parseInt(l.getId().replace("-", "")) > parseInt(frames[0].getId().replace("-", ""))) {
|
|
||||||
frames.unshift(l);
|
|
||||||
this.setState({
|
|
||||||
frames: frames,
|
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
let req = new StreamDeviceFramesRequest();
|
||||||
return <LogTable logs={this.state.frames} />;
|
req.setDevEui(props.device.getDevEui());
|
||||||
}
|
|
||||||
|
let cancelFunc = InternalStore.streamDeviceFrames(req, onMessage);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelFunc();
|
||||||
|
};
|
||||||
|
}, [props]);
|
||||||
|
|
||||||
|
return <LogTable logs={frames} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DeviceFrames;
|
export default DeviceFrames;
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Route, Switch, RouteComponentProps, Link } from "react-router-dom";
|
import { Route, Routes, useParams, Link, useNavigate, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
import { Space, Breadcrumb, Card, Button, PageHeader, Menu } from "antd";
|
import { Space, Breadcrumb, Card, Button, Menu } from "antd";
|
||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
|
||||||
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
||||||
import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||||
@ -30,239 +31,183 @@ import DeviceEvents from "./DeviceEvents";
|
|||||||
import DeviceQueue from "./DeviceQueue";
|
import DeviceQueue from "./DeviceQueue";
|
||||||
import DeviceActivation from "./DeviceActivation";
|
import DeviceActivation from "./DeviceActivation";
|
||||||
|
|
||||||
interface MatchParams {
|
interface IProps {
|
||||||
devEui: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps<MatchParams> {
|
|
||||||
tenant: Tenant;
|
tenant: Tenant;
|
||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function DeviceLayout(props: IProps) {
|
||||||
device?: Device;
|
const { devEui } = useParams();
|
||||||
deviceProfile?: DeviceProfile;
|
const navigate = useNavigate();
|
||||||
lastSeenAt?: Date;
|
const location = useLocation();
|
||||||
}
|
|
||||||
|
|
||||||
class DeviceLayout extends Component<IProps, IState> {
|
const [device, setDevice] = useState<Device | undefined>(undefined);
|
||||||
constructor(props: IProps) {
|
const [deviceProfile, setDeviceProfile] = useState<DeviceProfile | undefined>(undefined);
|
||||||
super(props);
|
const [lastSeenAt, setLastSeenAt] = useState<Date | undefined>(undefined);
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
useEffect(() => {
|
||||||
this.getDevice(this.getDeviceProfile);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDevice = (cb: () => void) => {
|
|
||||||
let req = new GetDeviceRequest();
|
let req = new GetDeviceRequest();
|
||||||
req.setDevEui(this.props.match.params.devEui);
|
req.setDevEui(devEui!);
|
||||||
|
|
||||||
DeviceStore.get(req, (resp: GetDeviceResponse) => {
|
DeviceStore.get(req, (resp: GetDeviceResponse) => {
|
||||||
this.setState(
|
setDevice(resp.getDevice());
|
||||||
{
|
|
||||||
device: resp.getDevice(),
|
|
||||||
},
|
|
||||||
cb,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (resp.getLastSeenAt() !== undefined) {
|
if (resp.getLastSeenAt() !== undefined) {
|
||||||
this.setState({
|
setLastSeenAt(resp.getLastSeenAt()!.toDate());
|
||||||
lastSeenAt: resp.getLastSeenAt()!.toDate(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
getDeviceProfile = () => {
|
let req = new GetDeviceProfileRequest();
|
||||||
let req = new GetDeviceProfileRequest();
|
req.setId(resp.getDevice()!.getDeviceProfileId());
|
||||||
req.setId(this.state.device!.getDeviceProfileId());
|
DeviceProfileStore.get(req, (resp: GetDeviceProfileResponse) => {
|
||||||
|
setDeviceProfile(resp.getDeviceProfile());
|
||||||
DeviceProfileStore.get(req, (resp: GetDeviceProfileResponse) => {
|
|
||||||
this.setState({
|
|
||||||
deviceProfile: resp.getDeviceProfile(),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
}, [devEui]);
|
||||||
|
|
||||||
deleteDevice = () => {
|
const deleteDevice = () => {
|
||||||
let req = new DeleteDeviceRequest();
|
let req = new DeleteDeviceRequest();
|
||||||
req.setDevEui(this.props.match.params.devEui);
|
req.setDevEui(devEui!);
|
||||||
|
|
||||||
DeviceStore.delete(req, () => {
|
DeviceStore.delete(req, () => {
|
||||||
this.props.history.push(`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}`);
|
navigate(`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const dp = deviceProfile;
|
||||||
const device = this.state.device;
|
if (!device || !dp) {
|
||||||
const dp = this.state.deviceProfile;
|
return null;
|
||||||
if (!device || !dp) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tenant = this.props.tenant;
|
|
||||||
const app = this.props.application;
|
|
||||||
|
|
||||||
const path = this.props.history.location.pathname;
|
|
||||||
let tab = "dashboard";
|
|
||||||
|
|
||||||
if (path.endsWith("edit")) {
|
|
||||||
tab = "edit";
|
|
||||||
}
|
|
||||||
if (path.endsWith("queue")) {
|
|
||||||
tab = "queue";
|
|
||||||
}
|
|
||||||
if (path.endsWith("keys")) {
|
|
||||||
tab = "keys";
|
|
||||||
}
|
|
||||||
if (path.endsWith("activation")) {
|
|
||||||
tab = "activation";
|
|
||||||
}
|
|
||||||
if (path.endsWith("events")) {
|
|
||||||
tab = "events";
|
|
||||||
}
|
|
||||||
if (path.endsWith("frames")) {
|
|
||||||
tab = "frames";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
|
||||||
<PageHeader
|
|
||||||
breadcrumbRender={() => (
|
|
||||||
<Breadcrumb>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>Tenants</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>
|
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}`}>{this.props.tenant.getName()}</Link>
|
|
||||||
</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>
|
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}/applications`}>Applications</Link>
|
|
||||||
</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>
|
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}`}>
|
|
||||||
{this.props.application.getName()}
|
|
||||||
</Link>
|
|
||||||
</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>
|
|
||||||
<Link to={`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}`}>
|
|
||||||
Devices
|
|
||||||
</Link>
|
|
||||||
</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<span>{device.getName()}</span>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
</Breadcrumb>
|
|
||||||
)}
|
|
||||||
title={device.getName()}
|
|
||||||
subTitle={`device eui: ${device.getDevEui()}`}
|
|
||||||
extra={[
|
|
||||||
<Admin tenantId={this.props.tenant.getId()} isDeviceAdmin>
|
|
||||||
<DeleteConfirm typ="device" confirm={device.getName()} onConfirm={this.deleteDevice}>
|
|
||||||
<Button danger type="primary">
|
|
||||||
Delete device
|
|
||||||
</Button>
|
|
||||||
</DeleteConfirm>
|
|
||||||
</Admin>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Card>
|
|
||||||
<Menu mode="horizontal" selectedKeys={[tab]} style={{ marginBottom: 24 }}>
|
|
||||||
<Menu.Item key="dashboard">
|
|
||||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}`}>
|
|
||||||
Dashboard
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="edit">
|
|
||||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/edit`}>
|
|
||||||
Configuration
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="keys" disabled={!dp.getSupportsOtaa()}>
|
|
||||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/keys`}>
|
|
||||||
OTAA keys
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="activation">
|
|
||||||
<Link
|
|
||||||
to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/activation`}
|
|
||||||
>
|
|
||||||
Activation
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="queue">
|
|
||||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/queue`}>
|
|
||||||
Queue
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="events">
|
|
||||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/events`}>
|
|
||||||
Events
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="frames">
|
|
||||||
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/frames`}>
|
|
||||||
LoRaWAN frames
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu>
|
|
||||||
<Switch>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={this.props.match.path}
|
|
||||||
render={props => (
|
|
||||||
<DeviceDashboard device={device} lastSeenAt={this.state.lastSeenAt} deviceProfile={dp} {...props} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/edit`}
|
|
||||||
render={props => <EditDevice device={device} application={app} tenant={tenant} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/keys`}
|
|
||||||
render={props => (
|
|
||||||
<SetDeviceKeys device={device} application={app} tenant={tenant} deviceProfile={dp} {...props} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/frames`}
|
|
||||||
render={props => <DeviceFrames device={device} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/events`}
|
|
||||||
render={props => <DeviceEvents device={device} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/queue`}
|
|
||||||
render={props => <DeviceQueue device={device} {...props} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={`${this.props.match.path}/activation`}
|
|
||||||
render={props => (
|
|
||||||
<DeviceActivation device={device} deviceProfile={dp} tenant={tenant} application={app} {...props} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
</Card>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tenant = props.tenant;
|
||||||
|
const app = props.application;
|
||||||
|
|
||||||
|
const path = location.pathname;
|
||||||
|
let tab = "dashboard";
|
||||||
|
|
||||||
|
if (path.endsWith("edit")) {
|
||||||
|
tab = "edit";
|
||||||
|
}
|
||||||
|
if (path.endsWith("queue")) {
|
||||||
|
tab = "queue";
|
||||||
|
}
|
||||||
|
if (path.endsWith("keys")) {
|
||||||
|
tab = "keys";
|
||||||
|
}
|
||||||
|
if (path.endsWith("activation")) {
|
||||||
|
tab = "activation";
|
||||||
|
}
|
||||||
|
if (path.endsWith("events")) {
|
||||||
|
tab = "events";
|
||||||
|
}
|
||||||
|
if (path.endsWith("frames")) {
|
||||||
|
tab = "frames";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
|
<PageHeader
|
||||||
|
breadcrumbRender={() => (
|
||||||
|
<Breadcrumb>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>Tenants</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>
|
||||||
|
<Link to={`/tenants/${props.tenant.getId()}`}>{props.tenant.getName()}</Link>
|
||||||
|
</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>
|
||||||
|
<Link to={`/tenants/${props.tenant.getId()}/applications`}>Applications</Link>
|
||||||
|
</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>
|
||||||
|
<Link to={`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}`}>
|
||||||
|
{props.application.getName()}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>
|
||||||
|
<Link to={`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}`}>Devices</Link>
|
||||||
|
</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<span>{device.getName()}</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
</Breadcrumb>
|
||||||
|
)}
|
||||||
|
title={device.getName()}
|
||||||
|
subTitle={`device eui: ${device.getDevEui()}`}
|
||||||
|
extra={[
|
||||||
|
<Admin tenantId={props.tenant.getId()} isDeviceAdmin>
|
||||||
|
<DeleteConfirm typ="device" confirm={device.getName()} onConfirm={deleteDevice}>
|
||||||
|
<Button danger type="primary">
|
||||||
|
Delete device
|
||||||
|
</Button>
|
||||||
|
</DeleteConfirm>
|
||||||
|
</Admin>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Card>
|
||||||
|
<Menu mode="horizontal" selectedKeys={[tab]} style={{ marginBottom: 24 }}>
|
||||||
|
<Menu.Item key="dashboard">
|
||||||
|
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}`}>
|
||||||
|
Dashboard
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="edit">
|
||||||
|
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/edit`}>
|
||||||
|
Configuration
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="keys" disabled={!dp.getSupportsOtaa()}>
|
||||||
|
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/keys`}>
|
||||||
|
OTAA keys
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="activation">
|
||||||
|
<Link
|
||||||
|
to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/activation`}
|
||||||
|
>
|
||||||
|
Activation
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="queue">
|
||||||
|
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/queue`}>
|
||||||
|
Queue
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="events">
|
||||||
|
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/events`}>
|
||||||
|
Events
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="frames">
|
||||||
|
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/devices/${device.getDevEui()}/frames`}>
|
||||||
|
LoRaWAN frames
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<DeviceDashboard device={device} lastSeenAt={lastSeenAt} deviceProfile={dp} />} />
|
||||||
|
<Route path="/edit" element={<EditDevice device={device} application={app} tenant={tenant} />} />
|
||||||
|
<Route
|
||||||
|
path="/keys"
|
||||||
|
element={<SetDeviceKeys device={device} application={app} tenant={tenant} deviceProfile={dp} />}
|
||||||
|
/>
|
||||||
|
<Route path="/frames" element={<DeviceFrames device={device} />} />
|
||||||
|
<Route path="/events" element={<DeviceEvents device={device} />} />
|
||||||
|
<Route path="/queue" element={<DeviceQueue device={device} />} />
|
||||||
|
<Route
|
||||||
|
path="/activation"
|
||||||
|
element={<DeviceActivation device={device} deviceProfile={dp} tenant={tenant} application={app} />}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DeviceLayout;
|
export default DeviceLayout;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { Struct } from "google-protobuf/google/protobuf/struct_pb";
|
import { Struct } from "google-protobuf/google/protobuf/struct_pb";
|
||||||
|
|
||||||
@ -25,87 +25,75 @@ interface IProps {
|
|||||||
device: Device;
|
device: Device;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function DeviceQueue(props: IProps) {
|
||||||
refreshCounter: number;
|
const [refreshCounter, setRefreshCounter] = useState<number>(0);
|
||||||
}
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
class DeviceQueue extends Component<IProps, IState> {
|
const columns: ColumnsType<DeviceQueueItem.AsObject> = [
|
||||||
formRef = React.createRef<any>();
|
{
|
||||||
|
title: "ID",
|
||||||
|
dataIndex: "id",
|
||||||
|
key: "id",
|
||||||
|
width: 350,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Is pending",
|
||||||
|
dataIndex: "isPending",
|
||||||
|
key: "isPending",
|
||||||
|
width: 100,
|
||||||
|
render: (text, record) => {
|
||||||
|
if (record.isPending) {
|
||||||
|
return "yes";
|
||||||
|
} else {
|
||||||
|
return "no";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Frame-counter",
|
||||||
|
dataIndex: "fCntDown",
|
||||||
|
key: "fCntDown",
|
||||||
|
width: 200,
|
||||||
|
render: (text, record) => {
|
||||||
|
if (record.isPending === true) {
|
||||||
|
return record.fCntDown;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Confirmed",
|
||||||
|
dataIndex: "confirmed",
|
||||||
|
key: "confirmed",
|
||||||
|
width: 100,
|
||||||
|
render: (text, record) => {
|
||||||
|
if (record.confirmed) {
|
||||||
|
return "yes";
|
||||||
|
} else {
|
||||||
|
return "no";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "FPort",
|
||||||
|
dataIndex: "fPort",
|
||||||
|
key: "fPort",
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Data (HEX)",
|
||||||
|
dataIndex: "data",
|
||||||
|
key: "data",
|
||||||
|
render: (text, record) => {
|
||||||
|
return Buffer.from(record.data as string, "base64").toString("hex");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
constructor(props: IProps) {
|
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
refreshCounter: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
columns = (): ColumnsType<DeviceQueueItem.AsObject> => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
title: "ID",
|
|
||||||
dataIndex: "id",
|
|
||||||
key: "id",
|
|
||||||
width: 350,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Is pending",
|
|
||||||
dataIndex: "isPending",
|
|
||||||
key: "isPending",
|
|
||||||
width: 100,
|
|
||||||
render: (text, record) => {
|
|
||||||
if (record.isPending) {
|
|
||||||
return "yes";
|
|
||||||
} else {
|
|
||||||
return "no";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Frame-counter",
|
|
||||||
dataIndex: "fCntDown",
|
|
||||||
key: "fCntDown",
|
|
||||||
width: 200,
|
|
||||||
render: (text, record) => {
|
|
||||||
if (record.isPending === true) {
|
|
||||||
return record.fCntDown;
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Confirmed",
|
|
||||||
dataIndex: "confirmed",
|
|
||||||
key: "confirmed",
|
|
||||||
width: 100,
|
|
||||||
render: (text, record) => {
|
|
||||||
if (record.confirmed) {
|
|
||||||
return "yes";
|
|
||||||
} else {
|
|
||||||
return "no";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "FPort",
|
|
||||||
dataIndex: "fPort",
|
|
||||||
key: "fPort",
|
|
||||||
width: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Data (HEX)",
|
|
||||||
dataIndex: "data",
|
|
||||||
key: "data",
|
|
||||||
render: (text, record) => {
|
|
||||||
return Buffer.from(record.data as string, "base64").toString("hex");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
|
||||||
let req = new GetDeviceQueueItemsRequest();
|
let req = new GetDeviceQueueItemsRequest();
|
||||||
req.setDevEui(this.props.device.getDevEui());
|
req.setDevEui(props.device.getDevEui());
|
||||||
|
|
||||||
DeviceStore.getQueue(req, (resp: GetDeviceQueueItemsResponse) => {
|
DeviceStore.getQueue(req, (resp: GetDeviceQueueItemsResponse) => {
|
||||||
const obj = resp.toObject();
|
const obj = resp.toObject();
|
||||||
@ -113,25 +101,23 @@ class DeviceQueue extends Component<IProps, IState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
refreshQueue = () => {
|
const refreshQueue = () => {
|
||||||
this.setState({
|
setRefreshCounter(refreshCounter + 1);
|
||||||
refreshCounter: this.state.refreshCounter + 1,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
flushQueue = () => {
|
const flushQueue = () => {
|
||||||
let req = new FlushDeviceQueueRequest();
|
let req = new FlushDeviceQueueRequest();
|
||||||
req.setDevEui(this.props.device.getDevEui());
|
req.setDevEui(props.device.getDevEui());
|
||||||
DeviceStore.flushQueue(req, () => {
|
DeviceStore.flushQueue(req, () => {
|
||||||
this.refreshQueue();
|
refreshQueue();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onEnqueue = (values: any) => {
|
const onEnqueue = (values: any) => {
|
||||||
let req = new EnqueueDeviceQueueItemRequest();
|
let req = new EnqueueDeviceQueueItemRequest();
|
||||||
let item = new DeviceQueueItem();
|
let item = new DeviceQueueItem();
|
||||||
|
|
||||||
item.setDevEui(this.props.device.getDevEui());
|
item.setDevEui(props.device.getDevEui());
|
||||||
item.setFPort(values.fPort);
|
item.setFPort(values.fPort);
|
||||||
item.setConfirmed(values.confirmed);
|
item.setConfirmed(values.confirmed);
|
||||||
|
|
||||||
@ -163,66 +149,58 @@ class DeviceQueue extends Component<IProps, IState> {
|
|||||||
req.setQueueItem(item);
|
req.setQueueItem(item);
|
||||||
|
|
||||||
DeviceStore.enqueue(req, _ => {
|
DeviceStore.enqueue(req, _ => {
|
||||||
this.formRef.current.resetFields();
|
form.resetFields();
|
||||||
this.refreshQueue();
|
refreshQueue();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
<Card title="Enqueue">
|
||||||
<Card title="Enqueue">
|
<Form layout="horizontal" onFinish={onEnqueue} form={form} initialValues={{ fPort: 1 }}>
|
||||||
<Form layout="horizontal" onFinish={this.onEnqueue} ref={this.formRef} initialValues={{ fPort: 1 }}>
|
<Row>
|
||||||
<Row>
|
<Space direction="horizontal" style={{ width: "100%" }} size="large">
|
||||||
<Space direction="horizontal" style={{ width: "100%" }} size="large">
|
<Form.Item name="confirmed" label="Confirmed" valuePropName="checked">
|
||||||
<Form.Item name="confirmed" label="Confirmed" valuePropName="checked">
|
<Checkbox />
|
||||||
<Checkbox />
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item name="fPort" label="FPort">
|
||||||
<Form.Item name="fPort" label="FPort">
|
<InputNumber min={1} max={254} />
|
||||||
<InputNumber min={1} max={254} />
|
</Form.Item>
|
||||||
</Form.Item>
|
</Space>
|
||||||
</Space>
|
</Row>
|
||||||
</Row>
|
<Tabs defaultActiveKey="1">
|
||||||
<Tabs defaultActiveKey="1">
|
<Tabs.TabPane tab="HEX" key="1">
|
||||||
<Tabs.TabPane tab="HEX" key="1">
|
<Form.Item name="hex">
|
||||||
<Form.Item name="hex">
|
<Input />
|
||||||
<Input />
|
</Form.Item>
|
||||||
</Form.Item>
|
</Tabs.TabPane>
|
||||||
</Tabs.TabPane>
|
<Tabs.TabPane tab="BASE64" key="2">
|
||||||
<Tabs.TabPane tab="BASE64" key="2">
|
<Form.Item name="base64">
|
||||||
<Form.Item name="base64">
|
<Input />
|
||||||
<Input />
|
</Form.Item>
|
||||||
</Form.Item>
|
</Tabs.TabPane>
|
||||||
</Tabs.TabPane>
|
<Tabs.TabPane tab="JSON" key="3">
|
||||||
<Tabs.TabPane tab="JSON" key="3">
|
<CodeEditor name="json" />
|
||||||
<CodeEditor name="json" value="{}" formRef={this.formRef} />
|
</Tabs.TabPane>
|
||||||
</Tabs.TabPane>
|
</Tabs>
|
||||||
</Tabs>
|
<Button type="primary" htmlType="submit">
|
||||||
<Button type="primary" htmlType="submit">
|
Enqueue
|
||||||
Enqueue
|
</Button>
|
||||||
</Button>
|
</Form>
|
||||||
</Form>
|
</Card>
|
||||||
</Card>
|
<Row justify="end">
|
||||||
<Row justify="end">
|
<Space direction="horizontal" size="large">
|
||||||
<Space direction="horizontal" size="large">
|
<Button icon={<RedoOutlined />} onClick={refreshQueue}>
|
||||||
<Button icon={<RedoOutlined />} onClick={this.refreshQueue}>
|
Reload
|
||||||
Reload
|
</Button>
|
||||||
</Button>
|
<Popconfirm title="Are you sure you want to flush the queue?" placement="left" onConfirm={flushQueue}>
|
||||||
<Popconfirm title="Are you sure you want to flush the queue?" placement="left" onConfirm={this.flushQueue}>
|
<Button icon={<DeleteOutlined />}>Flush queue</Button>
|
||||||
<Button icon={<DeleteOutlined />}>Flush queue</Button>
|
</Popconfirm>
|
||||||
</Popconfirm>
|
</Space>
|
||||||
</Space>
|
</Row>
|
||||||
</Row>
|
<DataTable columns={columns} getPage={getPage} refreshKey={refreshCounter} rowKey="id" noPagination />
|
||||||
<DataTable
|
</Space>
|
||||||
columns={this.columns()}
|
);
|
||||||
getPage={this.getPage}
|
|
||||||
refreshKey={this.state.refreshCounter}
|
|
||||||
rowKey="id"
|
|
||||||
noPagination
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DeviceQueue;
|
export default DeviceQueue;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
|
||||||
import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
|
||||||
@ -8,27 +7,25 @@ import { Device, UpdateDeviceRequest } from "@chirpstack/chirpstack-api-grpc-web
|
|||||||
import DeviceStore from "../../stores/DeviceStore";
|
import DeviceStore from "../../stores/DeviceStore";
|
||||||
import DeviceForm from "./DeviceForm";
|
import DeviceForm from "./DeviceForm";
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
tenant: Tenant;
|
tenant: Tenant;
|
||||||
application: Application;
|
application: Application;
|
||||||
device: Device;
|
device: Device;
|
||||||
}
|
}
|
||||||
|
|
||||||
class EditDevice extends Component<IProps> {
|
function EditDevice(props: IProps) {
|
||||||
onFinish = (obj: Device) => {
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const onFinish = (obj: Device) => {
|
||||||
let req = new UpdateDeviceRequest();
|
let req = new UpdateDeviceRequest();
|
||||||
req.setDevice(obj);
|
req.setDevice(obj);
|
||||||
|
|
||||||
DeviceStore.update(req, () => {
|
DeviceStore.update(req, () => {
|
||||||
this.props.history.push(
|
navigate(`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}/devices/${obj.getDevEui()}`);
|
||||||
`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}/devices/${obj.getDevEui()}`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return <DeviceForm initialValues={props.device} onFinish={onFinish} tenant={props.tenant} update />;
|
||||||
return <DeviceForm initialValues={this.props.device} onFinish={this.onFinish} tenant={this.props.tenant} update />;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditDevice;
|
export default EditDevice;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
@ -42,126 +42,105 @@ interface IProps {
|
|||||||
application: Application;
|
application: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function ListDevices(props: IProps) {
|
||||||
selectedRowIds: string[];
|
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
|
||||||
multicastGroups: MulticastGroupListItem[];
|
const [multicastGroups, setMulticastGroups] = useState<MulticastGroupListItem[]>([]);
|
||||||
relays: RelayListItem[];
|
const [relays, setRelays] = useState<RelayListItem[]>([]);
|
||||||
mgModalVisible: boolean;
|
const [mgModalVisible, setMgModalVisible] = useState<boolean>(false);
|
||||||
relayModalVisible: boolean;
|
const [relayModalVisible, setRelayModalVisible] = useState<boolean>(false);
|
||||||
mgSelected: string;
|
const [mgSelected, setMgSelected] = useState<string>("");
|
||||||
relaySelected: string;
|
const [relaySelected, setRelaySelected] = useState<string>("");
|
||||||
}
|
|
||||||
|
|
||||||
class ListDevices extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
selectedRowIds: [],
|
|
||||||
multicastGroups: [],
|
|
||||||
relays: [],
|
|
||||||
mgModalVisible: false,
|
|
||||||
relayModalVisible: false,
|
|
||||||
mgSelected: "",
|
|
||||||
relaySelected: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
let mgReq = new ListMulticastGroupsRequest();
|
let mgReq = new ListMulticastGroupsRequest();
|
||||||
mgReq.setLimit(999);
|
mgReq.setLimit(999);
|
||||||
mgReq.setApplicationId(this.props.application.getId());
|
mgReq.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
MulticastGroupStore.list(mgReq, (resp: ListMulticastGroupsResponse) => {
|
MulticastGroupStore.list(mgReq, (resp: ListMulticastGroupsResponse) => {
|
||||||
this.setState({
|
setMulticastGroups(resp.getResultList());
|
||||||
multicastGroups: resp.getResultList(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let relayReq = new ListRelaysRequest();
|
let relayReq = new ListRelaysRequest();
|
||||||
relayReq.setLimit(999);
|
relayReq.setLimit(999);
|
||||||
relayReq.setApplicationId(this.props.application.getId());
|
relayReq.setApplicationId(props.application.getId());
|
||||||
|
|
||||||
RelayStore.list(relayReq, (resp: ListRelaysResponse) => {
|
RelayStore.list(relayReq, (resp: ListRelaysResponse) => {
|
||||||
this.setState({
|
setRelays(resp.getResultList());
|
||||||
relays: resp.getResultList(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}, [props]);
|
||||||
|
|
||||||
columns = (): ColumnsType<DeviceListItem.AsObject> => {
|
const columns: ColumnsType<DeviceListItem.AsObject> = [
|
||||||
return [
|
{
|
||||||
{
|
title: "Last seen",
|
||||||
title: "Last seen",
|
dataIndex: "lastSeenAt",
|
||||||
dataIndex: "lastSeenAt",
|
key: "lastSeenAt",
|
||||||
key: "lastSeenAt",
|
width: 250,
|
||||||
width: 250,
|
render: (text, record) => {
|
||||||
render: (text, record) => {
|
if (record.lastSeenAt !== undefined) {
|
||||||
if (record.lastSeenAt !== undefined) {
|
let ts = new Date(0);
|
||||||
let ts = new Date(0);
|
ts.setUTCSeconds(record.lastSeenAt.seconds);
|
||||||
ts.setUTCSeconds(record.lastSeenAt.seconds);
|
return moment(ts).format("YYYY-MM-DD HH:mm:ss");
|
||||||
return moment(ts).format("YYYY-MM-DD HH:mm:ss");
|
}
|
||||||
}
|
return "Never";
|
||||||
return "Never";
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
title: "DevEUI",
|
{
|
||||||
dataIndex: "devEui",
|
title: "DevEUI",
|
||||||
key: "devEui",
|
dataIndex: "devEui",
|
||||||
width: 250,
|
key: "devEui",
|
||||||
render: (text, record) => (
|
width: 250,
|
||||||
<Link
|
render: (text, record) => (
|
||||||
to={`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/devices/${
|
<Link
|
||||||
record.devEui
|
to={`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/devices/${
|
||||||
}`}
|
record.devEui
|
||||||
>
|
}`}
|
||||||
{text}
|
>
|
||||||
</Link>
|
{text}
|
||||||
),
|
</Link>
|
||||||
},
|
),
|
||||||
{
|
},
|
||||||
title: "Name",
|
{
|
||||||
dataIndex: "name",
|
title: "Name",
|
||||||
key: "name",
|
dataIndex: "name",
|
||||||
},
|
key: "name",
|
||||||
{
|
},
|
||||||
title: "Device profile",
|
{
|
||||||
dataIndex: "deviceProfileName",
|
title: "Device profile",
|
||||||
key: "deviceProfileName",
|
dataIndex: "deviceProfileName",
|
||||||
render: (text, record) => (
|
key: "deviceProfileName",
|
||||||
<Link to={`/tenants/${this.props.application.getTenantId()}/device-profiles/${record.deviceProfileId}/edit`}>
|
render: (text, record) => (
|
||||||
{text}
|
<Link to={`/tenants/${props.application.getTenantId()}/device-profiles/${record.deviceProfileId}/edit`}>
|
||||||
</Link>
|
{text}
|
||||||
),
|
</Link>
|
||||||
},
|
),
|
||||||
{
|
},
|
||||||
title: "Battery",
|
{
|
||||||
dataIndex: "deviceStatus",
|
title: "Battery",
|
||||||
key: "deviceStatus",
|
dataIndex: "deviceStatus",
|
||||||
render: (text, record) => {
|
key: "deviceStatus",
|
||||||
if (record.deviceStatus === undefined) {
|
render: (text, record) => {
|
||||||
return;
|
if (record.deviceStatus === undefined) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (record.deviceStatus.externalPowerSource === true) {
|
if (record.deviceStatus.externalPowerSource === true) {
|
||||||
return <FontAwesomeIcon icon={faPlug} />;
|
return <FontAwesomeIcon icon={faPlug} />;
|
||||||
} else if (record.deviceStatus.batteryLevel > 75) {
|
} else if (record.deviceStatus.batteryLevel > 75) {
|
||||||
return <FontAwesomeIcon icon={faBatteryFull} />;
|
return <FontAwesomeIcon icon={faBatteryFull} />;
|
||||||
} else if (record.deviceStatus.batteryLevel > 50) {
|
} else if (record.deviceStatus.batteryLevel > 50) {
|
||||||
return <FontAwesomeIcon icon={faBatteryThreeQuarters} />;
|
return <FontAwesomeIcon icon={faBatteryThreeQuarters} />;
|
||||||
} else if (record.deviceStatus.batteryLevel > 25) {
|
} else if (record.deviceStatus.batteryLevel > 25) {
|
||||||
return <FontAwesomeIcon icon={faBatteryHalf} />;
|
return <FontAwesomeIcon icon={faBatteryHalf} />;
|
||||||
} else if (record.deviceStatus.batteryLevel > 0) {
|
} else if (record.deviceStatus.batteryLevel > 0) {
|
||||||
return <FontAwesomeIcon icon={faBatteryQuarter} />;
|
return <FontAwesomeIcon icon={faBatteryQuarter} />;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||||
let req = new ListDevicesRequest();
|
let req = new ListDevicesRequest();
|
||||||
req.setApplicationId(this.props.application.getId());
|
req.setApplicationId(props.application.getId());
|
||||||
req.setLimit(limit);
|
req.setLimit(limit);
|
||||||
req.setOffset(offset);
|
req.setOffset(offset);
|
||||||
|
|
||||||
@ -171,148 +150,114 @@ class ListDevices extends Component<IProps, IState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onRowsSelectChange = (ids: string[]) => {
|
const onRowsSelectChange = (ids: string[]) => {
|
||||||
this.setState({
|
setSelectedRowIds(ids);
|
||||||
selectedRowIds: ids,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
showMgModal = () => {
|
const showMgModal = () => {
|
||||||
this.setState({
|
setMgModalVisible(true);
|
||||||
mgModalVisible: true,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
showRelayModal = () => {
|
const showRelayModal = () => {
|
||||||
this.setState({
|
setRelayModalVisible(true);
|
||||||
relayModalVisible: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
hideMgModal = () => {
|
|
||||||
this.setState({
|
|
||||||
mgModalVisible: false,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
hideRelayModal = () => {
|
const hideMgModal = () => {
|
||||||
this.setState({
|
setMgModalVisible(false);
|
||||||
relayModalVisible: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMgSelected = (value: string) => {
|
|
||||||
this.setState({
|
|
||||||
mgSelected: value,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onRelaySelected = (value: string) => {
|
const hideRelayModal = () => {
|
||||||
this.setState({
|
setRelayModalVisible(false);
|
||||||
relaySelected: value,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleMgModalOk = () => {
|
const onMgSelected = (value: string) => {
|
||||||
for (let devEui of this.state.selectedRowIds) {
|
setMgSelected(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRelaySelected = (value: string) => {
|
||||||
|
setRelaySelected(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMgModalOk = () => {
|
||||||
|
for (let devEui of selectedRowIds) {
|
||||||
let req = new AddDeviceToMulticastGroupRequest();
|
let req = new AddDeviceToMulticastGroupRequest();
|
||||||
req.setMulticastGroupId(this.state.mgSelected);
|
req.setMulticastGroupId(mgSelected);
|
||||||
req.setDevEui(devEui);
|
req.setDevEui(devEui);
|
||||||
|
|
||||||
MulticastGroupStore.addDevice(req, () => {});
|
MulticastGroupStore.addDevice(req, () => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
setMgModalVisible(false);
|
||||||
mgModalVisible: false,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleRelayModalOk = () => {
|
const handleRelayModalOk = () => {
|
||||||
for (let devEui of this.state.selectedRowIds) {
|
for (let devEui of selectedRowIds) {
|
||||||
let req = new AddRelayDeviceRequest();
|
let req = new AddRelayDeviceRequest();
|
||||||
req.setRelayDevEui(this.state.relaySelected);
|
req.setRelayDevEui(relaySelected);
|
||||||
req.setDeviceDevEui(devEui);
|
req.setDeviceDevEui(devEui);
|
||||||
|
|
||||||
RelayStore.addDevice(req, () => {});
|
RelayStore.addDevice(req, () => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
setRelayModalVisible(false);
|
||||||
relayModalVisible: false,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const menu = (
|
||||||
const menu = (
|
<Menu>
|
||||||
<Menu>
|
<Menu.Item onClick={showMgModal}>Add to multicast-group</Menu.Item>
|
||||||
<Menu.Item onClick={this.showMgModal}>Add to multicast-group</Menu.Item>
|
<Menu.Item onClick={showRelayModal}>Add to relay</Menu.Item>
|
||||||
<Menu.Item onClick={this.showRelayModal}>Add to relay</Menu.Item>
|
</Menu>
|
||||||
</Menu>
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const mgOptions = this.state.multicastGroups.map((mg, i) => (
|
const mgOptions = multicastGroups.map((mg, i) => <Select.Option value={mg.getId()}>{mg.getName()}</Select.Option>);
|
||||||
<Select.Option value={mg.getId()}>{mg.getName()}</Select.Option>
|
|
||||||
));
|
|
||||||
|
|
||||||
const relayOptions = this.state.relays.map((r, i) => (
|
const relayOptions = relays.map((r, i) => <Select.Option value={r.getDevEui()}>{r.getName()}</Select.Option>);
|
||||||
<Select.Option value={r.getDevEui()}>{r.getName()}</Select.Option>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||||
<Modal
|
<Modal
|
||||||
title="Add selected devices to multicast-group"
|
title="Add selected devices to multicast-group"
|
||||||
visible={this.state.mgModalVisible}
|
visible={mgModalVisible}
|
||||||
onOk={this.handleMgModalOk}
|
onOk={handleMgModalOk}
|
||||||
onCancel={this.hideMgModal}
|
onCancel={hideMgModal}
|
||||||
okButtonProps={{ disabled: this.state.mgSelected === "" }}
|
okButtonProps={{ disabled: mgSelected === "" }}
|
||||||
>
|
>
|
||||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||||
<Select style={{ width: "100%" }} onChange={this.onMgSelected} placeholder="Select Multicast-group">
|
<Select style={{ width: "100%" }} onChange={onMgSelected} placeholder="Select Multicast-group">
|
||||||
{mgOptions}
|
{mgOptions}
|
||||||
</Select>
|
</Select>
|
||||||
</Space>
|
</Space>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal
|
<Modal
|
||||||
title="Add selected devices to relay"
|
title="Add selected devices to relay"
|
||||||
visible={this.state.relayModalVisible}
|
visible={relayModalVisible}
|
||||||
onOk={this.handleRelayModalOk}
|
onOk={handleRelayModalOk}
|
||||||
onCancel={this.hideRelayModal}
|
onCancel={hideRelayModal}
|
||||||
okButtonProps={{ disabled: this.state.relaySelected === "" }}
|
okButtonProps={{ disabled: relaySelected === "" }}
|
||||||
>
|
>
|
||||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||||
<Select style={{ width: "100%" }} onChange={this.onRelaySelected} placeholder="Select Relay">
|
<Select style={{ width: "100%" }} onChange={onRelaySelected} placeholder="Select Relay">
|
||||||
{relayOptions}
|
{relayOptions}
|
||||||
</Select>
|
</Select>
|
||||||
</Space>
|
</Space>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Admin tenantId={this.props.application.getTenantId()} isDeviceAdmin>
|
<Admin tenantId={props.application.getTenantId()} isDeviceAdmin>
|
||||||
<Space direction="horizontal" style={{ float: "right" }}>
|
<Space direction="horizontal" style={{ float: "right" }}>
|
||||||
<Button type="primary">
|
<Button type="primary">
|
||||||
<Link
|
<Link
|
||||||
to={`/tenants/${this.props.application.getTenantId()}/applications/${this.props.application.getId()}/devices/create`}
|
to={`/tenants/${props.application.getTenantId()}/applications/${props.application.getId()}/devices/create`}
|
||||||
>
|
|
||||||
Add device
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
<Dropdown
|
|
||||||
placement="bottomRight"
|
|
||||||
overlay={menu}
|
|
||||||
trigger={["click"]}
|
|
||||||
disabled={this.state.selectedRowIds.length === 0}
|
|
||||||
>
|
>
|
||||||
<Button>Selected devices</Button>
|
Add device
|
||||||
</Dropdown>
|
</Link>
|
||||||
</Space>
|
</Button>
|
||||||
</Admin>
|
<Dropdown placement="bottomRight" overlay={menu} trigger={["click"]} disabled={selectedRowIds.length === 0}>
|
||||||
<DataTable
|
<Button>Selected devices</Button>
|
||||||
columns={this.columns()}
|
</Dropdown>
|
||||||
getPage={this.getPage}
|
</Space>
|
||||||
onRowsSelectChange={this.onRowsSelectChange}
|
</Admin>
|
||||||
rowKey="devEui"
|
<DataTable columns={columns} getPage={getPage} onRowsSelectChange={onRowsSelectChange} rowKey="devEui" />
|
||||||
/>
|
</Space>
|
||||||
</Space>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ListDevices;
|
export default ListDevices;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Form, Button, Space, Popconfirm } from "antd";
|
import { Form, Button, Space, Popconfirm } from "antd";
|
||||||
|
|
||||||
@ -25,11 +25,11 @@ interface FormProps {
|
|||||||
onFinish: (obj: DeviceKeys) => void;
|
onFinish: (obj: DeviceKeys) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class LW10DeviceKeysForm extends Component<FormProps> {
|
function LW10DeviceKeysForm(props: FormProps) {
|
||||||
formRef = React.createRef<any>();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
onFinish = (values: DeviceKeys.AsObject) => {
|
const onFinish = (values: DeviceKeys.AsObject) => {
|
||||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
const v = Object.assign(props.initialValues.toObject(), values);
|
||||||
let dk = new DeviceKeys();
|
let dk = new DeviceKeys();
|
||||||
|
|
||||||
dk.setDevEui(v.devEui);
|
dk.setDevEui(v.devEui);
|
||||||
@ -37,187 +37,152 @@ class LW10DeviceKeysForm extends Component<FormProps> {
|
|||||||
// the AppKey has been renamed to the NwkKey and a new value AppKey was added.
|
// the AppKey has been renamed to the NwkKey and a new value AppKey was added.
|
||||||
dk.setNwkKey(v.nwkKey);
|
dk.setNwkKey(v.nwkKey);
|
||||||
|
|
||||||
this.props.onFinish(dk);
|
props.onFinish(dk);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
|
||||||
<Form
|
<AesKeyInput
|
||||||
layout="vertical"
|
label="Application key"
|
||||||
initialValues={this.props.initialValues.toObject()}
|
name="nwkKey"
|
||||||
onFinish={this.onFinish}
|
tooltip="For LoRaWAN 1.0 devices. In case your device supports LoRaWAN 1.1, update the device-profile first."
|
||||||
ref={this.formRef}
|
value={props.initialValues.getNwkKey()}
|
||||||
>
|
required
|
||||||
<AesKeyInput
|
/>
|
||||||
label="Application key"
|
<Form.Item>
|
||||||
name="nwkKey"
|
<Button type="primary" htmlType="submit">
|
||||||
tooltip="For LoRaWAN 1.0 devices. In case your device supports LoRaWAN 1.1, update the device-profile first."
|
Submit
|
||||||
value={this.props.initialValues.getNwkKey()}
|
</Button>
|
||||||
formRef={this.formRef}
|
</Form.Item>
|
||||||
required
|
</Form>
|
||||||
/>
|
);
|
||||||
<Form.Item>
|
|
||||||
<Button type="primary" htmlType="submit">
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class LW11DeviceKeysForm extends Component<FormProps> {
|
function LW11DeviceKeysForm(props: FormProps) {
|
||||||
formRef = React.createRef<any>();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
onFinish = (values: DeviceKeys.AsObject) => {
|
const onFinish = (values: DeviceKeys.AsObject) => {
|
||||||
const v = Object.assign(this.props.initialValues.toObject(), values);
|
const v = Object.assign(props.initialValues.toObject(), values);
|
||||||
let dk = new DeviceKeys();
|
let dk = new DeviceKeys();
|
||||||
|
|
||||||
dk.setDevEui(v.devEui);
|
dk.setDevEui(v.devEui);
|
||||||
dk.setAppKey(v.appKey);
|
dk.setAppKey(v.appKey);
|
||||||
dk.setNwkKey(v.nwkKey);
|
dk.setNwkKey(v.nwkKey);
|
||||||
|
|
||||||
this.props.onFinish(dk);
|
props.onFinish(dk);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} form={form}>
|
||||||
<Form
|
<AesKeyInput
|
||||||
layout="vertical"
|
label="Application key"
|
||||||
initialValues={this.props.initialValues.toObject()}
|
tooltip="For LoRaWAN 1.1 devices. In case your device does not support LoRaWAN 1.1, update the device-profile first."
|
||||||
onFinish={this.onFinish}
|
name="appKey"
|
||||||
ref={this.formRef}
|
value={props.initialValues.getAppKey()}
|
||||||
>
|
required
|
||||||
<AesKeyInput
|
/>
|
||||||
label="Application key"
|
<AesKeyInput
|
||||||
tooltip="For LoRaWAN 1.1 devices. In case your device does not support LoRaWAN 1.1, update the device-profile first."
|
label="Network key"
|
||||||
name="appKey"
|
tooltip="For LoRaWAN 1.1 devices. In case your device does not support LoRaWAN 1.1, update the device-profile first."
|
||||||
value={this.props.initialValues.getAppKey()}
|
name="nwkKey"
|
||||||
formRef={this.formRef}
|
value={props.initialValues.getNwkKey()}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<AesKeyInput
|
<Form.Item>
|
||||||
label="Network key"
|
<Button type="primary" htmlType="submit">
|
||||||
tooltip="For LoRaWAN 1.1 devices. In case your device does not support LoRaWAN 1.1, update the device-profile first."
|
Submit
|
||||||
name="nwkKey"
|
</Button>
|
||||||
value={this.props.initialValues.getNwkKey()}
|
</Form.Item>
|
||||||
formRef={this.formRef}
|
</Form>
|
||||||
required
|
);
|
||||||
/>
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="primary" htmlType="submit">
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {
|
interface IProps {
|
||||||
tenant: Tenant;
|
tenant: Tenant;
|
||||||
application: Application;
|
application: Application;
|
||||||
device: Device;
|
device: Device;
|
||||||
deviceProfile: DeviceProfile;
|
deviceProfile: DeviceProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
function SetDeviceKeys(props: IProps) {
|
||||||
deviceKeys?: DeviceKeys;
|
const navigate = useNavigate();
|
||||||
deviceKeysRequested: boolean;
|
const [deviceKeys, setDeviceKeys] = useState<DeviceKeys | undefined>(undefined);
|
||||||
}
|
const [deviceKeysRequested, setDeviceKeysRequested] = useState<boolean>(false);
|
||||||
|
|
||||||
class SetDeviceKeys extends Component<IProps, IState> {
|
useEffect(() => {
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
deviceKeysRequested: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.getDeviceKeys();
|
|
||||||
}
|
|
||||||
|
|
||||||
getDeviceKeys = () => {
|
|
||||||
let req = new GetDeviceKeysRequest();
|
let req = new GetDeviceKeysRequest();
|
||||||
req.setDevEui(this.props.device.getDevEui());
|
req.setDevEui(props.device.getDevEui());
|
||||||
|
|
||||||
DeviceStore.getKeys(req, (resp?: GetDeviceKeysResponse) => {
|
DeviceStore.getKeys(req, (resp?: GetDeviceKeysResponse) => {
|
||||||
if (resp) {
|
if (resp) {
|
||||||
this.setState({
|
setDeviceKeys(resp.getDeviceKeys());
|
||||||
deviceKeys: resp.getDeviceKeys(),
|
setDeviceKeysRequested(true);
|
||||||
deviceKeysRequested: true,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
setDeviceKeysRequested(true);
|
||||||
deviceKeysRequested: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
}, [props]);
|
||||||
|
|
||||||
onFinish = (obj: DeviceKeys) => {
|
const onFinish = (obj: DeviceKeys) => {
|
||||||
if (this.state.deviceKeys) {
|
if (deviceKeys) {
|
||||||
// this is an update
|
// this is an update
|
||||||
let req = new UpdateDeviceKeysRequest();
|
let req = new UpdateDeviceKeysRequest();
|
||||||
req.setDeviceKeys(obj);
|
req.setDeviceKeys(obj);
|
||||||
|
|
||||||
DeviceStore.updateKeys(req, () => {
|
DeviceStore.updateKeys(req, () => {
|
||||||
this.props.history.push(
|
navigate(
|
||||||
`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}/devices/${this.props.device.getDevEui()}`,
|
`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}/devices/${props.device.getDevEui()}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// this is a create
|
// this is a create
|
||||||
let req = new CreateDeviceKeysRequest();
|
let req = new CreateDeviceKeysRequest();
|
||||||
obj.setDevEui(this.props.device.getDevEui());
|
obj.setDevEui(props.device.getDevEui());
|
||||||
req.setDeviceKeys(obj);
|
req.setDeviceKeys(obj);
|
||||||
|
|
||||||
DeviceStore.createKeys(req, () => {
|
DeviceStore.createKeys(req, () => {
|
||||||
this.props.history.push(
|
navigate(
|
||||||
`/tenants/${this.props.tenant.getId()}/applications/${this.props.application.getId()}/devices/${this.props.device.getDevEui()}`,
|
`/tenants/${props.tenant.getId()}/applications/${props.application.getId()}/devices/${props.device.getDevEui()}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
flushDevNonces = () => {
|
const flushDevNonces = () => {
|
||||||
let req = new FlushDevNoncesRequest();
|
let req = new FlushDevNoncesRequest();
|
||||||
req.setDevEui(this.props.device.getDevEui());
|
req.setDevEui(props.device.getDevEui());
|
||||||
DeviceStore.flushDevNonces(req, () => {});
|
DeviceStore.flushDevNonces(req, () => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
if (!deviceKeysRequested) {
|
||||||
if (!this.state.deviceKeysRequested) {
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const macVersion = this.props.deviceProfile.getMacVersion();
|
|
||||||
const lw11 = macVersion === MacVersion.LORAWAN_1_1_0;
|
|
||||||
|
|
||||||
let initialValues = new DeviceKeys();
|
|
||||||
if (this.state.deviceKeys) {
|
|
||||||
initialValues = this.state.deviceKeys;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
|
||||||
{this.state.deviceKeys && (
|
|
||||||
<div style={{ float: "right" }}>
|
|
||||||
<Popconfirm
|
|
||||||
placement="left"
|
|
||||||
title="Are you sure you want to flush all device-nonces that have been used during previous OTAA activations?"
|
|
||||||
onConfirm={this.flushDevNonces}
|
|
||||||
>
|
|
||||||
<Button>Flush OTAA device nonces</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!lw11 && <LW10DeviceKeysForm initialValues={initialValues} onFinish={this.onFinish} />}
|
|
||||||
{lw11 && <LW11DeviceKeysForm initialValues={initialValues} onFinish={this.onFinish} />}
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const macVersion = props.deviceProfile.getMacVersion();
|
||||||
|
const lw11 = macVersion === MacVersion.LORAWAN_1_1_0;
|
||||||
|
|
||||||
|
let initialValues = new DeviceKeys();
|
||||||
|
if (deviceKeys) {
|
||||||
|
initialValues = deviceKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
|
{deviceKeys && (
|
||||||
|
<div style={{ float: "right" }}>
|
||||||
|
<Popconfirm
|
||||||
|
placement="left"
|
||||||
|
title="Are you sure you want to flush all device-nonces that have been used during previous OTAA activations?"
|
||||||
|
onConfirm={flushDevNonces}
|
||||||
|
>
|
||||||
|
<Button>Flush OTAA device nonces</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!lw11 && <LW10DeviceKeysForm initialValues={initialValues} onFinish={onFinish} />}
|
||||||
|
{lw11 && <LW11DeviceKeysForm initialValues={initialValues} onFinish={onFinish} />}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SetDeviceKeys;
|
export default SetDeviceKeys;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user