mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 16:43:18 +08:00
feat(web): NetX workbench, UME-only UI, i18n, and header status
- Replace sidebar app with workbench home and module tabs (openOrFocusModule) - Remove legacy pages; add module registry, shared tab channel, WEB.md - Add zh/en i18n, global ToastProvider, restore oclaw bridge pill in header Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
664775927b
commit
282dbe75af
33 changed files with 1631 additions and 1677 deletions
78
web/src/i18n/context.tsx
Normal file
78
web/src/i18n/context.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import en from "./en";
|
||||
import zh from "./zh";
|
||||
|
||||
export type Locale = "zh" | "en";
|
||||
|
||||
const STORAGE_KEY = "netx-locale";
|
||||
|
||||
const MESSAGES: Record<Locale, typeof zh> = { zh, en };
|
||||
|
||||
function resolveMessage(obj: unknown, path: string): string {
|
||||
const keys = path.split(".");
|
||||
let cur: unknown = obj;
|
||||
for (const k of keys) {
|
||||
if (cur == null || typeof cur !== "object") return path;
|
||||
cur = (cur as Record<string, unknown>)[k];
|
||||
}
|
||||
return typeof cur === "string" ? cur : path;
|
||||
}
|
||||
|
||||
function interpolate(text: string, vars?: Record<string, string | number>): string {
|
||||
if (!vars) return text;
|
||||
let out = text;
|
||||
for (const [k, v] of Object.entries(vars)) {
|
||||
out = out.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), String(v));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
type I18nContextValue = {
|
||||
locale: Locale;
|
||||
setLocale: (locale: Locale) => void;
|
||||
t: (key: string, vars?: Record<string, string | number>) => string;
|
||||
};
|
||||
|
||||
const I18nContext = createContext<I18nContextValue | null>(null);
|
||||
|
||||
export function I18nProvider({ children }: { children: ReactNode }) {
|
||||
const [locale, setLocaleState] = useState<Locale>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
return saved === "en" ? "en" : "zh";
|
||||
} catch {
|
||||
return "zh";
|
||||
}
|
||||
});
|
||||
|
||||
const setLocale = useCallback((next: Locale) => {
|
||||
setLocaleState(next);
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, next);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, []);
|
||||
|
||||
const t = useCallback(
|
||||
(key: string, vars?: Record<string, string | number>) => {
|
||||
const raw = resolveMessage(MESSAGES[locale], key);
|
||||
return interpolate(raw, vars);
|
||||
},
|
||||
[locale],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = locale === "en" ? "en" : "zh-CN";
|
||||
}, [locale]);
|
||||
|
||||
const value = useMemo(() => ({ locale, setLocale, t }), [locale, setLocale, t]);
|
||||
|
||||
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
|
||||
}
|
||||
|
||||
export function useI18n() {
|
||||
const ctx = useContext(I18nContext);
|
||||
if (!ctx) throw new Error("useI18n must be used within I18nProvider");
|
||||
return ctx;
|
||||
}
|
||||
138
web/src/i18n/en.ts
Normal file
138
web/src/i18n/en.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
const en = {
|
||||
common: {
|
||||
help: "Help",
|
||||
refresh: "Refresh",
|
||||
refreshing: "Refreshing…",
|
||||
query: "Search",
|
||||
clearFilters: "Clear filters",
|
||||
prevPage: "Previous",
|
||||
nextPage: "Next",
|
||||
perPage: "{{n}}/page",
|
||||
pagerMeta: "{{total}} items · page {{page}}/{{pages}}",
|
||||
opFailed: "Operation failed",
|
||||
empty: "-",
|
||||
},
|
||||
workbench: {
|
||||
title: "Workbench",
|
||||
backToWorkbench: "Back to Workbench",
|
||||
openInNewTab: "Open in new tab",
|
||||
openModule: "Open or switch to this module tab",
|
||||
monitoring: "Monitoring",
|
||||
cards: {
|
||||
umeSync: "UME Sync",
|
||||
umeSyncDesc: "UME alarms, subscription & inventory",
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
title: "UME Integration",
|
||||
titleUme: "UME Sync",
|
||||
navUme: "UME",
|
||||
netxApi: "netx api",
|
||||
oclawBridge: "oclaw bridge",
|
||||
language: "Language",
|
||||
moreMenu: "More",
|
||||
langZh: "中文",
|
||||
langEn: "English",
|
||||
},
|
||||
ume: {
|
||||
token: {
|
||||
title: "UME Token",
|
||||
disconnected: "disconnected",
|
||||
connected: "connected",
|
||||
connectedRenew: "connected (renew required)",
|
||||
expiresIn: "expires_in",
|
||||
needsRenew: "renew / 0s",
|
||||
preview: "preview",
|
||||
refreshStatus: "Refresh status",
|
||||
renewLogin: "Renew / login",
|
||||
renewing: "Renewing…",
|
||||
disconnect: "Disconnect token",
|
||||
disconnecting: "Disconnecting…",
|
||||
renewOkChanged: "Token renewed and updated",
|
||||
renewOkSame: "Renew completed (unchanged)",
|
||||
disconnectOk: "UME token disconnected",
|
||||
renewHint: "Token missing expiry or expired. Use Renew / login or wait for background keepalive.",
|
||||
},
|
||||
subscription: {
|
||||
title: "UME alarm subscription (WebSocket)",
|
||||
help:
|
||||
"Subscriptions must be created/cancelled manually. After establish (or on restart if a valid subscription exists in DB), the backend auto-connects WSS for live alarms. Pause/resume on runtime task alarms_current_ws_consumer stops/resumes WSS only (does not cancel the UME subscription).",
|
||||
sub: "subscription",
|
||||
subActive: "active",
|
||||
subInactive: "inactive",
|
||||
wssPaused: "paused",
|
||||
wssModeBanner:
|
||||
"Current alarms are maintained via WSS (mode: {{mode}}); scheduled REST full sync is paused. Use Sync current alarms below for full reconcile.",
|
||||
serverLostBanner:
|
||||
"UME server subscription is missing or expired; local state may still show active. Clear local record and re-subscribe.",
|
||||
establish: "Establish subscription",
|
||||
establishing: "Establishing…",
|
||||
reestablish: "Clear local & re-subscribe",
|
||||
reestablishing: "Re-subscribing…",
|
||||
cancel: "Cancel subscription",
|
||||
cancelling: "Cancelling…",
|
||||
refreshStatus: "Refresh subscription",
|
||||
clearLocalOnly: "Clear local only",
|
||||
clearing: "Clearing…",
|
||||
opFailed: "Subscription operation failed",
|
||||
confirmClearDefault:
|
||||
"UME alarm subscription no longer exists or has expired.\n\nClear local subscription record? You can establish again afterwards.",
|
||||
confirmReestablish: "Clear local record and re-establish subscription on UME. Continue?",
|
||||
titleLoginFirst: "Log in to UME token first",
|
||||
titleReestablish: "Clear stale local record and re-establish on UME",
|
||||
establishInvalid: "Establish did not return a valid id/uri",
|
||||
establishExists: "Subscription already exists (WSS will stay/connect)",
|
||||
establishOk: "Subscription established; WSS will connect",
|
||||
clearLocalOk: "Local subscription cleared; you can establish again",
|
||||
serverLostMsg: "UME subscription lost. Clear local record and re-subscribe?",
|
||||
cancelFailed: "Cancel failed: subscription may still exist on UME",
|
||||
cancelOkMissing: "Local cleared (UME subscription was already missing)",
|
||||
cancelOk: "Subscription cancelled",
|
||||
wsLogs: "WSS runtime log",
|
||||
wsActivity: "Last activity {{time}}",
|
||||
wsAutoRefresh: "Auto refresh",
|
||||
wsLogsEmpty: "No logs yet (connect WSS after establishing subscription)",
|
||||
},
|
||||
sync: {
|
||||
title: "UME sync",
|
||||
inventory: "Sync inventory",
|
||||
alarmsCurrent: "Sync current alarms",
|
||||
full: "Full sync",
|
||||
failed: "Sync failed",
|
||||
},
|
||||
tasks: {
|
||||
currentTitle: "Running jobs",
|
||||
running: "running",
|
||||
noRunning: "No running jobs",
|
||||
runtimeTitle: "Background tasks",
|
||||
runtimeOpFailed: "Background task operation failed",
|
||||
noRuntime: "No background task status",
|
||||
intervalTitle: "Configured sleep between loop iterations (clamped at process start)",
|
||||
lastRunTitle: "Last finished sync for scheduled tasks; refreshed at each loop tick",
|
||||
deletedTitle: "Reconcile deletes: inventory = NE rows; alarms_current = current alarm rows",
|
||||
},
|
||||
syncStatus: {
|
||||
title: "Sync history",
|
||||
empty: "No sync records",
|
||||
},
|
||||
ne: {
|
||||
title: "Network elements",
|
||||
keywordPh: "keyword(ne_id/ne_name/user_label/ip/host_name)",
|
||||
clearTitle: "Clear keyword and reset to page 1",
|
||||
expand: "Expand details",
|
||||
collapse: "Collapse details",
|
||||
},
|
||||
alarms: {
|
||||
title: "Current alarms",
|
||||
keywordPh: "keyword(alarm key/cause/ne_name/host_name/ip…)",
|
||||
hostNamePh: "host_name (contains)",
|
||||
allSeverity: "All severities",
|
||||
clearedAll: "is_cleared: all",
|
||||
clearTitle: "Clear filters and reset to page 1",
|
||||
filterByHost: "Filter by this host",
|
||||
noHostName: "No host_name (sync inventory first)",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default en;
|
||||
1
web/src/i18n/index.ts
Normal file
1
web/src/i18n/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { I18nProvider, useI18n, type Locale } from "./context";
|
||||
138
web/src/i18n/zh.ts
Normal file
138
web/src/i18n/zh.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
const zh = {
|
||||
common: {
|
||||
help: "帮助说明",
|
||||
refresh: "刷新",
|
||||
refreshing: "刷新中…",
|
||||
query: "查询",
|
||||
clearFilters: "清除筛选",
|
||||
prevPage: "上一页",
|
||||
nextPage: "下一页",
|
||||
perPage: "{{n}}/页",
|
||||
pagerMeta: "共 {{total}} 条 · 第 {{page}}/{{pages}} 页",
|
||||
opFailed: "操作失败",
|
||||
empty: "-",
|
||||
},
|
||||
workbench: {
|
||||
title: "工作台",
|
||||
backToWorkbench: "返回工作台",
|
||||
openInNewTab: "在新标签页中打开",
|
||||
openModule: "打开或切换到该模块页签",
|
||||
monitoring: "监控",
|
||||
cards: {
|
||||
umeSync: "UME同步",
|
||||
umeSyncDesc: "UME 告警同步、订阅与清单",
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
title: "UME 对接",
|
||||
titleUme: "UME同步",
|
||||
navUme: "UME 对接",
|
||||
netxApi: "netx api",
|
||||
oclawBridge: "oclaw bridge",
|
||||
language: "语言",
|
||||
moreMenu: "更多",
|
||||
langZh: "中文",
|
||||
langEn: "English",
|
||||
},
|
||||
ume: {
|
||||
token: {
|
||||
title: "UME Token 状态",
|
||||
disconnected: "disconnected",
|
||||
connected: "connected",
|
||||
connectedRenew: "connected (需续期)",
|
||||
expiresIn: "expires_in",
|
||||
needsRenew: "需续期 / 0s",
|
||||
preview: "preview",
|
||||
refreshStatus: "刷新状态",
|
||||
renewLogin: "手动续期/登录",
|
||||
renewing: "续期中…",
|
||||
disconnect: "断开 token",
|
||||
disconnecting: "断开中…",
|
||||
renewOkChanged: "续期成功,token 已更新",
|
||||
renewOkSame: "续期完成(与此前一致)",
|
||||
disconnectOk: "已断开 UME token",
|
||||
renewHint: "库中 token 缺少有效过期时间或已过期,请点「手动续期/登录」或等待后台自动续期",
|
||||
},
|
||||
subscription: {
|
||||
title: "UME 告警订阅(WebSocket)",
|
||||
help:
|
||||
"订阅需手动建立/取消;建立后(或重启后若库中仍有有效订阅)后台会自动连接 WSS 接收实时告警。后台任务 alarms_current_ws_consumer 的 pause/resume 会停止或恢复 WSS 连接(不会取消 UME 订阅)。",
|
||||
sub: "订阅",
|
||||
subActive: "已建立",
|
||||
subInactive: "未建立",
|
||||
wssPaused: "已暂停",
|
||||
wssModeBanner:
|
||||
"当前告警由 WSS 实时维护(模式: {{mode}}),定时 REST 全量同步已暂停。需要全量对账时请使用下方「同步当前告警」。",
|
||||
serverLostBanner:
|
||||
"UME 服务器侧告警订阅已丢失或已过期,本地记录可能仍显示「已建立」。请确认清除本地记录后重新订阅。",
|
||||
establish: "建立告警订阅",
|
||||
establishing: "建立订阅…",
|
||||
reestablish: "清除本地并重新订阅",
|
||||
reestablishing: "重新订阅…",
|
||||
cancel: "取消告警订阅",
|
||||
cancelling: "取消订阅…",
|
||||
refreshStatus: "刷新订阅状态",
|
||||
clearLocalOnly: "仅清除本地记录",
|
||||
clearing: "清除中…",
|
||||
opFailed: "订阅操作失败",
|
||||
confirmClearDefault:
|
||||
"UME 侧告警订阅已不存在或已过期(服务器订阅已丢失)。\n\n是否清除本地订阅记录?清除后可点击「建立告警订阅」重新订阅。",
|
||||
confirmReestablish: "将清除本地订阅记录并在 UME 上重新建立告警订阅,是否继续?",
|
||||
titleLoginFirst: "请先登录 UME token",
|
||||
titleReestablish: "清除本地失效记录并重新向 UME 建立订阅",
|
||||
establishInvalid: "建立订阅未返回有效 id/uri",
|
||||
establishExists: "订阅已存在,未重复建立(WSS 将保持/恢复连接)",
|
||||
establishOk: "告警订阅已建立,WSS 将自动连接",
|
||||
clearLocalOk: "已清除本地订阅记录,可重新建立订阅",
|
||||
serverLostMsg: "UME 侧告警订阅已丢失。请确认是否清除本地订阅记录后重新订阅。",
|
||||
cancelFailed: "取消订阅失败:UME 侧订阅可能仍存在,请查看错误详情后重试",
|
||||
cancelOkMissing: "本地已清除(UME 侧订阅此前已不存在)",
|
||||
cancelOk: "告警订阅已取消",
|
||||
wsLogs: "WSS 运行日志",
|
||||
wsActivity: "最近活动 {{time}}",
|
||||
wsAutoRefresh: "自动刷新",
|
||||
wsLogsEmpty: "暂无日志(建立订阅并连接 WSS 后会出现连接、收包、重连等记录)",
|
||||
},
|
||||
sync: {
|
||||
title: "UME 同步",
|
||||
inventory: "同步 Inventory",
|
||||
alarmsCurrent: "同步当前告警",
|
||||
full: "全量同步",
|
||||
failed: "同步失败",
|
||||
},
|
||||
tasks: {
|
||||
currentTitle: "当前任务",
|
||||
running: "running",
|
||||
noRunning: "当前无运行中的任务",
|
||||
runtimeTitle: "后台任务",
|
||||
runtimeOpFailed: "后台任务操作失败",
|
||||
noRuntime: "暂无后台任务状态",
|
||||
intervalTitle: "Configured sleep between loop iterations (clamped at process start)",
|
||||
lastRunTitle: "Last finished sync for scheduled tasks; refreshed at each loop tick",
|
||||
deletedTitle: "全量对账删除条数:inventory 为网元行,alarms_current 为当前告警行,其余域多为 0",
|
||||
},
|
||||
syncStatus: {
|
||||
title: "同步状态",
|
||||
empty: "暂无同步记录",
|
||||
},
|
||||
ne: {
|
||||
title: "网元清单",
|
||||
keywordPh: "keyword(ne_id/ne_name/user_label/ip/host_name)",
|
||||
clearTitle: "清空 keyword,回到第 1 页",
|
||||
expand: "展开详情",
|
||||
collapse: "收起详情",
|
||||
},
|
||||
alarms: {
|
||||
title: "当前告警",
|
||||
keywordPh: "keyword(告警键/原因/ne_name/host_name/ip 等)",
|
||||
hostNamePh: "host_name(含匹配)",
|
||||
allSeverity: "全部级别",
|
||||
clearedAll: "is_cleared: all",
|
||||
clearTitle: "清空 keyword、host_name、级别、is_cleared,回到第 1 页",
|
||||
filterByHost: "按该主机名筛选",
|
||||
noHostName: "无 host_name(需先同步网元)",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default zh;
|
||||
Loading…
Add table
Add a link
Reference in a new issue