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 { getTaskLogs } from "../services/taskService.js";
import { t } from "../lib/i18n"; import { t } from "../lib/i18n";
import TaskLogPanel from "./tasks/TaskLogPanel.svelte"; import TaskLogPanel from "./tasks/TaskLogPanel.svelte";
import Icon from "../lib/ui/Icon.svelte";
let { let {
show = $bindable(false), show = $bindable(false),
@@ -153,21 +154,23 @@
<div class="flex flex-col h-full w-full"> <div class="flex flex-col h-full w-full">
{#if loading && logs.length === 0} {#if loading && logs.length === 0}
<div <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 <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> ></div>
<span>{$t.tasks?.loading}</span> <span class="font-medium">{$t.tasks?.loading}</span>
</div> </div>
{:else if error} {:else if error}
<div <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> <div class="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center text-red-500 text-lg">
<span>{error}</span>
</div>
<span class="font-medium text-red-600">{error}</span>
<button <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 onclick={handleRefresh}>{$t.common?.retry}</button
> >
</div> </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" class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
> >
<div <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" aria-hidden="true"
onclick={() => { onclick={() => {
show = false; show = false;
@@ -212,33 +215,67 @@
role="presentation" role="presentation"
></div> ></div>
<span
class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>&#8203;</span
>
<div <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="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 <h3
class="text-lg font-medium text-gray-100" class="text-lg font-bold tracking-tight text-slate-900"
id="modal-title" id="modal-title"
> >
{$t.tasks?.logs_title} {$t.tasks?.logs_title}
</h3> </h3>
<button <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={() => { onclick={() => {
show = false; show = false;
onclose(); onclose();
}} }}
aria-label={$t.common?.close}></button aria-label={$t.common?.close}
> >
<Icon name="close" size={20} strokeWidth={2.5} />
</button>
</div> </div>
<div class="h-[500px]"> <div class="h-[550px] overflow-hidden">
{#if loading && logs.length === 0} {#if loading && logs.length === 0}
<p class="text-gray-500 text-center"> <div
{$t.tasks?.loading} class="flex flex-col items-center justify-center h-full space-y-4 text-slate-400"
</p> >
<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} {: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} {:else}
<TaskLogPanel <TaskLogPanel
{taskId} {taskId}
@@ -254,6 +291,6 @@
</div> </div>
{/if} {/if}
{/if} {/if}
// [/DEF:showModal:Component] <!-- [/DEF:showModal:Component] -->
<!-- [/DEF:TaskLogViewer:Component] --> <!-- [/DEF:TaskLogViewer:Component] -->

View File

@@ -41,6 +41,7 @@
import { assistantChatStore } from "$lib/stores/assistantChat.js"; import { assistantChatStore } from "$lib/stores/assistantChat.js";
import TaskLogViewer from "../../../components/TaskLogViewer.svelte"; import TaskLogViewer from "../../../components/TaskLogViewer.svelte";
import PasswordPrompt from "../../../components/PasswordPrompt.svelte"; import PasswordPrompt from "../../../components/PasswordPrompt.svelte";
import { getReportTypeProfile } from "../reports/reportTypeProfiles.js";
import { t } from "$lib/i18n"; import { t } from "$lib/i18n";
import { api } from "$lib/api.js"; import { api } from "$lib/api.js";
import Icon from "$lib/ui/Icon.svelte"; import Icon from "$lib/ui/Icon.svelte";
@@ -129,12 +130,25 @@
function llmValidationBadgeClass(tone) { function llmValidationBadgeClass(tone) {
if (tone === "fail") 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") 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") if (tone === "pass")
return "text-emerald-700 bg-emerald-100 border border-emerald-200"; return "text-green-700 bg-green-100 ring-1 ring-green-200";
return "text-slate-700 bg-slate-100 border border-slate-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() { function stopTaskDetailsPolling() {
@@ -342,10 +356,14 @@
const diffPayload = await gitService.getDiff( const diffPayload = await gitService.getDiff(
derivedTaskSummary.primaryDashboardId, derivedTaskSummary.primaryDashboardId,
); );
diffText = if (typeof diffPayload === "string") {
typeof diffPayload === "string" diffText = diffPayload;
? diffPayload } else if (diffPayload && typeof diffPayload === "object") {
: diffPayload?.diff || JSON.stringify(diffPayload, null, 2); diffText =
diffPayload.diff || JSON.stringify(diffPayload, null, 2);
} else {
diffText = "";
}
} catch (err) { } catch (err) {
addToast(err?.message || "Failed to load diff", "error"); addToast(err?.message || "Failed to load diff", "error");
diffText = ""; diffText = "";
@@ -525,7 +543,7 @@
{#if isOpen} {#if isOpen}
<div <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};`} style={`right: ${assistantOffset};`}
role="dialog" role="dialog"
aria-modal="false" aria-modal="false"
@@ -544,32 +562,28 @@
</span> </span>
{:else if activeTaskId} {:else if activeTaskId}
<button <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} onclick={goBackToList}
aria-label={$t.tasks?.back_to_list} aria-label={$t.tasks?.back_to_list}
> >
<Icon name="back" size={16} strokeWidth={2} /> <Icon name="back" size={16} strokeWidth={2} />
</button> </button>
{/if} {/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} {activeTaskId ? $t.tasks?.details_logs : $t.tasks?.recent}
</h2> </h2>
{#if shortTaskId} {#if shortTaskId}
<span <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 >{shortTaskId}</span
> >
{/if} {/if}
{#if taskStatus} {#if taskStatus}
<span <span
class="text-xs font-semibold uppercase tracking-wider px-2 py-0.5 rounded-full {taskStatus.toLowerCase() === class="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded {getStatusClass(taskStatus)}"
'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
> >
{taskStatus}
</span>
{/if} {/if}
{#if derivedActiveTaskValidation} {#if derivedActiveTaskValidation}
<span <span
@@ -593,7 +607,7 @@
{$t.nav?.reports} {$t.nav?.reports}
</button> </button>
<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} onclick={handleClose}
aria-label={$t.tasks?.close_drawer} aria-label={$t.tasks?.close_drawer}
> >
@@ -607,14 +621,19 @@
{#if activeTaskId} {#if activeTaskId}
{#if derivedTaskSummary} {#if derivedTaskSummary}
<div <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"> <div class="mb-3 flex items-center justify-between gap-2">
<h3 class="text-sm font-semibold text-slate-900"> <div class="flex items-center gap-2">
{$t.tasks?.summary_report || "Summary report"} <div class="p-1.5 bg-blue-50 text-blue-600 rounded-lg">
</h3> <Icon name="list" size={14} />
</div>
<h3 class="text-sm font-bold text-slate-900">
{$t.tasks?.summary_report || "Summary report"}
</h3>
</div>
<span <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} {taskStatus}
</span> </span>
@@ -643,9 +662,9 @@
</ul> </ul>
</div> </div>
{/if} {/if}
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2 mt-4">
<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-[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} onclick={handleOpenDashboardDeepLink}
disabled={!derivedTaskSummary?.primaryDashboardId || disabled={!derivedTaskSummary?.primaryDashboardId ||
!derivedTaskSummary?.targetEnvId} !derivedTaskSummary?.targetEnvId}
@@ -659,7 +678,7 @@
{/if} {/if}
</button> </button>
<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} onclick={handleShowDiff}
disabled={!derivedTaskSummary?.primaryDashboardId} disabled={!derivedTaskSummary?.primaryDashboardId}
> >
@@ -667,7 +686,7 @@
</button> </button>
{#if activeTaskDetails?.plugin_id === "llm_dashboard_validation"} {#if activeTaskDetails?.plugin_id === "llm_dashboard_validation"}
<button <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} onclick={handleOpenLlmReport}
> >
{$t.tasks?.open_llm_report || "Open LLM report"} {$t.tasks?.open_llm_report || "Open LLM report"}
@@ -713,54 +732,89 @@
<p>{$t.tasks?.loading}</p> <p>{$t.tasks?.loading}</p>
</div> </div>
{:else if recentTasks.length > 0} {:else if recentTasks.length > 0}
<div class="p-4"> <div class="p-5 space-y-4">
<h3 <div class="flex items-center justify-between">
class="text-sm font-semibold text-slate-100 mb-4 pb-2 border-b border-slate-800" <h3 class="text-[11px] font-bold uppercase tracking-widest text-slate-400">
> {$t.tasks?.recent}
{$t.tasks?.recent} </h3>
</h3> {#if loadingTasks}
{#each recentTasks as task} <div class="h-3 w-3 animate-spin rounded-full border border-slate-200 border-t-blue-500"></div>
{@const taskValidation = resolveLlmValidationStatus(task)} {/if}
<button </div>
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" <div class="grid gap-4">
onclick={() => selectTask(task)} {#each recentTasks as task}
> {@const taskValidation = resolveLlmValidationStatus(task)}
<span class="font-mono text-xs text-slate-500" {@const profile = getReportTypeProfile(
>{task.id?.substring(0, 8) || {
$t.common?.not_available || "llm_dashboard_validation": "llm_verification",
"N/A"}...</span "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" <div class="flex items-center justify-between gap-2 mb-3">
>{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"
>
<span <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> </span>
{taskValidation.label} <div class="flex items-center gap-2">
</span> {#if taskValidation}
{/if} <span
</button> class={`text-[10px] font-bold uppercase px-2 py-0.5 rounded-full inline-flex items-center gap-1 ${llmValidationBadgeClass(
{/each} 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> </div>
{:else} {:else}
<div <div
@@ -779,10 +833,10 @@
<!-- Footer --> <!-- Footer -->
<div <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> <div class="w-1.5 h-1.5 rounded-full bg-blue-500 animate-pulse"></div>
<p class="text-xs text-slate-500"> <p class="text-[11px] font-medium text-slate-500">
{$t.tasks?.footer_text} {$t.tasks?.footer_text}
</p> </p>
</div> </div>