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:
@@ -14,6 +14,12 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '$lib/components/ui/card/index.js';
|
import { Card, CardContent, CardHeader, CardTitle, 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 {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger
|
||||||
|
} from '$lib/components/ui/select/index.js';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '$lib/components/ui/tabs/index.js';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '$lib/components/ui/tabs/index.js';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -73,6 +79,12 @@
|
|||||||
let speakerForm = $state<BgpSpeakerCreate>({ endpoint: '', role: 'operator' });
|
let speakerForm = $state<BgpSpeakerCreate>({ endpoint: '', role: 'operator' });
|
||||||
let speakerSaving = $state(false);
|
let speakerSaving = $state(false);
|
||||||
let applyingId = $state<string | null>(null);
|
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() {
|
async function loadAll() {
|
||||||
peersLoading = true;
|
peersLoading = true;
|
||||||
@@ -123,8 +135,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function savePeer() {
|
async function savePeer() {
|
||||||
if (!peerForm.neighbor || !peerForm.remote_asn) {
|
if (!peerForm.neighbor.trim()) {
|
||||||
toast.error('Укажите адрес и ASN');
|
toast.error('Укажите адрес соседа');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!peerForm.remote_asn || peerForm.remote_asn <= 0) {
|
||||||
|
toast.error('Remote ASN должен быть больше 0');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
peerSaving = true;
|
peerSaving = true;
|
||||||
@@ -271,7 +287,7 @@
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={sessionBadge(p.session_state)}>{p.session_state || '—'}</Badge>
|
<Badge variant={sessionBadge(p.session_state)}>{p.session_state || '—'}</Badge>
|
||||||
</TableCell>
|
</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>
|
<TableCell>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<Button variant="ghost" size="icon-sm" onclick={() => openPeerEdit(p)}><Pencil class="size-3.5" /></Button>
|
<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 class="font-mono text-xs text-muted-foreground">{s.last_applied_revision_id ? s.last_applied_revision_id.slice(0, 8) + '…' : '—'}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div class="flex gap-1">
|
<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" />
|
<Play class="size-3" />
|
||||||
{applyingId === s.id ? 'Apply…' : 'Apply'}
|
{applyingId === s.id ? 'Apply…' : 'Apply'}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -365,8 +387,26 @@
|
|||||||
<Input id="p-asn" type="number" placeholder="65000" bind:value={peerForm.remote_asn} />
|
<Input id="p-asn" type="number" placeholder="65000" bind:value={peerForm.remote_asn} />
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<Label for="p-speaker">Спикер (ID, опционально)</Label>
|
<Label for="p-speaker">Спикер (опционально)</Label>
|
||||||
<Input id="p-speaker" placeholder="speaker-id" bind:value={peerForm.bgp_speaker_id} />
|
<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>
|
||||||
<div
|
<div
|
||||||
class="border-border bg-muted/30 flex flex-row items-center justify-between gap-4 rounded-lg border p-3"
|
class="border-border bg-muted/30 flex flex-row items-center justify-between gap-4 rounded-lg border p-3"
|
||||||
|
|||||||
Reference in New Issue
Block a user