import { ArrowLeftOutlined } from '@ant-design/icons' import { useQueryClient } from '@tanstack/react-query' import { Alert, Button, Card, Col, 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 statusColor(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 healthColor(score: number | null | undefined): string { if (score == null) return 'default' if (score >= 85) return 'green' if (score >= 65) return 'orange' return 'red' } function formatAge(seconds: number | null | undefined): string { if (seconds == null) return '-' if (seconds < 60) return `${seconds}с` if (seconds < 3600) return `${Math.floor(seconds / 60)}м` return `${Math.floor(seconds / 3600)}ч ${Math.floor((seconds % 3600) / 60)}м` } 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) => (
))}
{dayjs(points[0].bucket_start).format('DD.MM HH:mm')} {dayjs(points[points.length - 1].bucket_end).format('DD.MM HH:mm')} {!compact ? ( online offline нет данных ) : null}
) } 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 { 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 let warn = 0 const byRack = new Map() for (const row of rows) { if (row.ping_status === 'online') online += 1 if (row.disk_status === 'warn' || row.disk_status === 'failed') { warn += 1 } const rackName = row.rack_name && row.rack_name.trim() ? row.rack_name : 'Без стойки' if (!byRack.has(rackName)) byRack.set(rackName, []) byRack.get(rackName)?.push(row) } const rackGroups = Array.from(byRack.entries()) .sort((a, b) => { if (a[0] === 'Без стойки') return 1 if (b[0] === 'Без стойки') return -1 return a[0].localeCompare(b[0]) }) .map(([rack, rowsInRack]) => ({ rack, rows: [...rowsInRack].sort((a, b) => { const aProblem = a.has_problem ? 0 : 1 const bProblem = b.has_problem ? 0 : 1 if (aProblem !== bProblem) return aProblem - bProblem if (a.ping_status !== b.ping_status) { if (a.ping_status === 'offline') return -1 if (b.ping_status === 'offline') return 1 } return a.ip.localeCompare(b.ip) }), })) return { online, warn, rackGroups } }, [devices]) const columns: ColumnsType = [ { title: 'IP / Hostname', key: 'identity', render: (_, row) => ( {row.ip} {row.hostname ?? '—'} ), }, { title: 'Категория', dataIndex: 'category', key: 'category', width: 130, }, { title: 'Ping', key: 'ping', width: 190, render: (_, row) => ( {row.ping_status} {row.ping_checked_at ? dayjs(row.ping_checked_at).format('DD.MM HH:mm:ss') : '—'} {row.last_ping_ms != null ? ` • ${row.last_ping_ms}ms` : ''} ), }, { title: 'Uptime', key: 'uptime', width: 280, render: (_, row) => { const availability = availabilityByDevice.get(Number(row.device_id)) if (availabilityLoading) { return Загрузка... } if (!availability?.points?.length) { return Нет данных } return ( {availability.availability != null ? `${availability.availability}%` : '—'} ) }, }, { title: 'Свежесть', key: 'freshness', width: 110, render: (_, row) => {formatAge(row.ping_age_sec)}, }, { title: 'Disk', key: 'disk', width: 140, render: (_, row) => row.disk_status ? ( {row.disk_status} {row.disk_max_pct != null ? ` (${row.disk_max_pct}%)` : ''} ) : ( '—' ), }, { title: 'Проблема', key: 'problem', width: 100, render: (_, row) => (row.has_problem ? Да : Нет), }, ] 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} Проблемных {summary?.totals.problem_devices ?? 0} Online {grouped.online} Disk warn/failed {grouped.warn} setWindow(v as DashboardAvailabilityWindow)} options={[ { value: '24h', label: '24ч' }, { value: '3d', label: '3д' }, { value: '7d', label: '7д' }, ]} style={{ width: 120 }} /> } > {!grouped.rackGroups.length ? ( ) : ( {grouped.rackGroups.map((rackGroup) => ( loading={devicesLoading} dataSource={rackGroup.rows} rowKey="device_id" size="small" pagination={false} scroll={{ x: 1300 }} columns={columns} /> ))} )}
) }