backup worked

This commit is contained in:
2025-12-30 22:02:51 +03:00
parent fce0941e98
commit a747a163c8
22 changed files with 768 additions and 191 deletions

View File

@@ -7,6 +7,9 @@
"": {
"name": "frontend",
"version": "0.0.0",
"dependencies": {
"date-fns": "^4.1.0"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.49.2",
@@ -1279,6 +1282,16 @@
"node": ">=4"
}
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",

View File

@@ -17,5 +17,8 @@
"svelte": "^5.43.8",
"tailwindcss": "^3.0.0",
"vite": "^7.2.4"
},
"dependencies": {
"date-fns": "^4.1.0"
}
}

View File

@@ -22,6 +22,12 @@
>
Migration
</a>
<a
href="/tasks"
class="text-gray-600 hover:text-blue-600 font-medium {$page.url.pathname.startsWith('/tasks') ? 'text-blue-600 border-b-2 border-blue-600' : ''}"
>
Tasks
</a>
<a
href="/settings"
class="text-gray-600 hover:text-blue-600 font-medium {$page.url.pathname === '/settings' ? 'text-blue-600 border-b-2 border-blue-600' : ''}"

View File

@@ -0,0 +1,94 @@
<!-- [DEF:TaskList:Component] -->
<!--
@SEMANTICS: tasks, list, status, history
@PURPOSE: Displays a list of tasks with their status and execution details.
@LAYER: Component
@RELATION: USES -> api.js
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { formatDistanceToNow } from 'date-fns';
export let tasks: Array<any> = [];
export let loading: boolean = false;
const dispatch = createEventDispatcher();
function getStatusColor(status: string) {
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 animate-pulse';
case 'PENDING': return 'bg-gray-100 text-gray-800';
case 'AWAITING_INPUT':
case 'AWAITING_MAPPING': return 'bg-yellow-100 text-yellow-800';
default: return 'bg-gray-100 text-gray-800';
}
}
function formatTime(dateStr: string | null) {
if (!dateStr) return 'N/A';
try {
return formatDistanceToNow(new Date(dateStr), { addSuffix: true });
} catch (e) {
return 'Invalid date';
}
}
function handleTaskClick(taskId: string) {
dispatch('select', { id: taskId });
}
</script>
<div class="bg-white shadow overflow-hidden sm:rounded-md">
{#if loading && tasks.length === 0}
<div class="p-4 text-center text-gray-500">Loading tasks...</div>
{:else if tasks.length === 0}
<div class="p-4 text-center text-gray-500">No tasks found.</div>
{:else}
<ul class="divide-y divide-gray-200">
{#each tasks as task (task.id)}
<li>
<button
class="block hover:bg-gray-50 w-full text-left transition duration-150 ease-in-out focus:outline-none"
on:click={() => handleTaskClick(task.id)}
>
<div class="px-4 py-4 sm:px-6">
<div class="flex items-center justify-between">
<div class="text-sm font-medium text-blue-600 truncate">
{task.plugin_id.toUpperCase()}
<span class="ml-2 text-xs text-gray-400 font-mono">{task.id.substring(0, 8)}</span>
</div>
<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?.environment_id || task.params?.source_env_id}
<span class="mr-2">Env: {task.params.environment_id || task.params.source_env_id}</span>
{/if}
</p>
</div>
<div class="mt-2 flex items-center text-sm text-gray-500 sm:mt-0">
<svg class="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd" />
</svg>
<p>
Started {formatTime(task.started_at)}
</p>
</div>
</div>
</div>
</button>
</li>
{/each}
</ul>
{/if}
</div>
<!-- [/DEF:TaskList] -->

View File

@@ -100,19 +100,21 @@ async function requestApi(endpoint, method = 'GET', body = null) {
// [DEF:api:Data]
// @PURPOSE: API client object with specific methods.
export const api = {
getPlugins: () => fetchApi('/plugins/'),
getTasks: () => fetchApi('/tasks/'),
getPlugins: () => fetchApi('/plugins'),
getTasks: () => fetchApi('/tasks'),
getTask: (taskId) => fetchApi(`/tasks/${taskId}`),
createTask: (pluginId, params) => postApi('/tasks/', { plugin_id: pluginId, params }),
createTask: (pluginId, params) => postApi('/tasks', { plugin_id: pluginId, params }),
// Settings
getSettings: () => fetchApi('/settings/'),
getSettings: () => fetchApi('/settings'),
updateGlobalSettings: (settings) => requestApi('/settings/global', 'PATCH', settings),
getEnvironments: () => fetchApi('/settings/environments'),
addEnvironment: (env) => postApi('/settings/environments', env),
updateEnvironment: (id, env) => requestApi(`/settings/environments/${id}`, 'PUT', env),
deleteEnvironment: (id) => requestApi(`/settings/environments/${id}`, 'DELETE'),
testEnvironmentConnection: (id) => postApi(`/settings/environments/${id}/test`, {}),
updateEnvironmentSchedule: (id, schedule) => requestApi(`/environments/${id}/schedule`, 'PUT', schedule),
getEnvironmentsList: () => fetchApi('/environments'),
};
// [/DEF:api_module]
@@ -128,3 +130,5 @@ export const addEnvironment = api.addEnvironment;
export const updateEnvironment = api.updateEnvironment;
export const deleteEnvironment = api.deleteEnvironment;
export const testEnvironmentConnection = api.testEnvironmentConnection;
export const updateEnvironmentSchedule = api.updateEnvironmentSchedule;
export const getEnvironmentsList = api.getEnvironmentsList;

View File

@@ -13,7 +13,7 @@
<script>
// [SECTION: IMPORTS]
import { onMount } from 'svelte';
import { getSettings, updateGlobalSettings, getEnvironments, addEnvironment, updateEnvironment, deleteEnvironment, testEnvironmentConnection } from '../lib/api';
import { getSettings, updateGlobalSettings, getEnvironments, addEnvironment, updateEnvironment, deleteEnvironment, testEnvironmentConnection, updateEnvironmentSchedule } from '../lib/api';
import { addToast } from '../lib/toasts';
// [/SECTION]
@@ -38,7 +38,11 @@
url: '',
username: '',
password: '',
is_default: false
is_default: false,
backup_schedule: {
enabled: false,
cron_expression: '0 0 * * *'
}
};
let editingEnvId = null;
@@ -167,7 +171,11 @@
url: '',
username: '',
password: '',
is_default: false
is_default: false,
backup_schedule: {
enabled: false,
cron_expression: '0 0 * * *'
}
};
editingEnvId = null;
}
@@ -293,7 +301,21 @@
<label for="env_default" class="ml-2 block text-sm text-gray-900">Default Environment</label>
</div>
</div>
<div class="mt-4 flex gap-2">
<h3 class="text-lg font-medium mb-4 mt-6">Backup Schedule</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex items-center">
<input type="checkbox" id="backup_enabled" bind:checked={newEnv.backup_schedule.enabled} class="h-4 w-4 text-blue-600 border-gray-300 rounded" />
<label for="backup_enabled" class="ml-2 block text-sm text-gray-900">Enable Automatic Backups</label>
</div>
<div>
<label for="cron_expression" class="block text-sm font-medium text-gray-700">Cron Expression</label>
<input type="text" id="cron_expression" bind:value={newEnv.backup_schedule.cron_expression} placeholder="0 0 * * *" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
<p class="text-xs text-gray-500 mt-1">Example: 0 0 * * * (daily at midnight), */5 * * * * (every 5 minutes)</p>
</div>
</div>
<div class="mt-6 flex gap-2">
<button on:click={handleAddOrUpdateEnv} class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
{editingEnvId ? 'Update' : 'Add'} Environment
</button>

View File

@@ -0,0 +1,140 @@
<script>
import { onMount, onDestroy } from 'svelte';
import { getTasks, createTask, getEnvironmentsList } from '../../lib/api';
import { addToast } from '../../lib/toasts';
import TaskList from '../../components/TaskList.svelte';
import TaskLogViewer from '../../components/TaskLogViewer.svelte';
let tasks = [];
let environments = [];
let loading = true;
let selectedTaskId = null;
let pollInterval;
let showBackupModal = false;
let selectedEnvId = '';
async function loadInitialData() {
try {
loading = true;
const [tasksData, envsData] = await Promise.all([
getTasks(),
getEnvironmentsList()
]);
tasks = tasksData;
environments = envsData;
} catch (error) {
console.error('Failed to load tasks data:', error);
} finally {
loading = false;
}
}
async function refreshTasks() {
try {
const data = await getTasks();
// Ensure we don't try to parse HTML as JSON if the route returns 404
if (Array.isArray(data)) {
tasks = data;
}
} catch (error) {
console.error('Failed to refresh tasks:', error);
}
}
function handleSelectTask(event) {
selectedTaskId = event.detail.id;
}
async function handleRunBackup() {
if (!selectedEnvId) {
addToast('Please select an environment', 'error');
return;
}
try {
const task = await createTask('superset-backup', { environment_id: selectedEnvId });
addToast('Backup task started', 'success');
showBackupModal = false;
selectedTaskId = task.id;
await refreshTasks();
} catch (error) {
console.error('Failed to start backup:', error);
}
}
onMount(() => {
loadInitialData();
pollInterval = setInterval(refreshTasks, 3000);
});
onDestroy(() => {
if (pollInterval) clearInterval(pollInterval);
});
</script>
<div class="container mx-auto p-4 max-w-6xl">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">Task Management</h1>
<button
on:click={() => showBackupModal = true}
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md shadow-sm transition duration-150 font-medium"
>
Run Backup
</button>
</div>
<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">Recent Tasks</h2>
<TaskList {tasks} {loading} on:select={handleSelectTask} />
</div>
<div class="lg:col-span-2">
<h2 class="text-lg font-semibold mb-3 text-gray-700">Task Details & Logs</h2>
{#if selectedTaskId}
<div class="bg-white rounded-lg shadow-lg h-[600px] flex flex-col">
<TaskLogViewer taskId={selectedTaskId} />
</div>
{:else}
<div class="bg-gray-50 border-2 border-dashed border-gray-300 rounded-lg h-[600px] flex items-center justify-center text-gray-500">
<p>Select a task to view logs and details</p>
</div>
{/if}
</div>
</div>
</div>
{#if showBackupModal}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
<h3 class="text-xl font-bold mb-4">Run Manual Backup</h3>
<div class="mb-4">
<label for="env-select" class="block text-sm font-medium text-gray-700 mb-1">Target Environment</label>
<select
id="env-select"
bind:value={selectedEnvId}
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 p-2 border"
>
<option value="" disabled>-- Select Environment --</option>
{#each environments as env}
<option value={env.id}>{env.name}</option>
{/each}
</select>
</div>
<div class="flex justify-end space-x-3">
<button
on:click={() => showBackupModal = false}
class="px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-md transition"
>
Cancel
</button>
<button
on:click={handleRunBackup}
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition"
>
Start Backup
</button>
</div>
</div>
</div>
{/if}