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
This commit is contained in:
2026-05-21 18:47:41 +03:00
parent b529e8082a
commit 36643024cf
35 changed files with 269 additions and 1315 deletions

View File

@@ -54,7 +54,6 @@
</button>
<div class="absolute hidden group-hover:block bg-white shadow-lg rounded-md mt-1 py-2 w-48 z-10 border border-gray-100 before:absolute before:-top-2 before:left-0 before:right-0 before:h-2 before:content-[''] right-0">
<a href="/settings" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-primary">{$t.nav.settings_general}</a>
<a href="/settings/connections" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-primary">{$t.nav.settings_connections}</a>
<a href="/settings/git" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-primary">{$t.nav.settings_git}</a>
</div>
</div>

View File

@@ -1,95 +0,0 @@
<!-- #region ConnectionForm [C:3] [TYPE Component] [SEMANTICS connection, form, database, crud, create] -->
<!-- @BRIEF Component component: components/tools/ConnectionForm.svelte -->
<!-- @LAYER UI -->
<!--
@SEMANTICS: connection, form, settings
@PURPOSE: UI component for creating a new database connection configuration.
@LAYER: UI
@RELATION: USES -> frontend/src/services/connectionService.js
-->
<script>
// [SECTION: IMPORTS]
import { createConnection } from '../../services/connectionService.js';
import { addToast } from '../../lib/toasts.js';
import { t } from '../../lib/i18n';
import { Button, Input, Card } from '../../lib/ui';
// [/SECTION]
let { onsuccess = () => {} } = $props();
let name = $state('');
let type = $state('postgres');
let host = $state('');
let port = $state("5432");
let database = $state('');
let username = $state('');
let password = $state('');
let isSubmitting = $state(false);
// #region handleSubmit:Function [TYPE Function]
// @PURPOSE: Submits the connection form to the backend.
// @PRE: All required fields (name, host, database, username, password) must be filled.
// @POST: A new connection is created via the connection service and the parent success callback is invoked.
async function handleSubmit() {
if (!name || !host || !database || !username || !password) {
addToast($t.connections?.required_fields, 'warning');
return;
}
isSubmitting = true;
try {
const newConnection = await createConnection({
name, type, host, port: Number(port), database, username, password
});
addToast($t.connections?.created_success, 'success');
onsuccess(newConnection);
resetForm();
} catch (e) {
addToast(e.message, 'error');
} finally {
isSubmitting = false;
}
}
// #endregion handleSubmit:Function
// #region resetForm:Function [TYPE Function]
/* @PURPOSE: Resets the connection form fields to their default values.
@PRE: None.
@POST: All form input variables are reset.
*/
function resetForm() {
name = '';
host = '';
port = "5432";
database = '';
username = '';
password = '';
}
// #endregion resetForm:Function
</script>
<!-- [SECTION: TEMPLATE] -->
<Card title={$t.connections?.add_new}>
<form onsubmit={(event) => { event.preventDefault(); handleSubmit(); }} class="space-y-6">
<Input label={$t.connections?.name} bind:value={name} placeholder={$t.connections?.name_placeholder} />
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<Input label={$t.connections?.host} bind:value={host} placeholder={$t.connections?.host_placeholder} />
<Input label={$t.connections?.port} type="number" bind:value={port} />
</div>
<Input label={$t.connections?.db_name} bind:value={database} />
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<Input label={$t.connections?.user} bind:value={username} />
<Input label={$t.connections?.pass} type="password" bind:value={password} />
</div>
<div class="flex justify-end pt-2">
<Button type="submit" disabled={isSubmitting} isLoading={isSubmitting}>
{$t.connections?.create}
</Button>
</div>
</form>
</Card>
<!-- [/SECTION] -->
<!-- #endregion ConnectionForm -->

View File

@@ -1,88 +0,0 @@
<!-- #region ConnectionList [C:3] [TYPE Component] [SEMANTICS connection, list, crud, delete, manage] -->
<!-- @BRIEF Component component: components/tools/ConnectionList.svelte -->
<!-- @LAYER UI -->
<!--
@SEMANTICS: connection, list, settings
@PURPOSE: UI component for listing and deleting saved database connection configurations.
@LAYER: UI
@RELATION: USES -> frontend/src/services/connectionService.js
-->
<script>
// [SECTION: IMPORTS]
import { onMount } from 'svelte';
import { getConnections, deleteConnection } from '../../services/connectionService.js';
import { addToast } from '../../lib/toasts.js';
import { t } from '../../lib/i18n';
import { Button, Card } from '../../lib/ui';
// [/SECTION]
let connections = $state([]);
let isLoading = $state(true);
// #region fetchConnections:Function [TYPE Function]
// @PURPOSE: Fetches the list of connections from the backend.
// @PRE: None.
// @POST: connections array is populated.
async function fetchConnections() {
isLoading = true;
try {
connections = await getConnections();
} catch (e) {
addToast($t.connections?.fetch_failed, 'error');
} finally {
isLoading = false;
}
}
// #endregion fetchConnections:Function
// #region handleDelete:Function [TYPE Function]
// @PURPOSE: Deletes a connection configuration.
// @PRE: id is provided and user confirms deletion.
// @POST: Connection is deleted from backend and list is reloaded.
async function handleDelete(id) {
if (!confirm($t.connections?.delete_confirm)) return;
try {
await deleteConnection(id);
addToast($t.connections?.deleted_success, 'success');
await fetchConnections();
} catch (e) {
addToast(e.message, 'error');
}
}
// #endregion handleDelete:Function
onMount(fetchConnections);
// Expose fetchConnections to parent
export { fetchConnections };
</script>
<!-- [SECTION: TEMPLATE] -->
<Card title={$t.connections?.saved} padding="none">
<ul class="divide-y divide-gray-100">
{#if isLoading}
<li class="p-6 text-center text-gray-500">{$t.common.loading}</li>
{:else if connections.length === 0}
<li class="p-12 text-center text-gray-500 italic">{$t.connections?.no_saved}</li>
{:else}
{#each connections as conn}
<li class="p-6 flex items-center justify-between hover:bg-gray-50 transition-colors">
<div>
<div class="text-sm font-medium text-primary truncate">{conn.name}</div>
<div class="text-xs text-gray-400 mt-1 font-mono">{conn.type}://{conn.username}@{conn.host}:{conn.port}/{conn.database}</div>
</div>
<Button
variant="danger"
size="sm"
onclick={() => handleDelete(conn.id)}
>
{$t.connections?.delete}
</Button>
</li>
{/each}
{/if}
</ul>
</Card>
<!-- [/SECTION] -->
<!-- #endregion ConnectionList -->

View File

@@ -1,18 +1,16 @@
<!-- #region MapperTool [C:3] [TYPE Component] [SEMANTICS mapper, dataset, column, mapping, postgresql] -->
<!-- #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, postgresql, excel
@PURPOSE: UI component for mapping dataset column verbose names using the MapperPlugin.
@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
@RELATION: USES -> frontend/src/services/connectionService.js
-->
<script>
// [SECTION: IMPORTS]
import { onMount } from 'svelte';
import { runTask } from '../../services/toolsService.js';
import { getConnections } from '../../services/connectionService.js';
import { api } from '../../lib/api';
import { selectedTask } from '../../lib/stores.js';
import { addToast } from '../../lib/toasts.js';
@@ -22,26 +20,23 @@
// [/SECTION]
let envs = [];
let connections = [];
let selectedEnv = '';
let datasetId = '';
let source = 'postgres';
let selectedConnection = '';
let tableName = '';
let tableSchema = 'public';
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 and saved connections.
// @PURPOSE: Fetches environments.
// @PRE: None.
// @POST: envs and connections arrays are populated.
// @POST: envs array is populated.
async function fetchData() {
try {
envs = await api.fetchApi('/environments');
connections = await getConnections();
} catch (e) {
addToast($t.mapper.errors.fetch_failed, 'error');
}
@@ -49,7 +44,7 @@
// #endregion fetchData:Function
// #region handleRunMapper:Function [TYPE Function]
// @PURPOSE: Triggers the MapperPlugin task.
// @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() {
@@ -58,8 +53,8 @@
return;
}
if (source === 'postgres' && (!selectedConnection || !tableName)) {
addToast($t.mapper.errors.postgres_required, 'warning');
if (source === 'sqllab' && !databaseId) {
addToast($t.mapper.errors.sqllab_required, 'warning');
return;
}
@@ -75,9 +70,8 @@
env: env.name,
dataset_id: parseInt(datasetId),
source,
connection_id: selectedConnection,
table_name: tableName,
table_schema: tableSchema,
database_id: databaseId ? parseInt(databaseId) : undefined,
sql_query: sqlQuery || undefined,
excel_path: excelPath
});
@@ -169,8 +163,8 @@
<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="postgres" 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_postgres}</span>
<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" />
@@ -179,33 +173,23 @@
</div>
</fieldset>
{#if source === 'postgres'}
{#if source === 'sqllab'}
<div class="space-y-4 p-4 bg-gray-50 rounded-md border border-gray-100">
<div>
<Select
label={$t.mapper.connection}
bind:value={selectedConnection}
options={[
{ value: '', label: $t.mapper.select_connection },
...connections.map(c => ({ value: c.id, label: c.name }))
]}
<Input
label={$t.mapper.database_id}
type="number"
bind:value={databaseId}
placeholder={$t.mapper.database_id_placeholder}
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Input
label={$t.mapper.table_name}
type="text"
bind:value={tableName}
/>
</div>
<div>
<Input
label={$t.mapper.table_schema}
type="text"
bind:value={tableSchema}
/>
</div>
<div>
<Input
label={$t.mapper.sql_query_label}
type="text"
bind:value={sqlQuery}
placeholder={$t.mapper.sql_query_placeholder}
/>
</div>
</div>
{:else}
@@ -249,4 +233,4 @@
/>
</div>
<!-- [/SECTION] -->
<!-- #endregion MapperTool -->
<!-- #endregion MapperTool -->