config in nginx and app page for real time refetch and upload from website
This commit is contained in:
parent
c9726d00a0
commit
1f9336271c
|
@ -27,6 +27,17 @@ server {
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://backend/;
|
proxy_pass http://backend/;
|
||||||
|
|
||||||
|
# Cho phép upload file lớn (vd: 200MB)
|
||||||
|
client_max_body_size 200M;
|
||||||
|
|
||||||
|
# Truyền thẳng stream sang backend
|
||||||
|
proxy_request_buffering off;
|
||||||
|
|
||||||
|
# Tăng timeout khi upload
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
proxy_connect_timeout 300s;
|
||||||
|
proxy_send_timeout 300s;
|
||||||
|
|
||||||
# CORS headers
|
# CORS headers
|
||||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
|
||||||
|
|
25
package-lock.json
generated
25
package-lock.json
generated
|
@ -8,6 +8,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
|
@ -1621,6 +1622,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-progress": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-separator": {
|
"node_modules/@radix-ui/react-separator": {
|
||||||
"version": "1.1.7",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
|
|
29
src/components/ui/progress.tsx
Normal file
29
src/components/ui/progress.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Progress({
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
data-slot="progress"
|
||||||
|
className={cn(
|
||||||
|
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
data-slot="progress-indicator"
|
||||||
|
className="bg-primary h-full w-full flex-1 transition-all"
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Progress }
|
|
@ -1,5 +1,7 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { BASE_URL, API_ENDPOINTS } from "@/config/api";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { API_ENDPOINTS } from "@/config/api";
|
||||||
|
|
||||||
interface DeviceEventData {
|
interface DeviceEventData {
|
||||||
Message: string;
|
Message: string;
|
||||||
DeviceId: string;
|
DeviceId: string;
|
||||||
|
@ -7,49 +9,101 @@ interface DeviceEventData {
|
||||||
Timestamp?: string;
|
Timestamp?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseDeviceEventsOptions {
|
export function useDeviceEvents(roomName?: string) {
|
||||||
onRoomDeviceOnline?: (room: string, deviceId: string) => void;
|
const queryClient = useQueryClient();
|
||||||
onRoomDeviceOffline?: (room: string, deviceId: string) => void;
|
const reconnectTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
onDeviceOnlineInRoom?: (deviceId: string, room: string) => void;
|
|
||||||
onDeviceOfflineInRoom?: (deviceId: string, room: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDeviceEvents(options: UseDeviceEventsOptions) {
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onlineES = new EventSource(API_ENDPOINTS.SSE_EVENTS.DEVICE_ONLINE);
|
let onlineES: EventSource | null = null;
|
||||||
const offlineES = new EventSource(API_ENDPOINTS.SSE_EVENTS.DEVICE_OFFLINE);
|
let offlineES: EventSource | null = null;
|
||||||
|
|
||||||
onlineES.addEventListener("online", (event) => {
|
const connect = () => {
|
||||||
try {
|
onlineES = new EventSource(API_ENDPOINTS.SSE_EVENTS.DEVICE_ONLINE);
|
||||||
const data: DeviceEventData = JSON.parse(event.data);
|
offlineES = new EventSource(API_ENDPOINTS.SSE_EVENTS.DEVICE_OFFLINE);
|
||||||
options.onRoomDeviceOnline?.(data.Room, data.DeviceId);
|
|
||||||
options.onDeviceOnlineInRoom?.(data.DeviceId, data.Room);
|
onlineES.addEventListener("online", (event) => {
|
||||||
} catch (err) {
|
try {
|
||||||
console.error("Error parsing online event:", err);
|
const data: DeviceEventData = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (roomName && data.Room === roomName) {
|
||||||
|
queryClient.setQueryData(
|
||||||
|
["devices", roomName],
|
||||||
|
(oldDevices: any[] = []) =>
|
||||||
|
oldDevices.map((d) =>
|
||||||
|
d.macAddress === data.DeviceId
|
||||||
|
? { ...d, isOffline: false }
|
||||||
|
: d
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
queryClient.setQueryData(["rooms"], (oldRooms: any[] = []) =>
|
||||||
|
oldRooms.map((room) =>
|
||||||
|
room.name === data.Room
|
||||||
|
? {
|
||||||
|
...room,
|
||||||
|
numberOfOfflineDevices:
|
||||||
|
Math.max(0, room.numberOfOfflineDevices - 1),
|
||||||
|
}
|
||||||
|
: room
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error parsing online event:", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
offlineES.addEventListener("offline", (event) => {
|
||||||
|
try {
|
||||||
|
const data: DeviceEventData = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (roomName && data.Room === roomName) {
|
||||||
|
queryClient.setQueryData(
|
||||||
|
["devices", roomName],
|
||||||
|
(oldDevices: any[] = []) =>
|
||||||
|
oldDevices.map((d) =>
|
||||||
|
d.macAddress === data.DeviceId
|
||||||
|
? { ...d, isOffline: true }
|
||||||
|
: d
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
queryClient.setQueryData(["rooms"], (oldRooms: any[] = []) =>
|
||||||
|
oldRooms.map((room) =>
|
||||||
|
room.name === data.Room
|
||||||
|
? {
|
||||||
|
...room,
|
||||||
|
numberOfOfflineDevices: room.numberOfOfflineDevices + 1,
|
||||||
|
}
|
||||||
|
: room
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error parsing offline event:", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onError = (err: any) => {
|
||||||
|
console.error("SSE connection error:", err);
|
||||||
|
cleanup();
|
||||||
|
reconnectTimeout.current = setTimeout(connect, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
onlineES.onerror = onError;
|
||||||
|
offlineES.onerror = onError;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (onlineES) onlineES.close();
|
||||||
|
if (offlineES) offlineES.close();
|
||||||
|
if (reconnectTimeout.current) {
|
||||||
|
clearTimeout(reconnectTimeout.current);
|
||||||
|
reconnectTimeout.current = null;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
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) => {
|
connect();
|
||||||
console.error("Offline SSE connection error:", err);
|
return cleanup;
|
||||||
};
|
}, [roomName, queryClient]);
|
||||||
|
|
||||||
return () => {
|
|
||||||
onlineES.close();
|
|
||||||
offlineES.close();
|
|
||||||
};
|
|
||||||
}, [options]);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,9 @@ type MutationDataOptions<TInput, TOutput> = {
|
||||||
method?: Method // POST, PUT, PATCH, DELETE
|
method?: Method // POST, PUT, PATCH, DELETE
|
||||||
onSuccess?: (data: TOutput) => void
|
onSuccess?: (data: TOutput) => void
|
||||||
onError?: (error: any) => void
|
onError?: (error: any) => void
|
||||||
|
config?: {
|
||||||
|
onUploadProgress?: (e: ProgressEvent) => void
|
||||||
|
}
|
||||||
invalidate?: string[][] // List of queryKeys to invalidate
|
invalidate?: string[][] // List of queryKeys to invalidate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ import {
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
|
||||||
import { BASE_URL, API_ENDPOINTS } from "@/config/api";
|
import { BASE_URL, API_ENDPOINTS } from "@/config/api";
|
||||||
|
|
||||||
|
@ -70,14 +71,24 @@ function AppsComponent() {
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const [isUploadOpen, setIsUploadOpen] = useState(false);
|
const [isUploadOpen, setIsUploadOpen] = useState(false);
|
||||||
|
const [uploadPercent, setUploadPercent] = useState(0);
|
||||||
|
|
||||||
const uploadMutation = useMutationData<FormData>({
|
const uploadMutation = useMutationData<FormData>({
|
||||||
url: BASE_URL + API_ENDPOINTS.APP_VERSION.UPLOAD,
|
url: BASE_URL + API_ENDPOINTS.APP_VERSION.UPLOAD,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
config: {
|
||||||
|
onUploadProgress: (e: ProgressEvent) => {
|
||||||
|
if (e.total) {
|
||||||
|
const percent = Math.round((e.loaded * 100) / e.total);
|
||||||
|
setUploadPercent(percent);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Cập nhật thành công!");
|
toast.success("Cập nhật thành công!");
|
||||||
setIsUploadOpen(false);
|
setUploadPercent(0);
|
||||||
form.reset();
|
form.reset();
|
||||||
|
setIsUploadOpen(false);
|
||||||
},
|
},
|
||||||
onError: () => toast.error("Lỗi khi cập nhật phiên bản!"),
|
onError: () => toast.error("Lỗi khi cập nhật phiên bản!"),
|
||||||
});
|
});
|
||||||
|
@ -102,6 +113,7 @@ function AppsComponent() {
|
||||||
formData.append("files", file)
|
formData.append("files", file)
|
||||||
);
|
);
|
||||||
formData.append("newVersion", typedValue.newVersion);
|
formData.append("newVersion", typedValue.newVersion);
|
||||||
|
|
||||||
await uploadMutation.mutateAsync(formData);
|
await uploadMutation.mutateAsync(formData);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -131,13 +143,7 @@ function AppsComponent() {
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<form
|
<form className="space-y-4" onSubmit={form.handleSubmit}>
|
||||||
className="space-y-4"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
form.handleSubmit();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<form.Field name="newVersion">
|
<form.Field name="newVersion">
|
||||||
{(field) => (
|
{(field) => (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
@ -157,7 +163,7 @@ function AppsComponent() {
|
||||||
<Label>File ứng dụng</Label>
|
<Label>File ứng dụng</Label>
|
||||||
<Input
|
<Input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".exe,.zip,.apk"
|
accept=".exe,.msi,.apk"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e.target.files) {
|
if (e.target.files) {
|
||||||
field.handleChange(e.target.files);
|
field.handleChange(e.target.files);
|
||||||
|
@ -168,6 +174,14 @@ function AppsComponent() {
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
|
|
||||||
|
{uploadPercent > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Tiến trình upload</Label>
|
||||||
|
<Progress value={uploadPercent} className="w-full" />
|
||||||
|
<span>{uploadPercent}%</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -176,7 +190,9 @@ function AppsComponent() {
|
||||||
>
|
>
|
||||||
Hủy
|
Hủy
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit">Tải lên</Button>
|
<Button type="submit" disabled={uploadMutation.isPending}>
|
||||||
|
{uploadMutation.isPending ? "Đang tải..." : "Tải lên"}
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
|
@ -16,7 +16,6 @@ import {
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { useDeviceEvents } from "@/hooks/useDeviceEvents";
|
import { useDeviceEvents } from "@/hooks/useDeviceEvents";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authenticated/room/$roomName/")({
|
export const Route = createFileRoute("/_authenticated/room/$roomName/")({
|
||||||
|
@ -28,7 +27,6 @@ export const Route = createFileRoute("/_authenticated/room/$roomName/")({
|
||||||
|
|
||||||
function RoomDetailComponent() {
|
function RoomDetailComponent() {
|
||||||
const { roomName } = useParams({ from: "/_authenticated/room/$roomName/" });
|
const { roomName } = useParams({ from: "/_authenticated/room/$roomName/" });
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const { data: devices = [], isLoading } = useQueryData({
|
const { data: devices = [], isLoading } = useQueryData({
|
||||||
queryKey: ["devices", roomName],
|
queryKey: ["devices", roomName],
|
||||||
|
@ -36,30 +34,7 @@ function RoomDetailComponent() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Lắng nghe SSE và update state
|
// Lắng nghe SSE và update state
|
||||||
useDeviceEvents({
|
useDeviceEvents(roomName);
|
||||||
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>[] = [
|
const columns: ColumnDef<any>[] = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { API_ENDPOINTS, BASE_URL } from "@/config/api";
|
import { API_ENDPOINTS, BASE_URL } from "@/config/api";
|
||||||
import { useQueryData } from "@/hooks/useQueryData";
|
import { useQueryData } from "@/hooks/useQueryData";
|
||||||
import { useDeviceEvents } from "@/hooks/useDeviceEvents";
|
import { useDeviceEvents } from "@/hooks/useDeviceEvents";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
import {
|
import {
|
||||||
flexRender,
|
flexRender,
|
||||||
|
@ -30,7 +29,6 @@ export const Route = createFileRoute("/_authenticated/room/")({
|
||||||
});
|
});
|
||||||
|
|
||||||
function RoomComponent() {
|
function RoomComponent() {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { data: roomData = [], isLoading } = useQueryData({
|
const { data: roomData = [], isLoading } = useQueryData({
|
||||||
|
@ -40,35 +38,7 @@ function RoomComponent() {
|
||||||
|
|
||||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||||
|
|
||||||
useDeviceEvents({
|
useDeviceEvents();
|
||||||
onRoomDeviceOnline: (room) => {
|
|
||||||
queryClient.setQueryData(["rooms"], (oldRooms: any[] = []) =>
|
|
||||||
oldRooms.map((r) =>
|
|
||||||
r.name === room
|
|
||||||
? {
|
|
||||||
...r,
|
|
||||||
numberOfOfflineDevices: Math.max(
|
|
||||||
(r.numberOfOfflineDevices || 0) - 1,
|
|
||||||
0
|
|
||||||
),
|
|
||||||
}
|
|
||||||
: r
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onRoomDeviceOffline: (room) => {
|
|
||||||
queryClient.setQueryData(["rooms"], (oldRooms: any[] = []) =>
|
|
||||||
oldRooms.map((r) =>
|
|
||||||
r.name === room
|
|
||||||
? {
|
|
||||||
...r,
|
|
||||||
numberOfOfflineDevices: (r.numberOfOfflineDevices || 0) + 1,
|
|
||||||
}
|
|
||||||
: r
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns: ColumnDef<any>[] = [
|
const columns: ColumnDef<any>[] = [
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue
Block a user