fix(web): unify ReUI chrome and stop tab underline overlap
quality / commitlint (push) Skipped
quality / changes (push) Successful in 8s
quality / go (push) Skipped
quality / bird2 (push) Skipped
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 22s
quality / web (push) Successful in 1m0s
CD / quality (push) Successful in 1m35s
CD / publish (push) Successful in 2m59s

Секции страниц переведены на segmented Tabs (c-tabs-9), статус грида — ToggleGroup в toolbar. Убраны календарь задач, пустые вкладки мониторинга и demo-деревья ReUI. KPI overflow как в CFDM, DetailPanel на карточке модуля, один FrameSection.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-23 12:46:20 +07:00
co-authored by Cursor
parent 764a4e5b4e
commit 094f75913b
114 changed files with 594 additions and 16355 deletions
+67 -189
View File
@@ -1,22 +1,15 @@
import { createFileRoute, useSearch } from '@tanstack/react-router'
import { createFileRoute } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
import { AlertTriangle, Bird, Database, FileText, RefreshCw } from 'lucide-react'
import { Bird, RefreshCw } from 'lucide-react'
import { Button } from '@evobgp/ui/components/button'
import { PanelCard } from '@/components/panel-card'
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
import { StatusBadge } from '@/components/status-badge'
import { jobKindRu } from '@/lib/ui-labels'
import {
DashboardOperationsFlowCard,
MonitoringHealthCard,
} from '@/components/analytics'
import { MonitoringHealthCard } from '@/components/analytics'
import { MonitoringReadyGrid } from '@/components/monitoring/monitoring-ready-grid'
import { PageHeader } from '@/components/page-header'
import { QueryState } from '@/components/query-state'
import { IllustratedEmptyState } from '@/components/patterns/illustrated-empty-state'
import { SegmentedProgressCard } from '@/components/patterns/segmented-progress-card'
import { AnalyticsDashboardSkeleton } from '@/components/skeletons'
import { FrameSection } from '@/components/reui-kit'
import { StatusBadge } from '@/components/status-badge'
import { jobKindRu } from '@/lib/ui-labels'
import {
monitoringHealthQueryOptions,
@@ -26,45 +19,27 @@ import {
} from '@/queries/monitoring'
import { networkBirdQueryOptions } from '@/queries/network'
import { operationsJobsQueryOptions } from '@/queries/operations'
import { overviewModulesQueryOptions } from '@/queries/overview'
export const Route = createFileRoute('/_auth/monitoring')({
component: MonitoringComponent,
validateSearch: (search: Record<string, unknown>) => ({
tab: (search.tab === 'postgres' || search.tab === 'runtime-logs' ? search.tab : 'system') as
| 'system'
| 'postgres'
| 'runtime-logs',
}),
})
function MonitoringComponent() {
const search = useSearch({ from: '/_auth/monitoring' })
const navigate = Route.useNavigate()
const healthQ = useQuery(monitoringHealthQueryOptions())
const readyQ = useQuery(monitoringReadyQueryOptions())
const versionQ = useQuery(monitoringVersionQueryOptions())
const birdQ = useQuery(networkBirdQueryOptions())
const jobsQ = useQuery(operationsJobsQueryOptions())
const modulesQ = useQuery(overviewModulesQueryOptions())
const refreshing =
healthQ.isFetching ||
readyQ.isFetching ||
versionQ.isFetching ||
birdQ.isFetching ||
jobsQ.isFetching ||
modulesQ.isFetching
jobsQ.isFetching
const jobs = jobsQ.data?.items ?? []
const modules = modulesQ.data?.items ?? []
const failed = jobs.filter((j) =>
['failed', 'error', 'cancelled'].includes(j.status.toLowerCase()),
).length
const versionText = formatVersion(versionQ.data)
const analyticsLoading =
(healthQ.isLoading || readyQ.isLoading || jobsQ.isLoading) && jobs.length === 0
function refetchAll() {
void healthQ.refetch()
@@ -72,7 +47,6 @@ function MonitoringComponent() {
void versionQ.refetch()
void birdQ.refetch()
void jobsQ.refetch()
void modulesQ.refetch()
}
const failedJobs = jobs
@@ -80,7 +54,7 @@ function MonitoringComponent() {
.slice(0, 5)
return (
<div className="flex flex-col gap-2 md:gap-3">
<div className="flex flex-col gap-4 md:gap-6">
<PageHeader
title="Мониторинг"
description={`Состояние API, BGP и задач для диагностики инцидентов${
@@ -98,163 +72,67 @@ function MonitoringComponent() {
}
/>
<BadgeTabs
value={search.tab}
onValueChange={(tab) =>
navigate({ search: { tab: tab as 'system' | 'postgres' | 'runtime-logs' } })
}
items={[
{ value: 'system', label: 'Система' },
{ value: 'postgres', label: 'PostgreSQL' },
{ value: 'runtime-logs', label: 'Файловые логи' },
]}
<div className="grid items-stretch gap-4 md:gap-6 lg:grid-cols-2">
<MonitoringHealthCard
healthOk={healthQ.data?.ok ?? false}
ready={readyQ.data}
loading={healthQ.isLoading || readyQ.isLoading}
/>
<FrameSection
title={
<span className="flex items-center gap-2">
<Bird className="size-4" />
BGP на API-хосте
</span>
}
description="Статус BIRD на хосте API"
className="h-full"
contentClassName="px-5 py-4"
>
<QueryState
data={birdQ.data}
isLoading={birdQ.isLoading}
isError={birdQ.isError}
error={birdQ.error}
skeleton={<div className="h-40" />}
onRetry={() => birdQ.refetch()}
>
{(bird) => <BirdSummary bird={bird} />}
</QueryState>
</FrameSection>
</div>
<QueryState
data={readyQ.data}
isLoading={readyQ.isLoading}
isError={readyQ.isError}
error={readyQ.error}
skeleton={<div className="h-40" />}
onRetry={() => readyQ.refetch()}
>
<TabsContent value="system" className="mt-0 flex flex-col gap-2 md:gap-3">
{analyticsLoading ? (
<AnalyticsDashboardSkeleton />
) : (
<div className="grid items-stretch gap-2 md:gap-3 lg:grid-cols-2">
<MonitoringHealthCard
healthOk={healthQ.data?.ok ?? false}
ready={readyQ.data}
loading={healthQ.isLoading || readyQ.isLoading}
/>
<DashboardOperationsFlowCard jobs={jobs} modules={modules} loading={jobsQ.isLoading} />
</div>
)}
{(ready) => <MonitoringReadyGrid health={healthQ.data} ready={ready} />}
</QueryState>
<div className="grid items-stretch gap-2 md:gap-3 lg:grid-cols-2">
<QueryState
data={readyQ.data}
isLoading={readyQ.isLoading}
isError={readyQ.isError}
error={readyQ.error}
skeleton={<div className="h-40" />}
onRetry={() => readyQ.refetch()}
>
{(ready) => <MonitoringReadyGrid health={healthQ.data} ready={ready} />}
</QueryState>
<PanelCard
title={
<span className="flex items-center gap-2">
<Bird className="size-4" />
BGP на API-хосте
</span>
}
description="Статус BIRD на хосте API"
className="h-full"
contentClassName="px-5 py-4"
>
<QueryState
data={birdQ.data}
isLoading={birdQ.isLoading}
isError={birdQ.isError}
error={birdQ.error}
skeleton={<div className="h-40" />}
onRetry={() => birdQ.refetch()}
>
{(bird) => <BirdSummary bird={bird} />}
</QueryState>
</PanelCard>
</div>
<div className="grid items-stretch gap-2 md:gap-3 lg:grid-cols-2">
<div className="flex flex-col gap-2 md:gap-3">
<SegmentedProgressCard
title="Задачи"
description="Последние 100 задач"
primary={{
value: jobs.filter((j) => j.status === 'running' || j.status === 'queued').length,
label: 'Активных',
percent:
jobs.length > 0
? Math.round(
(jobs.filter((j) => j.status === 'running' || j.status === 'queued').length /
jobs.length) *
100,
)
: 0,
}}
secondary={{
value: failed,
label: 'С ошибками',
percent: jobs.length > 0 ? Math.round((failed / jobs.length) * 100) : 0,
}}
footer={`В выборке: ${jobs.length} задач`}
/>
{failedJobs.length > 0 ? (
<PanelCard title="Последние ошибки" contentClassName="px-5 py-4">
<ul className="flex flex-col gap-2">
{failedJobs.map((job) => (
<li key={job.job_id} className="rounded-lg border px-3 py-2 text-sm">
<div className="flex items-start justify-between gap-2">
<p className="font-medium">{jobKindRu(job.kind)}</p>
<StatusBadge status={job.status} />
</div>
{job.error ? (
<p className="mt-1 text-xs text-muted-foreground">
{job.error.slice(0, 120)}
{job.error.length > 120 ? '…' : ''}
</p>
) : null}
</li>
))}
</ul>
</PanelCard>
) : null}
</div>
<PanelCard
title={
<span className="flex items-center gap-2">
<AlertTriangle className="size-4 text-muted-foreground" />
Что проверять при деградации
</span>
}
description="Краткая шпаргалка для первичной диагностики"
className="h-full"
contentClassName="px-5 py-4"
>
<ul className="flex flex-col gap-3 text-sm text-muted-foreground">
<li>
<span className="font-medium text-foreground">API недоступен.</span> Если{' '}
<code className="text-xs">/v1/health</code> возвращает ошибку — проверьте процесс API и
его логи.
</li>
<li>
<span className="font-medium text-foreground">Готовность не «Готов».</span> Сначала{' '}
<code className="text-xs">PostgreSQL</code>, затем хранилище и очередь задач в проверках.
</li>
<li>
<span className="font-medium text-foreground">Низкая доля установленных BGP-сессий.</span> Проверьте{' '}
<code className="text-xs">/v1/bird/status</code>, затем состояние пиров в Сети.
</li>
<li>
<span className="font-medium text-foreground">Ошибки задач.</span> Откройте Операции и
проверьте последние неуспешные задачи.
</li>
</ul>
</PanelCard>
</div>
</TabsContent>
<TabsContent value="postgres" className="mt-0">
<IllustratedEmptyState
icon={Database}
title="PostgreSQL"
description="Состояние соединения и пула видно в проверке готовности на вкладке «Система»."
/>
</TabsContent>
<TabsContent value="runtime-logs" className="mt-0">
<IllustratedEmptyState
icon={FileText}
title="Файловые логи"
description="Логи API и конвейера задаются переменной EVOBGP_LOG_* и управляются в Настройках BIRD."
/>
</TabsContent>
</BadgeTabs>
{failedJobs.length > 0 ? (
<FrameSection title="Последние ошибки" contentClassName="px-5 py-4">
<ul className="flex flex-col gap-2">
{failedJobs.map((job) => (
<li key={job.job_id} className="rounded-lg border px-3 py-2 text-sm">
<div className="flex items-start justify-between gap-2">
<p className="font-medium">{jobKindRu(job.kind)}</p>
<StatusBadge status={job.status} />
</div>
{job.error ? (
<p className="mt-1 text-xs text-muted-foreground">
{job.error.slice(0, 120)}
{job.error.length > 120 ? '…' : ''}
</p>
) : null}
</li>
))}
</ul>
</FrameSection>
) : null}
</div>
)
}