+мониторинг отображение
This commit is contained in:
parent
011a10641f
commit
b230025ab4
5 changed files with 200 additions and 6 deletions
|
|
@ -472,6 +472,20 @@ async def run_dashboard_monitor_cycle(
|
||||||
logger.info("Dashboard monitor: %d devices loaded for checking", len(device_rows))
|
logger.info("Dashboard monitor: %d devices loaded for checking", len(device_rows))
|
||||||
|
|
||||||
checks = (await db.execute(select(DashboardCheckDefinition))).scalars().all()
|
checks = (await db.execute(select(DashboardCheckDefinition))).scalars().all()
|
||||||
|
|
||||||
|
await sse_manager.publish(
|
||||||
|
SSEEvent(
|
||||||
|
type="dashboard_monitor_cycle_start",
|
||||||
|
task_id=DASHBOARD_MONITOR_CHANNEL,
|
||||||
|
data={
|
||||||
|
"run_type": run_type,
|
||||||
|
"objects_allowed": len(allowed_object_ids),
|
||||||
|
"devices_total": len(device_rows),
|
||||||
|
"checks_planned": [c.check_key for c in checks if c.enabled],
|
||||||
|
"ts": datetime.now(timezone.utc).isoformat(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
overrides = (
|
overrides = (
|
||||||
await db.execute(
|
await db.execute(
|
||||||
select(DashboardObjectCheckOverride).where(
|
select(DashboardObjectCheckOverride).where(
|
||||||
|
|
@ -625,6 +639,7 @@ async def run_dashboard_monitor_cycle(
|
||||||
now=checked_at,
|
now=checked_at,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
check_failed = sum(1 for r in results if isinstance(r, Exception) or (not isinstance(r, Exception) and not r[0]))
|
||||||
await sse_manager.publish(
|
await sse_manager.publish(
|
||||||
SSEEvent(
|
SSEEvent(
|
||||||
type="dashboard_monitor_update",
|
type="dashboard_monitor_update",
|
||||||
|
|
@ -632,6 +647,8 @@ async def run_dashboard_monitor_cycle(
|
||||||
data={
|
data={
|
||||||
"check_key": check.check_key,
|
"check_key": check.check_key,
|
||||||
"processed": len(due_devices),
|
"processed": len(due_devices),
|
||||||
|
"due": len(due_devices),
|
||||||
|
"failed": check_failed,
|
||||||
"ts": datetime.now(timezone.utc).isoformat(),
|
"ts": datetime.now(timezone.utc).isoformat(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,12 @@ export interface DashboardMonitorStatus {
|
||||||
last_counts: Record<string, unknown> | null
|
last_counts: Record<string, unknown> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DashboardRunResponse {
|
||||||
|
accepted: boolean
|
||||||
|
running: boolean
|
||||||
|
detail: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface DashboardObjectPolicy {
|
export interface DashboardObjectPolicy {
|
||||||
object_id: number
|
object_id: number
|
||||||
mode: 'inherit' | 'include' | 'exclude'
|
mode: 'inherit' | 'include' | 'exclude'
|
||||||
|
|
@ -249,7 +255,7 @@ export const useDashboardRunNow = () => {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (body: DashboardScope & { force?: boolean }) =>
|
mutationFn: (body: DashboardScope & { force?: boolean }) =>
|
||||||
api.post('/api/v1/dashboard/monitor/run', body).then((r) => r.data),
|
api.post<DashboardRunResponse>('/api/v1/dashboard/monitor/run', body).then((r) => r.data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
qc.invalidateQueries({ queryKey: ['dashboard-monitor-status'] })
|
qc.invalidateQueries({ queryKey: ['dashboard-monitor-status'] })
|
||||||
qc.invalidateQueries({ queryKey: ['dashboard-home-summary'] })
|
qc.invalidateQueries({ queryKey: ['dashboard-home-summary'] })
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export function useDashboardMonitorSSE(handlers: DashboardMonitorHandlers) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(ev.data)
|
const data = JSON.parse(ev.data)
|
||||||
if (
|
if (
|
||||||
|
ev.event === 'dashboard_monitor_cycle_start' ||
|
||||||
ev.event === 'dashboard_monitor_update' ||
|
ev.event === 'dashboard_monitor_update' ||
|
||||||
ev.event === 'dashboard_monitor_cycle_done' ||
|
ev.event === 'dashboard_monitor_cycle_done' ||
|
||||||
ev.event === 'dashboard_monitor_cycle_failed'
|
ev.event === 'dashboard_monitor_cycle_failed'
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,12 @@ export function HomePage() {
|
||||||
|
|
||||||
const handleRunNow = async () => {
|
const handleRunNow = async () => {
|
||||||
try {
|
try {
|
||||||
await runNow.mutateAsync({ ...scope, force: true })
|
const result = await runNow.mutateAsync({ ...scope, force: true })
|
||||||
|
if (!result.accepted) {
|
||||||
|
message.warning('Мониторинг уже запущен')
|
||||||
|
} else {
|
||||||
message.success('Мониторинг запущен')
|
message.success('Мониторинг запущен')
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
message.error('Не удалось запустить мониторинг')
|
message.error('Не удалось запустить мониторинг')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import { ArrowLeftOutlined } from '@ant-design/icons'
|
import { ArrowLeftOutlined, CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Col,
|
Col,
|
||||||
Collapse,
|
Collapse,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
|
Progress,
|
||||||
Row,
|
Row,
|
||||||
Space,
|
Space,
|
||||||
Switch,
|
Switch,
|
||||||
|
|
@ -27,6 +29,7 @@ import {
|
||||||
usePatchDashboardObjectPolicy,
|
usePatchDashboardObjectPolicy,
|
||||||
usePatchDashboardTagPolicy,
|
usePatchDashboardTagPolicy,
|
||||||
} from '../api/dashboard'
|
} from '../api/dashboard'
|
||||||
|
import { useDashboardMonitorSSE } from '../hooks/useDashboardMonitorSSE'
|
||||||
import { useTags } from '../api/tags'
|
import { useTags } from '../api/tags'
|
||||||
|
|
||||||
function cityKey(city: string | null): string {
|
function cityKey(city: string | null): string {
|
||||||
|
|
@ -39,6 +42,36 @@ function formatCheckTitle(checkKey: string): string {
|
||||||
return checkKey
|
return checkKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CheckProgress {
|
||||||
|
processed: number
|
||||||
|
due: number
|
||||||
|
failed: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RunProgress {
|
||||||
|
phase: 'idle' | 'running' | 'done' | 'failed'
|
||||||
|
startedAt: string | null
|
||||||
|
objectsAllowed: number | null
|
||||||
|
devicesTotal: number | null
|
||||||
|
checksPlanned: string[]
|
||||||
|
checkProgress: Record<string, CheckProgress>
|
||||||
|
counts: Record<string, number> | null
|
||||||
|
error: string | null
|
||||||
|
finishedAt: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_PROGRESS: RunProgress = {
|
||||||
|
phase: 'idle',
|
||||||
|
startedAt: null,
|
||||||
|
objectsAllowed: null,
|
||||||
|
devicesTotal: null,
|
||||||
|
checksPlanned: [],
|
||||||
|
checkProgress: {},
|
||||||
|
counts: null,
|
||||||
|
error: null,
|
||||||
|
finishedAt: null,
|
||||||
|
}
|
||||||
|
|
||||||
export function HomeSettingsPage() {
|
export function HomeSettingsPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
|
@ -54,6 +87,52 @@ export function HomeSettingsPage() {
|
||||||
const patchChecks = usePatchDashboardChecks()
|
const patchChecks = usePatchDashboardChecks()
|
||||||
|
|
||||||
const [checkIntervalsMin, setCheckIntervalsMin] = useState<Record<string, number>>({})
|
const [checkIntervalsMin, setCheckIntervalsMin] = useState<Record<string, number>>({})
|
||||||
|
const [runProgress, setRunProgress] = useState<RunProgress>(EMPTY_PROGRESS)
|
||||||
|
|
||||||
|
useDashboardMonitorSSE({
|
||||||
|
onUpdate: (eventType, data) => {
|
||||||
|
if (eventType === 'dashboard_monitor_cycle_start') {
|
||||||
|
setRunProgress({
|
||||||
|
phase: 'running',
|
||||||
|
startedAt: (data.ts as string) ?? null,
|
||||||
|
objectsAllowed: (data.objects_allowed as number) ?? null,
|
||||||
|
devicesTotal: (data.devices_total as number) ?? null,
|
||||||
|
checksPlanned: (data.checks_planned as string[]) ?? [],
|
||||||
|
checkProgress: {},
|
||||||
|
counts: null,
|
||||||
|
error: null,
|
||||||
|
finishedAt: null,
|
||||||
|
})
|
||||||
|
} else if (eventType === 'dashboard_monitor_update') {
|
||||||
|
const key = data.check_key as string
|
||||||
|
setRunProgress((prev) => ({
|
||||||
|
...prev,
|
||||||
|
checkProgress: {
|
||||||
|
...prev.checkProgress,
|
||||||
|
[key]: {
|
||||||
|
processed: (data.processed as number) ?? 0,
|
||||||
|
due: (data.due as number) ?? 0,
|
||||||
|
failed: (data.failed as number) ?? 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
} else if (eventType === 'dashboard_monitor_cycle_done') {
|
||||||
|
setRunProgress((prev) => ({
|
||||||
|
...prev,
|
||||||
|
phase: 'done',
|
||||||
|
counts: (data.counts as Record<string, number>) ?? null,
|
||||||
|
finishedAt: (data.ts as string) ?? null,
|
||||||
|
}))
|
||||||
|
} else if (eventType === 'dashboard_monitor_cycle_failed') {
|
||||||
|
setRunProgress((prev) => ({
|
||||||
|
...prev,
|
||||||
|
phase: 'failed',
|
||||||
|
error: (data.error as string) ?? 'Неизвестная ошибка',
|
||||||
|
finishedAt: (data.ts as string) ?? null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const next: Record<string, number> = {}
|
const next: Record<string, number> = {}
|
||||||
|
|
@ -174,8 +253,12 @@ export function HomeSettingsPage() {
|
||||||
|
|
||||||
const handleRunNow = async () => {
|
const handleRunNow = async () => {
|
||||||
try {
|
try {
|
||||||
await runNow.mutateAsync({ force: true })
|
const result = await runNow.mutateAsync({ force: true })
|
||||||
|
if (!result.accepted) {
|
||||||
|
message.warning('Мониторинг уже запущен')
|
||||||
|
} else {
|
||||||
message.success('Мониторинг запущен')
|
message.success('Мониторинг запущен')
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
message.error('Не удалось запустить мониторинг')
|
message.error('Не удалось запустить мониторинг')
|
||||||
}
|
}
|
||||||
|
|
@ -216,6 +299,89 @@ 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} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{runProgress.phase !== 'idle' && (
|
||||||
|
<Card
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
{runProgress.phase === 'running' && <LoadingOutlined style={{ color: '#1677ff' }} />}
|
||||||
|
{runProgress.phase === 'done' && <CheckCircleOutlined style={{ color: '#52c41a' }} />}
|
||||||
|
{runProgress.phase === 'failed' && <CloseCircleOutlined style={{ color: '#ff4d4f' }} />}
|
||||||
|
<Typography.Text strong>Текущий прогон</Typography.Text>
|
||||||
|
<Badge
|
||||||
|
status={runProgress.phase === 'running' ? 'processing' : runProgress.phase === 'done' ? 'success' : 'error'}
|
||||||
|
text={runProgress.phase === 'running' ? 'выполняется' : runProgress.phase === 'done' ? 'завершён' : 'ошибка'}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size={8}>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
{runProgress.objectsAllowed != null && `Объектов: ${runProgress.objectsAllowed}`}
|
||||||
|
{runProgress.devicesTotal != null && ` • Устройств: ${runProgress.devicesTotal}`}
|
||||||
|
{runProgress.startedAt && ` • Начат: ${dayjs(runProgress.startedAt).format('HH:mm:ss')}`}
|
||||||
|
{runProgress.finishedAt && ` • Завершён: ${dayjs(runProgress.finishedAt).format('HH:mm:ss')}`}
|
||||||
|
</Typography.Text>
|
||||||
|
|
||||||
|
{runProgress.checksPlanned.map((checkKey) => {
|
||||||
|
const cp = runProgress.checkProgress[checkKey]
|
||||||
|
const due = cp?.due ?? 0
|
||||||
|
const processed = cp?.processed ?? 0
|
||||||
|
const failed = cp?.failed ?? 0
|
||||||
|
const pct = due > 0 ? Math.round((processed / due) * 100) : runProgress.phase === 'done' ? 100 : 0
|
||||||
|
return (
|
||||||
|
<div key={checkKey}>
|
||||||
|
<Row justify="space-between" style={{ marginBottom: 2 }}>
|
||||||
|
<Col>
|
||||||
|
<Typography.Text>{formatCheckTitle(checkKey)}</Typography.Text>
|
||||||
|
{failed > 0 && (
|
||||||
|
<Tag color="red" style={{ marginLeft: 8 }}>
|
||||||
|
{failed} ошибок
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
{cp ? `${processed} / ${due}` : '—'}
|
||||||
|
</Typography.Text>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Progress
|
||||||
|
percent={pct}
|
||||||
|
status={runProgress.phase === 'failed' ? 'exception' : failed > 0 ? 'normal' : undefined}
|
||||||
|
strokeColor={failed > 0 ? '#faad14' : undefined}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
{runProgress.phase === 'done' && runProgress.counts && (
|
||||||
|
<Row gutter={16} style={{ marginTop: 8 }}>
|
||||||
|
<Col>
|
||||||
|
<Typography.Text type="secondary">Проверено: </Typography.Text>
|
||||||
|
<Typography.Text strong>{runProgress.counts.devices_checked ?? 0}</Typography.Text>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Typography.Text type="secondary">Ошибок: </Typography.Text>
|
||||||
|
<Typography.Text strong style={{ color: (runProgress.counts.failed ?? 0) > 0 ? '#ff4d4f' : undefined }}>
|
||||||
|
{runProgress.counts.failed ?? 0}
|
||||||
|
</Typography.Text>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Typography.Text type="secondary">Пропущено: </Typography.Text>
|
||||||
|
<Typography.Text strong>{runProgress.counts.skipped_not_due ?? 0}</Typography.Text>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{runProgress.phase === 'failed' && runProgress.error && (
|
||||||
|
<Alert type="error" showIcon message={runProgress.error} style={{ marginTop: 8 }} />
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
<Card title="Плагины мониторинга" style={{ marginBottom: 16 }} loading={checksLoading}>
|
<Card title="Плагины мониторинга" style={{ marginBottom: 16 }} loading={checksLoading}>
|
||||||
{(checksData?.checks ?? []).length === 0 ? (
|
{(checksData?.checks ?? []).length === 0 ? (
|
||||||
<Alert
|
<Alert
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue