@@ -38,6 +38,11 @@ function parseLineCommand(line: LogLine): LogLineCommand | null {
3838 return null ;
3939}
4040
41+ function isLogElementInViewport(el : HTMLElement ): boolean {
42+ const rect = el .getBoundingClientRect ();
43+ return rect .top >= 0 && rect .bottom <= window .innerHeight ; // only check height but not width
44+ }
45+
4146const sfc = {
4247 name: ' RepoActionView' ,
4348 components: {
@@ -142,9 +147,14 @@ const sfc = {
142147 },
143148
144149 methods: {
145- // get the active container element, either the `job-step-logs` or the `job-log-list` in the `job-log-group`
146- getLogsContainer(stepIndex : number ) {
147- const el = this .$refs .logs [stepIndex ];
150+ // get the job step logs container ('.job-step-logs')
151+ getJobStepLogsContainer(stepIndex : number ): HTMLElement {
152+ return this .$refs .logs [stepIndex ];
153+ },
154+
155+ // get the active logs container element, either the `job-step-logs` or the `job-log-list` in the `job-log-group`
156+ getActiveLogsContainer(stepIndex : number ): HTMLElement {
157+ const el = this .getJobStepLogsContainer (stepIndex );
148158 return el ._stepLogsActiveContainer ?? el ;
149159 },
150160 // begin a log group
@@ -217,14 +227,15 @@ const sfc = {
217227 );
218228 },
219229
220- appendLogs (stepIndex : number , startTime : number , logLines : LogLine []) {
221- // position of the client view relative to the website top
222- const clientHeight = document . documentElement . clientHeight + window . scrollY ;
223- // height of the logs container relative to the website top
224- const logsContainerHeight = this . $refs . stepsContainer . getBoundingClientRect (). bottom + window . scrollY ;
230+ shouldAutoScroll (stepIndex : number ) : boolean {
231+ const el = this . getJobStepLogsContainer ( stepIndex );
232+ if ( ! el . lastChild ) return false ;
233+ return isLogElementInViewport ( el . lastChild );
234+ },
225235
236+ appendLogs(stepIndex : number , startTime : number , logLines : LogLine []) {
226237 for (const line of logLines ) {
227- const el = this .getLogsContainer (stepIndex );
238+ const el = this .getActiveLogsContainer (stepIndex );
228239 const cmd = parseLineCommand (line );
229240 if (cmd ?.name === ' group' ) {
230241 this .beginLogGroup (stepIndex , startTime , line , cmd );
@@ -235,12 +246,6 @@ const sfc = {
235246 }
236247 el .append (this .createLogLine (stepIndex , startTime , line ));
237248 }
238-
239- // scrolls to the bottom if job is running and the bottom of the logs container is visible
240- if (! this .run .done && logLines .length && clientHeight >= logsContainerHeight ) {
241- const newLogsContainerHeight = this .$refs .stepsContainer .getBoundingClientRect ().bottom + window .scrollY ;
242- window .scrollTo ({top: clientHeight + (newLogsContainerHeight - logsContainerHeight ), behavior: ' smooth' });
243- }
244249 },
245250
246251 async deleteArtifact(name : string ) {
@@ -289,13 +294,30 @@ const sfc = {
289294 this .currentJobStepsStates [i ] = {cursor: null , expanded: false };
290295 }
291296 }
297+
298+ // find the step indexes that need to auto-scroll
299+ const autoScrollStepIndexes = new Map <number , boolean >();
300+ for (const logs of job .logs .stepsLog ?? []) {
301+ if (autoScrollStepIndexes .has (logs .step )) continue ;
302+ autoScrollStepIndexes .set (logs .step , this .shouldAutoScroll (logs .step ));
303+ }
304+
292305 // append logs to the UI
293306 for (const logs of job .logs .stepsLog ?? []) {
294307 // save the cursor, it will be passed to backend next time
295308 this .currentJobStepsStates [logs .step ].cursor = logs .cursor ;
296309 this .appendLogs (logs .step , logs .started , logs .lines );
297310 }
298311
312+ // auto-scroll to the last log line of the last step
313+ let autoScrollJobStepElement: HTMLElement ;
314+ for (let stepIndex = 0 ; stepIndex < this .currentJob .steps .length ; stepIndex ++ ) {
315+ if (! autoScrollStepIndexes .get (stepIndex )) continue ;
316+ autoScrollJobStepElement = this .getJobStepLogsContainer (stepIndex );
317+ }
318+ autoScrollJobStepElement ?.lastElementChild .scrollIntoView ({behavior: ' smooth' , block: ' nearest' });
319+
320+ // clear the interval timer if the job is done
299321 if (this .run .done && this .intervalID ) {
300322 clearInterval (this .intervalID );
301323 this .intervalID = null ;
@@ -478,7 +500,7 @@ export function initRepositoryActionView() {
478500 </div >
479501 </div >
480502
481- <div class =" action-view-right" ref = " stepsContainer " >
503+ <div class =" action-view-right" >
482504 <div class =" job-info-header" >
483505 <div class =" job-info-header-left gt-ellipsis" >
484506 <h3 class =" job-info-header-title gt-ellipsis" >
0 commit comments