diff --git a/backend/app/routers/snapshot_policies.py b/backend/app/routers/snapshot_policies.py index e9c2f32..ee3f234 100644 --- a/backend/app/routers/snapshot_policies.py +++ b/backend/app/routers/snapshot_policies.py @@ -269,7 +269,7 @@ async def dashboard_devices( # All eligible devices for this object+category devices_q = ( - select(Device.id, Device.ip, Device.hostname) + select(Device.id, Device.ip, Device.hostname, Device.status) .where( Device.object_id == obj_id, Device.category == policy.category, @@ -324,6 +324,7 @@ async def dashboard_devices( "device_id": d.id, "ip": d.ip, "hostname": d.hostname, + "status": d.status, "has_snapshot": has_snapshot, "last_collected_at": latest_map.get(d.id), "snapshot_id": snap_info[0] if snap_info else None, diff --git a/frontend/src/api/snapshotPolicies.ts b/frontend/src/api/snapshotPolicies.ts index e85c053..b3a219d 100644 --- a/frontend/src/api/snapshotPolicies.ts +++ b/frontend/src/api/snapshotPolicies.ts @@ -135,6 +135,7 @@ export interface DeviceSnapshotStatus { device_id: number ip: string hostname: string | null + status: string | null has_snapshot: boolean last_collected_at: string | null snapshot_id: number | null diff --git a/frontend/src/pages/SnapshotDashboard.tsx b/frontend/src/pages/SnapshotDashboard.tsx index 79ddb16..338e6e7 100644 --- a/frontend/src/pages/SnapshotDashboard.tsx +++ b/frontend/src/pages/SnapshotDashboard.tsx @@ -291,13 +291,12 @@ export function SnapshotDashboardPage() { {objects.length} ), - children: objects.map((obj) => ( - handleCollectObj(policyId, obj.object_id)} + children: ( + - )), + ), }))} /> @@ -331,51 +330,58 @@ export function SnapshotDashboardPage() { ) } -function ObjectPoliciesTable({ - obj, +function ObjectsTable({ + objects, onCollect, }: { - obj: ObjectSnapshotSummary - onCollect: (policyId: number) => void + objects: ObjectSnapshotSummary[] + onCollect: (policyId: number, objId: number) => void }) { - const [expandedKey, setExpandedKey] = useState(null) + const [expandedKeys, setExpandedKeys] = useState([]) - const columns: ColumnsType = [ + const columns: ColumnsType = [ { - title: 'Политика', - dataIndex: 'policy_label', - key: 'label', - }, - { - title: 'Категория', - dataIndex: 'category', - key: 'category', - render: (cat: string) => {cat}, + title: 'Объект', + key: 'name', + render: (_, obj) => ( + + {obj.object_name} + {obj.client && — {obj.client}} + + ), }, { title: 'Последний сбор', - dataIndex: 'last_collected_at', key: 'last_collected', - render: (val: string | null) => - val ? ( - - {dayjs(val).fromNow()} + width: 160, + render: (_, obj) => { + const latest = obj.policies.reduce((acc, p) => { + if (!p.last_collected_at) return acc + if (!acc) return p.last_collected_at + return p.last_collected_at > acc ? p.last_collected_at : acc + }, null) + return latest ? ( + + {dayjs(latest).fromNow()} ) : ( — - ), + ) + }, }, { title: 'Покрытие', key: 'coverage', - render: (_, row) => { - const { success_count, total_devices } = row - if (total_devices === 0) return нет устройств - const ratio = success_count / total_devices + width: 120, + render: (_, obj) => { + const total = obj.policies.reduce((s, p) => s + p.total_devices, 0) + const success = obj.policies.reduce((s, p) => s + p.success_count, 0) + if (total === 0) return нет устройств + const ratio = success / total const color = ratio >= 1 ? 'green' : ratio > 0 ? 'orange' : 'red' return ( - {success_count} / {total_devices} + {success} / {total} ) }, @@ -383,16 +389,16 @@ function ObjectPoliciesTable({ { title: '', key: 'actions', - width: 60, - render: (_, row) => ( - + width: 50, + render: (_, obj) => ( + } onClick={(e) => { e.stopPropagation() - onCollect(row.policy_id) + obj.policies.forEach((p) => onCollect(p.policy_id, obj.object_id)) }} /> @@ -401,30 +407,71 @@ function ObjectPoliciesTable({ ] return ( - - - {obj.object_name} - {obj.client && — {obj.client}} - - setExpandedKey(expanded ? record.policy_id : null), - expandedRowRender: (record) => ( - + + setExpandedKeys( + expanded + ? [...expandedKeys, record.object_id] + : expandedKeys.filter((k) => k !== record.object_id) ), - }} + expandedRowRender: (record) => ( + + ), + }} + /> + ) +} + +function ObjectDevicesExpanded({ + obj, + onCollect, +}: { + obj: ObjectSnapshotSummary + onCollect: (policyId: number, objId: number) => void +}) { + if (obj.policies.length === 1) { + return ( + + ) + } + + return ( + + {obj.policies.map((p) => ( + + + {p.policy_label} + {p.category} + + } + onClick={() => onCollect(p.policy_id, obj.object_id)} + /> + + + + + ))} ) } @@ -483,14 +530,13 @@ function DevicesList({ render: (val: string | null) => val || —, }, { - title: 'Статус', + title: 'Снапшот', key: 'status', - render: (_, row) => - row.has_snapshot ? ( - Есть - ) : ( - Нет - ), + render: (_, row) => { + if (row.has_snapshot) return Есть + if (row.status === 'offline') return Нет пинга + return Нет снапшота + }, }, { title: 'Собран',