feat(frontend): polish task drawer and task log modal
This commit is contained in:
@@ -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"
|
||||
>​</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] -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user