@ -2,7 +2,7 @@
import { SvgIcon } from '../svg.ts' ;
import ActionRunStatus from './ActionRunStatus.vue' ;
import { defineComponent , type PropType } from 'vue' ;
import { createElementFromAttrs, toggleElem } from '../utils/dom.ts' ;
import { addDelegatedEventListener, createElementFromAttrs, toggleElem } from '../utils/dom.ts' ;
import { formatDatetime } from '../utils/time.ts' ;
import { renderAnsi } from '../render/ansi.ts' ;
import { POST , DELETE } from '../modules/fetch.ts' ;
@ -40,6 +40,12 @@ type Step = {
status : RunStatus ,
}
type JobStepState = {
cursor : string | null ,
expanded : boolean ,
manuallyCollapsed : boolean , / / w h e t h e r t h e u s e r m a n u a l l y c o l l a p s e d t h e s t e p , u s e d t o a v o i d a u t o - e x p a n d i n g i t a g a i n
}
function parseLineCommand ( line : LogLine ) : LogLineCommand | null {
for ( const prefix of LogLinePrefixesGroup ) {
if ( line . message . startsWith ( prefix ) ) {
@ -54,9 +60,10 @@ function parseLineCommand(line: LogLine): LogLineCommand | null {
return null ;
}
function isLogElementInViewport ( el : Element ): boolean {
function isLogElementInViewport ( el : Element , { extraViewPortHeight } = { extraViewPortHeight : 0 } ): boolean {
const rect = el . getBoundingClientRect ( ) ;
return rect . top >= 0 && rect . bottom <= window . innerHeight ; / / o n l y c h e c k h e i g h t b u t n o t w i d t h
/ / o n l y c h e c k w h e t h e r b o t t o m i s i n v i e w p o r t , b e c a u s e t h e l o g e l e m e n t c a n b e a l o g g r o u p w h i c h i s u s u a l l y t a l l
return 0 <= rect . bottom && rect . bottom <= window . innerHeight + extraViewPortHeight ;
}
type LocaleStorageOptions = {
@ -104,7 +111,7 @@ export default defineComponent({
/ / i n t e r n a l s t a t e
loadingAbortController : null as AbortController | null ,
intervalID : null as IntervalId | null ,
currentJobStepsStates : [ ] as Array < Record< string , any > >,
currentJobStepsStates : [ ] as Array < JobStepState >,
artifacts : [ ] as Array < Record < string , any > > ,
menuVisible : false ,
isFullScreen : false ,
@ -181,6 +188,19 @@ export default defineComponent({
/ / l o a d j o b d a t a a n d t h e n a u t o - r e l o a d p e r i o d i c a l l y
/ / n e e d t o a w a i t f i r s t l o a d J o b s o t h i s . c u r r e n t J o b S t e p s S t a t e s i s i n i t i a l i z e d a n d c a n b e u s e d i n h a s h C h a n g e L i s t e n e r
await this . loadJob ( ) ;
/ / a u t o - s c r o l l t o t h e b o t t o m o f t h e l o g g r o u p w h e n i t i s o p e n e d
/ / " t o g g l e " e v e n t d o e s n ' t b u b b l e , s o w e n e e d t o u s e ' c l i c k ' e v e n t d e l e g a t i o n t o h a n d l e i t
addDelegatedEventListener ( this . elStepsContainer ( ) , 'click' , 'summary.job-log-group-summary' , ( el , _ ) => {
if ( ! this . optionAlwaysAutoScroll ) return ;
const elJobLogGroup = el . closest ( 'details.job-log-group' ) as HTMLDetailsElement ;
setTimeout ( ( ) => {
if ( elJobLogGroup . open && ! isLogElementInViewport ( elJobLogGroup ) ) {
elJobLogGroup . scrollIntoView ( { behavior : 'smooth' , block : 'end' } ) ;
}
} , 0 ) ;
} ) ;
this . intervalID = setInterval ( ( ) => this . loadJob ( ) , 1000 ) ;
document . body . addEventListener ( 'click' , this . closeDropdown ) ;
this . hashChangeListener ( ) ;
@ -252,6 +272,8 @@ export default defineComponent({
this . currentJobStepsStates [ idx ] . expanded = ! this . currentJobStepsStates [ idx ] . expanded ;
if ( this . currentJobStepsStates [ idx ] . expanded ) {
this . loadJobForce ( ) ; / / t r y t o l o a d t h e d a t a i m m e d i a t e l y i n s t e a d o f w a i t i n g f o r n e x t t i m e r i n t e r v a l
} else if ( this . currentJob . steps [ idx ] . status === 'running' ) {
this . currentJobStepsStates [ idx ] . manuallyCollapsed = true ;
}
} ,
/ / c a n c e l a r u n
@ -293,7 +315,8 @@ export default defineComponent({
const el = this . getJobStepLogsContainer ( stepIndex ) ;
/ / i f t h e l o g s c o n t a i n e r i s e m p t y , t h e n a u t o - s c r o l l i f t h e s t e p i s e x p a n d e d
if ( ! el . lastChild ) return this . currentJobStepsStates [ stepIndex ] . expanded ;
return isLogElementInViewport ( el . lastChild as Element ) ;
/ / u s e e x t r a V i e w P o r t H e i g h t t o t o l e r a t e s o m e e x t r a " v i r t u a l v i e w p o r t " h e i g h t ( f o r e x a m p l e : t h e l a s t l i n e i s p a r t i a l l y v i s i b l e )
return isLogElementInViewport ( el . lastChild as Element , { extraViewPortHeight : 5 } ) ;
} ,
appendLogs ( stepIndex : number , startTime : number , logLines : LogLine [ ] ) {
@ -343,7 +366,6 @@ export default defineComponent({
const abortController = new AbortController ( ) ;
this . loadingAbortController = abortController ;
try {
const isFirstLoad = ! this . run . status ;
const job = await this . fetchJobData ( abortController ) ;
if ( this . loadingAbortController !== abortController ) return ;
@ -353,10 +375,15 @@ export default defineComponent({
/ / s y n c t h e c u r r e n t J o b S t e p s S t a t e s t o s t o r e t h e j o b s t e p s t a t e s
for ( let i = 0 ; i < this . currentJob . steps . length ; i ++ ) {
const expanded = isFirstLoad && this . optionAlwaysExpandRunning && this . currentJob . steps [ i ] . status === 'running' ;
const autoExpand = this . optionAlwaysExpandRunning && this . currentJob . steps [ i ] . status === 'running' ;
if ( ! this . currentJobStepsStates [ i ] ) {
/ / i n i t i a l s t a t e s f o r j o b s t e p s
this . currentJobStepsStates [ i ] = { cursor : null , expanded } ;
this . currentJobStepsStates [ i ] = { cursor : null , expanded : autoExpand , manuallyCollapsed : false } ;
} else {
/ / i f t h e s t e p i s n o t m a n u a l l y c o l l a p s e d b y u s e r , t h e n a u t o - e x p a n d i t i f o p t i o n i s e n a b l e d
if ( autoExpand && ! this . currentJobStepsStates [ i ] . manuallyCollapsed ) {
this . currentJobStepsStates [ i ] . expanded = true ;
}
}
}
@ -380,7 +407,10 @@ export default defineComponent({
if ( ! autoScrollStepIndexes . get ( stepIndex ) ) continue ;
autoScrollJobStepElement = this . getJobStepLogsContainer ( stepIndex ) ;
}
autoScrollJobStepElement ? . lastElementChild . scrollIntoView ( { behavior : 'smooth' , block : 'nearest' } ) ;
const lastLogElem = autoScrollJobStepElement ? . lastElementChild ;
if ( lastLogElem && ! isLogElementInViewport ( lastLogElem ) ) {
lastLogElem . scrollIntoView ( { behavior : 'smooth' , block : 'end' } ) ;
}
/ / c l e a r t h e i n t e r v a l t i m e r i f t h e j o b i s d o n e
if ( this . run . done && this . intervalID ) {
@ -408,9 +438,13 @@ export default defineComponent({
if ( this . menuVisible ) this . menuVisible = false ;
} ,
elStepsContainer ( ) : HTMLElement {
return this . $refs . stepsContainer as HTMLElement ;
} ,
toggleTimeDisplay ( type : 'seconds' | 'stamp' ) {
this . timeVisible [ ` log-time- ${ type } ` ] = ! this . timeVisible [ ` log-time- ${ type } ` ] ;
for ( const el of ( this . $refs . steps as HTMLElement ) . querySelectorAll ( ` .log-time- ${ type } ` ) ) {
for ( const el of this . elStepsContainer ( ) . querySelectorAll ( ` .log-time- ${ type } ` ) ) {
toggleElem ( el , this . timeVisible [ ` log-time- ${ type } ` ] ) ;
}
} ,
@ -419,6 +453,7 @@ export default defineComponent({
this . isFullScreen = ! this . isFullScreen ;
toggleFullScreen ( '.action-view-right' , this . isFullScreen , '.action-view-body' ) ;
} ,
async hashChangeListener ( ) {
const selectedLogStep = window . location . hash ;
if ( ! selectedLogStep ) return ;
@ -431,7 +466,7 @@ export default defineComponent({
/ / s o l o g l i n e c a n b e s e l e c t e d b y q u e r y S e l e c t o r
await this . loadJob ( ) ;
}
const logLine = ( this . $refs . steps as HTMLElement ) . querySelector ( selectedLogStep ) ;
const logLine = this . elStepsContainer ( ) . querySelector ( selectedLogStep ) ;
if ( ! logLine ) return ;
logLine . querySelector < HTMLAnchorElement > ( '.line-num' ) . click ( ) ;
} ,
@ -566,7 +601,7 @@ export default defineComponent({
< / div >
< / div >
< / div >
< div class = "job-step-container" ref = "steps " v-if ="currentJob.steps.length" >
< div class = "job-step-container" ref = "steps Container " v-if ="currentJob.steps.length" >
< div class = "job-step-section" v-for ="(jobStep, i) in currentJob.steps" :key ="i" >
< div class = "job-step-summary" @ click.stop = " isExpandable ( jobStep.status ) & & toggleStepLogs ( i ) " : class = "[currentJobStepsStates[i].expanded ? 'selected' : '', isExpandable(jobStep.status) && 'step-expandable']" >
<!-- If the job is done and the job step log is loaded for the first time , show the loading icon