import { Button, Checkbox, Input, Label, TextField } from "@heroui/react"; import { useEffect, useMemo, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiDelete, apiPatch, apiPost, fetchCliMeta, fetchCliProfiles, fetchManagedNeMeta, fetchUmeCliOverride, fetchUmeNe, postUmeConnectTest, } from "../services/api"; import { HopProxyFields, emptyHopProxyFields, type HopProxyFieldsState } from "./HopProxyFields"; import { HelpHint } from "./HelpHint"; import { FieldSelect } from "./ui/FieldSelect"; import { queryKeys } from "../constants/queryKeys"; import { useI18n } from "../i18n"; import { useToast } from "../hooks/useToast"; import type { CliConnectProfileItem, UmeCliOverrideItem, UmeNeItem } from "../types"; import { defaultHopTemplate, isAutoHopTemplate, patchHopVendorChange } from "../utils/hopProxy"; import { formatSystemTime } from "../utils/time"; function connectPillLevel(status: string): "up" | "down" | "unknown" | "warn" { const s = String(status || "").toLowerCase(); if (s === "pass" || s === "ok") return "up"; if (s === "fail" || s === "error") return "down"; if (s === "testing") return "warn"; return "unknown"; } function formatUmeNePickLabel(row: UmeNeItem): string { const name = String(row.user_label || row.ne_name || row.host_name || "").trim(); const ip = String(row.ip_address || "").trim(); const id = String(row.ne_id || "").trim(); const head = [name, ip].filter(Boolean).join(" · "); return head ? `${head} · ${id}` : id; } type ProfileForm = { id: string; name: string; username: string; password: string; port: number; protocol: string; device_type_default: string; vendor_default: string; is_default: boolean; hop_enabled: boolean; hop: HopProxyFieldsState; }; function applyHopTemplate(hop: HopProxyFieldsState, protocol: string, vrf: string, force = false): Partial { if (!force && !isAutoHopTemplate(hop.hop_command_template, hop.hop_vendor, hop.hop_protocol, hop.hop_vrf)) { return {}; } return { hop_command_template: defaultHopTemplate(hop.hop_vendor, protocol, vrf) }; } const emptyForm = (): ProfileForm => ({ id: "", name: "default", username: "", password: "", port: 22, protocol: "ssh", device_type_default: "zte_zxros", vendor_default: "ZTE", is_default: true, hop_enabled: false, hop: emptyHopProxyFields(), }); function profileToForm(row: CliConnectProfileItem): ProfileForm { return { id: row.id, name: row.name, username: row.username, password: "", port: row.port, protocol: row.protocol, device_type_default: row.device_type_default, vendor_default: row.vendor_default, is_default: row.is_default, hop_enabled: row.hop_enabled, hop: { hop_vendor: (row.hop_vendor || "bastion") as HopProxyFieldsState["hop_vendor"], hop_host: row.hop_host ?? "", hop_port: row.hop_port ?? 22, hop_protocol: row.hop_protocol ?? "ssh", hop_username: row.hop_username ?? "", hop_password: "", hop_command_template: row.hop_command_template ?? "", hop_vrf: row.hop_vrf ?? "", hop_target_auth_mode: (row.hop_target_auth_mode || "bastion_managed") as HopProxyFieldsState["hop_target_auth_mode"], hop_enter_system_view: Boolean(row.hop_enter_system_view), }, }; } export function UmeCliConnectPanel({ enabled = true, embedded = false }: { enabled?: boolean; embedded?: boolean }) { const { t } = useI18n(); const { showOk, showError } = useToast(); const queryClient = useQueryClient(); const [form, setForm] = useState(emptyForm); const [sampleUmeNeId, setSampleUmeNeId] = useState(""); const [sampleFilter, setSampleFilter] = useState(""); const [sampleOpen, setSampleOpen] = useState(false); const [debouncedFilter, setDebouncedFilter] = useState(""); const [testPending, setTestPending] = useState(false); const sampleSuggestRef = useRef(null); const testNeId = sampleUmeNeId.trim(); useEffect(() => { const timer = window.setTimeout(() => setDebouncedFilter(sampleFilter.trim()), 220); return () => window.clearTimeout(timer); }, [sampleFilter]); useEffect(() => { setTestPending(false); }, [testNeId]); const metaQuery = useQuery({ queryKey: queryKeys.cliMeta, queryFn: fetchCliMeta, enabled }); const neMetaQuery = useQuery({ queryKey: queryKeys.managedNeMeta, queryFn: fetchManagedNeMeta, enabled }); const profilesQuery = useQuery({ queryKey: queryKeys.cliProfiles, queryFn: fetchCliProfiles, enabled }); const sampleNeQuery = useQuery({ queryKey: queryKeys.umeNE(debouncedFilter, 1, 5), queryFn: () => fetchUmeNe({ keyword: debouncedFilter, page: 1, pageSize: 5 }), enabled: enabled && sampleOpen, staleTime: 5000, }); const sampleHits = useMemo( () => (sampleNeQuery.data?.items || []).slice(0, 5), [sampleNeQuery.data], ); useEffect(() => { if (!sampleOpen) return; const el = sampleSuggestRef.current; if (!el) return; const timer = window.setTimeout(() => { const body = el.closest('[data-slot="modal-body"], .modal__body'); if (body) { const elRect = el.getBoundingClientRect(); const bodyRect = body.getBoundingClientRect(); const overflow = elRect.bottom - bodyRect.bottom + 16; if (overflow > 0) { body.scrollBy({ top: overflow, behavior: "smooth" }); return; } } el.scrollIntoView({ block: "nearest", behavior: "smooth" }); }, 50); return () => window.clearTimeout(timer); }, [sampleOpen, sampleHits.length, sampleNeQuery.isFetching]); const overrideQuery = useQuery({ queryKey: queryKeys.umeCliOverride(testNeId), queryFn: () => fetchUmeCliOverride(testNeId), enabled: Boolean(testNeId), refetchInterval: (q) => { const status = String(q.state.data?.connect_status || "").toLowerCase(); return status === "testing" || testPending ? 2000 : false; }, }); const overrideResult: UmeCliOverrideItem | null = overrideQuery.data ?? null; const overrideStatus = String(overrideResult?.connect_status || "").toLowerCase(); const overrideTesting = overrideStatus === "testing" || (testPending && !overrideResult?.connect_tested_at); useEffect(() => { const items = profilesQuery.data?.items || []; const current = items.find((x) => x.id === form.id); const picked = current || items.find((x) => x.is_default) || items[0]; if (picked && (!form.id || !current)) { setForm(profileToForm(picked)); } }, [profilesQuery.data, form.id]); const saveMutation = useMutation({ mutationFn: async () => { if (form.hop_enabled) { if (!form.hop.hop_host.trim()) throw new Error(t("managedNe.hop.hostRequired")); if (!form.hop.hop_username.trim()) throw new Error(t("managedNe.hop.userRequired")); if (!form.id && !form.hop.hop_password) throw new Error(t("managedNe.hop.passwordRequired")); } const body = { name: form.name.trim(), username: form.username.trim(), password: form.password || undefined, port: form.port, protocol: form.protocol, device_type_default: form.device_type_default, vendor_default: form.vendor_default, is_default: form.is_default, hop_enabled: form.hop_enabled, hop_vendor: form.hop.hop_vendor, hop_host: form.hop.hop_host, hop_port: form.hop.hop_port, hop_protocol: form.hop.hop_protocol, hop_username: form.hop.hop_username, hop_password: form.hop_enabled ? form.hop.hop_password || undefined : undefined, hop_command_template: form.hop.hop_command_template, hop_vrf: form.hop.hop_vrf, hop_target_auth_mode: form.hop.hop_target_auth_mode, hop_enter_system_view: form.hop.hop_enter_system_view, }; if (form.id) { return apiPatch(`/v1/cli/profiles/${form.id}`, body); } return apiPost("/v1/cli/profiles", { ...body, password: form.password }); }, onSuccess: async (row) => { setForm(profileToForm(row)); await queryClient.invalidateQueries({ queryKey: queryKeys.cliProfiles }); await queryClient.invalidateQueries({ queryKey: queryKeys.cliMeta }); showOk(t("ume.cli.saved")); }, onError: (e: Error) => showError(e.message), }); const connectTestMutation = useMutation({ mutationFn: async () => { const id = sampleUmeNeId.trim(); if (!id) throw new Error(t("ume.cli.sampleNeRequired")); return postUmeConnectTest([id]); }, onSuccess: async () => { const id = sampleUmeNeId.trim(); setTestPending(true); showOk(t("ume.cli.connectTestSubmitted")); if (id) { await queryClient.invalidateQueries({ queryKey: queryKeys.umeCliOverride(id) }); } }, onError: (e: Error) => { setTestPending(false); showError(e.message); }, }); useEffect(() => { if (!testPending) return; if (!overrideResult) return; if (overrideStatus === "testing") return; setTestPending(false); void queryClient.invalidateQueries({ queryKey: queryKeys.cliTargetsAll }); }, [testPending, overrideResult, overrideStatus, queryClient]); const deviceTypes = neMetaQuery.data?.device_types ?? []; const vendors = neMetaQuery.data?.vendors ?? []; const cliReady = Boolean(metaQuery.data?.cli_profile_ready); const body = (
{!embedded ? (

{t("ume.cli.title")}

) : null}

{cliReady ? t("ume.cli.statusReady") : t("ume.cli.statusNotReady")}

setForm({ ...form, name })}> setForm({ ...form, username })} > setForm({ ...form, password })} > setForm({ ...form, device_type_default: e.target.value })} > {deviceTypes.map((dt) => ( ))} setForm({ ...form, vendor_default: e.target.value })} > {vendors.map((v) => ( ))}
{t("managedNe.hop.sectionTitle")}
{ setForm((prev) => ({ ...prev, hop_enabled, ...(hop_enabled ? { hop: { ...prev.hop, ...patchHopVendorChange(prev.hop.hop_vendor, prev.hop), ...applyHopTemplate(prev.hop, prev.hop.hop_protocol, prev.hop.hop_vrf, true), }, } : {}), })); }} > {t("managedNe.hop.enable")} {form.hop_enabled ? (
setForm((prev) => ({ ...prev, hop: { ...prev.hop, ...patch } }))} hopPasswordRequired={!form.id} hopPasswordOptional={Boolean(form.id)} />
) : null}
{form.id ? ( ) : null}

{t("ume.cli.connectTestTitle")}

{ setSampleFilter(e.target.value); setSampleUmeNeId(""); setSampleOpen(true); }} onFocus={() => setSampleOpen(true)} onBlur={() => { window.setTimeout(() => setSampleOpen(false), 160); }} onKeyDown={(e) => { if (e.key === "Escape") { setSampleOpen(false); (e.target as HTMLInputElement).blur(); } if (e.key === "Enter" && sampleHits[0]) { e.preventDefault(); const row = sampleHits[0]; setSampleUmeNeId(row.ne_id); setSampleFilter(formatUmeNePickLabel(row)); setSampleOpen(false); } }} placeholder={t("ume.cli.sampleNePh")} aria-label={t("ume.cli.connectTestTitle")} aria-autocomplete="list" aria-expanded={sampleOpen} />
{sampleOpen ? (
{sampleNeQuery.isLoading ? (
{t("ume.cli.sampleNeLoading")}
) : sampleHits.length === 0 ? (
{t("ume.cli.sampleNeEmpty")}
) : ( sampleHits.map((row) => { const title = String(row.user_label || row.ne_name || row.ne_id).trim() || row.ne_id; const meta = [row.ip_address, row.ne_type].filter(Boolean).join(" · "); return ( ); }) )}
) : null}
{testNeId ? (
{t("ume.cli.connectTestResult")}
{overrideQuery.isLoading && !overrideResult ? (

{t("ume.cli.connectTestWaiting")}

) : overrideTesting ? (

{t("ume.cli.connectTestRunning")}

) : overrideResult ? ( <>

{overrideResult.connect_status} {overrideResult.connect_message ? ( — {overrideResult.connect_message} ) : null} {overrideResult.connect_tested_at ? ( {formatSystemTime(overrideResult.connect_tested_at, { assumeUtcNaive: true })} ) : null}

                  {overrideResult.connect_detail?.trim() ||
                    overrideResult.connect_message?.trim() ||
                    t("managedNe.connectDetailEmpty")}
                
) : testPending ? (

{t("ume.cli.connectTestWaiting")}

) : (

{t("managedNe.connectDetailEmpty")}

)}
) : null}
{(profilesQuery.data?.items || []).length > 1 ? (
{t("ume.cli.existingProfiles")}
{(profilesQuery.data?.items || []).map((p) => ( ))}
) : null}
); if (embedded) return body; return
{body}
; }