diff --git a/netx_api/managed_ne_router.py b/netx_api/managed_ne_router.py index f691641..75b25e8 100644 --- a/netx_api/managed_ne_router.py +++ b/netx_api/managed_ne_router.py @@ -1,6 +1,7 @@ from __future__ import annotations from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile +from fastapi.responses import Response from sqlalchemy.orm import Session from .db import get_db @@ -9,6 +10,7 @@ from .ne_connect import schedule_connect_tests from .ne_crypto import credentials_configured from .ne_schemas import ConnectTestRequest, ManagedNeCreate, ManagedNeUpdate from .ne_service import ( + build_managed_ne_import_template, create_managed_ne, delete_managed_ne, get_managed_ne, @@ -70,6 +72,16 @@ def api_delete_managed_ne(ne_id: str, db: Session = Depends(get_db)): return delete_managed_ne(db, ne_id) +@router.get("/import/template") +def api_managed_ne_import_template(format: str = Query(default="xlsx")): + filename, payload, media_type = build_managed_ne_import_template(format) + return Response( + content=payload, + media_type=media_type, + headers={"content-disposition": f'attachment; filename="{filename}"'}, + ) + + @router.post("/import") async def api_import_managed_ne(file: UploadFile = File(...), db: Session = Depends(get_db)): content = await file.read() diff --git a/netx_api/ne_service.py b/netx_api/ne_service.py index 65d1c18..aff9b12 100644 --- a/netx_api/ne_service.py +++ b/netx_api/ne_service.py @@ -195,6 +195,51 @@ def delete_managed_ne(db: Session, ne_id: str) -> dict[str, bool]: return {"ok": True} +def build_managed_ne_import_template(fmt: str = "xlsx") -> tuple[str, bytes, str]: + """Return (filename, content, media_type) for bulk-import template.""" + rows = [ + { + "device_type": "cisco_ios", + "ip": "192.168.0.1", + "username": "admin", + "password": "your_password", + "port": 22, + "protocol": "ssh", + "name": "Core-SW1", + "vendor": "Cisco", + }, + { + "device_type": "huawei", + "ip": "192.168.0.2", + "username": "admin", + "password": "your_password", + "port": 22, + "protocol": "ssh", + "name": "AGG-01", + "vendor": "Huawei", + }, + ] + df = pd.DataFrame(rows, columns=list(IMPORT_COLUMNS)) + buf = BytesIO() + kind = str(fmt or "xlsx").strip().lower() + if kind == "csv": + df.to_csv(buf, index=False, encoding="utf-8-sig") + return ( + "managed_ne_import_template.csv", + buf.getvalue(), + "text/csv; charset=utf-8", + ) + device_types_df = pd.DataFrame({"device_type": list(SUPPORTED_DEVICE_TYPES)}) + with pd.ExcelWriter(buf, engine="openpyxl") as writer: + df.to_excel(writer, sheet_name="import", index=False) + device_types_df.to_excel(writer, sheet_name="device_type", index=False) + return ( + "managed_ne_import_template.xlsx", + buf.getvalue(), + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ) + + def import_managed_ne(db: Session, content: bytes, filename: str) -> ImportResult: _require_crypto() name = str(filename or "").lower() diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index bfa133b..a852cab 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -102,6 +102,7 @@ const en = { "NETX_CREDENTIAL_SECRET_KEY is not set. Configure a Fernet key in .env and restart the API to store passwords.", add: "Add NE", importBtn: "Import", + downloadTemplate: "Download template", importing: "Importing…", edit: "Edit", delete: "Delete", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index edb4437..3c160b5 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -101,6 +101,7 @@ const zh = { credsNotConfigured: "未配置 NETX_CREDENTIAL_SECRET_KEY,无法保存密码。请在 .env 中设置 Fernet 密钥后重启 API。", add: "新增网元", importBtn: "批量导入", + downloadTemplate: "下载导入模板", importing: "导入中…", edit: "编辑", delete: "删除", diff --git a/web/src/pages/NePage.tsx b/web/src/pages/NePage.tsx index d936e2a..3495ffd 100644 --- a/web/src/pages/NePage.tsx +++ b/web/src/pages/NePage.tsx @@ -7,6 +7,7 @@ import { fetchManagedNe, fetchManagedNeMeta, importManagedNe, + managedNeImportTemplateUrl, updateManagedNe, } from "../services/api"; import { queryKeys } from "../constants/queryKeys"; @@ -233,6 +234,12 @@ export function NePage() { +