feat: add bird configuration preview functionality with tabbed interface for fragments and prefixes. Implement fragment extraction and default path selection for improved user experience in previewing BIRD configurations.
CI / changes (push) Successful in 6s
CI / openapi (push) Has been skipped
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m3s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m3s
CI / docker-bird (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

This commit is contained in:
Denozordec
2026-04-05 23:47:15 +07:00
parent 693227163a
commit 1969a00151
+68 -10
View File
@@ -60,6 +60,24 @@
let previewData = $state<unknown>(null);
let prefixesData = $state<string[]>([]);
let previewLoading = $state(false);
let previewSubTab = $state<'bird' | 'prefixes'>('bird');
let birdPreviewPath = $state('');
function asPreviewFragments(d: unknown): Record<string, string> {
if (!d || typeof d !== 'object' || Array.isArray(d)) return {};
const out: Record<string, string> = {};
for (const [k, v] of Object.entries(d as Record<string, unknown>)) {
if (typeof v === 'string') out[k] = v;
}
return out;
}
function defaultBirdPreviewPath(frags: Record<string, string>): string {
if (frags['_bird_full_expanded.conf']) return '_bird_full_expanded.conf';
if (frags['bird.conf']) return 'bird.conf';
const keys = Object.keys(frags).sort();
return keys[0] ?? '';
}
// Diff
let diffDialog = $state(false);
@@ -112,8 +130,10 @@
previewRevision = rev;
previewLoading = true;
previewDialog = true;
previewSubTab = 'bird';
previewData = null;
prefixesData = [];
birdPreviewPath = '';
try {
const [prev, pfx] = await Promise.all([
apiJSON<unknown>(`/v1/revisions/${rev.id}/preview`),
@@ -121,6 +141,8 @@
]);
previewData = prev;
prefixesData = (pfx.items ?? []).map((p: { prefix: string }) => p.prefix);
const frags = asPreviewFragments(prev);
birdPreviewPath = defaultBirdPreviewPath(frags);
} catch (e) {
toast.error(e instanceof Error ? e.message : String(e));
} finally {
@@ -483,22 +505,58 @@
<!-- Preview dialog -->
<Dialog bind:open={previewDialog}>
<DialogContent class="sm:max-w-2xl">
<DialogContent class="sm:max-w-4xl">
<DialogHeader>
<DialogTitle>Ревизия {previewRevision?.id.slice(0, 8)}</DialogTitle>
<DialogDescription>Предпросмотр префиксов ревизии</DialogDescription>
<DialogDescription>
Срендеренный конфиг BIRD 2 (фрагменты из control plane) и материализованные префиксы.
</DialogDescription>
</DialogHeader>
{#if previewLoading}
<p class="text-muted-foreground py-4 text-center text-sm">Загрузка…</p>
{:else}
<p class="text-sm mb-2"><span class="font-medium">Префиксов:</span> {prefixesData.length}</p>
<ScrollArea class="max-h-80 rounded border p-3">
{#each prefixesData as pfx}
<p class="font-mono text-xs">{pfx}</p>
{:else}
<p class="text-muted-foreground text-xs">Нет префиксов</p>
{/each}
</ScrollArea>
<Tabs bind:value={previewSubTab} class="w-full">
<TabsList class="grid w-full max-w-md grid-cols-2">
<TabsTrigger value="bird">Конфиг BIRD</TabsTrigger>
<TabsTrigger value="prefixes">Префиксы</TabsTrigger>
</TabsList>
<TabsContent value="bird" class="mt-3 space-y-2">
{@const frags = asPreviewFragments(previewData)}
{#if Object.keys(frags).length === 0}
<p class="text-muted-foreground text-sm">Нет фрагментов превью (старая ревизия или пустой render).</p>
{:else}
<div class="flex flex-wrap items-center gap-2">
<label for="bird-frag" class="text-muted-foreground text-sm">Файл</label>
<select
id="bird-frag"
class="border-input bg-background ring-offset-background focus-visible:ring-ring max-w-xl flex-1 rounded-md border px-2 py-1.5 text-sm focus-visible:ring-2 focus-visible:outline-none"
bind:value={birdPreviewPath}
>
{#each Object.keys(frags).sort() as path}
<option value={path}>{path}</option>
{/each}
</select>
</div>
<p class="text-muted-foreground text-xs">
Совет: откройте <code class="bg-muted rounded px-1">_bird_full_expanded.conf</code> — один текст с
<code class="bg-muted rounded px-1">bird.conf</code> и содержимым всех include.
</p>
<ScrollArea class="max-h-[min(70vh,28rem)] rounded border">
<pre class="p-3 font-mono text-xs whitespace-pre-wrap break-words">{frags[birdPreviewPath] ?? ''}</pre>
</ScrollArea>
{/if}
</TabsContent>
<TabsContent value="prefixes" class="mt-3">
<p class="text-sm mb-2"><span class="font-medium">Префиксов:</span> {prefixesData.length}</p>
<ScrollArea class="max-h-80 rounded border p-3">
{#each prefixesData as pfx}
<p class="font-mono text-xs">{pfx}</p>
{:else}
<p class="text-muted-foreground text-xs">Нет префиксов</p>
{/each}
</ScrollArea>
</TabsContent>
</Tabs>
{/if}
</DialogContent>
</Dialog>