- ConnectionsTab.svelte: Settings tab for DB connection CRUD with list, add/edit form, test button, delete with dependency check - InsertMethodSelector.svelte: radio group for insert method choice (SQL Lab / Direct DB) with filtered connection dropdown - InsertMethodSelector.test.ts: component tests for all UX states - settings/+page.svelte, settings-utils.ts: register Connections tab
324 lines
14 KiB
Svelte
324 lines
14 KiB
Svelte
<!-- #region SettingsPage [C:4] [TYPE Page] [SEMANTICS sveltekit, settings, tabbed, environment, config] -->
|
|
<!-- @ingroup Routes -->
|
|
<!-- @BRIEF Consolidated Settings Page shell — thin layout that delegates each tab to its own component. -->
|
|
<!-- @LAYER Page -->
|
|
<!-- @RATIONALE Decomposed from 1451→287 lines by extracting 6 tab components and centralizing state in the page shell. The C4 rating reflects the page's role as tab orchestrator with settings load/save lifecycle. -->
|
|
<!-- @REJECTED Keeping all tab content inline was rejected due to INV_7 violations (single contract exceeded 150 lines, cyclomatic complexity > 10). -->
|
|
<!-- @DEPRECATED N/A -->
|
|
<!-- @REPLACED_BY N/A -->
|
|
<!-- @RELATION DEPENDS_ON -> [EXT:frontend:EnvironmentsTab] -->
|
|
<!-- @RELATION DEPENDS_ON -> [LoggingSettings] -->
|
|
<!-- @RELATION DEPENDS_ON -> [LlmSettings] -->
|
|
<!-- @RELATION DEPENDS_ON -> [MigrationSettings] -->
|
|
<!-- @RELATION DEPENDS_ON -> [StorageSettings] -->
|
|
<!-- @RELATION DEPENDS_ON -> [FeaturesSettings] -->
|
|
<!-- @RELATION DEPENDS_ON -> [EXT:frontend:SettingsUtils] -->
|
|
<!-- @PRE Route is loaded in the authenticated UI shell. -->
|
|
<!-- @POST Page exposes consolidated settings tabs; each tab manages its own state. -->
|
|
<!-- @SIDE_EFFECT Loads settings data on mount. -->
|
|
<!-- @UX_STATE Loading -> Skeleton loader. -->
|
|
<!-- @UX_STATE Loaded -> Tabbed settings interface. -->
|
|
<!-- @UX_STATE Error -> Error banner with retry. -->
|
|
<!-- @UX_FEEDBACK Toast on save. -->
|
|
<!-- @UX_RECOVERY Refresh button reloads settings. -->
|
|
<script lang="ts">
|
|
import { onMount } from "svelte";
|
|
import { t } from "$lib/i18n/index.svelte.js";
|
|
import { api } from "$lib/api.js";
|
|
import { addToast } from "$lib/toasts.svelte.js";
|
|
|
|
import {
|
|
normalizeTab,
|
|
readTabFromUrl,
|
|
writeTabToUrl,
|
|
normalizeLlmSettings,
|
|
} from "./settings-utils.js";
|
|
|
|
import EnvironmentsTab from "./EnvironmentsTab.svelte";
|
|
import LoggingSettings from "./LoggingSettings.svelte";
|
|
import LlmSettings from "./LlmSettings.svelte";
|
|
import MigrationSettings from "./MigrationSettings.svelte";
|
|
import StorageSettings from "./StorageSettings.svelte";
|
|
import FeaturesSettings from "./FeaturesSettings.svelte";
|
|
import SystemSettings from "./SystemSettings.svelte";
|
|
import GitSettingsPage from "./git/+page.svelte";
|
|
import AutomationSettingsPage from "./automation/+page.svelte";
|
|
import LanguagesSettings from "./LanguagesSettings.svelte";
|
|
import ConnectionsTab from "./ConnectionsTab.svelte";
|
|
|
|
let activeTab = $state("environments");
|
|
let settings = $state(null);
|
|
let isLoading = $state(true);
|
|
let error = $state(null);
|
|
|
|
onMount(() => {
|
|
activeTab = readTabFromUrl();
|
|
const syncTabFromUrl = () => {
|
|
activeTab = readTabFromUrl();
|
|
};
|
|
window.addEventListener("popstate", syncTabFromUrl);
|
|
window.addEventListener("hashchange", syncTabFromUrl);
|
|
void loadSettings();
|
|
return () => {
|
|
window.removeEventListener("popstate", syncTabFromUrl);
|
|
window.removeEventListener("hashchange", syncTabFromUrl);
|
|
};
|
|
});
|
|
|
|
async function loadSettings() {
|
|
isLoading = true;
|
|
error = null;
|
|
try {
|
|
const response = await api.getConsolidatedSettings();
|
|
response.llm = normalizeLlmSettings(response.llm);
|
|
settings = response;
|
|
} catch (err) {
|
|
error = err.message || $t.settings?.load_failed;
|
|
console.error("[SettingsPage][Coherence:Failed]", err);
|
|
} finally {
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
function handleTabChange(tab) {
|
|
const normalizedTab = normalizeTab(tab);
|
|
if (!normalizedTab) return;
|
|
activeTab = normalizedTab;
|
|
writeTabToUrl(activeTab);
|
|
}
|
|
|
|
async function handleSave() {
|
|
console.log("[SettingsPage][Action] Saving settings");
|
|
try {
|
|
settings.llm = normalizeLlmSettings(settings.llm);
|
|
await api.updateConsolidatedSettings(settings);
|
|
addToast($t.settings?.save_success, "success");
|
|
} catch (err) {
|
|
console.error("[SettingsPage][Coherence:Failed]", err);
|
|
addToast($t.settings?.save_failed, "error");
|
|
}
|
|
}
|
|
|
|
async function handleRefresh() {
|
|
await loadSettings();
|
|
}
|
|
</script>
|
|
|
|
<div class="mx-auto w-full max-w-7xl space-y-6">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h1 class="text-2xl font-bold text-text">
|
|
{$t.settings?.title}
|
|
</h1>
|
|
<button
|
|
class="inline-flex items-center justify-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-hover"
|
|
onclick={handleRefresh}
|
|
>
|
|
{$t.common?.refresh}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Error Banner -->
|
|
{#if error}
|
|
<div
|
|
class="bg-destructive-light border border-destructive-ring text-destructive px-4 py-3 rounded mb-4 flex items-center justify-between"
|
|
>
|
|
<span>{error}</span>
|
|
<button
|
|
class="px-4 py-2 bg-destructive text-white rounded hover:bg-destructive-hover transition-colors"
|
|
onclick={loadSettings}
|
|
>
|
|
{$t.common?.retry}
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Loading State -->
|
|
{#if isLoading}
|
|
<div class="rounded-xl border border-border bg-surface-card p-6 shadow-sm">
|
|
<div class="space-y-3">
|
|
<div class="h-8 animate-pulse rounded bg-surface-muted"></div>
|
|
<div class="h-8 animate-pulse rounded bg-surface-muted"></div>
|
|
<div class="h-8 animate-pulse rounded bg-surface-muted"></div>
|
|
<div class="h-8 animate-pulse rounded bg-surface-muted"></div>
|
|
<div class="h-8 animate-pulse rounded bg-surface-muted"></div>
|
|
</div>
|
|
</div>
|
|
{:else if settings}
|
|
<!-- Tabs -->
|
|
<div class="border-b border-border mb-6">
|
|
<button
|
|
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none border-b-2"
|
|
class:text-primary={activeTab === "environments"}
|
|
class:border-primary={activeTab === "environments"}
|
|
class:text-text-muted={activeTab !== "environments"}
|
|
class:border-transparent={activeTab !== "environments"}
|
|
class:hover:text-text={activeTab !== "environments"}
|
|
class:hover:border-border-strong={activeTab !== "environments"}
|
|
aria-current={activeTab === "environments" ? "page" : undefined}
|
|
onclick={() => handleTabChange("environments")}
|
|
>
|
|
{$t.settings?.environments}
|
|
</button>
|
|
<button
|
|
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none border-b-2"
|
|
class:text-primary={activeTab === "connections"}
|
|
class:border-primary={activeTab === "connections"}
|
|
class:text-text-muted={activeTab !== "connections"}
|
|
class:border-transparent={activeTab !== "connections"}
|
|
class:hover:text-text={activeTab !== "connections"}
|
|
class:hover:border-border-strong={activeTab !== "connections"}
|
|
aria-current={activeTab === "connections" ? "page" : undefined}
|
|
onclick={() => handleTabChange("connections")}
|
|
>
|
|
Connections
|
|
</button>
|
|
<button
|
|
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none border-b-2"
|
|
class:text-primary={activeTab === "logging"}
|
|
class:border-primary={activeTab === "logging"}
|
|
class:text-text-muted={activeTab !== "logging"}
|
|
class:border-transparent={activeTab !== "logging"}
|
|
class:hover:text-text={activeTab !== "logging"}
|
|
class:hover:border-border-strong={activeTab !== "logging"}
|
|
aria-current={activeTab === "logging" ? "page" : undefined}
|
|
onclick={() => handleTabChange("logging")}
|
|
>
|
|
{$t.settings?.logging}
|
|
</button>
|
|
<button
|
|
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none border-b-2"
|
|
class:text-primary={activeTab === "git"}
|
|
class:border-primary={activeTab === "git"}
|
|
class:text-text-muted={activeTab !== "git"}
|
|
class:border-transparent={activeTab !== "git"}
|
|
class:hover:text-text={activeTab !== "git"}
|
|
class:hover:border-border-strong={activeTab !== "git"}
|
|
aria-current={activeTab === "git" ? "page" : undefined}
|
|
onclick={() => handleTabChange("git")}
|
|
>
|
|
{$t.nav?.settings_git || $t.nav?.git || "Git"}
|
|
</button>
|
|
<button
|
|
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none border-b-2"
|
|
class:text-primary={activeTab === "llm"}
|
|
class:border-primary={activeTab === "llm"}
|
|
class:text-text-muted={activeTab !== "llm"}
|
|
class:border-transparent={activeTab !== "llm"}
|
|
class:hover:text-text={activeTab !== "llm"}
|
|
class:hover:border-border-strong={activeTab !== "llm"}
|
|
aria-current={activeTab === "llm" ? "page" : undefined}
|
|
onclick={() => handleTabChange("llm")}
|
|
>
|
|
{$t.settings?.llm}
|
|
</button>
|
|
<button
|
|
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none border-b-2"
|
|
class:text-primary={activeTab === "migration"}
|
|
class:border-primary={activeTab === "migration"}
|
|
class:text-text-muted={activeTab !== "migration"}
|
|
class:border-transparent={activeTab !== "migration"}
|
|
class:hover:text-text={activeTab !== "migration"}
|
|
class:hover:border-border-strong={activeTab !== "migration"}
|
|
aria-current={activeTab === "migration" ? "page" : undefined}
|
|
onclick={() => handleTabChange("migration")}
|
|
>
|
|
{$t.settings?.migration_sync}
|
|
</button>
|
|
<button
|
|
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none border-b-2"
|
|
class:text-primary={activeTab === "storage"}
|
|
class:border-primary={activeTab === "storage"}
|
|
class:text-text-muted={activeTab !== "storage"}
|
|
class:border-transparent={activeTab !== "storage"}
|
|
class:hover:text-text={activeTab !== "storage"}
|
|
class:hover:border-border-strong={activeTab !== "storage"}
|
|
aria-current={activeTab === "storage" ? "page" : undefined}
|
|
onclick={() => handleTabChange("storage")}
|
|
>
|
|
{$t.settings?.storage}
|
|
</button>
|
|
<button
|
|
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none border-b-2"
|
|
class:text-primary={activeTab === "features"}
|
|
class:border-primary={activeTab === "features"}
|
|
class:text-text-muted={activeTab !== "features"}
|
|
class:border-transparent={activeTab !== "features"}
|
|
class:hover:text-text={activeTab !== "features"}
|
|
class:hover:border-border-strong={activeTab !== "features"}
|
|
aria-current={activeTab === "features" ? "page" : undefined}
|
|
onclick={() => handleTabChange("features")}
|
|
>
|
|
{$t.settings?.features || "Features"}
|
|
</button>
|
|
<button
|
|
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none border-b-2"
|
|
class:text-primary={activeTab === "automation"}
|
|
class:border-primary={activeTab === "automation"}
|
|
class:text-text-muted={activeTab !== "automation"}
|
|
class:border-transparent={activeTab !== "automation"}
|
|
class:hover:text-text={activeTab !== "automation"}
|
|
class:hover:border-border-strong={activeTab !== "automation"}
|
|
aria-current={activeTab === "automation" ? "page" : undefined}
|
|
onclick={() => handleTabChange("automation")}
|
|
>
|
|
{$t.settings?.automation || "Automation"}
|
|
</button>
|
|
<button
|
|
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none border-b-2"
|
|
class:text-primary={activeTab === "languages"}
|
|
class:border-primary={activeTab === "languages"}
|
|
class:text-text-muted={activeTab !== "languages"}
|
|
class:border-transparent={activeTab !== "languages"}
|
|
class:hover:text-text={activeTab !== "languages"}
|
|
class:hover:border-border-strong={activeTab !== "languages"}
|
|
aria-current={activeTab === "languages" ? "page" : undefined}
|
|
onclick={() => handleTabChange("languages")}
|
|
>
|
|
{$t.settings?.languages || "Languages"}
|
|
</button>
|
|
<button
|
|
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none border-b-2"
|
|
class:text-primary={activeTab === "system"}
|
|
class:border-primary={activeTab === "system"}
|
|
class:text-text-muted={activeTab !== "system"}
|
|
class:border-transparent={activeTab !== "system"}
|
|
class:hover:text-text={activeTab !== "system"}
|
|
class:hover:border-border-strong={activeTab !== "system"}
|
|
aria-current={activeTab === "system" ? "page" : undefined}
|
|
onclick={() => handleTabChange("system")}
|
|
>
|
|
{$t.settings?.system || "System"}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tab Content -->
|
|
<div class="rounded-xl border border-border bg-surface-card p-6 shadow-sm">
|
|
{#if activeTab === "environments"}
|
|
<EnvironmentsTab bind:settings onSave={loadSettings} />
|
|
{:else if activeTab === "connections"}
|
|
<ConnectionsTab bind:settings onSave={handleSave} />
|
|
{:else if activeTab === "logging"}
|
|
<LoggingSettings bind:settings onSave={handleSave} />
|
|
{:else if activeTab === "git"}
|
|
<GitSettingsPage />
|
|
{:else if activeTab === "llm"}
|
|
<LlmSettings bind:settings onSave={handleSave} />
|
|
{:else if activeTab === "migration"}
|
|
<MigrationSettings />
|
|
{:else if activeTab === "storage"}
|
|
<StorageSettings bind:settings onSave={handleSave} />
|
|
{:else if activeTab === "features"}
|
|
<FeaturesSettings bind:settings onSave={handleSave} />
|
|
{:else if activeTab === "automation"}
|
|
<AutomationSettingsPage />
|
|
{:else if activeTab === "languages"}
|
|
<LanguagesSettings bind:settings onSave={handleSave} />
|
|
{:else if activeTab === "system"}
|
|
<SystemSettings bind:settings onSave={handleSave} />
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
<!-- #endregion SettingsPage -->
|