фикс фикса фикса снепшотов
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(
|
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")
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
|
|
|
||||||
|
|
@ -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,16 +90,20 @@ 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:
|
||||||
|
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)
|
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
|
success_count = stats[0] if stats else 0
|
||||||
last_collected_at = stats[1] if stats else None
|
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_id=policy.id,
|
||||||
policy_label=policy.label,
|
policy_label=policy.label,
|
||||||
category=policy.category,
|
category=policy.category,
|
||||||
|
|
@ -99,51 +111,41 @@ async def snapshot_dashboard(
|
||||||
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={
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
body: {
|
|
||||||
label: values.label,
|
label: values.label,
|
||||||
category: values.category,
|
category: values.category,
|
||||||
action_type: values.action_type,
|
action_type: values.action_type,
|
||||||
params,
|
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 />}
|
||||||
loading={collectMutation.isPending}
|
onClick={() => onCollect(row.policy_id)}
|
||||||
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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue