Закончили редизайн, обновили интерфейс бэкапа

This commit is contained in:
2026-01-26 22:12:35 +03:00
parent 16ffeb1ed6
commit 1042b35d1b
23 changed files with 572 additions and 94 deletions

View File

@@ -0,0 +1,241 @@
<!-- [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] -->