2
2
import {SvgIcon } from ' ../svg.ts' ;
3
3
import ActionRunStatus from ' ./ActionRunStatus.vue' ;
4
4
import {defineComponent , type PropType } from ' vue' ;
5
- import {createElementFromAttrs , toggleElem } from ' ../utils/dom.ts' ;
5
+ import {addDelegatedEventListener , createElementFromAttrs , toggleElem } from ' ../utils/dom.ts' ;
6
6
import {formatDatetime } from ' ../utils/time.ts' ;
7
7
import {renderAnsi } from ' ../render/ansi.ts' ;
8
8
import {POST , DELETE } from ' ../modules/fetch.ts' ;
@@ -40,6 +40,12 @@ type Step = {
40
40
status: RunStatus ,
41
41
}
42
42
43
+ type JobStepState = {
44
+ cursor: string | null ,
45
+ expanded: boolean ,
46
+ manuallyCollapsed: boolean , // whether the user manually collapsed the step, used to avoid auto-expanding it again
47
+ }
48
+
43
49
function parseLineCommand(line : LogLine ): LogLineCommand | null {
44
50
for (const prefix of LogLinePrefixesGroup ) {
45
51
if (line .message .startsWith (prefix )) {
@@ -54,9 +60,10 @@ function parseLineCommand(line: LogLine): LogLineCommand | null {
54
60
return null ;
55
61
}
56
62
57
- function isLogElementInViewport(el : Element ): boolean {
63
+ function isLogElementInViewport(el : Element , { extraViewPortHeight } = {extraViewPortHeight: 0 } ): boolean {
58
64
const rect = el .getBoundingClientRect ();
59
- return rect .top >= 0 && rect .bottom <= window .innerHeight ; // only check height but not width
65
+ // only check whether bottom is in viewport, because the log element can be a log group which is usually tall
66
+ return 0 <= rect .bottom && rect .bottom <= window .innerHeight + extraViewPortHeight ;
60
67
}
61
68
62
69
type LocaleStorageOptions = {
@@ -104,7 +111,7 @@ export default defineComponent({
104
111
// internal state
105
112
loadingAbortController: null as AbortController | null ,
106
113
intervalID: null as IntervalId | null ,
107
- currentJobStepsStates: [] as Array <Record < string , any > >,
114
+ currentJobStepsStates: [] as Array <JobStepState >,
108
115
artifacts: [] as Array <Record <string , any >>,
109
116
menuVisible: false ,
110
117
isFullScreen: false ,
@@ -181,6 +188,19 @@ export default defineComponent({
181
188
// load job data and then auto-reload periodically
182
189
// need to await first loadJob so this.currentJobStepsStates is initialized and can be used in hashChangeListener
183
190
await this .loadJob ();
191
+
192
+ // auto-scroll to the bottom of the log group when it is opened
193
+ // "toggle" event doesn't bubble, so we need to use 'click' event delegation to handle it
194
+ addDelegatedEventListener (this .elStepsContainer (), ' click' , ' summary.job-log-group-summary' , (el , _ ) => {
195
+ if (! this .optionAlwaysAutoScroll ) return ;
196
+ const elJobLogGroup = el .closest (' details.job-log-group' ) as HTMLDetailsElement ;
197
+ setTimeout (() => {
198
+ if (elJobLogGroup .open && ! isLogElementInViewport (elJobLogGroup )) {
199
+ elJobLogGroup .scrollIntoView ({behavior: ' smooth' , block: ' end' });
200
+ }
201
+ }, 0 );
202
+ });
203
+
184
204
this .intervalID = setInterval (() => this .loadJob (), 1000 );
185
205
document .body .addEventListener (' click' , this .closeDropdown );
186
206
this .hashChangeListener ();
@@ -252,6 +272,8 @@ export default defineComponent({
252
272
this .currentJobStepsStates [idx ].expanded = ! this .currentJobStepsStates [idx ].expanded ;
253
273
if (this .currentJobStepsStates [idx ].expanded ) {
254
274
this .loadJobForce (); // try to load the data immediately instead of waiting for next timer interval
275
+ } else if (this .currentJob .steps [idx ].status === ' running' ) {
276
+ this .currentJobStepsStates [idx ].manuallyCollapsed = true ;
255
277
}
256
278
},
257
279
// cancel a run
@@ -293,7 +315,8 @@ export default defineComponent({
293
315
const el = this .getJobStepLogsContainer (stepIndex );
294
316
// if the logs container is empty, then auto-scroll if the step is expanded
295
317
if (! el .lastChild ) return this .currentJobStepsStates [stepIndex ].expanded ;
296
- return isLogElementInViewport (el .lastChild as Element );
318
+ // use extraViewPortHeight to tolerate some extra "virtual view port" height (for example: the last line is partially visible)
319
+ return isLogElementInViewport (el .lastChild as Element , {extraViewPortHeight: 5 });
297
320
},
298
321
299
322
appendLogs(stepIndex : number , startTime : number , logLines : LogLine []) {
@@ -343,7 +366,6 @@ export default defineComponent({
343
366
const abortController = new AbortController ();
344
367
this .loadingAbortController = abortController ;
345
368
try {
346
- const isFirstLoad = ! this .run .status ;
347
369
const job = await this .fetchJobData (abortController );
348
370
if (this .loadingAbortController !== abortController ) return ;
349
371
@@ -353,10 +375,15 @@ export default defineComponent({
353
375
354
376
// sync the currentJobStepsStates to store the job step states
355
377
for (let i = 0 ; i < this .currentJob .steps .length ; i ++ ) {
356
- const expanded = isFirstLoad && this .optionAlwaysExpandRunning && this .currentJob .steps [i ].status === ' running' ;
378
+ const autoExpand = this .optionAlwaysExpandRunning && this .currentJob .steps [i ].status === ' running' ;
357
379
if (! this .currentJobStepsStates [i ]) {
358
380
// initial states for job steps
359
- this .currentJobStepsStates [i ] = {cursor: null , expanded };
381
+ this .currentJobStepsStates [i ] = {cursor: null , expanded: autoExpand , manuallyCollapsed: false };
382
+ } else {
383
+ // if the step is not manually collapsed by user, then auto-expand it if option is enabled
384
+ if (autoExpand && ! this .currentJobStepsStates [i ].manuallyCollapsed ) {
385
+ this .currentJobStepsStates [i ].expanded = true ;
386
+ }
360
387
}
361
388
}
362
389
@@ -380,7 +407,10 @@ export default defineComponent({
380
407
if (! autoScrollStepIndexes .get (stepIndex )) continue ;
381
408
autoScrollJobStepElement = this .getJobStepLogsContainer (stepIndex );
382
409
}
383
- autoScrollJobStepElement ?.lastElementChild .scrollIntoView ({behavior: ' smooth' , block: ' nearest' });
410
+ const lastLogElem = autoScrollJobStepElement ?.lastElementChild ;
411
+ if (lastLogElem && ! isLogElementInViewport (lastLogElem )) {
412
+ lastLogElem .scrollIntoView ({behavior: ' smooth' , block: ' end' });
413
+ }
384
414
385
415
// clear the interval timer if the job is done
386
416
if (this .run .done && this .intervalID ) {
@@ -408,9 +438,13 @@ export default defineComponent({
408
438
if (this .menuVisible ) this .menuVisible = false ;
409
439
},
410
440
441
+ elStepsContainer(): HTMLElement {
442
+ return this .$refs .stepsContainer as HTMLElement ;
443
+ },
444
+
411
445
toggleTimeDisplay(type : ' seconds' | ' stamp' ) {
412
446
this .timeVisible [` log-time-${type } ` ] = ! this .timeVisible [` log-time-${type } ` ];
413
- for (const el of ( this .$refs . steps as HTMLElement ).querySelectorAll (` .log-time-${type } ` )) {
447
+ for (const el of this .elStepsContainer ( ).querySelectorAll (` .log-time-${type } ` )) {
414
448
toggleElem (el , this .timeVisible [` log-time-${type } ` ]);
415
449
}
416
450
},
@@ -419,6 +453,7 @@ export default defineComponent({
419
453
this .isFullScreen = ! this .isFullScreen ;
420
454
toggleFullScreen (' .action-view-right' , this .isFullScreen , ' .action-view-body' );
421
455
},
456
+
422
457
async hashChangeListener() {
423
458
const selectedLogStep = window .location .hash ;
424
459
if (! selectedLogStep ) return ;
@@ -431,7 +466,7 @@ export default defineComponent({
431
466
// so logline can be selected by querySelector
432
467
await this .loadJob ();
433
468
}
434
- const logLine = ( this .$refs . steps as HTMLElement ).querySelector (selectedLogStep );
469
+ const logLine = this .elStepsContainer ( ).querySelector (selectedLogStep );
435
470
if (! logLine ) return ;
436
471
logLine .querySelector <HTMLAnchorElement >(' .line-num' ).click ();
437
472
},
@@ -566,7 +601,7 @@ export default defineComponent({
566
601
</div >
567
602
</div >
568
603
</div >
569
- <div class =" job-step-container" ref =" steps " v-if =" currentJob.steps.length" >
604
+ <div class =" job-step-container" ref =" stepsContainer " v-if =" currentJob.steps.length" >
570
605
<div class =" job-step-section" v-for =" (jobStep, i) in currentJob.steps" :key =" i" >
571
606
<div class =" job-step-summary" @click.stop =" isExpandable(jobStep.status) && toggleStepLogs(i)" :class =" [currentJobStepsStates[i].expanded ? 'selected' : '', isExpandable(jobStep.status) && 'step-expandable']" >
572
607
<!-- If the job is done and the job step log is loaded for the first time, show the loading icon
0 commit comments