фикс фикса фикса снепшотов

This commit is contained in:
dv 2026-05-28 15:00:05 +03:00
parent 599fe566b8
commit c03e542f81
6 changed files with 288 additions and 261 deletions

View file

@ -20,7 +20,6 @@ def upgrade() -> None:
op.create_table(
"snapshot_policies",
sa.Column("id", sa.Integer(), autoincrement=True, primary_key=True),
sa.Column("object_id", sa.Integer(), sa.ForeignKey("objects.id", ondelete="CASCADE"), nullable=False),
sa.Column("category", sa.String(50), nullable=False),
sa.Column("action_type", sa.String(100), nullable=False),
sa.Column("params", postgresql.JSONB(), nullable=False, server_default="{}"),
@ -29,9 +28,7 @@ def upgrade() -> None:
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
)
op.create_index("ix_snapshot_policies_object_id", "snapshot_policies", ["object_id"])
def downgrade() -> None:
op.drop_index("ix_snapshot_policies_object_id", table_name="snapshot_policies")
op.drop_table("snapshot_policies")

View file

@ -1,6 +1,6 @@
from sqlalchemy import Boolean, ForeignKey, Integer, String
from sqlalchemy import Boolean, Integer, String
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.orm import Mapped, mapped_column
from .base import Base, TimestampMixin
@ -9,13 +9,8 @@ class SnapshotPolicy(Base, TimestampMixin):
__tablename__ = "snapshot_policies"
id: Mapped[int] = mapped_column(Integer(), autoincrement=True, primary_key=True)
object_id: Mapped[int] = mapped_column(
Integer(), ForeignKey("objects.id", ondelete="CASCADE"), nullable=False, index=True
)
category: Mapped[str] = mapped_column(String(50), nullable=False)
action_type: Mapped[str] = mapped_column(String(100), nullable=False)
params: Mapped[dict] = mapped_column(JSONB(), nullable=False, server_default="{}")
label: Mapped[str] = mapped_column(String(255), nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False, server_default="true")
object: Mapped["Object"] = relationship("Object", backref="snapshot_policies")

View file

@ -1,6 +1,4 @@
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
@ -35,27 +33,37 @@ async def snapshot_dashboard(
db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user),
):
# 1. Load all active policies with their objects
policies_q = (
select(SnapshotPolicy, Object.name, Object.city, Object.client)
.join(Object, SnapshotPolicy.object_id == Object.id)
.where(SnapshotPolicy.is_active == True, Object.is_active == True)
.order_by(Object.city, Object.name, SnapshotPolicy.label)
# 1. Load all active global policies
policies_result = await db.execute(
select(SnapshotPolicy).where(SnapshotPolicy.is_active == True).order_by(SnapshotPolicy.label)
)
rows = await db.execute(policies_q)
policy_rows = rows.all()
if not policy_rows:
policies = policies_result.scalars().all()
if not policies:
return []
# 2. Count eligible devices per (object_id, category)
# Collect categories we care about
categories = list({p.category for p in policies})
# 2. Find all active objects that have devices in relevant categories
obj_q = (
select(Object.id, Object.name, Object.city, Object.client)
.where(Object.is_active == True)
.order_by(Object.city, Object.name)
)
obj_result = await db.execute(obj_q)
all_objects = obj_result.all()
if not all_objects:
return []
# 3. Count eligible devices per (object_id, category)
eligible_q = (
select(
Device.object_id,
Device.category,
func.count(Device.id).label("total"),
)
.where(Device.is_active == True)
.where(Device.is_active == True, Device.category.in_(categories))
.group_by(Device.object_id, Device.category)
)
eligible_result = await db.execute(eligible_q)
@ -63,7 +71,7 @@ async def snapshot_dashboard(
(r.object_id, r.category): r.total for r in eligible_result.all()
}
# 3. Snapshot stats: distinct devices with snapshots + last collected time
# 4. Snapshot stats per (object_id, category, label)
stats_q = (
select(
Device.object_id,
@ -73,7 +81,7 @@ async def snapshot_dashboard(
func.max(ConfigSnapshot.created_at).label("last_collected_at"),
)
.join(Device, ConfigSnapshot.device_id == Device.id)
.where(Device.is_active == True)
.where(Device.is_active == True, Device.category.in_(categories))
.group_by(Device.object_id, Device.category, ConfigSnapshot.label)
)
stats_result = await db.execute(stats_q)
@ -82,16 +90,20 @@ async def snapshot_dashboard(
for r in stats_result.all()
}
# 4. Assemble response grouped by object
objects_map: dict[int, ObjectSnapshotSummary] = {}
for policy, obj_name, obj_city, obj_client in policy_rows:
# 5. Assemble: for each object, build policy statuses
result = []
for obj_id, obj_name, obj_city, obj_client in all_objects:
obj_policies = []
for policy in policies:
total = eligible_map.get((obj_id, policy.category), 0)
if total == 0:
continue
snap_label = _policy_snapshot_label(policy)
stats = stats_map.get((policy.object_id, policy.category, snap_label))
stats = stats_map.get((obj_id, policy.category, snap_label))
success_count = stats[0] if stats else 0
last_collected_at = stats[1] if stats else None
total_devices = eligible_map.get((policy.object_id, policy.category), 0)
policy_status = PolicySnapshotStatus(
obj_policies.append(PolicySnapshotStatus(
policy_id=policy.id,
policy_label=policy.label,
category=policy.category,
@ -99,51 +111,41 @@ async def snapshot_dashboard(
params=policy.params,
last_collected_at=last_collected_at,
success_count=success_count,
total_devices=total_devices,
)
total_devices=total,
))
if policy.object_id not in objects_map:
objects_map[policy.object_id] = ObjectSnapshotSummary(
object_id=policy.object_id,
if obj_policies:
result.append(ObjectSnapshotSummary(
object_id=obj_id,
object_name=obj_name,
city=obj_city,
client=obj_client,
policies=[],
)
objects_map[policy.object_id].policies.append(policy_status)
policies=obj_policies,
))
return list(objects_map.values())
return result
# ─── CRUD ────────────────────────────────────────────────────────────────────
# ─── Policies CRUD (global) ──────────────────────────────────────────────────
@router.get("/objects/{obj_id}/snapshot-policies", response_model=list[SnapshotPolicyRead])
@router.get("/snapshot-policies", response_model=list[SnapshotPolicyRead])
async def list_policies(
obj_id: int,
db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user),
):
result = await db.execute(
select(SnapshotPolicy)
.where(SnapshotPolicy.object_id == obj_id)
.order_by(SnapshotPolicy.label)
select(SnapshotPolicy).order_by(SnapshotPolicy.label)
)
return result.scalars().all()
@router.post("/objects/{obj_id}/snapshot-policies", response_model=SnapshotPolicyRead, status_code=201)
@router.post("/snapshot-policies", response_model=SnapshotPolicyRead, status_code=201)
async def create_policy(
obj_id: int,
body: SnapshotPolicyCreate,
db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user),
):
obj = await db.get(Object, obj_id)
if obj is None:
raise HTTPException(status_code=404, detail="Object not found")
policy = SnapshotPolicy(
object_id=obj_id,
category=body.category,
action_type=body.action_type,
params=body.params,
@ -156,16 +158,15 @@ async def create_policy(
return policy
@router.put("/objects/{obj_id}/snapshot-policies/{policy_id}", response_model=SnapshotPolicyRead)
@router.put("/snapshot-policies/{policy_id}", response_model=SnapshotPolicyRead)
async def update_policy(
obj_id: int,
policy_id: int,
body: SnapshotPolicyUpdate,
db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user),
):
policy = await db.get(SnapshotPolicy, policy_id)
if policy is None or policy.object_id != obj_id:
if policy is None:
raise HTTPException(status_code=404, detail="Policy not found")
for field, value in body.model_dump(exclude_unset=True).items():
@ -176,15 +177,14 @@ async def update_policy(
return policy
@router.delete("/objects/{obj_id}/snapshot-policies/{policy_id}", status_code=204)
@router.delete("/snapshot-policies/{policy_id}", status_code=204)
async def delete_policy(
obj_id: int,
policy_id: int,
db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user),
):
policy = await db.get(SnapshotPolicy, policy_id)
if policy is None or policy.object_id != obj_id:
if policy is None:
raise HTTPException(status_code=404, detail="Policy not found")
await db.delete(policy)
@ -193,19 +193,49 @@ async def delete_policy(
# ─── Collect ─────────────────────────────────────────────────────────────────
@router.post("/objects/{obj_id}/snapshot-policies/{policy_id}/collect", status_code=202)
async def collect_policy(
obj_id: int,
@router.post("/snapshot-policies/{policy_id}/collect", status_code=202)
async def collect_all(
policy_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Collect snapshots for ALL objects matching this policy's category."""
policy = await db.get(SnapshotPolicy, policy_id)
if policy is None or policy.object_id != obj_id:
if policy is None:
raise HTTPException(status_code=404, detail="Policy not found")
if not policy.is_active:
raise HTTPException(status_code=400, detail="Policy is disabled")
task = await task_service.create_task(
action_type=policy.action_type,
target_scope={
"type": "category",
"category": policy.category,
},
params=policy.params,
user_id=current_user.id,
)
return {"task_id": task.id}
@router.post("/snapshot-policies/{policy_id}/collect/{obj_id}", status_code=202)
async def collect_for_object(
policy_id: int,
obj_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Collect snapshots for a specific object matching this policy's category."""
policy = await db.get(SnapshotPolicy, policy_id)
if policy is None:
raise HTTPException(status_code=404, detail="Policy not found")
if not policy.is_active:
raise HTTPException(status_code=400, detail="Policy is disabled")
obj = await db.get(Object, obj_id)
if obj is None:
raise HTTPException(status_code=404, detail="Object not found")
task = await task_service.create_task(
action_type=policy.action_type,
target_scope={

View file

@ -24,7 +24,6 @@ class SnapshotPolicyRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
object_id: int
category: str
action_type: str
params: dict

View file

@ -3,7 +3,6 @@ import { api } from './client'
export interface SnapshotPolicy {
id: number
object_id: number
category: string
action_type: string
params: Record<string, unknown>
@ -41,16 +40,16 @@ export const useSnapshotDashboard = () =>
.then((r) => r.data),
})
export const useSnapshotPolicies = (objId: number) =>
export const useSnapshotPolicies = () =>
useQuery({
queryKey: ['snapshot-policies', objId],
queryKey: ['snapshot-policies'],
queryFn: () =>
api
.get<SnapshotPolicy[]>(`/api/v1/objects/${objId}/snapshot-policies`)
.get<SnapshotPolicy[]>('/api/v1/snapshot-policies')
.then((r) => r.data),
})
export const useCreateSnapshotPolicy = (objId: number) => {
export const useCreateSnapshotPolicy = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (body: {
@ -61,16 +60,16 @@ export const useCreateSnapshotPolicy = (objId: number) => {
is_active?: boolean
}) =>
api
.post<SnapshotPolicy>(`/api/v1/objects/${objId}/snapshot-policies`, body)
.post<SnapshotPolicy>('/api/v1/snapshot-policies', body)
.then((r) => r.data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['snapshot-policies', objId] })
qc.invalidateQueries({ queryKey: ['snapshot-policies'] })
qc.invalidateQueries({ queryKey: ['snapshot-dashboard'] })
},
})
}
export const useUpdateSnapshotPolicy = (objId: number) => {
export const useUpdateSnapshotPolicy = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: ({
@ -85,63 +84,7 @@ export const useUpdateSnapshotPolicy = (objId: number) => {
is_active?: boolean
}) =>
api
.put<SnapshotPolicy>(
`/api/v1/objects/${objId}/snapshot-policies/${policyId}`,
body
)
.then((r) => r.data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['snapshot-policies', objId] })
qc.invalidateQueries({ queryKey: ['snapshot-dashboard'] })
},
})
}
export const useDeleteSnapshotPolicy = (objId: number) => {
const qc = useQueryClient()
return useMutation({
mutationFn: (policyId: number) =>
api.delete(`/api/v1/objects/${objId}/snapshot-policies/${policyId}`),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['snapshot-policies', objId] })
qc.invalidateQueries({ queryKey: ['snapshot-dashboard'] })
},
})
}
export const useCollectPolicy = (objId: number) => {
const qc = useQueryClient()
return useMutation({
mutationFn: (policyId: number) =>
api
.post<{ task_id: string }>(
`/api/v1/objects/${objId}/snapshot-policies/${policyId}/collect`
)
.then((r) => r.data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['tasks'] })
},
})
}
export const useCreateSnapshotPolicyDynamic = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: ({
objId,
body,
}: {
objId: number
body: {
category: string
action_type: string
params?: Record<string, unknown>
label: string
is_active?: boolean
}
}) =>
api
.post<SnapshotPolicy>(`/api/v1/objects/${objId}/snapshot-policies`, body)
.put<SnapshotPolicy>(`/api/v1/snapshot-policies/${policyId}`, body)
.then((r) => r.data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['snapshot-policies'] })
@ -149,3 +92,41 @@ export const useCreateSnapshotPolicyDynamic = () => {
},
})
}
export const useDeleteSnapshotPolicy = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (policyId: number) =>
api.delete(`/api/v1/snapshot-policies/${policyId}`),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['snapshot-policies'] })
qc.invalidateQueries({ queryKey: ['snapshot-dashboard'] })
},
})
}
export const useCollectPolicy = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (policyId: number) =>
api
.post<{ task_id: string }>(`/api/v1/snapshot-policies/${policyId}/collect`)
.then((r) => r.data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['tasks'] })
},
})
}
export const useCollectPolicyForObject = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: ({ policyId, objId }: { policyId: number; objId: number }) =>
api
.post<{ task_id: string }>(`/api/v1/snapshot-policies/${policyId}/collect/${objId}`)
.then((r) => r.data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['tasks'] })
},
})
}

View file

@ -3,9 +3,11 @@ import {
EditOutlined,
PlayCircleOutlined,
PlusOutlined,
SettingOutlined,
} from '@ant-design/icons'
import {
Button,
Card,
Collapse,
Form,
Input,
@ -26,14 +28,16 @@ import 'dayjs/locale/ru'
import { useMemo, useState } from 'react'
import {
useSnapshotDashboard,
useCreateSnapshotPolicyDynamic,
useSnapshotPolicies,
useCreateSnapshotPolicy,
useUpdateSnapshotPolicy,
useDeleteSnapshotPolicy,
useCollectPolicy,
useCollectPolicyForObject,
type SnapshotPolicy,
type ObjectSnapshotSummary,
type PolicySnapshotStatus,
} from '../api/snapshotPolicies'
import { useObjects } from '../api/objects'
dayjs.extend(relativeTime)
dayjs.locale('ru')
@ -58,7 +62,6 @@ const ACTION_OPTIONS = [
]
interface PolicyFormValues {
object_id?: number
label: string
category: string
action_type: string
@ -67,14 +70,20 @@ interface PolicyFormValues {
export function SnapshotDashboardPage() {
const { data: dashboard, isLoading } = useSnapshotDashboard()
const { data: allObjects } = useObjects()
const { data: policies } = useSnapshotPolicies()
const [searchText, setSearchText] = useState('')
const [modalOpen, setModalOpen] = useState(false)
const [editTarget, setEditTarget] = useState<{ objId: number; policy: PolicySnapshotStatus } | null>(null)
const [targetObjId, setTargetObjId] = useState<number | null>(null)
const [policyModalOpen, setPolicyModalOpen] = useState(false)
const [editPolicy, setEditPolicy] = useState<SnapshotPolicy | null>(null)
const [settingsOpen, setSettingsOpen] = useState(false)
const [form] = Form.useForm<PolicyFormValues>()
const actionType = Form.useWatch('action_type', form)
const createMutation = useCreateSnapshotPolicy()
const updateMutation = useUpdateSnapshotPolicy()
const deleteMutation = useDeleteSnapshotPolicy()
const collectAllMutation = useCollectPolicy()
const collectObjMutation = useCollectPolicyForObject()
const filtered = useMemo(() => {
if (!searchText || !dashboard) return dashboard ?? []
const q = searchText.toLowerCase()
@ -101,46 +110,35 @@ export function SnapshotDashboardPage() {
return sorted
}, [filtered])
const openCreate = (objId?: number) => {
const openCreatePolicy = () => {
form.resetFields()
setEditTarget(null)
if (objId) {
setTargetObjId(objId)
} else {
setTargetObjId(null)
}
setModalOpen(true)
setEditPolicy(null)
setPolicyModalOpen(true)
}
const openEdit = (objId: number, policy: PolicySnapshotStatus) => {
const openEditPolicy = (policy: SnapshotPolicy) => {
form.resetFields()
form.setFieldsValue({
label: policy.policy_label,
label: policy.label,
category: policy.category,
action_type: policy.action_type,
path: policy.action_type === 'get_file' ? (policy.params as any)?.path ?? '' : undefined,
})
setEditTarget({ objId, policy })
setTargetObjId(objId)
setModalOpen(true)
setEditPolicy(policy)
setPolicyModalOpen(true)
}
const handleSubmit = async () => {
const handlePolicySubmit = async () => {
const values = await form.validateFields()
const objId = targetObjId ?? values.object_id
if (!objId) {
message.error('Выберите объект')
return
}
const params: Record<string, unknown> = {}
if (values.action_type === 'get_file' && values.path) {
params.path = values.path
}
try {
if (editTarget) {
await updatePolicyMutation.mutateAsync({
policyId: editTarget.policy.policy_id,
if (editPolicy) {
await updateMutation.mutateAsync({
policyId: editPolicy.id,
label: values.label,
category: values.category,
action_type: values.action_type,
@ -148,25 +146,85 @@ export function SnapshotDashboardPage() {
})
message.success('Политика обновлена')
} else {
await createPolicyFor.mutateAsync({
objId,
body: {
await createMutation.mutateAsync({
label: values.label,
category: values.category,
action_type: values.action_type,
params,
},
})
message.success('Политика создана')
}
setModalOpen(false)
setPolicyModalOpen(false)
} catch {
message.error('Ошибка при сохранении')
}
}
const createPolicyFor = useCreateSnapshotPolicyDynamic()
const updatePolicyMutation = useUpdateSnapshotPolicy(targetObjId ?? 0)
const handleDeletePolicy = async (id: number) => {
try {
await deleteMutation.mutateAsync(id)
message.success('Политика удалена')
} catch {
message.error('Ошибка при удалении')
}
}
const handleCollectAll = async (policyId: number) => {
try {
const result = await collectAllMutation.mutateAsync(policyId)
message.success(`Задача создана: ${result.task_id.slice(0, 8)}...`)
} catch {
message.error('Ошибка при запуске сбора')
}
}
const handleCollectObj = async (policyId: number, objId: number) => {
try {
const result = await collectObjMutation.mutateAsync({ policyId, objId })
message.success(`Задача создана: ${result.task_id.slice(0, 8)}...`)
} catch {
message.error('Ошибка при запуске сбора')
}
}
// Policy settings columns
const policyColumns: ColumnsType<SnapshotPolicy> = [
{ title: 'Название', dataIndex: 'label', key: 'label' },
{
title: 'Категория',
dataIndex: 'category',
key: 'category',
render: (cat: string) => <Tag>{cat}</Tag>,
},
{
title: 'Действие',
dataIndex: 'action_type',
key: 'action_type',
render: (val: string, row) =>
val === 'get_file' ? `Get File: ${(row.params as any)?.path ?? ''}` : 'MikroTik Export',
},
{
title: '',
key: 'actions',
width: 160,
render: (_, row) => (
<Space size="small">
<Tooltip title="Собрать со всех объектов">
<Button
type="text"
size="small"
icon={<PlayCircleOutlined />}
onClick={() => handleCollectAll(row.id)}
/>
</Tooltip>
<Button type="text" size="small" icon={<EditOutlined />} onClick={() => openEditPolicy(row)} />
<Popconfirm title="Удалить политику?" onConfirm={() => handleDeletePolicy(row.id)}>
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
</Popconfirm>
</Space>
),
},
]
return (
<div>
@ -182,17 +240,39 @@ export function SnapshotDashboardPage() {
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
<Button type="primary" icon={<PlusOutlined />} onClick={() => openCreate()}>
Добавить политику
<Button icon={<SettingOutlined />} onClick={() => setSettingsOpen(!settingsOpen)}>
Политики
</Button>
</Space>
</div>
{settingsOpen && (
<Card size="small" style={{ marginBottom: 16 }} title="Глобальные политики сбора">
<Table
dataSource={policies ?? []}
columns={policyColumns}
rowKey="id"
size="small"
pagination={false}
/>
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={openCreatePolicy}
style={{ marginTop: 8, width: '100%' }}
>
Добавить политику
</Button>
</Card>
)}
{isLoading && <Typography.Text type="secondary">Загрузка...</Typography.Text>}
{!isLoading && filtered.length === 0 && (
<Typography.Text type="secondary">
Нет объектов с политиками снапшотов. Нажмите «Добавить политику» чтобы начать.
{(policies ?? []).length === 0
? 'Нет политик. Нажмите «Политики» чтобы настроить сбор снапшотов.'
: 'Нет объектов с устройствами подходящих категорий.'}
</Typography.Text>
)}
@ -210,36 +290,22 @@ export function SnapshotDashboardPage() {
<ObjectPoliciesTable
key={obj.object_id}
obj={obj}
onAddPolicy={() => openCreate(obj.object_id)}
onEditPolicy={(p) => openEdit(obj.object_id, p)}
onCollect={(policyId) => handleCollectObj(policyId, obj.object_id)}
/>
)),
}))}
/>
<Modal
title={editTarget ? 'Редактировать политику' : 'Новая политика'}
open={modalOpen}
onCancel={() => setModalOpen(false)}
onOk={handleSubmit}
confirmLoading={createPolicyFor.isPending || updatePolicyMutation.isPending}
title={editPolicy ? 'Редактировать политику' : 'Новая политика'}
open={policyModalOpen}
onCancel={() => setPolicyModalOpen(false)}
onOk={handlePolicySubmit}
confirmLoading={createMutation.isPending || updateMutation.isPending}
okText="Сохранить"
cancelText="Отмена"
>
<Form form={form} layout="vertical">
{!targetObjId && !editTarget && (
<Form.Item name="object_id" label="Объект" rules={[{ required: true, message: 'Выберите объект' }]}>
<Select
showSearch
placeholder="Выберите объект"
optionFilterProp="label"
options={(allObjects ?? []).map((o) => ({
value: o.id,
label: `${o.name}${o.city ? ` (${o.city})` : ''}`,
}))}
/>
</Form.Item>
)}
<Form.Item name="label" label="Название" rules={[{ required: true, message: 'Введите название' }]}>
<Input placeholder="CAPS config" />
</Form.Item>
@ -262,34 +328,11 @@ export function SnapshotDashboardPage() {
function ObjectPoliciesTable({
obj,
onAddPolicy,
onEditPolicy,
onCollect,
}: {
obj: ObjectSnapshotSummary
onAddPolicy: () => void
onEditPolicy: (p: PolicySnapshotStatus) => void
onCollect: (policyId: number) => void
}) {
const deleteMutation = useDeleteSnapshotPolicy(obj.object_id)
const collectMutation = useCollectPolicy(obj.object_id)
const handleCollect = async (policyId: number) => {
try {
const result = await collectMutation.mutateAsync(policyId)
message.success(`Задача создана: ${result.task_id.slice(0, 8)}...`)
} catch {
message.error('Ошибка при запуске сбора')
}
}
const handleDelete = async (policyId: number) => {
try {
await deleteMutation.mutateAsync(policyId)
message.success('Политика удалена')
} catch {
message.error('Ошибка при удалении')
}
}
const columns: ColumnsType<PolicySnapshotStatus> = [
{
title: 'Политика',
@ -333,51 +376,33 @@ function ObjectPoliciesTable({
{
title: '',
key: 'actions',
width: 140,
width: 60,
render: (_, row) => (
<Space size="small">
<Tooltip title="Собрать">
<Tooltip title="Собрать для этого объекта">
<Button
type="text"
size="small"
icon={<PlayCircleOutlined />}
loading={collectMutation.isPending}
onClick={() => handleCollect(row.policy_id)}
onClick={() => onCollect(row.policy_id)}
/>
</Tooltip>
<Tooltip title="Редактировать">
<Button
type="text"
size="small"
icon={<EditOutlined />}
onClick={() => onEditPolicy(row)}
/>
</Tooltip>
<Popconfirm title="Удалить политику?" onConfirm={() => handleDelete(row.policy_id)}>
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
</Popconfirm>
</Space>
),
},
]
return (
<div style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<Typography.Text strong>
{obj.object_name}
{obj.client && <Typography.Text type="secondary"> {obj.client}</Typography.Text>}
</Typography.Text>
<Button size="small" icon={<PlusOutlined />} onClick={onAddPolicy}>
Добавить
</Button>
</div>
<Table
dataSource={obj.policies}
columns={columns}
rowKey="policy_id"
size="small"
pagination={false}
style={{ marginTop: 4 }}
/>
</div>
)