ready for test
This commit is contained in:
@@ -10,20 +10,23 @@
|
||||
|
||||
<script lang="ts">
|
||||
// [SECTION: IMPORTS]
|
||||
import { onMount } from 'svelte';
|
||||
import EnvSelector from '../../components/EnvSelector.svelte';
|
||||
import DashboardGrid from '../../components/DashboardGrid.svelte';
|
||||
import MappingTable from '../../components/MappingTable.svelte';
|
||||
import TaskRunner from '../../components/TaskRunner.svelte';
|
||||
import TaskHistory from '../../components/TaskHistory.svelte';
|
||||
import TaskLogViewer from '../../components/TaskLogViewer.svelte';
|
||||
import PasswordPrompt from '../../components/PasswordPrompt.svelte';
|
||||
import { api } from '../../lib/api.js';
|
||||
import { selectedTask } from '../../lib/stores.js';
|
||||
import { resumeTask } from '../../services/taskService.js';
|
||||
import type { DashboardMetadata, DashboardSelection } from '../../types/dashboard';
|
||||
import { t } from '$lib/i18n';
|
||||
import { Button, Card, PageHeader } from '$lib/ui';
|
||||
import { onMount } from "svelte";
|
||||
import EnvSelector from "../../components/EnvSelector.svelte";
|
||||
import DashboardGrid from "../../components/DashboardGrid.svelte";
|
||||
import MappingTable from "../../components/MappingTable.svelte";
|
||||
import TaskRunner from "../../components/TaskRunner.svelte";
|
||||
import TaskHistory from "../../components/TaskHistory.svelte";
|
||||
import TaskLogViewer from "../../components/TaskLogViewer.svelte";
|
||||
import PasswordPrompt from "../../components/PasswordPrompt.svelte";
|
||||
import { api } from "../../lib/api.js";
|
||||
import { selectedTask } from "../../lib/stores.js";
|
||||
import { resumeTask } from "../../services/taskService.js";
|
||||
import type {
|
||||
DashboardMetadata,
|
||||
DashboardSelection,
|
||||
} from "../../types/dashboard";
|
||||
import { t } from "$lib/i18n";
|
||||
import { Button, Card, PageHeader } from "$lib/ui";
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: STATE]
|
||||
@@ -31,6 +34,7 @@
|
||||
let sourceEnvId = "";
|
||||
let targetEnvId = "";
|
||||
let replaceDb = false;
|
||||
let fixCrossFilters = true;
|
||||
let loading = true;
|
||||
let error = "";
|
||||
let dashboards: DashboardMetadata[] = [];
|
||||
@@ -40,12 +44,12 @@
|
||||
let mappings: any[] = [];
|
||||
let suggestions: any[] = [];
|
||||
let fetchingDbs = false;
|
||||
|
||||
|
||||
// UI State for Modals
|
||||
let showLogViewer = false;
|
||||
let logViewerTaskId: string | null = null;
|
||||
let logViewerTaskStatus: string | null = null;
|
||||
|
||||
|
||||
let showPasswordPrompt = false;
|
||||
let passwordPromptDatabases: string[] = [];
|
||||
let passwordPromptErrorMessage = "";
|
||||
@@ -101,13 +105,18 @@
|
||||
if (!sourceEnvId || !targetEnvId) return;
|
||||
fetchingDbs = true;
|
||||
error = "";
|
||||
|
||||
|
||||
try {
|
||||
const [src, tgt, maps, sugs] = await Promise.all([
|
||||
api.requestApi(`/environments/${sourceEnvId}/databases`),
|
||||
api.requestApi(`/environments/${targetEnvId}/databases`),
|
||||
api.requestApi(`/mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`),
|
||||
api.postApi(`/mappings/suggest`, { source_env_id: sourceEnvId, target_env_id: targetEnvId })
|
||||
api.requestApi(
|
||||
`/mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`,
|
||||
),
|
||||
api.postApi(`/mappings/suggest`, {
|
||||
source_env_id: sourceEnvId,
|
||||
target_env_id: targetEnvId,
|
||||
}),
|
||||
]);
|
||||
|
||||
sourceDatabases = src;
|
||||
@@ -130,22 +139,25 @@
|
||||
*/
|
||||
async function handleMappingUpdate(event: CustomEvent) {
|
||||
const { sourceUuid, targetUuid } = event.detail;
|
||||
const sDb = sourceDatabases.find(d => d.uuid === sourceUuid);
|
||||
const tDb = targetDatabases.find(d => d.uuid === targetUuid);
|
||||
|
||||
const sDb = sourceDatabases.find((d) => d.uuid === sourceUuid);
|
||||
const tDb = targetDatabases.find((d) => d.uuid === targetUuid);
|
||||
|
||||
if (!sDb || !tDb) return;
|
||||
|
||||
try {
|
||||
const savedMapping = await api.postApi('/mappings', {
|
||||
const savedMapping = await api.postApi("/mappings", {
|
||||
source_env_id: sourceEnvId,
|
||||
target_env_id: targetEnvId,
|
||||
source_db_uuid: sourceUuid,
|
||||
target_db_uuid: targetUuid,
|
||||
source_db_name: sDb.database_name,
|
||||
target_db_name: tDb.database_name
|
||||
target_db_name: tDb.database_name,
|
||||
});
|
||||
|
||||
mappings = [...mappings.filter(m => m.source_db_uuid !== sourceUuid), savedMapping];
|
||||
mappings = [
|
||||
...mappings.filter((m) => m.source_db_uuid !== sourceUuid),
|
||||
savedMapping,
|
||||
];
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
@@ -157,10 +169,10 @@
|
||||
// @PRE: event.detail contains task object.
|
||||
// @POST: logViewer state updated and showLogViewer set to true.
|
||||
function handleViewLogs(event: CustomEvent) {
|
||||
const task = event.detail;
|
||||
logViewerTaskId = task.id;
|
||||
logViewerTaskStatus = task.status;
|
||||
showLogViewer = true;
|
||||
const task = event.detail;
|
||||
logViewerTaskId = task.id;
|
||||
logViewerTaskStatus = task.status;
|
||||
showLogViewer = true;
|
||||
}
|
||||
// [/DEF:handleViewLogs:Function]
|
||||
|
||||
@@ -172,19 +184,23 @@
|
||||
// For now, we rely on the WebSocket or manual check.
|
||||
// Ideally, TaskHistory or TaskRunner emits an event when input is needed.
|
||||
// Or we watch selectedTask.
|
||||
|
||||
$: if ($selectedTask && $selectedTask.status === 'AWAITING_INPUT' && $selectedTask.input_request) {
|
||||
const req = $selectedTask.input_request;
|
||||
if (req.type === 'database_password') {
|
||||
passwordPromptDatabases = req.databases || [];
|
||||
passwordPromptErrorMessage = req.error_message || "";
|
||||
showPasswordPrompt = true;
|
||||
}
|
||||
} else if (!$selectedTask || $selectedTask.status !== 'AWAITING_INPUT') {
|
||||
// Close prompt if task is no longer waiting (e.g. resumed)
|
||||
// But only if we are viewing this task.
|
||||
// showPasswordPrompt = false;
|
||||
// Actually, don't auto-close, let the user or success handler close it.
|
||||
|
||||
$: if (
|
||||
$selectedTask &&
|
||||
$selectedTask.status === "AWAITING_INPUT" &&
|
||||
$selectedTask.input_request
|
||||
) {
|
||||
const req = $selectedTask.input_request;
|
||||
if (req.type === "database_password") {
|
||||
passwordPromptDatabases = req.databases || [];
|
||||
passwordPromptErrorMessage = req.error_message || "";
|
||||
showPasswordPrompt = true;
|
||||
}
|
||||
} else if (!$selectedTask || $selectedTask.status !== "AWAITING_INPUT") {
|
||||
// Close prompt if task is no longer waiting (e.g. resumed)
|
||||
// But only if we are viewing this task.
|
||||
// showPasswordPrompt = false;
|
||||
// Actually, don't auto-close, let the user or success handler close it.
|
||||
}
|
||||
// [/DEF:handlePasswordPrompt:Function]
|
||||
|
||||
@@ -193,18 +209,19 @@
|
||||
// @PRE: event.detail contains passwords.
|
||||
// @POST: resumeTask is called and showPasswordPrompt is hidden on success.
|
||||
async function handleResumeMigration(event: CustomEvent) {
|
||||
if (!$selectedTask) return;
|
||||
|
||||
const { passwords } = event.detail;
|
||||
try {
|
||||
await resumeTask($selectedTask.id, passwords);
|
||||
showPasswordPrompt = false;
|
||||
// Task status update will be handled by store/websocket
|
||||
} catch (e) {
|
||||
console.error("Failed to resume task:", e);
|
||||
passwordPromptErrorMessage = e.message || ($t.migration?.resume_failed || "Failed to resume task");
|
||||
// Keep prompt open
|
||||
}
|
||||
if (!$selectedTask) return;
|
||||
|
||||
const { passwords } = event.detail;
|
||||
try {
|
||||
await resumeTask($selectedTask.id, passwords);
|
||||
showPasswordPrompt = false;
|
||||
// Task status update will be handled by store/websocket
|
||||
} catch (e) {
|
||||
console.error("Failed to resume task:", e);
|
||||
passwordPromptErrorMessage =
|
||||
e.message || $t.migration?.resume_failed || "Failed to resume task";
|
||||
// Keep prompt open
|
||||
}
|
||||
}
|
||||
// [/DEF:handleResumeMigration:Function]
|
||||
|
||||
@@ -216,15 +233,21 @@
|
||||
*/
|
||||
async function startMigration() {
|
||||
if (!sourceEnvId || !targetEnvId) {
|
||||
error = $t.migration?.select_both_envs || "Please select both source and target environments.";
|
||||
error =
|
||||
$t.migration?.select_both_envs ||
|
||||
"Please select both source and target environments.";
|
||||
return;
|
||||
}
|
||||
if (sourceEnvId === targetEnvId) {
|
||||
error = $t.migration?.different_envs || "Source and target environments must be different.";
|
||||
error =
|
||||
$t.migration?.different_envs ||
|
||||
"Source and target environments must be different.";
|
||||
return;
|
||||
}
|
||||
if (selectedDashboardIds.length === 0) {
|
||||
error = $t.migration?.select_dashboards || "Please select at least one dashboard to migrate.";
|
||||
error =
|
||||
$t.migration?.select_dashboards ||
|
||||
"Please select at least one dashboard to migrate.";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -234,28 +257,37 @@
|
||||
selected_ids: selectedDashboardIds,
|
||||
source_env_id: sourceEnvId,
|
||||
target_env_id: targetEnvId,
|
||||
replace_db_config: replaceDb
|
||||
replace_db_config: replaceDb,
|
||||
fix_cross_filters: fixCrossFilters,
|
||||
};
|
||||
console.log(`[MigrationDashboard][Action] Starting migration with selection:`, selection);
|
||||
const result = await api.postApi('/migration/execute', selection);
|
||||
console.log(`[MigrationDashboard][Action] Migration started: ${result.task_id} - ${result.message}`);
|
||||
|
||||
console.log(
|
||||
`[MigrationDashboard][Action] Starting migration with selection:`,
|
||||
selection,
|
||||
);
|
||||
const result = await api.postApi("/migration/execute", selection);
|
||||
console.log(
|
||||
`[MigrationDashboard][Action] Migration started: ${result.task_id} - ${result.message}`,
|
||||
);
|
||||
|
||||
// Wait a brief moment for the backend to ensure the task is retrievable
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
|
||||
// Fetch full task details and switch to TaskRunner view
|
||||
try {
|
||||
const task = await api.getTask(result.task_id);
|
||||
selectedTask.set(task);
|
||||
} catch (fetchErr) {
|
||||
// Fallback: create a temporary task object to switch view immediately
|
||||
console.warn($t.migration?.task_placeholder_warn || "Could not fetch task details immediately, using placeholder.");
|
||||
console.warn(
|
||||
$t.migration?.task_placeholder_warn ||
|
||||
"Could not fetch task details immediately, using placeholder.",
|
||||
);
|
||||
selectedTask.set({
|
||||
id: result.task_id,
|
||||
plugin_id: 'superset-migration',
|
||||
status: 'RUNNING',
|
||||
logs: [],
|
||||
params: {}
|
||||
id: result.task_id,
|
||||
plugin_id: "superset-migration",
|
||||
status: "RUNNING",
|
||||
logs: [],
|
||||
params: {},
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -269,7 +301,7 @@
|
||||
<!-- [SECTION: TEMPLATE] -->
|
||||
<div class="max-w-4xl mx-auto p-6">
|
||||
<PageHeader title={$t.nav.migration} />
|
||||
|
||||
|
||||
<TaskHistory on:viewLogs={handleViewLogs} />
|
||||
|
||||
{#if $selectedTask}
|
||||
@@ -285,7 +317,9 @@
|
||||
{#if loading}
|
||||
<p>{$t.migration?.loading_envs || "Loading environments..."}</p>
|
||||
{:else if error}
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||||
<div
|
||||
class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -305,8 +339,10 @@
|
||||
|
||||
<!-- [DEF:DashboardSelectionSection:Component] -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-medium mb-4">{$t.migration?.select_dashboards_title || "Select Dashboards"}</h2>
|
||||
|
||||
<h2 class="text-lg font-medium mb-4">
|
||||
{$t.migration?.select_dashboards_title || "Select Dashboards"}
|
||||
</h2>
|
||||
|
||||
{#if sourceEnvId}
|
||||
<DashboardGrid
|
||||
{dashboards}
|
||||
@@ -314,30 +350,54 @@
|
||||
environmentId={sourceEnvId}
|
||||
/>
|
||||
{:else}
|
||||
<p class="text-gray-500 italic">{$t.dashboard?.select_source || "Select a source environment to view dashboards."}</p>
|
||||
<p class="text-gray-500 italic">
|
||||
{$t.dashboard?.select_source ||
|
||||
"Select a source environment to view dashboards."}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- [/DEF:DashboardSelectionSection:Component] -->
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<input
|
||||
id="fix-cross-filters"
|
||||
type="checkbox"
|
||||
bind:checked={fixCrossFilters}
|
||||
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label for="fix-cross-filters" class="ml-2 block text-sm text-gray-900">
|
||||
{$t.migration?.fix_cross_filters ||
|
||||
"Fix Cross-Filters (Auto-repair broken links during migration)"}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mb-4">
|
||||
<input
|
||||
id="replace-db"
|
||||
type="checkbox"
|
||||
bind:checked={replaceDb}
|
||||
on:change={() => { if (replaceDb && sourceDatabases.length === 0) fetchDatabases(); }}
|
||||
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label for="replace-db" class="ml-2 block text-sm text-gray-900">
|
||||
{$t.migration?.replace_db || "Replace Database (Apply Mappings)"}
|
||||
</label>
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="replace-db"
|
||||
type="checkbox"
|
||||
bind:checked={replaceDb}
|
||||
on:change={() => {
|
||||
if (replaceDb && sourceDatabases.length === 0) fetchDatabases();
|
||||
}}
|
||||
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label for="replace-db" class="ml-2 block text-sm text-gray-900">
|
||||
{$t.migration?.replace_db || "Replace Database (Apply Mappings)"}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if replaceDb}
|
||||
<div class="mb-8 p-4 border rounded-md bg-gray-50">
|
||||
<h3 class="text-md font-medium mb-4">{$t.migration?.database_mappings || "Database Mappings"}</h3>
|
||||
<h3 class="text-md font-medium mb-4">
|
||||
{$t.migration?.database_mappings || "Database Mappings"}
|
||||
</h3>
|
||||
{#if fetchingDbs}
|
||||
<p>{$t.migration?.loading_dbs || "Loading databases and suggestions..."}</p>
|
||||
<p>
|
||||
{$t.migration?.loading_dbs ||
|
||||
"Loading databases and suggestions..."}
|
||||
</p>
|
||||
{:else if sourceDatabases.length > 0}
|
||||
<MappingTable
|
||||
{sourceDatabases}
|
||||
@@ -359,7 +419,10 @@
|
||||
|
||||
<Button
|
||||
on:click={startMigration}
|
||||
disabled={!sourceEnvId || !targetEnvId || sourceEnvId === targetEnvId || selectedDashboardIds.length === 0}
|
||||
disabled={!sourceEnvId ||
|
||||
!targetEnvId ||
|
||||
sourceEnvId === targetEnvId ||
|
||||
selectedDashboardIds.length === 0}
|
||||
>
|
||||
{$t.migration?.start || "Start Migration"}
|
||||
</Button>
|
||||
@@ -368,21 +431,20 @@
|
||||
|
||||
<!-- Modals -->
|
||||
<TaskLogViewer
|
||||
bind:show={showLogViewer}
|
||||
taskId={logViewerTaskId}
|
||||
taskStatus={logViewerTaskStatus}
|
||||
on:close={() => showLogViewer = false}
|
||||
bind:show={showLogViewer}
|
||||
taskId={logViewerTaskId}
|
||||
taskStatus={logViewerTaskStatus}
|
||||
on:close={() => (showLogViewer = false)}
|
||||
/>
|
||||
|
||||
<PasswordPrompt
|
||||
bind:show={showPasswordPrompt}
|
||||
databases={passwordPromptDatabases}
|
||||
errorMessage={passwordPromptErrorMessage}
|
||||
on:resume={handleResumeMigration}
|
||||
on:cancel={() => showPasswordPrompt = false}
|
||||
bind:show={showPasswordPrompt}
|
||||
databases={passwordPromptDatabases}
|
||||
errorMessage={passwordPromptErrorMessage}
|
||||
on:resume={handleResumeMigration}
|
||||
on:cancel={() => (showPasswordPrompt = false)}
|
||||
/>
|
||||
|
||||
<!-- [/SECTION] -->
|
||||
|
||||
|
||||
<!-- [/DEF:MigrationDashboard:Component] -->
|
||||
|
||||
Reference in New Issue
Block a user