kin: auto-commit after pipeline
This commit is contained in:
parent
248934d5d7
commit
939a30a3de
6 changed files with 796 additions and 3 deletions
117
web/frontend/src/components/LiveConsole.vue
Normal file
117
web/frontend/src/components/LiveConsole.vue
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch, onUnmounted } from 'vue'
|
||||
import { api, type PipelineLog } from '../api'
|
||||
|
||||
const props = defineProps<{
|
||||
pipelineId: string
|
||||
pipelineStatus: string
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const logs = ref<PipelineLog[]>([])
|
||||
const error = ref('')
|
||||
const consoleEl = ref<HTMLElement | null>(null)
|
||||
let sinceId = 0
|
||||
let userScrolled = false
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const MAX_LOGS = 500
|
||||
|
||||
function levelClass(level: PipelineLog['level']): string {
|
||||
switch (level) {
|
||||
case 'INFO': return 'text-gray-300'
|
||||
case 'DEBUG': return 'text-gray-500'
|
||||
case 'ERROR': return 'text-red-400'
|
||||
case 'WARN': return 'text-yellow-400'
|
||||
}
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
if (!consoleEl.value) return
|
||||
const { scrollTop, clientHeight, scrollHeight } = consoleEl.value
|
||||
userScrolled = scrollTop + clientHeight < scrollHeight - 50
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
if (!consoleEl.value || userScrolled) return
|
||||
consoleEl.value.scrollTop = consoleEl.value.scrollHeight
|
||||
}
|
||||
|
||||
async function fetchLogs() {
|
||||
try {
|
||||
const newLogs = await api.getPipelineLogs(props.pipelineId, sinceId)
|
||||
if (!newLogs.length) return
|
||||
sinceId = Math.max(...newLogs.map(l => l.id))
|
||||
logs.value = [...logs.value, ...newLogs].slice(-MAX_LOGS)
|
||||
// Scroll after DOM update
|
||||
setTimeout(scrollToBottom, 0)
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
if (timer) return
|
||||
timer = setInterval(async () => {
|
||||
await fetchLogs()
|
||||
if (props.pipelineStatus !== 'running' && props.pipelineStatus !== 'in_progress') {
|
||||
stopPolling()
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (timer) { clearInterval(timer); timer = null }
|
||||
}
|
||||
|
||||
async function toggle() {
|
||||
visible.value = !visible.value
|
||||
if (visible.value) {
|
||||
// Reset on open
|
||||
userScrolled = false
|
||||
await fetchLogs()
|
||||
if (props.pipelineStatus === 'running' || props.pipelineStatus === 'in_progress') {
|
||||
startPolling()
|
||||
}
|
||||
} else {
|
||||
stopPolling()
|
||||
}
|
||||
}
|
||||
|
||||
// When status changes while panel is open — do final fetch and stop
|
||||
watch(() => props.pipelineStatus, async (newStatus) => {
|
||||
if (!visible.value) return
|
||||
if (newStatus !== 'running' && newStatus !== 'in_progress') {
|
||||
stopPolling()
|
||||
await fetchLogs()
|
||||
} else {
|
||||
startPolling()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPolling()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-4">
|
||||
<button
|
||||
@click="toggle"
|
||||
class="text-xs text-gray-500 hover:text-gray-300 border border-gray-800 rounded px-3 py-1.5 bg-gray-900/50 hover:bg-gray-900 transition-colors"
|
||||
>
|
||||
{{ visible ? '▲ Скрыть лог' : '▼ Показать лог' }}
|
||||
</button>
|
||||
|
||||
<div v-show="visible" class="mt-2 bg-gray-950 border border-gray-800 rounded-lg p-4 font-mono text-xs max-h-[400px] overflow-y-auto" ref="consoleEl" @scroll="onScroll">
|
||||
<div v-if="!logs.length && !error" class="text-gray-600">Нет записей...</div>
|
||||
<div v-if="error" class="text-red-400">Ошибка: {{ error }}</div>
|
||||
<div v-for="log in logs" :key="log.id" class="mb-1">
|
||||
<span class="text-gray-600">{{ log.ts }}</span>
|
||||
<span :class="[levelClass(log.level), 'ml-2 font-semibold']">[{{ log.level }}]</span>
|
||||
<span :class="[levelClass(log.level), 'ml-2']">{{ log.message }}</span>
|
||||
<pre v-if="log.extra_json" class="mt-0.5 ml-4 text-gray-500 whitespace-pre-wrap">{{ JSON.stringify(log.extra_json, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Loading…
Add table
Add a link
Reference in a new issue