фикс фикс фикс фикс фикса
This commit is contained in:
parent
c03e542f81
commit
057a798f3c
3 changed files with 264 additions and 2 deletions
|
|
@ -1,3 +1,5 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy import select, func
|
from sqlalchemy import select, func
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
@ -5,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from app.dependencies import get_current_user, get_db
|
from app.dependencies import get_current_user, get_db
|
||||||
from app.models.device import Device
|
from app.models.device import Device
|
||||||
from app.models.object import Object
|
from app.models.object import Object
|
||||||
from app.models.snapshot import ConfigSnapshot
|
from app.models.snapshot import ConfigSnapshot, SnapshotContent
|
||||||
from app.models.snapshot_policy import SnapshotPolicy
|
from app.models.snapshot_policy import SnapshotPolicy
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.schemas.snapshot_policy import (
|
from app.schemas.snapshot_policy import (
|
||||||
|
|
@ -247,3 +249,103 @@ async def collect_for_object(
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
)
|
)
|
||||||
return {"task_id": task.id}
|
return {"task_id": task.id}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Device details per object+policy ────────────────────────────────────────
|
||||||
|
|
||||||
|
@router.get("/snapshots/dashboard/{obj_id}/{policy_id}/devices")
|
||||||
|
async def dashboard_devices(
|
||||||
|
obj_id: int,
|
||||||
|
policy_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
_: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Return per-device snapshot status for a given object and policy."""
|
||||||
|
policy = await db.get(SnapshotPolicy, policy_id)
|
||||||
|
if policy is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Policy not found")
|
||||||
|
|
||||||
|
snap_label = _policy_snapshot_label(policy)
|
||||||
|
|
||||||
|
# All eligible devices for this object+category
|
||||||
|
devices_q = (
|
||||||
|
select(Device.id, Device.ip, Device.hostname)
|
||||||
|
.where(
|
||||||
|
Device.object_id == obj_id,
|
||||||
|
Device.category == policy.category,
|
||||||
|
Device.is_active == True,
|
||||||
|
)
|
||||||
|
.order_by(Device.ip)
|
||||||
|
)
|
||||||
|
devices_result = await db.execute(devices_q)
|
||||||
|
devices = devices_result.all()
|
||||||
|
|
||||||
|
if not devices:
|
||||||
|
return []
|
||||||
|
|
||||||
|
device_ids = [d.id for d in devices]
|
||||||
|
|
||||||
|
# Latest snapshot per device matching this label
|
||||||
|
latest_snap_q = (
|
||||||
|
select(
|
||||||
|
ConfigSnapshot.device_id,
|
||||||
|
func.max(ConfigSnapshot.created_at).label("last_at"),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
ConfigSnapshot.device_id.in_(device_ids),
|
||||||
|
ConfigSnapshot.label == snap_label,
|
||||||
|
)
|
||||||
|
.group_by(ConfigSnapshot.device_id)
|
||||||
|
)
|
||||||
|
latest_result = await db.execute(latest_snap_q)
|
||||||
|
latest_map: dict[int, object] = {r.device_id: r.last_at for r in latest_result.all()}
|
||||||
|
|
||||||
|
# Get snapshot IDs for content access (latest per device)
|
||||||
|
snap_ids_q = (
|
||||||
|
select(ConfigSnapshot.id, ConfigSnapshot.device_id, ConfigSnapshot.sha256)
|
||||||
|
.where(
|
||||||
|
ConfigSnapshot.device_id.in_(device_ids),
|
||||||
|
ConfigSnapshot.label == snap_label,
|
||||||
|
)
|
||||||
|
.order_by(ConfigSnapshot.created_at.desc())
|
||||||
|
)
|
||||||
|
snap_ids_result = await db.execute(snap_ids_q)
|
||||||
|
# Take first (latest) per device
|
||||||
|
device_snap_map: dict[int, tuple[int, str]] = {}
|
||||||
|
for r in snap_ids_result.all():
|
||||||
|
if r.device_id not in device_snap_map:
|
||||||
|
device_snap_map[r.device_id] = (r.id, r.sha256)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for d in devices:
|
||||||
|
has_snapshot = d.id in latest_map
|
||||||
|
snap_info = device_snap_map.get(d.id)
|
||||||
|
result.append({
|
||||||
|
"device_id": d.id,
|
||||||
|
"ip": d.ip,
|
||||||
|
"hostname": d.hostname,
|
||||||
|
"has_snapshot": has_snapshot,
|
||||||
|
"last_collected_at": latest_map.get(d.id),
|
||||||
|
"snapshot_id": snap_info[0] if snap_info else None,
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/snapshots/dashboard/content/{snapshot_id}")
|
||||||
|
async def get_snapshot_content(
|
||||||
|
snapshot_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
_: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Return raw snapshot content for download/copy."""
|
||||||
|
snap = await db.get(ConfigSnapshot, snapshot_id)
|
||||||
|
if snap is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Snapshot not found")
|
||||||
|
|
||||||
|
content_row = await db.get(SnapshotContent, snap.sha256)
|
||||||
|
if content_row is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Content not found")
|
||||||
|
|
||||||
|
return {"content": content_row.content}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,3 +130,27 @@ export const useCollectPolicyForObject = () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeviceSnapshotStatus {
|
||||||
|
device_id: number
|
||||||
|
ip: string
|
||||||
|
hostname: string | null
|
||||||
|
has_snapshot: boolean
|
||||||
|
last_collected_at: string | null
|
||||||
|
snapshot_id: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDashboardDevices = (objId: number, policyId: number, enabled: boolean) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: ['snapshot-dashboard-devices', objId, policyId],
|
||||||
|
queryFn: () =>
|
||||||
|
api
|
||||||
|
.get<DeviceSnapshotStatus[]>(`/api/v1/snapshots/dashboard/${objId}/${policyId}/devices`)
|
||||||
|
.then((r) => r.data),
|
||||||
|
enabled,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const fetchSnapshotContent = (snapshotId: number) =>
|
||||||
|
api
|
||||||
|
.get<{ content: string }>(`/api/v1/snapshots/dashboard/content/${snapshotId}`)
|
||||||
|
.then((r) => r.data.content)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import {
|
import {
|
||||||
|
CopyOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
PlayCircleOutlined,
|
PlayCircleOutlined,
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
|
|
@ -34,9 +36,12 @@ import {
|
||||||
useDeleteSnapshotPolicy,
|
useDeleteSnapshotPolicy,
|
||||||
useCollectPolicy,
|
useCollectPolicy,
|
||||||
useCollectPolicyForObject,
|
useCollectPolicyForObject,
|
||||||
|
useDashboardDevices,
|
||||||
|
fetchSnapshotContent,
|
||||||
type SnapshotPolicy,
|
type SnapshotPolicy,
|
||||||
type ObjectSnapshotSummary,
|
type ObjectSnapshotSummary,
|
||||||
type PolicySnapshotStatus,
|
type PolicySnapshotStatus,
|
||||||
|
type DeviceSnapshotStatus,
|
||||||
} from '../api/snapshotPolicies'
|
} from '../api/snapshotPolicies'
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
@ -333,6 +338,8 @@ function ObjectPoliciesTable({
|
||||||
obj: ObjectSnapshotSummary
|
obj: ObjectSnapshotSummary
|
||||||
onCollect: (policyId: number) => void
|
onCollect: (policyId: number) => void
|
||||||
}) {
|
}) {
|
||||||
|
const [expandedKey, setExpandedKey] = useState<number | null>(null)
|
||||||
|
|
||||||
const columns: ColumnsType<PolicySnapshotStatus> = [
|
const columns: ColumnsType<PolicySnapshotStatus> = [
|
||||||
{
|
{
|
||||||
title: 'Политика',
|
title: 'Политика',
|
||||||
|
|
@ -383,7 +390,10 @@ function ObjectPoliciesTable({
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<PlayCircleOutlined />}
|
icon={<PlayCircleOutlined />}
|
||||||
onClick={() => onCollect(row.policy_id)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onCollect(row.policy_id)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
|
|
@ -403,7 +413,133 @@ function ObjectPoliciesTable({
|
||||||
size="small"
|
size="small"
|
||||||
pagination={false}
|
pagination={false}
|
||||||
style={{ marginTop: 4 }}
|
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}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DevicesList({
|
||||||
|
objId,
|
||||||
|
objName,
|
||||||
|
policyId,
|
||||||
|
}: {
|
||||||
|
objId: number
|
||||||
|
objName: string
|
||||||
|
policyId: number
|
||||||
|
}) {
|
||||||
|
const { data: devices, isLoading } = useDashboardDevices(objId, policyId, true)
|
||||||
|
|
||||||
|
const handleCopy = async (snapshotId: number) => {
|
||||||
|
try {
|
||||||
|
const content = await fetchSnapshotContent(snapshotId)
|
||||||
|
await navigator.clipboard.writeText(content)
|
||||||
|
message.success('Скопировано в буфер обмена')
|
||||||
|
} catch {
|
||||||
|
message.error('Ошибка при копировании')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDownload = async (device: DeviceSnapshotStatus) => {
|
||||||
|
if (!device.snapshot_id) return
|
||||||
|
try {
|
||||||
|
const content = await fetchSnapshotContent(device.snapshot_id)
|
||||||
|
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `${objName}-${device.hostname || 'device'}-${device.ip}.txt`
|
||||||
|
a.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
} catch {
|
||||||
|
message.error('Ошибка при скачивании')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) return <Typography.Text type="secondary">Загрузка...</Typography.Text>
|
||||||
|
if (!devices || devices.length === 0) return <Typography.Text type="secondary">Нет устройств</Typography.Text>
|
||||||
|
|
||||||
|
const columns: ColumnsType<DeviceSnapshotStatus> = [
|
||||||
|
{
|
||||||
|
title: 'IP',
|
||||||
|
dataIndex: 'ip',
|
||||||
|
key: 'ip',
|
||||||
|
render: (ip: string) => <Typography.Text code>{ip}</Typography.Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Hostname',
|
||||||
|
dataIndex: 'hostname',
|
||||||
|
key: 'hostname',
|
||||||
|
render: (val: string | null) => val || <Typography.Text type="secondary">—</Typography.Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Статус',
|
||||||
|
key: 'status',
|
||||||
|
render: (_, row) =>
|
||||||
|
row.has_snapshot ? (
|
||||||
|
<Tag color="green">Есть</Tag>
|
||||||
|
) : (
|
||||||
|
<Tag color="red">Нет</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Собран',
|
||||||
|
dataIndex: 'last_collected_at',
|
||||||
|
key: 'last_collected_at',
|
||||||
|
render: (val: string | null) =>
|
||||||
|
val ? (
|
||||||
|
<Tooltip title={dayjs(val).format('DD.MM.YYYY HH:mm:ss')}>
|
||||||
|
{dayjs(val).fromNow()}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Typography.Text type="secondary">—</Typography.Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
key: 'actions',
|
||||||
|
width: 80,
|
||||||
|
render: (_, row) =>
|
||||||
|
row.snapshot_id ? (
|
||||||
|
<Space size="small">
|
||||||
|
<Tooltip title="Скопировать">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<CopyOutlined />}
|
||||||
|
onClick={() => handleCopy(row.snapshot_id!)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Скачать">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<DownloadOutlined />}
|
||||||
|
onClick={() => handleDownload(row)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
) : null,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
dataSource={devices}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="device_id"
|
||||||
|
size="small"
|
||||||
|
pagination={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue