ui: Implement queue-item expires-at timestamp in UI.

This commit is contained in:
Orne Brocaar 2024-09-19 10:17:42 +01:00
parent 3829f591e4
commit 4d7a4b22e1
4 changed files with 277 additions and 2 deletions

View File

@ -17,6 +17,9 @@ import type {
RemoveGatewayFromMulticastGroupRequest,
ListMulticastGroupQueueRequest,
ListMulticastGroupQueueResponse,
EnqueueMulticastGroupQueueItemRequest,
EnqueueMulticastGroupQueueItemResponse,
FlushMulticastGroupQueueRequest,
} from "@chirpstack/chirpstack-api-grpc-web/api/multicast_group_pb";
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) => {
this.client.listQueue(req, SessionStore.getMetadata(), (err, resp) => {
if (err !== null) {
@ -164,6 +181,17 @@ class MulticastGroupStore extends EventEmitter {
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();

View File

@ -1,17 +1,32 @@
import { useState } from "react";
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 { 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 { RedoOutlined, DeleteOutlined } from "@ant-design/icons";
import { Buffer } from "buffer";
import type { Device, GetDeviceQueueItemsResponse } from "@chirpstack/chirpstack-api-grpc-web/api/device_pb";
import {
EnqueueDeviceQueueItemRequest,
GetDeviceQueueItemsRequest,
GetDeviceQueueItemsResponse,
Device,
FlushDeviceQueueRequest,
DeviceQueueItem,
} from "@chirpstack/chirpstack-api-grpc-web/api/device_pb";
@ -34,6 +49,7 @@ interface FormRules {
hex: string;
base64: string;
json: string;
expiresAt?: DatePickerProps["value"];
}
function DeviceQueue(props: IProps) {
@ -114,6 +130,21 @@ function DeviceQueue(props: IProps) {
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) => {
@ -148,6 +179,10 @@ function DeviceQueue(props: IProps) {
item.setIsEncrypted(values.isEncrypted);
item.setFCntDown(values.fCntDown);
if (values.expiresAt !== null && values.expiresAt !== undefined) {
item.setExpiresAt(Timestamp.fromDate(values.expiresAt.toDate()));
}
if (values.hex !== undefined) {
item.setData(new Uint8Array(Buffer.from(values.hex, "hex")));
}
@ -217,6 +252,13 @@ function DeviceQueue(props: IProps) {
<InputNumber min={0} />
</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">

View File

@ -21,6 +21,7 @@ import ListMulticastGroupDevices from "./ListMulticastGroupDevices";
import ListMulticastGroupGateways from "./ListMulticastGroupGateways";
import EditMulticastGroup from "./EditMulticastGroup";
import Admin from "../../components/Admin";
import MulticastGroupQueue from "./MulticastGroupQueue";
interface IProps {
tenant: Tenant;
@ -68,6 +69,9 @@ function MulticastGroupLayout(props: IProps) {
if (path.endsWith("edit")) {
tab = "edit";
}
if (path.endsWith("queue")) {
tab = "queue";
}
return (
<Space direction="vertical" style={{ width: "100%" }} size="large">
@ -131,11 +135,17 @@ function MulticastGroupLayout(props: IProps) {
Configuration
</Link>
</Menu.Item>
<Menu.Item key="queue">
<Link to={`/tenants/${tenant.getId()}/applications/${app.getId()}/multicast-groups/${mg.getId()}/queue`}>
Queue
</Link>
</Menu.Item>
</Menu>
<Routes>
<Route path="/" element={<ListMulticastGroupDevices multicastGroup={mg} />} />
<Route path="/gateways" element={<ListMulticastGroupGateways multicastGroup={mg} application={app} />} />
<Route path="/edit" element={<EditMulticastGroup application={app} multicastGroup={mg} />} />
<Route path="/queue" element={<MulticastGroupQueue multicastGroup={mg} />} />
</Routes>
</Card>
</Space>

View 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;