feat(ne): batch delete selected managed network elements

Add POST /v1/managed-ne/batch-delete and toolbar button with confirmation on NE management page.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-05-28 19:02:16 +08:00
parent d3eae3351c
commit 778442dc57
7 changed files with 75 additions and 0 deletions

View file

@ -13,6 +13,7 @@ from .ne_service import (
batch_apply_hop_proxy,
build_managed_ne_import_template,
create_managed_ne,
batch_delete_managed_ne,
delete_managed_ne,
get_managed_ne,
import_managed_ne,
@ -96,6 +97,11 @@ def api_batch_apply_hop(body: BatchHopApplyRequest, db: Session = Depends(get_db
return batch_apply_hop_proxy(db, body.ids, body.hop)
@router.post("/batch-delete")
def api_batch_delete_managed_ne(body: ConnectTestRequest, db: Session = Depends(get_db)):
return batch_delete_managed_ne(db, body.ids)
@router.post("/connect-test")
def api_connect_test(body: ConnectTestRequest, db: Session = Depends(get_db)):
ids = [str(x).strip() for x in body.ids if str(x).strip()]

View file

@ -328,6 +328,21 @@ def delete_managed_ne(db: Session, ne_id: str) -> dict[str, bool]:
return {"ok": True}
def batch_delete_managed_ne(db: Session, ids: list[str]) -> dict[str, Any]:
ne_ids = [str(x).strip() for x in ids if str(x).strip()]
if not ne_ids:
raise HTTPException(status_code=400, detail="ids_required")
rows = db.query(ManagedNE).filter(ManagedNE.id.in_(ne_ids)).all()
found_ids = {str(r.id) for r in rows}
missing = [x for x in ne_ids if x not in found_ids]
if missing:
raise HTTPException(status_code=404, detail=f"managed_ne_not_found: {','.join(missing[:5])}")
for row in rows:
db.delete(row)
db.commit()
return {"ok": True, "deleted": len(rows)}
def build_managed_ne_import_template(fmt: str = "xlsx") -> tuple[str, bytes, str]:
"""Return (filename, content, media_type) for bulk-import template."""
rows = [

View file

@ -136,6 +136,11 @@ const en = {
edit: "Edit",
delete: "Delete",
confirmDelete: "Delete this network element?",
batchDelete: "Batch delete",
batchDeleting: "Deleting…",
batchDeleteConfirm: "Delete {{n}} selected network element(s)? This cannot be undone.",
batchDeleteDone: "Deleted {{n}} network element(s)",
batchDeleteSelectRequired: "Select network elements to delete",
keywordPh: "name / IP / user / tags",
allVendors: "All vendors",
allConnectStatus: "All connect status",

View file

@ -135,6 +135,11 @@ const zh = {
edit: "编辑",
delete: "删除",
confirmDelete: "确定删除该网元?",
batchDelete: "批量删除",
batchDeleting: "删除中…",
batchDeleteConfirm: "确定删除选中的 {{n}} 台网元?此操作不可恢复。",
batchDeleteDone: "已删除 {{n}} 台网元",
batchDeleteSelectRequired: "请先勾选要删除的网元",
keywordPh: "名称 / IP / 用户名 / 标签",
allVendors: "全部厂商",
allConnectStatus: "全部连通状态",

View file

@ -793,6 +793,21 @@ pre {
gap: 8px;
}
.btn--danger {
color: #b71c1c;
border-color: #ef9a9a;
background: #ffebee;
}
.btn--danger:hover:not(:disabled) {
background: #ffcdd2;
}
.btn--danger:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.table-actions {
display: flex;
gap: 8px;

View file

@ -2,6 +2,7 @@ import { useMemo, useRef, useState, type ReactNode } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
batchApplyHopManagedNe,
batchDeleteManagedNe,
connectTestManagedNe,
createManagedNe,
deleteManagedNe,
@ -203,6 +204,16 @@ export function NePage() {
onError: (err) => showError(String(err)),
});
const batchDeleteMutation = useMutation({
mutationFn: batchDeleteManagedNe,
onSuccess: async (res) => {
setSelected([]);
showOk(t("managedNe.batchDeleteDone", { n: res.deleted }));
await invalidateList();
},
onError: (err) => showError(String(err)),
});
const batchHopMutation = useMutation({
mutationFn: () =>
batchApplyHopManagedNe(selected, {
@ -361,6 +372,21 @@ export function NePage() {
>
{batchHopMutation.isPending ? t("managedNe.hop.applying") : t("managedNe.hop.batchAdd")}
</button>
<button
type="button"
className="btn btn--danger"
disabled={selected.length === 0 || batchDeleteMutation.isPending}
onClick={() => {
if (selected.length === 0) {
showError(t("managedNe.batchDeleteSelectRequired"));
return;
}
if (!window.confirm(t("managedNe.batchDeleteConfirm", { n: selected.length }))) return;
batchDeleteMutation.mutate(selected);
}}
>
{batchDeleteMutation.isPending ? t("managedNe.batchDeleting") : t("managedNe.batchDelete")}
</button>
<button type="button" onClick={() => invalidateList()}>
{t("common.refresh")}
</button>

View file

@ -126,6 +126,9 @@ export const updateManagedNe = (id: string, body: Record<string, unknown>) =>
export const deleteManagedNe = (id: string) => apiDelete<{ ok: boolean }>(`/v1/managed-ne/${id}`);
export const batchDeleteManagedNe = (ids: string[]) =>
apiPost<{ ok: boolean; deleted: number }>("/v1/managed-ne/batch-delete", { ids });
export const connectTestManagedNe = (ids: string[]) =>
apiPost<{ ok: boolean; submitted: number }>("/v1/managed-ne/connect-test", { ids });