241 lines
8.5 KiB
Svelte
241 lines
8.5 KiB
Svelte
<!-- [DEF:BackupManager:Component] -->
|
|
<!--
|
|
@SEMANTICS: backup, manager, orchestrator
|
|
@PURPOSE: Main container for backup management, handling creation and listing.
|
|
@LAYER: Feature
|
|
@RELATION: USES -> BackupList
|
|
@RELATION: USES -> api
|
|
|
|
@INVARIANT: Only one backup task can be triggered at a time from the UI.
|
|
-->
|
|
|
|
<script lang="ts">
|
|
// [SECTION: IMPORTS]
|
|
import { onMount } from 'svelte';
|
|
import { t } from '../../lib/i18n';
|
|
import { api, requestApi } from '../../lib/api';
|
|
import { addToast } from '../../lib/toasts';
|
|
import { Button, Card, Select, Input } from '../../lib/ui';
|
|
import BackupList from './BackupList.svelte';
|
|
import type { Backup } from '../../types/backup';
|
|
// [/SECTION]
|
|
|
|
// [SECTION: STATE]
|
|
let backups: Backup[] = [];
|
|
let environments: any[] = [];
|
|
let selectedEnvId = '';
|
|
let loading = true;
|
|
let creating = false;
|
|
let savingSchedule = false;
|
|
|
|
// Schedule state for selected environment
|
|
let scheduleEnabled = false;
|
|
let cronExpression = '0 0 * * *';
|
|
|
|
$: selectedEnv = environments.find(e => e.id === selectedEnvId);
|
|
$: if (selectedEnv) {
|
|
scheduleEnabled = selectedEnv.backup_schedule?.enabled ?? false;
|
|
cronExpression = selectedEnv.backup_schedule?.cron_expression ?? '0 0 * * *';
|
|
}
|
|
// [/SECTION]
|
|
|
|
// [DEF:loadData:Function]
|
|
/**
|
|
* @purpose Loads backups and environments from the backend.
|
|
*
|
|
* @pre API must be reachable.
|
|
* @post environments and backups stores are populated.
|
|
*
|
|
* @returns {Promise<void>}
|
|
* @side_effect Updates local state variables.
|
|
*/
|
|
// @RELATION: CALLS -> api.getEnvironmentsList
|
|
// @RELATION: CALLS -> api.requestApi
|
|
async function loadData() {
|
|
console.log("[BackupManager][Entry] Loading data.");
|
|
loading = true;
|
|
try {
|
|
const [envsData, storageData] = await Promise.all([
|
|
api.getEnvironmentsList(),
|
|
requestApi('/storage/files?category=backups')
|
|
]);
|
|
environments = envsData;
|
|
|
|
// Map storage files to Backup type
|
|
backups = (storageData || []).map((file: any) => ({
|
|
id: file.name,
|
|
name: file.name,
|
|
environment: file.path.split('/')[0] || 'Unknown',
|
|
created_at: file.created_at,
|
|
size_bytes: file.size,
|
|
status: 'success'
|
|
}));
|
|
console.log("[BackupManager][Action] Data loaded successfully.");
|
|
} catch (error) {
|
|
console.error("[BackupManager][Coherence:Failed] Load failed", error);
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
// [/DEF:loadData:Function]
|
|
|
|
// [DEF:handleCreateBackup:Function]
|
|
/**
|
|
* @purpose Triggers a new backup task for the selected environment.
|
|
*
|
|
* @pre selectedEnvId must be a valid environment ID.
|
|
* @post A new task is created on the backend.
|
|
*
|
|
* @returns {Promise<void>}
|
|
* @side_effect Dispatches a toast notification.
|
|
*/
|
|
// @RELATION: CALLS -> api.createTask
|
|
// [DEF:handleUpdateSchedule:Function]
|
|
/**
|
|
* @purpose Updates the backup schedule for the selected environment.
|
|
* @pre selectedEnvId must be set.
|
|
* @post Environment config is updated on the backend.
|
|
*/
|
|
async function handleUpdateSchedule() {
|
|
if (!selectedEnvId) return;
|
|
|
|
console.log(`[BackupManager][Action] Updating schedule for env: ${selectedEnvId}`);
|
|
savingSchedule = true;
|
|
try {
|
|
await api.updateEnvironmentSchedule(selectedEnvId, {
|
|
enabled: scheduleEnabled,
|
|
cron_expression: cronExpression
|
|
});
|
|
addToast($t.common.success, 'success');
|
|
|
|
// Update local state
|
|
environments = environments.map(e =>
|
|
e.id === selectedEnvId
|
|
? { ...e, backup_schedule: { enabled: scheduleEnabled, cron_expression: cronExpression } }
|
|
: e
|
|
);
|
|
} catch (error) {
|
|
console.error("[BackupManager][Coherence:Failed] Schedule update failed", error);
|
|
} finally {
|
|
savingSchedule = false;
|
|
}
|
|
}
|
|
// [/DEF:handleUpdateSchedule:Function]
|
|
|
|
async function handleCreateBackup() {
|
|
if (!selectedEnvId) {
|
|
addToast($t.tasks.select_env, 'error');
|
|
return;
|
|
}
|
|
|
|
console.log(`[BackupManager][Action] Triggering backup for env: ${selectedEnvId}`);
|
|
creating = true;
|
|
try {
|
|
await api.createTask('superset-backup', { environment_id: selectedEnvId });
|
|
addToast($t.common.success, 'success');
|
|
console.log("[BackupManager][Coherence:OK] Backup task triggered.");
|
|
} catch (error) {
|
|
console.error("[BackupManager][Coherence:Failed] Create failed", error);
|
|
} finally {
|
|
creating = false;
|
|
}
|
|
}
|
|
// [/DEF:handleCreateBackup:Function]
|
|
|
|
onMount(loadData);
|
|
</script>
|
|
|
|
<!-- [SECTION: TEMPLATE] -->
|
|
<div class="space-y-6">
|
|
<Card title={$t.tasks.manual_backup}>
|
|
<div class="space-y-4">
|
|
<div class="flex items-end gap-4">
|
|
<div class="flex-1">
|
|
<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>
|
|
<Button
|
|
variant="primary"
|
|
on:click={handleCreateBackup}
|
|
disabled={creating || !selectedEnvId}
|
|
>
|
|
{creating ? $t.common.loading : $t.tasks.start_backup}
|
|
</Button>
|
|
</div>
|
|
|
|
{#if selectedEnvId}
|
|
<div class="pt-6 border-t border-gray-100 mt-4">
|
|
<h3 class="text-sm font-semibold text-gray-800 mb-4 flex items-center gap-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
{$t.tasks.backup_schedule}
|
|
</h3>
|
|
|
|
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
|
<div class="flex flex-col md:flex-row md:items-start gap-6">
|
|
<div class="pt-8">
|
|
<label class="flex items-center gap-3 cursor-pointer group">
|
|
<div class="relative inline-flex items-center">
|
|
<input type="checkbox" bind:checked={scheduleEnabled} class="sr-only peer" />
|
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
|
<span class="ml-3 text-sm font-medium text-gray-700 group-hover:text-gray-900 transition-colors">{$t.tasks.schedule_enabled}</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="flex-1 space-y-2">
|
|
<Input
|
|
label={$t.tasks.cron_label}
|
|
placeholder="0 0 * * *"
|
|
bind:value={cronExpression}
|
|
disabled={!scheduleEnabled}
|
|
/>
|
|
<p class="text-xs text-gray-500 italic">{$t.tasks.cron_hint}</p>
|
|
</div>
|
|
|
|
<div class="pt-8">
|
|
<Button
|
|
variant="secondary"
|
|
on:click={handleUpdateSchedule}
|
|
disabled={savingSchedule}
|
|
class="min-w-[100px]"
|
|
>
|
|
{#if savingSchedule}
|
|
<span class="flex items-center gap-2">
|
|
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
{$t.common.loading}
|
|
</span>
|
|
{:else}
|
|
{$t.common.save}
|
|
{/if}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</Card>
|
|
|
|
<div class="space-y-3">
|
|
<h2 class="text-lg font-semibold text-gray-700">{$t.storage.backups}</h2>
|
|
{#if loading}
|
|
<div class="py-10 text-center text-gray-500">{$t.common.loading}</div>
|
|
{:else}
|
|
<BackupList {backups} />
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
<!-- [/SECTION] -->
|
|
|
|
<!-- [/DEF:BackupManager:Component] --> |