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