232 lines
9.8 KiB
Svelte
232 lines
9.8 KiB
Svelte
<!-- [DEF:frontend/src/routes/migration/mappings/+page.svelte:Module] -->
|
|
<!--
|
|
@TIER: CRITICAL
|
|
@SEMANTICS: migration-mappings, environment-selection, fuzzy-matching, persistence-contract, ux-state-machine
|
|
@PURPOSE: Render and orchestrate mapping management UI for source/target environments with backend persistence.
|
|
@LAYER: UI
|
|
@RELATION: [DEPENDS_ON] ->[api.client]
|
|
@RELATION: [DEPENDS_ON] ->[EnvSelector.svelte]
|
|
@RELATION: [DEPENDS_ON] ->[MappingTable.svelte]
|
|
@RELATION: [DEPENDS_ON] ->[i18n.t]
|
|
@RELATION: [BINDS_TO] ->[migration.mappings.route]
|
|
@INVARIANT: Persisted mapping state in backend remains the source of truth for rendered mapping pairs.
|
|
@PRE: Translation store and API client are available; route is mounted in authenticated UI shell.
|
|
@POST: UI exposes deterministic Idle/Loading/Error/Success states for environment loading, database fetch, and mapping save.
|
|
@SIDE_EFFECT: Performs network I/O to environment/database/mapping endpoints and mutates local UI state.
|
|
@DATA_CONTRACT: Input(Event: update{sourceUuid,targetUuid}) -> Model(MappingPayload); Output(UIState{environments,databases,mappings,suggestions,status})
|
|
@UX_STATE: Idle -> Await environment selection and user-triggered fetch.
|
|
@UX_STATE: Loading -> Show loading text/spinner while environments or databases are fetched.
|
|
@UX_STATE: Error -> Render red alert panel with backend error message.
|
|
@UX_STATE: Success -> Render green confirmation panel after mapping save.
|
|
@UX_FEEDBACK: Error panel for failed API calls; success panel for persisted mapping confirmation.
|
|
@UX_RECOVERY: Retry via "fetch databases" action; reselection of environments clears stale arrays.
|
|
@UX_REACTIVITY: Svelte bind/on directives and reactive template branches coordinate state transitions (legacy route; no semantic logic mutation in this task).
|
|
@TEST_CONTRACT: [Valid source/target env IDs + fetch click] -> [Databases, mappings, suggestions rendered]
|
|
@TEST_SCENARIO: [save_mapping_success] -> [success banner appears and mapping list replaces same-source item]
|
|
@TEST_SCENARIO: [fetch_env_fail] -> [error banner appears and loading state exits]
|
|
@TEST_FIXTURE: [migration_mapping_pair] -> file:backend/tests/fixtures/migration_dry_run_fixture.json
|
|
@TEST_EDGE: [missing_field] ->[event.detail lacks sourceUuid/targetUuid => no save]
|
|
@TEST_EDGE: [invalid_type] ->[non-string env IDs => fetch disabled/guarded]
|
|
@TEST_EDGE: [external_fail] ->[API request rejection => error state]
|
|
@TEST_INVARIANT: [backend_source_of_truth] -> VERIFIED_BY: [save_mapping_success, fetch_env_fail]
|
|
@CRITICAL_TRACE: Frontend scope; Python belief_scope/logger are not applicable in Svelte runtime. Reflective tracing, when added, must use console prefix [ID][REFLECT].
|
|
-->
|
|
|
|
<script lang="ts">
|
|
// [DEF:MappingsPageScript:Block]
|
|
// @PURPOSE: Define imports, state, and handlers that drive migration mappings page FSM.
|
|
// @RELATION: [CALLS] ->[fetchEnvironments]
|
|
// @RELATION: [CALLS] ->[fetchDatabases]
|
|
// @RELATION: [CALLS] ->[handleUpdate]
|
|
|
|
// [DEF:Imports:Block]
|
|
import { onMount } from 'svelte';
|
|
import { api } from '../../../lib/api.js';
|
|
import EnvSelector from '../../../components/EnvSelector.svelte';
|
|
import MappingTable from '../../../components/MappingTable.svelte';
|
|
import { t } from '$lib/i18n';
|
|
import { Button, PageHeader } from '$lib/ui';
|
|
// [/DEF:Imports:Block]
|
|
|
|
// [DEF:UiState:Store]
|
|
// @PURPOSE: Maintain local page state for environments, fetched databases, mappings, suggestions, and UX messages.
|
|
let environments = [];
|
|
let sourceEnvId = "";
|
|
let targetEnvId = "";
|
|
let sourceDatabases = [];
|
|
let targetDatabases = [];
|
|
let mappings = [];
|
|
let suggestions = [];
|
|
let loading = true;
|
|
let fetchingDbs = false;
|
|
let error = "";
|
|
let success = "";
|
|
// [/DEF:UiState:Store]
|
|
|
|
// [DEF:belief_scope:Function]
|
|
// @PURPOSE: Frontend semantic scope wrapper for CRITICAL trace boundaries without changing business behavior.
|
|
// @PRE: scopeId is non-empty and run is callable.
|
|
// @POST: Executes run exactly once and returns/rejects with the same outcome.
|
|
// @SIDE_EFFECT: Emits trace logs for semantic scope entrance/exit.
|
|
// @DATA_CONTRACT: Input(scopeId:string, run:() => Promise<T>) -> Output(Promise<T>)
|
|
async function belief_scope<T>(scopeId: string, run: () => Promise<T>): Promise<T> {
|
|
console.info(`[${scopeId}][REASON] belief_scope.enter`);
|
|
try {
|
|
return await run();
|
|
} finally {
|
|
console.info(`[${scopeId}][REFLECT] belief_scope.exit`);
|
|
}
|
|
}
|
|
// [/DEF:belief_scope:Function]
|
|
|
|
// [DEF:fetchEnvironments:Function]
|
|
// @PURPOSE: Load environment options for source/target selectors on initial mount.
|
|
// @PRE: API client is initialized and route has mounted.
|
|
// @POST: loading=false and environments populated on success or error message set on failure.
|
|
// @SIDE_EFFECT: Network I/O to environments endpoint; mutates environments/error/loading.
|
|
// @DATA_CONTRACT: Input(void) -> Output(EnvironmentSummary[])
|
|
async function fetchEnvironments() {
|
|
return belief_scope('migration.mappings.fetchEnvironments', async () => {
|
|
try {
|
|
environments = await api.getEnvironmentsList();
|
|
} catch (e) {
|
|
error = e.message;
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
});
|
|
}
|
|
// [/DEF:fetchEnvironments:Function]
|
|
|
|
onMount(fetchEnvironments);
|
|
|
|
// [DEF:fetchDatabases:Function]
|
|
// @PURPOSE: Fetch both environment database catalogs, existing mappings, and suggested matches.
|
|
// @PRE: sourceEnvId and targetEnvId are both selected and non-empty.
|
|
// @POST: fetchingDbs=false and sourceDatabases/targetDatabases/mappings/suggestions updated or error set.
|
|
// @SIDE_EFFECT: Concurrent network I/O to environments, mappings, and suggestion endpoints; clears transient messages.
|
|
// @DATA_CONTRACT: Input({sourceEnvId,targetEnvId}) -> Output({sourceDatabases,targetDatabases,mappings,suggestions})
|
|
async function fetchDatabases() {
|
|
return belief_scope('migration.mappings.fetchDatabases', async () => {
|
|
if (!sourceEnvId || !targetEnvId) return;
|
|
fetchingDbs = true;
|
|
error = "";
|
|
success = "";
|
|
|
|
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;
|
|
}
|
|
});
|
|
}
|
|
// [/DEF:fetchDatabases:Function]
|
|
|
|
// [DEF:handleUpdate:Function]
|
|
// @PURPOSE: Persist a selected mapping pair and reconcile local mapping list by source database UUID.
|
|
// @PRE: event.detail includes sourceUuid/targetUuid and matching source/target database records exist.
|
|
// @POST: mapping persisted; local mappings replaced for same source UUID; success or error feedback shown.
|
|
// @SIDE_EFFECT: POST /mappings network I/O; mutates mappings/success/error.
|
|
// @DATA_CONTRACT: Input(CustomEvent<{sourceUuid:string,targetUuid:string}>) -> Output(MappingRecord persisted + UI feedback)
|
|
async function handleUpdate(event: CustomEvent) {
|
|
return belief_scope('migration.mappings.handleUpdate', 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;
|
|
|
|
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];
|
|
success = $t.migration?.mapping_saved ;
|
|
} catch (e) {
|
|
error = e.message;
|
|
}
|
|
});
|
|
}
|
|
// [/DEF:handleUpdate:Function]
|
|
// [/DEF:MappingsPageScript:Block]
|
|
</script>
|
|
|
|
<!-- [DEF:MappingsPageTemplate:Block] -->
|
|
<div class="max-w-6xl mx-auto p-6">
|
|
<PageHeader title={$t.migration?.mapping_management } />
|
|
|
|
{#if loading}
|
|
<p>{$t.migration?.loading_envs }</p>
|
|
{:else}
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
|
<EnvSelector
|
|
label={$t.migration?.source_env }
|
|
bind:selectedId={sourceEnvId}
|
|
{environments}
|
|
onchange={() => { sourceDatabases = []; mappings = []; suggestions = []; }}
|
|
/>
|
|
<EnvSelector
|
|
label={$t.migration?.target_env }
|
|
bind:selectedId={targetEnvId}
|
|
{environments}
|
|
onchange={() => { targetDatabases = []; mappings = []; suggestions = []; }}
|
|
/>
|
|
</div>
|
|
|
|
<div class="mb-8">
|
|
<Button
|
|
onclick={fetchDatabases}
|
|
disabled={!sourceEnvId || !targetEnvId || sourceEnvId === targetEnvId || fetchingDbs}
|
|
isLoading={fetchingDbs}
|
|
>
|
|
{$t.migration?.fetch_dbs }
|
|
</Button>
|
|
</div>
|
|
|
|
{#if error}
|
|
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
|
{error}
|
|
</div>
|
|
{/if}
|
|
|
|
{#if success}
|
|
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
|
|
{success}
|
|
</div>
|
|
{/if}
|
|
|
|
{#if sourceDatabases.length > 0}
|
|
<MappingTable
|
|
{sourceDatabases}
|
|
{targetDatabases}
|
|
{mappings}
|
|
{suggestions}
|
|
onupdate={handleUpdate}
|
|
/>
|
|
{:else if !fetchingDbs && sourceEnvId && targetEnvId}
|
|
<p class="text-gray-500 italic">{$t.migration?.mapping_hint }</p>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
<!-- [/DEF:MappingsPageTemplate:Block] -->
|
|
|
|
<!-- [/DEF:frontend/src/routes/migration/mappings/+page.svelte:Module] -->
|