+мониторинг отображение
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))
|
||||
|
||||
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 = (
|
||||
await db.execute(
|
||||
select(DashboardObjectCheckOverride).where(
|
||||
|
|
@ -625,6 +639,7 @@ async def run_dashboard_monitor_cycle(
|
|||
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(
|
||||
SSEEvent(
|
||||
type="dashboard_monitor_update",
|
||||
|
|
@ -632,6 +647,8 @@ async def run_dashboard_monitor_cycle(
|
|||
data={
|
||||
"check_key": check.check_key,
|
||||
"processed": len(due_devices),
|
||||
"due": len(due_devices),
|
||||
"failed": check_failed,
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@ export interface DashboardMonitorStatus {
|
|||
last_counts: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export interface DashboardRunResponse {
|
||||
accepted: boolean
|
||||
running: boolean
|
||||
detail: string
|
||||
}
|
||||
|
||||
export interface DashboardObjectPolicy {
|
||||
object_id: number
|
||||
mode: 'inherit' | 'include' | 'exclude'
|
||||
|
|
@ -249,7 +255,7 @@ export const useDashboardRunNow = () => {
|
|||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
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: () => {
|
||||
qc.invalidateQueries({ queryKey: ['dashboard-monitor-status'] })
|
||||
qc.invalidateQueries({ queryKey: ['dashboard-home-summary'] })
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export function useDashboardMonitorSSE(handlers: DashboardMonitorHandlers) {
|
|||
try {
|
||||
const data = JSON.parse(ev.data)
|
||||
if (
|
||||
ev.event === 'dashboard_monitor_cycle_start' ||
|
||||
ev.event === 'dashboard_monitor_update' ||
|
||||
ev.event === 'dashboard_monitor_cycle_done' ||
|
||||
ev.event === 'dashboard_monitor_cycle_failed'
|
||||
|
|
|
|||
|
|
@ -124,8 +124,12 @@ export function HomePage() {
|
|||
|
||||
const handleRunNow = async () => {
|
||||
try {
|
||||
await runNow.mutateAsync({ ...scope, force: true })
|
||||
message.success('Мониторинг запущен')
|
||||
const result = await runNow.mutateAsync({ ...scope, force: true })
|
||||
if (!result.accepted) {
|
||||
message.warning('Мониторинг уже запущен')
|
||||
} else {
|
||||
message.success('Мониторинг запущен')
|
||||
}
|
||||
} catch {
|
||||
message.error('Не удалось запустить мониторинг')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { ArrowLeftOutlined } from '@ant-design/icons'
|
||||
import { ArrowLeftOutlined, CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Col,
|
||||
Collapse,
|
||||
InputNumber,
|
||||
Progress,
|
||||
Row,
|
||||
Space,
|
||||
Switch,
|
||||
|
|
@ -27,6 +29,7 @@ import {
|
|||
usePatchDashboardObjectPolicy,
|
||||
usePatchDashboardTagPolicy,
|
||||
} from '../api/dashboard'
|
||||
import { useDashboardMonitorSSE } from '../hooks/useDashboardMonitorSSE'
|
||||
import { useTags } from '../api/tags'
|
||||
|
||||
function cityKey(city: string | null): string {
|
||||
|
|
@ -39,6 +42,36 @@ function formatCheckTitle(checkKey: string): string {
|
|||
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() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
|
|
@ -54,6 +87,52 @@ export function HomeSettingsPage() {
|
|||
const patchChecks = usePatchDashboardChecks()
|
||||
|
||||
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(() => {
|
||||
const next: Record<string, number> = {}
|
||||
|
|
@ -174,8 +253,12 @@ export function HomeSettingsPage() {
|
|||
|
||||
const handleRunNow = async () => {
|
||||
try {
|
||||
await runNow.mutateAsync({ force: true })
|
||||
message.success('Мониторинг запущен')
|
||||
const result = await runNow.mutateAsync({ force: true })
|
||||
if (!result.accepted) {
|
||||
message.warning('Мониторинг уже запущен')
|
||||
} else {
|
||||
message.success('Мониторинг запущен')
|
||||
}
|
||||
} catch {
|
||||
message.error('Не удалось запустить мониторинг')
|
||||
}
|
||||
|
|
@ -216,6 +299,89 @@ export function HomeSettingsPage() {
|
|||
<Alert type="error" showIcon style={{ marginBottom: 12 }} message={monitorStatus.last_error} />
|
||||
) : 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}>
|
||||
{(checksData?.checks ?? []).length === 0 ? (
|
||||
<Alert
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue