фикс карточек
This commit is contained in:
parent
f58a6f13f9
commit
484a07afc3
1 changed files with 43 additions and 90 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
import { ArrowLeftOutlined } from '@ant-design/icons'
|
import { ArrowLeftOutlined } from '@ant-design/icons'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { Alert, Button, Card, Col, Empty, Row, Select, Space, Table, Tag, Typography } from 'antd'
|
import { Alert, Button, Card, Col, Collapse, Empty, Row, Select, Space, Table, Tag, Typography } from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
|
|
@ -56,13 +56,6 @@ function healthColor(score: number | null | undefined): string {
|
||||||
return 'red'
|
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 {
|
function uptimeColor(point: DashboardAvailabilityPoint): string {
|
||||||
if (point.samples === 0) return '#d9d9d9'
|
if (point.samples === 0) return '#d9d9d9'
|
||||||
if (point.offline > 0) return '#ff4d4f'
|
if (point.offline > 0) return '#ff4d4f'
|
||||||
|
|
@ -92,21 +85,6 @@ function UptimeBars({ points, compact = false }: { points: DashboardAvailability
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Row justify="space-between" style={{ marginTop: compact ? 4 : 6 }}>
|
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
|
||||||
{dayjs(points[0].bucket_start).format('DD.MM HH:mm')}
|
|
||||||
</Typography.Text>
|
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
|
||||||
{dayjs(points[points.length - 1].bucket_end).format('DD.MM HH:mm')}
|
|
||||||
</Typography.Text>
|
|
||||||
</Row>
|
|
||||||
{!compact ? (
|
|
||||||
<Space size={12} style={{ marginTop: 6 }} wrap>
|
|
||||||
<Space size={4}><span style={{ width: 10, height: 10, borderRadius: 2, background: '#52c41a', display: 'inline-block' }} /><Typography.Text type="secondary" style={{ fontSize: 12 }}>online</Typography.Text></Space>
|
|
||||||
<Space size={4}><span style={{ width: 10, height: 10, borderRadius: 2, background: '#ff4d4f', display: 'inline-block' }} /><Typography.Text type="secondary" style={{ fontSize: 12 }}>offline</Typography.Text></Space>
|
|
||||||
<Space size={4}><span style={{ width: 10, height: 10, borderRadius: 2, background: '#d9d9d9', display: 'inline-block' }} /><Typography.Text type="secondary" style={{ fontSize: 12 }}>нет данных</Typography.Text></Space>
|
|
||||||
</Space>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -148,15 +126,14 @@ export function HomeObjectDetailPage() {
|
||||||
const grouped = useMemo(() => {
|
const grouped = useMemo(() => {
|
||||||
const rows = devices?.devices ?? []
|
const rows = devices?.devices ?? []
|
||||||
let online = 0
|
let online = 0
|
||||||
let warn = 0
|
let offline = 0
|
||||||
|
let unknown = 0
|
||||||
const byRack = new Map<string, DashboardDeviceNOCItem[]>()
|
const byRack = new Map<string, DashboardDeviceNOCItem[]>()
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
if (row.ping_status === 'online') online += 1
|
if (row.ping_status === 'online') online += 1
|
||||||
|
if (row.ping_status === 'offline') offline += 1
|
||||||
if (row.disk_status === 'warn' || row.disk_status === 'failed') {
|
if (row.ping_status === 'unknown') unknown += 1
|
||||||
warn += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const rackName = row.rack_name && row.rack_name.trim() ? row.rack_name : 'Без стойки'
|
const rackName = row.rack_name && row.rack_name.trim() ? row.rack_name : 'Без стойки'
|
||||||
if (!byRack.has(rackName)) byRack.set(rackName, [])
|
if (!byRack.has(rackName)) byRack.set(rackName, [])
|
||||||
|
|
@ -165,35 +142,35 @@ export function HomeObjectDetailPage() {
|
||||||
|
|
||||||
const rackGroups = Array.from(byRack.entries())
|
const rackGroups = Array.from(byRack.entries())
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (a[0] === 'Без стойки') return 1
|
if (a[0] === 'Без стойки') return -1
|
||||||
if (b[0] === 'Без стойки') return -1
|
if (b[0] === 'Без стойки') return 1
|
||||||
return a[0].localeCompare(b[0])
|
return a[0].localeCompare(b[0])
|
||||||
})
|
})
|
||||||
.map(([rack, rowsInRack]) => ({
|
.map(([rack, rowsInRack]) => ({
|
||||||
rack,
|
rack,
|
||||||
rows: [...rowsInRack].sort((a, b) => {
|
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 !== b.ping_status) {
|
||||||
if (a.ping_status === 'offline') return -1
|
if (a.ping_status === 'offline') return -1
|
||||||
if (b.ping_status === 'offline') return 1
|
if (b.ping_status === 'offline') return 1
|
||||||
|
if (a.ping_status === 'unknown') return -1
|
||||||
|
if (b.ping_status === 'unknown') return 1
|
||||||
}
|
}
|
||||||
return a.ip.localeCompare(b.ip)
|
return a.ip.localeCompare(b.ip)
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return { online, warn, rackGroups }
|
return { online, offline, unknown, rackGroups }
|
||||||
}, [devices])
|
}, [devices])
|
||||||
|
|
||||||
const columns: ColumnsType<DashboardDeviceNOCItem> = [
|
const columns: ColumnsType<DashboardDeviceNOCItem> = [
|
||||||
{
|
{
|
||||||
title: 'IP / Hostname',
|
title: 'IP / Hostname',
|
||||||
key: 'identity',
|
key: 'identity',
|
||||||
|
width: 190,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Space direction="vertical" size={0}>
|
<Space direction="vertical" size={0} style={{ maxWidth: 170 }}>
|
||||||
<code>{row.ip}</code>
|
<code>{row.ip}</code>
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
<Typography.Text type="secondary" ellipsis={{ tooltip: row.hostname ?? '—' }} style={{ fontSize: 12, maxWidth: 170 }}>
|
||||||
{row.hostname ?? '—'}
|
{row.hostname ?? '—'}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
@ -203,18 +180,24 @@ export function HomeObjectDetailPage() {
|
||||||
title: 'Категория',
|
title: 'Категория',
|
||||||
dataIndex: 'category',
|
dataIndex: 'category',
|
||||||
key: 'category',
|
key: 'category',
|
||||||
width: 130,
|
width: 100,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Роль',
|
||||||
|
key: 'role',
|
||||||
|
width: 90,
|
||||||
|
render: (_, row) => row.role ?? '—',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Ping',
|
title: 'Ping',
|
||||||
key: 'ping',
|
key: 'ping',
|
||||||
width: 190,
|
width: 140,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Space direction="vertical" size={0}>
|
<Space direction="vertical" size={0}>
|
||||||
<Tag color={statusColor(row.ping_status)}>{row.ping_status}</Tag>
|
<Tag color={statusColor(row.ping_status)}>{row.ping_status}</Tag>
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
{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` : '—'}
|
||||||
{row.last_ping_ms != null ? ` • ${row.last_ping_ms}ms` : ''}
|
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
|
|
@ -222,7 +205,7 @@ export function HomeObjectDetailPage() {
|
||||||
{
|
{
|
||||||
title: 'Uptime',
|
title: 'Uptime',
|
||||||
key: 'uptime',
|
key: 'uptime',
|
||||||
width: 280,
|
width: 220,
|
||||||
render: (_, row) => {
|
render: (_, row) => {
|
||||||
const availability = availabilityByDevice.get(Number(row.device_id))
|
const availability = availabilityByDevice.get(Number(row.device_id))
|
||||||
|
|
||||||
|
|
@ -243,32 +226,6 @@ export function HomeObjectDetailPage() {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Свежесть',
|
|
||||||
key: 'freshness',
|
|
||||||
width: 110,
|
|
||||||
render: (_, row) => <Tag color={row.ping_fresh ? 'green' : 'orange'}>{formatAge(row.ping_age_sec)}</Tag>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Disk',
|
|
||||||
key: 'disk',
|
|
||||||
width: 140,
|
|
||||||
render: (_, row) =>
|
|
||||||
row.disk_status ? (
|
|
||||||
<Tag color={statusColor(row.disk_status)}>
|
|
||||||
{row.disk_status}
|
|
||||||
{row.disk_max_pct != null ? ` (${row.disk_max_pct}%)` : ''}
|
|
||||||
</Tag>
|
|
||||||
) : (
|
|
||||||
'—'
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Проблема',
|
|
||||||
key: 'problem',
|
|
||||||
width: 100,
|
|
||||||
render: (_, row) => (row.has_problem ? <Tag color="red">Да</Tag> : <Tag color="green">Нет</Tag>),
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -319,9 +276,17 @@ export function HomeObjectDetailPage() {
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} md={6}>
|
<Col xs={12} md={6}>
|
||||||
<Card size="small" loading={summaryLoading} style={{ borderRadius: 12 }}>
|
<Card size="small" loading={summaryLoading} style={{ borderRadius: 12 }}>
|
||||||
<Typography.Text type="secondary">Проблемных</Typography.Text>
|
<Typography.Text type="secondary">Unknown</Typography.Text>
|
||||||
<Typography.Title level={3} style={{ margin: 0 }}>
|
<Typography.Title level={3} style={{ margin: 0 }}>
|
||||||
{summary?.totals.problem_devices ?? 0}
|
{grouped.unknown}
|
||||||
|
</Typography.Title>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={12} md={6}>
|
||||||
|
<Card size="small" loading={devicesLoading} style={{ borderRadius: 12 }}>
|
||||||
|
<Typography.Text type="secondary">Offline</Typography.Text>
|
||||||
|
<Typography.Title level={3} style={{ margin: 0, color: '#cf1322' }}>
|
||||||
|
{grouped.offline}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
@ -333,14 +298,6 @@ export function HomeObjectDetailPage() {
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} md={6}>
|
|
||||||
<Card size="small" loading={devicesLoading} style={{ borderRadius: 12 }}>
|
|
||||||
<Typography.Text type="secondary">Disk warn/failed</Typography.Text>
|
|
||||||
<Typography.Title level={3} style={{ margin: 0, color: '#d46b08' }}>
|
|
||||||
{grouped.warn}
|
|
||||||
</Typography.Title>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
|
|
@ -363,29 +320,25 @@ export function HomeObjectDetailPage() {
|
||||||
{!grouped.rackGroups.length ? (
|
{!grouped.rackGroups.length ? (
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="Устройства не найдены" />
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="Устройства не найдены" />
|
||||||
) : (
|
) : (
|
||||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
<Collapse
|
||||||
{grouped.rackGroups.map((rackGroup) => (
|
defaultActiveKey={['Без стойки']}
|
||||||
<Card
|
items={grouped.rackGroups.map((rackGroup) => ({
|
||||||
key={rackGroup.rack}
|
key: rackGroup.rack,
|
||||||
size="small"
|
label: `${rackGroup.rack} (${rackGroup.rows.length})`,
|
||||||
title={`${rackGroup.rack} (${rackGroup.rows.length})`}
|
children: (
|
||||||
style={{ borderRadius: 10 }}
|
|
||||||
>
|
|
||||||
<Table<DashboardDeviceNOCItem>
|
<Table<DashboardDeviceNOCItem>
|
||||||
loading={devicesLoading}
|
loading={devicesLoading}
|
||||||
dataSource={rackGroup.rows}
|
dataSource={rackGroup.rows}
|
||||||
rowKey="device_id"
|
rowKey="device_id"
|
||||||
size="small"
|
size="small"
|
||||||
pagination={false}
|
pagination={false}
|
||||||
scroll={{ x: 1300 }}
|
|
||||||
columns={columns}
|
columns={columns}
|
||||||
/>
|
/>
|
||||||
</Card>
|
),
|
||||||
))}
|
}))}
|
||||||
</Space>
|
/>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue