diff --git a/web/src/components/UmeCliConnectPanel.tsx b/web/src/components/UmeCliConnectPanel.tsx index 64d5d5f..e92e542 100644 --- a/web/src/components/UmeCliConnectPanel.tsx +++ b/web/src/components/UmeCliConnectPanel.tsx @@ -7,6 +7,7 @@ import { fetchCliMeta, fetchCliProfiles, fetchManagedNeMeta, + fetchUmeCliOverride, postUmeConnectTest, } from "../services/api"; import { HopProxyFields, emptyHopProxyFields, type HopProxyFieldsState } from "./HopProxyFields"; @@ -14,7 +15,16 @@ import { HelpHint } from "./HelpHint"; import { queryKeys } from "../constants/queryKeys"; import { useI18n } from "../i18n"; import { useToast } from "../hooks/useToast"; -import type { CliConnectProfileItem } from "../types"; +import type { CliConnectProfileItem, UmeCliOverrideItem } from "../types"; +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"; +} type ProfileForm = { id: string; @@ -73,10 +83,29 @@ export function UmeCliConnectPanel() { const queryClient = useQueryClient(); const [form, setForm] = useState(emptyForm); const [sampleUmeNeId, setSampleUmeNeId] = useState(""); + const [testPending, setTestPending] = useState(false); + + const testNeId = sampleUmeNeId.trim(); + + useEffect(() => { + setTestPending(false); + }, [testNeId]); const metaQuery = useQuery({ queryKey: queryKeys.cliMeta, queryFn: fetchCliMeta }); const neMetaQuery = useQuery({ queryKey: queryKeys.managedNeMeta, queryFn: fetchManagedNeMeta }); const profilesQuery = useQuery({ queryKey: queryKeys.cliProfiles, queryFn: fetchCliProfiles }); + 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 || []; @@ -130,12 +159,28 @@ export function UmeCliConnectPanel() { if (!id) throw new Error(t("ume.cli.sampleNeRequired")); return postUmeConnectTest([id]); }, - onSuccess: () => { + 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); }, - onError: (e: Error) => 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); @@ -244,9 +289,48 @@ export function UmeCliConnectPanel() { style={{ minWidth: 320 }} /> + {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 ? (
diff --git a/web/src/constants/queryKeys.ts b/web/src/constants/queryKeys.ts index 2a613af..55e9ad4 100644 --- a/web/src/constants/queryKeys.ts +++ b/web/src/constants/queryKeys.ts @@ -34,8 +34,10 @@ export const queryKeys = { ["neCollectionRuns", jobId, page, status, keyword] as const, cliMeta: ["cliMeta"] as const, cliProfiles: ["cliProfiles"] as const, + cliTargetsAll: ["cliTargets"] as const, cliTargets: (keyword: string, page: number, pageSize: number) => ["cliTargets", keyword, page, pageSize] as const, + umeCliOverride: (umeNeId: string) => ["umeCliOverride", umeNeId] as const, umeCurrentAlarms: ( severity: string, cleared: string, diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index c08e073..27e7975 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -488,6 +488,9 @@ const en = { sampleNePh: "ume_ne_id (copy from NE list below)", sampleNeRequired: "ume_ne_id is required", connectTestSubmitted: "Connectivity test submitted", + connectTestWaiting: "Waiting for test result…", + connectTestRunning: "Connectivity test in progress…", + connectTestResult: "Test result", existingProfiles: "Existing profiles", default: "default", }, diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 1251d93..d0b4641 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -486,6 +486,9 @@ const zh = { sampleNePh: "ume_ne_id(从下方网元清单复制)", sampleNeRequired: "请填写 ume_ne_id", connectTestSubmitted: "已提交连通性测试", + connectTestWaiting: "等待测试结果…", + connectTestRunning: "连通性测试中…", + connectTestResult: "测试结果", existingProfiles: "已有配置", default: "默认", }, diff --git a/web/src/services/api.ts b/web/src/services/api.ts index 42c71b1..1adfed2 100644 --- a/web/src/services/api.ts +++ b/web/src/services/api.ts @@ -17,6 +17,7 @@ import type { CliConnectProfileItem, CliMeta, CliTargetListResponse, + UmeCliOverrideItem, } from "../types"; const parseApiResponse = async (res: Response): Promise> => { @@ -331,6 +332,9 @@ export const fetchCliTargets = (params: { export const postUmeConnectTest = (umeNeIds: string[]) => apiPost<{ ok: boolean; submitted: number }>("/v1/cli/ume-connect-test", { ume_ne_ids: umeNeIds }); +export const fetchUmeCliOverride = (umeNeId: string) => + apiGet(`/v1/cli/ume-overrides/${encodeURIComponent(umeNeId)}`); + export const fetchUmeCurrentAlarms = (params: { severity: string; isCleared: string; diff --git a/web/src/types.ts b/web/src/types.ts index 515dbfe..1415d20 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -268,6 +268,19 @@ export type CliTargetListResponse = { items: CliTargetItem[]; }; +export type UmeCliOverrideItem = { + ume_ne_id: string; + profile_id: string | null; + username_override: string; + device_type_override: string; + vendor_override: string; + connect_status: string; + connect_message: string; + connect_detail: string; + connect_tested_at: string | null; + updated_at: string; +}; + export type EligibleNeItem = { id: string; name: string;