mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 04:20:45 +08:00
Add named multi-panel traffic boards with dedicated wall tabs.
Persist board/panel layouts server-side, open each board outside the network shell so workbench reloads do not refresh walls, and fix mapped-baseline chart overlay plus fullscreen fit. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
214bc56774
commit
00d3e95e2d
22 changed files with 1923 additions and 345 deletions
|
|
@ -709,3 +709,39 @@ class PortTrafficEvent(Base):
|
|||
level: Mapped[str] = mapped_column(String(16), default="error", index=True) # info|warn|error
|
||||
message: Mapped[str] = mapped_column(Text, default="")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
|
||||
|
||||
|
||||
class PortTrafficBoard(Base):
|
||||
"""Named multi-panel traffic ops wall (cutover / duty view)."""
|
||||
|
||||
__tablename__ = "port_traffic_board"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True, default=lambda: uuid4().hex)
|
||||
name: Mapped[str] = mapped_column(String(256), default="", index=True)
|
||||
remark: Mapped[str] = mapped_column(String(1024), default="")
|
||||
cols: Mapped[int] = mapped_column(Integer, default=2)
|
||||
created_by: Mapped[str] = mapped_column(String(64), default="")
|
||||
updated_by: Mapped[str] = mapped_column(String(64), default="")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
|
||||
|
||||
|
||||
class PortTrafficPanel(Base):
|
||||
"""One chart cell on a traffic board."""
|
||||
|
||||
__tablename__ = "port_traffic_panel"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True, default=lambda: uuid4().hex)
|
||||
board_id: Mapped[str] = mapped_column(String(64), default="", index=True)
|
||||
title: Mapped[str] = mapped_column(String(256), default="")
|
||||
target_id: Mapped[str] = mapped_column(String(64), default="", index=True)
|
||||
range_hours: Mapped[int] = mapped_column(Integer, default=24)
|
||||
baseline: Mapped[str] = mapped_column(String(16), default="off") # off|day|week|shift|custom
|
||||
offset_hours: Mapped[int] = mapped_column(Integer, default=0)
|
||||
baseline_target_id: Mapped[str] = mapped_column(String(64), default="")
|
||||
y_mode: Mapped[str] = mapped_column(String(16), default="auto") # auto|current|util
|
||||
ord: Mapped[int] = mapped_column(Integer, default=0)
|
||||
col_span: Mapped[int] = mapped_column(Integer, default=1)
|
||||
row_span: Mapped[int] = mapped_column(Integer, default=1)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
|
|
|
|||
236
netx_api/port_traffic_board_service.py
Normal file
236
netx_api/port_traffic_board_service.py
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
"""Named traffic wall boards (multi-panel ops views)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .models import PortTrafficBoard, PortTrafficPanel, PortTrafficTarget
|
||||
from .port_traffic_schemas import (
|
||||
PortTrafficBoardCreate,
|
||||
PortTrafficBoardOut,
|
||||
PortTrafficBoardPanelsPut,
|
||||
PortTrafficBoardSummaryOut,
|
||||
PortTrafficBoardUpdate,
|
||||
PortTrafficPanelIn,
|
||||
PortTrafficPanelOut,
|
||||
)
|
||||
from .port_traffic_service import _target_out, _utcnow
|
||||
|
||||
|
||||
def _panel_out(db: Session, row: PortTrafficPanel) -> PortTrafficPanelOut:
|
||||
target = db.get(PortTrafficTarget, str(row.target_id or "")) if row.target_id else None
|
||||
baseline = (
|
||||
db.get(PortTrafficTarget, str(row.baseline_target_id or ""))
|
||||
if row.baseline_target_id
|
||||
else None
|
||||
)
|
||||
stale = not target or str(target.status or "") != "active"
|
||||
return PortTrafficPanelOut(
|
||||
id=str(row.id),
|
||||
board_id=str(row.board_id or ""),
|
||||
title=str(row.title or ""),
|
||||
target_id=str(row.target_id or ""),
|
||||
range_hours=int(row.range_hours or 24),
|
||||
baseline=str(row.baseline or "off"),
|
||||
offset_hours=int(row.offset_hours or 0),
|
||||
baseline_target_id=str(row.baseline_target_id or ""),
|
||||
y_mode=str(row.y_mode or "auto"),
|
||||
ord=int(row.ord or 0),
|
||||
col_span=max(1, int(row.col_span or 1)),
|
||||
row_span=max(1, int(row.row_span or 1)),
|
||||
stale=stale,
|
||||
target=_target_out(target) if target else None,
|
||||
baseline_target=_target_out(baseline) if baseline else None,
|
||||
created_at=row.created_at,
|
||||
updated_at=row.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _board_summary(db: Session, board: PortTrafficBoard) -> PortTrafficBoardSummaryOut:
|
||||
count = (
|
||||
db.query(func.count(PortTrafficPanel.id))
|
||||
.filter(PortTrafficPanel.board_id == board.id)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
return PortTrafficBoardSummaryOut(
|
||||
id=str(board.id),
|
||||
name=str(board.name or ""),
|
||||
remark=str(board.remark or ""),
|
||||
cols=max(1, min(4, int(board.cols or 2))),
|
||||
panel_count=int(count),
|
||||
created_by=str(board.created_by or ""),
|
||||
updated_by=str(board.updated_by or ""),
|
||||
created_at=board.created_at,
|
||||
updated_at=board.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _board_out(db: Session, board: PortTrafficBoard) -> PortTrafficBoardOut:
|
||||
panels = (
|
||||
db.query(PortTrafficPanel)
|
||||
.filter(PortTrafficPanel.board_id == board.id)
|
||||
.order_by(PortTrafficPanel.ord, PortTrafficPanel.created_at)
|
||||
.all()
|
||||
)
|
||||
summary = _board_summary(db, board)
|
||||
return PortTrafficBoardOut(
|
||||
**summary.model_dump(),
|
||||
panels=[_panel_out(db, p) for p in panels],
|
||||
)
|
||||
|
||||
|
||||
def _validate_panel_target(db: Session, target_id: str, *, allow_missing: bool = False) -> None:
|
||||
tid = str(target_id or "").strip()
|
||||
if not tid:
|
||||
raise HTTPException(status_code=400, detail="target_id_required")
|
||||
row = db.get(PortTrafficTarget, tid)
|
||||
if not row:
|
||||
if allow_missing:
|
||||
return
|
||||
raise HTTPException(status_code=400, detail="target_not_found")
|
||||
|
||||
|
||||
def _apply_panels(
|
||||
db: Session,
|
||||
board: PortTrafficBoard,
|
||||
panels: list[PortTrafficPanelIn],
|
||||
) -> None:
|
||||
cols = max(1, min(4, int(board.cols or 2)))
|
||||
db.query(PortTrafficPanel).filter(PortTrafficPanel.board_id == board.id).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
now = _utcnow()
|
||||
for i, item in enumerate(panels):
|
||||
_validate_panel_target(db, item.target_id)
|
||||
if item.baseline_target_id:
|
||||
_validate_panel_target(db, item.baseline_target_id, allow_missing=True)
|
||||
pid = str(item.id or "").strip() or uuid4().hex
|
||||
db.add(
|
||||
PortTrafficPanel(
|
||||
id=pid,
|
||||
board_id=str(board.id),
|
||||
title=str(item.title or "")[:256],
|
||||
target_id=str(item.target_id).strip(),
|
||||
range_hours=int(item.range_hours),
|
||||
baseline=str(item.baseline or "off"),
|
||||
offset_hours=int(item.offset_hours or 0),
|
||||
baseline_target_id=str(item.baseline_target_id or "").strip(),
|
||||
y_mode=str(item.y_mode or "auto"),
|
||||
ord=int(item.ord if item.ord is not None else i),
|
||||
col_span=max(1, min(cols, int(item.col_span or 1))),
|
||||
row_span=max(1, min(4, int(item.row_span or 1))),
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def list_boards(db: Session) -> list[PortTrafficBoardSummaryOut]:
|
||||
rows = db.query(PortTrafficBoard).order_by(PortTrafficBoard.updated_at.desc()).all()
|
||||
return [_board_summary(db, r) for r in rows]
|
||||
|
||||
|
||||
def get_board(db: Session, board_id: str) -> PortTrafficBoardOut:
|
||||
board = db.get(PortTrafficBoard, board_id)
|
||||
if not board:
|
||||
raise HTTPException(status_code=404, detail="board_not_found")
|
||||
return _board_out(db, board)
|
||||
|
||||
|
||||
def create_board(
|
||||
db: Session,
|
||||
body: PortTrafficBoardCreate,
|
||||
*,
|
||||
actor_user_id: str = "",
|
||||
) -> PortTrafficBoardOut:
|
||||
now = _utcnow()
|
||||
board = PortTrafficBoard(
|
||||
id=uuid4().hex,
|
||||
name=str(body.name).strip()[:256],
|
||||
remark=str(body.remark or "")[:1024],
|
||||
cols=max(1, min(4, int(body.cols or 2))),
|
||||
created_by=str(actor_user_id or ""),
|
||||
updated_by=str(actor_user_id or ""),
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
db.add(board)
|
||||
db.flush()
|
||||
if body.panels:
|
||||
_apply_panels(db, board, body.panels)
|
||||
db.commit()
|
||||
db.refresh(board)
|
||||
return _board_out(db, board)
|
||||
|
||||
|
||||
def update_board(
|
||||
db: Session,
|
||||
board_id: str,
|
||||
body: PortTrafficBoardUpdate,
|
||||
*,
|
||||
actor_user_id: str = "",
|
||||
) -> PortTrafficBoardOut:
|
||||
board = db.get(PortTrafficBoard, board_id)
|
||||
if not board:
|
||||
raise HTTPException(status_code=404, detail="board_not_found")
|
||||
data = body.model_dump(exclude_unset=True)
|
||||
if "name" in data and data["name"] is not None:
|
||||
board.name = str(data["name"]).strip()[:256]
|
||||
if "remark" in data and data["remark"] is not None:
|
||||
board.remark = str(data["remark"])[:1024]
|
||||
if "cols" in data and data["cols"] is not None:
|
||||
board.cols = max(1, min(4, int(data["cols"])))
|
||||
board.updated_by = str(actor_user_id or "")
|
||||
board.updated_at = _utcnow()
|
||||
db.commit()
|
||||
db.refresh(board)
|
||||
return _board_out(db, board)
|
||||
|
||||
|
||||
def put_board_panels(
|
||||
db: Session,
|
||||
board_id: str,
|
||||
body: PortTrafficBoardPanelsPut,
|
||||
*,
|
||||
actor_user_id: str = "",
|
||||
) -> PortTrafficBoardOut:
|
||||
board = db.get(PortTrafficBoard, board_id)
|
||||
if not board:
|
||||
raise HTTPException(status_code=404, detail="board_not_found")
|
||||
_apply_panels(db, board, body.panels or [])
|
||||
board.updated_by = str(actor_user_id or "")
|
||||
board.updated_at = _utcnow()
|
||||
db.commit()
|
||||
db.refresh(board)
|
||||
return _board_out(db, board)
|
||||
|
||||
|
||||
def delete_board(db: Session, board_id: str) -> dict[str, Any]:
|
||||
board = db.get(PortTrafficBoard, board_id)
|
||||
if not board:
|
||||
raise HTTPException(status_code=404, detail="board_not_found")
|
||||
db.query(PortTrafficPanel).filter(PortTrafficPanel.board_id == board_id).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
db.delete(board)
|
||||
db.commit()
|
||||
return {"ok": True, "id": board_id}
|
||||
|
||||
|
||||
def delete_panels_for_targets(db: Session, target_ids: list[str]) -> None:
|
||||
ids = [str(x) for x in target_ids if x]
|
||||
if not ids:
|
||||
return
|
||||
db.query(PortTrafficPanel).filter(PortTrafficPanel.target_id.in_(ids)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
db.query(PortTrafficPanel).filter(PortTrafficPanel.baseline_target_id.in_(ids)).update(
|
||||
{PortTrafficPanel.baseline_target_id: ""},
|
||||
synchronize_session=False,
|
||||
)
|
||||
|
|
@ -123,6 +123,54 @@ def ensure_port_traffic_series_schema(conn) -> None:
|
|||
"CREATE INDEX IF NOT EXISTS ix_port_traffic_event_level ON port_traffic_event (level)"
|
||||
)
|
||||
|
||||
conn.exec_driver_sql(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS port_traffic_board (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
name VARCHAR(256) DEFAULT '',
|
||||
remark VARCHAR(1024) DEFAULT '',
|
||||
cols INTEGER DEFAULT 2,
|
||||
created_by VARCHAR(64) DEFAULT '',
|
||||
updated_by VARCHAR(64) DEFAULT '',
|
||||
created_at TIMESTAMP,
|
||||
updated_at TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE INDEX IF NOT EXISTS ix_port_traffic_board_name ON port_traffic_board (name)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE INDEX IF NOT EXISTS ix_port_traffic_board_updated_at ON port_traffic_board (updated_at)"
|
||||
)
|
||||
|
||||
conn.exec_driver_sql(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS port_traffic_panel (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
board_id VARCHAR(64) DEFAULT '',
|
||||
title VARCHAR(256) DEFAULT '',
|
||||
target_id VARCHAR(64) DEFAULT '',
|
||||
range_hours INTEGER DEFAULT 24,
|
||||
baseline VARCHAR(16) DEFAULT 'off',
|
||||
offset_hours INTEGER DEFAULT 0,
|
||||
baseline_target_id VARCHAR(64) DEFAULT '',
|
||||
y_mode VARCHAR(16) DEFAULT 'auto',
|
||||
ord INTEGER DEFAULT 0,
|
||||
col_span INTEGER DEFAULT 1,
|
||||
row_span INTEGER DEFAULT 1,
|
||||
created_at TIMESTAMP,
|
||||
updated_at TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE INDEX IF NOT EXISTS ix_port_traffic_panel_board_id ON port_traffic_panel (board_id)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE INDEX IF NOT EXISTS ix_port_traffic_panel_target_id ON port_traffic_panel (target_id)"
|
||||
)
|
||||
|
||||
|
||||
def migrate_tasks_to_devices(db: Session) -> int:
|
||||
"""Collapse legacy port_traffic_task rows into per-NE devices; reassign targets/series."""
|
||||
|
|
|
|||
|
|
@ -12,11 +12,22 @@ from .db import get_db
|
|||
from .models import PortTrafficDevice
|
||||
from .port_traffic_schemas import (
|
||||
DiscoverPortsRequest,
|
||||
PortTrafficBoardCreate,
|
||||
PortTrafficBoardPanelsPut,
|
||||
PortTrafficBoardUpdate,
|
||||
PortTrafficDeviceCreate,
|
||||
PortTrafficDeviceUpdate,
|
||||
PortTrafficInterfacesPut,
|
||||
PortTrafficReplacePortRequest,
|
||||
)
|
||||
from .port_traffic_board_service import (
|
||||
create_board,
|
||||
delete_board,
|
||||
get_board,
|
||||
list_boards,
|
||||
put_board_panels,
|
||||
update_board,
|
||||
)
|
||||
from .port_traffic_service import (
|
||||
compare_targets,
|
||||
create_device,
|
||||
|
|
@ -50,6 +61,102 @@ def api_dashboard(db: Session = Depends(get_db)):
|
|||
return dashboard(db).model_dump()
|
||||
|
||||
|
||||
@router.get("/boards")
|
||||
def api_list_boards(db: Session = Depends(get_db)):
|
||||
return {"items": [b.model_dump() for b in list_boards(db)]}
|
||||
|
||||
|
||||
@router.post("/boards")
|
||||
def api_create_board(
|
||||
body: PortTrafficBoardCreate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
uid, uname = _actor(request)
|
||||
out = create_board(db, body, actor_user_id=uid)
|
||||
write_audit(
|
||||
db,
|
||||
action="port_traffic.board.create",
|
||||
actor_user_id=uid,
|
||||
actor_username=uname,
|
||||
method="POST",
|
||||
path="/v1/port-traffic/boards",
|
||||
status_code=200,
|
||||
detail={"id": out.id, "name": out.name},
|
||||
)
|
||||
return out.model_dump()
|
||||
|
||||
|
||||
@router.get("/boards/{board_id}")
|
||||
def api_get_board(board_id: str, db: Session = Depends(get_db)):
|
||||
return get_board(db, board_id).model_dump()
|
||||
|
||||
|
||||
@router.patch("/boards/{board_id}")
|
||||
def api_update_board(
|
||||
board_id: str,
|
||||
body: PortTrafficBoardUpdate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
uid, uname = _actor(request)
|
||||
out = update_board(db, board_id, body, actor_user_id=uid)
|
||||
write_audit(
|
||||
db,
|
||||
action="port_traffic.board.update",
|
||||
actor_user_id=uid,
|
||||
actor_username=uname,
|
||||
method="PATCH",
|
||||
path=f"/v1/port-traffic/boards/{board_id}",
|
||||
status_code=200,
|
||||
detail={"id": board_id},
|
||||
)
|
||||
return out.model_dump()
|
||||
|
||||
|
||||
@router.put("/boards/{board_id}/panels")
|
||||
def api_put_board_panels(
|
||||
board_id: str,
|
||||
body: PortTrafficBoardPanelsPut,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
uid, uname = _actor(request)
|
||||
out = put_board_panels(db, board_id, body, actor_user_id=uid)
|
||||
write_audit(
|
||||
db,
|
||||
action="port_traffic.board.panels_put",
|
||||
actor_user_id=uid,
|
||||
actor_username=uname,
|
||||
method="PUT",
|
||||
path=f"/v1/port-traffic/boards/{board_id}/panels",
|
||||
status_code=200,
|
||||
detail={"id": board_id, "count": len(out.panels)},
|
||||
)
|
||||
return out.model_dump()
|
||||
|
||||
|
||||
@router.delete("/boards/{board_id}")
|
||||
def api_delete_board(
|
||||
board_id: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
uid, uname = _actor(request)
|
||||
out = delete_board(db, board_id)
|
||||
write_audit(
|
||||
db,
|
||||
action="port_traffic.board.delete",
|
||||
actor_user_id=uid,
|
||||
actor_username=uname,
|
||||
method="DELETE",
|
||||
path=f"/v1/port-traffic/boards/{board_id}",
|
||||
status_code=200,
|
||||
detail={"id": board_id},
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
@router.get("/devices")
|
||||
def api_list_devices(
|
||||
page: int = Query(default=1, ge=1),
|
||||
|
|
|
|||
|
|
@ -199,3 +199,70 @@ class PortTrafficEventOut(BaseModel):
|
|||
class PortTrafficEventsOut(BaseModel):
|
||||
items: list[PortTrafficEventOut] = Field(default_factory=list)
|
||||
total: int = 0
|
||||
|
||||
|
||||
class PortTrafficPanelIn(BaseModel):
|
||||
id: str | None = None
|
||||
title: str = ""
|
||||
target_id: str = Field(min_length=1, max_length=64)
|
||||
range_hours: int = Field(default=24, ge=1, le=24 * 90)
|
||||
baseline: Literal["off", "day", "week", "shift", "custom"] = "off"
|
||||
offset_hours: int = Field(default=0, ge=0, le=24 * 90)
|
||||
baseline_target_id: str = ""
|
||||
y_mode: Literal["auto", "current", "util"] = "auto"
|
||||
ord: int = 0
|
||||
col_span: int = Field(default=1, ge=1, le=4)
|
||||
row_span: int = Field(default=1, ge=1, le=4)
|
||||
|
||||
|
||||
class PortTrafficPanelOut(BaseModel):
|
||||
id: str
|
||||
board_id: str
|
||||
title: str = ""
|
||||
target_id: str
|
||||
range_hours: int = 24
|
||||
baseline: str = "off"
|
||||
offset_hours: int = 0
|
||||
baseline_target_id: str = ""
|
||||
y_mode: str = "auto"
|
||||
ord: int = 0
|
||||
col_span: int = 1
|
||||
row_span: int = 1
|
||||
stale: bool = False
|
||||
target: PortTrafficTargetOut | None = None
|
||||
baseline_target: PortTrafficTargetOut | None = None
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
class PortTrafficBoardCreate(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=256)
|
||||
remark: str = Field(default="", max_length=1024)
|
||||
cols: int = Field(default=2, ge=1, le=4)
|
||||
panels: list[PortTrafficPanelIn] = Field(default_factory=list)
|
||||
|
||||
|
||||
class PortTrafficBoardUpdate(BaseModel):
|
||||
name: str | None = Field(default=None, min_length=1, max_length=256)
|
||||
remark: str | None = Field(default=None, max_length=1024)
|
||||
cols: int | None = Field(default=None, ge=1, le=4)
|
||||
|
||||
|
||||
class PortTrafficBoardSummaryOut(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
remark: str = ""
|
||||
cols: int = 2
|
||||
panel_count: int = 0
|
||||
created_by: str = ""
|
||||
updated_by: str = ""
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
class PortTrafficBoardOut(PortTrafficBoardSummaryOut):
|
||||
panels: list[PortTrafficPanelOut] = Field(default_factory=list)
|
||||
|
||||
|
||||
class PortTrafficBoardPanelsPut(BaseModel):
|
||||
panels: list[PortTrafficPanelIn] = Field(default_factory=list)
|
||||
|
|
|
|||
|
|
@ -267,6 +267,9 @@ def delete_device(db: Session, device_id: str) -> dict[str, Any]:
|
|||
ids = [str(t.id) for t in targets]
|
||||
series_ids = [str(t.series_id) for t in targets if t.series_id]
|
||||
if ids:
|
||||
from .port_traffic_board_service import delete_panels_for_targets
|
||||
|
||||
delete_panels_for_targets(db, ids)
|
||||
db.query(PortTrafficSample).filter(PortTrafficSample.target_row_id.in_(ids)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue