semantic update
This commit is contained in:
@@ -15,10 +15,10 @@
|
||||
@RELATION: [BINDS_TO] ->[frontend/src/components/TaskLogViewer.svelte]
|
||||
@RELATION: [BINDS_TO] ->[frontend/src/components/PasswordPrompt.svelte]
|
||||
@INVARIANT: Migration start is blocked unless source and target environments are selected, distinct, and at least one dashboard is selected.
|
||||
@UX_STATE: Idle -> User configures source/target environments, dashboard selection, and migration options.
|
||||
@UX_STATE: Loading -> Environment/database/dry-run fetch operations disable relevant actions and show progress text.
|
||||
@UX_STATE: Error -> Error banner/prompt message is shown while keeping user input intact for correction.
|
||||
@UX_STATE: Success -> Dry-run summary or active task view is rendered after successful API operations.
|
||||
@UX_STATE: [Idle] -> User configures source/target environments, dashboard selection, and migration options.
|
||||
@UX_STATE: [Loading] -> Environment/database/dry-run fetch operations disable relevant actions and show progress text.
|
||||
@UX_STATE: [Error] -> Error banner/prompt message is shown while keeping user input intact for correction.
|
||||
@UX_STATE: [Success] -> Dry-run summary or active task view is rendered after successful API operations.
|
||||
@UX_FEEDBACK: Inline error banner, disabled CTA states, loading labels, dry-run summary cards, modal dialogs.
|
||||
@UX_RECOVERY: User can adjust selection, refresh databases, retry dry-run/migration, resume task with passwords, or cancel modal flow.
|
||||
@UX_REACTIVITY: State transitions rely on Svelte reactive bindings and store subscription to selectedTask.
|
||||
@@ -102,9 +102,12 @@
|
||||
*/
|
||||
async function fetchEnvironments() {
|
||||
return belief_scope("fetchEnvironments", async () => {
|
||||
console.info("[fetchEnvironments][REASON] Initializing environment list for selection");
|
||||
try {
|
||||
environments = await api.getEnvironmentsList();
|
||||
console.info("[fetchEnvironments][REFLECT] Environments loaded", { count: environments.length });
|
||||
} catch (e) {
|
||||
console.error("[fetchEnvironments][EXPLORE] Failed to fetch environments", e);
|
||||
error = e.message;
|
||||
} finally {
|
||||
loading = false;
|
||||
@@ -122,10 +125,13 @@
|
||||
*/
|
||||
async function fetchDashboards(envId: string) {
|
||||
return belief_scope("fetchDashboards", async () => {
|
||||
console.info("[fetchDashboards][REASON] Fetching dashboards for environment", { envId });
|
||||
try {
|
||||
dashboards = await api.requestApi(`/environments/${envId}/dashboards`);
|
||||
selectedDashboardIds = []; // Reset selection when env changes
|
||||
console.info("[fetchDashboards][REFLECT] Dashboards loaded", { count: dashboards.length });
|
||||
} catch (e) {
|
||||
console.error("[fetchDashboards][EXPLORE] Failed to fetch dashboards", e);
|
||||
error = e.message;
|
||||
dashboards = [];
|
||||
}
|
||||
@@ -135,8 +141,18 @@
|
||||
|
||||
onMount(fetchEnvironments);
|
||||
|
||||
// Reactive: fetch dashboards when source env changes
|
||||
$: if (sourceEnvId) fetchDashboards(sourceEnvId);
|
||||
// [DEF:ReactiveDashboardFetch:Block]
|
||||
/**
|
||||
* @PURPOSE: Automatically fetch dashboards when the source environment is changed.
|
||||
* @PRE: sourceEnvId is not empty.
|
||||
* @POST: fetchDashboards is called with the new sourceEnvId.
|
||||
* @UX_STATE: [Loading] -> Triggered when sourceEnvId changes.
|
||||
*/
|
||||
$: if (sourceEnvId) {
|
||||
console.info("[ReactiveDashboardFetch][REASON] Source environment changed, fetching dashboards", { sourceEnvId });
|
||||
fetchDashboards(sourceEnvId);
|
||||
}
|
||||
// [/DEF:ReactiveDashboardFetch:Block]
|
||||
|
||||
// [DEF:fetchDatabases:Function]
|
||||
/**
|
||||
@@ -146,7 +162,11 @@
|
||||
*/
|
||||
async function fetchDatabases() {
|
||||
return belief_scope("fetchDatabases", async () => {
|
||||
if (!sourceEnvId || !targetEnvId) return;
|
||||
if (!sourceEnvId || !targetEnvId) {
|
||||
console.warn("[fetchDatabases][EXPLORE] Missing environment IDs for database fetch");
|
||||
return;
|
||||
}
|
||||
console.info("[fetchDatabases][REASON] Fetching databases and suggestions for mapping", { sourceEnvId, targetEnvId });
|
||||
fetchingDbs = true;
|
||||
error = "";
|
||||
|
||||
@@ -167,7 +187,13 @@
|
||||
targetDatabases = tgt;
|
||||
mappings = maps;
|
||||
suggestions = sugs;
|
||||
console.info("[fetchDatabases][REFLECT] Databases and mappings loaded", {
|
||||
sourceCount: src.length,
|
||||
targetCount: tgt.length,
|
||||
mappingCount: maps.length
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[fetchDatabases][EXPLORE] Failed to fetch databases", e);
|
||||
error = e.message;
|
||||
} finally {
|
||||
fetchingDbs = false;
|
||||
@@ -188,8 +214,12 @@
|
||||
const sDb = sourceDatabases.find((d) => d.uuid === sourceUuid);
|
||||
const tDb = targetDatabases.find((d) => d.uuid === targetUuid);
|
||||
|
||||
if (!sDb || !tDb) return;
|
||||
if (!sDb || !tDb) {
|
||||
console.warn("[handleMappingUpdate][EXPLORE] Database not found for mapping", { sourceUuid, targetUuid });
|
||||
return;
|
||||
}
|
||||
|
||||
console.info("[handleMappingUpdate][REASON] Updating database mapping", { sourceUuid, targetUuid });
|
||||
try {
|
||||
const savedMapping = await api.postApi("/mappings", {
|
||||
source_env_id: sourceEnvId,
|
||||
@@ -204,7 +234,9 @@
|
||||
...mappings.filter((m) => m.source_db_uuid !== sourceUuid),
|
||||
savedMapping,
|
||||
];
|
||||
console.info("[handleMappingUpdate][REFLECT] Mapping saved successfully");
|
||||
} catch (e) {
|
||||
console.error("[handleMappingUpdate][EXPLORE] Failed to save mapping", e);
|
||||
error = e.message;
|
||||
}
|
||||
});
|
||||
@@ -234,6 +266,13 @@
|
||||
// Ideally, TaskHistory or TaskRunner emits an event when input is needed.
|
||||
// Or we watch selectedTask.
|
||||
|
||||
// [DEF:ReactivePasswordPrompt:Block]
|
||||
/**
|
||||
* @PURPOSE: Monitor selected task for input requests and trigger password prompt.
|
||||
* @PRE: $selectedTask is not null and status is AWAITING_INPUT.
|
||||
* @POST: showPasswordPrompt is set to true if input_request is database_password.
|
||||
* @UX_STATE: [AwaitingInput] -> Password prompt modal is displayed.
|
||||
*/
|
||||
$: if (
|
||||
$selectedTask &&
|
||||
$selectedTask.status === "AWAITING_INPUT" &&
|
||||
@@ -241,6 +280,7 @@
|
||||
) {
|
||||
const req = $selectedTask.input_request;
|
||||
if (req.type === "database_password") {
|
||||
console.info("[ReactivePasswordPrompt][REASON] Task awaiting database passwords", { taskId: $selectedTask.id });
|
||||
passwordPromptDatabases = req.databases || [];
|
||||
passwordPromptErrorMessage = req.error_message || "";
|
||||
showPasswordPrompt = true;
|
||||
@@ -251,6 +291,7 @@
|
||||
// showPasswordPrompt = false;
|
||||
// Actually, don't auto-close, let the user or success handler close it.
|
||||
}
|
||||
// [/DEF:ReactivePasswordPrompt:Block]
|
||||
// [/DEF:handlePasswordPrompt:Function]
|
||||
|
||||
// [DEF:handleResumeMigration:Function]
|
||||
@@ -278,31 +319,41 @@
|
||||
|
||||
// [DEF:startMigration:Function]
|
||||
/**
|
||||
* @purpose Starts the migration process.
|
||||
* @pre sourceEnvId and targetEnvId must be set and different.
|
||||
* @post Migration task is started and selectedTask is updated.
|
||||
* @PURPOSE: Initiates the migration process by sending the selection to the backend.
|
||||
* @PRE: sourceEnvId and targetEnvId are set and different; at least one dashboard is selected.
|
||||
* @POST: A migration task is created and selectedTask store is updated.
|
||||
* @SIDE_EFFECT: Resets dryRunResult; updates error state on failure.
|
||||
* @UX_STATE: [Loading] -> [Success] or [Error]
|
||||
*/
|
||||
async function startMigration() {
|
||||
return belief_scope("startMigration", async () => {
|
||||
if (!sourceEnvId || !targetEnvId) {
|
||||
console.warn("[startMigration][EXPLORE] Missing environment selection");
|
||||
error =
|
||||
$t.migration?.select_both_envs ||
|
||||
"Please select both source and target environments.";
|
||||
return;
|
||||
}
|
||||
if (sourceEnvId === targetEnvId) {
|
||||
console.warn("[startMigration][EXPLORE] Source and target environments are identical");
|
||||
error =
|
||||
$t.migration?.different_envs ||
|
||||
"Source and target environments must be different.";
|
||||
return;
|
||||
}
|
||||
if (selectedDashboardIds.length === 0) {
|
||||
console.warn("[startMigration][EXPLORE] No dashboards selected");
|
||||
error =
|
||||
$t.migration?.select_dashboards ||
|
||||
"Please select at least one dashboard to migrate.";
|
||||
return;
|
||||
}
|
||||
|
||||
console.info("[startMigration][REASON] Initiating migration execution", {
|
||||
sourceEnvId,
|
||||
targetEnvId,
|
||||
dashboardCount: selectedDashboardIds.length
|
||||
});
|
||||
error = "";
|
||||
try {
|
||||
dryRunResult = null;
|
||||
@@ -313,14 +364,9 @@
|
||||
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.info("[startMigration][REFLECT] Migration task created", { taskId: result.task_id });
|
||||
|
||||
// Wait a brief moment for the backend to ensure the task is retrievable
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
@@ -329,12 +375,9 @@
|
||||
try {
|
||||
const task = await api.getTask(result.task_id);
|
||||
selectedTask.set(task);
|
||||
console.info("[startMigration][REFLECT] Task details fetched and store updated");
|
||||
} 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("[startMigration][EXPLORE] Could not fetch task details immediately, using placeholder", fetchErr);
|
||||
selectedTask.set({
|
||||
id: result.task_id,
|
||||
plugin_id: "superset-migration",
|
||||
@@ -344,7 +387,7 @@
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[MigrationDashboard][Failure] Migration failed:`, e);
|
||||
console.error("[startMigration][EXPLORE] Migration initiation failed", e);
|
||||
error = e.message;
|
||||
}
|
||||
});
|
||||
@@ -353,36 +396,38 @@
|
||||
|
||||
// [DEF:startDryRun:Function]
|
||||
/**
|
||||
* @purpose Builds pre-flight diff and risk summary without applying migration.
|
||||
* @pre source/target environments and selected dashboards are valid.
|
||||
* @post dryRunResult is populated with backend response.
|
||||
* @UX_STATE: Idle -> Dry Run button is enabled when selection is valid.
|
||||
* @UX_STATE: Loading -> Dry Run button shows "Dry Run..." and stays disabled.
|
||||
* @UX_STATE: Error -> error banner is displayed and dryRunResult resets to null.
|
||||
* @PURPOSE: Performs a dry-run migration to identify potential risks and changes.
|
||||
* @PRE: source/target environments and selected dashboards are valid.
|
||||
* @POST: dryRunResult is populated with the pre-flight analysis.
|
||||
* @UX_STATE: [Loading] -> [Success] or [Error]
|
||||
* @UX_FEEDBACK: User sees summary cards + risk block + JSON details after success.
|
||||
* @UX_RECOVERY: User can adjust selection and press Dry Run again.
|
||||
*/
|
||||
async function startDryRun() {
|
||||
return belief_scope("startDryRun", async () => {
|
||||
if (!sourceEnvId || !targetEnvId) {
|
||||
console.warn("[startDryRun][EXPLORE] Missing environment selection");
|
||||
error =
|
||||
$t.migration?.select_both_envs ||
|
||||
"Please select both source and target environments.";
|
||||
return;
|
||||
}
|
||||
if (sourceEnvId === targetEnvId) {
|
||||
console.warn("[startDryRun][EXPLORE] Source and target environments are identical");
|
||||
error =
|
||||
$t.migration?.different_envs ||
|
||||
"Source and target environments must be different.";
|
||||
return;
|
||||
}
|
||||
if (selectedDashboardIds.length === 0) {
|
||||
console.warn("[startDryRun][EXPLORE] No dashboards selected");
|
||||
error =
|
||||
$t.migration?.select_dashboards ||
|
||||
"Please select at least one dashboard to migrate.";
|
||||
return;
|
||||
}
|
||||
|
||||
console.info("[startDryRun][REASON] Initiating dry-run analysis", { sourceEnvId, targetEnvId });
|
||||
error = "";
|
||||
dryRunLoading = true;
|
||||
try {
|
||||
@@ -394,7 +439,9 @@
|
||||
fix_cross_filters: fixCrossFilters,
|
||||
};
|
||||
dryRunResult = await api.postApi("/migration/dry-run", selection);
|
||||
console.info("[startDryRun][REFLECT] Dry-run analysis completed", { riskScore: dryRunResult.risk.score });
|
||||
} catch (e) {
|
||||
console.error("[startDryRun][EXPLORE] Dry-run analysis failed", e);
|
||||
error = e.message;
|
||||
dryRunResult = null;
|
||||
} finally {
|
||||
@@ -418,20 +465,29 @@
|
||||
-->
|
||||
<!-- [SECTION: TEMPLATE] -->
|
||||
<div class="max-w-4xl mx-auto p-6">
|
||||
<!-- [DEF:MigrationHeader:Block] -->
|
||||
<PageHeader title={$t.nav.migration} />
|
||||
<!-- [/DEF:MigrationHeader:Block] -->
|
||||
|
||||
<!-- [DEF:TaskHistorySection:Block] -->
|
||||
<TaskHistory on:viewLogs={handleViewLogs} />
|
||||
<!-- [/DEF:TaskHistorySection:Block] -->
|
||||
|
||||
<!-- [DEF:ActiveTaskSection:Block] -->
|
||||
{#if $selectedTask}
|
||||
<div class="mt-6">
|
||||
<TaskRunner />
|
||||
<div class="mt-4">
|
||||
<Button variant="secondary" on:click={() => selectedTask.set(null)}>
|
||||
<Button variant="secondary" on:click={() => {
|
||||
console.info("[ActiveTaskSection][REASON] User cancelled active task view");
|
||||
selectedTask.set(null);
|
||||
}}>
|
||||
{$t.common.cancel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- [/DEF:ActiveTaskSection:Block] -->
|
||||
{#if loading}
|
||||
<p>{$t.migration?.loading_envs }</p>
|
||||
{:else if error}
|
||||
@@ -442,6 +498,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- [DEF:EnvironmentSelectionSection:Block] -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<EnvSelector
|
||||
label={$t.migration?.source_env }
|
||||
@@ -454,6 +511,7 @@
|
||||
{environments}
|
||||
/>
|
||||
</div>
|
||||
<!-- [/DEF:EnvironmentSelectionSection:Block] -->
|
||||
|
||||
<!-- [DEF:DashboardSelectionSection:Component] -->
|
||||
<div class="mb-8">
|
||||
@@ -476,6 +534,7 @@
|
||||
</div>
|
||||
<!-- [/DEF:DashboardSelectionSection:Component] -->
|
||||
|
||||
<!-- [DEF:MigrationOptionsSection:Block] -->
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<input
|
||||
@@ -496,6 +555,7 @@
|
||||
type="checkbox"
|
||||
bind:checked={replaceDb}
|
||||
on:change={() => {
|
||||
console.info("[MigrationOptionsSection][REASON] Database replacement toggled", { replaceDb });
|
||||
if (replaceDb && sourceDatabases.length === 0) fetchDatabases();
|
||||
}}
|
||||
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
|
||||
@@ -505,6 +565,7 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- [/DEF:MigrationOptionsSection:Block] -->
|
||||
|
||||
{#if replaceDb}
|
||||
<div class="mb-8 p-4 border rounded-md bg-gray-50">
|
||||
@@ -559,6 +620,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- [DEF:DryRunResultsSection:Block] -->
|
||||
{#if dryRunResult}
|
||||
<div class="mt-6 rounded-md border border-slate-200 bg-slate-50 p-4 space-y-3">
|
||||
<h3 class="text-base font-semibold">Pre-flight Diff</h3>
|
||||
@@ -599,15 +661,24 @@
|
||||
</details>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- [/DEF:DryRunResultsSection:Block] -->
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Modals -->
|
||||
<!-- [DEF:MigrationModals:Block] -->
|
||||
<!--
|
||||
@PURPOSE: Render overlay components for log viewing and password entry.
|
||||
@UX_STATE: [LogViewing] -> TaskLogViewer is visible.
|
||||
@UX_STATE: [AwaitingInput] -> PasswordPrompt is visible.
|
||||
-->
|
||||
<TaskLogViewer
|
||||
bind:show={showLogViewer}
|
||||
taskId={logViewerTaskId}
|
||||
taskStatus={logViewerTaskStatus}
|
||||
on:close={() => (showLogViewer = false)}
|
||||
on:close={() => {
|
||||
console.info("[MigrationModals][REASON] Closing log viewer");
|
||||
showLogViewer = false;
|
||||
}}
|
||||
/>
|
||||
|
||||
<PasswordPrompt
|
||||
@@ -615,8 +686,12 @@
|
||||
databases={passwordPromptDatabases}
|
||||
errorMessage={passwordPromptErrorMessage}
|
||||
on:resume={handleResumeMigration}
|
||||
on:cancel={() => (showPasswordPrompt = false)}
|
||||
on:cancel={() => {
|
||||
console.info("[MigrationModals][REASON] User cancelled password prompt");
|
||||
showPasswordPrompt = false;
|
||||
}}
|
||||
/>
|
||||
<!-- [/DEF:MigrationModals:Block] -->
|
||||
|
||||
<!-- [/SECTION] -->
|
||||
<!-- [/DEF:MigrationDashboardView:Block] -->
|
||||
|
||||
Reference in New Issue
Block a user