feat: enhance community label handling and improve UI localization across operations pages. Introduce community label mapping for better readability in job logs and update various components to reflect Russian translations, enhancing user experience and consistency in the interface.
CI / changes (push) Successful in 6s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 43s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m8s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m9s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 17s
CI / docker-go-prime (push) Successful in 23s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m5s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m29s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m31s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m27s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m27s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m15s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m28s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m30s
CI / changes (push) Successful in 6s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 43s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m8s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m9s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 17s
CI / docker-go-prime (push) Successful in 23s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m5s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m29s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m31s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m27s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m27s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m15s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m28s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m30s
This commit is contained in:
+62
-21
@@ -58,12 +58,13 @@ type Worker struct {
|
||||
}
|
||||
|
||||
type revisionLogEntry struct {
|
||||
Kind string `json:"kind"`
|
||||
Source string `json:"source"`
|
||||
Community string `json:"community"`
|
||||
PrefixCount int `json:"prefix_count"`
|
||||
Sample []string `json:"sample,omitempty"`
|
||||
Message string `json:"message"`
|
||||
Kind string `json:"kind"`
|
||||
Source string `json:"source"`
|
||||
Community string `json:"community"`
|
||||
CommunityLabel string `json:"community_label"`
|
||||
PrefixCount int `json:"prefix_count"`
|
||||
Sample []string `json:"sample,omitempty"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
var defaultWorkerHTTP = &http.Client{Timeout: 45 * time.Second}
|
||||
@@ -275,17 +276,54 @@ func (w *Worker) runRollback(j *Job) {
|
||||
"rollback_summary": map[string]any{
|
||||
"source_revision_id": src,
|
||||
"new_revision_id": newID,
|
||||
"message": fmt.Sprintf("Rollback %s -> %s", shortID(src), shortID(newID)),
|
||||
"message": fmt.Sprintf("Откат %s → %s", shortID(src), shortID(newID)),
|
||||
},
|
||||
})
|
||||
w.enqueueDeployAllSpeakers(j, j.TenantID, newID)
|
||||
j.Succeed()
|
||||
}
|
||||
|
||||
// buildCommunityLabelMap maps community UUID -> human-readable title (or BGP community string).
|
||||
func buildCommunityLabelMap(st store.Backend, tenantID string) map[string]string {
|
||||
out := make(map[string]string)
|
||||
if st == nil {
|
||||
return out
|
||||
}
|
||||
list, err := st.ListCommunities(tenantID)
|
||||
if err != nil || list == nil {
|
||||
return out
|
||||
}
|
||||
for _, c := range list {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
label := strings.TrimSpace(c.Title)
|
||||
if label == "" {
|
||||
label = strings.TrimSpace(c.Community)
|
||||
}
|
||||
if label == "" {
|
||||
label = c.ID
|
||||
}
|
||||
out[c.ID] = label
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func resolveCommunityLabel(commID string, byID map[string]string) string {
|
||||
if commID == "" || commID == "none" {
|
||||
return "без community"
|
||||
}
|
||||
if lbl, ok := byID[commID]; ok && strings.TrimSpace(lbl) != "" {
|
||||
return strings.TrimSpace(lbl)
|
||||
}
|
||||
return commID
|
||||
}
|
||||
|
||||
func (w *Worker) buildRevisionLogEntries(tenantID, revID string) ([]map[string]any, int, error) {
|
||||
if w == nil || w.Store == nil {
|
||||
return nil, 0, fmt.Errorf("store not configured")
|
||||
}
|
||||
commLabels := buildCommunityLabelMap(w.Store, tenantID)
|
||||
var all []store.PrefixRow
|
||||
cursor := ""
|
||||
for {
|
||||
@@ -333,14 +371,16 @@ func (w *Worker) buildRevisionLogEntries(tenantID, revID string) ([]map[string]a
|
||||
out := make([]map[string]any, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
g := groups[k]
|
||||
msg := humanLogMessage(g.kind, g.source, g.count, g.community, g.sample)
|
||||
cl := resolveCommunityLabel(g.community, commLabels)
|
||||
msg := humanLogMessage(g.kind, g.source, g.count, cl, g.sample)
|
||||
out = append(out, map[string]any{
|
||||
"kind": g.kind,
|
||||
"source": g.source,
|
||||
"community": g.community,
|
||||
"prefix_count": g.count,
|
||||
"sample": g.sample,
|
||||
"message": msg,
|
||||
"kind": g.kind,
|
||||
"source": g.source,
|
||||
"community": g.community,
|
||||
"community_label": cl,
|
||||
"prefix_count": g.count,
|
||||
"sample": g.sample,
|
||||
"message": msg,
|
||||
})
|
||||
}
|
||||
return out, len(all), nil
|
||||
@@ -364,22 +404,23 @@ func classifySource(src string) (kind, name string) {
|
||||
}
|
||||
}
|
||||
|
||||
func humanLogMessage(kind, source string, count int, community string, sample []string) string {
|
||||
// humanLogMessage builds a Russian log line; communityLabel is already resolved (title or BGP value).
|
||||
func humanLogMessage(kind, source string, count int, communityLabel string, sample []string) string {
|
||||
switch kind {
|
||||
case "asn":
|
||||
return fmt.Sprintf("AS%s -> %d префиксов добавлены в community %s", source, count, community)
|
||||
return fmt.Sprintf("AS%s: добавлено %d префиксов в сообщество «%s»", source, count, communityLabel)
|
||||
case "domain":
|
||||
ips := strings.Join(prettyDomainSample(sample), " ")
|
||||
if ips == "" {
|
||||
ips = "-"
|
||||
ips = "—"
|
||||
}
|
||||
return fmt.Sprintf("%s -> ip (%s) -> добавлены в community %s", source, ips, community)
|
||||
return fmt.Sprintf("%s: IP (%s) → добавлено в сообщество «%s»", source, ips, communityLabel)
|
||||
case "cdn":
|
||||
return fmt.Sprintf("CDN source %s -> %d префиксов добавлены в community %s", source, count, community)
|
||||
return fmt.Sprintf("CDN «%s»: добавлено %d префиксов в сообщество «%s»", source, count, communityLabel)
|
||||
case "ip_range":
|
||||
return fmt.Sprintf("IP ranges -> %d префиксов добавлены в community %s", count, community)
|
||||
return fmt.Sprintf("Статические диапазоны: добавлено %d префиксов в сообщество «%s»", count, communityLabel)
|
||||
default:
|
||||
return fmt.Sprintf("%s -> %d префиксов добавлены в community %s", source, count, community)
|
||||
return fmt.Sprintf("%s: добавлено %d префиксов в сообщество «%s»", source, count, communityLabel)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
{#if !collapsed}
|
||||
<div class="min-w-0 flex-1">
|
||||
<a href={resolve('/')} class="text-sidebar-foreground block truncate font-semibold tracking-tight">EvoBGP</a>
|
||||
<p class="text-sidebar-foreground/50 truncate text-xs">Control plane</p>
|
||||
<p class="text-sidebar-foreground/50 truncate text-xs">Панель управления</p>
|
||||
</div>
|
||||
{/if}
|
||||
<DropdownMenu>
|
||||
@@ -133,7 +133,8 @@
|
||||
href={resolve(item.href)}
|
||||
class={cn(
|
||||
buttonVariants({ variant: active ? 'secondary' : 'ghost', size: 'icon-sm' }),
|
||||
'w-full no-underline flex items-center justify-center'
|
||||
'w-full no-underline flex items-center justify-center',
|
||||
active && 'ring-sidebar-primary/50 bg-sidebar-accent/90 text-sidebar-accent-foreground ring-1'
|
||||
)}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
>
|
||||
@@ -147,7 +148,9 @@
|
||||
href={resolve(item.href)}
|
||||
class={cn(
|
||||
buttonVariants({ variant: active ? 'secondary' : 'ghost', size: 'sm' }),
|
||||
'w-full justify-start gap-2 no-underline'
|
||||
'w-full justify-start gap-2 no-underline',
|
||||
active &&
|
||||
'border-l-sidebar-primary bg-sidebar-accent/80 text-sidebar-accent-foreground border-l-2 shadow-sm'
|
||||
)}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
>
|
||||
@@ -171,7 +174,8 @@
|
||||
href={resolve(item.href)}
|
||||
class={cn(
|
||||
buttonVariants({ variant: active ? 'secondary' : 'ghost', size: 'icon-sm' }),
|
||||
'w-full no-underline flex items-center justify-center'
|
||||
'w-full no-underline flex items-center justify-center',
|
||||
active && 'ring-sidebar-primary/50 bg-sidebar-accent/90 text-sidebar-accent-foreground ring-1'
|
||||
)}
|
||||
>
|
||||
<Icon class="size-4" />
|
||||
@@ -184,7 +188,9 @@
|
||||
href={resolve(item.href)}
|
||||
class={cn(
|
||||
buttonVariants({ variant: active ? 'secondary' : 'ghost', size: 'sm' }),
|
||||
'w-full justify-start gap-2 no-underline'
|
||||
'w-full justify-start gap-2 no-underline',
|
||||
active &&
|
||||
'border-l-sidebar-primary bg-sidebar-accent/80 text-sidebar-accent-foreground border-l-2 shadow-sm'
|
||||
)}
|
||||
>
|
||||
<Icon class="size-4" />
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import Filter from '@lucide/svelte/icons/filter';
|
||||
import Search from '@lucide/svelte/icons/search';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import { jobKindFilterRu, jobStatusRu } from '$lib/ui-labels.js';
|
||||
|
||||
type Props = {
|
||||
searchQ: string;
|
||||
@@ -42,19 +43,19 @@
|
||||
|
||||
const statusOptions: { value: string; label: string }[] = [
|
||||
{ value: '', label: 'Все статусы' },
|
||||
{ value: 'queued', label: 'queued' },
|
||||
{ value: 'running', label: 'running' },
|
||||
{ value: 'succeeded', label: 'succeeded' },
|
||||
{ value: 'failed', label: 'failed' },
|
||||
{ value: 'cancelled', label: 'cancelled' }
|
||||
{ value: 'queued', label: jobStatusRu('queued') },
|
||||
{ value: 'running', label: jobStatusRu('running') },
|
||||
{ value: 'succeeded', label: jobStatusRu('succeeded') },
|
||||
{ value: 'failed', label: jobStatusRu('failed') },
|
||||
{ value: 'cancelled', label: jobStatusRu('cancelled') }
|
||||
];
|
||||
|
||||
const kindOptions: { value: string; label: string }[] = [
|
||||
{ value: '', label: 'Все типы' },
|
||||
{ value: 'module_refresh', label: 'module_refresh' },
|
||||
{ value: 'deploy_apply', label: 'deploy_apply' },
|
||||
{ value: 'revision_rollback', label: 'revision_rollback' },
|
||||
{ value: 'bird_reload', label: 'bird_reload' }
|
||||
{ value: 'module_refresh', label: jobKindFilterRu('module_refresh') },
|
||||
{ value: 'deploy_apply', label: jobKindFilterRu('deploy_apply') },
|
||||
{ value: 'revision_rollback', label: jobKindFilterRu('revision_rollback') },
|
||||
{ value: 'bird_reload', label: jobKindFilterRu('bird_reload') }
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -109,7 +110,7 @@
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="job-filter-status" class="text-xs">Статус (API)</Label>
|
||||
<Label for="job-filter-status" class="text-xs">Статус</Label>
|
||||
<select
|
||||
id="job-filter-status"
|
||||
class="border-border bg-background h-9 w-full rounded-md border px-2 text-sm"
|
||||
@@ -123,7 +124,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label for="job-filter-kind" class="text-xs">Тип задачи (API)</Label>
|
||||
<Label for="job-filter-kind" class="text-xs">Тип задачи</Label>
|
||||
<select
|
||||
id="job-filter-kind"
|
||||
class="border-border bg-background h-9 w-full rounded-md border px-2 text-sm"
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
import Link2 from '@lucide/svelte/icons/link-2';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { jobKindSubtitle, jobKindTitle } from '$lib/operations/job-kind-label.js';
|
||||
import { jobStatusRu, logKindRu, moduleTypeRu } from '$lib/ui-labels.js';
|
||||
import JobReportTableBlock from './job-report-table-block.svelte';
|
||||
import { asnReportColumns, reportRowColumns } from './job-report-columns.js';
|
||||
import type { RowData } from '@tanstack/table-core';
|
||||
@@ -84,7 +85,7 @@
|
||||
<div class="min-w-0 flex-1">
|
||||
<CardTitle class="text-base">Задачи</CardTitle>
|
||||
<CardDescription class="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<span>Фоновые задачи (ingest, apply, refresh)</span>
|
||||
<span>Фоновые задачи (ingest, применение, обновление)</span>
|
||||
{#if jobsFetchedTotal !== undefined}
|
||||
<span class="text-muted-foreground font-normal tabular-nums">
|
||||
· Показано {jobs.length} из {jobsFetchedTotal}
|
||||
@@ -168,7 +169,7 @@
|
||||
<CircleDot class="size-3.5 shrink-0" aria-hidden="true" />
|
||||
Статус
|
||||
</p>
|
||||
<Badge class="mt-1" variant={jobStatusVariant(job.status)}>{job.status}</Badge>
|
||||
<Badge class="mt-1" variant={jobStatusVariant(job.status)}>{jobStatusRu(job.status)}</Badge>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-md border border-chart-2/25 bg-chart-2/5 px-2.5 py-2 dark:bg-chart-2/10"
|
||||
@@ -221,7 +222,7 @@
|
||||
|
||||
{#if detailedJob.error}
|
||||
<div class="rounded-md border border-destructive/30 bg-destructive/5 p-3">
|
||||
<p class="mb-1 text-xs text-muted-foreground">error</p>
|
||||
<p class="mb-1 text-xs text-muted-foreground">Ошибка</p>
|
||||
<p class="text-sm break-words text-destructive">{detailedJob.error}</p>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -249,7 +250,7 @@
|
||||
<div class="min-w-0 rounded-md border bg-muted/60 px-2.5 py-2">
|
||||
<span
|
||||
class="text-[11px] tracking-wide text-muted-foreground uppercase"
|
||||
>source</span
|
||||
>Источник</span
|
||||
>
|
||||
<p
|
||||
class="mt-1 rounded bg-background px-1.5 py-0.5 font-mono text-xs break-all"
|
||||
@@ -260,29 +261,30 @@
|
||||
<div class="min-w-0 rounded-md border bg-muted/60 px-2.5 py-2">
|
||||
<span
|
||||
class="text-[11px] tracking-wide text-muted-foreground uppercase"
|
||||
>kind</span
|
||||
>Тип</span
|
||||
>
|
||||
<p
|
||||
class="mt-1 rounded bg-background px-1.5 py-0.5 font-mono text-xs break-all"
|
||||
>
|
||||
{entry.kind}
|
||||
{logKindRu(entry.kind)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="min-w-0 rounded-md border bg-muted/60 px-2.5 py-2">
|
||||
<span
|
||||
class="text-[11px] tracking-wide text-muted-foreground uppercase"
|
||||
>community</span
|
||||
>Сообщество BGP</span
|
||||
>
|
||||
<p
|
||||
class="mt-1 rounded bg-background px-1.5 py-0.5 font-mono text-xs break-all"
|
||||
class="mt-1 rounded bg-background px-1.5 py-0.5 text-xs break-words"
|
||||
title={entry.community !== 'none' ? entry.community : undefined}
|
||||
>
|
||||
{entry.community}
|
||||
{entry.community_label?.trim() || entry.community}
|
||||
</p>
|
||||
</div>
|
||||
<div class="min-w-0 rounded-md border bg-muted/60 px-2.5 py-2">
|
||||
<span
|
||||
class="text-[11px] tracking-wide text-muted-foreground uppercase"
|
||||
>prefixes</span
|
||||
>Префиксы</span
|
||||
>
|
||||
<p class="mt-1 rounded bg-background px-1.5 py-0.5 font-mono text-xs">
|
||||
{entry.prefix_count}
|
||||
@@ -292,7 +294,7 @@
|
||||
{#if entry.sample && entry.sample.length > 0}
|
||||
<div class="space-y-1.5">
|
||||
<p class="text-[11px] tracking-wide text-muted-foreground uppercase">
|
||||
sample
|
||||
Примеры
|
||||
</p>
|
||||
<div class="rounded-md border bg-muted/35 p-2.5">
|
||||
<div class="space-y-1.5">
|
||||
@@ -326,7 +328,7 @@
|
||||
<p class="text-sm font-medium">Операции по модулю</p>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{#if jobReport.module}
|
||||
<Badge variant="outline">{jobReport.module.type}</Badge>
|
||||
<Badge variant="outline">{moduleTypeRu(jobReport.module.type)}</Badge>
|
||||
<Badge variant="secondary">{jobReport.module.name}</Badge>
|
||||
{/if}
|
||||
{#if jobReport.revisionId}
|
||||
@@ -377,7 +379,7 @@
|
||||
class="text-chart-4 flex items-center gap-1 text-[11px] font-medium uppercase"
|
||||
>
|
||||
<Link2 class="size-3.5 shrink-0" aria-hidden="true" />
|
||||
CDN / IP Range
|
||||
CDN / IP-диапазоны
|
||||
</p>
|
||||
<p class="text-sm font-semibold">
|
||||
{jobReport.cdn.length}/{jobReport.ipRanges.length}
|
||||
@@ -389,7 +391,7 @@
|
||||
<p class="text-xs text-muted-foreground">Результат агрегации по типам</p>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{#each jobReport.aggregationByKind as row (`${job.job_id}-agg-${row.kind}`)}
|
||||
<Badge variant="outline">{row.kind}: {row.prefixCount}</Badge>
|
||||
<Badge variant="outline">{logKindRu(row.kind)}: {row.prefixCount}</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -425,7 +427,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0 space-y-1.5 rounded-lg border p-3">
|
||||
<p class="text-sm font-medium">IP range: итог по статическим диапазонам</p>
|
||||
<p class="text-sm font-medium">IP-диапазоны: итог по статическим диапазонам</p>
|
||||
<JobReportTableBlock
|
||||
rows={jobReport.ipRanges as RowData[]}
|
||||
columns={reportCols}
|
||||
@@ -437,7 +439,7 @@
|
||||
{/if}
|
||||
|
||||
<div class="space-y-1">
|
||||
<p class="text-xs text-muted-foreground">Meta (raw JSON)</p>
|
||||
<p class="text-xs text-muted-foreground">Meta (JSON)</p>
|
||||
<div class="rounded-md border bg-muted/30 p-3">
|
||||
<pre
|
||||
class="font-mono text-xs [overflow-wrap:anywhere] whitespace-pre-wrap">{JSON.stringify(
|
||||
|
||||
@@ -51,13 +51,13 @@
|
||||
<Play class="text-chart-1 size-5" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 space-y-1">
|
||||
<p class="font-semibold">Apply all speakers</p>
|
||||
<p class="text-muted-foreground max-w-[42ch] text-sm">Применить текущую конфигурацию на всех спикерах</p>
|
||||
<p class="font-semibold">Применить ко всем спикерам</p>
|
||||
<p class="text-muted-foreground max-w-[42ch] text-sm">Применить текущую конфигурацию на всех BIRD-спикерах</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button class="w-full shrink-0 self-start sm:w-auto sm:self-auto" onclick={onApply} disabled={applying}>
|
||||
<Play class="size-4" aria-hidden="true" />
|
||||
Apply
|
||||
Применить
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -76,7 +76,7 @@
|
||||
<RotateCcw class="text-chart-4 size-5" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 space-y-1">
|
||||
<p class="font-semibold">BIRD Reload</p>
|
||||
<p class="font-semibold">Перезагрузка BIRD</p>
|
||||
<p class="text-muted-foreground max-w-[42ch] text-sm">Перезагрузить конфигурацию BIRD на всех спикерах</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,7 +87,7 @@
|
||||
disabled={reloading}
|
||||
>
|
||||
<RotateCcw class="size-4" aria-hidden="true" />
|
||||
Reload
|
||||
Перезагрузить
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -127,7 +127,7 @@
|
||||
<span class="font-medium">{birdStatus.bgp_established}</span>
|
||||
<span class="text-muted-foreground">/</span>
|
||||
<span class="font-medium">{birdStatus.bgp_sessions_total}</span>
|
||||
<span class="text-muted-foreground"> Established / всего</span>
|
||||
<span class="text-muted-foreground"> установлено / всего</span>
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
|
||||
@@ -4,6 +4,8 @@ export type JobLogEntry = {
|
||||
kind: string;
|
||||
source: string;
|
||||
community: string;
|
||||
/** Человекочитаемое имя из справочника (title или BGP community). */
|
||||
community_label?: string;
|
||||
prefix_count: number;
|
||||
sample?: string[];
|
||||
message: string;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/** Русские подписи для enum из API (задачи, модули, логи refresh). */
|
||||
|
||||
export function jobStatusRu(status: string): string {
|
||||
switch (status) {
|
||||
case 'queued':
|
||||
return 'В очереди';
|
||||
case 'running':
|
||||
return 'Выполняется';
|
||||
case 'succeeded':
|
||||
return 'Успешно';
|
||||
case 'failed':
|
||||
return 'Ошибка';
|
||||
case 'cancelled':
|
||||
return 'Отменена';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
/** Подпись типа задачи для фильтров (значения API те же). */
|
||||
export function jobKindFilterRu(kind: string): string {
|
||||
switch (kind) {
|
||||
case 'module_refresh':
|
||||
return 'Обновление модуля';
|
||||
case 'deploy_apply':
|
||||
return 'Применение конфигурации';
|
||||
case 'revision_rollback':
|
||||
return 'Откат ревизии';
|
||||
case 'bird_reload':
|
||||
return 'Перезагрузка BIRD';
|
||||
default:
|
||||
return kind;
|
||||
}
|
||||
}
|
||||
|
||||
export function moduleTypeRu(type: string): string {
|
||||
switch (type) {
|
||||
case 'AS_PREFIXES':
|
||||
return 'AS (номера)';
|
||||
case 'CDN_CIDRS':
|
||||
return 'CDN CIDR';
|
||||
case 'DOMAINS':
|
||||
return 'Домены';
|
||||
case 'IP_RANGES':
|
||||
return 'IP-диапазоны';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
/** Тип строки в логе агрегации префиксов (поле kind). */
|
||||
export function logKindRu(kind: string): string {
|
||||
switch (kind) {
|
||||
case 'asn':
|
||||
return 'ASN';
|
||||
case 'domain':
|
||||
return 'Домен';
|
||||
case 'cdn':
|
||||
return 'CDN';
|
||||
case 'ip_range':
|
||||
return 'Статические диапазоны';
|
||||
case 'unknown':
|
||||
return 'Неизвестно';
|
||||
case 'source':
|
||||
return 'Источник';
|
||||
default:
|
||||
return kind;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ const resolve = (path: string) => path as any;
|
||||
import Activity from '@lucide/svelte/icons/activity';
|
||||
import Clock from '@lucide/svelte/icons/clock';
|
||||
import ArrowRight from '@lucide/svelte/icons/arrow-right';
|
||||
import LayoutDashboard from '@lucide/svelte/icons/layout-dashboard';
|
||||
|
||||
let healthy = $state<boolean | null>(null);
|
||||
let modules = $state(0);
|
||||
@@ -128,9 +129,17 @@ const resolve = (path: string) => path as any;
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Обзор</h1>
|
||||
<p class="text-muted-foreground mt-1 text-sm">Состояние EvoBGP control plane.</p>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="bg-primary/10 text-primary flex size-11 shrink-0 items-center justify-center rounded-xl"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<LayoutDashboard class="size-6" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Обзор</h1>
|
||||
<p class="text-muted-foreground mt-1 text-sm">Состояние панели управления EvoBGP.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Health -->
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
import Pencil from '@lucide/svelte/icons/pencil';
|
||||
import Trash2 from '@lucide/svelte/icons/trash-2';
|
||||
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
||||
import BookOpen from '@lucide/svelte/icons/book-open';
|
||||
|
||||
// --- Communities ---
|
||||
let communities = $state<BgpCommunity[]>([]);
|
||||
@@ -114,10 +115,10 @@
|
||||
const body = { ...commForm, title: commForm.title?.trim() || undefined };
|
||||
if (commEdit) {
|
||||
await apiMutate(`/v1/communities/${commEdit.id}`, 'PATCH', body);
|
||||
toast.success('Community обновлена');
|
||||
toast.success('Запись сообщества обновлена');
|
||||
} else {
|
||||
await apiMutate('/v1/communities', 'POST', body);
|
||||
toast.success('Community создана');
|
||||
toast.success('Сообщество создано');
|
||||
}
|
||||
commDialog = false;
|
||||
await loadComm();
|
||||
@@ -183,14 +184,22 @@
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Справочники</h1>
|
||||
<p class="text-muted-foreground mt-1 text-sm">BGP Communities и DoH-профили для резолвинга доменов.</p>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="bg-chart-2/15 text-chart-2 flex size-11 shrink-0 items-center justify-center rounded-xl"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<BookOpen class="size-6" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Справочники</h1>
|
||||
<p class="text-muted-foreground mt-1 text-sm">Сообщества BGP и DoH-профили для резолвинга доменов.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs value="communities">
|
||||
<TabsList>
|
||||
<TabsTrigger value="communities">Communities</TabsTrigger>
|
||||
<TabsTrigger value="communities">Сообщества BGP</TabsTrigger>
|
||||
<TabsTrigger value="doh">DoH профили</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -199,7 +208,7 @@
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between pb-2">
|
||||
<div>
|
||||
<CardTitle class="text-base">BGP Communities</CardTitle>
|
||||
<CardTitle class="text-base">Сообщества BGP</CardTitle>
|
||||
<CardDescription>Используются для тегирования префиксов</CardDescription>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -213,7 +222,7 @@
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Community</TableHead>
|
||||
<TableHead>Код сообщества</TableHead>
|
||||
<TableHead>Название</TableHead>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead class="w-20"></TableHead>
|
||||
@@ -235,7 +244,7 @@
|
||||
{:else}
|
||||
<TableRow>
|
||||
<TableCell colspan={4} class="text-muted-foreground text-center py-8">
|
||||
{commLoading ? 'Загрузка…' : 'Нет communities. Создайте первую.'}
|
||||
{commLoading ? 'Загрузка…' : 'Нет записей. Создайте первую.'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
@@ -302,11 +311,11 @@
|
||||
<Dialog bind:open={commDialog}>
|
||||
<DialogContent class="sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{commEdit ? 'Редактировать' : 'Новая'} community</DialogTitle>
|
||||
<DialogTitle>{commEdit ? 'Редактировать сообщество BGP' : 'Новое сообщество BGP'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="space-y-4 py-2">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="c-community">Community</Label>
|
||||
<Label for="c-community">Код сообщества</Label>
|
||||
<Input id="c-community" bind:value={commForm.community} placeholder="65001:120" />
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
@@ -324,7 +333,7 @@
|
||||
<AlertDialog open={!!commDeleteTarget} onOpenChange={(v) => { if (!v) commDeleteTarget = null; }}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Удалить community «{commDisplay(commDeleteTarget)}»?</AlertDialogTitle>
|
||||
<AlertDialogTitle>Удалить сообщество «{commDisplay(commDeleteTarget)}»?</AlertDialogTitle>
|
||||
<AlertDialogDescription>Это приведёт к удалению привязки во всех модулях.</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
||||
import ExternalLink from '@lucide/svelte/icons/external-link';
|
||||
import Trash2 from '@lucide/svelte/icons/trash-2';
|
||||
import { moduleTypeRu } from '$lib/ui-labels.js';
|
||||
import Boxes from '@lucide/svelte/icons/boxes';
|
||||
|
||||
let rows = $state<ModuleRow[]>([]);
|
||||
let loading = $state(false);
|
||||
@@ -155,9 +157,17 @@
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Модули префиксов</h1>
|
||||
<p class="text-muted-foreground mt-1 text-sm">Управление модулями — AS, CDN, домены, IP-диапазоны.</p>
|
||||
<div class="flex min-w-0 items-start gap-3">
|
||||
<div
|
||||
class="bg-chart-1/15 text-chart-1 flex size-11 shrink-0 items-center justify-center rounded-xl"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Boxes class="size-6" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Модули префиксов</h1>
|
||||
<p class="text-muted-foreground mt-1 text-sm">Управление модулями — AS, CDN, домены, IP-диапазоны.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" onclick={load} disabled={loading}>
|
||||
@@ -223,7 +233,7 @@
|
||||
</TableCell>
|
||||
<TableCell class="font-medium">{m.name}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={typeBadgeVariant(m.type)}>{m.type}</Badge>
|
||||
<Badge variant={typeBadgeVariant(m.type)}>{moduleTypeRu(m.type)}</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="text-muted-foreground">{m.priority}</TableCell>
|
||||
<TableCell class="text-muted-foreground font-mono text-xs">
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
import Save from '@lucide/svelte/icons/save';
|
||||
import Upload from '@lucide/svelte/icons/upload';
|
||||
import Download from '@lucide/svelte/icons/download';
|
||||
import { moduleTypeRu } from '$lib/ui-labels.js';
|
||||
|
||||
const moduleId = $derived(page.params.moduleId);
|
||||
|
||||
@@ -751,7 +752,7 @@
|
||||
<div class="min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h1 class="min-w-0 break-words text-2xl font-semibold tracking-tight">{mod.name}</h1>
|
||||
<Badge variant="outline">{mod.type}</Badge>
|
||||
<Badge variant="outline">{moduleTypeRu(mod.type)}</Badge>
|
||||
{#if mod.enabled}
|
||||
<Badge variant="default" class="text-xs">вкл</Badge>
|
||||
{:else}
|
||||
@@ -764,7 +765,7 @@
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" onclick={refreshMod} disabled={refreshing}>
|
||||
<RefreshCw class={refreshing ? 'animate-spin' : ''} />
|
||||
Refresh
|
||||
Обновить
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onclick={openEditMod}>
|
||||
<Pencil />
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '$lib/components/ui/card/index.js';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
||||
import Gauge from '@lucide/svelte/icons/gauge';
|
||||
|
||||
type HealthStatus = { status: string };
|
||||
type ReadyStatus = { status: string; checks?: Record<string, unknown> };
|
||||
@@ -42,10 +43,18 @@
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Мониторинг</h1>
|
||||
<p class="text-muted-foreground mt-1 text-sm">Доступность API и версия сборки.</p>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex min-w-0 items-start gap-3">
|
||||
<div
|
||||
class="bg-info/15 text-info flex size-11 shrink-0 items-center justify-center rounded-xl"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Gauge class="size-6" />
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onclick={load} disabled={loading}>
|
||||
<RefreshCw class={loading ? 'animate-spin' : ''} />
|
||||
@@ -57,17 +66,17 @@
|
||||
<!-- Health -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">Liveness</CardTitle>
|
||||
<CardTitle class="text-base">Доступность (liveness)</CardTitle>
|
||||
<CardDescription>GET /v1/health</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{#if health === null}
|
||||
<Badge variant="outline">—</Badge>
|
||||
{:else if health.ok}
|
||||
<Badge variant="default">OK</Badge>
|
||||
<Badge variant="default">ОК</Badge>
|
||||
{#if health.status}<p class="text-muted-foreground mt-1 text-xs">{health.status}</p>{/if}
|
||||
{:else}
|
||||
<Badge variant="destructive">FAIL</Badge>
|
||||
<Badge variant="destructive">Сбой</Badge>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -75,7 +84,7 @@
|
||||
<!-- Ready -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">Readiness</CardTitle>
|
||||
<CardTitle class="text-base">Готовность (readiness)</CardTitle>
|
||||
<CardDescription>GET /v1/ready</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
import Trash2 from '@lucide/svelte/icons/trash-2';
|
||||
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
||||
import Play from '@lucide/svelte/icons/play';
|
||||
import NetworkIcon from '@lucide/svelte/icons/network';
|
||||
|
||||
// --- Peers ---
|
||||
let peers = $state<PeerRow[]>([]);
|
||||
@@ -208,9 +209,17 @@
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Сеть</h1>
|
||||
<p class="text-muted-foreground mt-1 text-sm">BGP-пиры и спикеры (BIRD-агенты).</p>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="bg-chart-3/15 text-chart-3 flex size-11 shrink-0 items-center justify-center rounded-xl"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<NetworkIcon class="size-6" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Сеть</h1>
|
||||
<p class="text-muted-foreground mt-1 text-sm">BGP-пиры и спикеры (BIRD-агенты).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs value="peers">
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
ModulesResponse
|
||||
} from '$lib/api/types.js';
|
||||
import { jobKindTitle } from '$lib/operations/job-kind-label.js';
|
||||
import { jobStatusRu } from '$lib/ui-labels.js';
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '$lib/components/ui/tabs/index.js';
|
||||
@@ -64,6 +65,7 @@
|
||||
} from '$lib/dialog-layout.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Zap from '@lucide/svelte/icons/zap';
|
||||
|
||||
// Revisions
|
||||
let revisions = $state<RevisionRow[]>([]);
|
||||
@@ -324,7 +326,7 @@
|
||||
rollingBack = true;
|
||||
try {
|
||||
await apiMutate(`/v1/revisions/${rollbackTarget.id}/rollback`, 'POST', {});
|
||||
toast.success('Rollback выполнен');
|
||||
toast.success('Откат выполнен');
|
||||
rollbackTarget = null;
|
||||
await loadRevisions();
|
||||
} catch (e) {
|
||||
@@ -338,7 +340,7 @@
|
||||
try {
|
||||
const revId = revisions[0]?.id;
|
||||
if (!revId) {
|
||||
toast.error('Нет ревизий — сначала refresh модуля или дождитесь задачи render');
|
||||
toast.error('Нет ревизий — сначала обновите модуль или дождитесь задачи render');
|
||||
return;
|
||||
}
|
||||
const res = await apiMutate<{ job_id: string; status?: string }>('/v1/apply', 'POST', {
|
||||
@@ -352,10 +354,12 @@
|
||||
const job = await waitForJob(res.job_id, { timeoutMs: 180000 });
|
||||
const extra = summarizeJobBirdMeta(job);
|
||||
if (job.status === 'succeeded') {
|
||||
toast.success(extra ? `Apply успешно. ${extra}` : 'Apply успешно завершён');
|
||||
toast.success(extra ? `Применение успешно. ${extra}` : 'Конфигурация успешно применена');
|
||||
} else {
|
||||
toast.error(
|
||||
job.error ? `${job.status}: ${job.error}` : `Задача завершилась со статусом ${job.status}`
|
||||
job.error
|
||||
? `${jobStatusRu(job.status)}: ${job.error}`
|
||||
: `Задача завершилась со статусом ${jobStatusRu(job.status)}`
|
||||
);
|
||||
}
|
||||
await loadJobs();
|
||||
@@ -379,10 +383,12 @@
|
||||
const job = await waitForJob(res.job_id, { timeoutMs: 120000 });
|
||||
const extra = summarizeJobBirdMeta(job);
|
||||
if (job.status === 'succeeded') {
|
||||
toast.success(extra ? `Reload успешно. ${extra}` : 'birdc configure выполнен');
|
||||
toast.success(extra ? `Перезагрузка успешна. ${extra}` : 'Команда birdc configure выполнена');
|
||||
} else {
|
||||
toast.error(
|
||||
job.error ? `${job.status}: ${job.error}` : `Задача завершилась со статусом ${job.status}`
|
||||
job.error
|
||||
? `${jobStatusRu(job.status)}: ${job.error}`
|
||||
: `Задача завершилась со статусом ${jobStatusRu(job.status)}`
|
||||
);
|
||||
}
|
||||
await loadJobs();
|
||||
@@ -625,10 +631,15 @@
|
||||
const sample = Array.isArray(row.sample)
|
||||
? row.sample.filter((value): value is string => typeof value === 'string')
|
||||
: undefined;
|
||||
const communityLabel =
|
||||
typeof row.community_label === 'string' && row.community_label.trim().length > 0
|
||||
? row.community_label.trim()
|
||||
: undefined;
|
||||
parsed.push({
|
||||
kind,
|
||||
source,
|
||||
community,
|
||||
...(communityLabel ? { community_label: communityLabel } : {}),
|
||||
prefix_count: prefixCount,
|
||||
message,
|
||||
...(sample && sample.length > 0 ? { sample } : {})
|
||||
@@ -668,18 +679,26 @@
|
||||
}
|
||||
|
||||
function birdHealthyShortLabel(h: boolean | null | undefined): string {
|
||||
if (h === true) return 'OK';
|
||||
if (h === true) return 'ОК';
|
||||
if (h === false) return 'Проблема';
|
||||
return 'Н/Д';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Операции</h1>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Деплой конфигурации, управление ревизиями и задачами.
|
||||
</p>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="bg-chart-2/15 text-chart-2 flex size-11 shrink-0 items-center justify-center rounded-xl"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Zap class="size-6" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Операции</h1>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Деплой конфигурации, управление ревизиями и задачами.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OperationsQuickActions
|
||||
@@ -699,7 +718,7 @@
|
||||
<div class="overflow-x-auto pb-1 [scrollbar-gutter:stable]">
|
||||
<TabsList class="inline-flex min-w-max">
|
||||
<TabsTrigger value="revisions">Ревизии</TabsTrigger>
|
||||
<TabsTrigger value="diff">Diff</TabsTrigger>
|
||||
<TabsTrigger value="diff">Сравнение</TabsTrigger>
|
||||
<TabsTrigger value="jobs">Задачи</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
@@ -774,7 +793,7 @@
|
||||
<AlertDialog bind:open={applyConfirm}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Запустить Apply на всех спикерах?</AlertDialogTitle>
|
||||
<AlertDialogTitle>Применить конфигурацию на всех спикерах?</AlertDialogTitle>
|
||||
<AlertDialogDescription
|
||||
>Текущая конфигурация будет применена на всех BIRD-агентах. Требуется роль operator.</AlertDialogDescription
|
||||
>
|
||||
@@ -782,7 +801,7 @@
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Отмена</AlertDialogCancel>
|
||||
<AlertDialogAction onclick={doApply} disabled={applying}
|
||||
>{applying ? 'Apply…' : 'Применить'}</AlertDialogAction
|
||||
>{applying ? 'Применение…' : 'Применить'}</AlertDialogAction
|
||||
>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@@ -792,7 +811,7 @@
|
||||
<AlertDialog bind:open={reloadConfirm}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Reload BIRD?</AlertDialogTitle>
|
||||
<AlertDialogTitle>Перезагрузить BIRD?</AlertDialogTitle>
|
||||
<AlertDialogDescription
|
||||
>BIRD перезагрузит конфигурацию. Требуется роль operator.</AlertDialogDescription
|
||||
>
|
||||
@@ -800,7 +819,7 @@
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Отмена</AlertDialogCancel>
|
||||
<AlertDialogAction onclick={doBirdReload} disabled={reloading}
|
||||
>{reloading ? 'Reload…' : 'Reload'}</AlertDialogAction
|
||||
>{reloading ? 'Перезагрузка…' : 'Перезагрузить'}</AlertDialogAction
|
||||
>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@@ -861,7 +880,7 @@
|
||||
<DialogHeader class={dialogHeaderDocument}>
|
||||
<DialogTitle>Ревизия {previewRevision?.id.slice(0, 8)}…</DialogTitle>
|
||||
<DialogDescription>
|
||||
Срендеренный конфиг BIRD 2 (фрагменты из control plane) и материализованные префиксы.
|
||||
Сгенерированный конфиг BIRD 2 (фрагменты из control plane) и материализованные префиксы.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{#if previewLoading}
|
||||
@@ -964,7 +983,7 @@
|
||||
>{jobDetail.job_id}</span
|
||||
>
|
||||
<span class="text-muted-foreground">Статус</span><span
|
||||
><Badge variant={jobStatusVariant(jobDetail.status)}>{jobDetail.status}</Badge></span
|
||||
><Badge variant={jobStatusVariant(jobDetail.status)}>{jobStatusRu(jobDetail.status)}</Badge></span
|
||||
>
|
||||
<span class="text-muted-foreground">Создана</span><span
|
||||
>{formatDate(jobDetail.created_at)}</span
|
||||
@@ -983,7 +1002,7 @@
|
||||
</div>
|
||||
{#if jobDetail.meta && Object.keys(jobDetail.meta).length > 0}
|
||||
<div class="mt-4 space-y-2">
|
||||
<p class="text-sm font-medium text-muted-foreground">Meta</p>
|
||||
<p class="text-sm font-medium text-muted-foreground">Метаданные</p>
|
||||
<ScrollPreBlock
|
||||
variant="wrap"
|
||||
text={JSON.stringify(jobDetail.meta, null, 2)}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
} from '$lib/components/ui/table/index.js';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
||||
import CalendarClock from '@lucide/svelte/icons/calendar-clock';
|
||||
import { jobKindFilterRu, jobStatusRu, moduleTypeRu } from '$lib/ui-labels.js';
|
||||
|
||||
let modules = $state<ModuleRow[]>([]);
|
||||
let jobs = $state<JobRow[]>([]);
|
||||
@@ -47,7 +49,7 @@
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
if (res.status === 204) toast.message('Refresh не требуется (IP_RANGES)');
|
||||
if (res.status === 204) toast.message('Обновление не требуется (тип IP_RANGES)');
|
||||
else if (res.status === 202) { toast.success('Задача поставлена в очередь'); await load(); }
|
||||
else toast.error(`HTTP ${res.status}`);
|
||||
} catch (e) {
|
||||
@@ -77,10 +79,18 @@
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Расписание и задачи</h1>
|
||||
<p class="text-muted-foreground mt-1 text-sm">Интервалы обновления модулей и ручной запуск refresh.</p>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex min-w-0 items-start gap-3">
|
||||
<div
|
||||
class="bg-chart-4/15 text-chart-4 flex size-11 shrink-0 items-center justify-center rounded-xl"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<CalendarClock class="size-6" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Расписание и задачи</h1>
|
||||
<p class="text-muted-foreground mt-1 text-sm">Интервалы обновления модулей и ручной запуск обновления.</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onclick={load} disabled={loading}>
|
||||
<RefreshCw class={loading ? 'animate-spin' : ''} />
|
||||
@@ -106,7 +116,9 @@
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">Модули</CardTitle>
|
||||
<CardDescription>Запустить refresh вручную (CDN/домены/AS → очередь; IP_RANGES → 204)</CardDescription>
|
||||
<CardDescription
|
||||
>Запустить обновление вручную (CDN, домены, AS — в очередь; для IP_RANGES ответ 204)</CardDescription
|
||||
>
|
||||
</CardHeader>
|
||||
<CardContent class="p-0">
|
||||
<Table>
|
||||
@@ -123,13 +135,13 @@
|
||||
{#each modules as m (m.id)}
|
||||
<TableRow>
|
||||
<TableCell class="font-medium">{m.name}</TableCell>
|
||||
<TableCell><Badge variant="outline">{m.type}</Badge></TableCell>
|
||||
<TableCell><Badge variant="outline">{moduleTypeRu(m.type)}</Badge></TableCell>
|
||||
<TableCell>{intervalLabel(m.refresh_interval_sec)}</TableCell>
|
||||
<TableCell class="font-mono text-xs">{m.cron_expr || '—'}</TableCell>
|
||||
<TableCell class="text-right">
|
||||
<Button size="xs" variant="secondary" disabled={!!refreshing[m.id]} onclick={() => refreshModule(m.id)}>
|
||||
<RefreshCw class={refreshing[m.id] ? 'animate-spin' : ''} />
|
||||
{refreshing[m.id] ? '…' : 'Refresh'}
|
||||
{refreshing[m.id] ? '…' : 'Обновить'}
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -163,8 +175,8 @@
|
||||
<TableBody>
|
||||
{#each jobs as j (j.job_id)}
|
||||
<TableRow>
|
||||
<TableCell class="font-medium">{j.kind}</TableCell>
|
||||
<TableCell><Badge variant={jobStatusVariant(j.status)}>{j.status}</Badge></TableCell>
|
||||
<TableCell class="font-medium">{jobKindFilterRu(j.kind)}</TableCell>
|
||||
<TableCell><Badge variant={jobStatusVariant(j.status)}>{jobStatusRu(j.status)}</Badge></TableCell>
|
||||
<TableCell class="text-xs text-muted-foreground">{j.created_at ? new Date(j.created_at).toLocaleString('ru') : '—'}</TableCell>
|
||||
<TableCell class="text-xs text-destructive max-w-xs truncate">{j.error ?? ''}</TableCell>
|
||||
</TableRow>
|
||||
@@ -183,7 +195,7 @@
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">Операции обновления модулей</CardTitle>
|
||||
<CardDescription>Отдельная лента задач `module_refresh` для контроля по модулям.</CardDescription>
|
||||
<CardDescription>Отдельная лента задач обновления модулей для контроля по модулям.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="p-0">
|
||||
<Table>
|
||||
@@ -191,19 +203,21 @@
|
||||
<TableRow>
|
||||
<TableHead>Статус</TableHead>
|
||||
<TableHead>Создана</TableHead>
|
||||
<TableHead>revision_id</TableHead>
|
||||
<TableHead>Ревизия</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each refreshJobs as j (j.job_id)}
|
||||
<TableRow>
|
||||
<TableCell><Badge variant={jobStatusVariant(j.status)}>{j.status}</Badge></TableCell>
|
||||
<TableCell><Badge variant={jobStatusVariant(j.status)}>{jobStatusRu(j.status)}</Badge></TableCell>
|
||||
<TableCell class="text-xs">{j.created_at ? new Date(j.created_at).toLocaleString('ru') : '—'}</TableCell>
|
||||
<TableCell class="font-mono text-xs">{typeof j.meta?.revision_id === 'string' ? `${j.meta.revision_id.slice(0, 12)}…` : '—'}</TableCell>
|
||||
</TableRow>
|
||||
{:else}
|
||||
<TableRow>
|
||||
<TableCell colspan={3} class="text-muted-foreground text-center py-6">Нет задач module_refresh</TableCell>
|
||||
<TableCell colspan={3} class="text-muted-foreground text-center py-6"
|
||||
>Нет задач обновления модулей</TableCell
|
||||
>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { Textarea } from '$lib/components/ui/textarea/index.js';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Save from '@lucide/svelte/icons/save';
|
||||
import SettingsIcon from '@lucide/svelte/icons/settings';
|
||||
|
||||
let token = $state('');
|
||||
let apiSettings = $state<AppSettings | null>(null);
|
||||
@@ -63,9 +64,17 @@
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-2xl space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Настройки</h1>
|
||||
<p class="text-muted-foreground mt-1 text-sm">Токен доступа и параметры API.</p>
|
||||
<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"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<SettingsIcon class="size-6" />
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token -->
|
||||
|
||||
Reference in New Issue
Block a user