diff --git a/vite-app/src/components/LogsSection.tsx b/vite-app/src/components/LogsSection.tsx index 65b0ad3f..ae6881e1 100644 --- a/vite-app/src/components/LogsSection.tsx +++ b/vite-app/src/components/LogsSection.tsx @@ -9,6 +9,10 @@ import { getApiUrl } from "../config"; import Select from "./Select"; import Button from "./Button"; +const haveLogsChanged = (prevLogs: LogEntry[], nextLogs: LogEntry[]) => { + return prevLogs.length !== nextLogs.length; +}; + interface LogsSectionProps { rolloutId?: string; } @@ -19,6 +23,8 @@ export const LogsSection = observer(({ rolloutId }: LogsSectionProps) => { const [error, setError] = useState(null); const [selectedLevel, setSelectedLevel] = useState(""); const scrollContainerRef = useRef(null); + const isAtBottomRef = useRef(true); + const shouldAutoScrollRef = useRef(false); const fetchLogs = async (isInitialLoad = false) => { if (!rolloutId) return; @@ -93,7 +99,19 @@ export const LogsSection = observer(({ rolloutId }: LogsSectionProps) => { const data: LogsResponse = LogsResponseSchema.parse( await response.json() ); - setLogs(data.logs); + setLogs((prevLogs) => { + const hasChanges = haveLogsChanged(prevLogs, data.logs); + + if (!hasChanges) { + return prevLogs; + } + + if (isAtBottomRef.current) { + shouldAutoScrollRef.current = true; + } + + return data.logs; + }); } catch (err) { if (err instanceof Error && err.message.includes("Unexpected token")) { setError( @@ -115,11 +133,43 @@ export const LogsSection = observer(({ rolloutId }: LogsSectionProps) => { } }, [rolloutId, selectedLevel]); - // Auto-scroll to bottom whenever logs update useEffect(() => { const el = scrollContainerRef.current; - if (!el) return; + + if (!el) { + isAtBottomRef.current = true; + return; + } + + const handleScroll = () => { + const distanceFromBottom = + el.scrollHeight - el.scrollTop - el.clientHeight; + isAtBottomRef.current = distanceFromBottom <= 8; + }; + + el.addEventListener("scroll", handleScroll); + handleScroll(); + + return () => { + el.removeEventListener("scroll", handleScroll); + }; + }, [logs.length]); + + // Auto-scroll to bottom when new logs arrive and user was already at bottom + useEffect(() => { + if (!shouldAutoScrollRef.current) { + return; + } + + const el = scrollContainerRef.current; + if (!el) { + shouldAutoScrollRef.current = false; + return; + } + el.scrollTo({ top: el.scrollHeight, behavior: "smooth" }); + shouldAutoScrollRef.current = false; + isAtBottomRef.current = true; }, [logs]); if (!rolloutId) { @@ -170,13 +220,13 @@ export const LogsSection = observer(({ rolloutId }: LogsSectionProps) => { {logs.length > 0 && (
-
+
{logs.map((log, index) => (