mirror of
https://github.com/certimate-go/certimate.git
synced 2026-06-22 21:05:48 +08:00
Merge pull request #953 from fudiwei/dev
This commit is contained in:
commit
007385a071
@ -7,6 +7,7 @@ import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
@ -70,10 +71,23 @@ func NewACMEAccount(config *ACMEConfig, email string, register bool) (*ACMEAccou
|
||||
var regres *registration.Resource
|
||||
var regerr error
|
||||
if legoClient.GetExternalAccountRequired() {
|
||||
if config.EABKid == "" {
|
||||
return nil, errors.New("missing or invalid eab kid")
|
||||
}
|
||||
if config.EABHmacKey == "" {
|
||||
return nil, errors.New("missing or invalid eab hmac key")
|
||||
}
|
||||
|
||||
// patch, see https://github.com/go-acme/lego/issues/2634
|
||||
keyId := strings.TrimSpace(config.EABKid)
|
||||
keyEncoded := strings.TrimSpace(config.EABHmacKey)
|
||||
keyEncoded = strings.ReplaceAll(strings.ReplaceAll(keyEncoded, "+", "-"), "/", "_")
|
||||
keyEncoded = strings.TrimRight(keyEncoded, "=")
|
||||
|
||||
regres, regerr = legoClient.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: config.EABKid,
|
||||
HmacEncoded: config.EABHmacEncoded,
|
||||
Kid: keyId,
|
||||
HmacEncoded: keyEncoded,
|
||||
})
|
||||
} else {
|
||||
regres, regerr = legoClient.Registration.Register(registration.RegisterOptions{
|
||||
@ -81,7 +95,7 @@ func NewACMEAccount(config *ACMEConfig, email string, register bool) (*ACMEAccou
|
||||
})
|
||||
}
|
||||
if regerr != nil {
|
||||
return nil, fmt.Errorf("failed to register acme account: %w", err)
|
||||
return nil, fmt.Errorf("failed to register acme account: %w", regerr)
|
||||
}
|
||||
|
||||
account.ACMEAccount = ®res.Body
|
||||
|
||||
@ -34,7 +34,7 @@ type ACMEConfig struct {
|
||||
CAProvider domain.CAProviderType
|
||||
CADirUrl string
|
||||
EABKid string
|
||||
EABHmacEncoded string
|
||||
EABHmacKey string
|
||||
CertifierKeyType certcrypto.KeyType
|
||||
}
|
||||
|
||||
@ -93,12 +93,12 @@ func NewACMEConfig(options *ACMEConfigOptions) (*ACMEConfig, error) {
|
||||
ca.CADirUrl = endpoint
|
||||
}
|
||||
|
||||
eab := &domain.AccessConfigForACMEExternalAccountBinding{}
|
||||
eab := domain.AccessConfigForACMEExternalAccountBinding{}
|
||||
if err := xmaps.Populate(caAccessConfig, &eab); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ca.EABKid = eab.EabKid
|
||||
ca.EABHmacEncoded = eab.EabHmacKey
|
||||
ca.EABHmacKey = eab.EabHmacKey
|
||||
|
||||
return ca, nil
|
||||
}
|
||||
|
||||
@ -53,7 +53,7 @@ func (e *EvalResult) GetFloat64() (float64, error) {
|
||||
|
||||
floatValue, err := strconv.ParseFloat(stringValue, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse float64: %v", err)
|
||||
return 0, fmt.Errorf("failed to parse float64: %w", err)
|
||||
}
|
||||
return floatValue, nil
|
||||
}
|
||||
|
||||
@ -330,8 +330,8 @@ func (wd *workflowDispatcher) tryNextAsync() {
|
||||
|
||||
if hasSameWorkflowTask {
|
||||
wd.syslog.Warn(fmt.Sprintf("workflow run #%s is pending, because tasks that belonging to the same workflow already exists", pendingRunId))
|
||||
} else if len(wd.processingTasks) >= wd.concurrency {
|
||||
wd.syslog.Warn(fmt.Sprintf("workflow run #%s is pending, because the maximum concurrency limit has been reached", pendingRunId))
|
||||
} else if len(wd.processingTasks) >= wd.concurrency && wd.concurrency > 0 {
|
||||
wd.syslog.Warn(fmt.Sprintf("workflow run #%s is pending, because the maximum concurrency (limit: %d) has been reached", pendingRunId, wd.concurrency))
|
||||
} else {
|
||||
wd.taskMtx.RUnlock()
|
||||
wd.taskMtx.Lock()
|
||||
|
||||
@ -180,7 +180,7 @@ func (we *workflowEngine) executeNode(wfCtx *WorkflowContext, node *Node) error
|
||||
})
|
||||
}
|
||||
if _, err := we.wfoutputRepo.Save(execCtx.ctx, output); err != nil {
|
||||
we.syslog.Warn("failed to save node output")
|
||||
we.syslog.Error("failed to save node output", slog.Any("error", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -87,7 +87,7 @@ func (ne *bizApplyNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExe
|
||||
// 解析证书
|
||||
certX509, err := xcert.ParseCertificateFromPEM(obtainResp.FullChainCertificate)
|
||||
if err != nil {
|
||||
ne.logger.Warn("failed to parse certificate, may be the CA responded error")
|
||||
ne.logger.Warn("could not parse certificate, may be the CA responded error")
|
||||
return execRes, err
|
||||
}
|
||||
|
||||
@ -106,7 +106,7 @@ func (ne *bizApplyNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExe
|
||||
}
|
||||
certificate.PopulateFromX509(certX509)
|
||||
if certificate, err := ne.certificateRepo.Save(execCtx.ctx, certificate); err != nil {
|
||||
ne.logger.Warn("failed to save certificate")
|
||||
ne.logger.Warn("could not save certificate")
|
||||
return execRes, err
|
||||
} else {
|
||||
ne.logger.Info("certificate saved", slog.String("recordId", certificate.Id))
|
||||
@ -232,7 +232,7 @@ func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nod
|
||||
}
|
||||
legoConfig, err := certapply.NewACMEConfig(legoOptions)
|
||||
if err != nil {
|
||||
ne.logger.Warn("failed to initialize acme config")
|
||||
ne.logger.Warn("could not initialize acme config")
|
||||
return nil, err
|
||||
} else {
|
||||
ne.logger.Info("acme config initialized", slog.String("acmeDirUrl", legoConfig.CADirUrl))
|
||||
@ -242,7 +242,7 @@ func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nod
|
||||
// 注意此步骤仍需在主进程中进行,以保证并发安全
|
||||
legoUser, err := certapply.NewACMEAccountWithSingleFlight(legoConfig, nodeCfg.ContactEmail)
|
||||
if err != nil {
|
||||
ne.logger.Warn("failed to initialize acme account")
|
||||
ne.logger.Warn("could not initialize acme account")
|
||||
return nil, err
|
||||
} else {
|
||||
ne.logger.Info("acme account initialized", slog.String("acmeAcctUrl", legoUser.ACMEAcctUrl))
|
||||
@ -320,7 +320,7 @@ func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nod
|
||||
Request: obtainReq,
|
||||
})
|
||||
if err != nil {
|
||||
ne.logger.Warn("failed to obtain certificate")
|
||||
ne.logger.Warn("could not obtain certificate")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -339,14 +339,14 @@ func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nod
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
ne.logger.Warn("failed to initialize acme client")
|
||||
ne.logger.Warn("could not initialize acme client")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 执行申请证书请求
|
||||
obtainResp, err := legoClient.ObtainCertificate(execCtx.ctx, obtainReq)
|
||||
if err != nil {
|
||||
ne.logger.Warn("failed to obtain certificate")
|
||||
ne.logger.Warn("could not obtain certificate")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@ -43,7 +43,7 @@ func (ne *bizDeployNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeEx
|
||||
if len(s) == 2 {
|
||||
certificate, err := ne.certificateRepo.GetById(execCtx.ctx, s[1])
|
||||
if err != nil {
|
||||
ne.logger.Warn("failed to get input certificate")
|
||||
ne.logger.Warn("could not get input certificate")
|
||||
return execRes, err
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@ func (ne *bizDeployNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeEx
|
||||
PrivateKey: inputCertificate.PrivateKey,
|
||||
}
|
||||
if _, err := deployClient.DeployCertificate(execCtx.ctx, deployReq); err != nil {
|
||||
ne.logger.Warn("failed to deploy certificate")
|
||||
ne.logger.Warn("could not deploy certificate")
|
||||
return execRes, err
|
||||
}
|
||||
|
||||
|
||||
@ -67,7 +67,7 @@ func (ne *bizMonitorNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeE
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ne.logger.Warn("failed to monitor certificate")
|
||||
ne.logger.Warn("could not retrieve certificate")
|
||||
return execRes, err
|
||||
} else {
|
||||
if len(certs) == 0 {
|
||||
|
||||
@ -49,7 +49,7 @@ func (ne *bizNotifyNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeEx
|
||||
Message: nodeCfg.Message,
|
||||
}
|
||||
if _, err := notifyClient.SendNotification(execCtx.ctx, notifyReq); err != nil {
|
||||
ne.logger.Warn("failed to send notification")
|
||||
ne.logger.Warn("could not send notification")
|
||||
return execRes, err
|
||||
}
|
||||
|
||||
|
||||
@ -63,7 +63,7 @@ func (ne *bizUploadNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeEx
|
||||
}
|
||||
certificate.PopulateFromPEM(nodeCfg.Certificate, nodeCfg.PrivateKey)
|
||||
if certificate, err := ne.certificateRepo.Save(execCtx.ctx, certificate); err != nil {
|
||||
ne.logger.Warn("failed to save certificate")
|
||||
ne.logger.Warn("could not save certificate")
|
||||
return execRes, err
|
||||
} else {
|
||||
ne.logger.Info("certificate saved", slog.String("recordId", certificate.Id))
|
||||
|
||||
@ -1304,6 +1304,23 @@ func init() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// normalize field `nodeId` in collection `workflow`, `workflow_run`, `workflow_output`, `workflow_logs`
|
||||
for i := 0; i < 3; i++ {
|
||||
app.DB().NewQuery(`UPDATE workflow SET graphDraft=REPLACE(graphDraft, '"id":"-', '"id":"')`).Execute()
|
||||
app.DB().NewQuery(`UPDATE workflow SET graphDraft=REPLACE(graphDraft, '"id":"_', '"id":"')`).Execute()
|
||||
app.DB().NewQuery(`UPDATE workflow SET graphContent=REPLACE(graphContent, '"id":"-', '"id":"')`).Execute()
|
||||
app.DB().NewQuery(`UPDATE workflow SET graphContent=REPLACE(graphContent, '"id":"_', '"id":"')`).Execute()
|
||||
|
||||
app.DB().NewQuery(`UPDATE workflow_run SET graph=REPLACE(graph, '"id":"-', '"id":"')`).Execute()
|
||||
app.DB().NewQuery(`UPDATE workflow_run SET graph=REPLACE(graph, '"id":"_', '"id":"')`).Execute()
|
||||
|
||||
app.DB().NewQuery(`UPDATE workflow_output SET nodeId=SUBSTR(nodeId, 2) WHERE nodeId LIKE '-%'`).Execute()
|
||||
app.DB().NewQuery(`UPDATE workflow_output SET nodeId=SUBSTR(nodeId, 2) WHERE nodeId LIKE '_%'`).Execute()
|
||||
|
||||
app.DB().NewQuery(`UPDATE workflow_logs SET nodeId=SUBSTR(nodeId, 2) WHERE nodeId LIKE '-%'`).Execute()
|
||||
app.DB().NewQuery(`UPDATE workflow_logs SET nodeId=SUBSTR(nodeId, 2) WHERE nodeId LIKE '_%'`).Execute()
|
||||
}
|
||||
}
|
||||
|
||||
tracer.Printf("done")
|
||||
|
||||
33
pkg/utils/filepath/path.go
Normal file
33
pkg/utils/filepath/path.go
Normal file
@ -0,0 +1,33 @@
|
||||
package filepath
|
||||
|
||||
import (
|
||||
stdfilepath "path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 与标准库中的 [filepath.Dir] 类似,但会尝试保留原有的路径分隔符。
|
||||
//
|
||||
// 入参:
|
||||
// - path: 文件路径。
|
||||
//
|
||||
// 出参:
|
||||
// - 目录路径。
|
||||
func Dir(path string) string {
|
||||
const SEP_WIN = "\\"
|
||||
const SEP_UNIX = "/"
|
||||
|
||||
sep := SEP_UNIX
|
||||
if strings.Contains(path, SEP_WIN) && !strings.Contains(path, SEP_UNIX) {
|
||||
sep = SEP_WIN
|
||||
}
|
||||
|
||||
dir := stdfilepath.Dir(path)
|
||||
|
||||
if sep != SEP_UNIX && strings.Contains(dir, SEP_UNIX) {
|
||||
dir = strings.ReplaceAll(dir, SEP_UNIX, sep)
|
||||
} else if sep != SEP_WIN && strings.Contains(dir, SEP_WIN) {
|
||||
dir = strings.ReplaceAll(dir, SEP_WIN, sep)
|
||||
}
|
||||
|
||||
return dir
|
||||
}
|
||||
@ -5,11 +5,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/sftp"
|
||||
"github.com/povsister/scp"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
xfilepath "github.com/certimate-go/certimate/pkg/utils/filepath"
|
||||
)
|
||||
|
||||
// 与 [WriteRemote] 类似,但写入的是字符串内容。
|
||||
@ -97,7 +98,7 @@ func writeRemoteWithSFTP(sshCli *ssh.Client, path string, data []byte) error {
|
||||
}
|
||||
defer sftpCli.Close()
|
||||
|
||||
if err := sftpCli.MkdirAll(filepath.ToSlash(filepath.Dir(path))); err != nil {
|
||||
if err := sftpCli.MkdirAll(xfilepath.Dir(path)); err != nil {
|
||||
return fmt.Errorf("failed to create remote directory: %w", err)
|
||||
}
|
||||
|
||||
@ -122,7 +123,7 @@ func removeRemoteWithSFTP(sshCli *ssh.Client, path string) error {
|
||||
}
|
||||
defer sftpCli.Close()
|
||||
|
||||
if err := sftpCli.MkdirAll(filepath.ToSlash(filepath.Dir(path))); err != nil {
|
||||
if err := sftpCli.MkdirAll(xfilepath.Dir(path)); err != nil {
|
||||
return fmt.Errorf("failed to create remote directory: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ export interface AccessEditDrawerProps {
|
||||
const AccessEditDrawer = ({ afterClose, afterSubmit, mode, data, loading, trigger, usage, ...props }: AccessEditDrawerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { notification } = App.useApp();
|
||||
const { message, notification } = App.useApp();
|
||||
|
||||
const { createAccess, updateAccess } = useAccessesStore(useZustandShallowSelector(["createAccess", "updateAccess"]));
|
||||
|
||||
@ -57,6 +57,8 @@ const AccessEditDrawer = ({ afterClose, afterSubmit, mode, data, loading, trigge
|
||||
try {
|
||||
await formInst.validateFields();
|
||||
} catch (err) {
|
||||
message.warning(t("common.errmsg.form_invalid"));
|
||||
|
||||
setFormPending(false);
|
||||
throw err;
|
||||
}
|
||||
|
||||
@ -234,16 +234,6 @@ const BizDeployNodeConfigFieldsProviderLocal = () => {
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={[parentNamePath, "certPath"]}
|
||||
initialValue={initialValues.certPath}
|
||||
label={t("workflow_node.deploy.form.local_cert_path.label")}
|
||||
extra={t("workflow_node.deploy.form.local_cert_path.help")}
|
||||
rules={[formRule]}
|
||||
>
|
||||
<Input placeholder={t("workflow_node.deploy.form.local_cert_path.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Show when={fieldFormat === FORMAT_PEM}>
|
||||
<Form.Item
|
||||
name={[parentNamePath, "keyPath"]}
|
||||
@ -254,7 +244,19 @@ const BizDeployNodeConfigFieldsProviderLocal = () => {
|
||||
>
|
||||
<Input placeholder={t("workflow_node.deploy.form.local_key_path.placeholder")} />
|
||||
</Form.Item>
|
||||
</Show>
|
||||
|
||||
<Form.Item
|
||||
name={[parentNamePath, "certPath"]}
|
||||
initialValue={initialValues.certPath}
|
||||
label={t(`workflow_node.deploy.form.local_${fieldFormat === FORMAT_PEM ? "fullchaincert" : "cert"}_path.label`)}
|
||||
extra={t("workflow_node.deploy.form.local_cert_path.help")}
|
||||
rules={[formRule]}
|
||||
>
|
||||
<Input placeholder={t(`workflow_node.deploy.form.local_${fieldFormat === FORMAT_PEM ? "fullchaincert" : "cert"}_path.placeholder`)} />
|
||||
</Form.Item>
|
||||
|
||||
<Show when={fieldFormat === FORMAT_PEM}>
|
||||
<Form.Item
|
||||
name={[parentNamePath, "certPathForServerOnly"]}
|
||||
initialValue={initialValues.certPathForServerOnly}
|
||||
@ -411,9 +413,13 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
|
||||
return z
|
||||
.object({
|
||||
format: z.literal([FORMAT_PEM, FORMAT_PFX, FORMAT_JKS], t("workflow_node.deploy.form.local_format.placeholder")),
|
||||
keyPath: z
|
||||
.string()
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||||
.nullish(),
|
||||
certPath: z
|
||||
.string()
|
||||
.min(1, t("workflow_node.deploy.form.local_cert_path.tooltip"))
|
||||
.min(1, t("workflow_node.deploy.form.local_cert_path.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 })),
|
||||
certPathForServerOnly: z
|
||||
.string()
|
||||
@ -423,10 +429,6 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
|
||||
.string()
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||||
.nullish(),
|
||||
keyPath: z
|
||||
.string()
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||||
.nullish(),
|
||||
pfxPassword: z
|
||||
.string()
|
||||
.max(64, t("common.errmsg.string_max", { max: 256 }))
|
||||
@ -460,7 +462,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
|
||||
if (!values.keyPath?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: t("workflow_node.deploy.form.ssh_key_path.tooltip"),
|
||||
message: t("workflow_node.deploy.form.local_key_path.placeholder"),
|
||||
path: ["keyPath"],
|
||||
});
|
||||
}
|
||||
@ -472,7 +474,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
|
||||
if (!values.pfxPassword?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: t("workflow_node.deploy.form.ssh_pfx_password.tooltip"),
|
||||
message: t("workflow_node.deploy.form.local_pfx_password.placeholder"),
|
||||
path: ["pfxPassword"],
|
||||
});
|
||||
}
|
||||
@ -484,7 +486,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
|
||||
if (!values.jksAlias?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: t("workflow_node.deploy.form.ssh_jks_alias.tooltip"),
|
||||
message: t("workflow_node.deploy.form.local_jks_alias.placeholder"),
|
||||
path: ["jksAlias"],
|
||||
});
|
||||
}
|
||||
@ -492,7 +494,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
|
||||
if (!values.jksKeypass?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: t("workflow_node.deploy.form.ssh_jks_keypass.tooltip"),
|
||||
message: t("workflow_node.deploy.form.local_jks_keypass.placeholder"),
|
||||
path: ["jksKeypass"],
|
||||
});
|
||||
}
|
||||
@ -500,7 +502,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
|
||||
if (!values.jksStorepass?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: t("workflow_node.deploy.form.ssh_jks_storepass.tooltip"),
|
||||
message: t("workflow_node.deploy.form.local_jks_storepass.placeholder"),
|
||||
path: ["jksStorepass"],
|
||||
});
|
||||
}
|
||||
|
||||
@ -266,7 +266,7 @@ const BizDeployNodeConfigFieldsProviderSSH = () => {
|
||||
<Select
|
||||
options={[FORMAT_PEM, FORMAT_PFX, FORMAT_JKS].map((s) => ({
|
||||
key: s,
|
||||
label: <span className="font-mono">{t(`workflow_node.deploy.form.ssh_format.option.${s.toLowerCase()}.label`)}</span>,
|
||||
label: t(`workflow_node.deploy.form.ssh_format.option.${s.toLowerCase()}.label`),
|
||||
value: s,
|
||||
}))}
|
||||
placeholder={t("workflow_node.deploy.form.ssh_format.placeholder")}
|
||||
@ -274,16 +274,6 @@ const BizDeployNodeConfigFieldsProviderSSH = () => {
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={[parentNamePath, "certPath"]}
|
||||
initialValue={initialValues.certPath}
|
||||
label={t("workflow_node.deploy.form.ssh_cert_path.label")}
|
||||
extra={t("workflow_node.deploy.form.ssh_cert_path.help")}
|
||||
rules={[formRule]}
|
||||
>
|
||||
<Input placeholder={t("workflow_node.deploy.form.ssh_cert_path.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Show when={fieldFormat === FORMAT_PEM}>
|
||||
<Form.Item
|
||||
name={[parentNamePath, "keyPath"]}
|
||||
@ -294,7 +284,19 @@ const BizDeployNodeConfigFieldsProviderSSH = () => {
|
||||
>
|
||||
<Input placeholder={t("workflow_node.deploy.form.ssh_key_path.placeholder")} />
|
||||
</Form.Item>
|
||||
</Show>
|
||||
|
||||
<Form.Item
|
||||
name={[parentNamePath, "certPath"]}
|
||||
initialValue={initialValues.certPath}
|
||||
label={t(`workflow_node.deploy.form.ssh_${fieldFormat === FORMAT_PEM ? "fullchaincert" : "cert"}_path.label`)}
|
||||
extra={t("workflow_node.deploy.form.ssh_cert_path.help")}
|
||||
rules={[formRule]}
|
||||
>
|
||||
<Input placeholder={t(`workflow_node.deploy.form.ssh_${fieldFormat === FORMAT_PEM ? "fullchaincert" : "cert"}_path.placeholder`)} />
|
||||
</Form.Item>
|
||||
|
||||
<Show when={fieldFormat === FORMAT_PEM}>
|
||||
<Form.Item
|
||||
name={[parentNamePath, "certPathForServerOnly"]}
|
||||
initialValue={initialValues.certPathForServerOnly}
|
||||
@ -453,14 +455,14 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
|
||||
return z
|
||||
.object({
|
||||
format: z.literal([FORMAT_PEM, FORMAT_PFX, FORMAT_JKS], t("workflow_node.deploy.form.ssh_format.placeholder")),
|
||||
certPath: z
|
||||
.string()
|
||||
.min(1, t("workflow_node.deploy.form.ssh_cert_path.tooltip"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 })),
|
||||
keyPath: z
|
||||
.string()
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||||
.nullish(),
|
||||
certPath: z
|
||||
.string()
|
||||
.min(1, t("workflow_node.deploy.form.ssh_cert_path.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 })),
|
||||
certPathForServerOnly: z
|
||||
.string()
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||||
@ -502,7 +504,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
|
||||
if (!values.keyPath?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: t("workflow_node.deploy.form.ssh_key_path.tooltip"),
|
||||
message: t("workflow_node.deploy.form.ssh_key_path.placeholder"),
|
||||
path: ["keyPath"],
|
||||
});
|
||||
}
|
||||
@ -514,7 +516,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
|
||||
if (!values.pfxPassword?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: t("workflow_node.deploy.form.ssh_pfx_password.tooltip"),
|
||||
message: t("workflow_node.deploy.form.ssh_pfx_password.placeholder"),
|
||||
path: ["pfxPassword"],
|
||||
});
|
||||
}
|
||||
@ -526,7 +528,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
|
||||
if (!values.jksAlias?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: t("workflow_node.deploy.form.ssh_jks_alias.tooltip"),
|
||||
message: t("workflow_node.deploy.form.ssh_jks_alias.placeholder"),
|
||||
path: ["jksAlias"],
|
||||
});
|
||||
}
|
||||
@ -534,7 +536,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
|
||||
if (!values.jksKeypass?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: t("workflow_node.deploy.form.ssh_jks_keypass.tooltip"),
|
||||
message: t("workflow_node.deploy.form.ssh_jks_keypass.placeholder"),
|
||||
path: ["jksKeypass"],
|
||||
});
|
||||
}
|
||||
@ -542,7 +544,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
|
||||
if (!values.jksStorepass?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: t("workflow_node.deploy.form.ssh_jks_storepass.tooltip"),
|
||||
message: t("workflow_node.deploy.form.ssh_jks_storepass.placeholder"),
|
||||
path: ["jksStorepass"],
|
||||
});
|
||||
}
|
||||
|
||||
@ -84,7 +84,7 @@ export const NodeConfigDrawer = ({ children, afterClose, anchor, footer = true,
|
||||
try {
|
||||
await formInst.validateFields();
|
||||
} catch (err) {
|
||||
message.warning(t("workflow.detail.design.drawer.errmsg.invalid_form"));
|
||||
message.warning(t("common.errmsg.form_invalid"));
|
||||
|
||||
setFormPending(false);
|
||||
throw err;
|
||||
|
||||
@ -174,7 +174,9 @@ export const defaultNodeConfigForBizNotify = (): Partial<WorkflowNodeConfigForBi
|
||||
};
|
||||
|
||||
export const newNodeId = (): string => {
|
||||
return nanoid();
|
||||
return nanoid()
|
||||
.replace(/^[_-]+/g, "")
|
||||
.replace(/[_-]+$/g, "");
|
||||
};
|
||||
|
||||
export const newNode = (type: WorkflowNodeType, { i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }): WorkflowNode => {
|
||||
|
||||
@ -44,6 +44,7 @@
|
||||
"common.errmsg.port_invalid": "Please enter a valid port",
|
||||
"common.errmsg.ip_invalid": "Please enter a valid IP address",
|
||||
"common.errmsg.url_invalid": "Please enter a valid URL",
|
||||
"common.errmsg.form_invalid": "Please check the form content",
|
||||
|
||||
"common.notifier.bark": "Bark",
|
||||
"common.notifier.dingtalk": "DingTalk",
|
||||
|
||||
@ -76,7 +76,6 @@
|
||||
"workflow.detail.design.drawer.node_id.label": "Node ID: ",
|
||||
"workflow.detail.design.drawer.disabled.on.tooltip": "Disable",
|
||||
"workflow.detail.design.drawer.disabled.off.tooltip": "Enable",
|
||||
"workflow.detail.design.drawer.errmsg.invalid_form": "Please check the form content",
|
||||
"workflow.detail.design.action.publish.button": "Publish",
|
||||
"workflow.detail.design.action.publish.modal.title": "Publish changes",
|
||||
"workflow.detail.design.action.publish.modal.content": "Are you sure to publish your changes?",
|
||||
|
||||
@ -11,10 +11,10 @@
|
||||
"workflow_node.start.form.trigger.placeholder": "Please select trigger",
|
||||
"workflow_node.start.form.trigger.option.scheduled.label": "Scheduled",
|
||||
"workflow_node.start.form.trigger.option.manual.label": "Manual",
|
||||
"workflow_node.start.form.trigger_cron.label": "Cron expression",
|
||||
"workflow_node.start.form.trigger_cron.placeholder": "Please enter cron expression",
|
||||
"workflow_node.start.form.trigger_cron.errmsg.invalid": "Please enter a valid cron expression",
|
||||
"workflow_node.start.form.trigger_cron.tooltip": "Exactly 5 space separated segments.",
|
||||
"workflow_node.start.form.trigger_cron.label": "CRON expression",
|
||||
"workflow_node.start.form.trigger_cron.placeholder": "Please enter CRON expression",
|
||||
"workflow_node.start.form.trigger_cron.errmsg.invalid": "Please enter a valid CRON expression",
|
||||
"workflow_node.start.form.trigger_cron.tooltip": "Exactly 5 space separated segments, in standard <em>crontab</em> rules.",
|
||||
"workflow_node.start.form.trigger_cron.help": "Expected execution time for the last 5 times (the actual time zone is based on the server):",
|
||||
"workflow_node.start.form.trigger_cron.guide": "If you have multiple workflows, it is recommended to set them to run at different times of the day instead of always running at a specific time. And please don't always set it to midnight every day to avoid spikes in traffic. <br><br>Reference links:<br>1. <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">Let’s Encrypt rate limits</a><br>2. <a href=\"https://letsencrypt.org/docs/faq/#why-should-my-let-s-encrypt-acme-client-run-at-a-random-time\" target=\"_blank\">Why should my Let’s Encrypt (ACME) client run at a random time?</a>",
|
||||
|
||||
@ -612,17 +612,19 @@
|
||||
"workflow_node.deploy.form.local_format.option.pem.label": "PEM (*.pem, *.crt, *.key)",
|
||||
"workflow_node.deploy.form.local_format.option.pfx.label": "PFX (*.pfx, *.p12)",
|
||||
"workflow_node.deploy.form.local_format.option.jks.label": "JKS (*.jks)",
|
||||
"workflow_node.deploy.form.local_cert_path.label": "Certificate file saving path",
|
||||
"workflow_node.deploy.form.local_cert_path.placeholder": "Please enter saving path for certificate file",
|
||||
"workflow_node.deploy.form.local_cert_path.help": "Notes: It should include the full file path, not just the directory.",
|
||||
"workflow_node.deploy.form.local_key_path.label": "Certificate's private key file saving path",
|
||||
"workflow_node.deploy.form.local_key_path.placeholder": "Please enter saving path for certificate's private key file",
|
||||
"workflow_node.deploy.form.local_key_path.label": "Private key file path",
|
||||
"workflow_node.deploy.form.local_key_path.placeholder": "Please enter the local path for private key file",
|
||||
"workflow_node.deploy.form.local_key_path.help": "Notes: It should include the full file path, not just the directory.",
|
||||
"workflow_node.deploy.form.local_servercert_path.label": "Server certificate file saving path (Optional)",
|
||||
"workflow_node.deploy.form.local_servercert_path.placeholder": "Please enter saving path for server certificate file",
|
||||
"workflow_node.deploy.form.local_cert_path.label": "Certificate file path",
|
||||
"workflow_node.deploy.form.local_cert_path.placeholder": "Please enter the local path for certificate file",
|
||||
"workflow_node.deploy.form.local_cert_path.help": "Notes: It should include the full file path, not just the directory.",
|
||||
"workflow_node.deploy.form.local_fullchaincert_path.label": "Bundled fullchain certificate file path",
|
||||
"workflow_node.deploy.form.local_fullchaincert_path.placeholder": "Please enter the local path for bundled fullchain certificate",
|
||||
"workflow_node.deploy.form.local_servercert_path.label": "Server certificate file path (Optional)",
|
||||
"workflow_node.deploy.form.local_servercert_path.placeholder": "Please enter the local path for server certificate file",
|
||||
"workflow_node.deploy.form.local_servercert_path.help": "Notes: It should include the full file path, not just the directory.",
|
||||
"workflow_node.deploy.form.local_intermediacert_path.label": "Intermediate CA certificate file saving path (Optional)",
|
||||
"workflow_node.deploy.form.local_intermediacert_path.placeholder": "Please enter saving path for intermediate CA certificate file",
|
||||
"workflow_node.deploy.form.local_intermediacert_path.label": "Intermediate CA certificate file path (Optional)",
|
||||
"workflow_node.deploy.form.local_intermediacert_path.placeholder": "Please enter the local path for intermediate CA certificate file",
|
||||
"workflow_node.deploy.form.local_intermediacert_path.help": "Notes: It should include the full file path, not just the directory.",
|
||||
"workflow_node.deploy.form.local_pfx_password.label": "PFX password",
|
||||
"workflow_node.deploy.form.local_pfx_password.placeholder": "Please enter PFX password",
|
||||
@ -690,17 +692,19 @@
|
||||
"workflow_node.deploy.form.ssh_format.option.pem.label": "PEM (*.pem, *.crt, *.key)",
|
||||
"workflow_node.deploy.form.ssh_format.option.pfx.label": "PFX (*.pfx, *.p12)",
|
||||
"workflow_node.deploy.form.ssh_format.option.jks.label": "JKS (*.jks)",
|
||||
"workflow_node.deploy.form.ssh_cert_path.label": "Certificate file uploading path",
|
||||
"workflow_node.deploy.form.ssh_cert_path.placeholder": "Please enter uploading path for certificate file",
|
||||
"workflow_node.deploy.form.ssh_cert_path.help": "Notes: It should include the full file path, not just the directory.",
|
||||
"workflow_node.deploy.form.ssh_key_path.label": "Certificate's private key file uploading path",
|
||||
"workflow_node.deploy.form.ssh_key_path.placeholder": "Please enter uploading path for certificate's private key file",
|
||||
"workflow_node.deploy.form.ssh_key_path.label": "Private key file path",
|
||||
"workflow_node.deploy.form.ssh_key_path.placeholder": "Please enter the remote path for private key file",
|
||||
"workflow_node.deploy.form.ssh_key_path.help": "Notes: It should include the full file path, not just the directory.",
|
||||
"workflow_node.deploy.form.ssh_servercert_path.label": "Server certificate file uploading path (Optional)",
|
||||
"workflow_node.deploy.form.ssh_servercert_path.placeholder": "Please enter uploading path for server certificate file",
|
||||
"workflow_node.deploy.form.ssh_cert_path.label": "Certificate file path",
|
||||
"workflow_node.deploy.form.ssh_cert_path.placeholder": "Please enter the remote path for certificate",
|
||||
"workflow_node.deploy.form.ssh_cert_path.help": "Notes: It should include the full file path, not just the directory.",
|
||||
"workflow_node.deploy.form.ssh_fullchaincert_path.label": "Bundled fullchain certificate file path",
|
||||
"workflow_node.deploy.form.ssh_fullchaincert_path.placeholder": "Please enter the remote path for bundled fullchain certificate",
|
||||
"workflow_node.deploy.form.ssh_servercert_path.label": "Server certificate file path (Optional)",
|
||||
"workflow_node.deploy.form.ssh_servercert_path.placeholder": "Please enter the remote path for server certificate file",
|
||||
"workflow_node.deploy.form.ssh_servercert_path.help": "Notes: It should include the full file path, not just the directory.",
|
||||
"workflow_node.deploy.form.ssh_intermediacert_path.label": "Intermediate CA certificate file uploading path (Optional)",
|
||||
"workflow_node.deploy.form.ssh_intermediacert_path.placeholder": "Please enter uploading path for intermediate CA certificate file",
|
||||
"workflow_node.deploy.form.ssh_intermediacert_path.label": "Intermediate CA certificate file path (Optional)",
|
||||
"workflow_node.deploy.form.ssh_intermediacert_path.placeholder": "Please enter the remote path for intermediate CA certificate file",
|
||||
"workflow_node.deploy.form.ssh_intermediacert_path.help": "Notes: It should include the full file path, not just the directory.",
|
||||
"workflow_node.deploy.form.ssh_pfx_password.label": "PFX password",
|
||||
"workflow_node.deploy.form.ssh_pfx_password.placeholder": "Please enter PFX password",
|
||||
|
||||
@ -44,6 +44,7 @@
|
||||
"common.errmsg.port_invalid": "请输入正确的端口号",
|
||||
"common.errmsg.ip_invalid": "请输入正确的 IP 地址",
|
||||
"common.errmsg.url_invalid": "请输入正确的 URL 地址",
|
||||
"common.errmsg.form_invalid": "请检查表单内容",
|
||||
|
||||
"common.notifier.bark": "Bark",
|
||||
"common.notifier.dingtalk": "钉钉",
|
||||
|
||||
@ -76,7 +76,6 @@
|
||||
"workflow.detail.design.drawer.node_id.label": "节点 ID:",
|
||||
"workflow.detail.design.drawer.disabled.on.tooltip": "禁用",
|
||||
"workflow.detail.design.drawer.disabled.off.tooltip": "启用",
|
||||
"workflow.detail.design.drawer.errmsg.invalid_form": "请检查表单内容",
|
||||
"workflow.detail.design.action.publish.button": "发布更改",
|
||||
"workflow.detail.design.action.publish.modal.title": "发布更改",
|
||||
"workflow.detail.design.action.publish.modal.content": "确定要发布更改吗?",
|
||||
|
||||
@ -11,10 +11,10 @@
|
||||
"workflow_node.start.form.trigger.placeholder": "请选择触发方式",
|
||||
"workflow_node.start.form.trigger.option.scheduled.label": "定时触发",
|
||||
"workflow_node.start.form.trigger.option.manual.label": "手动触发",
|
||||
"workflow_node.start.form.trigger_cron.label": "Cron 表达式",
|
||||
"workflow_node.start.form.trigger_cron.placeholder": "请输入 Cron 表达式",
|
||||
"workflow_node.start.form.trigger_cron.errmsg.invalid": "请输入正确的 Cron 表达式",
|
||||
"workflow_node.start.form.trigger_cron.tooltip": "五段式表达式。<br>支持使用任意值(即 <strong>*</strong>)、值列表分隔符(即 <strong>,</strong>)、值的范围(即 <strong>-</strong>)、步骤值(即 <strong>/</strong>)等四种表达式。",
|
||||
"workflow_node.start.form.trigger_cron.label": "CRON 表达式",
|
||||
"workflow_node.start.form.trigger_cron.placeholder": "请输入 CRON 表达式",
|
||||
"workflow_node.start.form.trigger_cron.errmsg.invalid": "请输入正确的 CRON 表达式",
|
||||
"workflow_node.start.form.trigger_cron.tooltip": "五段式表达式,使用 <em>crontab</em> 标准语法规则。<br>支持使用任意值(即 <strong>*</strong>)、值列表分隔符(即 <strong>,</strong>)、值的范围(即 <strong>-</strong>)、步骤值(即 <strong>/</strong>)等四种表达式。",
|
||||
"workflow_node.start.form.trigger_cron.help": "预计最近 5 次运行时间(实际时区以服务器设置为准):",
|
||||
"workflow_node.start.form.trigger_cron.guide": "如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。也不要总是设置为每日零时,以免遭遇证书颁发机构的流量高峰。<br><br>参考链接:<br>1. <a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">Let’s Encrypt 速率限制</a><br>2. <a href=\"https://letsencrypt.org/zh-cn/docs/faq/#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E7%9A%84-let-s-encrypt-acme-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%E5%BA%94%E5%BD%93%E9%9A%8F%E6%9C%BA\" target=\"_blank\">为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?</a>",
|
||||
|
||||
@ -610,17 +610,19 @@
|
||||
"workflow_node.deploy.form.local_format.option.pem.label": "PEM 格式(*.pem, *.crt, *.key)",
|
||||
"workflow_node.deploy.form.local_format.option.pfx.label": "PFX 格式(*.pfx, *.p12)",
|
||||
"workflow_node.deploy.form.local_format.option.jks.label": "JKS 格式(*.jks)",
|
||||
"workflow_node.deploy.form.local_cert_path.label": "证书文件保存路径",
|
||||
"workflow_node.deploy.form.local_cert_path.placeholder": "请输入证书文件保存路径",
|
||||
"workflow_node.deploy.form.local_cert_path.help": "注意:路径需包含完整的文件名,而不是只有目录。",
|
||||
"workflow_node.deploy.form.local_key_path.label": "证书私钥文件保存路径",
|
||||
"workflow_node.deploy.form.local_key_path.placeholder": "请输入证书私钥文件保存路径",
|
||||
"workflow_node.deploy.form.local_key_path.label": "私钥文件路径",
|
||||
"workflow_node.deploy.form.local_key_path.placeholder": "请输入私钥文件本地路径",
|
||||
"workflow_node.deploy.form.local_key_path.help": "注意:路径需包含完整的文件名,而不是只有目录。",
|
||||
"workflow_node.deploy.form.local_servercert_path.label": "服务器证书文件保存路径(可选)",
|
||||
"workflow_node.deploy.form.local_servercert_path.placeholder": "请输入服务器证书文件保存路径",
|
||||
"workflow_node.deploy.form.local_cert_path.label": "证书文件路径",
|
||||
"workflow_node.deploy.form.local_cert_path.placeholder": "请输入证书文件本地路径",
|
||||
"workflow_node.deploy.form.local_cert_path.help": "注意:路径需包含完整的文件名,而不是只有目录。",
|
||||
"workflow_node.deploy.form.local_fullchaincert_path.label": "证书链文件路径",
|
||||
"workflow_node.deploy.form.local_fullchaincert_path.placeholder": "请输入证书链文件本地路径",
|
||||
"workflow_node.deploy.form.local_servercert_path.label": "服务器证书文件路径(可选)",
|
||||
"workflow_node.deploy.form.local_servercert_path.placeholder": "请输入服务器证书文件本地路径",
|
||||
"workflow_node.deploy.form.local_servercert_path.help": "注意:路径需包含完整的文件名,而不是只有目录。不填写时将不会保存服务器证书。",
|
||||
"workflow_node.deploy.form.local_intermediacert_path.label": "中间证书文件保存路径(可选)",
|
||||
"workflow_node.deploy.form.local_intermediacert_path.placeholder": "请输入中间证书文件保存路径",
|
||||
"workflow_node.deploy.form.local_intermediacert_path.label": "中间证书文件路径(可选)",
|
||||
"workflow_node.deploy.form.local_intermediacert_path.placeholder": "请输入中间证书文件本地路径",
|
||||
"workflow_node.deploy.form.local_intermediacert_path.help": "注意:路径需包含完整的文件名,而不是只有目录。不填写时将不会保存中间证书。",
|
||||
"workflow_node.deploy.form.local_pfx_password.label": "PFX 导出密码",
|
||||
"workflow_node.deploy.form.local_pfx_password.placeholder": "请输入 PFX 导出密码",
|
||||
@ -688,19 +690,21 @@
|
||||
"workflow_node.deploy.form.ssh_format.option.pem.label": "PEM 格式(*.pem, *.crt, *.key)",
|
||||
"workflow_node.deploy.form.ssh_format.option.pfx.label": "PFX 格式(*.pfx, *.p12)",
|
||||
"workflow_node.deploy.form.ssh_format.option.jks.label": "JKS 格式(*.jks)",
|
||||
"workflow_node.deploy.form.ssh_cert_path.label": "证书文件上传路径",
|
||||
"workflow_node.deploy.form.ssh_cert_path.placeholder": "请输入证书文件上传路径",
|
||||
"workflow_node.deploy.form.ssh_cert_path.help": "注意:路径需包含完整的文件名,而不是只有目录。",
|
||||
"workflow_node.deploy.form.ssh_key_path.label": "证书私钥文件上传路径",
|
||||
"workflow_node.deploy.form.ssh_key_path.placeholder": "请输入证书私钥文件上传路径",
|
||||
"workflow_node.deploy.form.ssh_key_path.label": "私钥文件路径",
|
||||
"workflow_node.deploy.form.ssh_key_path.placeholder": "请输入私钥文件远程路径",
|
||||
"workflow_node.deploy.form.ssh_key_path.help": "注意:路径需包含完整的文件名,而不是只有目录。",
|
||||
"workflow_node.deploy.form.ssh_pfx_password.label": "PFX 导出密码",
|
||||
"workflow_node.deploy.form.ssh_servercert_path.label": "服务器证书文件上传路径(可选)",
|
||||
"workflow_node.deploy.form.ssh_servercert_path.placeholder": "请输入服务器证书文件上传路径",
|
||||
"workflow_node.deploy.form.ssh_cert_path.label": "证书文件路径",
|
||||
"workflow_node.deploy.form.ssh_cert_path.placeholder": "请输入证书文件远程路径",
|
||||
"workflow_node.deploy.form.ssh_cert_path.help": "注意:路径需包含完整的文件名,而不是只有目录。",
|
||||
"workflow_node.deploy.form.ssh_fullchaincert_path.label": "证书链文件路径",
|
||||
"workflow_node.deploy.form.ssh_fullchaincert_path.placeholder": "请输入证书链文件远程路径",
|
||||
"workflow_node.deploy.form.ssh_servercert_path.label": "服务器证书文件路径(可选)",
|
||||
"workflow_node.deploy.form.ssh_servercert_path.placeholder": "请输入服务器证书文件远程路径",
|
||||
"workflow_node.deploy.form.ssh_servercert_path.help": "注意:路径需包含完整的文件名,而不是只有目录。不填写时将不上传服务器证书。",
|
||||
"workflow_node.deploy.form.ssh_intermediacert_path.label": "中间证书文件上传路径(可选)",
|
||||
"workflow_node.deploy.form.ssh_intermediacert_path.placeholder": "请输入中间证书文件上传路径",
|
||||
"workflow_node.deploy.form.ssh_intermediacert_path.label": "中间证书文件路径(可选)",
|
||||
"workflow_node.deploy.form.ssh_intermediacert_path.placeholder": "请输入中间证书文件远程路径",
|
||||
"workflow_node.deploy.form.ssh_intermediacert_path.help": "注意:路径需包含完整的文件名,而不是只有目录。不填写时将不上传中间证书。",
|
||||
"workflow_node.deploy.form.ssh_pfx_password.label": "PFX 导出密码",
|
||||
"workflow_node.deploy.form.ssh_pfx_password.placeholder": "请输入 PFX 导出密码",
|
||||
"workflow_node.deploy.form.ssh_pfx_password.tooltip": "这是什么?请参阅 <a href=\"https://learn.microsoft.com/zh-cn/windows-hardware/drivers/install/personal-information-exchange---pfx--files\" target=\"_blank\">https://learn.microsoft.com/zh-cn/windows-hardware/drivers/install/personal-information-exchange---pfx--files</a>",
|
||||
"workflow_node.deploy.form.ssh_jks_alias.label": "JKS 别名",
|
||||
|
||||
@ -268,6 +268,12 @@ const WorkflowRunHistoryTable = ({ className, style }: { className?: string; sty
|
||||
width: 48,
|
||||
render: (_, __, index) => index + 1,
|
||||
},
|
||||
{
|
||||
key: "id",
|
||||
title: "ID",
|
||||
width: 160,
|
||||
render: (_, record) => <span className="font-mono">{record.id}</span>,
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
title: t("workflow.props.name"),
|
||||
|
||||
@ -313,7 +313,7 @@ const WorkflowList = () => {
|
||||
|
||||
const handleRecordActiveChange = async (workflow: WorkflowModel) => {
|
||||
try {
|
||||
if (!workflow.enabled && !workflow.graphContent) {
|
||||
if (!workflow.enabled && !workflow.hasContent) {
|
||||
message.warning(t("workflow.action.enable.errmsg.unpublished"));
|
||||
return;
|
||||
}
|
||||
@ -326,7 +326,8 @@ const WorkflowList = () => {
|
||||
setTableData((prev) => {
|
||||
return prev.map((item) => {
|
||||
if (item.id === workflow.id) {
|
||||
return resp;
|
||||
item.enabled = resp.enabled;
|
||||
item.updated = resp.updated;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
@ -4,7 +4,16 @@ export const validCronExpression = (expr: string): boolean => {
|
||||
try {
|
||||
CronExpressionParser.parse(expr);
|
||||
|
||||
if (expr.trim().split(" ").length !== 5) return false; // pocketbase 后端仅支持五段式的表达式
|
||||
// pocketbase 后端仅支持标准 crontab 形式的表达式
|
||||
// 这里转译了来自 pocketbase 的 golang 代码来验证
|
||||
const segments = expr.trim().split(" ");
|
||||
if (segments.length !== 5) return false;
|
||||
parseCronSegment(segments[0], 0, 59);
|
||||
parseCronSegment(segments[1], 0, 23);
|
||||
parseCronSegment(segments[2], 1, 31);
|
||||
parseCronSegment(segments[3], 1, 12);
|
||||
parseCronSegment(segments[4], 0, 6);
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@ -19,3 +28,83 @@ export const getNextCronExecutions = (expr: string, times = 1): Date[] => {
|
||||
|
||||
return cron.take(times).map((date) => date.toDate());
|
||||
};
|
||||
|
||||
// transpile from:
|
||||
// https://github.com/pocketbase/pocketbase/blob/5d964c1b1d020f425299b32df03ecf44e0a0502e/tools/cron/schedule.go#L141-L218
|
||||
function parseCronSegment(segment: string, min: number, max: number): Set<number> {
|
||||
const slots = new Set<number>();
|
||||
|
||||
const list = segment.split(",");
|
||||
for (const p of list) {
|
||||
const stepParts = p.split("/");
|
||||
|
||||
let step: number;
|
||||
switch (stepParts.length) {
|
||||
case 1:
|
||||
{
|
||||
step = 1;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
{
|
||||
const parsedStep = parseInt(stepParts[1], 10);
|
||||
if (isNaN(parsedStep) || parsedStep < 1 || parsedStep > max) {
|
||||
throw new Error(`invalid segment step boundary - the step must be between 1 and the ${max}`);
|
||||
}
|
||||
step = parsedStep;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("invalid segment step format - must be in the format */n or 1-30/n");
|
||||
}
|
||||
|
||||
let rangeMin: number, rangeMax: number;
|
||||
if (stepParts[0] === "*") {
|
||||
rangeMin = min;
|
||||
rangeMax = max;
|
||||
} else {
|
||||
const rangeParts = stepParts[0].split("-");
|
||||
switch (rangeParts.length) {
|
||||
case 1:
|
||||
{
|
||||
if (step !== 1) {
|
||||
throw new Error("invalid segment step - step > 1 could be used only with the wildcard or range format");
|
||||
}
|
||||
const parsed = parseInt(rangeParts[0], 10);
|
||||
if (isNaN(parsed) || parsed < min || parsed > max) {
|
||||
throw new Error("invalid segment value - must be between the min and max of the segment");
|
||||
}
|
||||
rangeMin = parsed;
|
||||
rangeMax = rangeMin;
|
||||
}
|
||||
break;
|
||||
|
||||
case 2:
|
||||
{
|
||||
const parsedMin = parseInt(rangeParts[0], 10);
|
||||
if (isNaN(parsedMin) || parsedMin < min || parsedMin > max) {
|
||||
throw new Error(`invalid segment range minimum - must be between ${min} and ${max}`);
|
||||
}
|
||||
rangeMin = parsedMin;
|
||||
|
||||
const parsedMax = parseInt(rangeParts[1], 10);
|
||||
if (isNaN(parsedMax) || parsedMax < rangeMin || parsedMax > max) {
|
||||
throw new Error(`invalid segment range maximum - must be between ${rangeMin} and ${max}`);
|
||||
}
|
||||
rangeMax = parsedMax;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("invalid segment range format - the range must have 1 or 2 parts");
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = rangeMin; i <= rangeMax; i += step) {
|
||||
slots.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user