Refactor UI to function elements & update React + Ant.

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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