@@ -9,6 +9,10 @@ import { getApiUrl } from "../config";
99import Select from "./Select" ;
1010import Button from "./Button" ;
1111
12+ const haveLogsChanged = ( prevLogs : LogEntry [ ] , nextLogs : LogEntry [ ] ) => {
13+ return prevLogs . length !== nextLogs . length ;
14+ } ;
15+
1216interface LogsSectionProps {
1317 rolloutId ?: string ;
1418}
@@ -19,6 +23,8 @@ export const LogsSection = observer(({ rolloutId }: LogsSectionProps) => {
1923 const [ error , setError ] = useState < string | null > ( null ) ;
2024 const [ selectedLevel , setSelectedLevel ] = useState < string > ( "" ) ;
2125 const scrollContainerRef = useRef < HTMLDivElement | null > ( null ) ;
26+ const isAtBottomRef = useRef ( true ) ;
27+ const shouldAutoScrollRef = useRef ( false ) ;
2228
2329 const fetchLogs = async ( isInitialLoad = false ) => {
2430 if ( ! rolloutId ) return ;
@@ -93,7 +99,19 @@ export const LogsSection = observer(({ rolloutId }: LogsSectionProps) => {
9399 const data : LogsResponse = LogsResponseSchema . parse (
94100 await response . json ( )
95101 ) ;
96- setLogs ( data . logs ) ;
102+ setLogs ( ( prevLogs ) => {
103+ const hasChanges = haveLogsChanged ( prevLogs , data . logs ) ;
104+
105+ if ( ! hasChanges ) {
106+ return prevLogs ;
107+ }
108+
109+ if ( isAtBottomRef . current ) {
110+ shouldAutoScrollRef . current = true ;
111+ }
112+
113+ return data . logs ;
114+ } ) ;
97115 } catch ( err ) {
98116 if ( err instanceof Error && err . message . includes ( "Unexpected token" ) ) {
99117 setError (
@@ -115,11 +133,43 @@ export const LogsSection = observer(({ rolloutId }: LogsSectionProps) => {
115133 }
116134 } , [ rolloutId , selectedLevel ] ) ;
117135
118- // Auto-scroll to bottom whenever logs update
119136 useEffect ( ( ) => {
120137 const el = scrollContainerRef . current ;
121- if ( ! el ) return ;
138+
139+ if ( ! el ) {
140+ isAtBottomRef . current = true ;
141+ return ;
142+ }
143+
144+ const handleScroll = ( ) => {
145+ const distanceFromBottom =
146+ el . scrollHeight - el . scrollTop - el . clientHeight ;
147+ isAtBottomRef . current = distanceFromBottom <= 8 ;
148+ } ;
149+
150+ el . addEventListener ( "scroll" , handleScroll ) ;
151+ handleScroll ( ) ;
152+
153+ return ( ) => {
154+ el . removeEventListener ( "scroll" , handleScroll ) ;
155+ } ;
156+ } , [ logs . length ] ) ;
157+
158+ // Auto-scroll to bottom when new logs arrive and user was already at bottom
159+ useEffect ( ( ) => {
160+ if ( ! shouldAutoScrollRef . current ) {
161+ return ;
162+ }
163+
164+ const el = scrollContainerRef . current ;
165+ if ( ! el ) {
166+ shouldAutoScrollRef . current = false ;
167+ return ;
168+ }
169+
122170 el . scrollTo ( { top : el . scrollHeight , behavior : "smooth" } ) ;
171+ shouldAutoScrollRef . current = false ;
172+ isAtBottomRef . current = true ;
123173 } , [ logs ] ) ;
124174
125175 if ( ! rolloutId ) {
@@ -170,13 +220,13 @@ export const LogsSection = observer(({ rolloutId }: LogsSectionProps) => {
170220 { logs . length > 0 && (
171221 < div
172222 ref = { scrollContainerRef }
173- className = "max-h-[800px] min-h-4 overflow-auto border border-gray-200"
223+ className = "max-h-[800px] min-h-4 overflow-auto border border-gray-200 bg-white "
174224 >
175- < div >
225+ < div className = "min-w-max" >
176226 { logs . map ( ( log , index ) => (
177227 < div
178228 key = { index }
179- className = { `text-xs px-3 py-1 border-b border-gray-200 last:border-b-0 ${
229+ className = { `w-full text-xs px-3 py-1 border-b border-gray-200 last:border-b-0 ${
180230 index % 2 === 0 ? "bg-white" : "bg-gray-50"
181231 } `}
182232 >
0 commit comments