This commit is contained in:
2026-01-12 12:33:51 +03:00
parent 7a9b1a190a
commit 696aac32e7
30 changed files with 1511 additions and 593 deletions

View File

@@ -35,12 +35,25 @@
>
Tasks
</a>
<a
href="/settings"
class="text-gray-600 hover:text-blue-600 font-medium {$page.url.pathname === '/settings' ? 'text-blue-600 border-b-2 border-blue-600' : ''}"
>
Settings
</a>
<div class="relative inline-block group">
<button class="text-gray-600 hover:text-blue-600 font-medium pb-1 {$page.url.pathname.startsWith('/tools') ? 'text-blue-600 border-b-2 border-blue-600' : ''}">
Tools
</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">
<a href="/tools/search" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">Dataset Search</a>
<a href="/tools/mapper" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">Dataset Mapper</a>
<a href="/tools/debug" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">System Debug</a>
</div>
</div>
<div class="relative inline-block group">
<button class="text-gray-600 hover:text-blue-600 font-medium pb-1 {$page.url.pathname.startsWith('/settings') ? 'text-blue-600 border-b-2 border-blue-600' : ''}">
Settings
</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">
<a href="/settings" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">General Settings</a>
<a href="/settings/connections" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">Connections</a>
</div>
</div>
</nav>
</header>
<!-- [/DEF:Navbar:Component] -->

View File

@@ -0,0 +1,99 @@
<!-- [DEF:ConnectionForm:Component] -->
<!--
@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 { createEventDispatcher } from 'svelte';
import { createConnection } from '../../services/connectionService.js';
import { addToast } from '../../lib/toasts.js';
// [/SECTION]
const dispatch = createEventDispatcher();
let name = '';
let type = 'postgres';
let host = '';
let port = 5432;
let database = '';
let username = '';
let password = '';
let isSubmitting = false;
// [DEF:handleSubmit:Function]
// @PURPOSE: Submits the connection form to the backend.
async function handleSubmit() {
if (!name || !host || !database || !username || !password) {
addToast('Please fill in all required fields', 'warning');
return;
}
isSubmitting = true;
try {
const newConnection = await createConnection({
name, type, host, port, database, username, password
});
addToast('Connection created successfully', 'success');
dispatch('success', newConnection);
resetForm();
} catch (e) {
addToast(e.message, 'error');
} finally {
isSubmitting = false;
}
}
function resetForm() {
name = '';
host = '';
port = 5432;
database = '';
username = '';
password = '';
}
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h3 class="text-lg font-medium text-gray-900 mb-4">Add New Connection</h3>
<form on:submit|preventDefault={handleSubmit} class="space-y-4">
<div>
<label for="conn-name" class="block text-sm font-medium text-gray-700">Connection Name</label>
<input type="text" id="conn-name" bind:value={name} placeholder="e.g. Production DWH" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="conn-host" class="block text-sm font-medium text-gray-700">Host</label>
<input type="text" id="conn-host" bind:value={host} placeholder="10.0.0.1" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
<div>
<label for="conn-port" class="block text-sm font-medium text-gray-700">Port</label>
<input type="number" id="conn-port" bind:value={port} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
</div>
<div>
<label for="conn-db" class="block text-sm font-medium text-gray-700">Database Name</label>
<input type="text" id="conn-db" bind:value={database} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="conn-user" class="block text-sm font-medium text-gray-700">Username</label>
<input type="text" id="conn-user" bind:value={username} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
<div>
<label for="conn-pass" class="block text-sm font-medium text-gray-700">Password</label>
<input type="password" id="conn-pass" bind:value={password} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
</div>
<div class="flex justify-end pt-2">
<button type="submit" disabled={isSubmitting} class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50">
{isSubmitting ? 'Creating...' : 'Create Connection'}
</button>
</div>
</form>
</div>
<!-- [/SECTION] -->
<!-- [/DEF:ConnectionForm:Component] -->

View File

@@ -0,0 +1,82 @@
<!-- [DEF:ConnectionList:Component] -->
<!--
@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, createEventDispatcher } from 'svelte';
import { getConnections, deleteConnection } from '../../services/connectionService.js';
import { addToast } from '../../lib/toasts.js';
// [/SECTION]
const dispatch = createEventDispatcher();
let connections = [];
let isLoading = true;
// [DEF:fetchConnections:Function]
// @PURPOSE: Fetches the list of connections from the backend.
async function fetchConnections() {
isLoading = true;
try {
connections = await getConnections();
} catch (e) {
addToast('Failed to fetch connections', 'error');
} finally {
isLoading = false;
}
}
// [DEF:handleDelete:Function]
// @PURPOSE: Deletes a connection configuration.
async function handleDelete(id) {
if (!confirm('Are you sure you want to delete this connection?')) return;
try {
await deleteConnection(id);
addToast('Connection deleted', 'success');
await fetchConnections();
} catch (e) {
addToast(e.message, 'error');
}
}
onMount(fetchConnections);
// Expose fetchConnections to parent
export { fetchConnections };
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="bg-white shadow overflow-hidden sm:rounded-md border border-gray-200">
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200">
<h3 class="text-lg leading-6 font-medium text-gray-900">Saved Connections</h3>
</div>
<ul class="divide-y divide-gray-200">
{#if isLoading}
<li class="p-4 text-center text-gray-500">Loading...</li>
{:else if connections.length === 0}
<li class="p-8 text-center text-gray-500 italic">No connections saved yet.</li>
{:else}
{#each connections as conn}
<li class="p-4 flex items-center justify-between hover:bg-gray-50">
<div>
<div class="text-sm font-medium text-indigo-600 truncate">{conn.name}</div>
<div class="text-xs text-gray-500">{conn.type}://{conn.username}@{conn.host}:{conn.port}/{conn.database}</div>
</div>
<button
on:click={() => handleDelete(conn.id)}
class="ml-2 inline-flex items-center px-2 py-1 border border-transparent text-xs font-medium rounded text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
Delete
</button>
</li>
{/each}
{/if}
</ul>
</div>
<!-- [/SECTION] -->
<!-- [/DEF:ConnectionList:Component] -->

View File

@@ -0,0 +1,164 @@
<!-- [DEF:DebugTool:Component] -->
<!--
@SEMANTICS: debug, tool, api, structure
@PURPOSE: UI component for system diagnostics and debugging API responses.
@LAYER: UI
@RELATION: USES -> frontend/src/services/toolsService.js
-->
<script>
// [SECTION: IMPORTS]
import { onMount } from 'svelte';
import { runTask, getTaskStatus } from '../../services/toolsService.js';
import { selectedTask } from '../../lib/stores.js';
import { addToast } from '../../lib/toasts.js';
// [/SECTION]
let envs = [];
let action = 'test-db-api';
let selectedEnv = '';
let datasetId = '';
let sourceEnv = '';
let targetEnv = '';
let isRunning = false;
let results = null;
let pollInterval;
async function fetchEnvironments() {
try {
const res = await fetch('/api/environments');
envs = await res.json();
} catch (e) {
addToast('Failed to fetch environments', 'error');
}
}
async function handleRunDebug() {
isRunning = true;
results = null;
try {
let params = { action };
if (action === 'test-db-api') {
if (!sourceEnv || !targetEnv) {
addToast('Source and Target environments are required', 'warning');
isRunning = false;
return;
}
const sEnv = envs.find(e => e.id === sourceEnv);
const tEnv = envs.find(e => e.id === targetEnv);
params.source_env = sEnv.name;
params.target_env = tEnv.name;
} else {
if (!selectedEnv || !datasetId) {
addToast('Environment and Dataset ID are required', 'warning');
isRunning = false;
return;
}
const env = envs.find(e => e.id === selectedEnv);
params.env = env.name;
params.dataset_id = parseInt(datasetId);
}
const task = await runTask('system-debug', params);
selectedTask.set(task);
startPolling(task.id);
} catch (e) {
isRunning = false;
addToast(e.message, 'error');
}
}
function startPolling(taskId) {
if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(async () => {
try {
const task = await getTaskStatus(taskId);
selectedTask.set(task);
if (task.status === 'SUCCESS') {
clearInterval(pollInterval);
isRunning = false;
results = task.result;
addToast('Debug task completed', 'success');
} else if (task.status === 'FAILED') {
clearInterval(pollInterval);
isRunning = false;
addToast('Debug task failed', 'error');
}
} catch (e) {
clearInterval(pollInterval);
isRunning = false;
}
}, 2000);
}
onMount(fetchEnvironments);
</script>
<div class="space-y-6">
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h3 class="text-lg font-medium text-gray-900 mb-4">System Diagnostics</h3>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Debug Action</label>
<select bind:value={action} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option value="test-db-api">Test Database API (Compare Envs)</option>
<option value="get-dataset-structure">Get Dataset Structure (JSON)</option>
</select>
</div>
{#if action === 'test-db-api'}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="src-env" class="block text-sm font-medium text-gray-700">Source Environment</label>
<select id="src-env" bind:value={sourceEnv} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option value="" disabled>-- Select Source --</option>
{#each envs as env}
<option value={env.id}>{env.name}</option>
{/each}
</select>
</div>
<div>
<label for="tgt-env" class="block text-sm font-medium text-gray-700">Target Environment</label>
<select id="tgt-env" bind:value={targetEnv} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option value="" disabled>-- Select Target --</option>
{#each envs as env}
<option value={env.id}>{env.name}</option>
{/each}
</select>
</div>
</div>
{:else}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="debug-env" class="block text-sm font-medium text-gray-700">Environment</label>
<select id="debug-env" bind:value={selectedEnv} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option value="" disabled>-- Select Environment --</option>
{#each envs as env}
<option value={env.id}>{env.name}</option>
{/each}
</select>
</div>
<div>
<label for="debug-ds-id" class="block text-sm font-medium text-gray-700">Dataset ID</label>
<input type="number" id="debug-ds-id" bind:value={datasetId} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
</div>
{/if}
<div class="mt-4 flex justify-end">
<button on:click={handleRunDebug} disabled={isRunning} class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50">
{isRunning ? 'Running...' : 'Run Diagnostics'}
</button>
</div>
</div>
{#if results}
<div class="bg-white shadow overflow-hidden sm:rounded-md border border-gray-200">
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200">
<h3 class="text-lg leading-6 font-medium text-gray-900">Debug Output</h3>
</div>
<div class="p-4">
<pre class="text-xs text-gray-600 bg-gray-900 text-green-400 p-4 rounded-md overflow-x-auto h-96">{JSON.stringify(results, null, 2)}</pre>
</div>
</div>
{/if}
</div>

View File

@@ -0,0 +1,159 @@
<!-- [DEF:MapperTool:Component] -->
<!--
@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 { selectedTask } from '../../lib/stores.js';
import { addToast } from '../../lib/toasts.js';
// [/SECTION]
let envs = [];
let connections = [];
let selectedEnv = '';
let datasetId = '';
let source = 'postgres';
let selectedConnection = '';
let tableName = '';
let tableSchema = 'public';
let excelPath = '';
let isRunning = false;
// [DEF:fetchData:Function]
// @PURPOSE: Fetches environments and saved connections.
async function fetchData() {
try {
const envsRes = await fetch('/api/environments');
envs = await envsRes.json();
connections = await getConnections();
} catch (e) {
addToast('Failed to fetch data', 'error');
}
}
// [DEF:handleRunMapper:Function]
// @PURPOSE: Triggers the MapperPlugin task.
async function handleRunMapper() {
if (!selectedEnv || !datasetId) {
addToast('Please fill in required fields', 'warning');
return;
}
if (source === 'postgres' && (!selectedConnection || !tableName)) {
addToast('Connection and Table Name are required for postgres source', 'warning');
return;
}
if (source === 'excel' && !excelPath) {
addToast('Excel path is required for excel source', '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('Mapper task started', 'success');
} catch (e) {
addToast(e.message, 'error');
} finally {
isRunning = false;
}
}
onMount(fetchData);
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h3 class="text-lg font-medium text-gray-900 mb-4">Dataset Column Mapper</h3>
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="mapper-env" class="block text-sm font-medium text-gray-700">Environment</label>
<select id="mapper-env" bind:value={selectedEnv} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option value="" disabled>-- Select Environment --</option>
{#each envs as env}
<option value={env.id}>{env.name}</option>
{/each}
</select>
</div>
<div>
<label for="mapper-ds-id" class="block text-sm font-medium text-gray-700">Dataset ID</label>
<input type="number" id="mapper-ds-id" bind:value={datasetId} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Mapping Source</label>
<div class="mt-2 flex space-x-4">
<label class="inline-flex items-center">
<input type="radio" bind:group={source} value="postgres" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300" />
<span class="ml-2 text-sm text-gray-700">PostgreSQL</span>
</label>
<label class="inline-flex items-center">
<input type="radio" bind:group={source} value="excel" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300" />
<span class="ml-2 text-sm text-gray-700">Excel</span>
</label>
</div>
</div>
{#if source === 'postgres'}
<div class="space-y-4 p-4 bg-gray-50 rounded-md border border-gray-100">
<div>
<label for="mapper-conn" class="block text-sm font-medium text-gray-700">Saved Connection</label>
<select id="mapper-conn" bind:value={selectedConnection} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option value="" disabled>-- Select Connection --</option>
{#each connections as conn}
<option value={conn.id}>{conn.name}</option>
{/each}
</select>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="mapper-table" class="block text-sm font-medium text-gray-700">Table Name</label>
<input type="text" id="mapper-table" bind:value={tableName} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
<div>
<label for="mapper-schema" class="block text-sm font-medium text-gray-700">Table Schema</label>
<input type="text" id="mapper-schema" bind:value={tableSchema} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
</div>
</div>
{:else}
<div class="p-4 bg-gray-50 rounded-md border border-gray-100">
<label for="mapper-excel" class="block text-sm font-medium text-gray-700">Excel File Path</label>
<input type="text" id="mapper-excel" bind:value={excelPath} placeholder="/path/to/mapping.xlsx" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
{/if}
<div class="flex justify-end">
<button
on:click={handleRunMapper}
disabled={isRunning}
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
{isRunning ? 'Starting...' : 'Run Mapper'}
</button>
</div>
</div>
</div>
<!-- [/SECTION] -->
<!-- [/DEF:MapperTool:Component] -->

View File

@@ -0,0 +1,177 @@
<!-- [DEF:SearchTool:Component] -->
<!--
@SEMANTICS: search, tool, dataset, regex
@PURPOSE: UI component for searching datasets using the SearchPlugin.
@LAYER: UI
@RELATION: USES -> frontend/src/services/toolsService.js
-->
<script>
// [SECTION: IMPORTS]
import { onMount } from 'svelte';
import { runTask, getTaskStatus } from '../../services/toolsService.js';
import { selectedTask } from '../../lib/stores.js';
import { addToast } from '../../lib/toasts.js';
// [/SECTION]
let envs = [];
let selectedEnv = '';
let searchQuery = '';
let isRunning = false;
let results = null;
let pollInterval;
// [DEF:fetchEnvironments:Function]
// @PURPOSE: Fetches the list of available environments.
async function fetchEnvironments() {
try {
const res = await fetch('/api/environments');
envs = await res.json();
} catch (e) {
addToast('Failed to fetch environments', 'error');
}
}
// [DEF:handleSearch:Function]
// @PURPOSE: Triggers the SearchPlugin task.
async function handleSearch() {
if (!selectedEnv || !searchQuery) {
addToast('Please select environment and enter query', 'warning');
return;
}
isRunning = true;
results = null;
try {
// Find the environment name from ID
const env = envs.find(e => e.id === selectedEnv);
const task = await runTask('search-datasets', {
env: env.name,
query: searchQuery
});
selectedTask.set(task);
startPolling(task.id);
} catch (e) {
isRunning = false;
addToast(e.message, 'error');
}
}
// [DEF:startPolling:Function]
// @PURPOSE: Polls for task completion and results.
function startPolling(taskId) {
if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(async () => {
try {
const task = await getTaskStatus(taskId);
selectedTask.set(task);
if (task.status === 'SUCCESS') {
clearInterval(pollInterval);
isRunning = false;
results = task.result;
addToast('Search completed', 'success');
} else if (task.status === 'FAILED') {
clearInterval(pollInterval);
isRunning = false;
addToast('Search failed', 'error');
}
} catch (e) {
clearInterval(pollInterval);
isRunning = false;
addToast('Error polling task status', 'error');
}
}, 2000);
}
onMount(fetchEnvironments);
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="space-y-6">
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h3 class="text-lg font-medium text-gray-900 mb-4">Search Dataset Metadata</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 items-end">
<div>
<label for="env-select" class="block text-sm font-medium text-gray-700">Environment</label>
<select
id="env-select"
bind:value={selectedEnv}
class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
>
<option value="" disabled>-- Select Environment --</option>
{#each envs as env}
<option value={env.id}>{env.name}</option>
{/each}
</select>
</div>
<div>
<label for="search-query" class="block text-sm font-medium text-gray-700">Regex Pattern</label>
<input
type="text"
id="search-query"
bind:value={searchQuery}
placeholder="e.g. from dm.*\.account"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</div>
</div>
<div class="mt-4 flex justify-end">
<button
on:click={handleSearch}
disabled={isRunning}
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
{#if isRunning}
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Searching...
{:else}
Search
{/if}
</button>
</div>
</div>
{#if results}
<div class="bg-white shadow overflow-hidden sm:rounded-md border border-gray-200">
<div class="px-4 py-5 sm:px-6 flex justify-between items-center bg-gray-50 border-b border-gray-200">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Search Results
</h3>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{results.count} matches
</span>
</div>
<ul class="divide-y divide-gray-200">
{#each results.results as item}
<li class="p-4 hover:bg-gray-50">
<div class="flex items-center justify-between">
<div class="text-sm font-medium text-indigo-600 truncate">
{item.dataset_name} (ID: {item.dataset_id})
</div>
<div class="ml-2 flex-shrink-0 flex">
<p class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
Field: {item.field}
</p>
</div>
</div>
<div class="mt-2">
<pre class="text-xs text-gray-500 bg-gray-50 p-2 rounded border border-gray-100 overflow-x-auto">{item.match_context}</pre>
</div>
</li>
{/each}
{#if results.count === 0}
<li class="p-8 text-center text-gray-500 italic">
No matches found for the given pattern.
</li>
{/if}
</ul>
</div>
{/if}
</div>
<!-- [/SECTION] -->
<!-- [/DEF:SearchTool:Component] -->

View File

@@ -0,0 +1,34 @@
<!-- [DEF:ConnectionsSettingsPage:Component] -->
<!--
@SEMANTICS: settings, connections, page
@PURPOSE: Page for managing database connection configurations.
@LAYER: UI
-->
<script>
import ConnectionForm from '../../../components/tools/ConnectionForm.svelte';
import ConnectionList from '../../../components/tools/ConnectionList.svelte';
let listComponent;
function handleSuccess() {
if (listComponent) {
listComponent.fetchConnections();
}
}
</script>
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Connection Management</h1>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div>
<ConnectionForm on:success={handleSuccess} />
</div>
<div>
<ConnectionList bind:this={listComponent} />
</div>
</div>
</div>
</div>
<!-- [/DEF:ConnectionsSettingsPage:Component] -->

View File

@@ -0,0 +1,26 @@
<!-- [DEF:DebugPage:Component] -->
<!--
@SEMANTICS: debug, page, tool
@PURPOSE: Page for system diagnostics and debugging.
@LAYER: UI
-->
<script>
import DebugTool from '../../../components/tools/DebugTool.svelte';
import TaskRunner from '../../../components/TaskRunner.svelte';
</script>
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<h1 class="text-2xl font-semibold text-gray-900 mb-6">System Diagnostics</h1>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-2">
<DebugTool />
</div>
<div class="lg:col-span-1">
<TaskRunner />
</div>
</div>
</div>
</div>
<!-- [/DEF:DebugPage:Component] -->

View File

@@ -0,0 +1,26 @@
<!-- [DEF:MapperPage:Component] -->
<!--
@SEMANTICS: mapper, page, tool
@PURPOSE: Page for the dataset column mapper tool.
@LAYER: UI
-->
<script>
import MapperTool from '../../../components/tools/MapperTool.svelte';
import TaskRunner from '../../../components/TaskRunner.svelte';
</script>
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Dataset Column Mapper</h1>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-2">
<MapperTool />
</div>
<div class="lg:col-span-1">
<TaskRunner />
</div>
</div>
</div>
</div>
<!-- [/DEF:MapperPage:Component] -->

View File

@@ -0,0 +1,26 @@
<!-- [DEF:SearchPage:Component] -->
<!--
@SEMANTICS: search, page, tool
@PURPOSE: Page for the dataset search tool.
@LAYER: UI
-->
<script>
import SearchTool from '../../../components/tools/SearchTool.svelte';
import TaskRunner from '../../../components/TaskRunner.svelte';
</script>
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Dataset Search</h1>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-2">
<SearchTool />
</div>
<div class="lg:col-span-1">
<TaskRunner />
</div>
</div>
</div>
</div>
<!-- [/DEF:SearchPage:Component] -->

View File

@@ -0,0 +1,52 @@
/**
* Service for interacting with the Connection Management API.
*/
const API_BASE = '/api/settings/connections';
/**
* Fetch a list of saved connections.
* @returns {Promise<Array>} List of connections.
*/
export async function getConnections() {
const response = await fetch(API_BASE);
if (!response.ok) {
throw new Error(`Failed to fetch connections: ${response.statusText}`);
}
return await response.json();
}
/**
* Create a new connection configuration.
* @param {Object} connectionData - The connection data.
* @returns {Promise<Object>} The created connection instance.
*/
export async function createConnection(connectionData) {
const response = await fetch(API_BASE, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(connectionData)
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `Failed to create connection: ${response.statusText}`);
}
return await response.json();
}
/**
* Delete a connection configuration.
* @param {string} connectionId - The ID of the connection to delete.
*/
export async function deleteConnection(connectionId) {
const response = await fetch(`${API_BASE}/${connectionId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(`Failed to delete connection: ${response.statusText}`);
}
}

View File

@@ -0,0 +1,40 @@
/**
* Service for generic Task API communication used by Tools.
*/
const API_BASE = '/api/tasks';
/**
* Start a new task for a given plugin.
* @param {string} pluginId - The ID of the plugin to run.
* @param {Object} params - Parameters for the plugin.
* @returns {Promise<Object>} The created task instance.
*/
export async function runTask(pluginId, params) {
const response = await fetch(API_BASE, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ plugin_id: pluginId, params })
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `Failed to start task: ${response.statusText}`);
}
return await response.json();
}
/**
* Fetch details for a specific task (to poll status or get result).
* @param {string} taskId - The ID of the task.
* @returns {Promise<Object>} Task details.
*/
export async function getTaskStatus(taskId) {
const response = await fetch(`${API_BASE}/${taskId}`);
if (!response.ok) {
throw new Error(`Failed to fetch task ${taskId}: ${response.statusText}`);
}
return await response.json();
}