feat(managed-ne): optional password and hop columns in import

Allow creating/importing NEs without target password for bastion-managed workflows, and support optional hop_* columns in bulk import templates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-06-05 11:23:57 +08:00
parent bac4a609b1
commit 7d74e7b3f4
6 changed files with 167 additions and 53 deletions

View file

@ -169,6 +169,8 @@ 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.",
importResult: {
done: "Import done: {{inserted}} inserted, {{updated}} updated, {{failed}} failed row(s)",
},
@ -185,6 +187,8 @@ 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",

View file

@ -167,6 +167,8 @@ const zh = {
connectDetail: "详情",
connectDetailTitle: "连通性测试日志",
connectDetailEmpty: "暂无日志。请先执行连通性测试;失败时会记录完整错误与跳板上下文(不含密码)。",
importHint:
"导入模板含可选跳板列(hop_enabled 等);也可先导入网元再批量添加代理。密码可留空(堡垒机托管场景)。",
importResult: {
done: "导入完成:新增 {{inserted}},更新 {{updated}},失败 {{failed}} 行",
},
@ -183,6 +185,7 @@ const zh = {
deleted: "网元已删除",
passwordRequired: "请填写密码",
passwordOptional: "留空则不修改",
passwordHint: "直连网元需填写;经堡垒机托管时可留空,后续可用「批量添加代理」统一配置跳板。",
},
hop: {
sectionTitle: "跳板 / 代理",

View file

@ -185,10 +185,6 @@ export function NePage() {
...(form.password ? { password: form.password } : {}),
...(form.hop_password ? { hop_password: form.hop_password } : {}),
};
const bastionManaged =
form.hop_enabled &&
form.hop_vendor === "bastion" &&
form.hop_target_auth_mode === "bastion_managed";
if (form.hop_enabled) {
if (!form.hop_host.trim()) throw new Error(t("managedNe.hop.hostRequired"));
if (!form.hop_username.trim()) throw new Error(t("managedNe.hop.userRequired"));
@ -199,7 +195,6 @@ export function NePage() {
if (!form.hop_password) delete (body as { hop_password?: string }).hop_password;
return updateManagedNe(editing.id, body);
}
if (!form.password && !bastionManaged) throw new Error(t("managedNe.form.passwordRequired"));
return createManagedNe({ ...body, password: form.password || "" });
},
onSuccess: async () => {
@ -372,6 +367,7 @@ export function NePage() {
>
{importMutation.isPending ? t("managedNe.importing") : t("managedNe.importBtn")}
</button>
<span className="form-field-hint">{t("managedNe.importHint")}</span>
<input
ref={importRef}
type="file"
@ -638,37 +634,16 @@ export function NePage() {
/>
</label>
<label>
<FormLabel
required={
!editing &&
!(
form.hop_enabled &&
form.hop_vendor === "bastion" &&
form.hop_target_auth_mode === "bastion_managed"
)
}
>
<FormLabel>
{t("managedNe.col.password")}
{editing ||
(form.hop_enabled &&
form.hop_vendor === "bastion" &&
form.hop_target_auth_mode === "bastion_managed") ? (
<span className="form-label__optional"> ({t("managedNe.form.passwordOptional")})</span>
) : null}
<span className="form-label__optional"> ({t("managedNe.form.passwordOptional")})</span>
</FormLabel>
<input
type="password"
required={
!editing &&
!(
form.hop_enabled &&
form.hop_vendor === "bastion" &&
form.hop_target_auth_mode === "bastion_managed"
)
}
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>