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/ {
 | 
			
		||||
        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
 | 
			
		||||
        add_header 'Access-Control-Allow-Origin' '*' 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": {
 | 
			
		||||
        "@radix-ui/react-dialog": "^1.1.14",
 | 
			
		||||
        "@radix-ui/react-label": "^2.1.7",
 | 
			
		||||
        "@radix-ui/react-progress": "^1.1.7",
 | 
			
		||||
        "@radix-ui/react-separator": "^1.1.7",
 | 
			
		||||
        "@radix-ui/react-slot": "^1.2.3",
 | 
			
		||||
        "@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": {
 | 
			
		||||
      "version": "1.1.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@
 | 
			
		|||
  "dependencies": {
 | 
			
		||||
    "@radix-ui/react-dialog": "^1.1.14",
 | 
			
		||||
    "@radix-ui/react-label": "^2.1.7",
 | 
			
		||||
    "@radix-ui/react-progress": "^1.1.7",
 | 
			
		||||
    "@radix-ui/react-separator": "^1.1.7",
 | 
			
		||||
    "@radix-ui/react-slot": "^1.2.3",
 | 
			
		||||
    "@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 { BASE_URL, API_ENDPOINTS } from "@/config/api";
 | 
			
		||||
import { useEffect, useRef } from "react";
 | 
			
		||||
import { useQueryClient } from "@tanstack/react-query";
 | 
			
		||||
import { API_ENDPOINTS } from "@/config/api";
 | 
			
		||||
 | 
			
		||||
interface DeviceEventData {
 | 
			
		||||
  Message: string;
 | 
			
		||||
  DeviceId: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -7,23 +9,45 @@ interface DeviceEventData {
 | 
			
		|||
  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(roomName?: string) {
 | 
			
		||||
  const queryClient = useQueryClient();
 | 
			
		||||
  const reconnectTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
 | 
			
		||||
 | 
			
		||||
export function useDeviceEvents(options: UseDeviceEventsOptions) {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const onlineES = new EventSource(API_ENDPOINTS.SSE_EVENTS.DEVICE_ONLINE);
 | 
			
		||||
    const offlineES = new EventSource(API_ENDPOINTS.SSE_EVENTS.DEVICE_OFFLINE);
 | 
			
		||||
    let onlineES: EventSource | null = null;
 | 
			
		||||
    let offlineES: EventSource | null = null;
 | 
			
		||||
 | 
			
		||||
    const connect = () => {
 | 
			
		||||
      onlineES = new EventSource(API_ENDPOINTS.SSE_EVENTS.DEVICE_ONLINE);
 | 
			
		||||
      offlineES = new EventSource(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);
 | 
			
		||||
 | 
			
		||||
          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);
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -32,24 +56,54 @@ export function useDeviceEvents(options: UseDeviceEventsOptions) {
 | 
			
		|||
      offlineES.addEventListener("offline", (event) => {
 | 
			
		||||
        try {
 | 
			
		||||
          const data: DeviceEventData = JSON.parse(event.data);
 | 
			
		||||
        options.onRoomDeviceOffline?.(data.Room, data.DeviceId);
 | 
			
		||||
        options.onDeviceOfflineInRoom?.(data.DeviceId, data.Room);
 | 
			
		||||
 | 
			
		||||
          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);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    onlineES.onerror = (err) => {
 | 
			
		||||
      console.error("Online SSE connection error:", err);
 | 
			
		||||
      const onError = (err: any) => {
 | 
			
		||||
        console.error("SSE connection error:", err);
 | 
			
		||||
        cleanup();
 | 
			
		||||
        reconnectTimeout.current = setTimeout(connect, 5000);
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
    offlineES.onerror = (err) => {
 | 
			
		||||
      console.error("Offline SSE connection error:", err);
 | 
			
		||||
      onlineES.onerror = onError;
 | 
			
		||||
      offlineES.onerror = onError;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      onlineES.close();
 | 
			
		||||
      offlineES.close();
 | 
			
		||||
    const cleanup = () => {
 | 
			
		||||
      if (onlineES) onlineES.close();
 | 
			
		||||
      if (offlineES) offlineES.close();
 | 
			
		||||
      if (reconnectTimeout.current) {
 | 
			
		||||
        clearTimeout(reconnectTimeout.current);
 | 
			
		||||
        reconnectTimeout.current = null;
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }, [options]);
 | 
			
		||||
 | 
			
		||||
    connect();
 | 
			
		||||
    return cleanup;
 | 
			
		||||
  }, [roomName, queryClient]);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,9 @@ type MutationDataOptions<TInput, TOutput> = {
 | 
			
		|||
  method?: Method // POST, PUT, PATCH, DELETE
 | 
			
		||||
  onSuccess?: (data: TOutput) => void
 | 
			
		||||
  onError?: (error: any) => void
 | 
			
		||||
  config?: {
 | 
			
		||||
    onUploadProgress?: (e: ProgressEvent) => void
 | 
			
		||||
  }
 | 
			
		||||
  invalidate?: string[][] // List of queryKeys to invalidate
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,6 +33,7 @@ import {
 | 
			
		|||
  TableHeader,
 | 
			
		||||
  TableRow,
 | 
			
		||||
} from "@/components/ui/table";
 | 
			
		||||
import { Progress } from "@/components/ui/progress";
 | 
			
		||||
 | 
			
		||||
import { BASE_URL, API_ENDPOINTS } from "@/config/api";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -70,14 +71,24 @@ function AppsComponent() {
 | 
			
		|||
      : [];
 | 
			
		||||
 | 
			
		||||
  const [isUploadOpen, setIsUploadOpen] = useState(false);
 | 
			
		||||
  const [uploadPercent, setUploadPercent] = useState(0);
 | 
			
		||||
 | 
			
		||||
  const uploadMutation = useMutationData<FormData>({
 | 
			
		||||
    url: BASE_URL + API_ENDPOINTS.APP_VERSION.UPLOAD,
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    config: {
 | 
			
		||||
      onUploadProgress: (e: ProgressEvent) => {
 | 
			
		||||
        if (e.total) {
 | 
			
		||||
          const percent = Math.round((e.loaded * 100) / e.total);
 | 
			
		||||
          setUploadPercent(percent);
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    onSuccess: () => {
 | 
			
		||||
      toast.success("Cập nhật thành công!");
 | 
			
		||||
      setIsUploadOpen(false);
 | 
			
		||||
      setUploadPercent(0);
 | 
			
		||||
      form.reset();
 | 
			
		||||
      setIsUploadOpen(false);
 | 
			
		||||
    },
 | 
			
		||||
    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("newVersion", typedValue.newVersion);
 | 
			
		||||
 | 
			
		||||
      await uploadMutation.mutateAsync(formData);
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			@ -131,13 +143,7 @@ function AppsComponent() {
 | 
			
		|||
              </DialogDescription>
 | 
			
		||||
            </DialogHeader>
 | 
			
		||||
 | 
			
		||||
            <form
 | 
			
		||||
              className="space-y-4"
 | 
			
		||||
              onSubmit={(e) => {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                form.handleSubmit();
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
            <form className="space-y-4" onSubmit={form.handleSubmit}>
 | 
			
		||||
              <form.Field name="newVersion">
 | 
			
		||||
                {(field) => (
 | 
			
		||||
                  <div className="space-y-2">
 | 
			
		||||
| 
						 | 
				
			
			@ -157,7 +163,7 @@ function AppsComponent() {
 | 
			
		|||
                    <Label>File ứng dụng</Label>
 | 
			
		||||
                    <Input
 | 
			
		||||
                      type="file"
 | 
			
		||||
                      accept=".exe,.zip,.apk"
 | 
			
		||||
                      accept=".exe,.msi,.apk"
 | 
			
		||||
                      onChange={(e) => {
 | 
			
		||||
                        if (e.target.files) {
 | 
			
		||||
                          field.handleChange(e.target.files);
 | 
			
		||||
| 
						 | 
				
			
			@ -168,6 +174,14 @@ function AppsComponent() {
 | 
			
		|||
                )}
 | 
			
		||||
              </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>
 | 
			
		||||
                <Button
 | 
			
		||||
                  type="button"
 | 
			
		||||
| 
						 | 
				
			
			@ -176,7 +190,9 @@ function AppsComponent() {
 | 
			
		|||
                >
 | 
			
		||||
                  Hủy
 | 
			
		||||
                </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>
 | 
			
		||||
            </form>
 | 
			
		||||
          </DialogContent>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,6 @@ import {
 | 
			
		|||
  TableHeader,
 | 
			
		||||
  TableRow,
 | 
			
		||||
} from "@/components/ui/table";
 | 
			
		||||
import { useQueryClient } from "@tanstack/react-query";
 | 
			
		||||
import { useDeviceEvents } from "@/hooks/useDeviceEvents";
 | 
			
		||||
 | 
			
		||||
export const Route = createFileRoute("/_authenticated/room/$roomName/")({
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +27,6 @@ export const Route = createFileRoute("/_authenticated/room/$roomName/")({
 | 
			
		|||
 | 
			
		||||
function RoomDetailComponent() {
 | 
			
		||||
  const { roomName } = useParams({ from: "/_authenticated/room/$roomName/" });
 | 
			
		||||
  const queryClient = useQueryClient();
 | 
			
		||||
 | 
			
		||||
  const { data: devices = [], isLoading } = useQueryData({
 | 
			
		||||
    queryKey: ["devices", roomName],
 | 
			
		||||
| 
						 | 
				
			
			@ -36,30 +34,7 @@ function RoomDetailComponent() {
 | 
			
		|||
  });
 | 
			
		||||
 | 
			
		||||
  // 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
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  useDeviceEvents(roomName);
 | 
			
		||||
 | 
			
		||||
  const columns: ColumnDef<any>[] = [
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,6 @@
 | 
			
		|||
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,
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +29,6 @@ export const Route = createFileRoute("/_authenticated/room/")({
 | 
			
		|||
});
 | 
			
		||||
 | 
			
		||||
function RoomComponent() {
 | 
			
		||||
  const queryClient = useQueryClient();
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
  const { data: roomData = [], isLoading } = useQueryData({
 | 
			
		||||
| 
						 | 
				
			
			@ -40,35 +38,7 @@ function RoomComponent() {
 | 
			
		|||
 | 
			
		||||
  const [sorting, setSorting] = React.useState<SortingState>([]);
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
        )
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  useDeviceEvents();
 | 
			
		||||
 | 
			
		||||
  const columns: ColumnDef<any>[] = [
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user