фикс фикса фикса снепшотов
This commit is contained in:
parent
599fe566b8
commit
c03e542f81
6 changed files with 288 additions and 261 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue