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

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( op.create_table(
"snapshot_policies", "snapshot_policies",
sa.Column("id", sa.Integer(), autoincrement=True, primary_key=True), 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("category", sa.String(50), nullable=False),
sa.Column("action_type", sa.String(100), nullable=False), sa.Column("action_type", sa.String(100), nullable=False),
sa.Column("params", postgresql.JSONB(), nullable=False, server_default="{}"), 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("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), 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: def downgrade() -> None:
op.drop_index("ix_snapshot_policies_object_id", table_name="snapshot_policies")
op.drop_table("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.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column
from .base import Base, TimestampMixin from .base import Base, TimestampMixin
@ -9,13 +9,8 @@ class SnapshotPolicy(Base, TimestampMixin):
__tablename__ = "snapshot_policies" __tablename__ = "snapshot_policies"
id: Mapped[int] = mapped_column(Integer(), autoincrement=True, primary_key=True) 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) category: Mapped[str] = mapped_column(String(50), nullable=False)
action_type: Mapped[str] = mapped_column(String(100), nullable=False) action_type: Mapped[str] = mapped_column(String(100), nullable=False)
params: Mapped[dict] = mapped_column(JSONB(), nullable=False, server_default="{}") params: Mapped[dict] = mapped_column(JSONB(), nullable=False, server_default="{}")
label: Mapped[str] = mapped_column(String(255), nullable=False) label: Mapped[str] = mapped_column(String(255), nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False, server_default="true") 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
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import select, func from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@ -35,27 +33,37 @@ async def snapshot_dashboard(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user), _: User = Depends(get_current_user),
): ):
# 1. Load all active policies with their objects # 1. Load all active global policies
policies_q = ( policies_result = await db.execute(
select(SnapshotPolicy, Object.name, Object.city, Object.client) select(SnapshotPolicy).where(SnapshotPolicy.is_active == True).order_by(SnapshotPolicy.label)
.join(Object, SnapshotPolicy.object_id == Object.id)
.where(SnapshotPolicy.is_active == True, Object.is_active == True)
.order_by(Object.city, Object.name, SnapshotPolicy.label)
) )
rows = await db.execute(policies_q) policies = policies_result.scalars().all()
policy_rows = rows.all() if not policies:
if not policy_rows:
return [] 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 = ( eligible_q = (
select( select(
Device.object_id, Device.object_id,
Device.category, Device.category,
func.count(Device.id).label("total"), 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) .group_by(Device.object_id, Device.category)
) )
eligible_result = await db.execute(eligible_q) 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() (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 = ( stats_q = (
select( select(
Device.object_id, Device.object_id,
@ -73,7 +81,7 @@ async def snapshot_dashboard(
func.max(ConfigSnapshot.created_at).label("last_collected_at"), func.max(ConfigSnapshot.created_at).label("last_collected_at"),
) )
.join(Device, ConfigSnapshot.device_id == Device.id) .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) .group_by(Device.object_id, Device.category, ConfigSnapshot.label)
) )
stats_result = await db.execute(stats_q) stats_result = await db.execute(stats_q)
@ -82,68 +90,62 @@ async def snapshot_dashboard(
for r in stats_result.all() for r in stats_result.all()
} }
# 4. Assemble response grouped by object # 5. Assemble: for each object, build policy statuses
objects_map: dict[int, ObjectSnapshotSummary] = {} result = []
for policy, obj_name, obj_city, obj_client in policy_rows: for obj_id, obj_name, obj_city, obj_client in all_objects:
snap_label = _policy_snapshot_label(policy) obj_policies = []
stats = stats_map.get((policy.object_id, policy.category, snap_label)) for policy in policies:
success_count = stats[0] if stats else 0 total = eligible_map.get((obj_id, policy.category), 0)
last_collected_at = stats[1] if stats else None if total == 0:
total_devices = eligible_map.get((policy.object_id, policy.category), 0) continue
snap_label = _policy_snapshot_label(policy)
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
policy_status = PolicySnapshotStatus( obj_policies.append(PolicySnapshotStatus(
policy_id=policy.id, policy_id=policy.id,
policy_label=policy.label, policy_label=policy.label,
category=policy.category, category=policy.category,
action_type=policy.action_type, action_type=policy.action_type,
params=policy.params, params=policy.params,
last_collected_at=last_collected_at, last_collected_at=last_collected_at,
success_count=success_count, success_count=success_count,
total_devices=total_devices, total_devices=total,
) ))
if policy.object_id not in objects_map: if obj_policies:
objects_map[policy.object_id] = ObjectSnapshotSummary( result.append(ObjectSnapshotSummary(
object_id=policy.object_id, object_id=obj_id,
object_name=obj_name, object_name=obj_name,
city=obj_city, city=obj_city,
client=obj_client, client=obj_client,
policies=[], policies=obj_policies,
) ))
objects_map[policy.object_id].policies.append(policy_status)
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( async def list_policies(
obj_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user), _: User = Depends(get_current_user),
): ):
result = await db.execute( result = await db.execute(
select(SnapshotPolicy) select(SnapshotPolicy).order_by(SnapshotPolicy.label)
.where(SnapshotPolicy.object_id == obj_id)
.order_by(SnapshotPolicy.label)
) )
return result.scalars().all() 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( async def create_policy(
obj_id: int,
body: SnapshotPolicyCreate, body: SnapshotPolicyCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user), _: 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( policy = SnapshotPolicy(
object_id=obj_id,
category=body.category, category=body.category,
action_type=body.action_type, action_type=body.action_type,
params=body.params, params=body.params,
@ -156,16 +158,15 @@ async def create_policy(
return 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( async def update_policy(
obj_id: int,
policy_id: int, policy_id: int,
body: SnapshotPolicyUpdate, body: SnapshotPolicyUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user), _: User = Depends(get_current_user),
): ):
policy = await db.get(SnapshotPolicy, policy_id) 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") raise HTTPException(status_code=404, detail="Policy not found")
for field, value in body.model_dump(exclude_unset=True).items(): for field, value in body.model_dump(exclude_unset=True).items():
@ -176,15 +177,14 @@ async def update_policy(
return 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( async def delete_policy(
obj_id: int,
policy_id: int, policy_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user), _: User = Depends(get_current_user),
): ):
policy = await db.get(SnapshotPolicy, policy_id) 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") raise HTTPException(status_code=404, detail="Policy not found")
await db.delete(policy) await db.delete(policy)
@ -193,19 +193,49 @@ async def delete_policy(
# ─── Collect ───────────────────────────────────────────────────────────────── # ─── Collect ─────────────────────────────────────────────────────────────────
@router.post("/objects/{obj_id}/snapshot-policies/{policy_id}/collect", status_code=202) @router.post("/snapshot-policies/{policy_id}/collect", status_code=202)
async def collect_policy( async def collect_all(
obj_id: int,
policy_id: int, policy_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Collect snapshots for ALL objects matching this policy's category."""
policy = await db.get(SnapshotPolicy, policy_id) 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") raise HTTPException(status_code=404, detail="Policy not found")
if not policy.is_active: if not policy.is_active:
raise HTTPException(status_code=400, detail="Policy is disabled") 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( task = await task_service.create_task(
action_type=policy.action_type, action_type=policy.action_type,
target_scope={ target_scope={

View file

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

View file

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