фикс фикс фикс фикс фикса
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 sqlalchemy import select, func
|
||||
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.models.device import Device
|
||||
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.user import User
|
||||
from app.schemas.snapshot_policy import (
|
||||
|
|
@ -247,3 +249,103 @@ async def collect_for_object(
|
|||
user_id=current_user.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 {
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
DownloadOutlined,
|
||||
EditOutlined,
|
||||
PlayCircleOutlined,
|
||||
PlusOutlined,
|
||||
|
|
@ -34,9 +36,12 @@ import {
|
|||
useDeleteSnapshotPolicy,
|
||||
useCollectPolicy,
|
||||
useCollectPolicyForObject,
|
||||
useDashboardDevices,
|
||||
fetchSnapshotContent,
|
||||
type SnapshotPolicy,
|
||||
type ObjectSnapshotSummary,
|
||||
type PolicySnapshotStatus,
|
||||
type DeviceSnapshotStatus,
|
||||
} from '../api/snapshotPolicies'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
|
@ -333,6 +338,8 @@ function ObjectPoliciesTable({
|
|||
obj: ObjectSnapshotSummary
|
||||
onCollect: (policyId: number) => void
|
||||
}) {
|
||||
const [expandedKey, setExpandedKey] = useState<number | null>(null)
|
||||
|
||||
const columns: ColumnsType<PolicySnapshotStatus> = [
|
||||
{
|
||||
title: 'Политика',
|
||||
|
|
@ -383,7 +390,10 @@ function ObjectPoliciesTable({
|
|||
type="text"
|
||||
size="small"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={() => onCollect(row.policy_id)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onCollect(row.policy_id)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
|
|
@ -403,7 +413,133 @@ function ObjectPoliciesTable({
|
|||
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}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</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