Skip to content

Commit 9ea2721

Browse files
added log autoscroll (#2017)
log: added auto-scroll
1 parent 020e9e1 commit 9ea2721

File tree

6 files changed

+252
-59
lines changed

6 files changed

+252
-59
lines changed

changes.d/2017.feat.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added option to set automatic scrolling in log view

src/components/cylc/log/Log.vue

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,31 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
-->
1717

1818
<template>
19-
<pre><span
20-
v-for="(log, index) in computedLogs"
21-
:key="index"
22-
:class="wordWrap ? 'text-pre-wrap' : 'text-pre'"
23-
>{{ log }}</span></pre>
19+
<div ref="wrapper">
20+
<pre data-cy="log-text"><span
21+
v-for="(log, index) in computedLogs"
22+
:key="index"
23+
:class="wordWrap ? 'text-pre-wrap' : 'text-pre'"
24+
>{{ log }}</span></pre>
25+
<v-btn
26+
v-if="logs.length"
27+
position="fixed"
28+
location="bottom right"
29+
class="ma-3"
30+
@click="scrollToTop"
31+
:icon="$options.icons.mdiMouseMoveUp"
32+
/>
33+
<!-- a div to use for autoscrolling -->
34+
<div ref="autoScrollEnd"></div>
35+
</div>
2436
</template>
2537

2638
<script>
27-
39+
import { useTemplateRef, watch, onBeforeUnmount } from 'vue'
40+
import { when } from '@/utils'
41+
import {
42+
mdiMouseMoveUp
43+
} from '@mdi/js'
2844
export default {
2945
name: 'LogComponent',
3046
@@ -47,11 +63,48 @@ export default {
4763
required: false,
4864
default: false
4965
},
66+
autoScroll: {
67+
type: Boolean,
68+
required: false,
69+
default: false
70+
}
5071
},
5172
52-
data () {
73+
setup (props) {
74+
const wrapperRef = useTemplateRef('wrapper')
75+
const autoScrollEndRef = useTemplateRef('autoScrollEnd')
76+
77+
function scrollToEnd () {
78+
autoScrollEndRef.value?.scrollIntoView({ behavior: 'smooth' })
79+
}
80+
81+
function scrollToTop () {
82+
wrapperRef.value?.scrollIntoView({ behavior: 'smooth' })
83+
}
84+
85+
const ro = new ResizeObserver(scrollToEnd)
86+
87+
when(wrapperRef, () => {
88+
watch(
89+
() => props.autoScroll,
90+
(val) => {
91+
if (val) {
92+
scrollToEnd()
93+
ro.observe(wrapperRef.value)
94+
} else {
95+
ro.disconnect()
96+
}
97+
},
98+
{ immediate: true }
99+
)
100+
})
101+
102+
onBeforeUnmount(() => {
103+
ro.disconnect()
104+
})
105+
53106
return {
54-
match: ''
107+
scrollToTop
55108
}
56109
},
57110
@@ -78,12 +131,13 @@ export default {
78131
79132
stripTimestamp (logLine) {
80133
const regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+-][\d:]+)?\s(.*\s*)/
81-
this.match = logLine.match(regex)
82-
if (this.match) {
83-
return this.match[1]
84-
}
85-
return logLine
134+
return logLine.match(regex)?.[1] ?? logLine
86135
}
136+
},
137+
138+
// Misc options
139+
icons: {
140+
mdiMouseMoveUp
87141
}
88142
}
89143

src/views/Log.vue

Lines changed: 72 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -142,17 +142,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
142142
</v-container>
143143

144144
<!-- the log file viewer -->
145-
<v-row
145+
<div
146+
ref="logScroll"
146147
no-gutters
147-
class="overflow-auto px-4 pb-2"
148+
class="h-100 overflow-auto px-4 pb-2"
148149
>
149-
<v-col>
150-
<v-skeleton-loader
151-
v-if="id && file && results.connected == null"
152-
type="text@5"
153-
class="mx-n4 align-content-start"
154-
/>
155-
<template v-else>
150+
<v-skeleton-loader
151+
v-if="id && file && results.connected == null"
152+
type="text@5"
153+
class="mx-n4 align-content-start"
154+
/>
155+
<template v-else >
156+
<div class="d-flex flex-column justify-center">
156157
<v-alert
157158
v-if="results.error"
158159
type="error"
@@ -170,24 +171,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
170171
:logs="results.lines"
171172
:timestamps="timestamps"
172173
:word-wrap="wordWrap"
174+
:autoScroll="autoScroll"
173175
/>
174-
</template>
175-
</v-col>
176-
</v-row>
176+
</div>
177+
</template>
178+
</div>
177179
</v-container>
178180
</template>
179181

180182
<script>
181-
import { ref, computed } from 'vue'
182-
import { usePrevious, whenever } from '@vueuse/core'
183+
import { ref, computed, useTemplateRef } from 'vue'
184+
import { usePrevious, useScroll, whenever } from '@vueuse/core'
183185
import { useStore } from 'vuex'
184186
import {
185187
mdiClockOutline,
186-
mdiFileAlertOutline,
187188
mdiFolderRefresh,
188189
mdiPowerPlugOff,
189190
mdiPowerPlug,
190191
mdiWrap,
192+
mdiFileAlertOutline,
193+
mdiMouseMoveDown,
191194
} from '@mdi/js'
192195
import { btnProps } from '@/utils/viewToolbar'
193196
import graphqlMixin from '@/mixins/graphql'
@@ -380,6 +383,26 @@ export default {
380383
relativeID.value = value
381384
}, 500)
382385
386+
/** AutoScroll? */
387+
const autoScroll = useInitialOptions('autoScroll', { props, emit }, true)
388+
const logScrollEl = useTemplateRef('logScroll')
389+
const { arrivedState, directions } = useScroll(logScrollEl)
390+
// Turn on autoscroll when user scrolls to bottom:
391+
whenever(() => arrivedState.bottom && !arrivedState.top, () => {
392+
// (when page first loads both top and bottom are true)
393+
autoScroll.value = true
394+
})
395+
// Turn off autoscroll when user scrolls up:
396+
whenever(() => directions.top, () => {
397+
if (results.value.lines.length) {
398+
autoScroll.value = false
399+
}
400+
})
401+
// When autoscroll is turned off, cancel any smooth scroll in progress:
402+
whenever(() => !autoScroll.value, () => {
403+
logScrollEl.value?.scrollBy(0, 0)
404+
})
405+
383406
/** View toolbar button size */
384407
const toolbarBtnSize = '40'
385408
@@ -402,36 +425,12 @@ export default {
402425
jobLog: ref(relativeID.value == null ? 0 : 1),
403426
timestamps,
404427
wordWrap,
428+
autoScroll,
405429
reset,
406430
debouncedUpdateRelativeID,
407431
toolbarBtnSize,
408432
toolbarBtnProps: btnProps(toolbarBtnSize),
409-
}
410-
},
411-
412-
data () {
413-
return {
414-
controlGroups: [
415-
{
416-
title: 'Log',
417-
controls: [
418-
{
419-
title: 'Timestamps',
420-
icon: mdiClockOutline,
421-
action: 'toggle',
422-
value: this.timestamps,
423-
key: 'timestamps'
424-
},
425-
{
426-
title: 'Word wrap',
427-
icon: mdiWrap,
428-
action: 'toggle',
429-
value: this.wordWrap,
430-
key: 'wordWrap',
431-
},
432-
]
433-
}
434-
],
433+
logScrollEl
435434
}
436435
},
437436
@@ -474,6 +473,36 @@ export default {
474473
}
475474
}
476475
return this.workflowId
476+
},
477+
controlGroups () {
478+
return [
479+
{
480+
title: 'Log',
481+
controls: [
482+
{
483+
title: 'Timestamps',
484+
icon: mdiClockOutline,
485+
action: 'toggle',
486+
value: this.timestamps,
487+
key: 'timestamps'
488+
},
489+
{
490+
title: 'Word wrap',
491+
icon: mdiWrap,
492+
action: 'toggle',
493+
value: this.wordWrap,
494+
key: 'wordWrap',
495+
},
496+
{
497+
title: 'Auto scroll',
498+
icon: mdiMouseMoveDown,
499+
action: 'toggle',
500+
value: this.autoScroll,
501+
key: 'autoScroll',
502+
},
503+
]
504+
}
505+
]
477506
}
478507
},
479508
@@ -568,15 +597,15 @@ export default {
568597
this.file = null
569598
// go back to last chosen job if we are switching back to job logs
570599
this.relativeID = val ? this.previousRelativeID : null
571-
},
600+
}
572601
},
573602
574603
// Misc options
575604
icons: {
576-
mdiFileAlertOutline,
577605
mdiFolderRefresh,
578606
mdiPowerPlug,
579607
mdiPowerPlugOff,
608+
mdiFileAlertOutline
580609
}
581610
}
582611
</script>

0 commit comments

Comments
 (0)