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

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
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,

View file

@ -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

View file

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