feat: enhance settings page layout and section management
CI / changes (push) Successful in 7s
CI / openapi (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m5s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m7s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped

Redesigned the settings page to improve user experience by introducing a section-based layout for managing API keys and BIRD parameters. Added a new type for settings sections and implemented dynamic content rendering based on the selected section. Updated descriptions for clarity and improved the overall structure of the settings interface.
This commit is contained in:
Denozordec
2026-04-08 14:46:56 +07:00
parent 1386b63db2
commit d91667f698
+76 -34
View File
@@ -5,7 +5,7 @@
import { apiJSON, apiMutate } from '$lib/api/client.js'; import { apiJSON, apiMutate } from '$lib/api/client.js';
import type { AppSettings } from '$lib/api/types.js'; import type { AppSettings } from '$lib/api/types.js';
import { Button } from '$lib/components/ui/button/index.js'; import { Button } from '$lib/components/ui/button/index.js';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '$lib/components/ui/card/index.js'; import { Card, CardContent, CardHeader, CardDescription } from '$lib/components/ui/card/index.js';
import { Input } from '$lib/components/ui/input/index.js'; import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js'; import { Label } from '$lib/components/ui/label/index.js';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
@@ -39,6 +39,8 @@
| 'bird_bgp_source_ipv6' | 'bird_bgp_source_ipv6'
| 'revision_retention_minutes'; | 'revision_retention_minutes';
type SettingsSection = 'token' | 'bird' | 'revisions' | 'additional';
const knownFieldKeys: KnownFieldKey[] = [ const knownFieldKeys: KnownFieldKey[] = [
'bird_router_id', 'bird_router_id',
'bird_local_ipv4', 'bird_local_ipv4',
@@ -49,6 +51,15 @@
'revision_retention_minutes' 'revision_retention_minutes'
]; ];
let activeSection = $state<SettingsSection>('token');
const sectionItems: Array<{ id: SettingsSection; label: string; description: string }> = [
{ id: 'token', label: 'API-ключ', description: 'Авторизация в UI' },
{ id: 'bird', label: 'BIRD', description: 'Сетевые параметры' },
{ id: 'revisions', label: 'Ревизии', description: 'Хранение истории' },
{ id: 'additional', label: 'Дополнительно', description: 'Ключ-значение' }
];
function isValidIPv4(value: string): boolean { function isValidIPv4(value: string): boolean {
const parts = value.split('.'); const parts = value.split('.');
if (parts.length !== 4) return false; if (parts.length !== 4) return false;
@@ -257,7 +268,7 @@
}); });
</script> </script>
<div class="mx-auto max-w-2xl space-y-6"> <div class="mx-auto max-w-5xl space-y-6">
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<div <div
class="bg-muted text-muted-foreground flex size-11 shrink-0 items-center justify-center rounded-xl" class="bg-muted text-muted-foreground flex size-11 shrink-0 items-center justify-center rounded-xl"
@@ -267,48 +278,64 @@
</div> </div>
<div class="min-w-0"> <div class="min-w-0">
<h1 class="text-2xl font-semibold tracking-tight">Настройки</h1> <h1 class="text-2xl font-semibold tracking-tight">Настройки</h1>
<p class="text-muted-foreground mt-1 text-sm">Токен доступа и параметры API.</p> <p class="text-muted-foreground mt-1 text-sm">
Управление токеном доступа и глобальными параметрами control plane.
</p>
</div> </div>
</div> </div>
<!-- Token -->
<Card> <Card>
<CardHeader> <CardContent class="p-4 md:p-6">
<CardTitle class="text-base">API-ключ</CardTitle> <div class="grid gap-6 md:grid-cols-[220px_1fr]">
<CardDescription> <div class="space-y-1">
{#each sectionItems as section (section.id)}
<button
type="button"
class={[
'w-full rounded-lg border px-3 py-2 text-left transition-colors',
activeSection === section.id
? 'border-primary bg-muted text-foreground'
: 'border-transparent text-muted-foreground hover:border-border hover:bg-muted/70 hover:text-foreground'
]}
onclick={() => (activeSection = section.id)}
>
<div class="text-sm font-medium">{section.label}</div>
<div class="text-xs opacity-80">{section.description}</div>
</button>
{/each}
</div>
<div class="border-border rounded-xl border p-4 md:p-5">
{#if activeSection === 'token'}
<div class="space-y-4">
<div class="space-y-1">
<h2 class="text-base font-semibold">API-ключ</h2>
<p class="text-muted-foreground text-sm">
Bearer-токен хранится только в localStorage браузера. Для локального демо с Bearer-токен хранится только в localStorage браузера. Для локального демо с
<code class="bg-muted rounded px-1 py-0.5 text-xs">EVOBGP_DEV_INSECURE=1</code> <code class="bg-muted rounded px-1 py-0.5 text-xs">EVOBGP_DEV_INSECURE=1</code>
используйте токен <code class="bg-muted rounded px-1 py-0.5 text-xs">dev</code>. используйте токен <code class="bg-muted rounded px-1 py-0.5 text-xs">dev</code>.
</CardDescription> </p>
</CardHeader> </div>
<CardContent class="space-y-4">
<div class="space-y-2"> <div class="space-y-2">
<Label for="token">Токен</Label> <Label for="token">Токен</Label>
<Input id="token" type="password" autocomplete="off" bind:value={token} placeholder="Bearer …" /> <Input id="token" type="password" autocomplete="off" bind:value={token} placeholder="Bearer …" />
</div> </div>
<Button onclick={saveToken}> <Button onclick={saveToken}>
<Save /> <Save />
Сохранить токен Сохранить токен
</Button> </Button>
</CardContent> </div>
</Card> {:else if loadingSettings}
<!-- API Settings -->
<Card>
<CardHeader>
<CardTitle class="text-base">Настройки системы (API)</CardTitle>
<CardDescription>
<code class="text-xs">GET/PATCH /v1/settings</code> — глобальные параметры control plane (хранятся в БД).
Требуется роль operator.
</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
{#if loadingSettings}
<p class="text-muted-foreground text-sm">Загрузка…</p> <p class="text-muted-foreground text-sm">Загрузка…</p>
{:else if apiSettings !== null} {:else if apiSettings === null}
<Button variant="outline" onclick={loadApiSettings}>Загрузить настройки</Button>
{:else}
<div class="space-y-5"> <div class="space-y-5">
{#if activeSection === 'bird'}
<div class="space-y-3"> <div class="space-y-3">
<h3 class="text-sm font-medium">Параметры BIRD</h3> <h2 class="text-base font-semibold">Параметры BIRD</h2>
<div class="space-y-2"> <div class="space-y-2">
<Label for="bird-router-id">Router ID (bird_router_id)</Label> <Label for="bird-router-id">Router ID (bird_router_id)</Label>
@@ -336,7 +363,13 @@
<div class="space-y-2"> <div class="space-y-2">
<Label for="bird-local-asn">Локальный ASN (bird_local_asn)</Label> <Label for="bird-local-asn">Локальный ASN (bird_local_asn)</Label>
<Input id="bird-local-asn" type="number" min="1" bind:value={knownFields.bird_local_asn} placeholder="65001" /> <Input
id="bird-local-asn"
type="number"
min="1"
bind:value={knownFields.bird_local_asn}
placeholder="65001"
/>
{#if knownFieldErrors.bird_local_asn} {#if knownFieldErrors.bird_local_asn}
<p class="text-sm text-red-600">{knownFieldErrors.bird_local_asn}</p> <p class="text-sm text-red-600">{knownFieldErrors.bird_local_asn}</p>
{/if} {/if}
@@ -366,9 +399,9 @@
{/if} {/if}
</div> </div>
</div> </div>
{:else if activeSection === 'revisions'}
<div class="space-y-3"> <div class="space-y-3">
<h3 class="text-sm font-medium">Управление ревизиями</h3> <h2 class="text-base font-semibold">Управление ревизиями</h2>
<div class="space-y-2"> <div class="space-y-2">
<Label for="revision-retention-minutes"> <Label for="revision-retention-minutes">
@@ -390,10 +423,10 @@
</p> </p>
</div> </div>
</div> </div>
{:else if activeSection === 'additional'}
<div class="space-y-3"> <div class="space-y-3">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h3 class="text-sm font-medium">Дополнительные настройки (KV)</h3> <h2 class="text-base font-semibold">Дополнительные настройки (KV)</h2>
<Button variant="outline" size="sm" onclick={addAdditionalSetting}> <Button variant="outline" size="sm" onclick={addAdditionalSetting}>
<Plus class="size-4" /> <Plus class="size-4" />
Добавить строку Добавить строку
@@ -421,7 +454,7 @@
{/each} {/each}
</div> </div>
</div> </div>
</div> {/if}
{#if hasValidationErrors} {#if hasValidationErrors}
<p class="text-sm text-red-600"> <p class="text-sm text-red-600">
@@ -429,13 +462,22 @@
</p> </p>
{/if} {/if}
<div class="pt-2">
<Button onclick={saveApiSettings} disabled={!canSaveSettings}> <Button onclick={saveApiSettings} disabled={!canSaveSettings}>
<Save /> <Save />
{savingSettings ? 'Сохранение…' : 'Применить настройки'} {savingSettings ? 'Сохранение…' : 'Применить настройки'}
</Button> </Button>
{:else} </div>
<Button variant="outline" onclick={loadApiSettings}>Загрузить настройки</Button> </div>
{/if} {/if}
</div>
</div>
</CardContent> </CardContent>
<CardHeader class="pt-0">
<CardDescription>
<code class="text-xs">GET/PATCH /v1/settings</code> — глобальные параметры control plane (хранятся в БД).
Требуется роль operator.
</CardDescription>
</CardHeader>
</Card> </Card>
</div> </div>