mirror of
https://github.com/chirpstack/chirpstack.git
synced 2025-06-06 09:41:35 +00:00
ui: Implement queue-item expires-at timestamp in UI.
This commit is contained in:
parent
3829f591e4
commit
4d7a4b22e1
@ -17,6 +17,9 @@ import type {
|
|||||||
RemoveGatewayFromMulticastGroupRequest,
|
RemoveGatewayFromMulticastGroupRequest,
|
||||||
ListMulticastGroupQueueRequest,
|
ListMulticastGroupQueueRequest,
|
||||||
ListMulticastGroupQueueResponse,
|
ListMulticastGroupQueueResponse,
|
||||||
|
EnqueueMulticastGroupQueueItemRequest,
|
||||||
|
EnqueueMulticastGroupQueueItemResponse,
|
||||||
|
FlushMulticastGroupQueueRequest,
|
||||||
} from "@chirpstack/chirpstack-api-grpc-web/api/multicast_group_pb";
|
} from "@chirpstack/chirpstack-api-grpc-web/api/multicast_group_pb";
|
||||||
|
|
||||||
import SessionStore from "./SessionStore";
|
import SessionStore from "./SessionStore";
|
||||||
@ -154,6 +157,20 @@ class MulticastGroupStore extends EventEmitter {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enqueue = (
|
||||||
|
req: EnqueueMulticastGroupQueueItemRequest,
|
||||||
|
callbackFunc: (resp: EnqueueMulticastGroupQueueItemResponse) => void,
|
||||||
|
) => {
|
||||||
|
this.client.enqueue(req, SessionStore.getMetadata(), (err, resp) => {
|
||||||
|
if (err !== null) {
|
||||||
|
HandleError(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callbackFunc(resp);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
listQueue = (req: ListMulticastGroupQueueRequest, callbackFunc: (resp: ListMulticastGroupQueueResponse) => void) => {
|
listQueue = (req: ListMulticastGroupQueueRequest, callbackFunc: (resp: ListMulticastGroupQueueResponse) => void) => {
|
||||||
this.client.listQueue(req, SessionStore.getMetadata(), (err, resp) => {
|
this.client.listQueue(req, SessionStore.getMetadata(), (err, resp) => {
|
||||||
if (err !== null) {
|
if (err !== null) {
|
||||||
@ -164,6 +181,17 @@ class MulticastGroupStore extends EventEmitter {
|
|||||||
callbackFunc(resp);
|
callbackFunc(resp);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
flushQueue = (req: FlushMulticastGroupQueueRequest, callbackFunc: () => void) => {
|
||||||
|
this.client.flushQueue(req, SessionStore.getMetadata(), err => {
|
||||||
|
if (err !== null) {
|
||||||
|
HandleError(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callbackFunc();
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const multicastGroupStore = new MulticastGroupStore();
|
const multicastGroupStore = new MulticastGroupStore();
|
||||||
|
@ -1,17 +1,32 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { Struct } from "google-protobuf/google/protobuf/struct_pb";
|
import { Struct } from "google-protobuf/google/protobuf/struct_pb";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb";
|
||||||
|
|
||||||
import { Switch, notification } from "antd";
|
import { Switch, notification } from "antd";
|
||||||
import { Button, Tabs, Space, Card, Row, Form, Input, InputNumber, Popconfirm } from "antd";
|
import {
|
||||||
|
Button,
|
||||||
|
Tabs,
|
||||||
|
Space,
|
||||||
|
Card,
|
||||||
|
Row,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
Popconfirm,
|
||||||
|
DatePicker,
|
||||||
|
DatePickerProps,
|
||||||
|
} from "antd";
|
||||||
import type { ColumnsType } from "antd/es/table";
|
import type { ColumnsType } from "antd/es/table";
|
||||||
import { RedoOutlined, DeleteOutlined } from "@ant-design/icons";
|
import { RedoOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||||
import { Buffer } from "buffer";
|
import { Buffer } from "buffer";
|
||||||
|
|
||||||
import type { Device, GetDeviceQueueItemsResponse } from "@chirpstack/chirpstack-api-grpc-web/api/device_pb";
|
|
||||||
import {
|
import {
|
||||||
EnqueueDeviceQueueItemRequest,
|
EnqueueDeviceQueueItemRequest,
|
||||||
GetDeviceQueueItemsRequest,
|
GetDeviceQueueItemsRequest,
|
||||||
|
GetDeviceQueueItemsResponse,
|
||||||
|
Device,
|
||||||
FlushDeviceQueueRequest,
|
FlushDeviceQueueRequest,
|
||||||
DeviceQueueItem,
|
DeviceQueueItem,
|
||||||
} from "@chirpstack/chirpstack-api-grpc-web/api/device_pb";
|
} from "@chirpstack/chirpstack-api-grpc-web/api/device_pb";
|
||||||
@ -34,6 +49,7 @@ interface FormRules {
|
|||||||
hex: string;
|
hex: string;
|
||||||
base64: string;
|
base64: string;
|
||||||
json: string;
|
json: string;
|
||||||
|
expiresAt?: DatePickerProps["value"];
|
||||||
}
|
}
|
||||||
|
|
||||||
function DeviceQueue(props: IProps) {
|
function DeviceQueue(props: IProps) {
|
||||||
@ -114,6 +130,21 @@ function DeviceQueue(props: IProps) {
|
|||||||
return Buffer.from(record.data as string, "base64").toString("hex");
|
return Buffer.from(record.data as string, "base64").toString("hex");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Expires at",
|
||||||
|
dataIndex: "expiresAt",
|
||||||
|
key: "expiresAt",
|
||||||
|
width: 250,
|
||||||
|
render: (_text, record) => {
|
||||||
|
if (record.expiresAt !== undefined) {
|
||||||
|
const ts = new Date(0);
|
||||||
|
ts.setUTCSeconds(record.expiresAt.seconds);
|
||||||
|
return format(ts, "yyyy-MM-dd HH:mm:ss");
|
||||||
|
} else {
|
||||||
|
return "Never";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||||
@ -148,6 +179,10 @@ function DeviceQueue(props: IProps) {
|
|||||||
item.setIsEncrypted(values.isEncrypted);
|
item.setIsEncrypted(values.isEncrypted);
|
||||||
item.setFCntDown(values.fCntDown);
|
item.setFCntDown(values.fCntDown);
|
||||||
|
|
||||||
|
if (values.expiresAt !== null && values.expiresAt !== undefined) {
|
||||||
|
item.setExpiresAt(Timestamp.fromDate(values.expiresAt.toDate()));
|
||||||
|
}
|
||||||
|
|
||||||
if (values.hex !== undefined) {
|
if (values.hex !== undefined) {
|
||||||
item.setData(new Uint8Array(Buffer.from(values.hex, "hex")));
|
item.setData(new Uint8Array(Buffer.from(values.hex, "hex")));
|
||||||
}
|
}
|
||||||
@ -217,6 +252,13 @@ function DeviceQueue(props: IProps) {
|
|||||||
<InputNumber min={0} />
|
<InputNumber min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
<Form.Item
|
||||||
|
name="expiresAt"
|
||||||
|
label="Expires at"
|
||||||
|
tooltip="If set, the queue-item will automatically expire at the given timestamp if it wasn't sent yet."
|
||||||
|
>
|
||||||
|
<DatePicker showTime />
|
||||||
|
</Form.Item>
|
||||||
</Space>
|
</Space>
|
||||||
</Row>
|
</Row>
|
||||||
<Tabs defaultActiveKey="1">
|
<Tabs defaultActiveKey="1">
|
||||||
|
@ -21,6 +21,7 @@ import ListMulticastGroupDevices from "./ListMulticastGroupDevices";
|
|||||||
import ListMulticastGroupGateways from "./ListMulticastGroupGateways";
|
import ListMulticastGroupGateways from "./ListMulticastGroupGateways";
|
||||||
import EditMulticastGroup from "./EditMulticastGroup";
|
import EditMulticastGroup from "./EditMulticastGroup";
|
||||||
import Admin from "../../components/Admin";
|
import Admin from "../../components/Admin";
|
||||||
|
import MulticastGroupQueue from "./MulticastGroupQueue";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
tenant: Tenant;
|
tenant: Tenant;
|
||||||
@ -68,6 +69,9 @@ function MulticastGroupLayout(props: IProps) {
|
|||||||
if (path.endsWith("edit")) {
|
if (path.endsWith("edit")) {
|
||||||
tab = "edit";
|
tab = "edit";
|
||||||
}
|
}
|
||||||
|
if (path.endsWith("queue")) {
|
||||||
|
tab = "queue";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
@ -131,11 +135,17 @@ function MulticastGroupLayout(props: IProps) {
|
|||||||
Configuration
|
Configuration
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item key="queue">
|
||||||
|
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/multicast-groups/${mg.getId()}/queue`}>
|
||||||
|
Queue
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<ListMulticastGroupDevices multicastGroup={mg} />} />
|
<Route path="/" element={<ListMulticastGroupDevices multicastGroup={mg} />} />
|
||||||
<Route path="/gateways" element={<ListMulticastGroupGateways multicastGroup={mg} application={app} />} />
|
<Route path="/gateways" element={<ListMulticastGroupGateways multicastGroup={mg} application={app} />} />
|
||||||
<Route path="/edit" element={<EditMulticastGroup application={app} multicastGroup={mg} />} />
|
<Route path="/edit" element={<EditMulticastGroup application={app} multicastGroup={mg} />} />
|
||||||
|
<Route path="/queue" element={<MulticastGroupQueue multicastGroup={mg} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Card>
|
</Card>
|
||||||
</Space>
|
</Space>
|
||||||
|
195
ui/src/views/multicast-groups/MulticastGroupQueue.tsx
Normal file
195
ui/src/views/multicast-groups/MulticastGroupQueue.tsx
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Tabs,
|
||||||
|
Space,
|
||||||
|
Card,
|
||||||
|
Row,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
Popconfirm,
|
||||||
|
DatePicker,
|
||||||
|
DatePickerProps,
|
||||||
|
} from "antd";
|
||||||
|
import type { ColumnsType } from "antd/es/table";
|
||||||
|
import { RedoOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||||
|
import { Buffer } from "buffer";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EnqueueMulticastGroupQueueItemRequest,
|
||||||
|
ListMulticastGroupQueueRequest,
|
||||||
|
FlushMulticastGroupQueueRequest,
|
||||||
|
MulticastGroupQueueItem,
|
||||||
|
MulticastGroup,
|
||||||
|
ListMulticastGroupQueueResponse,
|
||||||
|
} from "@chirpstack/chirpstack-api-grpc-web/api/multicast_group_pb";
|
||||||
|
|
||||||
|
import { onFinishFailed } from "../helpers";
|
||||||
|
import type { GetPageCallbackFunc } from "../../components/DataTable";
|
||||||
|
import DataTable from "../../components/DataTable";
|
||||||
|
import MulticastGroupStore from "../../stores/MulticastGroupStore";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
multicastGroup: MulticastGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormRules {
|
||||||
|
fPort: number;
|
||||||
|
hex: string;
|
||||||
|
base64: string;
|
||||||
|
expiresAt?: DatePickerProps["value"];
|
||||||
|
}
|
||||||
|
|
||||||
|
function MulticastGroupQueue(props: IProps) {
|
||||||
|
const [refreshCounter, setRefreshCounter] = useState<number>(0);
|
||||||
|
const [form] = Form.useForm<FormRules>();
|
||||||
|
|
||||||
|
const columns: ColumnsType<MulticastGroupQueueItem.AsObject> = [
|
||||||
|
{
|
||||||
|
title: "Frame-counter",
|
||||||
|
dataIndex: "fCnt",
|
||||||
|
key: "fCnt",
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "FPort",
|
||||||
|
dataIndex: "fPort",
|
||||||
|
key: "fPort",
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Data (HEX)",
|
||||||
|
dataIndex: "data",
|
||||||
|
key: "data",
|
||||||
|
render: (text, record) => {
|
||||||
|
return Buffer.from(record.data as string, "base64").toString("hex");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Expires at",
|
||||||
|
dataIndex: "expiresAt",
|
||||||
|
key: "expiresAt",
|
||||||
|
width: 250,
|
||||||
|
render: (_text, record) => {
|
||||||
|
if (record.expiresAt !== undefined) {
|
||||||
|
const ts = new Date(0);
|
||||||
|
ts.setUTCSeconds(record.expiresAt.seconds);
|
||||||
|
return format(ts, "yyyy-MM-dd HH:mm:ss");
|
||||||
|
} else {
|
||||||
|
return "Never";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const getPage = (limit: number, offset: number, callbackFunc: GetPageCallbackFunc) => {
|
||||||
|
const req = new ListMulticastGroupQueueRequest();
|
||||||
|
req.setMulticastGroupId(props.multicastGroup.getId());
|
||||||
|
|
||||||
|
MulticastGroupStore.listQueue(req, (resp: ListMulticastGroupQueueResponse) => {
|
||||||
|
const obj = resp.toObject();
|
||||||
|
callbackFunc(obj.itemsList.length, obj.itemsList);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshQueue = () => {
|
||||||
|
setRefreshCounter(refreshCounter + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const flushQueue = () => {
|
||||||
|
const req = new FlushMulticastGroupQueueRequest();
|
||||||
|
req.setMulticastGroupId(props.multicastGroup.getId());
|
||||||
|
MulticastGroupStore.flushQueue(req, () => {
|
||||||
|
refreshQueue();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEnqueue = (values: FormRules) => {
|
||||||
|
const req = new EnqueueMulticastGroupQueueItemRequest();
|
||||||
|
const item = new MulticastGroupQueueItem();
|
||||||
|
|
||||||
|
item.setMulticastGroupId(props.multicastGroup.getId());
|
||||||
|
item.setFPort(values.fPort);
|
||||||
|
|
||||||
|
if (values.expiresAt !== null && values.expiresAt !== undefined) {
|
||||||
|
item.setExpiresAt(Timestamp.fromDate(values.expiresAt.toDate()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.base64 !== undefined) {
|
||||||
|
item.setData(new Uint8Array(Buffer.from(values.base64, "base64")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.hex !== undefined) {
|
||||||
|
item.setData(new Uint8Array(Buffer.from(values.hex, "hex")));
|
||||||
|
}
|
||||||
|
|
||||||
|
req.setQueueItem(item);
|
||||||
|
|
||||||
|
MulticastGroupStore.enqueue(req, _ => {
|
||||||
|
form.resetFields();
|
||||||
|
refreshQueue();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
|
<Card title="Enqueue">
|
||||||
|
<Form
|
||||||
|
layout="horizontal"
|
||||||
|
onFinish={onEnqueue}
|
||||||
|
onFinishFailed={onFinishFailed}
|
||||||
|
form={form}
|
||||||
|
initialValues={{ fPort: 1 }}
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
<Space direction="horizontal" style={{ width: "100%" }} size="large">
|
||||||
|
<Form.Item name="fPort" label="FPort">
|
||||||
|
<InputNumber min={1} max={254} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="expiresAt"
|
||||||
|
label="Expires at"
|
||||||
|
tooltip="If set, the queue-item will automatically expire at the given timestamp if it wasn't sent yet."
|
||||||
|
>
|
||||||
|
<DatePicker showTime />
|
||||||
|
</Form.Item>
|
||||||
|
</Space>
|
||||||
|
</Row>
|
||||||
|
<Tabs defaultActiveKey="1">
|
||||||
|
<Tabs.TabPane tab="HEX" key="1">
|
||||||
|
<Form.Item name="hex">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab="BASE64" key="2">
|
||||||
|
<Form.Item name="base64">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
Enqueue
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
<Row justify="end">
|
||||||
|
<Space direction="horizontal" size="large">
|
||||||
|
<Button icon={<RedoOutlined />} onClick={refreshQueue}>
|
||||||
|
Reload
|
||||||
|
</Button>
|
||||||
|
<Popconfirm title="Are you sure you want to flush the queue?" placement="left" onConfirm={flushQueue}>
|
||||||
|
<Button icon={<DeleteOutlined />}>Flush queue</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
</Row>
|
||||||
|
<DataTable columns={columns} getPage={getPage} refreshKey={refreshCounter} rowKey="id" noPagination />
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MulticastGroupQueue;
|
Loading…
x
Reference in New Issue
Block a user