From 04abf9dd76e074f6e6faf34f250ea936d7d63553 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 14 May 2025 00:40:18 +0800 Subject: [PATCH] feat(ui): TextFileInput --- ui/src/components/TextFileInput.tsx | 51 +++++++ .../access/AccessFormKubernetesConfig.tsx | 33 +---- .../components/access/AccessFormSSHConfig.tsx | 90 ++++--------- .../workflow/node/NotifyNodeConfigForm.tsx | 2 +- .../workflow/node/UploadNodeConfigForm.tsx | 125 ++++++++---------- ui/src/i18n/locales/en/nls.access.json | 2 - ui/src/i18n/locales/en/nls.common.json | 1 + .../i18n/locales/en/nls.workflow.nodes.json | 2 - ui/src/i18n/locales/zh/nls.access.json | 6 +- ui/src/i18n/locales/zh/nls.common.json | 1 + .../i18n/locales/zh/nls.workflow.nodes.json | 2 - 11 files changed, 144 insertions(+), 171 deletions(-) create mode 100644 ui/src/components/TextFileInput.tsx diff --git a/ui/src/components/TextFileInput.tsx b/ui/src/components/TextFileInput.tsx new file mode 100644 index 00000000..bae83c56 --- /dev/null +++ b/ui/src/components/TextFileInput.tsx @@ -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 { + accept?: UploadProps["accept"]; + uploadButtonProps?: Omit; + uploadText?: string; + onChange?: (value: string) => void; +} + +const TextFileInput = ({ className, style, accept, disabled, readOnly, uploadText, uploadButtonProps, onChange, ...props }: TextFileInputProps) => { + const { t } = useTranslation(); + + const fileInputRef = useRef(null); + + const handleButtonClick = () => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + const handleFileChange = async (e: ChangeEvent) => { + const { files } = e.target as HTMLInputElement; + if (files?.length) { + const value = await readFileContent(files[0]); + onChange?.(value); + } + }; + + return ( + + onChange?.(e.target.value)} /> + {!readOnly && ( + <> + + + + )} + + ); +}; + +export default TextFileInput; diff --git a/ui/src/components/access/AccessFormKubernetesConfig.tsx b/ui/src/components/access/AccessFormKubernetesConfig.tsx index bfb93ce4..73415bb1 100644 --- a/ui/src/components/access/AccessFormKubernetesConfig.tsx +++ b/ui/src/components/access/AccessFormKubernetesConfig.tsx @@ -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; @@ -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([]); - 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) => { onValuesChange?.(values); }; @@ -65,16 +45,13 @@ const AccessFormKubernetesConfig = ({ form: formInst, formName, disabled, initia name={formName} onValuesChange={handleFormChange} > - - } > - false} fileList={fieldKubeFileList} maxCount={1} onChange={handleKubeFileChange}> - - + ); diff --git a/ui/src/components/access/AccessFormSSHConfig.tsx b/ui/src/components/access/AccessFormSSHConfig.tsx index ebaeba90..db1790a4 100644 --- a/ui/src/components/access/AccessFormSSHConfig.tsx +++ b/ui/src/components/access/AccessFormSSHConfig.tsx @@ -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; @@ -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([]); - 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) => { onValuesChange?.(values); }; @@ -104,48 +84,36 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues -
-
- - - -
+ + + -
- } - > - - -
-
+ } + > + + -
-
- - - }> - false} fileList={fieldKeyFileList} maxCount={1} onChange={handleKeyFileChange}> - - - -
+ } + > + + -
- } - > - - -
-
+ } + > + + ); }; diff --git a/ui/src/components/workflow/node/NotifyNodeConfigForm.tsx b/ui/src/components/workflow/node/NotifyNodeConfigForm.tsx index dfc5b805..32488aeb 100644 --- a/ui/src/components/workflow/node/NotifyNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/NotifyNodeConfigForm.tsx @@ -177,7 +177,7 @@ const NotifyNodeConfigForm = forwardRef - + diff --git a/ui/src/components/workflow/node/UploadNodeConfigForm.tsx b/ui/src/components/workflow/node/UploadNodeConfigForm.tsx index d2ec323b..5dc16b0b 100644 --- a/ui/src/components/workflow/node/UploadNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/UploadNodeConfigForm.tsx @@ -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; @@ -70,65 +69,53 @@ const UploadNodeConfigForm = forwardRef { - if (file && file.status !== "removed") { - const certificate = await readFileContent(file.originFileObj ?? (file as unknown as File)); - - try { - const resp = await validateCertificate(certificate); - formInst.setFields([ - { - name: "domains", - value: resp.data.domains, - }, - { - name: "certificate", - value: certificate, - }, - ]); - } catch (e) { - formInst.setFields([ - { - name: "domains", - value: "", - }, - { - name: "certificate", - value: "", - errors: [getErrMsg(e)], - }, - ]); - } - } else { - formInst.setFieldValue("certificate", ""); + const handleCertificateChange = async (value: string) => { + try { + const resp = await validateCertificate(value); + formInst.setFields([ + { + name: "domains", + value: resp.data.domains, + }, + { + name: "certificate", + value: value, + }, + ]); + } catch (e) { + formInst.setFields([ + { + name: "domains", + value: "", + }, + { + name: "certificate", + value: value, + errors: [getErrMsg(e)], + }, + ]); } 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)); - - try { - await validatePrivateKey(privateKey); - formInst.setFields([ - { - name: "privateKey", - value: privateKey, - }, - ]); - } catch (e) { - formInst.setFields([ - { - name: "privateKey", - value: "", - errors: [getErrMsg(e)], - }, - ]); - } - } else { - formInst.setFieldValue("privateKey", ""); + const handlePrivateKeyChange = async (value: string) => { + try { + await validatePrivateKey(value); + formInst.setFields([ + { + name: "privateKey", + value: value, + }, + ]); + } catch (e) { + formInst.setFields([ + { + name: "privateKey", + value: value, + errors: [getErrMsg(e)], + }, + ]); } onValuesChange?.(formInst.getFieldsValue(true)); @@ -141,23 +128,19 @@ const UploadNodeConfigForm = forwardRef - - - - - false} maxCount={1} onChange={handleCertificateFileChange}> - - + - - - - - false} maxCount={1} onChange={handlePrivateKeyFileChange}> - - + ); diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index 33130d4a..63bed525 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -230,7 +230,6 @@ "access.form.jdcloud_access_key_secret.tooltip": "For more information, see https://docs.jdcloud.com/en/account-management/accesskey-management", "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 https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/

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", diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json index 32241963..ea5ab95a 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -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", diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 69794fbd..e8296302 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -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", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index ade8b3da..75be275e 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -223,8 +223,7 @@ "access.form.jdcloud_access_key_secret.placeholder": "请输入京东云 AccessKeySecret", "access.form.jdcloud_access_key_secret.tooltip": "这是什么?请参阅 https://docs.jdcloud.com/cn/account-management/accesskey-management", "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": "这是什么?请参阅 https://kubernetes.io/zh-cn/docs/concepts/configuration/organize-cluster-access-kubeconfig/

为空时,将使用 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 时必填。
该字段与密钥文件字段二选一,如果同时填写优先使用 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 时必填。
该字段与密码字段二选一,如果同时填写优先使用 SSH 密钥登录。", "access.form.ssh_key_passphrase.label": "SSH 密钥口令(可选)", "access.form.ssh_key_passphrase.placeholder": "请输入 SSH 密钥口令", diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index 8458c5b5..bb683d4c 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -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": "操作成功", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index f6f01dc9..acbad61b 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -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": "通知主题",