- Replace all bg-blue-600/hover:bg-blue-700 → bg-primary/hover:bg-primary-hover - Replace all bg-red-600/hover:bg-red-700 → bg-destructive/hover:bg-destructive-hover - Replace all focus:ring-blue-500 → focus-visible:ring-primary-ring - Replace all text-blue-600 → text-primary on interactive elements - Replace all bg-blue-50 → bg-primary-light on UI surfaces - Replace all bg-red-50 → bg-destructive-light on error surfaces - Replace all border-blue-200/300 → border-primary-ring - Replace all border-red-200 → border-destructive-light - Replace peer-checked:bg-blue-600 → peer-checked:bg-primary (toggles) - Replace focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 patterns - Keep status badge patterns (bg-*-100 text-*-700) and hover:bg-blue-50 intact 82 files changed, ~400 changes. Build passes.
253 lines
9.3 KiB
Svelte
253 lines
9.3 KiB
Svelte
<!-- #region MapperTool [C:3] [TYPE Component] [SEMANTICS mapper, dataset, column, mapping, postgresql] -->
|
|
<!-- @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.
|
|
@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';
|
|
import { t } from '../../lib/i18n';
|
|
import { Button, Card, Select, Input } from '../../lib/ui';
|
|
import DocPreview from '../llm/DocPreview.svelte';
|
|
// [/SECTION]
|
|
|
|
let envs = [];
|
|
let connections = [];
|
|
let selectedEnv = '';
|
|
let datasetId = '';
|
|
let source = 'postgres';
|
|
let selectedConnection = '';
|
|
let tableName = '';
|
|
let tableSchema = 'public';
|
|
let excelPath = '';
|
|
let isRunning = false;
|
|
let isGeneratingDocs = false;
|
|
let generatedDoc = null;
|
|
|
|
// #region fetchData:Function [TYPE Function]
|
|
// @PURPOSE: Fetches environments and saved connections.
|
|
// @PRE: None.
|
|
// @POST: envs and connections arrays are populated.
|
|
async function fetchData() {
|
|
try {
|
|
envs = await api.fetchApi('/environments');
|
|
connections = await getConnections();
|
|
} catch (e) {
|
|
addToast($t.mapper.errors.fetch_failed, 'error');
|
|
}
|
|
}
|
|
// #endregion fetchData:Function
|
|
|
|
// #region handleRunMapper:Function [TYPE Function]
|
|
// @PURPOSE: Triggers the MapperPlugin task.
|
|
// @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 === 'postgres' && (!selectedConnection || !tableName)) {
|
|
addToast($t.mapper.errors.postgres_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,
|
|
connection_id: selectedConnection,
|
|
table_name: tableName,
|
|
table_schema: tableSchema,
|
|
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="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>
|
|
</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 === 'postgres'}
|
|
<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 }))
|
|
]}
|
|
/>
|
|
</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>
|
|
</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 -->
|