кодировка

This commit is contained in:
dv 2026-05-05 15:00:06 +03:00
parent ecb929d780
commit b282de5355

View file

@ -1,4 +1,4 @@
import { useQueryClient } from '@tanstack/react-query'
import { useQueryClient } from '@tanstack/react-query'
import {
Alert,
Button,
@ -24,10 +24,10 @@ import { useTags } from '../api/tags'
import { useDashboardMonitorSSE } from '../hooks/useDashboardMonitorSSE'
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)}ì`
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 scopeToSearch(scope: DashboardScope): string {
@ -91,9 +91,9 @@ export function HomePage() {
const handleRunNow = async () => {
try {
await runNow.mutateAsync({ ...scope, force: true })
message.success('Ìîíèòîðèíã çàïóùåí')
message.success('Мониторинг запущен')
} catch {
message.error('Íå óäàëîñü çàïóñòèòü ìîíèòîðèíã')
message.error('Не удалось запустить мониторинг')
}
}
@ -203,13 +203,13 @@ export function HomePage() {
<Typography.Title level={4} style={{ margin: 0 }}>
NOC Dashboard
</Typography.Title>
<Typography.Text type="secondary">SLA, ïîêðûòèå ìîíèòîðèíãîì, èíöèäåíòû è î÷åðåäü äåéñòâèé</Typography.Text>
<Typography.Text type="secondary">SLA, покрытие мониторингом, инциденты и очередь действий</Typography.Text>
</Col>
<Col>
<Space wrap>
<Button onClick={() => navigate(`/home/settings${scopeToSearch(scope)}`)}>Íàñòðîéêè ìîíèòîðèíãà</Button>
<Button onClick={() => navigate(`/home/settings${scopeToSearch(scope)}`)}>Настройки мониторинга</Button>
<Button type="primary" onClick={handleRunNow} loading={runNow.isPending}>
Çàïóñòèòü ñåé÷àñ
Запустить сейчас
</Button>
</Space>
</Col>
@ -219,7 +219,7 @@ export function HomePage() {
<Row gutter={[8, 8]}>
<Col xs={24} sm={12} lg={6}>
<Select
placeholder="Ãîðîä"
placeholder="Город"
allowClear
style={{ width: '100%' }}
options={(meta?.cities ?? []).map((c) => ({ value: c, label: c }))}
@ -229,7 +229,7 @@ export function HomePage() {
</Col>
<Col xs={24} sm={12} lg={6}>
<Select
placeholder="Êëèåíò"
placeholder="Клиент"
allowClear
style={{ width: '100%' }}
options={(meta?.clients ?? []).map((c) => ({ value: c, label: c }))}
@ -240,7 +240,7 @@ export function HomePage() {
<Col xs={24} sm={12} lg={6}>
<Select
mode="multiple"
placeholder="Îáúåêòû"
placeholder="Объекты"
allowClear
style={{ width: '100%' }}
value={objectIds}
@ -251,7 +251,7 @@ export function HomePage() {
<Col xs={24} sm={12} lg={6}>
<Select
mode="multiple"
placeholder="Òåãè"
placeholder="Теги"
allowClear
style={{ width: '100%' }}
value={tagIds}
@ -263,12 +263,12 @@ export function HomePage() {
</Card>
<Row gutter={[12, 12]} style={{ marginBottom: 12 }}>
<Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Îáúåêòîâ â ñêîóïå" value={totals.objects} /></Card></Col>
<Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Óñòðîéñòâ" value={totals.devices} /></Card></Col>
<Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Èíöèäåíòíûõ îáúåêòîâ" value={incidentObjects.length} /></Card></Col>
<Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Ñèãíàëîâ ïðîáëåì" value={signalCount} /></Card></Col>
<Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Ïðîñðî÷åííûå îáúåêòû" value={stalePingObjects.length} /></Card></Col>
<Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Ñðåäíèé health" value={avgHealth ?? '—'} /></Card></Col>
<Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Объектов в скоупе" value={totals.objects} /></Card></Col>
<Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Устройств" value={totals.devices} /></Card></Col>
<Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Инцидентных объектов" value={incidentObjects.length} /></Card></Col>
<Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Сигналов проблем" value={signalCount} /></Card></Col>
<Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Просроченные объекты" value={stalePingObjects.length} /></Card></Col>
<Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Средний health" value={avgHealth ?? '—'} /></Card></Col>
</Row>
{monitorStatus?.last_error ? <Alert type="error" showIcon style={{ marginBottom: 12 }} message={monitorStatus.last_error} /> : null}
@ -276,9 +276,9 @@ export function HomePage() {
<Row gutter={[12, 12]}>
<Col xs={24} xl={16}>
<Space direction="vertical" size={12} style={{ width: '100%' }}>
<Card title="Îáúåêòû ñ èíöèäåíòàìè" size="small" style={{ borderRadius: 12 }}>
<Card title="Объекты с инцидентами" size="small" style={{ borderRadius: 12 }}>
{incidentObjects.length === 0 ? (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="Êðèòè÷íûõ îáúåêòîâ íåò" />
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="Критичных объектов нет" />
) : (
<List
dataSource={incidentObjects.slice(0, 12)}
@ -293,7 +293,7 @@ export function HomePage() {
<List.Item
actions={[
<Button key="open" type="link" onClick={() => navigate(`/home/objects/${item.object_id}${scopeToSearch(scope)}`)}>
Îòêðûòü
Открыть
</Button>,
]}
>
@ -301,17 +301,17 @@ export function HomePage() {
title={
<Space wrap size={8}>
<Typography.Text strong>{item.object_name}</Typography.Text>
<Tag>{item.city ?? '—'}</Tag>
<Tag color="blue">{item.client ?? '—'}</Tag>
<Tag color={healthColor(item.health_score)}>health {item.health_score ?? '—'}</Tag>
<Tag>{item.city ?? '—'}</Tag>
<Tag color="blue">{item.client ?? '—'}</Tag>
<Tag color={healthColor(item.health_score)}>health {item.health_score ?? '—'}</Tag>
</Space>
}
description={
<Space split={<span></span>} wrap>
<Space split={<span></span>} wrap>
<span>offline: {offline}</span>
<span>unknown: {unknown}</span>
<span>disk warn/failed: {diskWarn}/{diskFailed}</span>
<span>óñòðîéñòâ: {item.device_count}</span>
<span>устройств: {item.device_count}</span>
<span>age ping: {formatAge(ping?.max_age_sec)}</span>
</Space>
}
@ -323,15 +323,15 @@ export function HomePage() {
)}
</Card>
<Card title="Îáúåêòû ñ íåñâåæèìè äàííûìè ìîíèòîðèíãà" size="small" style={{ borderRadius: 12 }}>
<Card title="Объекты с несвежими данными мониторинга" size="small" style={{ borderRadius: 12 }}>
{stalePingObjects.length === 0 ? (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="Âñå îáúåêòû îáíîâëÿþòñÿ â ïðåäåëàõ SLA" />
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="Все объекты обновляются в пределах SLA" />
) : (
<List
size="small"
dataSource={stalePingObjects}
renderItem={(item) => (
<List.Item actions={[<Button key="open" type="link" onClick={() => navigate(`/home/objects/${item.obj.object_id}${scopeToSearch(scope)}`)}>Îòêðûòü</Button>]}>
<List.Item actions={[<Button key="open" type="link" onClick={() => navigate(`/home/objects/${item.obj.object_id}${scopeToSearch(scope)}`)}>Открыть</Button>]}>
<Space direction="vertical" size={0} style={{ width: '100%' }}>
<Row justify="space-between">
<Typography.Text>{item.obj.object_name}</Typography.Text>
@ -351,28 +351,28 @@ export function HomePage() {
<Col xs={24} xl={8}>
<Space direction="vertical" size={12} style={{ width: '100%' }}>
<Card title="SLA è ïîêðûòèå" size="small" style={{ borderRadius: 12 }}>
<Card title="SLA и покрытие" size="small" style={{ borderRadius: 12 }}>
<Space direction="vertical" size={10} style={{ width: '100%' }}>
<div>
<Row justify="space-between">
<Typography.Text>Äîñòóïíîñòü (online)</Typography.Text>
<Typography.Text strong>{availabilityPct != null ? `${availabilityPct}%` : '—'}</Typography.Text>
<Typography.Text>Доступность (online)</Typography.Text>
<Typography.Text strong>{availabilityPct != null ? `${availabilityPct}%` : '—'}</Typography.Text>
</Row>
<Progress percent={availabilityPct ?? 0} size="small" showInfo={false} strokeColor={availabilityPct != null && availabilityPct < 95 ? '#ff4d4f' : '#52c41a'} />
</div>
<div>
<Row justify="space-between">
<Typography.Text>Ïîêðûòèå ìîíèòîðèíãîì</Typography.Text>
<Typography.Text strong>{coveragePct != null ? `${coveragePct}%` : '—'}</Typography.Text>
<Typography.Text>Покрытие мониторингом</Typography.Text>
<Typography.Text strong>{coveragePct != null ? `${coveragePct}%` : '—'}</Typography.Text>
</Row>
<Progress percent={coveragePct ?? 0} size="small" showInfo={false} strokeColor="#1677ff" />
</div>
<div>
<Row justify="space-between">
<Typography.Text>Ñâåæåñòü äàííûõ</Typography.Text>
<Typography.Text strong>{freshnessPct != null ? `${freshnessPct}%` : '—'}</Typography.Text>
<Typography.Text>Свежесть данных</Typography.Text>
<Typography.Text strong>{freshnessPct != null ? `${freshnessPct}%` : '—'}</Typography.Text>
</Row>
<Progress percent={freshnessPct ?? 0} size="small" showInfo={false} strokeColor={freshnessPct != null && freshnessPct < 90 ? '#faad14' : '#52c41a'} />
</div>
@ -381,16 +381,16 @@ export function HomePage() {
<Tag color="green">online: {onlineTotal}</Tag>
<Tag color="red">offline: {offlineTotal}</Tag>
<Tag color="orange">unknown: {unknownTotal}</Tag>
<Tag>ïî ping: {monitoredTotal}</Tag>
<Tag>по ping: {monitoredTotal}</Tag>
</Space>
</Space>
</Card>
<Card title="Ñîñòîÿíèå öèêëà ìîíèòîðèíãà" size="small" style={{ borderRadius: 12 }}>
<Card title="Состояние цикла мониторинга" size="small" style={{ borderRadius: 12 }}>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<Row justify="space-between"><Typography.Text>Ñòàòóñ</Typography.Text><Tag color={runStatusColor(monitorStatus?.status)}>{monitorStatus?.status ?? '-'}</Tag></Row>
<Row justify="space-between"><Typography.Text>Îáúåêòû â ìîíèòîðèíãå</Typography.Text><Typography.Text strong>{summary?.included_count ?? totals.allowed_objects}</Typography.Text></Row>
<Row justify="space-between"><Typography.Text>Ïîñëåäíèé óñïåõ</Typography.Text><Typography.Text strong>{monitorStatus?.last_success_at ? dayjs(monitorStatus.last_success_at).format('DD.MM HH:mm:ss') : '—'}</Typography.Text></Row>
<Row justify="space-between"><Typography.Text>Статус</Typography.Text><Tag color={runStatusColor(monitorStatus?.status)}>{monitorStatus?.status ?? '-'}</Tag></Row>
<Row justify="space-between"><Typography.Text>Объекты в мониторинге</Typography.Text><Typography.Text strong>{summary?.included_count ?? totals.allowed_objects}</Typography.Text></Row>
<Row justify="space-between"><Typography.Text>Последний успех</Typography.Text><Typography.Text strong>{monitorStatus?.last_success_at ? dayjs(monitorStatus.last_success_at).format('DD.MM HH:mm:ss') : '—'}</Typography.Text></Row>
{lastCounts ? (
<Space direction="vertical" size={2} style={{ width: '100%' }}>
@ -405,7 +405,7 @@ export function HomePage() {
<Card title="Action queue" size="small" style={{ borderRadius: 12 }}>
{actionQueue.length === 0 ? (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="Êðèòè÷íûõ äåéñòâèé ñåé÷àñ íåò" />
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="Критичных действий сейчас нет" />
) : (
<List
size="small"
@ -414,7 +414,7 @@ export function HomePage() {
<List.Item
actions={[
<Button key="open" type="link" onClick={() => navigate(`/home/objects/${row.obj.object_id}${scopeToSearch(scope)}`)}>
Îòêðûòü
Открыть
</Button>,
]}
>
@ -427,7 +427,7 @@ export function HomePage() {
</Col>
</Row>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{row.reasons.join(' • ')}
{row.reasons.join(' • ')}
</Typography.Text>
</List.Item>
)}
@ -440,3 +440,4 @@ export function HomePage() {
</div>
)
}