feat: restore legacy data and add typed task result views
This commit is contained in:
114
frontend/src/components/tasks/TaskResultPanel.svelte
Normal file
114
frontend/src/components/tasks/TaskResultPanel.svelte
Normal file
@@ -0,0 +1,114 @@
|
||||
<script>
|
||||
let { task = null } = $props();
|
||||
|
||||
const result = $derived(task?.result || null);
|
||||
const pluginId = $derived(task?.plugin_id || '');
|
||||
|
||||
function statusColor(status) {
|
||||
switch (status) {
|
||||
case 'PASS':
|
||||
case 'SUCCESS':
|
||||
return 'bg-green-100 text-green-700';
|
||||
case 'WARN':
|
||||
case 'PARTIAL_SUCCESS':
|
||||
return 'bg-yellow-100 text-yellow-700';
|
||||
case 'FAIL':
|
||||
case 'FAILED':
|
||||
return 'bg-red-100 text-red-700';
|
||||
default:
|
||||
return 'bg-slate-100 text-slate-700';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !task}
|
||||
<div class="rounded-lg border border-dashed border-slate-200 bg-slate-50 p-6 text-sm text-slate-500">
|
||||
Выберите задачу, чтобы увидеть результат.
|
||||
</div>
|
||||
{:else if !result}
|
||||
<div class="rounded-lg border border-slate-200 bg-white p-4">
|
||||
<p class="text-sm text-slate-700">Для этой задачи нет структурированного результата.</p>
|
||||
</div>
|
||||
{:else if pluginId === 'llm_dashboard_validation'}
|
||||
<div class="space-y-4 rounded-lg border border-slate-200 bg-white p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-slate-900">LLM проверка дашборда</h3>
|
||||
<span class={`rounded-full px-2 py-1 text-xs font-semibold ${statusColor(result.status)}`}>{result.status || 'UNKNOWN'}</span>
|
||||
</div>
|
||||
<p class="text-sm text-slate-700">{result.summary || 'Нет summary'}</p>
|
||||
{#if result.issues?.length}
|
||||
<div>
|
||||
<p class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500">Проблемы ({result.issues.length})</p>
|
||||
<ul class="space-y-2">
|
||||
{#each result.issues as issue}
|
||||
<li class="rounded-md border border-slate-200 bg-slate-50 p-2 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class={`rounded px-2 py-0.5 text-xs font-semibold ${statusColor(issue.severity)}`}>{issue.severity}</span>
|
||||
<span class="text-slate-700">{issue.message}</span>
|
||||
</div>
|
||||
{#if issue.location}
|
||||
<p class="mt-1 text-xs text-slate-500">Локация: {issue.location}</p>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if pluginId === 'superset-backup'}
|
||||
<div class="space-y-4 rounded-lg border border-slate-200 bg-white p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-slate-900">Результат бэкапа</h3>
|
||||
<span class={`rounded-full px-2 py-1 text-xs font-semibold ${statusColor(result.status)}`}>{result.status || 'UNKNOWN'}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2 text-sm text-slate-700">
|
||||
<p>Environment: {result.environment || '-'}</p>
|
||||
<p>Total: {result.total_dashboards ?? 0}</p>
|
||||
<p>Успешно: {result.backed_up_dashboards ?? 0}</p>
|
||||
<p>Ошибок: {result.failed_dashboards ?? 0}</p>
|
||||
</div>
|
||||
{#if result.failures?.length}
|
||||
<div>
|
||||
<p class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500">Ошибки</p>
|
||||
<ul class="space-y-2">
|
||||
{#each result.failures as failure}
|
||||
<li class="rounded-md border border-red-200 bg-red-50 p-2 text-sm text-red-700">
|
||||
{failure.title || failure.id}: {failure.error}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if pluginId === 'superset-migration'}
|
||||
<div class="space-y-4 rounded-lg border border-slate-200 bg-white p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-slate-900">Результат миграции</h3>
|
||||
<span class={`rounded-full px-2 py-1 text-xs font-semibold ${statusColor(result.status)}`}>{result.status || 'UNKNOWN'}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2 text-sm text-slate-700">
|
||||
<p>Source: {result.source_environment || '-'}</p>
|
||||
<p>Target: {result.target_environment || '-'}</p>
|
||||
<p>Выбрано: {result.selected_dashboards ?? 0}</p>
|
||||
<p>Успешно: {result.migrated_dashboards?.length ?? 0}</p>
|
||||
<p>С ошибками: {result.failed_dashboards?.length ?? 0}</p>
|
||||
<p>Mappings: {result.mapping_count ?? 0}</p>
|
||||
</div>
|
||||
{#if result.failed_dashboards?.length}
|
||||
<div>
|
||||
<p class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500">Ошибки миграции</p>
|
||||
<ul class="space-y-2">
|
||||
{#each result.failed_dashboards as failed}
|
||||
<li class="rounded-md border border-red-200 bg-red-50 p-2 text-sm text-red-700">
|
||||
{failed.title || failed.id}: {failed.error}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rounded-lg border border-slate-200 bg-white p-4">
|
||||
<pre class="overflow-auto rounded bg-slate-50 p-3 text-xs text-slate-700">{JSON.stringify(result, null, 2)}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -149,7 +149,19 @@ export const api = {
|
||||
postApi,
|
||||
requestApi,
|
||||
getPlugins: () => fetchApi('/plugins'),
|
||||
getTasks: () => fetchApi('/tasks'),
|
||||
getTasks: (options = {}) => {
|
||||
const params = new URLSearchParams();
|
||||
if (options.limit != null) params.append('limit', String(options.limit));
|
||||
if (options.offset != null) params.append('offset', String(options.offset));
|
||||
if (options.status) params.append('status', options.status);
|
||||
if (options.task_type) params.append('task_type', options.task_type);
|
||||
if (options.completed_only != null) params.append('completed_only', String(Boolean(options.completed_only)));
|
||||
if (Array.isArray(options.plugin_id)) {
|
||||
options.plugin_id.forEach((pluginId) => params.append('plugin_id', pluginId));
|
||||
}
|
||||
const query = params.toString();
|
||||
return fetchApi(`/tasks${query ? `?${query}` : ''}`);
|
||||
},
|
||||
getTask: (taskId) => fetchApi(`/tasks/${taskId}`),
|
||||
createTask: (pluginId, params) => postApi('/tasks', { plugin_id: pluginId, params }),
|
||||
|
||||
|
||||
@@ -8,20 +8,26 @@
|
||||
-->
|
||||
<script>
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { getTasks, createTask, getEnvironmentsList } from '../../lib/api';
|
||||
import { addToast } from '../../lib/toasts';
|
||||
import { getTasks } from '../../lib/api';
|
||||
import TaskList from '../../components/TaskList.svelte';
|
||||
import TaskLogViewer from '../../components/TaskLogViewer.svelte';
|
||||
import TaskResultPanel from '../../components/tasks/TaskResultPanel.svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
import { Button, Card, PageHeader, Select } from '$lib/ui';
|
||||
import { PageHeader } from '$lib/ui';
|
||||
|
||||
let tasks = [];
|
||||
let environments = [];
|
||||
let loading = true;
|
||||
let selectedTaskId = null;
|
||||
let selectedTask = null;
|
||||
let pollInterval;
|
||||
let showBackupModal = false;
|
||||
let selectedEnvId = '';
|
||||
let taskTypeFilter = 'all';
|
||||
|
||||
const TASK_TYPE_OPTIONS = [
|
||||
{ value: 'all', label: 'Все типы' },
|
||||
{ value: 'llm_validation', label: 'LLM проверки' },
|
||||
{ value: 'backup', label: 'Бэкапы' },
|
||||
{ value: 'migration', label: 'Миграции' }
|
||||
];
|
||||
|
||||
// [DEF:loadInitialData:Function]
|
||||
/**
|
||||
@@ -30,16 +36,19 @@
|
||||
* @post tasks and environments variables are populated.
|
||||
*/
|
||||
async function loadInitialData() {
|
||||
console.log("[loadInitialData][Action] Loading initial tasks and environments");
|
||||
console.log("[loadInitialData][Action] Loading completed tasks");
|
||||
try {
|
||||
loading = true;
|
||||
const [tasksData, envsData] = await Promise.all([
|
||||
getTasks(),
|
||||
getEnvironmentsList()
|
||||
]);
|
||||
const tasksData = await getTasks({
|
||||
limit: 100,
|
||||
completed_only: true,
|
||||
task_type: taskTypeFilter === 'all' ? undefined : taskTypeFilter
|
||||
});
|
||||
tasks = tasksData;
|
||||
environments = envsData;
|
||||
console.log(`[loadInitialData][Coherence:OK] Data loaded context={{'tasks': ${tasks.length}, 'envs': ${environments.length}}}`);
|
||||
console.log(`[loadInitialData][Coherence:OK] Data loaded context={{'tasks': ${tasks.length}}}`);
|
||||
if (selectedTaskId && !tasks.some((task) => task.id === selectedTaskId)) {
|
||||
selectedTaskId = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[loadInitialData][Coherence:Failed] Failed to load tasks data context={{'error': '${error.message}'}}`);
|
||||
} finally {
|
||||
@@ -56,7 +65,11 @@
|
||||
*/
|
||||
async function refreshTasks() {
|
||||
try {
|
||||
const data = await getTasks();
|
||||
const data = await getTasks({
|
||||
limit: 100,
|
||||
completed_only: true,
|
||||
task_type: taskTypeFilter === 'all' ? undefined : taskTypeFilter
|
||||
});
|
||||
// Ensure we don't try to parse HTML as JSON if the route returns 404
|
||||
if (Array.isArray(data)) {
|
||||
tasks = data;
|
||||
@@ -79,32 +92,6 @@
|
||||
}
|
||||
// [/DEF:handleSelectTask:Function]
|
||||
|
||||
// [DEF:handleRunBackup:Function]
|
||||
/**
|
||||
* @purpose Triggers a manual backup task for the selected environment.
|
||||
* @pre selectedEnvId must not be empty.
|
||||
* @post Backup task is created and task list is refreshed.
|
||||
*/
|
||||
async function handleRunBackup() {
|
||||
if (!selectedEnvId) {
|
||||
addToast('Please select an environment', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[handleRunBackup][Action] Starting backup for env context={{'envId': '${selectedEnvId}'}}`);
|
||||
try {
|
||||
const task = await createTask('superset-backup', { environment_id: selectedEnvId });
|
||||
addToast('Backup task started', 'success');
|
||||
showBackupModal = false;
|
||||
selectedTaskId = task.id;
|
||||
await refreshTasks();
|
||||
console.log(`[handleRunBackup][Coherence:OK] Backup task created context={{'taskId': '${task.id}'}}`);
|
||||
} catch (error) {
|
||||
console.error(`[handleRunBackup][Coherence:Failed] Failed to start backup context={{'error': '${error.message}'}}`);
|
||||
}
|
||||
}
|
||||
// [/DEF:handleRunBackup:Function]
|
||||
|
||||
onMount(() => {
|
||||
loadInitialData();
|
||||
pollInterval = setInterval(refreshTasks, 3000);
|
||||
@@ -113,6 +100,13 @@
|
||||
onDestroy(() => {
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
});
|
||||
|
||||
function handleTaskTypeChange() {
|
||||
selectedTaskId = null;
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
$: selectedTask = tasks.find((task) => task.id === selectedTaskId) || null;
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-4 max-w-6xl">
|
||||
@@ -120,57 +114,46 @@
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div class="lg:col-span-1">
|
||||
<h2 class="text-lg font-semibold mb-3 text-gray-700">{$t.tasks.recent}</h2>
|
||||
<div class="mb-3 flex items-center justify-between gap-2">
|
||||
<h2 class="text-lg font-semibold text-gray-700">Результаты задач</h2>
|
||||
<select
|
||||
bind:value={taskTypeFilter}
|
||||
on:change={handleTaskTypeChange}
|
||||
class="rounded-md border border-gray-300 bg-white px-2 py-1 text-sm text-gray-700 focus:border-blue-500 focus:outline-none"
|
||||
>
|
||||
{#each TASK_TYPE_OPTIONS as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<TaskList {tasks} {loading} on:select={handleSelectTask} />
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-2">
|
||||
<h2 class="text-lg font-semibold mb-3 text-gray-700">{$t.tasks.details_logs}</h2>
|
||||
<h2 class="text-lg font-semibold mb-3 text-gray-700">Результат и логи</h2>
|
||||
{#if selectedTaskId}
|
||||
<Card padding="none">
|
||||
<div class="h-[600px] flex flex-col overflow-hidden rounded-lg">
|
||||
<TaskLogViewer
|
||||
taskId={selectedTaskId}
|
||||
taskStatus={tasks.find(t => t.id === selectedTaskId)?.status}
|
||||
inline={true}
|
||||
/>
|
||||
<div class="space-y-4">
|
||||
<TaskResultPanel task={selectedTask} />
|
||||
<div class="rounded-lg border border-slate-200 bg-white">
|
||||
<div class="border-b border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700">
|
||||
Логи задачи
|
||||
</div>
|
||||
<div class="h-[420px] flex flex-col overflow-hidden rounded-b-lg">
|
||||
<TaskLogViewer
|
||||
taskId={selectedTaskId}
|
||||
taskStatus={selectedTask?.status}
|
||||
inline={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="bg-gray-50 border-2 border-dashed border-gray-100 rounded-lg h-[600px] flex items-center justify-center text-gray-400">
|
||||
<p>{$t.tasks.select_task}</p>
|
||||
<p>Выберите задачу из списка слева</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showBackupModal}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/50 backdrop-blur-sm p-4">
|
||||
<div class="w-full max-w-md">
|
||||
<Card title={$t.tasks.manual_backup}>
|
||||
<div class="space-y-6">
|
||||
<Select
|
||||
label={$t.tasks.target_env}
|
||||
bind:value={selectedEnvId}
|
||||
options={[
|
||||
{ value: '', label: $t.tasks.select_env },
|
||||
...environments.map(e => ({ value: e.id, label: e.name }))
|
||||
]}
|
||||
/>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-2">
|
||||
<Button variant="secondary" on:click={() => showBackupModal = false}>
|
||||
{$t.common.cancel}
|
||||
</Button>
|
||||
<Button variant="primary" on:click={handleRunBackup}>
|
||||
Start Backup
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- [/DEF:TaskManagementPage:Component] -->
|
||||
<!-- [/DEF:TaskManagementPage:Component] -->
|
||||
|
||||
Reference in New Issue
Block a user