add devices list table
This commit is contained in:
parent
cf735f31bf
commit
00be81e18b
32
package-lock.json
generated
32
package-lock.json
generated
|
@ -16,6 +16,7 @@
|
|||
"@tanstack/react-query": "^5.83.0",
|
||||
"@tanstack/react-router": "^1.121.2",
|
||||
"@tanstack/react-router-devtools": "^1.121.2",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/router-plugin": "^1.121.2",
|
||||
"axios": "^1.11.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
@ -2496,6 +2497,25 @@
|
|||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-table": {
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
|
||||
"integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==",
|
||||
"dependencies": {
|
||||
"@tanstack/table-core": "8.21.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/router-core": {
|
||||
"version": "1.129.8",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.129.8.tgz",
|
||||
|
@ -2648,6 +2668,18 @@
|
|||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/table-core": {
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
|
||||
"integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-file-routes": {
|
||||
"version": "1.129.7",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.129.7.tgz",
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"@tanstack/react-query": "^5.83.0",
|
||||
"@tanstack/react-router": "^1.121.2",
|
||||
"@tanstack/react-router-devtools": "^1.121.2",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/router-plugin": "^1.121.2",
|
||||
"axios": "^1.11.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
|
|
@ -28,4 +28,8 @@ export const API_ENDPOINTS = {
|
|||
GET_ROOM_LIST: `/DeviceComm/rooms`,
|
||||
GET_DEVICE_FROM_ROOM: (roomName: string) => `/DeviceComm/room/${roomName}`,
|
||||
},
|
||||
SSE_EVENTS: {
|
||||
DEVICE_ONLINE: `/Sse/events/onlineDevices`,
|
||||
DEVICE_OFFLINE: `/Sse/events/offlineDevices`,
|
||||
},
|
||||
};
|
||||
|
|
55
src/hooks/useDeviceEvents.ts
Normal file
55
src/hooks/useDeviceEvents.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { useEffect } from "react";
|
||||
import { BASE_URL, API_ENDPOINTS } from "@/config/api";
|
||||
interface DeviceEventData {
|
||||
Message: string;
|
||||
DeviceId: string;
|
||||
Room: string;
|
||||
Timestamp?: string;
|
||||
}
|
||||
|
||||
interface UseDeviceEventsOptions {
|
||||
onRoomDeviceOnline?: (room: string, deviceId: string) => void;
|
||||
onRoomDeviceOffline?: (room: string, deviceId: string) => void;
|
||||
onDeviceOnlineInRoom?: (deviceId: string, room: string) => void;
|
||||
onDeviceOfflineInRoom?: (deviceId: string, room: string) => void;
|
||||
}
|
||||
|
||||
export function useDeviceEvents(options: UseDeviceEventsOptions) {
|
||||
useEffect(() => {
|
||||
const onlineES = new EventSource(`${BASE_URL}${API_ENDPOINTS.SSE_EVENTS.DEVICE_ONLINE}`);
|
||||
const offlineES = new EventSource(`${BASE_URL}${API_ENDPOINTS.SSE_EVENTS.DEVICE_OFFLINE}`);
|
||||
|
||||
onlineES.addEventListener("online", (event) => {
|
||||
try {
|
||||
const data: DeviceEventData = JSON.parse(event.data);
|
||||
options.onRoomDeviceOnline?.(data.Room, data.DeviceId);
|
||||
options.onDeviceOnlineInRoom?.(data.DeviceId, data.Room);
|
||||
} catch (err) {
|
||||
console.error("Error parsing online event:", err);
|
||||
}
|
||||
});
|
||||
|
||||
offlineES.addEventListener("offline", (event) => {
|
||||
try {
|
||||
const data: DeviceEventData = JSON.parse(event.data);
|
||||
options.onRoomDeviceOffline?.(data.Room, data.DeviceId);
|
||||
options.onDeviceOfflineInRoom?.(data.DeviceId, data.Room);
|
||||
} catch (err) {
|
||||
console.error("Error parsing offline event:", err);
|
||||
}
|
||||
});
|
||||
|
||||
onlineES.onerror = (err) => {
|
||||
console.error("Online SSE connection error:", err);
|
||||
};
|
||||
|
||||
offlineES.onerror = (err) => {
|
||||
console.error("Offline SSE connection error:", err);
|
||||
};
|
||||
|
||||
return () => {
|
||||
onlineES.close();
|
||||
offlineES.close();
|
||||
};
|
||||
}, [options]);
|
||||
}
|
|
@ -1,9 +1,158 @@
|
|||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { createFileRoute, useParams } from "@tanstack/react-router";
|
||||
import { useQueryData } from "@/hooks/useQueryData";
|
||||
import { API_ENDPOINTS, BASE_URL } from "@/config/api";
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type ColumnDef,
|
||||
} from "@tanstack/react-table";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useDeviceEvents } from "@/hooks/useDeviceEvents";
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/room/$roomName/')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
export const Route = createFileRoute("/_authenticated/room/$roomName/")({
|
||||
head: ({ params }) => ({
|
||||
meta: [{ title: `Danh sách thiết bị phòng ${params.roomName}` }],
|
||||
}),
|
||||
component: RoomDetailComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_authenticated/room/$roomName/"!</div>
|
||||
function RoomDetailComponent() {
|
||||
const { roomName } = useParams({ from: "/_authenticated/room/$roomName/" });
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: devices = [], isLoading } = useQueryData({
|
||||
queryKey: ["devices", roomName],
|
||||
url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.GET_DEVICE_FROM_ROOM(roomName),
|
||||
});
|
||||
|
||||
// Lắng nghe SSE và update state
|
||||
useDeviceEvents({
|
||||
onDeviceOnlineInRoom: (deviceId, room) => {
|
||||
if (room === roomName) {
|
||||
queryClient.setQueryData(
|
||||
["devices", roomName],
|
||||
(oldDevices: any[] = []) =>
|
||||
oldDevices.map((d) =>
|
||||
d.macAddress === deviceId ? { ...d, isOffline: false } : d
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
onDeviceOfflineInRoom: (deviceId, room) => {
|
||||
if (room === roomName) {
|
||||
queryClient.setQueryData(
|
||||
["devices", roomName],
|
||||
(oldDevices: any[] = []) =>
|
||||
oldDevices.map((d) =>
|
||||
d.macAddress === deviceId ? { ...d, isOffline: true } : d
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const columns: ColumnDef<any>[] = [
|
||||
{
|
||||
header: "STT",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
},
|
||||
{
|
||||
header: "MAC Address",
|
||||
accessorKey: "macAddress",
|
||||
},
|
||||
{
|
||||
header: "Thời gian thiết bị",
|
||||
accessorKey: "deviceTime",
|
||||
cell: ({ getValue }) => {
|
||||
const date = new Date(getValue() as string);
|
||||
return date.toLocaleString();
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Phiên bản",
|
||||
accessorKey: "version",
|
||||
},
|
||||
{
|
||||
header: "Địa chỉ IP",
|
||||
accessorKey: "ipAddress",
|
||||
},
|
||||
{
|
||||
header: "Trạng thái",
|
||||
accessorKey: "isOffline",
|
||||
cell: ({ getValue }) =>
|
||||
getValue() ? (
|
||||
<span className="text-red-500 font-semibold">Offline</span>
|
||||
) : (
|
||||
<span className="text-green-500 font-semibold">Online</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: devices,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
if (isLoading) return <div>Đang tải thiết bị...</div>;
|
||||
|
||||
return (
|
||||
<div className="w-full px-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Phòng: {roomName}</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Danh sách thiết bị trong phòng
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Thiết bị</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,161 @@
|
|||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { API_ENDPOINTS, BASE_URL } from "@/config/api";
|
||||
import { useQueryData } from "@/hooks/useQueryData";
|
||||
import { useDeviceEvents } from "@/hooks/useDeviceEvents";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
type ColumnDef,
|
||||
type SortingState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import React from "react";
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/room/')({
|
||||
export const Route = createFileRoute("/_authenticated/room/")({
|
||||
head: () => ({
|
||||
meta: [{ title: "Danh sách phòng" }],
|
||||
}),
|
||||
component: RoomComponent,
|
||||
})
|
||||
});
|
||||
|
||||
function RoomComponent() {
|
||||
return <div>Hello "/_authenticated/room/"!</div>
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: roomData = [], isLoading } = useQueryData({
|
||||
queryKey: ["rooms"],
|
||||
url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.GET_ROOM_LIST,
|
||||
});
|
||||
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
|
||||
useDeviceEvents({
|
||||
onRoomDeviceOnline: (room) => {
|
||||
queryClient.setQueryData(["rooms"], (oldRooms: any[] = []) =>
|
||||
oldRooms.map((r) =>
|
||||
r.name === room
|
||||
? { ...r, offlineCount: Math.max((r.offlineCount || 0) - 1, 0) }
|
||||
: r
|
||||
)
|
||||
);
|
||||
},
|
||||
onRoomDeviceOffline: (room) => {
|
||||
queryClient.setQueryData(["rooms"], (oldRooms: any[] = []) =>
|
||||
oldRooms.map((r) =>
|
||||
r.name === room
|
||||
? { ...r, offlineCount: (r.offlineCount || 0) + 1 }
|
||||
: r
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const columns: ColumnDef<any>[] = [
|
||||
{
|
||||
header: "STT",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
},
|
||||
{
|
||||
header: "Tên phòng",
|
||||
accessorKey: "name",
|
||||
},
|
||||
{
|
||||
header: "Số lượng thiết bị",
|
||||
accessorKey: "numberOfDevices",
|
||||
},
|
||||
{
|
||||
header: "Thiết bị offline",
|
||||
accessorKey: "offlineCount",
|
||||
cell: ({ getValue }) => getValue() || 0,
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: roomData,
|
||||
columns,
|
||||
state: { sorting },
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
});
|
||||
|
||||
if (isLoading) return <div>Đang tải...</div>;
|
||||
|
||||
return (
|
||||
<div className="w-full px-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Quản lý phòng</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Danh sách các phòng hiện có trong hệ thống
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Danh sách phòng</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
const isSorted = header.column.getIsSorted();
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className="cursor-pointer select-none"
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{isSorted ? (isSorted === "asc" ? " ▲" : " ▼") : ""}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
className="cursor-pointer hover:bg-gray-100"
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: "/room/$roomName",
|
||||
params: { roomName: row.original.name },
|
||||
})
|
||||
}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user