Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions ui/src/composables/use-follow-bottom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useScroll, useEventListener } from '@vueuse/core'

/**
* Stick-to-bottom autoscroll for a live run log: follows new entries while the
* user is at the bottom, any upward scroll or wheel-up stops it, scrolling back
* to the bottom resumes.
*
* Targets the current document — what actually scrolls both standalone and
* inside data-fair's fixed-height d-frame iframe (never window.top). VueUse's
* `arrivedState.bottom` carries a built-in 1px tolerance, and appending a log
* fires no scroll event, so following only ever turns off on a real upward
* scroll — no manual threshold needed.
*
* @param logCount reactive getter for the log length (the growth signal)
* @param isActive getter telling whether the run is still streaming
*/
export const useFollowBottom = (logCount: () => number, isActive: () => boolean) => {
// Start following so a freshly opened, still-running run pins to its tail even
// though the page loads scrolled to the top.
const following = ref(true)

const { arrivedState, directions, y } = useScroll(window, {
onScroll: () => {
if (directions.top) following.value = false // scrolled up → stop
else if (arrivedState.bottom) following.value = true // back at bottom → resume
}
})

// A wheel-up reaches us even when the page can't scroll (short page, or
// data-fair's auto-height embed where the parent scrolls) — the only
// "stop following" signal available there.
useEventListener(window, 'wheel', (e: WheelEvent) => { if (e.deltaY < 0) following.value = false }, { passive: true })

const pinToBottom = () => { y.value = (document.scrollingElement ?? document.documentElement).scrollHeight }

watch(logCount, () => { if (isActive() && following.value) pinToBottom() }, { flush: 'post' })

return { following }
}
17 changes: 16 additions & 1 deletion ui/src/pages/processings/[id]/runs/[runId].vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
/>
<v-expansion-panels
v-else
:model-value="[steps.length - 1]"
v-model="openPanels"
variant="accordion"
multiple
static
Expand Down Expand Up @@ -61,6 +61,7 @@

<script setup lang="ts">
import type { Run } from '#api/types'
import { useFollowBottom } from '~/composables/use-follow-bottom'

const { t } = useI18n()
const route = useRoute<'/processings/[id]/runs/[runId]'>()
Expand Down Expand Up @@ -98,6 +99,20 @@ const steps = computed(() => {
return steps
})

const { following } = useFollowBottom(
() => run.value?.log.length ?? 0,
() => run.value?.status === 'running'
)

// While following the tail keep only the latest step open; otherwise preserve the
// user's selection. Recomputed only when the step count changes, not on every log.
const openPanels = ref<number[]>([])
watch(() => steps.value.length, (stepCount) => {
if (stepCount <= 0) openPanels.value = []
else if (following.value) openPanels.value = [stepCount - 1]
else openPanels.value = openPanels.value.filter((i) => i < stepCount)
}, { immediate: true })

function getColor (step: Record<string, any>) {
let color = 'success'

Expand Down
Loading