diff --git a/ui/src/components/certificate/CertificateDetail.tsx b/ui/src/components/certificate/CertificateDetail.tsx index 94b4437a8..b8d502719 100644 --- a/ui/src/components/certificate/CertificateDetail.tsx +++ b/ui/src/components/certificate/CertificateDetail.tsx @@ -1,12 +1,12 @@ import { CopyToClipboard } from "react-copy-to-clipboard"; import { useTranslation } from "react-i18next"; -import { IconClipboard, IconDownload, IconThumbUp } from "@tabler/icons-react"; -import { App, Button, Dropdown, Form, Input, Tag, Tooltip } from "antd"; +import { IconClipboard, IconDownload } from "@tabler/icons-react"; +import { App, Button, Form, Input, Tag, Tooltip } from "antd"; import dayjs from "dayjs"; -import { saveAs } from "file-saver"; -import { download as downloadCertificate } from "@/api/certificates"; -import { CERTIFICATE_FORMATS, type CertificateFormatType, type CertificateModel } from "@/domain/certificate"; +import { type CertificateModel } from "@/domain/certificate"; + +import CertificateDownloadModal from "./CertificateDownloadModal"; export interface CertificateDetailProps { className?: string; @@ -19,19 +19,6 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => { const { message } = App.useApp(); - const handleDownloadClick = async (format: CertificateFormatType) => { - try { - const res = await downloadCertificate(data.id, format); - const bstr = atob(res.data.zipBytes); - const u8arr = Uint8Array.from(bstr, (ch) => ch.charCodeAt(0)); - const blob = new Blob([u8arr], { type: "application/zip" }); - saveAs(blob, `${data.id}-${data.subjectAltNames}.zip`); - } catch (err) { - console.error(err); - message.warning(t("common.text.operation_failed")); - } - }; - return (
@@ -94,33 +81,14 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
- , - onClick: () => handleDownloadClick(CERTIFICATE_FORMATS.PEM), - }, - { - key: "PFX", - label: "PFX", - onClick: () => handleDownloadClick(CERTIFICATE_FORMATS.PFX), - }, - { - key: "JKS", - label: "JKS", - onClick: () => handleDownloadClick(CERTIFICATE_FORMATS.JKS), - }, - ], - }} - trigger={["click", "hover"]} - > - - + } type="primary"> + {t("common.button.download")} + + } + />
); diff --git a/ui/src/components/certificate/CertificateDownloadModal.tsx b/ui/src/components/certificate/CertificateDownloadModal.tsx new file mode 100644 index 000000000..7eae8cb49 --- /dev/null +++ b/ui/src/components/certificate/CertificateDownloadModal.tsx @@ -0,0 +1,203 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"; +import { useControllableValue } from "ahooks"; +import { App, Button, Divider, Form, Input, Modal, Select, Table, type TableProps } from "antd"; +import { saveAs } from "file-saver"; + +import { download as downloadCertificate } from "@/api/certificates"; +import { CERTIFICATE_FORMATS, type CertificateFormatType, type CertificateModel } from "@/domain/certificate"; +import { useAntdForm, useTriggerElement } from "@/hooks"; + +export interface CertificateDownloadModalProps { + className?: string; + style?: React.CSSProperties; + afterClose?: () => void; + data: Pick; + open?: boolean; + trigger?: React.ReactNode; + onOpenChange?: (open: boolean) => void; +} + +const CertificateDownloadModal = ({ afterClose, data, trigger, ...props }: CertificateDownloadModalProps) => { + const { t } = useTranslation(); + + const { message } = App.useApp(); + + const [open, _setOpen] = useControllableValue(props, { + valuePropName: "open", + defaultValuePropName: "defaultOpen", + trigger: "onOpenChange", + }); + const setOpen = (open: boolean) => { + _setOpen(open); + setTableExpandedKeys([]); + formInst.resetFields(); + }; + + const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) }); + + const { form: formInst, formProps } = useAntdForm({ + name: "certficateDownloadAdvancedForm", + }); + + type TableRecord = { + format: CertificateFormatType; + advanced?: boolean; + advancedForm?: React.ReactNode; + }; + const tableColumns: TableProps["columns"] = [ + { + key: "format", + title: t("certificate.action.download.modal.form.format.label"), + render: (_, record) => ( +
+
{t(`certificate.action.download.modal.form.format.option.${record.format.toLowerCase()}.label`)}
+
{t(`certificate.action.download.modal.form.format.option.${record.format.toLowerCase()}.description`)}
+
+ ), + }, + { + key: "$action", + align: "end", + width: 32, + render: (_, record) => ( +
+ + + +
+ ), + }, + ]; + const tableData: TableRecord[] = [ + { + format: CERTIFICATE_FORMATS.PEM, + }, + { + format: CERTIFICATE_FORMATS.PFX, + advanced: true, + advancedForm: ( +
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+ + + +
+
+
+ ), + }, + ]; + const [tableExpandedKeys, setTableExpandedKeys] = useState([]); + + const handleCancelClick = () => {}; + + const handleDownloadClick = async (format: CertificateFormatType) => { + await formInst.validateFields(); + + try { + const res = await downloadCertificate(data.id, format, formInst.getFieldsValue()); + const bstr = atob(res.data.zipBytes); + const u8arr = Uint8Array.from(bstr, (ch) => ch.charCodeAt(0)); + const blob = new Blob([u8arr], { type: "application/zip" }); + saveAs(blob, `${data.subjectAltNames}_${data.id}_${format}.zip`.toLowerCase()); + + setOpen(false); + } catch (err) { + console.error(err); + message.warning(t("common.text.operation_failed")); + } + }; + + return ( + <> + {triggerEl} + + +
+ + columns={tableColumns} + dataSource={tableData} + expandable={{ + expandedRowKeys: tableExpandedKeys, + expandedRowRender: (record) => record.advancedForm, + rowExpandable: (record) => !!record.advanced, + showExpandColumn: false, + }} + rowHoverable={false} + pagination={false} + rowKey={(record) => record.format} + size="medium" + /> +
+
+ + ); +}; + +export default CertificateDownloadModal; diff --git a/ui/src/components/workflow/WorkflowRunDetail.tsx b/ui/src/components/workflow/WorkflowRunDetail.tsx index 37b98db92..318787a0b 100644 --- a/ui/src/components/workflow/WorkflowRunDetail.tsx +++ b/ui/src/components/workflow/WorkflowRunDetail.tsx @@ -412,7 +412,7 @@ const WorkflowRunArtifacts = ({ runData }: { runData: WorkflowRunModel }) => { { key: "$action", align: "end", - width: 32, + width: 64, render: (_, record) => (
{ }} pagination={false} rowKey={(record) => record.id} - size="small" + size="medium" /> ); }; diff --git a/ui/src/i18n/resources/en/nls.certificate.json b/ui/src/i18n/resources/en/nls.certificate.json index 552bffc66..7a0c2eb0e 100644 --- a/ui/src/i18n/resources/en/nls.certificate.json +++ b/ui/src/i18n/resources/en/nls.certificate.json @@ -30,6 +30,65 @@ "batch_delete.modal": { "title": "Delete certificates", "content": "Are you sure want to delete these {{count}} selected certificates?
This action cannot be undone." + }, + "download.modal": { + "title": "Download certificate", + "form": { + "format": { + "label": "Format", + "option": { + "pem": { + "label": "PEM (*.pem, *.crt, *.key)", + "description": "Suitable for most scenarios. Supported by servers like Apache, Nginx." + }, + "pfx": { + "label": "PFX (*.pfx, *.p12)", + "description": "Supported by servers like IIS." + }, + "jks": { + "label": "JKS (*.jks)", + "description": "Supported by servers like Tomcat." + } + } + }, + "pfx_password": { + "label": "PFX Password (Optional)", + "placeholder": "Please enter PFX password" + }, + "pfx_encoder": { + "label": "PFX Encoder (Optional)", + "placeholder": "Please select PFX encoder", + "option": { + "legacyrc2": { + "label": "LegacyRC2 (RC2, compatible with older systems, but less secure)" + }, + "legacydes": { + "label": "LegacyDES (3DES + SHA-1, default)" + }, + "modern2023": { + "label": "Modern2023 (AES-256-CBC + SHA-256 MAC, compatible with OpenSSL v1.1.1+)" + }, + "modern2026": { + "label": "Modern2026 (AES-256-CBC + PBMAC1 MAC, compatible with OpenSSL v3.4.0+)" + } + } + }, + "jks_alias": { + "label": "JKS Alias (Optional)", + "placeholder": "Please enter JKS alias" + }, + "jks_keypass": { + "label": "JKS Key password (Optional)", + "placeholder": "Please enter JKS key password" + }, + "jks_storepass": { + "label": "JKS Store password (Optional)", + "placeholder": "Please enter JKS store password" + } + }, + "action": { + "advanced.button": "Advanced" + } } }, diff --git a/ui/src/i18n/resources/zh/nls.certificate.json b/ui/src/i18n/resources/zh/nls.certificate.json index a23a7a4fe..eba234321 100644 --- a/ui/src/i18n/resources/zh/nls.certificate.json +++ b/ui/src/i18n/resources/zh/nls.certificate.json @@ -30,6 +30,65 @@ "batch_delete.modal": { "title": "删除证书", "content": "确定要删除这 {{count}} 个被选中的证书吗?
注意此操作不可撤销,请谨慎操作。" + }, + "download.modal": { + "title": "下载证书", + "form": { + "format": { + "label": "格式", + "option": { + "pem": { + "label": "PEM 格式(*.pem, *.crt, *.key)", + "description": "适用于大部分场景。Apache、Nginx 等服务器支持此格式。" + }, + "pfx": { + "label": "PFX 格式(*.pfx, *.p12)", + "description": "IIS 等服务器支持此格式。" + }, + "jks": { + "label": "JKS 格式(*.jks)", + "description": "Tomcat 等服务器支持此格式。" + } + } + }, + "pfx_password": { + "label": "PFX 密码(可选)", + "placeholder": "请输入 PFX 导出密码" + }, + "pfx_encoder": { + "label": "PFX 编码器(可选)", + "placeholder": "请选择 PFX 编码器", + "option": { + "legacyrc2": { + "label": "LegacyRC2(RC2,兼容旧系统,但安全性较低)" + }, + "legacydes": { + "label": "LegacyDES(3DES + SHA-1,默认值)" + }, + "modern2023": { + "label": "Modern2023(AES-256-CBC + SHA-256 MAC,适配 OpenSSL v1.1.1+)" + }, + "modern2026": { + "label": "Modern2026(AES-256-CBC + PBMAC1 MAC,适配 OpenSSL v3.4.0+)" + } + } + }, + "jks_alias": { + "label": "JKS 别名(可选)", + "placeholder": "请输入 JKS 别名" + }, + "jks_keypass": { + "label": "JKS 私钥访问口令(可选)", + "placeholder": "请输入 JKS 私钥访问口令" + }, + "jks_storepass": { + "label": "JKS 密钥库存储口令(可选)", + "placeholder": "请输入 JKS 密钥库存储口令" + } + }, + "action": { + "advanced.button": "高级" + } } },