@@ -38,6 +38,11 @@ function parseLineCommand(line: LogLine): LogLineCommand | null {
38
38
return null ;
39
39
}
40
40
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
+
41
46
const sfc = {
42
47
name: ' RepoActionView' ,
43
48
components: {
@@ -142,9 +147,14 @@ const sfc = {
142
147
},
143
148
144
149
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 );
148
158
return el ._stepLogsActiveContainer ?? el ;
149
159
},
150
160
// begin a log group
@@ -217,9 +227,15 @@ const sfc = {
217
227
);
218
228
},
219
229
230
+ shouldAutoScroll(stepIndex : number ): boolean {
231
+ const el = this .getJobStepLogsContainer (stepIndex );
232
+ if (! el .lastChild ) return false ;
233
+ return isLogElementInViewport (el .lastChild );
234
+ },
235
+
220
236
appendLogs(stepIndex : number , startTime : number , logLines : LogLine []) {
221
237
for (const line of logLines ) {
222
- const el = this .getLogsContainer (stepIndex );
238
+ const el = this .getActiveLogsContainer (stepIndex );
223
239
const cmd = parseLineCommand (line );
224
240
if (cmd ?.name === ' group' ) {
225
241
this .beginLogGroup (stepIndex , startTime , line , cmd );
@@ -278,13 +294,30 @@ const sfc = {
278
294
this .currentJobStepsStates [i ] = {cursor: null , expanded: false };
279
295
}
280
296
}
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
+
281
305
// append logs to the UI
282
306
for (const logs of job .logs .stepsLog ?? []) {
283
307
// save the cursor, it will be passed to backend next time
284
308
this .currentJobStepsStates [logs .step ].cursor = logs .cursor ;
285
309
this .appendLogs (logs .step , logs .started , logs .lines );
286
310
}
287
311
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
288
321
if (this .run .done && this .intervalID ) {
289
322
clearInterval (this .intervalID );
290
323
this .intervalID = null ;
0 commit comments