feat: improve peer speaker assignment UX in network settings

Replace manual speaker id input with a validated speaker selector and clearer speaker labels so operators can configure peers with fewer input mistakes and better context.

Made-with: Cursor
This commit is contained in:
Denozordec
2026-04-07 21:08:00 +07:00
parent 2da6d49368
commit f877ec89a7
+46 -6
View File
@@ -14,6 +14,12 @@
import { Card, CardContent, CardHeader, CardTitle, 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 {
Select,
SelectContent,
SelectItem,
SelectTrigger
} from '$lib/components/ui/select/index.js';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '$lib/components/ui/tabs/index.js';
import {
Dialog,
@@ -73,6 +79,12 @@
let speakerForm = $state<BgpSpeakerCreate>({ endpoint: '', role: 'operator' });
let speakerSaving = $state(false);
let applyingId = $state<string | null>(null);
const speakerById = $derived.by(() => new Map(speakers.map((s) => [s.id, s])));
function speakerLabelById(id: string | null | undefined) {
if (!id) return '—';
return speakerById.get(id)?.endpoint ?? id;
}
async function loadAll() {
peersLoading = true;
@@ -123,8 +135,12 @@
}
}
async function savePeer() {
if (!peerForm.neighbor || !peerForm.remote_asn) {
toast.error('Укажите адрес и ASN');
if (!peerForm.neighbor.trim()) {
toast.error('Укажите адрес соседа');
return;
}
if (!peerForm.remote_asn || peerForm.remote_asn <= 0) {
toast.error('Remote ASN должен быть больше 0');
return;
}
peerSaving = true;
@@ -271,7 +287,7 @@
<TableCell>
<Badge variant={sessionBadge(p.session_state)}>{p.session_state || '—'}</Badge>
</TableCell>
<TableCell class="text-muted-foreground font-mono text-xs">{p.bgp_speaker_id ? p.bgp_speaker_id.slice(0, 8) + '…' : '—'}</TableCell>
<TableCell class="text-muted-foreground text-xs">{speakerLabelById(p.bgp_speaker_id)}</TableCell>
<TableCell>
<div class="flex gap-1">
<Button variant="ghost" size="icon-sm" onclick={() => openPeerEdit(p)}><Pencil class="size-3.5" /></Button>
@@ -325,7 +341,13 @@
<TableCell class="font-mono text-xs text-muted-foreground">{s.last_applied_revision_id ? s.last_applied_revision_id.slice(0, 8) + '…' : '—'}</TableCell>
<TableCell>
<div class="flex gap-1">
<Button variant="outline" size="xs" onclick={() => applySpeaker(s.id)} disabled={applyingId === s.id}>
<Button
variant="outline"
size="xs"
title="Запустить применение ревизии на спикере"
onclick={() => applySpeaker(s.id)}
disabled={applyingId === s.id}
>
<Play class="size-3" />
{applyingId === s.id ? 'Apply…' : 'Apply'}
</Button>
@@ -365,8 +387,26 @@
<Input id="p-asn" type="number" placeholder="65000" bind:value={peerForm.remote_asn} />
</div>
<div class="space-y-1.5">
<Label for="p-speaker">Спикер (ID, опционально)</Label>
<Input id="p-speaker" placeholder="speaker-id" bind:value={peerForm.bgp_speaker_id} />
<Label for="p-speaker">Спикер (опционально)</Label>
<Select
type="single"
value={peerForm.bgp_speaker_id ?? ''}
onValueChange={(v) => {
peerForm = { ...peerForm, bgp_speaker_id: v || null };
}}
>
<SelectTrigger id="p-speaker" class="w-full">
{peerForm.bgp_speaker_id
? speakerLabelById(peerForm.bgp_speaker_id)
: 'Не выбрано'}
</SelectTrigger>
<SelectContent>
<SelectItem value="">Не выбрано</SelectItem>
{#each speakers as s (s.id)}
<SelectItem value={s.id}>{s.endpoint} ({s.id.slice(0, 8)}…)</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
<div
class="border-border bg-muted/30 flex flex-row items-center justify-between gap-4 rounded-lg border p-3"