semantic update

This commit is contained in:
2026-03-10 21:33:09 +03:00
parent 542835e0ff
commit b77fa45e4e
10 changed files with 2084 additions and 1499 deletions

View File

@@ -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] -->