quality / commitlint (push) Skipped
quality / changes (push) Successful in 9s
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 46s
quality / web (push) Successful in 1m16s
quality / go (push) Successful in 2m42s
quality / bird2 (push) Successful in 16s
CD / quality (push) Successful in 5m19s
CD / publish (push) Successful in 7m19s
- Deleted unused components: `DashboardActivityTimeline`, `DashboardFramePanel`, `DashboardModulesGrid`, `DashboardRecentJobsGrid`, and `DashboardRecentRevisionsGrid` to streamline the dashboard. - Updated `DashboardKpiGrid` to improve KPI display logic, including progress indicators and enhanced badge functionality. - Refactored `DashboardNetworkHealth` to provide better status representation based on loading states and network conditions. - Introduced new properties for KPI cards to support progress tracking and improved visual feedback. This cleanup aims to enhance performance and maintainability of the dashboard while providing a better user experience.
174 lines
4.7 KiB
TypeScript
174 lines
4.7 KiB
TypeScript
import {
|
|
AlertTriangle,
|
|
Boxes,
|
|
ListChecks,
|
|
Network,
|
|
} from 'lucide-react'
|
|
import type { ReactNode } from 'react'
|
|
|
|
import { KpiStatGrid, type KpiStatItem } from '@/components/reui-kit'
|
|
import { Badge } from '@/components/reui/badge'
|
|
|
|
import { aggregateNetworkMetrics, runningJobCount } from '@/queries/overview'
|
|
import type { JobRow, ModuleRow, PeerRow, SpeakerRow } from '@/types/api'
|
|
|
|
type KpiCard = KpiStatItem & { icon: ReactNode }
|
|
|
|
function ratioPercent(part: number, total: number): number | undefined {
|
|
if (total <= 0) return undefined
|
|
return Math.round((part / total) * 100)
|
|
}
|
|
|
|
function buildKpis({
|
|
modules,
|
|
peers,
|
|
speakers,
|
|
jobs,
|
|
loading,
|
|
}: {
|
|
modules: ModuleRow[]
|
|
peers: PeerRow[]
|
|
speakers: SpeakerRow[]
|
|
jobs: JobRow[]
|
|
loading?: boolean
|
|
}): KpiCard[] {
|
|
const enabledModules = modules.filter((m) => m.enabled !== false).length
|
|
const network = aggregateNetworkMetrics(peers, speakers)
|
|
const peersEnabled = network.peersEnabled
|
|
const bgpPct =
|
|
peersEnabled > 0 ? Math.round((network.peersEstablished / peersEnabled) * 100) : null
|
|
const running = runningJobCount(jobs)
|
|
const failedJobs = jobs.filter((j) =>
|
|
['failed', 'error', 'cancelled'].includes(j.status.toLowerCase()),
|
|
).length
|
|
const offlineSpeakers = Math.max(0, speakers.length - network.speakersOnline)
|
|
const riskCount = network.peersMismatch + failedJobs + offlineSpeakers
|
|
const disabledModules = Math.max(0, modules.length - enabledModules)
|
|
|
|
return [
|
|
{
|
|
id: 'modules',
|
|
icon: <Boxes aria-hidden />,
|
|
iconClassName: 'text-primary',
|
|
value: loading ? '—' : enabledModules,
|
|
label: 'Модули активны',
|
|
progress: loading ? undefined : ratioPercent(enabledModules, modules.length),
|
|
footer: (
|
|
<Badge
|
|
variant={
|
|
loading || modules.length === 0
|
|
? 'outline'
|
|
: disabledModules === 0
|
|
? 'success-light'
|
|
: 'warning-light'
|
|
}
|
|
size="sm"
|
|
>
|
|
{loading
|
|
? '…'
|
|
: modules.length === 0
|
|
? 'нет модулей'
|
|
: disabledModules === 0
|
|
? 'все активны'
|
|
: `${disabledModules} выкл`}
|
|
</Badge>
|
|
),
|
|
},
|
|
{
|
|
id: 'bgp',
|
|
icon: <Network aria-hidden />,
|
|
iconClassName: 'text-info',
|
|
value: loading || bgpPct === null ? '—' : `${bgpPct}%`,
|
|
label: 'BGP готовность',
|
|
progress: loading || bgpPct === null ? undefined : bgpPct,
|
|
footer: (
|
|
<Badge
|
|
variant={
|
|
bgpPct !== null && bgpPct >= 90
|
|
? 'success-light'
|
|
: bgpPct !== null && bgpPct < 70
|
|
? 'warning-light'
|
|
: 'outline'
|
|
}
|
|
size="sm"
|
|
>
|
|
{loading || bgpPct === null
|
|
? 'нет включённых пиров'
|
|
: bgpPct >= 90
|
|
? 'сессии в норме'
|
|
: `${network.peersEstablished} установлено`}
|
|
</Badge>
|
|
),
|
|
},
|
|
{
|
|
id: 'jobs',
|
|
icon: <ListChecks aria-hidden />,
|
|
iconClassName: 'text-focus',
|
|
value: loading ? '—' : String(running),
|
|
label: 'Активные задачи',
|
|
footer: (
|
|
<Badge variant={running > 0 ? 'info-light' : 'outline'} size="sm">
|
|
{loading ? '…' : running > 0 ? 'выполняются' : 'очередь пуста'}
|
|
</Badge>
|
|
),
|
|
},
|
|
{
|
|
id: 'risks',
|
|
icon: <AlertTriangle aria-hidden />,
|
|
iconClassName: 'text-destructive',
|
|
value: loading ? '—' : String(riskCount),
|
|
label: 'Риски',
|
|
variant: riskCount > 0 ? 'destructive' : 'default',
|
|
footer: (
|
|
<Badge variant={riskCount > 0 ? 'destructive-light' : 'success-light'} size="sm">
|
|
{loading
|
|
? '…'
|
|
: riskCount > 0
|
|
? `${failedJobs} задач · ${network.peersMismatch} расхождений`
|
|
: 'в норме'}
|
|
</Badge>
|
|
),
|
|
},
|
|
]
|
|
}
|
|
|
|
export function buildDashboardKpiCards({
|
|
modules,
|
|
peers,
|
|
speakers,
|
|
jobs,
|
|
loading,
|
|
}: {
|
|
modules: ModuleRow[]
|
|
peers: PeerRow[]
|
|
speakers: SpeakerRow[]
|
|
jobs: JobRow[]
|
|
loading?: boolean
|
|
}) {
|
|
return buildKpis({ modules, peers, speakers, jobs, loading }).map((card) => ({
|
|
...card,
|
|
id: card.id ?? String(card.label),
|
|
}))
|
|
}
|
|
|
|
export function DashboardKpiGrid({
|
|
modules,
|
|
peers,
|
|
speakers,
|
|
jobs,
|
|
loading,
|
|
}: {
|
|
modules: ModuleRow[]
|
|
peers: PeerRow[]
|
|
speakers: SpeakerRow[]
|
|
jobs: JobRow[]
|
|
loading?: boolean
|
|
}) {
|
|
return (
|
|
<KpiStatGrid
|
|
items={buildKpis({ modules, peers, speakers, jobs, loading })}
|
|
aria-label="KPI обзора"
|
|
/>
|
|
)
|
|
}
|