Закончили редизайн, обновили интерфейс бэкапа
This commit is contained in:
60
frontend/src/components/EnvSelector.svelte
Normal file
60
frontend/src/components/EnvSelector.svelte
Normal file
@@ -0,0 +1,60 @@
|
||||
<!-- [DEF:EnvSelector:Component] -->
|
||||
<!--
|
||||
@SEMANTICS: environment, selector, dropdown, migration
|
||||
@PURPOSE: Provides a UI component for selecting source and target environments.
|
||||
@LAYER: Feature
|
||||
@RELATION: BINDS_TO -> environments store
|
||||
|
||||
@INVARIANT: Source and target environments must be selectable from the list of configured environments.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
// [SECTION: IMPORTS]
|
||||
import { onMount, createEventDispatcher } from 'svelte';
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: PROPS]
|
||||
export let label: string = "Select Environment";
|
||||
export let selectedId: string = "";
|
||||
export let environments: Array<{id: string, name: string, url: string}> = [];
|
||||
// [/SECTION]
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// [DEF:handleSelect:Function]
|
||||
/**
|
||||
* @purpose Dispatches the selection change event.
|
||||
* @pre event.target must be an HTMLSelectElement.
|
||||
* @post selectedId is updated and 'change' event is dispatched.
|
||||
* @param {Event} event - The change event from the select element.
|
||||
*/
|
||||
function handleSelect(event: Event) {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
selectedId = target.value;
|
||||
dispatch('change', { id: selectedId });
|
||||
}
|
||||
// [/DEF:handleSelect:Function]
|
||||
</script>
|
||||
|
||||
<!-- [SECTION: TEMPLATE] -->
|
||||
<div class="flex flex-col space-y-1">
|
||||
<label for="env-select" class="text-sm font-medium text-gray-700">{label}</label>
|
||||
<select
|
||||
id="env-select"
|
||||
class="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
|
||||
value={selectedId}
|
||||
on:change={handleSelect}
|
||||
>
|
||||
<option value="" disabled>-- Choose an environment --</option>
|
||||
{#each environments as env}
|
||||
<option value={env.id}>{env.name} ({env.url})</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<!-- [/SECTION] -->
|
||||
|
||||
<style>
|
||||
/* Component specific styles */
|
||||
</style>
|
||||
|
||||
<!-- [/DEF:EnvSelector:Component] -->
|
||||
@@ -11,9 +11,14 @@
|
||||
import { t } from '../../lib/i18n';
|
||||
import { Button } from '../../lib/ui';
|
||||
import type { Backup } from '../../types/backup';
|
||||
import { goto } from '$app/navigation';
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: PROPS]
|
||||
/**
|
||||
* @type {Backup[]}
|
||||
* @description Array of backup objects to display.
|
||||
*/
|
||||
export let backups: Backup[] = [];
|
||||
// [/SECTION]
|
||||
|
||||
@@ -51,8 +56,13 @@
|
||||
{new Date(backup.created_at).toLocaleString()}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<Button variant="ghost" size="sm" class="text-blue-600 hover:text-blue-900">
|
||||
{$t.storage.table.download}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="text-blue-600 hover:text-blue-900"
|
||||
on:click={() => goto(`/tools/storage?path=backups/${backup.name}`)}
|
||||
>
|
||||
{$t.storage.table.go_to_storage}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -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] -->
|
||||
@@ -13,6 +13,8 @@
|
||||
import { getConnections } from '../../services/connectionService.js';
|
||||
import { selectedTask } from '../../lib/stores.js';
|
||||
import { addToast } from '../../lib/toasts.js';
|
||||
import { t } from '../../lib/i18n';
|
||||
import { Button, Card, Select, Input } from '../../lib/ui';
|
||||
// [/SECTION]
|
||||
|
||||
let envs = [];
|
||||
@@ -36,7 +38,7 @@
|
||||
envs = await envsRes.json();
|
||||
connections = await getConnections();
|
||||
} catch (e) {
|
||||
addToast('Failed to fetch data', 'error');
|
||||
addToast($t.mapper.errors.fetch_failed, 'error');
|
||||
}
|
||||
}
|
||||
// [/DEF:fetchData:Function]
|
||||
@@ -47,17 +49,17 @@
|
||||
// @POST: Mapper task is started and selectedTask is updated.
|
||||
async function handleRunMapper() {
|
||||
if (!selectedEnv || !datasetId) {
|
||||
addToast('Please fill in required fields', 'warning');
|
||||
addToast($t.mapper.errors.required_fields, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (source === 'postgres' && (!selectedConnection || !tableName)) {
|
||||
addToast('Connection and Table Name are required for postgres source', 'warning');
|
||||
addToast($t.mapper.errors.postgres_required, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (source === 'excel' && !excelPath) {
|
||||
addToast('Excel path is required for excel source', 'warning');
|
||||
addToast($t.mapper.errors.excel_required, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,7 +77,7 @@
|
||||
});
|
||||
|
||||
selectedTask.set(task);
|
||||
addToast('Mapper task started', 'success');
|
||||
addToast($t.mapper.success.started, 'success');
|
||||
} catch (e) {
|
||||
addToast(e.message, 'error');
|
||||
} finally {
|
||||
@@ -88,78 +90,94 @@
|
||||
</script>
|
||||
|
||||
<!-- [SECTION: TEMPLATE] -->
|
||||
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Dataset Column Mapper</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="mapper-env" class="block text-sm font-medium text-gray-700">Environment</label>
|
||||
<select id="mapper-env" bind:value={selectedEnv} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<option value="" disabled>-- Select Environment --</option>
|
||||
{#each envs as env}
|
||||
<option value={env.id}>{env.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="mapper-ds-id" class="block text-sm font-medium text-gray-700">Dataset ID</label>
|
||||
<input type="number" id="mapper-ds-id" bind:value={datasetId} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Mapping Source</label>
|
||||
<div class="mt-2 flex space-x-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" bind:group={source} value="postgres" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300" />
|
||||
<span class="ml-2 text-sm text-gray-700">PostgreSQL</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" bind:group={source} value="excel" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300" />
|
||||
<span class="ml-2 text-sm text-gray-700">Excel</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if source === 'postgres'}
|
||||
<div class="space-y-4 p-4 bg-gray-50 rounded-md border border-gray-100">
|
||||
<div class="space-y-6">
|
||||
<Card title={$t.mapper.title}>
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="mapper-conn" class="block text-sm font-medium text-gray-700">Saved Connection</label>
|
||||
<select id="mapper-conn" bind:value={selectedConnection} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<option value="" disabled>-- Select Connection --</option>
|
||||
{#each connections as conn}
|
||||
<option value={conn.id}>{conn.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<Select
|
||||
label={$t.mapper.environment}
|
||||
bind:value={selectedEnv}
|
||||
options={[
|
||||
{ value: '', label: $t.mapper.select_env },
|
||||
...envs.map(e => ({ value: e.id, label: e.name }))
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="mapper-table" class="block text-sm font-medium text-gray-700">Table Name</label>
|
||||
<input type="text" id="mapper-table" bind:value={tableName} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="mapper-schema" class="block text-sm font-medium text-gray-700">Table Schema</label>
|
||||
<input type="text" id="mapper-schema" bind:value={tableSchema} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
label={$t.mapper.dataset_id}
|
||||
type="number"
|
||||
bind:value={datasetId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="p-4 bg-gray-50 rounded-md border border-gray-100">
|
||||
<label for="mapper-excel" class="block text-sm font-medium text-gray-700">Excel File Path</label>
|
||||
<input type="text" id="mapper-excel" bind:value={excelPath} placeholder="/path/to/mapping.xlsx" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={handleRunMapper}
|
||||
disabled={isRunning}
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
|
||||
>
|
||||
{isRunning ? 'Starting...' : 'Run Mapper'}
|
||||
</button>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{$t.mapper.source}</label>
|
||||
<div class="flex space-x-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" bind:group={source} value="postgres" class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300" />
|
||||
<span class="ml-2 text-sm text-gray-700">{$t.mapper.source_postgres}</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" bind:group={source} value="excel" class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300" />
|
||||
<span class="ml-2 text-sm text-gray-700">{$t.mapper.source_excel}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if source === 'postgres'}
|
||||
<div class="space-y-4 p-4 bg-gray-50 rounded-md border border-gray-100">
|
||||
<div>
|
||||
<Select
|
||||
label={$t.mapper.connection}
|
||||
bind:value={selectedConnection}
|
||||
options={[
|
||||
{ value: '', label: $t.mapper.select_connection },
|
||||
...connections.map(c => ({ value: c.id, label: c.name }))
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Input
|
||||
label={$t.mapper.table_name}
|
||||
type="text"
|
||||
bind:value={tableName}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
label={$t.mapper.table_schema}
|
||||
type="text"
|
||||
bind:value={tableSchema}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="p-4 bg-gray-50 rounded-md border border-gray-100">
|
||||
<Input
|
||||
label={$t.mapper.excel_path}
|
||||
type="text"
|
||||
bind:value={excelPath}
|
||||
placeholder="/path/to/mapping.xlsx"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-end pt-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
on:click={handleRunMapper}
|
||||
disabled={isRunning}
|
||||
>
|
||||
{isRunning ? $t.mapper.starting : $t.mapper.run}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<!-- [/SECTION] -->
|
||||
<!-- [/DEF:MapperTool:Component] -->
|
||||
@@ -95,7 +95,10 @@ async function requestApi(endpoint, method = 'GET', body = null) {
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, options);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `API request failed with status ${response.status}`);
|
||||
const message = errorData.detail
|
||||
? (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail))
|
||||
: `API request failed with status ${response.status}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
@@ -132,6 +135,7 @@ export const api = {
|
||||
// [/DEF:api_module:Module]
|
||||
|
||||
// Export individual functions for easier use in components
|
||||
export { requestApi };
|
||||
export const getPlugins = api.getPlugins;
|
||||
export const getTasks = api.getTasks;
|
||||
export const getTask = api.getTask;
|
||||
|
||||
@@ -23,16 +23,8 @@
|
||||
*/
|
||||
function selectPlugin(plugin) {
|
||||
console.log(`[Dashboard][Action] Selecting plugin: ${plugin.id}`);
|
||||
if (plugin.id === 'superset-migration') {
|
||||
goto('/migration');
|
||||
} else if (plugin.id === 'git-integration') {
|
||||
goto('/git');
|
||||
} else if (plugin.id === 'superset-backup') {
|
||||
goto('/tools/backups');
|
||||
} else if (plugin.id === 'superset-storage') {
|
||||
goto('/tools/storage');
|
||||
} else if (plugin.id === 'superset-mapper') {
|
||||
goto('/tools/mapper');
|
||||
if (plugin.ui_route) {
|
||||
goto(plugin.ui_route);
|
||||
} else {
|
||||
selectedPlugin.set(plugin);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<script lang="ts">
|
||||
// [SECTION: IMPORTS]
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { listFiles, deleteFile } from '../../../services/storageService';
|
||||
import { addToast } from '../../../lib/toasts';
|
||||
import { t } from '../../../lib/i18n';
|
||||
@@ -90,6 +91,8 @@
|
||||
// [DEF:navigateUp:Function]
|
||||
/**
|
||||
* @purpose Navigates one level up in the directory structure.
|
||||
* @pre currentPath is set and deeper than activeTab root.
|
||||
* @post currentPath is moved up one directory level.
|
||||
*/
|
||||
function navigateUp() {
|
||||
if (!currentPath || currentPath === activeTab) return;
|
||||
@@ -100,7 +103,18 @@
|
||||
}
|
||||
// [/DEF:navigateUp:Function]
|
||||
|
||||
onMount(loadFiles);
|
||||
onMount(() => {
|
||||
const pathParam = $page.url.searchParams.get('path');
|
||||
if (pathParam) {
|
||||
currentPath = pathParam;
|
||||
if (pathParam.startsWith('repositorys')) {
|
||||
activeTab = 'repositorys';
|
||||
} else {
|
||||
activeTab = 'backups';
|
||||
}
|
||||
}
|
||||
loadFiles();
|
||||
});
|
||||
|
||||
$: if (activeTab) {
|
||||
// Reset path when switching tabs
|
||||
|
||||
Reference in New Issue
Block a user