diff --git a/netx_api/managed_ne_router.py b/netx_api/managed_ne_router.py index ef11f3e..0f9d217 100644 --- a/netx_api/managed_ne_router.py +++ b/netx_api/managed_ne_router.py @@ -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()] diff --git a/netx_api/ne_service.py b/netx_api/ne_service.py index 80eccce..e541228 100644 --- a/netx_api/ne_service.py +++ b/netx_api/ne_service.py @@ -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 = [ diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index e2aa6c6..5c290f0 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -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", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index b718acf..50adcf8 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -135,6 +135,11 @@ const zh = { edit: "编辑", delete: "删除", confirmDelete: "确定删除该网元?", + batchDelete: "批量删除", + batchDeleting: "删除中…", + batchDeleteConfirm: "确定删除选中的 {{n}} 台网元?此操作不可恢复。", + batchDeleteDone: "已删除 {{n}} 台网元", + batchDeleteSelectRequired: "请先勾选要删除的网元", keywordPh: "名称 / IP / 用户名 / 标签", allVendors: "全部厂商", allConnectStatus: "全部连通状态", diff --git a/web/src/index.css b/web/src/index.css index 8df66d1..04c72d6 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -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; diff --git a/web/src/pages/NePage.tsx b/web/src/pages/NePage.tsx index 0e379c8..391a617 100644 --- a/web/src/pages/NePage.tsx +++ b/web/src/pages/NePage.tsx @@ -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")} + diff --git a/web/src/services/api.ts b/web/src/services/api.ts index 3f278c2..418e144 100644 --- a/web/src/services/api.ts +++ b/web/src/services/api.ts @@ -126,6 +126,9 @@ export const updateManagedNe = (id: string, body: Record) => 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 });