feat(ui): TextFileInput

This commit is contained in:
Fu Diwei 2025-05-14 00:40:18 +08:00
parent 355059df3c
commit 04abf9dd76
11 changed files with 144 additions and 171 deletions

View File

@ -0,0 +1,51 @@
import { type ChangeEvent, useRef } from "react";
import { useTranslation } from "react-i18next";
import { UploadOutlined as UploadOutlinedIcon } from "@ant-design/icons";
import { Button, type ButtonProps, Input, Space, type UploadProps } from "antd";
import { type TextAreaProps } from "antd/es/input/TextArea";
import { mergeCls } from "@/utils/css";
import { readFileContent } from "@/utils/file";
export interface TextFileInputProps extends Omit<TextAreaProps, "onChange"> {
accept?: UploadProps["accept"];
uploadButtonProps?: Omit<ButtonProps, "disabled" | "onClick">;
uploadText?: string;
onChange?: (value: string) => void;
}
const TextFileInput = ({ className, style, accept, disabled, readOnly, uploadText, uploadButtonProps, onChange, ...props }: TextFileInputProps) => {
const { t } = useTranslation();
const fileInputRef = useRef<HTMLInputElement>(null);
const handleButtonClick = () => {
if (fileInputRef.current) {
fileInputRef.current.click();
}
};
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
const { files } = e.target as HTMLInputElement;
if (files?.length) {
const value = await readFileContent(files[0]);
onChange?.(value);
}
};
return (
<Space className={mergeCls("w-full", className)} style={style} direction="vertical" size="small">
<Input.TextArea {...props} disabled={disabled} readOnly={readOnly} onChange={(e) => onChange?.(e.target.value)} />
{!readOnly && (
<>
<Button {...uploadButtonProps} block disabled={disabled} icon={<UploadOutlinedIcon />} onClick={handleButtonClick}>
{uploadText ?? t("common.text.import_from_file")}
</Button>
<input ref={fileInputRef} type="file" style={{ display: "none" }} accept={accept} onChange={handleFileChange} />
</>
)}
</Space>
);
};
export default TextFileInput;

View File

@ -1,12 +1,10 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UploadOutlined as UploadOutlinedIcon } from "@ant-design/icons";
import { Button, Form, type FormInstance, Input, Upload, type UploadFile, type UploadProps } from "antd";
import { Form, type FormInstance } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import TextFileInput from "@/components/TextFileInput";
import { type AccessConfigForKubernetes } from "@/domain/access";
import { readFileContent } from "@/utils/file";
type AccessFormKubernetesConfigFieldValues = Nullish<AccessConfigForKubernetes>;
@ -34,24 +32,6 @@ const AccessFormKubernetesConfig = ({ form: formInst, formName, disabled, initia
});
const formRule = createSchemaFieldRule(formSchema);
const fieldKubeConfig = Form.useWatch("kubeConfig", formInst);
const [fieldKubeFileList, setFieldKubeFileList] = useState<UploadFile[]>([]);
useEffect(() => {
setFieldKubeFileList(initialValues?.kubeConfig?.trim() ? [{ uid: "-1", name: "kubeconfig", status: "done" }] : []);
}, [initialValues?.kubeConfig]);
const handleKubeFileChange: UploadProps["onChange"] = async ({ file }) => {
if (file && file.status !== "removed") {
formInst.setFieldValue("kubeConfig", await readFileContent(file.originFileObj ?? (file as unknown as File)));
setFieldKubeFileList([file]);
} else {
formInst.setFieldValue("kubeConfig", "");
setFieldKubeFileList([]);
}
onValuesChange?.(formInst.getFieldsValue(true));
};
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
@ -65,16 +45,13 @@ const AccessFormKubernetesConfig = ({ form: formInst, formName, disabled, initia
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item name="kubeConfig" noStyle rules={[formRule]}>
<Input.TextArea autoComplete="new-password" hidden placeholder={t("access.form.k8s_kubeconfig.placeholder")} value={fieldKubeConfig} />
</Form.Item>
<Form.Item
name="kubeConfig"
label={t("access.form.k8s_kubeconfig.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.k8s_kubeconfig.tooltip") }}></span>}
>
<Upload beforeUpload={() => false} fileList={fieldKubeFileList} maxCount={1} onChange={handleKubeFileChange}>
<Button icon={<UploadOutlinedIcon />}>{t("access.form.k8s_kubeconfig.upload")}</Button>
</Upload>
<TextFileInput allowClear autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("access.form.k8s_kubeconfig.placeholder")} />
</Form.Item>
</Form>
);

View File

@ -1,12 +1,10 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UploadOutlined as UploadOutlinedIcon } from "@ant-design/icons";
import { Button, Form, type FormInstance, Input, InputNumber, Upload, type UploadFile, type UploadProps } from "antd";
import { Form, type FormInstance, Input, InputNumber } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import TextFileInput from "@/components/TextFileInput";
import { type AccessConfigForSSH } from "@/domain/access";
import { readFileContent } from "@/utils/file";
import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators";
type AccessFormSSHConfigFieldValues = Nullish<AccessConfigForSSH>;
@ -59,24 +57,6 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
});
const formRule = createSchemaFieldRule(formSchema);
const fieldKey = Form.useWatch("key", formInst);
const [fieldKeyFileList, setFieldKeyFileList] = useState<UploadFile[]>([]);
useEffect(() => {
setFieldKeyFileList(initialValues?.key?.trim() ? [{ uid: "-1", name: "sshkey", status: "done" }] : []);
}, [initialValues?.key]);
const handleKeyFileChange: UploadProps["onChange"] = async ({ file }) => {
if (file && file.status !== "removed") {
formInst.setFieldValue("key", await readFileContent(file.originFileObj ?? (file as unknown as File)));
setFieldKeyFileList([file]);
} else {
formInst.setFieldValue("key", "");
setFieldKeyFileList([]);
}
onValuesChange?.(formInst.getFieldsValue(true));
};
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
@ -104,48 +84,36 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
</div>
</div>
<div className="flex space-x-2">
<div className="w-1/2">
<Form.Item name="username" label={t("access.form.ssh_username.label")} rules={[formRule]}>
<Input autoComplete="new-password" placeholder={t("access.form.ssh_username.placeholder")} />
</Form.Item>
</div>
<div className="w-1/2">
<Form.Item
name="password"
label={t("access.form.ssh_password.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_password.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.ssh_password.placeholder")} />
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_password.placeholder")} />
</Form.Item>
</div>
</div>
<div className="flex space-x-2">
<div className="w-1/2">
<Form.Item name="key" noStyle rules={[formRule]}>
<Input.TextArea autoComplete="new-password" hidden placeholder={t("access.form.ssh_key.placeholder")} value={fieldKey} />
<Form.Item
name="key"
label={t("access.form.ssh_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key.tooltip") }}></span>}
>
<TextFileInput allowClear autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("access.form.ssh_key.placeholder")} />
</Form.Item>
<Form.Item label={t("access.form.ssh_key.label")} tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key.tooltip") }}></span>}>
<Upload beforeUpload={() => false} fileList={fieldKeyFileList} maxCount={1} onChange={handleKeyFileChange}>
<Button icon={<UploadOutlinedIcon />}>{t("access.form.ssh_key.upload")}</Button>
</Upload>
</Form.Item>
</div>
<div className="w-1/2">
<Form.Item
name="keyPassphrase"
label={t("access.form.ssh_key_passphrase.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key_passphrase.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.ssh_key_passphrase.placeholder")} />
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_key_passphrase.placeholder")} />
</Form.Item>
</div>
</div>
</Form>
);
};

View File

@ -177,7 +177,7 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
</Form.Item>
<Form.Item name="message" label={t("workflow_node.notify.form.message.label")} rules={[formRule]}>
<Input.TextArea autoSize={{ minRows: 3, maxRows: 5 }} placeholder={t("workflow_node.notify.form.message.placeholder")} />
<Input.TextArea autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("workflow_node.notify.form.message.placeholder")} />
</Form.Item>
<Form.Item className="mb-0" htmlFor="null">

View File

@ -1,15 +1,14 @@
import { forwardRef, memo, useImperativeHandle } from "react";
import { useTranslation } from "react-i18next";
import { UploadOutlined as UploadOutlinedIcon } from "@ant-design/icons";
import { Button, Form, type FormInstance, Input, Upload, type UploadProps } from "antd";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validateCertificate, validatePrivateKey } from "@/api/certificates";
import TextFileInput from "@/components/TextFileInput";
import { type WorkflowNodeConfigForUpload } from "@/domain/workflow";
import { useAntdForm } from "@/hooks";
import { getErrMsg } from "@/utils/error";
import { readFileContent } from "@/utils/file";
type UploadNodeConfigFormFieldValues = Partial<WorkflowNodeConfigForUpload>;
@ -70,12 +69,9 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
} as UploadNodeConfigFormInstance;
});
const handleCertificateFileChange: UploadProps["onChange"] = async ({ file }) => {
if (file && file.status !== "removed") {
const certificate = await readFileContent(file.originFileObj ?? (file as unknown as File));
const handleCertificateChange = async (value: string) => {
try {
const resp = await validateCertificate(certificate);
const resp = await validateCertificate(value);
formInst.setFields([
{
name: "domains",
@ -83,7 +79,7 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
},
{
name: "certificate",
value: certificate,
value: value,
},
]);
} catch (e) {
@ -94,42 +90,33 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
},
{
name: "certificate",
value: "",
value: value,
errors: [getErrMsg(e)],
},
]);
}
} else {
formInst.setFieldValue("certificate", "");
}
onValuesChange?.(formInst.getFieldsValue(true));
};
const handlePrivateKeyFileChange: UploadProps["onChange"] = async ({ file }) => {
if (file && file.status !== "removed") {
const privateKey = await readFileContent(file.originFileObj ?? (file as unknown as File));
const handlePrivateKeyChange = async (value: string) => {
try {
await validatePrivateKey(privateKey);
await validatePrivateKey(value);
formInst.setFields([
{
name: "privateKey",
value: privateKey,
value: value,
},
]);
} catch (e) {
formInst.setFields([
{
name: "privateKey",
value: "",
value: value,
errors: [getErrMsg(e)],
},
]);
}
} else {
formInst.setFieldValue("privateKey", "");
}
onValuesChange?.(formInst.getFieldsValue(true));
};
@ -141,23 +128,19 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
</Form.Item>
<Form.Item name="certificate" label={t("workflow_node.upload.form.certificate.label")} rules={[formRule]}>
<Input.TextArea readOnly autoSize={{ minRows: 5, maxRows: 5 }} placeholder={t("workflow_node.upload.form.certificate.placeholder")} />
</Form.Item>
<Form.Item>
<Upload beforeUpload={() => false} maxCount={1} onChange={handleCertificateFileChange}>
<Button icon={<UploadOutlinedIcon />}>{t("workflow_node.upload.form.certificate.button")}</Button>
</Upload>
<TextFileInput
autoSize={{ minRows: 3, maxRows: 10 }}
placeholder={t("workflow_node.upload.form.certificate.placeholder")}
onChange={handleCertificateChange}
/>
</Form.Item>
<Form.Item name="privateKey" label={t("workflow_node.upload.form.private_key.label")} rules={[formRule]}>
<Input.TextArea readOnly autoSize={{ minRows: 5, maxRows: 5 }} placeholder={t("workflow_node.upload.form.private_key.placeholder")} />
</Form.Item>
<Form.Item>
<Upload beforeUpload={() => false} maxCount={1} onChange={handlePrivateKeyFileChange}>
<Button icon={<UploadOutlinedIcon />}>{t("workflow_node.upload.form.private_key.button")}</Button>
</Upload>
<TextFileInput
autoSize={{ minRows: 3, maxRows: 10 }}
placeholder={t("workflow_node.upload.form.private_key.placeholder")}
onChange={handlePrivateKeyChange}
/>
</Form.Item>
</Form>
);

View File

@ -230,7 +230,6 @@
"access.form.jdcloud_access_key_secret.tooltip": "For more information, see <a href=\"https://docs.jdcloud.com/en/account-management/accesskey-management\" target=\"_blank\">https://docs.jdcloud.com/en/account-management/accesskey-management</a>",
"access.form.k8s_kubeconfig.label": "KubeConfig",
"access.form.k8s_kubeconfig.placeholder": "Please enter KubeConfig file",
"access.form.k8s_kubeconfig.upload": "Choose File ...",
"access.form.k8s_kubeconfig.tooltip": "For more information, see <a href=\"https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/\" target=\"_blank\">https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/</a><br><br>Leave it blank to use the Pod's ServiceAccount.",
"access.form.larkbot_webhook_url.label": "Lark bot Webhook URL",
"access.form.larkbot_webhook_url.placeholder": "Please enter Lark bot Webhook URL",
@ -315,7 +314,6 @@
"access.form.ssh_password.tooltip": "Required when using password to connect to SSH.",
"access.form.ssh_key.label": "SSH key (Optional)",
"access.form.ssh_key.placeholder": "Please enter SSH key",
"access.form.ssh_key.upload": "Choose file ...",
"access.form.ssh_key.tooltip": "Required when using key to connect to SSH.",
"access.form.ssh_key_passphrase.label": "SSH key passphrase (Optional)",
"access.form.ssh_key_passphrase.placeholder": "Please enter SSH key passphrase",

View File

@ -11,6 +11,7 @@
"common.button.submit": "Submit",
"common.text.copied": "Copied",
"common.text.import_from_file": "Import from file ...",
"common.text.nodata": "No data available",
"common.text.operation_confirm": "Operation confirm",
"common.text.operation_succeeded": "Operation succeeded",

View File

@ -732,10 +732,8 @@
"workflow_node.upload.form.domains.placholder": "Please select certificate file",
"workflow_node.upload.form.certificate.label": "Certificate (PEM format)",
"workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
"workflow_node.upload.form.certificate.button": "Choose file ...",
"workflow_node.upload.form.private_key.label": "Private key (PEM format)",
"workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
"workflow_node.upload.form.private_key.button": "Choose file ...",
"workflow_node.notify.label": "Notification",
"workflow_node.notify.form.subject.label": "Subject",

View File

@ -223,8 +223,7 @@
"access.form.jdcloud_access_key_secret.placeholder": "请输入京东云 AccessKeySecret",
"access.form.jdcloud_access_key_secret.tooltip": "这是什么?请参阅 <a href=\"https://docs.jdcloud.com/cn/account-management/accesskey-management\" target=\"_blank\">https://docs.jdcloud.com/cn/account-management/accesskey-management</a>",
"access.form.k8s_kubeconfig.label": "KubeConfig",
"access.form.k8s_kubeconfig.placeholder": "请选择 KubeConfig 文件",
"access.form.k8s_kubeconfig.upload": "选择文件",
"access.form.k8s_kubeconfig.placeholder": "请输入 KubeConfig 文件内容",
"access.form.k8s_kubeconfig.tooltip": "这是什么?请参阅 <a href=\"https://kubernetes.io/zh-cn/docs/concepts/configuration/organize-cluster-access-kubeconfig/\" target=\"_blank\">https://kubernetes.io/zh-cn/docs/concepts/configuration/organize-cluster-access-kubeconfig/</a><br><br>为空时,将使用 Pod 的 ServiceAccount 作为凭证。",
"access.form.larkbot_webhook_url.label": "飞书群机器人 Webhook 地址",
"access.form.larkbot_webhook_url.placeholder": "请输入飞书群机器人 Webhook 地址",
@ -308,8 +307,7 @@
"access.form.ssh_password.placeholder": "请输入密码",
"access.form.ssh_password.tooltip": "使用密码连接到 SSH 时必填。<br>该字段与密钥文件字段二选一,如果同时填写优先使用 SSH 密钥登录。",
"access.form.ssh_key.label": "SSH 密钥(可选)",
"access.form.ssh_key.placeholder": "请输入 SSH 密钥文件",
"access.form.ssh_key.upload": "选择文件",
"access.form.ssh_key.placeholder": "请输入 SSH 密钥文件内容",
"access.form.ssh_key.tooltip": "使用 SSH 密钥连接到 SSH 时必填。<br>该字段与密码字段二选一,如果同时填写优先使用 SSH 密钥登录。",
"access.form.ssh_key_passphrase.label": "SSH 密钥口令(可选)",
"access.form.ssh_key_passphrase.placeholder": "请输入 SSH 密钥口令",

View File

@ -11,6 +11,7 @@
"common.button.submit": "提交",
"common.text.copied": "已复制",
"common.text.import_from_file": "从文件导入 ……",
"common.text.nodata": "暂无数据",
"common.text.operation_confirm": "操作确认",
"common.text.operation_succeeded": "操作成功",

View File

@ -731,10 +731,8 @@
"workflow_node.upload.form.domains.placeholder": "上传证书文件后显示",
"workflow_node.upload.form.certificate.label": "证书文件PEM 格式)",
"workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
"workflow_node.upload.form.certificate.button": "选择文件",
"workflow_node.upload.form.private_key.label": "私钥文件PEM 格式)",
"workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
"workflow_node.upload.form.private_key.button": "选择文件",
"workflow_node.notify.label": "推送通知",
"workflow_node.notify.form.subject.label": "通知主题",