feat: improve BGP protocol summary handling by introducing isBGPProtocolSummaryRow function. Update SummarizeProtocolsOutput and related tests to ensure evobgp_* static names are not counted as BGP sessions, enhancing accuracy in protocol summaries.
CI / changes (push) Successful in 5s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 23s
CI / bird2 (push) Has been cancelled
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been cancelled
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been cancelled
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been cancelled
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been cancelled
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been cancelled
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been cancelled
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been cancelled
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has started running
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been cancelled
CI / docker-bird (push) Has been cancelled
CI / changes (push) Successful in 5s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 23s
CI / bird2 (push) Has been cancelled
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been cancelled
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been cancelled
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been cancelled
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been cancelled
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been cancelled
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been cancelled
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been cancelled
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has started running
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been cancelled
CI / docker-bird (push) Has been cancelled
This commit is contained in:
@@ -11,6 +11,24 @@ type ProtocolsSummary struct {
|
|||||||
RawLineCount int
|
RawLineCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isBGPProtocolSummaryRow is true for BIRD "show protocols" summary rows where the
|
||||||
|
// second column (Proto) is BGP. Substring checks are unsafe: names like evobgp_* contain "bgp".
|
||||||
|
func isBGPProtocolSummaryRow(line string) bool {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
low := strings.ToLower(line)
|
||||||
|
if strings.HasPrefix(low, "name") || strings.HasPrefix(low, "table") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.EqualFold(fields[1], "BGP")
|
||||||
|
}
|
||||||
|
|
||||||
// SummarizeProtocolsOutput extracts BGP session heuristics from birdc output.
|
// SummarizeProtocolsOutput extracts BGP session heuristics from birdc output.
|
||||||
func SummarizeProtocolsOutput(output string) ProtocolsSummary {
|
func SummarizeProtocolsOutput(output string) ProtocolsSummary {
|
||||||
var s ProtocolsSummary
|
var s ProtocolsSummary
|
||||||
@@ -22,14 +40,12 @@ func SummarizeProtocolsOutput(output string) ProtocolsSummary {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
low := strings.ToLower(line)
|
low := strings.ToLower(line)
|
||||||
if strings.HasPrefix(low, "name") || strings.HasPrefix(low, "table") {
|
if !isBGPProtocolSummaryRow(line) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if strings.Contains(low, "bgp") {
|
s.BGPSessionsTotal++
|
||||||
s.BGPSessionsTotal++
|
if strings.Contains(low, "established") {
|
||||||
if strings.Contains(low, "established") {
|
s.BGPEstablished++
|
||||||
s.BGPEstablished++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
|
|||||||
@@ -12,3 +12,14 @@ uplink BGP --- start 10:00:01 Established
|
|||||||
t.Fatalf("got %+v", s)
|
t.Fatalf("got %+v", s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSummarizeProtocolsOutput_evoBGPNameNotCountedAsBGP(t *testing.T) {
|
||||||
|
sample := `Name Proto Table State Since Info
|
||||||
|
evobgp_prefixes_v4 Static master4 up 17:32:14.631
|
||||||
|
evobgp_prefixes_v6 Static master6 up 17:32:14.631
|
||||||
|
`
|
||||||
|
s := SummarizeProtocolsOutput(sample)
|
||||||
|
if s.BGPSessionsTotal != 0 || s.BGPEstablished != 0 {
|
||||||
|
t.Fatalf("evobgp_* static names must not match substring bgp: got %+v", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func CountEstablishedBGPSessions(showProtocolsOutput string) int {
|
|||||||
if line == "" || strings.HasPrefix(line, "name") || strings.HasPrefix(strings.ToLower(line), "table") {
|
if line == "" || strings.HasPrefix(line, "name") || strings.HasPrefix(strings.ToLower(line), "table") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !strings.Contains(strings.ToLower(line), "bgp") {
|
if !isBGPProtocolSummaryRow(line) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if strings.Contains(strings.ToLower(line), "established") {
|
if strings.Contains(strings.ToLower(line), "established") {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
/** preserve — вывод CLI (колонки birdc); wrap — конфиги и JSON с длинными строками */
|
||||||
|
type Variant = 'preserve' | 'wrap';
|
||||||
|
|
||||||
|
let {
|
||||||
|
variant = 'preserve',
|
||||||
|
text,
|
||||||
|
class: className = ''
|
||||||
|
}: {
|
||||||
|
variant?: Variant;
|
||||||
|
text: string;
|
||||||
|
class?: string;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const shell =
|
||||||
|
'border-border bg-muted/40 relative isolate min-w-0 overflow-auto rounded-lg border [scrollbar-gutter:stable] overscroll-contain';
|
||||||
|
|
||||||
|
const prePreserve =
|
||||||
|
'text-foreground m-0 block w-max min-w-full p-4 font-mono text-[0.8125rem] leading-normal whitespace-pre select-text';
|
||||||
|
|
||||||
|
const preWrap =
|
||||||
|
'text-foreground m-0 block min-w-0 w-full max-w-none p-4 font-mono text-[0.8125rem] leading-relaxed whitespace-pre-wrap break-words select-text';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={cn(shell, className)} data-slot="scroll-pre-block">
|
||||||
|
<pre class={variant === 'preserve' ? prePreserve : preWrap}>{text}</pre>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Общая разметка для модалок с прокручиваемым контентом (превью BIRD, вывод birdc, логи).
|
||||||
|
* DialogContent по умолчанию — grid + p-4; здесь переопределяем на flex-колонку без внешних отступов.
|
||||||
|
*/
|
||||||
|
export const dialogContentDocument =
|
||||||
|
'!flex w-[min(100vw-2rem,56rem)] max-h-[min(92vh,880px)] flex-col gap-0 overflow-hidden !p-0 sm:max-w-4xl';
|
||||||
|
|
||||||
|
export const dialogHeaderDocument =
|
||||||
|
'shrink-0 space-y-1.5 border-b border-border/70 px-6 pt-5 pb-3 pr-14 text-left';
|
||||||
|
|
||||||
|
export const dialogBodyDocument =
|
||||||
|
'flex min-h-0 min-w-0 flex-1 flex-col gap-3 overflow-hidden px-6 py-4';
|
||||||
|
|
||||||
|
/** Компактные модалки (детали задачи, формы): единая шапка и тело */
|
||||||
|
export const dialogContentPanel =
|
||||||
|
'!flex max-h-[min(90vh,40rem)] w-full max-w-lg flex-col gap-0 overflow-hidden !p-0 sm:max-w-lg';
|
||||||
|
|
||||||
|
export const dialogHeaderPanel =
|
||||||
|
'shrink-0 space-y-1.5 border-b border-border/70 px-6 pt-5 pb-3 pr-14 text-left';
|
||||||
|
|
||||||
|
export const dialogBodyPanel = 'min-h-0 flex-1 overflow-y-auto px-6 py-4';
|
||||||
@@ -40,6 +40,16 @@
|
|||||||
TableRow
|
TableRow
|
||||||
} from '$lib/components/ui/table/index.js';
|
} from '$lib/components/ui/table/index.js';
|
||||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||||
|
import ScrollPreBlock from '$lib/components/app/scroll-pre-block.svelte';
|
||||||
|
import {
|
||||||
|
dialogBodyDocument,
|
||||||
|
dialogBodyPanel,
|
||||||
|
dialogContentDocument,
|
||||||
|
dialogContentPanel,
|
||||||
|
dialogHeaderDocument,
|
||||||
|
dialogHeaderPanel
|
||||||
|
} from '$lib/dialog-layout.js';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
||||||
import Play from '@lucide/svelte/icons/play';
|
import Play from '@lucide/svelte/icons/play';
|
||||||
@@ -631,25 +641,23 @@
|
|||||||
|
|
||||||
<!-- Preview dialog -->
|
<!-- Preview dialog -->
|
||||||
<Dialog bind:open={previewDialog}>
|
<Dialog bind:open={previewDialog}>
|
||||||
<DialogContent
|
<DialogContent class={dialogContentDocument}>
|
||||||
class="!flex max-h-[min(96vh,880px)] w-[min(100vw-2rem,56rem)] flex-col gap-4 overflow-hidden sm:max-w-4xl"
|
<DialogHeader class={dialogHeaderDocument}>
|
||||||
>
|
|
||||||
<DialogHeader class="shrink-0 space-y-1.5 pr-8">
|
|
||||||
<DialogTitle>Ревизия {previewRevision?.id.slice(0, 8)}…</DialogTitle>
|
<DialogTitle>Ревизия {previewRevision?.id.slice(0, 8)}…</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Срендеренный конфиг BIRD 2 (фрагменты из control plane) и материализованные префиксы.
|
Срендеренный конфиг BIRD 2 (фрагменты из control plane) и материализованные префиксы.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{#if previewLoading}
|
{#if previewLoading}
|
||||||
<p class="text-muted-foreground py-4 text-center text-sm">Загрузка…</p>
|
<div class="text-muted-foreground px-6 py-10 text-center text-sm">Загрузка…</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex min-h-0 min-w-0 flex-1 flex-col">
|
<div class={cn(dialogBodyDocument, 'min-h-[min(44vh,400px)]')}>
|
||||||
<Tabs bind:value={previewSubTab} class="flex min-h-0 flex-1 flex-col gap-3">
|
<Tabs bind:value={previewSubTab} class="flex min-h-0 min-w-0 flex-1 flex-col gap-3">
|
||||||
<TabsList class="grid w-full max-w-md shrink-0 grid-cols-2">
|
<TabsList class="grid w-full max-w-md shrink-0 grid-cols-2">
|
||||||
<TabsTrigger value="bird">Конфиг BIRD</TabsTrigger>
|
<TabsTrigger value="bird">Конфиг BIRD</TabsTrigger>
|
||||||
<TabsTrigger value="prefixes">Префиксы</TabsTrigger>
|
<TabsTrigger value="prefixes">Префиксы</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="bird" class="mt-0 flex min-h-0 flex-1 flex-col gap-2 outline-none">
|
<TabsContent value="bird" class="mt-0 flex min-h-0 min-w-0 flex-1 flex-col gap-2 outline-none">
|
||||||
{@const frags = asPreviewFragments(previewData)}
|
{@const frags = asPreviewFragments(previewData)}
|
||||||
{#if Object.keys(frags).length === 0}
|
{#if Object.keys(frags).length === 0}
|
||||||
<p class="text-muted-foreground text-sm">Нет фрагментов превью (старая ревизия или пустой render).</p>
|
<p class="text-muted-foreground text-sm">Нет фрагментов превью (старая ревизия или пустой render).</p>
|
||||||
@@ -670,22 +678,20 @@
|
|||||||
Совет: откройте <code class="bg-muted rounded px-1">_bird_full_expanded.conf</code> — один текст с
|
Совет: откройте <code class="bg-muted rounded px-1">_bird_full_expanded.conf</code> — один текст с
|
||||||
<code class="bg-muted rounded px-1">bird.conf</code> и содержимым всех include.
|
<code class="bg-muted rounded px-1">bird.conf</code> и содержимым всех include.
|
||||||
</p>
|
</p>
|
||||||
<div
|
<ScrollPreBlock
|
||||||
class="border-border bg-muted/40 text-foreground relative isolate min-h-[12rem] min-w-0 flex-1 overflow-x-auto overflow-y-auto rounded-lg border [scrollbar-gutter:stable] overscroll-contain"
|
variant="wrap"
|
||||||
>
|
text={frags[birdPreviewPath] ?? ''}
|
||||||
<pre
|
class="min-h-[12rem] min-w-0 flex-1"
|
||||||
class="text-foreground m-0 block min-w-0 w-full max-w-none p-3 font-mono text-xs leading-relaxed whitespace-pre-wrap break-words select-text"
|
/>
|
||||||
>{frags[birdPreviewPath] ?? ''}</pre>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="prefixes" class="mt-0 flex min-h-0 flex-1 flex-col gap-2 outline-none">
|
<TabsContent value="prefixes" class="mt-0 flex min-h-0 min-w-0 flex-1 flex-col gap-2 outline-none">
|
||||||
<p class="text-sm shrink-0">
|
<p class="text-sm shrink-0">
|
||||||
<span class="font-medium">Префиксов:</span>
|
<span class="font-medium">Префиксов:</span>
|
||||||
{prefixesData.length}
|
{prefixesData.length}
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
class="border-border bg-muted/40 min-h-[10rem] min-w-0 flex-1 overflow-x-auto overflow-y-auto rounded-lg border p-3 [scrollbar-gutter:stable] overscroll-contain"
|
class="border-border bg-muted/40 flex min-h-[10rem] min-w-0 flex-1 flex-col overflow-auto rounded-lg border p-3 [scrollbar-gutter:stable] overscroll-contain"
|
||||||
>
|
>
|
||||||
{#each prefixesData as pfx}
|
{#each prefixesData as pfx}
|
||||||
<p class="font-mono text-xs">{pfx}</p>
|
<p class="font-mono text-xs">{pfx}</p>
|
||||||
@@ -702,39 +708,46 @@
|
|||||||
|
|
||||||
<!-- BIRD protocols excerpt -->
|
<!-- BIRD protocols excerpt -->
|
||||||
<Dialog bind:open={birdProtocolsOpen}>
|
<Dialog bind:open={birdProtocolsOpen}>
|
||||||
<DialogContent class="flex max-h-[min(90vh,720px)] flex-col gap-3 sm:max-w-3xl">
|
<DialogContent class={dialogContentDocument}>
|
||||||
<DialogHeader>
|
<DialogHeader class={dialogHeaderDocument}>
|
||||||
<DialogTitle>Вывод birdc (протоколы)</DialogTitle>
|
<DialogTitle>Вывод birdc (протоколы)</DialogTitle>
|
||||||
<DialogDescription>Фрагмент ответа на этом API-хосте; при длинном выводе обрезан на сервере.</DialogDescription>
|
<DialogDescription>
|
||||||
|
Фрагмент ответа на этом API-хосте; при длинном выводе обрезан на сервере. Таблица сохраняет выравнивание
|
||||||
|
колонок (горизонтальная прокрутка).
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<ScrollArea class="border-border bg-muted/40 max-h-[min(60vh,480px)] rounded-md border">
|
<div class="px-6 pb-6 pt-0">
|
||||||
<pre class="m-0 p-3 font-mono text-xs leading-relaxed whitespace-pre-wrap break-words select-text">{birdStatus?.protocols_excerpt ?? ''}</pre>
|
<ScrollPreBlock
|
||||||
</ScrollArea>
|
variant="preserve"
|
||||||
|
text={birdStatus?.protocols_excerpt ?? ''}
|
||||||
|
class="h-[min(65vh,560px)] w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<!-- Job detail dialog -->
|
<!-- Job detail dialog -->
|
||||||
<Dialog bind:open={jobDetailDialog}>
|
<Dialog bind:open={jobDetailDialog}>
|
||||||
<DialogContent class="sm:max-w-lg">
|
<DialogContent class={dialogContentPanel}>
|
||||||
<DialogHeader>
|
<DialogHeader class={dialogHeaderPanel}>
|
||||||
<DialogTitle>Задача: {jobDetail?.kind}</DialogTitle>
|
<DialogTitle>Задача: {jobDetail?.kind}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{#if jobDetail}
|
{#if jobDetail}
|
||||||
<div class="space-y-2 text-sm">
|
<div class={dialogBodyPanel}>
|
||||||
<div class="grid grid-cols-2 gap-x-4 gap-y-1">
|
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
|
||||||
<span class="text-muted-foreground">ID</span><span class="font-mono">{jobDetail.job_id}</span>
|
<span class="text-muted-foreground">ID</span><span class="font-mono break-all">{jobDetail.job_id}</span>
|
||||||
<span class="text-muted-foreground">Статус</span><span><Badge variant={jobStatusVariant(jobDetail.status)}>{jobDetail.status}</Badge></span>
|
<span class="text-muted-foreground">Статус</span><span><Badge variant={jobStatusVariant(jobDetail.status)}>{jobDetail.status}</Badge></span>
|
||||||
<span class="text-muted-foreground">Создана</span><span>{formatDate(jobDetail.created_at)}</span>
|
<span class="text-muted-foreground">Создана</span><span>{formatDate(jobDetail.created_at)}</span>
|
||||||
<span class="text-muted-foreground">Начата</span><span>{formatDate(jobDetail.started_at)}</span>
|
<span class="text-muted-foreground">Начата</span><span>{formatDate(jobDetail.started_at)}</span>
|
||||||
<span class="text-muted-foreground">Завершена</span><span>{formatDate(jobDetail.finished_at)}</span>
|
<span class="text-muted-foreground">Завершена</span><span>{formatDate(jobDetail.finished_at)}</span>
|
||||||
{#if jobDetail.error}
|
{#if jobDetail.error}
|
||||||
<span class="text-muted-foreground">Ошибка</span><span class="text-destructive">{jobDetail.error}</span>
|
<span class="text-muted-foreground">Ошибка</span><span class="text-destructive break-words">{jobDetail.error}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if jobDetail.meta && Object.keys(jobDetail.meta).length > 0}
|
{#if jobDetail.meta && Object.keys(jobDetail.meta).length > 0}
|
||||||
<div>
|
<div class="mt-4 space-y-2">
|
||||||
<p class="text-muted-foreground mb-1">Meta</p>
|
<p class="text-muted-foreground text-sm font-medium">Meta</p>
|
||||||
<pre class="bg-muted rounded p-2 text-xs overflow-auto max-h-40">{JSON.stringify(jobDetail.meta, null, 2)}</pre>
|
<ScrollPreBlock variant="wrap" text={JSON.stringify(jobDetail.meta, null, 2)} class="max-h-52 w-full" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user