feat(frontend): polish task drawer and task log modal

This commit is contained in:
2026-03-16 21:23:04 +03:00
parent 7e4124bc3f
commit ef5e20e390
2 changed files with 187 additions and 96 deletions

View File

@@ -39,6 +39,7 @@
import { getTaskLogs } from "../services/taskService.js";
import { t } from "../lib/i18n";
import TaskLogPanel from "./tasks/TaskLogPanel.svelte";
import Icon from "../lib/ui/Icon.svelte";
let {
show = $bindable(false),
@@ -153,21 +154,23 @@
<div class="flex flex-col h-full w-full">
{#if loading && logs.length === 0}
<div
class="flex items-center justify-center gap-3 h-full text-terminal-text-subtle text-sm"
class="flex items-center justify-center gap-3 h-full text-slate-400 text-sm"
>
<div
class="w-5 h-5 border-2 border-terminal-border border-t-primary rounded-full animate-spin"
class="w-5 h-5 border-2 border-slate-100 border-t-blue-500 rounded-full animate-spin"
></div>
<span>{$t.tasks?.loading}</span>
<span class="font-medium">{$t.tasks?.loading}</span>
</div>
{:else if error}
<div
class="flex items-center justify-center gap-2 h-full text-log-error text-sm"
class="flex flex-col items-center justify-center gap-3 h-full text-slate-500 text-sm p-4 text-center"
>
<span class="text-xl"></span>
<span>{error}</span>
<div class="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center text-red-500 text-lg">
</div>
<span class="font-medium text-red-600">{error}</span>
<button
class="bg-terminal-surface text-terminal-text-subtle border border-terminal-border rounded-md px-3 py-1 text-xs cursor-pointer transition-all hover:bg-terminal-border hover:text-terminal-text-bright"
class="bg-white text-slate-700 border border-slate-200 rounded-md px-4 py-1.5 text-xs font-semibold cursor-pointer transition-all hover:bg-slate-50 hover:border-slate-300"
onclick={handleRefresh}>{$t.common?.retry}</button
>
</div>
@@ -197,7 +200,7 @@
class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
>
<div
class="fixed inset-0 bg-gray-500/75 transition-opacity"
class="fixed inset-0 bg-slate-900/30 backdrop-blur-[2px] transition-opacity"
aria-hidden="true"
onclick={() => {
show = false;
@@ -212,33 +215,67 @@
role="presentation"
></div>
<span
class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>&#8203;</span
>
<div
class="inline-block align-bottom bg-gray-900 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full"
class="inline-block align-bottom bg-white rounded-xl text-left overflow-hidden shadow-2xl border border-slate-200 transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full"
>
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<div
class="flex justify-between items-center mb-5 pb-4 border-b border-slate-100"
>
<h3
class="text-lg font-medium text-gray-100"
class="text-lg font-bold tracking-tight text-slate-900"
id="modal-title"
>
{$t.tasks?.logs_title}
</h3>
<button
class="text-gray-500 hover:text-gray-300"
class="p-1.5 rounded-md text-slate-400 bg-transparent border-none cursor-pointer transition-all hover:text-slate-900 hover:bg-slate-100"
onclick={() => {
show = false;
onclose();
}}
aria-label={$t.common?.close}></button
aria-label={$t.common?.close}
>
<Icon name="close" size={20} strokeWidth={2.5} />
</button>
</div>
<div class="h-[500px]">
<div class="h-[550px] overflow-hidden">
{#if loading && logs.length === 0}
<p class="text-gray-500 text-center">
{$t.tasks?.loading}
</p>
<div
class="flex flex-col items-center justify-center h-full space-y-4 text-slate-400"
>
<div
class="w-10 h-10 border-4 border-slate-100 border-t-blue-500 rounded-full animate-spin"
></div>
<p class="text-sm font-semibold tracking-wide uppercase">
{$t.tasks?.loading}
</p>
</div>
{:else if error}
<p class="text-red-400 text-center">{error}</p>
<div
class="flex flex-col items-center justify-center h-full p-8 text-center space-y-4"
>
<div
class="w-14 h-14 rounded-full bg-red-50 flex items-center justify-center text-red-500 shadow-inner"
>
<span class="text-3xl"></span>
</div>
<p class="text-red-600 font-semibold text-lg">
{error}
</p>
<button
class="mt-2 rounded-lg border border-slate-200 bg-white px-5 py-2.5 text-sm font-bold text-slate-700 hover:bg-slate-50 hover:border-slate-300 transition-all shadow-sm"
onclick={handleRefresh}
>
{$t.common?.retry}
</button>
</div>
{:else}
<TaskLogPanel
{taskId}
@@ -254,6 +291,6 @@
</div>
{/if}
{/if}
// [/DEF:showModal:Component]
<!-- [/DEF:showModal:Component] -->
<!-- [/DEF:TaskLogViewer:Component] -->

View File

@@ -41,6 +41,7 @@
import { assistantChatStore } from "$lib/stores/assistantChat.js";
import TaskLogViewer from "../../../components/TaskLogViewer.svelte";
import PasswordPrompt from "../../../components/PasswordPrompt.svelte";
import { getReportTypeProfile } from "../reports/reportTypeProfiles.js";
import { t } from "$lib/i18n";
import { api } from "$lib/api.js";
import Icon from "$lib/ui/Icon.svelte";
@@ -129,12 +130,25 @@
function llmValidationBadgeClass(tone) {
if (tone === "fail")
return "text-rose-700 bg-rose-100 border border-rose-200";
return "text-red-700 bg-red-100 ring-1 ring-red-200";
if (tone === "warn")
return "text-amber-700 bg-amber-100 border border-amber-200";
return "text-amber-700 bg-amber-100 ring-1 ring-amber-200";
if (tone === "pass")
return "text-emerald-700 bg-emerald-100 border border-emerald-200";
return "text-slate-700 bg-slate-100 border border-slate-200";
return "text-green-700 bg-green-100 ring-1 ring-green-200";
return "text-slate-700 bg-slate-100 ring-1 ring-slate-200";
}
function getStatusClass(status) {
const s = status?.toLowerCase();
if (s === "success" || s === "completed")
return "bg-green-100 text-green-700 ring-1 ring-green-200";
if (s === "failed" || s === "error")
return "bg-red-100 text-red-700 ring-1 ring-red-200";
if (s === "running" || s === "in_progress")
return "bg-blue-100 text-blue-700 ring-1 ring-blue-200";
if (s === "partial" || s === "partial_success")
return "bg-amber-100 text-amber-700 ring-1 ring-amber-200";
return "bg-slate-100 text-slate-700 ring-1 ring-slate-200";
}
function stopTaskDetailsPolling() {
@@ -342,10 +356,14 @@
const diffPayload = await gitService.getDiff(
derivedTaskSummary.primaryDashboardId,
);
diffText =
typeof diffPayload === "string"
? diffPayload
: diffPayload?.diff || JSON.stringify(diffPayload, null, 2);
if (typeof diffPayload === "string") {
diffText = diffPayload;
} else if (diffPayload && typeof diffPayload === "object") {
diffText =
diffPayload.diff || JSON.stringify(diffPayload, null, 2);
} else {
diffText = "";
}
} catch (err) {
addToast(err?.message || "Failed to load diff", "error");
diffText = "";
@@ -525,7 +543,7 @@
{#if isOpen}
<div
class="fixed top-0 z-[72] flex h-full w-full max-w-[560px] flex-col border-l border-slate-200 bg-white shadow-[-8px_0_30px_rgba(15,23,42,0.15)] transition-[right] duration-300 ease-out"
class="fixed top-0 z-[72] flex h-full w-full max-w-[560px] flex-col border-l border-slate-100 bg-white shadow-[-12px_0_40px_rgba(15,23,42,0.1)] transition-[right] duration-300 ease-out"
style={`right: ${assistantOffset};`}
role="dialog"
aria-modal="false"
@@ -544,32 +562,28 @@
</span>
{:else if activeTaskId}
<button
class="flex items-center justify-center p-1.5 rounded-md text-slate-500 bg-transparent border-none cursor-pointer transition-all hover:text-slate-100 hover:bg-slate-800"
class="flex items-center justify-center p-1.5 rounded-md text-slate-500 bg-transparent border-none cursor-pointer transition-all hover:text-slate-900 hover:bg-slate-100"
onclick={goBackToList}
aria-label={$t.tasks?.back_to_list}
>
<Icon name="back" size={16} strokeWidth={2} />
</button>
{/if}
<h2 class="text-sm font-semibold tracking-tight text-slate-900">
<h2 class="text-sm font-bold tracking-tight text-slate-900">
{activeTaskId ? $t.tasks?.details_logs : $t.tasks?.recent}
</h2>
{#if shortTaskId}
<span
class="text-xs font-mono text-slate-500 bg-slate-800 px-2 py-0.5 rounded"
class="text-[10px] font-mono font-medium text-slate-500 bg-slate-50 border border-slate-200 px-2 py-0.5 rounded"
>{shortTaskId}</span
>
{/if}
{#if taskStatus}
<span
class="text-xs font-semibold uppercase tracking-wider px-2 py-0.5 rounded-full {taskStatus.toLowerCase() ===
'running'
? 'text-cyan-400 bg-cyan-400/10 border border-cyan-400/20'
: taskStatus.toLowerCase() === 'success'
? 'text-green-400 bg-green-400/10 border border-green-400/20'
: 'text-red-400 bg-red-400/10 border border-red-400/20'}"
>{taskStatus}</span
class="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded {getStatusClass(taskStatus)}"
>
{taskStatus}
</span>
{/if}
{#if derivedActiveTaskValidation}
<span
@@ -593,7 +607,7 @@
{$t.nav?.reports}
</button>
<button
class="p-1.5 rounded-md text-slate-500 bg-transparent border-none cursor-pointer transition-all hover:text-slate-100 hover:bg-slate-800"
class="p-1.5 rounded-md text-slate-500 bg-transparent border-none cursor-pointer transition-all hover:text-slate-900 hover:bg-slate-100"
onclick={handleClose}
aria-label={$t.tasks?.close_drawer}
>
@@ -607,14 +621,19 @@
{#if activeTaskId}
{#if derivedTaskSummary}
<div
class="mx-4 mt-4 rounded-lg border border-slate-200 bg-slate-50 p-3"
class="mx-4 mt-4 rounded-xl border border-slate-200 bg-white p-4 shadow-sm"
>
<div class="mb-2 flex items-center justify-between gap-2">
<h3 class="text-sm font-semibold text-slate-900">
{$t.tasks?.summary_report || "Summary report"}
</h3>
<div class="mb-3 flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<div class="p-1.5 bg-blue-50 text-blue-600 rounded-lg">
<Icon name="list" size={14} />
</div>
<h3 class="text-sm font-bold text-slate-900">
{$t.tasks?.summary_report || "Summary report"}
</h3>
</div>
<span
class="rounded-full bg-green-100 px-2 py-0.5 text-[11px] font-semibold text-green-700"
class="rounded-full px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-wider {getStatusClass(taskStatus)}"
>
{taskStatus}
</span>
@@ -643,9 +662,9 @@
</ul>
</div>
{/if}
<div class="flex flex-wrap gap-2">
<div class="flex flex-wrap gap-2 mt-4">
<button
class="rounded-md border border-slate-300 bg-white px-2.5 py-1.5 text-xs font-semibold text-slate-700 transition-colors hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-50"
class="flex-1 min-w-[120px] rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs font-bold text-slate-700 shadow-sm transition-all hover:border-slate-300 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50"
onclick={handleOpenDashboardDeepLink}
disabled={!derivedTaskSummary?.primaryDashboardId ||
!derivedTaskSummary?.targetEnvId}
@@ -659,7 +678,7 @@
{/if}
</button>
<button
class="rounded-md border border-slate-300 bg-white px-2.5 py-1.5 text-xs font-semibold text-slate-700 transition-colors hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-50"
class="flex-1 min-w-[100px] rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs font-bold text-slate-700 shadow-sm transition-all hover:border-slate-300 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50"
onclick={handleShowDiff}
disabled={!derivedTaskSummary?.primaryDashboardId}
>
@@ -667,7 +686,7 @@
</button>
{#if activeTaskDetails?.plugin_id === "llm_dashboard_validation"}
<button
class="rounded-md border border-indigo-300 bg-indigo-50 px-2.5 py-1.5 text-xs font-semibold text-indigo-700 transition-colors hover:bg-indigo-100"
class="flex-1 min-w-[120px] rounded-lg border border-indigo-200 bg-indigo-50 px-3 py-2 text-xs font-bold text-indigo-700 shadow-sm transition-all hover:border-indigo-300 hover:bg-indigo-100"
onclick={handleOpenLlmReport}
>
{$t.tasks?.open_llm_report || "Open LLM report"}
@@ -713,54 +732,89 @@
<p>{$t.tasks?.loading}</p>
</div>
{:else if recentTasks.length > 0}
<div class="p-4">
<h3
class="text-sm font-semibold text-slate-100 mb-4 pb-2 border-b border-slate-800"
>
{$t.tasks?.recent}
</h3>
{#each recentTasks as task}
{@const taskValidation = resolveLlmValidationStatus(task)}
<button
class="flex items-center gap-3 w-full p-3 mb-2 bg-slate-800 border border-slate-700 rounded-lg cursor-pointer transition-all hover:bg-slate-700 hover:border-slate-600 text-left"
onclick={() => selectTask(task)}
>
<span class="font-mono text-xs text-slate-500"
>{task.id?.substring(0, 8) ||
$t.common?.not_available ||
"N/A"}...</span
<div class="p-5 space-y-4">
<div class="flex items-center justify-between">
<h3 class="text-[11px] font-bold uppercase tracking-widest text-slate-400">
{$t.tasks?.recent}
</h3>
{#if loadingTasks}
<div class="h-3 w-3 animate-spin rounded-full border border-slate-200 border-t-blue-500"></div>
{/if}
</div>
<div class="grid gap-4">
{#each recentTasks as task}
{@const taskValidation = resolveLlmValidationStatus(task)}
{@const profile = getReportTypeProfile(
{
"llm_dashboard_validation": "llm_verification",
"superset-backup": "backup",
"superset-migration": "migration",
"documentation": "documentation",
}[task.plugin_id] || task.plugin_id,
)}
<button
class="group flex flex-col w-full p-4 bg-white border border-slate-200 rounded-xl shadow-sm transition-all hover:border-blue-200 hover:bg-slate-50/50 hover:shadow-md text-left"
onclick={() => selectTask(task)}
>
<span class="flex-1 text-sm text-slate-100 font-medium"
>{task.plugin_id || $t.common?.unknown}</span
>
<span
class="text-xs font-semibold uppercase px-2 py-1 rounded-full {task.status?.toLowerCase() ===
'running' || task.status?.toLowerCase() === 'pending'
? 'bg-cyan-500/15 text-cyan-400'
: task.status?.toLowerCase() === 'completed' ||
task.status?.toLowerCase() === 'success'
? 'bg-green-500/15 text-green-400'
: task.status?.toLowerCase() === 'failed' ||
task.status?.toLowerCase() === 'error'
? 'bg-red-500/15 text-red-400'
: 'bg-slate-500/15 text-slate-400'}"
>{task.status || $t.common?.unknown}</span
>
{#if taskValidation}
<span
class={`text-[10px] font-semibold uppercase px-2 py-1 rounded-full inline-flex items-center gap-1 ${llmValidationBadgeClass(taskValidation.tone)}`}
title="Dashboard validation result"
>
<div class="flex items-center justify-between gap-2 mb-3">
<span
class="inline-flex min-w-[16px] items-center justify-center rounded-full bg-white/70 px-1 text-[9px] font-bold"
class="rounded px-2 py-0.5 text-xs font-semibold {profile?.variant ||
'bg-slate-100 text-slate-700'}"
>
{taskValidation.icon}
{profile?.label
? typeof profile.label === 'function'
? profile.label()
: profile.label
: task.plugin_id || $t.common?.unknown}
</span>
{taskValidation.label}
</span>
{/if}
</button>
{/each}
<div class="flex items-center gap-2">
{#if taskValidation}
<span
class={`text-[10px] font-bold uppercase px-2 py-0.5 rounded-full inline-flex items-center gap-1 ${llmValidationBadgeClass(
taskValidation.tone,
)}`}
>
{taskValidation.label}
</span>
{/if}
<span
class="text-[10px] font-bold uppercase px-2 py-0.5 rounded-full {getStatusClass(
task.status,
)}"
>
{task.status || $t.common?.unknown}
</span>
</div>
</div>
<div class="flex flex-col gap-1">
<p class="text-sm font-semibold text-slate-900 line-clamp-1">
{task.params?.dashboard_id ||
(task.plugin_id === 'superset-migration' ? $t.nav?.migration : task.plugin_id) ||
$t.common?.not_available}
</p>
<p class="text-xs text-slate-500 line-clamp-1 opacity-70">
{task.result?.summary || task.id}
</p>
</div>
<div
class="mt-4 flex items-center justify-between border-t border-slate-50 pt-3 text-[10px] text-slate-400"
>
<div class="flex items-center gap-1.5">
<Icon name="hash" size={10} />
<span class="font-mono tracking-tighter"
>{task.id?.substring(0, 8) || 'N/A'}</span
>
</div>
<div class="flex items-center gap-1.5">
<Icon name="clock" size={10} />
<span>{task.updated_at ? new Date(task.updated_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) : ''}</span>
</div>
</div>
</button>
{/each}
</div>
</div>
{:else}
<div
@@ -779,10 +833,10 @@
<!-- Footer -->
<div
class="flex items-center gap-2 justify-center px-4 py-2.5 border-t border-slate-800 bg-slate-900"
class="flex items-center gap-2 justify-center px-4 py-3 border-t border-slate-100 bg-slate-50"
>
<div class="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse"></div>
<p class="text-xs text-slate-500">
<div class="w-1.5 h-1.5 rounded-full bg-blue-500 animate-pulse"></div>
<p class="text-[11px] font-medium text-slate-500">
{$t.tasks?.footer_text}
</p>
</div>