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,49 +9,101 @@ 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;
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
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) => {
|
||||
console.error("Offline SSE connection error:", err);
|
||||
};
|
||||
|
||||
return () => {
|
||||
onlineES.close();
|
||||
offlineES.close();
|
||||
};
|
||||
}, [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