diff --git a/frontend/package.json b/frontend/package.json index e9b3ca7..00699dd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-tooltip": "^5.28.0", + "react-use-websocket": "^4.11.1", "tailwind-merge": "^2.5.5", "tailwindcss": "^3.4.17" }, diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 938d292..d312142 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: react-tooltip: specifier: ^5.28.0 version: 5.28.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react-use-websocket: + specifier: ^4.11.1 + version: 4.11.1 tailwind-merge: specifier: ^2.5.5 version: 2.5.5 @@ -1964,6 +1967,9 @@ packages: react: '>=16.14.0' react-dom: '>=16.14.0' + react-use-websocket@4.11.1: + resolution: {integrity: sha512-39e8mK2a2A1h8uY3ePF45b2q0vwMOmaEy7J5qEhQg4n7vYa5oDLmqutG36kZQgAQ/3KCZS0brlGRbbZJ0+zfKQ==} + react@19.0.0: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} @@ -4668,6 +4674,8 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) + react-use-websocket@4.11.1: {} + react@19.0.0: {} read-cache@1.0.0: diff --git a/frontend/src/components/Demo.tsx b/frontend/src/components/Demo.tsx index 0298aeb..edd9cf6 100644 --- a/frontend/src/components/Demo.tsx +++ b/frontend/src/components/Demo.tsx @@ -41,7 +41,7 @@ const Demo = ({ class: className }: DemoProps) => { highlightedTimeoutRef.current = setTimeout(() => { highlightedTimeoutRef.current = null; setHighlightedToken(null); - }, 1250); + }, 1500); } return ( @@ -74,7 +74,7 @@ const Demo = ({ class: className }: DemoProps) => { className={cn( "transition-colors border hover:border-zinc-500 duration-100 ease-in border-transparent", { - "bg-zinc-500 animate-pulse-border border-white text-zinc-50": + "bg-zinc-500 animate-pulse-border border-zinc-300 text-zinc-50": highlightedToken === download.token, } )} diff --git a/frontend/src/components/useSocket.ts b/frontend/src/components/useSocket.ts index 928cd76..6f3abcb 100644 --- a/frontend/src/components/useSocket.ts +++ b/frontend/src/components/useSocket.ts @@ -1,5 +1,6 @@ import { withBackend } from "@/util"; import { useEffect, useRef, useState } from "react"; +import useWebSocket from "react-use-websocket"; export interface Download { token: number; @@ -16,107 +17,78 @@ export interface Executable { export interface UseSocketResult { id: number | null; - status: Status; executables: Executable[] | null; downloads: Download[] | null; buildLog: string | null; deleteDownload: (id: number) => void; } +export interface UseSocketProps { + notify?: (token: number) => void; +} + export type Status = "connected" | "disconnected" | "connecting"; -function useSocket(): UseSocketResult { +function useSocket({ notify }: UseSocketProps): UseSocketResult { + const { sendMessage, lastMessage, readyState } = useWebSocket( + withBackend( + window.location.protocol === "https:" ? "wss://" : "ws://", + "/ws" + ) + ); + const [id, setId] = useState(null); const [downloads, setDownloads] = useState(null); const [executables, setExecutables] = useState<{ build_log: string | null; executables: Executable[]; } | null>(null); - const [status, setStatus] = useState("connecting"); - const socketRef = useRef(null); - const allowReconnectRef = useRef(true); + useEffect(() => { + { + if (lastMessage == null) return; + const data = JSON.parse(lastMessage.data); + + if (data.type == undefined) + throw new Error("Received message without type"); + + switch (data.type) { + case "notify": + const token = data.token as number; + if (notify != null) notify(token); + break; + case "state": + setId(data.session.id as number); + setDownloads(data.session.downloads as Download[]); + break; + case "executables": + setExecutables({ + build_log: data.build_log, + executables: data.executables as Executable[], + }); + break; + default: + console.warn("Received unknown message type", data.type); + } + } + }, [lastMessage]); function deleteDownload(download_token: number) { - if (socketRef.current == null) { - console.error("Socket is null"); - return; - } else if (socketRef.current.readyState !== WebSocket.OPEN) { - console.error("Socket is not open", socketRef.current.readyState); - return; - } - socketRef.current.send( + if (readyState !== WebSocket.OPEN) return; + + sendMessage( JSON.stringify({ type: "delete-download-token", - token: download_token, + id: download_token, }) ); } - useEffect(() => { - function connect() { - const socket = new WebSocket( - withBackend( - window.location.protocol === "https:" ? "wss://" : "ws://", - "/ws" - ) - ); - socketRef.current = socket; - - socket.onmessage = (event) => { - const data = JSON.parse(event.data); - - if (data.type == undefined) - throw new Error("Received message without type"); - - switch (data.type) { - case "state": - setId(data.session.id as number); - setDownloads(data.session.downloads as Download[]); - break; - case "executables": - setExecutables({ - build_log: data.build_log, - executables: data.executables as Executable[], - }); - break; - default: - console.warn("Received unknown message type", data.type); - } - }; - - socket.onclose = (event) => { - console.warn("WebSocket connection closed", event); - - socketRef.current = null; - if (allowReconnectRef.current) { - setId(null); - setDownloads(null); - setExecutables(null); - - setTimeout(() => { - connect(); - }, 3000); - } - }; - } - - connect(); - - return () => { - // Close the socket when the component is unmounted - console.debug("Unmounting, closing WebSocket connection"); - socketRef.current?.close(); - allowReconnectRef.current = false; - }; - }, []); - return { id, downloads, - status, executables: executables?.executables ?? null, - buildLog: executables?.build_log, + buildLog: executables?.build_log ?? null, deleteDownload, }; } diff --git a/frontend/src/pages/index.astro b/frontend/src/pages/index.astro index f2d1afb..1a663bb 100644 --- a/frontend/src/pages/index.astro +++ b/frontend/src/pages/index.astro @@ -6,7 +6,7 @@ import Demo from "@/components/Demo";
Demo
- +
diff --git a/frontend/tailwind.config.mjs b/frontend/tailwind.config.mjs index 7ad90d8..955a793 100644 --- a/frontend/tailwind.config.mjs +++ b/frontend/tailwind.config.mjs @@ -4,16 +4,13 @@ export default { theme: { extend: { animation: { - "pulse-border": "pulse-border 1s ease-in-out infinite", + "pulse-border": "pulse-border 1s cubic-bezier(0.4, 0, 0.6, 1) infinite", "pulse-dark": "pulse-dark 2.5s ease-in-out infinite", }, keyframes: { "pulse-border": { - "0%, 100%": { - "--tw-border-opacity": "1", - }, "50%": { - "--tw-border-opacity": "0.5", + borderColor: "rgba(100, 100, 100)", }, }, "pulse-dark": {