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
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:
@@ -5,7 +5,7 @@
|
||||
import { apiJSON, apiMutate } from '$lib/api/client.js';
|
||||
import type { AppSettings } from '$lib/api/types.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 { Label } from '$lib/components/ui/label/index.js';
|
||||
import { toast } from 'svelte-sonner';
|
||||
@@ -39,6 +39,8 @@
|
||||
| 'bird_bgp_source_ipv6'
|
||||
| 'revision_retention_minutes';
|
||||
|
||||
type SettingsSection = 'token' | 'bird' | 'revisions' | 'additional';
|
||||
|
||||
const knownFieldKeys: KnownFieldKey[] = [
|
||||
'bird_router_id',
|
||||
'bird_local_ipv4',
|
||||
@@ -49,6 +51,15 @@
|
||||
'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 {
|
||||
const parts = value.split('.');
|
||||
if (parts.length !== 4) return false;
|
||||
@@ -257,7 +268,7 @@
|
||||
});
|
||||
</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="bg-muted text-muted-foreground flex size-11 shrink-0 items-center justify-center rounded-xl"
|
||||
@@ -267,175 +278,206 @@
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<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>
|
||||
|
||||
<!-- Token -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">API-ключ</CardTitle>
|
||||
<CardDescription>
|
||||
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">dev</code>.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="token">Токен</Label>
|
||||
<Input id="token" type="password" autocomplete="off" bind:value={token} placeholder="Bearer …" />
|
||||
</div>
|
||||
<Button onclick={saveToken}>
|
||||
<Save />
|
||||
Сохранить токен
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<CardContent class="p-4 md:p-6">
|
||||
<div class="grid gap-6 md:grid-cols-[220px_1fr]">
|
||||
<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>
|
||||
|
||||
<!-- API Settings -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">Настройки системы (API)</CardTitle>
|
||||
<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 браузера. Для локального демо с
|
||||
<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>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="token">Токен</Label>
|
||||
<Input id="token" type="password" autocomplete="off" bind:value={token} placeholder="Bearer …" />
|
||||
</div>
|
||||
|
||||
<Button onclick={saveToken}>
|
||||
<Save />
|
||||
Сохранить токен
|
||||
</Button>
|
||||
</div>
|
||||
{:else if loadingSettings}
|
||||
<p class="text-muted-foreground text-sm">Загрузка…</p>
|
||||
{:else if apiSettings === null}
|
||||
<Button variant="outline" onclick={loadApiSettings}>Загрузить настройки</Button>
|
||||
{:else}
|
||||
<div class="space-y-5">
|
||||
{#if activeSection === 'bird'}
|
||||
<div class="space-y-3">
|
||||
<h2 class="text-base font-semibold">Параметры BIRD</h2>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="bird-router-id">Router ID (bird_router_id)</Label>
|
||||
<Input id="bird-router-id" bind:value={knownFields.bird_router_id} placeholder="203.0.113.1" />
|
||||
{#if knownFieldErrors.bird_router_id}
|
||||
<p class="text-sm text-red-600">{knownFieldErrors.bird_router_id}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="bird-local-ipv4">Локальный IPv4 (bird_local_ipv4)</Label>
|
||||
<Input id="bird-local-ipv4" bind:value={knownFields.bird_local_ipv4} placeholder="198.51.100.10" />
|
||||
{#if knownFieldErrors.bird_local_ipv4}
|
||||
<p class="text-sm text-red-600">{knownFieldErrors.bird_local_ipv4}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="bird-local-ipv6">Локальный IPv6 (bird_local_ipv6)</Label>
|
||||
<Input id="bird-local-ipv6" bind:value={knownFields.bird_local_ipv6} placeholder="2001:db8::10" />
|
||||
{#if knownFieldErrors.bird_local_ipv6}
|
||||
<p class="text-sm text-red-600">{knownFieldErrors.bird_local_ipv6}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<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"
|
||||
/>
|
||||
{#if knownFieldErrors.bird_local_asn}
|
||||
<p class="text-sm text-red-600">{knownFieldErrors.bird_local_asn}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="bird-bgp-source-ipv4">BGP source IPv4 (bird_bgp_source_ipv4)</Label>
|
||||
<Input
|
||||
id="bird-bgp-source-ipv4"
|
||||
bind:value={knownFields.bird_bgp_source_ipv4}
|
||||
placeholder="198.51.100.11"
|
||||
/>
|
||||
{#if knownFieldErrors.bird_bgp_source_ipv4}
|
||||
<p class="text-sm text-red-600">{knownFieldErrors.bird_bgp_source_ipv4}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="bird-bgp-source-ipv6">BGP source IPv6 (bird_bgp_source_ipv6)</Label>
|
||||
<Input
|
||||
id="bird-bgp-source-ipv6"
|
||||
bind:value={knownFields.bird_bgp_source_ipv6}
|
||||
placeholder="2001:db8::11"
|
||||
/>
|
||||
{#if knownFieldErrors.bird_bgp_source_ipv6}
|
||||
<p class="text-sm text-red-600">{knownFieldErrors.bird_bgp_source_ipv6}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if activeSection === 'revisions'}
|
||||
<div class="space-y-3">
|
||||
<h2 class="text-base font-semibold">Управление ревизиями</h2>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="revision-retention-minutes">
|
||||
Время жизни ревизий, мин (revision_retention_minutes)
|
||||
</Label>
|
||||
<Input
|
||||
id="revision-retention-minutes"
|
||||
type="number"
|
||||
min="15"
|
||||
max="43200"
|
||||
bind:value={knownFields.revision_retention_minutes}
|
||||
placeholder="43200"
|
||||
/>
|
||||
{#if knownFieldErrors.revision_retention_minutes}
|
||||
<p class="text-sm text-red-600">{knownFieldErrors.revision_retention_minutes}</p>
|
||||
{/if}
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Старые ревизии удаляются автоматически. Последняя раскатанная ревизия не удаляется.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if activeSection === 'additional'}
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-base font-semibold">Дополнительные настройки (KV)</h2>
|
||||
<Button variant="outline" size="sm" onclick={addAdditionalSetting}>
|
||||
<Plus class="size-4" />
|
||||
Добавить строку
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if additionalSettings.length === 0}
|
||||
<p class="text-muted-foreground text-sm">Нет дополнительных параметров.</p>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-2">
|
||||
{#each additionalSettings as entry (entry.id)}
|
||||
<div class="grid grid-cols-1 gap-2 md:grid-cols-[1fr_1fr_auto]">
|
||||
<Input bind:value={entry.key} placeholder="Ключ (например, bird_log_level)" />
|
||||
<Input bind:value={entry.value} placeholder="Значение (строка)" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="Удалить строку"
|
||||
onclick={() => removeAdditionalSetting(entry.id)}
|
||||
>
|
||||
<Trash2 class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if hasValidationErrors}
|
||||
<p class="text-sm text-red-600">
|
||||
Есть ошибки в полях. Исправьте их, чтобы сохранить изменения.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="pt-2">
|
||||
<Button onclick={saveApiSettings} disabled={!canSaveSettings}>
|
||||
<Save />
|
||||
{savingSettings ? 'Сохранение…' : 'Применить настройки'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardHeader class="pt-0">
|
||||
<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>
|
||||
{:else if apiSettings !== null}
|
||||
<div class="space-y-5">
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-sm font-medium">Параметры BIRD</h3>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="bird-router-id">Router ID (bird_router_id)</Label>
|
||||
<Input id="bird-router-id" bind:value={knownFields.bird_router_id} placeholder="203.0.113.1" />
|
||||
{#if knownFieldErrors.bird_router_id}
|
||||
<p class="text-sm text-red-600">{knownFieldErrors.bird_router_id}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="bird-local-ipv4">Локальный IPv4 (bird_local_ipv4)</Label>
|
||||
<Input id="bird-local-ipv4" bind:value={knownFields.bird_local_ipv4} placeholder="198.51.100.10" />
|
||||
{#if knownFieldErrors.bird_local_ipv4}
|
||||
<p class="text-sm text-red-600">{knownFieldErrors.bird_local_ipv4}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="bird-local-ipv6">Локальный IPv6 (bird_local_ipv6)</Label>
|
||||
<Input id="bird-local-ipv6" bind:value={knownFields.bird_local_ipv6} placeholder="2001:db8::10" />
|
||||
{#if knownFieldErrors.bird_local_ipv6}
|
||||
<p class="text-sm text-red-600">{knownFieldErrors.bird_local_ipv6}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<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" />
|
||||
{#if knownFieldErrors.bird_local_asn}
|
||||
<p class="text-sm text-red-600">{knownFieldErrors.bird_local_asn}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="bird-bgp-source-ipv4">BGP source IPv4 (bird_bgp_source_ipv4)</Label>
|
||||
<Input
|
||||
id="bird-bgp-source-ipv4"
|
||||
bind:value={knownFields.bird_bgp_source_ipv4}
|
||||
placeholder="198.51.100.11"
|
||||
/>
|
||||
{#if knownFieldErrors.bird_bgp_source_ipv4}
|
||||
<p class="text-sm text-red-600">{knownFieldErrors.bird_bgp_source_ipv4}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="bird-bgp-source-ipv6">BGP source IPv6 (bird_bgp_source_ipv6)</Label>
|
||||
<Input
|
||||
id="bird-bgp-source-ipv6"
|
||||
bind:value={knownFields.bird_bgp_source_ipv6}
|
||||
placeholder="2001:db8::11"
|
||||
/>
|
||||
{#if knownFieldErrors.bird_bgp_source_ipv6}
|
||||
<p class="text-sm text-red-600">{knownFieldErrors.bird_bgp_source_ipv6}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-sm font-medium">Управление ревизиями</h3>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="revision-retention-minutes">
|
||||
Время жизни ревизий, мин (revision_retention_minutes)
|
||||
</Label>
|
||||
<Input
|
||||
id="revision-retention-minutes"
|
||||
type="number"
|
||||
min="15"
|
||||
max="43200"
|
||||
bind:value={knownFields.revision_retention_minutes}
|
||||
placeholder="43200"
|
||||
/>
|
||||
{#if knownFieldErrors.revision_retention_minutes}
|
||||
<p class="text-sm text-red-600">{knownFieldErrors.revision_retention_minutes}</p>
|
||||
{/if}
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Старые ревизии удаляются автоматически. Последняя раскатанная ревизия не удаляется.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium">Дополнительные настройки (KV)</h3>
|
||||
<Button variant="outline" size="sm" onclick={addAdditionalSetting}>
|
||||
<Plus class="size-4" />
|
||||
Добавить строку
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if additionalSettings.length === 0}
|
||||
<p class="text-muted-foreground text-sm">Нет дополнительных параметров.</p>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-2">
|
||||
{#each additionalSettings as entry (entry.id)}
|
||||
<div class="grid grid-cols-1 gap-2 md:grid-cols-[1fr_1fr_auto]">
|
||||
<Input bind:value={entry.key} placeholder="Ключ (например, bird_log_level)" />
|
||||
<Input bind:value={entry.value} placeholder="Значение (строка)" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="Удалить строку"
|
||||
onclick={() => removeAdditionalSetting(entry.id)}
|
||||
>
|
||||
<Trash2 class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if hasValidationErrors}
|
||||
<p class="text-sm text-red-600">
|
||||
Есть ошибки в полях. Исправьте их, чтобы сохранить изменения.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<Button onclick={saveApiSettings} disabled={!canSaveSettings}>
|
||||
<Save />
|
||||
{savingSettings ? 'Сохранение…' : 'Применить настройки'}
|
||||
</Button>
|
||||
{:else}
|
||||
<Button variant="outline" onclick={loadApiSettings}>Загрузить настройки</Button>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user