153 lines
6.5 KiB
Svelte
153 lines
6.5 KiB
Svelte
<!-- [DEF:TaskLogViewer:Component] -->
|
|
<!--
|
|
@SEMANTICS: task, log, viewer, modal
|
|
@PURPOSE: Displays detailed logs for a specific task in a modal.
|
|
@LAYER: UI
|
|
@RELATION: USES -> frontend/src/lib/api.js (inferred)
|
|
-->
|
|
<script>
|
|
import { createEventDispatcher, onMount, onDestroy } from 'svelte';
|
|
import { getTaskLogs } from '../services/taskService.js';
|
|
|
|
export let show = false;
|
|
export let taskId = null;
|
|
export let taskStatus = null; // To know if we should poll
|
|
|
|
const dispatch = createEventDispatcher();
|
|
|
|
let logs = [];
|
|
let loading = false;
|
|
let error = "";
|
|
let interval;
|
|
let autoScroll = true;
|
|
let logContainer;
|
|
|
|
async function fetchLogs() {
|
|
if (!taskId) return;
|
|
try {
|
|
logs = await getTaskLogs(taskId);
|
|
if (autoScroll) {
|
|
scrollToBottom();
|
|
}
|
|
} catch (e) {
|
|
error = e.message;
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
function scrollToBottom() {
|
|
if (logContainer) {
|
|
setTimeout(() => {
|
|
logContainer.scrollTop = logContainer.scrollHeight;
|
|
}, 0);
|
|
}
|
|
}
|
|
|
|
function handleScroll() {
|
|
if (!logContainer) return;
|
|
// If user scrolls up, disable auto-scroll
|
|
const { scrollTop, scrollHeight, clientHeight } = logContainer;
|
|
const atBottom = scrollHeight - scrollTop - clientHeight < 50;
|
|
autoScroll = atBottom;
|
|
}
|
|
|
|
function close() {
|
|
dispatch('close');
|
|
show = false;
|
|
}
|
|
|
|
function getLogLevelColor(level) {
|
|
switch (level) {
|
|
case 'INFO': return 'text-blue-600';
|
|
case 'WARNING': return 'text-yellow-600';
|
|
case 'ERROR': return 'text-red-600';
|
|
case 'DEBUG': return 'text-gray-500';
|
|
default: return 'text-gray-800';
|
|
}
|
|
}
|
|
|
|
// React to changes in show/taskId
|
|
$: if (show && taskId) {
|
|
logs = [];
|
|
loading = true;
|
|
error = "";
|
|
fetchLogs();
|
|
|
|
// Poll if task is running
|
|
if (taskStatus === 'RUNNING' || taskStatus === 'AWAITING_INPUT' || taskStatus === 'AWAITING_MAPPING') {
|
|
interval = setInterval(fetchLogs, 3000);
|
|
}
|
|
} else {
|
|
if (interval) clearInterval(interval);
|
|
}
|
|
|
|
onDestroy(() => {
|
|
if (interval) clearInterval(interval);
|
|
});
|
|
</script>
|
|
|
|
{#if show}
|
|
<div class="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
|
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
<!-- Background overlay -->
|
|
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true" on:click={close}></div>
|
|
|
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
|
|
|
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full">
|
|
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
|
<div class="sm:flex sm:items-start">
|
|
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
|
|
<h3 class="text-lg leading-6 font-medium text-gray-900 flex justify-between items-center" id="modal-title">
|
|
<span>Task Logs <span class="text-sm text-gray-500 font-normal">({taskId})</span></span>
|
|
<button on:click={fetchLogs} class="text-sm text-indigo-600 hover:text-indigo-900">Refresh</button>
|
|
</h3>
|
|
|
|
<div class="mt-4 border rounded-md bg-gray-50 p-4 h-96 overflow-y-auto font-mono text-sm"
|
|
bind:this={logContainer}
|
|
on:scroll={handleScroll}>
|
|
{#if loading && logs.length === 0}
|
|
<p class="text-gray-500 text-center">Loading logs...</p>
|
|
{:else if error}
|
|
<p class="text-red-500 text-center">{error}</p>
|
|
{:else if logs.length === 0}
|
|
<p class="text-gray-500 text-center">No logs available.</p>
|
|
{:else}
|
|
{#each logs as log}
|
|
<div class="mb-1 hover:bg-gray-100 p-1 rounded">
|
|
<span class="text-gray-400 text-xs mr-2">
|
|
{new Date(log.timestamp).toLocaleTimeString()}
|
|
</span>
|
|
<span class="font-bold text-xs mr-2 w-16 inline-block {getLogLevelColor(log.level)}">
|
|
[{log.level}]
|
|
</span>
|
|
<span class="text-gray-800 break-words">
|
|
{log.message}
|
|
</span>
|
|
{#if log.context}
|
|
<div class="ml-24 text-xs text-gray-500 mt-1 bg-gray-100 p-1 rounded overflow-x-auto">
|
|
<pre>{JSON.stringify(log.context, null, 2)}</pre>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
|
<button
|
|
type="button"
|
|
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
|
on:click={close}
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
<!-- [/DEF:TaskLogViewer] --> |