201 lines
9.1 KiB
Svelte
201 lines
9.1 KiB
Svelte
<!-- [DEF:TaskHistory:Component] -->
|
|
<!--
|
|
@SEMANTICS: task, history, list, status, monitoring
|
|
@PURPOSE: Displays a list of recent tasks with their status and allows selecting them for viewing logs.
|
|
@LAYER: UI
|
|
@RELATION: USES -> frontend/src/lib/stores.js
|
|
@RELATION: USES -> frontend/src/lib/api.js (inferred)
|
|
-->
|
|
<script>
|
|
import { onMount, onDestroy } from 'svelte';
|
|
import { selectedTask } from '../lib/stores.js';
|
|
import { api } from '../lib/api.js';
|
|
import { t } from '../lib/i18n';
|
|
|
|
let tasks = [];
|
|
let loading = true;
|
|
let error = "";
|
|
let interval;
|
|
|
|
// [DEF:fetchTasks:Function]
|
|
// @PURPOSE: Fetches the list of recent tasks from the API.
|
|
// @PRE: None.
|
|
// @POST: tasks array is updated and selectedTask status synchronized.
|
|
async function fetchTasks() {
|
|
try {
|
|
tasks = await api.fetchApi('/tasks?limit=10');
|
|
|
|
// [DEBUG] Check for tasks requiring attention
|
|
tasks.forEach(t => {
|
|
if (t.status === 'AWAITING_MAPPING' || t.status === 'AWAITING_INPUT') {
|
|
console.log(`[TaskHistory] Task ${t.id} is in state ${t.status}. Input required: ${t.input_required}`);
|
|
}
|
|
});
|
|
|
|
// Update selected task if it exists in the list (for status updates)
|
|
if ($selectedTask) {
|
|
const updatedTask = tasks.find(t => t.id === $selectedTask.id);
|
|
if (updatedTask && updatedTask.status !== $selectedTask.status) {
|
|
selectedTask.set(updatedTask);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
error = e.message;
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
// [/DEF:fetchTasks:Function]
|
|
|
|
// [DEF:clearTasks:Function]
|
|
// @PURPOSE: Clears tasks from the history, optionally filtered by status.
|
|
// @PRE: User confirms deletion via prompt.
|
|
// @POST: Tasks are deleted from backend and list is re-fetched.
|
|
async function clearTasks(status = null) {
|
|
if (!confirm($t.tasks?.clear_confirm )) return;
|
|
try {
|
|
let endpoint = '/tasks';
|
|
if (status) endpoint += `?status=${status}`;
|
|
|
|
await api.requestApi(endpoint, 'DELETE');
|
|
await fetchTasks();
|
|
} catch (e) {
|
|
error = e.message;
|
|
}
|
|
}
|
|
// [/DEF:clearTasks:Function]
|
|
|
|
// [DEF:selectTask:Function]
|
|
// @PURPOSE: Selects a task and fetches its full details.
|
|
// @PRE: task object is provided.
|
|
// @POST: selectedTask store is updated with full task details.
|
|
async function selectTask(task) {
|
|
try {
|
|
// Fetch the full task details (including logs) before setting it as selected
|
|
const fullTask = await api.getTask(task.id);
|
|
selectedTask.set(fullTask);
|
|
} catch (e) {
|
|
console.error($t.tasks?.fetch_task_details_failed , e);
|
|
selectedTask.set(task);
|
|
}
|
|
}
|
|
// [/DEF:selectTask:Function]
|
|
|
|
// [DEF:getStatusColor:Function]
|
|
// @PURPOSE: Returns the CSS color class for a given task status.
|
|
// @PRE: status string is provided.
|
|
// @POST: Returns tailwind color class string.
|
|
function getStatusColor(status) {
|
|
switch (status) {
|
|
case 'SUCCESS': return 'bg-green-100 text-green-800';
|
|
case 'FAILED': return 'bg-red-100 text-red-800';
|
|
case 'RUNNING': return 'bg-blue-100 text-blue-800';
|
|
case 'AWAITING_INPUT': return 'bg-orange-100 text-orange-800';
|
|
case 'AWAITING_MAPPING': return 'bg-yellow-100 text-yellow-800';
|
|
default: return 'bg-gray-100 text-gray-800';
|
|
}
|
|
}
|
|
// [/DEF:getStatusColor:Function]
|
|
|
|
// [DEF:onMount:Function]
|
|
// @PURPOSE: Initializes the component by fetching tasks and starting polling.
|
|
// @PRE: Component is mounting.
|
|
// @POST: Tasks are fetched and 5s polling interval is started.
|
|
onMount(() => {
|
|
fetchTasks();
|
|
interval = setInterval(fetchTasks, 5000); // Poll every 5s
|
|
});
|
|
// [/DEF:onMount:Function]
|
|
|
|
// [DEF:onDestroy:Function]
|
|
// @PURPOSE: Cleans up the polling interval when the component is destroyed.
|
|
// @PRE: Component is being destroyed.
|
|
// @POST: Polling interval is cleared.
|
|
onDestroy(() => {
|
|
clearInterval(interval);
|
|
});
|
|
// [/DEF:onDestroy:Function]
|
|
</script>
|
|
|
|
<div class="bg-white shadow overflow-hidden sm:rounded-lg mb-8">
|
|
<div class="px-4 py-5 sm:px-6 flex justify-between items-center">
|
|
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
|
{$t.tasks?.recent }
|
|
</h3>
|
|
<div class="flex space-x-4 items-center">
|
|
<div class="relative inline-block text-left group">
|
|
<button class="text-sm text-red-600 hover:text-red-900 focus:outline-none flex items-center py-2">
|
|
{$t.tasks?.clear_tasks }
|
|
<svg class="ml-1 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
|
</button>
|
|
<!-- Added a transparent bridge to prevent menu closing when moving cursor -->
|
|
<div class="absolute h-2 w-full top-full left-0"></div>
|
|
<div class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none hidden group-hover:block z-50">
|
|
<div class="py-1">
|
|
<button on:click={() => clearTasks()} class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">{$t.tasks?.clear_non_running }</button>
|
|
<button on:click={() => clearTasks('FAILED')} class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">{$t.tasks?.clear_failed }</button>
|
|
<button on:click={() => clearTasks('AWAITING_INPUT')} class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">{$t.tasks?.clear_awaiting_input }</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button
|
|
on:click={fetchTasks}
|
|
class="text-sm text-indigo-600 hover:text-indigo-900 focus:outline-none"
|
|
>
|
|
{$t.common?.refresh }
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{#if loading && tasks.length === 0}
|
|
<div class="p-4 text-center text-gray-500">{$t.tasks?.loading }</div>
|
|
{:else if error}
|
|
<div class="p-4 text-center text-red-500">{error}</div>
|
|
{:else if tasks.length === 0}
|
|
<div class="p-4 text-center text-gray-500">{$t.tasks?.no_tasks }</div>
|
|
{:else}
|
|
<ul class="divide-y divide-gray-200">
|
|
{#each tasks as task}
|
|
<li>
|
|
<button
|
|
class="w-full text-left block hover:bg-gray-50 focus:outline-none focus:bg-gray-50 transition duration-150 ease-in-out"
|
|
class:bg-indigo-50={$selectedTask && $selectedTask.id === task.id}
|
|
on:click={() => selectTask(task)}
|
|
>
|
|
<div class="px-4 py-4 sm:px-6">
|
|
<div class="flex items-center justify-between">
|
|
<p class="text-sm font-medium text-indigo-600 truncate">
|
|
{task.plugin_id}
|
|
<span class="text-gray-500 text-xs ml-2">({task.id.slice(0, 8)})</span>
|
|
</p>
|
|
<div class="ml-2 flex-shrink-0 flex">
|
|
<p class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {getStatusColor(task.status)}">
|
|
{task.status}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="mt-2 sm:flex sm:justify-between">
|
|
<div class="sm:flex">
|
|
<p class="flex items-center text-sm text-gray-500">
|
|
{#if task.params.from_env && task.params.to_env}
|
|
{task.params.from_env} → {task.params.to_env}
|
|
{:else}
|
|
{$t.tasks?.parameters }: {Object.keys(task.params).length} {$t.tasks?.keys }
|
|
{/if}
|
|
</p>
|
|
</div>
|
|
<div class="mt-2 flex items-center text-sm text-gray-500 sm:mt-0">
|
|
<p>
|
|
{$t.tasks?.started_label }: {new Date(task.started_at || task.created_at || Date.now()).toLocaleString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
{/if}
|
|
</div>
|
|
<!-- [/DEF:TaskHistory:Component] -->
|