Files
ss-tools/frontend/src/components/tools/MapperTool.svelte
busya 36643024cf refactor(connections): remove ConnectionConfig, migrate mapper to SQL Lab
Backend:
- Remove ConnectionConfig model, CRUD routes (connections.py), and tests
- Remove ensure_connection_configs_table from database.py
- Migrate MapperPlugin from psycopg2 direct PostgreSQL to SupersetSqlLabExecutor
- Add DatasetMapper.get_sqllab_mappings() with default information_schema query
- Update MapColumnsRequest: connection_id → database_id + sql_query
- Fix pydantic_settings v2 deprecation (Field(env=...) → validation_alias)
- Clean up app.py, routes/__init__.py, database.py imports

Frontend:
- Remove DatabaseConnectionsTab, ConnectionForm, ConnectionList components
- Remove /settings/connections route page and Navbar link
- Remove connectionService.js, connections i18n files
- Update MapperTool.svelte: postgres → sqllab source
- Update Map Columns modal: database selector + SQL query instead of connection_id
- Clean up settings page tabs, nav links, test mocks
- Clean up i18n keys (settings, nav, mapper)

Tests: 25/25 pass (14 030-feature + 11 route tests)
Build: succeeds
2026-05-21 18:47:41 +03:00

236 lines
8.6 KiB
Svelte

<!-- #region MapperTool [C:3] [TYPE Component] [SEMANTICS mapper, dataset, column, mapping, sqllab] -->
<!-- @BRIEF Component component: components/tools/MapperTool.svelte -->
<!-- @LAYER UI -->
<!--
@SEMANTICS: mapper, tool, dataset, sqllab, excel
@PURPOSE: UI component for mapping dataset column verbose names using Superset SQL Lab or Excel files.
@LAYER: UI
@RELATION: USES -> frontend/src/services/toolsService.js
-->
<script>
// [SECTION: IMPORTS]
import { onMount } from 'svelte';
import { runTask } from '../../services/toolsService.js';
import { api } from '../../lib/api';
import { selectedTask } from '../../lib/stores.js';
import { addToast } from '../../lib/toasts.js';
import { t } from '../../lib/i18n';
import { Button, Card, Select, Input } from '../../lib/ui';
import DocPreview from '../llm/DocPreview.svelte';
// [/SECTION]
let envs = [];
let selectedEnv = '';
let datasetId = '';
let source = 'sqllab';
let databaseId = '';
let sqlQuery = '';
let excelPath = '';
let isRunning = false;
let isGeneratingDocs = false;
let generatedDoc = null;
// #region fetchData:Function [TYPE Function]
// @PURPOSE: Fetches environments.
// @PRE: None.
// @POST: envs array is populated.
async function fetchData() {
try {
envs = await api.fetchApi('/environments');
} catch (e) {
addToast($t.mapper.errors.fetch_failed, 'error');
}
}
// #endregion fetchData:Function
// #region handleRunMapper:Function [TYPE Function]
// @PURPOSE: Triggers the MapperPlugin task via new sqllab/excel sources.
// @PRE: selectedEnv and datasetId are set; source-specific fields are valid.
// @POST: Mapper task is started and selectedTask is updated.
async function handleRunMapper() {
if (!selectedEnv || !datasetId) {
addToast($t.mapper.errors.required_fields, 'warning');
return;
}
if (source === 'sqllab' && !databaseId) {
addToast($t.mapper.errors.sqllab_required, 'warning');
return;
}
if (source === 'excel' && !excelPath) {
addToast($t.mapper.errors.excel_required, 'warning');
return;
}
isRunning = true;
try {
const env = envs.find(e => e.id === selectedEnv);
const task = await runTask('dataset-mapper', {
env: env.name,
dataset_id: parseInt(datasetId),
source,
database_id: databaseId ? parseInt(databaseId) : undefined,
sql_query: sqlQuery || undefined,
excel_path: excelPath
});
selectedTask.set(task);
addToast($t.mapper.success.started, 'success');
} catch (e) {
addToast(e.message, 'error');
} finally {
isRunning = false;
}
}
// #endregion handleRunMapper:Function
// #region handleGenerateDocs:Function [TYPE Function]
// @PURPOSE: Triggers the LLM Documentation task.
// @PRE: selectedEnv and datasetId are set.
// @POST: Documentation task is started.
async function handleGenerateDocs() {
if (!selectedEnv || !datasetId) {
addToast($t.mapper.errors.required_fields, 'warning');
return;
}
isGeneratingDocs = true;
try {
// Fetch active provider first
const providers = await api.fetchApi('/llm/providers');
const activeProvider = providers.find(p => p.is_active);
if (!activeProvider) {
addToast($t.mapper?.errors?.no_active_llm_provider, 'error');
return;
}
const task = await runTask('llm_documentation', {
dataset_id: datasetId,
environment_id: selectedEnv,
provider_id: activeProvider.id
});
selectedTask.set(task);
addToast($t.mapper?.success?.docs_started, 'success');
} catch (e) {
addToast(e.message || $t.mapper?.errors?.docs_start_failed, 'error');
} finally {
isGeneratingDocs = false;
}
}
// #endregion handleGenerateDocs:Function
async function handleApplyDoc(doc) {
try {
await api.put(`/mappings/datasets/${datasetId}/metadata`, doc);
generatedDoc = null;
addToast($t.mapper?.success?.docs_applied, 'success');
} catch (err) {
addToast(err.message || $t.mapper?.errors?.docs_apply_failed, 'error');
}
}
onMount(fetchData);
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="space-y-6">
<Card title={$t.mapper.title}>
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Select
label={$t.mapper.environment}
bind:value={selectedEnv}
options={[
{ value: '', label: $t.mapper.select_env },
...envs.map(e => ({ value: e.id, label: e.name }))
]}
/>
</div>
<div>
<Input
label={$t.mapper.dataset_id}
type="number"
bind:value={datasetId}
/>
</div>
</div>
<fieldset>
<legend class="block text-sm font-medium text-gray-700 mb-2">{$t.mapper.source}</legend>
<div class="flex space-x-4">
<label class="inline-flex items-center">
<input type="radio" bind:group={source} value="sqllab" class="focus-visible:ring-primary-ring h-4 w-4 text-primary border-gray-300" />
<span class="ml-2 text-sm text-gray-700">{$t.mapper.source_sqllab}</span>
</label>
<label class="inline-flex items-center">
<input type="radio" bind:group={source} value="excel" class="focus-visible:ring-primary-ring h-4 w-4 text-primary border-gray-300" />
<span class="ml-2 text-sm text-gray-700">{$t.mapper.source_excel}</span>
</label>
</div>
</fieldset>
{#if source === 'sqllab'}
<div class="space-y-4 p-4 bg-gray-50 rounded-md border border-gray-100">
<div>
<Input
label={$t.mapper.database_id}
type="number"
bind:value={databaseId}
placeholder={$t.mapper.database_id_placeholder}
/>
</div>
<div>
<Input
label={$t.mapper.sql_query_label}
type="text"
bind:value={sqlQuery}
placeholder={$t.mapper.sql_query_placeholder}
/>
</div>
</div>
{:else}
<div class="p-4 bg-gray-50 rounded-md border border-gray-100">
<Input
label={$t.mapper.excel_path}
type="text"
bind:value={excelPath}
placeholder={$t.mapper?.excel_placeholder}
/>
</div>
{/if}
<div class="flex justify-end pt-2 space-x-3">
<Button
variant="secondary"
onclick={handleGenerateDocs}
disabled={isGeneratingDocs || isRunning}
>
{#if isGeneratingDocs}
<span class="animate-spin mr-1"></span> {$t.mapper?.generating}
{:else}
<span class="mr-1"></span> {$t.datasets?.generate_docs}
{/if}
</Button>
<Button
variant="primary"
onclick={handleRunMapper}
disabled={isRunning}
>
{isRunning ? $t.mapper.starting : $t.mapper.run}
</Button>
</div>
</div>
</Card>
<DocPreview
documentation={generatedDoc}
onCancel={() => generatedDoc = null}
onSave={handleApplyDoc}
/>
</div>
<!-- [/SECTION] -->
<!-- #endregion MapperTool -->