фикс разметки и кодировки

This commit is contained in:
dv 2026-05-05 12:27:12 +03:00
parent 3feb05ce0f
commit ecb3f6b524
3 changed files with 163 additions and 144 deletions

View file

@ -10,19 +10,19 @@ import { useDashboardMonitorSSE } from '../hooks/useDashboardMonitorSSE'
function formatAge(seconds: number | null | undefined): string { function formatAge(seconds: number | null | undefined): string {
if (seconds == null) return '-' if (seconds == null) return '-'
if (seconds < 60) return `${seconds}ñ` if (seconds < 60) return `${seconds}с`
if (seconds < 3600) return `${Math.floor(seconds / 60)}ì` if (seconds < 3600) return `${Math.floor(seconds / 60)}м`
return `${Math.floor(seconds / 3600)}÷ ${Math.floor((seconds % 3600) / 60)}ì` return `${Math.floor(seconds / 3600)}ч ${Math.floor((seconds % 3600) / 60)}м`
} }
function reasonLabel(reason: string): string { function reasonLabel(reason: string): string {
const map: Record<string, string> = { const map: Record<string, string> = {
object_include_policy: 'Âêëþ÷åí ïîëèòèêîé îáúåêòà', object_include_policy: 'Включен политикой объекта',
object_exclude_policy: 'Èñêëþ÷åí ïîëèòèêîé îáúåêòà', object_exclude_policy: 'Исключен политикой объекта',
inherit_default: 'Íàñëåäîâàíèå ïî óìîë÷àíèþ', inherit_default: 'Наследование по умолчанию',
tag_policy_enabled: 'Âêëþ÷åí ïîëèòèêîé òåãîâ', tag_policy_enabled: 'Включен политикой тегов',
tag_policy_disabled: 'Èñêëþ÷åí ïîëèòèêîé òåãîâ', tag_policy_disabled: 'Исключен политикой тегов',
scope_filtered: 'Âíå òåêóùåãî scope', scope_filtered: 'Вне текущего scope',
} }
return map[reason] ?? reason return map[reason] ?? reason
} }
@ -72,9 +72,9 @@ export function HomePage() {
const handleRunNow = async () => { const handleRunNow = async () => {
try { try {
await runNow.mutateAsync({ ...scope, force: true }) await runNow.mutateAsync({ ...scope, force: true })
message.success('Ìîíèòîðèíã çàïóùåí') message.success('Мониторинг запущен')
} catch { } catch {
message.error('Íå óäàëîñü çàïóñòèòü ìîíèòîðèíã') message.error('Не удалось запустить мониторинг')
} }
} }
@ -82,7 +82,7 @@ export function HomePage() {
const columns = [ const columns = [
{ {
title: 'Îáúåêò', title: 'Объект',
key: 'object', key: 'object',
render: (_: unknown, row: any) => ( render: (_: unknown, row: any) => (
<Space> <Space>
@ -99,12 +99,12 @@ export function HomePage() {
), ),
}, },
{ {
title: 'Ïðè÷èíà', title: 'Причина',
key: 'reason', key: 'reason',
render: (_: unknown, row: any) => <Typography.Text>{reasonLabel(row.inclusion_reason)}</Typography.Text>, render: (_: unknown, row: any) => <Typography.Text>{reasonLabel(row.inclusion_reason)}</Typography.Text>,
}, },
{ {
title: 'Óñòðîéñòâ', title: 'Устройств',
dataIndex: 'device_count', dataIndex: 'device_count',
key: 'device_count', key: 'device_count',
}, },
@ -137,22 +137,26 @@ export function HomePage() {
return ( return (
<div> <div>
<Space style={{ width: '100%', justifyContent: 'space-between', marginBottom: 12 }} align="center"> <Row justify="space-between" align="middle" gutter={[8, 8]} style={{ marginBottom: 12 }}>
<Typography.Title level={4} style={{ margin: 0 }}> <Col>
NOC Äåøáîðä <Typography.Title level={4} style={{ margin: 0 }}>
</Typography.Title> NOC Дешборд
<Space> </Typography.Title>
<Button onClick={() => navigate(`/home/settings${scopeToSearch(scope)}`)}>Íàñòðîéêè ìîíèòîðèíãà</Button> </Col>
<Button type="primary" onClick={handleRunNow} loading={runNow.isPending}> <Col>
Çàïóñòèòü ñåé÷àñ <Space wrap>
</Button> <Button onClick={() => navigate(`/home/settings${scopeToSearch(scope)}`)}>Настройки мониторинга</Button>
</Space> <Button type="primary" onClick={handleRunNow} loading={runNow.isPending}>
</Space> Запустить сейчас
</Button>
</Space>
</Col>
</Row>
<Row gutter={12} style={{ marginBottom: 12 }}> <Row gutter={[12, 12]} style={{ marginBottom: 12 }}>
<Col span={5}> <Col xs={24} sm={12} lg={6}>
<Select <Select
placeholder="Ãîðîä" placeholder="Город"
allowClear allowClear
style={{ width: '100%' }} style={{ width: '100%' }}
options={(meta?.cities ?? []).map((c) => ({ value: c, label: c }))} options={(meta?.cities ?? []).map((c) => ({ value: c, label: c }))}
@ -160,9 +164,9 @@ export function HomePage() {
onChange={setCity} onChange={setCity}
/> />
</Col> </Col>
<Col span={5}> <Col xs={24} sm={12} lg={6}>
<Select <Select
placeholder="Êëèåíò" placeholder="Клиент"
allowClear allowClear
style={{ width: '100%' }} style={{ width: '100%' }}
options={(meta?.clients ?? []).map((c) => ({ value: c, label: c }))} options={(meta?.clients ?? []).map((c) => ({ value: c, label: c }))}
@ -170,10 +174,10 @@ export function HomePage() {
onChange={setClient} onChange={setClient}
/> />
</Col> </Col>
<Col span={7}> <Col xs={24} sm={12} lg={6}>
<Select <Select
mode="multiple" mode="multiple"
placeholder="Îáúåêòû" placeholder="Объекты"
allowClear allowClear
style={{ width: '100%' }} style={{ width: '100%' }}
value={objectIds} value={objectIds}
@ -181,10 +185,10 @@ export function HomePage() {
options={(objects ?? []).map((o) => ({ value: o.id, label: o.name }))} options={(objects ?? []).map((o) => ({ value: o.id, label: o.name }))}
/> />
</Col> </Col>
<Col span={7}> <Col xs={24} sm={12} lg={6}>
<Select <Select
mode="multiple" mode="multiple"
placeholder="Òåãè" placeholder="Теги"
allowClear allowClear
style={{ width: '100%' }} style={{ width: '100%' }}
value={tagIds} value={tagIds}
@ -197,9 +201,9 @@ export function HomePage() {
<Row gutter={12} style={{ marginBottom: 16 }}> <Row gutter={12} style={{ marginBottom: 16 }}>
<Col> <Col>
<Typography.Text type="secondary"> <Typography.Text type="secondary">
Ñòàòóñ: {monitorStatus?.status ?? '-'} Статус: {monitorStatus?.status ?? '-'}
{monitorStatus?.last_success_at {monitorStatus?.last_success_at
? ` · óñïåøíûé çàïóñê ${dayjs(monitorStatus.last_success_at).format('DD.MM HH:mm:ss')}` ? ` · успешный запуск ${dayjs(monitorStatus.last_success_at).format('DD.MM HH:mm:ss')}`
: ''} : ''}
</Typography.Text> </Typography.Text>
</Col> </Col>
@ -209,22 +213,22 @@ export function HomePage() {
<Alert type="error" showIcon style={{ marginBottom: 12 }} message={monitorStatus.last_error} /> <Alert type="error" showIcon style={{ marginBottom: 12 }} message={monitorStatus.last_error} />
)} )}
<Row gutter={12} style={{ marginBottom: 16 }}> <Row gutter={[12, 12]} style={{ marginBottom: 16 }}>
<Col span={6}> <Col xs={24} sm={12} lg={6}>
<Card><Statistic title="Îáúåêòû" value={totals.objects} /></Card> <Card><Statistic title="Объекты" value={totals.objects} /></Card>
</Col> </Col>
<Col span={6}> <Col xs={24} sm={12} lg={6}>
<Card><Statistic title="Â ìîíèòîðèíãå" value={summary?.included_count ?? totals.allowed_objects} /></Card> <Card><Statistic title="В мониторинге" value={summary?.included_count ?? totals.allowed_objects} /></Card>
</Col> </Col>
<Col span={6}> <Col xs={24} sm={12} lg={6}>
<Card><Statistic title="Âíå ìîíèòîðèíãà" value={summary?.excluded_count ?? totals.excluded_objects} /></Card> <Card><Statistic title="Вне мониторинга" value={summary?.excluded_count ?? totals.excluded_objects} /></Card>
</Col> </Col>
<Col span={6}> <Col xs={24} sm={12} lg={6}>
<Card><Statistic title="Óñòðîéñòâà" value={totals.devices} /></Card> <Card><Statistic title="Устройства" value={totals.devices} /></Card>
</Col> </Col>
</Row> </Row>
<Card title="Ïðîáëåìíûå îáúåêòû" style={{ marginBottom: 16 }}> <Card title="Проблемные объекты" style={{ marginBottom: 16 }}>
<Table <Table
size="small" size="small"
dataSource={summary?.problem_objects ?? []} dataSource={summary?.problem_objects ?? []}
@ -232,12 +236,13 @@ export function HomePage() {
columns={columns} columns={columns}
pagination={false} pagination={false}
loading={isLoading} loading={isLoading}
scroll={{ x: 900 }}
/> />
</Card> </Card>
<Row gutter={12}> <Row gutter={[12, 12]}>
<Col span={12}> <Col xs={24} xl={12}>
<Card title="Â ìîíèòîðèíãå" style={{ marginBottom: 16 }}> <Card title="В мониторинге" style={{ marginBottom: 16 }}>
<Table <Table
size="small" size="small"
dataSource={summary?.included_objects ?? []} dataSource={summary?.included_objects ?? []}
@ -245,11 +250,12 @@ export function HomePage() {
columns={columns} columns={columns}
pagination={{ pageSize: 8 }} pagination={{ pageSize: 8 }}
loading={isLoading} loading={isLoading}
scroll={{ x: 900 }}
/> />
</Card> </Card>
</Col> </Col>
<Col span={12}> <Col xs={24} xl={12}>
<Card title="Âíå ìîíèòîðèíãà" style={{ marginBottom: 16 }}> <Card title="Вне мониторинга" style={{ marginBottom: 16 }}>
<Table <Table
size="small" size="small"
dataSource={summary?.excluded_objects ?? []} dataSource={summary?.excluded_objects ?? []}
@ -257,6 +263,7 @@ export function HomePage() {
columns={columns} columns={columns}
pagination={{ pageSize: 8 }} pagination={{ pageSize: 8 }}
loading={isLoading} loading={isLoading}
scroll={{ x: 900 }}
/> />
</Card> </Card>
</Col> </Col>

View file

@ -2,6 +2,7 @@ import { ArrowLeftOutlined } from '@ant-design/icons'
import { Alert, Button, Card, Col, Row, Select, Space, Table, Tag, Typography } from 'antd' import { Alert, Button, Card, Col, Row, Select, Space, Table, Tag, Typography } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import { useNavigate, useParams, useSearchParams } from 'react-router-dom' import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { import {
useDashboardObjectAvailability, useDashboardObjectAvailability,
@ -12,16 +13,15 @@ import {
type DashboardScope, type DashboardScope,
} from '../api/dashboard' } from '../api/dashboard'
import { useDashboardMonitorSSE } from '../hooks/useDashboardMonitorSSE' import { useDashboardMonitorSSE } from '../hooks/useDashboardMonitorSSE'
import { useQueryClient } from '@tanstack/react-query'
function reasonLabel(reason: string): string { function reasonLabel(reason: string): string {
const map: Record<string, string> = { const map: Record<string, string> = {
object_include_policy: 'Âêëþ÷åí ïîëèòèêîé îáúåêòà', object_include_policy: 'Включен политикой объекта',
object_exclude_policy: 'Èñêëþ÷åí ïîëèòèêîé îáúåêòà', object_exclude_policy: 'Исключен политикой объекта',
inherit_default: 'Íàñëåäîâàíèå ïî óìîë÷àíèþ', inherit_default: 'Наследование по умолчанию',
tag_policy_enabled: 'Âêëþ÷åí ïîëèòèêîé òåãîâ', tag_policy_enabled: 'Включен политикой тегов',
tag_policy_disabled: 'Èñêëþ÷åí ïîëèòèêîé òåãîâ', tag_policy_disabled: 'Исключен политикой тегов',
scope_filtered: 'Âíå òåêóùåãî scope', scope_filtered: 'Вне текущего scope',
} }
return map[reason] ?? reason return map[reason] ?? reason
} }
@ -93,24 +93,28 @@ export function HomeObjectDetailPage() {
return ( return (
<div> <div>
<Space style={{ width: '100%', justifyContent: 'space-between', marginBottom: 12 }} align="center"> <Row justify="space-between" align="middle" gutter={[8, 8]} style={{ marginBottom: 12 }}>
<Space> <Col>
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/home')}> <Space wrap>
Íàçàä <Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/home')}>
</Button> Назад
<Typography.Title level={4} style={{ margin: 0 }}> </Button>
{object?.object_name ?? `Îáúåêò #${objectId}`} <Typography.Title level={4} style={{ margin: 0 }}>
</Typography.Title> {object?.object_name ?? `Объект #${objectId}`}
</Space> </Typography.Title>
<Space> </Space>
{object?.allowed ? <Tag color="green">Â ìîíèòîðèíãå</Tag> : <Tag color="red">Âíå ìîíèòîðèíãà</Tag>} </Col>
{object?.health_score != null && ( <Col>
<Tag color={object.health_score >= 80 ? 'green' : object.health_score >= 60 ? 'orange' : 'red'}> <Space wrap>
Health {object.health_score} {object?.allowed ? <Tag color="green">В мониторинге</Tag> : <Tag color="red">Вне мониторинга</Tag>}
</Tag> {object?.health_score != null && (
)} <Tag color={object.health_score >= 80 ? 'green' : object.health_score >= 60 ? 'orange' : 'red'}>
</Space> Health {object.health_score}
</Space> </Tag>
)}
</Space>
</Col>
</Row>
{object && ( {object && (
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 12 }}> <Typography.Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
@ -123,43 +127,43 @@ export function HomeObjectDetailPage() {
type="info" type="info"
showIcon showIcon
style={{ marginBottom: 12 }} style={{ marginBottom: 12 }}
message="Íåäîñòàòî÷íî äàííûõ äëÿ ãðàôèêà äîñòóïíîñòè â âûáðàííîì îêíå" message="Недостаточно данных для графика доступности в выбранном окне"
/> />
)} )}
<Row gutter={12} style={{ marginBottom: 16 }}> <Row gutter={[12, 12]} style={{ marginBottom: 16 }}>
<Col span={8}> <Col xs={24} sm={12} lg={8}>
<Card loading={summaryLoading}> <Card loading={summaryLoading}>
<Typography.Text type="secondary">Óñòðîéñòâ</Typography.Text> <Typography.Text type="secondary">Устройств</Typography.Text>
<Typography.Title level={3} style={{ margin: 0 }}>{summary?.totals.devices ?? 0}</Typography.Title> <Typography.Title level={3} style={{ margin: 0 }}>{summary?.totals.devices ?? 0}</Typography.Title>
</Card> </Card>
</Col> </Col>
<Col span={8}> <Col xs={24} sm={12} lg={8}>
<Card loading={summaryLoading}> <Card loading={summaryLoading}>
<Typography.Text type="secondary">Ïðîáëåìíûõ</Typography.Text> <Typography.Text type="secondary">Проблемных</Typography.Text>
<Typography.Title level={3} style={{ margin: 0 }}>{summary?.totals.problem_devices ?? 0}</Typography.Title> <Typography.Title level={3} style={{ margin: 0 }}>{summary?.totals.problem_devices ?? 0}</Typography.Title>
</Card> </Card>
</Col> </Col>
<Col span={8}> <Col xs={24} sm={12} lg={8}>
<Card loading={availLoading}> <Card loading={availLoading}>
<Typography.Text type="secondary">Äîñòóïíîñòü ({window})</Typography.Text> <Typography.Text type="secondary">Доступность ({window})</Typography.Text>
<Typography.Title level={3} style={{ margin: 0 }}> <Typography.Title level={3} style={{ margin: 0 }}>
{availability?.overall_availability_pct != null ? `${availability.overall_availability_pct}%` : '—'} {availability?.overall_availability_pct != null ? `${availability.overall_availability_pct}%` : '—'}
</Typography.Title> </Typography.Title>
</Card> </Card>
</Col> </Col>
</Row> </Row>
<Card <Card
title="Ãðàôèê äîñòóïíîñòè ïî Ping" title="График доступности по Ping"
extra={ extra={
<Select <Select
value={window} value={window}
onChange={(v) => setWindow(v as DashboardAvailabilityWindow)} onChange={(v) => setWindow(v as DashboardAvailabilityWindow)}
options={[ options={[
{ value: '24h', label: '24÷' }, { value: '24h', label: '24ч' },
{ value: '3d', label: '3ä' }, { value: '3d', label: '3д' },
{ value: '7d', label: '7ä' }, { value: '7d', label: '7д' },
]} ]}
style={{ width: 100 }} style={{ width: 100 }}
/> />
@ -169,13 +173,14 @@ export function HomeObjectDetailPage() {
<AvailabilityChart points={availability?.overall_series ?? []} /> <AvailabilityChart points={availability?.overall_series ?? []} />
</Card> </Card>
<Card title="Ñòàòóñ óñòðîéñòâ" style={{ marginBottom: 16 }}> <Card title="Статус устройств" style={{ marginBottom: 16 }}>
<Table <Table
loading={devicesLoading} loading={devicesLoading}
dataSource={devices?.devices ?? []} dataSource={devices?.devices ?? []}
rowKey="device_id" rowKey="device_id"
size="small" size="small"
pagination={{ pageSize: 20 }} pagination={{ pageSize: 20 }}
scroll={{ x: 1000 }}
columns={[ columns={[
{ {
title: 'IP / Hostname', title: 'IP / Hostname',
@ -183,11 +188,11 @@ export function HomeObjectDetailPage() {
render: (_: unknown, row: any) => ( render: (_: unknown, row: any) => (
<Space direction="vertical" size={0}> <Space direction="vertical" size={0}>
<code>{row.ip}</code> <code>{row.ip}</code>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>{row.hostname ?? '—'}</Typography.Text> <Typography.Text type="secondary" style={{ fontSize: 12 }}>{row.hostname ?? '—'}</Typography.Text>
</Space> </Space>
), ),
}, },
{ title: 'Êàòåãîðèÿ', dataIndex: 'category', key: 'category' }, { title: 'Категория', dataIndex: 'category', key: 'category' },
{ {
title: 'Ping', title: 'Ping',
key: 'ping', key: 'ping',
@ -197,8 +202,8 @@ export function HomeObjectDetailPage() {
{row.ping_status} {row.ping_status}
</Tag> </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.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>
), ),
@ -211,37 +216,38 @@ export function HomeObjectDetailPage() {
<Tag color={row.disk_status === 'ok' ? 'green' : row.disk_status === 'warn' ? 'orange' : 'red'}> <Tag color={row.disk_status === 'ok' ? 'green' : row.disk_status === 'warn' ? 'orange' : 'red'}>
{row.disk_status}{row.disk_max_pct != null ? ` (${row.disk_max_pct}%)` : ''} {row.disk_status}{row.disk_max_pct != null ? ` (${row.disk_max_pct}%)` : ''}
</Tag> </Tag>
) : '—', ) : '—',
}, },
{ {
title: 'Ïðîáëåìà', title: 'Проблема',
key: 'problem', key: 'problem',
render: (_: unknown, row: any) => row.has_problem ? <Tag color="red">Äà</Tag> : <Tag color="green">Íåò</Tag>, render: (_: unknown, row: any) => row.has_problem ? <Tag color="red">Да</Tag> : <Tag color="green">Нет</Tag>,
}, },
]} ]}
/> />
</Card> </Card>
<Card title="Per-device äîñòóïíîñòü (âûáîðêà)"> <Card title="Per-device доступность (выборка)">
<Table <Table
loading={availLoading} loading={availLoading}
dataSource={availability?.devices ?? []} dataSource={availability?.devices ?? []}
rowKey="device_id" rowKey="device_id"
size="small" size="small"
pagination={{ pageSize: 10 }} pagination={{ pageSize: 10 }}
scroll={{ x: 900 }}
columns={[ columns={[
{ title: 'IP', dataIndex: 'ip', key: 'ip', render: (v: string) => <code>{v}</code> }, { title: 'IP', dataIndex: 'ip', key: 'ip', render: (v: string) => <code>{v}</code> },
{ title: 'Hostname', dataIndex: 'hostname', key: 'hostname', render: (v: string | null) => v ?? '—' }, { title: 'Hostname', dataIndex: 'hostname', key: 'hostname', render: (v: string | null) => v ?? '—' },
{ {
title: 'Availability', title: 'Availability',
key: 'availability_pct', key: 'availability_pct',
render: (_: unknown, row: any) => row.availability_pct != null ? `${row.availability_pct}%` : '—', render: (_: unknown, row: any) => row.availability_pct != null ? `${row.availability_pct}%` : '—',
}, },
{ title: 'Samples', dataIndex: 'samples', key: 'samples' }, { title: 'Samples', dataIndex: 'samples', key: 'samples' },
{ {
title: 'Ïîñëåäíèé ñòàòóñ', title: 'Последний статус',
key: 'last_status', key: 'last_status',
render: (_: unknown, row: any) => row.last_status ? <Tag>{row.last_status}</Tag> : '—', render: (_: unknown, row: any) => row.last_status ? <Tag>{row.last_status}</Tag> : '—',
}, },
]} ]}
/> />

View file

@ -110,32 +110,36 @@ export function HomeSettingsPage() {
const handleRunNow = async () => { const handleRunNow = async () => {
try { try {
await runNow.mutateAsync({ force: true }) await runNow.mutateAsync({ force: true })
message.success('Ìîíèòîðèíã çàïóùåí') message.success('Мониторинг запущен')
} catch { } catch {
message.error('Íå óäàëîñü çàïóñòèòü ìîíèòîðèíã') message.error('Не удалось запустить мониторинг')
} }
} }
return ( return (
<div> <div>
<Space style={{ width: '100%', justifyContent: 'space-between', marginBottom: 12 }} align="center"> <Row justify="space-between" align="middle" gutter={[8, 8]} style={{ marginBottom: 12 }}>
<Space> <Col>
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/home')}> <Space wrap>
Íàçàä <Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/home')}>
Назад
</Button>
<Typography.Title level={4} style={{ margin: 0 }}>
Настройки мониторинга
</Typography.Title>
</Space>
</Col>
<Col>
<Button type="primary" onClick={handleRunNow} loading={runNow.isPending}>
Запустить сейчас
</Button> </Button>
<Typography.Title level={4} style={{ margin: 0 }}> </Col>
Íàñòðîéêè ìîíèòîðèíãà </Row>
</Typography.Title>
</Space>
<Button type="primary" onClick={handleRunNow} loading={runNow.isPending}>
Çàïóñòèòü ñåé÷àñ
</Button>
</Space>
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 12 }}> <Typography.Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
Ñòàòóñ: {monitorStatus?.status ?? '-'} Статус: {monitorStatus?.status ?? '-'}
{monitorStatus?.last_success_at {monitorStatus?.last_success_at
? ` · óñïåøíûé çàïóñê ${dayjs(monitorStatus.last_success_at).format('DD.MM HH:mm:ss')}` ? ` · успешный запуск ${dayjs(monitorStatus.last_success_at).format('DD.MM HH:mm:ss')}`
: ''} : ''}
</Typography.Text> </Typography.Text>
@ -143,12 +147,12 @@ export function HomeSettingsPage() {
<Alert type="error" showIcon style={{ marginBottom: 12 }} message={monitorStatus.last_error} /> <Alert type="error" showIcon style={{ marginBottom: 12 }} message={monitorStatus.last_error} />
)} )}
<Card title="Ðàñïèñàíèå ïðîâåðîê" style={{ marginBottom: 16 }}> <Card title="Расписание проверок" style={{ marginBottom: 16 }}>
<Space direction="vertical" style={{ width: '100%' }}> <Space direction="vertical" style={{ width: '100%' }}>
{(checksData?.checks ?? []).map((check) => ( {(checksData?.checks ?? []).map((check) => (
<Row key={check.check_key} gutter={12} align="middle"> <Row key={check.check_key} gutter={[12, 12]} align="middle">
<Col span={4}><Typography.Text strong>{check.check_key}</Typography.Text></Col> <Col xs={24} md={6}><Typography.Text strong>{check.check_key}</Typography.Text></Col>
<Col span={3}> <Col xs={24} md={4}>
<Switch <Switch
checked={check.enabled} checked={check.enabled}
onChange={async (enabled) => { onChange={async (enabled) => {
@ -156,7 +160,7 @@ export function HomeSettingsPage() {
}} }}
/> />
</Col> </Col>
<Col span={5}> <Col xs={24} md={6}>
<InputNumber <InputNumber
min={30} min={30}
value={check.interval_sec_default} value={check.interval_sec_default}
@ -168,7 +172,7 @@ export function HomeSettingsPage() {
}} }}
/> />
</Col> </Col>
<Col span={10}> <Col xs={24} md={8}>
<Select <Select
mode="multiple" mode="multiple"
allowClear allowClear
@ -185,10 +189,10 @@ export function HomeSettingsPage() {
</Space> </Space>
</Card> </Card>
<Card title="Ïîëèòèêà ïî òåãàì" style={{ marginBottom: 16 }}> <Card title="Политика по тегам" style={{ marginBottom: 16 }}>
<Row gutter={[12, 8]}> <Row gutter={[12, 8]}>
{(tags ?? []).map((tag) => ( {(tags ?? []).map((tag) => (
<Col span={8} key={tag.id}> <Col xs={24} md={12} xl={8} key={tag.id}>
<Space> <Space>
<Tag color={tag.color}>{tag.name}</Tag> <Tag color={tag.color}>{tag.name}</Tag>
<Switch <Switch
@ -204,16 +208,17 @@ export function HomeSettingsPage() {
</Row> </Row>
</Card> </Card>
<Card title="Ïîëèòèêà îáúåêòîâ è overrides" style={{ marginBottom: 16 }}> <Card title="Политика объектов и overrides" style={{ marginBottom: 16 }}>
<Table <Table
size="small" size="small"
rowKey="id" rowKey="id"
pagination={{ pageSize: 8 }} pagination={{ pageSize: 8 }}
dataSource={objects ?? []} dataSource={objects ?? []}
scroll={{ x: 650 }}
columns={[ columns={[
{ title: 'Îáúåêò', dataIndex: 'name', key: 'name' }, { title: 'Объект', dataIndex: 'name', key: 'name' },
{ {
title: 'Ðåæèì', title: 'Режим',
key: 'mode', key: 'mode',
render: (_: unknown, row: any) => ( render: (_: unknown, row: any) => (
<Select <Select
@ -237,14 +242,14 @@ export function HomeSettingsPage() {
<Space wrap> <Space wrap>
<Select <Select
placeholder="Îáúåêò" placeholder="Объект"
style={{ width: 220 }} style={{ width: 220 }}
value={overrideObjectId} value={overrideObjectId}
onChange={setOverrideObjectId} onChange={setOverrideObjectId}
options={(objects ?? []).map((o) => ({ value: o.id, label: o.name }))} options={(objects ?? []).map((o) => ({ value: o.id, label: o.name }))}
/> />
<Select <Select
placeholder="Ïðîâåðêà" placeholder="Проверка"
style={{ width: 160 }} style={{ width: 160 }}
value={overrideCheckKey} value={overrideCheckKey}
onChange={setOverrideCheckKey} onChange={setOverrideCheckKey}
@ -262,13 +267,13 @@ export function HomeSettingsPage() {
]} ]}
/> />
<InputNumber <InputNumber
placeholder="Èíòåðâàë (ñåê)" placeholder="Интервал (сек)"
min={30} min={30}
value={overrideInterval} value={overrideInterval}
onChange={(v) => setOverrideInterval(typeof v === 'number' ? v : undefined)} onChange={(v) => setOverrideInterval(typeof v === 'number' ? v : undefined)}
/> />
<Select <Select
placeholder="Ðåæèì êàòåãîðèé" placeholder="Режим категорий"
style={{ width: 170 }} style={{ width: 170 }}
value={overrideCategoriesMode} value={overrideCategoriesMode}
onChange={(v) => setOverrideCategoriesMode(v as OverrideCategoriesMode)} onChange={(v) => setOverrideCategoriesMode(v as OverrideCategoriesMode)}
@ -281,7 +286,7 @@ export function HomeSettingsPage() {
mode="multiple" mode="multiple"
allowClear allowClear
disabled={overrideCategoriesMode !== 'custom'} disabled={overrideCategoriesMode !== 'custom'}
placeholder="Êàòåãîðèè override" placeholder="Категории override"
style={{ width: 280 }} style={{ width: 280 }}
value={overrideCategories} value={overrideCategories}
onChange={setOverrideCategories} onChange={setOverrideCategories}
@ -291,7 +296,7 @@ export function HomeSettingsPage() {
type="primary" type="primary"
onClick={async () => { onClick={async () => {
if (!overrideObjectId || !overrideCheckKey) { if (!overrideObjectId || !overrideCheckKey) {
message.warning('Âûáåðèòå îáúåêò è ïðîâåðêó') message.warning('Выберите объект и проверку')
return return
} }
await patchOverrides.mutateAsync([ await patchOverrides.mutateAsync([
@ -309,15 +314,15 @@ export function HomeSettingsPage() {
: null, : null,
}, },
]) ])
message.success('Override îáíîâëåí') message.success('Override обновлен')
}} }}
> >
Ñîõðàíèòü override Сохранить override
</Button> </Button>
<Button <Button
onClick={async () => { onClick={async () => {
if (!overrideObjectId || !overrideCheckKey) { if (!overrideObjectId || !overrideCheckKey) {
message.warning('Âûáåðèòå îáúåêò è ïðîâåðêó') message.warning('Выберите объект и проверку')
return return
} }
await patchOverrides.mutateAsync([ await patchOverrides.mutateAsync([
@ -329,10 +334,10 @@ export function HomeSettingsPage() {
categories_override: null, categories_override: null,
}, },
]) ])
message.success('Override î÷èùåí') message.success('Override очищен')
}} }}
> >
Î÷èñòèòü override Очистить override
</Button> </Button>
</Space> </Space>
@ -343,14 +348,15 @@ export function HomeSettingsPage() {
rowKey={(row) => `${row.object_id}:${row.check_key}`} rowKey={(row) => `${row.object_id}:${row.check_key}`}
pagination={{ pageSize: 6 }} pagination={{ pageSize: 6 }}
dataSource={checksData?.object_overrides ?? []} dataSource={checksData?.object_overrides ?? []}
scroll={{ x: 900 }}
columns={[ columns={[
{ {
title: 'Îáúåêò', title: 'Объект',
key: 'object', key: 'object',
render: (_: unknown, row: DashboardObjectOverride) => render: (_: unknown, row: DashboardObjectOverride) =>
objects?.find((o) => o.id === row.object_id)?.name ?? row.object_id, objects?.find((o) => o.id === row.object_id)?.name ?? row.object_id,
}, },
{ title: 'Ïðîâåðêà', dataIndex: 'check_key', key: 'check_key' }, { title: 'Проверка', dataIndex: 'check_key', key: 'check_key' },
{ {
title: 'Enabled', title: 'Enabled',
key: 'enabled_override', key: 'enabled_override',
@ -358,13 +364,13 @@ export function HomeSettingsPage() {
row.enabled_override == null ? 'inherit' : row.enabled_override ? 'enabled' : 'disabled', row.enabled_override == null ? 'inherit' : row.enabled_override ? 'enabled' : 'disabled',
}, },
{ {
title: 'Èíòåðâàë', title: 'Интервал',
key: 'interval_sec_override', key: 'interval_sec_override',
render: (_: unknown, row: DashboardObjectOverride) => render: (_: unknown, row: DashboardObjectOverride) =>
row.interval_sec_override == null ? 'inherit' : `${row.interval_sec_override}s`, row.interval_sec_override == null ? 'inherit' : `${row.interval_sec_override}s`,
}, },
{ {
title: 'Êàòåãîðèè', title: 'Категории',
key: 'categories_override', key: 'categories_override',
render: (_: unknown, row: DashboardObjectOverride) => render: (_: unknown, row: DashboardObjectOverride) =>
row.categories_override == null row.categories_override == null