Files
ss-tools/frontend/src/routes/settings/+page.svelte
2026-02-10 12:53:01 +03:00

271 lines
9.3 KiB
Svelte

<!-- [DEF:SettingsPage:Page] -->
<script>
/**
* @TIER: CRITICAL
* @PURPOSE: Consolidated Settings Page - All settings in one place with tabbed navigation
* @LAYER: UI
* @RELATION: BINDS_TO -> sidebarStore
* @INVARIANT: Always shows tabbed interface with all settings categories
*
* @UX_STATE: Loading -> Shows skeleton loader
* @UX_STATE: Loaded -> Shows tabbed settings interface
* @UX_STATE: Error -> Shows error banner with retry button
* @UX_FEEDBACK: Toast notifications on save success/failure
* @UX_RECOVERY: Refresh button reloads settings data
*/
import { onMount } from 'svelte';
import { t } from '$lib/i18n';
import { api } from '$lib/api.js';
import { addToast } from '$lib/toasts';
// State
let activeTab = 'environments';
let settings = null;
let isLoading = true;
let error = null;
// Load settings on mount
onMount(async () => {
await loadSettings();
});
// Load consolidated settings from API
async function loadSettings() {
isLoading = true;
error = null;
try {
const response = await api.getConsolidatedSettings();
settings = response;
} catch (err) {
error = err.message || 'Failed to load settings';
console.error('[SettingsPage][Coherence:Failed]', err);
} finally {
isLoading = false;
}
}
// Handle tab change
function handleTabChange(tab) {
activeTab = tab;
}
// Get tab class
function getTabClass(tab) {
return activeTab === tab
? 'text-blue-600 border-b-2 border-blue-600'
: 'text-gray-600 hover:text-gray-800 border-transparent hover:border-gray-300';
}
// Handle save
async function handleSave() {
console.log('[SettingsPage][Action] Saving settings');
try {
await api.updateConsolidatedSettings(settings);
addToast($t.settings?.save_success || 'Settings saved', 'success');
} catch (err) {
console.error('[SettingsPage][Coherence:Failed]', err);
addToast($t.settings?.save_failed || 'Failed to save settings', 'error');
}
}
</script>
<style>
.container {
@apply max-w-7xl mx-auto px-4 py-6;
}
.header {
@apply flex items-center justify-between mb-6;
}
.title {
@apply text-2xl font-bold text-gray-900;
}
.refresh-btn {
@apply px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors;
}
.error-banner {
@apply bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4 flex items-center justify-between;
}
.retry-btn {
@apply px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors;
}
.tabs {
@apply border-b border-gray-200 mb-6;
}
.tab-btn {
@apply px-4 py-2 text-sm font-medium transition-colors focus:outline-none;
}
.tab-content {
@apply bg-white rounded-lg p-6 border border-gray-200;
}
.skeleton {
@apply animate-pulse bg-gray-200 rounded;
}
</style>
<div class="container">
<!-- Header -->
<div class="header">
<h1 class="title">{$t.settings?.title || 'Settings'}</h1>
<button class="refresh-btn" on:click={loadSettings}>
{$t.common?.refresh || 'Refresh'}
</button>
</div>
<!-- Error Banner -->
{#if error}
<div class="error-banner">
<span>{error}</span>
<button class="retry-btn" on:click={loadSettings}>
{$t.common?.retry || 'Retry'}
</button>
</div>
{/if}
<!-- Loading State -->
{#if isLoading}
<div class="tab-content">
<div class="skeleton h-8"></div>
<div class="skeleton h-8"></div>
<div class="skeleton h-8"></div>
<div class="skeleton h-8"></div>
<div class="skeleton h-8"></div>
</div>
{:else if settings}
<!-- Tabs -->
<div class="tabs">
<button
class="tab-btn {getTabClass('environments')}"
on:click={() => handleTabChange('environments')}
>
{$t.settings?.environments || 'Environments'}
</button>
<button
class="tab-btn {getTabClass('connections')}"
on:click={() => handleTabChange('connections')}
>
{$t.settings?.connections || 'Connections'}
</button>
<button
class="tab-btn {getTabClass('llm')}"
on:click={() => handleTabChange('llm')}
>
{$t.settings?.llm || 'LLM'}
</button>
<button
class="tab-btn {getTabClass('logging')}"
on:click={() => handleTabChange('logging')}
>
{$t.settings?.logging || 'Logging'}
</button>
<button
class="tab-btn {getTabClass('storage')}"
on:click={() => handleTabChange('storage')}
>
{$t.settings?.storage || 'Storage'}
</button>
</div>
<!-- Tab Content -->
<div class="tab-content">
{#if activeTab === 'environments'}
<!-- Environments Tab -->
<div class="text-lg font-medium mb-4">
<h2 class="text-xl font-bold mb-4">{$t.settings?.environments || 'Superset Environments'}</h2>
<p class="text-gray-600 mb-6">
{$t.settings?.env_description || 'Configure Superset environments for dashboards and datasets.'}
</p>
<div class="flex justify-end mb-6">
<button class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700">
{$t.settings?.env_add || 'Add Environment'}
</button>
</div>
{#if settings.environments && settings.environments.length > 0}
<div class="mt-6">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.connections?.name || "Name"}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">URL</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.connections?.user || "Username"}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Default</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.settings?.env_actions || "Actions"}</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{#each settings.environments as env}
<tr>
<td class="px-6 py-4 whitespace-nowrap">{env.name}</td>
<td class="px-6 py-4 whitespace-nowrap">{env.url}</td>
<td class="px-6 py-4 whitespace-nowrap">{env.username}</td>
<td class="px-6 py-4 whitespace-nowrap">{env.is_default ? 'Yes' : 'No'}</td>
<td class="px-6 py-4 whitespace-nowrap">
<button class="text-green-600 hover:text-green-900 mr-4" on:click={() => handleTestEnv(env.id)}>
{$t.settings?.env_test || "Test"}
</button>
<button class="text-indigo-600 hover:text-indigo-900 mr-4" on:click={() => editEnv(env)}>
{$t.common.edit}
</button>
<button class="text-red-600 hover:text-red-900" on:click={() => handleDeleteEnv(env.id)}>
{$t.settings?.env_delete || "Delete"}
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
{:else if activeTab === 'connections'}
<!-- Connections Tab -->
<div class="text-lg font-medium mb-4">
<h2 class="text-xl font-bold mb-4">{$t.settings?.connections || 'Database Connections'}</h2>
<p class="text-gray-600 mb-6">
{$t.settings?.connections_description || 'Configure database connections for data mapping.'}
</p>
</div>
{:else if activeTab === 'llm'}
<!-- LLM Tab -->
<div class="text-lg font-medium mb-4">
<h2 class="text-xl font-bold mb-4">{$t.settings?.llm || 'LLM Providers'}</h2>
<p class="text-gray-600 mb-6">
{$t.settings?.llm_description || 'Configure LLM providers for dataset documentation.'}
</p>
<div class="flex justify-end mb-6">
<button class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700">
{$t.llm?.add_provider || 'Add Provider'}
</button>
</div>
</div>
{:else if activeTab === 'logging'}
<!-- Logging Tab -->
<div class="text-lg font-medium mb-4">
<h2 class="text-xl font-bold mb-4">{$t.settings?.logging || 'Logging Configuration'}</h2>
<p class="text-gray-600 mb-6">
{$t.settings?.logging_description || 'Configure logging and task log levels.'}
</p>
</div>
{:else if activeTab === 'storage'}
<!-- Storage Tab -->
<div class="text-lg font-medium mb-4">
<h2 class="text-xl font-bold mb-4">{$t.settings?.storage || 'File Storage Configuration'}</h2>
<p class="text-gray-600 mb-6">
{$t.settings?.storage_description || 'Configure file storage paths and patterns.'}
</p>
</div>
{/if}
</div>
</div>
<!-- [/DEF:SettingsPage:Page] -->