117 lines
3.4 KiB
Vue
117 lines
3.4 KiB
Vue
<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>
|