import { useMemo, useRef, useState, type ReactNode } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { connectTestManagedNe, createManagedNe, deleteManagedNe, fetchManagedNe, fetchManagedNeMeta, importManagedNe, managedNeImportTemplateUrl, updateManagedNe, } from "../services/api"; import { queryKeys } from "../constants/queryKeys"; import { useI18n } from "../i18n"; import { useToast } from "../hooks/useToast"; import type { ManagedNeItem } from "../types"; import { pageCount } from "../utils/display"; import { formatSystemTime } from "../utils/time"; import { isAutoHopTemplate, zteHopTemplate } from "../utils/zteHop"; type FormState = { name: string; vendor: string; device_type: string; ip_address: string; port: number; protocol: string; username: string; password: string; tags: string; remark: string; hop_enabled: boolean; hop_host: string; hop_port: number; hop_protocol: string; hop_username: string; hop_password: string; hop_command_template: string; hop_vrf: string; }; const emptyForm = (): FormState => ({ name: "", vendor: "ZTE", device_type: "zte_zxros", ip_address: "", port: 22, protocol: "ssh", username: "", password: "", tags: "", remark: "", hop_enabled: false, hop_host: "", hop_port: 22, hop_protocol: "ssh", hop_username: "", hop_password: "", hop_command_template: zteHopTemplate("ssh", ""), hop_vrf: "", }); function applyHopTemplate(prev: FormState, protocol: string, vrf: string, force = false): Partial { if (!force && !isAutoHopTemplate(prev.hop_command_template, prev.hop_protocol, prev.hop_vrf)) { return {}; } return { hop_command_template: zteHopTemplate(protocol, vrf) }; } function FormLabel({ children, required }: { children: ReactNode; required?: boolean }) { return ( {children} {required ? ( ) : null} ); } function connectPillLevel(status: string): "up" | "down" | "unknown" | "warn" { if (status === "pass") return "up"; if (status === "fail") return "down"; if (status === "testing") return "warn"; return "unknown"; } export function NePage() { const { t } = useI18n(); const { showOk, showError } = useToast(); const queryClient = useQueryClient(); const importRef = useRef(null); const [keyword, setKeyword] = useState(""); const [vendorFilter, setVendorFilter] = useState(""); const [statusFilter, setStatusFilter] = useState(""); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(50); const [selected, setSelected] = useState([]); const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(null); const [form, setForm] = useState(emptyForm); const metaQuery = useQuery({ queryKey: queryKeys.managedNeMeta, queryFn: fetchManagedNeMeta, staleTime: 60_000, }); const listQuery = useQuery({ queryKey: queryKeys.managedNe(keyword, vendorFilter, statusFilter, page, pageSize), queryFn: () => fetchManagedNe({ keyword, vendor: vendorFilter, connectStatus: statusFilter, page, pageSize, }), refetchInterval: (q) => { const items = q.state.data?.items || []; return items.some((x) => x.connect_status === "testing") ? 2000 : false; }, }); const total = listQuery.data?.total ?? 0; const pages = pageCount(total, pageSize); const perPage = (n: number) => t("common.perPage", { n }); const invalidateList = () => queryClient.invalidateQueries({ queryKey: queryKeys.managedNeAll }); const saveMutation = useMutation({ mutationFn: async () => { const body = { name: form.name, vendor: form.vendor, device_type: form.device_type, ip_address: form.ip_address, port: form.port, protocol: form.protocol, username: form.username, tags: form.tags, remark: form.remark, hop_enabled: form.hop_enabled, hop_vendor: "zte", hop_host: form.hop_host, hop_port: form.hop_port, hop_protocol: form.hop_protocol, hop_username: form.hop_username, hop_command_template: form.hop_command_template, hop_vrf: form.hop_vrf, ...(form.password ? { password: form.password } : {}), ...(form.hop_password ? { hop_password: form.hop_password } : {}), }; if (form.hop_enabled) { if (!form.hop_host.trim()) throw new Error(t("managedNe.hop.hostRequired")); if (!form.hop_username.trim()) throw new Error(t("managedNe.hop.userRequired")); if (!editing && !form.hop_password) throw new Error(t("managedNe.hop.passwordRequired")); } if (editing) { if (!form.password) delete (body as { password?: string }).password; if (!form.hop_password) delete (body as { hop_password?: string }).hop_password; return updateManagedNe(editing.id, body); } if (!form.password) throw new Error(t("managedNe.form.passwordRequired")); return createManagedNe({ ...body, password: form.password }); }, onSuccess: async () => { setModalOpen(false); setEditing(null); setForm(emptyForm()); showOk(editing ? t("managedNe.form.updated") : t("managedNe.form.created")); await invalidateList(); }, onError: (err) => showError(String(err)), }); const deleteMutation = useMutation({ mutationFn: deleteManagedNe, onSuccess: async () => { showOk(t("managedNe.form.deleted")); await invalidateList(); }, onError: (err) => showError(String(err)), }); const connectMutation = useMutation({ mutationFn: connectTestManagedNe, onSuccess: async (res) => { showOk(t("managedNe.connect.submitted", { n: res.submitted })); await invalidateList(); }, onError: (err) => showError(String(err)), }); const importMutation = useMutation({ mutationFn: importManagedNe, onSuccess: async (res) => { showOk( t("managedNe.importResult.done", { inserted: res.inserted, updated: res.updated, failed: res.failed.length, }), ); await invalidateList(); }, onError: (err) => showError(String(err)), }); const vendors = metaQuery.data?.vendors ?? []; const deviceTypes = metaQuery.data?.device_types ?? []; const credsOk = metaQuery.data?.credentials_configured ?? false; const allSelected = useMemo(() => { const items = listQuery.data?.items ?? []; return items.length > 0 && items.every((x) => selected.includes(x.id)); }, [listQuery.data?.items, selected]); const openCreate = () => { setEditing(null); setForm(emptyForm()); setModalOpen(true); }; const openEdit = (row: ManagedNeItem) => { setEditing(row); setForm({ name: row.name, vendor: row.vendor, device_type: row.device_type, ip_address: row.ip_address, port: row.port, protocol: row.protocol, username: row.username, password: "", tags: row.tags, remark: row.remark, hop_enabled: row.hop_enabled, hop_host: row.hop_host, hop_port: row.hop_port, hop_protocol: row.hop_protocol, hop_username: row.hop_username, hop_password: "", hop_command_template: isAutoHopTemplate( row.hop_command_template, row.hop_protocol, row.hop_vrf, ) ? zteHopTemplate(row.hop_protocol, row.hop_vrf) : row.hop_command_template || zteHopTemplate(row.hop_protocol, row.hop_vrf), hop_vrf: row.hop_vrf, }); setModalOpen(true); }; const toggleSelect = (id: string) => { setSelected((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id])); }; const toggleSelectAll = () => { const items = listQuery.data?.items ?? []; if (allSelected) { const pageIds = new Set(items.map((x) => x.id)); setSelected((prev) => prev.filter((id) => !pageIds.has(id))); } else { const ids = items.map((x) => x.id); setSelected((prev) => [...new Set([...prev, ...ids])]); } }; return (
{!credsOk ? (

{t("managedNe.credsNotConfigured")}

) : null}

{t("managedNe.title")}

{ const file = e.target.files?.[0]; e.target.value = ""; if (file) importMutation.mutate(file); }} />
setKeyword(e.target.value)} />
{(listQuery.data?.items || []).map((row) => ( ))}
{t("managedNe.col.name")} {t("managedNe.col.vendor")} {t("managedNe.col.deviceType")} {t("managedNe.col.ip")} {t("managedNe.col.user")} {t("managedNe.col.connect")} {t("managedNe.col.testedAt")} {t("managedNe.col.actions")}
toggleSelect(row.id)} /> {row.name || row.ip_address} {row.vendor} {row.device_type} {row.ip_address}:{row.port}/{row.protocol} {row.hop_enabled ? ( {t("managedNe.hop.badge")} ) : null} {row.username} {row.connect_status} {row.connect_tested_at ? formatSystemTime(row.connect_tested_at, { assumeUtcNaive: true }) : t("common.empty")}
{t("common.pagerMeta", { total, page, pages })}
{modalOpen ? (
setModalOpen(false)}>
e.stopPropagation()}>

{editing ? t("managedNe.form.editTitle") : t("managedNe.form.createTitle")}

{t("managedNe.form.requiredHint")}

{t("managedNe.hop.sectionTitle")} {form.hop_enabled ? (
) : null}
) : null}
); }