From 1ce828264845468b12a8d3986f6e1c7613b844f5 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 9 Sep 2025 09:38:56 +0800 Subject: [PATCH 1/7] feat(ui): app loader --- ui/index.html | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/ui/index.html b/ui/index.html index ba75b047..f056f604 100644 --- a/ui/index.html +++ b/ui/index.html @@ -4,10 +4,63 @@ + Certimate - Your Trusted Partner in SSL Automation -
+
+
+ +
From 586a11f91c8b5525a6aea1ebd9246bf05928f8d1 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 9 Sep 2025 09:39:09 +0800 Subject: [PATCH 2/7] chore(ui): improve i18n --- .../access/forms/AccessConfigFieldsProviderBaotaPanel.tsx | 6 +++--- .../access/forms/AccessConfigFieldsProviderPowerDNS.tsx | 6 +++--- .../access/forms/AccessConfigFieldsProviderRatPanel.tsx | 6 +++--- .../access/forms/AccessConfigFieldsProviderSafeLine.tsx | 6 +++--- .../access/forms/AccessConfigFieldsProviderWebhook.tsx | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderBaotaPanel.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderBaotaPanel.tsx index 67802dd6..87e6ad76 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderBaotaPanel.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderBaotaPanel.tsx @@ -40,12 +40,12 @@ const AccessConfigFormFieldsProviderBaotaPanel = () => { diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderPowerDNS.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderPowerDNS.tsx index 37d0d8cc..b7bbbfe6 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderPowerDNS.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderPowerDNS.tsx @@ -39,12 +39,12 @@ const AccessConfigFormFieldsProviderPowerDNS = () => { diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderRatPanel.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderRatPanel.tsx index 675617b4..03c2597b 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderRatPanel.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderRatPanel.tsx @@ -50,12 +50,12 @@ const AccessConfigFormFieldsProviderRatPanel = () => { diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderSafeLine.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderSafeLine.tsx index 815b2f63..7b1970b2 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderSafeLine.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderSafeLine.tsx @@ -39,12 +39,12 @@ const AccessConfigFormFieldsProviderSafeLine = () => { diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderWebhook.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderWebhook.tsx index 4079dc72..381f671f 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderWebhook.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderWebhook.tsx @@ -300,12 +300,12 @@ const AccessConfigFormFieldsProviderWebhook = ({ usage = "none" }: AccessConfigF From 1bb6da46f151ff7e57873ab95f0bf14adbfead79 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 9 Sep 2025 10:03:58 +0800 Subject: [PATCH 3/7] feat: workflow dispatcher compensation mechanism --- internal/workflow/dispatcher/dispatcher.go | 25 ++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/internal/workflow/dispatcher/dispatcher.go b/internal/workflow/dispatcher/dispatcher.go index b53189e6..7345f94f 100644 --- a/internal/workflow/dispatcher/dispatcher.go +++ b/internal/workflow/dispatcher/dispatcher.go @@ -101,6 +101,26 @@ func (wd *workflowDispatcher) Bootup(ctx context.Context) error { } wd.booted = true + + ticker := time.NewTicker(1 * time.Minute) + go func() { + defer ticker.Stop() + + for { + select { + case <-ticker.C: + // 无需准确获取,不用加锁 + if len(wd.processingTasks) < wd.concurrency && len(wd.pendingRunQueue) > 0 { + wd.tryNextAsync() + } + default: + if !wd.booted { + return + } + } + } + }() + return nil } @@ -305,9 +325,9 @@ func (wd *workflowDispatcher) tryExecuteAsync(task *taskInfo) { }) // 执行工作流 - wd.syslog.Info(fmt.Sprintf("workflow run #%s was started", task.RunId)) + wd.syslog.Info(fmt.Sprintf("workflow run #%s (work#%s) was started", task.RunId, task.WorkflowId)) we.Invoke(task.ctx, workflowRun.WorkflowId, workflowRun.Id, workflowRun.Graph) - wd.syslog.Info(fmt.Sprintf("workflow run #%s was stopped", task.RunId)) + wd.syslog.Info(fmt.Sprintf("workflow run #%s (work#%s) was stopped", task.RunId, task.WorkflowId)) } func (wd *workflowDispatcher) tryNextAsync() { @@ -341,6 +361,7 @@ func (wd *workflowDispatcher) tryNextAsync() { task := &taskInfo{WorkflowId: workflowRun.WorkflowId, RunId: workflowRun.Id, ctx: ctxRun, cancel: ctxCancel} wd.pendingRunQueue = append(wd.pendingRunQueue[:i], wd.pendingRunQueue[i+1:]...) wd.processingTasks[pendingRunId] = task + wd.syslog.Info(fmt.Sprintf("workflow run #%s (work#%s) is being dispatched ...", task.RunId, task.WorkflowId)) go func() { wd.tryExecuteAsync(task) }() return } From 114b89f26eefd65869586d61e08a7224c1c2edfa Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 9 Sep 2025 10:44:07 +0800 Subject: [PATCH 4/7] Update CONTRIBUTING.md --- CONTRIBUTING.md | 6 ++++++ CONTRIBUTING_EN.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b913fa48..674363b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,11 @@ # 贡献指南 +
+ +中文 | [English](CONTRIBUTING_EN.md) + +
+ 非常感谢你抽出时间来帮助改进 Certimate!以下是向 Certimate 提交 Pull Request 时的操作指南。 我们需要保持敏捷和快速迭代,同时也希望确保贡献者能获得尽可能流畅的参与体验。这份贡献指南旨在帮助你熟悉代码库和我们的工作方式,让你可以尽快进入有趣的开发环节。 diff --git a/CONTRIBUTING_EN.md b/CONTRIBUTING_EN.md index 61a575b6..b5837867 100644 --- a/CONTRIBUTING_EN.md +++ b/CONTRIBUTING_EN.md @@ -1,5 +1,11 @@ # Contribution Guide +
+ +[中文](CONTRIBUTING.md) | English + +
+ Thank you for taking the time to improve Certimate! Below is a guide for submitting a PR (Pull Request) to the Certimate repository. We need to be nimble and ship fast given where we are, but we also want to make sure that contributors like you get as smooth an experience at contributing as possible. We've assembled this contribution guide for that purpose, aiming at getting you familiarized with the codebase & how we work with contributors, so you could quickly jump to the fun part. From 1d106f5575844951606a04b6faffd5eaaac98a16 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 9 Sep 2025 13:56:07 +0800 Subject: [PATCH 5/7] refactor: clean code --- internal/certdeploy/deployers/sp_ssh.go | 2 +- .../acme-http01/providers/ssh/ssh.go | 44 +------------- pkg/core/ssl-deployer/providers/ssh/ssh.go | 58 ++++-------------- pkg/utils/ssh/client.go | 59 +++++++++++++++++++ 4 files changed, 73 insertions(+), 90 deletions(-) create mode 100644 pkg/utils/ssh/client.go diff --git a/internal/certdeploy/deployers/sp_ssh.go b/internal/certdeploy/deployers/sp_ssh.go index e939e6f1..d2591905 100644 --- a/internal/certdeploy/deployers/sp_ssh.go +++ b/internal/certdeploy/deployers/sp_ssh.go @@ -44,10 +44,10 @@ func init() { PreCommand: xmaps.GetString(options.ProviderExtendedConfig, "preCommand"), PostCommand: xmaps.GetString(options.ProviderExtendedConfig, "postCommand"), OutputFormat: ssh.OutputFormatType(xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "format", string(ssh.OUTPUT_FORMAT_PEM))), + OutputKeyPath: xmaps.GetString(options.ProviderExtendedConfig, "keyPath"), OutputCertPath: xmaps.GetString(options.ProviderExtendedConfig, "certPath"), OutputServerCertPath: xmaps.GetString(options.ProviderExtendedConfig, "certPathForServerOnly"), OutputIntermediaCertPath: xmaps.GetString(options.ProviderExtendedConfig, "certPathForIntermediaOnly"), - OutputKeyPath: xmaps.GetString(options.ProviderExtendedConfig, "keyPath"), PfxPassword: xmaps.GetString(options.ProviderExtendedConfig, "pfxPassword"), JksAlias: xmaps.GetString(options.ProviderExtendedConfig, "jksAlias"), JksKeypass: xmaps.GetString(options.ProviderExtendedConfig, "jksKeypass"), diff --git a/pkg/core/ssl-applicator/acme-http01/providers/ssh/ssh.go b/pkg/core/ssl-applicator/acme-http01/providers/ssh/ssh.go index 9863d6aa..d23055ab 100644 --- a/pkg/core/ssl-applicator/acme-http01/providers/ssh/ssh.go +++ b/pkg/core/ssl-applicator/acme-http01/providers/ssh/ssh.go @@ -6,7 +6,6 @@ import ( "net" "path/filepath" "strconv" - "strings" "github.com/go-acme/lego/v4/challenge/http01" "golang.org/x/crypto/ssh" @@ -239,54 +238,17 @@ func (p *provider) createSshClient(conn net.Conn, host string, port int32, authM } } - authentications := make([]ssh.AuthMethod, 0) switch authMethod { case AUTH_METHOD_NONE: - { - } + return xssh.NewClient(conn, host, int(port), username) case AUTH_METHOD_PASSWORD: - { - authentications = append(authentications, ssh.Password(password)) - authentications = append(authentications, ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) { - if len(questions) == 1 { - return []string{password}, nil - } - return nil, fmt.Errorf("unexpected keyboard interactive question [%s]", strings.Join(questions, ", ")) - })) - } + return xssh.NewClientWithPassword(conn, host, int(port), username, password) case AUTH_METHOD_KEY: - { - var signer ssh.Signer - var err error - - if keyPassphrase != "" { - signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(key), []byte(keyPassphrase)) - } else { - signer, err = ssh.ParsePrivateKey([]byte(key)) - } - - if err != nil { - return nil, err - } - - authentications = append(authentications, ssh.PublicKeys(signer)) - } + return xssh.NewClientWithKey(conn, host, int(port), username, key, keyPassphrase) default: return nil, fmt.Errorf("unsupported auth method '%s'", authMethod) } - - addr := net.JoinHostPort(host, strconv.Itoa(int(port))) - sshConn, chans, reqs, err := ssh.NewClientConn(conn, addr, &ssh.ClientConfig{ - User: username, - Auth: authentications, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - }) - if err != nil { - return nil, err - } - - return ssh.NewClient(sshConn, chans, reqs), nil } diff --git a/pkg/core/ssl-deployer/providers/ssh/ssh.go b/pkg/core/ssl-deployer/providers/ssh/ssh.go index 666cc6c7..3ff297f2 100644 --- a/pkg/core/ssl-deployer/providers/ssh/ssh.go +++ b/pkg/core/ssl-deployer/providers/ssh/ssh.go @@ -8,7 +8,6 @@ import ( "log/slog" "net" "strconv" - "strings" "golang.org/x/crypto/ssh" @@ -52,6 +51,8 @@ type SSLDeployerProviderConfig struct { PostCommand string `json:"postCommand,omitempty"` // 输出证书格式。 OutputFormat OutputFormatType `json:"outputFormat,omitempty"` + // 输出私钥文件路径。 + OutputKeyPath string `json:"outputKeyPath,omitempty"` // 输出证书文件路径。 OutputCertPath string `json:"outputCertPath,omitempty"` // 输出服务器证书文件路径。 @@ -60,8 +61,6 @@ type SSLDeployerProviderConfig struct { // 输出中间证书文件路径。 // 选填。 OutputIntermediaCertPath string `json:"outputIntermediaCertPath,omitempty"` - // 输出私钥文件路径。 - OutputKeyPath string `json:"outputKeyPath,omitempty"` // PFX 导出密码。 // 证书格式为 PFX 时必填。 PfxPassword string `json:"pfxPassword,omitempty"` @@ -192,6 +191,11 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke // 上传证书和私钥文件 switch d.config.OutputFormat { case OUTPUT_FORMAT_PEM: + if err := xssh.WriteRemoteString(client, d.config.OutputKeyPath, privkeyPEM, d.config.UseSCP); err != nil { + return nil, fmt.Errorf("failed to upload private key file: %w", err) + } + d.logger.Info("ssl private key file uploaded", slog.String("path", d.config.OutputKeyPath)) + if err := xssh.WriteRemoteString(client, d.config.OutputCertPath, certPEM, d.config.UseSCP); err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } @@ -211,11 +215,6 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke d.logger.Info("ssl intermedia certificate file uploaded", slog.String("path", d.config.OutputIntermediaCertPath)) } - if err := xssh.WriteRemoteString(client, d.config.OutputKeyPath, privkeyPEM, d.config.UseSCP); err != nil { - return nil, fmt.Errorf("failed to upload private key file: %w", err) - } - d.logger.Info("ssl private key file uploaded", slog.String("path", d.config.OutputKeyPath)) - case OUTPUT_FORMAT_PFX: pfxData, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, d.config.PfxPassword) if err != nil { @@ -282,56 +281,19 @@ func createSshClient(conn net.Conn, host string, port int32, authMethod string, } } - authentications := make([]ssh.AuthMethod, 0) switch authMethod { case AUTH_METHOD_NONE: - { - } + return xssh.NewClient(conn, host, int(port), username) case AUTH_METHOD_PASSWORD: - { - authentications = append(authentications, ssh.Password(password)) - authentications = append(authentications, ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) { - if len(questions) == 1 { - return []string{password}, nil - } - return nil, fmt.Errorf("unexpected keyboard interactive question [%s]", strings.Join(questions, ", ")) - })) - } + return xssh.NewClientWithPassword(conn, host, int(port), username, password) case AUTH_METHOD_KEY: - { - var signer ssh.Signer - var err error - - if keyPassphrase != "" { - signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(key), []byte(keyPassphrase)) - } else { - signer, err = ssh.ParsePrivateKey([]byte(key)) - } - - if err != nil { - return nil, err - } - - authentications = append(authentications, ssh.PublicKeys(signer)) - } + return xssh.NewClientWithKey(conn, host, int(port), username, key, keyPassphrase) default: return nil, fmt.Errorf("unsupported auth method '%s'", authMethod) } - - addr := net.JoinHostPort(host, strconv.Itoa(int(port))) - sshConn, chans, reqs, err := ssh.NewClientConn(conn, addr, &ssh.ClientConfig{ - User: username, - Auth: authentications, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - }) - if err != nil { - return nil, err - } - - return ssh.NewClient(sshConn, chans, reqs), nil } func execSshCommand(sshCli *ssh.Client, command string) (string, string, error) { diff --git a/pkg/utils/ssh/client.go b/pkg/utils/ssh/client.go new file mode 100644 index 00000000..af5189b7 --- /dev/null +++ b/pkg/utils/ssh/client.go @@ -0,0 +1,59 @@ +package ssh + +import ( + "fmt" + "net" + "strconv" + "strings" + + "golang.org/x/crypto/ssh" +) + +func NewClient(conn net.Conn, host string, port int, username string) (*ssh.Client, error) { + authentications := make([]ssh.AuthMethod, 0) + return newClientWithAuthMethods(conn, host, port, username, authentications) +} + +func NewClientWithPassword(conn net.Conn, host string, port int, username string, password string) (*ssh.Client, error) { + authentications := make([]ssh.AuthMethod, 0) + authentications = append(authentications, ssh.Password(password)) + authentications = append(authentications, ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) { + if len(questions) == 1 { + return []string{password}, nil + } + return nil, fmt.Errorf("unexpected keyboard interactive question [%s]", strings.Join(questions, ", ")) + })) + return newClientWithAuthMethods(conn, host, port, username, authentications) +} + +func NewClientWithKey(conn net.Conn, host string, port int, username string, key, keyPassphrase string) (*ssh.Client, error) { + var signer ssh.Signer + var err error + if keyPassphrase != "" { + signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(key), []byte(keyPassphrase)) + } else { + signer, err = ssh.ParsePrivateKey([]byte(key)) + } + if err != nil { + return nil, err + } + + authentications := make([]ssh.AuthMethod, 0) + authentications = append(authentications, ssh.PublicKeys(signer)) + return newClientWithAuthMethods(conn, host, port, username, authentications) +} + +func newClientWithAuthMethods(conn net.Conn, host string, port int, username string, authMethods []ssh.AuthMethod) (*ssh.Client, error) { + addr := net.JoinHostPort(host, strconv.Itoa(int(port))) + + sshConn, chans, reqs, err := ssh.NewClientConn(conn, addr, &ssh.ClientConfig{ + User: username, + Auth: authMethods, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + }) + if err != nil { + return nil, err + } + + return ssh.NewClient(sshConn, chans, reqs), nil +} From 2be911840f47032f8270b106f83c2dc6a994045d Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 9 Sep 2025 21:28:52 +0800 Subject: [PATCH 6/7] chore(ui): improve i18n --- ui/src/components/CodeInput.tsx | 14 ++++-- ui/src/components/TextFileInput.tsx | 10 ++-- .../AccessConfigFieldsProviderACMECA.tsx | 9 +--- .../AccessConfigFieldsProviderActalisSSL.tsx | 25 +++++----- ...essConfigFieldsProviderGlobalSignAtlas.tsx | 25 +++++----- ...onfigFieldsProviderGoogleTrustServices.tsx | 25 +++++----- .../AccessConfigFieldsProviderSSLCom.tsx | 25 +++++----- .../AccessConfigFieldsProviderSectigo.tsx | 25 +++++----- .../AccessConfigFieldsProviderZeroSSL.tsx | 25 +++++----- ui/src/i18n/locales/en/nls.access.json | 46 ++++--------------- ui/src/i18n/locales/en/nls.workflow.json | 10 ++-- ui/src/i18n/locales/zh/nls.access.json | 46 ++++--------------- ui/src/i18n/locales/zh/nls.workflow.json | 10 ++-- ui/src/pages/settings/SettingsSSLProvider.tsx | 46 +++++++++---------- ui/src/pages/workflows/WorkflowDetail.tsx | 8 ++-- ui/src/pages/workflows/WorkflowList.tsx | 10 ++-- 16 files changed, 150 insertions(+), 209 deletions(-) diff --git a/ui/src/components/CodeInput.tsx b/ui/src/components/CodeInput.tsx index 9064bf7e..4220e414 100644 --- a/ui/src/components/CodeInput.tsx +++ b/ui/src/components/CodeInput.tsx @@ -1,4 +1,4 @@ -import { useMemo, useRef } from "react"; +import { useContext, useMemo, useRef } from "react"; import { json } from "@codemirror/lang-json"; import { yaml } from "@codemirror/lang-yaml"; import { StreamLanguage } from "@codemirror/language"; @@ -9,6 +9,7 @@ import { vscodeDark, vscodeLight } from "@uiw/codemirror-theme-vscode"; import CodeMirror, { type ReactCodeMirrorProps, type ReactCodeMirrorRef } from "@uiw/react-codemirror"; import { useFocusWithin, useHover } from "ahooks"; import { theme } from "antd"; +import DisabledContext from "antd/es/config-provider/DisabledContext"; import { useBrowserTheme } from "@/hooks"; import { mergeCls } from "@/utils/css"; @@ -16,13 +17,17 @@ import { mergeCls } from "@/utils/css"; export interface CodeInputProps extends Omit { disabled?: boolean; language?: string | string[]; + readOnly?: boolean; } -const CodeInput = ({ className, style, disabled, language, ...props }: CodeInputProps) => { +const CodeInput = ({ className, style, disabled, language, readOnly, ...props }: CodeInputProps) => { const { token: themeToken } = theme.useToken(); const { theme: browserTheme } = useBrowserTheme(); + const injectedDisabled = useContext(DisabledContext); + const mergedDisabled = disabled ?? injectedDisabled; + const cmRef = useRef(null); const isFocusing = useFocusWithin(cmRef.current?.editor); const isHovering = useHover(cmRef.current?.editor); @@ -74,8 +79,8 @@ const CodeInput = ({ className, style, disabled, language, ...props }: CodeInput paddingInline: themeToken.Input?.paddingInline, fontSize: themeToken.Input?.inputFontSize, lineHeight: themeToken.lineHeight, - color: disabled ? themeToken.colorTextDisabled : themeToken.colorText, - backgroundColor: disabled + color: mergedDisabled ? themeToken.colorTextDisabled : themeToken.colorText, + backgroundColor: mergedDisabled ? themeToken.colorBgContainerDisabled : isFocusing ? (themeToken.Input?.activeBg ?? themeToken.colorBgContainer) @@ -106,6 +111,7 @@ const CodeInput = ({ className, style, disabled, language, ...props }: CodeInput indentOnInput: false, }} extensions={cmExtensions} + readOnly={readOnly || mergedDisabled} theme={cmTheme} /> diff --git a/ui/src/components/TextFileInput.tsx b/ui/src/components/TextFileInput.tsx index 61fcda2a..5e68df26 100644 --- a/ui/src/components/TextFileInput.tsx +++ b/ui/src/components/TextFileInput.tsx @@ -1,7 +1,8 @@ -import { type ChangeEvent, useRef } from "react"; +import { type ChangeEvent, useContext, useRef } from "react"; import { useTranslation } from "react-i18next"; import { IconFileImport } from "@tabler/icons-react"; import { Button, type ButtonProps, Input, type UploadProps } from "antd"; +import DisabledContext from "antd/es/config-provider/DisabledContext"; import { type TextAreaProps } from "antd/es/input/TextArea"; import { readFileAsText } from "@/utils/file"; @@ -16,6 +17,9 @@ export interface TextFileInputProps extends Omit { const TextFileInput = ({ className, style, accept, disabled, readOnly, uploadText, uploadButtonProps, onChange, ...props }: TextFileInputProps) => { const { t } = useTranslation(); + const injectedDisabled = useContext(DisabledContext); + const mergedDisabled = disabled ?? injectedDisabled; + const fileInputRef = useRef(null); const handleButtonClick = () => { @@ -35,10 +39,10 @@ const TextFileInput = ({ className, style, accept, disabled, readOnly, uploadTex return (
- onChange?.(e.target.value)} /> + onChange?.(e.target.value)} /> {!readOnly && ( <> - diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderACMECA.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderACMECA.tsx index 22523232..46631ae5 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderACMECA.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderACMECA.tsx @@ -27,13 +27,7 @@ const AccessConfigFormFieldsProviderACMECA = () => { - } - > + @@ -42,7 +36,6 @@ const AccessConfigFormFieldsProviderACMECA = () => { initialValue={initialValues.eabHmacKey} label={t("access.form.acmeca_eab_hmac_key.label")} rules={[formRule]} - tooltip={} > diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderActalisSSL.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderActalisSSL.tsx index e0d95932..5d0438f1 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderActalisSSL.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderActalisSSL.tsx @@ -3,6 +3,8 @@ import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import Tips from "@/components/Tips"; + import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderActalisSSL = () => { @@ -17,24 +19,21 @@ const AccessConfigFormFieldsProviderActalisSSL = () => { return ( <> - } - > - + + } > - + + + + + } /> ); @@ -53,11 +52,11 @@ const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) = return z.object({ eabKid: z .string() - .min(1, t("access.form.actalisssl_eab_kid.placeholder")) + .min(1, t("access.form.shared_acme_eab_kid.placeholder")) .max(256, t("common.errmsg.string_max", { max: 256 })), eabHmacKey: z .string() - .min(1, t("access.form.actalisssl_eab_hmac_key.placeholder")) + .min(1, t("access.form.shared_acme_eab_hmac_key.placeholder")) .max(256, t("common.errmsg.string_max", { max: 256 })), }); }; diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderGlobalSignAtlas.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderGlobalSignAtlas.tsx index 076d28b8..68c75d8e 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderGlobalSignAtlas.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderGlobalSignAtlas.tsx @@ -3,6 +3,8 @@ import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import Tips from "@/components/Tips"; + import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderGobalSignAtlas = () => { @@ -17,24 +19,21 @@ const AccessConfigFormFieldsProviderGobalSignAtlas = () => { return ( <> - } - > - + + } > - + + + + + } /> ); @@ -53,11 +52,11 @@ const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) = return z.object({ eabKid: z .string() - .min(1, t("access.form.globalsignatlas_eab_kid.placeholder")) + .min(1, t("access.form.shared_acme_eab_kid.placeholder")) .max(256, t("common.errmsg.string_max", { max: 256 })), eabHmacKey: z .string() - .min(1, t("access.form.globalsignatlas_eab_hmac_key.placeholder")) + .min(1, t("access.form.shared_acme_eab_hmac_key.placeholder")) .max(256, t("common.errmsg.string_max", { max: 256 })), }); }; diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderGoogleTrustServices.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderGoogleTrustServices.tsx index 4f5da5b5..ae5edab5 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderGoogleTrustServices.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderGoogleTrustServices.tsx @@ -3,6 +3,8 @@ import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import Tips from "@/components/Tips"; + import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderGoogleTrustServices = () => { @@ -17,24 +19,21 @@ const AccessConfigFormFieldsProviderGoogleTrustServices = () => { return ( <> - } - > - + + } > - + + + + + } /> ); @@ -53,11 +52,11 @@ const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) = return z.object({ eabKid: z .string() - .min(1, t("access.form.googletrustservices_eab_kid.placeholder")) + .min(1, t("access.form.shared_acme_eab_kid.placeholder")) .max(256, t("common.errmsg.string_max", { max: 256 })), eabHmacKey: z .string() - .min(1, t("access.form.googletrustservices_eab_hmac_key.placeholder")) + .min(1, t("access.form.shared_acme_eab_hmac_key.placeholder")) .max(256, t("common.errmsg.string_max", { max: 256 })), }); }; diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderSSLCom.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderSSLCom.tsx index 036c5571..6e5b8318 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderSSLCom.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderSSLCom.tsx @@ -3,6 +3,8 @@ import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import Tips from "@/components/Tips"; + import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderSSLCom = () => { @@ -17,24 +19,21 @@ const AccessConfigFormFieldsProviderSSLCom = () => { return ( <> - } - > - + + } > - + + + + + } /> ); @@ -53,11 +52,11 @@ const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) = return z.object({ eabKid: z .string() - .min(1, t("access.form.sslcom_eab_kid.placeholder")) + .min(1, t("access.form.shared_acme_eab_kid.placeholder")) .max(256, t("common.errmsg.string_max", { max: 256 })), eabHmacKey: z .string() - .min(1, t("access.form.sslcom_eab_hmac_key.placeholder")) + .min(1, t("access.form.shared_acme_eab_hmac_key.placeholder")) .max(256, t("common.errmsg.string_max", { max: 256 })), }); }; diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderSectigo.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderSectigo.tsx index 8465b78b..a7361477 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderSectigo.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderSectigo.tsx @@ -3,6 +3,8 @@ import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import Tips from "@/components/Tips"; + import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderSectigo = () => { @@ -33,24 +35,21 @@ const AccessConfigFormFieldsProviderSectigo = () => { /> - } - > - + + } > - + + + + + } /> ); @@ -71,11 +70,11 @@ const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) = validationType: z.string().nonempty(t("access.form.sectigo_validation_type.placeholder")), eabKid: z .string() - .min(1, t("access.form.sectigo_eab_kid.placeholder")) + .min(1, t("access.form.shared_acme_eab_kid.placeholder")) .max(256, t("common.errmsg.string_max", { max: 256 })), eabHmacKey: z .string() - .min(1, t("access.form.sectigo_eab_hmac_key.placeholder")) + .min(1, t("access.form.shared_acme_eab_hmac_key.placeholder")) .max(256, t("common.errmsg.string_max", { max: 256 })), }); }; diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderZeroSSL.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderZeroSSL.tsx index 5d8b75fe..c324a781 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderZeroSSL.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderZeroSSL.tsx @@ -3,6 +3,8 @@ import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import Tips from "@/components/Tips"; + import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderZeroSSL = () => { @@ -17,24 +19,21 @@ const AccessConfigFormFieldsProviderZeroSSL = () => { return ( <> - } - > - + + } > - + + + + + } /> ); @@ -53,11 +52,11 @@ const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) = return z.object({ eabKid: z .string() - .min(1, t("access.form.zerossl_eab_kid.placeholder")) + .min(1, t("access.form.shared_acme_eab_kid.placeholder")) .max(256, t("common.errmsg.string_max", { max: 256 })), eabHmacKey: z .string() - .min(1, t("access.form.zerossl_eab_hmac_key.placeholder")) + .min(1, t("access.form.shared_acme_eab_hmac_key.placeholder")) .max(256, t("common.errmsg.string_max", { max: 256 })), }); }; diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index 8b285ccd..ae62c7f3 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -42,6 +42,10 @@ "access.form.provider.placeholder": "Please select a provider", "access.form.provider.help": "DNS provider: The provider that hosts your domain names and manages your DNS records.
Hosting provider: The provider that hosts your servers or cloud services for deploying certificates.", "access.form.provider.search.placeholder": "Search provider ...", + "access.form.shared_acme_eab_kid.label": "ACME EAB KID", + "access.form.shared_acme_eab_kid.placeholder": "Please enter ACME EAB KID", + "access.form.shared_acme_eab_hmac_key.label": "ACME EAB HMAC key", + "access.form.shared_acme_eab_hmac_key.placeholder": "Please enter ACME EAB HMAC key", "access.form.shared_allow_insecure_conns.label": "Insecure SSL/TLS connections", "access.form.shared_allow_insecure_conns.switch.on": "Allow", "access.form.shared_allow_insecure_conns.switch.off": "Disallow", @@ -78,12 +82,7 @@ "access.form.acmehttpreq_password.label": "HTTP Basic Auth password (Optional)", "access.form.acmehttpreq_password.placeholder": "Please enter HTTP Basic Auth password", "access.form.acmehttpreq_password.tooltip": "For more information, see https://go-acme.github.io/lego/dns/httpreq/", - "access.form.actalisssl_eab_kid.label": "ACME EAB KID", - "access.form.actalisssl_eab_kid.placeholder": "Please enter ACME EAB KID", - "access.form.actalisssl_eab_kid.tooltip": "For more information, see https://www.actalis.com/manage-with-acme", - "access.form.actalisssl_eab_hmac_key.label": "ACME EAB HMAC key", - "access.form.actalisssl_eab_hmac_key.placeholder": "Please enter ACME EAB HMAC key", - "access.form.actalisssl_eab_hmac_key.tooltip": "For more information, see https://www.actalis.com/manage-with-acme", + "access.form.actalisssl_eab.guide": "Learn more about using EAB key in Actalis SSL:
https://www.actalis.com/manage-with-acme", "access.form.aliyun_access_key_id.label": "Aliyun AccessKeyId", "access.form.aliyun_access_key_id.placeholder": "Please enter Aliyun AccessKeyId", "access.form.aliyun_access_key_id.tooltip": "For more information, see https://www.alibabacloud.com/help/en/acr/create-and-obtain-an-accesskey-pair", @@ -284,18 +283,8 @@ "access.form.goedge_access_key.label": "GoEdge AccessKey", "access.form.goedge_access_key.placeholder": "Please enter GoEdge AccessKey", "access.form.goedge_access_key.tooltip": "For more information, see https://goedge.cloud/docs/API/Auth.md", - "access.form.globalsignatlas_eab_kid.label": "ACME EAB KID", - "access.form.globalsignatlas_eab_kid.placeholder": "Please enter ACME EAB KID", - "access.form.globalsignatlas_eab_kid.tooltip": "For more information, see https://www.globalsign.com/en/acme-automated-certificate-management", - "access.form.globalsignatlas_eab_hmac_key.label": "ACME EAB HMAC key", - "access.form.globalsignatlas_eab_hmac_key.placeholder": "Please enter ACME EAB HMAC key", - "access.form.globalsignatlas_eab_hmac_key.tooltip": "For more information, see https://www.globalsign.com/en/acme-automated-certificate-management", - "access.form.googletrustservices_eab_kid.label": "ACME EAB KID", - "access.form.googletrustservices_eab_kid.placeholder": "Please enter ACME EAB KID", - "access.form.googletrustservices_eab_kid.tooltip": "For more information, see https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", - "access.form.googletrustservices_eab_hmac_key.label": "ACME EAB HMAC key", - "access.form.googletrustservices_eab_hmac_key.placeholder": "Please enter ACME EAB HMAC key", - "access.form.googletrustservices_eab_hmac_key.tooltip": "For more information, see https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", + "access.form.globalsignatlas_eab.guide": "Learn more about using EAB key in GlobalSign Atlas:
https://www.globalsign.com/en/acme-automated-certificate-management", + "access.form.googletrustservices_eab.guide": "Learn more about using EAB key in Google Trust Services:
https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", "access.form.hetzner_api_token.label": "Hetzner API token", "access.form.hetzner_api_token.placeholder": "Please enter Hetzner API token", "access.form.hetzner_api_token.tooltip": "For more information, see https://docs.hetzner.com/cloud/api/getting-started/generating-api-token", @@ -425,12 +414,7 @@ "access.form.sectigo_validation_type.option.dv.label": "DV (Domain Validation)", "access.form.sectigo_validation_type.option.ov.label": "OV (Organization Validation)", "access.form.sectigo_validation_type.option.ev.label": "EV (Extended Validation)", - "access.form.sectigo_eab_kid.label": "ACME EAB KID", - "access.form.sectigo_eab_kid.placeholder": "Please enter ACME EAB KID", - "access.form.sectigo_eab_kid.tooltip": "For more information, see https://www.sectigo.com/enterprise-solutions/certificate-manager/integrations-acme", - "access.form.sectigo_eab_hmac_key.label": "ACME EAB HMAC key", - "access.form.sectigo_eab_hmac_key.placeholder": "Please enter ACME EAB HMAC key", - "access.form.sectigo_eab_hmac_key.tooltip": "For more information, see https://www.sectigo.com/enterprise-solutions/certificate-manager/integrations-acme", + "access.form.sectigo_eab.guide": "Learn more about using EAB key in Sectigo:
https://www.sectigo.com/enterprise-solutions/certificate-manager/integrations-acme", "access.form.slackbot_token.label": "Slack bot token", "access.form.slackbot_token.placeholder": "Please enter Slack bot token", "access.form.slackbot_token.tooltip": "For more information, see https://docs.slack.dev/authentication/tokens#bot", @@ -465,12 +449,7 @@ "access.form.ssh_jump_servers.errmsg.invalid": "Please configure valid jump servers", "access.form.ssh_jump_servers.item.label": "Jump server", "access.form.ssh_jump_servers.add": "Add jump server", - "access.form.sslcom_eab_kid.label": "ACME EAB KID", - "access.form.sslcom_eab_kid.placeholder": "Please enter ACME EAB KID", - "access.form.sslcom_eab_kid.tooltip": "For more information, see https://www.ssl.com/how-to/generate-acme-credentials-for-reseller-customers/", - "access.form.sslcom_eab_hmac_key.label": "ACME EAB HMAC key", - "access.form.sslcom_eab_hmac_key.placeholder": "Please enter ACME EAB HMAC key", - "access.form.sslcom_eab_hmac_key.tooltip": "For more information, see https://www.ssl.com/how-to/generate-acme-credentials-for-reseller-customers/", + "access.form.sslcom_eab.guide": "Learn more about using EAB key in SSL.com:
https://www.ssl.com/how-to/generate-acme-credentials-for-reseller-customers/", "access.form.telegrambot_token.label": "Telegram bot token", "access.form.telegrambot_token.placeholder": "Please enter Telegram bot token", "access.form.telegrambot_token.tooltip": "How to get it? Please refer to https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a", @@ -559,10 +538,5 @@ "access.form.westcn_api_password.label": "West.cn API password", "access.form.westcn_api_password.placeholder": "Please enter West.cn API password", "access.form.westcn_api_password.tooltip": "For more information, see https://www.west.cn/CustomerCenter/doc/apiv2.html", - "access.form.zerossl_eab_kid.label": "ACME EAB KID", - "access.form.zerossl_eab_kid.placeholder": "Please enter ACME EAB KID", - "access.form.zerossl_eab_kid.tooltip": "For more information, see https://zerossl.com/documentation/acme/", - "access.form.zerossl_eab_hmac_key.label": "ACME EAB HMAC key", - "access.form.zerossl_eab_hmac_key.placeholder": "Please enter ACME EAB HMAC key", - "access.form.zerossl_eab_hmac_key.tooltip": "For more information, see https://zerossl.com/documentation/acme/" + "access.form.zerossl_eab.guide": "Learn more about using EAB key in ZeroSSL:
https://zerossl.com/documentation/acme/" } diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index 0deeadeb..2f0ab70e 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -21,11 +21,11 @@ "workflow.action.enable.button": "Activate", "workflow.action.enable.errmsg.unpublished": "Please complete the orchestration and publish the changes first", "workflow.action.disable.button": "Deactivate", - "workflow.action.run.button": "Run", - "workflow.action.run.menu": "Run", - "workflow.action.run.modal.title": "Run workflow", - "workflow.action.run.modal.content": "You have unpublished changes. Do you really want to run this workflow based on the last published version?", - "workflow.action.run.prompt": "Running... Please check the history later", + "workflow.action.execute.button": "Execute", + "workflow.action.execute.menu": "Execute", + "workflow.action.execute.modal.title": "Execute workflow", + "workflow.action.execute.modal.content": "You have unpublished changes. Do you really want to execute this workflow based on the last published version?", + "workflow.action.execute.prompt": "Running... Please check the history later", "workflow.props.name": "Name", "workflow.props.description": "Description", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index d48b50da..f8229a8f 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -41,6 +41,10 @@ "access.form.provider.placeholder": "请选择提供商", "access.form.provider.help": "提供商分为两种类型:
【DNS 提供商】你的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理域名解析记录。
【主机提供商】你的服务器或云服务的托管方,用于部署签发的证书。", "access.form.provider.search.placeholder": "搜索提供商……", + "access.form.shared_acme_eab_kid.label": "ACME EAB KID", + "access.form.shared_acme_eab_kid.placeholder": "请输入 ACME EAB KID", + "access.form.shared_acme_eab_hmac_key.label": "ACME EAB HMAC Key", + "access.form.shared_acme_eab_hmac_key.placeholder": "请输入 ACME EAB HMAC Key", "access.form.shared_allow_insecure_conns.label": "忽略 SSL/TLS 证书错误", "access.form.shared_allow_insecure_conns.switch.on": "允许", "access.form.shared_allow_insecure_conns.switch.off": "不允许", @@ -77,12 +81,7 @@ "access.form.acmehttpreq_password.label": "HTTP 基本认证密码(可选)", "access.form.acmehttpreq_password.placeholder": "请输入 HTTP 基本认证密码", "access.form.acmehttpreq_password.tooltip": "这是什么?请参阅 https://go-acme.github.io/lego/dns/httpreq/", - "access.form.actalisssl_eab_kid.label": "ACME EAB KID", - "access.form.actalisssl_eab_kid.placeholder": "请输入 ACME EAB KID", - "access.form.actalisssl_eab_kid.tooltip": "这是什么?请参阅 https://www.actalis.com/manage-with-acme", - "access.form.actalisssl_eab_hmac_key.label": "ACME EAB HMAC Key", - "access.form.actalisssl_eab_hmac_key.placeholder": "请输入 ACME EAB HMAC Key", - "access.form.actalisssl_eab_hmac_key.tooltip": "这是什么?请参阅 https://www.actalis.com/manage-with-acme", + "access.form.actalisssl_eab.guide": "点击下方链接了解如何获取 Actalis SSL EAB:
https://www.actalis.com/manage-with-acme", "access.form.aliyun_access_key_id.label": "阿里云 AccessKeyId", "access.form.aliyun_access_key_id.placeholder": "请输入阿里云 AccessKeyId", "access.form.aliyun_access_key_id.tooltip": "这是什么?请参阅 https://help.aliyun.com/zh/ram/user-guide/create-an-accesskey-pair", @@ -283,18 +282,8 @@ "access.form.goedge_access_key.label": "GoEdge AccessKey", "access.form.goedge_access_key.placeholder": "请输入 GoEdge AccessKey", "access.form.goedge_access_key.tooltip": "这是什么?请参阅 https://goedge.cloud/docs/API/Auth.md", - "access.form.globalsignatlas_eab_kid.label": "ACME EAB KID", - "access.form.globalsignatlas_eab_kid.placeholder": "请输入 ACME EAB KID", - "access.form.globalsignatlas_eab_kid.tooltip": "这是什么?请参阅 https://globalsign.cn/acme-automated-certificate-management", - "access.form.globalsignatlas_eab_hmac_key.label": "ACME EAB HMAC Key", - "access.form.globalsignatlas_eab_hmac_key.placeholder": "请输入 ACME EAB HMAC Key", - "access.form.globalsignatlas_eab_hmac_key.tooltip": "这是什么?请参阅 https://globalsign.cn/acme-automated-certificate-management", - "access.form.googletrustservices_eab_kid.label": "ACME EAB KID", - "access.form.googletrustservices_eab_kid.placeholder": "请输入 ACME EAB KID", - "access.form.googletrustservices_eab_kid.tooltip": "这是什么?请参阅 https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", - "access.form.googletrustservices_eab_hmac_key.label": "ACME EAB HMAC Key", - "access.form.googletrustservices_eab_hmac_key.placeholder": "请输入 ACME EAB HMAC Key", - "access.form.googletrustservices_eab_hmac_key.tooltip": "这是什么?请参阅 https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", + "access.form.globalsignatlas_eab.guide": "点击下方链接了解如何获取 GlobalSign Atlas EAB:
https://globalsign.cn/acme-automated-certificate-management", + "access.form.googletrustservices_eab.guide": "点击下方链接了解如何获取 Google Trust Services EAB:
https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", "access.form.hetzner_api_token.label": "Hetzner API Token", "access.form.hetzner_api_token.placeholder": "请输入 Hetzner API Token", "access.form.hetzner_api_token.tooltip": "这是什么?请参阅 https://docs.hetzner.com/cloud/api/getting-started/generating-api-token", @@ -424,12 +413,7 @@ "access.form.sectigo_validation_type.option.dv.label": "DV(域名型)", "access.form.sectigo_validation_type.option.ov.label": "OV(企业型)", "access.form.sectigo_validation_type.option.ev.label": "EV(增强型)", - "access.form.sectigo_eab_kid.label": "ACME EAB KID", - "access.form.sectigo_eab_kid.placeholder": "请输入 ACME EAB KID", - "access.form.sectigo_eab_kid.tooltip": "这是什么?请参阅 https://www.sectigo.com/enterprise-solutions/certificate-manager/integrations-acme", - "access.form.sectigo_eab_hmac_key.label": "ACME EAB HMAC Key", - "access.form.sectigo_eab_hmac_key.placeholder": "请输入 ACME EAB HMAC Key", - "access.form.sectigo_eab_hmac_key.tooltip": "这是什么?请参阅 https://www.sectigo.com/enterprise-solutions/certificate-manager/integrations-acme", + "access.form.sectigo_eab.guide": "点击下方链接了解如何获取 Sectigo EAB:
https://www.sectigo.com/enterprise-solutions/certificate-manager/integrations-acme", "access.form.slackbot_token.label": "Slack 机器人 Token", "access.form.slackbot_token.placeholder": "请输入 Slack 机器人 Token", "access.form.slackbot_token.tooltip": "这是什么?请参阅 https://docs.slack.dev/authentication/tokens#bot", @@ -464,12 +448,7 @@ "access.form.ssh_jump_servers.errmsg.invalid": "请配置有效的跳板机信息", "access.form.ssh_jump_servers.item.label": "跳板机", "access.form.ssh_jump_servers.add": "添加跳板机", - "access.form.sslcom_eab_kid.label": "ACME EAB KID", - "access.form.sslcom_eab_kid.placeholder": "请输入 ACME EAB KID", - "access.form.sslcom_eab_kid.tooltip": "这是什么?请参阅 https://www.ssl.com/how-to/generate-acme-credentials-for-reseller-customers/", - "access.form.sslcom_eab_hmac_key.label": "ACME EAB HMAC Key", - "access.form.sslcom_eab_hmac_key.placeholder": "请输入 ACME EAB HMAC Key", - "access.form.sslcom_eab_hmac_key.tooltip": "这是什么?请参阅 https://www.ssl.com/how-to/generate-acme-credentials-for-reseller-customers/", + "access.form.sslcom_eab.guide": "点击下方链接了解如何获取 SSL.com EAB:
https://www.ssl.com/how-to/generate-acme-credentials-for-reseller-customers/", "access.form.telegrambot_token.label": "Telegram 机器人 API Token", "access.form.telegrambot_token.placeholder": "请输入 Telegram 机器人 API Token", "access.form.telegrambot_token.tooltip": "如何获取此参数?请参阅 https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a", @@ -558,10 +537,5 @@ "access.form.westcn_api_password.label": "西部数码 API 密码", "access.form.westcn_api_password.placeholder": "请输入西部数码 API 密码", "access.form.westcn_api_password.tooltip": "这是什么?请参阅 https://www.west.cn/CustomerCenter/doc/apiv2.html", - "access.form.zerossl_eab_kid.label": "ACME EAB KID", - "access.form.zerossl_eab_kid.placeholder": "请输入 ACME EAB KID", - "access.form.zerossl_eab_kid.tooltip": "这是什么?请参阅 https://zerossl.com/documentation/acme/", - "access.form.zerossl_eab_hmac_key.label": "ACME EAB HMAC Key", - "access.form.zerossl_eab_hmac_key.placeholder": "请输入 ACME EAB HMAC Key", - "access.form.zerossl_eab_hmac_key.tooltip": "这是什么?请参阅 https://zerossl.com/documentation/acme/" + "access.form.zerossl_eab.guide": "点击下方链接了解如何获取 ZeroSSL EAB:
https://zerossl.com/documentation/acme/" } diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index 67e5948b..19f2d513 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -21,11 +21,11 @@ "workflow.action.enable.button": "启用", "workflow.action.enable.errmsg.unpublished": "请先完成流程编排并发布更改", "workflow.action.disable.button": "停用", - "workflow.action.run.button": "运行", - "workflow.action.run.menu": "运行工作流", - "workflow.action.run.modal.title": "运行工作流", - "workflow.action.run.modal.content": "你有尚未发布的更改。确定要以最近一次发布的版本继续运行吗?", - "workflow.action.run.prompt": "运行中……请稍后查看运行历史", + "workflow.action.execute.button": "运行", + "workflow.action.execute.menu": "运行工作流", + "workflow.action.execute.modal.title": "运行工作流", + "workflow.action.execute.modal.content": "你有尚未发布的更改。确定要以最近一次发布的版本继续运行吗?", + "workflow.action.execute.prompt": "运行中……请稍后查看运行历史", "workflow.props.name": "名称", "workflow.props.description": "描述", diff --git a/ui/src/pages/settings/SettingsSSLProvider.tsx b/ui/src/pages/settings/SettingsSSLProvider.tsx index cc0f417e..556fc5a3 100644 --- a/ui/src/pages/settings/SettingsSSLProvider.tsx +++ b/ui/src/pages/settings/SettingsSSLProvider.tsx @@ -203,39 +203,35 @@ const InternalSharedForm = ({ children, provider }: { children?: React.ReactNode }; const InternalSharedFormEabFields = ({ i18nKey }: { i18nKey: string }) => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); + + const hasGuide = i18n.exists(`access.form.${i18nKey}_eab.guide`); const formSchema = z.object({ endpoint: z.url(t("common.errmsg.url_invalid")), eabKid: z - .string(t(`access.form.${i18nKey}_eab_kid.label`)) - .min(1, t(`access.form.${i18nKey}_eab_kid.label`)) + .string(t("access.form.shared_acme_eab_kid.label")) + .min(1, t("access.form.shared_acme_eab_kid.placeholder")) .max(256, t("common.errmsg.string_max", { max: 256 })), eabHmacKey: z - .string(t(`access.form.${i18nKey}_eab_hmac_key.label`)) - .min(1, t(`access.form.${i18nKey}_eab_hmac_key.label`)) + .string(t("access.form.shared_acme_eab_hmac_key.label")) + .min(1, t("access.form.shared_acme_eab_hmac_key.placeholder")) .max(256, t("common.errmsg.string_max", { max: 256 })), }); const formRule = createSchemaFieldRule(formSchema); return ( <> - } - > - + + - } - > - + + + + + ); @@ -356,20 +352,20 @@ const InternalSettingsFormProviderACMECA = () => { } + tooltip={} > - + } + tooltip={} > - + ); diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx index 4c9d7121..d71f8d9e 100644 --- a/ui/src/pages/workflows/WorkflowDetail.tsx +++ b/ui/src/pages/workflows/WorkflowDetail.tsx @@ -68,8 +68,8 @@ const WorkflowDetail = () => { const { promise, resolve } = Promise.withResolvers(); if (workflow.hasDraft) { modal.confirm({ - title: t("workflow.action.run.modal.title"), - content: t("workflow.action.run.modal.content"), + title: t("workflow.action.execute.modal.title"), + content: t("workflow.action.execute.modal.content"), onOk: () => resolve(void 0), }); } else { @@ -82,7 +82,7 @@ const WorkflowDetail = () => { await startWorkflowRun(workflow.id); - message.info(t("workflow.action.run.prompt")); + message.info(t("workflow.action.execute.prompt")); } catch (err) { setRunButtonLoading(false); @@ -137,7 +137,7 @@ const WorkflowDetail = () => {
diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx index 0eceb7a7..adfeee40 100644 --- a/ui/src/pages/workflows/WorkflowList.tsx +++ b/ui/src/pages/workflows/WorkflowList.tsx @@ -157,8 +157,8 @@ const WorkflowList = () => { }, }, { - key: "run", - label: t("workflow.action.run.menu"), + key: "execute", + label: t("workflow.action.execute.menu"), icon: ( @@ -166,7 +166,7 @@ const WorkflowList = () => { ), disabled: !record.hasContent, onClick: () => { - handleRecordRunClick(record); + handleRecordExecuteClick(record); }, }, { @@ -339,11 +339,11 @@ const WorkflowList = () => { } }; - const handleRecordRunClick = async (workflow: WorkflowModel) => { + const handleRecordExecuteClick = async (workflow: WorkflowModel) => { try { await startWorkflowRun(workflow.id); - message.info(t("workflow.action.run.prompt")); + message.info(t("workflow.action.execute.prompt")); } catch (err) { console.error(err); notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); From e7c1f90bc4f6a3baf9bf6f9a06a447479913adec Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 9 Sep 2025 23:08:06 +0800 Subject: [PATCH 7/7] feat(ui): new WorkflowNew page --- ui/src/i18n/locales/en/nls.workflow.json | 7 +- ui/src/i18n/locales/zh/nls.workflow.json | 15 +- ui/src/pages/workflows/WorkflowNew.tsx | 173 ++++++++++++++--------- 3 files changed, 116 insertions(+), 79 deletions(-) diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index 2f0ab70e..219fa5c2 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -42,13 +42,14 @@ "workflow.new.title": "Create Workflow", "workflow.new.subtitle": "Using a workflow to monitor, apply, deploy and notify.", - "workflow.new.templates.title": "Choose a Workflow Template", + "workflow.new.button.create": "Create from blank", + "workflow.new.button.import": "Import from file", + "workflow.new.templates.title": "Choose a Template", + "workflow.new.templates.subtitle": "Use these template workflows to quickly initialize your automated certificate management workflow.", "workflow.new.templates.template.standard.title": "Standard template", "workflow.new.templates.template.standard.description": "A standard operating procedure that includes application, deployment, and notification steps.", "workflow.new.templates.template.certtest.title": "Monitoring template", "workflow.new.templates.template.certtest.description": "A monitoring operating procedure that includes monitoring, and notification steps.", - "workflow.new.templates.template.empty.title": "Empty template", - "workflow.new.templates.template.empty.description": "Customize all the contents of the workflow from the beginning.", "workflow.new.templates.default_name": "Untitled workflow", "workflow.new.templates.default_description": "Created at {{date}}", diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index 19f2d513..612aa56d 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -42,13 +42,14 @@ "workflow.new.title": "新建工作流", "workflow.new.subtitle": "使用工作流来监控域名、申请证书、部署上传和发送通知。", - "workflow.new.templates.title": "选择工作流模板", - "workflow.new.templates.template.standard.title": "标准模板", - "workflow.new.templates.template.standard.description": "一个包含证书申请 + 证书部署 + 消息通知步骤的工作流程。", - "workflow.new.templates.template.certtest.title": "监控模板", - "workflow.new.templates.template.certtest.description": "一个包含证书监控 + 消息通知步骤的工作流程。", - "workflow.new.templates.template.empty.title": "空白模板", - "workflow.new.templates.template.empty.description": "从零开始自定义工作流的任务内容。", + "workflow.new.button.create": "创建空白工作流", + "workflow.new.button.import": "从文件导入……", + "workflow.new.templates.title": "选择模板", + "workflow.new.templates.subtitle": "使用这些模板快速初始化你的自动化证书管理工作流。", + "workflow.new.templates.template.standard.title": "标准业务流程", + "workflow.new.templates.template.standard.description": "一个包含证书申请 + 证书部署 + 消息通知步骤的工作流程,可适用于绝大多数业务场景。", + "workflow.new.templates.template.certtest.title": "域名证书监控", + "workflow.new.templates.template.certtest.description": "一个包含证书监控 + 消息通知步骤的工作流程,可在线上证书到期前或已过期时发出告警。", "workflow.new.templates.default_name": "未命名工作流", "workflow.new.templates.default_description": "创建于 {{date}}", diff --git a/ui/src/pages/workflows/WorkflowNew.tsx b/ui/src/pages/workflows/WorkflowNew.tsx index ac86c91e..ef4518d3 100644 --- a/ui/src/pages/workflows/WorkflowNew.tsx +++ b/ui/src/pages/workflows/WorkflowNew.tsx @@ -1,9 +1,11 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; -import { App, Card, Col, Row, Spin, Typography } from "antd"; +import { IconArrowRight, IconCode, IconSquarePlus2 } from "@tabler/icons-react"; +import { App, Button, Card, Spin, Typography } from "antd"; import dayjs from "dayjs"; +import WorkflowGraphImportModal from "@/components/workflow/WorkflowGraphImportModal"; import { WORKFLOW_NODE_TYPES, type WorkflowModel, @@ -15,10 +17,10 @@ import { import { save as saveWorkflow } from "@/repository/workflow"; import { getErrMsg } from "@/utils/error"; +const TEMPLATE_KEY_BLANK = "blank" as const; const TEMPLATE_KEY_STANDARD = "standard" as const; const TEMPLATE_KEY_CERTTEST = "certtest" as const; -const TEMPLATE_KEY_EMPTY = "empty" as const; -type TemplateKeys = typeof TEMPLATE_KEY_EMPTY | typeof TEMPLATE_KEY_CERTTEST | typeof TEMPLATE_KEY_STANDARD; +type TemplateKeys = typeof TEMPLATE_KEY_BLANK | typeof TEMPLATE_KEY_CERTTEST | typeof TEMPLATE_KEY_STANDARD; const WorkflowNew = () => { const navigate = useNavigate(); @@ -27,21 +29,56 @@ const WorkflowNew = () => { const { notification } = App.useApp(); - const templateGridSpans = { - xs: { flex: "100%" }, - md: { flex: "50%" }, - lg: { flex: "50%" }, - xl: { flex: "33.3333%" }, - xxl: { flex: "33.3333%" }, - }; + const templates = [ + { + key: TEMPLATE_KEY_STANDARD, + name: t("workflow.new.templates.template.standard.title"), + description: t("workflow.new.templates.template.standard.description"), + image: "/imgs/workflow/tpl-standard.png", + }, + { + key: TEMPLATE_KEY_CERTTEST, + name: t("workflow.new.templates.template.certtest.title"), + description: t("workflow.new.templates.template.certtest.description"), + image: "/imgs/workflow/tpl-certtest.png", + }, + ]; const [templateSelectKey, setTemplateSelectKey] = useState(); + const [templatePending, setTemplatePending] = useState(false); - const [pending, setPending] = useState(false); + const renderTemplateCard = ({ key, name, description, image }: { key: TemplateKeys; name: string; description: string; image: string }) => { + return ( + } + hoverable + onClick={() => handleTemplateClick(key)} + > +
+ +
{name}
+ +
+ } + description={description} + /> + {templatePending && } +
+ + ); + }; + + const { modalProps: workflowImportModalProps, ...workflowImportModal } = WorkflowGraphImportModal.useModal(); const handleTemplateClick = async (key: TemplateKeys) => { - if (pending) return; + if (templatePending) return; setTemplateSelectKey(key); + setTemplatePending(true); try { let workflow = {} as WorkflowModel; @@ -51,7 +88,7 @@ const WorkflowNew = () => { workflow.hasDraft = true; switch (key) { - case TEMPLATE_KEY_EMPTY: + case TEMPLATE_KEY_BLANK: { const startNode = newNode(WORKFLOW_NODE_TYPES.START, { i18n: i18n }); const endNode = newNode(WORKFLOW_NODE_TYPES.END, { i18n: i18n }); @@ -206,11 +243,35 @@ const WorkflowNew = () => { throw err; } finally { - setPending(false); + setTemplatePending(false); setTemplateSelectKey(void 0); } }; + const handleImportClick = async () => { + if (templatePending) return; + + workflowImportModal.open().then(async (graph) => { + setTemplatePending(true); + + try { + let workflow = {} as WorkflowModel; + workflow.name = t("workflow.new.templates.default_name"); + workflow.description = t("workflow.new.templates.default_description", { date: dayjs().format("YYYY-MM-DD HH:mm") }); + workflow.graphDraft = graph; + workflow.hasDraft = true; + workflow = await saveWorkflow(workflow); + navigate(`/workflows/${workflow.id}`, { replace: true }); + } catch (err) { + notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); + + throw err; + } finally { + setTemplatePending(false); + } + }); + }; + return (
@@ -219,65 +280,39 @@ const WorkflowNew = () => {
- -
{t("workflow.new.templates.title")}
-
- - - - } - hoverable - onClick={() => handleTemplateClick(TEMPLATE_KEY_STANDARD)} - > -
- - +
+
+ +
+ +
- - - } - hoverable - onClick={() => handleTemplateClick(TEMPLATE_KEY_CERTTEST)} - > -
- - -
-
- + +
+
- - } - hoverable - onClick={() => handleTemplateClick(TEMPLATE_KEY_EMPTY)} - > -
- - -
-
- - +
+

{t("workflow.new.templates.title")}

+ +
{t("workflow.new.templates.subtitle")}
+
+ +
+ {templates.map((template) => renderTemplateCard(template))} +
+
);