diff --git a/backend/alembic/versions/0020_snapshot_policies.py b/backend/alembic/versions/0020_snapshot_policies.py index c9abcba..7c79d74 100644 --- a/backend/alembic/versions/0020_snapshot_policies.py +++ b/backend/alembic/versions/0020_snapshot_policies.py @@ -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") diff --git a/backend/app/models/snapshot_policy.py b/backend/app/models/snapshot_policy.py index 8b1ff89..9870c28 100644 --- a/backend/app/models/snapshot_policy.py +++ b/backend/app/models/snapshot_policy.py @@ -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") diff --git a/backend/app/routers/snapshot_policies.py b/backend/app/routers/snapshot_policies.py index d4461ea..01b55fd 100644 --- a/backend/app/routers/snapshot_policies.py +++ b/backend/app/routers/snapshot_policies.py @@ -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,68 +90,62 @@ 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: - snap_label = _policy_snapshot_label(policy) - stats = stats_map.get((policy.object_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) + # 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((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( - policy_id=policy.id, - policy_label=policy.label, - category=policy.category, - action_type=policy.action_type, - params=policy.params, - last_collected_at=last_collected_at, - success_count=success_count, - total_devices=total_devices, - ) + obj_policies.append(PolicySnapshotStatus( + policy_id=policy.id, + policy_label=policy.label, + category=policy.category, + action_type=policy.action_type, + params=policy.params, + last_collected_at=last_collected_at, + success_count=success_count, + 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={ diff --git a/backend/app/schemas/snapshot_policy.py b/backend/app/schemas/snapshot_policy.py index 6cb61dc..2aba645 100644 --- a/backend/app/schemas/snapshot_policy.py +++ b/backend/app/schemas/snapshot_policy.py @@ -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 diff --git a/frontend/src/api/snapshotPolicies.ts b/frontend/src/api/snapshotPolicies.ts index 56e500b..9dc90e2 100644 --- a/frontend/src/api/snapshotPolicies.ts +++ b/frontend/src/api/snapshotPolicies.ts @@ -3,7 +3,6 @@ import { api } from './client' export interface SnapshotPolicy { id: number - object_id: number category: string action_type: string params: Record @@ -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(`/api/v1/objects/${objId}/snapshot-policies`) + .get('/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(`/api/v1/objects/${objId}/snapshot-policies`, body) + .post('/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( - `/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 - label: string - is_active?: boolean - } - }) => - api - .post(`/api/v1/objects/${objId}/snapshot-policies`, body) + .put(`/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'] }) + }, + }) +} diff --git a/frontend/src/pages/SnapshotDashboard.tsx b/frontend/src/pages/SnapshotDashboard.tsx index 013777a..703089e 100644 --- a/frontend/src/pages/SnapshotDashboard.tsx +++ b/frontend/src/pages/SnapshotDashboard.tsx @@ -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(null) + const [policyModalOpen, setPolicyModalOpen] = useState(false) + const [editPolicy, setEditPolicy] = useState(null) + const [settingsOpen, setSettingsOpen] = useState(false) const [form] = Form.useForm() 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 = {} 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: { - label: values.label, - category: values.category, - action_type: values.action_type, - params, - }, + 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 = [ + { title: 'Название', dataIndex: 'label', key: 'label' }, + { + title: 'Категория', + dataIndex: 'category', + key: 'category', + render: (cat: string) => {cat}, + }, + { + 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) => ( + + + + {settingsOpen && ( + + + + + )} + {isLoading && Загрузка...} {!isLoading && filtered.length === 0 && ( - Нет объектов с политиками снапшотов. Нажмите «Добавить политику» чтобы начать. + {(policies ?? []).length === 0 + ? 'Нет политик. Нажмите «Политики» чтобы настроить сбор снапшотов.' + : 'Нет объектов с устройствами подходящих категорий.'} )} @@ -210,36 +290,22 @@ export function SnapshotDashboardPage() { openCreate(obj.object_id)} - onEditPolicy={(p) => openEdit(obj.object_id, p)} + onCollect={(policyId) => handleCollectObj(policyId, obj.object_id)} /> )), }))} /> 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="Отмена" >
- {!targetObjId && !editTarget && ( - - @@ -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 = [ { title: 'Политика', @@ -333,51 +376,33 @@ function ObjectPoliciesTable({ { title: '', key: 'actions', - width: 140, + width: 60, render: (_, row) => ( - - - - + + {obj.object_name} + {obj.client && — {obj.client}} +
)