mirror of
https://github.com/chirpstack/chirpstack.git
synced 2025-06-16 14:28:14 +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:
@ -1,4 +1,4 @@
|
||||
import { Component } from "react";
|
||||
import React, { PropsWithChildren, useState, useEffect } from "react";
|
||||
|
||||
import SessionStore from "../stores/SessionStore";
|
||||
|
||||
@ -9,73 +9,45 @@ interface IProps {
|
||||
isTenantAdmin?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
admin: boolean;
|
||||
}
|
||||
function Admin(props: PropsWithChildren<IProps>) {
|
||||
const [admin, setAdmin] = useState<boolean>(false);
|
||||
|
||||
class Admin extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
admin: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
SessionStore.on("change", this.setIsAdmin);
|
||||
this.setIsAdmin();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
SessionStore.removeListener("change", this.setIsAdmin);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: IProps) {
|
||||
if (prevProps === this.props) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setIsAdmin();
|
||||
}
|
||||
|
||||
setIsAdmin = () => {
|
||||
if (!this.props.isDeviceAdmin && !this.props.isGatewayAdmin && !this.props.isTenantAdmin) {
|
||||
this.setState({
|
||||
admin: SessionStore.isAdmin(),
|
||||
});
|
||||
const setIsAdmin = () => {
|
||||
if (!props.isDeviceAdmin && !props.isGatewayAdmin && !props.isTenantAdmin) {
|
||||
setAdmin(SessionStore.isAdmin());
|
||||
} else {
|
||||
if (this.props.tenantId === undefined) {
|
||||
if (props.tenantId === undefined) {
|
||||
throw new Error("No tenantId is given");
|
||||
}
|
||||
|
||||
if (this.props.isTenantAdmin) {
|
||||
this.setState({
|
||||
admin: SessionStore.isAdmin() || SessionStore.isTenantAdmin(this.props.tenantId),
|
||||
});
|
||||
if (props.isTenantAdmin) {
|
||||
setAdmin(SessionStore.isAdmin() || SessionStore.isTenantAdmin(props.tenantId));
|
||||
}
|
||||
|
||||
if (this.props.isDeviceAdmin) {
|
||||
this.setState({
|
||||
admin: SessionStore.isAdmin() || SessionStore.isTenantDeviceAdmin(this.props.tenantId),
|
||||
});
|
||||
if (props.isDeviceAdmin) {
|
||||
setAdmin(SessionStore.isAdmin() || SessionStore.isTenantDeviceAdmin(props.tenantId));
|
||||
}
|
||||
|
||||
if (this.props.isGatewayAdmin) {
|
||||
this.setState({
|
||||
admin: SessionStore.isAdmin() || SessionStore.isTenantGatewayAdmin(this.props.tenantId),
|
||||
});
|
||||
if (props.isGatewayAdmin) {
|
||||
setAdmin(SessionStore.isAdmin() || SessionStore.isTenantGatewayAdmin(props.tenantId));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.admin) {
|
||||
return this.props.children;
|
||||
}
|
||||
useEffect(() => {
|
||||
SessionStore.on("change", setIsAdmin);
|
||||
setIsAdmin();
|
||||
|
||||
return null;
|
||||
return () => {
|
||||
SessionStore.removeListener("change", setIsAdmin);
|
||||
};
|
||||
}, [props]);
|
||||
|
||||
if (admin) {
|
||||
return <div>{props.children}</div>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default Admin;
|
||||
|
@ -1,11 +1,10 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { notification, Input, Select, Button, Space, Form, Dropdown, Menu } from "antd";
|
||||
import { ReloadOutlined, CopyOutlined } from "@ant-design/icons";
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
interface IProps {
|
||||
formRef: React.RefObject<any>;
|
||||
label: string;
|
||||
name: string;
|
||||
required?: boolean;
|
||||
@ -14,42 +13,29 @@ interface IProps {
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
byteOrder: string;
|
||||
value: string;
|
||||
}
|
||||
function AesKeyInput(props: IProps) {
|
||||
const form = Form.useFormInstance();
|
||||
const [byteOrder, setByteOrder] = useState<string>("msb");
|
||||
const [value, setValue] = useState<string>("");
|
||||
|
||||
class AesKeyInput extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
byteOrder: "msb",
|
||||
value: "",
|
||||
};
|
||||
}
|
||||
useEffect(() => {
|
||||
if (props.value) {
|
||||
setValue(props.value);
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
updateField = () => {
|
||||
let value = this.state.value;
|
||||
|
||||
if (this.state.byteOrder === "lsb") {
|
||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||
value = bytes.reverse().join("");
|
||||
const updateField = (v: string) => {
|
||||
if (byteOrder === "lsb") {
|
||||
const bytes = v.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||
v = bytes.reverse().join("");
|
||||
}
|
||||
|
||||
this.props.formRef.current.setFieldsValue({
|
||||
[this.props.name]: value,
|
||||
form.setFieldsValue({
|
||||
[props.name]: v,
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.value) {
|
||||
this.setState({
|
||||
value: this.props.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let v = e.target.value;
|
||||
const match = v.match(/[A-Fa-f0-9]/g);
|
||||
|
||||
@ -62,50 +48,37 @@ class AesKeyInput extends Component<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
value: value,
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
setValue(value);
|
||||
updateField(value);
|
||||
};
|
||||
|
||||
onByteOrderSelect = (v: string) => {
|
||||
if (v === this.state.byteOrder) {
|
||||
const onByteOrderSelect = (v: string) => {
|
||||
if (v === byteOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
byteOrder: v,
|
||||
});
|
||||
setByteOrder(v);
|
||||
|
||||
const current = this.state.value;
|
||||
const current = value;
|
||||
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||
const vv = bytes.reverse().join("");
|
||||
|
||||
this.setState(
|
||||
{
|
||||
value: bytes.reverse().join(""),
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
setValue(vv);
|
||||
updateField(vv);
|
||||
};
|
||||
|
||||
generateRandom = () => {
|
||||
const generateRandom = () => {
|
||||
let cryptoObj = window.crypto || window.Crypto;
|
||||
let b = new Uint8Array(16);
|
||||
cryptoObj.getRandomValues(b);
|
||||
|
||||
let key = Buffer.from(b).toString("hex");
|
||||
this.setState(
|
||||
{
|
||||
value: key,
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
setValue(key);
|
||||
updateField(key);
|
||||
};
|
||||
|
||||
copyToClipboard = () => {
|
||||
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
|
||||
const copyToClipboard = () => {
|
||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
|
||||
|
||||
if (bytes !== null && navigator.clipboard !== undefined) {
|
||||
navigator.clipboard
|
||||
@ -126,8 +99,8 @@ class AesKeyInput extends Component<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
copyToClipboardHexArray = () => {
|
||||
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
|
||||
const copyToClipboardHexArray = () => {
|
||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
|
||||
|
||||
if (bytes !== null && navigator.clipboard !== undefined) {
|
||||
navigator.clipboard
|
||||
@ -153,72 +126,70 @@ class AesKeyInput extends Component<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const copyMenu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: (
|
||||
<Button type="text" onClick={this.copyToClipboard}>
|
||||
HEX string
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: (
|
||||
<Button type="text" onClick={this.copyToClipboardHexArray}>
|
||||
HEX array
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const copyMenu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: (
|
||||
<Button type="text" onClick={copyToClipboard}>
|
||||
HEX string
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: (
|
||||
<Button type="text" onClick={copyToClipboardHexArray}>
|
||||
HEX array
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const addon = (
|
||||
<Space size="large">
|
||||
<Select value={this.state.byteOrder} onChange={this.onByteOrderSelect}>
|
||||
<Select.Option value="msb">MSB</Select.Option>
|
||||
<Select.Option value="lsb">LSB</Select.Option>
|
||||
</Select>
|
||||
<Button type="text" size="small" shape="circle" onClick={this.generateRandom}>
|
||||
<ReloadOutlined />
|
||||
const addon = (
|
||||
<Space size="large">
|
||||
<Select value={byteOrder} onChange={onByteOrderSelect}>
|
||||
<Select.Option value="msb">MSB</Select.Option>
|
||||
<Select.Option value="lsb">LSB</Select.Option>
|
||||
</Select>
|
||||
<Button type="text" size="small" onClick={generateRandom}>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
<Dropdown overlay={copyMenu}>
|
||||
<Button type="text" size="small">
|
||||
<CopyOutlined />
|
||||
</Button>
|
||||
<Dropdown overlay={copyMenu}>
|
||||
<Button type="text" size="small">
|
||||
<CopyOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
);
|
||||
</Dropdown>
|
||||
</Space>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
rules={[
|
||||
{
|
||||
required: this.props.required,
|
||||
message: `Please enter a valid ${this.props.label}`,
|
||||
pattern: new RegExp(/[A-Fa-f0-9]{32}/g),
|
||||
},
|
||||
]}
|
||||
label={this.props.label}
|
||||
name={this.props.name}
|
||||
tooltip={this.props.tooltip}
|
||||
>
|
||||
<Input hidden />
|
||||
<Input.Password
|
||||
id={`${this.props.name}Render`}
|
||||
onChange={this.onChange}
|
||||
addonAfter={!this.props.disabled && addon}
|
||||
style={{ fontFamily: "monospace" }}
|
||||
value={this.state.value}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form.Item
|
||||
rules={[
|
||||
{
|
||||
required: props.required,
|
||||
message: `Please enter a valid ${props.label}`,
|
||||
pattern: new RegExp(/[A-Fa-f0-9]{32}/g),
|
||||
},
|
||||
]}
|
||||
label={props.label}
|
||||
name={props.name}
|
||||
tooltip={props.tooltip}
|
||||
>
|
||||
<Input hidden />
|
||||
<Input
|
||||
id={`${props.name}Render`}
|
||||
onChange={onChange}
|
||||
addonAfter={!props.disabled && addon}
|
||||
className="input-code"
|
||||
value={value}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export default AesKeyInput;
|
||||
|
@ -1,10 +1,15 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { Select } from "antd";
|
||||
|
||||
export type OptionsCallbackFunc = (o: { label: string; value: string }[]) => void;
|
||||
export type OptionCallbackFunc = (o: { label: string; value: string }) => void;
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
placeholder: string;
|
||||
className: string;
|
||||
@ -14,93 +19,59 @@ interface IProps {
|
||||
onSelect?: (s: string) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
option?: { label: string; value: string };
|
||||
options: { label: string; value: string }[];
|
||||
}
|
||||
function AutoComplete({ placeholder, className, value, getOption, getOptions, onSelect }: IProps) {
|
||||
const [option, setOption] = useState<Option | undefined>(undefined);
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
|
||||
class Autocomplete extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.value && this.props.value !== "") {
|
||||
this.props.getOption(this.props.value, (o: { label: string; value: string }) => {
|
||||
this.setState({
|
||||
options: [o],
|
||||
});
|
||||
useEffect(() => {
|
||||
if (value && value !== "") {
|
||||
getOption(value, (o: Option) => {
|
||||
setOptions([o]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [value, getOption]);
|
||||
|
||||
componentDidUpdate(prevProps: IProps) {
|
||||
if (this.props.value === prevProps.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.value && this.props.value !== "") {
|
||||
this.props.getOption(this.props.value, (o: { label: string; value: string }) => {
|
||||
this.setState({
|
||||
options: [o],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onFocus = () => {
|
||||
this.props.getOptions("", options => {
|
||||
if (this.state.option !== undefined) {
|
||||
const selected = this.state.option.value;
|
||||
const onFocus = () => {
|
||||
getOptions("", options => {
|
||||
if (option !== undefined) {
|
||||
const selected = option.value;
|
||||
|
||||
if (options.find(e => e.value === selected) === undefined) {
|
||||
options.unshift(this.state.option);
|
||||
options.unshift(option);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
options: options,
|
||||
});
|
||||
setOptions(options);
|
||||
});
|
||||
};
|
||||
|
||||
onSearch = (value: string) => {
|
||||
this.props.getOptions(value, options => {
|
||||
this.setState({
|
||||
options: options,
|
||||
});
|
||||
const onSearch = (value: string) => {
|
||||
getOptions(value, options => {
|
||||
setOptions(options);
|
||||
});
|
||||
};
|
||||
|
||||
onSelect = (value: string, option: any) => {
|
||||
this.setState({
|
||||
option: { label: option.label, value: option.value },
|
||||
});
|
||||
const onSelectFn = (value: string, option: any) => {
|
||||
setOption({ label: option.label, value: option.value });
|
||||
|
||||
if (this.props.onSelect !== undefined) {
|
||||
this.props.onSelect(value);
|
||||
if (onSelect !== undefined) {
|
||||
onSelect(value);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { getOption, getOptions, ...otherProps } = this.props;
|
||||
|
||||
return (
|
||||
<Select
|
||||
showSearch
|
||||
options={this.state.options}
|
||||
onFocus={this.onFocus}
|
||||
onSearch={this.onSearch}
|
||||
onSelect={this.onSelect}
|
||||
filterOption={false}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
showSearch
|
||||
options={options}
|
||||
onFocus={onFocus}
|
||||
onSearch={onSearch}
|
||||
onSelect={onSelectFn}
|
||||
filterOption={false}
|
||||
placeholder={placeholder}
|
||||
className={className}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Autocomplete;
|
||||
export default AutoComplete;
|
||||
|
@ -1,11 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Form } from "antd";
|
||||
|
||||
import Autocomplete, { OptionCallbackFunc, OptionsCallbackFunc } from "./Autocomplete";
|
||||
|
||||
interface IProps {
|
||||
formRef: React.RefObject<any>;
|
||||
label: string;
|
||||
name: string;
|
||||
required?: boolean;
|
||||
@ -14,28 +11,35 @@ interface IProps {
|
||||
getOptions: (s: string, fn: OptionsCallbackFunc) => void;
|
||||
}
|
||||
|
||||
class AutocompleteInput extends Component<IProps> {
|
||||
render() {
|
||||
return (
|
||||
<Form.Item
|
||||
rules={[
|
||||
{
|
||||
required: this.props.required,
|
||||
message: `Please select a ${this.props.label}`,
|
||||
},
|
||||
]}
|
||||
label={this.props.label}
|
||||
name={this.props.name}
|
||||
>
|
||||
<Autocomplete
|
||||
placeholder={`Select a ${this.props.label}`}
|
||||
className=""
|
||||
getOption={this.props.getOption}
|
||||
getOptions={this.props.getOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
function AutocompleteInput(props: IProps) {
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
const onSelect = (value: string) => {
|
||||
form.setFieldsValue({
|
||||
[props.name]: value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
rules={[
|
||||
{
|
||||
required: props.required,
|
||||
message: `Please select a ${props.label}`,
|
||||
},
|
||||
]}
|
||||
label={props.label}
|
||||
name={props.name}
|
||||
>
|
||||
<Autocomplete
|
||||
placeholder={`Select a ${props.label}`}
|
||||
className=""
|
||||
getOption={props.getOption}
|
||||
getOptions={props.getOptions}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export default AutocompleteInput;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Controlled as CodeMirror } from "react-codemirror2";
|
||||
|
||||
import { Form } from "antd";
|
||||
@ -6,88 +6,49 @@ import { Form } from "antd";
|
||||
import "codemirror/mode/javascript/javascript";
|
||||
|
||||
interface IProps {
|
||||
formRef: React.RefObject<any>;
|
||||
label?: string;
|
||||
name: string;
|
||||
required?: boolean;
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
value: string;
|
||||
reloadKey: number;
|
||||
}
|
||||
function CodeEditor(props: IProps) {
|
||||
const form = Form.useFormInstance();
|
||||
const [value, setValue] = useState<string>("");
|
||||
const [reloadKey, setReloadKey] = useState<number>(1);
|
||||
|
||||
class CodeEditor extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: "",
|
||||
reloadKey: 0,
|
||||
};
|
||||
}
|
||||
useEffect(() => {
|
||||
setValue(form.getFieldValue(props.name));
|
||||
setReloadKey(k => k + 1);
|
||||
}, [form, props]);
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.value) {
|
||||
this.setState({
|
||||
value: this.props.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(oldProps: IProps) {
|
||||
if (this.props === oldProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.value) {
|
||||
this.setState({
|
||||
value: this.props.value,
|
||||
reloadKey: this.state.reloadKey + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateField = () => {
|
||||
let value = this.state.value;
|
||||
|
||||
this.props.formRef.current.setFieldsValue({
|
||||
[this.props.name]: value,
|
||||
const handleChange = (editor: any, data: any, newCode: string) => {
|
||||
setValue(newCode);
|
||||
form.setFieldsValue({
|
||||
[props.name]: newCode,
|
||||
});
|
||||
};
|
||||
|
||||
handleChange = (editor: any, data: any, newCode: string) => {
|
||||
this.setState(
|
||||
{
|
||||
value: newCode,
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
const codeMirrorOptions = {
|
||||
lineNumbers: true,
|
||||
mode: "javascript",
|
||||
theme: "base16-light",
|
||||
readOnly: props.disabled,
|
||||
};
|
||||
|
||||
render() {
|
||||
const codeMirrorOptions = {
|
||||
lineNumbers: true,
|
||||
mode: "javascript",
|
||||
theme: "base16-light",
|
||||
readOnly: this.props.disabled,
|
||||
};
|
||||
|
||||
return (
|
||||
<Form.Item label={this.props.label} name={this.props.name} tooltip={this.props.tooltip}>
|
||||
<div style={{ border: "1px solid #cccccc" }}>
|
||||
<CodeMirror
|
||||
key={`code-editor-refresh-${this.state.reloadKey}`}
|
||||
value={this.state.value}
|
||||
options={codeMirrorOptions}
|
||||
onBeforeChange={this.handleChange}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form.Item label={props.label} name={props.name} tooltip={props.tooltip}>
|
||||
<div style={{ border: "1px solid #cccccc" }}>
|
||||
<CodeMirror
|
||||
key={`code-editor-refresh-${reloadKey}`}
|
||||
value={value}
|
||||
options={codeMirrorOptions}
|
||||
onBeforeChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export default CodeEditor;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { Table } from "antd";
|
||||
import { ColumnsType } from "antd/es/table";
|
||||
@ -16,113 +16,81 @@ interface IProps {
|
||||
noPagination?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
totalCount: number;
|
||||
pageSize: number;
|
||||
currentPage: number;
|
||||
rows: object[];
|
||||
loading: boolean;
|
||||
}
|
||||
function DataTable(props: IProps) {
|
||||
const [totalCount, setTotalCount] = useState<number>(0);
|
||||
const [pageSize, setPageSize] = useState<number>(SessionStore.getRowsPerPage());
|
||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||
const [rows, setRows] = useState<object[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
class DataTable extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
const onChangePage = (page: number, pz?: number | void) => {
|
||||
setLoading(true);
|
||||
|
||||
this.state = {
|
||||
totalCount: 0,
|
||||
pageSize: SessionStore.getRowsPerPage(),
|
||||
currentPage: 1,
|
||||
rows: [],
|
||||
loading: true,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.onChangePage(this.state.currentPage, this.state.pageSize);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: IProps) {
|
||||
if (this.props === prevProps) {
|
||||
return;
|
||||
if (!pz) {
|
||||
pz = pageSize;
|
||||
}
|
||||
|
||||
this.onChangePage(this.state.currentPage, this.state.pageSize);
|
||||
}
|
||||
|
||||
onChangePage = (page: number, pageSize?: number | void) => {
|
||||
this.setState(
|
||||
{
|
||||
loading: true,
|
||||
},
|
||||
() => {
|
||||
let pz = pageSize;
|
||||
if (!pz) {
|
||||
pz = this.state.pageSize;
|
||||
}
|
||||
|
||||
this.props.getPage(pz, (page - 1) * pz, (totalCount: number, rows: object[]) => {
|
||||
this.setState({
|
||||
currentPage: page,
|
||||
totalCount: totalCount,
|
||||
rows: rows,
|
||||
pageSize: pz || 0,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
props.getPage(pz, (page - 1) * pz, (totalCount: number, rows: object[]) => {
|
||||
setCurrentPage(page);
|
||||
setTotalCount(totalCount);
|
||||
setRows(rows);
|
||||
setPageSize(pz || 0);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
onShowSizeChange = (page: number, pageSize: number) => {
|
||||
this.onChangePage(page, pageSize);
|
||||
const onShowSizeChange = (page: number, pageSize: number) => {
|
||||
onChangePage(page, pageSize);
|
||||
SessionStore.setRowsPerPage(pageSize);
|
||||
};
|
||||
|
||||
onRowsSelectChange = (ids: React.Key[]) => {
|
||||
const onRowsSelectChange = (ids: React.Key[]) => {
|
||||
const idss = ids as string[];
|
||||
if (this.props.onRowsSelectChange) {
|
||||
this.props.onRowsSelectChange(idss);
|
||||
if (props.onRowsSelectChange) {
|
||||
props.onRowsSelectChange(idss);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { getPage, refreshKey, ...otherProps } = this.props;
|
||||
let loadingProps = undefined;
|
||||
if (this.state.loading) {
|
||||
loadingProps = {
|
||||
delay: 300,
|
||||
};
|
||||
}
|
||||
useEffect(() => {
|
||||
onChangePage(currentPage, pageSize);
|
||||
}, [props, currentPage, pageSize]);
|
||||
|
||||
let pagination = undefined;
|
||||
if (this.props.noPagination === undefined || this.props.noPagination === false) {
|
||||
pagination = {
|
||||
current: this.state.currentPage,
|
||||
total: this.state.totalCount,
|
||||
pageSize: this.state.pageSize,
|
||||
onChange: this.onChangePage,
|
||||
showSizeChanger: true,
|
||||
onShowSizeChange: this.onShowSizeChange,
|
||||
};
|
||||
}
|
||||
|
||||
let rowSelection = undefined;
|
||||
if (this.props.onRowsSelectChange) {
|
||||
rowSelection = {
|
||||
onChange: this.onRowsSelectChange,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
loading={loadingProps}
|
||||
dataSource={this.state.rows}
|
||||
pagination={pagination || false}
|
||||
rowSelection={rowSelection}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
const { getPage, refreshKey, ...otherProps } = props;
|
||||
let loadingProps = undefined;
|
||||
if (loading) {
|
||||
loadingProps = {
|
||||
delay: 300,
|
||||
};
|
||||
}
|
||||
|
||||
let pagination = undefined;
|
||||
if (props.noPagination === undefined || props.noPagination === false) {
|
||||
pagination = {
|
||||
current: currentPage,
|
||||
total: totalCount,
|
||||
pageSize: pageSize,
|
||||
onChange: onChangePage,
|
||||
showSizeChanger: true,
|
||||
onShowSizeChange: onShowSizeChange,
|
||||
};
|
||||
}
|
||||
|
||||
let rowSelection = undefined;
|
||||
if (props.onRowsSelectChange) {
|
||||
rowSelection = {
|
||||
onChange: onRowsSelectChange,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
loading={loadingProps}
|
||||
dataSource={rows}
|
||||
pagination={pagination || false}
|
||||
rowSelection={rowSelection}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default DataTable;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { useState, PropsWithChildren } from "react";
|
||||
import { Popover, Button, Typography, Space, Input } from "antd";
|
||||
|
||||
interface IProps {
|
||||
@ -8,51 +7,32 @@ interface IProps {
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
interface ConfirmState {
|
||||
confirm: string;
|
||||
}
|
||||
function DeleteConfirmContent(props: IProps) {
|
||||
const [confirm, setConfirm] = useState<string>("");
|
||||
|
||||
class DeleteConfirmContent extends Component<IProps, ConfirmState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
confirm: "",
|
||||
};
|
||||
}
|
||||
|
||||
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
confirm: e.target.value,
|
||||
});
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setConfirm(e.target.value);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Space direction="vertical">
|
||||
<Typography.Text>
|
||||
Enter '{this.props.confirm}' to confirm you want to delete this {this.props.typ}:
|
||||
</Typography.Text>
|
||||
<Input placeholder={this.props.confirm} onChange={this.onChange} />
|
||||
<Button
|
||||
onClick={this.props.onConfirm}
|
||||
disabled={this.state.confirm !== this.props.confirm}
|
||||
style={{ float: "right" }}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Space direction="vertical">
|
||||
<Typography.Text>
|
||||
Enter '{props.confirm}' to confirm you want to delete this {props.typ}:
|
||||
</Typography.Text>
|
||||
<Input placeholder={props.confirm} onChange={onChange} />
|
||||
<Button onClick={props.onConfirm} disabled={confirm !== props.confirm} style={{ float: "right" }}>
|
||||
Delete
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
class DeleteConfirm extends Component<IProps> {
|
||||
render() {
|
||||
return (
|
||||
<Popover content={<DeleteConfirmContent {...this.props} />} trigger="click" placement="left">
|
||||
{this.props.children}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
function DeleteConfirm(props: PropsWithChildren<IProps>) {
|
||||
return (
|
||||
<Popover content={<DeleteConfirmContent {...props} />} trigger="click" placement="left">
|
||||
{props.children}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeleteConfirm;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { notification, Input, Select, Button, Space, Form, Dropdown, Menu } from "antd";
|
||||
import { ReloadOutlined, CopyOutlined } from "@ant-design/icons";
|
||||
@ -8,7 +8,6 @@ import { GetRandomDevAddrRequest, GetRandomDevAddrResponse } from "@chirpstack/c
|
||||
import DeviceStore from "../stores/DeviceStore";
|
||||
|
||||
interface IProps {
|
||||
formRef: React.RefObject<any>;
|
||||
label: string;
|
||||
name: string;
|
||||
devEui: string;
|
||||
@ -17,42 +16,29 @@ interface IProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
byteOrder: string;
|
||||
value: string;
|
||||
}
|
||||
function DevAddrInput(props: IProps) {
|
||||
const form = Form.useFormInstance();
|
||||
const [byteOrder, setByteOrder] = useState<string>("msb");
|
||||
const [value, setValue] = useState<string>("");
|
||||
|
||||
class DevAddrInput extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
byteOrder: "msb",
|
||||
value: "",
|
||||
};
|
||||
}
|
||||
useEffect(() => {
|
||||
if (props.value) {
|
||||
setValue(props.value);
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
updateField = () => {
|
||||
let value = this.state.value;
|
||||
|
||||
if (this.state.byteOrder === "lsb") {
|
||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||
value = bytes.reverse().join("");
|
||||
const updateField = (v: string) => {
|
||||
if (byteOrder === "lsb") {
|
||||
const bytes = v.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||
v = bytes.reverse().join("");
|
||||
}
|
||||
|
||||
this.props.formRef.current.setFieldsValue({
|
||||
[this.props.name]: value,
|
||||
form.setFieldsValue({
|
||||
[props.name]: v,
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.value) {
|
||||
this.setState({
|
||||
value: this.props.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let v = e.target.value;
|
||||
const match = v.match(/[A-Fa-f0-9]/g);
|
||||
|
||||
@ -65,50 +51,37 @@ class DevAddrInput extends Component<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
value: value,
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
setValue(value);
|
||||
updateField(value);
|
||||
};
|
||||
|
||||
onByteOrderSelect = (v: string) => {
|
||||
if (v === this.state.byteOrder) {
|
||||
const onByteOrderSelect = (v: string) => {
|
||||
if (v === byteOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
byteOrder: v,
|
||||
});
|
||||
setByteOrder(v);
|
||||
|
||||
const current = this.state.value;
|
||||
const current = value;
|
||||
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||
const vv = bytes.reverse().join("");
|
||||
|
||||
this.setState(
|
||||
{
|
||||
value: bytes.reverse().join(""),
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
setValue(vv);
|
||||
updateField(vv);
|
||||
};
|
||||
|
||||
generateRandom = () => {
|
||||
const generateRandom = () => {
|
||||
let req = new GetRandomDevAddrRequest();
|
||||
req.setDevEui(this.props.devEui);
|
||||
req.setDevEui(props.devEui);
|
||||
|
||||
DeviceStore.getRandomDevAddr(req, (resp: GetRandomDevAddrResponse) => {
|
||||
this.setState(
|
||||
{
|
||||
value: resp.getDevAddr(),
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
setValue(resp.getDevAddr());
|
||||
updateField(resp.getDevAddr());
|
||||
});
|
||||
};
|
||||
|
||||
copyToClipboard = () => {
|
||||
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
|
||||
const copyToClipboard = () => {
|
||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
|
||||
|
||||
if (bytes !== null && navigator.clipboard !== undefined) {
|
||||
navigator.clipboard
|
||||
@ -129,8 +102,8 @@ class DevAddrInput extends Component<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
copyToClipboardHexArray = () => {
|
||||
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
|
||||
const copyToClipboardHexArray = () => {
|
||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
|
||||
|
||||
if (bytes !== null && navigator.clipboard !== undefined) {
|
||||
navigator.clipboard
|
||||
@ -156,71 +129,69 @@ class DevAddrInput extends Component<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const copyMenu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: (
|
||||
<Button type="text" onClick={this.copyToClipboard}>
|
||||
HEX string
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: (
|
||||
<Button type="text" onClick={this.copyToClipboardHexArray}>
|
||||
HEX array
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const copyMenu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: (
|
||||
<Button type="text" onClick={copyToClipboard}>
|
||||
HEX string
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: (
|
||||
<Button type="text" onClick={copyToClipboardHexArray}>
|
||||
HEX array
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const addon = (
|
||||
<Space size="large">
|
||||
<Select value={this.state.byteOrder} onChange={this.onByteOrderSelect}>
|
||||
<Select.Option value="msb">MSB</Select.Option>
|
||||
<Select.Option value="lsb">LSB</Select.Option>
|
||||
</Select>
|
||||
<Button type="text" size="small" shape="circle" onClick={this.generateRandom}>
|
||||
<ReloadOutlined />
|
||||
const addon = (
|
||||
<Space size="large">
|
||||
<Select value={byteOrder} onChange={onByteOrderSelect}>
|
||||
<Select.Option value="msb">MSB</Select.Option>
|
||||
<Select.Option value="lsb">LSB</Select.Option>
|
||||
</Select>
|
||||
<Button type="text" size="small" onClick={generateRandom}>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
<Dropdown overlay={copyMenu}>
|
||||
<Button type="text" size="small">
|
||||
<CopyOutlined />
|
||||
</Button>
|
||||
<Dropdown overlay={copyMenu}>
|
||||
<Button type="text" size="small">
|
||||
<CopyOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
);
|
||||
</Dropdown>
|
||||
</Space>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
rules={[
|
||||
{
|
||||
required: this.props.required,
|
||||
message: `Please enter a valid ${this.props.label}`,
|
||||
pattern: new RegExp(/[A-Fa-f0-9]{8}/g),
|
||||
},
|
||||
]}
|
||||
label={this.props.label}
|
||||
name={this.props.name}
|
||||
>
|
||||
<Input hidden />
|
||||
<Input
|
||||
id={`${this.props.name}Render`}
|
||||
onChange={this.onChange}
|
||||
addonAfter={!this.props.disabled && addon}
|
||||
style={{ fontFamily: "monospace" }}
|
||||
value={this.state.value}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form.Item
|
||||
rules={[
|
||||
{
|
||||
required: props.required,
|
||||
message: `Please enter a valid ${props.label}`,
|
||||
pattern: new RegExp(/[A-Fa-f0-9]{8}/g),
|
||||
},
|
||||
]}
|
||||
label={props.label}
|
||||
name={props.name}
|
||||
>
|
||||
<Input hidden />
|
||||
<Input
|
||||
id={`${props.name}Render`}
|
||||
onChange={onChange}
|
||||
addonAfter={!props.disabled && addon}
|
||||
className="input-code"
|
||||
value={value}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export default DevAddrInput;
|
||||
|
@ -1,11 +1,10 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { notification, Input, Select, Button, Space, Form, Dropdown, Menu } from "antd";
|
||||
import { ReloadOutlined, CopyOutlined } from "@ant-design/icons";
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
interface IProps {
|
||||
formRef: React.RefObject<any>;
|
||||
label: string;
|
||||
name: string;
|
||||
required?: boolean;
|
||||
@ -14,42 +13,29 @@ interface IProps {
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
byteOrder: string;
|
||||
value: string;
|
||||
}
|
||||
function EuiInput(props: IProps) {
|
||||
const form = Form.useFormInstance();
|
||||
const [byteOrder, setByteOrder] = useState<string>("msb");
|
||||
const [value, setValue] = useState<string>("");
|
||||
|
||||
class EuiInput extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
byteOrder: "msb",
|
||||
value: "",
|
||||
};
|
||||
}
|
||||
useEffect(() => {
|
||||
if (props.value) {
|
||||
setValue(props.value);
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
updateField = () => {
|
||||
let value = this.state.value;
|
||||
|
||||
if (this.state.byteOrder === "lsb") {
|
||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||
value = bytes.reverse().join("");
|
||||
const updateField = (v: string) => {
|
||||
if (byteOrder === "lsb") {
|
||||
const bytes = v.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||
v = bytes.reverse().join("");
|
||||
}
|
||||
|
||||
this.props.formRef.current.setFieldsValue({
|
||||
[this.props.name]: value,
|
||||
form.setFieldsValue({
|
||||
[props.name]: v,
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.value) {
|
||||
this.setState({
|
||||
value: this.props.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let v = e.target.value;
|
||||
const match = v.match(/[A-Fa-f0-9]/g);
|
||||
|
||||
@ -62,50 +48,37 @@ class EuiInput extends Component<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
value: value,
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
setValue(value);
|
||||
updateField(value);
|
||||
};
|
||||
|
||||
onByteOrderSelect = (v: string) => {
|
||||
if (v === this.state.byteOrder) {
|
||||
const onByteOrderSelect = (v: string) => {
|
||||
if (v === byteOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
byteOrder: v,
|
||||
});
|
||||
setByteOrder(v);
|
||||
|
||||
const current = this.state.value;
|
||||
const current = value;
|
||||
const bytes = current.match(/[A-Fa-f0-9]{2}/g) || [];
|
||||
const vv = bytes.reverse().join("");
|
||||
|
||||
this.setState(
|
||||
{
|
||||
value: bytes.reverse().join(""),
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
setValue(vv);
|
||||
updateField(vv);
|
||||
};
|
||||
|
||||
generateRandom = () => {
|
||||
const generateRandom = () => {
|
||||
let cryptoObj = window.crypto || window.Crypto;
|
||||
let b = new Uint8Array(8);
|
||||
cryptoObj.getRandomValues(b);
|
||||
|
||||
let key = Buffer.from(b).toString("hex");
|
||||
this.setState(
|
||||
{
|
||||
value: key,
|
||||
},
|
||||
this.updateField,
|
||||
);
|
||||
setValue(key);
|
||||
updateField(key);
|
||||
};
|
||||
|
||||
copyToClipboard = () => {
|
||||
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
|
||||
const copyToClipboard = () => {
|
||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
|
||||
|
||||
if (bytes !== null && navigator.clipboard !== undefined) {
|
||||
navigator.clipboard
|
||||
@ -126,8 +99,8 @@ class EuiInput extends Component<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
copyToClipboardHexArray = () => {
|
||||
const bytes = this.state.value.match(/[A-Fa-f0-9]{2}/g);
|
||||
const copyToClipboardHexArray = () => {
|
||||
const bytes = value.match(/[A-Fa-f0-9]{2}/g);
|
||||
|
||||
if (bytes !== null && navigator.clipboard !== undefined) {
|
||||
navigator.clipboard
|
||||
@ -153,72 +126,70 @@ class EuiInput extends Component<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const copyMenu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: (
|
||||
<Button type="text" onClick={this.copyToClipboard}>
|
||||
HEX string
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: (
|
||||
<Button type="text" onClick={this.copyToClipboardHexArray}>
|
||||
HEX array
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const copyMenu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: (
|
||||
<Button type="text" onClick={copyToClipboard}>
|
||||
HEX string
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: (
|
||||
<Button type="text" onClick={copyToClipboardHexArray}>
|
||||
HEX array
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const addon = (
|
||||
<Space size="large">
|
||||
<Select value={this.state.byteOrder} onChange={this.onByteOrderSelect}>
|
||||
<Select.Option value="msb">MSB</Select.Option>
|
||||
<Select.Option value="lsb">LSB</Select.Option>
|
||||
</Select>
|
||||
<Button type="text" size="small" onClick={this.generateRandom}>
|
||||
<ReloadOutlined />
|
||||
const addon = (
|
||||
<Space size="large">
|
||||
<Select value={byteOrder} onChange={onByteOrderSelect}>
|
||||
<Select.Option value="msb">MSB</Select.Option>
|
||||
<Select.Option value="lsb">LSB</Select.Option>
|
||||
</Select>
|
||||
<Button type="text" size="small" onClick={generateRandom}>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
<Dropdown overlay={copyMenu}>
|
||||
<Button type="text" size="small">
|
||||
<CopyOutlined />
|
||||
</Button>
|
||||
<Dropdown overlay={copyMenu}>
|
||||
<Button type="text" size="small">
|
||||
<CopyOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
);
|
||||
</Dropdown>
|
||||
</Space>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
rules={[
|
||||
{
|
||||
required: this.props.required,
|
||||
message: `Please enter a valid ${this.props.label}`,
|
||||
pattern: new RegExp(/[A-Fa-f0-9]{16}/g),
|
||||
},
|
||||
]}
|
||||
label={this.props.label}
|
||||
name={this.props.name}
|
||||
tooltip={this.props.tooltip}
|
||||
>
|
||||
<Input hidden />
|
||||
<Input
|
||||
id={`${this.props.name}Render`}
|
||||
onChange={this.onChange}
|
||||
addonAfter={!this.props.disabled && addon}
|
||||
style={{ fontFamily: "monospace" }}
|
||||
value={this.state.value}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form.Item
|
||||
rules={[
|
||||
{
|
||||
required: props.required,
|
||||
message: `Please enter a valid ${props.label}`,
|
||||
pattern: new RegExp(/[A-Fa-f0-9]{16}/g),
|
||||
},
|
||||
]}
|
||||
label={props.label}
|
||||
name={props.name}
|
||||
tooltip={props.tooltip}
|
||||
>
|
||||
<Input hidden />
|
||||
<Input
|
||||
id={`${props.name}Render`}
|
||||
onChange={onChange}
|
||||
addonAfter={!props.disabled && addon}
|
||||
className="input-code"
|
||||
value={value}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export default EuiInput;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link, withRouter, RouteComponentProps } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
import { Button, Menu, Dropdown, Input, AutoComplete } from "antd";
|
||||
import { UserOutlined, DownOutlined, QuestionOutlined } from "@ant-design/icons";
|
||||
@ -14,15 +14,6 @@ import {
|
||||
import InternalStore from "../stores/InternalStore";
|
||||
import SessionStore from "../stores/SessionStore";
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
user: User;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
searchResult?: GlobalSearchResponse;
|
||||
settings?: SettingsResponse;
|
||||
}
|
||||
|
||||
const renderTitle = (title: string) => <span>{title}</span>;
|
||||
|
||||
const renderItem = (title: string, url: string) => ({
|
||||
@ -30,22 +21,19 @@ const renderItem = (title: string, url: string) => ({
|
||||
label: <Link to={url}>{title}</Link>,
|
||||
});
|
||||
|
||||
class Header extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
function Header({ user }: { user: User }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
const [settings, setSettings] = useState<SettingsResponse | undefined>(undefined);
|
||||
const [searchResult, setSearchResult] = useState<GlobalSearchResponse | undefined>(undefined);
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
InternalStore.settings((resp: SettingsResponse) => {
|
||||
this.setState({
|
||||
settings: resp,
|
||||
});
|
||||
setSettings(resp);
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
onSearch = (search: string) => {
|
||||
const onSearch = (search: string) => {
|
||||
if (search.length < 3) {
|
||||
return;
|
||||
}
|
||||
@ -55,14 +43,11 @@ class Header extends Component<IProps, IState> {
|
||||
req.setSearch(search);
|
||||
|
||||
InternalStore.globalSearch(req, (resp: GlobalSearchResponse) => {
|
||||
this.setState({
|
||||
searchResult: resp,
|
||||
});
|
||||
setSearchResult(resp);
|
||||
});
|
||||
};
|
||||
|
||||
onLogout = () => {
|
||||
let settings = this.state.settings;
|
||||
const onLogout = () => {
|
||||
if (settings === undefined) {
|
||||
return;
|
||||
}
|
||||
@ -71,117 +56,112 @@ class Header extends Component<IProps, IState> {
|
||||
|
||||
if (!oidc.getEnabled() || oidc.getLogoutUrl() === "") {
|
||||
SessionStore.logout(true, () => {
|
||||
this.props.history.push("/login");
|
||||
navigate("/login");
|
||||
});
|
||||
} else {
|
||||
SessionStore.logout(false, () => {
|
||||
window.location.assign(oidc.getLogoutUrl());
|
||||
navigate(oidc.getLogoutUrl());
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.settings === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (settings === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let oidcEnabled = this.state.settings!.getOpenidConnect()!.getEnabled();
|
||||
let oidcEnabled = settings!.getOpenidConnect()!.getEnabled();
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
{!oidcEnabled && (
|
||||
<Menu.Item>
|
||||
<Link to={`/users/${this.props.user.getId()}/password`}>Change password</Link>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item onClick={this.onLogout}>Logout</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
const menu = (
|
||||
<Menu>
|
||||
{!oidcEnabled && (
|
||||
<Menu.Item>
|
||||
<Link to={`/users/${user.getId()}/password`}>Change password</Link>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item onClick={onLogout}>Logout</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
let options: {
|
||||
label: any;
|
||||
options: any[];
|
||||
}[] = [
|
||||
{
|
||||
label: renderTitle("Tenants"),
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
label: renderTitle("Gateways"),
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
label: renderTitle("Applications"),
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
label: renderTitle("Devices"),
|
||||
options: [],
|
||||
},
|
||||
];
|
||||
let options: {
|
||||
label: any;
|
||||
options: any[];
|
||||
}[] = [
|
||||
{
|
||||
label: renderTitle("Tenants"),
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
label: renderTitle("Gateways"),
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
label: renderTitle("Applications"),
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
label: renderTitle("Devices"),
|
||||
options: [],
|
||||
},
|
||||
];
|
||||
|
||||
if (this.state.searchResult !== undefined) {
|
||||
for (const res of this.state.searchResult.getResultList()) {
|
||||
if (res.getKind() === "tenant") {
|
||||
options[0].options.push(renderItem(res.getTenantName(), `/tenants/${res.getTenantId()}`));
|
||||
}
|
||||
if (searchResult !== undefined) {
|
||||
for (const res of searchResult.getResultList()) {
|
||||
if (res.getKind() === "tenant") {
|
||||
options[0].options.push(renderItem(res.getTenantName(), `/tenants/${res.getTenantId()}`));
|
||||
}
|
||||
|
||||
if (res.getKind() === "gateway") {
|
||||
options[1].options.push(
|
||||
renderItem(res.getGatewayName(), `/tenants/${res.getTenantId()}/gateways/${res.getGatewayId()}`),
|
||||
);
|
||||
}
|
||||
if (res.getKind() === "gateway") {
|
||||
options[1].options.push(
|
||||
renderItem(res.getGatewayName(), `/tenants/${res.getTenantId()}/gateways/${res.getGatewayId()}`),
|
||||
);
|
||||
}
|
||||
|
||||
if (res.getKind() === "application") {
|
||||
options[2].options.push(
|
||||
renderItem(
|
||||
res.getApplicationName(),
|
||||
`/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (res.getKind() === "application") {
|
||||
options[2].options.push(
|
||||
renderItem(res.getApplicationName(), `/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}`),
|
||||
);
|
||||
}
|
||||
|
||||
if (res.getKind() === "device") {
|
||||
options[3].options.push(
|
||||
renderItem(
|
||||
res.getDeviceName(),
|
||||
`/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}/devices/${res.getDeviceDevEui()}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (res.getKind() === "device") {
|
||||
options[3].options.push(
|
||||
renderItem(
|
||||
res.getDeviceName(),
|
||||
`/tenants/${res.getTenantId()}/applications/${res.getApplicationId()}/devices/${res.getDeviceDevEui()}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<img className="logo" alt="ChirpStack" src="/logo.png" />
|
||||
<div className="actions">
|
||||
<div className="search">
|
||||
<AutoComplete
|
||||
dropdownClassName="search-dropdown"
|
||||
dropdownMatchSelectWidth={500}
|
||||
options={options}
|
||||
onSearch={this.onSearch}
|
||||
>
|
||||
<Input.Search placeholder="Search..." style={{ width: 500, marginTop: -5 }} />
|
||||
</AutoComplete>
|
||||
</div>
|
||||
<div className="help">
|
||||
<a href="https://www.chirpstack.io" target="_blank" rel="noreferrer">
|
||||
<Button icon={<QuestionOutlined />} />
|
||||
</a>
|
||||
</div>
|
||||
<div className="user">
|
||||
<Dropdown overlay={menu} placement="bottomRight" trigger={["click"]}>
|
||||
<Button type="primary" icon={<UserOutlined />}>
|
||||
{this.props.user.getEmail()} <DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
return (
|
||||
<div>
|
||||
<img className="logo" alt="ChirpStack" src="/logo.png" />
|
||||
<div className="actions">
|
||||
<div className="search">
|
||||
<AutoComplete
|
||||
dropdownClassName="search-dropdown"
|
||||
dropdownMatchSelectWidth={500}
|
||||
options={options}
|
||||
onSearch={onSearch}
|
||||
>
|
||||
<Input.Search placeholder="Search..." style={{ width: 500, marginTop: -5 }} />
|
||||
</AutoComplete>
|
||||
</div>
|
||||
<div className="help">
|
||||
<a href="https://www.chirpstack.io" target="_blank" rel="noreferrer">
|
||||
<Button icon={<QuestionOutlined />} />
|
||||
</a>
|
||||
</div>
|
||||
<div className="user">
|
||||
<Dropdown overlay={menu} placement="bottomRight" trigger={["click"]}>
|
||||
<Button type="primary" icon={<UserOutlined />}>
|
||||
{user.getEmail()} <DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(Header);
|
||||
export default Header;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import moment from "moment";
|
||||
import JSONTreeOriginal from "react-json-tree";
|
||||
import fileDownload from "js-file-download";
|
||||
@ -12,160 +13,142 @@ interface IProps {
|
||||
logs: LogItem[];
|
||||
}
|
||||
|
||||
interface IState {
|
||||
drawerOpen: boolean;
|
||||
body: any;
|
||||
drawerTitle: any;
|
||||
}
|
||||
function LogTable(props: IProps) {
|
||||
const [drawerOpen, setDrawerOpen] = useState<boolean>(false);
|
||||
const [body, setBody] = useState<any>(null);
|
||||
const [drawerTitle, setDrawerTitle] = useState<any>(null);
|
||||
|
||||
class LogTable extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
drawerOpen: false,
|
||||
body: null,
|
||||
drawerTitle: null,
|
||||
};
|
||||
}
|
||||
|
||||
onDrawerClose = () => {
|
||||
this.setState({
|
||||
drawerOpen: false,
|
||||
});
|
||||
const onDrawerClose = () => {
|
||||
setDrawerOpen(false);
|
||||
};
|
||||
|
||||
onDrawerOpen = (time: any, body: any) => {
|
||||
const onDrawerOpen = (time: any, body: any) => {
|
||||
let ts = new Date(0);
|
||||
ts.setUTCSeconds(time.seconds);
|
||||
let drawerTitle = moment(ts).format("YYYY-MM-DD HH:mm:ss");
|
||||
|
||||
return () => {
|
||||
this.setState({
|
||||
body: body,
|
||||
drawerTitle: drawerTitle,
|
||||
drawerOpen: true,
|
||||
});
|
||||
setBody(body);
|
||||
setDrawerTitle(drawerTitle);
|
||||
setDrawerOpen(true);
|
||||
};
|
||||
};
|
||||
|
||||
downloadSingleFrame = () => {
|
||||
fileDownload(JSON.stringify(JSON.parse(this.state.body), null, 4), "single-log.json", "application/json");
|
||||
const downloadSingleFrame = () => {
|
||||
fileDownload(JSON.stringify(JSON.parse(body), null, 4), "single-log.json", "application/json");
|
||||
};
|
||||
|
||||
downloadFrames = () => {
|
||||
let items = this.props.logs.map((l, i) => JSON.parse(l.getBody()));
|
||||
const downloadFrames = () => {
|
||||
let items = props.logs.map((l, i) => JSON.parse(l.getBody()));
|
||||
fileDownload(JSON.stringify(items, null, 4), "log.json");
|
||||
};
|
||||
|
||||
render() {
|
||||
let items = this.props.logs.map((l, i) => l.toObject());
|
||||
let body = JSON.parse(this.state.body);
|
||||
let items = props.logs.map((l, i) => l.toObject());
|
||||
let bodyJson = JSON.parse(body);
|
||||
|
||||
const theme = {
|
||||
scheme: "google",
|
||||
author: "seth wright (http://sethawright.com)",
|
||||
base00: "#000000",
|
||||
base01: "#282a2e",
|
||||
base02: "#373b41",
|
||||
base03: "#969896",
|
||||
base04: "#b4b7b4",
|
||||
base05: "#c5c8c6",
|
||||
base06: "#e0e0e0",
|
||||
base07: "#ffffff",
|
||||
base08: "#CC342B",
|
||||
base09: "#F96A38",
|
||||
base0A: "#FBA922",
|
||||
base0B: "#198844",
|
||||
base0C: "#3971ED",
|
||||
base0D: "#3971ED",
|
||||
base0E: "#A36AC7",
|
||||
base0F: "#3971ED",
|
||||
};
|
||||
const theme = {
|
||||
scheme: "google",
|
||||
author: "seth wright (http://sethawright.com)",
|
||||
base00: "#000000",
|
||||
base01: "#282a2e",
|
||||
base02: "#373b41",
|
||||
base03: "#969896",
|
||||
base04: "#b4b7b4",
|
||||
base05: "#c5c8c6",
|
||||
base06: "#e0e0e0",
|
||||
base07: "#ffffff",
|
||||
base08: "#CC342B",
|
||||
base09: "#F96A38",
|
||||
base0A: "#FBA922",
|
||||
base0B: "#198844",
|
||||
base0C: "#3971ED",
|
||||
base0D: "#3971ED",
|
||||
base0E: "#A36AC7",
|
||||
base0F: "#3971ED",
|
||||
};
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Drawer
|
||||
title={`Details: ${this.state.drawerTitle}`}
|
||||
placement="right"
|
||||
width={650}
|
||||
onClose={this.onDrawerClose}
|
||||
visible={this.state.drawerOpen}
|
||||
extra={<Button onClick={this.downloadSingleFrame}>Download</Button>}
|
||||
>
|
||||
<JSONTreeOriginal
|
||||
data={body}
|
||||
theme={theme}
|
||||
hideRoot={true}
|
||||
shouldExpandNode={() => {
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
{items.length !== 0 && (
|
||||
<Space direction="horizontal" style={{ float: "right" }} size="large">
|
||||
<Spin size="small" />
|
||||
<Button onClick={this.downloadFrames}>Download</Button>
|
||||
</Space>
|
||||
)}
|
||||
<Table
|
||||
showHeader={false}
|
||||
loading={items.length === 0}
|
||||
dataSource={items}
|
||||
pagination={false}
|
||||
columns={[
|
||||
{
|
||||
title: "Time",
|
||||
dataIndex: "time",
|
||||
key: "time",
|
||||
width: 200,
|
||||
render: (text, obj) => {
|
||||
let ts = new Date(0);
|
||||
ts.setUTCSeconds(obj.time!.seconds);
|
||||
return moment(ts).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
width: 200,
|
||||
render: (text, obj) => (
|
||||
<Button
|
||||
icon={<ZoomInOutlined />}
|
||||
type="primary"
|
||||
shape="round"
|
||||
size="small"
|
||||
onClick={this.onDrawerOpen(obj.time, obj.body)}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Properties",
|
||||
dataIndex: "properties",
|
||||
key: "properties",
|
||||
render: (text, obj) =>
|
||||
obj.propertiesMap.map((p, i) => {
|
||||
if (p[1] !== "") {
|
||||
return (
|
||||
<Tag>
|
||||
<pre>
|
||||
{p[0]}: {p[1]}
|
||||
</pre>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
]}
|
||||
return (
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Drawer
|
||||
title={`Details: ${drawerTitle}`}
|
||||
placement="right"
|
||||
width={650}
|
||||
onClose={onDrawerClose}
|
||||
visible={drawerOpen}
|
||||
extra={<Button onClick={downloadSingleFrame}>Download</Button>}
|
||||
>
|
||||
<JSONTreeOriginal
|
||||
data={bodyJson}
|
||||
theme={theme}
|
||||
hideRoot={true}
|
||||
shouldExpandNode={() => {
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
</Drawer>
|
||||
{items.length !== 0 && (
|
||||
<Space direction="horizontal" style={{ float: "right" }} size="large">
|
||||
<Spin size="small" />
|
||||
<Button onClick={downloadFrames}>Download</Button>
|
||||
</Space>
|
||||
)}
|
||||
<Table
|
||||
showHeader={false}
|
||||
loading={items.length === 0}
|
||||
dataSource={items}
|
||||
pagination={false}
|
||||
columns={[
|
||||
{
|
||||
title: "Time",
|
||||
dataIndex: "time",
|
||||
key: "time",
|
||||
width: 200,
|
||||
render: (text, obj) => {
|
||||
let ts = new Date(0);
|
||||
ts.setUTCSeconds(obj.time!.seconds);
|
||||
return moment(ts).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
width: 200,
|
||||
render: (text, obj) => (
|
||||
<Button
|
||||
icon={<ZoomInOutlined />}
|
||||
type="primary"
|
||||
shape="round"
|
||||
size="small"
|
||||
onClick={onDrawerOpen(obj.time, obj.body)}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Properties",
|
||||
dataIndex: "properties",
|
||||
key: "properties",
|
||||
render: (text, obj) =>
|
||||
obj.propertiesMap.map((p, i) => {
|
||||
if (p[1] !== "") {
|
||||
return (
|
||||
<Tag>
|
||||
<pre>
|
||||
{p[0]}: {p[1]}
|
||||
</pre>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogTable;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useEffect, PropsWithChildren } from "react";
|
||||
|
||||
import L, { LatLngTuple, FitBoundsOptions } from "leaflet";
|
||||
import "leaflet.awesome-markers";
|
||||
import { MarkerProps as LMarkerProps } from "react-leaflet";
|
||||
import { MarkerProps as LMarkerProps, useMap } from "react-leaflet";
|
||||
import { MapContainer, Marker as LMarker, TileLayer } from "react-leaflet";
|
||||
|
||||
interface IProps {
|
||||
@ -12,77 +12,48 @@ interface IProps {
|
||||
boundsOptions?: FitBoundsOptions;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
map?: L.Map;
|
||||
}
|
||||
function MapControl(props: { center?: [number, number]; bounds?: LatLngTuple[]; boundsOptions?: FitBoundsOptions }) {
|
||||
const map = useMap();
|
||||
|
||||
class Map extends Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
setMap = (map: L.Map) => {
|
||||
this.setState(
|
||||
{
|
||||
map: map,
|
||||
},
|
||||
() => {
|
||||
// This is needed as setMap is called after the map has been created.
|
||||
// There is a small amount of time where componentDidUpdate can't update
|
||||
// the map with the new center because setMap hasn't been called yet.
|
||||
// In such case, the map would never update to the new center.
|
||||
if (this.props.center !== undefined) {
|
||||
map.panTo(this.props.center);
|
||||
}
|
||||
|
||||
if (this.props.bounds !== undefined) {
|
||||
map.fitBounds(this.props.bounds, this.props.boundsOptions);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
componentDidUpdate(oldProps: IProps) {
|
||||
if (this.props === oldProps) {
|
||||
useEffect(() => {
|
||||
if (map === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.map) {
|
||||
if (this.props.center !== undefined) {
|
||||
this.state.map.flyTo(this.props.center);
|
||||
}
|
||||
|
||||
if (this.props.bounds !== undefined) {
|
||||
this.state.map.flyToBounds(this.props.bounds, this.props.boundsOptions);
|
||||
}
|
||||
if (props.center !== undefined) {
|
||||
map.flyTo(props.center);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = {
|
||||
height: this.props.height,
|
||||
};
|
||||
if (props.bounds !== undefined) {
|
||||
map.flyToBounds(props.bounds, props.boundsOptions);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<MapContainer
|
||||
bounds={this.props.bounds}
|
||||
boundsOptions={this.props.boundsOptions}
|
||||
center={this.props.center}
|
||||
zoom={13}
|
||||
scrollWheelZoom={false}
|
||||
animate={true}
|
||||
style={style}
|
||||
whenCreated={this.setMap}
|
||||
>
|
||||
<TileLayer
|
||||
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
{this.props.children}
|
||||
</MapContainer>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function Map(props: PropsWithChildren<IProps>) {
|
||||
const style = {
|
||||
height: props.height,
|
||||
};
|
||||
|
||||
return (
|
||||
<MapContainer
|
||||
bounds={props.bounds}
|
||||
boundsOptions={props.boundsOptions}
|
||||
center={props.center}
|
||||
zoom={13}
|
||||
scrollWheelZoom={false}
|
||||
style={style}
|
||||
>
|
||||
<TileLayer
|
||||
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
{props.children}
|
||||
<MapControl bounds={props.bounds} boundsOptions={props.boundsOptions} center={props.center} />
|
||||
</MapContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export type MarkerColor =
|
||||
@ -103,22 +74,20 @@ interface MarkerProps extends LMarkerProps {
|
||||
color: MarkerColor;
|
||||
}
|
||||
|
||||
export class Marker extends Component<MarkerProps> {
|
||||
render() {
|
||||
const { faIcon, color, position, ...otherProps } = this.props;
|
||||
export function Marker(props: MarkerProps) {
|
||||
const { faIcon, color, position, ...otherProps } = props;
|
||||
|
||||
const iconMarker = L.AwesomeMarkers.icon({
|
||||
icon: faIcon,
|
||||
prefix: "fa",
|
||||
markerColor: color,
|
||||
});
|
||||
const iconMarker = L.AwesomeMarkers.icon({
|
||||
icon: faIcon,
|
||||
prefix: "fa",
|
||||
markerColor: color,
|
||||
});
|
||||
|
||||
return (
|
||||
<LMarker icon={iconMarker} position={position} {...otherProps}>
|
||||
{this.props.children}
|
||||
</LMarker>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<LMarker icon={iconMarker} position={position} {...otherProps}>
|
||||
{props.children}
|
||||
</LMarker>
|
||||
);
|
||||
}
|
||||
|
||||
export default Map;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { withRouter, RouteComponentProps, Link } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import { Menu, MenuProps } from "antd";
|
||||
import {
|
||||
@ -24,46 +24,18 @@ import Autocomplete, { OptionCallbackFunc, OptionsCallbackFunc } from "../compon
|
||||
import TenantStore from "../stores/TenantStore";
|
||||
import SessionStore from "../stores/SessionStore";
|
||||
|
||||
interface IState {
|
||||
tenantId: string;
|
||||
selectedKey: string;
|
||||
}
|
||||
function SideMenu() {
|
||||
const [tenantId, setTenantId] = useState<string>("");
|
||||
const [selectedKey, setSelectedKey] = useState<string>("");
|
||||
|
||||
class SideMenu extends Component<RouteComponentProps, IState> {
|
||||
constructor(props: RouteComponentProps) {
|
||||
super(props);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
this.state = {
|
||||
tenantId: "",
|
||||
selectedKey: "ns-dashboard",
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
SessionStore.on("tenant.change", this.setTenant);
|
||||
this.setTenant();
|
||||
this.parseLocation();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
SessionStore.removeListener("tenant.change", this.setTenant);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: RouteComponentProps) {
|
||||
if (this.props === prevProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.parseLocation();
|
||||
}
|
||||
|
||||
setTenant = () => {
|
||||
this.setState({
|
||||
tenantId: SessionStore.getTenantId(),
|
||||
});
|
||||
const setTenant = () => {
|
||||
setTenantId(SessionStore.getTenantId());
|
||||
};
|
||||
|
||||
getTenantOptions = (search: string, fn: OptionsCallbackFunc) => {
|
||||
const getTenantOptions = (search: string, fn: OptionsCallbackFunc) => {
|
||||
let req = new ListTenantsRequest();
|
||||
req.setSearch(search);
|
||||
req.setLimit(10);
|
||||
@ -76,7 +48,7 @@ class SideMenu extends Component<RouteComponentProps, IState> {
|
||||
});
|
||||
};
|
||||
|
||||
getTenantOption = (id: string, fn: OptionCallbackFunc) => {
|
||||
const getTenantOption = (id: string, fn: OptionCallbackFunc) => {
|
||||
TenantStore.get(id, (resp: GetTenantResponse) => {
|
||||
const tenant = resp.getTenant();
|
||||
if (tenant) {
|
||||
@ -85,167 +57,208 @@ class SideMenu extends Component<RouteComponentProps, IState> {
|
||||
});
|
||||
};
|
||||
|
||||
onTenantSelect = (value: string) => {
|
||||
const onTenantSelect = (value: string) => {
|
||||
SessionStore.setTenantId(value);
|
||||
this.props.history.push(`/tenants/${value}`);
|
||||
navigate(`/tenants/${value}`);
|
||||
};
|
||||
|
||||
parseLocation = () => {
|
||||
const path = this.props.history.location.pathname;
|
||||
const parseLocation = () => {
|
||||
const path = location.pathname;
|
||||
const tenantRe = /\/tenants\/([\w-]{36})/g;
|
||||
const match = tenantRe.exec(path);
|
||||
|
||||
if (match !== null && this.state.tenantId !== match[1]) {
|
||||
if (match !== null && tenantId !== match[1]) {
|
||||
SessionStore.setTenantId(match[1]);
|
||||
}
|
||||
|
||||
// ns dashboard
|
||||
if (path === "/dashboard") {
|
||||
this.setState({ selectedKey: "ns-dashboard" });
|
||||
setSelectedKey("ns-dashboard");
|
||||
}
|
||||
|
||||
// ns tenants
|
||||
if (/\/tenants(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
|
||||
this.setState({ selectedKey: "ns-tenants" });
|
||||
setSelectedKey("ns-tenants");
|
||||
}
|
||||
|
||||
// ns tenants
|
||||
if (/\/users(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
|
||||
this.setState({ selectedKey: "ns-users" });
|
||||
setSelectedKey("ns-users");
|
||||
}
|
||||
|
||||
// ns api keys
|
||||
if (/\/api-keys(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
|
||||
this.setState({ selectedKey: "ns-api-keys" });
|
||||
setSelectedKey("ns-api-keys");
|
||||
}
|
||||
|
||||
// ns device-profile templates
|
||||
if (/\/device-profile-templates(\/([\w-]{36}\/edit|create))?/g.exec(path)) {
|
||||
this.setState({ selectedKey: "ns-device-profile-templates" });
|
||||
setSelectedKey("ns-device-profile-templates");
|
||||
}
|
||||
|
||||
if (/\/regions.*/g.exec(path)) {
|
||||
this.setState({ selectedKey: "ns-regions" });
|
||||
setSelectedKey("ns-regions");
|
||||
}
|
||||
|
||||
// tenant dashboard
|
||||
if (/\/tenants\/[\w-]{36}/g.exec(path)) {
|
||||
this.setState({ selectedKey: "tenant-dashboard" });
|
||||
setSelectedKey("tenant-dashboard");
|
||||
}
|
||||
|
||||
// tenant users
|
||||
if (/\/tenants\/[\w-]{36}\/users.*/g.exec(path)) {
|
||||
this.setState({ selectedKey: "tenant-users" });
|
||||
setSelectedKey("tenant-users");
|
||||
}
|
||||
|
||||
// tenant api-keys
|
||||
if (/\/tenants\/[\w-]{36}\/api-keys.*/g.exec(path)) {
|
||||
this.setState({ selectedKey: "tenant-api-keys" });
|
||||
setSelectedKey("tenant-api-keys");
|
||||
}
|
||||
|
||||
// tenant device-profiles
|
||||
if (/\/tenants\/[\w-]{36}\/device-profiles.*/g.exec(path)) {
|
||||
this.setState({ selectedKey: "tenant-device-profiles" });
|
||||
setSelectedKey("tenant-device-profiles");
|
||||
}
|
||||
|
||||
// tenant gateways
|
||||
if (/\/tenants\/[\w-]{36}\/gateways.*/g.exec(path)) {
|
||||
this.setState({ selectedKey: "tenant-gateways" });
|
||||
setSelectedKey("tenant-gateways");
|
||||
}
|
||||
|
||||
// tenant applications
|
||||
if (/\/tenants\/[\w-]{36}\/applications.*/g.exec(path)) {
|
||||
this.setState({ selectedKey: "tenant-applications" });
|
||||
setSelectedKey("tenant-applications");
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const tenantId = this.state.tenantId;
|
||||
let items: MenuProps["items"] = [];
|
||||
useEffect(() => {
|
||||
SessionStore.on("tenant.change", setTenant);
|
||||
setTenant();
|
||||
parseLocation();
|
||||
|
||||
if (SessionStore.isAdmin()) {
|
||||
items.push({
|
||||
key: "ns",
|
||||
label: "Network Server",
|
||||
icon: <CloudOutlined />,
|
||||
children: [
|
||||
{ key: "ns-dashboard", icon: <DashboardOutlined />, label: <Link to="/dashboard">Dashboard</Link> },
|
||||
{ key: "ns-tenants", icon: <HomeOutlined />, label: <Link to="/tenants">Tenants</Link> },
|
||||
{ key: "ns-users", icon: <UserOutlined />, label: <Link to="/users">Users</Link> },
|
||||
{ key: "ns-api-keys", icon: <KeyOutlined />, label: <Link to="/api-keys">API Keys</Link> },
|
||||
{
|
||||
key: "ns-device-profile-templates",
|
||||
icon: <ControlOutlined />,
|
||||
label: <Link to="/device-profile-templates">Device Profile Templates</Link>,
|
||||
},
|
||||
{ key: "ns-regions", icon: <CompassOutlined />, label: <Link to="/regions">Regions</Link> },
|
||||
],
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
key: "ns",
|
||||
label: "Network Server",
|
||||
icon: <CloudOutlined />,
|
||||
children: [{ key: "ns-regions", icon: <CompassOutlined />, label: <Link to="/regions">Regions</Link> }],
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
SessionStore.removeListener("tenant.change", setTenant);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (tenantId !== "") {
|
||||
items.push({
|
||||
key: "tenant",
|
||||
label: "Tenant",
|
||||
icon: <HomeOutlined />,
|
||||
children: [
|
||||
{
|
||||
key: "tenant-dashboard",
|
||||
icon: <DashboardOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}`}>Dashboard</Link>,
|
||||
},
|
||||
{ key: "tenant-users", icon: <UserOutlined />, label: <Link to={`/tenants/${tenantId}/users`}>Users</Link> },
|
||||
{
|
||||
key: "tenant-api-keys",
|
||||
icon: <KeyOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}/api-keys`}>API Keys</Link>,
|
||||
},
|
||||
{
|
||||
key: "tenant-device-profiles",
|
||||
icon: <ControlOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}/device-profiles`}>Device Profiles</Link>,
|
||||
},
|
||||
{
|
||||
key: "tenant-gateways",
|
||||
icon: <WifiOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}/gateways`}>Gateways</Link>,
|
||||
},
|
||||
{
|
||||
key: "tenant-applications",
|
||||
icon: <AppstoreOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}/applications`}>Applications</Link>,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
useEffect(() => {
|
||||
parseLocation();
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Autocomplete
|
||||
placeholder="Select tenant"
|
||||
className="organiation-select"
|
||||
getOption={this.getTenantOption}
|
||||
getOptions={this.getTenantOptions}
|
||||
onSelect={this.onTenantSelect}
|
||||
value={this.state.tenantId}
|
||||
/>
|
||||
<Menu
|
||||
mode="inline"
|
||||
openKeys={["ns", "tenant"]}
|
||||
selectedKeys={[this.state.selectedKey]}
|
||||
expandIcon={<div></div>}
|
||||
items={items}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
let items: MenuProps["items"] = [];
|
||||
|
||||
if (SessionStore.isAdmin()) {
|
||||
items.push({
|
||||
key: "ns",
|
||||
label: "Network Server",
|
||||
icon: <CloudOutlined />,
|
||||
children: [
|
||||
{
|
||||
key: "ns-dashboard",
|
||||
icon: <DashboardOutlined />,
|
||||
label: <Link to="/dashboard">Dashboard</Link>,
|
||||
},
|
||||
{
|
||||
key: "ns-tenants",
|
||||
icon: <HomeOutlined />,
|
||||
label: <Link to="/tenants">Tenants</Link>,
|
||||
},
|
||||
{
|
||||
key: "ns-users",
|
||||
icon: <UserOutlined />,
|
||||
label: <Link to="/users">Users</Link>,
|
||||
},
|
||||
{
|
||||
key: "ns-api-keys",
|
||||
icon: <KeyOutlined />,
|
||||
label: <Link to="/api-keys">API Keys</Link>,
|
||||
},
|
||||
{
|
||||
key: "ns-device-profile-templates",
|
||||
icon: <ControlOutlined />,
|
||||
label: <Link to="/device-profile-templates">Device Profile Templates</Link>,
|
||||
},
|
||||
{
|
||||
key: "ns-regions",
|
||||
icon: <CompassOutlined />,
|
||||
label: <Link to="/regions">Regions</Link>,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
key: "ns",
|
||||
label: "Network Server",
|
||||
icon: <CloudOutlined />,
|
||||
children: [
|
||||
{
|
||||
key: "ns-regions",
|
||||
icon: <CompassOutlined />,
|
||||
label: <Link to="/regions">Regions</Link>,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (tenantId !== "") {
|
||||
items.push({
|
||||
key: "tenant",
|
||||
label: "Tenant",
|
||||
icon: <HomeOutlined />,
|
||||
children: [
|
||||
{
|
||||
key: "tenant-dashboard",
|
||||
icon: <DashboardOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}`}>Dashboard</Link>,
|
||||
},
|
||||
{
|
||||
key: "tenant-users",
|
||||
icon: <UserOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}/users`}>Users</Link>,
|
||||
},
|
||||
{
|
||||
key: "tenant-api-keys",
|
||||
icon: <KeyOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}/api-keys`}>API Keys</Link>,
|
||||
},
|
||||
{
|
||||
key: "tenant-device-profiles",
|
||||
icon: <ControlOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}/device-profiles`}>Device Profiles</Link>,
|
||||
},
|
||||
{
|
||||
key: "tenant-gateways",
|
||||
icon: <WifiOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}/gateways`}>Gateways</Link>,
|
||||
},
|
||||
{
|
||||
key: "tenant-applications",
|
||||
icon: <AppstoreOutlined />,
|
||||
label: <Link to={`/tenants/${tenantId}/applications`}>Applications</Link>,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Autocomplete
|
||||
placeholder="Select tenant"
|
||||
className="organiation-select"
|
||||
getOption={getTenantOption}
|
||||
getOptions={getTenantOptions}
|
||||
onSelect={onTenantSelect}
|
||||
value={tenantId}
|
||||
/>
|
||||
<Menu
|
||||
mode="inline"
|
||||
openKeys={["ns", "tenant"]}
|
||||
selectedKeys={[selectedKey]}
|
||||
expandIcon={<div></div>}
|
||||
items={items}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(SideMenu);
|
||||
export default SideMenu;
|
||||
|
@ -1,5 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
import { TimeUnit } from "chart.js";
|
||||
@ -13,82 +11,80 @@ interface IProps {
|
||||
aggregation: Aggregation;
|
||||
}
|
||||
|
||||
class MetricBar extends Component<IProps> {
|
||||
render() {
|
||||
let unit: TimeUnit = "hour";
|
||||
if (this.props.aggregation === Aggregation.DAY) {
|
||||
unit = "day";
|
||||
} else if (this.props.aggregation === Aggregation.MONTH) {
|
||||
unit = "month";
|
||||
}
|
||||
|
||||
let backgroundColors = [
|
||||
"#8bc34a",
|
||||
"#ff5722",
|
||||
"#ff9800",
|
||||
"#ffc107",
|
||||
"#ffeb3b",
|
||||
"#cddc39",
|
||||
"#4caf50",
|
||||
"#009688",
|
||||
"#00bcd4",
|
||||
"#03a9f4",
|
||||
"#2196f3",
|
||||
"#3f51b5",
|
||||
"#673ab7",
|
||||
"#9c27b0",
|
||||
"#e91e63",
|
||||
];
|
||||
|
||||
const animation: false = false;
|
||||
|
||||
const options = {
|
||||
animation: animation,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
},
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
x: {
|
||||
type: "time" as const,
|
||||
time: {
|
||||
unit: unit,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let data: {
|
||||
labels: number[];
|
||||
datasets: {
|
||||
label: string;
|
||||
data: number[];
|
||||
backgroundColor: string;
|
||||
}[];
|
||||
} = {
|
||||
labels: this.props.metric.getTimestampsList().map(v => moment(v.toDate()).valueOf()),
|
||||
datasets: [],
|
||||
};
|
||||
|
||||
for (let ds of this.props.metric.getDatasetsList()) {
|
||||
data.datasets.push({
|
||||
label: ds.getLabel(),
|
||||
data: ds.getDataList(),
|
||||
backgroundColor: backgroundColors.shift()!,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={this.props.metric.getName()} className="dashboard-chart">
|
||||
<Bar data={data} options={options} />
|
||||
</Card>
|
||||
);
|
||||
function MetricBar(props: IProps) {
|
||||
let unit: TimeUnit = "hour";
|
||||
if (props.aggregation === Aggregation.DAY) {
|
||||
unit = "day";
|
||||
} else if (props.aggregation === Aggregation.MONTH) {
|
||||
unit = "month";
|
||||
}
|
||||
|
||||
let backgroundColors = [
|
||||
"#8bc34a",
|
||||
"#ff5722",
|
||||
"#ff9800",
|
||||
"#ffc107",
|
||||
"#ffeb3b",
|
||||
"#cddc39",
|
||||
"#4caf50",
|
||||
"#009688",
|
||||
"#00bcd4",
|
||||
"#03a9f4",
|
||||
"#2196f3",
|
||||
"#3f51b5",
|
||||
"#673ab7",
|
||||
"#9c27b0",
|
||||
"#e91e63",
|
||||
];
|
||||
|
||||
const animation: false = false;
|
||||
|
||||
const options = {
|
||||
animation: animation,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
},
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
x: {
|
||||
type: "time" as const,
|
||||
time: {
|
||||
unit: unit,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let data: {
|
||||
labels: number[];
|
||||
datasets: {
|
||||
label: string;
|
||||
data: number[];
|
||||
backgroundColor: string;
|
||||
}[];
|
||||
} = {
|
||||
labels: props.metric.getTimestampsList().map(v => moment(v.toDate()).valueOf()),
|
||||
datasets: [],
|
||||
};
|
||||
|
||||
for (let ds of props.metric.getDatasetsList()) {
|
||||
data.datasets.push({
|
||||
label: ds.getLabel(),
|
||||
data: ds.getDataList(),
|
||||
backgroundColor: backgroundColors.shift()!,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={props.metric.getName()} className="dashboard-chart">
|
||||
<Bar data={data} options={options} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricBar;
|
||||
|
@ -1,5 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
import { TimeUnit } from "chart.js";
|
||||
@ -14,83 +12,81 @@ interface IProps {
|
||||
zeroToNull?: boolean;
|
||||
}
|
||||
|
||||
class MetricChart extends Component<IProps> {
|
||||
render() {
|
||||
let unit: TimeUnit = "hour";
|
||||
let tooltipFormat = "LT";
|
||||
if (this.props.aggregation === Aggregation.DAY) {
|
||||
unit = "day";
|
||||
tooltipFormat = "MMM Do";
|
||||
} else if (this.props.aggregation === Aggregation.MONTH) {
|
||||
unit = "month";
|
||||
tooltipFormat = "MMM YYYY";
|
||||
}
|
||||
|
||||
const animation: false = false;
|
||||
|
||||
const options = {
|
||||
animation: animation,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
x: {
|
||||
type: "time" as const,
|
||||
time: {
|
||||
unit: unit,
|
||||
tooltipFormat: tooltipFormat,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let prevValue = 0;
|
||||
let data = {
|
||||
labels: this.props.metric.getTimestampsList().map(v => moment(v.toDate()).valueOf()),
|
||||
datasets: this.props.metric.getDatasetsList().map(v => {
|
||||
return {
|
||||
label: v.getLabel(),
|
||||
borderColor: "rgba(33, 150, 243, 1)",
|
||||
backgroundColor: "rgba(0, 0, 0, 0)",
|
||||
lineTension: 0,
|
||||
pointBackgroundColor: "rgba(33, 150, 243, 1)",
|
||||
data: v.getDataList().map(v => {
|
||||
if (v === 0 && this.props.zeroToNull) {
|
||||
return null;
|
||||
} else {
|
||||
if (this.props.metric.getKind() === MetricKind.COUNTER) {
|
||||
let val = v - prevValue;
|
||||
prevValue = v;
|
||||
if (val < 0) {
|
||||
return 0;
|
||||
}
|
||||
return val;
|
||||
} else {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
let name = this.props.metric.getName();
|
||||
if (this.props.metric.getKind() === MetricKind.COUNTER) {
|
||||
name = `${name} (per ${unit})`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={name} className="dashboard-chart">
|
||||
<Line height={75} options={options} data={data} />
|
||||
</Card>
|
||||
);
|
||||
function MetricChart(props: IProps) {
|
||||
let unit: TimeUnit = "hour";
|
||||
let tooltipFormat = "LT";
|
||||
if (props.aggregation === Aggregation.DAY) {
|
||||
unit = "day";
|
||||
tooltipFormat = "MMM Do";
|
||||
} else if (props.aggregation === Aggregation.MONTH) {
|
||||
unit = "month";
|
||||
tooltipFormat = "MMM YYYY";
|
||||
}
|
||||
|
||||
const animation: false = false;
|
||||
|
||||
const options = {
|
||||
animation: animation,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
x: {
|
||||
type: "time" as const,
|
||||
time: {
|
||||
unit: unit,
|
||||
tooltipFormat: tooltipFormat,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let prevValue = 0;
|
||||
let data = {
|
||||
labels: props.metric.getTimestampsList().map(v => moment(v.toDate()).valueOf()),
|
||||
datasets: props.metric.getDatasetsList().map(v => {
|
||||
return {
|
||||
label: v.getLabel(),
|
||||
borderColor: "rgba(33, 150, 243, 1)",
|
||||
backgroundColor: "rgba(0, 0, 0, 0)",
|
||||
lineTension: 0,
|
||||
pointBackgroundColor: "rgba(33, 150, 243, 1)",
|
||||
data: v.getDataList().map(v => {
|
||||
if (v === 0 && props.zeroToNull) {
|
||||
return null;
|
||||
} else {
|
||||
if (props.metric.getKind() === MetricKind.COUNTER) {
|
||||
let val = v - prevValue;
|
||||
prevValue = v;
|
||||
if (val < 0) {
|
||||
return 0;
|
||||
}
|
||||
return val;
|
||||
} else {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
let name = props.metric.getName();
|
||||
if (props.metric.getKind() === MetricKind.COUNTER) {
|
||||
name = `${name} (per ${unit})`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={name} className="dashboard-chart">
|
||||
<Line height={75} options={options} data={data} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricChart;
|
||||
|
@ -1,5 +1,3 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Card } from "antd";
|
||||
|
||||
import { color } from "chart.js/helpers";
|
||||
@ -16,138 +14,136 @@ interface IProps {
|
||||
aggregation: Aggregation;
|
||||
}
|
||||
|
||||
class MetricHeatmap extends Component<IProps> {
|
||||
render() {
|
||||
let unit: TimeUnit = "hour";
|
||||
if (this.props.aggregation === Aggregation.DAY) {
|
||||
unit = "day";
|
||||
} else if (this.props.aggregation === Aggregation.MONTH) {
|
||||
unit = "month";
|
||||
}
|
||||
function MetricHeatmap(props: IProps) {
|
||||
let unit: TimeUnit = "hour";
|
||||
if (props.aggregation === Aggregation.DAY) {
|
||||
unit = "day";
|
||||
} else if (props.aggregation === Aggregation.MONTH) {
|
||||
unit = "month";
|
||||
}
|
||||
|
||||
const animation: false = false;
|
||||
const animation: false = false;
|
||||
|
||||
let options = {
|
||||
animation: animation,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
type: "category" as const,
|
||||
offset: true,
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
let options = {
|
||||
animation: animation,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
type: "category" as const,
|
||||
offset: true,
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
x: {
|
||||
type: "time" as const,
|
||||
time: {
|
||||
unit: unit,
|
||||
},
|
||||
x: {
|
||||
type: "time" as const,
|
||||
time: {
|
||||
unit: unit,
|
||||
},
|
||||
offset: true,
|
||||
labels: props.metric.getTimestampsList().map(v => moment(v.toDate().valueOf())),
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title: () => {
|
||||
return "";
|
||||
},
|
||||
offset: true,
|
||||
labels: this.props.metric.getTimestampsList().map(v => moment(v.toDate().valueOf())),
|
||||
grid: {
|
||||
display: false,
|
||||
label: (ctx: any) => {
|
||||
const v = ctx.dataset.data[ctx.dataIndex].v;
|
||||
return "Count: " + v;
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title: () => {
|
||||
return "";
|
||||
},
|
||||
label: (ctx: any) => {
|
||||
const v = ctx.dataset.data[ctx.dataIndex].v;
|
||||
return "Count: " + v;
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let dataData: {
|
||||
x: number;
|
||||
y: string;
|
||||
v: number;
|
||||
}[] = [];
|
||||
|
||||
let data = {
|
||||
labels: props.metric.getDatasetsList().map(v => v.getLabel()),
|
||||
datasets: [
|
||||
{
|
||||
label: "Heatmap",
|
||||
data: dataData,
|
||||
minValue: -1,
|
||||
maxValue: -1,
|
||||
fromColor: props.fromColor.match(/\d+/g)!.map(Number),
|
||||
toColor: props.toColor.match(/\d+/g)!.map(Number),
|
||||
backgroundColor: (ctx: any): string => {
|
||||
if (
|
||||
ctx.dataset === undefined ||
|
||||
ctx.dataset.data === undefined ||
|
||||
ctx.dataset.data[ctx.dataIndex] === undefined
|
||||
) {
|
||||
return color("white").rgbString();
|
||||
}
|
||||
|
||||
const value = ctx.dataset.data[ctx.dataIndex].v;
|
||||
const steps = ctx.dataset.maxValue - ctx.dataset.minValue + 1;
|
||||
const step = value - ctx.dataset.minValue;
|
||||
const factor = (1 / steps) * step;
|
||||
|
||||
let result: [number, number, number] = ctx.dataset.fromColor.slice();
|
||||
for (var i = 0; i < 3; i++) {
|
||||
result[i] = Math.round(result[i] + factor * (ctx.dataset.toColor[i] - ctx.dataset.fromColor[i]));
|
||||
}
|
||||
|
||||
return color(result).rgbString();
|
||||
},
|
||||
borderWidth: 0,
|
||||
width: (ctx: any) => {
|
||||
return (ctx.chart.chartArea || {}).width / props.metric.getTimestampsList().length - 1;
|
||||
},
|
||||
height: (ctx: any) => {
|
||||
return (ctx.chart.chartArea || {}).height / props.metric.getDatasetsList().length - 1;
|
||||
},
|
||||
},
|
||||
};
|
||||
],
|
||||
};
|
||||
|
||||
let dataData: {
|
||||
x: number;
|
||||
y: string;
|
||||
v: number;
|
||||
}[] = [];
|
||||
data.labels.sort();
|
||||
|
||||
let data = {
|
||||
labels: this.props.metric.getDatasetsList().map(v => v.getLabel()),
|
||||
datasets: [
|
||||
{
|
||||
label: "Heatmap",
|
||||
data: dataData,
|
||||
minValue: -1,
|
||||
maxValue: -1,
|
||||
fromColor: this.props.fromColor.match(/\d+/g)!.map(Number),
|
||||
toColor: this.props.toColor.match(/\d+/g)!.map(Number),
|
||||
backgroundColor: (ctx: any): string => {
|
||||
if (
|
||||
ctx.dataset === undefined ||
|
||||
ctx.dataset.data === undefined ||
|
||||
ctx.dataset.data[ctx.dataIndex] === undefined
|
||||
) {
|
||||
return color("white").rgbString();
|
||||
}
|
||||
const tsList = props.metric.getTimestampsList();
|
||||
const dsList = props.metric.getDatasetsList();
|
||||
|
||||
const value = ctx.dataset.data[ctx.dataIndex].v;
|
||||
const steps = ctx.dataset.maxValue - ctx.dataset.minValue + 1;
|
||||
const step = value - ctx.dataset.minValue;
|
||||
const factor = (1 / steps) * step;
|
||||
for (let i = 0; i < tsList.length; i++) {
|
||||
for (let ds of dsList) {
|
||||
let v = ds.getDataList()[i];
|
||||
if (v === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let result: [number, number, number] = ctx.dataset.fromColor.slice();
|
||||
for (var i = 0; i < 3; i++) {
|
||||
result[i] = Math.round(result[i] + factor * (ctx.dataset.toColor[i] - ctx.dataset.fromColor[i]));
|
||||
}
|
||||
data.datasets[0].data.push({
|
||||
x: moment(tsList[i].toDate()).valueOf(),
|
||||
y: ds.getLabel(),
|
||||
v: v,
|
||||
});
|
||||
|
||||
return color(result).rgbString();
|
||||
},
|
||||
borderWidth: 0,
|
||||
width: (ctx: any) => {
|
||||
return (ctx.chart.chartArea || {}).width / this.props.metric.getTimestampsList().length - 1;
|
||||
},
|
||||
height: (ctx: any) => {
|
||||
return (ctx.chart.chartArea || {}).height / this.props.metric.getDatasetsList().length - 1;
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
if (data.datasets[0].minValue === -1 || data.datasets[0].minValue > v) {
|
||||
data.datasets[0].minValue = v;
|
||||
}
|
||||
|
||||
data.labels.sort();
|
||||
|
||||
const tsList = this.props.metric.getTimestampsList();
|
||||
const dsList = this.props.metric.getDatasetsList();
|
||||
|
||||
for (let i = 0; i < tsList.length; i++) {
|
||||
for (let ds of dsList) {
|
||||
let v = ds.getDataList()[i];
|
||||
if (v === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
data.datasets[0].data.push({
|
||||
x: moment(tsList[i].toDate()).valueOf(),
|
||||
y: ds.getLabel(),
|
||||
v: v,
|
||||
});
|
||||
|
||||
if (data.datasets[0].minValue === -1 || data.datasets[0].minValue > v) {
|
||||
data.datasets[0].minValue = v;
|
||||
}
|
||||
|
||||
if (data.datasets[0].maxValue < v) {
|
||||
data.datasets[0].maxValue = v;
|
||||
}
|
||||
if (data.datasets[0].maxValue < v) {
|
||||
data.datasets[0].maxValue = v;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={this.props.metric.getName()} className="dashboard-chart">
|
||||
<Chart type="matrix" data={data} options={options} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={props.metric.getName()} className="dashboard-chart">
|
||||
<Chart type="matrix" data={data} options={options} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricHeatmap;
|
||||
|
Reference in New Issue
Block a user