diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index fed38fc9b..2240df4f0 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -41,19 +41,20 @@ type Applicant interface { } type ApplicantWithWorkflowNodeConfig struct { - Node *domain.WorkflowNode - Logger *slog.Logger + WorkflowId string + Node *domain.WorkflowNode + Logger *slog.Logger } func NewWithWorkflowNode(config ApplicantWithWorkflowNodeConfig) (Applicant, error) { if config.Node == nil { - return nil, fmt.Errorf("node is nil") + return nil, fmt.Errorf("the node is nil") } - if config.Node.Type != domain.WorkflowNodeTypeApply { - return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeApply)) + if config.Node.Type != domain.WorkflowNodeTypeBizApply { + return nil, fmt.Errorf("the node type is '%s', expected '%s'", string(config.Node.Type), string(domain.WorkflowNodeTypeBizApply)) } - nodeCfg := config.Node.GetConfigForApply() + nodeCfg := config.Node.Data.Config.AsBizApply() options := &applicantProviderOptions{ Domains: lo.Filter(strings.Split(nodeCfg.Domains, ";"), func(s string, _ int) bool { return s != "" }), ContactEmail: nodeCfg.ContactEmail, @@ -109,8 +110,8 @@ func NewWithWorkflowNode(config ApplicantWithWorkflowNodeConfig) (Applicant, err options.CAProviderAccessConfig = sslProviderConfig.Config[options.CAProvider] } - certRepo := repository.NewCertificateRepository() - lastCertificate, _ := certRepo.GetByWorkflowNodeId(context.Background(), config.Node.Id) + certificateRepo := repository.NewCertificateRepository() + lastCertificate, _ := certificateRepo.GetByWorkflowIdAndNodeId(context.Background(), config.WorkflowId, config.Node.Id) if lastCertificate != nil && !lastCertificate.ACMERenewed { newCertSan := slices.Clone(options.Domains) oldCertSan := strings.Split(lastCertificate.SubjectAltNames, ";") diff --git a/internal/certificate/service.go b/internal/certificate/service.go index c844b17f9..6656ba5fd 100644 --- a/internal/certificate/service.go +++ b/internal/certificate/service.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "fmt" + "log/slog" "strings" "time" @@ -17,16 +18,6 @@ import ( xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) -type certificateRepository interface { - ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error) - GetById(ctx context.Context, id string) (*domain.Certificate, error) - DeleteWhere(ctx context.Context, exprs ...dbx.Expression) (int, error) -} - -type settingsRepository interface { - GetByName(ctx context.Context, name string) (*domain.Settings, error) -} - type CertificateService struct { certificateRepo certificateRepository settingsRepo settingsRepository @@ -41,27 +32,8 @@ func NewCertificateService(certificateRepo certificateRepository, settingsRepo s func (s *CertificateService) InitSchedule(ctx context.Context) error { // 每日清理过期证书 - app.GetScheduler().MustAdd("certificateExpiredCleanup", "0 0 * * *", func() { - settings, err := s.settingsRepo.GetByName(ctx, "persistence") - if err != nil { - app.GetLogger().Error("failed to get persistence settings", "err", err) - return - } - - persistenceSettings, _ := settings.UnmarshalContentAsPersistence() - if persistenceSettings != nil && persistenceSettings.ExpiredCertificatesMaxDaysRetention != 0 { - ret, err := s.certificateRepo.DeleteWhere( - context.Background(), - dbx.NewExp(fmt.Sprintf("validityNotAfter 0 { - app.GetLogger().Info(fmt.Sprintf("cleanup %d expired certificates", ret)) - } - } + app.GetScheduler().MustAdd("cleanupCertificateExpired", "0 0 * * *", func() { + s.cleanupExpiredCertificates(context.Background()) }) return nil @@ -218,3 +190,29 @@ func (s *CertificateService) ValidatePrivateKey(ctx context.Context, req *dtos.C IsValid: true, }, nil } + +func (s *CertificateService) cleanupExpiredCertificates(ctx context.Context) error { + settings, err := s.settingsRepo.GetByName(ctx, "persistence") + if err != nil { + app.GetLogger().Error("failed to get persistence settings", slog.Any("error", err)) + return err + } + + persistenceSettings, _ := settings.UnmarshalContentAsPersistence() + if persistenceSettings != nil && persistenceSettings.ExpiredCertificatesMaxDaysRetention != 0 { + ret, err := s.certificateRepo.DeleteWhere( + context.Background(), + dbx.NewExp(fmt.Sprintf("validityNotAfter 0 { + app.GetLogger().Info(fmt.Sprintf("cleanup %d expired certificates", ret)) + } + } + + return nil +} diff --git a/internal/certificate/service_deps.go b/internal/certificate/service_deps.go new file mode 100644 index 000000000..7b22dbbad --- /dev/null +++ b/internal/certificate/service_deps.go @@ -0,0 +1,19 @@ +package certificate + +import ( + "context" + + "github.com/pocketbase/dbx" + + "github.com/certimate-go/certimate/internal/domain" +) + +type certificateRepository interface { + ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error) + GetById(ctx context.Context, id string) (*domain.Certificate, error) + DeleteWhere(ctx context.Context, exprs ...dbx.Expression) (int, error) +} + +type settingsRepository interface { + GetByName(ctx context.Context, name string) (*domain.Settings, error) +} diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index be98eee2f..cec8e38f0 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -23,13 +23,13 @@ type DeployerWithWorkflowNodeConfig struct { func NewWithWorkflowNode(config DeployerWithWorkflowNodeConfig) (Deployer, error) { if config.Node == nil { - return nil, fmt.Errorf("node is nil") + return nil, fmt.Errorf("the node is nil") } - if config.Node.Type != domain.WorkflowNodeTypeDeploy { - return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeDeploy)) + if config.Node.Type != domain.WorkflowNodeTypeBizDeploy { + return nil, fmt.Errorf("the node type is '%s', expected '%s'", string(config.Node.Type), string(domain.WorkflowNodeTypeBizDeploy)) } - nodeCfg := config.Node.GetConfigForDeploy() + nodeCfg := config.Node.Data.Config.AsBizDeploy() options := &deployerProviderOptions{ Provider: domain.DeploymentProviderType(nodeCfg.Provider), ProviderAccessConfig: make(map[string]any), diff --git a/internal/domain/certificate.go b/internal/domain/certificate.go index ffa4b24a8..4c4266e8d 100644 --- a/internal/domain/certificate.go +++ b/internal/domain/certificate.go @@ -31,7 +31,6 @@ type Certificate struct { ACMERenewed bool `json:"acmeRenewed" db:"acmeRenewed"` WorkflowId string `json:"workflowId" db:"workflowRef"` WorkflowRunId string `json:"workflowRunId" db:"workflowRunRef"` - WorkflowOutputId string `json:"workflowOutputId" db:"workflowOutputRef"` WorkflowNodeId string `json:"workflowNodeId" db:"workflowNodeId"` DeletedAt *time.Time `json:"deleted" db:"deleted"` } @@ -111,7 +110,7 @@ func (c *Certificate) PopulateFromPEM(certPEM, privkeyPEM string) *Certificate { certX509, _ := xcert.ParseCertificateFromPEM(certPEM) if certX509 != nil { - c.PopulateFromX509(certX509) + return c.PopulateFromX509(certX509) } return c diff --git a/internal/domain/dtos/workflow.go b/internal/domain/dtos/workflow.go index 55b12954a..ea4b1fe0a 100644 --- a/internal/domain/dtos/workflow.go +++ b/internal/domain/dtos/workflow.go @@ -7,7 +7,13 @@ type WorkflowStartRunReq struct { RunTrigger domain.WorkflowTriggerType `json:"trigger"` } +type WorkflowStartRunResp struct { + RunId string `json:"runId"` +} + type WorkflowCancelRunReq struct { WorkflowId string `json:"-"` RunId string `json:"-"` } + +type WorkflowCancelRunResp struct{} diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 20048af27..236db82c9 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -2,6 +2,7 @@ package domain import ( "encoding/json" + "fmt" "time" "github.com/certimate-go/certimate/internal/domain/expr" @@ -17,30 +18,56 @@ type Workflow struct { Trigger WorkflowTriggerType `json:"trigger" db:"trigger"` TriggerCron string `json:"triggerCron" db:"triggerCron"` Enabled bool `json:"enabled" db:"enabled"` - Content *WorkflowNode `json:"content" db:"content"` - Draft *WorkflowNode `json:"draft" db:"draft"` + GraphDraft *WorkflowGraph `json:"graphDraft" db:"graphDraft"` + GraphContent *WorkflowGraph `json:"graphContent" db:"graphContent"` HasDraft bool `json:"hasDraft" db:"hasDraft"` + HasContent bool `json:"hasContent" db:"hasContent"` LastRunId string `json:"lastRunId" db:"lastRunRef"` LastRunStatus WorkflowRunStatusType `json:"lastRunStatus" db:"lastRunStatus"` LastRunTime time.Time `json:"lastRunTime" db:"lastRunTime"` } -type WorkflowNodeType string +type WorkflowGraph struct { + Nodes []*WorkflowNode `json:"nodes"` +} -const ( - WorkflowNodeTypeStart = WorkflowNodeType("start") - WorkflowNodeTypeEnd = WorkflowNodeType("end") - WorkflowNodeTypeApply = WorkflowNodeType("apply") - WorkflowNodeTypeUpload = WorkflowNodeType("upload") - WorkflowNodeTypeMonitor = WorkflowNodeType("monitor") - WorkflowNodeTypeDeploy = WorkflowNodeType("deploy") - WorkflowNodeTypeNotify = WorkflowNodeType("notify") - WorkflowNodeTypeBranch = WorkflowNodeType("branch") - WorkflowNodeTypeCondition = WorkflowNodeType("condition") - WorkflowNodeTypeExecuteResultBranch = WorkflowNodeType("execute_result_branch") - WorkflowNodeTypeExecuteSuccess = WorkflowNodeType("execute_success") - WorkflowNodeTypeExecuteFailure = WorkflowNodeType("execute_failure") -) +func (g *WorkflowGraph) GetNodeById(nodeId string) (*WorkflowNode, bool) { + return g.getNodeInBlocksById(g.Nodes, nodeId) +} + +func (g *WorkflowGraph) getNodeInBlocksById(blocks []*WorkflowNode, nodeId string) (*WorkflowNode, bool) { + for _, node := range blocks { + if node.Id == nodeId { + return node, true + } + + if len(node.Blocks) > 0 { + if found, ok := g.getNodeInBlocksById(node.Blocks, nodeId); ok { + return found, true + } + } + } + + return nil, false +} + +func (g *WorkflowGraph) Verify() error { + if len(g.Nodes) < 2 { + return fmt.Errorf("invalid nodes length of graph") + } else if g.Nodes[0].Type != WorkflowNodeTypeStart { + return fmt.Errorf("the first node is not a start node") + } else if g.Nodes[len(g.Nodes)-1].Type != WorkflowNodeTypeEnd { + return fmt.Errorf("the last node is not an end node") + } + + return nil +} + +func (g *WorkflowGraph) Clone() *WorkflowGraph { + return &WorkflowGraph{ + Nodes: g.Nodes, + } +} type WorkflowTriggerType string @@ -50,31 +77,126 @@ const ( ) type WorkflowNode struct { - Id string `json:"id"` - Type WorkflowNodeType `json:"type"` - Name string `json:"name"` - - Config map[string]any `json:"config"` - Inputs []WorkflowNodeIO `json:"inputs,omitempty"` - Outputs []WorkflowNodeIO `json:"outputs,omitempty"` - - Next *WorkflowNode `json:"next,omitempty"` - Branches []WorkflowNode `json:"branches,omitempty"` - - Validated bool `json:"validated"` + Id string `json:"id"` // 节点 ID 只在该工作流中唯一,在全局中不保证唯一性 + Type WorkflowNodeType `json:"type"` + Data WorkflowNodeData `json:"data"` + Blocks []*WorkflowNode `json:"blocks,omitempty"` } -type WorkflowNodeConfigForApply struct { +type WorkflowNodeType string + +const ( + WorkflowNodeTypeStart = WorkflowNodeType("start") + WorkflowNodeTypeEnd = WorkflowNodeType("end") + WorkflowNodeTypeCondition = WorkflowNodeType("condition") + WorkflowNodeTypeBranchBlock = WorkflowNodeType("branchBlock") + WorkflowNodeTypeTryCatch = WorkflowNodeType("tryCatch") + WorkflowNodeTypeTryBlock = WorkflowNodeType("tryBlock") + WorkflowNodeTypeCatchBlock = WorkflowNodeType("catchBlock") + WorkflowNodeTypeBizApply = WorkflowNodeType("bizApply") + WorkflowNodeTypeBizUpload = WorkflowNodeType("bizUpload") + WorkflowNodeTypeBizMonitor = WorkflowNodeType("bizMonitor") + WorkflowNodeTypeBizDeploy = WorkflowNodeType("bizDeploy") + WorkflowNodeTypeBizNotify = WorkflowNodeType("bizNotify") +) + +type WorkflowNodeData struct { + Name string `json:"name"` + Config WorkflowNodeConfig `json:"config"` +} + +type WorkflowNodeConfig map[string]any + +func (c WorkflowNodeConfig) AsBizApply() WorkflowNodeConfigForBizApply { + return WorkflowNodeConfigForBizApply{ + Domains: xmaps.GetString(c, "domains"), + ContactEmail: xmaps.GetString(c, "contactEmail"), + ChallengeType: xmaps.GetString(c, "challengeType"), + Provider: xmaps.GetString(c, "provider"), + ProviderAccessId: xmaps.GetString(c, "providerAccessId"), + ProviderConfig: xmaps.GetKVMapAny(c, "providerConfig"), + KeyAlgorithm: xmaps.GetOrDefaultString(c, "keyAlgorithm", string(CertificateKeyAlgorithmTypeRSA2048)), + CAProvider: xmaps.GetString(c, "caProvider"), + CAProviderAccessId: xmaps.GetString(c, "caProviderAccessId"), + CAProviderConfig: xmaps.GetKVMapAny(c, "caProviderConfig"), + ACMEProfile: xmaps.GetString(c, "acmeProfile"), + Nameservers: xmaps.GetString(c, "nameservers"), + DnsPropagationWait: xmaps.GetInt32(c, "dnsPropagationWait"), + DnsPropagationTimeout: xmaps.GetInt32(c, "dnsPropagationTimeout"), + DnsTTL: xmaps.GetInt32(c, "dnsTTL"), + DisableFollowCNAME: xmaps.GetBool(c, "disableFollowCNAME"), + DisableARI: xmaps.GetBool(c, "disableARI"), + SkipBeforeExpiryDays: xmaps.GetInt32(c, "skipBeforeExpiryDays"), + } +} + +func (c WorkflowNodeConfig) AsBizUpload() WorkflowNodeConfigForBizUpload { + return WorkflowNodeConfigForBizUpload{ + Certificate: xmaps.GetString(c, "certificate"), + PrivateKey: xmaps.GetString(c, "privateKey"), + Domains: xmaps.GetString(c, "domains"), + } +} + +func (c WorkflowNodeConfig) AsBizMonitor() WorkflowNodeConfigForBizMonitor { + host := xmaps.GetString(c, "host") + return WorkflowNodeConfigForBizMonitor{ + Host: host, + Port: xmaps.GetOrDefaultInt32(c, "port", 443), + Domain: xmaps.GetOrDefaultString(c, "domain", host), + RequestPath: xmaps.GetString(c, "path"), + } +} + +func (c WorkflowNodeConfig) AsBizDeploy() WorkflowNodeConfigForBizDeploy { + return WorkflowNodeConfigForBizDeploy{ + CertificateOutputNodeId: xmaps.GetString(c, "certificateOutputNodeId"), + Provider: xmaps.GetString(c, "provider"), + ProviderAccessId: xmaps.GetString(c, "providerAccessId"), + ProviderConfig: xmaps.GetKVMapAny(c, "providerConfig"), + SkipOnLastSucceeded: xmaps.GetBool(c, "skipOnLastSucceeded"), + } +} + +func (c WorkflowNodeConfig) AsBizNotify() WorkflowNodeConfigForBizNotify { + return WorkflowNodeConfigForBizNotify{ + Provider: xmaps.GetString(c, "provider"), + ProviderAccessId: xmaps.GetString(c, "providerAccessId"), + ProviderConfig: xmaps.GetKVMapAny(c, "providerConfig"), + Subject: xmaps.GetString(c, "subject"), + Message: xmaps.GetString(c, "message"), + SkipOnAllPrevSkipped: xmaps.GetBool(c, "skipOnAllPrevSkipped"), + } +} + +func (c WorkflowNodeConfig) AsBranchBlock() WorkflowNodeConfigForBranchBlock { + expression := c["expression"] + if expression == nil { + return WorkflowNodeConfigForBranchBlock{} + } + + exprRaw, _ := json.Marshal(expression) + expr, err := expr.UnmarshalExpr([]byte(exprRaw)) + if err != nil { + return WorkflowNodeConfigForBranchBlock{} + } + + return WorkflowNodeConfigForBranchBlock{ + Expression: expr, + } +} + +type WorkflowNodeConfigForBizApply struct { Domains string `json:"domains"` // 域名列表,以半角分号分隔 ContactEmail string `json:"contactEmail"` // 联系邮箱 - ChallengeType string `json:"challengeType"` // TODO: 验证方式。目前仅支持 dns-01 + ChallengeType string `json:"challengeType"` // 验证方式。目前仅支持 dns-01 Provider string `json:"provider"` // DNS 提供商 ProviderAccessId string `json:"providerAccessId"` // DNS 提供商授权记录 ID ProviderConfig map[string]any `json:"providerConfig,omitempty"` // DNS 提供商额外配置 + KeyAlgorithm string `json:"keyAlgorithm,omitempty"` // 证书算法 CAProvider string `json:"caProvider,omitempty"` // CA 提供商(零值时使用全局配置) CAProviderAccessId string `json:"caProviderAccessId,omitempty"` // CA 提供商授权记录 ID CAProviderConfig map[string]any `json:"caProviderConfig,omitempty"` // CA 提供商额外配置 - KeyAlgorithm string `json:"keyAlgorithm,omitempty"` // 证书算法 ACMEProfile string `json:"acmeProfile,omitempty"` // ACME Profiles Extension Nameservers string `json:"nameservers,omitempty"` // DNS 服务器列表,以半角分号分隔 DnsPropagationWait int32 `json:"dnsPropagationWait,omitempty"` // DNS 传播等待时间,等同于 lego 的 `--dns-propagation-wait` 参数 @@ -82,31 +204,31 @@ type WorkflowNodeConfigForApply struct { DnsTTL int32 `json:"dnsTTL,omitempty"` // DNS 解析记录 TTL(零值时使用提供商的默认值) DisableFollowCNAME bool `json:"disableFollowCNAME,omitempty"` // 是否关闭 CNAME 跟随 DisableARI bool `json:"disableARI,omitempty"` // 是否关闭 ARI - SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays,omitempty"` // 证书到期前多少天前跳过续期(零值时默认值 30) + SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays,omitempty"` // 证书到期前多少天前跳过续期 } -type WorkflowNodeConfigForUpload struct { +type WorkflowNodeConfigForBizUpload struct { Certificate string `json:"certificate"` // 证书 PEM 内容 PrivateKey string `json:"privateKey"` // 私钥 PEM 内容 Domains string `json:"domains,omitempty"` } -type WorkflowNodeConfigForMonitor struct { +type WorkflowNodeConfigForBizMonitor struct { Host string `json:"host"` // 主机地址 Port int32 `json:"port,omitempty"` // 端口(零值时默认值 443) Domain string `json:"domain,omitempty"` // 域名(零值时默认值 [Host]) RequestPath string `json:"requestPath,omitempty"` // 请求路径 } -type WorkflowNodeConfigForDeploy struct { - Certificate string `json:"certificate"` // 前序节点输出的证书,形如“${NodeId}#certificate” - Provider string `json:"provider"` // 主机提供商 - ProviderAccessId string `json:"providerAccessId,omitempty"` // 主机提供商授权记录 ID - ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 主机提供商额外配置 - SkipOnLastSucceeded bool `json:"skipOnLastSucceeded"` // 上次部署成功时是否跳过 +type WorkflowNodeConfigForBizDeploy struct { + CertificateOutputNodeId string `json:"certificateOutputNodeId"` // 前序证书输出节点 ID + Provider string `json:"provider"` // 主机提供商 + ProviderAccessId string `json:"providerAccessId,omitempty"` // 主机提供商授权记录 ID + ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 主机提供商额外配置 + SkipOnLastSucceeded bool `json:"skipOnLastSucceeded"` // 上次部署成功时是否跳过 } -type WorkflowNodeConfigForNotify struct { +type WorkflowNodeConfigForBizNotify struct { Provider string `json:"provider"` // 通知提供商 ProviderAccessId string `json:"providerAccessId"` // 通知提供商授权记录 ID ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 通知提供商额外配置 @@ -115,98 +237,6 @@ type WorkflowNodeConfigForNotify struct { SkipOnAllPrevSkipped bool `json:"skipOnAllPrevSkipped"` // 前序节点均已跳过时是否跳过 } -type WorkflowNodeConfigForCondition struct { +type WorkflowNodeConfigForBranchBlock struct { Expression expr.Expr `json:"expression"` // 条件表达式 } - -func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply { - return WorkflowNodeConfigForApply{ - Domains: xmaps.GetString(n.Config, "domains"), - ContactEmail: xmaps.GetString(n.Config, "contactEmail"), - ChallengeType: xmaps.GetString(n.Config, "challengeType"), - Provider: xmaps.GetString(n.Config, "provider"), - ProviderAccessId: xmaps.GetString(n.Config, "providerAccessId"), - ProviderConfig: xmaps.GetKVMapAny(n.Config, "providerConfig"), - CAProvider: xmaps.GetString(n.Config, "caProvider"), - CAProviderAccessId: xmaps.GetString(n.Config, "caProviderAccessId"), - CAProviderConfig: xmaps.GetKVMapAny(n.Config, "caProviderConfig"), - KeyAlgorithm: xmaps.GetOrDefaultString(n.Config, "keyAlgorithm", string(CertificateKeyAlgorithmTypeRSA2048)), - ACMEProfile: xmaps.GetString(n.Config, "acmeProfile"), - Nameservers: xmaps.GetString(n.Config, "nameservers"), - DnsPropagationWait: xmaps.GetInt32(n.Config, "dnsPropagationWait"), - DnsPropagationTimeout: xmaps.GetInt32(n.Config, "dnsPropagationTimeout"), - DnsTTL: xmaps.GetInt32(n.Config, "dnsTTL"), - DisableFollowCNAME: xmaps.GetBool(n.Config, "disableFollowCNAME"), - DisableARI: xmaps.GetBool(n.Config, "disableARI"), - SkipBeforeExpiryDays: xmaps.GetOrDefaultInt32(n.Config, "skipBeforeExpiryDays", 30), - } -} - -func (n *WorkflowNode) GetConfigForUpload() WorkflowNodeConfigForUpload { - return WorkflowNodeConfigForUpload{ - Certificate: xmaps.GetString(n.Config, "certificate"), - PrivateKey: xmaps.GetString(n.Config, "privateKey"), - Domains: xmaps.GetString(n.Config, "domains"), - } -} - -func (n *WorkflowNode) GetConfigForMonitor() WorkflowNodeConfigForMonitor { - host := xmaps.GetString(n.Config, "host") - return WorkflowNodeConfigForMonitor{ - Host: host, - Port: xmaps.GetOrDefaultInt32(n.Config, "port", 443), - Domain: xmaps.GetOrDefaultString(n.Config, "domain", host), - RequestPath: xmaps.GetString(n.Config, "path"), - } -} - -func (n *WorkflowNode) GetConfigForDeploy() WorkflowNodeConfigForDeploy { - return WorkflowNodeConfigForDeploy{ - Certificate: xmaps.GetString(n.Config, "certificate"), - Provider: xmaps.GetString(n.Config, "provider"), - ProviderAccessId: xmaps.GetString(n.Config, "providerAccessId"), - ProviderConfig: xmaps.GetKVMapAny(n.Config, "providerConfig"), - SkipOnLastSucceeded: xmaps.GetBool(n.Config, "skipOnLastSucceeded"), - } -} - -func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify { - return WorkflowNodeConfigForNotify{ - Provider: xmaps.GetString(n.Config, "provider"), - ProviderAccessId: xmaps.GetString(n.Config, "providerAccessId"), - ProviderConfig: xmaps.GetKVMapAny(n.Config, "providerConfig"), - Subject: xmaps.GetString(n.Config, "subject"), - Message: xmaps.GetString(n.Config, "message"), - SkipOnAllPrevSkipped: xmaps.GetBool(n.Config, "skipOnAllPrevSkipped"), - } -} - -func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition { - expression := n.Config["expression"] - if expression == nil { - return WorkflowNodeConfigForCondition{} - } - - exprRaw, _ := json.Marshal(expression) - expr, err := expr.UnmarshalExpr([]byte(exprRaw)) - if err != nil { - return WorkflowNodeConfigForCondition{} - } - - return WorkflowNodeConfigForCondition{ - Expression: expr, - } -} - -type WorkflowNodeIO struct { - Label string `json:"label"` - Name string `json:"name"` - Type string `json:"type"` - Required bool `json:"required"` - Value any `json:"value"` - ValueSelector WorkflowNodeIOValueSelector `json:"valueSelector"` -} - -type WorkflowNodeIOValueSelector = expr.ExprValueSelector - -const WorkflowNodeIONameCertificate string = "certificate" diff --git a/internal/domain/workflow_output.go b/internal/domain/workflow_output.go index 1ad97a3d9..b83c14bb1 100644 --- a/internal/domain/workflow_output.go +++ b/internal/domain/workflow_output.go @@ -4,10 +4,17 @@ const CollectionNameWorkflowOutput = "workflow_output" type WorkflowOutput struct { Meta - WorkflowId string `json:"workflowId" db:"workflowRef"` - RunId string `json:"runId" db:"runRef"` - NodeId string `json:"nodeId" db:"nodeId"` - Node *WorkflowNode `json:"node" db:"node"` - Outputs []WorkflowNodeIO `json:"outputs" db:"outputs"` - Succeeded bool `json:"succeeded" db:"succeeded"` + WorkflowId string `json:"workflowId" db:"workflowRef"` + RunId string `json:"runId" db:"runRef"` + NodeId string `json:"nodeId" db:"nodeId"` + NodeConfig WorkflowNodeConfig `json:"nodeConfig" db:"nodeConfig"` + Outputs []*WorkflowOutputEntry `json:"outputs" db:"outputs"` + Succeeded bool `json:"succeeded" db:"succeeded"` +} + +type WorkflowOutputEntry struct { + Type string `json:"type"` + Name string `json:"name"` + Value string `json:"value"` + ValueType string `json:"valueType"` } diff --git a/internal/domain/workflow_run.go b/internal/domain/workflow_run.go index f409aa107..63e4880e8 100644 --- a/internal/domain/workflow_run.go +++ b/internal/domain/workflow_run.go @@ -13,16 +13,16 @@ type WorkflowRun struct { Trigger WorkflowTriggerType `json:"trigger" db:"trigger"` StartedAt time.Time `json:"startedAt" db:"startedAt"` EndedAt time.Time `json:"endedAt" db:"endedAt"` - Detail *WorkflowNode `json:"detail" db:"detail"` + Graph *WorkflowGraph `json:"graph" db:"graph"` Error string `json:"error" db:"error"` } type WorkflowRunStatusType string const ( - WorkflowRunStatusTypePending WorkflowRunStatusType = "pending" - WorkflowRunStatusTypeRunning WorkflowRunStatusType = "running" - WorkflowRunStatusTypeSucceeded WorkflowRunStatusType = "succeeded" - WorkflowRunStatusTypeFailed WorkflowRunStatusType = "failed" - WorkflowRunStatusTypeCanceled WorkflowRunStatusType = "canceled" + WorkflowRunStatusTypePending WorkflowRunStatusType = "pending" + WorkflowRunStatusTypeProcessing WorkflowRunStatusType = "processing" + WorkflowRunStatusTypeSucceeded WorkflowRunStatusType = "succeeded" + WorkflowRunStatusTypeFailed WorkflowRunStatusType = "failed" + WorkflowRunStatusTypeCanceled WorkflowRunStatusType = "canceled" ) diff --git a/internal/notify/notifier.go b/internal/notify/notifier.go index 975b7ab26..8220ac9c0 100644 --- a/internal/notify/notifier.go +++ b/internal/notify/notifier.go @@ -23,13 +23,13 @@ type NotifierWithWorkflowNodeConfig struct { func NewWithWorkflowNode(config NotifierWithWorkflowNodeConfig) (Notifier, error) { if config.Node == nil { - return nil, fmt.Errorf("node is nil") + return nil, fmt.Errorf("the node is nil") } - if config.Node.Type != domain.WorkflowNodeTypeNotify { - return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeNotify)) + if config.Node.Type != domain.WorkflowNodeTypeBizNotify { + return nil, fmt.Errorf("the node type is '%s', expected '%s'", string(config.Node.Type), string(domain.WorkflowNodeTypeBizNotify)) } - nodeCfg := config.Node.GetConfigForNotify() + nodeCfg := config.Node.Data.Config.AsBizNotify() options := ¬ifierProviderOptions{ Provider: domain.NotificationProviderType(nodeCfg.Provider), ProviderAccessConfig: make(map[string]any), diff --git a/internal/repository/access.go b/internal/repository/access.go index a0e7e9351..0ceeb067f 100644 --- a/internal/repository/access.go +++ b/internal/repository/access.go @@ -36,12 +36,12 @@ func (r *AccessRepository) GetById(ctx context.Context, id string) (*domain.Acce func (r *AccessRepository) castRecordToModel(record *core.Record) (*domain.Access, error) { if record == nil { - return nil, fmt.Errorf("record is nil") + return nil, fmt.Errorf("the record is nil") } config := make(map[string]any) if err := record.UnmarshalJSONField("config", &config); err != nil { - return nil, err + return nil, fmt.Errorf("field 'config' is malformed") } access := &domain.Access{ diff --git a/internal/repository/acme_account.go b/internal/repository/acme_account.go index 7b46e77c1..e8b45654b 100644 --- a/internal/repository/acme_account.go +++ b/internal/repository/acme_account.go @@ -86,12 +86,12 @@ func (r *AcmeAccountRepository) Save(ctx context.Context, acmeAccount *domain.Ac func (r *AcmeAccountRepository) castRecordToModel(record *core.Record) (*domain.AcmeAccount, error) { if record == nil { - return nil, fmt.Errorf("record is nil") + return nil, fmt.Errorf("the record is nil") } resource := ®istration.Resource{} if err := record.UnmarshalJSONField("resource", resource); err != nil { - return nil, err + return nil, fmt.Errorf("field 'resource' is malformed") } acmeAccount := &domain.AcmeAccount{ diff --git a/internal/repository/certificate.go b/internal/repository/certificate.go index 5c8abd33b..a8152ab23 100644 --- a/internal/repository/certificate.go +++ b/internal/repository/certificate.go @@ -58,12 +58,13 @@ func (r *CertificateRepository) GetById(ctx context.Context, id string) (*domain return r.castRecordToModel(record) } -func (r *CertificateRepository) GetByWorkflowNodeId(ctx context.Context, workflowNodeId string) (*domain.Certificate, error) { +func (r *CertificateRepository) GetByWorkflowIdAndNodeId(ctx context.Context, workflowId string, workflowNodeId string) (*domain.Certificate, error) { records, err := app.GetApp().FindRecordsByFilter( domain.CollectionNameCertificate, - "workflowNodeId={:workflowNodeId} && deleted=null", + "workflowRef={:workflowId} && workflowNodeId={:workflowNodeId} && deleted=null", "-created", 1, 0, + dbx.Params{"workflowId": workflowId}, dbx.Params{"workflowNodeId": workflowNodeId}, ) if err != nil { @@ -132,7 +133,6 @@ func (r *CertificateRepository) Save(ctx context.Context, certificate *domain.Ce record.Set("acmeRenewed", certificate.ACMERenewed) record.Set("workflowRef", certificate.WorkflowId) record.Set("workflowRunRef", certificate.WorkflowRunId) - record.Set("workflowOutputRef", certificate.WorkflowOutputId) record.Set("workflowNodeId", certificate.WorkflowNodeId) if err := app.GetApp().Save(record); err != nil { return certificate, err @@ -169,7 +169,7 @@ func (r *CertificateRepository) DeleteWhere(ctx context.Context, exprs ...dbx.Ex func (r *CertificateRepository) castRecordToModel(record *core.Record) (*domain.Certificate, error) { if record == nil { - return nil, fmt.Errorf("record is nil") + return nil, fmt.Errorf("the record is nil") } certificate := &domain.Certificate{ @@ -194,7 +194,6 @@ func (r *CertificateRepository) castRecordToModel(record *core.Record) (*domain. ACMERenewed: record.GetBool("acmeRenewed"), WorkflowId: record.GetString("workflowRef"), WorkflowRunId: record.GetString("workflowRunRef"), - WorkflowOutputId: record.GetString("workflowOutputRef"), WorkflowNodeId: record.GetString("workflowNodeId"), } return certificate, nil diff --git a/internal/repository/workflow.go b/internal/repository/workflow.go index a5c32a8b5..803ddc61c 100644 --- a/internal/repository/workflow.go +++ b/internal/repository/workflow.go @@ -79,9 +79,10 @@ func (r *WorkflowRepository) Save(ctx context.Context, workflow *domain.Workflow record.Set("trigger", string(workflow.Trigger)) record.Set("triggerCron", workflow.TriggerCron) record.Set("enabled", workflow.Enabled) - record.Set("content", workflow.Content) - record.Set("draft", workflow.Draft) + record.Set("graphDraft", workflow.GraphDraft) + record.Set("graphContent", workflow.GraphContent) record.Set("hasDraft", workflow.HasDraft) + record.Set("hasContent", workflow.HasContent) record.Set("lastRunRef", workflow.LastRunId) record.Set("lastRunStatus", string(workflow.LastRunStatus)) record.Set("lastRunTime", workflow.LastRunTime) @@ -97,17 +98,17 @@ func (r *WorkflowRepository) Save(ctx context.Context, workflow *domain.Workflow func (r *WorkflowRepository) castRecordToModel(record *core.Record) (*domain.Workflow, error) { if record == nil { - return nil, fmt.Errorf("record is nil") + return nil, fmt.Errorf("the record is nil") } - content := &domain.WorkflowNode{} - if err := record.UnmarshalJSONField("content", content); err != nil { - return nil, err + graphDraft := &domain.WorkflowGraph{} + if err := record.UnmarshalJSONField("graphDraft", graphDraft); err != nil { + return nil, fmt.Errorf("field 'graphDraft' is malformed") } - draft := &domain.WorkflowNode{} - if err := record.UnmarshalJSONField("draft", draft); err != nil { - return nil, err + graphContent := &domain.WorkflowGraph{} + if err := record.UnmarshalJSONField("graphContent", graphContent); err != nil { + return nil, fmt.Errorf("field 'graphContent' is malformed") } workflow := &domain.Workflow{ @@ -121,9 +122,10 @@ func (r *WorkflowRepository) castRecordToModel(record *core.Record) (*domain.Wor Trigger: domain.WorkflowTriggerType(record.GetString("trigger")), TriggerCron: record.GetString("triggerCron"), Enabled: record.GetBool("enabled"), - Content: content, - Draft: draft, + GraphDraft: graphDraft, + GraphContent: graphContent, HasDraft: record.GetBool("hasDraft"), + HasContent: record.GetBool("hasContent"), LastRunId: record.GetString("lastRunRef"), LastRunStatus: domain.WorkflowRunStatusType(record.GetString("lastRunStatus")), LastRunTime: record.GetDateTime("lastRunTime").Time(), diff --git a/internal/repository/workflow_log.go b/internal/repository/workflow_log.go index 948326a67..4994ba10c 100644 --- a/internal/repository/workflow_log.go +++ b/internal/repository/workflow_log.go @@ -85,12 +85,12 @@ func (r *WorkflowLogRepository) Save(ctx context.Context, workflowLog *domain.Wo func (r *WorkflowLogRepository) castRecordToModel(record *core.Record) (*domain.WorkflowLog, error) { if record == nil { - return nil, fmt.Errorf("record is nil") + return nil, fmt.Errorf("the record is nil") } logdata := make(map[string]any) if err := record.UnmarshalJSONField("data", &logdata); err != nil { - return nil, err + return nil, fmt.Errorf("field 'data' is malformed") } workflowLog := &domain.WorkflowLog{ diff --git a/internal/repository/workflow_output.go b/internal/repository/workflow_output.go index 88ff9ea73..e994bad21 100644 --- a/internal/repository/workflow_output.go +++ b/internal/repository/workflow_output.go @@ -51,70 +51,19 @@ func (r *WorkflowOutputRepository) Save(ctx context.Context, workflowOutput *dom return workflowOutput, nil } -func (r *WorkflowOutputRepository) SaveWithCertificate(ctx context.Context, workflowOutput *domain.WorkflowOutput, certificate *domain.Certificate) (*domain.WorkflowOutput, error) { - record, err := r.saveRecord(workflowOutput) - if err != nil { - return workflowOutput, err - } else { - workflowOutput.Id = record.Id - workflowOutput.CreatedAt = record.GetDateTime("created").Time() - workflowOutput.UpdatedAt = record.GetDateTime("updated").Time() - } - - if certificate == nil { - panic("certificate is nil") - } else { - if certificate.WorkflowId != "" && certificate.WorkflowId != workflowOutput.WorkflowId { - return workflowOutput, fmt.Errorf("certificate #%s is not belong to workflow #%s", certificate.Id, workflowOutput.WorkflowId) - } - if certificate.WorkflowRunId != "" && certificate.WorkflowRunId != workflowOutput.RunId { - return workflowOutput, fmt.Errorf("certificate #%s is not belong to workflow run #%s", certificate.Id, workflowOutput.RunId) - } - if certificate.WorkflowNodeId != "" && certificate.WorkflowNodeId != workflowOutput.NodeId { - return workflowOutput, fmt.Errorf("certificate #%s is not belong to workflow node #%s", certificate.Id, workflowOutput.NodeId) - } - if certificate.WorkflowOutputId != "" && certificate.WorkflowOutputId != workflowOutput.Id { - return workflowOutput, fmt.Errorf("certificate #%s is not belong to workflow output #%s", certificate.Id, workflowOutput.Id) - } - - certificate.WorkflowId = workflowOutput.WorkflowId - certificate.WorkflowRunId = workflowOutput.RunId - certificate.WorkflowNodeId = workflowOutput.NodeId - certificate.WorkflowOutputId = workflowOutput.Id - certificate, err := NewCertificateRepository().Save(ctx, certificate) - if err != nil { - return workflowOutput, err - } - - // 写入证书 ID 到工作流输出结果中 - for i, item := range workflowOutput.Outputs { - if item.Name == string(domain.WorkflowNodeIONameCertificate) { - workflowOutput.Outputs[i].Value = certificate.Id - break - } - } - record.Set("outputs", workflowOutput.Outputs) - if err := app.GetApp().Save(record); err != nil { - return workflowOutput, err - } - } - - return workflowOutput, err -} - func (r *WorkflowOutputRepository) castRecordToModel(record *core.Record) (*domain.WorkflowOutput, error) { if record == nil { - return nil, fmt.Errorf("record is nil") + return nil, fmt.Errorf("the record is nil") } - node := &domain.WorkflowNode{} - if err := record.UnmarshalJSONField("node", node); err != nil { - return nil, err + nodeConfig := make(domain.WorkflowNodeConfig) + if err := record.UnmarshalJSONField("nodeConfig", &nodeConfig); err != nil { + return nil, fmt.Errorf("field 'nodeConfig' is malformed") } - outputs := make([]domain.WorkflowNodeIO, 0) + outputs := make([]*domain.WorkflowOutputEntry, 0) if err := record.UnmarshalJSONField("outputs", &outputs); err != nil { - return nil, err + return nil, fmt.Errorf("field 'outputs' is malformed") } workflowOutput := &domain.WorkflowOutput{ @@ -126,7 +75,7 @@ func (r *WorkflowOutputRepository) castRecordToModel(record *core.Record) (*doma WorkflowId: record.GetString("workflowRef"), RunId: record.GetString("runRef"), NodeId: record.GetString("nodeId"), - Node: node, + NodeConfig: nodeConfig, Outputs: outputs, Succeeded: record.GetBool("succeeded"), } @@ -151,7 +100,7 @@ func (r *WorkflowOutputRepository) saveRecord(workflowOutput *domain.WorkflowOut record.Set("workflowRef", workflowOutput.WorkflowId) record.Set("runRef", workflowOutput.RunId) record.Set("nodeId", workflowOutput.NodeId) - record.Set("node", workflowOutput.Node) + record.Set("nodeConfig", workflowOutput.NodeConfig) record.Set("outputs", workflowOutput.Outputs) record.Set("succeeded", workflowOutput.Succeeded) if err := app.GetApp().Save(record); err != nil { diff --git a/internal/repository/workflow_run.go b/internal/repository/workflow_run.go index d128e9bcd..0fb5a7679 100644 --- a/internal/repository/workflow_run.go +++ b/internal/repository/workflow_run.go @@ -49,13 +49,50 @@ func (r *WorkflowRunRepository) Save(ctx context.Context, workflowRun *domain.Wo } } + record.Set("workflowRef", workflowRun.WorkflowId) + record.Set("trigger", string(workflowRun.Trigger)) + record.Set("status", string(workflowRun.Status)) + record.Set("startedAt", workflowRun.StartedAt) + record.Set("endedAt", workflowRun.EndedAt) + record.Set("graph", workflowRun.Graph) + record.Set("error", workflowRun.Error) + err = app.GetApp().Save(record) + if err != nil { + return workflowRun, err + } + + workflowRun.Id = record.Id + workflowRun.CreatedAt = record.GetDateTime("created").Time() + workflowRun.UpdatedAt = record.GetDateTime("updated").Time() + return workflowRun, nil +} + +func (r *WorkflowRunRepository) SaveWithCascading(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) { + collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowRun) + if err != nil { + return workflowRun, err + } + + var record *core.Record + if workflowRun.Id == "" { + record = core.NewRecord(collection) + } else { + record, err = app.GetApp().FindRecordById(collection, workflowRun.Id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return workflowRun, err + } + record = core.NewRecord(collection) + } + } + err = app.GetApp().RunInTransaction(func(txApp core.App) error { record.Set("workflowRef", workflowRun.WorkflowId) record.Set("trigger", string(workflowRun.Trigger)) record.Set("status", string(workflowRun.Status)) record.Set("startedAt", workflowRun.StartedAt) record.Set("endedAt", workflowRun.EndedAt) - record.Set("detail", workflowRun.Detail) + record.Set("graph", workflowRun.Graph) record.Set("error", workflowRun.Error) err = txApp.Save(record) if err != nil { @@ -122,12 +159,12 @@ func (r *WorkflowRunRepository) DeleteWhere(ctx context.Context, exprs ...dbx.Ex func (r *WorkflowRunRepository) castRecordToModel(record *core.Record) (*domain.WorkflowRun, error) { if record == nil { - return nil, fmt.Errorf("record is nil") + return nil, fmt.Errorf("the record is nil") } - detail := &domain.WorkflowNode{} - if err := record.UnmarshalJSONField("detail", &detail); err != nil { - return nil, err + graph := &domain.WorkflowGraph{} + if err := record.UnmarshalJSONField("graph", &graph); err != nil { + return nil, fmt.Errorf("field 'graph' is malformed") } workflowRun := &domain.WorkflowRun{ @@ -141,7 +178,7 @@ func (r *WorkflowRunRepository) castRecordToModel(record *core.Record) (*domain. Trigger: domain.WorkflowTriggerType(record.GetString("trigger")), StartedAt: record.GetDateTime("startedAt").Time(), EndedAt: record.GetDateTime("endedAt").Time(), - Detail: detail, + Graph: graph, Error: record.GetString("error"), } return workflowRun, nil diff --git a/internal/rest/handlers/certificate.go b/internal/rest/handlers/certificate.go index fa75f59a3..24369aa09 100644 --- a/internal/rest/handlers/certificate.go +++ b/internal/rest/handlers/certificate.go @@ -38,11 +38,12 @@ func (handler *CertificateHandler) archiveFile(e *core.RequestEvent) error { return resp.Err(e, err) } - if res, err := handler.service.DownloadArchivedFile(e.Request.Context(), req); err != nil { + res, err := handler.service.DownloadArchivedFile(e.Request.Context(), req) + if err != nil { return resp.Err(e, err) - } else { - return resp.Ok(e, res) } + + return resp.Ok(e, res) } func (handler *CertificateHandler) validateCertificate(e *core.RequestEvent) error { @@ -51,11 +52,12 @@ func (handler *CertificateHandler) validateCertificate(e *core.RequestEvent) err return resp.Err(e, err) } - if res, err := handler.service.ValidateCertificate(e.Request.Context(), req); err != nil { + res, err := handler.service.ValidateCertificate(e.Request.Context(), req) + if err != nil { return resp.Err(e, err) - } else { - return resp.Ok(e, res) } + + return resp.Ok(e, res) } func (handler *CertificateHandler) validatePrivateKey(e *core.RequestEvent) error { @@ -64,9 +66,10 @@ func (handler *CertificateHandler) validatePrivateKey(e *core.RequestEvent) erro return resp.Err(e, err) } - if res, err := handler.service.ValidatePrivateKey(e.Request.Context(), req); err != nil { + res, err := handler.service.ValidatePrivateKey(e.Request.Context(), req) + if err != nil { return resp.Err(e, err) - } else { - return resp.Ok(e, res) } + + return resp.Ok(e, res) } diff --git a/internal/rest/handlers/statistics.go b/internal/rest/handlers/statistics.go index 0e141be41..cc069a2c4 100644 --- a/internal/rest/handlers/statistics.go +++ b/internal/rest/handlers/statistics.go @@ -28,9 +28,10 @@ func NewStatisticsHandler(router *router.RouterGroup[*core.RequestEvent], servic } func (handler *StatisticsHandler) get(e *core.RequestEvent) error { - if statistics, err := handler.service.Get(e.Request.Context()); err != nil { + res, err := handler.service.Get(e.Request.Context()) + if err != nil { return resp.Err(e, err) - } else { - return resp.Ok(e, statistics) } + + return resp.Ok(e, res) } diff --git a/internal/rest/handlers/workflow.go b/internal/rest/handlers/workflow.go index 2c88d5823..3de0a5544 100644 --- a/internal/rest/handlers/workflow.go +++ b/internal/rest/handlers/workflow.go @@ -11,8 +11,8 @@ import ( ) type workflowService interface { - StartRun(ctx context.Context, req *dtos.WorkflowStartRunReq) error - CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) error + StartRun(ctx context.Context, req *dtos.WorkflowStartRunReq) (*dtos.WorkflowStartRunResp, error) + CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) (*dtos.WorkflowCancelRunResp, error) Shutdown(ctx context.Context) } @@ -37,11 +37,12 @@ func (handler *WorkflowHandler) run(e *core.RequestEvent) error { return resp.Err(e, err) } - if err := handler.service.StartRun(e.Request.Context(), req); err != nil { + res, err := handler.service.StartRun(e.Request.Context(), req) + if err != nil { return resp.Err(e, err) } - return resp.Ok(e, nil) + return resp.Ok(e, res) } func (handler *WorkflowHandler) cancel(e *core.RequestEvent) error { @@ -49,9 +50,10 @@ func (handler *WorkflowHandler) cancel(e *core.RequestEvent) error { req.WorkflowId = e.Request.PathValue("workflowId") req.RunId = e.Request.PathValue("runId") - if err := handler.service.CancelRun(e.Request.Context(), req); err != nil { + res, err := handler.service.CancelRun(e.Request.Context(), req) + if err != nil { return resp.Err(e, err) } - return resp.Ok(e, nil) + return resp.Ok(e, res) } diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index 0bfce2b0f..3e9110bed 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -1,6 +1,8 @@ package scheduler import ( + "log/slog" + "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/certificate" "github.com/certimate-go/certimate/internal/repository" @@ -17,10 +19,10 @@ func Register() { certificateSvc := certificate.NewCertificateService(certificateRepo, settingsRepo) if err := InitWorkflowScheduler(workflowSvc); err != nil { - app.GetLogger().Error("failed to init workflow scheduler", "err", err) + app.GetLogger().Error("failed to init workflow scheduler", slog.Any("error", err)) } if err := InitCertificateScheduler(certificateSvc); err != nil { - app.GetLogger().Error("failed to init certificate scheduler", "err", err) + app.GetLogger().Error("failed to init certificate scheduler", slog.Any("error", err)) } } diff --git a/internal/statistics/service.go b/internal/statistics/service.go index ea041a15a..b9d2b37ac 100644 --- a/internal/statistics/service.go +++ b/internal/statistics/service.go @@ -6,10 +6,6 @@ import ( "github.com/certimate-go/certimate/internal/domain" ) -type statisticsRepository interface { - Get(ctx context.Context) (*domain.Statistics, error) -} - type StatisticsService struct { statRepo statisticsRepository } diff --git a/internal/statistics/service_deps.go b/internal/statistics/service_deps.go new file mode 100644 index 000000000..2c53fa2d5 --- /dev/null +++ b/internal/statistics/service_deps.go @@ -0,0 +1,11 @@ +package statistics + +import ( + "context" + + "github.com/certimate-go/certimate/internal/domain" +) + +type statisticsRepository interface { + Get(ctx context.Context) (*domain.Statistics, error) +} diff --git a/internal/workflow/dispatcher/deps.go b/internal/workflow/dispatcher/deps.go new file mode 100644 index 000000000..baa0e85bd --- /dev/null +++ b/internal/workflow/dispatcher/deps.go @@ -0,0 +1,22 @@ +package dispatcher + +import ( + "context" + + "github.com/certimate-go/certimate/internal/domain" +) + +type workflowRepository interface { + GetById(ctx context.Context, id string) (*domain.Workflow, error) + Save(ctx context.Context, workflow *domain.Workflow) (*domain.Workflow, error) +} + +type workflowRunRepository interface { + GetById(ctx context.Context, id string) (*domain.WorkflowRun, error) + Save(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) + SaveWithCascading(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) +} + +type workflowLogRepository interface { + Save(ctx context.Context, workflowLog *domain.WorkflowLog) (*domain.WorkflowLog, error) +} diff --git a/internal/workflow/dispatcher/dispatcher.go b/internal/workflow/dispatcher/dispatcher.go index 70ff43c56..806c2e9b0 100644 --- a/internal/workflow/dispatcher/dispatcher.go +++ b/internal/workflow/dispatcher/dispatcher.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log" + "log/slog" "os" "runtime" "runtime/debug" @@ -12,10 +13,11 @@ import ( "sync" "time" - "github.com/samber/lo" - "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/internal/repository" + "github.com/certimate-go/certimate/internal/workflow/engine" + "github.com/certimate-go/certimate/pkg/logging" ) var maxWorkers = 1 @@ -32,275 +34,290 @@ func init() { } } -type workflowWorker struct { - Data *WorkflowWorkerData - Cancel context.CancelFunc +type WorkflowDispatcher interface { + Bootup(ctx context.Context) error + Shutdown(ctx context.Context) error + Start(ctx context.Context, runId string) error + Cancel(ctx context.Context, runId string) error } -type WorkflowWorkerData struct { - WorkflowId string - WorkflowContent *domain.WorkflowNode - RunId string -} +type workflowDispatcher struct { + booted bool + concurrency int -type WorkflowDispatcher struct { - semaphore chan struct{} - - queue []*WorkflowWorkerData - queueMutex sync.Mutex - - workers map[string]*workflowWorker // key: WorkflowId - workerIdMap map[string]string // key: RunId, value: WorkflowId - workerMutex sync.Mutex - - chWork chan *WorkflowWorkerData - chCandi chan struct{} - - wg sync.WaitGroup + taskMtx sync.RWMutex + pendingRunQueue []string + processingTasks map[string]*taskInfo // Key: RunId workflowRepo workflowRepository workflowRunRepo workflowRunRepository workflowLogRepo workflowLogRepository } -func newWorkflowDispatcher(workflowRepo workflowRepository, workflowRunRepo workflowRunRepository, workflowLogRepo workflowLogRepository) *WorkflowDispatcher { - dispatcher := &WorkflowDispatcher{ - semaphore: make(chan struct{}, maxWorkers), +var _ WorkflowDispatcher = (*workflowDispatcher)(nil) - queue: make([]*WorkflowWorkerData, 0), - queueMutex: sync.Mutex{}, - - workers: make(map[string]*workflowWorker), - workerIdMap: make(map[string]string), - workerMutex: sync.Mutex{}, - - chWork: make(chan *WorkflowWorkerData), - chCandi: make(chan struct{}, 1), - - workflowRepo: workflowRepo, - workflowRunRepo: workflowRunRepo, - workflowLogRepo: workflowLogRepo, +func (wd *workflowDispatcher) Bootup(ctx context.Context) error { + if wd.booted { + return errors.New("could not re-bootup") } - go func() { - for { - select { - case <-dispatcher.chWork: - dispatcher.dequeueWorker() + wd.taskMtx.Lock() + defer wd.taskMtx.Unlock() - case <-dispatcher.chCandi: - dispatcher.dequeueWorker() - } - } - }() + if _, err := app.GetDB().NewQuery("UPDATE workflow SET lastRunStatus = 'canceled' WHERE lastRunStatus = 'pending' OR lastRunStatus = 'processing'").Execute(); err != nil { + return err + } + if _, err := app.GetDB().NewQuery("UPDATE workflow_run SET status = 'canceled' WHERE status = 'pending' OR status = 'processing'").Execute(); err != nil { + return err + } - return dispatcher + wd.booted = true + return nil } -func (d *WorkflowDispatcher) Dispatch(data *WorkflowWorkerData) { - if data == nil { - panic("worker data is nil") +func (wd *workflowDispatcher) Shutdown(ctx context.Context) error { + if !wd.booted { + return errors.New("could not re-shutdown") } - d.enqueueWorker(data) + wd.taskMtx.Lock() + defer wd.taskMtx.Unlock() - select { - case d.chWork <- data: - default: + for runId, task := range wd.processingTasks { + task.cancel() + delete(wd.processingTasks, runId) } + + wd.booted = false + wd.pendingRunQueue = make([]string, 0) + wd.processingTasks = make(map[string]*taskInfo) + return nil } -func (d *WorkflowDispatcher) Cancel(runId string) { - hasWorker := false +func (wd *workflowDispatcher) Start(ctx context.Context, runId string) error { + wd.taskMtx.Lock() + defer wd.taskMtx.Unlock() - // 取消正在执行的 WorkflowRun - d.workerMutex.Lock() - if workflowId, ok := d.workerIdMap[runId]; ok { - if worker, ok := d.workers[workflowId]; ok { - hasWorker = true - worker.Cancel() - delete(d.workers, workflowId) - delete(d.workerIdMap, runId) + if _, exists := wd.processingTasks[runId]; exists { + return errors.New("workflow run is already processing") + } + + for _, pendingRunId := range wd.pendingRunQueue { + if pendingRunId == runId { + return errors.New("workflow run is already in the queue") } } - d.workerMutex.Unlock() - // 移除排队中的 WorkflowRun - d.queueMutex.Lock() - d.queue = lo.Filter(d.queue, func(d *WorkflowWorkerData, _ int) bool { - return d.RunId != runId - }) - d.queueMutex.Unlock() + wd.pendingRunQueue = append(wd.pendingRunQueue, runId) + go func() { wd.tryNextAsync() }() - // 已挂起,查询 WorkflowRun 并更新其状态为 Canceled - if !hasWorker { - if run, err := d.workflowRunRepo.GetById(context.Background(), runId); err == nil { - if run.Status == domain.WorkflowRunStatusTypePending || run.Status == domain.WorkflowRunStatusTypeRunning { - run.Status = domain.WorkflowRunStatusTypeCanceled - d.workflowRunRepo.Save(context.Background(), run) - } + return nil +} + +func (wd *workflowDispatcher) Cancel(ctx context.Context, runId string) error { + wd.taskMtx.Lock() + defer wd.taskMtx.Unlock() + + workflowRun, err := wd.workflowRunRepo.GetById(ctx, runId) + if err != nil { + return err + } else if workflowRun.Status != domain.WorkflowRunStatusTypePending && workflowRun.Status != domain.WorkflowRunStatusTypeProcessing { + return errors.New("workflow run is already completed") + } + + workflow, err := wd.workflowRepo.GetById(ctx, workflowRun.WorkflowId) + if err != nil { + return err + } + + workflowRun.Status = domain.WorkflowRunStatusTypeCanceled + if workflow.LastRunId == workflowRun.Id { + _, err := wd.workflowRunRepo.SaveWithCascading(ctx, workflowRun) + if err != nil { + return err + } + } else { + _, err := wd.workflowRunRepo.Save(ctx, workflowRun) + if err != nil { + return err } } -} -func (d *WorkflowDispatcher) Shutdown() { - // 清空排队中的 WorkflowRun - d.queueMutex.Lock() - d.queue = make([]*WorkflowWorkerData, 0) - d.queueMutex.Unlock() - - // 等待所有正在执行的 WorkflowRun 完成 - d.workerMutex.Lock() - for _, worker := range d.workers { - worker.Cancel() - delete(d.workers, worker.Data.WorkflowId) - delete(d.workerIdMap, worker.Data.RunId) + if task, exists := wd.processingTasks[runId]; exists { + task.cancel() + delete(wd.processingTasks, runId) } - d.workerMutex.Unlock() - d.wg.Wait() -} -func (d *WorkflowDispatcher) enqueueWorker(data *WorkflowWorkerData) { - d.queueMutex.Lock() - defer d.queueMutex.Unlock() - d.queue = append(d.queue, data) -} - -func (d *WorkflowDispatcher) dequeueWorker() { - for { - select { - case d.semaphore <- struct{}{}: - default: - // 达到最大并发数 - return + for i, pendingRunId := range wd.pendingRunQueue { + if pendingRunId == runId { + wd.pendingRunQueue = append(wd.pendingRunQueue[:i], wd.pendingRunQueue[i+1:]...) + break } - - d.queueMutex.Lock() - if len(d.queue) == 0 { - d.queueMutex.Unlock() - <-d.semaphore - return - } - - data := d.queue[0] - d.queue = d.queue[1:] - d.queueMutex.Unlock() - - // 检查是否有相同 WorkflowId 的 WorkflowRun 正在执行 - // 如果有,则重新排队,以保证同一个工作流同一时间内只有一个正在执行 - // 即不同 WorkflowId 的任务并行化,相同 WorkflowId 的任务串行化 - d.workerMutex.Lock() - if _, exists := d.workers[data.WorkflowId]; exists { - d.queueMutex.Lock() - d.queue = append(d.queue, data) - d.queueMutex.Unlock() - d.workerMutex.Unlock() - - <-d.semaphore - - continue - } - - ctx, cancel := context.WithCancel(context.Background()) - d.workers[data.WorkflowId] = &workflowWorker{data, cancel} - d.workerIdMap[data.RunId] = data.WorkflowId - d.workerMutex.Unlock() - - d.wg.Add(1) - go d.work(ctx, data) } + + go func() { wd.tryNextAsync() }() + + return nil } -func (d *WorkflowDispatcher) work(ctx context.Context, data *WorkflowWorkerData) { - var run *domain.WorkflowRun - var err error +func (wd *workflowDispatcher) tryExecuteAsync(task *taskInfo) { + var workflowRun *domain.WorkflowRun + // 捕获 panic defer func() { - // 捕获 panic,避免影响其他工作流的执行 if r := recover(); r != nil { - log.Default().Println("WorkflowId:", data.WorkflowId, "RunId:", data.RunId) - log.Default().Println("Recovered from panic:", r) - log.Default().Println("Stack trace:", string(debug.Stack())) - if run != nil { - run.Status = domain.WorkflowRunStatusTypeFailed - run.EndedAt = time.Now() - run.Error = fmt.Sprintf("workflow run panic: %v", r) - if _, err := d.workflowRunRepo.Save(ctx, run); err != nil { - log.Default().Println("Failed to save workflow run after panic:", err) + slog.Default().Warn(fmt.Sprintf("workflow dispatcher panic: %v, stack trace: %s", r, string(debug.Stack())), slog.Any("workflowId", task.WorkflowId), slog.Any("runId", task.RunId)) + app.GetLogger().Error(fmt.Sprintf("workflow dispatcher panic: %v", r), slog.Any("workflowId", task.WorkflowId), slog.Any("runId", task.RunId)) + + if workflowRun != nil { + workflowRun.Status = domain.WorkflowRunStatusTypeFailed + workflowRun.EndedAt = time.Now() + workflowRun.Error = fmt.Sprintf("workflow dispatcher panic: %v", r) + if _, err := wd.workflowRunRepo.SaveWithCascading(context.Background(), workflowRun); err != nil { + log.Default().Println("failed to save workflow run after panic", slog.Any("error", err)) } } } - - <-d.semaphore - d.workerMutex.Lock() - delete(d.workers, data.WorkflowId) - delete(d.workerIdMap, data.RunId) - d.workerMutex.Unlock() - - d.wg.Done() - - // 尝试取出排队中的其他 WorkflowRun 继续执行 - select { - case d.chCandi <- struct{}{}: - default: - } }() - // 查询 WorkflowRun - run, err = d.workflowRunRepo.GetById(ctx, data.RunId) - if err != nil { + // 尝试继续执行等待队列中的任务 + defer func() { + delete(wd.processingTasks, task.RunId) + wd.tryNextAsync() + }() + + // 查询运行实体,并级联更新状态 + if run, err := wd.workflowRunRepo.GetById(task.ctx, task.RunId); err != nil { if !(errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) { - app.GetLogger().Error(fmt.Sprintf("failed to get workflow run #%s", data.RunId), "err", err) + app.GetLogger().Error(fmt.Sprintf("failed to get workflow run #%s record", task.RunId), slog.Any("error", err)) } return - } else if run.Status != domain.WorkflowRunStatusTypePending { - return - } else if ctx.Err() != nil { - run.Status = domain.WorkflowRunStatusTypeCanceled - d.workflowRunRepo.Save(ctx, run) - return + } else { + workflowRun = run + + if run.Status == domain.WorkflowRunStatusTypePending { + run.Status = domain.WorkflowRunStatusTypeProcessing + wd.workflowRunRepo.SaveWithCascading(task.ctx, run) + } else { + // WTF? That should be impossible! + return + } } - // 更新 WorkflowRun 状态为 Running - run.Status = domain.WorkflowRunStatusTypeRunning - if _, err := d.workflowRunRepo.Save(ctx, run); err != nil { - if !(errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) { - panic(err) + // 初始化工作流引擎 + logsBuf := make(domain.WorkflowLogs, 0) + we := engine.NewWorkflowEngine() + we.OnEnd(func(ctx context.Context) error { + if errmsg := logsBuf.ErrorString(); errmsg == "" { + workflowRun.Status = domain.WorkflowRunStatusTypeSucceeded + workflowRun.EndedAt = time.Now() + } else { + workflowRun.Status = domain.WorkflowRunStatusTypeFailed + workflowRun.EndedAt = time.Now() + workflowRun.Error = errmsg } - return - } + wd.workflowRunRepo.SaveWithCascading(task.ctx, workflowRun) + return nil + }) + we.OnError(func(ctx context.Context, err error) error { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + workflowRun.Status = domain.WorkflowRunStatusTypeCanceled + wd.workflowRunRepo.SaveWithCascading(context.Background(), workflowRun) + } else { + workflowRun.Status = domain.WorkflowRunStatusTypeFailed + workflowRun.EndedAt = time.Now() + workflowRun.Error = err.Error() + wd.workflowRunRepo.SaveWithCascading(task.ctx, workflowRun) + } + return nil + }) + we.OnNodeError(func(ctx context.Context, node *engine.Node, err error) error { + log := domain.WorkflowLog{} + log.WorkflowId = task.WorkflowId + log.RunId = task.RunId + log.NodeId = node.Id + log.NodeName = node.Data.Name + log.Timestamp = time.Now().UnixMilli() + log.Level = int32(slog.LevelError) + log.Message = err.Error() + log.CreatedAt = time.Now() + logsBuf = append(logsBuf, log) + + if _, err := wd.workflowLogRepo.Save(ctx, &log); err != nil { + app.GetLogger().Error(err.Error()) + } + + return nil + }) + we.OnNodeLogging(func(ctx context.Context, node *engine.Node, record logging.Record) error { + log := domain.WorkflowLog{} + log.WorkflowId = task.WorkflowId + log.RunId = task.RunId + log.NodeId = node.Id + log.NodeName = node.Data.Name + log.Timestamp = record.Time.UnixMilli() + log.Level = int32(record.Level) + log.Message = record.Message + log.Data = record.Data + log.CreatedAt = time.Now() + logsBuf = append(logsBuf, log) + + if _, err := wd.workflowLogRepo.Save(ctx, &log); err != nil { + app.GetLogger().Error(err.Error()) + } + + return nil + }) // 执行工作流 - invoker := newWorkflowInvokerWithData(d.workflowLogRepo, data) - if runErr := invoker.Invoke(ctx); runErr != nil { - if errors.Is(runErr, context.Canceled) { - run.Status = domain.WorkflowRunStatusTypeCanceled - } else { - run.Status = domain.WorkflowRunStatusTypeFailed - run.EndedAt = time.Now() - run.Error = runErr.Error() + app.GetLogger().Info(fmt.Sprintf("start to invoke workflow run #%s", task.RunId)) + we.Invoke(task.ctx, workflowRun.WorkflowId, workflowRun.Id, workflowRun.Graph) +} + +func (wd *workflowDispatcher) tryNextAsync() { + wd.taskMtx.RLock() + + for i, pendingRunId := range wd.pendingRunQueue { + workflowRun, err := wd.workflowRunRepo.GetById(context.Background(), pendingRunId) + if err != nil { + app.GetLogger().Error(fmt.Sprintf("failed to get workflow run #%s record", pendingRunId), slog.Any("error", err)) + continue } - if _, err := d.workflowRunRepo.Save(ctx, run); err != nil { - if !(errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) { - panic(err) + var hasSameWorkflowTask bool // 相同 Workflow 的任务同一时间只能有一个 Run 在执行 + for _, task := range wd.processingTasks { + if task.WorkflowId == workflowRun.WorkflowId { + hasSameWorkflowTask = true + break } } - return - } + if !hasSameWorkflowTask && len(wd.processingTasks) < wd.concurrency { + wd.taskMtx.RUnlock() + wd.taskMtx.Lock() + defer wd.taskMtx.Unlock() - // 更新 WorkflowRun 状态为 Succeeded/Failed - run.EndedAt = time.Now() - run.Error = invoker.GetLogs().ErrorString() - if run.Error == "" { - run.Status = domain.WorkflowRunStatusTypeSucceeded - } else { - run.Status = domain.WorkflowRunStatusTypeFailed - } - if _, err := d.workflowRunRepo.Save(ctx, run); err != nil { - if !(errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) { - panic(err) + ctxRun, ctxCancel := context.WithCancel(context.Background()) + 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 + go func() { wd.tryExecuteAsync(task) }() + return } } + + wd.taskMtx.RUnlock() +} + +func newWorkflowDispatcher() WorkflowDispatcher { + return &workflowDispatcher{ + concurrency: maxWorkers, + + pendingRunQueue: make([]string, 0), + processingTasks: make(map[string]*taskInfo), + + workflowRepo: repository.NewWorkflowRepository(), + workflowRunRepo: repository.NewWorkflowRunRepository(), + workflowLogRepo: repository.NewWorkflowLogRepository(), + } } diff --git a/internal/workflow/dispatcher/invoker.go b/internal/workflow/dispatcher/invoker.go deleted file mode 100644 index 27bc439b5..000000000 --- a/internal/workflow/dispatcher/invoker.go +++ /dev/null @@ -1,142 +0,0 @@ -package dispatcher - -import ( - "context" - "errors" - "log/slog" - - "github.com/certimate-go/certimate/internal/domain" - nodes "github.com/certimate-go/certimate/internal/workflow/node-processor" - "github.com/certimate-go/certimate/pkg/logging" -) - -type workflowInvoker struct { - workflowId string - workflowContent *domain.WorkflowNode - runId string - logs []domain.WorkflowLog - - workflowLogRepo workflowLogRepository -} - -func newWorkflowInvokerWithData(workflowLogRepo workflowLogRepository, data *WorkflowWorkerData) *workflowInvoker { - if data == nil { - panic("worker data is nil") - } - - return &workflowInvoker{ - workflowId: data.WorkflowId, - workflowContent: data.WorkflowContent, - runId: data.RunId, - logs: make([]domain.WorkflowLog, 0), - - workflowLogRepo: workflowLogRepo, - } -} - -func (w *workflowInvoker) Invoke(ctx context.Context) error { - ctx = context.WithValue(ctx, "workflow_id", w.workflowId) - ctx = context.WithValue(ctx, "workflow_run_id", w.runId) - return w.processNode(ctx, w.workflowContent) -} - -func (w *workflowInvoker) GetLogs() domain.WorkflowLogs { - return w.logs -} - -func (w *workflowInvoker) processNode(ctx context.Context, node *domain.WorkflowNode) error { - current := node - for current != nil { - select { - case <-ctx.Done(): - return ctx.Err() - default: - } - - if current.Type == domain.WorkflowNodeTypeBranch || current.Type == domain.WorkflowNodeTypeExecuteResultBranch { - for _, branch := range current.Branches { - if err := w.processNode(ctx, &branch); err != nil { - // 并行分支的某一分支发生错误时,忽略此错误,继续执行其他分支 - if !(errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) { - continue - } - return err - } - } - } - - var processor nodes.NodeProcessor - var procErr error - for { - if current.Type != domain.WorkflowNodeTypeBranch && current.Type != domain.WorkflowNodeTypeExecuteResultBranch { - processor, procErr = nodes.GetProcessor(current) - if procErr != nil { - panic(procErr) - } - - processor.SetLogger(slog.New(logging.NewHookHandler(&logging.HookHandlerOptions{ - Level: slog.LevelDebug, - WriteFunc: func(ctx context.Context, record *logging.Record) error { - log := domain.WorkflowLog{} - log.WorkflowId = w.workflowId - log.RunId = w.runId - log.NodeId = current.Id - log.NodeName = current.Name - log.Timestamp = record.Time.UnixMilli() - log.Level = int32(record.Level) - log.Message = record.Message - log.Data = record.Data - log.CreatedAt = record.Time - if _, err := w.workflowLogRepo.Save(ctx, &log); err != nil { - return err - } - - w.logs = append(w.logs, log) - return nil - }, - }))) - - procErr = processor.Process(ctx) - if procErr != nil { - if current.Type != domain.WorkflowNodeTypeCondition { - processor.GetLogger().Error(procErr.Error()) - } - break - } - - nodeOutputs := processor.GetOutputs() - if len(nodeOutputs) > 0 { - ctx = nodes.AddNodeOutput(ctx, current.Id, nodeOutputs) - } - } - - break - } - - // TODO: 优化可读性 - if procErr != nil && current.Type == domain.WorkflowNodeTypeCondition { - current = nil - procErr = nil - return nil - } else if procErr != nil && current.Next != nil && current.Next.Type != domain.WorkflowNodeTypeExecuteResultBranch { - return procErr - } else if procErr != nil && current.Next != nil && current.Next.Type == domain.WorkflowNodeTypeExecuteResultBranch { - current = w.getBranchByType(current.Next.Branches, domain.WorkflowNodeTypeExecuteFailure) - } else if procErr == nil && current.Next != nil && current.Next.Type == domain.WorkflowNodeTypeExecuteResultBranch { - current = w.getBranchByType(current.Next.Branches, domain.WorkflowNodeTypeExecuteSuccess) - } else { - current = current.Next - } - } - - return nil -} - -func (w *workflowInvoker) getBranchByType(branches []domain.WorkflowNode, nodeType domain.WorkflowNodeType) *domain.WorkflowNode { - for _, branch := range branches { - if branch.Type == nodeType { - return &branch - } - } - return nil -} diff --git a/internal/workflow/dispatcher/singleton.go b/internal/workflow/dispatcher/singleton.go index 8e5a7865a..a582fe12a 100644 --- a/internal/workflow/dispatcher/singleton.go +++ b/internal/workflow/dispatcher/singleton.go @@ -1,36 +1,17 @@ package dispatcher import ( - "context" "sync" - - "github.com/certimate-go/certimate/internal/domain" - "github.com/certimate-go/certimate/internal/repository" ) -type workflowRepository interface { - GetById(ctx context.Context, id string) (*domain.Workflow, error) - Save(ctx context.Context, workflow *domain.Workflow) (*domain.Workflow, error) -} - -type workflowRunRepository interface { - GetById(ctx context.Context, id string) (*domain.WorkflowRun, error) - Save(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) -} - -type workflowLogRepository interface { - Save(ctx context.Context, workflowLog *domain.WorkflowLog) (*domain.WorkflowLog, error) -} - var ( - instance *WorkflowDispatcher + instance WorkflowDispatcher intanceOnce sync.Once ) -func GetSingletonDispatcher() *WorkflowDispatcher { +func GetSingletonDispatcher() WorkflowDispatcher { intanceOnce.Do(func() { - instance = newWorkflowDispatcher(repository.NewWorkflowRepository(), repository.NewWorkflowRunRepository(), repository.NewWorkflowLogRepository()) + instance = newWorkflowDispatcher() }) - return instance } diff --git a/internal/workflow/dispatcher/task.go b/internal/workflow/dispatcher/task.go new file mode 100644 index 000000000..2b2ebecc7 --- /dev/null +++ b/internal/workflow/dispatcher/task.go @@ -0,0 +1,13 @@ +package dispatcher + +import ( + "context" +) + +type taskInfo struct { + WorkflowId string + RunId string + + ctx context.Context + cancel context.CancelFunc +} diff --git a/internal/workflow/engine/context.go b/internal/workflow/engine/context.go new file mode 100644 index 000000000..2efc7eafb --- /dev/null +++ b/internal/workflow/engine/context.go @@ -0,0 +1,58 @@ +package engine + +import ( + "context" +) + +type WorkflowContext struct { + WorkflowId string + RunId string + RunGraph *Graph + + engine WorkflowEngine + variables VariableManager + inputs InOutManager + + ctx context.Context +} + +func (c *WorkflowContext) SetExecutingWorkflow(workflowId string, runId string, runGraph *Graph) *WorkflowContext { + c.WorkflowId = workflowId + c.RunId = runId + c.RunGraph = runGraph + return c +} + +func (c *WorkflowContext) SetEngine(engine WorkflowEngine) *WorkflowContext { + c.engine = engine + return c +} + +func (c *WorkflowContext) SetVariablesManager(inputs VariableManager) *WorkflowContext { + c.variables = inputs + return c +} + +func (c *WorkflowContext) SetInputsManager(manager InOutManager) *WorkflowContext { + c.inputs = manager + return c +} + +func (c *WorkflowContext) SetContext(ctx context.Context) *WorkflowContext { + c.ctx = ctx + return c +} + +func (c *WorkflowContext) Clone() *WorkflowContext { + return &WorkflowContext{ + WorkflowId: c.WorkflowId, + RunId: c.RunId, + RunGraph: c.RunGraph, + + engine: c.engine, + variables: c.variables, + inputs: c.inputs, + + ctx: c.ctx, + } +} diff --git a/internal/workflow/engine/deps.go b/internal/workflow/engine/deps.go new file mode 100644 index 000000000..a7c30e95b --- /dev/null +++ b/internal/workflow/engine/deps.go @@ -0,0 +1,26 @@ +package engine + +import ( + "context" + + "github.com/certimate-go/certimate/internal/domain" +) + +type certificateRepository interface { + GetById(ctx context.Context, id string) (*domain.Certificate, error) + GetByWorkflowRunIdAndNodeId(ctx context.Context, workflowRunId string, workflowNodeId string) (*domain.Certificate, error) + Save(ctx context.Context, certificate *domain.Certificate) (*domain.Certificate, error) +} + +type workflowRunRepository interface { + GetById(ctx context.Context, workflowRunId string) (*domain.WorkflowRun, error) +} + +type workflowOutputRepository interface { + GetByNodeId(ctx context.Context, workflowNodeId string) (*domain.WorkflowOutput, error) + Save(ctx context.Context, workflowOutput *domain.WorkflowOutput) (*domain.WorkflowOutput, error) +} + +type settingsRepository interface { + GetByName(ctx context.Context, name string) (*domain.Settings, error) +} diff --git a/internal/workflow/engine/engine.go b/internal/workflow/engine/engine.go new file mode 100644 index 000000000..fa41f93bc --- /dev/null +++ b/internal/workflow/engine/engine.go @@ -0,0 +1,296 @@ +package engine + +import ( + "context" + "errors" + "fmt" + "log/slog" + "sync" + + "github.com/samber/lo" + + "github.com/certimate-go/certimate/internal/app" + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/internal/repository" + "github.com/certimate-go/certimate/pkg/logging" +) + +type WorkflowEngine interface { + Invoke(ctx context.Context, workflowId string, runId string, graph *Graph) error + + OnStart(callback func(ctx context.Context) error) + OnEnd(callback func(ctx context.Context) error) + OnError(callback func(ctx context.Context, err error) error) + OnNodeStart(callback func(ctx context.Context, node *Node) error) + OnNodeEnd(callback func(ctx context.Context, node *Node, res *NodeExecutionResult) error) + OnNodeError(callback func(ctx context.Context, node *Node, err error) error) + OnNodeLogging(callback func(ctx context.Context, node *Node, log logging.Record) error) +} + +type workflowEngine struct { + logger *slog.Logger + + executors map[NodeType]NodeExecutor + + hooksMtx sync.RWMutex + onStartHooks [](func(ctx context.Context) error) + onEndHooks [](func(ctx context.Context) error) + onErrorHooks [](func(ctx context.Context, err error) error) + onNodeStartHooks [](func(ctx context.Context, node *Node) error) + onNodeEndHooks [](func(ctx context.Context, node *Node, res *NodeExecutionResult) error) + onNodeErrorHooks [](func(ctx context.Context, node *Node, err error) error) + onNodeLoggingHooks [](func(ctx context.Context, node *Node, log logging.Record) error) + + wfoutputRepo workflowOutputRepository +} + +var _ WorkflowEngine = (*workflowEngine)(nil) + +func (we *workflowEngine) Invoke(ctx context.Context, workflowId string, runId string, runGraph *Graph) error { + we.fireOnStartHooks(ctx) + + defer func() { + if r := recover(); r != nil { + we.fireOnErrorHooks(ctx, fmt.Errorf("workflow engine panic: %v", r)) + } + }() + + wfCtx := (&WorkflowContext{}). + SetExecutingWorkflow(workflowId, runId, runGraph). + SetEngine(we). + SetVariablesManager(newVariableManager()). + SetInputsManager(newInOutManager()). + SetContext(ctx) + if err := we.executeBlocks(wfCtx, runGraph.Nodes); err != nil { + if !errors.Is(err, errInterrupted) { + we.fireOnErrorHooks(ctx, err) + return err + } + } + + we.fireOnEndHooks(ctx) + + return nil +} + +func (we *workflowEngine) OnStart(callback func(ctx context.Context) error) { + we.hooksMtx.Lock() + defer we.hooksMtx.Unlock() + we.onStartHooks = append(we.onStartHooks, callback) +} + +func (we *workflowEngine) OnEnd(callback func(ctx context.Context) error) { + we.hooksMtx.Lock() + defer we.hooksMtx.Unlock() + we.onEndHooks = append(we.onEndHooks, callback) +} + +func (we *workflowEngine) OnError(callback func(ctx context.Context, err error) error) { + we.hooksMtx.Lock() + defer we.hooksMtx.Unlock() + we.onErrorHooks = append(we.onErrorHooks, callback) +} + +func (we *workflowEngine) OnNodeStart(callback func(ctx context.Context, node *Node) error) { + we.hooksMtx.Lock() + defer we.hooksMtx.Unlock() + we.onNodeStartHooks = append(we.onNodeStartHooks, callback) +} + +func (we *workflowEngine) OnNodeEnd(callback func(ctx context.Context, node *Node, res *NodeExecutionResult) error) { + we.hooksMtx.Lock() + defer we.hooksMtx.Unlock() + we.onNodeEndHooks = append(we.onNodeEndHooks, callback) +} + +func (we *workflowEngine) OnNodeError(callback func(ctx context.Context, node *Node, err error) error) { + we.hooksMtx.Lock() + defer we.hooksMtx.Unlock() + we.onNodeErrorHooks = append(we.onNodeErrorHooks, callback) +} + +func (we *workflowEngine) OnNodeLogging(callback func(ctx context.Context, node *Node, log logging.Record) error) { + we.hooksMtx.Lock() + defer we.hooksMtx.Unlock() + we.onNodeLoggingHooks = append(we.onNodeLoggingHooks, callback) +} + +func (we *workflowEngine) executeNode(wfCtx *WorkflowContext, node *Node) error { + executor, ok := we.executors[node.Type] + if !ok { + err := fmt.Errorf("workflow engine: no executor registered for node type: '%s'", node.Type) + return err + } else { + logger := slog.New(logging.NewHookHandler(&logging.HookHandlerOptions{ + Level: slog.LevelDebug, + WriteFunc: func(ctx context.Context, record *logging.Record) error { + we.fireOnNodeLoggingHooks(ctx, node, *record) + return nil + }, + })) + executor.SetLogger(logger) + } + + we.fireOnNodeStartHooks(wfCtx.ctx, node) + + execCtx := newNodeExecutionContext(wfCtx, node) + execRes, err := executor.Execute(execCtx) + if err != nil { + we.fireOnNodeErrorHooks(wfCtx.ctx, node, err) + return err + } + + we.fireOnNodeEndHooks(wfCtx.ctx, node, execRes) + + if execRes != nil { + if execRes.Variables != nil { + for _, variable := range execRes.Variables { + wfCtx.variables.Add(variable) + } + } + + if execRes.Outputs != nil { + for _, output := range execRes.Outputs { + wfCtx.inputs.Add(output) + } + } + + execOutputs := lo.Filter(execRes.Outputs, func(state InOutState, _ int) bool { return state.Persistent }) + if execRes.outputForced || len(execOutputs) > 0 { + output := &domain.WorkflowOutput{ + WorkflowId: execCtx.WorkflowId, + RunId: execCtx.RunId, + NodeId: execCtx.Node.Id, + NodeConfig: execCtx.Node.Data.Config, + Succeeded: true, // 目前恒为 true + } + if len(execOutputs) > 0 { + output.Outputs = lo.Map(execOutputs, func(state InOutState, _ int) *domain.WorkflowOutputEntry { + return &domain.WorkflowOutputEntry{ + Name: state.Name, + Type: state.Type, + Value: state.ValueString(), + ValueType: state.ValueType, + } + }) + } + if _, err := we.wfoutputRepo.Save(execCtx.ctx, output); err != nil { + we.logger.Warn("failed to save node output") + } + } + + if execRes.Interrupted { + return errInterrupted + } + } + + return nil +} + +func (we *workflowEngine) executeBlocks(wfCtx *WorkflowContext, blocks []*Node) error { + for _, node := range blocks { + select { + case <-wfCtx.ctx.Done(): + return wfCtx.ctx.Err() + default: + } + + err := we.executeNode(wfCtx, node) + if err != nil { + return err + } + } + + return nil +} + +func (we *workflowEngine) fireOnStartHooks(ctx context.Context) { + we.hooksMtx.RLock() + defer we.hooksMtx.RUnlock() + for _, cb := range we.onStartHooks { + if cbErr := cb(ctx); cbErr != nil { + we.logger.Error("workflow engine: error in onStart hook", slog.Any("error", cbErr)) + } + } +} + +func (we *workflowEngine) fireOnEndHooks(ctx context.Context) { + we.hooksMtx.RLock() + defer we.hooksMtx.RUnlock() + for _, cb := range we.onEndHooks { + if cbErr := cb(ctx); cbErr != nil { + we.logger.Error("workflow engine: error in onEnd hook", slog.Any("error", cbErr)) + } + } +} + +func (we *workflowEngine) fireOnErrorHooks(ctx context.Context, err error) { + we.hooksMtx.RLock() + defer we.hooksMtx.RUnlock() + for _, cb := range we.onErrorHooks { + if cbErr := cb(ctx, err); cbErr != nil { + we.logger.Error("workflow engine: error in onError hook", slog.Any("error", cbErr)) + } + } +} + +func (we *workflowEngine) fireOnNodeStartHooks(ctx context.Context, node *Node) { + we.hooksMtx.RLock() + defer we.hooksMtx.RUnlock() + for _, cb := range we.onNodeStartHooks { + if cbErr := cb(ctx, node); cbErr != nil { + we.logger.Error("workflow engine: error in onNodeStart hook", slog.Any("error", cbErr)) + } + } +} + +func (we *workflowEngine) fireOnNodeEndHooks(ctx context.Context, node *Node, result *NodeExecutionResult) { + we.hooksMtx.RLock() + defer we.hooksMtx.RUnlock() + for _, cb := range we.onNodeEndHooks { + if cbErr := cb(ctx, node, result); cbErr != nil { + we.logger.Error("workflow engine: error in onNodeEnd hook", slog.Any("error", cbErr)) + } + } +} + +func (we *workflowEngine) fireOnNodeErrorHooks(ctx context.Context, node *Node, err error) { + we.hooksMtx.RLock() + defer we.hooksMtx.RUnlock() + for _, cb := range we.onNodeErrorHooks { + if cbErr := cb(ctx, node, err); cbErr != nil { + we.logger.Error("workflow engine: error in onNodeError hook", slog.Any("error", cbErr)) + } + } +} + +func (we *workflowEngine) fireOnNodeLoggingHooks(ctx context.Context, node *Node, log logging.Record) { + we.hooksMtx.RLock() + defer we.hooksMtx.RUnlock() + for _, cb := range we.onNodeLoggingHooks { + if cbErr := cb(ctx, node, log); cbErr != nil { + we.logger.Error("workflow engine: error in onNodeLogging hook", slog.Any("error", cbErr)) + } + } +} + +func NewWorkflowEngine() WorkflowEngine { + engine := &workflowEngine{ + logger: app.GetLogger(), + executors: make(map[NodeType]NodeExecutor), + wfoutputRepo: repository.NewWorkflowOutputRepository(), + } + engine.executors[NodeTypeStart] = newStartNodeExecutor() + engine.executors[NodeTypeEnd] = newEndNodeExecutor() + engine.executors[NodeTypeCondition] = newConditionNodeExecutor() + engine.executors[NodeTypeBranchBlock] = newBranchBlockNodeExecutor() + engine.executors[NodeTypeTryCatch] = newTryCatchNodeExecutor() + engine.executors[NodeTypeTryBlock] = newTryBlockNodeExecutor() + engine.executors[NodeTypeCatchBlock] = newCatchBlockNodeExecutor() + engine.executors[NodeTypeBizApply] = newBizApplyNodeExecutor() + engine.executors[NodeTypeBizUpload] = newBizUploadNodeExecutor() + engine.executors[NodeTypeBizMonitor] = newBizMonitorNodeExecutor() + engine.executors[NodeTypeBizDeploy] = newBizDeployNodeExecutor() + engine.executors[NodeTypeBizNotify] = newBizNotifyNodeExecutor() + return engine +} diff --git a/internal/workflow/engine/errors.go b/internal/workflow/engine/errors.go new file mode 100644 index 000000000..8b83d52dd --- /dev/null +++ b/internal/workflow/engine/errors.go @@ -0,0 +1,7 @@ +package engine + +import ( + "errors" +) + +var errInterrupted = errors.New("workflow engine: interrupted, may be ended") diff --git a/internal/workflow/engine/executor.go b/internal/workflow/engine/executor.go new file mode 100644 index 000000000..a55182bbf --- /dev/null +++ b/internal/workflow/engine/executor.go @@ -0,0 +1,157 @@ +package engine + +import ( + "context" + "log/slog" + "sync" +) + +type NodeExecutor interface { + withLogger + + Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) +} + +type nodeExecutor struct { + logger *slog.Logger +} + +func (e *nodeExecutor) SetLogger(logger *slog.Logger) { + e.logger = logger +} + +type NodeExecutionContext struct { + WorkflowContext + + Node *Node +} + +func (c *NodeExecutionContext) SetExecutingWorkflow(workflowId string, runId string, runGraph *Graph) *NodeExecutionContext { + c.WorkflowContext.SetExecutingWorkflow(workflowId, runId, runGraph) + return c +} + +func (c *NodeExecutionContext) SetExecutingNode(node *Node) *NodeExecutionContext { + c.Node = node + return c +} + +func (c *NodeExecutionContext) SetEngine(engine WorkflowEngine) *NodeExecutionContext { + c.WorkflowContext.SetEngine(engine) + return c +} + +func (c *NodeExecutionContext) SetVariablesManager(variables VariableManager) *NodeExecutionContext { + c.WorkflowContext.SetVariablesManager(variables) + return c +} + +func (c *NodeExecutionContext) SetInputsManager(inputs InOutManager) *NodeExecutionContext { + c.WorkflowContext.SetInputsManager(inputs) + return c +} + +func (c *NodeExecutionContext) SetContext(ctx context.Context) *NodeExecutionContext { + c.WorkflowContext.SetContext(ctx) + return c +} + +func newNodeExecutionContext(wfCtx *WorkflowContext, node *Node) *NodeExecutionContext { + return (&NodeExecutionContext{}). + SetExecutingWorkflow(wfCtx.WorkflowId, wfCtx.RunId, wfCtx.RunGraph). + SetExecutingNode(node). + SetEngine(wfCtx.engine). + SetVariablesManager(wfCtx.variables). + SetInputsManager(wfCtx.inputs). + SetContext(wfCtx.ctx) +} + +type NodeExecutionResult struct { + node *Node + + Interrupted bool // 是否中断执行(通常由 End 节点主动触发) + + variablesMtx sync.Mutex + Variables []VariableState + + outputForced bool // 即使 Outputs 为空,也强制持久化输出 + outputsMtx sync.Mutex + Outputs []InOutState +} + +func (r *NodeExecutionResult) AddVariable(key string, value any, valueType string) { + r.AddVariableWithScope("", key, value, valueType) +} + +func (r *NodeExecutionResult) AddVariableWithScope(scope string, key string, value any, valueType string) { + r.addVariableState(VariableState{ + Scope: scope, + Key: key, + Value: value, + ValueType: valueType, + }) +} + +func (r *NodeExecutionResult) addVariableState(state VariableState) { + r.variablesMtx.Lock() + defer r.variablesMtx.Unlock() + + if r.Variables == nil { + r.Variables = make([]VariableState, 0) + } + + for i, item := range r.Variables { + if item.Scope == state.Scope && item.Key == state.Key { + r.Variables[i] = state + return + } + } + r.Variables = append(r.Variables, state) +} + +func (r *NodeExecutionResult) AddOutput(stype string, key string, value any, valueType string) { + r.addOutputState(InOutState{ + NodeId: r.node.Id, + Type: stype, + Name: key, + Value: value, + ValueType: valueType, + Persistent: false, + }) +} + +func (r *NodeExecutionResult) AddOutputWithPersistent(stype string, key string, value any, valueType string) { + r.addOutputState(InOutState{ + NodeId: r.node.Id, + Type: stype, + Name: key, + Value: value, + ValueType: valueType, + Persistent: true, + }) +} + +func (r *NodeExecutionResult) addOutputState(state InOutState) { + r.outputsMtx.Lock() + defer r.outputsMtx.Unlock() + + if r.Outputs == nil { + r.Outputs = make([]InOutState, 0) + } + + for i, t := range r.Outputs { + if t.NodeId == state.NodeId && t.Name == state.Name { + r.Outputs[i] = state + return + } + } + r.Outputs = append(r.Outputs, state) +} + +func newNodeExecutionResult(node *Node) *NodeExecutionResult { + return &NodeExecutionResult{ + node: node, + Variables: make([]VariableState, 0), + Outputs: make([]InOutState, 0), + } +} diff --git a/internal/workflow/engine/executor_bizapply.go b/internal/workflow/engine/executor_bizapply.go new file mode 100644 index 000000000..f69797c24 --- /dev/null +++ b/internal/workflow/engine/executor_bizapply.go @@ -0,0 +1,197 @@ +package engine + +import ( + "fmt" + "log/slog" + "maps" + "math" + "time" + + "github.com/certimate-go/certimate/internal/applicant" + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/internal/repository" + xcert "github.com/certimate-go/certimate/pkg/utils/cert" +) + +/** + * Result Variables: + * - node.skipped: boolean + * - certificate.validity: boolean + * - certificate.daysLeft: number + * + * Result Outputs: + * - ref: certificate: string + */ +type bizApplyNodeExecutor struct { + nodeExecutor + + certificateRepo certificateRepository + wfoutputRepo workflowOutputRepository +} + +func (ne *bizApplyNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { + execRes := newNodeExecutionResult(execCtx.Node) + + nodeCfg := execCtx.Node.Data.Config.AsBizApply() + ne.logger.Info("ready to request certificate ...", slog.Any("config", nodeCfg)) + + // 查询上次执行结果 + lastOutput, lastCertificate, err := ne.getLastOutputArtifacts(execCtx) + if err != nil { + return execRes, err + } else if lastCertificate != nil { + execRes.AddOutput(stateIOTypeRef, "certificate", fmt.Sprintf("%s#%s", domain.CollectionNameCertificate, lastCertificate.Id), "string") + execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, time.Now().After(lastCertificate.ValidityNotAfter), "boolean") + execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDaysLeft, int32(time.Until(lastCertificate.ValidityNotAfter).Hours()/24), "number") + } + + // 检测是否可以跳过本次执行 + if skippable, reason := ne.checkCanSkip(execCtx, lastOutput, lastCertificate); skippable { + ne.logger.Info(fmt.Sprintf("skip this application, because %s", reason)) + + execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, true, "boolean") + return execRes, nil + } else if reason != "" { + ne.logger.Info(fmt.Sprintf("re-apply, because %s", reason)) + } else { + ne.logger.Info("no found last issued certificate, begin to apply") + } + + // 初始化申请器 + // TODO: 解耦 + applicant, err := applicant.NewWithWorkflowNode(applicant.ApplicantWithWorkflowNodeConfig{ + WorkflowId: execCtx.WorkflowId, + Node: execCtx.Node, + Logger: ne.logger, + }) + if err != nil { + ne.logger.Warn("failed to create applicant provider") + return execRes, err + } + + // 申请证书 + applyResult, err := applicant.Apply(execCtx.ctx) + if err != nil { + ne.logger.Warn("failed to obtain certificate") + return execRes, err + } + + // 解析证书 + certX509, err := xcert.ParseCertificateFromPEM(applyResult.FullChainCertificate) + if err != nil { + ne.logger.Warn("failed to parse certificate, may be the CA responded error") + return execRes, err + } + + // 保存证书实体 + certificate := &domain.Certificate{ + Source: domain.CertificateSourceTypeRequest, + Certificate: applyResult.FullChainCertificate, + PrivateKey: applyResult.PrivateKey, + IssuerCertificate: applyResult.IssuerCertificate, + ACMEAccountUrl: applyResult.ACMEAccountUrl, + ACMECertUrl: applyResult.ACMECertUrl, + ACMECertStableUrl: applyResult.ACMECertStableUrl, + WorkflowId: execCtx.WorkflowId, + WorkflowRunId: execCtx.RunId, + WorkflowNodeId: execCtx.Node.Id, + } + certificate.PopulateFromX509(certX509) + if certificate, err := ne.certificateRepo.Save(execCtx.ctx, certificate); err != nil { + ne.logger.Warn("failed to save certificate") + return execRes, err + } else { + ne.logger.Info("certificate saved", slog.String("recordId", certificate.Id)) + } + + // 保存 ARI 记录 + if lastCertificate != nil && applyResult.ARIReplaced { + lastCertificate.ACMERenewed = true + ne.certificateRepo.Save(execCtx.ctx, lastCertificate) + } + + // 节点输出 + execRes.AddOutputWithPersistent(stateIOTypeRef, "certificate", fmt.Sprintf("%s#%s", domain.CollectionNameCertificate, certificate.Id), "string") + execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, false, "boolean") + execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, true, "boolean") + execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDaysLeft, int32(time.Until(certificate.ValidityNotAfter).Hours()/24), "number") + + ne.logger.Info("application completed") + return execRes, nil +} + +func (ne *bizApplyNodeExecutor) getLastOutputArtifacts(execCtx *NodeExecutionContext) (*domain.WorkflowOutput, *domain.Certificate, error) { + lastOutput, err := ne.wfoutputRepo.GetByNodeId(execCtx.ctx, execCtx.Node.Id) + if err != nil && !domain.IsRecordNotFoundError(err) { + return nil, nil, fmt.Errorf("failed to get last output record of node #%s: %w", execCtx.Node.Id, err) + } + + if lastOutput != nil { + lastCertificate, err := ne.certificateRepo.GetByWorkflowRunIdAndNodeId(execCtx.ctx, lastOutput.RunId, lastOutput.NodeId) + if err != nil && !domain.IsRecordNotFoundError(err) { + return lastOutput, nil, fmt.Errorf("failed to get last certificate record of node #%s: %w", execCtx.Node.Id, err) + } + + return lastOutput, lastCertificate, nil + } + + return lastOutput, nil, nil +} + +func (ne *bizApplyNodeExecutor) checkCanSkip(execCtx *NodeExecutionContext, lastOutput *domain.WorkflowOutput, lastCertificate *domain.Certificate) (_skip bool, _reason string) { + thisNodeCfg := execCtx.Node.Data.Config.AsBizApply() + + if lastOutput != nil && lastOutput.Succeeded { + // 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致 + lastNodeCfg := lastOutput.NodeConfig.AsBizApply() + + if thisNodeCfg.Domains != lastNodeCfg.Domains { + return false, "the configuration item 'Domains' changed" + } + if thisNodeCfg.ContactEmail != lastNodeCfg.ContactEmail { + return false, "the configuration item 'ContactEmail' changed" + } + if thisNodeCfg.Provider != lastNodeCfg.Provider { + return false, "the configuration item 'Provider' changed" + } + if thisNodeCfg.ProviderAccessId != lastNodeCfg.ProviderAccessId { + return false, "the configuration item 'ProviderAccessId' changed" + } + if !maps.Equal(thisNodeCfg.ProviderConfig, lastNodeCfg.ProviderConfig) { + return false, "the configuration item 'ProviderConfig' changed" + } + if thisNodeCfg.CAProvider != lastNodeCfg.CAProvider { + return false, "the configuration item 'CAProvider' changed" + } + if thisNodeCfg.CAProviderAccessId != lastNodeCfg.CAProviderAccessId { + return false, "the configuration item 'CAProviderAccessId' changed" + } + if !maps.Equal(thisNodeCfg.CAProviderConfig, lastNodeCfg.CAProviderConfig) { + return false, "the configuration item 'CAProviderConfig' changed" + } + if thisNodeCfg.KeyAlgorithm != lastNodeCfg.KeyAlgorithm { + return false, "the configuration item 'KeyAlgorithm' changed" + } + } + + if lastCertificate != nil { + renewalInterval := time.Duration(thisNodeCfg.SkipBeforeExpiryDays) * time.Hour * 24 + expirationTime := time.Until(lastCertificate.ValidityNotAfter) + daysLeft := int(math.Floor(expirationTime.Hours() / 24)) + if expirationTime > renewalInterval { + return true, fmt.Sprintf("the last issued certificate #%s expires in %d day(s), next renewal will be in %d day(s)", lastCertificate.Id, daysLeft, thisNodeCfg.SkipBeforeExpiryDays) + } + + return false, fmt.Sprintf("the last issued certificate #%s expires in %d day(s)", lastCertificate.Id, daysLeft) + } + + return false, "" +} + +func newBizApplyNodeExecutor() NodeExecutor { + return &bizApplyNodeExecutor{ + nodeExecutor: nodeExecutor{logger: slog.Default()}, + certificateRepo: repository.NewCertificateRepository(), + wfoutputRepo: repository.NewWorkflowOutputRepository(), + } +} diff --git a/internal/workflow/engine/executor_bizdeploy.go b/internal/workflow/engine/executor_bizdeploy.go new file mode 100644 index 000000000..73dc965be --- /dev/null +++ b/internal/workflow/engine/executor_bizdeploy.go @@ -0,0 +1,133 @@ +package engine + +import ( + "fmt" + "log/slog" + "maps" + "strings" + + "github.com/certimate-go/certimate/internal/deployer" + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/internal/repository" +) + +/** + * Result Variables: + * - node.skipped: boolean + */ +type bizDeployNodeExecutor struct { + nodeExecutor + + certificateRepo certificateRepository + wfoutputRepo workflowOutputRepository +} + +func (ne *bizDeployNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { + execRes := newNodeExecutionResult(execCtx.Node) + + nodeCfg := execCtx.Node.Data.Config.AsBizDeploy() + ne.logger.Info("ready to deploy certificate ...", slog.Any("config", nodeCfg)) + + // 查询上次执行结果 + lastOutput, err := ne.getLastOutputArtifacts(execCtx) + if err != nil { + return execRes, err + } + + // 获取前序节点输出证书 + var inputCertificate *domain.Certificate + if inputState, ok := execCtx.inputs.Get(nodeCfg.CertificateOutputNodeId, "certificate"); ok { + if inputStateValue, ok := inputState.Value.(string); ok { + s := strings.Split(inputStateValue, "#") + if len(s) == 2 { + certificate, err := ne.certificateRepo.GetById(execCtx.ctx, s[1]) + if err != nil { + ne.logger.Warn("failed to get input certificate") + return execRes, err + } + + inputCertificate = certificate + } + } + } + if inputCertificate == nil { + return execRes, fmt.Errorf("invalid input certificate") + } + + // 检测是否可以跳过本次执行 + if lastOutput != nil && inputCertificate.CreatedAt.Before(lastOutput.UpdatedAt) { + if skippable, reason := ne.checkCanSkip(execCtx, lastOutput); skippable { + ne.logger.Info(fmt.Sprintf("skip this deployment, because %s", reason)) + + execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, true, "boolean") + return execRes, nil + } else if reason != "" { + ne.logger.Info(fmt.Sprintf("re-deploy, because %s", reason)) + } + } + + // 初始化部署器 + // TODO: 解耦 + deployer, err := deployer.NewWithWorkflowNode(deployer.DeployerWithWorkflowNodeConfig{ + Node: execCtx.Node, + Logger: ne.logger, + CertificatePEM: inputCertificate.Certificate, + PrivateKeyPEM: inputCertificate.PrivateKey, + }) + if err != nil { + ne.logger.Warn("failed to create deployer provider") + return execRes, err + } + + // 部署证书 + if err := deployer.Deploy(execCtx.ctx); err != nil { + ne.logger.Warn("failed to deploy certificate") + return execRes, err + } + + // 节点输出 + execRes.outputForced = true + execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, false, "boolean") + + ne.logger.Info("deployment completed") + return execRes, nil +} + +func (ne *bizDeployNodeExecutor) getLastOutputArtifacts(execCtx *NodeExecutionContext) (*domain.WorkflowOutput, error) { + lastOutput, err := ne.wfoutputRepo.GetByNodeId(execCtx.ctx, execCtx.Node.Id) + if err != nil && !domain.IsRecordNotFoundError(err) { + return nil, fmt.Errorf("failed to get last output record of node #%s: %w", execCtx.Node.Id, err) + } + + return lastOutput, nil +} + +func (ne *bizDeployNodeExecutor) checkCanSkip(execCtx *NodeExecutionContext, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) { + thisNodeCfg := execCtx.Node.Data.Config.AsBizDeploy() + + if lastOutput != nil && lastOutput.Succeeded { + // 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致 + lastNodeCfg := lastOutput.NodeConfig.AsBizDeploy() + + if thisNodeCfg.ProviderAccessId != lastNodeCfg.ProviderAccessId { + return false, "the configuration item 'ProviderAccessId' changed" + } + if !maps.Equal(thisNodeCfg.ProviderConfig, lastNodeCfg.ProviderConfig) { + return false, "the configuration item 'ProviderConfig' changed" + } + + if thisNodeCfg.SkipOnLastSucceeded { + return true, "the last deployment already completed" + } + } + + return false, "" +} + +func newBizDeployNodeExecutor() NodeExecutor { + return &bizDeployNodeExecutor{ + nodeExecutor: nodeExecutor{logger: slog.Default()}, + certificateRepo: repository.NewCertificateRepository(), + wfoutputRepo: repository.NewWorkflowOutputRepository(), + } +} diff --git a/internal/workflow/engine/executor_bizmonitor.go b/internal/workflow/engine/executor_bizmonitor.go new file mode 100644 index 000000000..d4616e012 --- /dev/null +++ b/internal/workflow/engine/executor_bizmonitor.go @@ -0,0 +1,157 @@ +package engine + +import ( + "crypto/x509" + "fmt" + "log/slog" + "math" + "net" + "net/http" + "strconv" + "strings" + "time" + + "github.com/certimate-go/certimate/internal/repository" + xhttp "github.com/certimate-go/certimate/pkg/utils/http" + xtls "github.com/certimate-go/certimate/pkg/utils/tls" +) + +/** + * Result Variables: + * - certificate.validity: boolean + * - certificate.daysLeft: number + */ +type bizMonitorNodeExecutor struct { + nodeExecutor + + certificateRepo certificateRepository +} + +func (ne *bizMonitorNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { + execRes := newNodeExecutionResult(execCtx.Node) + + nodeCfg := execCtx.Node.Data.Config.AsBizMonitor() + ne.logger.Info("ready to monitor certificate ...", slog.Any("config", nodeCfg)) + + targetAddr := net.JoinHostPort(nodeCfg.Host, strconv.Itoa(int(nodeCfg.Port))) + if nodeCfg.Port == 0 { + targetAddr = net.JoinHostPort(nodeCfg.Host, "443") + } + + targetDomain := nodeCfg.Domain + if targetDomain == "" { + targetDomain = nodeCfg.Host + } + + ne.logger.Info(fmt.Sprintf("retrieving certificate at %s (domain: %s)", targetAddr, targetDomain)) + + const MAX_ATTEMPTS = 3 + const RETRY_INTERVAL = 2 * time.Second + var err error + var certs []*x509.Certificate + for attempt := 0; attempt < MAX_ATTEMPTS; attempt++ { + if attempt > 0 { + ne.logger.Info(fmt.Sprintf("retry %d time(s) ...", attempt, targetAddr)) + + select { + case <-execCtx.ctx.Done(): + return execRes, execCtx.ctx.Err() + case <-time.After(RETRY_INTERVAL): + } + } + + certs, err = ne.tryRetrievePeerCertificates(execCtx, targetAddr, targetDomain, nodeCfg.RequestPath) + if err == nil { + break + } + } + + if err != nil { + ne.logger.Warn("failed to monitor certificate") + return execRes, err + } else { + if len(certs) == 0 { + ne.logger.Warn("no ssl certificates retrieved in http response") + + execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, false, "boolean") + execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDaysLeft, 0, "number") + } else { + cert := certs[0] // 只取证书链中的第一个证书,即服务器证书 + ne.logger.Info(fmt.Sprintf("ssl certificate retrieved (serial='%s', subject='%s', issuer='%s', not_before='%s', not_after='%s', sans='%s')", + cert.SerialNumber, cert.Subject.String(), cert.Issuer.String(), + cert.NotBefore.Format(time.RFC3339), cert.NotAfter.Format(time.RFC3339), + strings.Join(cert.DNSNames, ";")), + ) + + now := time.Now() + isCertPeriodValid := now.Before(cert.NotAfter) && now.After(cert.NotBefore) + isCertHostMatched := true + if err := cert.VerifyHostname(targetDomain); err != nil { + isCertHostMatched = false + } + + validated := isCertPeriodValid && isCertHostMatched + daysLeft := int(math.Floor(time.Until(cert.NotAfter).Hours() / 24)) + execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, validated, "boolean") + execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDaysLeft, daysLeft, "number") + + if validated { + ne.logger.Info(fmt.Sprintf("the certificate is valid, and will expire in %d day(s)", daysLeft)) + } else { + if !isCertHostMatched { + ne.logger.Warn("the certificate is invalid, because it is not matched the host") + } else if !isCertPeriodValid { + ne.logger.Warn("the certificate is invalid, because it is either expired or not yet valid") + } else { + ne.logger.Warn("the certificate is invalid") + } + } + } + } + + ne.logger.Info("monitoring completed") + return execRes, nil +} + +func (ne *bizMonitorNodeExecutor) tryRetrievePeerCertificates(execCtx *NodeExecutionContext, addr, domain, requestPath string) ([]*x509.Certificate, error) { + transport := xhttp.NewDefaultTransport() + transport.TLSClientConfig = xtls.NewInsecureConfig() + + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + Timeout: 30 * time.Second, + Transport: transport, + } + + url := fmt.Sprintf("https://%s/%s", addr, strings.TrimLeft(requestPath, "/")) + req, err := http.NewRequestWithContext(execCtx.ctx, http.MethodHead, url, nil) + if err != nil { + err = fmt.Errorf("failed to create http request: %w", err) + ne.logger.Warn(err.Error()) + return nil, err + } + + req.Header.Set("Host", domain) + req.Header.Set("User-Agent", "certimate") + resp, err := client.Do(req) + if err != nil { + err = fmt.Errorf("failed to send http request: %w", err) + ne.logger.Warn(err.Error()) + return nil, err + } + defer resp.Body.Close() + + if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 { + return make([]*x509.Certificate, 0), nil + } + return resp.TLS.PeerCertificates, nil +} + +func newBizMonitorNodeExecutor() NodeExecutor { + return &bizMonitorNodeExecutor{ + nodeExecutor: nodeExecutor{logger: slog.Default()}, + certificateRepo: repository.NewCertificateRepository(), + } +} diff --git a/internal/workflow/engine/executor_biznotify.go b/internal/workflow/engine/executor_biznotify.go new file mode 100644 index 000000000..c15c74ce5 --- /dev/null +++ b/internal/workflow/engine/executor_biznotify.go @@ -0,0 +1,74 @@ +package engine + +import ( + "fmt" + "log/slog" + + "github.com/certimate-go/certimate/internal/notify" + "github.com/certimate-go/certimate/internal/repository" +) + +type bizNotifyNodeExecutor struct { + nodeExecutor + + settingsRepo settingsRepository +} + +func (ne *bizNotifyNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { + execRes := newNodeExecutionResult(execCtx.Node) + + nodeCfg := execCtx.Node.Data.Config.AsBizNotify() + ne.logger.Info("ready to send notification ...", slog.Any("config", nodeCfg)) + + // 检测是否可以跳过本次执行 + if skippable := ne.checkCanSkip(execCtx); skippable { + ne.logger.Info(fmt.Sprintf("skip this notification, because all the previous nodes have been skipped")) + return execRes, nil + } + + // 初始化通知器 + deployer, err := notify.NewWithWorkflowNode(notify.NotifierWithWorkflowNodeConfig{ + Node: execCtx.Node, + Logger: ne.logger, + Subject: nodeCfg.Subject, + Message: nodeCfg.Message, + }) + if err != nil { + ne.logger.Warn("failed to create notifier provider") + return execRes, err + } + + // 推送通知 + if err := deployer.Notify(execCtx.ctx); err != nil { + ne.logger.Warn("failed to send notification") + return execRes, err + } + + ne.logger.Info("notification completed") + return execRes, nil +} + +func (ne *bizNotifyNodeExecutor) checkCanSkip(execCtx *NodeExecutionContext) (_skip bool) { + thisNodeCfg := execCtx.Node.Data.Config.AsBizNotify() + if !thisNodeCfg.SkipOnAllPrevSkipped { + return false + } + + var total, skipped int32 + for _, variable := range execCtx.variables.All() { + if variable.Scope != "" && variable.Key == stateVarKeyNodeSkipped { + total++ + if variable.Value == true { + skipped++ + } + } + } + return total > 0 && skipped == total +} + +func newBizNotifyNodeExecutor() NodeExecutor { + return &bizNotifyNodeExecutor{ + nodeExecutor: nodeExecutor{logger: slog.Default()}, + settingsRepo: repository.NewSettingsRepository(), + } +} diff --git a/internal/workflow/engine/executor_bizupload.go b/internal/workflow/engine/executor_bizupload.go new file mode 100644 index 000000000..aa8a5eeff --- /dev/null +++ b/internal/workflow/engine/executor_bizupload.go @@ -0,0 +1,128 @@ +package engine + +import ( + "fmt" + "log/slog" + "strings" + "time" + + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/internal/repository" +) + +/** + * Result Variables: + * - node.skipped: boolean + * - certificate.validity: boolean + * - certificate.daysLeft: number + * + * Result Outputs: + * - ref: certificate: string + */ +type bizUploadNodeExecutor struct { + nodeExecutor + + certificateRepo certificateRepository + wfoutputRepo workflowOutputRepository +} + +func (ne *bizUploadNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { + execRes := newNodeExecutionResult(execCtx.Node) + + nodeCfg := execCtx.Node.Data.Config.AsBizUpload() + ne.logger.Info("ready to upload certiticate ...", slog.Any("config", nodeCfg)) + + // 查询上次执行结果 + lastOutput, lastCertificate, err := ne.getLastOutputArtifacts(execCtx) + if err != nil { + return execRes, err + } else if lastCertificate != nil { + execRes.AddOutput(stateIOTypeRef, "certificate", fmt.Sprintf("%s#%s", domain.CollectionNameCertificate, lastCertificate.Id), "string") + execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, time.Now().After(lastCertificate.ValidityNotAfter), "boolean") + execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDaysLeft, int32(time.Until(lastCertificate.ValidityNotAfter).Hours()/24), "number") + } + + // 检测是否可以跳过本次执行 + if skippable, reason := ne.checkCanSkip(execCtx, lastOutput, lastCertificate); skippable { + ne.logger.Info(fmt.Sprintf("skip this uploading, because %s", reason)) + + execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, true, "boolean") + return execRes, nil + } else if reason != "" { + ne.logger.Info(fmt.Sprintf("re-upload, because %s", reason)) + } else { + ne.logger.Info("no found last uploaded certificate, begin to upload") + } + + // 保存证书实体 + certificate := &domain.Certificate{ + Source: domain.CertificateSourceTypeUpload, + WorkflowId: execCtx.WorkflowId, + WorkflowRunId: execCtx.RunId, + WorkflowNodeId: execCtx.Node.Id, + } + certificate.PopulateFromPEM(nodeCfg.Certificate, nodeCfg.PrivateKey) + if certificate, err := ne.certificateRepo.Save(execCtx.ctx, certificate); err != nil { + ne.logger.Warn("failed to save certificate") + return execRes, err + } else { + ne.logger.Info("certificate saved", slog.String("recordId", certificate.Id)) + } + + // 节点输出 + execRes.AddOutputWithPersistent(stateIOTypeRef, "certificate", fmt.Sprintf("%s#%s", domain.CollectionNameCertificate, certificate.Id), "string") + execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, false, "boolean") + execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, true, "boolean") + execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDaysLeft, int32(time.Until(certificate.ValidityNotAfter).Hours()/24), "number") + + ne.logger.Info("uploading completed") + return execRes, nil +} + +func (ne *bizUploadNodeExecutor) getLastOutputArtifacts(execCtx *NodeExecutionContext) (*domain.WorkflowOutput, *domain.Certificate, error) { + lastOutput, err := ne.wfoutputRepo.GetByNodeId(execCtx.ctx, execCtx.Node.Id) + if err != nil && !domain.IsRecordNotFoundError(err) { + return nil, nil, fmt.Errorf("failed to get last output record of node #%s: %w", execCtx.Node.Id, err) + } + + if lastOutput != nil { + lastCertificate, err := ne.certificateRepo.GetByWorkflowRunIdAndNodeId(execCtx.ctx, lastOutput.RunId, lastOutput.NodeId) + if err != nil && !domain.IsRecordNotFoundError(err) { + return lastOutput, nil, fmt.Errorf("failed to get last certificate record of node #%s: %w", execCtx.Node.Id, err) + } + + return lastOutput, lastCertificate, nil + } + + return lastOutput, nil, nil +} + +func (ne *bizUploadNodeExecutor) checkCanSkip(execCtx *NodeExecutionContext, lastOutput *domain.WorkflowOutput, lastCertificate *domain.Certificate) (_skip bool, _reason string) { + thisNodeCfg := execCtx.Node.Data.Config.AsBizUpload() + + if lastOutput != nil && lastOutput.Succeeded { + // 比较和上次上传时的关键配置(即影响证书上传的)参数是否一致 + lastNodeCfg := lastOutput.NodeConfig.AsBizUpload() + + if strings.TrimSpace(thisNodeCfg.Certificate) != strings.TrimSpace(lastNodeCfg.Certificate) { + return false, "the configuration item 'Certificate' changed" + } + if strings.TrimSpace(thisNodeCfg.PrivateKey) != strings.TrimSpace(lastNodeCfg.PrivateKey) { + return false, "the configuration item 'PrivateKey' changed" + } + } + + if lastCertificate != nil { + return true, "the last uploaded certificate already exists" + } + + return false, "" +} + +func newBizUploadNodeExecutor() NodeExecutor { + return &bizUploadNodeExecutor{ + nodeExecutor: nodeExecutor{logger: slog.Default()}, + certificateRepo: repository.NewCertificateRepository(), + wfoutputRepo: repository.NewWorkflowOutputRepository(), + } +} diff --git a/internal/workflow/engine/executor_condition.go b/internal/workflow/engine/executor_condition.go new file mode 100644 index 000000000..32bff15b1 --- /dev/null +++ b/internal/workflow/engine/executor_condition.go @@ -0,0 +1,106 @@ +package engine + +import ( + "errors" + "fmt" + "log/slog" + + "github.com/samber/lo" +) + +type conditionNodeExecutor struct { + nodeExecutor +} + +func (ne *conditionNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { + var engine *workflowEngine + if we, ok := execCtx.engine.(*workflowEngine); !ok { + panic("impossible!") + } else { + engine = we + } + + execRes := newNodeExecutionResult(execCtx.Node) + + errs := make([]error, 0) + blocks := lo.Filter(execCtx.Node.Blocks, func(n *Node, _ int) bool { return n.Type == NodeTypeBranchBlock }) + for _, node := range blocks { + select { + case <-execCtx.ctx.Done(): + return execRes, execCtx.ctx.Err() + default: + } + + err := engine.executeNode(execCtx.Clone(), node) + if err != nil { + if errors.Is(err, errInterrupted) { + return execRes, err + } + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return execRes, fmt.Errorf("error occurred when executing child nodes: %w", errors.Join(errs...)) + } + + return execRes, nil +} + +func newConditionNodeExecutor() NodeExecutor { + return &conditionNodeExecutor{ + nodeExecutor: nodeExecutor{logger: slog.Default()}, + } +} + +type branchBlockNodeExecutor struct { + nodeExecutor +} + +func (ne *branchBlockNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { + execRes := newNodeExecutionResult(execCtx.Node) + + nodeCfg := execCtx.Node.Data.Config.AsBranchBlock() + if nodeCfg.Expression == nil { + ne.logger.Info("enter this branch without any conditions") + } else { + variables := lo.Reduce(execCtx.variables.All(), func(acc map[string]map[string]any, state VariableState, _ int) map[string]map[string]any { + if _, ok := acc[state.Scope]; !ok { + acc[state.Scope] = make(map[string]any) + } + + // 这里需要把所有值都转换为字符串形式,因为 Expression.Eval 仅支持字符串类型的值 + acc[state.Scope][state.Key] = state.ValueString() + return acc + }, make(map[string]map[string]any)) + + rs, err := nodeCfg.Expression.Eval(variables) + if err != nil { + ne.logger.Warn(fmt.Sprintf("failed to eval expr: %+v", err)) + return execRes, err + } + + if rs.Value == false { + ne.logger.Info("skip this branch, because condition not met") + return execRes, nil + } else { + ne.logger.Info("enter this branch, because condition met") + } + } + + if engine, ok := execCtx.engine.(*workflowEngine); !ok { + panic("impossible!") + } else { + if err := engine.executeBlocks(execCtx.Clone(), execCtx.Node.Blocks); err != nil { + return execRes, err + } + } + + return execRes, nil +} + +func newBranchBlockNodeExecutor() NodeExecutor { + return &branchBlockNodeExecutor{ + nodeExecutor: nodeExecutor{logger: slog.Default()}, + } +} diff --git a/internal/workflow/engine/executor_end.go b/internal/workflow/engine/executor_end.go new file mode 100644 index 000000000..1a9a02fb7 --- /dev/null +++ b/internal/workflow/engine/executor_end.go @@ -0,0 +1,22 @@ +package engine + +import ( + "log/slog" +) + +type endNodeExecutor struct { + nodeExecutor +} + +func (e *endNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { + execRes := newNodeExecutionResult(execCtx.Node) + execRes.Interrupted = true + + return execRes, nil +} + +func newEndNodeExecutor() NodeExecutor { + return &endNodeExecutor{ + nodeExecutor: nodeExecutor{logger: slog.Default()}, + } +} diff --git a/internal/workflow/engine/executor_start.go b/internal/workflow/engine/executor_start.go new file mode 100644 index 000000000..ab0ada84e --- /dev/null +++ b/internal/workflow/engine/executor_start.go @@ -0,0 +1,21 @@ +package engine + +import ( + "log/slog" +) + +type startNodeExecutor struct { + nodeExecutor +} + +func (e *startNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { + execRes := newNodeExecutionResult(execCtx.Node) + + return execRes, nil +} + +func newStartNodeExecutor() NodeExecutor { + return &startNodeExecutor{ + nodeExecutor: nodeExecutor{logger: slog.Default()}, + } +} diff --git a/internal/workflow/engine/executor_trycatch.go b/internal/workflow/engine/executor_trycatch.go new file mode 100644 index 000000000..28d8844ec --- /dev/null +++ b/internal/workflow/engine/executor_trycatch.go @@ -0,0 +1,130 @@ +package engine + +import ( + "errors" + "fmt" + "log/slog" + + "github.com/samber/lo" +) + +type tryCatchNodeExecutor struct { + nodeExecutor +} + +func (ne *tryCatchNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { + var engine *workflowEngine + if we, ok := execCtx.engine.(*workflowEngine); !ok { + panic("impossible!") + } else { + engine = we + } + + execRes := newNodeExecutionResult(execCtx.Node) + + tryErrs := make([]error, 0) + tryBlocks := lo.Filter(execCtx.Node.Blocks, func(n *Node, _ int) bool { return n.Type == NodeTypeTryBlock }) + for _, node := range tryBlocks { + select { + case <-execCtx.ctx.Done(): + return execRes, execCtx.ctx.Err() + default: + } + + err := engine.executeNode(execCtx.Clone(), node) + if err != nil { + if errors.Is(err, errInterrupted) { + return execRes, err + } + tryErrs = append(tryErrs, err) + } + } + + if len(tryErrs) > 0 { + catchErrs := make([]error, 0) + catchBlocks := lo.Filter(execCtx.Node.Blocks, func(n *Node, _ int) bool { return n.Type == NodeTypeCatchBlock }) + for _, node := range catchBlocks { + select { + case <-execCtx.ctx.Done(): + return execRes, execCtx.ctx.Err() + default: + } + + err := engine.executeNode(execCtx.Clone(), node) + if err != nil { + if errors.Is(err, errInterrupted) { + return execRes, err + } + catchErrs = append(catchErrs, err) + } + } + + if len(catchErrs) > 0 { + return execRes, fmt.Errorf("error occurred when executing child nodes: %w", errors.Join(append(tryErrs, catchErrs...)...)) + } + + return execRes, fmt.Errorf("error occurred when executing child nodes: %w", errors.Join(tryErrs...)) + } + + return execRes, nil +} + +func newTryCatchNodeExecutor() NodeExecutor { + return &tryCatchNodeExecutor{ + nodeExecutor: nodeExecutor{logger: slog.Default()}, + } +} + +type tryBlockNodeExecutor struct { + nodeExecutor +} + +func (ne *tryBlockNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { + var engine *workflowEngine + if we, ok := execCtx.engine.(*workflowEngine); !ok { + panic("impossible!") + } else { + engine = we + } + + execRes := newNodeExecutionResult(execCtx.Node) + + if err := engine.executeBlocks(execCtx.Clone(), execCtx.Node.Blocks); err != nil { + return execRes, err + } + + return execRes, nil +} + +func newTryBlockNodeExecutor() NodeExecutor { + return &tryBlockNodeExecutor{ + nodeExecutor: nodeExecutor{logger: slog.Default()}, + } +} + +type catchBlockNodeExecutor struct { + nodeExecutor +} + +func (ne *catchBlockNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { + execRes := newNodeExecutionResult(execCtx.Node) + + var engine *workflowEngine + if we, ok := execCtx.engine.(*workflowEngine); !ok { + panic("impossible!") + } else { + engine = we + } + + if err := engine.executeBlocks(execCtx.Clone(), execCtx.Node.Blocks); err != nil { + return execRes, err + } + + return execRes, nil +} + +func newCatchBlockNodeExecutor() NodeExecutor { + return &catchBlockNodeExecutor{ + nodeExecutor: nodeExecutor{logger: slog.Default()}, + } +} diff --git a/internal/workflow/engine/logger.go b/internal/workflow/engine/logger.go new file mode 100644 index 000000000..6997ffb3b --- /dev/null +++ b/internal/workflow/engine/logger.go @@ -0,0 +1,9 @@ +package engine + +import ( + "log/slog" +) + +type withLogger interface { + SetLogger(logger *slog.Logger) +} diff --git a/internal/workflow/engine/models.go b/internal/workflow/engine/models.go new file mode 100644 index 000000000..079b676c5 --- /dev/null +++ b/internal/workflow/engine/models.go @@ -0,0 +1,26 @@ +package engine + +import ( + "github.com/certimate-go/certimate/internal/domain" +) + +type Node = domain.WorkflowNode + +type NodeType = domain.WorkflowNodeType + +const ( + NodeTypeStart = domain.WorkflowNodeTypeStart + NodeTypeEnd = domain.WorkflowNodeTypeEnd + NodeTypeCondition = domain.WorkflowNodeTypeCondition + NodeTypeBranchBlock = domain.WorkflowNodeTypeBranchBlock + NodeTypeTryCatch = domain.WorkflowNodeTypeTryCatch + NodeTypeTryBlock = domain.WorkflowNodeTypeTryBlock + NodeTypeCatchBlock = domain.WorkflowNodeTypeCatchBlock + NodeTypeBizApply = domain.WorkflowNodeTypeBizApply + NodeTypeBizUpload = domain.WorkflowNodeTypeBizUpload + NodeTypeBizMonitor = domain.WorkflowNodeTypeBizMonitor + NodeTypeBizDeploy = domain.WorkflowNodeTypeBizDeploy + NodeTypeBizNotify = domain.WorkflowNodeTypeBizNotify +) + +type Graph = domain.WorkflowGraph diff --git a/internal/workflow/engine/state.go b/internal/workflow/engine/state.go new file mode 100644 index 000000000..ee4e92816 --- /dev/null +++ b/internal/workflow/engine/state.go @@ -0,0 +1,283 @@ +package engine + +import ( + "fmt" + "slices" + "strconv" + "sync" +) + +type VariableState struct { + Scope string // 零值时表示全局的,否则表示指定节点的 + Key string + Value any + ValueType string +} + +func (s VariableState) ValueString() string { + switch s.ValueType { + case "string": + return s.Value.(string) + case "number": + return fmt.Sprintf("%d", s.Value) + case "boolean": + return strconv.FormatBool(s.Value.(bool)) + default: + return fmt.Sprintf("%v", s.Value) + } +} + +type VariableManager interface { + All() []VariableState + Erase() + + Add(entry VariableState) + Set(name string, value any, valueType string) + SetScoped(scope string, name string, value any, valueType string) + Get(name string) (*VariableState, bool) + GetScoped(scope string, key string) (*VariableState, bool) + Take(key string) (*VariableState, bool) + TakeScoped(scope string, key string) (*VariableState, bool) + Remove(key string) bool + RemoveScoped(scope string, key string) bool +} + +type variableManager struct { + statesMtx sync.RWMutex + states []VariableState +} + +var _ VariableManager = (*variableManager)(nil) + +func (m *variableManager) All() []VariableState { + m.statesMtx.RLock() + defer m.statesMtx.RUnlock() + + if m.states == nil { + return make([]VariableState, 0) + } + + return slices.Clone(m.states) +} + +func (m *variableManager) Erase() { + m.statesMtx.Lock() + defer m.statesMtx.Unlock() + + m.states = make([]VariableState, 0) +} + +func (m *variableManager) Add(state VariableState) { + m.statesMtx.Lock() + defer m.statesMtx.Unlock() + + if m.states == nil { + m.states = make([]VariableState, 0) + } + + for i, item := range m.states { + if item.Scope == state.Scope && item.Key == state.Key { + m.states[i] = state + return + } + } + m.states = append(m.states, state) +} + +func (m *variableManager) Set(key string, value any, valueType string) { + m.SetScoped("", key, value, valueType) +} + +func (m *variableManager) SetScoped(scope string, key string, value any, valueType string) { + m.Add(VariableState{Scope: scope, Key: key, Value: value, ValueType: valueType}) +} + +func (m *variableManager) Get(key string) (*VariableState, bool) { + return m.GetScoped("", key) +} + +func (m *variableManager) GetScoped(scope string, key string) (*VariableState, bool) { + m.statesMtx.RLock() + defer m.statesMtx.RUnlock() + + if m.states == nil { + return nil, false + } + + for _, item := range m.states { + if item.Scope == scope && item.Key == key { + return &item, true + } + } + return nil, false +} + +func (m *variableManager) Take(key string) (*VariableState, bool) { + return m.TakeScoped("", key) +} + +func (m *variableManager) TakeScoped(scope string, key string) (*VariableState, bool) { + m.statesMtx.Lock() + defer m.statesMtx.Unlock() + + if m.states == nil { + return nil, false + } + + for i, item := range m.states { + if item.Scope == scope && item.Key == key { + m.states = slices.Delete(m.states, i, i+1) + return &item, true + } + } + return nil, false +} + +func (m *variableManager) Remove(key string) bool { + return m.RemoveScoped("", key) +} + +func (m *variableManager) RemoveScoped(scope string, key string) bool { + _, ok := m.TakeScoped(scope, key) + return ok +} + +func newVariableManager() VariableManager { + return &variableManager{ + states: make([]VariableState, 0), + } +} + +type InOutState struct { + NodeId string + Type string + Name string + Value any + ValueType string + Persistent bool +} + +func (s InOutState) ValueString() string { + switch s.ValueType { + case "string": + return s.Value.(string) + case "number": + return fmt.Sprintf("%d", s.Value) + case "boolean": + return strconv.FormatBool(s.Value.(bool)) + default: + return fmt.Sprintf("%v", s.Value) + } +} + +type InOutManager interface { + All() []InOutState + Erase() + + Add(state InOutState) + Set(nodeId string, stype string, name string, value any, valueType string, persistent bool) + Get(nodeId string, name string) (*InOutState, bool) + Take(nodeId string, name string) (*InOutState, bool) + Remove(nodeId string, name string) bool +} + +type inoutManager struct { + statesMtx sync.RWMutex + states []InOutState +} + +var _ InOutManager = (*inoutManager)(nil) + +func (m *inoutManager) All() []InOutState { + m.statesMtx.RLock() + defer m.statesMtx.RUnlock() + + if m.states == nil { + return make([]InOutState, 0) + } + + return slices.Clone(m.states) +} + +func (m *inoutManager) Erase() { + m.statesMtx.Lock() + defer m.statesMtx.Unlock() + + m.states = make([]InOutState, 0) +} + +func (m *inoutManager) Add(state InOutState) { + m.statesMtx.Lock() + defer m.statesMtx.Unlock() + + if m.states == nil { + m.states = make([]InOutState, 0) + } + + for i, item := range m.states { + if item.NodeId == state.NodeId && item.Name == state.Name { + m.states[i] = state + return + } + } + m.states = append(m.states, state) +} + +func (m *inoutManager) Set(nodeId string, stype string, name string, value any, valueType string, persistent bool) { + m.Add(InOutState{NodeId: nodeId, Type: stype, Name: name, Value: value, ValueType: valueType, Persistent: persistent}) +} + +func (m *inoutManager) Get(nodeId string, name string) (*InOutState, bool) { + m.statesMtx.RLock() + defer m.statesMtx.RUnlock() + + if m.states == nil { + return nil, false + } + + for _, item := range m.states { + if item.NodeId == nodeId && item.Name == name { + return &item, true + } + } + return nil, false +} + +func (m *inoutManager) Take(nodeId string, name string) (*InOutState, bool) { + m.statesMtx.Lock() + defer m.statesMtx.Unlock() + + if m.states == nil { + return nil, false + } + + for i, item := range m.states { + if item.NodeId == nodeId && item.Name == name { + m.states = slices.Delete(m.states, i, i+1) + return &item, true + } + } + return nil, false +} + +func (m *inoutManager) Remove(nodeId string, name string) bool { + _, ok := m.Take(nodeId, name) + return ok +} + +func newInOutManager() InOutManager { + return &inoutManager{ + states: make([]InOutState, 0), + } +} + +const ( + stateIOTypeRef = "ref" + stateIOType = "recordRef" +) + +const ( + stateVarKeyNodeSkipped = "node.skipped" // ValueType: "boolean" + stateVarKeyCertificateValidity = "certificate.validity" // ValueType: "boolean" + stateVarKeyCertificateDaysLeft = "certificate.daysLeft" // ValueType: "number" +) diff --git a/internal/workflow/event.go b/internal/workflow/event.go index a8a04e47c..1af72a9ef 100644 --- a/internal/workflow/event.go +++ b/internal/workflow/event.go @@ -13,35 +13,38 @@ import ( ) func Register() { - app := app.GetApp() - app.OnRecordCreateRequest(domain.CollectionNameWorkflow).BindFunc(func(e *core.RecordRequestEvent) error { + pb := app.GetApp() + pb.OnRecordCreateRequest(domain.CollectionNameWorkflow).BindFunc(func(e *core.RecordRequestEvent) error { if err := e.Next(); err != nil { return err } if err := onWorkflowRecordCreateOrUpdate(e.Request.Context(), e.Record); err != nil { + app.GetLogger().Error(err.Error()) return err } return nil }) - app.OnRecordUpdateRequest(domain.CollectionNameWorkflow).BindFunc(func(e *core.RecordRequestEvent) error { + pb.OnRecordUpdateRequest(domain.CollectionNameWorkflow).BindFunc(func(e *core.RecordRequestEvent) error { if err := e.Next(); err != nil { return err } if err := onWorkflowRecordCreateOrUpdate(e.Request.Context(), e.Record); err != nil { + app.GetLogger().Error(err.Error()) return err } return nil }) - app.OnRecordDeleteRequest(domain.CollectionNameWorkflow).BindFunc(func(e *core.RecordRequestEvent) error { + pb.OnRecordDeleteRequest(domain.CollectionNameWorkflow).BindFunc(func(e *core.RecordRequestEvent) error { if err := e.Next(); err != nil { return err } if err := onWorkflowRecordDelete(e.Request.Context(), e.Record); err != nil { + app.GetLogger().Error(err.Error()) return err } diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go deleted file mode 100644 index 2878daee9..000000000 --- a/internal/workflow/node-processor/apply_node.go +++ /dev/null @@ -1,174 +0,0 @@ -package nodeprocessor - -import ( - "context" - "fmt" - "log/slog" - "strconv" - "time" - - "golang.org/x/exp/maps" - - "github.com/certimate-go/certimate/internal/applicant" - "github.com/certimate-go/certimate/internal/domain" - "github.com/certimate-go/certimate/internal/repository" - xcert "github.com/certimate-go/certimate/pkg/utils/cert" -) - -type applyNode struct { - node *domain.WorkflowNode - *nodeProcessor - *nodeOutputer - - certRepo certificateRepository - outputRepo workflowOutputRepository -} - -func NewApplyNode(node *domain.WorkflowNode) *applyNode { - return &applyNode{ - node: node, - nodeProcessor: newNodeProcessor(node), - nodeOutputer: newNodeOutputer(), - - certRepo: repository.NewCertificateRepository(), - outputRepo: repository.NewWorkflowOutputRepository(), - } -} - -func (n *applyNode) Process(ctx context.Context) error { - nodeCfg := n.node.GetConfigForApply() - n.logger.Info("ready to obtain certificate ...", slog.Any("config", nodeCfg)) - - // 查询上次执行结果 - lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id) - if err != nil && !domain.IsRecordNotFoundError(err) { - return err - } - - // 检测是否可以跳过本次执行 - if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable { - n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(true) - n.logger.Info(fmt.Sprintf("skip this application, because %s", reason)) - return nil - } else if reason != "" { - n.logger.Info(fmt.Sprintf("re-apply, because %s", reason)) - } - - // 初始化申请器 - applicant, err := applicant.NewWithWorkflowNode(applicant.ApplicantWithWorkflowNodeConfig{ - Node: n.node, - Logger: n.logger, - }) - if err != nil { - n.logger.Warn("failed to create applicant provider") - return err - } - - // 申请证书 - applyResult, err := applicant.Apply(ctx) - if err != nil { - n.logger.Warn("failed to obtain certificate") - return err - } - - // 解析证书并生成实体 - certX509, err := xcert.ParseCertificateFromPEM(applyResult.FullChainCertificate) - if err != nil { - n.logger.Warn("failed to parse certificate, may be the CA responded error") - return err - } - - certificate := &domain.Certificate{ - Source: domain.CertificateSourceTypeRequest, - Certificate: applyResult.FullChainCertificate, - PrivateKey: applyResult.PrivateKey, - IssuerCertificate: applyResult.IssuerCertificate, - ACMEAccountUrl: applyResult.ACMEAccountUrl, - ACMECertUrl: applyResult.ACMECertUrl, - ACMECertStableUrl: applyResult.ACMECertStableUrl, - } - certificate.PopulateFromX509(certX509) - - // 保存执行结果 - output := &domain.WorkflowOutput{ - WorkflowId: getContextWorkflowId(ctx), - RunId: getContextWorkflowRunId(ctx), - NodeId: n.node.Id, - Node: n.node, - Succeeded: true, - Outputs: n.node.Outputs, - } - if _, err := n.outputRepo.SaveWithCertificate(ctx, output, certificate); err != nil { - n.logger.Warn("failed to save node output") - return err - } - - // 保存 ARI 记录 - if applyResult.ARIReplaced && lastOutput != nil { - lastCertificate, _ := n.certRepo.GetByWorkflowRunIdAndNodeId(ctx, lastOutput.RunId, lastOutput.NodeId) - if lastCertificate != nil { - lastCertificate.ACMERenewed = true - n.certRepo.Save(ctx, lastCertificate) - } - } - - // 记录中间结果 - n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(false) - n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true) - n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(time.Until(certificate.ValidityNotAfter).Hours()/24), 10) - - n.logger.Info("application completed") - return nil -} - -func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) { - if lastOutput != nil && lastOutput.Succeeded { - // 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致 - thisNodeCfg := n.node.GetConfigForApply() - lastNodeCfg := lastOutput.Node.GetConfigForApply() - - if thisNodeCfg.Domains != lastNodeCfg.Domains { - return false, "the configuration item 'Domains' changed" - } - if thisNodeCfg.ContactEmail != lastNodeCfg.ContactEmail { - return false, "the configuration item 'ContactEmail' changed" - } - if thisNodeCfg.Provider != lastNodeCfg.Provider { - return false, "the configuration item 'Provider' changed" - } - if thisNodeCfg.ProviderAccessId != lastNodeCfg.ProviderAccessId { - return false, "the configuration item 'ProviderAccessId' changed" - } - if !maps.Equal(thisNodeCfg.ProviderConfig, lastNodeCfg.ProviderConfig) { - return false, "the configuration item 'ProviderConfig' changed" - } - if thisNodeCfg.CAProvider != lastNodeCfg.CAProvider { - return false, "the configuration item 'CAProvider' changed" - } - if thisNodeCfg.CAProviderAccessId != lastNodeCfg.CAProviderAccessId { - return false, "the configuration item 'CAProviderAccessId' changed" - } - if !maps.Equal(thisNodeCfg.CAProviderConfig, lastNodeCfg.CAProviderConfig) { - return false, "the configuration item 'CAProviderConfig' changed" - } - if thisNodeCfg.KeyAlgorithm != lastNodeCfg.KeyAlgorithm { - return false, "the configuration item 'KeyAlgorithm' changed" - } - - lastCertificate, _ := n.certRepo.GetByWorkflowRunIdAndNodeId(ctx, lastOutput.RunId, lastOutput.NodeId) - if lastCertificate != nil { - renewalInterval := time.Duration(thisNodeCfg.SkipBeforeExpiryDays) * time.Hour * 24 - expirationTime := time.Until(lastCertificate.ValidityNotAfter) - if expirationTime > renewalInterval { - daysLeft := int(expirationTime.Hours() / 24) - // TODO: 优化此处逻辑,[checkCanSkip] 方法不应该修改中间结果,违背单一职责 - n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true) - n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(daysLeft), 10) - - return true, fmt.Sprintf("the certificate has already been issued (expires in %d day(s), next renewal in %d day(s))", daysLeft, thisNodeCfg.SkipBeforeExpiryDays) - } - } - } - - return false, "" -} diff --git a/internal/workflow/node-processor/condition_node.go b/internal/workflow/node-processor/condition_node.go deleted file mode 100644 index 3b87ddf99..000000000 --- a/internal/workflow/node-processor/condition_node.go +++ /dev/null @@ -1,52 +0,0 @@ -package nodeprocessor - -import ( - "context" - "errors" - "fmt" - - "github.com/certimate-go/certimate/internal/domain" - "github.com/certimate-go/certimate/internal/domain/expr" -) - -type conditionNode struct { - node *domain.WorkflowNode - *nodeProcessor - *nodeOutputer -} - -func NewConditionNode(node *domain.WorkflowNode) *conditionNode { - return &conditionNode{ - node: node, - nodeProcessor: newNodeProcessor(node), - nodeOutputer: newNodeOutputer(), - } -} - -func (n *conditionNode) Process(ctx context.Context) error { - nodeCfg := n.node.GetConfigForCondition() - if nodeCfg.Expression == nil { - n.logger.Info("without any conditions, enter this branch") - return nil - } - - rs, err := n.evalExpr(ctx, nodeCfg.Expression) - if err != nil { - n.logger.Warn(fmt.Sprintf("failed to eval condition expression: %w", err)) - return err - } - - if rs.Value == false { - n.logger.Info("condition not met, skip this branch") - return errors.New("condition not met") // TODO: 错误处理 - } else { - n.logger.Info("condition met, enter this branch") - } - - return nil -} - -func (n *conditionNode) evalExpr(ctx context.Context, expression expr.Expr) (*expr.EvalResult, error) { - variables := GetAllNodeOutputs(ctx) - return expression.Eval(variables) -} diff --git a/internal/workflow/node-processor/const.go b/internal/workflow/node-processor/const.go deleted file mode 100644 index 21bdf167c..000000000 --- a/internal/workflow/node-processor/const.go +++ /dev/null @@ -1,7 +0,0 @@ -package nodeprocessor - -const ( - outputKeyForCertificateValidity = "certificate.validity" - outputKeyForCertificateDaysLeft = "certificate.daysLeft" - outputKeyForNodeSkipped = "node.skipped" -) diff --git a/internal/workflow/node-processor/context.go b/internal/workflow/node-processor/context.go deleted file mode 100644 index d600554d9..000000000 --- a/internal/workflow/node-processor/context.go +++ /dev/null @@ -1,103 +0,0 @@ -package nodeprocessor - -import ( - "context" - "sync" -) - -// 定义上下文键类型,避免键冲突 -type workflowContextKey string - -const ( - nodeOutputsKey workflowContextKey = "node_outputs" -) - -// 带互斥锁的节点输出容器 -type nodeOutputsContainer struct { - sync.RWMutex - outputs map[string]map[string]any -} - -// 创建新的并发安全的节点输出容器 -func newNodeOutputsContainer() *nodeOutputsContainer { - return &nodeOutputsContainer{ - outputs: make(map[string]map[string]any), - } -} - -// 获取节点输出容器 -func getNodeOutputsContainer(ctx context.Context) *nodeOutputsContainer { - value := ctx.Value(nodeOutputsKey) - if value == nil { - return nil - } - return value.(*nodeOutputsContainer) -} - -// 添加节点输出到上下文 -func AddNodeOutput(ctx context.Context, nodeId string, output map[string]any) context.Context { - container := getNodeOutputsContainer(ctx) - if container == nil { - container = newNodeOutputsContainer() - } - - container.Lock() - defer container.Unlock() - - // 创建输出的深拷贝 - // TODO: 暂时使用浅拷贝,等后续值类型扩充后修改 - outputCopy := make(map[string]any, len(output)) - for k, v := range output { - outputCopy[k] = v - } - - container.outputs[nodeId] = outputCopy - return context.WithValue(ctx, nodeOutputsKey, container) -} - -// 从上下文获取节点输出 -func GetNodeOutput(ctx context.Context, nodeId string) map[string]any { - container := getNodeOutputsContainer(ctx) - if container == nil { - container = newNodeOutputsContainer() - } - - container.RLock() - defer container.RUnlock() - - output, exists := container.outputs[nodeId] - if !exists { - return nil - } - - outputCopy := make(map[string]any, len(output)) - for k, v := range output { - outputCopy[k] = v - } - - return outputCopy -} - -// 获取所有节点输出 -func GetAllNodeOutputs(ctx context.Context) map[string]map[string]any { - container := getNodeOutputsContainer(ctx) - if container == nil { - container = newNodeOutputsContainer() - } - - container.RLock() - defer container.RUnlock() - - // 创建所有输出的深拷贝 - // TODO: 暂时使用浅拷贝,等后续值类型扩充后修改 - allOutputs := make(map[string]map[string]any, len(container.outputs)) - for nodeId, output := range container.outputs { - nodeCopy := make(map[string]any, len(output)) - for k, v := range output { - nodeCopy[k] = v - } - allOutputs[nodeId] = nodeCopy - } - - return allOutputs -} diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go deleted file mode 100644 index 973fe1f77..000000000 --- a/internal/workflow/node-processor/deploy_node.go +++ /dev/null @@ -1,129 +0,0 @@ -package nodeprocessor - -import ( - "context" - "fmt" - "log/slog" - "strconv" - "strings" - - "golang.org/x/exp/maps" - - "github.com/certimate-go/certimate/internal/deployer" - "github.com/certimate-go/certimate/internal/domain" - "github.com/certimate-go/certimate/internal/repository" -) - -type deployNode struct { - node *domain.WorkflowNode - *nodeProcessor - *nodeOutputer - - certRepo certificateRepository - outputRepo workflowOutputRepository -} - -func NewDeployNode(node *domain.WorkflowNode) *deployNode { - return &deployNode{ - node: node, - nodeProcessor: newNodeProcessor(node), - nodeOutputer: newNodeOutputer(), - - certRepo: repository.NewCertificateRepository(), - outputRepo: repository.NewWorkflowOutputRepository(), - } -} - -func (n *deployNode) Process(ctx context.Context) error { - nodeCfg := n.node.GetConfigForDeploy() - n.logger.Info("ready to deploy certificate ...", slog.Any("config", nodeCfg)) - - // 查询上次执行结果 - lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id) - if err != nil && !domain.IsRecordNotFoundError(err) { - return err - } - - // 获取前序节点输出证书 - const DELIMITER = "#" - previousNodeOutputCertificateSource := n.node.GetConfigForDeploy().Certificate - previousNodeOutputCertificateSourceSlice := strings.Split(previousNodeOutputCertificateSource, DELIMITER) - if len(previousNodeOutputCertificateSourceSlice) != 2 { - n.logger.Warn("invalid certificate source", slog.String("certificate.source", previousNodeOutputCertificateSource)) - return fmt.Errorf("invalid certificate source: %s", previousNodeOutputCertificateSource) - } - certificate, err := n.certRepo.GetByWorkflowNodeId(ctx, previousNodeOutputCertificateSourceSlice[0]) - if err != nil { - n.logger.Warn("invalid certificate source", slog.String("certificate.source", previousNodeOutputCertificateSource)) - return err - } - - // 检测是否可以跳过本次执行 - if lastOutput != nil && certificate.CreatedAt.Before(lastOutput.UpdatedAt) { - if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable { - n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(true) - n.logger.Info(fmt.Sprintf("skip this deployment, because %s", reason)) - return nil - } else if reason != "" { - n.logger.Info(fmt.Sprintf("re-deploy, because %s", reason)) - } - } - - // 初始化部署器 - deployer, err := deployer.NewWithWorkflowNode(deployer.DeployerWithWorkflowNodeConfig{ - Node: n.node, - Logger: n.logger, - CertificatePEM: certificate.Certificate, - PrivateKeyPEM: certificate.PrivateKey, - }) - if err != nil { - n.logger.Warn("failed to create deployer provider") - return err - } - - // 部署证书 - if err := deployer.Deploy(ctx); err != nil { - n.logger.Warn("failed to deploy certificate") - return err - } - - // 保存执行结果 - output := &domain.WorkflowOutput{ - WorkflowId: getContextWorkflowId(ctx), - RunId: getContextWorkflowRunId(ctx), - NodeId: n.node.Id, - Node: n.node, - Succeeded: true, - } - if _, err := n.outputRepo.Save(ctx, output); err != nil { - n.logger.Warn("failed to save node output") - return err - } - - // 记录中间结果 - n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(false) - - n.logger.Info("deployment completed") - return nil -} - -func (n *deployNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) { - if lastOutput != nil && lastOutput.Succeeded { - // 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致 - thisNodeCfg := n.node.GetConfigForDeploy() - lastNodeCfg := lastOutput.Node.GetConfigForDeploy() - - if thisNodeCfg.ProviderAccessId != lastNodeCfg.ProviderAccessId { - return false, "the configuration item 'ProviderAccessId' changed" - } - if !maps.Equal(thisNodeCfg.ProviderConfig, lastNodeCfg.ProviderConfig) { - return false, "the configuration item 'ProviderConfig' changed" - } - - if thisNodeCfg.SkipOnLastSucceeded { - return true, "the certificate has already been deployed" - } - } - - return false, "" -} diff --git a/internal/workflow/node-processor/execute_failure_node.go b/internal/workflow/node-processor/execute_failure_node.go deleted file mode 100644 index 38892241c..000000000 --- a/internal/workflow/node-processor/execute_failure_node.go +++ /dev/null @@ -1,27 +0,0 @@ -package nodeprocessor - -import ( - "context" - - "github.com/certimate-go/certimate/internal/domain" -) - -type executeFailureNode struct { - node *domain.WorkflowNode - *nodeProcessor - *nodeOutputer -} - -func NewExecuteFailureNode(node *domain.WorkflowNode) *executeFailureNode { - return &executeFailureNode{ - node: node, - nodeProcessor: newNodeProcessor(node), - nodeOutputer: newNodeOutputer(), - } -} - -func (n *executeFailureNode) Process(ctx context.Context) error { - // 此类型节点不需要执行任何操作,直接返回 - - return nil -} diff --git a/internal/workflow/node-processor/execute_success_node.go b/internal/workflow/node-processor/execute_success_node.go deleted file mode 100644 index 47a8645e1..000000000 --- a/internal/workflow/node-processor/execute_success_node.go +++ /dev/null @@ -1,27 +0,0 @@ -package nodeprocessor - -import ( - "context" - - "github.com/certimate-go/certimate/internal/domain" -) - -type executeSuccessNode struct { - node *domain.WorkflowNode - *nodeProcessor - *nodeOutputer -} - -func NewExecuteSuccessNode(node *domain.WorkflowNode) *executeSuccessNode { - return &executeSuccessNode{ - node: node, - nodeProcessor: newNodeProcessor(node), - nodeOutputer: newNodeOutputer(), - } -} - -func (n *executeSuccessNode) Process(ctx context.Context) error { - // 此类型节点不需要执行任何操作,直接返回 - - return nil -} diff --git a/internal/workflow/node-processor/monitor_node.go b/internal/workflow/node-processor/monitor_node.go deleted file mode 100644 index 10021eab3..000000000 --- a/internal/workflow/node-processor/monitor_node.go +++ /dev/null @@ -1,152 +0,0 @@ -package nodeprocessor - -import ( - "context" - "crypto/x509" - "fmt" - "log/slog" - "math" - "net" - "net/http" - "strconv" - "strings" - "time" - - "github.com/certimate-go/certimate/internal/domain" - xhttp "github.com/certimate-go/certimate/pkg/utils/http" - xtls "github.com/certimate-go/certimate/pkg/utils/tls" -) - -type monitorNode struct { - node *domain.WorkflowNode - *nodeProcessor - *nodeOutputer -} - -func NewMonitorNode(node *domain.WorkflowNode) *monitorNode { - return &monitorNode{ - node: node, - nodeProcessor: newNodeProcessor(node), - nodeOutputer: newNodeOutputer(), - } -} - -func (n *monitorNode) Process(ctx context.Context) error { - nodeCfg := n.node.GetConfigForMonitor() - n.logger.Info("ready to monitor certificate ...", slog.Any("config", nodeCfg)) - - targetAddr := net.JoinHostPort(nodeCfg.Host, strconv.Itoa(int(nodeCfg.Port))) - if nodeCfg.Port == 0 { - targetAddr = net.JoinHostPort(nodeCfg.Host, "443") - } - - targetDomain := nodeCfg.Domain - if targetDomain == "" { - targetDomain = nodeCfg.Host - } - - n.logger.Info(fmt.Sprintf("retrieving certificate at %s (domain: %s)", targetAddr, targetDomain)) - - const MAX_ATTEMPTS = 3 - const RETRY_INTERVAL = 2 * time.Second - var certs []*x509.Certificate - var err error - for attempt := 0; attempt < MAX_ATTEMPTS; attempt++ { - if attempt > 0 { - n.logger.Info(fmt.Sprintf("retry %d time(s) ...", attempt, targetAddr)) - - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(RETRY_INTERVAL): - } - } - - certs, err = n.tryRetrievePeerCertificates(ctx, targetAddr, targetDomain, nodeCfg.RequestPath) - if err == nil { - break - } - } - - if err != nil { - n.logger.Warn("failed to monitor certificate") - return err - } else { - if len(certs) == 0 { - n.logger.Warn("no ssl certificates retrieved in http response") - - n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(false) - n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(0, 10) - } else { - cert := certs[0] // 只取证书链中的第一个证书,即服务器证书 - n.logger.Info(fmt.Sprintf("ssl certificate retrieved (serial='%s', subject='%s', issuer='%s', not_before='%s', not_after='%s', sans='%s')", - cert.SerialNumber, cert.Subject.String(), cert.Issuer.String(), - cert.NotBefore.Format(time.RFC3339), cert.NotAfter.Format(time.RFC3339), - strings.Join(cert.DNSNames, ";")), - ) - - now := time.Now() - isCertPeriodValid := now.Before(cert.NotAfter) && now.After(cert.NotBefore) - isCertHostMatched := true - if err := cert.VerifyHostname(targetDomain); err != nil { - isCertHostMatched = false - } - - validated := isCertPeriodValid && isCertHostMatched - daysLeft := int(math.Floor(cert.NotAfter.Sub(now).Hours() / 24)) - n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(validated) - n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(daysLeft), 10) - - if validated { - n.logger.Info(fmt.Sprintf("the certificate is valid, and will expire in %d day(s)", daysLeft)) - } else { - if !isCertHostMatched { - n.logger.Warn("the certificate is invalid, because it is not matched the host") - } else if !isCertPeriodValid { - n.logger.Warn("the certificate is invalid, because it is either expired or not yet valid") - } else { - n.logger.Warn("the certificate is invalid") - } - } - } - } - - n.logger.Info("monitoring completed") - return nil -} - -func (n *monitorNode) tryRetrievePeerCertificates(ctx context.Context, addr, domain, requestPath string) ([]*x509.Certificate, error) { - transport := xhttp.NewDefaultTransport() - transport.TLSClientConfig = xtls.NewInsecureConfig() - - client := &http.Client{ - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - Timeout: 30 * time.Second, - Transport: transport, - } - - url := fmt.Sprintf("https://%s/%s", addr, strings.TrimLeft(requestPath, "/")) - req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) - if err != nil { - err = fmt.Errorf("failed to create http request: %w", err) - n.logger.Warn(err.Error()) - return nil, err - } - - req.Header.Set("Host", domain) - req.Header.Set("User-Agent", "certimate") - resp, err := client.Do(req) - if err != nil { - err = fmt.Errorf("failed to send http request: %w", err) - n.logger.Warn(err.Error()) - return nil, err - } - defer resp.Body.Close() - - if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 { - return make([]*x509.Certificate, 0), nil - } - return resp.TLS.PeerCertificates, nil -} diff --git a/internal/workflow/node-processor/monitor_node_test.go b/internal/workflow/node-processor/monitor_node_test.go deleted file mode 100644 index 53afb21c0..000000000 --- a/internal/workflow/node-processor/monitor_node_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package nodeprocessor_test - -import ( - "context" - "log/slog" - "testing" - - "github.com/certimate-go/certimate/internal/domain" - nodeprocessor "github.com/certimate-go/certimate/internal/workflow/node-processor" -) - -func Test_MonitorNode(t *testing.T) { - t.Run("Monitor", func(t *testing.T) { - node := nodeprocessor.NewMonitorNode(&domain.WorkflowNode{ - Id: "test", - Type: domain.WorkflowNodeTypeMonitor, - Name: "test", - Config: map[string]any{ - "host": "baidu.com", - "port": 443, - }, - }) - node.SetLogger(slog.Default()) - if err := node.Process(context.Background()); err != nil { - t.Errorf("err: %+v", err) - } - }) -} diff --git a/internal/workflow/node-processor/notify_node.go b/internal/workflow/node-processor/notify_node.go deleted file mode 100644 index 090a9f2de..000000000 --- a/internal/workflow/node-processor/notify_node.go +++ /dev/null @@ -1,80 +0,0 @@ -package nodeprocessor - -import ( - "context" - "fmt" - "log/slog" - "strconv" - - "github.com/certimate-go/certimate/internal/domain" - "github.com/certimate-go/certimate/internal/notify" - "github.com/certimate-go/certimate/internal/repository" -) - -type notifyNode struct { - node *domain.WorkflowNode - *nodeProcessor - *nodeOutputer - - settingsRepo settingsRepository -} - -func NewNotifyNode(node *domain.WorkflowNode) *notifyNode { - return ¬ifyNode{ - node: node, - nodeProcessor: newNodeProcessor(node), - nodeOutputer: newNodeOutputer(), - - settingsRepo: repository.NewSettingsRepository(), - } -} - -func (n *notifyNode) Process(ctx context.Context) error { - nodeCfg := n.node.GetConfigForNotify() - n.logger.Info("ready to send notification ...", slog.Any("config", nodeCfg)) - - // 检测是否可以跳过本次执行 - if skippable := n.checkCanSkip(ctx); skippable { - n.logger.Info(fmt.Sprintf("skip this notification, because all the previous nodes have been skipped")) - return nil - } - - // 初始化通知器 - deployer, err := notify.NewWithWorkflowNode(notify.NotifierWithWorkflowNodeConfig{ - Node: n.node, - Logger: n.logger, - Subject: nodeCfg.Subject, - Message: nodeCfg.Message, - }) - if err != nil { - n.logger.Warn("failed to create notifier provider") - return err - } - - // 推送通知 - if err := deployer.Notify(ctx); err != nil { - n.logger.Warn("failed to send notification") - return err - } - - n.logger.Info("notification completed") - return nil -} - -func (n *notifyNode) checkCanSkip(ctx context.Context) (_skip bool) { - thisNodeCfg := n.node.GetConfigForNotify() - if !thisNodeCfg.SkipOnAllPrevSkipped { - return false - } - - prevNodeOutputs := GetAllNodeOutputs(ctx) - for _, nodeOutput := range prevNodeOutputs { - if nodeOutput[outputKeyForNodeSkipped] != nil { - if nodeOutput[outputKeyForNodeSkipped].(string) != strconv.FormatBool(true) { - return false - } - } - } - - return true -} diff --git a/internal/workflow/node-processor/processor.go b/internal/workflow/node-processor/processor.go deleted file mode 100644 index 10e0de9b1..000000000 --- a/internal/workflow/node-processor/processor.go +++ /dev/null @@ -1,104 +0,0 @@ -package nodeprocessor - -import ( - "context" - "fmt" - "io" - "log/slog" - - "github.com/certimate-go/certimate/internal/domain" -) - -type NodeProcessor interface { - GetLogger() *slog.Logger - SetLogger(*slog.Logger) - - Process(ctx context.Context) error - - GetOutputs() map[string]any -} - -type nodeProcessor struct { - logger *slog.Logger -} - -func (n *nodeProcessor) GetLogger() *slog.Logger { - return n.logger -} - -func (n *nodeProcessor) SetLogger(logger *slog.Logger) { - if logger == nil { - panic("logger is nil") - } - - n.logger = logger -} - -type nodeOutputer struct { - outputs map[string]any -} - -func newNodeOutputer() *nodeOutputer { - return &nodeOutputer{ - outputs: make(map[string]any), - } -} - -func (n *nodeOutputer) GetOutputs() map[string]any { - return n.outputs -} - -type certificateRepository interface { - GetByWorkflowNodeId(ctx context.Context, workflowNodeId string) (*domain.Certificate, error) - GetByWorkflowRunIdAndNodeId(ctx context.Context, workflowRunId string, workflowNodeId string) (*domain.Certificate, error) - Save(ctx context.Context, certificate *domain.Certificate) (*domain.Certificate, error) -} - -type workflowOutputRepository interface { - GetByNodeId(ctx context.Context, workflowNodeId string) (*domain.WorkflowOutput, error) - Save(ctx context.Context, workflowOutput *domain.WorkflowOutput) (*domain.WorkflowOutput, error) - SaveWithCertificate(ctx context.Context, workflowOutput *domain.WorkflowOutput, certificate *domain.Certificate) (*domain.WorkflowOutput, error) -} - -type settingsRepository interface { - GetByName(ctx context.Context, name string) (*domain.Settings, error) -} - -func newNodeProcessor(node *domain.WorkflowNode) *nodeProcessor { - return &nodeProcessor{ - logger: slog.New(slog.NewTextHandler(io.Discard, nil)), - } -} - -func GetProcessor(node *domain.WorkflowNode) (NodeProcessor, error) { - switch node.Type { - case domain.WorkflowNodeTypeStart: - return NewStartNode(node), nil - case domain.WorkflowNodeTypeApply: - return NewApplyNode(node), nil - case domain.WorkflowNodeTypeUpload: - return NewUploadNode(node), nil - case domain.WorkflowNodeTypeMonitor: - return NewMonitorNode(node), nil - case domain.WorkflowNodeTypeDeploy: - return NewDeployNode(node), nil - case domain.WorkflowNodeTypeNotify: - return NewNotifyNode(node), nil - case domain.WorkflowNodeTypeCondition: - return NewConditionNode(node), nil - case domain.WorkflowNodeTypeExecuteSuccess: - return NewExecuteSuccessNode(node), nil - case domain.WorkflowNodeTypeExecuteFailure: - return NewExecuteFailureNode(node), nil - } - - return nil, fmt.Errorf("unsupported node type: %s", string(node.Type)) -} - -func getContextWorkflowId(ctx context.Context) string { - return ctx.Value("workflow_id").(string) -} - -func getContextWorkflowRunId(ctx context.Context) string { - return ctx.Value("workflow_run_id").(string) -} diff --git a/internal/workflow/node-processor/start_node.go b/internal/workflow/node-processor/start_node.go deleted file mode 100644 index b91420036..000000000 --- a/internal/workflow/node-processor/start_node.go +++ /dev/null @@ -1,28 +0,0 @@ -package nodeprocessor - -import ( - "context" - - "github.com/certimate-go/certimate/internal/domain" -) - -type startNode struct { - node *domain.WorkflowNode - *nodeProcessor - *nodeOutputer -} - -func NewStartNode(node *domain.WorkflowNode) *startNode { - return &startNode{ - node: node, - nodeProcessor: newNodeProcessor(node), - nodeOutputer: newNodeOutputer(), - } -} - -func (n *startNode) Process(ctx context.Context) error { - // 此类型节点不需要执行任何操作,直接返回 - n.logger.Info("workflow is started") - - return nil -} diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go deleted file mode 100644 index 3112d0806..000000000 --- a/internal/workflow/node-processor/upload_node.go +++ /dev/null @@ -1,107 +0,0 @@ -package nodeprocessor - -import ( - "context" - "fmt" - "log/slog" - "strconv" - "strings" - "time" - - "github.com/certimate-go/certimate/internal/domain" - "github.com/certimate-go/certimate/internal/repository" -) - -type uploadNode struct { - node *domain.WorkflowNode - *nodeProcessor - *nodeOutputer - - certRepo certificateRepository - outputRepo workflowOutputRepository -} - -func NewUploadNode(node *domain.WorkflowNode) *uploadNode { - return &uploadNode{ - node: node, - nodeProcessor: newNodeProcessor(node), - nodeOutputer: newNodeOutputer(), - - certRepo: repository.NewCertificateRepository(), - outputRepo: repository.NewWorkflowOutputRepository(), - } -} - -func (n *uploadNode) Process(ctx context.Context) error { - nodeCfg := n.node.GetConfigForUpload() - n.logger.Info("ready to upload certiticate ...", slog.Any("config", nodeCfg)) - - // 查询上次执行结果 - lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id) - if err != nil && !domain.IsRecordNotFoundError(err) { - return err - } - - // 检测是否可以跳过本次执行 - if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable { - n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(true) - n.logger.Info(fmt.Sprintf("skip this uploading, because %s", reason)) - return nil - } else if reason != "" { - n.logger.Info(fmt.Sprintf("re-upload, because %s", reason)) - } - - // 生成证书实体 - certificate := &domain.Certificate{ - Source: domain.CertificateSourceTypeUpload, - } - certificate.PopulateFromPEM(nodeCfg.Certificate, nodeCfg.PrivateKey) - - // 保存执行结果 - output := &domain.WorkflowOutput{ - WorkflowId: getContextWorkflowId(ctx), - RunId: getContextWorkflowRunId(ctx), - NodeId: n.node.Id, - Node: n.node, - Succeeded: true, - Outputs: n.node.Outputs, - } - if _, err := n.outputRepo.SaveWithCertificate(ctx, output, certificate); err != nil { - n.logger.Warn("failed to save node output") - return err - } - - // 记录中间结果 - n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(false) - n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true) - n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(time.Until(certificate.ValidityNotAfter).Hours()/24), 10) - - n.logger.Info("uploading completed") - return nil -} - -func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) { - if lastOutput != nil && lastOutput.Succeeded { - // 比较和上次上传时的关键配置(即影响证书上传的)参数是否一致 - thisNodeCfg := n.node.GetConfigForUpload() - lastNodeCfg := lastOutput.Node.GetConfigForUpload() - - if strings.TrimSpace(thisNodeCfg.Certificate) != strings.TrimSpace(lastNodeCfg.Certificate) { - return false, "the configuration item 'Certificate' changed" - } - if strings.TrimSpace(thisNodeCfg.PrivateKey) != strings.TrimSpace(lastNodeCfg.PrivateKey) { - return false, "the configuration item 'PrivateKey' changed" - } - - lastCertificate, _ := n.certRepo.GetByWorkflowRunIdAndNodeId(ctx, lastOutput.RunId, lastOutput.NodeId) - if lastCertificate != nil { - daysLeft := int(time.Until(lastCertificate.ValidityNotAfter).Hours() / 24) - n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(daysLeft > 0) - n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(daysLeft), 10) - - return true, "the certificate has already been uploaded" - } - } - - return false, "" -} diff --git a/internal/workflow/service.go b/internal/workflow/service.go index d7cd29ef2..02260cae7 100644 --- a/internal/workflow/service.go +++ b/internal/workflow/service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "log/slog" "time" "github.com/pocketbase/dbx" @@ -14,24 +15,8 @@ import ( "github.com/certimate-go/certimate/internal/workflow/dispatcher" ) -type workflowRepository interface { - ListEnabledScheduled(ctx context.Context) ([]*domain.Workflow, error) - GetById(ctx context.Context, id string) (*domain.Workflow, error) - Save(ctx context.Context, workflow *domain.Workflow) (*domain.Workflow, error) -} - -type workflowRunRepository interface { - GetById(ctx context.Context, id string) (*domain.WorkflowRun, error) - Save(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) - DeleteWhere(ctx context.Context, exprs ...dbx.Expression) (int, error) -} - -type settingsRepository interface { - GetByName(ctx context.Context, name string) (*domain.Settings, error) -} - type WorkflowService struct { - dispatcher *dispatcher.WorkflowDispatcher + dispatcher dispatcher.WorkflowDispatcher workflowRepo workflowRepository workflowRunRepo workflowRunRepository @@ -50,33 +35,17 @@ func NewWorkflowService(workflowRepo workflowRepository, workflowRunRepo workflo } func (s *WorkflowService) InitSchedule(ctx context.Context) error { - // 每日清理工作流执行历史 - app.GetScheduler().MustAdd("workflowHistoryRunsCleanup", "0 0 * * *", func() { - settings, err := s.settingsRepo.GetByName(ctx, "persistence") - if err != nil { - app.GetLogger().Error("failed to get persistence settings", "err", err) - return - } - - persistenceSettings, _ := settings.UnmarshalContentAsPersistence() - if persistenceSettings != nil && persistenceSettings.WorkflowRunsMaxDaysRetention != 0 { - ret, err := s.workflowRunRepo.DeleteWhere( - context.Background(), - dbx.NewExp(fmt.Sprintf("status!='%s'", string(domain.WorkflowRunStatusTypePending))), - dbx.NewExp(fmt.Sprintf("status!='%s'", string(domain.WorkflowRunStatusTypeRunning))), - dbx.NewExp(fmt.Sprintf("endedAt 0 { - app.GetLogger().Info(fmt.Sprintf("cleanup %d workflow history runs", ret)) - } - } + // 每日清理工作流运行历史 + app.GetScheduler().MustAdd("cleanupWorkflowHistoryRuns", "0 0 * * *", func() { + s.cleanupHistoryRuns(context.Background()) }) - // 工作流 + // 初始化工作流调度器 + if err := s.dispatcher.Bootup(ctx); err != nil { + panic(err) + } + + // 注册工作流后台任务 { workflows, err := s.workflowRepo.ListEnabledScheduled(ctx) if err != nil { @@ -87,12 +56,13 @@ func (s *WorkflowService) InitSchedule(ctx context.Context) error { var errs []error err := app.GetScheduler().Add(fmt.Sprintf("workflow#%s", workflow.Id), workflow.TriggerCron, func() { - s.StartRun(ctx, &dtos.WorkflowStartRunReq{ + s.StartRun(context.Background(), &dtos.WorkflowStartRunReq{ WorkflowId: workflow.Id, RunTrigger: domain.WorkflowTriggerTypeScheduled, }) }) if err != nil { + app.GetLogger().Error(fmt.Sprintf("failed to add workflow #%s to scheduler: %w", workflow.Id), slog.Any("error", err)) errs = append(errs, err) } @@ -105,58 +75,86 @@ func (s *WorkflowService) InitSchedule(ctx context.Context) error { return nil } -func (s *WorkflowService) StartRun(ctx context.Context, req *dtos.WorkflowStartRunReq) error { +func (s *WorkflowService) StartRun(ctx context.Context, req *dtos.WorkflowStartRunReq) (*dtos.WorkflowStartRunResp, error) { workflow, err := s.workflowRepo.GetById(ctx, req.WorkflowId) if err != nil { - return err + return nil, err } - if workflow.LastRunStatus == domain.WorkflowRunStatusTypePending || workflow.LastRunStatus == domain.WorkflowRunStatusTypeRunning { - return errors.New("workflow is already pending or running") + if workflow.LastRunStatus == domain.WorkflowRunStatusTypePending || workflow.LastRunStatus == domain.WorkflowRunStatusTypeProcessing { + return nil, errors.New("workflow is already pending or processing") + } else if workflow.GraphContent == nil { + return nil, errors.New("workflow graph content is empty") + } else if err := workflow.GraphContent.Verify(); err != nil { + return nil, fmt.Errorf("workflow graph content is invalid: %w", err) } - run := &domain.WorkflowRun{ + workflowRun := &domain.WorkflowRun{ WorkflowId: workflow.Id, Status: domain.WorkflowRunStatusTypePending, Trigger: req.RunTrigger, StartedAt: time.Now(), - Detail: workflow.Content, + Graph: workflow.GraphContent.Clone(), } - if resp, err := s.workflowRunRepo.Save(ctx, run); err != nil { - return err + if resp, err := s.workflowRunRepo.Save(ctx, workflowRun); err != nil { + return nil, err } else { - run = resp + workflowRun = resp } - s.dispatcher.Dispatch(&dispatcher.WorkflowWorkerData{ - WorkflowId: run.WorkflowId, - WorkflowContent: run.Detail, - RunId: run.Id, - }) + s.dispatcher.Start(ctx, workflowRun.Id) - return nil + return &dtos.WorkflowStartRunResp{RunId: workflowRun.Id}, nil } -func (s *WorkflowService) CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) error { +func (s *WorkflowService) CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) (*dtos.WorkflowCancelRunResp, error) { workflow, err := s.workflowRepo.GetById(ctx, req.WorkflowId) if err != nil { - return err + return nil, err } workflowRun, err := s.workflowRunRepo.GetById(ctx, req.RunId) if err != nil { - return err + return nil, err } else if workflowRun.WorkflowId != workflow.Id { - return errors.New("workflow run not found") - } else if workflowRun.Status != domain.WorkflowRunStatusTypePending && workflowRun.Status != domain.WorkflowRunStatusTypeRunning { - return errors.New("workflow run is not pending or running") + return nil, errors.New("workflow run not found") + } else if workflowRun.Status != domain.WorkflowRunStatusTypePending && workflowRun.Status != domain.WorkflowRunStatusTypeProcessing { + return nil, errors.New("workflow run is not pending or processing") } - s.dispatcher.Cancel(workflowRun.Id) + s.dispatcher.Cancel(ctx, workflowRun.Id) - return nil + return &dtos.WorkflowCancelRunResp{}, nil } func (s *WorkflowService) Shutdown(ctx context.Context) { - s.dispatcher.Shutdown() + s.dispatcher.Shutdown(ctx) +} + +func (s *WorkflowService) cleanupHistoryRuns(ctx context.Context) error { + settings, err := s.settingsRepo.GetByName(ctx, "persistence") + if err != nil { + app.GetLogger().Error("failed to get persistence settings", slog.Any("error", err)) + return err + } + + persistenceSettings, _ := settings.UnmarshalContentAsPersistence() + if persistenceSettings != nil && persistenceSettings.WorkflowRunsMaxDaysRetention != 0 { + ret, err := s.workflowRunRepo.DeleteWhere( + ctx, + dbx.NewExp(fmt.Sprintf("status!='%s'", string(domain.WorkflowRunStatusTypePending))), + dbx.NewExp(fmt.Sprintf("status!='%s'", string(domain.WorkflowRunStatusTypeProcessing))), + dbx.NewExp(fmt.Sprintf("endedAt 0 { + app.GetLogger().Info(fmt.Sprintf("cleanup %d workflow history runs", ret)) + } + } + + return nil } diff --git a/internal/workflow/service_deps.go b/internal/workflow/service_deps.go new file mode 100644 index 000000000..4f542034c --- /dev/null +++ b/internal/workflow/service_deps.go @@ -0,0 +1,26 @@ +package workflow + +import ( + "context" + + "github.com/pocketbase/dbx" + + "github.com/certimate-go/certimate/internal/domain" +) + +type workflowRepository interface { + ListEnabledScheduled(ctx context.Context) ([]*domain.Workflow, error) + GetById(ctx context.Context, id string) (*domain.Workflow, error) + Save(ctx context.Context, workflow *domain.Workflow) (*domain.Workflow, error) +} + +type workflowRunRepository interface { + GetById(ctx context.Context, id string) (*domain.WorkflowRun, error) + Save(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) + SaveWithCascading(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) + DeleteWhere(ctx context.Context, exprs ...dbx.Expression) (int, error) +} + +type settingsRepository interface { + GetByName(ctx context.Context, name string) (*domain.Settings, error) +} diff --git a/main.go b/main.go index 8e3477c99..94dc6e199 100644 --- a/main.go +++ b/main.go @@ -68,6 +68,6 @@ func main() { }) if err := app.Start(); err != nil { - slog.Error("[CERTIMATE] Start failed.", "err", err) + slog.Error("[CERTIMATE] Start failed.", slog.Any("error", err)) } } diff --git a/migrations/1753272000_v0.4.0_migrate.go b/migrations/1753272000_v0.4.0_migrate.go deleted file mode 100644 index 4523c8ae5..000000000 --- a/migrations/1753272000_v0.4.0_migrate.go +++ /dev/null @@ -1,581 +0,0 @@ -package migrations - -import ( - "encoding/json" - - "github.com/pocketbase/pocketbase/core" - m "github.com/pocketbase/pocketbase/migrations" -) - -func init() { - m.Register(func(app core.App) error { - tracer := NewTracer("v0.4.0") - tracer.Printf("go ...") - - // update collection `access` - { - collection, err := app.FindCollectionByNameOrId("4yzbv8urny5ja1e") - if err != nil { - return err - } else if collection != nil { - records, err := app.FindAllRecords(collection) - if err != nil { - return err - } - - for _, record := range records { - changed := false - - provider := record.GetString("provider") - config := make(map[string]any) - if err := record.UnmarshalJSONField("config", &config); err != nil { - return err - } - - switch provider { - case "discordbot", "mattermost", "slackbot": - if _, ok := config["defaultChannelId"]; ok { - config["channelId"] = config["defaultChannelId"] - delete(config, "defaultChannelId") - record.Set("config", config) - changed = true - } - - case "email": - if _, ok := config["defaultSenderAddress"]; ok { - config["senderAddress"] = config["defaultSenderAddress"] - delete(config, "defaultSenderAddress") - record.Set("config", config) - changed = true - } - if _, ok := config["defaultSenderName"]; ok { - config["senderName"] = config["defaultSenderName"] - delete(config, "defaultSenderName") - record.Set("config", config) - changed = true - } - if _, ok := config["defaultReceiverAddress"]; ok { - config["receiverAddress"] = config["defaultReceiverAddress"] - delete(config, "defaultReceiverAddress") - record.Set("config", config) - changed = true - } - - case "telegrambot": - if _, ok := config["defaultChatId"]; ok { - config["chatId"] = config["defaultChatId"] - delete(config, "defaultChatId") - record.Set("config", config) - changed = true - } - - case "webhook": - if _, ok := config["defaultDataForDeployment"]; ok { - config["dataForDeployment"] = config["defaultDataForDeployment"] - delete(config, "defaultDataForDeployment") - record.Set("config", config) - changed = true - } - if _, ok := config["defaultDataForNotification"]; ok { - config["dataForNotification"] = config["defaultDataForNotification"] - delete(config, "defaultDataForNotification") - record.Set("config", config) - changed = true - } - } - - if changed { - if err := app.Save(record); err != nil { - return err - } - - tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) - } - } - } - } - - // update collection `certificate` - { - collection, err := app.FindCollectionByNameOrId("4szxr9x43tpj6np") - if err != nil { - return err - } else if collection != nil { - if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{ - "hidden": false, - "id": "by9hetqi", - "maxSelect": 1, - "name": "source", - "presentable": false, - "required": false, - "system": false, - "type": "select", - "values": [ - "request", - "upload" - ] - }`)); err != nil { - return err - } - - if err := collection.Fields.AddMarshaledJSONAt(9, []byte(`{ - "hidden": false, - "id": "v40aqzpd", - "max": "", - "min": "", - "name": "validityNotBefore", - "presentable": false, - "required": false, - "system": false, - "type": "date" - }`)); err != nil { - return err - } - - if err := collection.Fields.AddMarshaledJSONAt(10, []byte(`{ - "hidden": false, - "id": "zgpdby2k", - "max": "", - "min": "", - "name": "validityNotAfter", - "presentable": false, - "required": false, - "system": false, - "type": "date" - }`)); err != nil { - return err - } - - if err := collection.Fields.AddMarshaledJSONAt(15, []byte(`{ - "cascadeDelete": false, - "collectionId": "tovyif5ax6j62ur", - "hidden": false, - "id": "uvqfamb1", - "maxSelect": 1, - "minSelect": 0, - "name": "workflowRef", - "presentable": false, - "required": false, - "system": false, - "type": "relation" - }`)); err != nil { - return err - } - - if err := collection.Fields.AddMarshaledJSONAt(16, []byte(`{ - "cascadeDelete": false, - "collectionId": "qjp8lygssgwyqyz", - "hidden": false, - "id": "relation3917999135", - "maxSelect": 1, - "minSelect": 0, - "name": "workflowRunRef", - "presentable": false, - "required": false, - "system": false, - "type": "relation" - }`)); err != nil { - return err - } - - if err := collection.Fields.AddMarshaledJSONAt(17, []byte(`{ - "cascadeDelete": false, - "collectionId": "bqnxb95f2cooowp", - "hidden": false, - "id": "2ohlr0yd", - "maxSelect": 1, - "minSelect": 0, - "name": "workflowOutputRef", - "presentable": false, - "required": false, - "system": false, - "type": "relation" - }`)); err != nil { - return err - } - - if err := json.Unmarshal([]byte(`{ - "indexes": [ - "CREATE INDEX `+"`"+`idx_Jx8TXzDCmw`+"`"+` ON `+"`"+`certificate`+"`"+` (`+"`"+`workflowRef`+"`"+`)", - "CREATE INDEX `+"`"+`idx_2cRXqNDyyp`+"`"+` ON `+"`"+`certificate`+"`"+` (`+"`"+`workflowRunRef`+"`"+`)", - "CREATE INDEX `+"`"+`idx_kcKpgAZapk`+"`"+` ON `+"`"+`certificate`+"`"+` (`+"`"+`workflowNodeId`+"`"+`)" - ] - }`), &collection); err != nil { - return err - } - - if err := app.Save(collection); err != nil { - return err - } - - if _, err := app.DB().NewQuery("UPDATE certificate SET source = 'request' WHERE source = 'workflow'").Execute(); err != nil { - return err - } - - tracer.Printf("collection '%s' updated", collection.Name) - } - } - - // update collection `workflow` - { - collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur") - if err != nil { - return err - } else if collection != nil { - if err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{ - "hidden": false, - "id": "vqoajwjq", - "maxSelect": 1, - "name": "trigger", - "presentable": false, - "required": false, - "system": false, - "type": "select", - "values": [ - "manual", - "scheduled" - ] - }`)); err != nil { - return err - } - - if err := collection.Fields.AddMarshaledJSONAt(9, []byte(`{ - "cascadeDelete": false, - "collectionId": "qjp8lygssgwyqyz", - "hidden": false, - "id": "a23wkj9x", - "maxSelect": 1, - "minSelect": 0, - "name": "lastRunRef", - "presentable": false, - "required": false, - "system": false, - "type": "relation" - }`)); err != nil { - return err - } - - if err := app.Save(collection); err != nil { - return err - } - - if _, err := app.DB().NewQuery("UPDATE workflow SET trigger = 'scheduled' WHERE trigger = 'auto'").Execute(); err != nil { - return err - } - - tracer.Printf("collection '%s' updated", collection.Name) - - records, err := app.FindAllRecords(collection) - if err != nil { - return err - } else { - for _, record := range records { - changed := false - - draft := make(map[string]any) - if err := record.UnmarshalJSONField("draft", &draft); err == nil { - if _, ok := draft["config"]; ok { - config := draft["config"].(map[string]any) - if _, ok := config["trigger"]; ok { - trigger := config["trigger"].(string) - if trigger == "auto" { - config["trigger"] = "scheduled" - record.Set("draft", draft) - changed = true - } - } - } - } - - content := make(map[string]any) - if err := record.UnmarshalJSONField("content", &content); err == nil { - if _, ok := content["config"]; ok { - config := content["config"].(map[string]any) - if _, ok := config["trigger"]; ok { - trigger := config["trigger"].(string) - if trigger == "auto" { - config["trigger"] = "scheduled" - record.Set("content", content) - changed = true - } - } - } - } - - if changed { - if err := app.Save(record); err != nil { - return err - } - - tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) - } - } - } - } - } - - // update collection `workflow_run` - { - collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz") - if err != nil { - return err - } else if collection != nil { - if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{ - "cascadeDelete": true, - "collectionId": "tovyif5ax6j62ur", - "hidden": false, - "id": "m8xfsyyy", - "maxSelect": 1, - "minSelect": 0, - "name": "workflowRef", - "presentable": false, - "required": false, - "system": false, - "type": "relation" - }`)); err != nil { - return err - } - - if err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{ - "hidden": false, - "id": "jlroa3fk", - "maxSelect": 1, - "name": "trigger", - "presentable": false, - "required": false, - "system": false, - "type": "select", - "values": [ - "manual", - "scheduled" - ] - }`)); err != nil { - return err - } - - if err := json.Unmarshal([]byte(`{ - "indexes": [ - "CREATE INDEX `+"`"+`idx_7ZpfjTFsD2`+"`"+` ON `+"`"+`workflow_run`+"`"+` (`+"`"+`workflowRef`+"`"+`)" - ] - }`), &collection); err != nil { - return err - } - - if err := app.Save(collection); err != nil { - return err - } - - if _, err := app.DB().NewQuery("UPDATE workflow_run SET trigger = 'scheduled' WHERE trigger = 'auto'").Execute(); err != nil { - return err - } - - tracer.Printf("collection '%s' updated", collection.Name) - - records, err := app.FindAllRecords(collection) - if err != nil { - return err - } else { - for _, record := range records { - changed := false - - detail := make(map[string]any) - if err := record.UnmarshalJSONField("detail", &detail); err == nil { - if _, ok := detail["config"]; ok { - config := detail["config"].(map[string]any) - if _, ok := config["trigger"]; ok { - trigger := config["trigger"].(string) - if trigger == "auto" { - config["trigger"] = "scheduled" - record.Set("detail", detail) - changed = true - } - } - } - } - - if changed { - if err := app.Save(record); err != nil { - return err - } - - tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) - } - } - } - } - } - - // update collection `workflow_output` - { - collection, err := app.FindCollectionByNameOrId("bqnxb95f2cooowp") - if err != nil { - return err - } else if collection != nil { - if err := json.Unmarshal([]byte(`{ - "indexes": [ - "CREATE INDEX `+"`"+`idx_BYoQPsz4my`+"`"+` ON `+"`"+`workflow_output`+"`"+` (`+"`"+`workflowRef`+"`"+`)", - "CREATE INDEX `+"`"+`idx_O9zxLETuxJ`+"`"+` ON `+"`"+`workflow_output`+"`"+` (`+"`"+`runRef`+"`"+`)", - "CREATE INDEX `+"`"+`idx_luac8Ul34G`+"`"+` ON `+"`"+`workflow_output`+"`"+` (`+"`"+`nodeId`+"`"+`)" - ] - }`), &collection); err != nil { - return err - } - - if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{ - "cascadeDelete": true, - "collectionId": "tovyif5ax6j62ur", - "hidden": false, - "id": "jka88auc", - "maxSelect": 1, - "minSelect": 0, - "name": "workflowRef", - "presentable": false, - "required": false, - "system": false, - "type": "relation" - }`)); err != nil { - return err - } - - if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{ - "cascadeDelete": true, - "collectionId": "qjp8lygssgwyqyz", - "hidden": false, - "id": "relation821863227", - "maxSelect": 1, - "minSelect": 0, - "name": "runRef", - "presentable": false, - "required": false, - "system": false, - "type": "relation" - }`)); err != nil { - return err - } - - if err := app.Save(collection); err != nil { - return err - } - - tracer.Printf("collection '%s' updated", collection.Name) - } - } - - // update collection `workflow_logs` - { - collection, err := app.FindCollectionByNameOrId("pbc_1682296116") - if err != nil { - return err - } else if collection != nil { - if field := collection.Fields.GetByName("level"); field != nil && field.Type() == "text" { - if _, err := app.DB().NewQuery("UPDATE workflow_logs SET level = -4 WHERE level = 'DEBUG'").Execute(); err != nil { - return err - } - if _, err := app.DB().NewQuery("UPDATE workflow_logs SET level = 0 WHERE level = 'INFO'").Execute(); err != nil { - return err - } - if _, err := app.DB().NewQuery("UPDATE workflow_logs SET level = 4 WHERE level = 'WARN'").Execute(); err != nil { - return err - } - if _, err := app.DB().NewQuery("UPDATE workflow_logs SET level = 8 WHERE level = 'ERROR'").Execute(); err != nil { - return err - } - - if err := collection.Fields.AddMarshaledJSONAt(7, []byte(`{ - "hidden": false, - "id": "number760395071", - "max": null, - "min": null, - "name": "levelTmp", - "onlyInt": false, - "presentable": false, - "required": false, - "system": false, - "type": "number" - }`)); err != nil { - return err - } - if err := app.Save(collection); err != nil { - return err - } - - collection.Fields.RemoveById(field.GetId()) - if err := app.Save(collection); err != nil { - return err - } - - if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{ - "hidden": false, - "id": "number760395071", - "max": null, - "min": null, - "name": "level", - "onlyInt": false, - "presentable": false, - "required": false, - "system": false, - "type": "number" - }`)); err != nil { - return err - } - if err := app.Save(collection); err != nil { - return err - } - } - - if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{ - "cascadeDelete": true, - "collectionId": "tovyif5ax6j62ur", - "hidden": false, - "id": "relation3371272342", - "maxSelect": 1, - "minSelect": 0, - "name": "workflowRef", - "presentable": false, - "required": false, - "system": false, - "type": "relation" - }`)); err != nil { - return err - } - - if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{ - "cascadeDelete": true, - "collectionId": "qjp8lygssgwyqyz", - "hidden": false, - "id": "relation821863227", - "maxSelect": 1, - "minSelect": 0, - "name": "runRef", - "presentable": false, - "required": false, - "system": false, - "type": "relation" - }`)); err != nil { - return err - } - - if err := json.Unmarshal([]byte(`{ - "indexes": [ - "CREATE INDEX `+"`"+`idx_IOlpy6XuJ2`+"`"+` ON `+"`"+`workflow_logs`+"`"+` (`+"`"+`workflowRef`+"`"+`)", - "CREATE INDEX `+"`"+`idx_qVlTb2yl7v`+"`"+` ON `+"`"+`workflow_logs`+"`"+` (`+"`"+`runRef`+"`"+`)", - "CREATE INDEX `+"`"+`idx_UL4tdCXNlA`+"`"+` ON `+"`"+`workflow_logs`+"`"+` (`+"`"+`nodeId`+"`"+`)" - ] - }`), &collection); err != nil { - return err - } - - if err := app.Save(collection); err != nil { - return err - } - - tracer.Printf("collection '%s' updated", collection.Name) - } - } - - tracer.Printf("done") - return nil - }, func(app core.App) error { - return nil - }) -} diff --git a/migrations/1755187200_cm0.4.0_migrate.go b/migrations/1755187200_cm0.4.0_migrate.go new file mode 100644 index 000000000..0a4e6f0e1 --- /dev/null +++ b/migrations/1755187200_cm0.4.0_migrate.go @@ -0,0 +1,1115 @@ +package migrations + +import ( + "encoding/json" + "fmt" + "regexp" + "strings" + + "github.com/pocketbase/pocketbase/core" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/samber/lo" +) + +func init() { + m.Register(func(app core.App) error { + tracer := NewTracer("v0.4.0") + tracer.Printf("go ...") + + // update collection `settings` + // - delete records: 'notifyChannels', 'notifyTemplates' + { + collection, err := app.FindCollectionByNameOrId("dy6ccjb60spfy6p") + if err != nil { + return err + } else if collection != nil { + if _, err := app.DB().NewQuery("DELETE FROM settings WHERE name = 'notifyChannels'").Execute(); err != nil { + return err + } + + if _, err := app.DB().NewQuery("DELETE FROM settings WHERE name = 'notifyTemplates'").Execute(); err != nil { + return err + } + + tracer.Printf("collection '%s' updated", collection.Name) + } + } + + // update collection `access` + // - modify field `config` schema: rename property `defaultReceiver` to `receiver` + // - delete records: 'local' + { + collection, err := app.FindCollectionByNameOrId("4yzbv8urny5ja1e") + if err != nil { + return err + } else if collection != nil { + if _, err := app.DB().NewQuery("DELETE FROM access WHERE provider = 'local'").Execute(); err != nil { + return err + } + + records, err := app.FindAllRecords(collection) + if err != nil { + return err + } + + for _, record := range records { + changed := false + + provider := record.GetString("provider") + config := make(map[string]any) + if err := record.UnmarshalJSONField("config", &config); err != nil { + return err + } + + switch provider { + case "discordbot", "mattermost", "slackbot": + if _, ok := config["defaultChannelId"]; ok { + config["channelId"] = config["defaultChannelId"] + delete(config, "defaultChannelId") + record.Set("config", config) + changed = true + } + + case "email": + if _, ok := config["defaultSenderAddress"]; ok { + config["senderAddress"] = config["defaultSenderAddress"] + delete(config, "defaultSenderAddress") + record.Set("config", config) + changed = true + } + if _, ok := config["defaultSenderName"]; ok { + config["senderName"] = config["defaultSenderName"] + delete(config, "defaultSenderName") + record.Set("config", config) + changed = true + } + if _, ok := config["defaultReceiverAddress"]; ok { + config["receiverAddress"] = config["defaultReceiverAddress"] + delete(config, "defaultReceiverAddress") + record.Set("config", config) + changed = true + } + + case "telegrambot": + if _, ok := config["defaultChatId"]; ok { + config["chatId"] = config["defaultChatId"] + delete(config, "defaultChatId") + record.Set("config", config) + changed = true + } + + case "webhook": + if _, ok := config["defaultDataForDeployment"]; ok { + config["dataForDeployment"] = config["defaultDataForDeployment"] + delete(config, "defaultDataForDeployment") + record.Set("config", config) + changed = true + } + if _, ok := config["defaultDataForNotification"]; ok { + config["dataForNotification"] = config["defaultDataForNotification"] + delete(config, "defaultDataForNotification") + record.Set("config", config) + changed = true + } + } + + if changed { + if err := app.Save(record); err != nil { + return err + } + + tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) + } + } + } + } + + // update collection `certificate` + // - modify field `source` candidates + // - rename field `effectAt` to `validityNotBefore` + // - rename field `expireAt` to `validityNotAfter` + // - rename field `workflowId` to `workflowRef` + // - rename field `workflowRunId` to `workflowRunRef` + // - rename field `workflowOutputId`(aka `workflowOutputRef`) + { + collection, err := app.FindCollectionByNameOrId("4szxr9x43tpj6np") + if err != nil { + return err + } else if collection != nil { + if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{ + "hidden": false, + "id": "by9hetqi", + "maxSelect": 1, + "name": "source", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "request", + "upload" + ] + }`)); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(9, []byte(`{ + "hidden": false, + "id": "v40aqzpd", + "max": "", + "min": "", + "name": "validityNotBefore", + "presentable": false, + "required": false, + "system": false, + "type": "date" + }`)); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(10, []byte(`{ + "hidden": false, + "id": "zgpdby2k", + "max": "", + "min": "", + "name": "validityNotAfter", + "presentable": false, + "required": false, + "system": false, + "type": "date" + }`)); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(15, []byte(`{ + "cascadeDelete": false, + "collectionId": "tovyif5ax6j62ur", + "hidden": false, + "id": "uvqfamb1", + "maxSelect": 1, + "minSelect": 0, + "name": "workflowRef", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }`)); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(16, []byte(`{ + "cascadeDelete": false, + "collectionId": "qjp8lygssgwyqyz", + "hidden": false, + "id": "relation3917999135", + "maxSelect": 1, + "minSelect": 0, + "name": "workflowRunRef", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }`)); err != nil { + return err + } + + collection.Fields.RemoveByName("workflowOutputId") + collection.Fields.RemoveByName("workflowOutputRef") + + if err := json.Unmarshal([]byte(`{ + "indexes": [ + "CREATE INDEX `+"`"+`idx_Jx8TXzDCmw`+"`"+` ON `+"`"+`certificate`+"`"+` (`+"`"+`workflowRef`+"`"+`)", + "CREATE INDEX `+"`"+`idx_2cRXqNDyyp`+"`"+` ON `+"`"+`certificate`+"`"+` (`+"`"+`workflowRunRef`+"`"+`)", + "CREATE INDEX `+"`"+`idx_kcKpgAZapk`+"`"+` ON `+"`"+`certificate`+"`"+` (`+"`"+`workflowNodeId`+"`"+`)" + ] + }`), &collection); err != nil { + return err + } + + if err := app.Save(collection); err != nil { + return err + } + + if _, err := app.DB().NewQuery("UPDATE certificate SET source = 'request' WHERE source = 'workflow'").Execute(); err != nil { + return err + } + + tracer.Printf("collection '%s' updated", collection.Name) + } + } + + // update collection `workflow` + // - modify field `trigger` candidates, and cascading migrate field `graphDraft` / `graphContent` + // - modify field `lastRunStatus` candidates + // - rename field `lastRunRefId` to `lastRunRef` + // - rename field `draft` to `graphDraft` + // - rename field `content` to `graphContent` + // - add field `hasContent` + { + collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur") + if err != nil { + return err + } else if collection != nil { + if err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{ + "hidden": false, + "id": "vqoajwjq", + "maxSelect": 1, + "name": "trigger", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "manual", + "scheduled" + ] + }`)); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{ + "hidden": false, + "id": "g9ohkk5o", + "maxSize": 5000000, + "name": "graphDraft", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }`)); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(7, []byte(`{ + "hidden": false, + "id": "awlphkfe", + "maxSize": 5000000, + "name": "graphContent", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }`)); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(9, []byte(`{ + "hidden": false, + "id": "bool3832150317", + "name": "hasContent", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }`)); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(10, []byte(`{ + "cascadeDelete": false, + "collectionId": "qjp8lygssgwyqyz", + "hidden": false, + "id": "a23wkj9x", + "maxSelect": 1, + "minSelect": 0, + "name": "lastRunRef", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }`)); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(11, []byte(`{ + "hidden": false, + "id": "zivdxh23", + "maxSelect": 1, + "name": "lastRunStatus", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "pending", + "processing", + "succeeded", + "failed", + "canceled" + ] + }`)); err != nil { + return err + } + + if err := app.Save(collection); err != nil { + return err + } + + if _, err := app.DB().NewQuery("UPDATE workflow SET trigger = 'scheduled' WHERE trigger = 'auto'").Execute(); err != nil { + return err + } + + if _, err := app.DB().NewQuery("UPDATE workflow SET hasContent = TRUE WHERE graphContent IS NOT NULL").Execute(); err != nil { + return err + } + + if _, err := app.DB().NewQuery("UPDATE workflow SET lastRunStatus = 'processing' WHERE lastRunStatus = 'running'").Execute(); err != nil { + return err + } + + tracer.Printf("collection '%s' updated", collection.Name) + + records, err := app.FindAllRecords(collection) + if err != nil { + return err + } else { + for _, record := range records { + changed := false + + graphDraft := make(map[string]any) + if err := record.UnmarshalJSONField("graphDraft", &graphDraft); err == nil { + if _, ok := graphDraft["config"]; ok { + config := graphDraft["config"].(map[string]any) + if _, ok := config["trigger"]; ok { + trigger := config["trigger"].(string) + if trigger == "auto" { + config["trigger"] = "scheduled" + record.Set("graphDraft", graphDraft) + changed = true + } + } + } + } + + graphContent := make(map[string]any) + if err := record.UnmarshalJSONField("graphContent", &graphContent); err == nil { + if _, ok := graphContent["config"]; ok { + config := graphContent["config"].(map[string]any) + if _, ok := config["trigger"]; ok { + trigger := config["trigger"].(string) + if trigger == "auto" { + config["trigger"] = "scheduled" + record.Set("graphContent", graphContent) + changed = true + } + } + } + } + + if changed { + if err := app.Save(record); err != nil { + return err + } + + tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) + } + } + } + } + } + + // update collection `workflow_run` + // - modify field `trigger` candidates, and cascading migrate field `graph` + // - modify field `status` candidates + // - rename field `detail` to `graph` + // - rename field `workflowId` to `workflowRef` + { + collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz") + if err != nil { + return err + } else if collection != nil { + if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{ + "cascadeDelete": true, + "collectionId": "tovyif5ax6j62ur", + "hidden": false, + "id": "m8xfsyyy", + "maxSelect": 1, + "minSelect": 0, + "name": "workflowRef", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }`)); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{ + "hidden": false, + "id": "qldmh0tw", + "maxSelect": 1, + "name": "status", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "pending", + "processing", + "succeeded", + "failed", + "canceled" + ] + }`)); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{ + "hidden": false, + "id": "jlroa3fk", + "maxSelect": 1, + "name": "trigger", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "manual", + "scheduled" + ] + }`)); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{ + "hidden": false, + "id": "json772177811", + "maxSize": 5000000, + "name": "graph", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }`)); err != nil { + return err + } + + if err := json.Unmarshal([]byte(`{ + "indexes": [ + "CREATE INDEX `+"`"+`idx_7ZpfjTFsD2`+"`"+` ON `+"`"+`workflow_run`+"`"+` (`+"`"+`workflowRef`+"`"+`)" + ] + }`), &collection); err != nil { + return err + } + + if err := app.Save(collection); err != nil { + return err + } + + if _, err := app.DB().NewQuery("UPDATE workflow_run SET trigger = 'scheduled' WHERE trigger = 'auto'").Execute(); err != nil { + return err + } + + if _, err := app.DB().NewQuery("UPDATE workflow_run SET status = 'processing' WHERE status = 'running'").Execute(); err != nil { + return err + } + + tracer.Printf("collection '%s' updated", collection.Name) + + records, err := app.FindAllRecords(collection) + if err != nil { + return err + } else { + for _, record := range records { + changed := false + + graphContent := make(map[string]any) + if err := record.UnmarshalJSONField("graph", &graphContent); err == nil { + if _, ok := graphContent["config"]; ok { + config := graphContent["config"].(map[string]any) + if _, ok := config["trigger"]; ok { + trigger := config["trigger"].(string) + if trigger == "auto" { + config["trigger"] = "scheduled" + record.Set("graph", graphContent) + changed = true + } + } + } + } + + if changed { + if err := app.Save(record); err != nil { + return err + } + + tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) + } + } + } + } + } + + // update collection `workflow_output` + // - rename field `workflowId` to `workflowRef` + // - rename field `runId` to `runRef` + // - rename field `node` to `nodeConfig` + { + collection, err := app.FindCollectionByNameOrId("bqnxb95f2cooowp") + if err != nil { + return err + } else if collection != nil { + if err := json.Unmarshal([]byte(`{ + "indexes": [ + "CREATE INDEX `+"`"+`idx_BYoQPsz4my`+"`"+` ON `+"`"+`workflow_output`+"`"+` (`+"`"+`workflowRef`+"`"+`)", + "CREATE INDEX `+"`"+`idx_O9zxLETuxJ`+"`"+` ON `+"`"+`workflow_output`+"`"+` (`+"`"+`runRef`+"`"+`)", + "CREATE INDEX `+"`"+`idx_luac8Ul34G`+"`"+` ON `+"`"+`workflow_output`+"`"+` (`+"`"+`nodeId`+"`"+`)" + ] + }`), &collection); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{ + "cascadeDelete": true, + "collectionId": "tovyif5ax6j62ur", + "hidden": false, + "id": "jka88auc", + "maxSelect": 1, + "minSelect": 0, + "name": "workflowRef", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }`)); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{ + "cascadeDelete": true, + "collectionId": "qjp8lygssgwyqyz", + "hidden": false, + "id": "relation821863227", + "maxSelect": 1, + "minSelect": 0, + "name": "runRef", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }`)); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(4, []byte(`{ + "hidden": false, + "id": "json2239752261", + "maxSize": 5000000, + "name": "nodeConfig", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }`)); err != nil { + return err + } + + if err := app.Save(collection); err != nil { + return err + } + + tracer.Printf("collection '%s' updated", collection.Name) + } + } + + // update collection `workflow_logs` + // - modify field `level` type + // - rename field `workflowId` to `workflowRef` + // - rename field `runId` to `runRef` + // - migrate field `message` + { + collection, err := app.FindCollectionByNameOrId("pbc_1682296116") + if err != nil { + return err + } else if collection != nil { + if field := collection.Fields.GetByName("level"); field != nil && field.Type() == "text" { + if _, err := app.DB().NewQuery("UPDATE workflow_logs SET level = '-4' WHERE level = 'DEBUG'").Execute(); err != nil { + return err + } + if _, err := app.DB().NewQuery("UPDATE workflow_logs SET level = '0' WHERE level = 'INFO'").Execute(); err != nil { + return err + } + if _, err := app.DB().NewQuery("UPDATE workflow_logs SET level = '4' WHERE level = 'WARN'").Execute(); err != nil { + return err + } + if _, err := app.DB().NewQuery("UPDATE workflow_logs SET level = '8' WHERE level = 'ERROR'").Execute(); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(7, []byte(`{ + "hidden": false, + "id": "number760395071", + "max": null, + "min": null, + "name": "levelTmp", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }`)); err != nil { + return err + } + if err := app.Save(collection); err != nil { + return err + } + + if _, err := app.DB().NewQuery("UPDATE workflow_logs SET levelTmp = level").Execute(); err != nil { + return err + } + + collection.Fields.RemoveById(field.GetId()) + if err := app.Save(collection); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{ + "hidden": false, + "id": "number760395071", + "max": null, + "min": null, + "name": "level", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }`)); err != nil { + return err + } + if err := app.Save(collection); err != nil { + return err + } + } + + if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{ + "cascadeDelete": true, + "collectionId": "tovyif5ax6j62ur", + "hidden": false, + "id": "relation3371272342", + "maxSelect": 1, + "minSelect": 0, + "name": "workflowRef", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }`)); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{ + "cascadeDelete": true, + "collectionId": "qjp8lygssgwyqyz", + "hidden": false, + "id": "relation821863227", + "maxSelect": 1, + "minSelect": 0, + "name": "runRef", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }`)); err != nil { + return err + } + + if err := json.Unmarshal([]byte(`{ + "indexes": [ + "CREATE INDEX `+"`"+`idx_IOlpy6XuJ2`+"`"+` ON `+"`"+`workflow_logs`+"`"+` (`+"`"+`workflowRef`+"`"+`)", + "CREATE INDEX `+"`"+`idx_qVlTb2yl7v`+"`"+` ON `+"`"+`workflow_logs`+"`"+` (`+"`"+`runRef`+"`"+`)", + "CREATE INDEX `+"`"+`idx_UL4tdCXNlA`+"`"+` ON `+"`"+`workflow_logs`+"`"+` (`+"`"+`nodeId`+"`"+`)" + ] + }`), &collection); err != nil { + return err + } + + if _, err := app.DB().NewQuery("UPDATE workflow_logs SET message = REPLACE(message, 'certificiate', 'certificate') WHERE level = 0").Execute(); err != nil { + return err + } + if _, err := app.DB().NewQuery("UPDATE workflow_logs SET message = REPLACE(message, 'ready to apply certificate', 'ready to request certificate') WHERE level = 0").Execute(); err != nil { + return err + } + if _, err := app.DB().NewQuery("UPDATE workflow_logs SET message = REPLACE(message, 'ready to obtain certificate', 'ready to request certificate') WHERE level = 0").Execute(); err != nil { + return err + } + + if err := app.Save(collection); err != nil { + return err + } + + tracer.Printf("collection '%s' updated", collection.Name) + } + } + + // adapt to new workflow data structure + { + type dLegacyWorkflowNode struct { + Id string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Config map[string]any `json:"config,omitempty"` + Next *dLegacyWorkflowNode `json:"next,omitempty"` + Branches []*dLegacyWorkflowNode `json:"branches,omitempty"` + } + + type dWorkflowNodeData struct { + Name string `json:"name"` + Config map[string]any `json:"config,omitempty"` + } + + type dWorkflowNode struct { + Id string `json:"id"` + Type string `json:"type"` + Data dWorkflowNodeData `json:"data"` + Blocks []*dWorkflowNode `json:"blocks,omitempty"` + } + + convertNode := func(root *dLegacyWorkflowNode) []*dWorkflowNode { + lang := lo. + IfF(root == nil, func() string { return "zh" }). + ElseIf(regexp.MustCompile(`[\p{Han}]`).MatchString(root.Name), "zh"). + Else("en") + + var deepConvertNode func(node *dLegacyWorkflowNode) []*dWorkflowNode + deepConvertNode = func(node *dLegacyWorkflowNode) []*dWorkflowNode { + temp := make([]*dWorkflowNode, 0) + + current := node + for current != nil { + current.Config = lo.PickBy(current.Config, func(key string, value any) bool { + str, ok := value.(string) + return !ok || str != "" + }) + + switch current.Type { + case "start": + temp = append(temp, &dWorkflowNode{ + Id: current.Id, + Type: "start", + Data: dWorkflowNodeData{ + Name: current.Name, + Config: current.Config, + }, + }) + + case "apply": + if _, ok := current.Config["challengeType"].(string); !ok { + current.Config["challengeType"] = "dns-01" + } + + temp = append(temp, &dWorkflowNode{ + Id: current.Id, + Type: "bizApply", + Data: dWorkflowNodeData{ + Name: current.Name, + Config: current.Config, + }, + }) + + case "deploy": + if s, ok := current.Config["certificate"].(string); ok { + current.Config["certificateOutputNodeId"] = strings.Split(s, "#")[0] + delete(current.Config, "certificate") + } + + temp = append(temp, &dWorkflowNode{ + Id: current.Id, + Type: "bizDeploy", + Data: dWorkflowNodeData{ + Name: current.Name, + Config: current.Config, + }, + }) + + case "upload", "monitor", "notify": + temp = append(temp, &dWorkflowNode{ + Id: current.Id, + Type: "biz" + strings.Title(current.Type), + Data: dWorkflowNodeData{ + Name: current.Name, + Config: current.Config, + }, + }) + + case "execute_result_branch": + if len(temp) == 0 { + break + } + + tryNode, _ := lo.Last(temp) + temp = lo.DropRight(temp, 1) + + branches := lo.GroupBy(current.Branches, func(b *dLegacyWorkflowNode) string { return b.Type }) + successBranch := lo.IfF(len(branches["execute_success"]) > 0, func() *dLegacyWorkflowNode { + return branches["execute_success"][0] + }).Else(nil) + failureBranch := lo.IfF(len(branches["execute_failure"]) > 0, func() *dLegacyWorkflowNode { + return branches["execute_failure"][0] + }).Else(nil) + successBranchId := lo.If(successBranch != nil, successBranch.Id).Else(core.GenerateDefaultRandomId()) + failureBranchId := lo.If(failureBranch != nil, failureBranch.Id).Else(core.GenerateDefaultRandomId()) + + catchBlocks := lo.If(failureBranch != nil && failureBranch.Next != nil, deepConvertNode(failureBranch.Next)).Else([]*dWorkflowNode{}) + catchBlocks = append(catchBlocks, &dWorkflowNode{ + Id: core.GenerateDefaultRandomId(), + Type: "end", + Data: dWorkflowNodeData{ + Name: lo.If(lang == "en", "End").Else("结束"), + }, + }) + + tryCatchNode := &dWorkflowNode{ + Id: current.Id, + Type: "tryCatch", + Data: dWorkflowNodeData{ + Name: lo.If(lang == "en", "Try to ...").Else("尝试执行…"), + Config: current.Config, + }, + Blocks: []*dWorkflowNode{ + { + Id: successBranchId, + Type: "tryBlock", + Data: dWorkflowNodeData{ + Name: "", + }, + Blocks: []*dWorkflowNode{tryNode}, + }, + { + Id: failureBranchId, + Type: "catchBlock", + Data: dWorkflowNodeData{ + Name: lo.If(lang == "en", "On failed ...").Else("若执行失败…"), + }, + Blocks: catchBlocks, + }, + }, + } + + temp = append(temp, tryCatchNode) + current = successBranch + + case "branch": + branchNode := &dWorkflowNode{ + Id: current.Id, + Type: "condition", + Data: dWorkflowNodeData{ + Name: lo.If(lang == "en", "Parallel").Else("并行"), + Config: current.Config, + }, + Blocks: lo.Map(current.Branches, func(b *dLegacyWorkflowNode, _ int) *dWorkflowNode { + return &dWorkflowNode{ + Id: b.Id, + Type: "branchBlock", + Data: dWorkflowNodeData{ + Name: b.Name, + Config: b.Config, + }, + Blocks: deepConvertNode(b.Next), + } + }), + } + + temp = append(temp, branchNode) + } + + if current != nil { + current = current.Next + } + } + + return temp + } + + nodes := lo.Ternary(root == nil, []*dWorkflowNode{ + { + Id: core.GenerateDefaultRandomId(), + Type: "start", + Data: dWorkflowNodeData{ + Name: lo.If(lang == "en", "Start").Else("开始"), + }, + }, + }, deepConvertNode(root)) + + return append(nodes, &dWorkflowNode{ + Id: core.GenerateDefaultRandomId(), + Type: "end", + Data: dWorkflowNodeData{ + Name: lo.If(lang == "en", "End").Else("结束"), + }, + }) + } + + // update collection `workflow` + // - migrate field `graphDraft` / `graphContent` + { + collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur") + if err != nil { + return err + } else if collection != nil { + records, err := app.FindAllRecords(collection) + if err != nil { + return err + } else { + for _, record := range records { + changed := false + + graphDraft := make(map[string]any) + if err := record.UnmarshalJSONField("graphDraft", &graphDraft); err == nil { + if len(graphDraft) > 0 { + if _, ok := graphDraft["nodes"]; !ok { + legacyRootNode := &dLegacyWorkflowNode{} + if err := record.UnmarshalJSONField("graphDraft", legacyRootNode); err != nil { + return err + } else { + graphDraft = make(map[string]any) + graphDraft["nodes"] = convertNode(legacyRootNode) + record.Set("graphDraft", graphDraft) + changed = true + } + } + } + } + + graphContent := make(map[string]any) + if err := record.UnmarshalJSONField("graphContent", &graphContent); err == nil { + if len(graphContent) > 0 { + if _, ok := graphContent["nodes"]; !ok { + legacyRootNode := &dLegacyWorkflowNode{} + if err := record.UnmarshalJSONField("graphContent", legacyRootNode); err != nil { + return err + } else { + graphContent = make(map[string]any) + graphContent["nodes"] = convertNode(legacyRootNode) + record.Set("graphContent", graphContent) + record.Set("hasContent", true) + changed = true + } + } + } + } + + if changed { + if err := app.Save(record); err != nil { + return err + } + + tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) + } + } + } + } + } + + // update collection `workflow_run` + // - migrate field `graph` + { + collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz") + if err != nil { + return err + } else if collection != nil { + records, err := app.FindAllRecords(collection) + if err != nil { + return err + } else { + for _, record := range records { + changed := false + + graphContent := make(map[string]any) + if err := record.UnmarshalJSONField("graph", &graphContent); err == nil { + if len(graphContent) > 0 { + if _, ok := graphContent["nodes"]; !ok { + legacyRootNode := &dLegacyWorkflowNode{} + if err := record.UnmarshalJSONField("graph", legacyRootNode); err != nil { + return err + } else { + graphContent = make(map[string]any) + graphContent["nodes"] = convertNode(legacyRootNode) + record.Set("graph", graphContent) + changed = true + } + } + } + } + + if changed { + if err := app.Save(record); err != nil { + return err + } + + tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) + } + } + } + } + } + + // update collection `workflow_output` + // - migrate field `nodeConfig` + // - migrate field `outputs` + { + collection, err := app.FindCollectionByNameOrId("bqnxb95f2cooowp") + if err != nil { + return err + } else if collection != nil { + records, err := app.FindAllRecords(collection) + if err != nil { + return err + } else { + for _, record := range records { + changed := false + + nodeConfig := make(map[string]any) + if err := record.UnmarshalJSONField("nodeConfig", &nodeConfig); err == nil { + if _, ok := nodeConfig["id"]; ok { + if _, ok := nodeConfig["type"]; ok { + if _, ok := nodeConfig["config"]; ok { + record.Set("nodeConfig", nodeConfig["config"]) + changed = true + } + } + } + } + + outputs := make([]map[string]any, 0) + if err := record.UnmarshalJSONField("outputs", &outputs); err == nil { + for i, output := range outputs { + if _, ok := output["label"]; ok { + output["valueType"] = "string" + delete(output, "label") + delete(output, "required") + delete(output, "valueSelector") + + if output["type"] == "certificate" { + output["type"] = "ref" + output["value"] = fmt.Sprintf("certificate#%s", output["value"]) + } + + outputs[i] = output + } else { + continue + } + record.Set("outputs", outputs) + changed = true + } + } + + if changed { + if err := app.Save(record); err != nil { + return err + } + + tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) + } + } + } + } + } + } + + tracer.Printf("done") + return nil + }, func(app core.App) error { + return nil + }) +} diff --git a/pkg/logging/handler.go b/pkg/logging/handler.go index 519cf18bf..c69c4bbf1 100644 --- a/pkg/logging/handler.go +++ b/pkg/logging/handler.go @@ -34,7 +34,7 @@ func NewHookHandler(opts *HookHandlerOptions) *HookHandler { } if h.options.WriteFunc == nil { - panic("`options.WriteFunc` is nil") + panic("the `options.WriteFunc` is nil") } if h.options.Level == nil { diff --git a/pkg/utils/cert/converter.go b/pkg/utils/cert/converter.go index f6fddcde0..f7b1652aa 100644 --- a/pkg/utils/cert/converter.go +++ b/pkg/utils/cert/converter.go @@ -18,7 +18,7 @@ import ( // - err: 错误。 func ConvertCertificateToPEM(cert *x509.Certificate) (_certPEM string, _err error) { if cert == nil { - return "", errors.New("`cert` is nil") + return "", errors.New("the input certificate is nil") } block := &pem.Block{ @@ -39,7 +39,7 @@ func ConvertCertificateToPEM(cert *x509.Certificate) (_certPEM string, _err erro // - err: 错误。 func ConvertECPrivateKeyToPEM(privkey *ecdsa.PrivateKey) (_privkeyPEM string, _err error) { if privkey == nil { - return "", errors.New("`privkey` is nil") + return "", errors.New("the input private key is nil") } data, _err := x509.MarshalECPrivateKey(privkey) diff --git a/ui/index.html b/ui/index.html index fe1c7f6b9..ba75b0475 100644 --- a/ui/index.html +++ b/ui/index.html @@ -4,7 +4,7 @@ - Certimate - Your Trusted SSL Automation Partner + Certimate - Your Trusted Partner in SSL Automation
diff --git a/ui/package-lock.json b/ui/package-lock.json index 400857945..ea3764c85 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -9,22 +9,22 @@ "dependencies": { "@codemirror/lang-json": "^6.0.2", "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "^6.11.2", + "@codemirror/language": "^6.11.3", "@codemirror/legacy-modes": "^6.5.1", - "@flowgram.ai/document": "^0.3.5", - "@flowgram.ai/fixed-layout-editor": "^0.3.5", - "@flowgram.ai/minimap-plugin": "^0.3.5", + "@flowgram.ai/document": "^0.4.0", + "@flowgram.ai/fixed-layout-editor": "^0.4.0", + "@flowgram.ai/minimap-plugin": "^0.4.0", "@tabler/icons-react": "^3.34.1", - "@uiw/codemirror-extensions-basic-setup": "^4.24.2", - "@uiw/codemirror-theme-vscode": "^4.24.2", - "@uiw/react-codemirror": "^4.24.2", + "@uiw/codemirror-extensions-basic-setup": "^4.25.1", + "@uiw/codemirror-theme-vscode": "^4.25.1", + "@uiw/react-codemirror": "^4.25.1", "ahooks": "^3.9.0", "antd": "^5.27.0", "antd-zod": "^7.0.0", "clsx": "^2.1.1", "cron-parser": "^5.3.0", "file-saver": "^2.0.5", - "i18next": "^25.3.4", + "i18next": "^25.3.6", "i18next-browser-languagedetector": "^8.2.0", "immer": "^10.1.1", "nanoid": "^5.1.5", @@ -34,15 +34,15 @@ "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1", "react-i18next": "^15.6.1", - "react-router-dom": "^7.8.0", + "react-router-dom": "^7.8.1", "tailwind-merge": "^3.3.1", "zod": "^4.0.17", "zustand": "^5.0.7" }, "devDependencies": { "@eslint/js": "^9.33.0", - "@tailwindcss/postcss": "^4.1.11", - "@tailwindcss/vite": "^4.1.11", + "@tailwindcss/postcss": "^4.1.12", + "@tailwindcss/vite": "^4.1.12", "@types/file-saver": "^2.0.7", "@types/fs-extra": "^11.0.4", "@types/node": "^22.16.5", @@ -61,7 +61,7 @@ "eslint-plugin-react-refresh": "^0.4.20", "fs-extra": "^11.3.1", "prettier": "^3.6.2", - "tailwindcss": "^4.1.11", + "tailwindcss": "^4.1.12", "typescript": "^5.8.3", "typescript-eslint": "^8.39.1", "vite": "^6.3.5" @@ -1691,9 +1691,9 @@ } }, "node_modules/@codemirror/language": { - "version": "6.11.2", - "resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.11.2.tgz", - "integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==", + "version": "6.11.3", + "resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.11.3.tgz", + "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -2408,12 +2408,12 @@ } }, "node_modules/@flowgram.ai/background-plugin": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/background-plugin/-/background-plugin-0.3.5.tgz", - "integrity": "sha512-4iUka9SQNNceNQwivTBE1XPJjNSj4tP/6dieO2CVInPEl/PgIRodgTM5gjVvrnr/Mvrk1231+ttFL68HsVnByA==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/background-plugin/-/background-plugin-0.4.0.tgz", + "integrity": "sha512-aHyIZ0O87L2CLrEUZQf3MyWDyHqpjchg9CUpZETUFYIeAXGP0Uk0/31C3slLPKdsYFGuW5y54E0ighII0tbTBQ==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "inversify": "^6.0.1", "reflect-metadata": "~0.2.2" }, @@ -2423,11 +2423,11 @@ } }, "node_modules/@flowgram.ai/command": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/command/-/command-0.3.5.tgz", - "integrity": "sha512-FWGaOD0+vGaKzDq2+c7NtzTyLofK/9rUoXA8YLbEnG+CNSaCDh9Q2+93YrTowRcCvSWxSHewGlLSquj2uAP3Wg==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/command/-/command-0.4.0.tgz", + "integrity": "sha512-sRro1cAG84qIvx04DzN79Wv1Ldv2CtUEKL1TjG6URQ0wYPEcF89mZF5vUG3U4sU9bXBYvfa9hSsJpSOofDaF8Q==", "dependencies": { - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/utils": "0.4.0", "inversify": "^6.0.1", "reflect-metadata": "~0.2.2" }, @@ -2437,12 +2437,12 @@ } }, "node_modules/@flowgram.ai/core": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/core/-/core-0.3.5.tgz", - "integrity": "sha512-XrrogUnSjari3FipYUqpyiPKy30vewI5Jp9khRgyXc9cZiLVbdOd8Ki/LzHyxQc0E5R2H84XHHCpt1eWcKGMew==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/core/-/core-0.4.0.tgz", + "integrity": "sha512-/dgEGYJt/hQNYBViRXdv8zoUorm8vXhH3n/1p1W9xdM076mkTjIfesJHEFU+LLy4/ZZVH+G200O76XqGGK5zog==", "dependencies": { - "@flowgram.ai/command": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/command": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "@phosphor/messaging": "^1.3.0", "@tweenjs/tween.js": "^18", "clsx": "^1.1.1", @@ -2482,12 +2482,12 @@ } }, "node_modules/@flowgram.ai/document": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/document/-/document-0.3.5.tgz", - "integrity": "sha512-Y6Rrka33peD56bfJ5kDm2Ah+aDXiLJZdhepVoyvVBupySDnsGvAl7D4s0/dev+zQb6XsatgWAJf1SZ3UzdeTxQ==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/document/-/document-0.4.0.tgz", + "integrity": "sha512-LXy3gTGusK+e4qJxRl1DhfwGS6p5rQnRJq/XFXhHMnEnE/fwG6ZSJOsFDcVnNdU8Z5pwW7393FwNfyqIZoabIA==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "inversify": "^6.0.1", "lodash": "^4.17.21", "nanoid": "^4.0.2", @@ -2512,29 +2512,29 @@ } }, "node_modules/@flowgram.ai/editor": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/editor/-/editor-0.3.5.tgz", - "integrity": "sha512-/DCivTv4Gog8bXJ/qL10CTAGcwitnP3+Mnb6bJAjwvfX/y5FCD0OL9SKRIVXQhip4zxmFrmZmwnswJPnpbvxtA==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/editor/-/editor-0.4.0.tgz", + "integrity": "sha512-/ATStbs+jtwMErOv/7XMbsLs6yjvb8r9t8xcwfhOrqYGGEgWpaIX1jadifcIyr1T6V1yjZdiWTUIjZvVqyPm+A==", "dependencies": { - "@flowgram.ai/background-plugin": "0.3.5", - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/document": "0.3.5", - "@flowgram.ai/form": "0.3.5", - "@flowgram.ai/form-core": "0.3.5", - "@flowgram.ai/group-plugin": "0.3.5", - "@flowgram.ai/history": "0.3.5", - "@flowgram.ai/history-node-plugin": "0.3.5", - "@flowgram.ai/i18n-plugin": "0.3.5", - "@flowgram.ai/materials-plugin": "0.3.5", - "@flowgram.ai/node": "0.3.5", - "@flowgram.ai/node-core-plugin": "0.3.5", - "@flowgram.ai/node-variable-plugin": "0.3.5", - "@flowgram.ai/playground-react": "0.3.5", - "@flowgram.ai/redux-devtool-plugin": "0.3.5", - "@flowgram.ai/renderer": "0.3.5", - "@flowgram.ai/shortcuts-plugin": "0.3.5", - "@flowgram.ai/utils": "0.3.5", - "@flowgram.ai/variable-plugin": "0.3.5", + "@flowgram.ai/background-plugin": "0.4.0", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/document": "0.4.0", + "@flowgram.ai/form": "0.4.0", + "@flowgram.ai/form-core": "0.4.0", + "@flowgram.ai/group-plugin": "0.4.0", + "@flowgram.ai/history": "0.4.0", + "@flowgram.ai/history-node-plugin": "0.4.0", + "@flowgram.ai/i18n-plugin": "0.4.0", + "@flowgram.ai/materials-plugin": "0.4.0", + "@flowgram.ai/node": "0.4.0", + "@flowgram.ai/node-core-plugin": "0.4.0", + "@flowgram.ai/node-variable-plugin": "0.4.0", + "@flowgram.ai/playground-react": "0.4.0", + "@flowgram.ai/redux-devtool-plugin": "0.4.0", + "@flowgram.ai/renderer": "0.4.0", + "@flowgram.ai/shortcuts-plugin": "0.4.0", + "@flowgram.ai/utils": "0.4.0", + "@flowgram.ai/variable-plugin": "0.4.0", "inversify": "^6.0.1", "reflect-metadata": "~0.2.2" }, @@ -2544,13 +2544,13 @@ } }, "node_modules/@flowgram.ai/fixed-drag-plugin": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/fixed-drag-plugin/-/fixed-drag-plugin-0.3.5.tgz", - "integrity": "sha512-sytXPYyiNU6RpLKsTPeXd83BWQjQaIrgQRGKuSoTYI9LUe8fDKz558xE/kJaLOvoyfeQwvpKj1kp03LAZ63+zA==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/fixed-drag-plugin/-/fixed-drag-plugin-0.4.0.tgz", + "integrity": "sha512-Wl1CPlY4vTDFU5FBdyVcSWh+WpKTSk21PztErmqKseFgkSJAFDfKFyNIy/Fq0WzpIJoNLi3Zf6FGCySWk8+d7g==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/document": "0.3.5", - "@flowgram.ai/renderer": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/document": "0.4.0", + "@flowgram.ai/renderer": "0.4.0", "inversify": "^6.0.1", "reflect-metadata": "~0.2.2" }, @@ -2560,29 +2560,29 @@ } }, "node_modules/@flowgram.ai/fixed-history-plugin": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/fixed-history-plugin/-/fixed-history-plugin-0.3.5.tgz", - "integrity": "sha512-V7QTfMykzmVfn0eaq1z7C/ukQnJhP8kr2qGUK/fxjOqhacgcBYW1+Pc+XsQWHQbo5dADr2kTlr42dgqjqY2Grg==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/fixed-history-plugin/-/fixed-history-plugin-0.4.0.tgz", + "integrity": "sha512-1QMe3A28loPHOEurOW+vhSXffVJ1D4uZHp/g46IlOCxRY4WHvL4np9PegAd65rvG1D+Wud237k+AIJAM8/LbvQ==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/document": "0.3.5", - "@flowgram.ai/form-core": "0.3.5", - "@flowgram.ai/history": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/document": "0.4.0", + "@flowgram.ai/form-core": "0.4.0", + "@flowgram.ai/history": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "inversify": "^6.0.1", "lodash": "^4.17.21", "reflect-metadata": "~0.2.2" } }, "node_modules/@flowgram.ai/fixed-layout-core": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/fixed-layout-core/-/fixed-layout-core-0.3.5.tgz", - "integrity": "sha512-ojhQaIOweR5QH7gsc7fWavb5aF270jHu0m0RIjLtc7dFCUMiadlU+tkm2sP7Y1BM76aPXsG3hnjHUk7wM2sU4A==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/fixed-layout-core/-/fixed-layout-core-0.4.0.tgz", + "integrity": "sha512-sc6IQHlYIq6xpiQvEved8e1OUaIU+hkCUQPWRt8t6AvmCzahjLcHso34p7IdOoDT0bbXHWTUzLQk6bgRF+wyEQ==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/document": "0.3.5", - "@flowgram.ai/renderer": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/document": "0.4.0", + "@flowgram.ai/renderer": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "inversify": "^6.0.1", "lodash": "^4.17.21", "reflect-metadata": "~0.2.2" @@ -2593,19 +2593,19 @@ } }, "node_modules/@flowgram.ai/fixed-layout-editor": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/fixed-layout-editor/-/fixed-layout-editor-0.3.5.tgz", - "integrity": "sha512-bwMChriT0e0msP/6UGS6gAH+r3wqPDI2PObV01c/+JxkPjTHK/B+e1WoI5xZriQB7WQ0Y0/v2pxxrYmiGP3rvw==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/fixed-layout-editor/-/fixed-layout-editor-0.4.0.tgz", + "integrity": "sha512-sxq9JM0GwB4EVRP8B2f84gZXpUgFoswcVgMBt+CdxecZV5gJzURmKl6Xe6Fh3y+7tOoWEo0e5/mK4MElLSeYeA==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/editor": "0.3.5", - "@flowgram.ai/fixed-drag-plugin": "0.3.5", - "@flowgram.ai/fixed-history-plugin": "0.3.5", - "@flowgram.ai/fixed-layout-core": "0.3.5", - "@flowgram.ai/history": "0.3.5", - "@flowgram.ai/reactive": "0.3.5", - "@flowgram.ai/select-box-plugin": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/editor": "0.4.0", + "@flowgram.ai/fixed-drag-plugin": "0.4.0", + "@flowgram.ai/fixed-history-plugin": "0.4.0", + "@flowgram.ai/fixed-layout-core": "0.4.0", + "@flowgram.ai/history": "0.4.0", + "@flowgram.ai/reactive": "0.4.0", + "@flowgram.ai/select-box-plugin": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "inversify": "^6.0.1", "reflect-metadata": "~0.2.2" }, @@ -2615,12 +2615,12 @@ } }, "node_modules/@flowgram.ai/form": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/form/-/form-0.3.5.tgz", - "integrity": "sha512-PmoG4k+aAdzt0v2aZKb3A/ArpUe0NKsACU68185PyXfGl8AU+GWP8ujdx3txKm2VTVEDjOFWsLSMh1/3RkRgkA==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/form/-/form-0.4.0.tgz", + "integrity": "sha512-DU4n7RY4v+WYwPHd0IIFUS7c/kqWBF+/G7p4ocWOP/PR+SCo1vwNHDK9NHWYxXmQdt2JWrM9l55HKGHUiOPYdA==", "dependencies": { - "@flowgram.ai/reactive": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/reactive": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "fast-equals": "^2.0.0", "lodash": "^4.17.21", "nanoid": "^4.0.2" @@ -2631,13 +2631,13 @@ } }, "node_modules/@flowgram.ai/form-core": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/form-core/-/form-core-0.3.5.tgz", - "integrity": "sha512-fgzjJT9cUIYcclRSbMfoAHS9hqN8AvbvaZcx2EXlQDVpGAZsrTrMe4WmPe1mQ3eCakj9oj4VKqu8kipfT6spSg==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/form-core/-/form-core-0.4.0.tgz", + "integrity": "sha512-xoS0T5PGkwtvQfs9Xf+7rLVmIZzOkeMBWHN+ERRUQMFRjquZnkEeBrkIB2ZBG0i/Uh0dq0Lq8b+vjsRFpemj+Q==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/document": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/document": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "inversify": "^6.0.1", "lodash": "^4.17.21", "reflect-metadata": "~0.2.2" @@ -2665,16 +2665,16 @@ } }, "node_modules/@flowgram.ai/free-layout-core": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/free-layout-core/-/free-layout-core-0.3.5.tgz", - "integrity": "sha512-ZyjSFe+/Wg8wYXtxylMfLTuapOwSucHIXmJXD4Oz6WJQk39mJ6Cfds8pbp9aPDpFYfhRdQJOLAb9nUOh5+QnBw==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/free-layout-core/-/free-layout-core-0.4.0.tgz", + "integrity": "sha512-wPa2lB32rHI6SS7BWHpE7OU6mk0hlpcpbW50d5oBekAMHu1ygOLH2oKEDaWAae2wJmdfQqMrTMWmBgXos5epLw==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/document": "0.3.5", - "@flowgram.ai/form-core": "0.3.5", - "@flowgram.ai/node": "0.3.5", - "@flowgram.ai/reactive": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/document": "0.4.0", + "@flowgram.ai/form-core": "0.4.0", + "@flowgram.ai/node": "0.4.0", + "@flowgram.ai/reactive": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "inversify": "^6.0.1", "lodash-es": "^4.17.21", "nanoid": "^4.0.2", @@ -2703,14 +2703,14 @@ } }, "node_modules/@flowgram.ai/group-plugin": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/group-plugin/-/group-plugin-0.3.5.tgz", - "integrity": "sha512-FRic+LX9NjjW18sd7A/lV1v+wBJKwDnFUranye1nK2f9znN51k1GLm/0W/Ch0N0pZXlV8clCHYaMN9TIIZMHtQ==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/group-plugin/-/group-plugin-0.4.0.tgz", + "integrity": "sha512-Sqcf4uTINRiQ/k9uBcI4WjAoIpHa3XH+M+LgUzRNYDTLN90iSk7YN3/7+hXkqTyg4cgQt+pyv8rG7HRneEm2jA==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/document": "0.3.5", - "@flowgram.ai/renderer": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/document": "0.4.0", + "@flowgram.ai/renderer": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "inversify": "^6.0.1", "reflect-metadata": "~0.2.2" }, @@ -2720,12 +2720,12 @@ } }, "node_modules/@flowgram.ai/history": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/history/-/history-0.3.5.tgz", - "integrity": "sha512-G0vU9vNJdpbHSnBx8GG8VOyTfD5Qy4L4j8S577SHl3RxhIIh7vr1tm5REEzzhxJWsvOID+v1+73XhSTzTmVi8w==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/history/-/history-0.4.0.tgz", + "integrity": "sha512-XTKkakg5X+64fnBCSSYSaY276z3F11DWNHcvSD7aKG+DcM4f219dFh4Uy78ceHlEVB1Z7IqWJexCtDVOtoIl1A==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "inversify": "^6.0.1", "lodash": "^4.17.21", "nanoid": "^4.0.2", @@ -2733,17 +2733,17 @@ } }, "node_modules/@flowgram.ai/history-node-plugin": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/history-node-plugin/-/history-node-plugin-0.3.5.tgz", - "integrity": "sha512-oeoy0DJdP0dAfcOLRnKPB/r6wJcnd2rtkVwVrrpQCr3H2QF7IBNKmQFf+ueXR6e8onbNoS0b7stYqCRIFeeCaQ==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/history-node-plugin/-/history-node-plugin-0.4.0.tgz", + "integrity": "sha512-rsFmJJGBDg2ZYLFD8SKYoQ1YpJ+apNGsqmc4krGefDJXW8QAFEm5KKBt5Yr+PvxNgz1QmGB+sguPHqnMoSPcCA==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/document": "0.3.5", - "@flowgram.ai/form": "0.3.5", - "@flowgram.ai/form-core": "0.3.5", - "@flowgram.ai/history": "0.3.5", - "@flowgram.ai/node": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/document": "0.4.0", + "@flowgram.ai/form": "0.4.0", + "@flowgram.ai/form-core": "0.4.0", + "@flowgram.ai/history": "0.4.0", + "@flowgram.ai/node": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "inversify": "^6.0.1", "lodash": "^4.17.21", "reflect-metadata": "~0.2.2" @@ -2772,32 +2772,32 @@ } }, "node_modules/@flowgram.ai/i18n": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/i18n/-/i18n-0.3.5.tgz", - "integrity": "sha512-5G4wMZ3QjCIEeU2g0Cqa+vjvEoVZxs/hFx/9AV1HusBBDjeV3LOod8AM50VwIDbL/nGjLHQo2e7Nf6WBMOj86Q==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/i18n/-/i18n-0.4.0.tgz", + "integrity": "sha512-TMSeRy9Ds5Ha5qorkICPRr1hGEiGPx7UwEXrR4JF7bYsSbMAGZ74BXdHt8dxsaFLgPamtNAvvH6ifJP/6V+wLg==", "dependencies": { - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/utils": "0.4.0", "i18n-js": "^4.5.1" } }, "node_modules/@flowgram.ai/i18n-plugin": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/i18n-plugin/-/i18n-plugin-0.3.5.tgz", - "integrity": "sha512-rm+nQ5r7QAWSEjEyO+cT9raIVOGDa5QIdezI+NiCco27VFGsRY28dQXROd0jf/mDK91IfHrSec5nVks5bE9ZMg==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/i18n-plugin/-/i18n-plugin-0.4.0.tgz", + "integrity": "sha512-aeQt/c5LbsdL5OVsUQxVbUu40K0M1sUalnqrgY9iaGYrr0mYlXPGL0iXp44MBJPgxsbfLfo7HGbP7FEq3U8CbA==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/i18n": "0.3.5" + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/i18n": "0.4.0" } }, "node_modules/@flowgram.ai/materials-plugin": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/materials-plugin/-/materials-plugin-0.3.5.tgz", - "integrity": "sha512-ZORaEEDBPKXbTD9LIhPX8PfPfeIYiyJsPkwKm+wtSEoKbP7G+tahSMTVPDwLiIeWbQ93lEZK1/JK2XpL6rA19Q==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/materials-plugin/-/materials-plugin-0.4.0.tgz", + "integrity": "sha512-TYkWIu/iZ2aQDfXQWdRxEvRE5DHQonMlB3t9YshdAvyrg7N5sCVTjrw+3mqgm5tX8NoCEAQ4YDDQ7lEY5+wjrA==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/form-core": "0.3.5", - "@flowgram.ai/renderer": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/form-core": "0.4.0", + "@flowgram.ai/renderer": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "inversify": "^6.0.1", "reflect-metadata": "~0.2.2" }, @@ -2808,14 +2808,14 @@ } }, "node_modules/@flowgram.ai/minimap-plugin": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/minimap-plugin/-/minimap-plugin-0.3.5.tgz", - "integrity": "sha512-+mQx4SoaoA0EghPRmBxqIK5jbHQrzswfg9CQ3kqk5Yu5rhPTpmr+IaHbRK6FWpuehJZNxkjcUKAY705rtFIZcA==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/minimap-plugin/-/minimap-plugin-0.4.0.tgz", + "integrity": "sha512-0icvzo0hWBQksLZO9Fs2F/wbRoox14Q3gHkIjaCsPIkLJi9YFSYxfQcmxdeq6Jm6p6mACiBrK7pfWKxcDl3ZoA==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/document": "0.3.5", - "@flowgram.ai/renderer": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/document": "0.4.0", + "@flowgram.ai/renderer": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "inversify": "^6.0.1", "lodash": "^4.17.21", "reflect-metadata": "~0.2.2" @@ -2827,15 +2827,15 @@ } }, "node_modules/@flowgram.ai/node": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/node/-/node-0.3.5.tgz", - "integrity": "sha512-OQvcuMH3T+l9RCXfAt4tWX6tijXtCp/Kkp/pcoNFfJ7o1abfRHMKEE66i51TPn89MYrIjRxH0GY+qcm9UOuiQQ==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/node/-/node-0.4.0.tgz", + "integrity": "sha512-y0GK95LKtZelLicyLO1kkYtpSij2qJVsewAJ4ASSqAKrB3Ukqx7Q5hM66p8nXGLcsE/0r2o2tmBiWf7Pk5u6lQ==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/document": "0.3.5", - "@flowgram.ai/form": "0.3.5", - "@flowgram.ai/form-core": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/document": "0.4.0", + "@flowgram.ai/form": "0.4.0", + "@flowgram.ai/form-core": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "inversify": "^6.0.1", "lodash": "^4.17.21", "nanoid": "^4.0.2", @@ -2847,16 +2847,16 @@ } }, "node_modules/@flowgram.ai/node-core-plugin": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/node-core-plugin/-/node-core-plugin-0.3.5.tgz", - "integrity": "sha512-SdWDx2ghcRbb6imacoAi+j/tZIVPcLt7FGYBg0FpXH2r0jVioXjq1BRq11GpvQqlCyDuE/J+IP21NWi1DSwKcA==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/node-core-plugin/-/node-core-plugin-0.4.0.tgz", + "integrity": "sha512-9HSrjvGQRu3k/IzmXz06XrerDuTZ8ZqDfMhdFXmV5eXm0aLhUH1SqDVcTDOT1d0lyoDjZKmBShgrBGG7ECDhMQ==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/document": "0.3.5", - "@flowgram.ai/form-core": "0.3.5", - "@flowgram.ai/node": "0.3.5", - "@flowgram.ai/renderer": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/document": "0.4.0", + "@flowgram.ai/form-core": "0.4.0", + "@flowgram.ai/node": "0.4.0", + "@flowgram.ai/renderer": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "inversify": "^6.0.1", "reflect-metadata": "~0.2.2" }, @@ -2867,16 +2867,16 @@ } }, "node_modules/@flowgram.ai/node-variable-plugin": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/node-variable-plugin/-/node-variable-plugin-0.3.5.tgz", - "integrity": "sha512-uZXyuBQDC9GHnJsx8w+aVM3Eb9+DNNH0qe1rZzS5MrrRG7A85t/OsGPL98p2kZJVU6Q6N+38REchsV9KdYot+Q==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/node-variable-plugin/-/node-variable-plugin-0.4.0.tgz", + "integrity": "sha512-2Am/R0xRxhG4TDRgcTAMwp0yRI7SvK2vRgFMvEe6mjmnAZJooD19scyPkt7/TYlFmdCNGLasgFDhF7ORZ8qUtg==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/document": "0.3.5", - "@flowgram.ai/form-core": "0.3.5", - "@flowgram.ai/node": "0.3.5", - "@flowgram.ai/utils": "0.3.5", - "@flowgram.ai/variable-plugin": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/document": "0.4.0", + "@flowgram.ai/form-core": "0.4.0", + "@flowgram.ai/node": "0.4.0", + "@flowgram.ai/utils": "0.4.0", + "@flowgram.ai/variable-plugin": "0.4.0", "inversify": "^6.0.1", "lodash": "^4.17.21", "reflect-metadata": "~0.2.2" @@ -2905,14 +2905,14 @@ } }, "node_modules/@flowgram.ai/playground-react": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/playground-react/-/playground-react-0.3.5.tgz", - "integrity": "sha512-zBcHDS7hQ58F4o3yw0eGNv3U2qcp2qvdRJmHD0oyyCCfygBk1iqycqB8mK0J0CaferPIr7xf7CW4QAsJ/EAvbQ==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/playground-react/-/playground-react-0.4.0.tgz", + "integrity": "sha512-8B6vfIRdPcfAj8WbYBeIC+PJS34QH44BF5Xfux21KqhcqYjyjn+S46KYo6WuGi4MMIx8MUheSivyKc4kKbhW+Q==", "dependencies": { - "@flowgram.ai/background-plugin": "0.3.5", - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/shortcuts-plugin": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/background-plugin": "0.4.0", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/shortcuts-plugin": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "inversify": "^6.0.1", "reflect-metadata": "~0.2.2" }, @@ -2922,11 +2922,11 @@ } }, "node_modules/@flowgram.ai/reactive": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/reactive/-/reactive-0.3.5.tgz", - "integrity": "sha512-qzb7bB7SXk2JkikaCRxzKAs5rvSJqAfCCSwgbXSo3/1r4XsARuGYE0iyoobl0Fa4gD9kUk1FSv7htsxTqja9SQ==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/reactive/-/reactive-0.4.0.tgz", + "integrity": "sha512-wdMQCyEYS+hCHWo3zWEmUjJl1lJylf4oVbn/f6LTRTqZPSNqL02k1o5xVHo/L43GhbhRZzyK2/JCaGpowzF4Kg==", "dependencies": { - "@flowgram.ai/utils": "0.3.5" + "@flowgram.ai/utils": "0.4.0" }, "peerDependencies": { "react": ">=16.8", @@ -2934,12 +2934,12 @@ } }, "node_modules/@flowgram.ai/redux-devtool-plugin": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/redux-devtool-plugin/-/redux-devtool-plugin-0.3.5.tgz", - "integrity": "sha512-3mLlbz+RI5mPKVXLwqH2tT4L9ngmMZlq2Z4zmMl/F83JSOi1PCdYlD6J4FHG2jRBES7iI37VTFDxiJoHbP9tFw==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/redux-devtool-plugin/-/redux-devtool-plugin-0.4.0.tgz", + "integrity": "sha512-EDNpr6fLXT/OnoUkjanv2Cs0vzU93NLXtUMxYa1JWwm3mjXGZIiVeIve1FGfarMzJcnky3xv3CjhzZnLxd0rkg==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/variable-core": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/variable-core": "0.4.0", "inversify": "^6.0.1", "reflect-metadata": "~0.2.2", "styled-components": "^5" @@ -2994,14 +2994,14 @@ } }, "node_modules/@flowgram.ai/renderer": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/renderer/-/renderer-0.3.5.tgz", - "integrity": "sha512-YheWVjZ1CUIUfjfQwf7G/4jeyWsYWKG9+4zJW+co5U8isNAnVH2l+8Ahibx5fOIEnmzerOVRN73dKSSetwDdPg==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/renderer/-/renderer-0.4.0.tgz", + "integrity": "sha512-Qhpsh6tE8x+9ZTo1/rrCakaPIHgKCcX/8QE1ovHxHH9gRsxknihPHXA6t1sKRE0vyXcS6ZT5MxJ2zoAAY/vYjA==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/document": "0.3.5", - "@flowgram.ai/i18n": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/document": "0.4.0", + "@flowgram.ai/i18n": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "inversify": "^6.0.1", "lodash": "^4.17.21", "reflect-metadata": "~0.2.2" @@ -3012,12 +3012,12 @@ } }, "node_modules/@flowgram.ai/select-box-plugin": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/select-box-plugin/-/select-box-plugin-0.3.5.tgz", - "integrity": "sha512-8I1+KoE9LQaDIfte/AyC4SwB9pcmb/HcQQNKklZbSPR8He3Fu4dLrTcquLO6dkRx/aF6dvugT3/78ise3mijoQ==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/select-box-plugin/-/select-box-plugin-0.4.0.tgz", + "integrity": "sha512-p9donmLCrw1JSo1gLKetzBJvE6huIgXI+fVjkHzR4/WKACDXQ7IsILW63ZNprfHGvvhaLanKWhAoPTabEfsWEg==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/renderer": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/renderer": "0.4.0", "inversify": "^6.0.1", "reflect-metadata": "~0.2.2" }, @@ -3027,13 +3027,13 @@ } }, "node_modules/@flowgram.ai/shortcuts-plugin": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/shortcuts-plugin/-/shortcuts-plugin-0.3.5.tgz", - "integrity": "sha512-68RV3LAgsZxeHvUpHCQpa4k80J06D9zpRgCjXfCpyBqUkXfMqp/uvv3pM/LBynl+4OyLzbFCxt0dsXDQP6mjNw==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/shortcuts-plugin/-/shortcuts-plugin-0.4.0.tgz", + "integrity": "sha512-paU8n4/ZW1qHK3SIJkFXdeD/gcwkQmcJaJOhTIsfRAmKftF3TJ/YBunld53vAaHnvZ7nxHzrcofg1oYxWpPCmQ==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/document": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/document": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "inversify": "^6.0.1", "reflect-metadata": "~0.2.2", "styled-components": "^5" @@ -3088,9 +3088,9 @@ } }, "node_modules/@flowgram.ai/utils": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/utils/-/utils-0.3.5.tgz", - "integrity": "sha512-t8KZ7c0+evAqo0e+wE3Kw58UGRkM0+DSit+X0wqerbVZe0K3j6fIIU/C0xuiVxhvxYAyCHpbNaJR0REE6jCcgg==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-WqfXPJCD3wElHBMHfPd8PjKWAZ7WYDpC/l74oCNMttSWGCQbgnnNUTu+Iux6Ifv33wn75tFnh+XbUEnnHPv8HA==", "dependencies": { "clsx": "^1.1.1", "inversify": "^6.0.1", @@ -3128,12 +3128,12 @@ } }, "node_modules/@flowgram.ai/variable-core": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/variable-core/-/variable-core-0.3.5.tgz", - "integrity": "sha512-A+FwAF3uFOnULsnRz6UeVofax3fR9nIY4f+9f2td1+SEN/sCrI15vmsb9rWynC2OfSdx3k+IMzuQrpP/BwGVPw==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/variable-core/-/variable-core-0.4.0.tgz", + "integrity": "sha512-LcjZw1oTibottQpq/C5+FtYAJz/GVZg1nQ0p+f2v91LFqm7LyTO0sVISDqtUbt++OWeGWdolMhhpFXgVgOw26Q==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/utils": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/utils": "0.4.0", "fast-equals": "^2.0.0", "inversify": "^6.0.1", "lodash": "^4.17.21", @@ -3164,15 +3164,15 @@ } }, "node_modules/@flowgram.ai/variable-layout": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/variable-layout/-/variable-layout-0.3.5.tgz", - "integrity": "sha512-ah0J5Jzj0nDHNMdjMrZ7VJtxi5DkVgolNhLjN6HyoC4JyxRTMqDSwT1JQI09pLoxqhC+LuEZ4D36eJqAY883uw==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/variable-layout/-/variable-layout-0.4.0.tgz", + "integrity": "sha512-TIZvr7hzzt5B/m0YAHhtLQIZQ87tXbzqegQv82n0oTjOWMXd9Mis/4GWkoQC94BsPBoDPPzYG45WZ3QDIulBng==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/document": "0.3.5", - "@flowgram.ai/free-layout-core": "0.3.5", - "@flowgram.ai/utils": "0.3.5", - "@flowgram.ai/variable-core": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/document": "0.4.0", + "@flowgram.ai/free-layout-core": "0.4.0", + "@flowgram.ai/utils": "0.4.0", + "@flowgram.ai/variable-core": "0.4.0", "inversify": "^6.0.1", "reflect-metadata": "~0.2.2", "styled-components": "^5" @@ -3227,16 +3227,16 @@ } }, "node_modules/@flowgram.ai/variable-plugin": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/@flowgram.ai/variable-plugin/-/variable-plugin-0.3.5.tgz", - "integrity": "sha512-Y7kgLgV/jZS/u9BfcstmxxHYi9NbGZqr2jsBXkOiV/+eYsSaVZW11YXZaC0ZNetl5/SVR68zbvzqjJV6nKIIHg==", + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@flowgram.ai/variable-plugin/-/variable-plugin-0.4.0.tgz", + "integrity": "sha512-N46yICT7oGHjZ0T821xOs+hlwW9/pdUK7qNXbSLSZL7gX4HoXCREQSKWVtqfpfMkwTnOeIKQ2VudN7gzXrrVMw==", "dependencies": { - "@flowgram.ai/core": "0.3.5", - "@flowgram.ai/document": "0.3.5", - "@flowgram.ai/free-layout-core": "0.3.5", - "@flowgram.ai/utils": "0.3.5", - "@flowgram.ai/variable-core": "0.3.5", - "@flowgram.ai/variable-layout": "0.3.5", + "@flowgram.ai/core": "0.4.0", + "@flowgram.ai/document": "0.4.0", + "@flowgram.ai/free-layout-core": "0.4.0", + "@flowgram.ai/utils": "0.4.0", + "@flowgram.ai/variable-core": "0.4.0", + "@flowgram.ai/variable-layout": "0.4.0", "inversify": "^6.0.1", "reflect-metadata": "~0.2.2", "styled-components": "^5" @@ -3390,6 +3390,16 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -3987,24 +3997,24 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.1.11.tgz", - "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.1.12.tgz", + "integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==", "dev": true, "dependencies": { - "@ampproject/remapping": "^2.3.0", - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.11" + "tailwindcss": "4.1.12" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.1.11.tgz", - "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.1.12.tgz", + "integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -4015,24 +4025,24 @@ "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-x64": "4.1.11", - "@tailwindcss/oxide-freebsd-x64": "4.1.11", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-x64-musl": "4.1.11", - "@tailwindcss/oxide-wasm32-wasi": "4.1.11", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" + "@tailwindcss/oxide-android-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-x64": "4.1.12", + "@tailwindcss/oxide-freebsd-x64": "4.1.12", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.12", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-x64-musl": "4.1.12", + "@tailwindcss/oxide-wasm32-wasi": "4.1.12", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.12" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", - "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz", + "integrity": "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==", "cpu": [ "arm64" ], @@ -4046,9 +4056,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", - "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz", + "integrity": "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==", "cpu": [ "arm64" ], @@ -4062,9 +4072,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", - "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz", + "integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==", "cpu": [ "x64" ], @@ -4078,9 +4088,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", - "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz", + "integrity": "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==", "cpu": [ "x64" ], @@ -4094,9 +4104,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", - "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz", + "integrity": "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==", "cpu": [ "arm" ], @@ -4110,9 +4120,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", - "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz", + "integrity": "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==", "cpu": [ "arm64" ], @@ -4126,9 +4136,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", - "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz", + "integrity": "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==", "cpu": [ "arm64" ], @@ -4142,9 +4152,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", - "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz", + "integrity": "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==", "cpu": [ "x64" ], @@ -4158,9 +4168,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", - "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz", + "integrity": "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==", "cpu": [ "x64" ], @@ -4174,9 +4184,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", - "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz", + "integrity": "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -4191,11 +4201,11 @@ "dev": true, "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.11", - "@tybys/wasm-util": "^0.9.0", + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@emnapi/wasi-threads": "^1.0.4", + "@napi-rs/wasm-runtime": "^0.2.12", + "@tybys/wasm-util": "^0.10.0", "tslib": "^2.8.0" }, "engines": { @@ -4203,9 +4213,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", - "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz", + "integrity": "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==", "cpu": [ "arm64" ], @@ -4219,9 +4229,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", - "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz", + "integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==", "cpu": [ "x64" ], @@ -4235,27 +4245,27 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/postcss/-/postcss-4.1.11.tgz", - "integrity": "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==", + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/@tailwindcss/postcss/-/postcss-4.1.12.tgz", + "integrity": "sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ==", "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.11", - "@tailwindcss/oxide": "4.1.11", + "@tailwindcss/node": "4.1.12", + "@tailwindcss/oxide": "4.1.12", "postcss": "^8.4.41", - "tailwindcss": "4.1.11" + "tailwindcss": "4.1.12" } }, "node_modules/@tailwindcss/vite": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/vite/-/vite-4.1.11.tgz", - "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==", + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/@tailwindcss/vite/-/vite-4.1.12.tgz", + "integrity": "sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ==", "dev": true, "dependencies": { - "@tailwindcss/node": "4.1.11", - "@tailwindcss/oxide": "4.1.11", - "tailwindcss": "4.1.11" + "@tailwindcss/node": "4.1.12", + "@tailwindcss/oxide": "4.1.12", + "tailwindcss": "4.1.12" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" @@ -4643,9 +4653,9 @@ } }, "node_modules/@uiw/codemirror-extensions-basic-setup": { - "version": "4.24.2", - "resolved": "https://registry.npmmirror.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.24.2.tgz", - "integrity": "sha512-wW/gjLRvVUeYyhdh2TApn25cvdcR+Rhg6R/j3eTOvXQzU1HNzNYCVH4YKVIfgtfdM/Xs+N8fkk+rbr1YvBppCg==", + "version": "4.25.1", + "resolved": "https://registry.npmmirror.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.1.tgz", + "integrity": "sha512-zxgA2QkvP3ZDKxTBc9UltNFTrSeFezGXcZtZj6qcsBxiMzowoEMP5mVwXcKjpzldpZVRuY+JCC+RsekEgid4vg==", "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", @@ -4669,20 +4679,20 @@ } }, "node_modules/@uiw/codemirror-theme-vscode": { - "version": "4.24.2", - "resolved": "https://registry.npmmirror.com/@uiw/codemirror-theme-vscode/-/codemirror-theme-vscode-4.24.2.tgz", - "integrity": "sha512-5rLvAMKNjqtHSy3BZnI1Q4ZLqUDDJC5fKOp8Mv5VjPqEmAxEAIGrIXBnhAFlRg5VjHbHEMtM1O5ugL3IeHoBdQ==", + "version": "4.25.1", + "resolved": "https://registry.npmmirror.com/@uiw/codemirror-theme-vscode/-/codemirror-theme-vscode-4.25.1.tgz", + "integrity": "sha512-vZXvErVDvHLFWCo78Y22R2grisGkWJFFKkdOu9+tcRjWfOVb7K3fDu/H4fDr6JujLTaL+LY/vP/LZYAGJXVqgA==", "dependencies": { - "@uiw/codemirror-themes": "4.24.2" + "@uiw/codemirror-themes": "4.25.1" }, "funding": { "url": "https://jaywcjlove.github.io/#/sponsor" } }, "node_modules/@uiw/codemirror-themes": { - "version": "4.24.2", - "resolved": "https://registry.npmmirror.com/@uiw/codemirror-themes/-/codemirror-themes-4.24.2.tgz", - "integrity": "sha512-0fQusJE08APL+1WM0xYqvampVM2RpABHjvZbcJWHdRc+teZbrP8907VW1ZX1tO/7/xgRRt4Fc3RHPCcTs7b0NQ==", + "version": "4.25.1", + "resolved": "https://registry.npmmirror.com/@uiw/codemirror-themes/-/codemirror-themes-4.25.1.tgz", + "integrity": "sha512-6o8tQ8bdq14RuVFpZ7l9u8KnuPq824uG3U1VV933Uhv8mfaxaoaOQSjv6T2bQUPhjH6ZlEu5+tAMkOfIL21eIQ==", "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -4698,15 +4708,15 @@ } }, "node_modules/@uiw/react-codemirror": { - "version": "4.24.2", - "resolved": "https://registry.npmmirror.com/@uiw/react-codemirror/-/react-codemirror-4.24.2.tgz", - "integrity": "sha512-kp7DhTq4RR+M2zJBQBrHn1dIkBrtOskcwJX4vVsKGByReOvfMrhqRkGTxYMRDTX6x75EG2mvBJPDKYcUQcHWBw==", + "version": "4.25.1", + "resolved": "https://registry.npmmirror.com/@uiw/react-codemirror/-/react-codemirror-4.25.1.tgz", + "integrity": "sha512-eESBKHndoYkaEGlKCwRO4KrnTw1HkWBxVpEeqntoWTpoFEUYxdLWUYmkPBVk4/u8YzVy9g91nFfIRpqe5LjApg==", "dependencies": { "@babel/runtime": "^7.18.6", "@codemirror/commands": "^6.1.0", "@codemirror/state": "^6.1.1", "@codemirror/theme-one-dark": "^6.0.0", - "@uiw/codemirror-extensions-basic-setup": "4.24.2", + "@uiw/codemirror-extensions-basic-setup": "4.25.1", "codemirror": "^6.0.0" }, "funding": { @@ -5899,9 +5909,9 @@ "integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==" }, "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "version": "5.18.3", + "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -7049,9 +7059,9 @@ } }, "node_modules/i18next": { - "version": "25.3.4", - "resolved": "https://registry.npmmirror.com/i18next/-/i18next-25.3.4.tgz", - "integrity": "sha512-AHklEYFLiRRxW1Cb6zE9lfnEtYvsydRC8nRS3RSKGX3zCqZ8nLZwMaUsrb80YuccPNv2RNokDL8LkTNnp+6mDw==", + "version": "25.3.6", + "resolved": "https://registry.npmmirror.com/i18next/-/i18next-25.3.6.tgz", + "integrity": "sha512-dThZ0CTCM3sUG/qS0ZtQYZQcUI6DtBN8yBHK+SKEqihPcEYmjVWh/YJ4luic73Iq6Uxhp6q7LJJntRK5+1t7jQ==", "funding": [ { "type": "individual", @@ -9158,9 +9168,9 @@ } }, "node_modules/react-router": { - "version": "7.8.0", - "resolved": "https://registry.npmmirror.com/react-router/-/react-router-7.8.0.tgz", - "integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==", + "version": "7.8.1", + "resolved": "https://registry.npmmirror.com/react-router/-/react-router-7.8.1.tgz", + "integrity": "sha512-5cy/M8DHcG51/KUIka1nfZ2QeylS4PJRs6TT8I4PF5axVsI5JUxp0hC0NZ/AEEj8Vw7xsEoD7L/6FY+zoYaOGA==", "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -9179,11 +9189,11 @@ } }, "node_modules/react-router-dom": { - "version": "7.8.0", - "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.8.0.tgz", - "integrity": "sha512-ntInsnDVnVRdtSu6ODmTQ41cbluak/ENeTif7GBce0L6eztFg6/e1hXAysFQI8X25C8ipKmT9cClbJwxx3Kaqw==", + "version": "7.8.1", + "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.8.1.tgz", + "integrity": "sha512-NkgBCF3sVgCiAWIlSt89GR2PLaksMpoo3HDCorpRfnCEfdtRPLiuTf+CNXvqZMI5SJLZCLpVCvcZrTdtGW64xQ==", "dependencies": { - "react-router": "7.8.0" + "react-router": "7.8.1" }, "engines": { "node": ">=20.0.0" @@ -10004,9 +10014,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.1.11.tgz", - "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.1.12.tgz", + "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==", "dev": true }, "node_modules/tapable": { diff --git a/ui/package.json b/ui/package.json index 78b7ede42..e8dffe533 100644 --- a/ui/package.json +++ b/ui/package.json @@ -11,22 +11,22 @@ "dependencies": { "@codemirror/lang-json": "^6.0.2", "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "^6.11.2", + "@codemirror/language": "^6.11.3", "@codemirror/legacy-modes": "^6.5.1", - "@flowgram.ai/document": "^0.3.5", - "@flowgram.ai/fixed-layout-editor": "^0.3.5", - "@flowgram.ai/minimap-plugin": "^0.3.5", + "@flowgram.ai/document": "^0.4.0", + "@flowgram.ai/fixed-layout-editor": "^0.4.0", + "@flowgram.ai/minimap-plugin": "^0.4.0", "@tabler/icons-react": "^3.34.1", - "@uiw/codemirror-extensions-basic-setup": "^4.24.2", - "@uiw/codemirror-theme-vscode": "^4.24.2", - "@uiw/react-codemirror": "^4.24.2", + "@uiw/codemirror-extensions-basic-setup": "^4.25.1", + "@uiw/codemirror-theme-vscode": "^4.25.1", + "@uiw/react-codemirror": "^4.25.1", "ahooks": "^3.9.0", "antd": "^5.27.0", "antd-zod": "^7.0.0", "clsx": "^2.1.1", "cron-parser": "^5.3.0", "file-saver": "^2.0.5", - "i18next": "^25.3.4", + "i18next": "^25.3.6", "i18next-browser-languagedetector": "^8.2.0", "immer": "^10.1.1", "nanoid": "^5.1.5", @@ -36,15 +36,15 @@ "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1", "react-i18next": "^15.6.1", - "react-router-dom": "^7.8.0", + "react-router-dom": "^7.8.1", "tailwind-merge": "^3.3.1", "zod": "^4.0.17", "zustand": "^5.0.7" }, "devDependencies": { "@eslint/js": "^9.33.0", - "@tailwindcss/postcss": "^4.1.11", - "@tailwindcss/vite": "^4.1.11", + "@tailwindcss/postcss": "^4.1.12", + "@tailwindcss/vite": "^4.1.12", "@types/file-saver": "^2.0.7", "@types/fs-extra": "^11.0.4", "@types/node": "^22.16.5", @@ -63,7 +63,7 @@ "eslint-plugin-react-refresh": "^0.4.20", "fs-extra": "^11.3.1", "prettier": "^3.6.2", - "tailwindcss": "^4.1.11", + "tailwindcss": "^4.1.12", "typescript": "^5.8.3", "typescript-eslint": "^8.39.1", "vite": "^6.3.5" diff --git a/ui/src/components/CodeInput.tsx b/ui/src/components/CodeInput.tsx index 90c1fc4a6..9064bf7ed 100644 --- a/ui/src/components/CodeInput.tsx +++ b/ui/src/components/CodeInput.tsx @@ -7,7 +7,7 @@ import { shell } from "@codemirror/legacy-modes/mode/shell"; import { basicSetup } from "@uiw/codemirror-extensions-basic-setup"; import { vscodeDark, vscodeLight } from "@uiw/codemirror-theme-vscode"; import CodeMirror, { type ReactCodeMirrorProps, type ReactCodeMirrorRef } from "@uiw/react-codemirror"; -import { useFocusWithin } from "ahooks"; +import { useFocusWithin, useHover } from "ahooks"; import { theme } from "antd"; import { useBrowserTheme } from "@/hooks"; @@ -24,7 +24,8 @@ const CodeInput = ({ className, style, disabled, language, ...props }: CodeInput const { theme: browserTheme } = useBrowserTheme(); const cmRef = useRef(null); - const isFocusWithin = useFocusWithin(cmRef.current?.editor); + const isFocusing = useFocusWithin(cmRef.current?.editor); + const isHovering = useHover(cmRef.current?.editor); const cmTheme = useMemo(() => { if (browserTheme === "dark") { @@ -66,13 +67,30 @@ const CodeInput = ({ className, style, disabled, language, ...props }: CodeInput return (
diff --git a/ui/src/components/DrawerForm.tsx b/ui/src/components/DrawerForm.tsx index 25fc6cf34..7c2c1a4fe 100644 --- a/ui/src/components/DrawerForm.tsx +++ b/ui/src/components/DrawerForm.tsx @@ -58,7 +58,7 @@ const DrawerForm = = any>({ form: formInst, formPending, formProps, - submit, + submit: submitForm, } = useAntdForm({ form, onSubmit: (values) => { @@ -75,25 +75,22 @@ const DrawerForm = = any>({ const mergedDrawerProps: DrawerProps = { ...drawerProps, closeIcon: false, - afterOpenChange: (open) => { - if (!open && !mergedFormProps.preserve) { - formInst.resetFields(); - } - - drawerProps?.afterOpenChange?.(open); - }, onClose: async (e) => { if (formPending) return; // 关闭 Drawer 时 Promise.reject 阻止关闭 await drawerProps?.onClose?.(e); setOpen(false); + + if (!mergedFormProps.preserve) { + formInst.resetFields(); + } }, }; const handleOkClick = async () => { // 提交表单返回 Promise.reject 时不关闭 Drawer - await submit(); + await submitForm(); setOpen(false); }; diff --git a/ui/src/components/access/AccessEditDrawer.tsx b/ui/src/components/access/AccessEditDrawer.tsx index 08cf1a69b..c091f3144 100644 --- a/ui/src/components/access/AccessEditDrawer.tsx +++ b/ui/src/components/access/AccessEditDrawer.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { startTransition, useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { IconX } from "@tabler/icons-react"; import { useControllableValue } from "ahooks"; @@ -12,6 +12,8 @@ import { getErrMsg } from "@/utils/error"; import AccessForm, { type AccessFormInstance, type AccessFormProps } from "./AccessForm"; export interface AccessEditDrawerProps { + afterClose?: () => void; + afterSubmit?: (record: AccessModel) => void; data?: AccessFormProps["initialValues"]; loading?: boolean; mode: AccessFormProps["mode"]; @@ -19,10 +21,9 @@ export interface AccessEditDrawerProps { trigger?: React.ReactNode; usage?: AccessFormProps["usage"]; onOpenChange?: (open: boolean) => void; - afterSubmit?: (record: AccessModel) => void; } -const AccessEditDrawer = ({ mode, data, loading, trigger, usage, afterSubmit, ...props }: AccessEditDrawerProps) => { +const AccessEditDrawer = ({ afterClose, afterSubmit, mode, data, loading, trigger, usage, ...props }: AccessEditDrawerProps) => { const { t } = useTranslation(); const { notification } = App.useApp(); @@ -99,7 +100,8 @@ const AccessEditDrawer = ({ mode, data, loading, trigger, usage, afterSubmit, .. {triggerEl} !open && afterClose?.()} + autoFocus closeIcon={false} destroyOnHidden footer={ @@ -141,29 +143,41 @@ const AccessEditDrawer = ({ mode, data, loading, trigger, usage, afterSubmit, .. ); }; -const useProps = () => { - const [data, setData] = useState(); +const useDrawer = () => { + type DataType = AccessEditDrawerProps["data"]; + const [data, setData] = useState(); const [open, setOpen] = useState(false); - const onOpenChange = (open: boolean) => { + const onOpenChange = useCallback((open: boolean) => { setOpen(open); - - if (!open) { - setData(void 0); - } - }; + }, []); return { - data, - open, - setData, - setOpen, - onOpenChange, + drawerProps: { + afterClose: () => { + startTransition(() => { + if (!open) { + setData(void 0); + } + }); + }, + data, + open, + onOpenChange, + }, + + open: (data: NonNullable) => { + setData(data); + setOpen(true); + }, + close: () => { + setOpen(false); + }, }; }; const _default = Object.assign(AccessEditDrawer, { - useProps, + useDrawer, }); export default _default; diff --git a/ui/src/components/access/AccessFormSSHConfig.tsx b/ui/src/components/access/AccessFormSSHConfig.tsx index 0e9771f8a..0f232011a 100644 --- a/ui/src/components/access/AccessFormSSHConfig.tsx +++ b/ui/src/components/access/AccessFormSSHConfig.tsx @@ -1,6 +1,6 @@ import { useTranslation } from "react-i18next"; import { IconCircleArrowDown, IconCircleArrowUp, IconCircleMinus, IconCirclePlus } from "@tabler/icons-react"; -import { Button, Collapse, Form, type FormInstance, Input, InputNumber, Select } from "antd"; +import { Button, Collapse, Form, type FormInstance, Input, InputNumber, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; @@ -135,13 +135,12 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
- - - {t("access.form.ssh_auth_method.option.none.label")} - - - {t("access.form.ssh_auth_method.option.password.label")} - - - {t("access.form.ssh_auth_method.option.key.label")} - - + ({ + key: s, + label: t(`access.form.ssh_auth_method.option.${s}.label`), + value: s, + }))} + /> diff --git a/ui/src/components/access/AccessSelect.tsx b/ui/src/components/access/AccessSelect.tsx index 239bde584..cf028db9e 100644 --- a/ui/src/components/access/AccessSelect.tsx +++ b/ui/src/components/access/AccessSelect.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useAsyncEffect } from "ahooks"; import { Avatar, Select, type SelectProps, Typography, theme } from "antd"; import { type AccessModel } from "@/domain/access"; @@ -15,9 +16,7 @@ const AccessSelect = ({ onFilter, ...props }: AccessTypeSelectProps) => { const { token: themeToken } = theme.useToken(); const { accesses, loadedAtOnce, fetchAccesses } = useAccessesStore(useZustandShallowSelector(["accesses", "loadedAtOnce", "fetchAccesses"])); - useEffect(() => { - fetchAccesses(false); - }, []); + useAsyncEffect(() => fetchAccesses(false), []); const [options, setOptions] = useState>([]); useEffect(() => { diff --git a/ui/src/components/certificate/CertificateDetailDrawer.tsx b/ui/src/components/certificate/CertificateDetailDrawer.tsx index e050d057a..7a48724bc 100644 --- a/ui/src/components/certificate/CertificateDetailDrawer.tsx +++ b/ui/src/components/certificate/CertificateDetailDrawer.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { startTransition, useCallback, useState } from "react"; import { IconX } from "@tabler/icons-react"; import { useControllableValue } from "ahooks"; import { Button, Drawer, Flex } from "antd"; @@ -9,6 +9,7 @@ import { useTriggerElement } from "@/hooks"; import CertificateDetail from "./CertificateDetail"; export interface CertificateDetailDrawerProps { + afterClose?: () => void; data?: CertificateModel; loading?: boolean; open?: boolean; @@ -16,7 +17,7 @@ export interface CertificateDetailDrawerProps { onOpenChange?: (open: boolean) => void; } -const CertificateDetailDrawer = ({ data, loading, trigger, ...props }: CertificateDetailDrawerProps) => { +const CertificateDetailDrawer = ({ afterClose, data, loading, trigger, ...props }: CertificateDetailDrawerProps) => { const [open, setOpen] = useControllableValue(props, { valuePropName: "open", defaultValuePropName: "defaultOpen", @@ -30,7 +31,8 @@ const CertificateDetailDrawer = ({ data, loading, trigger, ...props }: Certifica {triggerEl} !open && afterClose?.()} + autoFocus closeIcon={false} destroyOnHidden open={open} @@ -60,29 +62,41 @@ const CertificateDetailDrawer = ({ data, loading, trigger, ...props }: Certifica ); }; -const useProps = () => { - const [data, setData] = useState(); +const useDrawer = () => { + type DataType = CertificateDetailDrawerProps["data"]; + const [data, setData] = useState(); const [open, setOpen] = useState(false); - const onOpenChange = (open: boolean) => { + const onOpenChange = useCallback((open: boolean) => { setOpen(open); - - if (!open) { - setData(void 0); - } - }; + }, []); return { - data, - open, - setData, - setOpen, - onOpenChange, + drawerProps: { + afterClose: () => { + startTransition(() => { + if (!open) { + setData(void 0); + } + }); + }, + data, + open, + onOpenChange, + }, + + open: (data: NonNullable) => { + setData(data); + setOpen(true); + }, + close: () => { + setOpen(false); + }, }; }; const _default = Object.assign(CertificateDetailDrawer, { - useProps, + useDrawer, }); export default _default; diff --git a/ui/src/components/provider/DeploymentProviderPicker.tsx b/ui/src/components/provider/DeploymentProviderPicker.tsx index 5e3dddcc7..ae2e5122e 100644 --- a/ui/src/components/provider/DeploymentProviderPicker.tsx +++ b/ui/src/components/provider/DeploymentProviderPicker.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useSize } from "ahooks"; +import { useAsyncEffect, useSize } from "ahooks"; import { Avatar, Card, Checkbox, Empty, Flex, Input, type InputRef, Tabs, Tooltip, Typography } from "antd"; import Show from "@/components/Show"; @@ -23,9 +23,7 @@ const DeploymentProviderPicker = ({ className, style, autoFocus, gap = "middle", const { t } = useTranslation(); const { accesses, fetchAccesses } = useAccessesStore(useZustandShallowSelector(["accesses", "fetchAccesses"])); - useEffect(() => { - fetchAccesses(false); - }, []); + useAsyncEffect(() => fetchAccesses(false), []); const wrapperRef = useRef(null); const wrapperSize = useSize(wrapperRef); diff --git a/ui/src/components/workflow/WorkflowElement.tsx b/ui/src/components/workflow/WorkflowElement.tsx deleted file mode 100644 index 7b1325a21..000000000 --- a/ui/src/components/workflow/WorkflowElement.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { memo, useMemo } from "react"; - -import { type WorkflowNode, WorkflowNodeType } from "@/domain/workflow"; - -import ApplyNode from "./node/ApplyNode"; -import BranchNode from "./node/BranchNode"; -import ConditionNode from "./node/ConditionNode"; -import DeployNode from "./node/DeployNode"; -import EndNode from "./node/EndNode"; -import ExecuteResultBranchNode from "./node/ExecuteResultBranchNode"; -import ExecuteResultNode from "./node/ExecuteResultNode"; -import MonitorNode from "./node/MonitorNode"; -import NotifyNode from "./node/NotifyNode"; -import StartNode from "./node/StartNode"; -import UnknownNode from "./node/UnknownNode"; -import UploadNode from "./node/UploadNode"; - -/** - * - * @deprecated - */ -export interface WorkflowElementProps { - node: WorkflowNode; - disabled?: boolean; - branchId?: string; - branchIndex?: number; -} - -/** - * - * @deprecated - */ -const WorkflowElement = ({ node, disabled, branchId, branchIndex }: WorkflowElementProps) => { - const nodeEl = useMemo(() => { - switch (node.type) { - case WorkflowNodeType.Start: - return ; - - case WorkflowNodeType.Apply: - return ; - - case WorkflowNodeType.Upload: - return ; - - case WorkflowNodeType.Monitor: - return ; - - case WorkflowNodeType.Deploy: - return ; - - case WorkflowNodeType.Notify: - return ; - - case WorkflowNodeType.Branch: - return ; - - case WorkflowNodeType.ExecuteResultBranch: - return ; - - case WorkflowNodeType.ExecuteSuccess: - case WorkflowNodeType.ExecuteFailure: - return ; - - case WorkflowNodeType.Condition: - return ; - - case WorkflowNodeType.End: - return ; - - default: - console.warn(`[certimate] unsupported workflow node type: '${node.type}'`); - return ; - } - }, [node, disabled, branchId, branchIndex]); - - return <>{nodeEl}; -}; - -export default memo(WorkflowElement); diff --git a/ui/src/components/workflow/WorkflowElements.tsx b/ui/src/components/workflow/WorkflowElements.tsx deleted file mode 100644 index c9d64876f..000000000 --- a/ui/src/components/workflow/WorkflowElements.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { useMemo } from "react"; - -import WorkflowElement from "@/components/workflow/WorkflowElement"; -import { WorkflowNodeType, newNode } from "@/domain/workflow"; -import { useZustandShallowSelector } from "@/hooks"; -import { useWorkflowStore } from "@/stores/workflow"; - -/** - * - * @deprecated - */ -export interface WorkflowElementsProps { - className?: string; - style?: React.CSSProperties; - disabled?: boolean; -} - -/** - * - * @deprecated - */ -const WorkflowElements = ({ className, style, disabled }: WorkflowElementsProps) => { - const { workflow } = useWorkflowStore(useZustandShallowSelector(["workflow"])); - - const elements = useMemo(() => { - const root = workflow.draft; - const nodes: JSX.Element[] = []; - - let current = root as typeof root | undefined; - while (current) { - nodes.push(); - current = current.next; - } - - nodes.push(); - - return nodes; - }, [workflow, disabled]); - - return ( -
-
{elements}
-
- ); -}; - -export default WorkflowElements; diff --git a/ui/src/components/workflow/WorkflowElementsContainer.tsx b/ui/src/components/workflow/WorkflowElementsContainer.tsx deleted file mode 100644 index 339315bc2..000000000 --- a/ui/src/components/workflow/WorkflowElementsContainer.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useState } from "react"; -import { IconMaximize, IconMinus, IconPlus } from "@tabler/icons-react"; -import { Button, Card, Typography } from "antd"; - -import WorkflowElements from "@/components/workflow/WorkflowElements"; -import { mergeCls } from "@/utils/css"; - -/** - * - * @deprecated - */ -export interface WorkflowElementsProps { - className?: string; - style?: React.CSSProperties; - disabled?: boolean; -} - -/** - * - * @deprecated - */ -const WorkflowElementsContainer = ({ className, style, disabled }: WorkflowElementsProps) => { - const [scale, setScale] = useState(1); - - const MIN_SCALE = 0.2; - const MAX_SCALE = 2; - const STEP_SCALE = 0.05; - - return ( -
-
-
-
-
- -
-
-
-
- -
-
-
- -
-
-
-
-
-
-
- ); -}; - -export default WorkflowElementsContainer; diff --git a/ui/src/components/workflow/WorkflowRunDetail.tsx b/ui/src/components/workflow/WorkflowRunDetail.tsx index 06fcb5a2e..fff54849a 100644 --- a/ui/src/components/workflow/WorkflowRunDetail.tsx +++ b/ui/src/components/workflow/WorkflowRunDetail.tsx @@ -54,7 +54,7 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin }, { refreshDeps: [runId, runStatus], - pollingInterval: runStatus === WORKFLOW_RUN_STATUSES.PENDING || runStatus === WORKFLOW_RUN_STATUSES.RUNNING ? 3000 : 0, + pollingInterval: runStatus === WORKFLOW_RUN_STATUSES.PENDING || runStatus === WORKFLOW_RUN_STATUSES.PROCESSING ? 3000 : 0, pollingWhenHidden: false, throttleWait: 500, onSuccess: (res) => { diff --git a/ui/src/components/workflow/WorkflowRunDetailDrawer.tsx b/ui/src/components/workflow/WorkflowRunDetailDrawer.tsx index 67982b1d6..4419062cc 100644 --- a/ui/src/components/workflow/WorkflowRunDetailDrawer.tsx +++ b/ui/src/components/workflow/WorkflowRunDetailDrawer.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { startTransition, useCallback, useState } from "react"; import { IconX } from "@tabler/icons-react"; import { useControllableValue } from "ahooks"; import { Button, Drawer, Flex } from "antd"; @@ -10,6 +10,7 @@ import { useTriggerElement } from "@/hooks"; import WorkflowRunDetail from "./WorkflowRunDetail"; export interface WorkflowRunDetailDrawerProps { + afterClose?: () => void; data?: WorkflowRunModel; loading?: boolean; open?: boolean; @@ -17,7 +18,7 @@ export interface WorkflowRunDetailDrawerProps { onOpenChange?: (open: boolean) => void; } -const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowRunDetailDrawerProps) => { +const WorkflowRunDetailDrawer = ({ afterClose, data, loading, trigger, ...props }: WorkflowRunDetailDrawerProps) => { const [open, setOpen] = useControllableValue(props, { valuePropName: "open", defaultValuePropName: "defaultOpen", @@ -31,7 +32,7 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR {triggerEl} !open && afterClose?.()} closeIcon={false} destroyOnHidden open={open} @@ -61,29 +62,41 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR ); }; -const useProps = () => { - const [data, setData] = useState(); +const useDrawer = () => { + type DataType = WorkflowRunDetailDrawerProps["data"]; + const [data, setData] = useState(); const [open, setOpen] = useState(false); - const onOpenChange = (open: boolean) => { + const onOpenChange = useCallback((open: boolean) => { setOpen(open); - - if (!open) { - setData(undefined); - } - }; + }, []); return { - data, - open, - setData, - setOpen, - onOpenChange, + drawerProps: { + afterClose: () => { + startTransition(() => { + if (!open) { + setData(void 0); + } + }); + }, + data, + open, + onOpenChange, + }, + + open: (data: NonNullable) => { + setData(data); + setOpen(true); + }, + close: () => { + setOpen(false); + }, }; }; const _default = Object.assign(WorkflowRunDetailDrawer, { - useProps, + useDrawer, }); export default _default; diff --git a/ui/src/components/workflow/WorkflowStatus.tsx b/ui/src/components/workflow/WorkflowStatus.tsx index 1b4ffcba1..0e5259d37 100644 --- a/ui/src/components/workflow/WorkflowStatus.tsx +++ b/ui/src/components/workflow/WorkflowStatus.tsx @@ -24,7 +24,7 @@ const useColor = (value: WorkflorRunStatusType | string, defaultColor?: string | return themeToken.colorTextSecondary; } break; - case WORKFLOW_RUN_STATUSES.RUNNING: + case WORKFLOW_RUN_STATUSES.PROCESSING: if (defaultColor == null || !defaultColor) { return themeToken.colorInfo; } @@ -73,7 +73,7 @@ const WorkflowStatusIcon = ({ className, style, size = "1.25em", type = "outline {type === "filled" ? : } ); - case WORKFLOW_RUN_STATUSES.RUNNING: + case WORKFLOW_RUN_STATUSES.PROCESSING: return ( @@ -125,7 +125,7 @@ const WorkflowStatus = ({ className, style, children, showIcon = true, type, val switch (value) { case WORKFLOW_RUN_STATUSES.PENDING: - case WORKFLOW_RUN_STATUSES.RUNNING: + case WORKFLOW_RUN_STATUSES.PROCESSING: case WORKFLOW_RUN_STATUSES.SUCCEEDED: case WORKFLOW_RUN_STATUSES.FAILED: case WORKFLOW_RUN_STATUSES.CANCELED: diff --git a/ui/src/components/workflow/designer/Designer.tsx b/ui/src/components/workflow/designer/Designer.tsx index bee18f0a7..8acb7e65b 100644 --- a/ui/src/components/workflow/designer/Designer.tsx +++ b/ui/src/components/workflow/designer/Designer.tsx @@ -1,7 +1,8 @@ -import { forwardRef, useImperativeHandle, useMemo, useRef } from "react"; +import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from "react"; import { ConstantKeys, EditorRenderer, + EditorState, FixedLayoutEditorProvider, type FixedLayoutPluginContext, type FixedLayoutProps, @@ -27,6 +28,8 @@ export interface DesignerProps { children?: React.ReactNode; initialData?: FlowDocumentJSON; readonly?: boolean; + onDocumentChange?: (ctx: FixedLayoutPluginContext) => void; + onNodeChange?: (ctx: FixedLayoutPluginContext, node: FlowNodeEntity) => void; onNodeClick?: (ctx: FixedLayoutPluginContext, node: FlowNodeEntity) => void; } @@ -35,149 +38,181 @@ export interface DesignerInstance extends FixedLayoutPluginContext { validateAllNodes(): Promise; } -const Designer = forwardRef(({ className, style, children, initialData, readonly, onNodeClick }, ref) => { - const { token: themeToken } = theme.useToken(); +const Designer = forwardRef( + ({ className, style, children, initialData, readonly, onDocumentChange, onNodeChange, onNodeClick }, ref) => { + const { token: themeToken } = theme.useToken(); - const flowgramEditorRef = useRef(null); - const flowgramEditorProps = useMemo( - () => ({ - initialData: initialData, + const rendered = useRef(false); - constants: { - [ConstantKeys.BASE_COLOR]: themeToken.colorBorder, - [ConstantKeys.BASE_ACTIVATED_COLOR]: themeToken.colorPrimary, - [ConstantKeys.NODE_SPACING]: 48, - [ConstantKeys.BRANCH_SPACING]: 48, - }, + const flowgramEditorRef = useRef(null); + const flowgramEditorProps = useMemo( + () => ({ + initialData: initialData, - background: { - backgroundColor: themeToken.colorBgContainer, - dotSize: 0, - }, - - playground: { - autoFocus: true, - autoResize: true, - preventGlobalGesture: true, - }, - - selectBox: { - enable: false, - }, - - scroll: { - enableScrollLimit: true, - }, - - readonly: readonly, - - nodeEngine: { - enable: true, - }, - - variableEngine: { - enable: true, - }, - - materials: { - components: getAllElements(), - renderTexts: { - [FlowTextKey.TRY_START_TEXT]: "Try", - [FlowTextKey.TRY_END_TEXT]: "Then", - [FlowTextKey.CATCH_TEXT]: "Catch", + constants: { + [ConstantKeys.BASE_COLOR]: themeToken.colorBorder, + [ConstantKeys.BASE_ACTIVATED_COLOR]: themeToken.colorPrimary, + [ConstantKeys.NODE_SPACING]: 48, + [ConstantKeys.BRANCH_SPACING]: 48, }, - renderDefaultNode: NodeRender, - }, - nodeRegistries: getAllNodeRegistries(), + background: { + backgroundColor: themeToken.colorBgContainer, + dotSize: 0, + }, - getNodeDefaultRegistry(type) { - return { - type, - meta: { - defaultExpanded: true, + playground: { + autoFocus: true, + autoResize: true, + preventGlobalGesture: true, + }, + + selectBox: { + enable: false, + }, + + scroll: { + enableScrollLimit: true, + }, + + readonly: readonly, + + nodeEngine: { + enable: true, + }, + + variableEngine: { + enable: true, + }, + + materials: { + components: getAllElements(), + renderTexts: { + [FlowTextKey.TRY_START_TEXT]: "Try", + [FlowTextKey.TRY_END_TEXT]: "Then", + [FlowTextKey.CATCH_TEXT]: "Catch", }, - formMeta: { - render: () => , - }, - }; - }, + renderDefaultNode: NodeRender, + }, - plugins: () => [ - createMinimapPlugin({ - disableLayer: true, - enableDisplayAllNodes: true, - canvasStyle: { - canvasWidth: 160, - canvasHeight: 160, - }, - }), - ], + nodeRegistries: getAllNodeRegistries(), - onAllLayersRendered: (ctx) => { - // 画布初始化后向下滚动一点,露出可能被 Alert 遮挡的部分 - setTimeout(() => { - ctx.playground.config.scroll({ scrollY: -80 }); - }, 1); - }, - }), - [themeToken, initialData, readonly] - ); + getNodeDefaultRegistry(type) { + return { + type, + meta: { + defaultExpanded: true, + }, + formMeta: { + render: () => , + }, + }; + }, - useImperativeHandle(ref, () => { - return { - get container() { - return flowgramEditorRef.current!.container; - }, - get document() { - return flowgramEditorRef.current!.document; - }, - get playground() { - return flowgramEditorRef.current!.playground; - }, - get operation() { - return flowgramEditorRef.current!.operation; - }, - get clipboard() { - return flowgramEditorRef.current!.clipboard; - }, - get selection() { - return flowgramEditorRef.current!.selection; - }, - get history() { - return flowgramEditorRef.current!.history; - }, + plugins: () => [ + createMinimapPlugin({ + disableLayer: true, + enableDisplayAllNodes: true, + canvasStyle: { + canvasWidth: 160, + canvasHeight: 160, + }, + }), + ], - get(identifier) { - return flowgramEditorRef.current!.get(identifier); - }, - getAll(identifier) { - return flowgramEditorRef.current!.getAll(identifier); - }, - validateNode(node) { - if (typeof node === "string") { - node = flowgramEditorRef.current!.document.getNode(node)!; + onInit: (ctx) => { + const maybeMobile = ["android", "ios", "iphone", "ipad", "micromessenger"].some((s) => navigator.userAgent.includes(s)); + if (maybeMobile) { + ctx.playground.editorState.changeState(EditorState.STATE_MOUSE_FRIENDLY_SELECT.id); + } + }, + + onAllLayersRendered: (ctx) => { + rendered.current = true; + + // 画布初始化后向下滚动一点,露出可能被 Alert 遮挡的部分 + setTimeout(() => { + ctx.playground.config.scroll({ scrollY: -80 }); + }, 1); + }, + }), + [themeToken, initialData, readonly, onDocumentChange] + ); + + useEffect(() => { + const d = flowgramEditorRef.current!.document.originTree.onTreeChange(() => { + if (rendered.current) { + onDocumentChange?.(flowgramEditorRef.current!); } + }); - const form = getNodeForm(node); - return form ? form.validate().then((res) => res && !form.state.invalid) : Promise.resolve(true); - }, - validateAllNodes() { - const nodes = flowgramEditorRef.current!.document.getAllNodes(); - const forms = nodes.map((node) => getNodeForm(node)).filter((form) => form != null); - return Promise.allSettled(forms.map((form) => form.validate())).then((res) => forms.every((form, index) => res[index] && !form.state.invalid)); - }, - }; - }); + return () => d.dispose(); + }, [onDocumentChange]); - return ( - - onNodeClick?.(flowgramEditorRef.current!, node) }}> - - {children} - - - ); -}); + useImperativeHandle(ref, () => { + return { + get clipboard() { + return flowgramEditorRef.current!.clipboard; + }, + get container() { + return flowgramEditorRef.current!.container; + }, + get document() { + return flowgramEditorRef.current!.document; + }, + get history() { + return flowgramEditorRef.current!.history; + }, + get operation() { + return flowgramEditorRef.current!.operation; + }, + get playground() { + return flowgramEditorRef.current!.playground; + }, + get selection() { + return flowgramEditorRef.current!.selection; + }, + get tools() { + return flowgramEditorRef.current!.tools; + }, + + get(identifier) { + return flowgramEditorRef.current!.get(identifier); + }, + getAll(identifier) { + return flowgramEditorRef.current!.getAll(identifier); + }, + validateNode(node) { + if (typeof node === "string") { + node = flowgramEditorRef.current!.document.getNode(node)!; + } + + const form = getNodeForm(node); + return form ? form.validate().then((res) => res && !form.state.invalid) : Promise.resolve(true); + }, + validateAllNodes() { + const nodes = flowgramEditorRef.current!.document.getAllNodes(); + const forms = nodes.map((node) => getNodeForm(node)).filter((form) => form != null); + return Promise.allSettled(forms.map((form) => form.validate())).then((res) => forms.every((form, index) => res[index] && !form.state.invalid)); + }, + }; + }); + + return ( + + onDocumentChange?.(flowgramEditorRef.current!), + onNodeChange: (node) => onNodeChange?.(flowgramEditorRef.current!, node), + onNodeClick: (node) => onNodeClick?.(flowgramEditorRef.current!, node), + }} + > + + {children} + + + ); + } +); export default Designer; diff --git a/ui/src/components/workflow/designer/DesignerContext.ts b/ui/src/components/workflow/designer/DesignerContext.ts index 4db5975eb..b0579a187 100644 --- a/ui/src/components/workflow/designer/DesignerContext.ts +++ b/ui/src/components/workflow/designer/DesignerContext.ts @@ -2,10 +2,14 @@ import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; export type DesignerContextType = { + onDocumentChange: () => void; + onNodeChange: (node: FlowNodeEntity) => void; onNodeClick: (node: FlowNodeEntity) => void; }; export const DesignerContext = createContext({ + onDocumentChange: () => {}, + onNodeChange: () => {}, onNodeClick: () => {}, }); diff --git a/ui/src/components/workflow/designer/NodeDrawer.tsx b/ui/src/components/workflow/designer/NodeDrawer.tsx index 0c6e26dfa..c7e6ade74 100644 --- a/ui/src/components/workflow/designer/NodeDrawer.tsx +++ b/ui/src/components/workflow/designer/NodeDrawer.tsx @@ -1,4 +1,4 @@ -import { startTransition, useMemo, useState } from "react"; +import { startTransition, useCallback, useMemo, useState } from "react"; import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; import { useControllableValue } from "ahooks"; @@ -14,6 +14,7 @@ import StartNodeConfigDrawer from "./forms/StartNodeConfigDrawer"; import { NodeType } from "./nodes/typings"; export interface NodeDrawerProps { + afterClose?: () => void; children?: React.ReactNode; loading?: boolean; node?: FlowNodeEntity; @@ -68,31 +69,41 @@ const NodeDrawer = ({ node, trigger, ...props }: NodeDrawerProps) => { ); }; -const useProps = () => { - const [node, setNode] = useState(); +const useDrawer = () => { + type NodeDataType = NodeDrawerProps["node"]; + const [node, setNode] = useState(); const [open, setOpen] = useState(false); - const onOpenChange = (open: boolean) => { + const onOpenChange = useCallback((open: boolean) => { setOpen(open); - - startTransition(() => { - if (!open) { - setNode(void 0); - } - }); - }; + }, []); return { - node, - open, - setNode, - setOpen, - onOpenChange, + drawerProps: { + afterClose: () => { + startTransition(() => { + if (!open) { + setNode(void 0); + } + }); + }, + node, + open, + onOpenChange, + }, + + open: (node: NonNullable) => { + setNode(node); + setOpen(true); + }, + close: () => { + setOpen(false); + }, }; }; const _default = Object.assign(NodeDrawer, { - useProps, + useDrawer, }); export default _default; diff --git a/ui/src/components/workflow/designer/NodeRender.tsx b/ui/src/components/workflow/designer/NodeRender.tsx index e1553678e..9969aa03b 100644 --- a/ui/src/components/workflow/designer/NodeRender.tsx +++ b/ui/src/components/workflow/designer/NodeRender.tsx @@ -14,16 +14,25 @@ const Node = (_: NodeProps) => { const nodeRender = useNodeRender(); - const designer = useDesignerContext(); + const { onDocumentChange: fireOnDocumentChange, onNodeChange: fireOnNodeChange, onNodeClick: fireOnNodeClick } = useDesignerContext(); useEffect(() => { const d = ctx.document.originTree.onTreeChange(() => refresh()); + return () => d.dispose(); }, []); useEffect(() => { - const d1 = nodeRender.form?.onFormValuesChange?.(() => refresh()); - const d2 = nodeRender.form?.onValidate?.(() => refresh()); + const d1 = nodeRender.form?.onFormValuesChange?.(() => { + refresh(); + + fireOnNodeChange(nodeRender.node); + fireOnDocumentChange(); + }); + const d2 = nodeRender.form?.onValidate?.(() => { + refresh(); + }); + return () => { d1?.dispose(); d2?.dispose(); @@ -33,7 +42,7 @@ const Node = (_: NodeProps) => { const handleNodeClick = () => { const node = nodeRender.node; if (node.getNodeRegistry().meta?.clickable) { - designer.onNodeClick?.(node); + fireOnNodeClick(node); } }; diff --git a/ui/src/components/workflow/designer/Toolbar.tsx b/ui/src/components/workflow/designer/Toolbar.tsx index a7518a5d5..8759b0c86 100644 --- a/ui/src/components/workflow/designer/Toolbar.tsx +++ b/ui/src/components/workflow/designer/Toolbar.tsx @@ -19,18 +19,19 @@ const Toolbar = ({ className, style }: ToolbarProps) => { const ctx = useClientContext(); const { playground } = ctx; - const tools = usePlaygroundTools({ minZoom: 0.1, maxZoom: 3, padding: 48 }); + const tools = usePlaygroundTools({ minZoom: 0.1, maxZoom: 3 }); const refresh = useRefresh(); useEffect(() => { const d = playground.config.onReadonlyOrDisabledChange(() => refresh()); + return () => d.dispose(); }, [playground]); - const [isMinimapVisible, setIsMinimapVisible] = useState(window.screen.availWidth >= 1024); + const [isMinimapVisible, setIsMinimapVisible] = useState(() => window.screen.availWidth >= 1024); - const [isMouseFriendly, setIsMouseFriendly] = useState(playground.editorState.is(EditorState.STATE_MOUSE_FRIENDLY_SELECT.id)); + const [isMouseFriendly, setIsMouseFriendly] = useState(() => playground.editorState.is(EditorState.STATE_MOUSE_FRIENDLY_SELECT.id)); const handleToggleLayout = useCallback(() => { if (tools.isVertical) { diff --git a/ui/src/components/workflow/designer/_util.ts b/ui/src/components/workflow/designer/_util.ts new file mode 100644 index 000000000..ca7c886b8 --- /dev/null +++ b/ui/src/components/workflow/designer/_util.ts @@ -0,0 +1,115 @@ +import { FlowNodeBaseType, type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; + +import { type WorkflowNode as _WorkflowNode, duplicateNode as _duplicateNode } from "@/domain/workflow"; + +import { type NodeJSON, NodeType } from "./nodes/typings"; + +/** + * 克隆节点 JSON 对象。节点及其子节点 ID 均会重新分配。 + * @param {NodeJSON} node + * @param {Object} options + * @returns {NodeJSON} + */ +export const duplicateNodeJSON = (node: NodeJSON, options?: { withCopySuffix?: boolean }) => { + return _duplicateNode(node as _WorkflowNode, options); +}; + +/** + * 获取指定节点到根节点为止的所有前序节点。不包括自身和根节点或开始节点。 + * @param {FlowNodeEntity} node + * @returns {FlowNodeEntity[]} + */ +export const getAllPreviousNodes = (node: FlowNodeEntity): FlowNodeEntity[] => { + if (node == null) return []; + + // TODO: 不应该获取到旁路分支 + // // 先获取单一链路(即不包含分支)的全部节点 + // const chains: FlowNodeEntity[] = []; + // let chain: FlowNodeEntity | undefined = node; + // while (chain) { + // if (chain.isStart || chain.flowNodeType === FlowNodeBaseType.ROOT) { + // break; + // } + + // chains.push(chain); + // chain = chain.pre ?? chain.parent; + // } + + // 再获取实际的全部节点 + const visited = new Set(); + const result: FlowNodeEntity[] = []; + let current: FlowNodeEntity | undefined = node; + while (current) { + if (current.isStart || current.flowNodeType === FlowNodeBaseType.ROOT) { + break; + } + + if (current.flowNodeType === NodeType.Condition) { + /** + * condition + * blockIcon + * inlineBlocks + * branchBlock_1 + * blockOrderIcon + * ... + * branchBlock_2 + * blockOrderIcon + * ... + */ + current.lastBlock?.blocks?.forEach((block) => { + block.allChildren?.forEach((child) => { + if (!visited.has(child.id)) { + visited.add(child.id); + result.push(child); + } + }); + }); + } else if (current.flowNodeType === NodeType.TryCatch) { + /** + * tryCatch + * blockIcon + * mainInlineBlocks + * tryBlock + * trySlot + * ... + * catchInlineBlocks + * catchBlock_1 + * blockOrderIcon + * ... + * end + * catchBlock_2 + * blockOrderIcon + * ... + * end + */ + current.lastBlock?.blocks?.forEach((block) => { + block.allChildren?.forEach((child) => { + if (!visited.has(child.id)) { + visited.add(child.id); + result.push(child); + } + }); + }); + } + + if (!visited.has(current.id)) { + visited.add(current.id); + result.push(current); + } + current = current.pre ?? current.parent; + } + + const prevNodes = result.filter((e) => { + if (e.id === node.id) return false; + if (e.isTypeOrExtendType(FlowNodeBaseType.BLOCK_ICON)) return false; + if (e.isTypeOrExtendType(FlowNodeBaseType.BLOCK_ORDER_ICON)) return false; + if (e.isTypeOrExtendType(FlowNodeBaseType.INLINE_BLOCKS)) return false; + if (e.isTypeOrExtendType("trySlot")) return false; + + return true; + }); + // console.log(node.document.toString()); + // console.log(node.document.root); + // console.log(prevNodes); + return prevNodes; +}; diff --git a/ui/src/components/workflow/designer/elements/Adder.tsx b/ui/src/components/workflow/designer/elements/Adder.tsx index ef2307caf..f52c1d320 100644 --- a/ui/src/components/workflow/designer/elements/Adder.tsx +++ b/ui/src/components/workflow/designer/elements/Adder.tsx @@ -31,12 +31,12 @@ const Adder = ({ from, hoverActivated }: AdderProps) => { }) .reduce( (acc, registry) => { - let group = acc.find((item) => item!.key === registry.kindType); + let group = acc.find((item) => item!.key === registry.kind); if (!group) { group = { - key: registry.kindType, + key: registry.kind, type: "group", - label: registry.kindType ? t(`workflow_node.kind.${registry.kindType}`) : null, + label: registry.kind ? t(`workflow_node.kind.${registry.kind}`) : null, children: [], }; acc.push(group); diff --git a/ui/src/components/workflow/designer/elements/BranchAdder.tsx b/ui/src/components/workflow/designer/elements/BranchAdder.tsx index 9b8c52d17..5723be277 100644 --- a/ui/src/components/workflow/designer/elements/BranchAdder.tsx +++ b/ui/src/components/workflow/designer/elements/BranchAdder.tsx @@ -18,11 +18,15 @@ const BranchAdder = ({ node }: BranchAdderProps) => { let block: FlowNodeEntity; switch (node.flowNodeType) { case NodeType.Condition: - block = operation.addBlock(node, BranchBlockNodeRegistry.onAdd!(ctx, node)); + { + block = operation.addBlock(node, BranchBlockNodeRegistry.onAdd!(ctx, node)); + } break; case NodeType.TryCatch: - block = operation.addBlock(node, CatchBlockNodeRegistry.onAdd!(ctx, node)); + { + block = operation.addBlock(node, CatchBlockNodeRegistry.onAdd!(ctx, node)); + } break; default: @@ -38,7 +42,8 @@ const BranchAdder = ({ node }: BranchAdderProps) => { }, 1); }; - return playground.config.readonlyOrDisabled ? null : ( + // TryCatch 暂不支持添加分支 + return playground.config.readonlyOrDisabled || node.flowNodeType === NodeType.TryCatch ? null : (
node.firstChild?.renderData?.toggleMouseEnter()} diff --git a/ui/src/components/workflow/designer/elements/index.ts b/ui/src/components/workflow/designer/elements/index.ts index f975f718c..ebf9b0bae 100644 --- a/ui/src/components/workflow/designer/elements/index.ts +++ b/ui/src/components/workflow/designer/elements/index.ts @@ -17,14 +17,13 @@ export const getAllElements = () => { [FlowRendererKey.COLLAPSE]: Collapse, [FlowRendererKey.TRY_CATCH_COLLAPSE]: TryCatchCollapse, - [FlowRendererKey.SLOT_COLLPASE]: Null, + [FlowRendererKey.SLOT_COLLAPSE]: Null, [FlowRendererKey.DRAG_NODE]: DragNode, [FlowRendererKey.DRAG_HIGHLIGHT_ADDER]: DragHighlightAdder, [FlowRendererKey.DRAG_BRANCH_HIGHLIGHT_ADDER]: DragHighlightAdder, [FlowRendererKey.DRAGGABLE_ADDER]: DraggingAdder, - [FlowRendererKey.CONTEXT_MENU_POPOVER]: Null, [FlowRendererKey.SELECTOR_BOX_POPOVER]: Null, }; }; diff --git a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigDrawer.tsx b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigDrawer.tsx index 6d736aa83..6781808b6 100644 --- a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigDrawer.tsx +++ b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigDrawer.tsx @@ -7,6 +7,7 @@ import BizApplyNodeConfigForm from "./BizApplyNodeConfigForm"; import { NodeType } from "../nodes/typings"; export interface BizApplyNodeConfigDrawerProps { + afterClose?: () => void; loading?: boolean; node: FlowNodeEntity; open?: boolean; diff --git a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx index 57a220304..7c21001b8 100644 --- a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx +++ b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx @@ -3,7 +3,7 @@ import { getI18n, useTranslation } from "react-i18next"; import { Link } from "react-router"; import { type FlowNodeEntity, getNodeForm } from "@flowgram.ai/fixed-layout-editor"; import { IconChevronRight, IconCircleMinus, IconPlus } from "@tabler/icons-react"; -import { useControllableValue } from "ahooks"; +import { useAsyncEffect, useControllableValue } from "ahooks"; import { type AnchorProps, AutoComplete, Button, Divider, Flex, Form, type FormInstance, Input, InputNumber, Select, Switch, Typography } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; @@ -15,7 +15,7 @@ import ACMEDns01ProviderSelect from "@/components/provider/ACMEDns01ProviderSele import CAProviderSelect from "@/components/provider/CAProviderSelect"; import Show from "@/components/Show"; import { ACCESS_USAGES, ACME_DNS01_PROVIDERS, accessProvidersMap, acmeDns01ProvidersMap, caProvidersMap } from "@/domain/provider"; -import { type WorkflowNodeConfigForApply, defaultNodeConfigForApply } from "@/domain/workflow"; +import { type WorkflowNodeConfigForBizApply, defaultNodeConfigForBizApply } from "@/domain/workflow"; import { useAntdForm, useZustandShallowSelector } from "@/hooks"; import { useAccessesStore } from "@/stores/access"; import { useContactEmailsStore } from "@/stores/contact"; @@ -46,7 +46,7 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps) const { accesses } = useAccessesStore(useZustandShallowSelector("accesses")); const initialValues = useMemo(() => { - return getNodeForm(node)?.getValueIn("config") as WorkflowNodeConfigForApply | undefined; + return getNodeForm(node)?.getValueIn("config") as WorkflowNodeConfigForBizApply | undefined; }, [node]); const formSchema = getSchema({ i18n }); @@ -245,6 +245,16 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps) + + ({ - label: e, - value: e, - }))} - placeholder={t("workflow_node.apply.form.key_algorithm.placeholder")} - /> - - void }) => { const { emails, fetchEmails, removeEmail } = useContactEmailsStore(); - useEffect(() => { - fetchEmails(false); - }, []); + useAsyncEffect(() => fetchEmails(false), []); const [value, setValue] = useControllableValue(props, { valuePropName: "value", @@ -538,7 +536,7 @@ const getInitialValues = (): Nullish>> => { domains: "", provider: "", providerAccessId: "", - ...defaultNodeConfigForApply(), + ...defaultNodeConfigForBizApply(), }; }; @@ -555,13 +553,13 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) }, t("common.errmsg.domain_invalid")), contactEmail: z.email(t("common.errmsg.email_invalid")), challengeType: z.string().nullish(), - provider: z.string().nonempty(t("workflow_node.apply.form.provider.placeholder")), - providerAccessId: z.string().nonempty(t("workflow_node.apply.form.provider_access.placeholder")), + provider: z.string(t("workflow_node.apply.form.provider.placeholder")).nonempty(t("workflow_node.apply.form.provider.placeholder")), + providerAccessId: z.string(t("workflow_node.apply.form.provider_access.placeholder")).nonempty(t("workflow_node.apply.form.provider_access.placeholder")), providerConfig: z.any().nullish(), caProvider: z.string().nullish(), caProviderAccessId: z.string().nullish(), caProviderConfig: z.any().nullish(), - keyAlgorithm: z.string().nonempty(t("workflow_node.apply.form.key_algorithm.placeholder")), + keyAlgorithm: z.string(t("workflow_node.apply.form.key_algorithm.placeholder")).nonempty(t("workflow_node.apply.form.key_algorithm.placeholder")), nameservers: z .string() .nullish() diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigDrawer.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigDrawer.tsx index 8e43fbc3a..bab398d4c 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigDrawer.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigDrawer.tsx @@ -7,6 +7,7 @@ import BizDeployNodeConfigForm from "./BizDeployNodeConfigForm"; import { NodeType } from "../nodes/typings"; export interface BizDeployNodeConfigDrawerProps { + afterClose?: () => void; loading?: boolean; node: FlowNodeEntity; open?: boolean; diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigForm.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigForm.tsx index b762100cb..f9d90cd88 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigForm.tsx @@ -12,10 +12,10 @@ import DeploymentProviderPicker from "@/components/provider/DeploymentProviderPi import DeploymentProviderSelect from "@/components/provider/DeploymentProviderSelect"; import Show from "@/components/Show"; import { ACCESS_USAGES, DEPLOYMENT_PROVIDERS, accessProvidersMap, deploymentProvidersMap } from "@/domain/provider"; -import { type WorkflowNodeConfigForDeploy, WorkflowNodeType, defaultNodeConfigForDeploy } from "@/domain/workflow"; -import { useAntdForm, useZustandShallowSelector } from "@/hooks"; -import { useWorkflowStore } from "@/stores/workflow"; +import { type WorkflowNodeConfigForBizDeploy, defaultNodeConfigForBizDeploy } from "@/domain/workflow"; +import { useAntdForm } from "@/hooks"; +import { getAllPreviousNodes } from "../_util"; import { FormNestedFieldsContextProvider, NodeFormContextProvider } from "./_context"; import BizDeployNodeConfigFormProvider1PanelConsole from "./BizDeployNodeConfigFormProvider1PanelConsole"; import BizDeployNodeConfigFormProvider1PanelSite from "./BizDeployNodeConfigFormProvider1PanelSite"; @@ -126,13 +126,21 @@ const BizDeployNodeConfigForm = ({ node, ...props }: BizDeployNodeConfigFormProp const { token: themeToken } = theme.useToken(); - const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"])); - const initialValues = useMemo(() => { - return getNodeForm(node)?.getValueIn("config") as WorkflowNodeConfigForDeploy | undefined; + return getNodeForm(node)?.getValueIn("config") as WorkflowNodeConfigForBizDeploy | undefined; }, [node]); - const formSchema = getSchema({ i18n }); + const formSchema = getSchema({ i18n }).superRefine((values, ctx) => { + if (values.certificateOutputNodeId) { + if (!certificateOutputNodeIdOptions.some((option) => option.value === values.certificateOutputNodeId)) { + ctx.addIssue({ + code: "custom", + message: t("workflow_node.deploy.form.certificate_output_node_id.placeholder"), + path: ["certificateOutputNodeId"], + }); + } + } + }); const formRule = createSchemaFieldRule(formSchema); const { form: formInst, formProps } = useAntdForm({ form: props.form, @@ -142,6 +150,17 @@ const BizDeployNodeConfigForm = ({ node, ...props }: BizDeployNodeConfigFormProp const fieldProvider = Form.useWatch("provider", { form: formInst, preserve: true }); + const certificateOutputNodeIdOptions = useMemo(() => { + return getAllPreviousNodes(node) + .filter((node) => node.flowNodeType === NodeType.BizApply || node.flowNodeType === NodeType.BizUpload) + .map((node) => { + return { + label: getNodeForm(node)?.getValueIn("name"), + value: node.id, + }; + }); + }, [node]); + const [showProviderAccess, setShowProviderAccess] = useState(false); useEffect(() => { // 内置的部署提供商(如本地部署)无需显示授权信息字段 @@ -153,25 +172,6 @@ const BizDeployNodeConfigForm = ({ node, ...props }: BizDeployNodeConfigFormProp } }, [fieldProvider]); - // TODO: 适配 flowgram - const certificateCandidates = useMemo(() => { - const previousNodes = getWorkflowOuptutBeforeId(node.id, "certificate"); - return previousNodes - .filter((node) => node.type === WorkflowNodeType.Apply || node.type === WorkflowNodeType.Upload) - .map((item) => { - return { - label: item.name, - options: (item.outputs ?? [])?.map((output) => { - return { - label: output.label, - value: `${item.id}#${output.name}`, - }; - }), - }; - }) - .filter((group) => group.options.length > 0); - }, [node.id]); - const NestedProviderConfigFields = useMemo(() => { /* 注意:如果追加新的子组件,请保持以 ASCII 排序。 @@ -369,7 +369,6 @@ const BizDeployNodeConfigForm = ({ node, ...props }: BizDeployNodeConfigFormProp const handleProviderPick = (value: string) => { formInst.setFieldValue("provider", value); - console.log(); }; const handleProviderSelect = (value?: string | undefined) => { @@ -380,7 +379,7 @@ const BizDeployNodeConfigForm = ({ node, ...props }: BizDeployNodeConfigFormProp const oldValues = formInst.getFieldsValue(); const newValues: Record = {}; for (const key in oldValues) { - if (key === "provider" || key === "providerAccessId" || key === "certificate" || key === "skipOnLastSucceeded") { + if (key === "certificateOutputNodeId" || key === "provider" || key === "providerAccessId" || key === "skipOnLastSucceeded") { newValues[key] = oldValues[key]; } else { delete newValues[key]; @@ -455,22 +454,24 @@ const BizDeployNodeConfigForm = ({ node, ...props }: BizDeployNodeConfigFormProp ({ - label: e, - value: e.toLowerCase(), - }))} - placeholder={t("workflow_node.apply.form.challenge_type.placeholder")} - /> - - - - - - - - { - if (option.reserve) return false; - - const provider = accessProvidersMap.get(option.provider); - return !!provider?.usages?.includes(ACCESS_USAGES.DNS); - }} - /> - - - - - {nestedFormEl} - - - - {t("workflow_node.apply.form_anchor.certificate.title")} - - - -
- - - - - - - - - - - - - - } - > - - - - } - > - - - - } - > - - - - } - > - - -
- - - - {t("workflow_node.apply.form_anchor.strategy.title")} - - - -
- } - > - -
{t("workflow_node.apply.form.skip_before_expiry_days.prefix")}
- - - -
{t("workflow_node.apply.form.skip_before_expiry_days.suffix")}
-
-
-
- - ); - } -); - -const EmailInput = memo( - ({ disabled, placeholder, ...props }: { disabled?: boolean; placeholder?: string; value?: string; onChange?: (value: string) => void }) => { - const { emails, fetchEmails, removeEmail } = useContactEmailsStore(); - useEffect(() => { - fetchEmails(false); - }, []); - - const [value, setValue] = useControllableValue(props, { - valuePropName: "value", - defaultValuePropName: "defaultValue", - trigger: "onChange", - }); - - const [inputValue, setInputValue] = useState(); - - const renderOptionLabel = (email: string, removable: boolean = false) => ( -
- {email} - {removable && ( -
- ); - - const options = useMemo(() => { - const temp = emails.map((email) => ({ - label: renderOptionLabel(email, true), - value: email, - })); - - if (!!inputValue && temp.every((option) => option.value !== inputValue)) { - temp.unshift({ - label: renderOptionLabel(inputValue), - value: inputValue, - }); - } - - return temp; - }, [emails, inputValue]); - - const handleChange = (value: string) => { - setValue(value); - }; - - const handleSearch = (value: string) => { - setInputValue(value?.trim()); - }; - - return ( - - ); - } -); - -export default memo(ApplyNodeConfigForm); diff --git a/ui/src/components/workflow/node/ApplyNodeConfigFormAWSRoute53Config.tsx b/ui/src/components/workflow/node/ApplyNodeConfigFormAWSRoute53Config.tsx deleted file mode 100644 index 0725e7ad2..000000000 --- a/ui/src/components/workflow/node/ApplyNodeConfigFormAWSRoute53Config.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type ApplyNodeConfigFormAWSRoute53ConfigFieldValues = Nullish<{ - region: string; - hostedZoneId: string; -}>; - -export interface ApplyNodeConfigFormAWSRoute53ConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: ApplyNodeConfigFormAWSRoute53ConfigFieldValues; - onValuesChange?: (values: ApplyNodeConfigFormAWSRoute53ConfigFieldValues) => void; -} - -const initFormModel = (): ApplyNodeConfigFormAWSRoute53ConfigFieldValues => { - return { - region: "us-east-1", - hostedZoneId: "", - }; -}; - -const ApplyNodeConfigFormAWSRoute53Config = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: ApplyNodeConfigFormAWSRoute53ConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z.string(t("workflow_node.apply.form.aws_route53_region.placeholder")).nonempty(t("workflow_node.apply.form.aws_route53_region.placeholder")), - hostedZoneId: z - .string(t("workflow_node.apply.form.aws_route53_hosted_zone_id.placeholder")) - .nonempty(t("workflow_node.apply.form.aws_route53_hosted_zone_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - -
- ); -}; - -export default ApplyNodeConfigFormAWSRoute53Config; diff --git a/ui/src/components/workflow/node/ApplyNodeConfigFormAliyunESAConfig.tsx b/ui/src/components/workflow/node/ApplyNodeConfigFormAliyunESAConfig.tsx deleted file mode 100644 index fdd537c0b..000000000 --- a/ui/src/components/workflow/node/ApplyNodeConfigFormAliyunESAConfig.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type ApplyNodeConfigFormAliyunESAConfigFieldValues = Nullish<{ - region: string; -}>; - -export interface ApplyNodeConfigFormAliyunESAConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: ApplyNodeConfigFormAliyunESAConfigFieldValues; - onValuesChange?: (values: ApplyNodeConfigFormAliyunESAConfigFieldValues) => void; -} - -const initFormModel = (): ApplyNodeConfigFormAliyunESAConfigFieldValues => { - return {}; -}; - -const ApplyNodeConfigFormAliyunESAConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: ApplyNodeConfigFormAliyunESAConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z.string(t("workflow_node.apply.form.aliyun_esa_region.placeholder")).nonempty(t("workflow_node.apply.form.aliyun_esa_region.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default ApplyNodeConfigFormAliyunESAConfig; diff --git a/ui/src/components/workflow/node/ApplyNodeConfigFormHuaweiCloudDNSConfig.tsx b/ui/src/components/workflow/node/ApplyNodeConfigFormHuaweiCloudDNSConfig.tsx deleted file mode 100644 index 772fea9c1..000000000 --- a/ui/src/components/workflow/node/ApplyNodeConfigFormHuaweiCloudDNSConfig.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type ApplyNodeConfigFormHuaweiCloudDNSConfigFieldValues = Nullish<{ - region: string; -}>; - -export interface ApplyNodeConfigFormHuaweiCloudDNSConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: ApplyNodeConfigFormHuaweiCloudDNSConfigFieldValues; - onValuesChange?: (values: ApplyNodeConfigFormHuaweiCloudDNSConfigFieldValues) => void; -} - -const initFormModel = (): ApplyNodeConfigFormHuaweiCloudDNSConfigFieldValues => { - return { - region: "cn-north-1", - }; -}; - -const ApplyNodeConfigFormHuaweiCloudDNSConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: ApplyNodeConfigFormHuaweiCloudDNSConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z - .string(t("workflow_node.apply.form.huaweicloud_dns_region.placeholder")) - .nonempty(t("workflow_node.apply.form.huaweicloud_dns_region.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default ApplyNodeConfigFormHuaweiCloudDNSConfig; diff --git a/ui/src/components/workflow/node/ApplyNodeConfigFormJDCloudDNSConfig.tsx b/ui/src/components/workflow/node/ApplyNodeConfigFormJDCloudDNSConfig.tsx deleted file mode 100644 index b90156a96..000000000 --- a/ui/src/components/workflow/node/ApplyNodeConfigFormJDCloudDNSConfig.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type ApplyNodeConfigFormJDCloudDNSConfigFieldValues = Nullish<{ - regionId: string; -}>; - -export interface ApplyNodeConfigFormJDCloudDNSConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: ApplyNodeConfigFormJDCloudDNSConfigFieldValues; - onValuesChange?: (values: ApplyNodeConfigFormJDCloudDNSConfigFieldValues) => void; -} - -const initFormModel = (): ApplyNodeConfigFormJDCloudDNSConfigFieldValues => { - return { - regionId: "cn-north-1", - }; -}; - -const ApplyNodeConfigFormJDCloudDNSConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: ApplyNodeConfigFormJDCloudDNSConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - regionId: z - .string(t("workflow_node.apply.form.jdcloud_dns_region_id.placeholder")) - .nonempty(t("workflow_node.apply.form.jdcloud_dns_region_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default ApplyNodeConfigFormJDCloudDNSConfig; diff --git a/ui/src/components/workflow/node/ApplyNodeConfigFormTencentCloudEOConfig.tsx b/ui/src/components/workflow/node/ApplyNodeConfigFormTencentCloudEOConfig.tsx deleted file mode 100644 index 48a73e9cb..000000000 --- a/ui/src/components/workflow/node/ApplyNodeConfigFormTencentCloudEOConfig.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type ApplyNodeConfigFormTencentCloudEOConfigFieldValues = Nullish<{ - zoneId: string; -}>; - -export interface ApplyNodeConfigFormTencentCloudEOConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: ApplyNodeConfigFormTencentCloudEOConfigFieldValues; - onValuesChange?: (values: ApplyNodeConfigFormTencentCloudEOConfigFieldValues) => void; -} - -const initFormModel = (): ApplyNodeConfigFormTencentCloudEOConfigFieldValues => { - return {}; -}; - -const ApplyNodeConfigFormTencentCloudEOConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: ApplyNodeConfigFormTencentCloudEOConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - zoneId: z - .string(t("workflow_node.apply.form.tencentcloud_eo_zone_id.placeholder")) - .nonempty(t("workflow_node.apply.form.tencentcloud_eo_zone_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default ApplyNodeConfigFormTencentCloudEOConfig; diff --git a/ui/src/components/workflow/node/BranchNode.tsx b/ui/src/components/workflow/node/BranchNode.tsx deleted file mode 100644 index a755ea51c..000000000 --- a/ui/src/components/workflow/node/BranchNode.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { memo } from "react"; -import { useTranslation } from "react-i18next"; -import { Button, theme } from "antd"; - -import { type WorkflowNode } from "@/domain/workflow"; -import { useZustandShallowSelector } from "@/hooks"; -import { useWorkflowStore } from "@/stores/workflow"; - -import AddNode from "./AddNode"; -import WorkflowElement from "../WorkflowElement"; -import { type SharedNodeProps } from "./_SharedNode"; - -export interface BrandNodeProps extends SharedNodeProps {} - -const BranchNode = ({ node, disabled }: BrandNodeProps) => { - const { t } = useTranslation(); - - const { addBranch } = useWorkflowStore(useZustandShallowSelector(["addBranch"])); - - const { token: themeToken } = theme.useToken(); - - const renderBranch = (node: WorkflowNode, branchNodeId?: string, branchIndex?: number) => { - const elements: JSX.Element[] = []; - - let current = node as typeof node | undefined; - while (current) { - elements.push(); - current = current.next; - } - - return elements; - }; - - return ( - <> -
- - - {node.branches?.map((branch, index) => ( -
- {index == 0 && ( - <> -
-
- - )} - {index == node.branches!.length - 1 && ( - <> -
-
- - )} -
{renderBranch(branch, node.id, index)}
-
- ))} -
- - - - ); -}; - -export default memo(BranchNode); diff --git a/ui/src/components/workflow/node/ConditionNode.tsx b/ui/src/components/workflow/node/ConditionNode.tsx deleted file mode 100644 index 594cc95f6..000000000 --- a/ui/src/components/workflow/node/ConditionNode.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { memo, useRef, useState } from "react"; -import { IconDotsVertical, IconFilter, IconFilterFilled } from "@tabler/icons-react"; -import { Button, Card, Popover } from "antd"; -import { produce } from "immer"; - -import { useZustandShallowSelector } from "@/hooks"; -import { useWorkflowStore } from "@/stores/workflow"; - -import SharedNode, { type SharedNodeProps } from "./_SharedNode"; -import AddNode from "./AddNode"; -import ConditionNodeConfigForm, { type ConditionNodeConfigFormFieldValues, type ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm"; - -export interface ConditionNodeProps extends SharedNodeProps { - branchId: string; - branchIndex: number; -} - -const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeProps) => { - const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"])); - - const [formPending, setFormPending] = useState(false); - const formRef = useRef(null); - const getFormValues = () => formRef.current!.getFieldsValue() as ConditionNodeConfigFormFieldValues; - - const [drawerOpen, setDrawerOpen] = useState(false); - - const handleDrawerConfirm = async () => { - setFormPending(true); - try { - await formRef.current!.validateFields(); - } catch (err) { - setFormPending(false); - throw err; - } - - try { - const newValues = getFormValues(); - const newNode = produce(node, (draft) => { - draft.config = { - ...newValues, - }; - draft.validated = true; - }); - await updateNode(newNode); - } finally { - setFormPending(false); - } - }; - - return ( - <> - } variant="text" />} - /> - } - placement="rightTop" - > -
- setDrawerOpen(true)}> -
-
e.stopPropagation()}> - -
setDrawerOpen(true)}> - {node.config?.expression ? ( -
-
-
-
-
-
- - setDrawerOpen(open)} - > - - - - - - ); -}; - -export default memo(ConditionNode); diff --git a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx deleted file mode 100644 index 81656de2f..000000000 --- a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { forwardRef, memo, useImperativeHandle, useRef } from "react"; -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { type Expr, type WorkflowNodeConfigForCondition, defaultNodeConfigForCondition } from "@/domain/workflow"; -import { useAntdForm } from "@/hooks"; - -import ConditionNodeConfigFormExpressionEditor, { type ConditionNodeConfigFormExpressionEditorInstance } from "./ConditionNodeConfigFormExpressionEditor"; - -export interface ConditionNodeConfigFormFieldValues { - expression?: Expr | undefined; -} - -export interface ConditionNodeConfigFormProps { - className?: string; - style?: React.CSSProperties; - disabled?: boolean; - initialValues?: Partial; - nodeId: string; - onValuesChange?: (values: WorkflowNodeConfigForCondition) => void; -} - -export interface ConditionNodeConfigFormInstance { - getFieldsValue: () => ReturnType["getFieldsValue"]>; - resetFields: FormInstance["resetFields"]; - validateFields: FormInstance["validateFields"]; -} - -const initFormModel = (): ConditionNodeConfigFormFieldValues => { - return defaultNodeConfigForCondition(); -}; - -const ConditionNodeConfigForm = forwardRef( - ({ className, style, disabled, initialValues, nodeId, onValuesChange }, ref) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - expression: z.any().nullish(), - }); - const formRule = createSchemaFieldRule(formSchema); - const { form: formInst, formProps } = useAntdForm({ - name: "workflowNodeConditionConfigForm", - initialValues: initialValues ?? initFormModel(), - }); - - const editorRef = useRef(null); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - useImperativeHandle(ref, () => { - return { - getFieldsValue: formInst.getFieldsValue, - resetFields: formInst.resetFields, - validateFields: (nameList, config) => { - const t1 = formInst.validateFields(nameList, config); - const t2 = editorRef.current!.validate(); - return Promise.all([t1, t2]).then(() => t1); - }, - } as ConditionNodeConfigFormInstance; - }); - - return ( -
- - - -
- ); - } -); - -export default memo(ConditionNodeConfigForm); diff --git a/ui/src/components/workflow/node/ConditionNodeConfigFormExpressionEditor.tsx b/ui/src/components/workflow/node/ConditionNodeConfigFormExpressionEditor.tsx deleted file mode 100644 index 6f853ad17..000000000 --- a/ui/src/components/workflow/node/ConditionNodeConfigFormExpressionEditor.tsx +++ /dev/null @@ -1,388 +0,0 @@ -import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { IconCircleMinus, IconCirclePlus } from "@tabler/icons-react"; -import { useControllableValue } from "ahooks"; -import { Button, Form, Input, Radio, Select, theme } from "antd"; - -import Show from "@/components/Show"; -import type { Expr, ExprComparisonOperator, ExprLogicalOperator, ExprValue, ExprValueSelector, ExprValueType } from "@/domain/workflow"; -import { ExprType } from "@/domain/workflow"; -import { useAntdFormName, useZustandShallowSelector } from "@/hooks"; -import { useWorkflowStore } from "@/stores/workflow"; - -export interface ConditionNodeConfigFormExpressionEditorProps { - className?: string; - style?: React.CSSProperties; - defaultValue?: Expr; - disabled?: boolean; - nodeId: string; - value?: Expr; - onChange?: (value: Expr) => void; -} - -export interface ConditionNodeConfigFormExpressionEditorInstance { - validate: () => Promise; -} - -// 表单内部使用的扁平结构 -type ConditionItem = { - // 选择器,格式为 "${nodeId}#${outputName}#${valueType}" - // 将 [ExprValueSelector] 转为字符串形式,以便于结构化存储。 - leftSelector?: string; - // 比较运算符。 - operator?: ExprComparisonOperator; - // 值。 - // 将 [ExprValue] 转为字符串形式,以便于结构化存储。 - rightValue?: string; -}; - -type ConditionFormValues = { - conditions: ConditionItem[]; - logicalOperator: ExprLogicalOperator; -}; - -const initFormModel = (): ConditionFormValues => { - return { - conditions: [{}], - logicalOperator: "and", - }; -}; - -const exprToFormValues = (expr?: Expr): ConditionFormValues => { - if (!expr) return initFormModel(); - - const conditions: ConditionItem[] = []; - let logicalOp: ExprLogicalOperator = "and"; - - const extractExpr = (expr: Expr): void => { - if (expr.type === ExprType.Comparison) { - if (expr.left.type == ExprType.Variant && expr.right.type == ExprType.Constant) { - conditions.push({ - leftSelector: expr.left.selector?.id != null ? `${expr.left.selector.id}#${expr.left.selector.name}#${expr.left.selector.type}` : undefined, - operator: expr.operator != null ? expr.operator : undefined, - rightValue: expr.right?.value != null ? String(expr.right.value) : undefined, - }); - } else { - console.warn("[certimate] invalid comparison expression: left must be a variant and right must be a constant", expr); - } - } else if (expr.type === ExprType.Logical) { - logicalOp = expr.operator || "and"; - extractExpr(expr.left); - extractExpr(expr.right); - } - }; - - extractExpr(expr); - - return { - conditions: conditions, - logicalOperator: logicalOp, - }; -}; - -const formValuesToExpr = (values: ConditionFormValues): Expr | undefined => { - const wrapExpr = (condition: ConditionItem): Expr => { - const [id, name, type] = (condition.leftSelector?.split("#") ?? ["", "", ""]) as [string, string, ExprValueType]; - const valid = !!id && !!name && !!type; - - const left: Expr = { - type: ExprType.Variant, - selector: valid - ? { - id: id, - name: name, - type: type, - } - : ({} as ExprValueSelector), - }; - - const right: Expr = { - type: ExprType.Constant, - value: condition.rightValue!, - valueType: type, - }; - - return { - type: ExprType.Comparison, - operator: condition.operator!, - left, - right, - }; - }; - - if (values.conditions.length === 0) { - return undefined; - } - - // 只有一个条件时,直接返回比较表达式 - if (values.conditions.length === 1) { - const { leftSelector, operator, rightValue } = values.conditions[0]; - if (!leftSelector || !operator || !rightValue) { - return undefined; - } - return wrapExpr(values.conditions[0]); - } - - // 多个条件时,通过逻辑运算符连接 - let expr: Expr = wrapExpr(values.conditions[0]); - for (let i = 1; i < values.conditions.length; i++) { - expr = { - type: ExprType.Logical, - operator: values.logicalOperator, - left: expr, - right: wrapExpr(values.conditions[i]), - }; - } - return expr; -}; - -const ConditionNodeConfigFormExpressionEditor = forwardRef( - ({ className, style, disabled, nodeId, ...props }, ref) => { - const { t } = useTranslation(); - - const { token: themeToken } = theme.useToken(); - - const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"])); - - const [value, setValue] = useControllableValue(props, { - valuePropName: "value", - defaultValuePropName: "defaultValue", - trigger: "onChange", - }); - - const [formInst] = Form.useForm(); - const formName = useAntdFormName({ form: formInst, name: "workflowNodeConditionConfigFormExpressionEditorForm" }); - const [formModel, setFormModel] = useState(initFormModel()); - - useEffect(() => { - if (value) { - const formValues = exprToFormValues(value); - formInst.setFieldsValue(formValues); - setFormModel(formValues); - } else { - formInst.resetFields(); - setFormModel(initFormModel()); - } - }, [value]); - - const ciSelectorCandidates = useMemo(() => { - const previousNodes = getWorkflowOuptutBeforeId(nodeId); - return previousNodes - .map((node) => { - const group = { - label: node.name, - options: Array<{ label: string; value: string }>(), - }; - - for (const output of node.outputs ?? []) { - switch (output.type) { - case "certificate": - group.options.push({ - label: `${output.label} - ${t("workflow.variables.selector.validity.label")}`, - value: `${node.id}#${output.name}.validity#boolean`, - }); - group.options.push({ - label: `${output.label} - ${t("workflow.variables.selector.days_left.label")}`, - value: `${node.id}#${output.name}.daysLeft#number`, - }); - break; - - default: - group.options.push({ - label: `${output.label}`, - value: `${node.id}#${output.name}#${output.type}`, - }); - console.warn("[certimate] invalid workflow output type in condition expressions", output); - break; - } - } - - return group; - }) - .filter((item) => item.options.length > 0); - }, [nodeId]); - - const getValueTypeBySelector = (selector: string): ExprValueType | undefined => { - if (!selector) return; - - const parts = selector.split("#"); - if (parts.length >= 3) { - return parts[2].toLowerCase() as ExprValueType; - } - }; - - const getOperatorsBySelector = (selector: string): { value: ExprComparisonOperator; label: string }[] => { - const valueType = getValueTypeBySelector(selector); - return getOperatorsByValueType(valueType!); - }; - - const getOperatorsByValueType = (valueType: ExprValue): { value: ExprComparisonOperator; label: string }[] => { - switch (valueType) { - case "number": - return [ - { value: "eq", label: t("workflow_node.condition.form.expression.operator.option.eq.label") }, - { value: "neq", label: t("workflow_node.condition.form.expression.operator.option.neq.label") }, - { value: "gt", label: t("workflow_node.condition.form.expression.operator.option.gt.label") }, - { value: "gte", label: t("workflow_node.condition.form.expression.operator.option.gte.label") }, - { value: "lt", label: t("workflow_node.condition.form.expression.operator.option.lt.label") }, - { value: "lte", label: t("workflow_node.condition.form.expression.operator.option.lte.label") }, - ]; - - case "string": - return [ - { value: "eq", label: t("workflow_node.condition.form.expression.operator.option.eq.label") }, - { value: "neq", label: t("workflow_node.condition.form.expression.operator.option.neq.label") }, - ]; - - case "boolean": - return [ - { value: "eq", label: t("workflow_node.condition.form.expression.operator.option.eq.alias_is_label") }, - { value: "neq", label: t("workflow_node.condition.form.expression.operator.option.neq.alias_not_label") }, - ]; - - default: - return []; - } - }; - - const handleFormChange = (_: undefined, values: ConditionFormValues) => { - setValue(formValuesToExpr(values)); - }; - - useImperativeHandle(ref, () => { - return { - validate: async () => { - await formInst.validateFields(); - }, - } as ConditionNodeConfigFormExpressionEditorInstance; - }); - - return ( -
- 1}> - - - {t("workflow_node.condition.form.expression.logical_operator.option.and.label")} - {t("workflow_node.condition.form.expression.logical_operator.option.or.label")} - - - - - - {(fields, { add, remove }) => ( -
- {fields.map(({ key, name: index, ...rest }) => ( -
- {/* 左:变量选择器 */} - - - - ); - }} - - - {/* 右:输入控件,根据变量类型决定组件 */} - { - return prevValues.conditions?.[index]?.leftSelector !== currentValues.conditions?.[index]?.leftSelector; - }} - > - {({ getFieldValue }) => { - const leftSelector = getFieldValue(["conditions", index, "leftSelector"]); - const valueType = getValueTypeBySelector(leftSelector); - - return ( - - {valueType === "string" ? ( - - ) : valueType === "number" ? ( - - ) : valueType === "boolean" ? ( - - ) : ( - - )} - - ); - }} - - -
- ))} - - - - -
- )} -
-
- ); - } -); - -export default ConditionNodeConfigFormExpressionEditor; diff --git a/ui/src/components/workflow/node/DeployNode.tsx b/ui/src/components/workflow/node/DeployNode.tsx deleted file mode 100644 index 2e374d872..000000000 --- a/ui/src/components/workflow/node/DeployNode.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { memo, useEffect, useMemo, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Avatar, Flex, Typography } from "antd"; -import { produce } from "immer"; - -import { deploymentProvidersMap } from "@/domain/provider"; -import { type WorkflowNodeConfigForDeploy, WorkflowNodeType } from "@/domain/workflow"; -import { useZustandShallowSelector } from "@/hooks"; -import { useWorkflowStore } from "@/stores/workflow"; - -import SharedNode, { type SharedNodeProps } from "./_SharedNode"; -import DeployNodeConfigForm, { type DeployNodeConfigFormInstance } from "./DeployNodeConfigForm"; - -export interface DeployNodeProps extends SharedNodeProps {} - -const DeployNode = ({ node, disabled }: DeployNodeProps) => { - if (node.type !== WorkflowNodeType.Deploy) { - console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Deploy}`); - } - - const { t } = useTranslation(); - - const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"])); - - const formRef = useRef(null); - const [formPending, setFormPending] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForDeploy; - - const [drawerOpen, setDrawerOpen] = useState(false); - const [drawerFooterShow, setDrawerFooterShow] = useState(true); - - useEffect(() => { - setDrawerFooterShow(!!(node.config as WorkflowNodeConfigForDeploy)?.provider); - }, [node.config?.provider]); - - const wrappedEl = useMemo(() => { - if (node.type !== WorkflowNodeType.Deploy) { - console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Deploy}`); - } - - if (!node.validated) { - return {t("workflow_node.action.configure_node")}; - } - - const config = (node.config as WorkflowNodeConfigForDeploy) ?? {}; - const provider = deploymentProvidersMap.get(config.provider); - return ( - - - {t(provider?.name ?? "")} - - ); - }, [node]); - - const handleDrawerConfirm = async () => { - setFormPending(true); - try { - await formRef.current!.validateFields(); - } catch (err) { - setFormPending(false); - throw err; - } - - try { - const newValues = getFormValues(); - const newNode = produce(node, (draft) => { - draft.config = { - ...newValues, - }; - draft.validated = true; - }); - await updateNode(newNode); - } finally { - setFormPending(false); - } - }; - - const handleFormValuesChange = (values: Partial) => { - setDrawerFooterShow(!!values.provider); - }; - - return ( - <> - setDrawerOpen(true)}> - {wrappedEl} - - - { - setDrawerFooterShow(!!(node.config as WorkflowNodeConfigForDeploy)?.provider); - setDrawerOpen(open); - }} - > - - - - ); -}; - -export default memo(DeployNode); diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx deleted file mode 100644 index f43b1e9d0..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ /dev/null @@ -1,579 +0,0 @@ -import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { IconPlus } from "@tabler/icons-react"; -import { Button, Divider, Flex, Form, type FormInstance, Select, Switch, Typography, theme } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import AccessEditDrawer from "@/components/access/AccessEditDrawer"; -import AccessSelect from "@/components/access/AccessSelect"; -import DeploymentProviderPicker from "@/components/provider/DeploymentProviderPicker.tsx"; -import DeploymentProviderSelect from "@/components/provider/DeploymentProviderSelect.tsx"; -import Show from "@/components/Show"; -import { ACCESS_USAGES, DEPLOYMENT_PROVIDERS, accessProvidersMap, deploymentProvidersMap } from "@/domain/provider"; -import { type WorkflowNodeConfigForDeploy, WorkflowNodeType, defaultNodeConfigForDeploy } from "@/domain/workflow"; -import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks"; -import { useWorkflowStore } from "@/stores/workflow"; - -import DeployNodeConfigForm1PanelConsoleConfig from "./DeployNodeConfigForm1PanelConsoleConfig"; -import DeployNodeConfigForm1PanelSiteConfig from "./DeployNodeConfigForm1PanelSiteConfig"; -import DeployNodeConfigFormAliyunALBConfig from "./DeployNodeConfigFormAliyunALBConfig"; -import DeployNodeConfigFormAliyunAPIGWConfig from "./DeployNodeConfigFormAliyunAPIGWConfig"; -import DeployNodeConfigFormAliyunCASConfig from "./DeployNodeConfigFormAliyunCASConfig"; -import DeployNodeConfigFormAliyunCASDeployConfig from "./DeployNodeConfigFormAliyunCASDeployConfig"; -import DeployNodeConfigFormAliyunCDNConfig from "./DeployNodeConfigFormAliyunCDNConfig"; -import DeployNodeConfigFormAliyunCLBConfig from "./DeployNodeConfigFormAliyunCLBConfig"; -import DeployNodeConfigFormAliyunDCDNConfig from "./DeployNodeConfigFormAliyunDCDNConfig"; -import DeployNodeConfigFormAliyunDDoSConfig from "./DeployNodeConfigFormAliyunDDoSConfig"; -import DeployNodeConfigFormAliyunESAConfig from "./DeployNodeConfigFormAliyunESAConfig"; -import DeployNodeConfigFormAliyunFCConfig from "./DeployNodeConfigFormAliyunFCConfig"; -import DeployNodeConfigFormAliyunGAConfig from "./DeployNodeConfigFormAliyunGAConfig"; -import DeployNodeConfigFormAliyunLiveConfig from "./DeployNodeConfigFormAliyunLiveConfig"; -import DeployNodeConfigFormAliyunNLBConfig from "./DeployNodeConfigFormAliyunNLBConfig"; -import DeployNodeConfigFormAliyunOSSConfig from "./DeployNodeConfigFormAliyunOSSConfig"; -import DeployNodeConfigFormAliyunVODConfig from "./DeployNodeConfigFormAliyunVODConfig"; -import DeployNodeConfigFormAliyunWAFConfig from "./DeployNodeConfigFormAliyunWAFConfig"; -import DeployNodeConfigFormAPISIXConfig from "./DeployNodeConfigFormAPISIXConfig"; -import DeployNodeConfigFormAWSACMConfig from "./DeployNodeConfigFormAWSACMConfig"; -import DeployNodeConfigFormAWSCloudFrontConfig from "./DeployNodeConfigFormAWSCloudFrontConfig"; -import DeployNodeConfigFormAWSIAMConfig from "./DeployNodeConfigFormAWSIAMConfig"; -import DeployNodeConfigFormAzureKeyVaultConfig from "./DeployNodeConfigFormAzureKeyVaultConfig"; -import DeployNodeConfigFormBaiduCloudAppBLBConfig from "./DeployNodeConfigFormBaiduCloudAppBLBConfig"; -import DeployNodeConfigFormBaiduCloudBLBConfig from "./DeployNodeConfigFormBaiduCloudBLBConfig"; -import DeployNodeConfigFormBaiduCloudCDNConfig from "./DeployNodeConfigFormBaiduCloudCDNConfig"; -import DeployNodeConfigFormBaishanCDNConfig from "./DeployNodeConfigFormBaishanCDNConfig"; -import DeployNodeConfigFormBaotaPanelConsoleConfig from "./DeployNodeConfigFormBaotaPanelConsoleConfig"; -import DeployNodeConfigFormBaotaPanelSiteConfig from "./DeployNodeConfigFormBaotaPanelSiteConfig"; -import DeployNodeConfigFormBaotaWAFSiteConfig from "./DeployNodeConfigFormBaotaWAFSiteConfig"; -import DeployNodeConfigFormBunnyCDNConfig from "./DeployNodeConfigFormBunnyCDNConfig.tsx"; -import DeployNodeConfigFormBytePlusCDNConfig from "./DeployNodeConfigFormBytePlusCDNConfig"; -import DeployNodeConfigFormCdnflyConfig from "./DeployNodeConfigFormCdnflyConfig"; -import DeployNodeConfigFormCTCCCloudAOConfig from "./DeployNodeConfigFormCTCCCloudAOConfig"; -import DeployNodeConfigFormCTCCCloudCDNConfig from "./DeployNodeConfigFormCTCCCloudCDNConfig"; -import DeployNodeConfigFormCTCCCloudELBConfig from "./DeployNodeConfigFormCTCCCloudELBConfig"; -import DeployNodeConfigFormCTCCCloudICDNConfig from "./DeployNodeConfigFormCTCCCloudICDNConfig"; -import DeployNodeConfigFormCTCCCloudLVDNConfig from "./DeployNodeConfigFormCTCCCloudLVDNConfig"; -import DeployNodeConfigFormDogeCloudCDNConfig from "./DeployNodeConfigFormDogeCloudCDNConfig"; -import DeployNodeConfigFormEdgioApplicationsConfig from "./DeployNodeConfigFormEdgioApplicationsConfig"; -import DeployNodeConfigFormFlexCDNConfig from "./DeployNodeConfigFormFlexCDNConfig"; -import DeployNodeConfigFormGcoreCDNConfig from "./DeployNodeConfigFormGcoreCDNConfig"; -import DeployNodeConfigFormGoEdgeConfig from "./DeployNodeConfigFormGoEdgeConfig"; -import DeployNodeConfigFormHuaweiCloudCDNConfig from "./DeployNodeConfigFormHuaweiCloudCDNConfig"; -import DeployNodeConfigFormHuaweiCloudELBConfig from "./DeployNodeConfigFormHuaweiCloudELBConfig"; -import DeployNodeConfigFormHuaweiCloudWAFConfig from "./DeployNodeConfigFormHuaweiCloudWAFConfig"; -import DeployNodeConfigFormJDCloudALBConfig from "./DeployNodeConfigFormJDCloudALBConfig"; -import DeployNodeConfigFormJDCloudCDNConfig from "./DeployNodeConfigFormJDCloudCDNConfig"; -import DeployNodeConfigFormJDCloudLiveConfig from "./DeployNodeConfigFormJDCloudLiveConfig"; -import DeployNodeConfigFormJDCloudVODConfig from "./DeployNodeConfigFormJDCloudVODConfig"; -import DeployNodeConfigFormKongConfig from "./DeployNodeConfigFormKongConfig"; -import DeployNodeConfigFormKubernetesSecretConfig from "./DeployNodeConfigFormKubernetesSecretConfig"; -import DeployNodeConfigFormLeCDNConfig from "./DeployNodeConfigFormLeCDNConfig"; -import DeployNodeConfigFormLocalConfig from "./DeployNodeConfigFormLocalConfig"; -import DeployNodeConfigFormNetlifySiteConfig from "./DeployNodeConfigFormNetlifySiteConfig"; -import DeployNodeConfigFormProxmoxVEConfig from "./DeployNodeConfigFormProxmoxVEConfig"; -import DeployNodeConfigFormQiniuCDNConfig from "./DeployNodeConfigFormQiniuCDNConfig"; -import DeployNodeConfigFormQiniuKodoConfig from "./DeployNodeConfigFormQiniuKodoConfig"; -import DeployNodeConfigFormQiniuPiliConfig from "./DeployNodeConfigFormQiniuPiliConfig"; -import DeployNodeConfigFormRainYunRCDNConfig from "./DeployNodeConfigFormRainYunRCDNConfig"; -import DeployNodeConfigFormRatPanelSiteConfig from "./DeployNodeConfigFormRatPanelSiteConfig"; -import DeployNodeConfigFormSafeLineConfig from "./DeployNodeConfigFormSafeLineConfig"; -import DeployNodeConfigFormSSHConfig from "./DeployNodeConfigFormSSHConfig.tsx"; -import DeployNodeConfigFormTencentCloudCDNConfig from "./DeployNodeConfigFormTencentCloudCDNConfig.tsx"; -import DeployNodeConfigFormTencentCloudCLBConfig from "./DeployNodeConfigFormTencentCloudCLBConfig.tsx"; -import DeployNodeConfigFormTencentCloudCOSConfig from "./DeployNodeConfigFormTencentCloudCOSConfig.tsx"; -import DeployNodeConfigFormTencentCloudCSSConfig from "./DeployNodeConfigFormTencentCloudCSSConfig.tsx"; -import DeployNodeConfigFormTencentCloudECDNConfig from "./DeployNodeConfigFormTencentCloudECDNConfig.tsx"; -import DeployNodeConfigFormTencentCloudEOConfig from "./DeployNodeConfigFormTencentCloudEOConfig.tsx"; -import DeployNodeConfigFormTencentCloudGAAPConfig from "./DeployNodeConfigFormTencentCloudGAAPConfig.tsx"; -import DeployNodeConfigFormTencentCloudSCFConfig from "./DeployNodeConfigFormTencentCloudSCFConfig"; -import DeployNodeConfigFormTencentCloudSSLConfig from "./DeployNodeConfigFormTencentCloudSSLConfig"; -import DeployNodeConfigFormTencentCloudSSLDeployConfig from "./DeployNodeConfigFormTencentCloudSSLDeployConfig"; -import DeployNodeConfigFormTencentCloudSSLUpdateConfig from "./DeployNodeConfigFormTencentCloudSSLUpdateConfig"; -import DeployNodeConfigFormTencentCloudVODConfig from "./DeployNodeConfigFormTencentCloudVODConfig"; -import DeployNodeConfigFormTencentCloudWAFConfig from "./DeployNodeConfigFormTencentCloudWAFConfig"; -import DeployNodeConfigFormUCloudUCDNConfig from "./DeployNodeConfigFormUCloudUCDNConfig.tsx"; -import DeployNodeConfigFormUCloudUS3Config from "./DeployNodeConfigFormUCloudUS3Config.tsx"; -import DeployNodeConfigFormUniCloudWebHostConfig from "./DeployNodeConfigFormUniCloudWebHostConfig.tsx"; -import DeployNodeConfigFormUpyunCDNConfig from "./DeployNodeConfigFormUpyunCDNConfig.tsx"; -import DeployNodeConfigFormUpyunFileConfig from "./DeployNodeConfigFormUpyunFileConfig.tsx"; -import DeployNodeConfigFormVolcEngineALBConfig from "./DeployNodeConfigFormVolcEngineALBConfig.tsx"; -import DeployNodeConfigFormVolcEngineCDNConfig from "./DeployNodeConfigFormVolcEngineCDNConfig.tsx"; -import DeployNodeConfigFormVolcEngineCertCenterConfig from "./DeployNodeConfigFormVolcEngineCertCenterConfig.tsx"; -import DeployNodeConfigFormVolcEngineCLBConfig from "./DeployNodeConfigFormVolcEngineCLBConfig.tsx"; -import DeployNodeConfigFormVolcEngineDCDNConfig from "./DeployNodeConfigFormVolcEngineDCDNConfig.tsx"; -import DeployNodeConfigFormVolcEngineImageXConfig from "./DeployNodeConfigFormVolcEngineImageXConfig.tsx"; -import DeployNodeConfigFormVolcEngineLiveConfig from "./DeployNodeConfigFormVolcEngineLiveConfig.tsx"; -import DeployNodeConfigFormVolcEngineTOSConfig from "./DeployNodeConfigFormVolcEngineTOSConfig.tsx"; -import DeployNodeConfigFormWangsuCDNConfig from "./DeployNodeConfigFormWangsuCDNConfig.tsx"; -import DeployNodeConfigFormWangsuCDNProConfig from "./DeployNodeConfigFormWangsuCDNProConfig.tsx"; -import DeployNodeConfigFormWangsuCertificateConfig from "./DeployNodeConfigFormWangsuCertificateConfig.tsx"; -import DeployNodeConfigFormWebhookConfig from "./DeployNodeConfigFormWebhookConfig.tsx"; - -type DeployNodeConfigFormFieldValues = Partial; - -export interface DeployNodeConfigFormProps { - className?: string; - style?: React.CSSProperties; - disabled?: boolean; - initialValues?: DeployNodeConfigFormFieldValues; - nodeId: string; - onValuesChange?: (values: DeployNodeConfigFormFieldValues) => void; -} - -export interface DeployNodeConfigFormInstance { - getFieldsValue: () => ReturnType["getFieldsValue"]>; - resetFields: FormInstance["resetFields"]; - validateFields: FormInstance["validateFields"]; -} - -const initFormModel = (): DeployNodeConfigFormFieldValues => { - return defaultNodeConfigForDeploy(); -}; - -const DeployNodeConfigForm = forwardRef( - ({ className, style, disabled, initialValues, nodeId, onValuesChange }, ref) => { - const { t } = useTranslation(); - - const { token: themeToken } = theme.useToken(); - - const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"])); - - const formSchema = z.object({ - certificate: z.string(t("workflow_node.deploy.form.certificate.placeholder")).nonempty(t("workflow_node.deploy.form.certificate.placeholder")), - provider: z.string(t("workflow_node.deploy.form.provider.placeholder")).nonempty(t("workflow_node.deploy.form.provider.placeholder")), - providerAccessId: z - .string(t("workflow_node.deploy.form.provider_access.placeholder")) - .nullish() - .refine((v) => { - if (!fieldProvider) return true; - - const provider = deploymentProvidersMap.get(fieldProvider); - return !!provider?.builtin || !!v; - }, t("workflow_node.deploy.form.provider_access.placeholder")), - providerConfig: z.any().nullish(), - skipOnLastSucceeded: z.boolean().nullish(), - }); - const formRule = createSchemaFieldRule(formSchema); - const { form: formInst, formProps } = useAntdForm({ - name: "workflowNodeDeployConfigForm", - initialValues: initialValues ?? initFormModel(), - }); - - const fieldProvider = Form.useWatch("provider", { form: formInst, preserve: true }); - - const [showProviderAccess, setShowProviderAccess] = useState(false); - useEffect(() => { - // 内置的部署提供商(如本地部署)无需显示授权信息字段 - if (fieldProvider) { - const provider = deploymentProvidersMap.get(fieldProvider); - setShowProviderAccess(!provider?.builtin); - } else { - setShowProviderAccess(false); - } - }, [fieldProvider]); - - const certificateCandidates = useMemo(() => { - const previousNodes = getWorkflowOuptutBeforeId(nodeId, "certificate"); - return previousNodes - .filter((node) => node.type === WorkflowNodeType.Apply || node.type === WorkflowNodeType.Upload) - .map((item) => { - return { - label: item.name, - options: (item.outputs ?? [])?.map((output) => { - return { - label: output.label, - value: `${item.id}#${output.name}`, - }; - }), - }; - }) - .filter((group) => group.options.length > 0); - }, [nodeId]); - - const [nestedFormInst] = Form.useForm(); - const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "workflowNodeDeployConfigFormProviderConfigForm" }); - const nestedFormEl = useMemo(() => { - const nestedFormProps = { - form: nestedFormInst, - formName: nestedFormName, - disabled: disabled, - initialValues: initialValues?.providerConfig, - }; - - /* - 注意:如果追加新的子组件,请保持以 ASCII 排序。 - NOTICE: If you add new child component, please keep ASCII order. - */ - switch (fieldProvider) { - case DEPLOYMENT_PROVIDERS["1PANEL_CONSOLE"]: - return ; - case DEPLOYMENT_PROVIDERS["1PANEL_SITE"]: - return ; - case DEPLOYMENT_PROVIDERS.ALIYUN_ALB: - return ; - case DEPLOYMENT_PROVIDERS.ALIYUN_APIGW: - return ; - case DEPLOYMENT_PROVIDERS.ALIYUN_CAS: - return ; - case DEPLOYMENT_PROVIDERS.ALIYUN_CAS_DEPLOY: - return ; - case DEPLOYMENT_PROVIDERS.ALIYUN_CLB: - return ; - case DEPLOYMENT_PROVIDERS.ALIYUN_CDN: - return ; - case DEPLOYMENT_PROVIDERS.ALIYUN_DCDN: - return ; - case DEPLOYMENT_PROVIDERS.ALIYUN_DDOS: - return ; - case DEPLOYMENT_PROVIDERS.ALIYUN_ESA: - return ; - case DEPLOYMENT_PROVIDERS.ALIYUN_FC: - return ; - case DEPLOYMENT_PROVIDERS.ALIYUN_GA: - return ; - case DEPLOYMENT_PROVIDERS.ALIYUN_LIVE: - return ; - case DEPLOYMENT_PROVIDERS.ALIYUN_NLB: - return ; - case DEPLOYMENT_PROVIDERS.ALIYUN_OSS: - return ; - case DEPLOYMENT_PROVIDERS.ALIYUN_VOD: - return ; - case DEPLOYMENT_PROVIDERS.ALIYUN_WAF: - return ; - case DEPLOYMENT_PROVIDERS.APISIX: - return ; - case DEPLOYMENT_PROVIDERS.AWS_ACM: - return ; - case DEPLOYMENT_PROVIDERS.AWS_CLOUDFRONT: - return ; - case DEPLOYMENT_PROVIDERS.AWS_IAM: - return ; - case DEPLOYMENT_PROVIDERS.AZURE_KEYVAULT: - return ; - case DEPLOYMENT_PROVIDERS.BAIDUCLOUD_APPBLB: - return ; - case DEPLOYMENT_PROVIDERS.BAIDUCLOUD_BLB: - return ; - case DEPLOYMENT_PROVIDERS.BAIDUCLOUD_CDN: - return ; - case DEPLOYMENT_PROVIDERS.BAISHAN_CDN: - return ; - case DEPLOYMENT_PROVIDERS.BAOTAPANEL_CONSOLE: - return ; - case DEPLOYMENT_PROVIDERS.BAOTAPANEL_SITE: - return ; - case DEPLOYMENT_PROVIDERS.BAOTAWAF_SITE: - return ; - case DEPLOYMENT_PROVIDERS.BUNNY_CDN: - return ; - case DEPLOYMENT_PROVIDERS.BYTEPLUS_CDN: - return ; - case DEPLOYMENT_PROVIDERS.CDNFLY: - return ; - case DEPLOYMENT_PROVIDERS.CTCCCLOUD_AO: - return ; - case DEPLOYMENT_PROVIDERS.CTCCCLOUD_CDN: - return ; - case DEPLOYMENT_PROVIDERS.CTCCCLOUD_ELB: - return ; - case DEPLOYMENT_PROVIDERS.CTCCCLOUD_ICDN: - return ; - case DEPLOYMENT_PROVIDERS.CTCCCLOUD_LVDN: - return ; - case DEPLOYMENT_PROVIDERS.DOGECLOUD_CDN: - return ; - case DEPLOYMENT_PROVIDERS.EDGIO_APPLICATIONS: - return ; - case DEPLOYMENT_PROVIDERS.FLEXCDN: - return ; - case DEPLOYMENT_PROVIDERS.GCORE_CDN: - return ; - case DEPLOYMENT_PROVIDERS.GOEDGE: - return ; - case DEPLOYMENT_PROVIDERS.HUAWEICLOUD_CDN: - return ; - case DEPLOYMENT_PROVIDERS.HUAWEICLOUD_ELB: - return ; - case DEPLOYMENT_PROVIDERS.HUAWEICLOUD_WAF: - return ; - case DEPLOYMENT_PROVIDERS.JDCLOUD_ALB: - return ; - case DEPLOYMENT_PROVIDERS.JDCLOUD_CDN: - return ; - case DEPLOYMENT_PROVIDERS.JDCLOUD_LIVE: - return ; - case DEPLOYMENT_PROVIDERS.JDCLOUD_VOD: - return ; - case DEPLOYMENT_PROVIDERS.KONG: - return ; - case DEPLOYMENT_PROVIDERS.KUBERNETES_SECRET: - return ; - case DEPLOYMENT_PROVIDERS.LECDN: - return ; - case DEPLOYMENT_PROVIDERS.LOCAL: - return ; - case DEPLOYMENT_PROVIDERS.NETLIFY_SITE: - return ; - case DEPLOYMENT_PROVIDERS.PROXMOXVE: - return ; - case DEPLOYMENT_PROVIDERS.QINIU_CDN: - return ; - case DEPLOYMENT_PROVIDERS.QINIU_KODO: - return ; - case DEPLOYMENT_PROVIDERS.QINIU_PILI: - return ; - case DEPLOYMENT_PROVIDERS.RAINYUN_RCDN: - return ; - case DEPLOYMENT_PROVIDERS.RATPANEL_SITE: - return ; - case DEPLOYMENT_PROVIDERS.SAFELINE: - return ; - case DEPLOYMENT_PROVIDERS.SSH: - return ; - case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CDN: - return ; - case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CLB: - return ; - case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_COS: - return ; - case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CSS: - return ; - case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_ECDN: - return ; - case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_EO: - return ; - case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_GAAP: - return ; - case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_SCF: - return ; - case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_SSL: - return ; - case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_SSL_DEPLOY: - return ; - case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_SSL_UPDATE: - return ; - case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_VOD: - return ; - case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_WAF: - return ; - case DEPLOYMENT_PROVIDERS.UCLOUD_UCDN: - return ; - case DEPLOYMENT_PROVIDERS.UCLOUD_US3: - return ; - case DEPLOYMENT_PROVIDERS.UNICLOUD_WEBHOST: - return ; - case DEPLOYMENT_PROVIDERS.UPYUN_CDN: - return ; - case DEPLOYMENT_PROVIDERS.UPYUN_FILE: - return ; - case DEPLOYMENT_PROVIDERS.VOLCENGINE_ALB: - return ; - case DEPLOYMENT_PROVIDERS.VOLCENGINE_CDN: - return ; - case DEPLOYMENT_PROVIDERS.VOLCENGINE_CERTCENTER: - return ; - case DEPLOYMENT_PROVIDERS.VOLCENGINE_CLB: - return ; - case DEPLOYMENT_PROVIDERS.VOLCENGINE_DCDN: - return ; - case DEPLOYMENT_PROVIDERS.VOLCENGINE_IMAGEX: - return ; - case DEPLOYMENT_PROVIDERS.VOLCENGINE_LIVE: - return ; - case DEPLOYMENT_PROVIDERS.VOLCENGINE_TOS: - return ; - case DEPLOYMENT_PROVIDERS.WANGSU_CDN: - return ; - case DEPLOYMENT_PROVIDERS.WANGSU_CDNPRO: - return ; - case DEPLOYMENT_PROVIDERS.WANGSU_CERTIFICATE: - return ; - case DEPLOYMENT_PROVIDERS.WEBHOOK: - return ; - } - }, [disabled, initialValues?.providerConfig, fieldProvider, nestedFormInst, nestedFormName]); - - const handleProviderPick = (value: string) => { - formInst.setFieldValue("provider", value); - onValuesChange?.(formInst.getFieldsValue(true)); - }; - - const handleProviderSelect = (value?: string | undefined) => { - // 切换部署目标时重置表单,避免其他部署目标的配置字段影响当前部署目标 - if (initialValues?.provider === value) { - formInst.resetFields(); - } else { - const oldValues = formInst.getFieldsValue(); - const newValues: Record = {}; - for (const key in oldValues) { - if (key === "provider" || key === "providerAccessId" || key === "certificate" || key === "skipOnLastSucceeded") { - newValues[key] = oldValues[key]; - } else { - newValues[key] = undefined; - } - } - formInst.setFieldsValue(newValues); - - if (deploymentProvidersMap.get(fieldProvider)?.provider !== deploymentProvidersMap.get(value!)?.provider) { - formInst.setFieldValue("providerAccessId", undefined); - onValuesChange?.(formInst.getFieldsValue(true)); - } - } - }; - - const handleFormProviderChange = (name: string) => { - if (name === nestedFormName) { - formInst.setFieldValue("providerConfig", nestedFormInst.getFieldsValue()); - onValuesChange?.(formInst.getFieldsValue(true)); - } - }; - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values as DeployNodeConfigFormFieldValues); - }; - - useImperativeHandle(ref, () => { - return { - getFieldsValue: () => { - const values = formInst.getFieldsValue(true); - values.providerConfig = nestedFormInst.getFieldsValue(); - return values; - }, - resetFields: (fields) => { - formInst.resetFields(fields); - - if (!!fields && fields.includes("providerConfig")) { - nestedFormInst.resetFields(fields); - } - }, - validateFields: (nameList, config) => { - const t1 = formInst.validateFields(nameList, config); - const t2 = nestedFormInst.validateFields(undefined, config); - return Promise.all([t1, t2]).then(() => t1); - }, - } as DeployNodeConfigFormInstance; - }); - - return ( - -
- - } - > - - - - - - - } - > - - - - - - - - - } - > - - - - - - } - > - - - - - ); -}; - -export default DeployNodeConfigForm1PanelSiteConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAPISIXConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAPISIXConfig.tsx deleted file mode 100644 index f6ffeaa54..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAPISIXConfig.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; - -type DeployNodeConfigFormAPISIXConfigFieldValues = Nullish<{ - resourceType: string; - certificateId?: string; -}>; - -export interface DeployNodeConfigFormAPISIXConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAPISIXConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAPISIXConfigFieldValues) => void; -} - -const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; - -const initFormModel = (): DeployNodeConfigFormAPISIXConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_CERTIFICATE, - certificateId: "", - }; -}; - -const DeployNodeConfigFormAPISIXConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormAPISIXConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t("workflow_node.deploy.form.apisix_resource_type.placeholder")), - certificateId: z - .string() - .nullish() - .refine((v) => fieldResourceType !== RESOURCE_TYPE_CERTIFICATE || !!v?.trim(), t("workflow_node.deploy.form.apisix_certificate_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormAPISIXConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAWSACMConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAWSACMConfig.tsx deleted file mode 100644 index 57b5d770f..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAWSACMConfig.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type DeployNodeConfigFormAWSACMConfigFieldValues = Nullish<{ - region: string; - certificateArn?: string; -}>; - -export interface DeployNodeConfigFormAWSACMConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAWSACMConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAWSACMConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormAWSACMConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormAWSACMConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormAWSACMConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z.string(t("workflow_node.deploy.form.aws_acm_region.placeholder")).nonempty(t("workflow_node.deploy.form.aws_acm_region.placeholder")), - certificateArn: z.string().nullish(), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormAWSACMConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAWSCloudFrontConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAWSCloudFrontConfig.tsx deleted file mode 100644 index a729112a7..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAWSCloudFrontConfig.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type DeployNodeConfigFormAWSCloudFrontConfigFieldValues = Nullish<{ - region: string; - distributionId: string; - certificateSource: string; -}>; - -export interface DeployNodeConfigFormAWSCloudFrontConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAWSCloudFrontConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAWSCloudFrontConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormAWSCloudFrontConfigFieldValues => { - return { - certificateSource: "ACM", - }; -}; - -const DeployNodeConfigFormAWSCloudFrontConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormAWSCloudFrontConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z.string().nonempty(t("workflow_node.deploy.form.aws_cloudfront_region.placeholder")), - distributionId: z.string().nonempty(t("workflow_node.deploy.form.aws_cloudfront_distribution_id.placeholder")), - certificateSource: z.string().nonempty(t("workflow_node.deploy.form.aws_cloudfront_certificate_source.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - - - - - -
- ); -}; - -export default DeployNodeConfigFormAWSCloudFrontConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAWSIAMConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAWSIAMConfig.tsx deleted file mode 100644 index 4e4a6dfc4..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAWSIAMConfig.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type DeployNodeConfigFormAWSIAMConfigFieldValues = Nullish<{ - region: string; - certificatePath?: string; -}>; - -export interface DeployNodeConfigFormAWSIAMConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAWSIAMConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAWSIAMConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormAWSIAMConfigFieldValues => { - return { - certificatePath: "/", - }; -}; - -const DeployNodeConfigFormAWSIAMConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormAWSIAMConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z.string(t("workflow_node.deploy.form.aws_iam_region.placeholder")).nonempty(t("workflow_node.deploy.form.aws_iam_region.placeholder")), - certificatePath: z - .string() - .nullish() - .refine((v) => { - if (!v) return true; - return v.startsWith("/") && v.endsWith("/"); - }, t("workflow_node.deploy.form.aws_iam_certificate_path.errmsg.invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormAWSIAMConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunALBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunALBConfig.tsx deleted file mode 100644 index a2ac4cb26..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunALBConfig.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormAliyunALBConfigFieldValues = Nullish<{ - region: string; - resourceType: string; - loadbalancerId?: string; - listenerId?: string; - domain?: string; -}>; - -export interface DeployNodeConfigFormAliyunALBConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAliyunALBConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAliyunALBConfigFieldValues) => void; -} - -const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; -const RESOURCE_TYPE_LISTENER = "listener" as const; - -const initFormModel = (): DeployNodeConfigFormAliyunALBConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_LISTENER, - }; -}; - -const DeployNodeConfigFormAliyunALBConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormAliyunALBConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z.string(t("workflow_node.deploy.form.aliyun_alb_region.placeholder")).nonempty(t("workflow_node.deploy.form.aliyun_alb_region.placeholder")), - resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.aliyun_alb_resource_type.placeholder")), - loadbalancerId: z - .string() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish() - .refine((v) => fieldResourceType !== RESOURCE_TYPE_LOADBALANCER || !!v?.trim(), t("workflow_node.deploy.form.aliyun_alb_loadbalancer_id.placeholder")), - listenerId: z - .string() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish() - .refine((v) => fieldResourceType !== RESOURCE_TYPE_LISTENER || !!v?.trim(), t("workflow_node.deploy.form.aliyun_alb_listener_id.placeholder")), - domain: z - .string() - .nullish() - .refine((v) => { - if (![RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].includes(fieldResourceType)) return true; - return !v || validDomainName(v!, { allowWildcard: true }); - }, t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - - - - - - } - > - - - - - - } - > - - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormAliyunALBConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunAPIGWConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunAPIGWConfig.tsx deleted file mode 100644 index f4710aef3..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunAPIGWConfig.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormAliyunAPIGWConfigFieldValues = Nullish<{ - serviceType: string; - region: string; - gatewayId?: string; - groupId?: string; - domain?: string; -}>; - -export interface DeployNodeConfigFormAliyunAPIGWConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAliyunAPIGWConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAliyunAPIGWConfigFieldValues) => void; -} - -const SERVICE_TYPE_CLOUDNATIVE = "cloudnative" as const; -const SERVICE_TYPE_TRADITIONAL = "traditional" as const; - -const initFormModel = (): DeployNodeConfigFormAliyunAPIGWConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormAliyunAPIGWConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormAliyunAPIGWConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - serviceType: z.literal([SERVICE_TYPE_CLOUDNATIVE, SERVICE_TYPE_TRADITIONAL], t("workflow_node.deploy.form.aliyun_apigw_service_type.placeholder")), - region: z.string(t("workflow_node.deploy.form.aliyun_apigw_region.placeholder")).nonempty(t("workflow_node.deploy.form.aliyun_apigw_region.placeholder")), - gatewayId: z - .string() - .nullish() - .refine((v) => fieldServiceType !== SERVICE_TYPE_CLOUDNATIVE || !!v?.trim(), t("workflow_node.deploy.form.aliyun_apigw_gateway_id.placeholder")), - groupId: z - .string() - .nullish() - .refine((v) => fieldServiceType !== SERVICE_TYPE_TRADITIONAL || !!v?.trim(), t("workflow_node.deploy.form.aliyun_apigw_group_id.placeholder")), - domain: z - .string() - .nonempty(t("workflow_node.deploy.form.aliyun_apigw_domain.placeholder")) - .refine((v) => validDomainName(v!, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldServiceType = Form.useWatch("serviceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - - - - } - > - - - - - } - > - - - - - - } - > - - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormAliyunAPIGWConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCASConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCASConfig.tsx deleted file mode 100644 index bd310884f..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCASConfig.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type DeployNodeConfigFormAliyunCASConfigFieldValues = Nullish<{ - region: string; -}>; - -export interface DeployNodeConfigFormAliyunCASConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAliyunCASConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAliyunCASConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormAliyunCASConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormAliyunCASConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormAliyunCASConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z.string(t("workflow_node.deploy.form.aliyun_cas_region.placeholder")).nonempty(t("workflow_node.deploy.form.aliyun_cas_region.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormAliyunCASConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCASDeployConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCASDeployConfig.tsx deleted file mode 100644 index 4e619a7d4..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCASDeployConfig.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Alert, Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import MultipleSplitValueInput from "@/components/MultipleSplitValueInput"; - -type DeployNodeConfigFormAliyunCASDeployConfigFieldValues = Nullish<{ - region: string; - resourceIds: string; - contactIds?: string; -}>; - -export interface DeployNodeConfigFormAliyunCASDeployConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAliyunCASDeployConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAliyunCASDeployConfigFieldValues) => void; -} - -const MULTIPLE_INPUT_SEPARATOR = ";"; - -const initFormModel = (): DeployNodeConfigFormAliyunCASDeployConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormAliyunCASDeployConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormAliyunCASDeployConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z - .string(t("workflow_node.deploy.form.aliyun_casdeploy_region.placeholder")) - .nonempty(t("workflow_node.deploy.form.aliyun_casdeploy_region.placeholder")), - resourceIds: z.string(t("workflow_node.deploy.form.aliyun_casdeploy_resource_ids.placeholder")).refine((v) => { - if (!v) return false; - return String(v) - .split(MULTIPLE_INPUT_SEPARATOR) - .every((e) => /^[1-9]\d*$/.test(e)); - }, t("workflow_node.deploy.form.aliyun_casdeploy_resource_ids.errmsg.invalid")), - contactIds: z - .string(t("workflow_node.deploy.form.aliyun_casdeploy_contact_ids.placeholder")) - .nullish() - .refine((v) => { - if (!v) return true; - return String(v) - .split(MULTIPLE_INPUT_SEPARATOR) - .every((e) => /^[1-9]\d*$/.test(e)); - }, t("workflow_node.deploy.form.aliyun_casdeploy_contact_ids.errmsg.invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - } /> - - - } - > - - - - } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormAliyunCASDeployConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCDNConfig.tsx deleted file mode 100644 index 831292541..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCDNConfig.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormAliyunCDNConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormAliyunCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAliyunCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAliyunCDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormAliyunCDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormAliyunCDNConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormAliyunCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z - .string(t("workflow_node.deploy.form.aliyun_cdn_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormAliyunCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCLBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCLBConfig.tsx deleted file mode 100644 index dcbe17aee..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCLBConfig.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; -import { validDomainName, validPortNumber } from "@/utils/validators"; - -type DeployNodeConfigFormAliyunCLBConfigFieldValues = Nullish<{ - region: string; - resourceType: string; - loadbalancerId?: string; - listenerPort?: number; - domain?: string; -}>; - -export interface DeployNodeConfigFormAliyunCLBConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAliyunCLBConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAliyunCLBConfigFieldValues) => void; -} - -const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; -const RESOURCE_TYPE_LISTENER = "listener" as const; - -const initFormModel = (): DeployNodeConfigFormAliyunCLBConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_LISTENER, - listenerPort: 443, - }; -}; - -const DeployNodeConfigFormAliyunCLBConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormAliyunCLBConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.aliyun_clb_resource_type.placeholder")), - region: z.string(t("workflow_node.deploy.form.aliyun_clb_region.placeholder")).nonempty(t("workflow_node.deploy.form.aliyun_clb_region.placeholder")), - loadbalancerId: z - .string(t("workflow_node.deploy.form.aliyun_clb_loadbalancer_id.placeholder")) - .min(1, t("workflow_node.deploy.form.aliyun_clb_loadbalancer_id.placeholder")) - .max(64, t("common.errmsg.string_max", { max: 64 })), - listenerPort: z.preprocess( - (v) => (v == null || v === "" ? undefined : Number(v)), - z - .number() - .nullish() - .refine((v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v!), t("workflow_node.deploy.form.aliyun_clb_listener_port.placeholder")) - ), - domain: z - .string() - .nullish() - .refine((v) => { - if (![RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].includes(fieldResourceType)) return true; - return !v || validDomainName(v!, { allowWildcard: true }); - }, t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - - - - - } - > - - - - - } - > - - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormAliyunCLBConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunDCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunDCDNConfig.tsx deleted file mode 100644 index 4b1b614b4..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunDCDNConfig.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormAliyunDCDNConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormAliyunDCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAliyunDCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAliyunDCDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormAliyunDCDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormAliyunDCDNConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormAliyunDCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z - .string(t("workflow_node.deploy.form.aliyun_dcdn_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormAliyunDCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunDDoSConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunDDoSConfig.tsx deleted file mode 100644 index 6f442e377..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunDDoSConfig.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormAliyunDDoSConfigFieldValues = Nullish<{ - region: string; - domain: string; -}>; - -export interface DeployNodeConfigFormAliyunDDoSConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAliyunDDoSConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAliyunDDoSConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormAliyunDDoSConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormAliyunDDoSConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormAliyunDDoSConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z.string(t("workflow_node.deploy.form.aliyun_ddos_region.placeholder")).nonempty(t("workflow_node.deploy.form.aliyun_ddos_region.placeholder")), - domain: z - .string(t("workflow_node.deploy.form.aliyun_ddos_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormAliyunDDoSConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunESAConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunESAConfig.tsx deleted file mode 100644 index 8758823fb..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunESAConfig.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type DeployNodeConfigFormAliyunESAConfigFieldValues = Nullish<{ - region: string; - siteId: string | number; -}>; - -export interface DeployNodeConfigFormAliyunESAConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAliyunESAConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAliyunESAConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormAliyunESAConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormAliyunESAConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormAliyunESAConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z.string(t("workflow_node.deploy.form.aliyun_esa_region.placeholder")).nonempty(t("workflow_node.deploy.form.aliyun_esa_region.placeholder")), - siteId: z.union([z.string(), z.number().int()]).refine((v) => { - return /^\d+$/.test(v + "") && +v > 0; - }, t("workflow_node.deploy.form.aliyun_esa_site_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormAliyunESAConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunFCConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunFCConfig.tsx deleted file mode 100644 index 3302b8461..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunFCConfig.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormAliyunFCConfigFieldValues = Nullish<{ - region: string; - serviceVersion: string; - domain: string; -}>; - -export interface DeployNodeConfigFormAliyunFCConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAliyunFCConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAliyunFCConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormAliyunFCConfigFieldValues => { - return { - serviceVersion: "3.0", - }; -}; - -const DeployNodeConfigFormAliyunFCConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormAliyunFCConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - serviceVersion: z.literal(["2.0", "3.0"], t("workflow_node.deploy.form.aliyun_fc_service_version.placeholder")), - region: z.string(t("workflow_node.deploy.form.aliyun_fc_region.placeholder")).nonempty(t("workflow_node.deploy.form.aliyun_fc_region.placeholder")), - domain: z - .string(t("workflow_node.deploy.form.aliyun_fc_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormAliyunFCConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunGAConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunGAConfig.tsx deleted file mode 100644 index 64aefb6d0..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunGAConfig.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormAliyunGAConfigFieldValues = Nullish<{ - resourceType: string; - acceleratorId?: string; - listenerId?: string; - domain?: string; -}>; - -export interface DeployNodeConfigFormAliyunGAConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAliyunGAConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAliyunGAConfigFieldValues) => void; -} - -const RESOURCE_TYPE_ACCELERATOR = "accelerator" as const; -const RESOURCE_TYPE_LISTENER = "listener" as const; - -const initFormModel = (): DeployNodeConfigFormAliyunGAConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_LISTENER, - }; -}; - -const DeployNodeConfigFormAliyunGAConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormAliyunGAConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - resourceType: z.literal([RESOURCE_TYPE_ACCELERATOR, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.aliyun_ga_resource_type.placeholder")), - acceleratorId: z.string().max(64, t("common.errmsg.string_max", { max: 64 })), - listenerId: z - .string() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish() - .refine((v) => fieldResourceType !== RESOURCE_TYPE_LISTENER || !!v?.trim(), t("workflow_node.deploy.form.aliyun_ga_listener_id.placeholder")), - domain: z - .string() - .nullish() - .refine((v) => { - if (![RESOURCE_TYPE_ACCELERATOR, RESOURCE_TYPE_LISTENER].includes(fieldResourceType)) return true; - return !v || validDomainName(v!); - }, t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - - - - } - > - - - - - } - > - - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormAliyunGAConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunLiveConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunLiveConfig.tsx deleted file mode 100644 index d45c86246..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunLiveConfig.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormAliyunLiveConfigFieldValues = Nullish<{ - region: string; - domain: string; -}>; - -export interface DeployNodeConfigFormAliyunLiveConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAliyunLiveConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAliyunLiveConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormAliyunLiveConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormAliyunLiveConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormAliyunLiveConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z.string(t("workflow_node.deploy.form.aliyun_live_region.placeholder")).nonempty(t("workflow_node.deploy.form.aliyun_live_region.placeholder")), - domain: z - .string(t("workflow_node.deploy.form.aliyun_live_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormAliyunLiveConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunNLBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunNLBConfig.tsx deleted file mode 100644 index 7a6e9a024..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunNLBConfig.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; - -type DeployNodeConfigFormAliyunNLBConfigFieldValues = Nullish<{ - region: string; - resourceType: string; - loadbalancerId?: string; - listenerId?: string; -}>; - -export interface DeployNodeConfigFormAliyunNLBConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAliyunNLBConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAliyunNLBConfigFieldValues) => void; -} - -const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; -const RESOURCE_TYPE_LISTENER = "listener" as const; - -const initFormModel = (): DeployNodeConfigFormAliyunNLBConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_LISTENER, - }; -}; - -const DeployNodeConfigFormAliyunNLBConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormAliyunNLBConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z.string(t("workflow_node.deploy.form.aliyun_nlb_region.placeholder")).nonempty(t("workflow_node.deploy.form.aliyun_nlb_region.placeholder")), - resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.aliyun_nlb_resource_type.placeholder")), - loadbalancerId: z - .string() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish() - .refine((v) => fieldResourceType !== RESOURCE_TYPE_LOADBALANCER || !!v?.trim(), t("workflow_node.deploy.form.aliyun_nlb_loadbalancer_id.placeholder")), - listenerId: z - .string() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish() - .refine((v) => fieldResourceType !== RESOURCE_TYPE_LISTENER || !!v?.trim(), t("workflow_node.deploy.form.aliyun_nlb_listener_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - - - - - - } - > - - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormAliyunNLBConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunOSSConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunOSSConfig.tsx deleted file mode 100644 index b6265cec4..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunOSSConfig.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormAliyunOSSConfigFieldValues = Nullish<{ - region: string; - bucket: string; - domain: string; -}>; - -export interface DeployNodeConfigFormAliyunOSSConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAliyunOSSConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAliyunOSSConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormAliyunOSSConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormAliyunOSSConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormAliyunOSSConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z.string(t("workflow_node.deploy.form.aliyun_oss_region.placeholder")).nonempty(t("workflow_node.deploy.form.aliyun_oss_region.placeholder")), - bucket: z.string(t("workflow_node.deploy.form.aliyun_oss_bucket.placeholder")).nonempty(t("workflow_node.deploy.form.aliyun_oss_bucket.placeholder")), - domain: z - .string(t("workflow_node.deploy.form.aliyun_oss_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormAliyunOSSConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunVODConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunVODConfig.tsx deleted file mode 100644 index 14a3f4977..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunVODConfig.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormAliyunVODConfigFieldValues = Nullish<{ - region: string; - domain: string; -}>; - -export interface DeployNodeConfigFormAliyunVODConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAliyunVODConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAliyunVODConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormAliyunVODConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormAliyunVODConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormAliyunVODConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z.string(t("workflow_node.deploy.form.aliyun_vod_region.placeholder")).nonempty(t("workflow_node.deploy.form.aliyun_vod_region.placeholder")), - domain: z.string(t("workflow_node.deploy.form.aliyun_vod_domain.placeholder")).refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormAliyunVODConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunWAFConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunWAFConfig.tsx deleted file mode 100644 index 5a64698b0..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunWAFConfig.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormAliyunWAFConfigFieldValues = Nullish<{ - region: string; - serviceVersion: string; - instanceId: string; - domain?: string; -}>; - -export interface DeployNodeConfigFormAliyunWAFConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAliyunWAFConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAliyunWAFConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormAliyunWAFConfigFieldValues => { - return { - serviceVersion: "3.0", - }; -}; - -const DeployNodeConfigFormAliyunWAFConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormAliyunWAFConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z.string(t("workflow_node.deploy.form.aliyun_waf_region.placeholder")).nonempty(t("workflow_node.deploy.form.aliyun_waf_region.placeholder")), - serviceVersion: z.literal("3.0", t("workflow_node.deploy.form.aliyun_waf_service_version.placeholder")), - instanceId: z - .string(t("workflow_node.deploy.form.aliyun_waf_instance_id.placeholder")) - .nonempty(t("workflow_node.deploy.form.aliyun_waf_instance_id.placeholder")) - .max(64, t("common.errmsg.string_max", { max: 64 })), - domain: z - .string() - .nullish() - .refine((v) => { - return !v || validDomainName(v!, { allowWildcard: true }); - }, t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - - - - - } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormAliyunWAFConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAzureKeyVaultConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAzureKeyVaultConfig.tsx deleted file mode 100644 index eb884eca0..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAzureKeyVaultConfig.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type DeployNodeConfigFormAzureKeyVaultConfigFieldValues = Nullish<{ - keyvaultName: string; - certificateName?: string; -}>; - -export interface DeployNodeConfigFormAzureKeyVaultConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormAzureKeyVaultConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormAzureKeyVaultConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormAzureKeyVaultConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormAzureKeyVaultConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormAzureKeyVaultConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - keyvaultName: z - .string(t("workflow_node.deploy.form.azure_keyvault_name.placeholder")) - .nonempty(t("workflow_node.deploy.form.azure_keyvault_name.placeholder")), - certificateName: z - .string() - .nullish() - .refine((v) => { - if (!v) return true; - return /^[a-zA-Z0-9-]{1,127}$/.test(v); - }, t("workflow_node.deploy.form.azure_keyvault_certificate_name.errmsg.invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormAzureKeyVaultConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudAppBLBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudAppBLBConfig.tsx deleted file mode 100644 index 300f9c3b3..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudAppBLBConfig.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; -import { validDomainName, validPortNumber } from "@/utils/validators"; - -type DeployNodeConfigFormBaiduCloudAppBLBConfigFieldValues = Nullish<{ - region: string; - resourceType: string; - loadbalancerId?: string; - listenerPort?: number; - domain?: string; -}>; - -export interface DeployNodeConfigFormBaiduCloudAppBLBConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormBaiduCloudAppBLBConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormBaiduCloudAppBLBConfigFieldValues) => void; -} - -const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; -const RESOURCE_TYPE_LISTENER = "listener" as const; - -const initFormModel = (): DeployNodeConfigFormBaiduCloudAppBLBConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_LISTENER, - listenerPort: 443, - }; -}; - -const DeployNodeConfigFormBaiduCloudAppBLBConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormBaiduCloudAppBLBConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z - .string(t("workflow_node.deploy.form.baiducloud_appblb_region.placeholder")) - .nonempty(t("workflow_node.deploy.form.baiducloud_appblb_region.placeholder")), - resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.baiducloud_appblb_resource_type.placeholder")), - loadbalancerId: z - .string(t("workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.placeholder")) - .min(1, t("workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.placeholder")) - .max(64, t("common.errmsg.string_max", { max: 64 })), - listenerPort: z.preprocess( - (v) => (v == null || v === "" ? undefined : Number(v)), - z - .number() - .refine( - (v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v!), - t("workflow_node.deploy.form.baiducloud_appblb_listener_port.placeholder") - ) - .nullish() - ), - domain: z - .string() - .nullish() - .refine((v) => { - if (![RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].includes(fieldResourceType)) return true; - return !v || validDomainName(v!, { allowWildcard: true }); - }, t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - - - - - } - > - - - - - } - > - - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormBaiduCloudAppBLBConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudBLBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudBLBConfig.tsx deleted file mode 100644 index 78b1fc75b..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudBLBConfig.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; -import { validDomainName, validPortNumber } from "@/utils/validators"; - -type DeployNodeConfigFormBaiduCloudBLBConfigFieldValues = Nullish<{ - region: string; - resourceType: string; - loadbalancerId?: string; - listenerPort?: number; - domain?: string; -}>; - -export interface DeployNodeConfigFormBaiduCloudBLBConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormBaiduCloudBLBConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormBaiduCloudBLBConfigFieldValues) => void; -} - -const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; -const RESOURCE_TYPE_LISTENER = "listener" as const; - -const initFormModel = (): DeployNodeConfigFormBaiduCloudBLBConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_LISTENER, - listenerPort: 443, - }; -}; - -const DeployNodeConfigFormBaiduCloudBLBConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormBaiduCloudBLBConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z - .string(t("workflow_node.deploy.form.baiducloud_blb_region.placeholder")) - .nonempty(t("workflow_node.deploy.form.baiducloud_blb_region.placeholder")), - resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.baiducloud_blb_resource_type.placeholder")), - loadbalancerId: z - .string(t("workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.placeholder")) - .min(1, t("workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.placeholder")) - .max(64, t("common.errmsg.string_max", { max: 64 })), - listenerPort: z.preprocess( - (v) => (v == null || v === "" ? undefined : Number(v)), - z - .number() - .nullish() - .refine( - (v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v!), - t("workflow_node.deploy.form.baiducloud_blb_listener_port.placeholder") - ) - ), - domain: z - .string() - .nullish() - .refine((v) => { - if (![RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].includes(fieldResourceType)) return true; - return !v || validDomainName(v!, { allowWildcard: true }); - }, t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - - - - - } - > - - - - - } - > - - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormBaiduCloudBLBConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudCDNConfig.tsx deleted file mode 100644 index 30fdff78b..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudCDNConfig.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormBaiduCloudCDNConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormBaiduCloudCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormBaiduCloudCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormBaiduCloudCDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormBaiduCloudCDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormBaiduCloudCDNConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormBaiduCloudCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z - .string(t("workflow_node.deploy.form.baiducloud_cdn_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormBaiduCloudCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormBaishanCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormBaishanCDNConfig.tsx deleted file mode 100644 index a7c7c5935..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormBaishanCDNConfig.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormBaishanCDNConfigFieldValues = Nullish<{ - domain: string; - certificateId?: string | number; -}>; - -export interface DeployNodeConfigFormBaishanCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormBaishanCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormBaishanCDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormBaishanCDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormBaishanCDNConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormBaishanCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z - .string(t("workflow_node.deploy.form.baishan_cdn_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - certificateId: z - .union([z.string(), z.number().int()]) - .nullish() - .refine((v) => { - if (!v) return true; - return /^\d+$/.test(v + "") && +v > 0; - }, t("workflow_node.deploy.form.baishan_cdn_certificate_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormBaishanCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormBaotaPanelConsoleConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormBaotaPanelConsoleConfig.tsx deleted file mode 100644 index 42be5ebe8..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormBaotaPanelConsoleConfig.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Switch } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type DeployNodeConfigFormBaotaPanelConsoleConfigFieldValues = Nullish<{ - autoRestart?: boolean; -}>; - -export interface DeployNodeConfigFormBaotaPanelConsoleConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormBaotaPanelConsoleConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormBaotaPanelConsoleConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormBaotaPanelConsoleConfigFieldValues => { - return { - autoRestart: true, - }; -}; - -const DeployNodeConfigFormBaotaPanelConsoleConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormBaotaPanelConsoleConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - autoRestart: z.boolean().nullish(), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - - -
- ); -}; - -export default DeployNodeConfigFormBaotaPanelConsoleConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormBaotaPanelSiteConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormBaotaPanelSiteConfig.tsx deleted file mode 100644 index edb33e893..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormBaotaPanelSiteConfig.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import MultipleSplitValueInput from "@/components/MultipleSplitValueInput"; -import Show from "@/components/Show"; - -type DeployNodeConfigFormBaotaPanelSiteConfigFieldValues = Nullish<{ - siteType: string; - siteName?: string; - siteNames?: string; -}>; - -export interface DeployNodeConfigFormBaotaPanelSiteConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormBaotaPanelSiteConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormBaotaPanelSiteConfigFieldValues) => void; -} - -const SITE_TYPE_PHP = "php"; -const SITE_TYPE_OTHER = "other"; - -const MULTIPLE_INPUT_SEPARATOR = ";"; - -const initFormModel = (): DeployNodeConfigFormBaotaPanelSiteConfigFieldValues => { - return { - siteType: SITE_TYPE_OTHER, - siteName: "", - siteNames: "", - }; -}; - -const DeployNodeConfigFormBaotaPanelSiteConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormBaotaPanelSiteConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - siteType: z.literal([SITE_TYPE_PHP, SITE_TYPE_OTHER], t("workflow_node.deploy.form.baotapanel_site_type.placeholder")), - siteName: z - .string() - .nullish() - .refine((v) => { - if (fieldSiteType !== SITE_TYPE_PHP) return true; - return !!v?.trim(); - }, t("workflow_node.deploy.form.baotapanel_site_name.placeholder")), - siteNames: z - .string() - .nullish() - .refine((v) => { - if (fieldSiteType !== SITE_TYPE_OTHER) return true; - if (!v) return false; - return String(v) - .split(MULTIPLE_INPUT_SEPARATOR) - .every((e) => !!e.trim()); - }, t("workflow_node.deploy.form.baotapanel_site_names.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldSiteType = Form.useWatch("siteType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - - - - - } - > - - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormBaotaPanelSiteConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormBaotaWAFSiteConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormBaotaWAFSiteConfig.tsx deleted file mode 100644 index e06b43ba4..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormBaotaWAFSiteConfig.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, InputNumber } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validPortNumber } from "@/utils/validators"; - -type DeployNodeConfigFormBaotaWAFSiteConfigFieldValues = Nullish<{ - siteName: string; - sitePort: number; -}>; - -export interface DeployNodeConfigFormBaotaWAFSiteConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormBaotaWAFSiteConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormBaotaWAFSiteConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormBaotaWAFSiteConfigFieldValues => { - return { - siteName: "", - sitePort: 443, - }; -}; - -const DeployNodeConfigFormBaotaWAFSiteConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormBaotaWAFSiteConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - siteName: z.string().nonempty(t("workflow_node.deploy.form.baotawaf_site_name.placeholder")), - sitePort: z.preprocess( - (v) => Number(v), - z - .number() - .int(t("workflow_node.deploy.form.baotawaf_site_port.placeholder")) - .refine((v) => validPortNumber(v), t("common.errmsg.port_invalid")) - ), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - - - -
- ); -}; - -export default DeployNodeConfigFormBaotaWAFSiteConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormBunnyCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormBunnyCDNConfig.tsx deleted file mode 100644 index 81a0b8ecf..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormBunnyCDNConfig.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormBunnyCDNConfigFieldValues = Nullish<{ - pullZoneId: string | number; - hostname: string; -}>; - -export interface DeployNodeConfigFormBunnyCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormBunnyCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormBunnyCDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormBunnyCDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormBunnyCDNConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormBunnyCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - pullZoneId: z.union([z.string(), z.number().int()]).refine((v) => { - return /^\d+$/.test(v + "") && +v! > 0; - }, t("workflow_node.deploy.form.bunny_cdn_pull_zone_id.placeholder")), - hostname: z - .string(t("workflow_node.deploy.form.bunny_cdn_hostname.placeholder")) - .nonempty(t("workflow_node.deploy.form.bunny_cdn_hostname.placeholder")) - .refine((v) => { - return validDomainName(v!, { allowWildcard: true }); - }, t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormBunnyCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormBytePlusCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormBytePlusCDNConfig.tsx deleted file mode 100644 index b2a1fa762..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormBytePlusCDNConfig.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormBytePlusCDNConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormBytePlusCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormBytePlusCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormBytePlusCDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormBytePlusCDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormBytePlusCDNConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormBytePlusCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z - .string(t("workflow_node.deploy.form.byteplus_cdn_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormBytePlusCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudAOConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudAOConfig.tsx deleted file mode 100644 index d37e81f83..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudAOConfig.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormCTCCCloudAOConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormCTCCCloudAOConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormCTCCCloudAOConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormCTCCCloudAOConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormCTCCCloudAOConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormCTCCCloudAOConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormCTCCCloudAOConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z - .string(t("workflow_node.deploy.form.ctcccloud_ao_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormCTCCCloudAOConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudCDNConfig.tsx deleted file mode 100644 index e076453ac..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudCDNConfig.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormCTCCCloudCDNConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormCTCCCloudCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormCTCCCloudCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormCTCCCloudCDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormCTCCCloudCDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormCTCCCloudCDNConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormCTCCCloudCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z - .string(t("workflow_node.deploy.form.ctcccloud_cdn_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormCTCCCloudCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudELBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudELBConfig.tsx deleted file mode 100644 index 0c0e65d0e..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudELBConfig.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; - -type DeployNodeConfigFormCTCCCloudELBConfigFieldValues = Nullish<{ - regionId: string; - resourceType: string; - loadbalancerId?: string; - listenerId?: string; -}>; - -export interface DeployNodeConfigFormCTCCCloudELBConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormCTCCCloudELBConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormCTCCCloudELBConfigFieldValues) => void; -} - -const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; -const RESOURCE_TYPE_LISTENER = "listener" as const; - -const initFormModel = (): DeployNodeConfigFormCTCCCloudELBConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_LISTENER, - }; -}; - -const DeployNodeConfigFormCTCCCloudELBConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormCTCCCloudELBConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - regionId: z - .string(t("workflow_node.deploy.form.ctcccloud_elb_region_id.placeholder")) - .nonempty(t("workflow_node.deploy.form.ctcccloud_elb_region_id.placeholder")), - resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.ctcccloud_elb_resource_type.placeholder")), - loadbalancerId: z - .string() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish() - .refine((v) => fieldResourceType !== RESOURCE_TYPE_LOADBALANCER || !!v?.trim(), t("workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.placeholder")), - listenerId: z - .string() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish() - .refine((v) => fieldResourceType !== RESOURCE_TYPE_LISTENER || !!v?.trim(), t("workflow_node.deploy.form.ctcccloud_elb_listener_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - - - - - - } - > - - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormCTCCCloudELBConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudICDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudICDNConfig.tsx deleted file mode 100644 index 28a8dbbf9..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudICDNConfig.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormCTCCCloudICDNConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormCTCCCloudICDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormCTCCCloudICDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormCTCCCloudICDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormCTCCCloudICDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormCTCCCloudICDNConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormCTCCCloudICDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z - .string(t("workflow_node.deploy.form.ctcccloud_icdn_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormCTCCCloudICDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudLVDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudLVDNConfig.tsx deleted file mode 100644 index efc918cb8..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudLVDNConfig.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormCTCCCloudLVDNConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormCTCCCloudLVDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormCTCCCloudLVDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormCTCCCloudLVDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormCTCCCloudLVDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormCTCCCloudLVDNConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormCTCCCloudLVDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z.string(t("workflow_node.deploy.form.ctcccloud_lvdn_domain.placeholder")).refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormCTCCCloudLVDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormCdnflyConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormCdnflyConfig.tsx deleted file mode 100644 index 8b322d260..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormCdnflyConfig.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; - -type DeployNodeConfigFormCdnflyConfigFieldValues = Nullish<{ - resourceType: string; - siteId?: string | number; - certificateId?: string | number; -}>; - -export interface DeployNodeConfigFormCdnflyConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormCdnflyConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormCdnflyConfigFieldValues) => void; -} - -const RESOURCE_TYPE_SITE = "site" as const; -const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; - -const initFormModel = (): DeployNodeConfigFormCdnflyConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_SITE, - }; -}; - -const DeployNodeConfigFormCdnflyConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormCdnflyConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - resourceType: z.literal([RESOURCE_TYPE_SITE, RESOURCE_TYPE_CERTIFICATE], t("workflow_node.deploy.form.cdnfly_resource_type.placeholder")), - siteId: z - .union([z.string(), z.number().int()]) - .nullish() - .refine((v) => { - if (fieldResourceType !== RESOURCE_TYPE_SITE) return true; - return /^\d+$/.test(v + "") && +v! > 0; - }, t("workflow_node.deploy.form.cdnfly_site_id.placeholder")), - certificateId: z - .union([z.string(), z.number().int()]) - .nullish() - .refine((v) => { - if (fieldResourceType !== RESOURCE_TYPE_CERTIFICATE) return true; - return /^\d+$/.test(v + "") && +v! > 0; - }, t("workflow_node.deploy.form.cdnfly_certificate_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - - - - - } - > - - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormCdnflyConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormDogeCloudCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormDogeCloudCDNConfig.tsx deleted file mode 100644 index 8b59503c6..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormDogeCloudCDNConfig.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormDogeCloudCDNConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormDogeCloudCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormDogeCloudCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormDogeCloudCDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormDogeCloudCDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormDogeCloudCDNConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormDogeCloudCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z.string(t("workflow_node.deploy.form.dogecloud_cdn_domain.placeholder")).refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormDogeCloudCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormEdgioApplicationsConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormEdgioApplicationsConfig.tsx deleted file mode 100644 index 1f6a49d16..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormEdgioApplicationsConfig.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type DeployNodeConfigFormEdgioApplicationsConfigFieldValues = Nullish<{ - environmentId: string; -}>; - -export interface DeployNodeConfigFormEdgioApplicationsConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormEdgioApplicationsConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormEdgioApplicationsConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormEdgioApplicationsConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormEdgioApplicationsConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormEdgioApplicationsConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - environmentId: z - .string(t("workflow_node.deploy.form.edgio_applications_environment_id.placeholder")) - .min(1, t("workflow_node.deploy.form.edgio_applications_environment_id.placeholder")) - .max(64, t("common.errmsg.string_max", { max: 64 })), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormEdgioApplicationsConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormFlexCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormFlexCDNConfig.tsx deleted file mode 100644 index a61b34df3..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormFlexCDNConfig.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; - -type DeployNodeConfigFormFlexCDNConfigFieldValues = Nullish<{ - resourceType: string; - certificateId?: string | number; -}>; - -export interface DeployNodeConfigFormFlexCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormFlexCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormFlexCDNConfigFieldValues) => void; -} - -const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; - -const initFormModel = (): DeployNodeConfigFormFlexCDNConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_CERTIFICATE, - certificateId: "", - }; -}; - -const DeployNodeConfigFormFlexCDNConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormFlexCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t("workflow_node.deploy.form.flexcdn_resource_type.placeholder")), - certificateId: z - .union([z.string(), z.number().int()]) - .nullish() - .refine((v) => { - if (fieldResourceType !== RESOURCE_TYPE_CERTIFICATE) return true; - return /^\d+$/.test(v + "") && +v! > 0; - }, t("workflow_node.deploy.form.flexcdn_certificate_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormFlexCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormGcoreCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormGcoreCDNConfig.tsx deleted file mode 100644 index 896424f6b..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormGcoreCDNConfig.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type DeployNodeConfigFormGcoreCDNConfigFieldValues = Nullish<{ - resourceId: string | number; - certificateId?: string | number; -}>; - -export interface DeployNodeConfigFormGcoreCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormGcoreCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormGcoreCDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormGcoreCDNConfigFieldValues => { - return { - resourceId: "", - }; -}; - -const DeployNodeConfigFormGcoreCDNConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormGcoreCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - resourceId: z.union([z.string(), z.number().int()]).refine((v) => { - return /^\d+$/.test(v + "") && +v > 0; - }, t("workflow_node.deploy.form.gcore_cdn_resource_id.placeholder")), - certificateId: z - .union([z.string(), z.number().int()]) - .nullish() - .refine((v) => { - if (!v) return true; - return /^\d+$/.test(v + "") && +v > 0; - }, t("workflow_node.deploy.form.gcore_cdn_certificate_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormGcoreCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormGoEdgeConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormGoEdgeConfig.tsx deleted file mode 100644 index 28603844a..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormGoEdgeConfig.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; - -type DeployNodeConfigFormGoEdgeConfigFieldValues = Nullish<{ - resourceType: string; - certificateId?: string | number; -}>; - -export interface DeployNodeConfigFormGoEdgeConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormGoEdgeConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormGoEdgeConfigFieldValues) => void; -} - -const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; - -const initFormModel = (): DeployNodeConfigFormGoEdgeConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_CERTIFICATE, - certificateId: "", - }; -}; - -const DeployNodeConfigFormGoEdgeConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormGoEdgeConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t("workflow_node.deploy.form.goedge_resource_type.placeholder")), - certificateId: z - .union([z.string(), z.number().int()]) - .nullish() - .refine((v) => { - if (fieldResourceType !== RESOURCE_TYPE_CERTIFICATE) return true; - return /^\d+$/.test(v + "") && +v! > 0; - }, t("workflow_node.deploy.form.goedge_certificate_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormGoEdgeConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormHuaweiCloudCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormHuaweiCloudCDNConfig.tsx deleted file mode 100644 index 460a5e817..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormHuaweiCloudCDNConfig.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormHuaweiCloudCDNConfigFieldValues = Nullish<{ - region: string; - domain: string; -}>; - -export interface DeployNodeConfigFormHuaweiCloudCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormHuaweiCloudCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormHuaweiCloudCDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormHuaweiCloudCDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormHuaweiCloudCDNConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormHuaweiCloudCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z - .string(t("workflow_node.deploy.form.huaweicloud_cdn_region.placeholder")) - .nonempty(t("workflow_node.deploy.form.huaweicloud_cdn_region.placeholder")), - domain: z - .string(t("workflow_node.deploy.form.huaweicloud_cdn_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormHuaweiCloudCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormHuaweiCloudELBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormHuaweiCloudELBConfig.tsx deleted file mode 100644 index b3d023566..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormHuaweiCloudELBConfig.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; - -type DeployNodeConfigFormHuaweiCloudELBConfigFieldValues = Nullish<{ - region: string; - resourceType: string; - certificateId?: string; - loadbalancerId?: string; - listenerId?: string; -}>; - -export interface DeployNodeConfigFormHuaweiCloudELBConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormHuaweiCloudELBConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormHuaweiCloudELBConfigFieldValues) => void; -} - -const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; -const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; -const RESOURCE_TYPE_LISTENER = "listener" as const; - -const initFormModel = (): DeployNodeConfigFormHuaweiCloudELBConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_LISTENER, - }; -}; - -const DeployNodeConfigFormHuaweiCloudELBConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormHuaweiCloudELBConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z - .string(t("workflow_node.deploy.form.huaweicloud_elb_region.placeholder")) - .nonempty(t("workflow_node.deploy.form.huaweicloud_elb_region.placeholder")), - resourceType: z.literal( - [RESOURCE_TYPE_CERTIFICATE, RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], - t("workflow_node.deploy.form.huaweicloud_elb_resource_type.placeholder") - ), - certificateId: z - .string() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish() - .refine((v) => fieldResourceType !== RESOURCE_TYPE_CERTIFICATE || !!v?.trim(), t("workflow_node.deploy.form.huaweicloud_elb_certificate_id.placeholder")), - loadbalancerId: z - .string() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish() - .refine( - (v) => fieldResourceType !== RESOURCE_TYPE_LOADBALANCER || !!v?.trim(), - t("workflow_node.deploy.form.huaweicloud_elb_loadbalancer_id.placeholder") - ), - listenerId: z - .string() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish() - .refine((v) => fieldResourceType !== RESOURCE_TYPE_LISTENER || !!v?.trim(), t("workflow_node.deploy.form.huaweicloud_elb_listener_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - - - - - - } - > - - - - - - } - > - - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormHuaweiCloudELBConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormHuaweiCloudWAFConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormHuaweiCloudWAFConfig.tsx deleted file mode 100644 index ebfe00776..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormHuaweiCloudWAFConfig.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormHuaweiCloudWAFConfigFieldValues = Nullish<{ - region: string; - resourceType: string; - certificateId?: string; - domain?: string; - listenerId?: string; -}>; - -export interface DeployNodeConfigFormHuaweiCloudWAFConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormHuaweiCloudWAFConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormHuaweiCloudWAFConfigFieldValues) => void; -} - -const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; -const RESOURCE_TYPE_CLOUDSERVER = "cloudserver" as const; -const RESOURCE_TYPE_PREMIUMHOST = "premiumhost" as const; - -const initFormModel = (): DeployNodeConfigFormHuaweiCloudWAFConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormHuaweiCloudWAFConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormHuaweiCloudWAFConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z - .string(t("workflow_node.deploy.form.huaweicloud_waf_region.placeholder")) - .nonempty(t("workflow_node.deploy.form.huaweicloud_waf_region.placeholder")), - resourceType: z.literal( - [RESOURCE_TYPE_CERTIFICATE, RESOURCE_TYPE_CLOUDSERVER, RESOURCE_TYPE_PREMIUMHOST], - t("workflow_node.deploy.form.huaweicloud_waf_resource_type.placeholder") - ), - certificateId: z - .string() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish() - .refine((v) => { - if (fieldResourceType !== RESOURCE_TYPE_CERTIFICATE) return true; - return !!v?.trim(); - }, t("workflow_node.deploy.form.huaweicloud_waf_certificate_id.placeholder")), - domain: z - .string() - .nullish() - .refine((v) => { - if (fieldResourceType !== RESOURCE_TYPE_CLOUDSERVER && fieldResourceType !== RESOURCE_TYPE_PREMIUMHOST) return true; - return validDomainName(v!, { allowWildcard: true }); - }, t("workflow_node.deploy.form.huaweicloud_waf_domain.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - - - - - - } - > - - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormHuaweiCloudWAFConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormJDCloudALBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormJDCloudALBConfig.tsx deleted file mode 100644 index 5b9127c1b..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormJDCloudALBConfig.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormJDCloudALBConfigFieldValues = Nullish<{ - regionId: string; - resourceType: string; - loadbalancerId?: string; - listenerId?: string; - domain?: string; -}>; - -export interface DeployNodeConfigFormJDCloudALBConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormJDCloudALBConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormJDCloudALBConfigFieldValues) => void; -} - -const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; -const RESOURCE_TYPE_LISTENER = "listener" as const; - -const initFormModel = (): DeployNodeConfigFormJDCloudALBConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_LISTENER, - }; -}; - -const DeployNodeConfigFormJDCloudALBConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormJDCloudALBConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - regionId: z - .string(t("workflow_node.deploy.form.jdcloud_alb_region_id.placeholder")) - .nonempty(t("workflow_node.deploy.form.jdcloud_alb_region_id.placeholder")), - resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.jdcloud_alb_resource_type.placeholder")), - loadbalancerId: z - .string() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish() - .refine((v) => fieldResourceType !== RESOURCE_TYPE_LOADBALANCER || !!v?.trim(), t("workflow_node.deploy.form.jdcloud_alb_loadbalancer_id.placeholder")), - listenerId: z - .string() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish() - .refine((v) => fieldResourceType !== RESOURCE_TYPE_LISTENER || !!v?.trim(), t("workflow_node.deploy.form.jdcloud_alb_listener_id.placeholder")), - domain: z - .string() - .nullish() - .refine((v) => { - if (![RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].includes(fieldResourceType)) return true; - return !v || validDomainName(v!, { allowWildcard: true }); - }, t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - - - - - - } - > - - - - - - } - > - - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormJDCloudALBConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormJDCloudCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormJDCloudCDNConfig.tsx deleted file mode 100644 index f35202e6b..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormJDCloudCDNConfig.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormJDCloudCDNConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormJDCloudCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormJDCloudCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormJDCloudCDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormJDCloudCDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormJDCloudCDNConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormJDCloudCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z - .string(t("workflow_node.deploy.form.jdcloud_cdn_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormJDCloudCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormJDCloudLiveConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormJDCloudLiveConfig.tsx deleted file mode 100644 index c1e064144..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormJDCloudLiveConfig.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormJDCloudLiveConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormJDCloudLiveConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormJDCloudLiveConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormJDCloudLiveConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormJDCloudLiveConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormJDCloudLiveConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormJDCloudLiveConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z.string(t("workflow_node.deploy.form.jdcloud_live_domain.placeholder")).refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormJDCloudLiveConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormJDCloudVODConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormJDCloudVODConfig.tsx deleted file mode 100644 index 4c48ed521..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormJDCloudVODConfig.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormJDCloudVODConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormJDCloudVODConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormJDCloudVODConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormJDCloudVODConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormJDCloudVODConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormJDCloudVODConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormJDCloudVODConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z.string(t("workflow_node.deploy.form.jdcloud_vod_domain.placeholder")).refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormJDCloudVODConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormKongConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormKongConfig.tsx deleted file mode 100644 index 3f131e148..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormKongConfig.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; - -type DeployNodeConfigFormKongConfigFieldValues = Nullish<{ - resourceType: string; - certificateId?: string; -}>; - -export interface DeployNodeConfigFormKongConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormKongConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormKongConfigFieldValues) => void; -} - -const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; - -const initFormModel = (): DeployNodeConfigFormKongConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_CERTIFICATE, - certificateId: "", - }; -}; - -const DeployNodeConfigFormKongConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormKongConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t("workflow_node.deploy.form.kong_resource_type.placeholder")), - workspace: z.string().nullish(), - certificateId: z - .string() - .nullish() - .refine((v) => fieldResourceType !== RESOURCE_TYPE_CERTIFICATE || !!v?.trim(), t("workflow_node.deploy.form.kong_certificate_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - - - - } - > - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormKongConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormKubernetesSecretConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormKubernetesSecretConfig.tsx deleted file mode 100644 index add0e32fc..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormKubernetesSecretConfig.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type DeployNodeConfigFormKubernetesSecretConfigFieldValues = Nullish<{ - namespace: string; - secretName: string; - secretType: string; - secretDataKeyForCrt: string; - secretDataKeyForKey: string; -}>; - -export interface DeployNodeConfigFormKubernetesSecretConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormKubernetesSecretConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormKubernetesSecretConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormKubernetesSecretConfigFieldValues => { - return { - namespace: "default", - secretType: "kubernetes.io/tls", - secretDataKeyForCrt: "tls.crt", - secretDataKeyForKey: "tls.key", - }; -}; - -const DeployNodeConfigFormKubernetesSecretConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormKubernetesSecretConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - namespace: z - .string(t("workflow_node.deploy.form.k8s_namespace.placeholder")) - .nonempty(t("workflow_node.deploy.form.k8s_namespace.placeholder")) - .max(256, t("common.errmsg.string_max", { max: 256 })), - secretName: z - .string(t("workflow_node.deploy.form.k8s_secret_name.placeholder")) - .nonempty(t("workflow_node.deploy.form.k8s_secret_name.placeholder")) - .max(256, t("common.errmsg.string_max", { max: 256 })), - secretType: z - .string(t("workflow_node.deploy.form.k8s_secret_type.placeholder")) - .nonempty(t("workflow_node.deploy.form.k8s_secret_type.placeholder")) - .max(256, t("common.errmsg.string_max", { max: 256 })), - secretDataKeyForCrt: z - .string(t("workflow_node.deploy.form.k8s_secret_data_key_for_crt.placeholder")) - .nonempty(t("workflow_node.deploy.form.k8s_secret_data_key_for_crt.placeholder")) - .max(256, t("common.errmsg.string_max", { max: 256 })), - secretDataKeyForKey: z - .string(t("workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder")) - .nonempty(t("workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder")) - .max(256, t("common.errmsg.string_max", { max: 256 })), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - - - } - > - - - - } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormKubernetesSecretConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormLeCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormLeCDNConfig.tsx deleted file mode 100644 index 7da4e7e65..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormLeCDNConfig.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; - -type DeployNodeConfigFormLeCDNConfigFieldValues = Nullish<{ - resourceType: string; - certificateId?: string | number; - clientId?: string | number; -}>; - -export interface DeployNodeConfigFormLeCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormLeCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormLeCDNConfigFieldValues) => void; -} - -const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; - -const initFormModel = (): DeployNodeConfigFormLeCDNConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_CERTIFICATE, - certificateId: "", - clientId: "", - }; -}; - -const DeployNodeConfigFormLeCDNConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormLeCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t("workflow_node.deploy.form.lecdn_resource_type.placeholder")), - certificateId: z - .union([z.string(), z.number().int()]) - .nullish() - .refine((v) => { - if (fieldResourceType !== RESOURCE_TYPE_CERTIFICATE) return true; - return /^\d+$/.test(v + "") && +v! > 0; - }, t("workflow_node.deploy.form.lecdn_certificate_id.placeholder")), - clientId: z - .union([z.string(), z.number().int()]) - .nullish() - .refine((v) => { - if (fieldResourceType !== RESOURCE_TYPE_CERTIFICATE) return true; - if (v == null || v === "") return true; - return /^\d+$/.test(v + "") && +v! > 0; - }, t("workflow_node.deploy.form.lecdn_client_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - - - - - } - > - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormLeCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx deleted file mode 100644 index fc2fe9f5f..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx +++ /dev/null @@ -1,484 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { IconChevronDown } from "@tabler/icons-react"; -import { Alert, Button, Dropdown, Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import CodeInput from "@/components/CodeInput"; -import Show from "@/components/Show"; -import { CERTIFICATE_FORMATS } from "@/domain/certificate"; - -type DeployNodeConfigFormLocalConfigFieldValues = Nullish<{ - format: string; - certPath: string; - certPathForServerOnly?: string; - certPathForIntermediaOnly?: string; - keyPath?: string; - pfxPassword?: string; - jksAlias?: string; - jksKeypass?: string; - jksStorepass?: string; - shellEnv?: string; - preCommand?: string; - postCommand?: string; -}>; - -export interface DeployNodeConfigFormLocalConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormLocalConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormLocalConfigFieldValues) => void; -} - -const FORMAT_PEM = CERTIFICATE_FORMATS.PEM; -const FORMAT_PFX = CERTIFICATE_FORMATS.PFX; -const FORMAT_JKS = CERTIFICATE_FORMATS.JKS; - -const SHELLENV_SH = "sh" as const; -const SHELLENV_CMD = "cmd" as const; -const SHELLENV_POWERSHELL = "powershell" as const; - -const initFormModel = (): DeployNodeConfigFormLocalConfigFieldValues => { - return { - format: FORMAT_PEM, - certPath: "/etc/ssl/certimate/cert.crt", - keyPath: "/etc/ssl/certimate/cert.key", - shellEnv: SHELLENV_SH, - }; -}; - -export const initPresetScript = ( - key: "sh_backup_files" | "ps_backup_files" | "sh_reload_nginx" | "ps_binding_iis" | "ps_binding_netsh" | "ps_binding_rdp", - params?: { - certPath?: string; - certPathForServerOnly?: string; - certPathForIntermediaOnly?: string; - keyPath?: string; - pfxPassword?: string; - jksAlias?: string; - jksKeypass?: string; - jksStorepass?: string; - } -) => { - switch (key) { - case "sh_backup_files": - return `# 请将以下路径替换为实际值 -cp "${params?.certPath || ""}" "${params?.certPath || ""}.bak" 2>/dev/null || : -cp "${params?.keyPath || ""}" "${params?.keyPath || ""}.bak" 2>/dev/null || : - `.trim(); - - case "ps_backup_files": - return `# 请将以下路径替换为实际值 -if (Test-Path -Path "${params?.certPath || ""}" -PathType Leaf) { - Copy-Item -Path "${params?.certPath || ""}" -Destination "${params?.certPath || ""}.bak" -Force -} -if (Test-Path -Path "${params?.keyPath || ""}" -PathType Leaf) { - Copy-Item -Path "${params?.keyPath || ""}" -Destination "${params?.keyPath || ""}.bak" -Force -} - `.trim(); - - case "sh_reload_nginx": - return `# *** 需要 root 权限 *** - -sudo service nginx reload - `.trim(); - - case "ps_binding_iis": - return `# *** 需要管理员权限 *** - -# 请将以下变量替换为实际值 -$pfxPath = "${params?.certPath || ""}" # PFX 文件路径(与表单中保持一致) -$pfxPassword = "${params?.pfxPassword || ""}" # PFX 密码(与表单中保持一致) -$siteName = "" # IIS 网站名称 -$domain = "" # 域名 -$ipaddr = "" # 绑定 IP,“*”表示所有 IP 绑定 -$port = "" # 绑定端口 - -# 导入证书到本地计算机的个人存储区 -$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable -# 获取 Thumbprint -$thumbprint = $cert.Thumbprint -# 导入 WebAdministration 模块 -Import-Module WebAdministration -# 检查是否已存在 HTTPS 绑定 -$existingBinding = Get-WebBinding -Name "$siteName" -Protocol "https" -Port $port -HostHeader "$domain" -ErrorAction SilentlyContinue -if (!$existingBinding) { - # 添加新的 HTTPS 绑定 - New-WebBinding -Name "$siteName" -Protocol "https" -Port $port -IPAddress "$ipaddr" -HostHeader "$domain" -} -# 获取绑定对象 -$binding = Get-WebBinding -Name "$siteName" -Protocol "https" -Port $port -IPAddress "$ipaddr" -HostHeader "$domain" -# 绑定 SSL 证书 -$binding.AddSslCertificate($thumbprint, "My") -# 删除目录下的证书文件 -Remove-Item -Path "$pfxPath" -Force - `.trim(); - - case "ps_binding_netsh": - return `# *** 需要管理员权限 *** - -# 请将以下变量替换为实际值 -$pfxPath = "${params?.certPath || ""}" # PFX 文件路径(与表单中保持一致) -$pfxPassword = "${params?.pfxPassword || ""}" # PFX 密码(与表单中保持一致) -$ipaddr = "" # 绑定 IP,“0.0.0.0”表示所有 IP 绑定,可填入域名 -$port = "" # 绑定端口 - -# 导入证书到本地计算机的个人存储区 -$addr = $ipaddr + ":" + $port -$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable -# 获取 Thumbprint -$thumbprint = $cert.Thumbprint -# 检测端口是否绑定证书,如绑定则删除绑定 -$isExist = netsh http show sslcert ipport=$addr -if ($isExist -like "*$addr*"){ netsh http delete sslcert ipport=$addr } -# 绑定到端口 -netsh http add sslcert ipport=$addr certhash=$thumbprint -# 删除目录下的证书文件 -Remove-Item -Path "$pfxPath" -Force - `.trim(); - - case "ps_binding_rdp": - return `# *** 需要管理员权限 *** - -# 请将以下变量替换为实际值 -$pfxPath = "${params?.certPath || ""}" # PFX 文件路径(与表单中保持一致) -$pfxPassword = "${params?.pfxPassword || ""}" # PFX 密码(与表单中保持一致) - -# 导入证书到本地计算机的个人存储区 -$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable -# 获取 Thumbprint -$thumbprint = $cert.Thumbprint -# 绑定到 RDP -$rdpCertPath = "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp" -Set-ItemProperty -Path $rdpCertPath -Name "SSLCertificateSHA1Hash" -Value "$thumbprint" - `.trim(); - } -}; - -const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormLocalConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - format: z.literal([FORMAT_PEM, FORMAT_PFX, FORMAT_JKS], t("workflow_node.deploy.form.local_format.placeholder")), - certPath: z - .string() - .min(1, t("workflow_node.deploy.form.local_cert_path.tooltip")) - .max(256, t("common.errmsg.string_max", { max: 256 })), - certPathForServerOnly: z - .string() - .max(256, t("common.errmsg.string_max", { max: 256 })) - .nullish(), - certPathForIntermediaOnly: z - .string() - .max(256, t("common.errmsg.string_max", { max: 256 })) - .nullish(), - keyPath: z - .string() - .max(256, t("common.errmsg.string_max", { max: 256 })) - .nullish() - .refine((v) => fieldFormat !== FORMAT_PEM || !!v?.trim(), t("workflow_node.deploy.form.local_key_path.tooltip")), - pfxPassword: z - .string() - .max(64, t("common.errmsg.string_max", { max: 256 })) - .nullish() - .refine((v) => fieldFormat !== FORMAT_PFX || !!v?.trim(), t("workflow_node.deploy.form.local_pfx_password.tooltip")), - jksAlias: z - .string() - .max(64, t("common.errmsg.string_max", { max: 256 })) - .nullish() - .refine((v) => fieldFormat !== FORMAT_JKS || !!v?.trim(), t("workflow_node.deploy.form.local_jks_alias.tooltip")), - jksKeypass: z - .string() - .max(64, t("common.errmsg.string_max", { max: 256 })) - .nullish() - .refine((v) => fieldFormat !== FORMAT_JKS || !!v?.trim(), t("workflow_node.deploy.form.local_jks_keypass.tooltip")), - jksStorepass: z - .string() - .max(64, t("common.errmsg.string_max", { max: 256 })) - .nullish() - .refine((v) => fieldFormat !== FORMAT_JKS || !!v?.trim(), t("workflow_node.deploy.form.local_jks_storepass.tooltip")), - shellEnv: z.literal([SHELLENV_SH, SHELLENV_CMD, SHELLENV_POWERSHELL], t("workflow_node.deploy.form.local_shell_env.placeholder")), - preCommand: z - .string() - .max(20480, t("common.errmsg.string_max", { max: 20480 })) - .nullish(), - postCommand: z - .string() - .max(20480, t("common.errmsg.string_max", { max: 20480 })) - .nullish(), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldFormat = Form.useWatch("format", formInst); - const fieldCertPath = Form.useWatch("certPath", formInst); - - const handleFormatSelect = (value: string) => { - if (fieldFormat === value) return; - - switch (value) { - case FORMAT_PEM: - { - if (/(.pfx|.jks)$/.test(fieldCertPath)) { - formInst.setFieldValue("certPath", fieldCertPath.replace(/(.pfx|.jks)$/, ".crt")); - } - } - break; - - case FORMAT_PFX: - { - if (/(.crt|.jks)$/.test(fieldCertPath)) { - formInst.setFieldValue("certPath", fieldCertPath.replace(/(.crt|.jks)$/, ".pfx")); - } - } - break; - - case FORMAT_JKS: - { - if (/(.crt|.pfx)$/.test(fieldCertPath)) { - formInst.setFieldValue("certPath", fieldCertPath.replace(/(.crt|.pfx)$/, ".jks")); - } - } - break; - } - }; - - const handlePresetPreScriptClick = (key: string) => { - switch (key) { - case "sh_backup_files": - case "ps_backup_files": - { - const presetScriptParams = { - certPath: formInst.getFieldValue("certPath"), - keyPath: formInst.getFieldValue("keyPath"), - }; - formInst.setFieldValue("shellEnv", SHELLENV_SH); - formInst.setFieldValue("preCommand", initPresetScript(key, presetScriptParams)); - } - break; - } - }; - - const handlePresetPostScriptClick = (key: string) => { - switch (key) { - case "sh_reload_nginx": - { - formInst.setFieldValue("shellEnv", SHELLENV_SH); - formInst.setFieldValue("postCommand", initPresetScript(key)); - } - break; - - case "ps_binding_iis": - case "ps_binding_netsh": - case "ps_binding_rdp": - { - const presetScriptParams = { - certPath: formInst.getFieldValue("certPath"), - pfxPassword: formInst.getFieldValue("pfxPassword"), - }; - formInst.setFieldValue("shellEnv", SHELLENV_POWERSHELL); - formInst.setFieldValue("postCommand", initPresetScript(key, presetScriptParams)); - } - break; - } - }; - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - } /> - - - - - - - } - > - - - - - } - > - - - - } - > - - - - } - > - - - - - - } - > - - - - - - } - > - - - - } - > - - - - } - > - - - - - - - - - - - - - - - - - - - - - -
- ); -}; - -export default DeployNodeConfigFormLocalConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormNetlifySiteConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormNetlifySiteConfig.tsx deleted file mode 100644 index 6d862d03a..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormNetlifySiteConfig.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type DeployNodeConfigFormNetlifySiteConfigFieldValues = Nullish<{ - siteId: string; -}>; - -export interface DeployNodeConfigFormNetlifySiteConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormNetlifySiteConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormNetlifySiteConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormNetlifySiteConfigFieldValues => { - return { - siteId: "", - }; -}; - -const DeployNodeConfigFormNetlifySiteConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormNetlifySiteConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - siteId: z.string().nonempty(t("workflow_node.deploy.form.netlify_site_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormNetlifySiteConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormProxmoxVEConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormProxmoxVEConfig.tsx deleted file mode 100644 index 6153dc106..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormProxmoxVEConfig.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Switch } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type DeployNodeConfigFormProxmoxVEConfigFieldValues = Nullish<{ - nodeName: string; - autoRestart?: boolean; -}>; - -export interface DeployNodeConfigFormProxmoxVEConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormProxmoxVEConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormProxmoxVEConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormProxmoxVEConfigFieldValues => { - return { - autoRestart: true, - }; -}; - -const DeployNodeConfigFormProxmoxVEConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormProxmoxVEConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - nodeName: z.string(t("workflow_node.deploy.form.proxmoxve_node_name.placeholder")).nonempty(t("workflow_node.deploy.form.proxmoxve_node_name.placeholder")), - autoRestart: z.boolean().nullish(), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - - - - - - -
- ); -}; - -export default DeployNodeConfigFormProxmoxVEConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormQiniuCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormQiniuCDNConfig.tsx deleted file mode 100644 index 4cd077fd5..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormQiniuCDNConfig.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormQiniuCDNConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormQiniuCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormQiniuCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormQiniuCDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormQiniuCDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormQiniuCDNConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormQiniuCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z - .string(t("workflow_node.deploy.form.qiniu_cdn_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormQiniuCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormQiniuKodoConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormQiniuKodoConfig.tsx deleted file mode 100644 index 699fabf39..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormQiniuKodoConfig.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormQiniuKodoConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormQiniuKodoConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormQiniuKodoConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormQiniuKodoConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormQiniuKodoConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormQiniuKodoConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormQiniuKodoConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z.string(t("workflow_node.deploy.form.qiniu_kodo_domain.placeholder")).refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormQiniuKodoConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormQiniuPiliConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormQiniuPiliConfig.tsx deleted file mode 100644 index b26a2b299..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormQiniuPiliConfig.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormQiniuPiliConfigFieldValues = Nullish<{ - hub: string; - domain: string; -}>; - -export interface DeployNodeConfigFormQiniuPiliConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormQiniuPiliConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormQiniuPiliConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormQiniuPiliConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormQiniuPiliConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormQiniuPiliConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - hub: z.string(t("workflow_node.deploy.form.qiniu_pili_hub.placeholder")).nonempty(t("workflow_node.deploy.form.qiniu_pili_hub.placeholder")), - domain: z - .string(t("workflow_node.deploy.form.qiniu_pili_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormQiniuPiliConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormRainYunRCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormRainYunRCDNConfig.tsx deleted file mode 100644 index 0a55623c3..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormRainYunRCDNConfig.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormRainYunRCDNConfigFieldValues = Nullish<{ - instanceId: string | number; - domain: string; -}>; - -export interface DeployNodeConfigFormRainYunRCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormRainYunRCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormRainYunRCDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormRainYunRCDNConfigFieldValues => { - return { - instanceId: "", - }; -}; - -const DeployNodeConfigFormRainYunRCDNConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormRainYunRCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - instanceId: z.union([z.string(), z.number().int()]).refine((v) => { - return /^\d+$/.test(v + "") && +v > 0; - }, t("workflow_node.deploy.form.rainyun_rcdn_instance_id.placeholder")), - domain: z - .string(t("workflow_node.deploy.form.rainyun_rcdn_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormRainYunRCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormRatPanelSiteConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormRatPanelSiteConfig.tsx deleted file mode 100644 index 4cf91cc2c..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormRatPanelSiteConfig.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type DeployNodeConfigFormRatPanelSiteConfigFieldValues = Nullish<{ - siteName: string; -}>; - -export interface DeployNodeConfigFormRatPanelSiteConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormRatPanelSiteConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormRatPanelSiteConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormRatPanelSiteConfigFieldValues => { - return { - siteName: "", - }; -}; - -const DeployNodeConfigFormRatPanelSiteConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormRatPanelSiteConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - siteName: z.string().nonempty(t("workflow_node.deploy.form.ratpanel_site_name.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormRatPanelSiteConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx deleted file mode 100644 index 4d2de8dc3..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx +++ /dev/null @@ -1,528 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { IconChevronDown } from "@tabler/icons-react"; -import { Button, Dropdown, Form, type FormInstance, Input, Select, Switch } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import CodeInput from "@/components/CodeInput"; -import Show from "@/components/Show"; -import { CERTIFICATE_FORMATS } from "@/domain/certificate"; - -import { initPresetScript as _initPresetScript } from "./DeployNodeConfigFormLocalConfig"; - -type DeployNodeConfigFormSSHConfigFieldValues = Nullish<{ - format: string; - certPath: string; - certPathForServerOnly?: string; - certPathForIntermediaOnly?: string; - keyPath?: string; - pfxPassword?: string; - jksAlias?: string; - jksKeypass?: string; - jksStorepass?: string; - preCommand?: string; - postCommand?: string; - useSCP?: boolean; -}>; - -export interface DeployNodeConfigFormSSHConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormSSHConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormSSHConfigFieldValues) => void; -} - -const FORMAT_PEM = CERTIFICATE_FORMATS.PEM; -const FORMAT_PFX = CERTIFICATE_FORMATS.PFX; -const FORMAT_JKS = CERTIFICATE_FORMATS.JKS; - -const initFormModel = (): DeployNodeConfigFormSSHConfigFieldValues => { - return { - format: FORMAT_PEM, - certPath: "/etc/ssl/certimate/cert.crt", - keyPath: "/etc/ssl/certimate/cert.key", - }; -}; - -const initPresetScript = ( - key: Parameters[0] | "sh_replace_synologydsm_ssl" | "sh_replace_fnos_ssl" | "sh_replace_qnap_ssl", - params?: Parameters[1] -) => { - switch (key) { - case "sh_replace_synologydsm_ssl": - return `# *** 需要 root 权限 *** -# 注意仅支持替换证书,需本身已开启过一次 HTTPS -# 脚本参考 https://github.com/catchdave/ssl-certs/blob/main/replace_synology_ssl_certs.sh - -# 请将以下变量替换为实际值 -$tmpFullchainPath = "${params?.certPath || ""}" # 证书文件路径(与表单中保持一致) -$tmpCertPath = "${params?.certPathForServerOnly || ""}" # 服务器证书文件路径(与表单中保持一致) -$tmpKeyPath = "${params?.keyPath || ""}" # 私钥文件路径(与表单中保持一致) - -DEBUG=1 -error_exit() { echo "[ERROR] $1"; exit 1; } -warn() { echo "[WARN] $1"; } -info() { echo "[INFO] $1"; } -debug() { [[ "\${DEBUG}" ]] && echo "[DEBUG] $1"; } - -certs_src_dir="/usr/syno/etc/certificate/system/default" -target_cert_dirs=( - "/usr/syno/etc/certificate/system/FQDN" - "/usr/local/etc/certificate/ScsiTarget/pkg-scsi-plugin-server/" - "/usr/local/etc/certificate/SynologyDrive/SynologyDrive/" - "/usr/local/etc/certificate/WebDAVServer/webdav/" - "/usr/local/etc/certificate/ActiveBackup/ActiveBackup/" - "/usr/syno/etc/certificate/smbftpd/ftpd/") - -# 获取证书目录 -default_dir_name=$(/dev/null && /usr/syno/bin/synopkg restart ScsiTarget -/usr/syno/bin/synopkg is_onoff SynologyDrive 1>/dev/null && /usr/syno/bin/synopkg restart SynologyDrive -/usr/syno/bin/synopkg is_onoff WebDAVServer 1>/dev/null && /usr/syno/bin/synopkg restart WebDAVServer -/usr/syno/bin/synopkg is_onoff ActiveBackup 1>/dev/null && /usr/syno/bin/synopkg restart ActiveBackup -if ! /usr/syno/bin/synow3tool --gen-all && sudo /usr/syno/bin/synosystemctl restart nginx; then - warn "nginx failed to restart" -fi - -info "Completed" - `.trim(); - - case "sh_replace_fnos_ssl": - return `# *** 需要 root 权限 *** -# 注意仅支持替换证书,需本身已开启过一次 HTTPS -# 脚本参考 https://github.com/lfgyx/fnos_certificate_update/blob/main/src/update_cert.sh - -# 请将以下变量替换为实际值 -# 飞牛证书实际存放路径请在 \`/usr/trim/etc/network_cert_all.conf\` 中查看,注意不要修改文件名 -$tmpFullchainPath = "${params?.certPath || ""}" # 证书文件路径(与表单中保持一致) -$tmpCertPath = "${params?.certPathForServerOnly || ""}" # 服务器证书文件路径(与表单中保持一致) -$tmpKeyPath = "${params?.keyPath || ""}" # 私钥文件路径(与表单中保持一致) -$fnFullchainPath = "/usr/trim/var/trim_connect/ssls/example.com/1234567890/fullchain.crt" # 飞牛证书文件路径 -$fnCertPath = "/usr/trim/var/trim_connect/ssls/example.com/1234567890/example.com.crt" # 飞牛服务器证书文件路径 -$fnKeyPath = "/usr/trim/var/trim_connect/ssls/example.com/1234567890/example.com.key" # 飞牛私钥文件路径 -$domain = "" # 域名 - -# 复制文件 -cp -rf "$tmpFullchainPath" "$fnFullchainPath" -cp -rf "$tmpCertPath" "$fnCertPath" -cp -rf "$tmpKeyPath" "$fnKeyPath" -chmod 755 "$fnFullchainPath" -chmod 755 "$fnCertPath" -chmod 755 "$fnKeyPath" - -# 更新数据库 -NEW_EFFECT_DATE=$(openssl x509 -startdate -noout -in "$fnCertPath" | sed "s/^.*=\\(.*\\)$/\\1/") -NEW_EFFECT_TIMESTAMP=$(date -d "$NEW_EFFECT_DATE" +%s%3N) -NEW_EXPIRY_DATE=$(openssl x509 -enddate -noout -in "$fnCertPath" | sed "s/^.*=\\(.*\\)$/\\1/") -NEW_EXPIRY_TIMESTAMP=$(date -d "$NEW_EXPIRY_DATE" +%s%3N) -psql -U postgres -d trim_connect -c "UPDATE cert SET valid_from=$NEW_EFFECT_TIMESTAMP, valid_to=$NEW_EXPIRY_TIMESTAMP WHERE domain='$domain'" - -# 重启服务 -systemctl restart webdav.service -systemctl restart smbftpd.service -systemctl restart trim_nginx.service - `.trim(); - - case "sh_replace_qnap_ssl": - return `# *** 需要 root 权限 *** -# 注意仅支持替换证书,需本身已开启过一次 HTTPS - -# 请将以下变量替换为实际值 -$tmpFullchainPath = "${params?.certPath || ""}" # 证书文件路径(与表单中保持一致) -$tmpKeyPath = "${params?.keyPath || ""}" # 私钥文件路径(与表单中保持一致) - -# 复制文件 -cp -rf "$tmpFullchainPath" /etc/stunnel/backup.cert -cp -rf "$tmpKeyPath" /etc/stunnel/backup.key -cat /etc/stunnel/backup.key > /etc/stunnel/stunnel.pem -cat /etc/stunnel/backup.cert >> /etc/stunnel/stunnel.pem -chmod 600 /etc/stunnel/backup.cert -chmod 600 /etc/stunnel/backup.key -chmod 600 /etc/stunnel/stunnel.pem - -# 重启服务 -/etc/init.d/stunnel.sh restart -/etc/init.d/reverse_proxy.sh reload - `.trim(); - } - - return _initPresetScript(key as Parameters[0], params); -}; - -const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormSSHConfigProps) => { - const { t } = useTranslation(); - - const formSchema = 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() - .refine((v) => fieldFormat !== FORMAT_PEM || !!v?.trim(), t("workflow_node.deploy.form.ssh_key_path.tooltip")), - certPathForServerOnly: z - .string() - .max(256, t("common.errmsg.string_max", { max: 256 })) - .nullish(), - certPathForIntermediaOnly: z - .string() - .max(256, t("common.errmsg.string_max", { max: 256 })) - .nullish(), - pfxPassword: z - .string() - .max(64, t("common.errmsg.string_max", { max: 256 })) - .nullish() - .refine((v) => fieldFormat !== FORMAT_PFX || !!v?.trim(), t("workflow_node.deploy.form.ssh_pfx_password.tooltip")), - jksAlias: z - .string() - .max(64, t("common.errmsg.string_max", { max: 256 })) - .nullish() - .refine((v) => fieldFormat !== FORMAT_JKS || !!v?.trim(), t("workflow_node.deploy.form.ssh_jks_alias.tooltip")), - jksKeypass: z - .string() - .max(64, t("common.errmsg.string_max", { max: 256 })) - .nullish() - .refine((v) => fieldFormat !== FORMAT_JKS || !!v?.trim(), t("workflow_node.deploy.form.ssh_jks_keypass.tooltip")), - jksStorepass: z - .string() - .max(64, t("common.errmsg.string_max", { max: 256 })) - .nullish() - .refine((v) => fieldFormat !== FORMAT_JKS || !!v?.trim(), t("workflow_node.deploy.form.ssh_jks_storepass.tooltip")), - preCommand: z - .string() - .max(20480, t("common.errmsg.string_max", { max: 20480 })) - .nullish(), - postCommand: z - .string() - .max(20480, t("common.errmsg.string_max", { max: 20480 })) - .nullish(), - useSCP: z.boolean().nullish(), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldFormat = Form.useWatch("format", formInst); - const fieldCertPath = Form.useWatch("certPath", formInst); - - const handleFormatSelect = (value: string) => { - if (fieldFormat === value) return; - - switch (value) { - case FORMAT_PEM: - { - if (/(.pfx|.jks)$/.test(fieldCertPath)) { - formInst.setFieldValue("certPath", fieldCertPath.replace(/(.pfx|.jks)$/, ".crt")); - } - } - break; - - case FORMAT_PFX: - { - if (/(.crt|.jks)$/.test(fieldCertPath)) { - formInst.setFieldValue("certPath", fieldCertPath.replace(/(.crt|.jks)$/, ".pfx")); - } - } - break; - - case FORMAT_JKS: - { - if (/(.crt|.pfx)$/.test(fieldCertPath)) { - formInst.setFieldValue("certPath", fieldCertPath.replace(/(.crt|.pfx)$/, ".jks")); - } - } - break; - } - }; - - const handlePresetPreScriptClick = (key: string) => { - switch (key) { - case "sh_backup_files": - case "ps_backup_files": - { - const presetScriptParams = { - certPath: formInst.getFieldValue("certPath"), - keyPath: formInst.getFieldValue("keyPath"), - }; - formInst.setFieldValue("preCommand", initPresetScript(key, presetScriptParams)); - } - break; - } - }; - - const handlePresetPostScriptClick = (key: string) => { - switch (key) { - case "sh_reload_nginx": - { - formInst.setFieldValue("postCommand", initPresetScript(key)); - } - break; - - case "sh_replace_synologydsm_ssl": - case "sh_replace_fnos_ssl": - case "sh_replace_qnap_ssl": - { - const presetScriptParams = { - certPath: formInst.getFieldValue("certPath"), - certPathForServerOnly: formInst.getFieldValue("certPathForServerOnly"), - certPathForIntermediaOnly: formInst.getFieldValue("certPathForIntermediaOnly"), - keyPath: formInst.getFieldValue("keyPath"), - }; - formInst.setFieldValue("postCommand", initPresetScript(key, presetScriptParams)); - } - break; - - case "ps_binding_iis": - case "ps_binding_netsh": - case "ps_binding_rdp": - { - const presetScriptParams = { - certPath: formInst.getFieldValue("certPath"), - pfxPassword: formInst.getFieldValue("pfxPassword"), - }; - formInst.setFieldValue("postCommand", initPresetScript(key, presetScriptParams)); - } - break; - } - }; - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - - - - } - > - - - - - } - > - - - - } - > - - - - } - > - - - - - - } - > - - - - - - } - > - - - - } - > - - - - } - > - - - - - - - - - - - - - - - - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormSSHConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormSafeLineConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormSafeLineConfig.tsx deleted file mode 100644 index 70756560a..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormSafeLineConfig.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; - -type DeployNodeConfigFormSafeLineConfigFieldValues = Nullish<{ - resourceType: string; - certificateId?: string | number; -}>; - -export interface DeployNodeConfigFormSafeLineConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormSafeLineConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormSafeLineConfigFieldValues) => void; -} - -const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; - -const initFormModel = (): DeployNodeConfigFormSafeLineConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_CERTIFICATE, - certificateId: "", - }; -}; - -const DeployNodeConfigFormSafeLineConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormSafeLineConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t("workflow_node.deploy.form.safeline_resource_type.placeholder")), - certificateId: z - .union([z.string(), z.number().int()]) - .nullish() - .refine((v) => { - if (fieldResourceType !== RESOURCE_TYPE_CERTIFICATE) return true; - return /^\d+$/.test(v + "") && +v! > 0; - }, t("workflow_node.deploy.form.safeline_certificate_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormSafeLineConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCDNConfig.tsx deleted file mode 100644 index 1efd647e0..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCDNConfig.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormTencentCloudCDNConfigFieldValues = Nullish<{ - endpoint?: string; - domain: string; -}>; - -export interface DeployNodeConfigFormTencentCloudCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormTencentCloudCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormTencentCloudCDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormTencentCloudCDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormTencentCloudCDNConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormTencentCloudCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - endpoint: z.string().nullish(), - domain: z - .string(t("workflow_node.deploy.form.tencentcloud_cdn_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormTencentCloudCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCLBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCLBConfig.tsx deleted file mode 100644 index cc9486d83..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCLBConfig.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormTencentCloudCLBConfigFieldValues = Nullish<{ - endpoint?: string; - region: string; - resourceType: string; - loadbalancerId?: string; - listenerId?: string; - domain?: string; -}>; - -export interface DeployNodeConfigFormTencentCloudCLBConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormTencentCloudCLBConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormTencentCloudCLBConfigFieldValues) => void; -} - -const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; -const RESOURCE_TYPE_LISTENER = "listener" as const; -const RESOURCE_TYPE_RULEDOMAIN = "ruledomain" as const; -const RESOURCE_TYPE_VIA_SSLDEPLOY = "ssl-deploy" as const; - -const initFormModel = (): DeployNodeConfigFormTencentCloudCLBConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_LISTENER, - }; -}; - -const DeployNodeConfigFormTencentCloudCLBConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormTencentCloudCLBConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - endpoint: z.string().nullish(), - resourceType: z.literal( - [RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER, RESOURCE_TYPE_RULEDOMAIN, RESOURCE_TYPE_VIA_SSLDEPLOY], - t("workflow_node.deploy.form.tencentcloud_clb_resource_type.placeholder") - ), - region: z - .string(t("workflow_node.deploy.form.tencentcloud_clb_region.placeholder")) - .nonempty(t("workflow_node.deploy.form.tencentcloud_clb_region.placeholder")), - loadbalancerId: z - .string() - .min(1, t("workflow_node.deploy.form.tencentcloud_clb_loadbalancer_id.placeholder")) - .max(64, t("common.errmsg.string_max", { max: 64 })), - listenerId: z - .string() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish() - .refine( - (v) => ![RESOURCE_TYPE_LISTENER, RESOURCE_TYPE_RULEDOMAIN, RESOURCE_TYPE_VIA_SSLDEPLOY].includes(fieldResourceType) || !!v?.trim(), - t("workflow_node.deploy.form.tencentcloud_clb_listener_id.placeholder") - ), - domain: z - .string() - .nullish() - .refine((v) => RESOURCE_TYPE_RULEDOMAIN !== fieldResourceType || validDomainName(v!, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - - - - - - - } - > - - - - - } - > - - - - - - } - > - - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormTencentCloudCLBConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCOSConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCOSConfig.tsx deleted file mode 100644 index 900c751ff..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCOSConfig.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormTencentCloudCOSConfigFieldValues = Nullish<{ - region: string; - bucket: string; - domain: string; -}>; - -export interface DeployNodeConfigFormTencentCloudCOSConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormTencentCloudCOSConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormTencentCloudCOSConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormTencentCloudCOSConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormTencentCloudCOSConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormTencentCloudCOSConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z - .string(t("workflow_node.deploy.form.tencentcloud_cos_region.placeholder")) - .nonempty(t("workflow_node.deploy.form.tencentcloud_cos_region.placeholder")), - bucket: z - .string(t("workflow_node.deploy.form.tencentcloud_cos_bucket.placeholder")) - .nonempty(t("workflow_node.deploy.form.tencentcloud_cos_bucket.placeholder")), - domain: z.string(t("workflow_node.deploy.form.tencentcloud_cos_domain.placeholder")).refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormTencentCloudCOSConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCSSConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCSSConfig.tsx deleted file mode 100644 index 5adcf7cb9..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCSSConfig.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormTencentCloudCSSConfigFieldValues = Nullish<{ - endpoint?: string; - domain: string; -}>; - -export interface DeployNodeConfigFormTencentCloudCSSConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormTencentCloudCSSConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormTencentCloudCSSConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormTencentCloudCSSConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormTencentCloudCSSConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormTencentCloudCSSConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - endpoint: z.string().nullish(), - domain: z - .string(t("workflow_node.deploy.form.tencentcloud_css_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormTencentCloudCSSConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudECDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudECDNConfig.tsx deleted file mode 100644 index 779544601..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudECDNConfig.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormTencentCloudECDNConfigFieldValues = Nullish<{ - endpoint?: string; - domain?: string; -}>; - -export interface DeployNodeConfigFormTencentCloudECDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormTencentCloudECDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormTencentCloudECDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormTencentCloudECDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormTencentCloudECDNConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormTencentCloudECDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - endpoint: z.string().nullish(), - domain: z - .string(t("workflow_node.deploy.form.tencentcloud_ecdn_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormTencentCloudECDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudEOConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudEOConfig.tsx deleted file mode 100644 index b9381440e..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudEOConfig.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; -import MultipleSplitValueInput from "@/components/MultipleSplitValueInput"; - -type DeployNodeConfigFormTencentCloudEOConfigFieldValues = Nullish<{ - endpoint?: string; - zoneId: string; - domains: string; -}>; - -export interface DeployNodeConfigFormTencentCloudEOConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormTencentCloudEOConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormTencentCloudEOConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormTencentCloudEOConfigFieldValues => { - return {}; -}; - -const MULTIPLE_INPUT_SEPARATOR = ";"; - -const DeployNodeConfigFormTencentCloudEOConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormTencentCloudEOConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - endpoint: z.string().nullish(), - zoneId: z - .string(t("workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder")) - .nonempty(t("workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder")), - domains: z.string(t("workflow_node.deploy.form.tencentcloud_eo_domains.placeholder")).refine((v) => { - if (!v) return false; - return String(v) - .split(MULTIPLE_INPUT_SEPARATOR) - .every((e) => validDomainName(e, { allowWildcard: true })); - }, t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormTencentCloudEOConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudGAAPConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudGAAPConfig.tsx deleted file mode 100644 index 44360457b..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudGAAPConfig.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; - -type DeployNodeConfigFormTencentCloudGAAPConfigFieldValues = Nullish<{ - endpoint?: string; - resourceType: string; - proxyId?: string; - listenerId?: string; -}>; - -export interface DeployNodeConfigFormTencentCloudGAAPConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormTencentCloudGAAPConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormTencentCloudGAAPConfigFieldValues) => void; -} - -const RESOURCE_TYPE_LISTENER = "listener" as const; - -const initFormModel = (): DeployNodeConfigFormTencentCloudGAAPConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_LISTENER, - listenerId: "", - }; -}; - -const DeployNodeConfigFormTencentCloudGAAPConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormTencentCloudGAAPConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - endpoint: z.string().nullish(), - resourceType: z.literal(RESOURCE_TYPE_LISTENER, t("workflow_node.deploy.form.tencentcloud_gaap_resource_type.placeholder")), - proxyId: z.string().nullish(), - listenerId: z - .string() - .nullish() - .refine( - (v) => ![RESOURCE_TYPE_LISTENER].includes(fieldResourceType) || !!v?.trim(), - t("workflow_node.deploy.form.tencentcloud_gaap_listener_id.placeholder") - ), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - - - - - } - > - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormTencentCloudGAAPConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSCFConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSCFConfig.tsx deleted file mode 100644 index ff0b60402..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSCFConfig.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormTencentCloudSCFConfigFieldValues = Nullish<{ - endpoint?: string; - region: string; - domain: string; -}>; - -export interface DeployNodeConfigFormTencentCloudSCFConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormTencentCloudSCFConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormTencentCloudSCFConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormTencentCloudSCFConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormTencentCloudSCFConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormTencentCloudSCFConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - endpoint: z.string().nullish(), - region: z - .string(t("workflow_node.deploy.form.tencentcloud_scf_region.placeholder")) - .nonempty(t("workflow_node.deploy.form.tencentcloud_scf_region.placeholder")), - domain: z.string(t("workflow_node.deploy.form.tencentcloud_scf_domain.placeholder")).refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormTencentCloudSCFConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSSLConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSSLConfig.tsx deleted file mode 100644 index 377734c49..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSSLConfig.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type DeployNodeConfigFormTencentCloudSSLConfigFieldValues = Nullish<{ - endpoint?: string; -}>; - -export interface DeployNodeConfigFormTencentCloudSSLConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormTencentCloudSSLConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormTencentCloudSSLConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormTencentCloudSSLConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormTencentCloudSSLConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormTencentCloudSSLConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - endpoint: z.string().nullish(), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormTencentCloudSSLConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSSLDeployConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSSLDeployConfig.tsx deleted file mode 100644 index 0e5bcc925..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSSLDeployConfig.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Alert, AutoComplete, Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import MultipleSplitValueInput from "@/components/MultipleSplitValueInput"; - -type DeployNodeConfigFormTencentCloudSSLDeployConfigFieldValues = Nullish<{ - endpoint?: string; - region: string; - resourceType: string; - resourceIds: string; -}>; - -export interface DeployNodeConfigFormTencentCloudSSLDeployConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormTencentCloudSSLDeployConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormTencentCloudSSLDeployConfigFieldValues) => void; -} - -const MULTIPLE_INPUT_SEPARATOR = ";"; - -const initFormModel = (): DeployNodeConfigFormTencentCloudSSLDeployConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormTencentCloudSSLDeployConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormTencentCloudSSLDeployConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - endpoint: z.string().nullish(), - region: z - .string(t("workflow_node.deploy.form.tencentcloud_ssldeploy_region.placeholder")) - .nonempty(t("workflow_node.deploy.form.tencentcloud_ssldeploy_region.placeholder")), - resourceType: z - .string(t("workflow_node.deploy.form.tencentcloud_ssldeploy_resource_type.placeholder")) - .nonempty(t("workflow_node.deploy.form.tencentcloud_ssldeploy_resource_type.placeholder")), - resourceIds: z.string(t("workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.placeholder")).refine((v) => { - if (!v) return false; - return String(v) - .split(MULTIPLE_INPUT_SEPARATOR) - .every((e) => /^[A-Za-z0-9*._\-|]+$/.test(e)); - }, t("workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.errmsg.invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - } /> - - - } - > - - - - } - > - - - - } - > - ({ value }))} - placeholder={t("workflow_node.deploy.form.tencentcloud_ssldeploy_resource_type.placeholder")} - filterOption={(inputValue, option) => option!.value.toLowerCase().includes(inputValue.toLowerCase())} - /> - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormTencentCloudSSLDeployConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSSLUpdateConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSSLUpdateConfig.tsx deleted file mode 100644 index 98d994b8d..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSSLUpdateConfig.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Alert, Form, type FormInstance, Input, Switch } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import MultipleSplitValueInput from "@/components/MultipleSplitValueInput"; - -type DeployNodeConfigFormTencentCloudSSLUpdateConfigFieldValues = Nullish<{ - endpoint?: string; - certificateId: string; - resourceTypes: string; - resourceRegions?: string; - isReplaced?: boolean; -}>; - -export interface DeployNodeConfigFormTencentCloudSSLUpdateConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormTencentCloudSSLUpdateConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormTencentCloudSSLUpdateConfigFieldValues) => void; -} - -const MULTIPLE_INPUT_SEPARATOR = ";"; - -const initFormModel = (): DeployNodeConfigFormTencentCloudSSLUpdateConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormTencentCloudSSLUpdateConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormTencentCloudSSLUpdateConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - endpoint: z.string().nullish(), - certificateId: z - .string(t("workflow_node.deploy.form.tencentcloud_sslupdate_certificate_id.placeholder")) - .nonempty(t("workflow_node.deploy.form.tencentcloud_sslupdate_certificate_id.placeholder")), - resourceTypes: z.string(t("workflow_node.deploy.form.tencentcloud_sslupdate_resource_types.placeholder")).refine((v) => { - if (!v) return false; - return String(v) - .split(MULTIPLE_INPUT_SEPARATOR) - .every((e) => !!e.trim()); - }, t("workflow_node.deploy.form.tencentcloud_sslupdate_resource_types.placeholder")), - resourceRegions: z - .string(t("workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.placeholder")) - .nullish() - .refine((v) => { - if (!v) return true; - return String(v) - .split(MULTIPLE_INPUT_SEPARATOR) - .every((e) => !!e.trim()); - }, t("workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.placeholder")), - isReplaced: z.boolean().nullish(), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - } /> - - - } - > - - - - } - > - - - - } - > - - - - } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormTencentCloudSSLUpdateConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudVODConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudVODConfig.tsx deleted file mode 100644 index 68427535b..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudVODConfig.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormTencentCloudVODConfigFieldValues = Nullish<{ - endpoint?: string; - subAppId?: string | number; - domain: string; -}>; - -export interface DeployNodeConfigFormTencentCloudVODConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormTencentCloudVODConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormTencentCloudVODConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormTencentCloudVODConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormTencentCloudVODConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormTencentCloudVODConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - endpoint: z.string().nullish(), - subAppId: z - .union([z.string(), z.number().int()]) - .nullish() - .refine((v) => { - if (v == null) return true; - return /^\d+$/.test(v + "") && +v > 0; - }, t("workflow_node.deploy.form.tencentcloud_vod_sub_app_id.placeholder")), - domain: z.string(t("workflow_node.deploy.form.tencentcloud_vod_domain.placeholder")).refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormTencentCloudVODConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudWAFConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudWAFConfig.tsx deleted file mode 100644 index cc8a7aae1..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudWAFConfig.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormTencentCloudWAFConfigFieldValues = Nullish<{ - endpoint?: string; - region: string; - domain: string; - domainId: string; - instanceId: string; -}>; - -export interface DeployNodeConfigFormTencentCloudWAFConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormTencentCloudWAFConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormTencentCloudWAFConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormTencentCloudWAFConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormTencentCloudWAFConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormTencentCloudWAFConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - endpoint: z.string().nullish(), - region: z - .string(t("workflow_node.deploy.form.tencentcloud_waf_region.placeholder")) - .nonempty(t("workflow_node.deploy.form.tencentcloud_waf_region.placeholder")), - domain: z.string(t("workflow_node.deploy.form.tencentcloud_waf_domain.placeholder")).refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")), - domainId: z - .string(t("workflow_node.deploy.form.tencentcloud_waf_domain_id.placeholder")) - .nonempty(t("workflow_node.deploy.form.tencentcloud_waf_domain_id.placeholder")), - instanceId: z - .string(t("workflow_node.deploy.form.tencentcloud_waf_instance_id.placeholder")) - .nonempty(t("workflow_node.deploy.form.tencentcloud_waf_instance_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - - - } - > - - - - } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormTencentCloudWAFConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormUCloudUCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormUCloudUCDNConfig.tsx deleted file mode 100644 index 9cf2906db..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormUCloudUCDNConfig.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type DeployNodeConfigFormUCloudUCDNConfigFieldValues = Nullish<{ - domainId: string; -}>; - -export interface DeployNodeConfigFormUCloudUCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormUCloudUCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormUCloudUCDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormUCloudUCDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormUCloudUCDNConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormUCloudUCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domainId: z - .string(t("workflow_node.deploy.form.ucloud_ucdn_domain_id.placeholder")) - .nonempty(t("workflow_node.deploy.form.ucloud_ucdn_domain_id.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormUCloudUCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormUCloudUS3Config.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormUCloudUS3Config.tsx deleted file mode 100644 index b01722ea2..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormUCloudUS3Config.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormUCloudUS3ConfigFieldValues = Nullish<{ - region: string; - bucket: string; - domain: string; -}>; - -export interface DeployNodeConfigFormUCloudUS3ConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormUCloudUS3ConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormUCloudUS3ConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormUCloudUS3ConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormUCloudUS3Config = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormUCloudUS3ConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z.string(t("workflow_node.deploy.form.ucloud_us3_region.placeholder")).nonempty(t("workflow_node.deploy.form.ucloud_us3_region.placeholder")), - bucket: z.string(t("workflow_node.deploy.form.ucloud_us3_bucket.placeholder")).nonempty(t("workflow_node.deploy.form.ucloud_us3_bucket.placeholder")), - domain: z.string(t("workflow_node.deploy.form.ucloud_us3_domain.placeholder")).refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormUCloudUS3Config; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormUniCloudWebHostConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormUniCloudWebHostConfig.tsx deleted file mode 100644 index 056a4ba71..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormUniCloudWebHostConfig.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Alert, Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormUniCloudWebHostConfigFieldValues = Nullish<{ - spaceProvider: string; - spaceId: string; - domain: string; -}>; - -export interface DeployNodeConfigFormUniCloudWebHostConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormUniCloudWebHostConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormUniCloudWebHostConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormUniCloudWebHostConfigFieldValues => { - return { - spaceProvider: "tencent", - spaceId: "", - domain: "", - }; -}; - -const DeployNodeConfigFormUniCloudWebHostConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormUniCloudWebHostConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - spaceProvider: z.string().nonempty(t("workflow_node.deploy.form.unicloud_webhost_space_provider.placeholder")), - spaceId: z.string().nonempty(t("workflow_node.deploy.form.unicloud_webhost_space_id.placeholder")), - domain: z.string().refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - } /> - - - - - - - - - -
- ); -}; - -export default DeployNodeConfigFormUniCloudWebHostConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormUpyunCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormUpyunCDNConfig.tsx deleted file mode 100644 index 13b802479..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormUpyunCDNConfig.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Alert, Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormUpyunCDNConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormUpyunCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormUpyunCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormUpyunCDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormUpyunCDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormUpyunCDNConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormUpyunCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z - .string(t("workflow_node.deploy.form.upyun_cdn_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - } /> - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormUpyunCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormUpyunFileConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormUpyunFileConfig.tsx deleted file mode 100644 index 1232b642a..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormUpyunFileConfig.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Alert, Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormUpyunFileConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormUpyunFileConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormUpyunFileConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormUpyunFileConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormUpyunFileConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormUpyunFileConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormUpyunFileConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z.string(t("workflow_node.deploy.form.upyun_file_domain.placeholder")).refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - } /> - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormUpyunFileConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineALBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineALBConfig.tsx deleted file mode 100644 index 0603bf9fa..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineALBConfig.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormVolcEngineALBConfigFieldValues = Nullish<{ - region: string; - resourceType: string; - loadbalancerId?: string; - listenerId?: string; - domain?: string; -}>; - -export interface DeployNodeConfigFormVolcEngineALBConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormVolcEngineALBConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormVolcEngineALBConfigFieldValues) => void; -} - -const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; -const RESOURCE_TYPE_LISTENER = "listener" as const; - -const initFormModel = (): DeployNodeConfigFormVolcEngineALBConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_LISTENER, - }; -}; - -const DeployNodeConfigFormVolcEngineALBConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormVolcEngineALBConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z - .string(t("workflow_node.deploy.form.volcengine_alb_region.placeholder")) - .nonempty(t("workflow_node.deploy.form.volcengine_alb_region.placeholder")), - resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.volcengine_alb_resource_type.placeholder")), - loadbalancerId: z - .string() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish() - .refine( - (v) => ![RESOURCE_TYPE_LOADBALANCER].includes(fieldResourceType) || !!v?.trim(), - t("workflow_node.deploy.form.volcengine_alb_loadbalancer_id.placeholder") - ), - listenerId: z - .string() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish() - .refine( - (v) => ![RESOURCE_TYPE_LISTENER].includes(fieldResourceType) || !!v?.trim(), - t("workflow_node.deploy.form.volcengine_alb_listener_id.placeholder") - ), - domain: z - .string() - .nullish() - .refine((v) => { - if (![RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].includes(fieldResourceType)) return true; - return !v || validDomainName(v!, { allowWildcard: true }); - }, t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - - - - - - } - > - - - - - - } - > - - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormVolcEngineALBConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCDNConfig.tsx deleted file mode 100644 index 175733c7d..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCDNConfig.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormVolcEngineCDNConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormVolcEngineCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormVolcEngineCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormVolcEngineCDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormVolcEngineCDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormVolcEngineCDNConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormVolcEngineCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z - .string(t("workflow_node.deploy.form.volcengine_cdn_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormVolcEngineCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCLBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCLBConfig.tsx deleted file mode 100644 index 39aa1a93f..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCLBConfig.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import Show from "@/components/Show"; - -type DeployNodeConfigFormVolcEngineCLBConfigFieldValues = Nullish<{ - region: string; - resourceType: string; - loadbalancerId?: string; - listenerId?: string; -}>; - -export interface DeployNodeConfigFormVolcEngineCLBConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormVolcEngineCLBConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormVolcEngineCLBConfigFieldValues) => void; -} - -const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; -const RESOURCE_TYPE_LISTENER = "listener" as const; - -const initFormModel = (): DeployNodeConfigFormVolcEngineCLBConfigFieldValues => { - return { - resourceType: RESOURCE_TYPE_LISTENER, - }; -}; - -const DeployNodeConfigFormVolcEngineCLBConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormVolcEngineCLBConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z - .string(t("workflow_node.deploy.form.volcengine_clb_region.placeholder")) - .nonempty(t("workflow_node.deploy.form.volcengine_clb_region.placeholder")), - resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.volcengine_clb_resource_type.placeholder")), - loadbalancerId: z - .string() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish() - .refine( - (v) => ![RESOURCE_TYPE_LOADBALANCER].includes(fieldResourceType) || !!v?.trim(), - t("workflow_node.deploy.form.volcengine_clb_loadbalancer_id.placeholder") - ), - listenerId: z - .string() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish() - .refine( - (v) => ![RESOURCE_TYPE_LISTENER].includes(fieldResourceType) || !!v?.trim(), - t("workflow_node.deploy.form.volcengine_clb_listener_id.placeholder") - ), - }); - const formRule = createSchemaFieldRule(formSchema); - - const fieldResourceType = Form.useWatch("resourceType", formInst); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - - - - - - } - > - - - - - - } - > - - - -
- ); -}; - -export default DeployNodeConfigFormVolcEngineCLBConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCertCenterConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCertCenterConfig.tsx deleted file mode 100644 index 9c53fe117..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCertCenterConfig.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type DeployNodeConfigFormVolcEngineCertCenterConfigFieldValues = Nullish<{ - region: string; -}>; - -export interface DeployNodeConfigFormVolcEngineCertCenterConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormVolcEngineCertCenterConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormVolcEngineCertCenterConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormVolcEngineCertCenterConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormVolcEngineCertCenterConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormVolcEngineCertCenterConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z - .string(t("workflow_node.deploy.form.volcengine_certcenter_region.placeholder")) - .nonempty(t("workflow_node.deploy.form.volcengine_certcenter_region.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - - -
- ); -}; - -export default DeployNodeConfigFormVolcEngineCertCenterConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineDCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineDCDNConfig.tsx deleted file mode 100644 index f8c983c16..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineDCDNConfig.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormVolcEngineDCDNConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormVolcEngineDCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormVolcEngineDCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormVolcEngineDCDNConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormVolcEngineDCDNConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormVolcEngineDCDNConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormVolcEngineDCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z - .string(t("workflow_node.deploy.form.volcengine_dcdn_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormVolcEngineDCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineImageXConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineImageXConfig.tsx deleted file mode 100644 index 88dfcedf1..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineImageXConfig.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormVolcEngineImageXConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormVolcEngineImageXConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormVolcEngineImageXConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormVolcEngineImageXConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormVolcEngineImageXConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormVolcEngineImageXConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormVolcEngineImageXConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z - .string(t("workflow_node.deploy.form.volcengine_imagex_region.placeholder")) - .nonempty(t("workflow_node.deploy.form.volcengine_imagex_region.placeholder")), - serviceId: z - .string(t("workflow_node.deploy.form.volcengine_imagex_service_id.placeholder")) - .nonempty(t("workflow_node.deploy.form.volcengine_imagex_service_id.placeholder")), - domain: z.string(t("workflow_node.deploy.form.volcengine_imagex_domain.placeholder")).refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormVolcEngineImageXConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineLiveConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineLiveConfig.tsx deleted file mode 100644 index 54cc9c239..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineLiveConfig.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormVolcEngineLiveConfigFieldValues = Nullish<{ - domain: string; -}>; - -export interface DeployNodeConfigFormVolcEngineLiveConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormVolcEngineLiveConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormVolcEngineLiveConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormVolcEngineLiveConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormVolcEngineLiveConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormVolcEngineLiveConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domain: z - .string(t("workflow_node.deploy.form.volcengine_live_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormVolcEngineLiveConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineTOSConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineTOSConfig.tsx deleted file mode 100644 index de47b85a1..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineTOSConfig.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormVolcEngineTOSConfigFieldValues = Nullish<{ - region: string; - bucket: string; - domain: string; -}>; - -export interface DeployNodeConfigFormVolcEngineTOSConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormVolcEngineTOSConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormVolcEngineTOSConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormVolcEngineTOSConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormVolcEngineTOSConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormVolcEngineTOSConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - region: z - .string(t("workflow_node.deploy.form.volcengine_tos_region.placeholder")) - .nonempty(t("workflow_node.deploy.form.volcengine_tos_region.placeholder")), - bucket: z - .string(t("workflow_node.deploy.form.volcengine_tos_bucket.placeholder")) - .nonempty(t("workflow_node.deploy.form.volcengine_tos_bucket.placeholder")), - domain: z.string(t("workflow_node.deploy.form.volcengine_tos_domain.placeholder")).refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormVolcEngineTOSConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx deleted file mode 100644 index 9867f2033..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import MultipleSplitValueInput from "@/components/MultipleSplitValueInput"; -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormWangsuCDNConfigFieldValues = Nullish<{ - domains: string; -}>; - -export interface DeployNodeConfigFormWangsuCDNConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormWangsuCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormWangsuCDNConfigFieldValues) => void; -} - -const MULTIPLE_INPUT_SEPARATOR = ";"; - -const initFormModel = (): DeployNodeConfigFormWangsuCDNConfigFieldValues => { - return { - domains: "", - }; -}; - -const DeployNodeConfigFormWangsuCDNConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormWangsuCDNConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domains: z - .string() - .nullish() - .refine((v) => { - if (!v) return false; - return String(v) - .split(MULTIPLE_INPUT_SEPARATOR) - .every((e) => validDomainName(e, { allowWildcard: true })); - }, t("workflow_node.deploy.form.wangsu_cdn_domains.placeholder")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormWangsuCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNProConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNProConfig.tsx deleted file mode 100644 index 34d303fb7..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNProConfig.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Select } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validDomainName } from "@/utils/validators"; - -type DeployNodeConfigFormWangsuCDNProConfigFieldValues = Nullish<{ - environment: string; - domain: string; - certificateId?: string; - webhookId?: string; -}>; - -export interface DeployNodeConfigFormWangsuCDNProConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormWangsuCDNProConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormWangsuCDNProConfigFieldValues) => void; -} - -const ENVIRONMENT_PRODUCTION = "production" as const; -const ENVIRONMENT_STAGING = "stating" as const; - -const initFormModel = (): DeployNodeConfigFormWangsuCDNProConfigFieldValues => { - return { - environment: ENVIRONMENT_PRODUCTION, - }; -}; - -const DeployNodeConfigFormWangsuCDNProConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormWangsuCDNProConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - resourceType: z.literal([ENVIRONMENT_PRODUCTION, ENVIRONMENT_STAGING], t("workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder")), - domain: z - .string(t("workflow_node.deploy.form.wangsu_cdnpro_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - certificateId: z.string().nullish(), - webhookId: z.string().nullish(), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- - - - - } - > - - - - } - > - - - - } - > - - -
- ); -}; - -export default DeployNodeConfigFormWangsuCDNProConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCertificateConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCertificateConfig.tsx deleted file mode 100644 index dcee2ec16..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCertificateConfig.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type DeployNodeConfigFormWangsuCertificateConfigFieldValues = Nullish<{ - certificateId?: string; -}>; - -export interface DeployNodeConfigFormWangsuCertificateConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormWangsuCertificateConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormWangsuCertificateConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormWangsuCertificateConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormWangsuCertificateConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: DeployNodeConfigFormWangsuCertificateConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - certificateId: z.string().nullish(), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default DeployNodeConfigFormWangsuCertificateConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormWebhookConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormWebhookConfig.tsx deleted file mode 100644 index 68d1c26a9..000000000 --- a/ui/src/components/workflow/node/DeployNodeConfigFormWebhookConfig.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Alert, Form, type FormInstance } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import CodeInput from "@/components/CodeInput"; - -type DeployNodeConfigFormWebhookConfigFieldValues = Nullish<{ - webhookData: string; -}>; - -export interface DeployNodeConfigFormWebhookConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: DeployNodeConfigFormWebhookConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormWebhookConfigFieldValues) => void; -} - -const initFormModel = (): DeployNodeConfigFormWebhookConfigFieldValues => { - return {}; -}; - -const DeployNodeConfigFormWebhookConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormWebhookConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - webhookData: z - .string() - .nullish() - .refine((v) => { - if (!v) return true; - - try { - const obj = JSON.parse(v); - return typeof obj === "object" && !Array.isArray(obj); - } catch { - return false; - } - }, t("workflow_node.deploy.form.webhook_data.errmsg.json_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleWebhookDataBlur = () => { - const value = formInst.getFieldValue("webhookData"); - try { - const json = JSON.stringify(JSON.parse(value), null, 2); - formInst.setFieldValue("webhookData", json); - } catch { - return; - } - }; - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - - } /> - -
- ); -}; - -export default DeployNodeConfigFormWebhookConfig; diff --git a/ui/src/components/workflow/node/EndNode.tsx b/ui/src/components/workflow/node/EndNode.tsx deleted file mode 100644 index de419bcf6..000000000 --- a/ui/src/components/workflow/node/EndNode.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { memo } from "react"; -import { useTranslation } from "react-i18next"; -import { Typography } from "antd"; - -const EndNode = () => { - const { t } = useTranslation(); - - return ( -
-
-
- {t("workflow_node.end.label")} -
-
- ); -}; - -export default memo(EndNode); diff --git a/ui/src/components/workflow/node/ExecuteResultBranchNode.tsx b/ui/src/components/workflow/node/ExecuteResultBranchNode.tsx deleted file mode 100644 index 550fac2d3..000000000 --- a/ui/src/components/workflow/node/ExecuteResultBranchNode.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { memo } from "react"; -import { theme } from "antd"; - -import { type WorkflowNode } from "@/domain/workflow"; - -import AddNode from "./AddNode"; -import WorkflowElement from "../WorkflowElement"; -import { type SharedNodeProps } from "./_SharedNode"; - -export interface BrandNodeProps extends SharedNodeProps {} - -const ExecuteResultBranchNode = ({ node, disabled }: BrandNodeProps) => { - const { token: themeToken } = theme.useToken(); - - const renderBranch = (node: WorkflowNode, branchNodeId?: string, branchIndex?: number) => { - const elements: JSX.Element[] = []; - - let current = node as typeof node | undefined; - while (current) { - elements.push(); - current = current.next; - } - - return elements; - }; - - return ( - <> -
- {node.branches?.map((branch, index) => ( -
- {index == 0 && ( - <> -
-
- - )} - {node.branches && index == node.branches.length - 1 && ( - <> -
-
- - )} -
{renderBranch(branch, node.id, index)}
-
- ))} -
- - - - ); -}; - -export default memo(ExecuteResultBranchNode); diff --git a/ui/src/components/workflow/node/ExecuteResultNode.tsx b/ui/src/components/workflow/node/ExecuteResultNode.tsx deleted file mode 100644 index c556f6a0b..000000000 --- a/ui/src/components/workflow/node/ExecuteResultNode.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { memo } from "react"; -import { IconCircleCheck, IconCircleX, IconDotsVertical } from "@tabler/icons-react"; -import { Button, Card, Popover, theme } from "antd"; - -import { WorkflowNodeType } from "@/domain/workflow"; -import SharedNode, { type SharedNodeProps } from "./_SharedNode"; -import AddNode from "./AddNode"; - -export interface ConditionNodeProps extends SharedNodeProps { - branchId: string; - branchIndex: number; -} - -const ExecuteResultNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeProps) => { - const { token: themeToken } = theme.useToken(); - - return ( - <> - } variant="text" />} - /> - } - placement="rightTop" - > -
- -
-
- {node.type === WorkflowNodeType.ExecuteSuccess ? ( - - ) : ( - - )} - -
-
-
-
-
- - - - ); -}; - -export default memo(ExecuteResultNode); diff --git a/ui/src/components/workflow/node/MonitorNode.tsx b/ui/src/components/workflow/node/MonitorNode.tsx deleted file mode 100644 index 21167a698..000000000 --- a/ui/src/components/workflow/node/MonitorNode.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { memo, useMemo, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Flex, Typography } from "antd"; -import { produce } from "immer"; - -import { type WorkflowNodeConfigForMonitor, WorkflowNodeType } from "@/domain/workflow"; -import { useZustandShallowSelector } from "@/hooks"; -import { useWorkflowStore } from "@/stores/workflow"; - -import SharedNode, { type SharedNodeProps } from "./_SharedNode"; -import MonitorNodeConfigForm, { type MonitorNodeConfigFormInstance } from "./MonitorNodeConfigForm"; - -export interface MonitorNodeProps extends SharedNodeProps {} - -const MonitorNode = ({ node, disabled }: MonitorNodeProps) => { - if (node.type !== WorkflowNodeType.Monitor) { - console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Monitor}`); - } - - const { t } = useTranslation(); - - const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"])); - - const formRef = useRef(null); - const [formPending, setFormPending] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForMonitor; - - const [drawerOpen, setDrawerOpen] = useState(false); - - const wrappedEl = useMemo(() => { - if (node.type !== WorkflowNodeType.Monitor) { - console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Monitor}`); - } - - if (!node.validated) { - return {t("workflow_node.action.configure_node")}; - } - - const config = (node.config as WorkflowNodeConfigForMonitor) ?? {}; - return ( - - {config.domain || config.host || ""} - - ); - }, [node]); - - const handleDrawerConfirm = async () => { - setFormPending(true); - try { - await formRef.current!.validateFields(); - } catch (err) { - setFormPending(false); - throw err; - } - - try { - const newValues = getFormValues(); - const newNode = produce(node, (draft) => { - draft.config = { - ...newValues, - }; - draft.validated = true; - }); - await updateNode(newNode); - } finally { - setFormPending(false); - } - }; - - return ( - <> - setDrawerOpen(true)}> - {wrappedEl} - - - setDrawerOpen(open)} - > - - - - ); -}; - -export default memo(MonitorNode); diff --git a/ui/src/components/workflow/node/MonitorNodeConfigForm.tsx b/ui/src/components/workflow/node/MonitorNodeConfigForm.tsx deleted file mode 100644 index dc845db4a..000000000 --- a/ui/src/components/workflow/node/MonitorNodeConfigForm.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { forwardRef, memo, useImperativeHandle } from "react"; -import { useTranslation } from "react-i18next"; -import { Alert, Form, type FormInstance, Input, InputNumber } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { type WorkflowNodeConfigForMonitor, defaultNodeConfigForMonitor } from "@/domain/workflow"; -import { useAntdForm } from "@/hooks"; -import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators"; - -type MonitorNodeConfigFormFieldValues = Partial; - -export interface MonitorNodeConfigFormProps { - className?: string; - style?: React.CSSProperties; - disabled?: boolean; - initialValues?: MonitorNodeConfigFormFieldValues; - onValuesChange?: (values: MonitorNodeConfigFormFieldValues) => void; -} - -export interface MonitorNodeConfigFormInstance { - getFieldsValue: () => ReturnType["getFieldsValue"]>; - resetFields: FormInstance["resetFields"]; - validateFields: FormInstance["validateFields"]; -} - -const initFormModel = (): MonitorNodeConfigFormFieldValues => { - return defaultNodeConfigForMonitor(); -}; - -const MonitorNodeConfigForm = forwardRef( - ({ className, style, disabled, initialValues, onValuesChange }, ref) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - host: z.string().refine((v) => { - return validDomainName(v) || validIPv4Address(v) || validIPv6Address(v); - }, t("common.errmsg.host_invalid")), - port: z.preprocess( - (v) => Number(v), - z - .number() - .int(t("workflow_node.monitor.form.port.placeholder")) - .refine((v) => validPortNumber(v), t("common.errmsg.port_invalid")) - ), - domain: z - .string() - .nullish() - .refine((v) => { - if (!v) return true; - return validDomainName(v); - }, t("common.errmsg.domain_invalid")), - requestPath: z.string().nullish(), - }); - const formRule = createSchemaFieldRule(formSchema); - const { form: formInst, formProps } = useAntdForm({ - name: "workflowNodeMonitorConfigForm", - initialValues: initialValues ?? initFormModel(), - }); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values as MonitorNodeConfigFormFieldValues); - }; - - useImperativeHandle(ref, () => { - return { - getFieldsValue: () => { - return formInst.getFieldsValue(true); - }, - resetFields: (fields) => { - return formInst.resetFields(fields as (keyof MonitorNodeConfigFormFieldValues)[]); - }, - validateFields: (nameList, config) => { - return formInst.validateFields(nameList, config); - }, - } as MonitorNodeConfigFormInstance; - }); - - return ( -
- - } /> - - -
-
- - - -
- -
- - - -
-
- - - - - - - - -
- ); - } -); - -export default memo(MonitorNodeConfigForm); diff --git a/ui/src/components/workflow/node/NotifyNode.tsx b/ui/src/components/workflow/node/NotifyNode.tsx deleted file mode 100644 index 67a1229cd..000000000 --- a/ui/src/components/workflow/node/NotifyNode.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { memo, useMemo, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Avatar, Flex, Typography } from "antd"; -import { produce } from "immer"; - -import { notificationProvidersMap } from "@/domain/provider"; -import { type WorkflowNodeConfigForNotify, WorkflowNodeType } from "@/domain/workflow"; -import { useZustandShallowSelector } from "@/hooks"; -import { useWorkflowStore } from "@/stores/workflow"; - -import SharedNode, { type SharedNodeProps } from "./_SharedNode"; -import NotifyNodeConfigForm, { type NotifyNodeConfigFormInstance } from "./NotifyNodeConfigForm"; - -export interface NotifyNodeProps extends SharedNodeProps {} - -const NotifyNode = ({ node, disabled }: NotifyNodeProps) => { - if (node.type !== WorkflowNodeType.Notify) { - console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Notify}`); - } - - const { t } = useTranslation(); - - const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"])); - - const formRef = useRef(null); - const [formPending, setFormPending] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForNotify; - - const [drawerOpen, setDrawerOpen] = useState(false); - - const wrappedEl = useMemo(() => { - if (node.type !== WorkflowNodeType.Notify) { - console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Notify}`); - } - - if (!node.validated) { - return {t("workflow_node.action.configure_node")}; - } - - const config = (node.config as WorkflowNodeConfigForNotify) ?? {}; - const provider = notificationProvidersMap.get(config.provider); - return ( - - - {t(provider?.name ?? "\u00A0")} - - {config.subject ?? ""} - - - ); - }, [node]); - - const handleDrawerConfirm = async () => { - setFormPending(true); - try { - await formRef.current!.validateFields(); - } catch (err) { - setFormPending(false); - throw err; - } - - try { - const newValues = getFormValues(); - const newNode = produce(node, (draft) => { - draft.config = { - ...newValues, - }; - draft.validated = true; - }); - await updateNode(newNode); - } finally { - setFormPending(false); - } - }; - - return ( - <> - setDrawerOpen(true)}> - {wrappedEl} - - - setDrawerOpen(open)} - > - - - - ); -}; - -export default memo(NotifyNode); diff --git a/ui/src/components/workflow/node/NotifyNodeConfigForm.tsx b/ui/src/components/workflow/node/NotifyNodeConfigForm.tsx deleted file mode 100644 index 65112f8a8..000000000 --- a/ui/src/components/workflow/node/NotifyNodeConfigForm.tsx +++ /dev/null @@ -1,270 +0,0 @@ -import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { IconPlus } from "@tabler/icons-react"; -import { Button, Divider, Flex, Form, type FormInstance, Input, Switch, Typography } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import AccessEditDrawer from "@/components/access/AccessEditDrawer"; -import AccessSelect from "@/components/access/AccessSelect"; -import NotificationProviderSelect from "@/components/provider/NotificationProviderSelect"; -import Show from "@/components/Show"; -import { ACCESS_USAGES, NOTIFICATION_PROVIDERS, accessProvidersMap, notificationProvidersMap } from "@/domain/provider"; -import { type WorkflowNodeConfigForNotify, defaultNodeConfigForNotify } from "@/domain/workflow"; -import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks"; -import { useAccessesStore } from "@/stores/access"; - -import NotifyNodeConfigFormDiscordBotConfig from "./NotifyNodeConfigFormDiscordBotConfig"; -import NotifyNodeConfigFormEmailConfig from "./NotifyNodeConfigFormEmailConfig"; -import NotifyNodeConfigFormMattermostConfig from "./NotifyNodeConfigFormMattermostConfig"; -import NotifyNodeConfigFormSlackBotConfig from "./NotifyNodeConfigFormSlackBotConfig"; -import NotifyNodeConfigFormTelegramBotConfig from "./NotifyNodeConfigFormTelegramBotConfig"; -import NotifyNodeConfigFormWebhookConfig from "./NotifyNodeConfigFormWebhookConfig"; - -type NotifyNodeConfigFormFieldValues = Partial; - -export interface NotifyNodeConfigFormProps { - className?: string; - style?: React.CSSProperties; - disabled?: boolean; - initialValues?: NotifyNodeConfigFormFieldValues; - onValuesChange?: (values: NotifyNodeConfigFormFieldValues) => void; -} - -export interface NotifyNodeConfigFormInstance { - getFieldsValue: () => ReturnType["getFieldsValue"]>; - resetFields: FormInstance["resetFields"]; - validateFields: FormInstance["validateFields"]; -} - -const initFormModel = (): NotifyNodeConfigFormFieldValues => { - return defaultNodeConfigForNotify(); -}; - -const NotifyNodeConfigForm = forwardRef( - ({ className, style, disabled, initialValues, onValuesChange }, ref) => { - const { t } = useTranslation(); - - const { accesses } = useAccessesStore(useZustandShallowSelector("accesses")); - - const formSchema = z.object({ - subject: z - .string(t("workflow_node.notify.form.subject.placeholder")) - .min(1, t("workflow_node.notify.form.subject.placeholder")) - .max(1000, t("common.errmsg.string_max", { max: 1000 })), - message: z - .string(t("workflow_node.notify.form.message.placeholder")) - .min(1, t("workflow_node.notify.form.message.placeholder")) - .max(1000, t("common.errmsg.string_max", { max: 1000 })), - provider: z.string(t("workflow_node.notify.form.provider.placeholder")).nonempty(t("workflow_node.notify.form.provider.placeholder")), - providerAccessId: z - .string(t("workflow_node.notify.form.provider_access.placeholder")) - .nonempty(t("workflow_node.notify.form.provider_access.placeholder")), - providerConfig: z.any().nullish(), - skipOnAllPrevSkipped: z.boolean().nullish(), - }); - const formRule = createSchemaFieldRule(formSchema); - const { form: formInst, formProps } = useAntdForm({ - name: "workflowNodeNotifyConfigForm", - initialValues: initialValues ?? initFormModel(), - }); - - const fieldProvider = Form.useWatch("provider", { form: formInst, preserve: true }); - const fieldProviderAccessId = Form.useWatch("providerAccessId", formInst); - - const [showProvider, setShowProvider] = useState(false); - useEffect(() => { - // 通常情况下每个授权信息只对应一个消息通知提供商,此时无需显示消息通知提供商字段; - // 如果对应多个,则显示。 - if (fieldProviderAccessId) { - const access = accesses.find((e) => e.id === fieldProviderAccessId); - const providers = Array.from(notificationProvidersMap.values()).filter((e) => e.provider === access?.provider); - setShowProvider(providers.length > 1); - } else { - setShowProvider(false); - } - }, [accesses, fieldProviderAccessId]); - - const [nestedFormInst] = Form.useForm(); - const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "workflowNodeNotifyConfigFormProviderConfigForm" }); - const nestedFormEl = useMemo(() => { - const nestedFormProps = { - form: nestedFormInst, - formName: nestedFormName, - disabled: disabled, - initialValues: initialValues?.providerConfig, - }; - - /* - 注意:如果追加新的子组件,请保持以 ASCII 排序。 - NOTICE: If you add new child component, please keep ASCII order. - */ - switch (fieldProvider) { - case NOTIFICATION_PROVIDERS.DISCORDBOT: - return ; - case NOTIFICATION_PROVIDERS.EMAIL: - return ; - case NOTIFICATION_PROVIDERS.MATTERMOST: - return ; - case NOTIFICATION_PROVIDERS.SLACKBOT: - return ; - case NOTIFICATION_PROVIDERS.TELEGRAMBOT: - return ; - case NOTIFICATION_PROVIDERS.WEBHOOK: - return ; - } - }, [disabled, initialValues?.providerConfig, fieldProvider, nestedFormInst, nestedFormName]); - - const handleProviderSelect = (value: string) => { - // 切换消息通知提供商时联动授权信息 - if (initialValues?.provider === value) { - formInst.setFieldValue("providerAccessId", initialValues?.providerAccessId); - onValuesChange?.(formInst.getFieldsValue(true)); - } else { - if (notificationProvidersMap.get(fieldProvider)?.provider !== notificationProvidersMap.get(value)?.provider) { - formInst.setFieldValue("providerAccessId", undefined); - onValuesChange?.(formInst.getFieldsValue(true)); - } - } - }; - - const handleProviderAccessSelect = (value: string) => { - // 切换授权信息时联动消息通知提供商 - const access = accesses.find((access) => access.id === value); - const provider = Array.from(notificationProvidersMap.values()).find((provider) => provider.provider === access?.provider); - if (fieldProvider !== provider?.type) { - formInst.setFieldValue("provider", provider?.type); - onValuesChange?.(formInst.getFieldsValue(true)); - } - }; - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values as NotifyNodeConfigFormFieldValues); - }; - - useImperativeHandle(ref, () => { - return { - getFieldsValue: () => { - const values = formInst.getFieldsValue(true); - values.providerConfig = nestedFormInst.getFieldsValue(); - return values; - }, - resetFields: (fields) => { - formInst.resetFields(fields); - - if (!!fields && fields.includes("providerConfig")) { - nestedFormInst.resetFields(fields); - } - }, - validateFields: (nameList, config) => { - const t1 = formInst.validateFields(nameList, config); - const t2 = nestedFormInst.validateFields(undefined, config); - return Promise.all([t1, t2]).then(() => t1); - }, - } as NotifyNodeConfigFormInstance; - }); - - return ( -
- - - - - - - - - - - - - - { - if (option.reserve !== "notification") return false; - - const provider = accessProvidersMap.get(option.provider); - return !!provider?.usages?.includes(ACCESS_USAGES.NOTIFICATION); - }} - /> - - - - - - - {t("workflow_node.notify.form.params_config.label")} - - - - {nestedFormEl} - - - - - {t("workflow_node.notify.form_anchor.strategy.title")} - - - - - - -
{t("workflow_node.notify.form.skip_on_all_prev_skipped.prefix")}
- - - -
{t("workflow_node.notify.form.skip_on_all_prev_skipped.suffix")}
-
-
-
- - ); - } -); - -export default memo(NotifyNodeConfigForm); diff --git a/ui/src/components/workflow/node/NotifyNodeConfigFormDiscordBotConfig.tsx b/ui/src/components/workflow/node/NotifyNodeConfigFormDiscordBotConfig.tsx deleted file mode 100644 index 649eeac52..000000000 --- a/ui/src/components/workflow/node/NotifyNodeConfigFormDiscordBotConfig.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type NotifyNodeConfigFormDiscordBotConfigFieldValues = Nullish<{ - channelId?: string; -}>; - -export interface NotifyNodeConfigFormDiscordBotConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: NotifyNodeConfigFormDiscordBotConfigFieldValues; - onValuesChange?: (values: NotifyNodeConfigFormDiscordBotConfigFieldValues) => void; -} - -const initFormModel = (): NotifyNodeConfigFormDiscordBotConfigFieldValues => { - return {}; -}; - -const NotifyNodeConfigFormDiscordBotConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: NotifyNodeConfigFormDiscordBotConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - channelId: z.string().nullish(), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default NotifyNodeConfigFormDiscordBotConfig; diff --git a/ui/src/components/workflow/node/NotifyNodeConfigFormEmailConfig.tsx b/ui/src/components/workflow/node/NotifyNodeConfigFormEmailConfig.tsx deleted file mode 100644 index 0fb941aea..000000000 --- a/ui/src/components/workflow/node/NotifyNodeConfigFormEmailConfig.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validEmailAddress } from "@/utils/validators"; - -type NotifyNodeConfigFormEmailConfigFieldValues = Nullish<{ - receiverAddress?: string; -}>; - -export interface NotifyNodeConfigFormEmailConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: NotifyNodeConfigFormEmailConfigFieldValues; - onValuesChange?: (values: NotifyNodeConfigFormEmailConfigFieldValues) => void; -} - -const initFormModel = (): NotifyNodeConfigFormEmailConfigFieldValues => { - return {}; -}; - -const NotifyNodeConfigFormEmailConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: NotifyNodeConfigFormEmailConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - receiverAddress: z - .string() - .nullish() - .refine((v) => { - if (!v) return true; - return validEmailAddress(v); - }, t("common.errmsg.email_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default NotifyNodeConfigFormEmailConfig; diff --git a/ui/src/components/workflow/node/NotifyNodeConfigFormMattermostConfig.tsx b/ui/src/components/workflow/node/NotifyNodeConfigFormMattermostConfig.tsx deleted file mode 100644 index c20e1fca1..000000000 --- a/ui/src/components/workflow/node/NotifyNodeConfigFormMattermostConfig.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type NotifyNodeConfigFormMattermostConfigFieldValues = Nullish<{ - channelId?: string; -}>; - -export interface NotifyNodeConfigFormMattermostConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: NotifyNodeConfigFormMattermostConfigFieldValues; - onValuesChange?: (values: NotifyNodeConfigFormMattermostConfigFieldValues) => void; -} - -const initFormModel = (): NotifyNodeConfigFormMattermostConfigFieldValues => { - return {}; -}; - -const NotifyNodeConfigFormMattermostConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: NotifyNodeConfigFormMattermostConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - channelId: z.string().nullish(), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default NotifyNodeConfigFormMattermostConfig; diff --git a/ui/src/components/workflow/node/NotifyNodeConfigFormSlackBotConfig.tsx b/ui/src/components/workflow/node/NotifyNodeConfigFormSlackBotConfig.tsx deleted file mode 100644 index 8df075b3f..000000000 --- a/ui/src/components/workflow/node/NotifyNodeConfigFormSlackBotConfig.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type NotifyNodeConfigFormSlackBotConfigFieldValues = Nullish<{ - channelId?: string; -}>; - -export interface NotifyNodeConfigFormSlackBotConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: NotifyNodeConfigFormSlackBotConfigFieldValues; - onValuesChange?: (values: NotifyNodeConfigFormSlackBotConfigFieldValues) => void; -} - -const initFormModel = (): NotifyNodeConfigFormSlackBotConfigFieldValues => { - return {}; -}; - -const NotifyNodeConfigFormSlackBotConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: NotifyNodeConfigFormSlackBotConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - channelId: z.string().nullish(), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default NotifyNodeConfigFormSlackBotConfig; diff --git a/ui/src/components/workflow/node/NotifyNodeConfigFormTelegramBotConfig.tsx b/ui/src/components/workflow/node/NotifyNodeConfigFormTelegramBotConfig.tsx deleted file mode 100644 index d254e5962..000000000 --- a/ui/src/components/workflow/node/NotifyNodeConfigFormTelegramBotConfig.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -type NotifyNodeConfigFormTelegramBotConfigFieldValues = Nullish<{ - chatId?: string | number; -}>; - -export interface NotifyNodeConfigFormTelegramBotConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: NotifyNodeConfigFormTelegramBotConfigFieldValues; - onValuesChange?: (values: NotifyNodeConfigFormTelegramBotConfigFieldValues) => void; -} - -const initFormModel = (): NotifyNodeConfigFormTelegramBotConfigFieldValues => { - return {}; -}; - -const NotifyNodeConfigFormTelegramBotConfig = ({ - form: formInst, - formName, - disabled, - initialValues, - onValuesChange, -}: NotifyNodeConfigFormTelegramBotConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - chatId: z - .preprocess( - (v) => (v == null || v === "" ? undefined : Number(v)), - z - .number() - .nullish() - .refine((v) => { - if (v == null || v + "" === "") return true; - return !Number.isNaN(+v!) && +v! !== 0; - }, t("workflow_node.notify.form.telegrambot_chat_id.placeholder")) - ) - .nullish(), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - -
- ); -}; - -export default NotifyNodeConfigFormTelegramBotConfig; diff --git a/ui/src/components/workflow/node/NotifyNodeConfigFormWebhookConfig.tsx b/ui/src/components/workflow/node/NotifyNodeConfigFormWebhookConfig.tsx deleted file mode 100644 index 634f93939..000000000 --- a/ui/src/components/workflow/node/NotifyNodeConfigFormWebhookConfig.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Alert, Form, type FormInstance } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import CodeInput from "@/components/CodeInput"; - -type NotifyNodeConfigFormWebhookConfigFieldValues = Nullish<{ - webhookData: string; -}>; - -export interface NotifyNodeConfigFormWebhookConfigProps { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: NotifyNodeConfigFormWebhookConfigFieldValues; - onValuesChange?: (values: NotifyNodeConfigFormWebhookConfigFieldValues) => void; -} - -const initFormModel = (): NotifyNodeConfigFormWebhookConfigFieldValues => { - return {}; -}; - -const NotifyNodeConfigFormWebhookConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: NotifyNodeConfigFormWebhookConfigProps) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - webhookData: z - .string() - .nullish() - .refine((v) => { - if (!v) return true; - - try { - const obj = JSON.parse(v); - return typeof obj === "object" && !Array.isArray(obj); - } catch { - return false; - } - }, t("workflow_node.notify.form.webhook_data.errmsg.json_invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - - const handleWebhookDataBlur = () => { - const value = formInst.getFieldValue("webhookData"); - try { - const json = JSON.stringify(JSON.parse(value), null, 2); - formInst.setFieldValue("webhookData", json); - } catch { - return; - } - }; - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values); - }; - - return ( -
- } - > - - - - - } /> - -
- ); -}; - -export default NotifyNodeConfigFormWebhookConfig; diff --git a/ui/src/components/workflow/node/StartNode.tsx b/ui/src/components/workflow/node/StartNode.tsx deleted file mode 100644 index ff5eeebbe..000000000 --- a/ui/src/components/workflow/node/StartNode.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { memo, useMemo, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Typography } from "antd"; -import { produce } from "immer"; - -import { WORKFLOW_TRIGGERS, type WorkflowNodeConfigForStart, WorkflowNodeType } from "@/domain/workflow"; -import { useZustandShallowSelector } from "@/hooks"; -import { useWorkflowStore } from "@/stores/workflow"; - -import SharedNode, { type SharedNodeProps } from "./_SharedNode"; -import StartNodeConfigForm, { type StartNodeConfigFormInstance } from "./StartNodeConfigForm"; - -export interface StartNodeProps extends SharedNodeProps {} - -const StartNode = ({ node, disabled }: StartNodeProps) => { - if (node.type !== WorkflowNodeType.Start) { - console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Start}`); - } - - const { t } = useTranslation(); - - const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"])); - - const formRef = useRef(null); - const [formPending, setFormPending] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForStart; - - const [drawerOpen, setDrawerOpen] = useState(false); - - const wrappedEl = useMemo(() => { - if (node.type !== WorkflowNodeType.Start) { - console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Start}`); - } - - if (!node.validated) { - return {t("workflow_node.action.configure_node")}; - } - - const config = (node.config as WorkflowNodeConfigForStart) ?? {}; - return ( -
- - {config.trigger === WORKFLOW_TRIGGERS.SCHEDULED - ? t("workflow.props.trigger.scheduled") - : config.trigger === WORKFLOW_TRIGGERS.MANUAL - ? t("workflow.props.trigger.manual") - : "\u00A0"} - - - {config.trigger === WORKFLOW_TRIGGERS.SCHEDULED ? config.triggerCron : ""} - -
- ); - }, [node]); - - const handleDrawerConfirm = async () => { - setFormPending(true); - try { - await formRef.current!.validateFields(); - } catch (err) { - setFormPending(false); - throw err; - } - - try { - const newValues = getFormValues(); - const newNode = produce(node, (draft) => { - draft.config = { - ...newValues, - }; - draft.validated = true; - }); - await updateNode(newNode); - } finally { - setFormPending(false); - } - }; - - return ( - <> - setDrawerOpen(true)}> - {wrappedEl} - - - setDrawerOpen(open)} - > - - - - ); -}; - -export default memo(StartNode); diff --git a/ui/src/components/workflow/node/StartNodeConfigForm.tsx b/ui/src/components/workflow/node/StartNodeConfigForm.tsx deleted file mode 100644 index a343c9fd6..000000000 --- a/ui/src/components/workflow/node/StartNodeConfigForm.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { forwardRef, memo, useEffect, useImperativeHandle, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Alert, Form, type FormInstance, Input, Radio } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import dayjs from "dayjs"; -import { z } from "zod"; - -import Show from "@/components/Show"; -import { WORKFLOW_TRIGGERS, type WorkflowNodeConfigForStart, type WorkflowTriggerType, defaultNodeConfigForStart } from "@/domain/workflow"; -import { useAntdForm } from "@/hooks"; -import { getNextCronExecutions, validCronExpression } from "@/utils/cron"; - -type StartNodeConfigFormFieldValues = Partial; - -export interface StartNodeConfigFormProps { - className?: string; - style?: React.CSSProperties; - disabled?: boolean; - initialValues?: StartNodeConfigFormFieldValues; - onValuesChange?: (values: StartNodeConfigFormFieldValues) => void; -} - -export interface StartNodeConfigFormInstance { - getFieldsValue: () => ReturnType["getFieldsValue"]>; - resetFields: FormInstance["resetFields"]; - validateFields: FormInstance["validateFields"]; -} - -const initFormModel = (): StartNodeConfigFormFieldValues => { - return defaultNodeConfigForStart(); -}; - -const StartNodeConfigForm = forwardRef( - ({ className, style, disabled, initialValues, onValuesChange }, ref) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - trigger: z.string(t("workflow_node.start.form.trigger.placeholder")).min(1, t("workflow_node.start.form.trigger.placeholder")), - triggerCron: z - .string() - .nullish() - .refine((v) => { - if (fieldTrigger !== WORKFLOW_TRIGGERS.SCHEDULED) return true; - return validCronExpression(v!); - }, t("workflow_node.start.form.trigger_cron.errmsg.invalid")), - }); - const formRule = createSchemaFieldRule(formSchema); - const { form: formInst, formProps } = useAntdForm({ - name: "workflowNodeStartConfigForm", - initialValues: initialValues ?? initFormModel(), - }); - - const fieldTrigger = Form.useWatch("trigger", formInst); - const fieldTriggerCron = Form.useWatch("triggerCron", formInst); - const [fieldTriggerCronExpectedExecutions, setFieldTriggerCronExpectedExecutions] = useState([]); - useEffect(() => { - setFieldTriggerCronExpectedExecutions(getNextCronExecutions(fieldTriggerCron, 5)); - }, [fieldTriggerCron]); - - const handleTriggerChange = (value: string) => { - if (value === WORKFLOW_TRIGGERS.SCHEDULED) { - formInst.setFieldValue("triggerCron", formProps.initialValues?.triggerCron || initFormModel().triggerCron); - } else { - formInst.setFieldValue("triggerCron", undefined); - } - - onValuesChange?.(formInst.getFieldsValue(true)); - }; - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values as StartNodeConfigFormFieldValues); - }; - - useImperativeHandle(ref, () => { - return { - getFieldsValue: () => { - return formInst.getFieldsValue(true); - }, - resetFields: (fields) => { - return formInst.resetFields(fields); - }, - validateFields: (nameList, config) => { - return formInst.validateFields(nameList, config); - }, - } as StartNodeConfigFormInstance; - }); - - return ( -
- - handleTriggerChange(e.target.value)}> - {t("workflow_node.start.form.trigger.option.scheduled.label")} - {t("workflow_node.start.form.trigger.option.manual.label")} - - - - - - - - } /> - - -
- ); - } -); - -export default memo(StartNodeConfigForm); diff --git a/ui/src/components/workflow/node/UnknownNode.tsx b/ui/src/components/workflow/node/UnknownNode.tsx deleted file mode 100644 index 620a5451e..000000000 --- a/ui/src/components/workflow/node/UnknownNode.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { memo } from "react"; -import { IconTrashX } from "@tabler/icons-react"; -import { Alert, Button, Card } from "antd"; - -import { useZustandShallowSelector } from "@/hooks"; -import { useWorkflowStore } from "@/stores/workflow"; - -import { type SharedNodeProps } from "./_SharedNode"; -import AddNode from "./AddNode"; - -export interface MonitorNodeProps extends SharedNodeProps {} - -const UnknownNode = ({ node, disabled }: MonitorNodeProps) => { - const { removeNode } = useWorkflowStore(useZustandShallowSelector(["removeNode"])); - - const handleClickRemove = () => { - removeNode(node); - }; - - return ( - <> -
- -
- -
- INVALID NODE -
- PLEASE REMOVE -
-
- } - /> -
- -
- - - - ); -}; - -export default memo(UnknownNode); diff --git a/ui/src/components/workflow/node/UploadNode.tsx b/ui/src/components/workflow/node/UploadNode.tsx deleted file mode 100644 index 38db12c62..000000000 --- a/ui/src/components/workflow/node/UploadNode.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { memo, useMemo, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Flex, Typography } from "antd"; -import { produce } from "immer"; - -import { type WorkflowNodeConfigForUpload, WorkflowNodeType } from "@/domain/workflow"; -import { useZustandShallowSelector } from "@/hooks"; -import { useWorkflowStore } from "@/stores/workflow"; - -import SharedNode, { type SharedNodeProps } from "./_SharedNode"; -import UploadNodeConfigForm, { type UploadNodeConfigFormInstance } from "./UploadNodeConfigForm"; - -export interface UploadNodeProps extends SharedNodeProps {} - -const UploadNode = ({ node, disabled }: UploadNodeProps) => { - if (node.type !== WorkflowNodeType.Upload) { - console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Upload}`); - } - - const { t } = useTranslation(); - - const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"])); - - const formRef = useRef(null); - const [formPending, setFormPending] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForUpload; - - const [drawerOpen, setDrawerOpen] = useState(false); - - const wrappedEl = useMemo(() => { - if (node.type !== WorkflowNodeType.Upload) { - console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Upload}`); - } - - if (!node.validated) { - return {t("workflow_node.action.configure_node")}; - } - - const config = (node.config as WorkflowNodeConfigForUpload) ?? {}; - return ( - - {config.domains ?? ""} - - ); - }, [node]); - - const handleDrawerConfirm = async () => { - setFormPending(true); - try { - await formRef.current!.validateFields(); - } catch (err) { - setFormPending(false); - throw err; - } - - try { - const newValues = getFormValues(); - const newNode = produce(node, (draft) => { - draft.config = { - ...newValues, - }; - draft.validated = true; - }); - await updateNode(newNode); - } finally { - setFormPending(false); - } - }; - - return ( - <> - setDrawerOpen(true)}> - {wrappedEl} - - - setDrawerOpen(open)} - > - - - - ); -}; - -export default memo(UploadNode); diff --git a/ui/src/components/workflow/node/UploadNodeConfigForm.tsx b/ui/src/components/workflow/node/UploadNodeConfigForm.tsx deleted file mode 100644 index 1f435a8f9..000000000 --- a/ui/src/components/workflow/node/UploadNodeConfigForm.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { forwardRef, memo, useImperativeHandle } from "react"; -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { validateCertificate, validatePrivateKey } from "@/api/certificates"; -import TextFileInput from "@/components/TextFileInput"; -import { type WorkflowNodeConfigForUpload, defaultNodeConfigForUpload } from "@/domain/workflow"; -import { useAntdForm } from "@/hooks"; -import { getErrMsg } from "@/utils/error"; - -type UploadNodeConfigFormFieldValues = Partial; - -export interface UploadNodeConfigFormProps { - className?: string; - style?: React.CSSProperties; - disabled?: boolean; - initialValues?: UploadNodeConfigFormFieldValues; - onValuesChange?: (values: UploadNodeConfigFormFieldValues) => void; -} - -export interface UploadNodeConfigFormInstance { - getFieldsValue: () => ReturnType["getFieldsValue"]>; - resetFields: FormInstance["resetFields"]; - validateFields: FormInstance["validateFields"]; -} - -const initFormModel = (): UploadNodeConfigFormFieldValues => { - return defaultNodeConfigForUpload(); -}; - -const UploadNodeConfigForm = forwardRef( - ({ className, style, disabled, initialValues, onValuesChange }, ref) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - domains: z.string().nullish(), - certificate: z - .string(t("workflow_node.upload.form.certificate.placeholder")) - .min(1, t("workflow_node.upload.form.certificate.placeholder")) - .max(20480, t("common.errmsg.string_max", { max: 20480 })), - privateKey: z - .string(t("workflow_node.upload.form.private_key.placeholder")) - .min(1, t("workflow_node.upload.form.private_key.placeholder")) - .max(20480, t("common.errmsg.string_max", { max: 20480 })), - }); - const formRule = createSchemaFieldRule(formSchema); - const { form: formInst, formProps } = useAntdForm({ - name: "workflowNodeUploadConfigForm", - initialValues: initialValues ?? initFormModel(), - }); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values as UploadNodeConfigFormFieldValues); - }; - - useImperativeHandle(ref, () => { - return { - getFieldsValue: () => { - return formInst.getFieldsValue(true); - }, - resetFields: (fields) => { - return formInst.resetFields(fields as (keyof UploadNodeConfigFormFieldValues)[]); - }, - validateFields: (nameList, config) => { - return formInst.validateFields(nameList, config); - }, - } as UploadNodeConfigFormInstance; - }); - - const handleCertificateChange = async (value: string) => { - try { - const resp = await validateCertificate(value); - formInst.setFields([ - { - name: "domains", - value: resp.data.domains, - }, - { - name: "certificate", - value: value, - }, - ]); - } catch (e) { - formInst.setFields([ - { - name: "domains", - value: "", - }, - { - name: "certificate", - value: value, - errors: [getErrMsg(e)], - }, - ]); - } - - onValuesChange?.(formInst.getFieldsValue(true)); - }; - - const handlePrivateKeyChange = async (value: string) => { - try { - await validatePrivateKey(value); - formInst.setFields([ - { - name: "privateKey", - value: value, - }, - ]); - } catch (e) { - formInst.setFields([ - { - name: "privateKey", - value: value, - errors: [getErrMsg(e)], - }, - ]); - } - - onValuesChange?.(formInst.getFieldsValue(true)); - }; - - return ( -
- - - - - - - - - - - -
- ); - } -); - -export default memo(UploadNodeConfigForm); diff --git a/ui/src/components/workflow/node/_SharedNode.tsx b/ui/src/components/workflow/node/_SharedNode.tsx deleted file mode 100644 index 5c7e853f3..000000000 --- a/ui/src/components/workflow/node/_SharedNode.tsx +++ /dev/null @@ -1,389 +0,0 @@ -import { useMemo, useRef } from "react"; -import { useTranslation } from "react-i18next"; -import { IconCopy, IconDotsVertical, IconLabel, IconTrashX } from "@tabler/icons-react"; -import { useControllableValue } from "ahooks"; -import { App, Button, Card, Drawer, Dropdown, Flex, Input, type InputRef, type MenuProps, Popover } from "antd"; -import { produce } from "immer"; -import { isEqual } from "radash"; - -import { type WorkflowNode, WorkflowNodeType } from "@/domain/workflow"; -import { useZustandShallowSelector } from "@/hooks"; -import { useWorkflowStore } from "@/stores/workflow"; - -import AddNode from "./AddNode"; - -export interface SharedNodeProps { - node: WorkflowNode; - disabled?: boolean; -} - -// #region Title -type SharedNodeTitleProps = SharedNodeProps & { - className?: string; - style?: React.CSSProperties; -}; - -const SharedNodeTitle = ({ className, style, node, disabled }: SharedNodeTitleProps) => { - const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"])); - - const handleBlur = (e: React.FocusEvent) => { - const oldName = node.name; - const newName = e.target.innerText.replaceAll("\r", "").replaceAll("\n", "").trim().substring(0, 64) || oldName; - if (oldName === newName) { - return; - } - - updateNode( - produce(node, (draft) => { - draft.name = newName; - }) - ); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.code === "Enter") { - e.preventDefault(); - e.currentTarget.blur(); - } - }; - - return ( -
-
- {node.name} -
-
- ); -}; -// #endregion - -// #region Menu -type SharedNodeMenuProps = SharedNodeProps & { - branchId?: string; - branchIndex?: number; - menus?: Array<"rename" | "duplicate" | "remove">; - trigger: React.ReactNode; - afterUpdate?: () => void; - afterDelete?: () => void; -}; - -const isNodeBranchLike = (node: WorkflowNode) => { - return ( - node.type === WorkflowNodeType.Branch || - node.type === WorkflowNodeType.Condition || - node.type === WorkflowNodeType.ExecuteResultBranch || - node.type === WorkflowNodeType.ExecuteSuccess || - node.type === WorkflowNodeType.ExecuteFailure - ); -}; - -const isNodeUnduplicatable = (node: WorkflowNode) => { - return ( - node.type === WorkflowNodeType.Start || - node.type === WorkflowNodeType.End || - node.type === WorkflowNodeType.Branch || - node.type === WorkflowNodeType.ExecuteResultBranch || - node.type === WorkflowNodeType.ExecuteSuccess || - node.type === WorkflowNodeType.ExecuteFailure - ); -}; - -const isNodeUnremovable = (node: WorkflowNode) => { - return node.type === WorkflowNodeType.Start || node.type === WorkflowNodeType.End; -}; - -const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex, afterUpdate, afterDelete }: SharedNodeMenuProps) => { - const { t } = useTranslation(); - - const { modal } = App.useApp(); - - const { duplicateNode, updateNode, removeNode, duplicateBranch, removeBranch } = useWorkflowStore( - useZustandShallowSelector(["duplicateNode", "updateNode", "removeNode", "duplicateBranch", "removeBranch"]) - ); - - const nameInputRef = useRef(null); - const nameRef = useRef(); - - const handleRenameConfirm = async () => { - const oldName = node.name; - const newName = nameRef.current?.replaceAll("\r", "")?.replaceAll("\n", "").trim()?.substring(0, 64) || oldName; - if (oldName === newName) { - return; - } - - await updateNode( - produce(node, (draft) => { - draft.name = newName; - }) - ); - - afterUpdate?.(); - }; - - const handleDuplicateClick = async () => { - if (isNodeBranchLike(node)) { - await duplicateBranch(branchId!, branchIndex!); - } else { - await duplicateNode(node); - } - }; - - const handleRemoveClick = async () => { - if (isNodeBranchLike(node)) { - await removeBranch(branchId!, branchIndex!); - } else { - await removeNode(node); - } - - afterDelete?.(); - }; - - const menuItems = useMemo(() => { - let temp = [ - { - key: "rename", - disabled: disabled, - label: isNodeBranchLike(node) ? t("workflow_node.action.rename_branch") : t("workflow_node.action.rename_node"), - icon: ( - - - - ), - onClick: () => { - nameRef.current = node.name; - - const dialog = modal.confirm({ - title: isNodeBranchLike(node) ? t("workflow_node.action.rename_branch") : t("workflow_node.action.rename_node"), - content: ( -
- (nameRef.current = e.target.value)} - onPressEnter={async () => { - await handleRenameConfirm(); - dialog.destroy(); - }} - /> -
- ), - icon: null, - okText: t("common.button.save"), - onOk: handleRenameConfirm, - }); - setTimeout(() => nameInputRef.current?.focus(), 1); - }, - }, - { - key: "duplicate", - disabled: disabled || isNodeUnduplicatable(node), - label: isNodeBranchLike(node) ? t("workflow_node.action.duplicate_branch") : t("workflow_node.action.duplicate_node"), - icon: ( - - - - ), - onClick: handleDuplicateClick, - }, - { - type: "divider", - }, - { - key: "remove", - disabled: disabled || isNodeUnremovable(node), - label: isNodeBranchLike(node) ? t("workflow_node.action.remove_branch") : t("workflow_node.action.remove_node"), - icon: ( - - - - ), - danger: true, - onClick: handleRemoveClick, - }, - ] satisfies MenuProps["items"]; - - if (menus) { - temp = temp.filter((item) => item.type === "divider" || menus.includes(item.key as "rename" | "remove")); - temp = temp.filter((item, index, array) => { - if (item.type !== "divider") return true; - return index === 0 || array[index - 1].type !== "divider"; - }); - if (temp[0]?.type === "divider") { - temp.shift(); - } - if (temp[temp.length - 1]?.type === "divider") { - temp.pop(); - } - } - - return temp; - }, [disabled, node]); - - return ( - - {trigger} - - ); -}; -// #endregion - -// #region Block -type SharedNodeBlockProps = SharedNodeProps & { - children: React.ReactNode; - onClick?: (e: React.MouseEvent) => void; -}; - -const SharedNodeBlock = ({ children, node, disabled, onClick }: SharedNodeBlockProps) => { - const handleNodeClick = (e: React.MouseEvent) => { - onClick?.(e); - }; - - return ( - <> - } variant="text" />} />} - placement="rightTop" - > -
- -
- -
- -
-
{children}
-
-
-
-
- - - - ); -}; -// #endregion - -// #region EditDrawer -type SharedNodeEditDrawerProps = SharedNodeProps & { - children: React.ReactNode; - footer?: boolean; - loading?: boolean; - open?: boolean; - pending?: boolean; - onOpenChange?: (open: boolean) => void; - onConfirm: () => void | Promise; - getConfigNewValues: () => NonNullable; // 用于获取节点配置的新值,以便在抽屉关闭前进行对比,决定是否提示保存 -}; - -const SharedNodeConfigDrawer = ({ - children, - node, - disabled, - footer = true, - loading, - pending, - onConfirm, - getConfigNewValues, - ...props -}: SharedNodeEditDrawerProps) => { - const { t } = useTranslation(); - - const { modal } = App.useApp(); - - const [open, setOpen] = useControllableValue(props, { - valuePropName: "open", - defaultValuePropName: "defaultOpen", - trigger: "onOpenChange", - }); - - const handleConfirmClick = async () => { - await onConfirm(); - setOpen(false); - }; - - const handleCancelClick = () => { - if (pending) return; - - setOpen(false); - }; - - const handleClose = () => { - if (pending) return; - - const oldValues = JSON.parse(JSON.stringify(node.config ?? {})); - const newValues = JSON.parse(JSON.stringify(getConfigNewValues())); - const changed = !isEqual(oldValues, {}) && !isEqual(oldValues, newValues); - - const { promise, resolve, reject } = Promise.withResolvers(); - if (changed) { - modal.confirm({ - title: t("common.text.operation_confirm"), - content: t("workflow.detail.design.unsaved_changes.confirm"), - onOk: () => resolve(void 0), - onCancel: () => reject(), - }); - } else { - resolve(void 0); - } - - promise.then(() => setOpen(false)); - }; - - return ( - } type="text" />} - afterDelete={() => { - setOpen(false); - }} - /> - } - footer={ - !!footer && ( - - - - - ) - } - loading={loading} - maskClosable={!pending} - open={open} - title={
{node.name}
} - width={720} - onClose={handleClose} - > - {children} -
- ); -}; -// #endregion - -export default { - Title: SharedNodeTitle, - Menu: SharedNodeMenu, - Block: SharedNodeBlock, - ConfigDrawer: SharedNodeConfigDrawer, -}; diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index d8a5312b2..a2cea54e4 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -1,8 +1,7 @@ -import dayjs from "dayjs"; -import { Immer, produce } from "immer"; +import { getI18n } from "react-i18next"; +import { Immer } from "immer"; import { nanoid } from "nanoid"; -import i18n from "@/i18n"; import { type WorkflowRunModel } from "./workflowRun"; export interface WorkflowModel extends BaseModel { @@ -11,9 +10,10 @@ export interface WorkflowModel extends BaseModel { trigger: string; triggerCron?: string; enabled?: boolean; - content?: WorkflowNode; - draft?: WorkflowNode; + graphDraft?: WorkflowGraph; + graphContent?: WorkflowGraph; hasDraft?: boolean; + hasContent?: boolean; lastRunRef?: string; lastRunStatus?: string; lastRunTime?: string; @@ -22,6 +22,10 @@ export interface WorkflowModel extends BaseModel { }; } +export interface WorkflowGraph { + nodes: WorkflowNode[]; +} + export const WORKFLOW_TRIGGERS = Object.freeze({ SCHEDULED: "scheduled", MANUAL: "manual", @@ -30,106 +34,32 @@ export const WORKFLOW_TRIGGERS = Object.freeze({ export type WorkflowTriggerType = (typeof WORKFLOW_TRIGGERS)[keyof typeof WORKFLOW_TRIGGERS]; // #region Node -export enum WorkflowNodeType { - Start = "start", - End = "end", - Apply = "apply", - Upload = "upload", - Monitor = "monitor", - Deploy = "deploy", - Notify = "notify", - Branch = "branch", - Condition = "condition", - ExecuteResultBranch = "execute_result_branch", - ExecuteSuccess = "execute_success", - ExecuteFailure = "execute_failure", - Custom = "custom", -} +export const WORKFLOW_NODE_TYPES = Object.freeze({ + START: "start", + END: "end", + CONDITION: "condition", + BRANCHBLOCK: "branchBlock", + TRYCATCH: "tryCatch", + TRYBLOCK: "tryBlock", + CATCHBLOCK: "catchBlock", + BIZ_APPLY: "bizApply", + BIZ_UPLOAD: "bizUpload", + BIZ_MONITOR: "bizMonitor", + BIZ_DEPLOY: "bizDeploy", + BIZ_NOTIFY: "bizNotify", +} as const); -const workflowNodeTypeDefaultNames: Map = new Map([ - [WorkflowNodeType.Start, i18n.t("workflow_node.start.default_name")], - [WorkflowNodeType.End, i18n.t("workflow_node.end.default_name")], - [WorkflowNodeType.Apply, i18n.t("workflow_node.apply.default_name")], - [WorkflowNodeType.Upload, i18n.t("workflow_node.upload.default_name")], - [WorkflowNodeType.Monitor, i18n.t("workflow_node.monitor.default_name")], - [WorkflowNodeType.Deploy, i18n.t("workflow_node.deploy.default_name")], - [WorkflowNodeType.Notify, i18n.t("workflow_node.notify.default_name")], - [WorkflowNodeType.Branch, i18n.t("workflow_node.branch.default_name")], - [WorkflowNodeType.Condition, i18n.t("workflow_node.condition.default_name")], - [WorkflowNodeType.ExecuteResultBranch, i18n.t("workflow_node.execute_result_branch.default_name")], - [WorkflowNodeType.ExecuteSuccess, i18n.t("workflow_node.execute_success.default_name")], - [WorkflowNodeType.ExecuteFailure, i18n.t("workflow_node.execute_failure.default_name")], -]); - -const workflowNodeTypeDefaultInputs: Map = new Map([ - [WorkflowNodeType.Apply, []], - [WorkflowNodeType.Upload, []], - [WorkflowNodeType.Monitor, []], - [ - WorkflowNodeType.Deploy, - [ - { - name: "certificate", - type: "certificate", - required: true, - label: i18n.t("workflow.variables.type.certificate.label"), - }, - ], - ], - [WorkflowNodeType.Notify, []], -]); - -const workflowNodeTypeDefaultOutputs: Map = new Map([ - [ - WorkflowNodeType.Apply, - [ - { - name: "certificate", - type: "certificate", - required: true, - label: i18n.t("workflow.variables.type.certificate.label"), - }, - ], - ], - [ - WorkflowNodeType.Upload, - [ - { - name: "certificate", - type: "certificate", - required: true, - label: i18n.t("workflow.variables.type.certificate.label"), - }, - ], - ], - [ - WorkflowNodeType.Monitor, - [ - { - name: "certificate", - type: "certificate", - required: true, - label: i18n.t("workflow.variables.type.certificate.label"), - }, - ], - ], - [WorkflowNodeType.Deploy, []], - [WorkflowNodeType.Notify, []], -]); +export type WorkflowNodeType = (typeof WORKFLOW_NODE_TYPES)[keyof typeof WORKFLOW_NODE_TYPES]; export type WorkflowNode = { id: string; - name: string; type: WorkflowNodeType; - - config?: Record; - inputs?: WorkflowNodeIO[]; - outputs?: WorkflowNodeIO[]; - - next?: WorkflowNode; - branches?: WorkflowNode[]; - - validated?: boolean; + data: { + name?: string; + disabled?: boolean; + config?: Record; + }; + blocks?: WorkflowNode[]; }; export type WorkflowNodeConfigForStart = { @@ -140,11 +70,18 @@ export type WorkflowNodeConfigForStart = { export const defaultNodeConfigForStart = (): Partial => { return { trigger: WORKFLOW_TRIGGERS.MANUAL, - triggerCron: "0 0 * * *", }; }; -export type WorkflowNodeConfigForApply = { +export type WorkflowNodeConfigForBranchBlock = { + expression?: Expr; +}; + +export const defaultNodeConfigForBranchBlock = (): Partial => { + return {}; +}; + +export type WorkflowNodeConfigForBizApply = { domains: string; contactEmail: string; challengeType: string; @@ -164,7 +101,7 @@ export type WorkflowNodeConfigForApply = { skipBeforeExpiryDays: number; }; -export const defaultNodeConfigForApply = (): Partial => { +export const defaultNodeConfigForBizApply = (): Partial => { return { challengeType: "dns-01", keyAlgorithm: "RSA2048", @@ -172,25 +109,25 @@ export const defaultNodeConfigForApply = (): Partial }; }; -export type WorkflowNodeConfigForUpload = { +export type WorkflowNodeConfigForBizUpload = { certificateId: string; domains: string; certificate: string; privateKey: string; }; -export const defaultNodeConfigForUpload = (): Partial => { +export const defaultNodeConfigForBizUpload = (): Partial => { return {}; }; -export type WorkflowNodeConfigForMonitor = { +export type WorkflowNodeConfigForBizMonitor = { host: string; port: number; domain?: string; requestPath?: string; }; -export const defaultNodeConfigForMonitor = (): Partial => { +export const defaultNodeConfigForBizMonitor = (): Partial => { return { host: "", port: 443, @@ -198,21 +135,21 @@ export const defaultNodeConfigForMonitor = (): Partial; skipOnLastSucceeded: boolean; }; -export const defaultNodeConfigForDeploy = (): Partial => { +export const defaultNodeConfigForBizDeploy = (): Partial => { return { skipOnLastSucceeded: true, }; }; -export type WorkflowNodeConfigForNotify = { +export type WorkflowNodeConfigForBizNotify = { subject: string; message: string; provider: string; @@ -221,35 +158,219 @@ export type WorkflowNodeConfigForNotify = { skipOnAllPrevSkipped?: boolean; }; -export const defaultNodeConfigForNotify = (): Partial => { - return { - subject: "", - message: "", - }; -}; - -export type WorkflowNodeConfigForCondition = { - expression?: Expr; -}; - -export const defaultNodeConfigForCondition = (): Partial => { +export const defaultNodeConfigForBizNotify = (): Partial => { return {}; }; -export type WorkflowNodeConfigForBranch = never; - -export type WorkflowNodeConfigForEnd = never; - -export type WorkflowNodeIO = { - name: string; - type: string; - required: boolean; - label: string; - value?: string; - valueSelector?: WorkflowNodeIOValueSelector; +export const newNodeId = (): string => { + return nanoid(); }; -export type WorkflowNodeIOValueSelector = ExprValueSelector; +export const newNode = (type: WorkflowNodeType, { i18n = getI18n() }: { i18n?: ReturnType }): WorkflowNode => { + const { t } = i18n; + + switch (type) { + case WORKFLOW_NODE_TYPES.START: + return { + id: newNodeId(), + type: type, + data: { + name: t("workflow_node.start.default_name"), + config: defaultNodeConfigForStart(), + }, + }; + + case WORKFLOW_NODE_TYPES.END: + return { + id: newNodeId(), + type: type, + data: { + name: t("workflow_node.end.default_name"), + }, + }; + + case WORKFLOW_NODE_TYPES.CONDITION: { + const branch1 = newNode(WORKFLOW_NODE_TYPES.BRANCHBLOCK, { i18n }); + branch1.data.name = `${branch1.data.name} 1`; + const branch2 = newNode(WORKFLOW_NODE_TYPES.BRANCHBLOCK, { i18n }); + branch2.data.name = `${branch2.data.name} 2`; + + return { + id: newNodeId(), + type: type, + data: { + name: t("workflow_node.condition.default_name"), + config: defaultNodeConfigForBranchBlock(), + }, + blocks: [branch1, branch2], + }; + } + + case WORKFLOW_NODE_TYPES.BRANCHBLOCK: + return { + id: newNodeId(), + type: type, + blocks: [], + data: { + name: t("workflow_node.branch_block.default_name"), + config: defaultNodeConfigForBranchBlock(), + }, + }; + + case WORKFLOW_NODE_TYPES.TRYCATCH: + return { + id: newNodeId(), + type: type, + data: { + name: t("workflow_node.try_catch.default_name"), + }, + blocks: [newNode(WORKFLOW_NODE_TYPES.TRYBLOCK, { i18n }), newNode(WORKFLOW_NODE_TYPES.CATCHBLOCK, { i18n })], + }; + + case WORKFLOW_NODE_TYPES.TRYBLOCK: + return { + id: newNodeId(), + type: type, + data: { + name: "", + }, + blocks: [], + }; + + case WORKFLOW_NODE_TYPES.CATCHBLOCK: + return { + id: newNodeId(), + type: type, + blocks: [newNode(WORKFLOW_NODE_TYPES.END, { i18n })], + data: { + name: t("workflow_node.catch_block.default_name"), + }, + }; + + case WORKFLOW_NODE_TYPES.BIZ_APPLY: + return { + id: newNodeId(), + type: type, + data: { + name: t("workflow_node.apply.default_name"), + config: defaultNodeConfigForBizApply(), + }, + }; + + case WORKFLOW_NODE_TYPES.BIZ_UPLOAD: + return { + id: newNodeId(), + type: type, + data: { + name: t("workflow_node.upload.default_name"), + config: defaultNodeConfigForBizUpload(), + }, + }; + + case WORKFLOW_NODE_TYPES.BIZ_MONITOR: + return { + id: newNodeId(), + type: type, + data: { + name: t("workflow_node.monitor.default_name"), + config: defaultNodeConfigForBizMonitor(), + }, + }; + + case WORKFLOW_NODE_TYPES.BIZ_DEPLOY: + return { + id: newNodeId(), + type: type, + data: { + name: t("workflow_node.deploy.default_name"), + config: defaultNodeConfigForBizDeploy(), + }, + }; + + case WORKFLOW_NODE_TYPES.BIZ_NOTIFY: + return { + id: newNodeId(), + type: type, + data: { + name: t("workflow_node.notify.default_name"), + config: defaultNodeConfigForBizNotify(), + }, + }; + + default: + throw new Error("Invalid value of `nodeType`"); + } +}; + +export const duplicateNode = (node: WorkflowNode, options?: { withCopySuffix?: boolean }) => { + const { produce } = new Immer({ autoFreeze: false }); + const deepClone = (node: WorkflowNode, { withCopySuffix, nodeIdMap }: { withCopySuffix: boolean; nodeIdMap: Map }) => { + return produce(node, (draft) => { + draft.data ??= {}; + draft.id = newNodeId(); + draft.data.name = withCopySuffix ? `${draft.data?.name || ""}-copy` : `${draft.data?.name || ""}`; + + nodeIdMap.set(node.id, draft.id); // 原节点 ID 映射到新节点 ID + + if (draft.blocks) { + draft.blocks = draft.blocks.map((block) => deepClone(block as WorkflowNode, { withCopySuffix: false, nodeIdMap })); + } + + if (draft.data?.config) { + switch (draft.type) { + case WORKFLOW_NODE_TYPES.BIZ_DEPLOY: + { + const prevNodeId = draft.data.config.certificateOutputNodeId as string; + if (nodeIdMap.has(prevNodeId)) { + draft.data.config = { + ...draft.data.config, + certificateOutputNodeId: nodeIdMap.get(prevNodeId), + }; + } + } + break; + + case WORKFLOW_NODE_TYPES.CONDITION: + { + const stack = [] as Expr[]; + const expr = draft.data.config.expression as Expr; + if (expr) { + stack.push(expr); + while (stack.length > 0) { + const n = stack.pop()!; + if ("left" in n) { + stack.push(n.left); + if ("selector" in n.left) { + const prevNodeId = n.left.selector.id; + if (nodeIdMap.has(prevNodeId)) { + n.left.selector.id = nodeIdMap.get(prevNodeId)!; + } + } + } + if ("right" in n) { + stack.push(n.right); + } + } + draft.data.config = { + ...draft.data.config, + expression: expr, + }; + } + } + break; + } + } + + return draft; + }); + }; + + return deepClone(node, { withCopySuffix: options?.withCopySuffix ?? true, nodeIdMap: new Map() }); +}; + +export const duplicateNodes = (nodes: WorkflowNode[], options?: { withCopySuffix?: boolean }) => { + return nodes.map((node) => duplicateNode(node, options)); +}; // #endregion // #region Expression @@ -279,606 +400,3 @@ export type LogicalExpr = { type: ExprType.Logical; operator: ExprLogicalOperato export type NotExpr = { type: ExprType.Not; expr: Expr }; export type Expr = ConstantExpr | VariantExpr | ComparisonExpr | LogicalExpr | NotExpr; // #endregion - -const isBranchNode = (node: WorkflowNode) => { - return node.type === WorkflowNodeType.Branch || node.type === WorkflowNodeType.ExecuteResultBranch; -}; - -type InitWorkflowOptions = { - template?: "standard" | "certtest"; -}; - -export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel => { - const root = newNode(WorkflowNodeType.Start, { - nodeConfig: { trigger: WORKFLOW_TRIGGERS.MANUAL }, - }); - - switch (options.template) { - case "standard": - { - let current = root; - - const applyNode = newNode(WorkflowNodeType.Apply, { - nodeConfig: defaultNodeConfigForApply(), - }); - current.next = applyNode; - - current = current.next; - current.next = newNode(WorkflowNodeType.ExecuteResultBranch); - - current = current.next!.branches![1]; - current.next = newNode(WorkflowNodeType.Notify, { - nodeConfig: { - ...defaultNodeConfigForNotify(), - subject: "[Certimate] Workflow Failure Alert!", - message: "Your workflow run for the certificate application has failed. Please check the details.", - } as WorkflowNodeConfigForNotify, - }); - - current = applyNode.next!.branches![0]; - current.next = newNode(WorkflowNodeType.Deploy, { - nodeConfig: { - ...defaultNodeConfigForDeploy(), - certificate: `${applyNode.id}#certificate`, - } as WorkflowNodeConfigForDeploy, - }); - - current = current.next; - current.next = newNode(WorkflowNodeType.ExecuteResultBranch); - - current = current.next!.branches![1]; - current.next = newNode(WorkflowNodeType.Notify, { - nodeConfig: { - ...defaultNodeConfigForNotify(), - subject: "[Certimate] Workflow Failure Alert!", - message: "Your workflow run for the certificate deployment has failed. Please check the details.", - } as WorkflowNodeConfigForNotify, - }); - } - break; - - case "certtest": - { - let current = root; - - const monitorNode = newNode(WorkflowNodeType.Monitor, { - nodeConfig: defaultNodeConfigForMonitor(), - }); - current.next = monitorNode; - - current = current.next; - current.next = newNode(WorkflowNodeType.ExecuteResultBranch); - - current = current.next!.branches![1]; - current.next = newNode(WorkflowNodeType.Notify, { - nodeConfig: { - ...defaultNodeConfigForNotify(), - subject: "[Certimate] Workflow Failure Alert!", - message: "Your workflow run for the certificate monitoring has failed. Please check the details.", - } as WorkflowNodeConfigForNotify, - }); - - current = monitorNode.next!.branches![0]; - const branchNode = newNode(WorkflowNodeType.Branch); - current.next = branchNode; - - current = branchNode.branches![0]; - current.name = i18n.t("workflow_node.condition.default_name.template_certtest_on_expire_soon"); - current.config = { - expression: { - left: { - left: { - selector: { - id: monitorNode.id, - name: "certificate.validity", - type: "boolean", - }, - type: "var", - }, - operator: "eq", - right: { - type: "const", - value: "true", - valueType: "boolean", - }, - type: "comparison", - }, - operator: "and", - right: { - left: { - selector: { - id: monitorNode.id, - name: "certificate.daysLeft", - type: "number", - }, - type: "var", - }, - operator: "lte", - right: { - type: "const", - value: "30", - valueType: "number", - }, - type: "comparison", - }, - type: "logical", - }, - } as WorkflowNodeConfigForCondition; - current.next = newNode(WorkflowNodeType.Notify, { - nodeConfig: { - ...defaultNodeConfigForNotify(), - subject: "[Certimate] Certificate Expiry Alert!", - message: "The certificate will expire soon. Please pay attention to your website.", - } as WorkflowNodeConfigForNotify, - }); - - current = branchNode.branches![1]; - current.name = i18n.t("workflow_node.condition.default_name.template_certtest_on_expired"); - current.config = { - expression: { - left: { - selector: { - id: monitorNode.id, - name: "certificate.validity", - type: "boolean", - }, - type: "var", - }, - operator: "eq", - right: { - type: "const", - value: "false", - valueType: "boolean", - }, - type: "comparison", - }, - } as WorkflowNodeConfigForCondition; - current.next = newNode(WorkflowNodeType.Notify, { - nodeConfig: { - ...defaultNodeConfigForNotify(), - subject: "[Certimate] Certificate Expiry Alert!", - message: "The certificate has already expired. Please pay attention to your website.", - } as WorkflowNodeConfigForNotify, - }); - } - break; - } - - return { - id: null!, - name: `MyWorkflow-${dayjs().format("YYYYMMDDHHmmss")}`, - trigger: root.config!.trigger as string, - triggerCron: root.config!.triggerCron as string, - enabled: false, - draft: root, - hasDraft: true, - created: new Date().toISOString(), - updated: new Date().toISOString(), - }; -}; - -type NewNodeOptions = { - nodeName?: string; - nodeConfig?: Record; - branchIndex?: number; -}; - -export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions = {}): WorkflowNode => { - const nodeTypeName = workflowNodeTypeDefaultNames.get(nodeType) || ""; - const nodeName = options.branchIndex != null ? `${nodeTypeName} ${options.branchIndex + 1}` : nodeTypeName; - - const node: WorkflowNode = { - id: nanoid(), - name: options.nodeName ?? nodeName, - type: nodeType, - config: options.nodeConfig, - }; - - switch (nodeType) { - case WorkflowNodeType.Apply: - case WorkflowNodeType.Upload: - case WorkflowNodeType.Monitor: - case WorkflowNodeType.Deploy: - { - node.inputs = workflowNodeTypeDefaultInputs.get(nodeType); - node.outputs = workflowNodeTypeDefaultOutputs.get(nodeType); - } - break; - - case WorkflowNodeType.Condition: - { - node.validated = true; - } - break; - - case WorkflowNodeType.Branch: - { - node.branches = [newNode(WorkflowNodeType.Condition, { branchIndex: 0 }), newNode(WorkflowNodeType.Condition, { branchIndex: 1 })]; - } - break; - - case WorkflowNodeType.ExecuteResultBranch: - { - node.branches = [newNode(WorkflowNodeType.ExecuteSuccess), newNode(WorkflowNodeType.ExecuteFailure)]; - } - break; - - case WorkflowNodeType.ExecuteSuccess: - case WorkflowNodeType.ExecuteFailure: - { - node.validated = true; - } - break; - } - - return node; -}; - -type CloneNodeOptions = { - withCopySuffix?: boolean; - nodeIdMap?: Map; -}; - -export const cloneNode = (sourceNode: WorkflowNode, { withCopySuffix, nodeIdMap }: CloneNodeOptions = { withCopySuffix: true }): WorkflowNode => { - const { produce } = new Immer({ autoFreeze: false }); - const deepClone = (node: WorkflowNode): WorkflowNode => { - return produce(node, (draft) => { - draft.id = nanoid(); - - nodeIdMap ??= new Map(); // 原节点 ID 映射到新节点 ID - nodeIdMap.set(node.id, draft.id); - - if (draft.next) { - draft.next = cloneNode(draft.next, { withCopySuffix, nodeIdMap }); - } - - if (draft.branches) { - draft.branches = draft.branches.map((branch) => cloneNode(branch, { withCopySuffix, nodeIdMap })); - } - - if (draft.config) { - switch (draft.type) { - case WorkflowNodeType.Deploy: - { - const prevNodeId = (draft.config as WorkflowNodeConfigForDeploy).certificate?.split("#")?.[0]; - if (nodeIdMap.has(prevNodeId)) { - draft.config = { - ...draft.config, - certificate: `${nodeIdMap.get(prevNodeId)}#certificate`, - } as WorkflowNodeConfigForDeploy; - } - } - break; - - case WorkflowNodeType.Condition: - { - const stack = [] as Expr[]; - - const expr = (draft.config as WorkflowNodeConfigForCondition).expression; - if (expr) { - stack.push(expr); - - while (stack.length > 0) { - const n = stack.pop()!; - - if ("left" in n) { - stack.push(n.left); - - if ("selector" in n.left) { - const prevNodeId = n.left.selector.id; - if (nodeIdMap.has(prevNodeId)) { - n.left.selector.id = nodeIdMap.get(prevNodeId)!; - } - } - } - - if ("right" in n) { - stack.push(n.right); - } - } - - draft.config = { - ...draft.config, - expression: expr, - } as WorkflowNodeConfigForCondition; - } - } - break; - } - } - - return draft; - }); - }; - - const copyNode = produce(sourceNode, (draft) => { - draft.name = withCopySuffix ? `${draft.name}-copy` : draft.name; - }); - return deepClone(copyNode); -}; - -export const addNode = (root: WorkflowNode, targetNode: WorkflowNode, previousNodeId: string) => { - return produce(root, (draft) => { - let current = draft; - while (current) { - if (current.id === previousNodeId && !isBranchNode(targetNode)) { - targetNode.next = current.next; - current.next = targetNode; - break; - } else if (current.id === previousNodeId && isBranchNode(targetNode)) { - targetNode.branches![0].next = current.next; - current.next = targetNode; - break; - } - - if (isBranchNode(current)) { - current.branches ??= []; - current.branches = current.branches.map((branch) => addNode(branch, targetNode, previousNodeId)); - } - - current = current.next as WorkflowNode; - } - - return draft; - }); -}; - -export const duplicateNode = (root: WorkflowNode, targetNode: WorkflowNode) => { - if (isBranchNode(targetNode)) { - throw new Error("Cannot duplicate a branch node directly. Use `duplicateBranch` instead."); - } - - const copiedNode = cloneNode(targetNode); - return addNode(root, copiedNode, targetNode.id); -}; - -export const updateNode = (root: WorkflowNode, targetNode: WorkflowNode) => { - if (isBranchNode(targetNode)) { - throw new Error("Cannot update a branch node directly. Use `updateBranch` instead."); - } - - return produce(root, (draft) => { - let current = draft; - while (current) { - if (current.id === targetNode.id) { - // Object.assign(current, targetNode); - // TODO: 暂时这么处理,避免 #485 #489,后续再优化 - current.type = targetNode.type; - current.name = targetNode.name; - current.config = targetNode.config; - current.inputs = targetNode.inputs; - current.outputs = targetNode.outputs; - current.next = targetNode.next; - current.branches = targetNode.branches; - current.validated = targetNode.validated; - break; - } - - if (isBranchNode(current)) { - current.branches ??= []; - current.branches = current.branches.map((branch) => updateNode(branch, targetNode)); - } - - current = current.next as WorkflowNode; - } - - return draft; - }); -}; - -export const removeNode = (root: WorkflowNode, targetNodeId: string) => { - return produce(root, (draft) => { - let current = draft; - while (current) { - if (current.next?.id === targetNodeId) { - current.next = current.next.next; - break; - } - - if (isBranchNode(current)) { - current.branches ??= []; - current.branches = current.branches.map((branch) => removeNode(branch, targetNodeId)); - } - - current = current.next as WorkflowNode; - } - - return draft; - }); -}; - -export const addBranch = (root: WorkflowNode, branchNodeId: string) => { - return produce(root, (draft) => { - let current = draft; - while (current) { - if (current.id === branchNodeId) { - if (current.type !== WorkflowNodeType.Branch) { - return draft; - } - - current.branches ??= []; - current.branches.push( - newNode(WorkflowNodeType.Condition, { - branchIndex: current.branches.length, - }) - ); - break; - } - - if (isBranchNode(current)) { - current.branches ??= []; - current.branches = current.branches.map((branch) => addBranch(branch, branchNodeId)); - } - - current = current.next as WorkflowNode; - } - - return draft; - }); -}; - -export const duplicateBranch = (root: WorkflowNode, branchNodeId: string, branchIndex: number) => { - return produce(root, (draft) => { - let current = draft; - let last: WorkflowNode | undefined = { - id: "", - name: "", - type: WorkflowNodeType.Start, - next: draft, - }; - while (current && last) { - if (current.id === branchNodeId) { - if (!isBranchNode(current)) { - return draft; - } - - current.branches ??= []; - current.branches.splice(branchIndex + 1, 0, cloneNode(current.branches[branchIndex])); - - break; - } - - if (isBranchNode(current)) { - current.branches ??= []; - current.branches = current.branches.map((branch) => duplicateBranch(branch, branchNodeId, branchIndex)); - } - - current = current.next as WorkflowNode; - last = last.next; - } - - return draft; - }); -}; - -export const removeBranch = (root: WorkflowNode, branchNodeId: string, branchIndex: number) => { - return produce(root, (draft) => { - let current = draft; - let last: WorkflowNode | undefined = { - id: "", - name: "", - type: WorkflowNodeType.Start, - next: draft, - }; - while (current && last) { - if (current.id === branchNodeId) { - if (!isBranchNode(current)) { - return draft; - } - - current.branches ??= []; - current.branches.splice(branchIndex, 1); - - // 如果仅剩一个分支,删除分支节点,将分支节点的下一个节点挂载到当前节点 - if (current.branches.length === 1) { - const branch = current.branches[0]; - if (branch.next) { - last.next = branch.next; - let lastNode: WorkflowNode | undefined = branch.next; - while (lastNode?.next) { - lastNode = lastNode.next; - } - lastNode.next = current.next; - } else { - last.next = current.next; - } - } - - break; - } - - if (isBranchNode(current)) { - current.branches ??= []; - current.branches = current.branches.map((branch) => removeBranch(branch, branchNodeId, branchIndex)); - } - - current = current.next as WorkflowNode; - last = last.next; - } - - return draft; - }); -}; - -export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, typeFilter?: string | string[]): WorkflowNode[] => { - // 某个分支的节点,不应该能获取到相邻分支上节点的输出 - const outputs: WorkflowNode[] = []; - - const filter = (io: WorkflowNodeIO) => { - if (typeFilter == null) { - return true; - } - - if (Array.isArray(typeFilter) && typeFilter.includes(io.type)) { - return true; - } else if (io.type === typeFilter) { - return true; - } - - return false; - }; - - const traverse = (current: WorkflowNode, output: WorkflowNode[]) => { - if (!current) { - return false; - } - if (current.id === nodeId) { - return true; - } - - if (current.type !== WorkflowNodeType.Branch && current.outputs && current.outputs.some((io) => filter(io))) { - output.push({ - ...current, - outputs: current.outputs.filter((io) => filter(io)), - }); - } - - if (isBranchNode(current)) { - let currentLength = output.length; - const latestOutput = output.length > 0 ? output[output.length - 1] : null; - for (const branch of current.branches!) { - if (branch.type === WorkflowNodeType.ExecuteFailure) { - output.splice(output.length - 1); - currentLength -= 1; - } - if (traverse(branch, output)) { - return true; - } - // 如果当前分支没有输出,清空之前的输出 - if (output.length > currentLength) { - output.splice(currentLength); - } - if (latestOutput && branch.type === WorkflowNodeType.ExecuteFailure) { - output.push(latestOutput); - currentLength += 1; - } - } - } - - return traverse(current.next as WorkflowNode, output); - }; - - traverse(root, outputs); - return outputs; -}; - -export const isAllNodesValidated = (node: WorkflowNode): boolean => { - let current = node as typeof node | undefined; - while (current) { - if (isBranchNode(current)) { - for (const branch of current.branches!) { - if (!isAllNodesValidated(branch)) { - return false; - } - } - } else { - if (!current.validated) { - return false; - } - } - - current = current.next; - } - - return true; -}; diff --git a/ui/src/domain/workflowRun.ts b/ui/src/domain/workflowRun.ts index bd118e480..c899f427a 100644 --- a/ui/src/domain/workflowRun.ts +++ b/ui/src/domain/workflowRun.ts @@ -14,7 +14,7 @@ export interface WorkflowRunModel extends BaseModel { export const WORKFLOW_RUN_STATUSES = Object.freeze({ PENDING: "pending", - RUNNING: "running", + PROCESSING: "processing", SUCCEEDED: "succeeded", FAILED: "failed", CANCELED: "canceled", diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index 5c5b934c7..605a612cc 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -19,7 +19,7 @@ "workflow.action.batch_delete.modal.title": "Delete workflows", "workflow.action.batch_delete.modal.content": "Are you sure want to delete these {{count}} selected workflows?
This action cannot be undone.", "workflow.action.enable.button": "Activate", - "workflow.action.enable.errmsg.uncompleted": "Please complete the orchestration and publish the changes first", + "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", diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 420130d2f..5646b884d 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -1,7 +1,7 @@ { + "workflow_node.kind.basis": "Basis", "workflow_node.kind.business": "Business", "workflow_node.kind.logic": "Logic", - "workflow_node.kind.common": "Common", "workflow_node.start.label": "Start", "workflow_node.start.help": "Define the trigger for workflow initiation. A workflow can only have one start node.", @@ -14,8 +14,8 @@ "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. Time zone is based on the server.", - "workflow_node.start.form.trigger_cron.help": "Expected execution time for the last 5 times:", + "workflow_node.start.form.trigger_cron.tooltip": "Exactly 5 space separated segments.", + "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.

Reference links:
1. Let’s Encrypt rate limits
2. Why should my Let’s Encrypt (ACME) client run at a random time?", "workflow_node.apply.label": "Request certificate", @@ -62,6 +62,8 @@ "workflow_node.apply.form.tencentcloud_eo_zone_id.label": "Tencent Cloud EdgeOne zone ID", "workflow_node.apply.form.tencentcloud_eo_zone_id.placeholder": "Please enter Tencent Cloud EdgeOne zone ID", "workflow_node.apply.form.tencentcloud_eo_zone_id.tooltip": "For more information, see https://console.tencentcloud.com/edgeone", + "workflow_node.apply.form.key_algorithm.label": "Certificate key algorithm", + "workflow_node.apply.form.key_algorithm.placeholder": "Please select certificate key algorithm", "workflow_node.apply.form.ca_provider.label": "Certificate authority (Optional)", "workflow_node.apply.form.ca_provider.placeholder": "Please select a certificate authority", "workflow_node.apply.form.ca_provider.tooltip": "Used to issue SSL certificates.", @@ -69,8 +71,6 @@ "workflow_node.apply.form.ca_provider_access.label": "Certificate authority credential", "workflow_node.apply.form.ca_provider_access.placeholder": "Please select an credential of the certificate authority", "workflow_node.apply.form.ca_provider_access.button": "Create", - "workflow_node.apply.form.key_algorithm.label": "Certificate key algorithm", - "workflow_node.apply.form.key_algorithm.placeholder": "Please select certificate key algorithm", "workflow_node.apply.form.acme_profile.label": "ACME certificate profile (Optional)", "workflow_node.apply.form.acme_profile.placeholder": "Please enter ACME certificate profile", "workflow_node.apply.form.acme_profile.tooltip": "It determines the ACME profile which will be used to affect issuance of the certificate requested. If you don't understand this option, just keep it by default.
Click here to learn more.", @@ -137,6 +137,9 @@ "workflow_node.deploy.form_anchor.deployment.title": "Deployment settings", "workflow_node.deploy.form_anchor.strategy.tab": "Strategy", "workflow_node.deploy.form_anchor.strategy.title": "Strategy settings", + "workflow_node.deploy.form.certificate_output_node_id.label": "Certificate to deploy", + "workflow_node.deploy.form.certificate_output_node_id.placeholder": "Please select certificate to deploy", + "workflow_node.deploy.form.certificate_output_node_id.help": "Notes: The certificate to be deployed comes from the previous nodes of application or upload.", "workflow_node.deploy.form.provider.label": "Deploy target", "workflow_node.deploy.form.provider.placeholder": "Please select deploy target", "workflow_node.deploy.form.provider.search.placeholder": "Search deploy target ...", @@ -144,9 +147,6 @@ "workflow_node.deploy.form.provider_access.placeholder": "Please select an credential of Hosting provider", "workflow_node.deploy.form.provider_access.tooltip": "Used to invoke API during deployment.", "workflow_node.deploy.form.provider_access.button": "Create", - "workflow_node.deploy.form.certificate.label": "Certificate", - "workflow_node.deploy.form.certificate.placeholder": "Please select certificate", - "workflow_node.deploy.form.certificate.help": "Notes: The certificate to be deployed comes from the previous nodes of application or upload.", "workflow_node.deploy.form.1panel_console_auto_restart.label": "Auto restart 1Panel after deployment", "workflow_node.deploy.form.1panel_site_node_name.label": "1Panel node name (Optional)", "workflow_node.deploy.form.1panel_site_node_name.placeholder": "Please enter 1Panel node name", @@ -985,6 +985,8 @@ "workflow_node.notify.help": "Invoke the APIs of the service provider to push message notifications.", "workflow_node.notify.default_name": "Notification", "workflow_node.notify.form_anchor.parameters.tab": "Parameters", + "workflow_node.notify.form_anchor.channel.tab": "Channel", + "workflow_node.notify.form_anchor.channel.title": "Channel settings", "workflow_node.notify.form_anchor.strategy.tab": "Strategy", "workflow_node.notify.form_anchor.strategy.title": "Strategy settings", "workflow_node.notify.form.subject.label": "Subject", @@ -1067,45 +1069,5 @@ "workflow_node.end.label": "End", "workflow_node.end.help": "Suspend the workflow and exit. Usually the last node of a workflow.", - "workflow_node.end.default_name": "End", - - "@deprecated": "TODO: remove in v0.4.x below", - "workflow_node.action.configure_node": "Configure node", - "workflow_node.action.add_node": "Add node", - "workflow_node.action.rename_node": "Rename node", - "workflow_node.action.duplicate_node": "Duplicate node", - "workflow_node.action.remove_node": "Delete node", - "workflow_node.action.add_branch": "Add branch", - "workflow_node.action.rename_branch": "Rename branch", - "workflow_node.action.duplicate_branch": "Duplicate branch", - "workflow_node.action.remove_branch": "Delete branch", - "workflow_node.branch.label": "Parallel/Conditional branch", - "workflow_node.branch.default_name": "Branch", - "workflow_node.condition.form.expression.label": "Conditions to enter the branch", - "workflow_node.condition.form.expression.logical_operator.errmsg": "Please select logical operator of conditions", - "workflow_node.condition.form.expression.logical_operator.option.and.label": "Meeting all of the conditions (AND)", - "workflow_node.condition.form.expression.logical_operator.option.or.label": "Meeting any of the conditions (OR)", - "workflow_node.condition.form.expression.variable.placeholder": "Please select", - "workflow_node.condition.form.expression.variable.errmsg": "Please select variable", - "workflow_node.condition.form.expression.operator.placeholder": "Please select", - "workflow_node.condition.form.expression.operator.errmsg": "Please select operator", - "workflow_node.condition.form.expression.operator.option.eq.label": "equal to", - "workflow_node.condition.form.expression.operator.option.eq.alias_is_label": "is", - "workflow_node.condition.form.expression.operator.option.neq.label": "not equal to", - "workflow_node.condition.form.expression.operator.option.neq.alias_not_label": "is not", - "workflow_node.condition.form.expression.operator.option.gt.label": "greater than", - "workflow_node.condition.form.expression.operator.option.gte.label": "greater than or equal to", - "workflow_node.condition.form.expression.operator.option.lt.label": "less than", - "workflow_node.condition.form.expression.operator.option.lte.label": "less than or equal to", - "workflow_node.condition.form.expression.value.placeholder": "Please enter", - "workflow_node.condition.form.expression.value.errmsg": "Please enter value", - "workflow_node.condition.form.expression.value.option.true.label": "True", - "workflow_node.condition.form.expression.value.option.false.label": "False", - "workflow_node.condition.form.expression.add_condition.button": "Add condition", - "workflow_node.execute_result_branch.label": "Execution result branch", - "workflow_node.execute_result_branch.default_name": "Branch", - "workflow_node.execute_success.label": "If the previous node succeeded ...", - "workflow_node.execute_success.default_name": "On Succeeded", - "workflow_node.execute_failure.label": "If the previous node failed ...", - "workflow_node.execute_failure.default_name": "On Failed" + "workflow_node.end.default_name": "End" } diff --git a/ui/src/i18n/locales/en/nls.workflow.runs.json b/ui/src/i18n/locales/en/nls.workflow.runs.json index a06b7d2a3..525a68079 100644 --- a/ui/src/i18n/locales/en/nls.workflow.runs.json +++ b/ui/src/i18n/locales/en/nls.workflow.runs.json @@ -1,9 +1,9 @@ { "workflow_run.action.view.menu": "View details", - "workflow_run.action.cancel.menu": "Cancel workflow run", - "workflow_run.action.cancel.modal.title": "Cancel run", + "workflow_run.action.cancel.menu": "Cancel run", + "workflow_run.action.cancel.modal.title": "Cancel workflow run", "workflow_run.action.cancel.modal.content": "Are you sure to cancel this run?", - "workflow_run.action.delete.menu": "Delete workflow run", + "workflow_run.action.delete.menu": "Delete run", "workflow_run.action.delete.modal.title": "Delete \"{{name}}\"", "workflow_run.action.delete.modal.content": "Are you sure want to delete this workflow run?
This action cannot be undone.", "workflow_run.action.batch_delete.modal.title": "Delete workflow runs", @@ -16,7 +16,7 @@ "workflow_run.props.status": "Status", "workflow_run.props.status.pending": "Pending", - "workflow_run.props.status.running": "Running", + "workflow_run.props.status.processing": "Processing", "workflow_run.props.status.succeeded": "Succeeded", "workflow_run.props.status.failed": "Failed", "workflow_run.props.status.canceled": "Canceled", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 79b54605a..353fcabf5 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -39,7 +39,7 @@ "access.form.name.placeholder": "请输入授权名称", "access.form.provider.label": "提供商", "access.form.provider.placeholder": "请选择提供商", - "access.form.provider.help": "提供商分为两种类型:
【DNS 提供商】你的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理你的域名解析记录。
【主机提供商】你的服务器或云服务的托管方,用于部署签发的证书。", + "access.form.provider.help": "提供商分为两种类型:
【DNS 提供商】你的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理域名解析记录。
【主机提供商】你的服务器或云服务的托管方,用于部署签发的证书。", "access.form.provider.search.placeholder": "搜索提供商……", "access.form.common_allow_insecure_conns.label": "忽略 SSL/TLS 证书错误", "access.form.common_allow_insecure_conns.switch.on": "允许", diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index 9bc0f507a..580d01a11 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -19,7 +19,7 @@ "workflow.action.batch_delete.modal.title": "删除工作流", "workflow.action.batch_delete.modal.content": "确定要删除这 {{count}} 个被选中的工作流吗?
注意此操作不可撤销,请谨慎操作。", "workflow.action.enable.button": "启用", - "workflow.action.enable.errmsg.uncompleted": "请先完成流程编排并发布更改", + "workflow.action.enable.errmsg.unpublished": "请先完成流程编排并发布更改", "workflow.action.disable.button": "停用", "workflow.action.run.button": "运行", "workflow.action.run.menu": "运行工作流", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index d89e1e4dc..b098eea67 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -1,7 +1,7 @@ { + "workflow_node.kind.basis": "基础", "workflow_node.kind.business": "业务", "workflow_node.kind.logic": "逻辑", - "workflow_node.kind.common": "常规", "workflow_node.start.label": "开始", "workflow_node.start.help": "定义工作流启动的触发条件。一个工作流只能有一个开始节点。", @@ -14,8 +14,8 @@ "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": "五段式表达式,支持使用任意值(即 *)、值列表分隔符(即 ,)、值的范围(即 -)、步骤值(即 /)等四种表达式。时区以服务器设置为准。", - "workflow_node.start.form.trigger_cron.help": "预计最近 5 次运行时间:", + "workflow_node.start.form.trigger_cron.tooltip": "五段式表达式。
支持使用任意值(即 *)、值列表分隔符(即 ,)、值的范围(即 -)、步骤值(即 /)等四种表达式。", + "workflow_node.start.form.trigger_cron.help": "预计最近 5 次运行时间(实际时区以服务器设置为准):", "workflow_node.start.form.trigger_cron.guide": "如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。也不要总是设置为每日零时,以免遭遇证书颁发机构的流量高峰。

参考链接:
1. Let’s Encrypt 速率限制
2. 为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?", "workflow_node.apply.label": "申请签发证书", @@ -62,14 +62,14 @@ "workflow_node.apply.form.tencentcloud_eo_zone_id.label": "腾讯云 EdgeOne 站点 ID", "workflow_node.apply.form.tencentcloud_eo_zone_id.placeholder": "请输入腾讯云 EdgeOne 站点 ID", "workflow_node.apply.form.tencentcloud_eo_zone_id.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/edgeone", + "workflow_node.apply.form.key_algorithm.label": "证书算法", + "workflow_node.apply.form.key_algorithm.placeholder": "请选择证书算法", "workflow_node.apply.form.ca_provider.label": "证书颁发机构(可选)", "workflow_node.apply.form.ca_provider.placeholder": "请选择证书颁发机构", "workflow_node.apply.form.ca_provider.button": "设置", "workflow_node.apply.form.ca_provider_access.label": "证书颁发机构授权", "workflow_node.apply.form.ca_provider_access.placeholder": "请选择证书颁发机构授权", "workflow_node.apply.form.ca_provider_access.button": "新建", - "workflow_node.apply.form.key_algorithm.label": "证书算法", - "workflow_node.apply.form.key_algorithm.placeholder": "请选择证书算法", "workflow_node.apply.form.acme_profile.label": "ACME 证书配置(可选)", "workflow_node.apply.form.acme_profile.placeholder": "请输入 ACME 证书配置", "workflow_node.apply.form.acme_profile.tooltip": "表示证书颁发时使用的 ACME 证书配置。如果你不了解该选项的用途,保持默认即可。
点此了解更多。", @@ -136,6 +136,9 @@ "workflow_node.deploy.form_anchor.deployment.title": "部署设置", "workflow_node.deploy.form_anchor.strategy.tab": "执行策略", "workflow_node.deploy.form_anchor.strategy.title": "执行策略", + "workflow_node.deploy.form.certificate_output_node_id.label": "待部署证书", + "workflow_node.deploy.form.certificate_output_node_id.placeholder": "请选择待部署证书", + "workflow_node.deploy.form.certificate_output_node_id.help": "提示:待部署证书来自之前的申请或上传节点,如果选项为空请先检查前序节点。", "workflow_node.deploy.form.provider.label": "部署目标", "workflow_node.deploy.form.provider.placeholder": "请选择部署目标", "workflow_node.deploy.form.provider.search.placeholder": "搜索部署目标……", @@ -143,9 +146,6 @@ "workflow_node.deploy.form.provider_access.placeholder": "请选择主机提供商授权", "workflow_node.deploy.form.provider_access.tooltip": "用于部署证书时调用相关 API,注意与申请阶段所需的 DNS 提供商相区分。", "workflow_node.deploy.form.provider_access.button": "新建", - "workflow_node.deploy.form.certificate.label": "待部署证书", - "workflow_node.deploy.form.certificate.placeholder": "请选择待部署证书", - "workflow_node.deploy.form.certificate.help": "提示:待部署证书来自之前的申请或上传节点,如果选项为空请先检查前序节点。", "workflow_node.deploy.form.1panel_console_auto_restart.label": "部署后自动重启 1Panel 服务", "workflow_node.deploy.form.1panel_site_node_name.label": "1Panel 子节点名称(可选)", "workflow_node.deploy.form.1panel_site_node_name.placeholder": "请输入 1Panel 子节点名称", @@ -983,6 +983,8 @@ "workflow_node.notify.help": "调用服务提供商相关 API,将消息通知推送到指定的目标。", "workflow_node.notify.default_name": "通知", "workflow_node.notify.form_anchor.parameters.tab": "参数设置", + "workflow_node.notify.form_anchor.channel.tab": "渠道设置", + "workflow_node.notify.form_anchor.channel.title": "渠道设置", "workflow_node.notify.form_anchor.strategy.tab": "执行策略", "workflow_node.notify.form_anchor.strategy.title": "执行策略", "workflow_node.notify.form.subject.label": "通知主题", @@ -1024,8 +1026,8 @@ "workflow_node.condition.label": "并行/条件分支", "workflow_node.condition.help": "当满足指定的条件时,进入相应分支。某一分支中的节点执行失败不影响平行分支继续执行。", "workflow_node.condition.default_name": "并行", - "workflow_node.condition.default_name.template_certtest_on_expire_soon": "若网站证书即将到期 ...", - "workflow_node.condition.default_name.template_certtest_on_expired": "若网站证书已到期 ...", + "workflow_node.condition.default_name.template_certtest_on_expire_soon": "若网站证书即将过期…", + "workflow_node.condition.default_name.template_certtest_on_expired": "若网站证书已过期…", "workflow_node.branch_block.label": "分支", "workflow_node.branch_block.default_name": "分支", @@ -1065,45 +1067,5 @@ "workflow_node.end.label": "结束", "workflow_node.end.help": "中止工作流运行并退出。通常是一个工作流的最后一个节点。", - "workflow_node.end.default_name": "结束", - - "@deprecated": "TODO: remove in v0.4.x below", - "workflow_node.action.configure_node": "配置节点", - "workflow_node.action.add_node": "添加节点", - "workflow_node.action.rename_node": "重命名", - "workflow_node.action.duplicate_node": "复制节点", - "workflow_node.action.remove_node": "删除节点", - "workflow_node.action.add_branch": "添加分支", - "workflow_node.action.rename_branch": "重命名", - "workflow_node.action.duplicate_branch": "复制分支", - "workflow_node.action.remove_branch": "删除分支", - "workflow_node.branch.label": "并行/条件分支", - "workflow_node.branch.default_name": "分支", - "workflow_node.condition.form.expression.label": "分支进入条件", - "workflow_node.condition.form.expression.logical_operator.errmsg": "请选择条件组合方式", - "workflow_node.condition.form.expression.logical_operator.option.and.label": "满足以下所有条件 (AND)", - "workflow_node.condition.form.expression.logical_operator.option.or.label": "满足以下任一条件 (OR)", - "workflow_node.condition.form.expression.variable.placeholder": "请选择", - "workflow_node.condition.form.expression.variable.errmsg": "请选择变量", - "workflow_node.condition.form.expression.operator.placeholder": "请选择", - "workflow_node.condition.form.expression.operator.errmsg": "请选择运算符", - "workflow_node.condition.form.expression.operator.option.eq.label": "等于", - "workflow_node.condition.form.expression.operator.option.eq.alias_is_label": "为", - "workflow_node.condition.form.expression.operator.option.neq.label": "不等于", - "workflow_node.condition.form.expression.operator.option.neq.alias_not_label": "不为", - "workflow_node.condition.form.expression.operator.option.gt.label": "大于", - "workflow_node.condition.form.expression.operator.option.gte.label": "大于等于", - "workflow_node.condition.form.expression.operator.option.lt.label": "小于", - "workflow_node.condition.form.expression.operator.option.lte.label": "小于等于", - "workflow_node.condition.form.expression.value.placeholder": "请输入", - "workflow_node.condition.form.expression.value.errmsg": "请输入值", - "workflow_node.condition.form.expression.value.option.true.label": "真", - "workflow_node.condition.form.expression.value.option.false.label": "假", - "workflow_node.condition.form.expression.add_condition.button": "添加条件", - "workflow_node.execute_result_branch.label": "执行结果分支", - "workflow_node.execute_result_branch.default_name": "分支", - "workflow_node.execute_success.label": "若上一节点执行成功…", - "workflow_node.execute_success.default_name": "执行成功", - "workflow_node.execute_failure.label": "若上一节点执行失败…", - "workflow_node.execute_failure.default_name": "执行失败" + "workflow_node.end.default_name": "结束" } diff --git a/ui/src/i18n/locales/zh/nls.workflow.runs.json b/ui/src/i18n/locales/zh/nls.workflow.runs.json index 08fd62651..f38749b43 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.runs.json +++ b/ui/src/i18n/locales/zh/nls.workflow.runs.json @@ -16,7 +16,7 @@ "workflow_run.props.status": "状态", "workflow_run.props.status.pending": "等待运行", - "workflow_run.props.status.running": "运行中", + "workflow_run.props.status.processing": "运行中", "workflow_run.props.status.succeeded": "已成功", "workflow_run.props.status.failed": "已失败", "workflow_run.props.status.canceled": "已取消", diff --git a/ui/src/pages/ConsoleLayout.tsx b/ui/src/pages/ConsoleLayout.tsx index f9676b2ba..c2964657a 100644 --- a/ui/src/pages/ConsoleLayout.tsx +++ b/ui/src/pages/ConsoleLayout.tsx @@ -107,7 +107,7 @@ const ConsoleLayout = () => {
{ items: [ { key: "rollback", - disabled: !allowRollback, + disabled: workflowRollbackDisabled, label: t("workflow.detail.design.action.rollback.button"), icon: , onClick: handleRollbackClick, @@ -168,6 +151,7 @@ const WorkflowDetailDesign = () => {
+
@@ -181,185 +165,17 @@ const WorkflowDetailDesign = () => {
- + {designerError && ( +
+ +
+ )} + +
); }; -const compactWorkflowDraft = (root: WorkflowNode | undefined) => { - const { t } = getI18n(); - - // TODO: 仅为兼容适配 v0.3.x 数据,正式上线后待删除 - const res: FlowDocumentJSON["nodes"] = []; - - if (!root) { - res.push({ - id: nanoid(), - type: "start", - data: { - name: t("workflow_node.start.default_name"), - }, - }); - } else { - const convert = (node: WorkflowNode | undefined) => { - const temp: typeof res = []; - - let current: typeof node = node; - while (current) { - switch (current.type) { - case WorkflowNodeType.Start: - temp.push({ - id: current.id, - type: "start", - data: { - name: current.name, - config: current.config, - }, - }); - break; - - case WorkflowNodeType.Apply: - temp.push({ - id: current.id, - type: "bizApply", - data: { - name: current.name, - config: current.config, - }, - }); - break; - - case WorkflowNodeType.Upload: - temp.push({ - id: current.id, - type: "bizUpload", - data: { - name: current.name, - config: current.config, - }, - }); - break; - - case WorkflowNodeType.Monitor: - temp.push({ - id: current.id, - type: "bizMonitor", - data: { - name: current.name, - config: current.config, - }, - }); - break; - - case WorkflowNodeType.Deploy: - temp.push({ - id: current.id, - type: "bizDeploy", - data: { - name: current.name, - config: current.config, - }, - }); - break; - - case WorkflowNodeType.Notify: - temp.push({ - id: current.id, - type: "bizNotify", - data: { - name: current.name, - config: current.config, - }, - }); - break; - - case WorkflowNodeType.ExecuteResultBranch: { - const tryNode = temp.pop()!; - temp.push({ - id: current.id, - type: "tryCatch", - blocks: [ - { - id: current.branches?.find((b) => b.type === WorkflowNodeType.ExecuteSuccess)?.id || nanoid(), - type: "tryBlock", - blocks: [tryNode], - data: { - name: current.branches?.find((b) => b.type === WorkflowNodeType.ExecuteSuccess)?.name, - }, - }, - { - id: current.branches?.find((b) => b.type === WorkflowNodeType.ExecuteFailure)?.id || nanoid(), - type: "catchBlock", - blocks: [ - ...convert(current.branches?.find((b) => b.type === WorkflowNodeType.ExecuteFailure)?.next), - { - id: nanoid(), - type: "end", - data: { - name: t("workflow_node.end.default_name"), - }, - }, - ], - data: { - name: current.branches?.find((b) => b.type === WorkflowNodeType.ExecuteFailure)?.name, - }, - }, - ], - data: { - name: current.name, - config: current.config, - }, - }); - - current = current.branches?.find((b) => b.type === WorkflowNodeType.ExecuteSuccess); - break; - } - - case WorkflowNodeType.Branch: { - temp.push({ - id: current.id, - type: "condition", - blocks: - current.branches?.map((branch) => { - return { - id: branch.id, - type: "branchBlock", - blocks: convert(branch.next), - data: { - name: branch.name, - config: branch.config, - }, - }; - }) ?? [], - data: { - name: current.name, - config: current.config, - }, - }); - break; - } - } - - current = current?.next; - } - - return temp; - }; - - res.push(...convert(root)); - } - - res.push({ - id: nanoid(), - type: "end", - data: { - name: t("workflow_node.end.default_name"), - }, - }); - - return res; -}; - export default WorkflowDetailDesign; diff --git a/ui/src/pages/workflows/WorkflowDetailDesignLegacy.tsx b/ui/src/pages/workflows/WorkflowDetailDesignLegacy.tsx deleted file mode 100644 index 2f085f093..000000000 --- a/ui/src/pages/workflows/WorkflowDetailDesignLegacy.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { IconArrowBackUp, IconDots } from "@tabler/icons-react"; -import { Alert, App, Button, Card, Dropdown, Space } from "antd"; -import { isEqual } from "radash"; - -import Show from "@/components/Show"; -import WorkflowElementsContainer from "@/components/workflow/WorkflowElementsContainer"; -import { isAllNodesValidated } from "@/domain/workflow"; -import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun"; -import { useZustandShallowSelector } from "@/hooks"; -import { useWorkflowStore } from "@/stores/workflow"; -import { getErrMsg } from "@/utils/error"; - -/** - * - * @deprecated - */ -const WorkflowDetailDesign = () => { - const { t } = useTranslation(); - - const { message, modal, notification } = App.useApp(); - - const { workflow, ...workflowState } = useWorkflowStore(useZustandShallowSelector(["workflow", "init", "publish", "rollback"])); - - const [isPendingOrRunning, setIsPendingOrRunning] = useState(false); - const [allowRollback, setAllowRollback] = useState(false); - const [allowPublish, setAllowPublish] = useState(false); - - useEffect(() => { - const pending = workflow.lastRunStatus === WORKFLOW_RUN_STATUSES.PENDING || workflow.lastRunStatus === WORKFLOW_RUN_STATUSES.RUNNING; - setIsPendingOrRunning(pending); - }, [workflow]); - - useEffect(() => { - const hasContent = !!workflow.content; - const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content); - setAllowRollback(!isPendingOrRunning && hasContent && hasChanges); - setAllowPublish(!isPendingOrRunning && hasChanges); - }, [workflow.content, workflow.draft, workflow.hasDraft, isPendingOrRunning]); - - const handleRollbackClick = () => { - modal.confirm({ - title: t("workflow.detail.design.action.rollback.modal.title"), - content: t("workflow.detail.design.action.rollback.modal.content"), - onOk: async () => { - try { - await workflowState.rollback(); - - message.success(t("common.text.operation_succeeded")); - } catch (err) { - console.error(err); - notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); - } - }, - }); - }; - - const handlePublishClick = () => { - if (!isAllNodesValidated(workflow.draft!)) { - message.warning(t("workflow.detail.design.uncompleted_design.alert")); - return; - } - - modal.confirm({ - title: t("workflow.detail.design.action.publish.modal.title"), - content: t("workflow.detail.design.action.publish.modal.content"), - onOk: async () => { - try { - await workflowState.publish(); - - message.success(t("common.text.operation_succeeded")); - } catch (err) { - console.error(err); - notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); - } - }, - }); - }; - - return ( -
- -
-
-
- - 该子页面即将在 v0.4.0 中被移除。 -
- This subpage will be dropped in v0.4.0. -
- } - showIcon - closable - type="warning" - /> -
-
-
- - {t("workflow.detail.design.unpublished_draft.alert")}
} showIcon type="warning" /> - -
- - - , - onClick: handleRollbackClick, - }, - ], - }} - trigger={["click"]} - > -
-
-
-
- - - - - - ); -}; - -export default WorkflowDetailDesign; diff --git a/ui/src/pages/workflows/WorkflowDetailRuns.tsx b/ui/src/pages/workflows/WorkflowDetailRuns.tsx index b2a2469c6..7659d35bd 100644 --- a/ui/src/pages/workflows/WorkflowDetailRuns.tsx +++ b/ui/src/pages/workflows/WorkflowDetailRuns.tsx @@ -99,7 +99,7 @@ const WorkflowDetailRuns = () => { fixed: "right", width: 64, render: (_, record) => { - const cancelDisabled = !([WORKFLOW_RUN_STATUSES.PENDING, WORKFLOW_RUN_STATUSES.RUNNING] as string[]).includes(record.status); + const cancelDisabled = !([WORKFLOW_RUN_STATUSES.PENDING, WORKFLOW_RUN_STATUSES.PROCESSING] as string[]).includes(record.status); const deleteDisabled = !cancelDisabled; return ( @@ -120,10 +120,10 @@ const WorkflowDetailRuns = () => { }, { key: "cancel", - label: t("workflow_run.action.cancel.menu"), + label: {t("workflow_run.action.cancel.menu")}, icon: ( - + ), disabled: cancelDisabled, @@ -224,7 +224,7 @@ const WorkflowDetailRuns = () => { ); useEffect(() => { - const items = tableData.filter((e) => e.status === WORKFLOW_RUN_STATUSES.PENDING || e.status === WORKFLOW_RUN_STATUSES.RUNNING); + const items = tableData.filter((e) => e.status === WORKFLOW_RUN_STATUSES.PENDING || e.status === WORKFLOW_RUN_STATUSES.PROCESSING); for (const item of items) { subscribeWorkflowRun(item.id, (cb) => { setTableData((prev) => { @@ -236,10 +236,10 @@ const WorkflowDetailRuns = () => { }); if (cb.record.id === detailDrawerProps.data?.id) { - setDetailRecord({ ...detailDrawerProps.data, ...cb.record }); + detailDrawerProps.data = { ...detailDrawerProps.data, ...cb.record }; } - if (cb.record.status !== WORKFLOW_RUN_STATUSES.PENDING && cb.record.status !== WORKFLOW_RUN_STATUSES.RUNNING) { + if (cb.record.status !== WORKFLOW_RUN_STATUSES.PENDING && cb.record.status !== WORKFLOW_RUN_STATUSES.PROCESSING) { unsubscribeWorkflowRun(item.id); } }); @@ -257,11 +257,10 @@ const WorkflowDetailRuns = () => { setPageSize(pageSize); }; - const { setData: setDetailRecord, setOpen: setDetailOpen, ...detailDrawerProps } = WorkflowRunDetailDrawer.useProps(); + const { drawerProps: detailDrawerProps, ...detailDrawer } = WorkflowRunDetailDrawer.useDrawer(); const handleRecordDetailClick = (workflowRun: WorkflowRunModel) => { - setDetailRecord(workflowRun); - setDetailOpen(true); + detailDrawer.open(workflowRun); }; const handleRecordCancelClick = (workflowRun: WorkflowRunModel) => { @@ -326,7 +325,7 @@ const WorkflowDetailRuns = () => { okButtonProps: { danger: true }, onOk: async () => { // 未结束的记录不允许删除 - records = records.filter((record) => !([WORKFLOW_RUN_STATUSES.PENDING, WORKFLOW_RUN_STATUSES.RUNNING] as string[]).includes(record.status)); + records = records.filter((record) => !([WORKFLOW_RUN_STATUSES.PENDING, WORKFLOW_RUN_STATUSES.PROCESSING] as string[]).includes(record.status)); try { const resp = await removeWorkflowRun(records); if (resp) { diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx index bc48a7f4f..59f4f6620 100644 --- a/ui/src/pages/workflows/WorkflowList.tsx +++ b/ui/src/pages/workflows/WorkflowList.tsx @@ -11,7 +11,7 @@ import { startRun as startWorkflowRun } from "@/api/workflows"; import Empty from "@/components/Empty"; import Show from "@/components/Show"; import WorkflowStatus from "@/components/workflow/WorkflowStatus"; -import { WORKFLOW_TRIGGERS, type WorkflowModel, cloneNode, initWorkflow, isAllNodesValidated } from "@/domain/workflow"; +import { WORKFLOW_TRIGGERS, type WorkflowModel, duplicateNodes } from "@/domain/workflow"; import { useAppSettings } from "@/hooks"; import { list as listWorkflows, remove as removeWorkflow, save as saveWorkflow } from "@/repository/workflow"; import { getErrMsg } from "@/utils/error"; @@ -164,6 +164,7 @@ const WorkflowList = () => {
), + disabled: !record.hasContent, onClick: () => { handleRecordRunClick(record); }, @@ -312,8 +313,8 @@ const WorkflowList = () => { const handleRecordActiveChange = async (workflow: WorkflowModel) => { try { - if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) { - message.warning(t("workflow.action.enable.errmsg.uncompleted")); + if (!workflow.enabled && !workflow.graphContent) { + message.warning(t("workflow.action.enable.errmsg.unpublished")); return; } @@ -359,11 +360,7 @@ const WorkflowList = () => { description: workflow.description, trigger: workflow.trigger, triggerCron: workflow.triggerCron, - draft: workflow.content - ? cloneNode(workflow.content, { withCopySuffix: false }) - : workflow.draft - ? cloneNode(workflow.draft, { withCopySuffix: false }) - : initWorkflow().draft, + graphDraft: { nodes: duplicateNodes(workflow.graphContent?.nodes ?? workflow.graphDraft?.nodes ?? [], { withCopySuffix: false }) }, hasDraft: true, } as WorkflowModel; const resp = await saveWorkflow(workflowCopy); diff --git a/ui/src/pages/workflows/WorkflowNew.tsx b/ui/src/pages/workflows/WorkflowNew.tsx index 8930a2ba7..18f23acef 100644 --- a/ui/src/pages/workflows/WorkflowNew.tsx +++ b/ui/src/pages/workflows/WorkflowNew.tsx @@ -4,7 +4,14 @@ import { useNavigate } from "react-router-dom"; import { App, Card, Col, Row, Spin, Typography } from "antd"; import dayjs from "dayjs"; -import { type WorkflowModel, initWorkflow } from "@/domain/workflow"; +import { + WORKFLOW_NODE_TYPES, + type WorkflowModel, + type WorkflowNodeConfigForBizDeploy, + type WorkflowNodeConfigForBizNotify, + type WorkflowNodeConfigForBranchBlock, + newNode, +} from "@/domain/workflow"; import { save as saveWorkflow } from "@/repository/workflow"; import { getErrMsg } from "@/utils/error"; @@ -16,7 +23,7 @@ type TemplateKeys = typeof TEMPLATE_KEY_EMPTY | typeof TEMPLATE_KEY_CERTTEST | t const WorkflowNew = () => { const navigate = useNavigate(); - const { t } = useTranslation(); + const { i18n, t } = useTranslation(); const { notification } = App.useApp(); @@ -37,27 +44,169 @@ const WorkflowNew = () => { setTemplateSelectKey(key); try { - let workflow: WorkflowModel; + 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 = { nodes: [] }; + workflow.hasDraft = true; switch (key) { case TEMPLATE_KEY_EMPTY: - workflow = initWorkflow(); + { + const startNode = newNode(WORKFLOW_NODE_TYPES.START, { i18n: i18n }); + const endNode = newNode(WORKFLOW_NODE_TYPES.END, { i18n: i18n }); + + workflow.graphDraft!.nodes = [startNode, endNode]; + } break; case TEMPLATE_KEY_STANDARD: - workflow = initWorkflow({ template: "standard" }); + { + const startNode = newNode(WORKFLOW_NODE_TYPES.START, { i18n: i18n }); + const tryCatchNode = newNode(WORKFLOW_NODE_TYPES.TRYCATCH, { i18n: i18n }); + const applyNode = newNode(WORKFLOW_NODE_TYPES.BIZ_APPLY, { i18n: i18n }); + const deployNode = newNode(WORKFLOW_NODE_TYPES.BIZ_DEPLOY, { i18n: i18n }); + const notifyOnSuccessNode = newNode(WORKFLOW_NODE_TYPES.BIZ_NOTIFY, { i18n: i18n }); + const notifyOnFailureNode = newNode(WORKFLOW_NODE_TYPES.BIZ_NOTIFY, { i18n: i18n }); + const endNode = newNode(WORKFLOW_NODE_TYPES.END, { i18n: i18n }); + + deployNode.data.config = { + ...deployNode.data.config, + certificateOutputNodeId: applyNode.id, + } as WorkflowNodeConfigForBizDeploy; + + notifyOnSuccessNode.data.config = { + ...notifyOnSuccessNode.data.config, + subject: "[Certimate] Workflow Complete", + message: "Your workflow run has completed successfully.", + skipOnAllPrevSkipped: true, + } as WorkflowNodeConfigForBizNotify; + + notifyOnFailureNode.data.config = { + ...notifyOnFailureNode.data.config, + subject: "[Certimate] Workflow Failure Alert!", + message: "Your workflow run has failed. Please check the details.", + } as WorkflowNodeConfigForBizNotify; + + tryCatchNode.blocks!.at(0)!.blocks ??= []; + tryCatchNode.blocks!.at(0)!.blocks!.push(applyNode, deployNode, notifyOnSuccessNode); + tryCatchNode.blocks!.at(1)!.blocks ??= []; + tryCatchNode.blocks!.at(1)!.blocks!.unshift(notifyOnFailureNode); + + workflow.graphDraft!.nodes = [startNode, tryCatchNode, endNode]; + } break; case TEMPLATE_KEY_CERTTEST: - workflow = initWorkflow({ template: "certtest" }); + { + const startNode = newNode(WORKFLOW_NODE_TYPES.START, { i18n: i18n }); + const tryCatchNode = newNode(WORKFLOW_NODE_TYPES.TRYCATCH, { i18n: i18n }); + const monitorNode = newNode(WORKFLOW_NODE_TYPES.BIZ_MONITOR, { i18n: i18n }); + const conditionNode = newNode(WORKFLOW_NODE_TYPES.CONDITION, { i18n: i18n }); + const notifyOnExpireSoonNode = newNode(WORKFLOW_NODE_TYPES.BIZ_NOTIFY, { i18n: i18n }); + const notifyOnExpiredNode = newNode(WORKFLOW_NODE_TYPES.BIZ_NOTIFY, { i18n: i18n }); + const notifyOnFailureNode = newNode(WORKFLOW_NODE_TYPES.BIZ_NOTIFY, { i18n: i18n }); + const endNode = newNode(WORKFLOW_NODE_TYPES.END, { i18n: i18n }); + + notifyOnExpireSoonNode.data.config = { + ...notifyOnExpireSoonNode.data.config, + subject: "[Certimate] Certificate Expiry Alert!", + message: "The certificate which you are monitoring will expire soon. Please pay attention to your website.", + } as WorkflowNodeConfigForBizNotify; + + notifyOnExpiredNode.data.config = { + ...notifyOnExpiredNode.data.config, + subject: "[Certimate] Certificate Expiry Alert!", + message: "The certificate which you are monitoring has already expired. Please pay attention to your website.", + } as WorkflowNodeConfigForBizNotify; + + notifyOnFailureNode.data.config = { + ...notifyOnFailureNode.data.config, + subject: "[Certimate] Workflow Failure Alert!", + message: "Your workflow run has failed. Please check the details.", + } as WorkflowNodeConfigForBizNotify; + + tryCatchNode.blocks!.at(0)!.blocks ??= []; + tryCatchNode.blocks!.at(0)!.blocks!.push(monitorNode, conditionNode); + tryCatchNode.blocks!.at(1)!.blocks ??= []; + tryCatchNode.blocks!.at(1)!.blocks!.unshift(notifyOnFailureNode); + + conditionNode.blocks!.at(0)!.data.name = t("workflow_node.condition.default_name.template_certtest_on_expire_soon"); + conditionNode.blocks!.at(0)!.data.config = { + ...conditionNode.blocks!.at(0)!.data.config, + expression: { + left: { + left: { + selector: { + id: monitorNode.id, + name: "certificate.validity", + type: "boolean", + }, + type: "var", + }, + operator: "eq", + right: { + type: "const", + value: "true", + valueType: "boolean", + }, + type: "comparison", + }, + operator: "and", + right: { + left: { + selector: { + id: monitorNode.id, + name: "certificate.daysLeft", + type: "number", + }, + type: "var", + }, + operator: "lte", + right: { + type: "const", + value: "30", + valueType: "number", + }, + type: "comparison", + }, + type: "logical", + }, + } as WorkflowNodeConfigForBranchBlock; + conditionNode.blocks!.at(0)!.blocks ??= []; + conditionNode.blocks!.at(0)!.blocks!.push(notifyOnExpireSoonNode); + conditionNode.blocks!.at(1)!.data.name = t("workflow_node.condition.default_name.template_certtest_on_expired"); + conditionNode.blocks!.at(1)!.data.config = { + ...conditionNode.blocks!.at(1)!.data.config, + expression: { + left: { + selector: { + id: monitorNode.id, + name: "certificate.validity", + type: "boolean", + }, + type: "var", + }, + operator: "eq", + right: { + type: "const", + value: "false", + valueType: "boolean", + }, + type: "comparison", + }, + } as WorkflowNodeConfigForBranchBlock; + conditionNode.blocks!.at(1)!.blocks ??= []; + conditionNode.blocks!.at(1)!.blocks!.push(notifyOnExpiredNode); + + workflow.graphDraft!.nodes = [startNode, tryCatchNode, endNode]; + } break; default: - throw "Invalid state: `templateSelectKey`"; + throw "Invalid value of `templateSelectKey`"; } - 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 = await saveWorkflow(workflow); navigate(`/workflows/${workflow.id}`, { replace: true }); } catch (err) { diff --git a/ui/src/repository/certificate.ts b/ui/src/repository/certificate.ts index da322d21e..ff14f28fa 100644 --- a/ui/src/repository/certificate.ts +++ b/ui/src/repository/certificate.ts @@ -16,7 +16,7 @@ export const list = async (request: ListRequest) => { const filters: string[] = ["deleted=null"]; if (request.keyword) { - filters.push(pb.filter("(subjectAltNames~{:keyword} || serialNumber={:keyword})", { keyword: request.keyword })); + filters.push(pb.filter("(id={:keyword} || serialNumber={:keyword} || subjectAltNames~{:keyword})", { keyword: request.keyword })); } if (request.state === "expireSoon") { filters.push(pb.filter("validityNotAfter<{:expiredAt} && validityNotAfter>@now", { expiredAt: dayjs().add(20, "d").toDate() })); diff --git a/ui/src/repository/workflow.ts b/ui/src/repository/workflow.ts index c7a931a2e..d1ef1a2cf 100644 --- a/ui/src/repository/workflow.ts +++ b/ui/src/repository/workflow.ts @@ -17,7 +17,7 @@ export const list = async (request: ListRequest) => { const filters: string[] = []; if (request.keyword) { - filters.push(pb.filter("name~{:keyword}", { keyword: request.keyword })); + filters.push(pb.filter("(id={:keyword} || name~{:keyword})", { keyword: request.keyword })); } if (request.enabled != null) { filters.push(pb.filter("enabled={:enabled}", { enabled: request.enabled })); diff --git a/ui/src/routers/index.tsx b/ui/src/routers/index.tsx index 9ea8dd07a..5e192a1e7 100644 --- a/ui/src/routers/index.tsx +++ b/ui/src/routers/index.tsx @@ -15,7 +15,6 @@ import SettingsPersistence from "@/pages/settings/SettingsPersistence"; import SettingsSSLProvider from "@/pages/settings/SettingsSSLProvider"; import WorkflowDetail from "@/pages/workflows/WorkflowDetail"; import WorkflowDetailDesign from "@/pages/workflows/WorkflowDetailDesign"; -import WorkflowDetailDesignLegacy from "@/pages/workflows/WorkflowDetailDesignLegacy"; import WorkflowDetailRuns from "@/pages/workflows/WorkflowDetailRuns"; import WorkflowList from "@/pages/workflows/WorkflowList"; import WorkflowNew from "@/pages/workflows/WorkflowNew"; @@ -57,10 +56,6 @@ export const router = createHashRouter([ path: "/workflows/:id/design", element: , }, - { - path: "/workflows/:id/design-legacy", - element: , - }, { path: "/workflows/:id/runs", element: , diff --git a/ui/src/stores/workflow/index.ts b/ui/src/stores/workflow/index.ts index efcdbebf1..99b1fdc3c 100644 --- a/ui/src/stores/workflow/index.ts +++ b/ui/src/stores/workflow/index.ts @@ -1,21 +1,11 @@ import { produce } from "immer"; +import { isEqual } from "radash"; import { create } from "zustand"; -import { - type WorkflowModel, - type WorkflowNodeConfigForStart, - addBranch, - addNode, - duplicateBranch, - duplicateNode, - getOutputBeforeNodeId, - removeBranch, - removeNode, - updateNode, -} from "@/domain/workflow"; +import { WORKFLOW_NODE_TYPES, type WorkflowModel, type WorkflowNodeConfigForStart } from "@/domain/workflow"; import { get as getWorkflow, save as saveWorkflow, subscribe as subscribeWorkflow } from "@/repository/workflow"; -import { type WorkflowState, type WorkflowStore } from "./types"; +import { type WorkflowStore } from "./types"; export const useWorkflowStore = create((set, get) => { const ensureInitialized = () => { @@ -54,19 +44,34 @@ export const useWorkflowStore = create((set, get) => { }); }, - setBaseInfo: async (name, description) => { + setName: async (name) => { ensureInitialized(); const resp = await saveWorkflow({ id: get().workflow.id!, name: name || "", - description: description || "", }); - set((state: WorkflowState) => { + set((state) => { return { workflow: produce(state.workflow, (draft) => { draft.name = resp.name; + }), + }; + }); + }, + + setDescription: async (description) => { + ensureInitialized(); + + const resp = await saveWorkflow({ + id: get().workflow.id!, + description: description || "", + }); + + set((state) => { + return { + workflow: produce(state.workflow, (draft) => { draft.description = resp.description; }), }; @@ -81,7 +86,7 @@ export const useWorkflowStore = create((set, get) => { enabled: enabled, }); - set((state: WorkflowState) => { + set((state) => { return { workflow: produce(state.workflow, (draft) => { draft.enabled = resp.enabled; @@ -90,26 +95,47 @@ export const useWorkflowStore = create((set, get) => { }); }, + orchestrate: async (graph) => { + ensureInitialized(); + + const resp = await saveWorkflow({ + id: get().workflow.id!, + graphDraft: graph, + hasDraft: !isEqual(graph, get().workflow.graphContent), + }); + + set((state) => { + return { + workflow: produce(state.workflow, (draft) => { + draft.graphDraft = resp.graphDraft; + draft.hasDraft = resp.hasDraft; + }), + }; + }); + }, + publish: async () => { ensureInitialized(); - const root = get().workflow.draft!; - const startConfig = root.config as WorkflowNodeConfigForStart; + const graph = get().workflow.graphDraft!; + if (graph?.nodes?.[0]?.type !== WORKFLOW_NODE_TYPES.START) throw "Workflow nodes tree of draft in invalid"; + const startConfig = graph.nodes[0].data.config as WorkflowNodeConfigForStart; const resp = await saveWorkflow({ id: get().workflow.id!, trigger: startConfig.trigger, triggerCron: startConfig.triggerCron, - content: root, + graphContent: graph, + hasContent: true, hasDraft: false, }); - set((state: WorkflowState) => { + set((state) => { return { workflow: produce(state.workflow, (draft) => { draft.trigger = resp.trigger; draft.triggerCron = resp.triggerCron; - draft.content = resp.content; - draft.draft = resp.draft; + draft.graphContent = resp.graphContent; + draft.hasContent = resp.hasContent; draft.hasDraft = resp.hasDraft; }), }; @@ -119,171 +145,29 @@ export const useWorkflowStore = create((set, get) => { rollback: async () => { ensureInitialized(); - const root = get().workflow.content!; - const startConfig = root.config as WorkflowNodeConfigForStart; + const graph = get().workflow.graphContent!; + if (graph?.nodes?.[0]?.type !== WORKFLOW_NODE_TYPES.START) throw "Workflow nodes tree of content in invalid"; + const startConfig = graph.nodes[0].data.config as WorkflowNodeConfigForStart; const resp = await saveWorkflow({ id: get().workflow.id!, - draft: root, - hasDraft: false, trigger: startConfig.trigger, triggerCron: startConfig.triggerCron, + hasContent: true, + graphDraft: graph, + hasDraft: false, }); - set((state: WorkflowState) => { + set((state) => { return { workflow: produce(state.workflow, (draft) => { draft.trigger = resp.trigger; draft.triggerCron = resp.triggerCron; - draft.content = resp.content; - draft.draft = resp.draft; + draft.hasContent = resp.hasContent; + draft.graphDraft = resp.graphDraft; draft.hasDraft = resp.hasDraft; }), }; }); }, - - addNode: async (node, previousNodeId) => { - ensureInitialized(); - - const root = addNode(get().workflow.draft!, node, previousNodeId); - const resp = await saveWorkflow({ - id: get().workflow.id!, - draft: root, - hasDraft: true, - }); - - set((state: WorkflowState) => { - return { - workflow: produce(state.workflow, (draft) => { - draft.draft = resp.draft; - draft.hasDraft = resp.hasDraft; - }), - }; - }); - }, - - duplicateNode: async (node) => { - ensureInitialized(); - - const root = duplicateNode(get().workflow.draft!, node); - const resp = await saveWorkflow({ - id: get().workflow.id!, - draft: root, - hasDraft: true, - }); - - set((state: WorkflowState) => { - return { - workflow: produce(state.workflow, (draft) => { - draft.draft = resp.draft; - draft.hasDraft = resp.hasDraft; - }), - }; - }); - }, - - updateNode: async (node) => { - ensureInitialized(); - - const root = updateNode(get().workflow.draft!, node); - const resp = await saveWorkflow({ - id: get().workflow.id!, - draft: root, - hasDraft: true, - }); - - set((state: WorkflowState) => { - return { - workflow: produce(state.workflow, (draft) => { - draft.draft = resp.draft; - draft.hasDraft = resp.hasDraft; - }), - }; - }); - }, - - removeNode: async (node) => { - ensureInitialized(); - - const root = removeNode(get().workflow.draft!, node.id); - const resp = await saveWorkflow({ - id: get().workflow.id!, - draft: root, - hasDraft: true, - }); - - set((state: WorkflowState) => { - return { - workflow: produce(state.workflow, (draft) => { - draft.draft = resp.draft; - draft.hasDraft = resp.hasDraft; - }), - }; - }); - }, - - addBranch: async (branchId) => { - ensureInitialized(); - - const root = addBranch(get().workflow.draft!, branchId); - const resp = await saveWorkflow({ - id: get().workflow.id!, - draft: root, - hasDraft: true, - }); - - set((state: WorkflowState) => { - return { - workflow: produce(state.workflow, (draft) => { - draft.draft = resp.draft; - draft.hasDraft = resp.hasDraft; - }), - }; - }); - }, - - duplicateBranch: async (branchId, index) => { - ensureInitialized(); - - const root = duplicateBranch(get().workflow.draft!, branchId, index); - const resp = await saveWorkflow({ - id: get().workflow.id!, - draft: root, - hasDraft: true, - }); - - set((state: WorkflowState) => { - return { - workflow: produce(state.workflow, (draft) => { - draft.draft = resp.draft; - draft.hasDraft = resp.hasDraft; - }), - }; - }); - }, - - removeBranch: async (branchId, index) => { - ensureInitialized(); - - const root = removeBranch(get().workflow.draft!, branchId, index); - const resp = await saveWorkflow({ - id: get().workflow.id!, - draft: root, - hasDraft: true, - }); - - set((state: WorkflowState) => { - return { - workflow: produce(state.workflow, (draft) => { - draft.draft = resp.draft; - draft.hasDraft = resp.hasDraft; - }), - }; - }); - }, - - getWorkflowOuptutBeforeId: (nodeId, typeFilter) => { - return getOutputBeforeNodeId(get().workflow.draft!, nodeId, typeFilter); - }, }; }); diff --git a/ui/src/stores/workflow/types.ts b/ui/src/stores/workflow/types.ts index da85e6f1b..dc280282a 100644 --- a/ui/src/stores/workflow/types.ts +++ b/ui/src/stores/workflow/types.ts @@ -1,4 +1,4 @@ -import { type WorkflowModel, type WorkflowNode } from "@/domain/workflow"; +import { type WorkflowGraph, type WorkflowModel } from "@/domain/workflow"; export interface WorkflowState { workflow: WorkflowModel; @@ -9,20 +9,13 @@ export interface WorkflowActions { init(id: string): void; destroy(): void; - setBaseInfo: (name: string, description: string) => void; - setEnabled(enabled: boolean): void; + setName: (name: Required["name"]) => void; + setDescription: (description: Required["description"]) => void; + setEnabled(enabled: Required["enabled"]): void; + + orchestrate(graph: WorkflowGraph): void; publish(): void; rollback(): void; - - addNode: (node: WorkflowNode, previousNodeId: string) => void; - duplicateNode: (node: WorkflowNode) => void; - updateNode: (node: WorkflowNode) => void; - removeNode: (node: WorkflowNode) => void; - addBranch: (branchId: string) => void; - duplicateBranch: (branchId: string, index: number) => void; - removeBranch: (branchId: string, index: number) => void; - - getWorkflowOuptutBeforeId: (nodeId: string, typeFilter?: string | string[]) => WorkflowNode[]; } export interface WorkflowStore extends WorkflowState, WorkflowActions {} diff --git a/ui/src/utils/browser.ts b/ui/src/utils/browser.ts index 735bb26ae..911e55277 100644 --- a/ui/src/utils/browser.ts +++ b/ui/src/utils/browser.ts @@ -1,3 +1,11 @@ export const isBrowserHappy = () => { - return typeof Promise.withResolvers === "function"; + try { + if (typeof Promise.withResolvers !== "function") return false; + if (typeof CSS.supports !== "function") return false; + if (!CSS.supports("color", "oklch(0 0 0)")) return false; + } catch (_) { + return false; + } + + return true; };