semantic clean up

This commit is contained in:
2026-03-10 19:38:10 +03:00
parent 31717870e3
commit 542835e0ff
31 changed files with 5392 additions and 6647 deletions

View File

@@ -1,11 +1,45 @@
<!-- [DEF:frontend/src/routes/migration/+page.svelte:Module] -->
<!--
@TIER: CRITICAL
@SEMANTICS: migration, dashboard, environment, selection, database-replacement, dry-run, task-resume
@PURPOSE: Main migration dashboard page for environment selection, dry-run validation, and migration execution.
@LAYER: UI
@RELATION: [DEPENDS_ON] ->[frontend/src/lib/api.js]
@RELATION: [DEPENDS_ON] ->[frontend/src/lib/stores.js]
@RELATION: [DEPENDS_ON] ->[frontend/src/services/taskService.js]
@RELATION: [BINDS_TO] ->[frontend/src/components/EnvSelector.svelte]
@RELATION: [BINDS_TO] ->[frontend/src/components/DashboardGrid.svelte]
@RELATION: [BINDS_TO] ->[frontend/src/components/MappingTable.svelte]
@RELATION: [BINDS_TO] ->[frontend/src/components/TaskRunner.svelte]
@RELATION: [BINDS_TO] ->[frontend/src/components/TaskHistory.svelte]
@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_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.
@TEST_CONTRACT: [DashboardSelection + Environment IDs] -> [DryRunResult | TaskStartResult | ValidationError]
@TEST_SCENARIO: start_migration_valid_selection -> Starts backend task and switches to task view.
@TEST_SCENARIO: start_dry_run_valid_selection -> Renders pre-flight diff summary and risk panel.
@TEST_SCENARIO: awaiting_input_task_selected -> Opens password prompt with requested databases.
@TEST_FIXTURE: migration_dry_run_fixture -> file:backend/tests/fixtures/migration_dry_run_fixture.json
@TEST_EDGE: missing_field -> Empty source/target/selection blocks action and surfaces validation message.
@TEST_EDGE: invalid_type -> Malformed API payload is surfaced through error state.
@TEST_EDGE: external_fail -> API failures set error state and preserve recoverable UI controls.
@TEST_INVARIANT: migration_guardrails_enforced -> VERIFIED_BY: [start_migration_valid_selection, start_dry_run_valid_selection, missing_field]
-->
<!-- [DEF:MigrationDashboard:Component] -->
<!--
@SEMANTICS: migration, dashboard, environment, selection, database-replacement
@PURPOSE: Main dashboard for configuring and starting migrations.
@LAYER: Page
@RELATION: USES -> EnvSelector
@INVARIANT: Migration cannot start without source and target environments.
@PURPOSE: Orchestrate migration UI workflow and route user actions to backend APIs and task store.
@PRE: API client and component dependencies are available; i18n store is initialized.
@POST: User can progress through selection, dry-run, migration start, and task resume flows.
@SIDE_EFFECT: Performs HTTP requests, mutates local UI state, updates selectedTask store.
@DATA_CONTRACT: DashboardSelection -> MigrationDryRunResult | Task DTO (from /tasks endpoints)
-->
<script lang="ts">
@@ -58,6 +92,8 @@
let passwordPromptErrorMessage = "";
// [/SECTION]
const belief_scope = <T>(_id: string, fn: () => T): T => fn();
// [DEF:fetchEnvironments:Function]
/**
* @purpose Fetches the list of environments from the API.
@@ -65,13 +101,15 @@
* @post environments state is updated.
*/
async function fetchEnvironments() {
try {
environments = await api.getEnvironmentsList();
} catch (e) {
error = e.message;
} finally {
loading = false;
}
return belief_scope("fetchEnvironments", async () => {
try {
environments = await api.getEnvironmentsList();
} catch (e) {
error = e.message;
} finally {
loading = false;
}
});
}
// [/DEF:fetchEnvironments:Function]
@@ -83,13 +121,15 @@
* @post dashboards state is updated.
*/
async function fetchDashboards(envId: string) {
try {
dashboards = await api.requestApi(`/environments/${envId}/dashboards`);
selectedDashboardIds = []; // Reset selection when env changes
} catch (e) {
error = e.message;
dashboards = [];
}
return belief_scope("fetchDashboards", async () => {
try {
dashboards = await api.requestApi(`/environments/${envId}/dashboards`);
selectedDashboardIds = []; // Reset selection when env changes
} catch (e) {
error = e.message;
dashboards = [];
}
});
}
// [/DEF:fetchDashboards:Function]
@@ -105,32 +145,34 @@
* @post sourceDatabases, targetDatabases, mappings, and suggestions are updated.
*/
async function fetchDatabases() {
if (!sourceEnvId || !targetEnvId) return;
fetchingDbs = true;
error = "";
return belief_scope("fetchDatabases", async () => {
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,
}),
]);
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,
}),
]);
sourceDatabases = src;
targetDatabases = tgt;
mappings = maps;
suggestions = sugs;
} catch (e) {
error = e.message;
} finally {
fetchingDbs = false;
}
sourceDatabases = src;
targetDatabases = tgt;
mappings = maps;
suggestions = sugs;
} catch (e) {
error = e.message;
} finally {
fetchingDbs = false;
}
});
}
// [/DEF:fetchDatabases:Function]
@@ -141,29 +183,31 @@
* @post Mapping is saved and local mappings list is updated.
*/
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);
return belief_scope("handleMappingUpdate", async () => {
const { sourceUuid, targetUuid } = event.detail;
const sDb = sourceDatabases.find((d) => d.uuid === sourceUuid);
const tDb = targetDatabases.find((d) => d.uuid === targetUuid);
if (!sDb || !tDb) return;
if (!sDb || !tDb) return;
try {
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,
});
try {
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,
});
mappings = [
...mappings.filter((m) => m.source_db_uuid !== sourceUuid),
savedMapping,
];
} catch (e) {
error = e.message;
}
mappings = [
...mappings.filter((m) => m.source_db_uuid !== sourceUuid),
savedMapping,
];
} catch (e) {
error = e.message;
}
});
}
// [/DEF:handleMappingUpdate:Function]
@@ -172,10 +216,12 @@
// @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;
return belief_scope("handleViewLogs", () => {
const task = event.detail;
logViewerTaskId = task.id;
logViewerTaskStatus = task.status;
showLogViewer = true;
});
}
// [/DEF:handleViewLogs:Function]
@@ -212,19 +258,21 @@
// @PRE: event.detail contains passwords.
// @POST: resumeTask is called and showPasswordPrompt is hidden on success.
async function handleResumeMigration(event: CustomEvent) {
if (!$selectedTask) return;
return belief_scope("handleResumeMigration", async () => {
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 ;
// Keep prompt open
}
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;
// Keep prompt open
}
});
}
// [/DEF:handleResumeMigration:Function]
@@ -235,69 +283,71 @@
* @post Migration task is started and selectedTask is updated.
*/
async function startMigration() {
if (!sourceEnvId || !targetEnvId) {
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.";
return;
}
if (selectedDashboardIds.length === 0) {
error =
$t.migration?.select_dashboards ||
"Please select at least one dashboard to migrate.";
return;
}
error = "";
try {
dryRunResult = null;
const selection: DashboardSelection = {
selected_ids: selectedDashboardIds,
source_env_id: sourceEnvId,
target_env_id: targetEnvId,
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}`,
);
// Wait a brief moment for the backend to ensure the task is retrievable
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.",
);
selectedTask.set({
id: result.task_id,
plugin_id: "superset-migration",
status: "RUNNING",
logs: [],
params: {},
});
return belief_scope("startMigration", async () => {
if (!sourceEnvId || !targetEnvId) {
error =
$t.migration?.select_both_envs ||
"Please select both source and target environments.";
return;
}
} catch (e) {
console.error(`[MigrationDashboard][Failure] Migration failed:`, e);
error = e.message;
}
if (sourceEnvId === targetEnvId) {
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.";
return;
}
error = "";
try {
dryRunResult = null;
const selection: DashboardSelection = {
selected_ids: selectedDashboardIds,
source_env_id: sourceEnvId,
target_env_id: targetEnvId,
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}`,
);
// Wait a brief moment for the backend to ensure the task is retrievable
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.",
);
selectedTask.set({
id: result.task_id,
plugin_id: "superset-migration",
status: "RUNNING",
logs: [],
params: {},
});
}
} catch (e) {
console.error(`[MigrationDashboard][Failure] Migration failed:`, e);
error = e.message;
}
});
}
// [/DEF:startMigration:Function]
@@ -313,46 +363,59 @@
* @UX_RECOVERY: User can adjust selection and press Dry Run again.
*/
async function startDryRun() {
if (!sourceEnvId || !targetEnvId) {
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.";
return;
}
if (selectedDashboardIds.length === 0) {
error =
$t.migration?.select_dashboards ||
"Please select at least one dashboard to migrate.";
return;
}
return belief_scope("startDryRun", async () => {
if (!sourceEnvId || !targetEnvId) {
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.";
return;
}
if (selectedDashboardIds.length === 0) {
error =
$t.migration?.select_dashboards ||
"Please select at least one dashboard to migrate.";
return;
}
error = "";
dryRunLoading = true;
try {
const selection: DashboardSelection = {
selected_ids: selectedDashboardIds,
source_env_id: sourceEnvId,
target_env_id: targetEnvId,
replace_db_config: replaceDb,
fix_cross_filters: fixCrossFilters,
};
dryRunResult = await api.postApi("/migration/dry-run", selection);
} catch (e) {
error = e.message;
dryRunResult = null;
} finally {
dryRunLoading = false;
}
error = "";
dryRunLoading = true;
try {
const selection: DashboardSelection = {
selected_ids: selectedDashboardIds,
source_env_id: sourceEnvId,
target_env_id: targetEnvId,
replace_db_config: replaceDb,
fix_cross_filters: fixCrossFilters,
};
dryRunResult = await api.postApi("/migration/dry-run", selection);
} catch (e) {
error = e.message;
dryRunResult = null;
} finally {
dryRunLoading = false;
}
});
}
// [/DEF:startDryRun:Function]
// [/DEF:MigrationDashboard:Component]
</script>
<!-- [DEF:MigrationDashboardView:Block] -->
<!--
@PURPOSE: Render migration configuration controls, action CTAs, dry-run results, and modal entry points.
@UX_STATE: Idle -> Configuration form is interactive.
@UX_STATE: Loading -> Loading messages and disabled buttons prevent duplicate actions.
@UX_STATE: Error -> Error banner is displayed without discarding current selection.
@UX_STATE: Success -> Dry-run details or TaskRunner content is presented.
@UX_FEEDBACK: Buttons, banners, cards, and dialogs provide immediate operation feedback.
@UX_RECOVERY: User can retry operations from the same page state.
-->
<!-- [SECTION: TEMPLATE] -->
<div class="max-w-4xl mx-auto p-6">
<PageHeader title={$t.nav.migration} />
@@ -556,5 +619,5 @@
/>
<!-- [/SECTION] -->
<!-- [/DEF:MigrationDashboard:Component] -->
<!-- [/DEF:MigrationDashboardView:Block] -->
<!-- [/DEF:frontend/src/routes/migration/+page.svelte:Module] -->