diff --git a/web/src/components/HelpHint.tsx b/web/src/components/HelpHint.tsx index 11c5c8c..e39287e 100644 --- a/web/src/components/HelpHint.tsx +++ b/web/src/components/HelpHint.tsx @@ -1,4 +1,4 @@ -import { useEffect, useLayoutEffect, useRef, useState } from "react"; +import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { createPortal } from "react-dom"; type Props = { @@ -9,20 +9,160 @@ type Props = { }; const VIEWPORT_MARGIN = 8; -const POPOVER_MAX_WIDTH = 360; +const POPOVER_MAX_WIDTH = 420; + +type HelpBlock = + | { kind: "title"; text: string } + | { kind: "list"; items: string[] } + | { kind: "para"; text: string }; + +const SECTION_RE = /^[【\[](.+?)[】\]]\s*$/; +const BULLET_RE = /^[·•\-\*]\s+(.+)$/; +/** Highlight field names / env keys / templates — skip short ALLCAPS acronyms (UME, DSH). */ +const CODEISH_RE = + /(`[^`]+`|\{[A-Za-z0-9_./:@-]+\}|[A-Z][A-Z0-9_]{5,}(?::[a-z]+)?|[a-z]+(?:_[a-z0-9]+)+|[a-z]+[A-Z][A-Za-z0-9]*)/g; + +function parseHelpBlocks(raw: string): HelpBlock[] { + const lines = String(raw || "") + .replace(/\r\n/g, "\n") + .split("\n") + .map((l) => l.trimEnd()); + const blocks: HelpBlock[] = []; + let listBuf: string[] = []; + let paraBuf: string[] = []; + + const flushList = () => { + if (!listBuf.length) return; + blocks.push({ kind: "list", items: listBuf }); + listBuf = []; + }; + const flushPara = () => { + if (!paraBuf.length) return; + blocks.push({ kind: "para", text: paraBuf.join(" ").trim() }); + paraBuf = []; + }; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) { + flushList(); + flushPara(); + continue; + } + const section = trimmed.match(SECTION_RE); + if (section) { + flushList(); + flushPara(); + blocks.push({ kind: "title", text: section[1].trim() }); + continue; + } + const bullet = trimmed.match(BULLET_RE); + if (bullet) { + flushPara(); + listBuf.push(bullet[1].trim()); + continue; + } + flushList(); + paraBuf.push(trimmed); + } + flushList(); + flushPara(); + return blocks; +} + +function renderInline(text: string): ReactNode[] { + const nodes: ReactNode[] = []; + let last = 0; + let m: RegExpExecArray | null; + const re = new RegExp(CODEISH_RE.source, "g"); + while ((m = re.exec(text)) !== null) { + if (m.index > last) nodes.push(text.slice(last, m.index)); + const raw = m[0]; + const body = raw.startsWith("`") && raw.endsWith("`") ? raw.slice(1, -1) : raw; + nodes.push( + + {body} + , + ); + last = m.index + raw.length; + } + if (last < text.length) nodes.push(text.slice(last)); + return nodes; +} + +/** Long single paragraphs → clause list so plain help is still scannable. */ +function expandPlainPara(text: string): HelpBlock[] { + const parts = text + .split(/(?<=[。;;])\s*/) + .map((s) => s.replace(/[。;;]\s*$/, "").trim()) + .filter(Boolean); + if (parts.length >= 2 && text.length >= 48) { + return [{ kind: "list", items: parts }]; + } + return [{ kind: "para", text }]; +} + +function HelpBody({ text, nowrap }: { text: string; nowrap: boolean }) { + const blocks = useMemo(() => { + const parsed = parseHelpBlocks(text); + if (nowrap) return parsed; + if (parsed.length === 1 && parsed[0].kind === "para") { + return expandPlainPara(parsed[0].text); + } + return parsed.flatMap((b) => (b.kind === "para" && b.text.length >= 96 ? expandPlainPara(b.text) : [b])); + }, [text, nowrap]); + + if (nowrap) { + return
{renderInline(text)}
; + } + return ( +
+ {blocks.map((b, i) => { + if (b.kind === "title") { + return ( +
+ {b.text} +
+ ); + } + if (b.kind === "list") { + return ( + + ); + } + return ( +

+ {renderInline(b.text)} +

+ ); + })} +
+ ); +} export function HelpHint({ text, ariaLabel, align = "start", nowrap = false }: Props) { const [open, setOpen] = useState(false); const [popoverStyle, setPopoverStyle] = useState({}); const rootRef = useRef(null); const popoverRef = useRef(null); + const structured = useMemo(() => { + const parsed = parseHelpBlocks(text); + if (parsed.some((b) => b.kind === "title" || b.kind === "list")) return true; + // Long plain paragraphs are auto-split into a list in HelpBody. + return parsed.length === 1 && parsed[0].kind === "para" && parsed[0].text.length >= 48; + }, [text]); const updatePopoverPosition = () => { const el = rootRef.current; if (!el) return; const rect = el.getBoundingClientRect(); const popover = popoverRef.current; - const maxWidth = Math.min(POPOVER_MAX_WIDTH, window.innerWidth - VIEWPORT_MARGIN * 2); + const preferred = structured && !nowrap ? POPOVER_MAX_WIDTH : 360; + const maxWidth = Math.min(preferred, window.innerWidth - VIEWPORT_MARGIN * 2); const popWidth = popover?.offsetWidth || maxWidth; const popHeight = popover?.offsetHeight || 0; @@ -40,6 +180,7 @@ export function HelpHint({ text, ariaLabel, align = "start", nowrap = false }: P left, right: "auto", maxWidth, + maxHeight: Math.min(window.innerHeight - VIEWPORT_MARGIN * 2, 480), }); }; @@ -55,7 +196,7 @@ export function HelpHint({ text, ariaLabel, align = "start", nowrap = false }: P window.removeEventListener("resize", onReflow); window.removeEventListener("scroll", onReflow, true); }; - }, [open, align, text, nowrap]); + }, [open, align, text, nowrap, structured]); useEffect(() => { if (!open) return; @@ -98,11 +239,11 @@ export function HelpHint({ text, ariaLabel, align = "start", nowrap = false }: P ref={popoverRef} className={`help-hint__popover help-hint__popover--portal${ nowrap ? " help-hint__popover--nowrap" : "" - }`} + }${structured && !nowrap ? " help-hint__popover--structured" : ""}`} style={popoverStyle} role="tooltip" > - {text} + , document.body, ) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 6e10532..1ad92c0 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -997,6 +997,7 @@ const en = { }, bizMonitorTpl: { title: "Monitor templates", + help: "Compare templates define align/diff; monitor templates define migrate success on the same sheets.", hint: "Compare templates define HOW to align/diff; monitor templates define WHEN dual-device migrate succeeds — one sheet tab per compare sheet.", create: "New monitor template", edit: "Edit", @@ -1568,6 +1569,8 @@ const en = { loginFailed: "Login failed", logout: "Sign out", sessionsTitle: "Login sessions", + sessionsHelp: + "Behind a proxy, IP is the real client; use Client to tell browsers apart. Revoked sessions must sign in again.", sessionsHint: "Manage browser/device logins for this account. Behind a reverse proxy, IP is taken from trusted forwarded headers; use the Client column to tell browsers apart on the same public IP. Revoked sessions must sign in again.", sessionsEmpty: "No active sessions.", @@ -1611,7 +1614,7 @@ const en = { colIp: "IP", apiKeysTitle: "API Key management", apiKeysHelp: - "The web UI uses login sessions, not API keys — keys are for MCP/scripts and netxops. Defaults on create: alarms:read + ne:read + ne:write + ne:exec + biz-monitor:read; refine later with Edit scopes (takes effect immediately, no need to regenerate). Secret is shown once. Admins may issue for other users; scopes cannot exceed the owner's. Unused scopes are reserved. System-wide cap: {{max}} non-revoked keys (revoke or delete to free a slot).", + "For MCP/scripts and netxops (web UI uses sessions). Secret shown once; Edit scopes later. Cap {{max}} non-revoked keys.", tokenQuota: "Active {{active}} / {{max}}", tokenLimitReached: "API key limit reached ({{max}}). Revoke or delete unused keys first.", tokenName: "Name", @@ -1889,16 +1892,16 @@ const en = { }, help: "[Bulk import]\n" + - "· Required columns: device_type, ip, username, port, protocol, name, vendor. Download the template first.\n" + - "· password may be empty (required for direct login; optional for bastion-managed or batch proxy later).\n" + - "· Recommended flow: import NEs first, select rows, then use Batch add proxy.\n\n" + + "· Required: device_type, ip, username, port, protocol, name, vendor (download template first)\n" + + "· password may be empty for bastion-managed / later Batch add proxy; required for direct login\n" + + "· Flow: import → select rows → Batch add proxy\n\n" + "[Jump / bastion]\n" + - "· Bastion SSH username template: {hop_user}@{target_user}@{target_ip} (bastion host is separate IP or FQDN); CLI: ssh user@target@ip@bastion-host.\n" + - "· Bastion-managed: set Jump password (Vault); target password optional. Manual mode needs target password.\n" + - "· Jump host accepts IP (192.0.2.10) or domain (ssh-bastion.example.com). JumpServer/CBH often use port 2222; some sites use 22.\n\n" + + "· Username template: {hop_user}@{target_user}@{target_ip} (bastion host filled separately)\n" + + "· Managed mode: Jump password (Vault); target password optional. Manual mode needs target password\n" + + "· JumpServer/CBH ports often 2222; some sites use 22\n\n" + "[Connectivity / edit]\n" + - "· NETX_CREDENTIAL_SECRET_KEY required to store passwords.\n" + - "· Leave password blank on edit to keep unchanged; open Details after failed connect-test for hop context (no secrets).", + "· Storing passwords needs NETX_CREDENTIAL_SECRET_KEY\n" + + "· Blank password on edit keeps current; use Details hop context after failed connect (no secrets)", importResult: { done: "Import done: {{inserted}} inserted, {{updated}} updated, {{failed}} failed row(s)", }, @@ -2005,7 +2008,12 @@ const en = { 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).", + "[Subscription]\n" + + "· Create / cancel manually\n" + + "· After establish (or restart with a valid DB record), backend auto-connects WSS\n\n" + + "[Task control]\n" + + "· alarms_current_ws_consumer pause/resume only stops/starts WSS\n" + + "· Does not cancel the UME subscription", sub: "subscription", subActive: "active", subInactive: "inactive", @@ -2046,7 +2054,14 @@ const en = { title: "AI alarm monitor", showPanel: "Details", hidePanel: "Close", - help: "Exact match by notificationId, or substring match on alarm description (case-insensitive; nativeProbableCause, etc.). Optionally restrict to UME inventory ne_type values (multi-select; empty = all devices). Matched alerts are broadcast by NetX (server) to subscribed Netx Ops (DSH) clients.", + help: + "[Match]\n" + + "· Exact notificationId\n" + + "· Or description keyword (case-insensitive; includes nativeProbableCause)\n\n" + + "[Scope]\n" + + "· Optional Inventory ne_type filter; empty = all devices\n\n" + + "[Deliver]\n" + + "· NetX broadcasts hits to subscribed Netx Ops (DSH) clients", hub: "DSH subscribers", hubConnected: "subscribed", hubEmpty: "no subscribers", @@ -2196,7 +2211,11 @@ const en = { title: "UME CLI connection profile", showPanel: "Details", hidePanel: "Close", - hint: "Unified SSH credentials and hop/bastion for UME inventory NEs. OClaw MCP can exec show commands via ume_ne_id without per-device managed_ne entries.", + hint: + "[Purpose]\n" + + "· Unified SSH credentials and hop/bastion for Inventory NEs\n\n" + + "[MCP]\n" + + "· Exec show via ume_ne_id without per-device managed_ne entries", statusReady: "CLI profile is ready for UME NE command execution.", statusNotReady: "Set target username and hop/bastion settings (if enabled).", profileName: "Profile name", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index d4e7a84..c770ea1 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -992,6 +992,7 @@ const zh = { }, bizMonitorTpl: { title: "监控模板", + help: "对比模板定义怎么对齐/比对;监控模板按同表定义搬迁成功条件。", hint: "对比模板定义「怎么对齐/比对」;监控模板按同样的表(sheet)定义「怎样算搬迁成功/失败」。", create: "新建监控模板", edit: "编辑", @@ -1556,6 +1557,7 @@ const zh = { loginFailed: "登录失败", logout: "退出", sessionsTitle: "登录会话", + sessionsHelp: "反代后 IP 取真实客户端;同公网可用「客户端」列区分。踢掉后需重新登录。", sessionsHint: "管理当前账号在各浏览器/设备上的登录。IP 经反代时会取真实客户端地址;同公网出口可用「客户端」列区分浏览器/系统。踢掉会话后对方需重新登录。", sessionsEmpty: "当前没有活跃会话。", @@ -1599,7 +1601,7 @@ const zh = { colIp: "IP", apiKeysTitle: "API Key 管理", apiKeysHelp: - "网页登录不用 API Key,Key 给 MCP/脚本与 netxops 用。生成时默认 alarms:read + ne:read + ne:write + ne:exec + biz-monitor:read,之后用「改权限」增减(保存即生效,不必重生明文)。明文仅创建时显示一次。管理员可代其他用户签发,权限不能超过所属用户已有能力;未接的项为预留。全站未吊销 Key 上限 {{max}} 把(吊销或删除后可再建)。", + "供 MCP/脚本与 netxops 使用(网页登录用会话)。明文仅创建时显示一次;可用「改权限」调整。未吊销上限 {{max}}。", tokenQuota: "未吊销 {{active}} / {{max}}", tokenLimitReached: "已达 API Key 上限({{max}}),请先吊销或删除不用的 Key", tokenName: "名称", @@ -1873,16 +1875,16 @@ const zh = { }, help: "【批量导入】\n" + - "· 必填列:device_type、ip、username、port、protocol、name、vendor;可先下载模板。\n" + - "· password 可留空(直连需填;堡垒机托管或后续批量添加代理时可空)。\n" + - "· 推荐流程:先导入网元 → 勾选网元 → 点「批量添加代理」统一配置跳板/堡垒机。\n\n" + + "· 必填列:device_type、ip、username、port、protocol、name、vendor(可先下载模板)\n" + + "· password 可留空:直连需填;堡垒机托管或后续「批量添加代理」时可空\n" + + "· 推荐:导入网元 → 勾选 →「批量添加代理」统一配置跳板\n\n" + "【跳板 / 堡垒机】\n" + - "· 堡垒机 SSH 用户名模板:{hop_user}@{target_user}@{target_ip}(堡垒机地址单独填在跳板地址);命令行等价:ssh 用户@目标@IP@堡垒机。\n" + - "· 堡垒机托管时填「跳板密码」(Vault 密码),目标密码可留空;手动模式需填目标密码。\n" + - "· JumpServer/CBH 常用跳板端口 2222,部分现场为 22。\n\n" + + "· 用户名模板:{hop_user}@{target_user}@{target_ip}(堡垒机地址另填跳板地址)\n" + + "· 托管模式填跳板密码(Vault);目标密码可空。手动模式需填目标密码\n" + + "· JumpServer/CBH 跳板端口多为 2222,部分现场为 22\n\n" + "【连通性 / 编辑】\n" + - "· 保存密码需配置 NETX_CREDENTIAL_SECRET_KEY。\n" + - "· 编辑时密码留空表示不修改;连通性失败请点「详情」查看 hop 上下文(不含密码)。", + "· 保存密码需配置 NETX_CREDENTIAL_SECRET_KEY\n" + + "· 编辑时密码留空表示不改;失败请看「详情」中的 hop 上下文(不含密码)", importResult: { done: "导入完成:新增 {{inserted}},更新 {{updated}},失败 {{failed}} 行", }, @@ -1989,7 +1991,12 @@ const zh = { subscription: { title: "UME 告警订阅(WebSocket)", help: - "订阅需手动建立/取消;建立后(或重启后若库中仍有有效订阅)后台会自动连接 WSS 接收实时告警。后台任务 alarms_current_ws_consumer 的 pause/resume 会停止或恢复 WSS 连接(不会取消 UME 订阅)。", + "【订阅】\n" + + "· 需手动建立 / 取消\n" + + "· 建立后(或重启且库中仍有效)后台自动连 WSS 收实时告警\n\n" + + "【任务控制】\n" + + "· alarms_current_ws_consumer 的 pause/resume 只停/启 WSS\n" + + "· 不会取消 UME 侧订阅", sub: "订阅", subActive: "已建立", subInactive: "未建立", @@ -2030,7 +2037,14 @@ const zh = { title: "AI 告警监控", showPanel: "详情", hidePanel: "关闭", - help: "按 notificationId 精确匹配,或按告警描述关键字模糊匹配(不区分大小写,匹配 nativeProbableCause 等字段);可选限定 UME Inventory 中的网元类型 (ne_type),不选则匹配全部设备。命中后由 NetX 作为服务器广播给已订阅的 Netx Ops(DSH)客户端。", + help: + "【匹配】\n" + + "· notificationId 精确匹配\n" + + "· 或告警描述关键字模糊匹配(不区分大小写,含 nativeProbableCause 等)\n\n" + + "【范围】\n" + + "· 可选限定 Inventory 的 ne_type;不选则匹配全部设备\n\n" + + "【投递】\n" + + "· 命中后由 NetX 广播给已订阅的 Netx Ops(DSH)客户端", hub: "DSH 订阅", hubConnected: "有订阅", hubEmpty: "无订阅", @@ -2180,7 +2194,11 @@ const zh = { title: "UME CLI 连接配置", showPanel: "详情", hidePanel: "关闭", - hint: "为 UME 清单网元配置统一 SSH 凭据与跳板/堡垒机;OClaw MCP 可通过 ume_ne_id 直接执行 show 命令,无需逐台录入 managed_ne。", + hint: + "【用途】\n" + + "· 为 Inventory 网元配置统一 SSH 凭据与跳板/堡垒机\n\n" + + "【MCP】\n" + + "· 可用 ume_ne_id 直接执行 show,无需逐台录入 managed_ne", statusReady: "CLI 配置已就绪,可对 UME 网元执行命令。", statusNotReady: "请填写目标用户名,并配置跳板/堡垒机(若启用)。", profileName: "配置名称", diff --git a/web/src/index.css b/web/src/index.css index 9d2866c..b257605 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1918,15 +1918,16 @@ pre { z-index: 40; width: min(360px, 80vw); padding: 10px 12px; - border: 1px solid rgba(148, 163, 184, 0.28); - border-radius: 8px; - background: #0f172a; - color: #e2e8f0; - font-size: 13px; + border: 1px solid var(--nm-ops-line, #2b3748); + border-radius: 6px; + background: #0c1521; + color: #d7e3f2; + font-size: 12px; font-weight: 400; - line-height: 1.45; + line-height: 1.5; white-space: pre-line; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); + box-shadow: none; + overflow: auto; } .help-hint--align-end .help-hint__popover { @@ -1939,12 +1940,80 @@ pre { width: auto; } +.help-hint__popover--structured { + width: min(420px, 86vw); + white-space: normal; + padding: 12px 14px; +} + .help-hint__popover--nowrap { width: max-content; max-width: min(90vw, 720px); white-space: nowrap; } +.help-hint__body { + display: flex; + flex-direction: column; + gap: 10px; + min-width: 0; +} + +.help-hint__plain { + white-space: pre-line; +} + +.help-hint__section-title { + margin: 2px 0 0; + padding-bottom: 5px; + border-bottom: 1px solid var(--nm-ops-line, #2b3748); + color: var(--nm-ops-accent, #6ba9ec); + font-size: 11px; + font-weight: 650; + letter-spacing: 0.06em; + text-transform: uppercase; + line-height: 1.35; +} + +.help-hint__section-title:first-child { + margin-top: 0; +} + +.help-hint__list { + margin: 0; + padding: 0 0 0 1.05rem; + display: flex; + flex-direction: column; + gap: 5px; + color: var(--nm-ops-muted, #9aa9bc); +} + +.help-hint__list > li { + padding-left: 2px; + line-height: 1.45; +} + +.help-hint__list > li::marker { + color: #526f94; +} + +.help-hint__para { + margin: 0; + color: var(--nm-ops-muted, #9aa9bc); + line-height: 1.5; +} + +.help-hint__code { + display: inline; + padding: 0 4px; + border: 1px solid rgba(43, 55, 72, 0.9); + border-radius: 3px; + background: #111c2a; + color: #c4ddfa; + font: 11px/1.45 ui-monospace, SFMono-Regular, Consolas, monospace; + white-space: nowrap; +} + .panel__toolbar-end { margin-left: auto; display: flex; @@ -2226,9 +2295,9 @@ pre { } .system-page .panel__hint { - margin: -6px 0 12px; - color: var(--nm-muted); - font-size: 13px; + margin: 0 0 10px; + color: var(--nm-ops-muted, var(--nm-muted)); + font-size: 12px; } .ume-page .panel > h3, @@ -3246,15 +3315,20 @@ pre { grid-template-columns: 1fr; } +/* Status / empty-state copy only — conceptual docs go in HelpHint on the title. */ .panel__hint { - margin: 0 0 12px; - font-size: 13px; - color: #94a3b8; - line-height: 1.45; + margin: 0 0 10px; + font-size: 12px; + color: var(--nm-ops-muted, #9aa9bc); + line-height: 1.4; } .panel__hint--live { - color: var(--nm-accent, #3b82f6); + color: var(--nm-ops-accent, #6ba9ec); + border: 1px solid var(--nm-ops-line, #2b3748); + border-radius: 4px; + padding: 8px 10px; + background: rgba(107, 169, 236, 0.06); } .collect-commands { diff --git a/web/src/pages/ApiTokensPage.tsx b/web/src/pages/ApiTokensPage.tsx index 4a92837..94e5472 100644 --- a/web/src/pages/ApiTokensPage.tsx +++ b/web/src/pages/ApiTokensPage.tsx @@ -2,6 +2,7 @@ import { Button, Checkbox, Input, Modal } from "@heroui/react"; import { useMemo, useState, type FormEvent } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useAuth } from "../auth/AuthContext"; +import { HelpHint } from "../components/HelpHint"; import { AppModalShell } from "../components/ui/AppModalShell"; import { FieldSelect } from "../components/ui/FieldSelect"; import { useI18n } from "../i18n"; @@ -226,14 +227,9 @@ export function ApiTokensPage() {
-

+

{t("auth.apiKeysTitle")} - - ? - - {t("auth.apiKeysHelp", { max: tokenMax || 20 })} - - +

{tokenMax > 0 ? ( diff --git a/web/src/pages/CollectPage.tsx b/web/src/pages/CollectPage.tsx index ebddf82..fdd03e1 100644 --- a/web/src/pages/CollectPage.tsx +++ b/web/src/pages/CollectPage.tsx @@ -639,7 +639,6 @@ export function CollectPage() { />
-

{t("collect.form.commandsHint")}

{policyScopeMode === "selected" ? (
@@ -817,7 +816,6 @@ export function CollectPage() { -

{t("collect.create.hint")}

-

{t("collect.form.commandsHint")}

@@ -854,7 +851,7 @@ export function CollectPage() { ) : null}
{selectedList.length === 0 ? ( -

{t("collect.create.selectedEmpty")}

+

{t("collect.create.selectedEmpty")}

) : (
{selectedList.map((row) => { @@ -884,7 +881,6 @@ export function CollectPage() {

{t("collect.create.pickTitle")}

-

{t("collect.create.pickHint")}

-

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

@@ -966,7 +965,6 @@ export function UmePage() { -

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

diff --git a/web/src/pages/UsersPage.tsx b/web/src/pages/UsersPage.tsx index 6555eb8..4c31263 100644 --- a/web/src/pages/UsersPage.tsx +++ b/web/src/pages/UsersPage.tsx @@ -69,7 +69,6 @@ export function UsersPage() {

{t("auth.usersTitle")}

-

{t("auth.usersHint")}

diff --git a/web/src/pages/audit/TaskOverviewPage.tsx b/web/src/pages/audit/TaskOverviewPage.tsx index 276801f..703fa6c 100644 --- a/web/src/pages/audit/TaskOverviewPage.tsx +++ b/web/src/pages/audit/TaskOverviewPage.tsx @@ -97,7 +97,6 @@ export function TaskOverviewPage() { {query.isFetching ? t("common.refreshing") : t("common.refresh")}
-

{t("audit.tasks.hint")}

diff --git a/web/src/pages/network/BizComparePage.tsx b/web/src/pages/network/BizComparePage.tsx index 8c215e1..b2c62bc 100644 --- a/web/src/pages/network/BizComparePage.tsx +++ b/web/src/pages/network/BizComparePage.tsx @@ -2498,9 +2498,6 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage {activeTab === "templates" ? (
-

- {t("bizCompare.templateImportHint")} -

diff --git a/web/src/pages/network/BizMigrationPage.tsx b/web/src/pages/network/BizMigrationPage.tsx index b4628ac..d0fddc8 100644 --- a/web/src/pages/network/BizMigrationPage.tsx +++ b/web/src/pages/network/BizMigrationPage.tsx @@ -1,6 +1,6 @@ import { Button, Input, Modal } from "@heroui/react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { ListPager } from "../../components/ListPager"; import { AppModalShell } from "../../components/ui/AppModalShell"; import { FieldSelect } from "../../components/ui/FieldSelect"; @@ -383,6 +383,7 @@ function fmtTime(v?: string | null) { export function BizMigrationPage() { const { t } = useI18n(); + const navigate = useNavigate(); const { showOk, showError } = useToast(); const [projects, setProjects] = useState([]); @@ -1694,17 +1695,25 @@ export function BizMigrationPage() {

{t("bizMigration.title")}

+ +
-

- {t("bizMigration.hintShort")}{" "} - {t("network.nav.monitorTemplates")} - {" · "} - {t("network.nav.compareTemplates")} -

diff --git a/web/src/pages/network/BizMonitorTemplatesPage.tsx b/web/src/pages/network/BizMonitorTemplatesPage.tsx index ca40de9..7ac5bd5 100644 --- a/web/src/pages/network/BizMonitorTemplatesPage.tsx +++ b/web/src/pages/network/BizMonitorTemplatesPage.tsx @@ -1,6 +1,7 @@ import { Button, Input, Modal } from "@heroui/react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; +import { HelpHint } from "../../components/HelpHint"; import { AppModalShell } from "../../components/ui/AppModalShell"; import { FieldSelect } from "../../components/ui/FieldSelect"; import { useDebouncedValue } from "../../hooks/useDebouncedValue"; @@ -806,6 +807,7 @@ function DualPatternList({ export function BizMonitorTemplatesPage() { const { t } = useI18n(); + const navigate = useNavigate(); const { showOk, showError } = useToast(); const [items, setItems] = useState([]); @@ -1105,7 +1107,10 @@ export function BizMonitorTemplatesPage() { return (
-

{t("bizMonitorTpl.title")}

+

+ {t("bizMonitorTpl.title")} + +

+
-

{t("bizMonitorTpl.hint")}

-

- {t("bizMonitorTpl.openCompare")} - {" · "} - {t("bizMonitorTpl.templateImportHint")} -