mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 05:30:46 +08:00
ui(managed-ne): replace import hint with help popover
Move bulk import and bastion/jump notes into a toolbar help popover to keep the action bar clean. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
7d74e7b3f4
commit
81c028949b
5 changed files with 114 additions and 78 deletions
|
|
@ -3,9 +3,10 @@ import { useEffect, useRef, useState } from "react";
|
|||
type Props = {
|
||||
text: string;
|
||||
ariaLabel: string;
|
||||
align?: "start" | "end";
|
||||
};
|
||||
|
||||
export function HelpHint({ text, ariaLabel }: Props) {
|
||||
export function HelpHint({ text, ariaLabel, align = "start" }: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const rootRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ export function HelpHint({ text, ariaLabel }: Props) {
|
|||
}, [open]);
|
||||
|
||||
return (
|
||||
<span className="help-hint" ref={rootRef}>
|
||||
<span className={`help-hint${align === "end" ? " help-hint--align-end" : ""}`} ref={rootRef}>
|
||||
<button
|
||||
type="button"
|
||||
className="help-hint__trigger"
|
||||
|
|
|
|||
|
|
@ -169,8 +169,19 @@ const en = {
|
|||
connectDetailTitle: "Connectivity test log",
|
||||
connectDetailEmpty:
|
||||
"No log yet. Run a connectivity test first; failures store full errors and hop context (passwords excluded).",
|
||||
importHint:
|
||||
"Import template includes optional hop columns (hop_enabled, etc.), or import NEs first then batch-add proxy. Password may be empty for bastion-managed targets.",
|
||||
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" +
|
||||
"· Optional hop columns: hop_enabled, hop_vendor, hop_host, hop_port, hop_username, hop_password, hop_target_auth_mode, hop_command_template.\n" +
|
||||
"· Or import NEs first, select rows, then use Batch add proxy.\n\n" +
|
||||
"[Jump / bastion]\n" +
|
||||
"· Bastion SSH username template: {hop_user}@{target_user}@{target_ip}@{hop_host}; target account = NE Username.\n" +
|
||||
"· Bastion-managed: set Jump password (Vault); target password optional. Manual mode needs target password.\n" +
|
||||
"· JumpServer/CBH often use port 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).",
|
||||
importResult: {
|
||||
done: "Import done: {{inserted}} inserted, {{updated}} updated, {{failed}} failed row(s)",
|
||||
},
|
||||
|
|
@ -187,8 +198,6 @@ const en = {
|
|||
deleted: "Network element deleted",
|
||||
passwordRequired: "Password is required",
|
||||
passwordOptional: "leave blank to keep unchanged",
|
||||
passwordHint:
|
||||
"Required for direct login; leave empty when bastion manages target credentials. Use batch proxy later.",
|
||||
},
|
||||
hop: {
|
||||
sectionTitle: "Jump host / proxy",
|
||||
|
|
|
|||
|
|
@ -167,8 +167,19 @@ const zh = {
|
|||
connectDetail: "详情",
|
||||
connectDetailTitle: "连通性测试日志",
|
||||
connectDetailEmpty: "暂无日志。请先执行连通性测试;失败时会记录完整错误与跳板上下文(不含密码)。",
|
||||
importHint:
|
||||
"导入模板含可选跳板列(hop_enabled 等);也可先导入网元再批量添加代理。密码可留空(堡垒机托管场景)。",
|
||||
help:
|
||||
"【批量导入】\n" +
|
||||
"· 必填列:device_type、ip、username、port、protocol、name、vendor;可先下载模板。\n" +
|
||||
"· password 可留空(直连需填;堡垒机托管或后续批量添加代理时可空)。\n" +
|
||||
"· 可选跳板列:hop_enabled、hop_vendor、hop_host、hop_port、hop_username、hop_password、hop_target_auth_mode、hop_command_template。\n" +
|
||||
"· 也可先导入网元,勾选后点「批量添加代理」统一配置跳板。\n\n" +
|
||||
"【跳板 / 堡垒机】\n" +
|
||||
"· 堡垒机 SSH 用户名模板:{hop_user}@{target_user}@{target_ip}@{hop_host};目标账号填网元「用户名」。\n" +
|
||||
"· 堡垒机托管时填「跳板密码」(Vault 密码),目标密码可留空;手动模式需填目标密码。\n" +
|
||||
"· JumpServer/CBH 常用跳板端口 2222,部分现场为 22。\n\n" +
|
||||
"【连通性 / 编辑】\n" +
|
||||
"· 保存密码需配置 NETX_CREDENTIAL_SECRET_KEY。\n" +
|
||||
"· 编辑时密码留空表示不修改;连通性失败请点「详情」查看 hop 上下文(不含密码)。",
|
||||
importResult: {
|
||||
done: "导入完成:新增 {{inserted}},更新 {{updated}},失败 {{failed}} 行",
|
||||
},
|
||||
|
|
@ -185,7 +196,6 @@ const zh = {
|
|||
deleted: "网元已删除",
|
||||
passwordRequired: "请填写密码",
|
||||
passwordOptional: "留空则不修改",
|
||||
passwordHint: "直连网元需填写;经堡垒机托管时可留空,后续可用「批量添加代理」统一配置跳板。",
|
||||
},
|
||||
hop: {
|
||||
sectionTitle: "跳板 / 代理",
|
||||
|
|
|
|||
|
|
@ -699,9 +699,23 @@ pre {
|
|||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1.45;
|
||||
white-space: pre-line;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.help-hint--align-end .help-hint__popover {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.panel__toolbar-end {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mini-chart__bar-wrap {
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
managedNeImportTemplateUrl,
|
||||
updateManagedNe,
|
||||
} from "../services/api";
|
||||
import { HelpHint } from "../components/HelpHint";
|
||||
import { HopProxyFields, emptyHopProxyFields, type HopProxyFieldsState } from "../components/HopProxyFields";
|
||||
import { queryKeys } from "../constants/queryKeys";
|
||||
import { useI18n } from "../i18n";
|
||||
|
|
@ -350,74 +351,76 @@ export function NePage() {
|
|||
<section className="panel">
|
||||
<div className="panel__toolbar">
|
||||
<h2>{t("managedNe.title")}</h2>
|
||||
<div className="panel__actions">
|
||||
<button type="button" onClick={openCreate} disabled={!credsOk}>
|
||||
{t("managedNe.add")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.location.assign(managedNeImportTemplateUrl("xlsx"))}
|
||||
>
|
||||
{t("managedNe.downloadTemplate")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => importRef.current?.click()}
|
||||
disabled={!credsOk || importMutation.isPending}
|
||||
>
|
||||
{importMutation.isPending ? t("managedNe.importing") : t("managedNe.importBtn")}
|
||||
</button>
|
||||
<span className="form-field-hint">{t("managedNe.importHint")}</span>
|
||||
<input
|
||||
ref={importRef}
|
||||
type="file"
|
||||
accept=".csv,.xlsx,.xls"
|
||||
hidden
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
e.target.value = "";
|
||||
if (file) importMutation.mutate(file);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={selected.length === 0 || connectMutation.isPending}
|
||||
onClick={() => connectMutation.mutate(selected)}
|
||||
>
|
||||
{connectMutation.isPending ? t("managedNe.connect.running") : t("managedNe.connect.run")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!credsOk || selected.length === 0 || batchHopMutation.isPending}
|
||||
onClick={() => {
|
||||
if (selected.length === 0) {
|
||||
showError(t("managedNe.hop.selectRequired"));
|
||||
return;
|
||||
}
|
||||
setBatchHop(emptyHopProxyFields());
|
||||
setBatchHopOpen(true);
|
||||
}}
|
||||
>
|
||||
{batchHopMutation.isPending ? t("managedNe.hop.applying") : t("managedNe.hop.batchAdd")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--danger"
|
||||
disabled={selected.length === 0 || batchDeleteMutation.isPending}
|
||||
onClick={() => {
|
||||
if (selected.length === 0) {
|
||||
showError(t("managedNe.batchDeleteSelectRequired"));
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(t("managedNe.batchDeleteConfirm", { n: selected.length }))) return;
|
||||
batchDeleteMutation.mutate(selected);
|
||||
}}
|
||||
>
|
||||
{batchDeleteMutation.isPending ? t("managedNe.batchDeleting") : t("managedNe.batchDelete")}
|
||||
</button>
|
||||
<button type="button" onClick={() => invalidateList()}>
|
||||
{t("common.refresh")}
|
||||
</button>
|
||||
<div className="panel__toolbar-end">
|
||||
<div className="panel__actions">
|
||||
<button type="button" onClick={openCreate} disabled={!credsOk}>
|
||||
{t("managedNe.add")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.location.assign(managedNeImportTemplateUrl("xlsx"))}
|
||||
>
|
||||
{t("managedNe.downloadTemplate")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => importRef.current?.click()}
|
||||
disabled={!credsOk || importMutation.isPending}
|
||||
>
|
||||
{importMutation.isPending ? t("managedNe.importing") : t("managedNe.importBtn")}
|
||||
</button>
|
||||
<input
|
||||
ref={importRef}
|
||||
type="file"
|
||||
accept=".csv,.xlsx,.xls"
|
||||
hidden
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
e.target.value = "";
|
||||
if (file) importMutation.mutate(file);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={selected.length === 0 || connectMutation.isPending}
|
||||
onClick={() => connectMutation.mutate(selected)}
|
||||
>
|
||||
{connectMutation.isPending ? t("managedNe.connect.running") : t("managedNe.connect.run")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!credsOk || selected.length === 0 || batchHopMutation.isPending}
|
||||
onClick={() => {
|
||||
if (selected.length === 0) {
|
||||
showError(t("managedNe.hop.selectRequired"));
|
||||
return;
|
||||
}
|
||||
setBatchHop(emptyHopProxyFields());
|
||||
setBatchHopOpen(true);
|
||||
}}
|
||||
>
|
||||
{batchHopMutation.isPending ? t("managedNe.hop.applying") : t("managedNe.hop.batchAdd")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--danger"
|
||||
disabled={selected.length === 0 || batchDeleteMutation.isPending}
|
||||
onClick={() => {
|
||||
if (selected.length === 0) {
|
||||
showError(t("managedNe.batchDeleteSelectRequired"));
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(t("managedNe.batchDeleteConfirm", { n: selected.length }))) return;
|
||||
batchDeleteMutation.mutate(selected);
|
||||
}}
|
||||
>
|
||||
{batchDeleteMutation.isPending ? t("managedNe.batchDeleting") : t("managedNe.batchDelete")}
|
||||
</button>
|
||||
<button type="button" onClick={() => invalidateList()}>
|
||||
{t("common.refresh")}
|
||||
</button>
|
||||
</div>
|
||||
<HelpHint text={t("managedNe.help")} ariaLabel={t("common.help")} align="end" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -643,7 +646,6 @@ export function NePage() {
|
|||
value={form.password}
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
/>
|
||||
<span className="form-field-hint">{t("managedNe.form.passwordHint")}</span>
|
||||
</label>
|
||||
<label>
|
||||
<FormLabel>{t("managedNe.col.tags")}</FormLabel>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue