фикс фикс фикс фикс фикс фикс

This commit is contained in:
dv 2026-05-28 16:13:57 +03:00
parent 057a798f3c
commit 2cc1c71cfa
3 changed files with 114 additions and 66 deletions

View file

@ -269,7 +269,7 @@ async def dashboard_devices(
# All eligible devices for this object+category # All eligible devices for this object+category
devices_q = ( devices_q = (
select(Device.id, Device.ip, Device.hostname) select(Device.id, Device.ip, Device.hostname, Device.status)
.where( .where(
Device.object_id == obj_id, Device.object_id == obj_id,
Device.category == policy.category, Device.category == policy.category,
@ -324,6 +324,7 @@ async def dashboard_devices(
"device_id": d.id, "device_id": d.id,
"ip": d.ip, "ip": d.ip,
"hostname": d.hostname, "hostname": d.hostname,
"status": d.status,
"has_snapshot": has_snapshot, "has_snapshot": has_snapshot,
"last_collected_at": latest_map.get(d.id), "last_collected_at": latest_map.get(d.id),
"snapshot_id": snap_info[0] if snap_info else None, "snapshot_id": snap_info[0] if snap_info else None,

View file

@ -135,6 +135,7 @@ export interface DeviceSnapshotStatus {
device_id: number device_id: number
ip: string ip: string
hostname: string | null hostname: string | null
status: string | null
has_snapshot: boolean has_snapshot: boolean
last_collected_at: string | null last_collected_at: string | null
snapshot_id: number | null snapshot_id: number | null

View file

@ -291,13 +291,12 @@ export function SnapshotDashboardPage() {
<Tag>{objects.length}</Tag> <Tag>{objects.length}</Tag>
</Space> </Space>
), ),
children: objects.map((obj) => ( children: (
<ObjectPoliciesTable <ObjectsTable
key={obj.object_id} objects={objects}
obj={obj} onCollect={handleCollectObj}
onCollect={(policyId) => handleCollectObj(policyId, obj.object_id)}
/> />
)), ),
}))} }))}
/> />
@ -331,51 +330,58 @@ export function SnapshotDashboardPage() {
) )
} }
function ObjectPoliciesTable({ function ObjectsTable({
obj, objects,
onCollect, onCollect,
}: { }: {
obj: ObjectSnapshotSummary objects: ObjectSnapshotSummary[]
onCollect: (policyId: number) => void onCollect: (policyId: number, objId: number) => void
}) { }) {
const [expandedKey, setExpandedKey] = useState<number | null>(null) const [expandedKeys, setExpandedKeys] = useState<number[]>([])
const columns: ColumnsType<PolicySnapshotStatus> = [ const columns: ColumnsType<ObjectSnapshotSummary> = [
{ {
title: 'Политика', title: 'Объект',
dataIndex: 'policy_label', key: 'name',
key: 'label', render: (_, obj) => (
}, <span>
{ <Typography.Text strong>{obj.object_name}</Typography.Text>
title: 'Категория', {obj.client && <Typography.Text type="secondary"> {obj.client}</Typography.Text>}
dataIndex: 'category', </span>
key: 'category', ),
render: (cat: string) => <Tag>{cat}</Tag>,
}, },
{ {
title: 'Последний сбор', title: 'Последний сбор',
dataIndex: 'last_collected_at',
key: 'last_collected', key: 'last_collected',
render: (val: string | null) => width: 160,
val ? ( render: (_, obj) => {
<Tooltip title={dayjs(val).format('DD.MM.YYYY HH:mm:ss')}> const latest = obj.policies.reduce<string | null>((acc, p) => {
{dayjs(val).fromNow()} 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 ? (
<Tooltip title={dayjs(latest).format('DD.MM.YYYY HH:mm:ss')}>
{dayjs(latest).fromNow()}
</Tooltip> </Tooltip>
) : ( ) : (
<Typography.Text type="secondary"></Typography.Text> <Typography.Text type="secondary"></Typography.Text>
), )
},
}, },
{ {
title: 'Покрытие', title: 'Покрытие',
key: 'coverage', key: 'coverage',
render: (_, row) => { width: 120,
const { success_count, total_devices } = row render: (_, obj) => {
if (total_devices === 0) return <Tag>нет устройств</Tag> const total = obj.policies.reduce((s, p) => s + p.total_devices, 0)
const ratio = success_count / total_devices const success = obj.policies.reduce((s, p) => s + p.success_count, 0)
if (total === 0) return <Tag>нет устройств</Tag>
const ratio = success / total
const color = ratio >= 1 ? 'green' : ratio > 0 ? 'orange' : 'red' const color = ratio >= 1 ? 'green' : ratio > 0 ? 'orange' : 'red'
return ( return (
<Tag color={color}> <Tag color={color}>
{success_count} / {total_devices} {success} / {total}
</Tag> </Tag>
) )
}, },
@ -383,16 +389,16 @@ function ObjectPoliciesTable({
{ {
title: '', title: '',
key: 'actions', key: 'actions',
width: 60, width: 50,
render: (_, row) => ( render: (_, obj) => (
<Tooltip title="Собрать для этого объекта"> <Tooltip title="Собрать все политики для объекта">
<Button <Button
type="text" type="text"
size="small" size="small"
icon={<PlayCircleOutlined />} icon={<PlayCircleOutlined />}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onCollect(row.policy_id) obj.policies.forEach((p) => onCollect(p.policy_id, obj.object_id))
}} }}
/> />
</Tooltip> </Tooltip>
@ -401,30 +407,71 @@ function ObjectPoliciesTable({
] ]
return ( return (
<div style={{ marginBottom: 16 }}> <Table
<Typography.Text strong> dataSource={objects}
{obj.object_name} columns={columns}
{obj.client && <Typography.Text type="secondary"> {obj.client}</Typography.Text>} rowKey="object_id"
</Typography.Text> size="small"
<Table pagination={false}
dataSource={obj.policies} expandable={{
columns={columns} expandedRowKeys: expandedKeys,
rowKey="policy_id" onExpand: (expanded, record) =>
size="small" setExpandedKeys(
pagination={false} expanded
style={{ marginTop: 4 }} ? [...expandedKeys, record.object_id]
expandable={{ : expandedKeys.filter((k) => k !== record.object_id)
expandedRowKeys: expandedKey !== null ? [expandedKey] : [],
onExpand: (expanded, record) => setExpandedKey(expanded ? record.policy_id : null),
expandedRowRender: (record) => (
<DevicesList
objId={obj.object_id}
objName={obj.object_name}
policyId={record.policy_id}
/>
), ),
}} expandedRowRender: (record) => (
<ObjectDevicesExpanded
obj={record}
onCollect={onCollect}
/>
),
}}
/>
)
}
function ObjectDevicesExpanded({
obj,
onCollect,
}: {
obj: ObjectSnapshotSummary
onCollect: (policyId: number, objId: number) => void
}) {
if (obj.policies.length === 1) {
return (
<DevicesList
objId={obj.object_id}
objName={obj.object_name}
policyId={obj.policies[0].policy_id}
/> />
)
}
return (
<div>
{obj.policies.map((p) => (
<div key={p.policy_id} style={{ marginBottom: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<Typography.Text strong>{p.policy_label}</Typography.Text>
<Tag>{p.category}</Tag>
<Tooltip title="Собрать">
<Button
type="text"
size="small"
icon={<PlayCircleOutlined />}
onClick={() => onCollect(p.policy_id, obj.object_id)}
/>
</Tooltip>
</div>
<DevicesList
objId={obj.object_id}
objName={obj.object_name}
policyId={p.policy_id}
/>
</div>
))}
</div> </div>
) )
} }
@ -483,14 +530,13 @@ function DevicesList({
render: (val: string | null) => val || <Typography.Text type="secondary"></Typography.Text>, render: (val: string | null) => val || <Typography.Text type="secondary"></Typography.Text>,
}, },
{ {
title: 'Статус', title: 'Снапшот',
key: 'status', key: 'status',
render: (_, row) => render: (_, row) => {
row.has_snapshot ? ( if (row.has_snapshot) return <Tag color="green">Есть</Tag>
<Tag color="green">Есть</Tag> if (row.status === 'offline') return <Tag color="red">Нет пинга</Tag>
) : ( return <Tag color="orange">Нет снапшота</Tag>
<Tag color="red">Нет</Tag> },
),
}, },
{ {
title: 'Собран', title: 'Собран',