mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-08 21:49:52 +00:00
497 lines
19 KiB
TypeScript
497 lines
19 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
||
import { DownOutlined as DownOutlinedIcon } from "@ant-design/icons";
|
||
import { Alert, Button, Dropdown, Form, type FormInstance, Input, Select } from "antd";
|
||
import { createSchemaFieldRule } from "antd-zod";
|
||
import { z } from "zod";
|
||
|
||
import CodeInput from "@/components/CodeInput";
|
||
import Show from "@/components/Show";
|
||
import { CERTIFICATE_FORMATS } from "@/domain/certificate";
|
||
|
||
type DeployNodeConfigFormLocalConfigFieldValues = Nullish<{
|
||
format: string;
|
||
certPath: string;
|
||
certPathForServerOnly?: string;
|
||
certPathForIntermediaOnly?: string;
|
||
keyPath?: string;
|
||
pfxPassword?: string;
|
||
jksAlias?: string;
|
||
jksKeypass?: string;
|
||
jksStorepass?: string;
|
||
shellEnv?: string;
|
||
preCommand?: string;
|
||
postCommand?: string;
|
||
}>;
|
||
|
||
export type DeployNodeConfigFormLocalConfigProps = {
|
||
form: FormInstance;
|
||
formName: string;
|
||
disabled?: boolean;
|
||
initialValues?: DeployNodeConfigFormLocalConfigFieldValues;
|
||
onValuesChange?: (values: DeployNodeConfigFormLocalConfigFieldValues) => void;
|
||
};
|
||
|
||
const FORMAT_PEM = CERTIFICATE_FORMATS.PEM;
|
||
const FORMAT_PFX = CERTIFICATE_FORMATS.PFX;
|
||
const FORMAT_JKS = CERTIFICATE_FORMATS.JKS;
|
||
|
||
const SHELLENV_SH = "sh" as const;
|
||
const SHELLENV_CMD = "cmd" as const;
|
||
const SHELLENV_POWERSHELL = "powershell" as const;
|
||
|
||
const initFormModel = (): DeployNodeConfigFormLocalConfigFieldValues => {
|
||
return {
|
||
format: FORMAT_PEM,
|
||
certPath: "/etc/ssl/certimate/cert.crt",
|
||
keyPath: "/etc/ssl/certimate/cert.key",
|
||
shellEnv: SHELLENV_SH,
|
||
};
|
||
};
|
||
|
||
export const initPresetScript = (
|
||
key: "sh_backup_files" | "ps_backup_files" | "sh_reload_nginx" | "ps_binding_iis" | "ps_binding_netsh" | "ps_binding_rdp",
|
||
params?: {
|
||
certPath?: string;
|
||
certPathForServerOnly?: string;
|
||
certPathForIntermediaOnly?: string;
|
||
keyPath?: string;
|
||
pfxPassword?: string;
|
||
jksAlias?: string;
|
||
jksKeypass?: string;
|
||
jksStorepass?: string;
|
||
}
|
||
) => {
|
||
switch (key) {
|
||
case "sh_backup_files":
|
||
return `# 请将以下路径替换为实际值
|
||
cp "${params?.certPath || "<your-cert-path>"}" "${params?.certPath || "<your-cert-path>"}.bak" 2>/dev/null || :
|
||
cp "${params?.keyPath || "<your-key-path>"}" "${params?.keyPath || "<your-key-path>"}.bak" 2>/dev/null || :
|
||
`.trim();
|
||
|
||
case "ps_backup_files":
|
||
return `# 请将以下路径替换为实际值
|
||
if (Test-Path -Path "${params?.certPath || "<your-cert-path>"}" -PathType Leaf) {
|
||
Copy-Item -Path "${params?.certPath || "<your-cert-path>"}" -Destination "${params?.certPath || "<your-cert-path>"}.bak" -Force
|
||
}
|
||
if (Test-Path -Path "${params?.keyPath || "<your-key-path>"}" -PathType Leaf) {
|
||
Copy-Item -Path "${params?.keyPath || "<your-key-path>"}" -Destination "${params?.keyPath || "<your-key-path>"}.bak" -Force
|
||
}
|
||
`.trim();
|
||
|
||
case "sh_reload_nginx":
|
||
return `# *** 需要 root 权限 ***
|
||
|
||
sudo service nginx reload
|
||
`.trim();
|
||
|
||
case "ps_binding_iis":
|
||
return `# *** 需要管理员权限 ***
|
||
|
||
# 请将以下变量替换为实际值
|
||
$pfxPath = "${params?.certPath || "<your-cert-path>"}" # PFX 文件路径(与表单中保持一致)
|
||
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX 密码(与表单中保持一致)
|
||
$siteName = "<your-site-name>" # IIS 网站名称
|
||
$domain = "<your-domain-name>" # 域名
|
||
$ipaddr = "<your-binding-ip>" # 绑定 IP,“*”表示所有 IP 绑定
|
||
$port = "<your-binding-port>" # 绑定端口
|
||
|
||
# 导入证书到本地计算机的个人存储区
|
||
$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable
|
||
# 获取 Thumbprint
|
||
$thumbprint = $cert.Thumbprint
|
||
# 导入 WebAdministration 模块
|
||
Import-Module WebAdministration
|
||
# 检查是否已存在 HTTPS 绑定
|
||
$existingBinding = Get-WebBinding -Name "$siteName" -Protocol "https" -Port $port -HostHeader "$domain" -ErrorAction SilentlyContinue
|
||
if (!$existingBinding) {
|
||
# 添加新的 HTTPS 绑定
|
||
New-WebBinding -Name "$siteName" -Protocol "https" -Port $port -IPAddress "$ipaddr" -HostHeader "$domain"
|
||
}
|
||
# 获取绑定对象
|
||
$binding = Get-WebBinding -Name "$siteName" -Protocol "https" -Port $port -IPAddress "$ipaddr" -HostHeader "$domain"
|
||
# 绑定 SSL 证书
|
||
$binding.AddSslCertificate($thumbprint, "My")
|
||
# 删除目录下的证书文件
|
||
Remove-Item -Path "$pfxPath" -Force
|
||
`.trim();
|
||
|
||
case "ps_binding_netsh":
|
||
return `# *** 需要管理员权限 ***
|
||
|
||
# 请将以下变量替换为实际值
|
||
$pfxPath = "${params?.certPath || "<your-cert-path>"}" # PFX 文件路径(与表单中保持一致)
|
||
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX 密码(与表单中保持一致)
|
||
$ipaddr = "<your-binding-ip>" # 绑定 IP,“0.0.0.0”表示所有 IP 绑定,可填入域名
|
||
$port = "<your-binding-port>" # 绑定端口
|
||
|
||
# 导入证书到本地计算机的个人存储区
|
||
$addr = $ipaddr + ":" + $port
|
||
$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable
|
||
# 获取 Thumbprint
|
||
$thumbprint = $cert.Thumbprint
|
||
# 检测端口是否绑定证书,如绑定则删除绑定
|
||
$isExist = netsh http show sslcert ipport=$addr
|
||
if ($isExist -like "*$addr*"){ netsh http delete sslcert ipport=$addr }
|
||
# 绑定到端口
|
||
netsh http add sslcert ipport=$addr certhash=$thumbprint
|
||
# 删除目录下的证书文件
|
||
Remove-Item -Path "$pfxPath" -Force
|
||
`.trim();
|
||
|
||
case "ps_binding_rdp":
|
||
return `# *** 需要管理员权限 ***
|
||
|
||
# 请将以下变量替换为实际值
|
||
$pfxPath = "${params?.certPath || "<your-cert-path>"}" # PFX 文件路径(与表单中保持一致)
|
||
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX 密码(与表单中保持一致)
|
||
|
||
# 导入证书到本地计算机的个人存储区
|
||
$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable
|
||
# 获取 Thumbprint
|
||
$thumbprint = $cert.Thumbprint
|
||
# 绑定到 RDP
|
||
$rdpCertPath = "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp"
|
||
Set-ItemProperty -Path $rdpCertPath -Name "SSLCertificateSHA1Hash" -Value "$thumbprint"
|
||
`.trim();
|
||
}
|
||
};
|
||
|
||
const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormLocalConfigProps) => {
|
||
const { t } = useTranslation();
|
||
|
||
const formSchema = z.object({
|
||
format: z.union([z.literal(FORMAT_PEM), z.literal(FORMAT_PFX), z.literal(FORMAT_JKS)], {
|
||
message: t("workflow_node.deploy.form.local_format.placeholder"),
|
||
}),
|
||
certPath: z
|
||
.string()
|
||
.min(1, t("workflow_node.deploy.form.local_cert_path.tooltip"))
|
||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||
.trim(),
|
||
certPathForServerOnly: z
|
||
.string()
|
||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||
.trim()
|
||
.nullish(),
|
||
certPathForIntermediaOnly: z
|
||
.string()
|
||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||
.trim()
|
||
.nullish(),
|
||
keyPath: z
|
||
.string()
|
||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||
.trim()
|
||
.nullish()
|
||
.refine((v) => fieldFormat !== FORMAT_PEM || !!v?.trim(), { message: t("workflow_node.deploy.form.local_key_path.tooltip") }),
|
||
pfxPassword: z
|
||
.string()
|
||
.max(64, t("common.errmsg.string_max", { max: 256 }))
|
||
.trim()
|
||
.nullish()
|
||
.refine((v) => fieldFormat !== FORMAT_PFX || !!v?.trim(), { message: t("workflow_node.deploy.form.local_pfx_password.tooltip") }),
|
||
jksAlias: z
|
||
.string()
|
||
.max(64, t("common.errmsg.string_max", { max: 256 }))
|
||
.trim()
|
||
.nullish()
|
||
.refine((v) => fieldFormat !== FORMAT_JKS || !!v?.trim(), { message: t("workflow_node.deploy.form.local_jks_alias.tooltip") }),
|
||
jksKeypass: z
|
||
.string()
|
||
.max(64, t("common.errmsg.string_max", { max: 256 }))
|
||
.trim()
|
||
.nullish()
|
||
.refine((v) => fieldFormat !== FORMAT_JKS || !!v?.trim(), { message: t("workflow_node.deploy.form.local_jks_keypass.tooltip") }),
|
||
jksStorepass: z
|
||
.string()
|
||
.max(64, t("common.errmsg.string_max", { max: 256 }))
|
||
.trim()
|
||
.nullish()
|
||
.refine((v) => fieldFormat !== FORMAT_JKS || !!v?.trim(), { message: t("workflow_node.deploy.form.local_jks_storepass.tooltip") }),
|
||
shellEnv: z.union([z.literal(SHELLENV_SH), z.literal(SHELLENV_CMD), z.literal(SHELLENV_POWERSHELL)], {
|
||
message: t("workflow_node.deploy.form.local_shell_env.placeholder"),
|
||
}),
|
||
preCommand: z
|
||
.string()
|
||
.max(20480, t("common.errmsg.string_max", { max: 20480 }))
|
||
.nullish(),
|
||
postCommand: z
|
||
.string()
|
||
.max(20480, t("common.errmsg.string_max", { max: 20480 }))
|
||
.nullish(),
|
||
});
|
||
const formRule = createSchemaFieldRule(formSchema);
|
||
|
||
const fieldFormat = Form.useWatch("format", formInst);
|
||
const fieldCertPath = Form.useWatch("certPath", formInst);
|
||
|
||
const handleFormatSelect = (value: string) => {
|
||
if (fieldFormat === value) return;
|
||
|
||
switch (value) {
|
||
case FORMAT_PEM:
|
||
{
|
||
if (/(.pfx|.jks)$/.test(fieldCertPath)) {
|
||
formInst.setFieldValue("certPath", fieldCertPath.replace(/(.pfx|.jks)$/, ".crt"));
|
||
}
|
||
}
|
||
break;
|
||
|
||
case FORMAT_PFX:
|
||
{
|
||
if (/(.crt|.jks)$/.test(fieldCertPath)) {
|
||
formInst.setFieldValue("certPath", fieldCertPath.replace(/(.crt|.jks)$/, ".pfx"));
|
||
}
|
||
}
|
||
break;
|
||
|
||
case FORMAT_JKS:
|
||
{
|
||
if (/(.crt|.pfx)$/.test(fieldCertPath)) {
|
||
formInst.setFieldValue("certPath", fieldCertPath.replace(/(.crt|.pfx)$/, ".jks"));
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
};
|
||
|
||
const handlePresetPreScriptClick = (key: string) => {
|
||
switch (key) {
|
||
case "sh_backup_files":
|
||
case "ps_backup_files":
|
||
{
|
||
const presetScriptParams = {
|
||
certPath: formInst.getFieldValue("certPath"),
|
||
keyPath: formInst.getFieldValue("keyPath"),
|
||
};
|
||
formInst.setFieldValue("shellEnv", SHELLENV_SH);
|
||
formInst.setFieldValue("preCommand", initPresetScript(key, presetScriptParams));
|
||
}
|
||
break;
|
||
}
|
||
};
|
||
|
||
const handlePresetPostScriptClick = (key: string) => {
|
||
switch (key) {
|
||
case "sh_reload_nginx":
|
||
{
|
||
formInst.setFieldValue("shellEnv", SHELLENV_SH);
|
||
formInst.setFieldValue("postCommand", initPresetScript(key));
|
||
}
|
||
break;
|
||
|
||
case "ps_binding_iis":
|
||
case "ps_binding_netsh":
|
||
case "ps_binding_rdp":
|
||
{
|
||
const presetScriptParams = {
|
||
certPath: formInst.getFieldValue("certPath"),
|
||
pfxPassword: formInst.getFieldValue("pfxPassword"),
|
||
};
|
||
formInst.setFieldValue("shellEnv", SHELLENV_POWERSHELL);
|
||
formInst.setFieldValue("postCommand", initPresetScript(key, presetScriptParams));
|
||
}
|
||
break;
|
||
}
|
||
};
|
||
|
||
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
|
||
onValuesChange?.(values);
|
||
};
|
||
|
||
return (
|
||
<Form
|
||
form={formInst}
|
||
disabled={disabled}
|
||
initialValues={initialValues ?? initFormModel()}
|
||
layout="vertical"
|
||
name={formName}
|
||
onValuesChange={handleFormChange}
|
||
>
|
||
<Form.Item>
|
||
<Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local.guide") }}></span>} />
|
||
</Form.Item>
|
||
|
||
<Form.Item name="format" label={t("workflow_node.deploy.form.local_format.label")} rules={[formRule]}>
|
||
<Select placeholder={t("workflow_node.deploy.form.local_format.placeholder")} onSelect={handleFormatSelect}>
|
||
<Select.Option key={FORMAT_PEM} value={FORMAT_PEM}>
|
||
{t("workflow_node.deploy.form.local_format.option.pem.label")}
|
||
</Select.Option>
|
||
<Select.Option key={FORMAT_PFX} value={FORMAT_PFX}>
|
||
{t("workflow_node.deploy.form.local_format.option.pfx.label")}
|
||
</Select.Option>
|
||
<Select.Option key={FORMAT_JKS} value={FORMAT_JKS}>
|
||
{t("workflow_node.deploy.form.local_format.option.jks.label")}
|
||
</Select.Option>
|
||
</Select>
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="certPath"
|
||
label={t("workflow_node.deploy.form.local_cert_path.label")}
|
||
rules={[formRule]}
|
||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_cert_path.tooltip") }}></span>}
|
||
>
|
||
<Input placeholder={t("workflow_node.deploy.form.local_cert_path.placeholder")} />
|
||
</Form.Item>
|
||
|
||
<Show when={fieldFormat === FORMAT_PEM}>
|
||
<Form.Item
|
||
name="keyPath"
|
||
label={t("workflow_node.deploy.form.local_key_path.label")}
|
||
rules={[formRule]}
|
||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_key_path.tooltip") }}></span>}
|
||
>
|
||
<Input placeholder={t("workflow_node.deploy.form.local_key_path.placeholder")} />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="certPathForServerOnly"
|
||
label={t("workflow_node.deploy.form.local_servercert_path.label")}
|
||
rules={[formRule]}
|
||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_servercert_path.tooltip") }}></span>}
|
||
>
|
||
<Input allowClear placeholder={t("workflow_node.deploy.form.local_servercert_path.placeholder")} />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="certPathForIntermediaOnly"
|
||
label={t("workflow_node.deploy.form.local_intermediacert_path.label")}
|
||
rules={[formRule]}
|
||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_intermediacert_path.tooltip") }}></span>}
|
||
>
|
||
<Input allowClear placeholder={t("workflow_node.deploy.form.local_intermediacert_path.placeholder")} />
|
||
</Form.Item>
|
||
</Show>
|
||
|
||
<Show when={fieldFormat === FORMAT_PFX}>
|
||
<Form.Item
|
||
name="pfxPassword"
|
||
label={t("workflow_node.deploy.form.local_pfx_password.label")}
|
||
rules={[formRule]}
|
||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_pfx_password.tooltip") }}></span>}
|
||
>
|
||
<Input placeholder={t("workflow_node.deploy.form.local_pfx_password.placeholder")} />
|
||
</Form.Item>
|
||
</Show>
|
||
|
||
<Show when={fieldFormat === FORMAT_JKS}>
|
||
<Form.Item
|
||
name="jksAlias"
|
||
label={t("workflow_node.deploy.form.local_jks_alias.label")}
|
||
rules={[formRule]}
|
||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_jks_alias.tooltip") }}></span>}
|
||
>
|
||
<Input placeholder={t("workflow_node.deploy.form.local_jks_alias.placeholder")} />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="jksKeypass"
|
||
label={t("workflow_node.deploy.form.local_jks_keypass.label")}
|
||
rules={[formRule]}
|
||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_jks_keypass.tooltip") }}></span>}
|
||
>
|
||
<Input placeholder={t("workflow_node.deploy.form.local_jks_keypass.placeholder")} />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="jksStorepass"
|
||
label={t("workflow_node.deploy.form.local_jks_storepass.label")}
|
||
rules={[formRule]}
|
||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_jks_storepass.tooltip") }}></span>}
|
||
>
|
||
<Input placeholder={t("workflow_node.deploy.form.local_jks_storepass.placeholder")} />
|
||
</Form.Item>
|
||
</Show>
|
||
|
||
<Form.Item name="shellEnv" label={t("workflow_node.deploy.form.local_shell_env.label")} rules={[formRule]}>
|
||
<Select placeholder={t("workflow_node.deploy.form.local_shell_env.placeholder")}>
|
||
<Select.Option key={SHELLENV_SH} value={SHELLENV_SH}>
|
||
{t("workflow_node.deploy.form.local_shell_env.option.sh.label")}
|
||
</Select.Option>
|
||
<Select.Option key={SHELLENV_CMD} value={SHELLENV_CMD}>
|
||
{t("workflow_node.deploy.form.local_shell_env.option.cmd.label")}
|
||
</Select.Option>
|
||
<Select.Option key={SHELLENV_POWERSHELL} value={SHELLENV_POWERSHELL}>
|
||
{t("workflow_node.deploy.form.local_shell_env.option.powershell.label")}
|
||
</Select.Option>
|
||
</Select>
|
||
</Form.Item>
|
||
|
||
<Form.Item className="mb-0" htmlFor="null">
|
||
<label className="mb-1 block">
|
||
<div className="flex w-full items-center justify-between gap-4">
|
||
<div className="max-w-full grow truncate">
|
||
<span>{t("workflow_node.deploy.form.local_pre_command.label")}</span>
|
||
</div>
|
||
<div className="text-right">
|
||
<Dropdown
|
||
menu={{
|
||
items: ["sh_backup_files", "ps_backup_files"].map((key) => ({
|
||
key,
|
||
label: t(`workflow_node.deploy.form.local_preset_scripts.option.${key}.label`),
|
||
onClick: () => handlePresetPreScriptClick(key),
|
||
})),
|
||
}}
|
||
trigger={["click"]}
|
||
>
|
||
<Button size="small" type="link">
|
||
{t("workflow_node.deploy.form.local_preset_scripts.button")}
|
||
<DownOutlinedIcon />
|
||
</Button>
|
||
</Dropdown>
|
||
</div>
|
||
</div>
|
||
</label>
|
||
<Form.Item name="preCommand" rules={[formRule]}>
|
||
<CodeInput
|
||
height="auto"
|
||
minHeight="64px"
|
||
maxHeight="256px"
|
||
language={["shell", "powershell"]}
|
||
placeholder={t("workflow_node.deploy.form.local_pre_command.placeholder")}
|
||
/>
|
||
</Form.Item>
|
||
</Form.Item>
|
||
|
||
<Form.Item className="mb-0" htmlFor="null">
|
||
<label className="mb-1 block">
|
||
<div className="flex w-full items-center justify-between gap-4">
|
||
<div className="max-w-full grow truncate">
|
||
<span>{t("workflow_node.deploy.form.local_post_command.label")}</span>
|
||
</div>
|
||
<div className="text-right">
|
||
<Dropdown
|
||
menu={{
|
||
items: ["sh_reload_nginx", "ps_binding_iis", "ps_binding_netsh", "ps_binding_rdp"].map((key) => ({
|
||
key,
|
||
label: t(`workflow_node.deploy.form.local_preset_scripts.option.${key}.label`),
|
||
onClick: () => handlePresetPostScriptClick(key),
|
||
})),
|
||
}}
|
||
trigger={["click"]}
|
||
>
|
||
<Button size="small" type="link">
|
||
{t("workflow_node.deploy.form.local_preset_scripts.button")}
|
||
<DownOutlinedIcon />
|
||
</Button>
|
||
</Dropdown>
|
||
</div>
|
||
</div>
|
||
</label>
|
||
<Form.Item name="postCommand" rules={[formRule]}>
|
||
<CodeInput
|
||
height="auto"
|
||
minHeight="64px"
|
||
maxHeight="256px"
|
||
language={["shell", "powershell"]}
|
||
placeholder={t("workflow_node.deploy.form.local_post_command.placeholder")}
|
||
/>
|
||
</Form.Item>
|
||
</Form.Item>
|
||
</Form>
|
||
);
|
||
};
|
||
|
||
export default DeployNodeConfigFormLocalConfig;
|