@@ -176,23 +176,45 @@ export function useStepsPoller(props: RunPollerProps) {
176176 run . stages = refreshStagesFromSteps ( run . stages , steps ) ;
177177
178178 const [ openStageId , setOpenStageId ] = useState ( "" ) ;
179+ const openStage = useMemo ( ( ) => {
180+ const findStage = ( stages : StageInfo [ ] ) : StageInfo | null => {
181+ for ( const stage of stages ) {
182+ if ( String ( stage . id ) === openStageId ) return stage ;
183+ if ( stage . children . length > 0 ) {
184+ const result = findStage ( stage . children ) ;
185+ if ( result ) return result ;
186+ }
187+ }
188+ return null ;
189+ } ;
190+ return openStageId ? findStage ( run . stages ) : null ;
191+ } , [ run . stages , openStageId ] ) ;
179192 const [ expandedSteps , setExpandedSteps ] = useState < string [ ] > ( [ ] ) ;
180193 const collapsedSteps = useRef ( new Set < string > ( ) ) ;
181- const currentDefaultStep = useRef ( "" ) ;
194+ const tailStep = useRef ( "" ) ;
182195
183196 const [ tailLogs , setTailLogs ] = useState ( true ) ;
197+ const [ tailStage , setTailStage ] = useState ( "" ) ;
184198 // Make the latest tailLogs state available without a re-render.
185199 const tailLogsRef = useRef ( tailLogs ) ;
186200 const startTailingLogs = useCallback ( ( ) => {
187201 history . replaceState ( { } , "" , "?" ) ; // Unset query parameters.
188202 scrollToStepOnce . current = "" ; // Unset from manually selected node.
189203 tailLogsRef . current = true ;
190204 setTailLogs ( true ) ;
191- } , [ ] ) ;
205+ if ( openStage ?. state === "running" ) {
206+ // Keep tailing in the current stage.
207+ setTailStage ( openStageId ) ;
208+ } else {
209+ // Select the next best stage using the default step.
210+ setTailStage ( "" ) ;
211+ }
212+ } , [ openStageId , openStage ?. state ] ) ;
192213 const stopTailingLogs = useCallback ( ( ) => {
193214 scrollToStepOnce . current = "" ;
194215 tailLogsRef . current = false ;
195216 setTailLogs ( false ) ;
217+ setTailStage ( "" ) ;
196218 } , [ ] ) ;
197219
198220 // "scroll" events do not have a flag for telling "user has scrolled" vs programmatic "element.scrollIntoView()" apart.
@@ -241,7 +263,7 @@ export function useStepsPoller(props: RunPollerProps) {
241263 const scrollToTail = useCallback (
242264 ( stepId : string , element : HTMLDivElement ) => {
243265 const scrollDefaultStep =
244- tailLogsRef . current && stepId === currentDefaultStep . current ;
266+ tailLogsRef . current && stepId === tailStep . current ;
245267 if ( ! scrollDefaultStep ) {
246268 if ( stepId !== scrollToStepOnce . current ) return ;
247269 const stepBuffer = stepBuffersRef . current . get ( stepId ) ;
@@ -325,32 +347,41 @@ export function useStepsPoller(props: RunPollerProps) {
325347 } , [ steps , expandLastStageStep , stopTailingLogs ] ) ;
326348
327349 useEffect ( ( ) => {
328- const defaultStep = getDefaultSelectedStep ( steps , runIsComplete ) ;
350+ let defaultStep ;
351+ if ( tailStage ) {
352+ defaultStep = steps . filter ( ( s ) => s . stageId === tailStage ) . pop ( ) || null ;
353+ if (
354+ defaultStep &&
355+ defaultStep . state !== "running" &&
356+ stepBuffersRef . current . get ( defaultStep . id ) ?. stopTailing
357+ ) {
358+ // Tailed in full. Let the user resume tailing in another stage.
359+ stopTailingLogs ( ) ;
360+ }
361+ } else {
362+ defaultStep = getDefaultSelectedStep ( steps , runIsComplete ) ;
363+ }
329364 if ( ! defaultStep ) return ;
330- currentDefaultStep . current = defaultStep . id ;
365+ tailStep . current = defaultStep . id ;
331366 if ( ! tailLogsRef . current ) return ;
332367 setOpenStageId ( defaultStep . stageId ) ;
333368 if ( collapsedSteps . current . has ( defaultStep . id ) ) return ;
334369 setExpandedSteps ( ( prev ) => {
335370 if ( prev . includes ( defaultStep . id ) ) return prev ;
336371 return [ ...prev , defaultStep . id ] ;
337372 } ) ;
338- } , [ steps , tailLogs , runIsComplete ] ) ;
373+ } , [ steps , tailLogs , runIsComplete , tailStage , stopTailingLogs ] ) ;
339374
340- const handleStageSelect = useCallback (
341- ( nodeId : string ) => {
342- stopTailingLogs ( ) ;
343-
344- if ( ! nodeId ) return ;
345- if ( nodeId === openStageId ) return ; // skip if already selected
375+ const handleStageSelect = useCallback ( ( nodeId : string ) => {
376+ if ( ! nodeId ) return ;
346377
378+ setTailStage ( nodeId ) ;
379+ setOpenStageId ( ( openStageId ) => {
380+ if ( nodeId === openStageId ) return openStageId ; // skip if already selected
347381 history . replaceState ( { } , "" , `?selected-node=` + nodeId ) ;
348-
349- setOpenStageId ( nodeId ) ;
350- expandLastStageStep ( steps , nodeId ) ;
351- } ,
352- [ openStageId , steps , stopTailingLogs , expandLastStageStep ] ,
353- ) ;
382+ return nodeId ;
383+ } ) ;
384+ } , [ ] ) ;
354385
355386 const onStepToggle = useCallback (
356387 ( nodeId : string ) => {
@@ -372,20 +403,6 @@ export function useStepsPoller(props: RunPollerProps) {
372403 return steps . filter ( ( step ) => step . stageId === openStageId ) ;
373404 } , [ steps , openStageId ] ) ;
374405
375- const openStage = useMemo ( ( ) => {
376- const findStage = ( stages : StageInfo [ ] ) : StageInfo | null => {
377- for ( const stage of stages ) {
378- if ( String ( stage . id ) === openStageId ) return stage ;
379- if ( stage . children . length > 0 ) {
380- const result = findStage ( stage . children ) ;
381- if ( result ) return result ;
382- }
383- }
384- return null ;
385- } ;
386- return openStageId ? findStage ( run . stages ) : null ;
387- } , [ run . stages , openStageId ] ) ;
388-
389406 return {
390407 openStage,
391408 openStageSteps,
0 commit comments