backup worked
This commit is contained in:
13
frontend/package-lock.json
generated
13
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -17,5 +17,8 @@
|
||||
"svelte": "^5.43.8",
|
||||
"tailwindcss": "^3.0.0",
|
||||
"vite": "^7.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"date-fns": "^4.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' : ''}"
|
||||
|
||||
94
frontend/src/components/TaskList.svelte
Normal file
94
frontend/src/components/TaskList.svelte
Normal 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] -->
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
140
frontend/src/routes/tasks/+page.svelte
Normal file
140
frontend/src/routes/tasks/+page.svelte
Normal 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}
|
||||
Reference in New Issue
Block a user