diff --git a/apps/fabro-web/app/routes/runs.tsx b/apps/fabro-web/app/routes/runs.tsx index 85c6e9bfe..9f802fb8e 100644 --- a/apps/fabro-web/app/routes/runs.tsx +++ b/apps/fabro-web/app/routes/runs.tsx @@ -1,5 +1,5 @@ -import { useState, useCallback, useRef } from "react"; -import { Link } from "react-router"; +import { useState, useCallback, useEffect, useRef } from "react"; +import { Link, useRevalidator } from "react-router"; import { ChevronDownIcon, ChevronRightIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { DndContext, @@ -523,6 +523,38 @@ export default function Runs({ loaderData }: any) { const [collapsed, setCollapsed] = useState>(new Set()); const [columns, setColumns] = useState(initialColumns); const lowerQuery = query.toLowerCase(); + const revalidator = useRevalidator(); + + const STATUS_EVENTS = new Set([ + "run.submitted", "run.starting", "run.running", + "run.paused", "run.completed", "run.failed", + ]); + + useEffect(() => { + const source = new EventSource("/api/v1/attach"); + let debounceTimer: ReturnType | undefined; + + source.onmessage = (msg) => { + try { + const payload = JSON.parse(msg.data); + if (STATUS_EVENTS.has(payload.event)) { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => revalidator.revalidate(), 500); + } + } catch { + // ignore malformed events + } + }; + + return () => { + clearTimeout(debounceTimer); + source.close(); + }; + }, []); + + useEffect(() => { + setColumns(initialColumns); + }, [initialColumns]); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),