import { ArrowLeftOutlined } from '@ant-design/icons' import { useQueryClient } from '@tanstack/react-query' import { Alert, Button, Card, Col, Collapse, Empty, Row, Select, Space, Table, Tag, Typography } from 'antd' import type { ColumnsType } from 'antd/es/table' import dayjs from 'dayjs' import { useMemo, useState } from 'react' import { useNavigate, useParams, useSearchParams } from 'react-router-dom' import { useDashboardObjectAvailability, useDashboardObjectDevices, useDashboardObjectSummary, type DashboardAvailabilityPoint, type DashboardAvailabilityWindow, type DashboardDeviceNOCItem, type DashboardScope, } from '../api/dashboard' import { useDashboardMonitorSSE } from '../hooks/useDashboardMonitorSSE' function reasonLabel(reason: string): string { const map: Record = { object_include_policy: 'Включен политикой объекта', object_exclude_policy: 'Исключен политикой объекта', inherit_default: 'Наследование по умолчанию', tag_policy_enabled: 'Включен политикой тегов', tag_policy_disabled: 'Исключен политикой тегов', scope_filtered: 'Вне текущего фильтра', } return map[reason] ?? reason } function parseScope(params: URLSearchParams): DashboardScope { const city = params.get('city') ?? undefined const client = params.get('client') ?? undefined const object_ids = params.getAll('object_ids').map(Number).filter((v) => Number.isFinite(v)) const tag_ids = params.getAll('tag_ids').map(Number).filter((v) => Number.isFinite(v)) return { city, client, object_ids, tag_ids } } function pingStatusColor(status: string | null | undefined): string { if (status === 'online') return 'green' if (status === 'offline') return 'red' if (status === 'warn' || status === 'failed') return 'orange' return 'default' } function diskStatusColor(status: string | null | undefined): string { if (status === 'ok') return 'green' if (status === 'warn') return 'orange' if (status === 'failed') return 'red' return 'default' } function healthColor(score: number | null | undefined): string { if (score == null) return 'default' if (score >= 85) return 'green' if (score >= 65) return 'orange' return 'red' } function uptimeColor(point: DashboardAvailabilityPoint): string { if (point.samples === 0) return '#d9d9d9' if (point.offline > 0) return '#ff4d4f' return '#52c41a' } function UptimeBars({ points, compact = false }: { points: DashboardAvailabilityPoint[]; compact?: boolean }) { if (!points.length) { return Нет точек в выбранном окне } return (
{points.map((point, idx) => (
))}
) } type PingFilter = 'all' | 'offline' | 'unknown' | 'online' const PING_FILTER_OPTIONS: { value: PingFilter; label: string; color?: string }[] = [ { value: 'all', label: 'Все' }, { value: 'offline', label: 'Offline', color: '#ff4d4f' }, { value: 'unknown', label: 'Unknown', color: '#fa8c16' }, { value: 'online', label: 'Online', color: '#52c41a' }, ] export function HomeObjectDetailPage() { const navigate = useNavigate() const qc = useQueryClient() const { id } = useParams<{ id: string }>() const objectId = Number(id) const [search] = useSearchParams() const scope = useMemo(() => parseScope(search), [search]) const [window, setWindow] = useState('24h') const [pingFilter, setPingFilter] = useState('all') const { data: summary, isLoading: summaryLoading } = useDashboardObjectSummary(objectId, scope) const { data: devices, isLoading: devicesLoading } = useDashboardObjectDevices(objectId, scope) const { data: availabilityData, isLoading: availabilityLoading } = useDashboardObjectAvailability(objectId, window, undefined, true) useDashboardMonitorSSE({ onUpdate: () => { qc.invalidateQueries({ queryKey: ['dashboard-object-summary', objectId] }) qc.invalidateQueries({ queryKey: ['dashboard-object-devices', objectId] }) qc.invalidateQueries({ queryKey: ['dashboard-object-availability', objectId] }) }, }) const object = summary?.object const availabilityByDevice = useMemo(() => { const map = new Map() for (const row of availabilityData?.devices ?? []) { map.set(row.device_id, { points: row.series ?? [], availability: row.availability_pct }) } return map }, [availabilityData]) const grouped = useMemo(() => { const rows = devices?.devices ?? [] let online = 0, offline = 0, unknown = 0 const byRack = new Map() for (const row of rows) { if (row.ping_status === 'online') online++ if (row.ping_status === 'offline') offline++ if (row.ping_status === 'unknown') unknown++ const rackName = row.rack_name?.trim() || 'Без стойки' if (!byRack.has(rackName)) byRack.set(rackName, []) byRack.get(rackName)!.push(row) } const rackGroups = Array.from(byRack.entries()) .sort(([a], [b]) => { if (a === 'Без стойки') return -1 if (b === 'Без стойки') return 1 return a.localeCompare(b) }) .map(([rack, rowsInRack]) => ({ rack, rows: [...rowsInRack].sort((a, b) => { const order = { offline: 0, unknown: 1, online: 2 } const ao = order[a.ping_status as keyof typeof order] ?? 3 const bo = order[b.ping_status as keyof typeof order] ?? 3 if (ao !== bo) return ao - bo return a.ip.localeCompare(b.ip) }), })) return { online, offline, unknown, rackGroups } }, [devices]) // Применяем фильтр по ping статусу к каждой стойке const filteredRackGroups = useMemo(() => { if (pingFilter === 'all') return grouped.rackGroups return grouped.rackGroups .map((rg) => ({ ...rg, rows: rg.rows.filter((r) => r.ping_status === pingFilter) })) .filter((rg) => rg.rows.length > 0) }, [grouped.rackGroups, pingFilter]) const columns: ColumnsType = [ { title: 'IP / Hostname', key: 'identity', width: 190, render: (_, row) => ( {row.ip} {row.hostname ?? '—'} ), }, { title: 'Категория', dataIndex: 'category', key: 'category', width: 110, ellipsis: true, }, { title: 'Роль', key: 'role', width: 80, render: (_, row) => row.role ?? '—', }, { title: 'Ping', key: 'ping', width: 130, render: (_, row) => ( {row.ping_status} {row.last_ping_ms != null ? `${row.last_ping_ms}ms` : '—'} ), }, { title: 'Disk', key: 'disk', width: 90, render: (_, row) => { if (!row.disk_status) return return ( {row.disk_status} {row.disk_max_pct != null && ( {row.disk_max_pct}% )} ) }, }, { title: 'Uptime', key: 'uptime', width: 200, render: (_, row) => { const availability = availabilityByDevice.get(Number(row.device_id)) if (availabilityLoading) return Загрузка... if (!availability?.points?.length) return Нет данных return ( {availability.availability != null ? `${availability.availability}%` : '—'} ) }, }, ] return (
{object?.object_name ?? `Объект #${objectId}`} {object?.allowed ? В мониторинге : Вне мониторинга} health {object?.health_score ?? '-'} окно {window} {object ? ( {reasonLabel(object.inclusion_reason)} ) : null} {availabilityData?.insufficient_data ? ( ) : null} {/* Счётчики */} Устройств {summary?.totals.devices ?? 0} setPingFilter(pingFilter === 'unknown' ? 'all' : 'unknown')} > Unknown 0 ? '#fa8c16' : undefined }}> {grouped.unknown} setPingFilter(pingFilter === 'offline' ? 'all' : 'offline')} > Offline {grouped.offline} setPingFilter(pingFilter === 'online' ? 'all' : 'online')} > Online {grouped.online} Устройства объекта {pingFilter !== 'all' && ( o.value === pingFilter)?.color ? undefined : undefined} style={{ background: PING_FILTER_OPTIONS.find((o) => o.value === pingFilter)?.color, color: '#fff', border: 'none' }} closable onClose={() => setPingFilter('all')} > {PING_FILTER_OPTIONS.find((o) => o.value === pingFilter)?.label} )} } size="small" style={{ borderRadius: 12 }} extra={ {PING_FILTER_OPTIONS.map((opt) => ( ))}