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
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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user