diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index f1200094..d361cf83 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -53,35 +53,35 @@ func NewWithWorkflowNode(config ApplicantWithWorkflowNodeConfig) (Applicant, err return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeApply)) } - nodeConfig := config.Node.GetConfigForApply() + nodeCfg := config.Node.GetConfigForApply() options := &applicantProviderOptions{ - Domains: sliceutil.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }), - ContactEmail: nodeConfig.ContactEmail, - Provider: domain.ACMEDns01ProviderType(nodeConfig.Provider), + Domains: sliceutil.Filter(strings.Split(nodeCfg.Domains, ";"), func(s string) bool { return s != "" }), + ContactEmail: nodeCfg.ContactEmail, + Provider: domain.ACMEDns01ProviderType(nodeCfg.Provider), ProviderAccessConfig: make(map[string]any), - ProviderServiceConfig: nodeConfig.ProviderConfig, - CAProvider: domain.CAProviderType(nodeConfig.CAProvider), + ProviderServiceConfig: nodeCfg.ProviderConfig, + CAProvider: domain.CAProviderType(nodeCfg.CAProvider), CAProviderAccessConfig: make(map[string]any), - CAProviderServiceConfig: nodeConfig.CAProviderConfig, - KeyAlgorithm: nodeConfig.KeyAlgorithm, - Nameservers: sliceutil.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }), - DnsPropagationWait: nodeConfig.DnsPropagationWait, - DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout, - DnsTTL: nodeConfig.DnsTTL, - DisableFollowCNAME: nodeConfig.DisableFollowCNAME, + CAProviderServiceConfig: nodeCfg.CAProviderConfig, + KeyAlgorithm: nodeCfg.KeyAlgorithm, + Nameservers: sliceutil.Filter(strings.Split(nodeCfg.Nameservers, ";"), func(s string) bool { return s != "" }), + DnsPropagationWait: nodeCfg.DnsPropagationWait, + DnsPropagationTimeout: nodeCfg.DnsPropagationTimeout, + DnsTTL: nodeCfg.DnsTTL, + DisableFollowCNAME: nodeCfg.DisableFollowCNAME, } accessRepo := repository.NewAccessRepository() - if nodeConfig.ProviderAccessId != "" { - if access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId); err != nil { - return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) + if nodeCfg.ProviderAccessId != "" { + if access, err := accessRepo.GetById(context.Background(), nodeCfg.ProviderAccessId); err != nil { + return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err) } else { options.ProviderAccessConfig = access.Config } } - if nodeConfig.CAProviderAccessId != "" { - if access, err := accessRepo.GetById(context.Background(), nodeConfig.CAProviderAccessId); err != nil { - return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.CAProviderAccessId, err) + if nodeCfg.CAProviderAccessId != "" { + if access, err := accessRepo.GetById(context.Background(), nodeCfg.CAProviderAccessId); err != nil { + return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.CAProviderAccessId, err) } else { options.CAProviderAccessId = access.Id options.CAProviderAccessConfig = access.Config diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index e4a28746..c73120ba 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -29,18 +29,18 @@ func NewWithWorkflowNode(config DeployerWithWorkflowNodeConfig) (Deployer, error return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeDeploy)) } - nodeConfig := config.Node.GetConfigForDeploy() + nodeCfg := config.Node.GetConfigForDeploy() options := &deployerProviderOptions{ - Provider: domain.DeploymentProviderType(nodeConfig.Provider), + Provider: domain.DeploymentProviderType(nodeCfg.Provider), ProviderAccessConfig: make(map[string]any), - ProviderServiceConfig: nodeConfig.ProviderConfig, + ProviderServiceConfig: nodeCfg.ProviderConfig, } accessRepo := repository.NewAccessRepository() - if nodeConfig.ProviderAccessId != "" { - access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId) + if nodeCfg.ProviderAccessId != "" { + access, err := accessRepo.GetById(context.Background(), nodeCfg.ProviderAccessId) if err != nil { - return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) + return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err) } else { options.ProviderAccessConfig = access.Config } diff --git a/internal/domain/expr.go b/internal/domain/expr/expr.go similarity index 69% rename from internal/domain/expr.go rename to internal/domain/expr/expr.go index 01730e3d..755a876c 100644 --- a/internal/domain/expr.go +++ b/internal/domain/expr/expr.go @@ -1,4 +1,4 @@ -package domain +package expr import ( "encoding/json" @@ -6,41 +6,38 @@ import ( "strconv" ) -type Value any - type ( - ComparisonOperator string - LogicalOperator string - ValueType string - ExprType string + ExprType string + ExprComparisonOperator string + ExprLogicalOperator string + ExprValueType string ) const ( - GreaterThan ComparisonOperator = ">" - LessThan ComparisonOperator = "<" - GreaterOrEqual ComparisonOperator = ">=" - LessOrEqual ComparisonOperator = "<=" - Equal ComparisonOperator = "==" - NotEqual ComparisonOperator = "!=" - Is ComparisonOperator = "is" + GreaterThan ExprComparisonOperator = "gt" + GreaterOrEqual ExprComparisonOperator = "gte" + LessThan ExprComparisonOperator = "lt" + LessOrEqual ExprComparisonOperator = "lte" + Equal ExprComparisonOperator = "eq" + NotEqual ExprComparisonOperator = "neq" - And LogicalOperator = "and" - Or LogicalOperator = "or" - Not LogicalOperator = "not" + And ExprLogicalOperator = "and" + Or ExprLogicalOperator = "or" + Not ExprLogicalOperator = "not" - Number ValueType = "number" - String ValueType = "string" - Boolean ValueType = "boolean" + Number ExprValueType = "number" + String ExprValueType = "string" + Boolean ExprValueType = "boolean" - ConstExprType ExprType = "const" - VarExprType ExprType = "var" - CompareExprType ExprType = "compare" - LogicalExprType ExprType = "logical" - NotExprType ExprType = "not" + ConstantExprType ExprType = "const" + VariantExprType ExprType = "var" + ComparisonExprType ExprType = "comparison" + LogicalExprType ExprType = "logical" + NotExprType ExprType = "not" ) type EvalResult struct { - Type ValueType + Type ExprValueType Value any } @@ -88,13 +85,20 @@ func (e *EvalResult) GreaterThan(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } - switch e.Type { - case Number: + switch e.Type { + case String: + return &EvalResult{ + Type: Boolean, + Value: e.Value.(string) > other.Value.(string), + }, nil + + case Number: left, err := e.GetFloat64() if err != nil { return nil, err } + right, err := other.GetFloat64() if err != nil { return nil, err @@ -104,14 +108,9 @@ func (e *EvalResult) GreaterThan(other *EvalResult) (*EvalResult, error) { Type: Boolean, Value: left > right, }, nil - case String: - return &EvalResult{ - Type: Boolean, - Value: e.Value.(string) > other.Value.(string), - }, nil default: - return nil, fmt.Errorf("unsupported type: %s", e.Type) + return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } @@ -119,28 +118,32 @@ func (e *EvalResult) GreaterOrEqual(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } + switch e.Type { - case Number: - left, err := e.GetFloat64() - if err != nil { - return nil, err - } - right, err := other.GetFloat64() - if err != nil { - return nil, err - } - return &EvalResult{ - Type: Boolean, - Value: left >= right, - }, nil case String: return &EvalResult{ Type: Boolean, Value: e.Value.(string) >= other.Value.(string), }, nil + case Number: + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left >= right, + }, nil + default: - return nil, fmt.Errorf("unsupported type: %s", e.Type) + return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } @@ -148,28 +151,32 @@ func (e *EvalResult) LessThan(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } + switch e.Type { - case Number: - left, err := e.GetFloat64() - if err != nil { - return nil, err - } - right, err := other.GetFloat64() - if err != nil { - return nil, err - } - return &EvalResult{ - Type: Boolean, - Value: left < right, - }, nil case String: return &EvalResult{ Type: Boolean, Value: e.Value.(string) < other.Value.(string), }, nil + case Number: + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left < right, + }, nil + default: - return nil, fmt.Errorf("unsupported type: %s", e.Type) + return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } @@ -177,28 +184,32 @@ func (e *EvalResult) LessOrEqual(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } + switch e.Type { - case Number: - left, err := e.GetFloat64() - if err != nil { - return nil, err - } - right, err := other.GetFloat64() - if err != nil { - return nil, err - } - return &EvalResult{ - Type: Boolean, - Value: left <= right, - }, nil case String: return &EvalResult{ Type: Boolean, Value: e.Value.(string) <= other.Value.(string), }, nil + case Number: + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left <= right, + }, nil + default: - return nil, fmt.Errorf("unsupported type: %s", e.Type) + return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } @@ -206,28 +217,48 @@ func (e *EvalResult) Equal(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } + switch e.Type { - case Number: - left, err := e.GetFloat64() - if err != nil { - return nil, err - } - right, err := other.GetFloat64() - if err != nil { - return nil, err - } - return &EvalResult{ - Type: Boolean, - Value: left == right, - }, nil case String: return &EvalResult{ Type: Boolean, Value: e.Value.(string) == other.Value.(string), }, nil + case Number: + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left == right, + }, nil + + case Boolean: + left, err := e.GetBool() + if err != nil { + return nil, err + } + + right, err := other.GetBool() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left == right, + }, nil + default: - return nil, fmt.Errorf("unsupported type: %s", e.Type) + return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } @@ -235,28 +266,48 @@ func (e *EvalResult) NotEqual(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } + switch e.Type { - case Number: - left, err := e.GetFloat64() - if err != nil { - return nil, err - } - right, err := other.GetFloat64() - if err != nil { - return nil, err - } - return &EvalResult{ - Type: Boolean, - Value: left != right, - }, nil case String: return &EvalResult{ Type: Boolean, Value: e.Value.(string) != other.Value.(string), }, nil + case Number: + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left != right, + }, nil + + case Boolean: + left, err := e.GetBool() + if err != nil { + return nil, err + } + + right, err := other.GetBool() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left != right, + }, nil + default: - return nil, fmt.Errorf("unsupported type: %s", e.Type) + return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } @@ -264,22 +315,26 @@ func (e *EvalResult) And(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } + switch e.Type { case Boolean: left, err := e.GetBool() if err != nil { return nil, err } + right, err := other.GetBool() if err != nil { return nil, err } + return &EvalResult{ Type: Boolean, Value: left && right, }, nil + default: - return nil, fmt.Errorf("unsupported type: %s", e.Type) + return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } @@ -287,22 +342,25 @@ func (e *EvalResult) Or(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } + switch e.Type { case Boolean: left, err := e.GetBool() if err != nil { return nil, err } + right, err := other.GetBool() if err != nil { return nil, err } + return &EvalResult{ Type: Boolean, Value: left || right, }, nil default: - return nil, fmt.Errorf("unsupported type: %s", e.Type) + return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } @@ -310,67 +368,52 @@ func (e *EvalResult) Not() (*EvalResult, error) { if e.Type != Boolean { return nil, fmt.Errorf("type mismatch: %s", e.Type) } + boolValue, err := e.GetBool() if err != nil { return nil, err } + return &EvalResult{ Type: Boolean, Value: !boolValue, }, nil } -func (e *EvalResult) Is(other *EvalResult) (*EvalResult, error) { - if e.Type != other.Type { - return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) - } - switch e.Type { - case Boolean: - left, err := e.GetBool() - if err != nil { - return nil, err - } - right, err := other.GetBool() - if err != nil { - return nil, err - } - return &EvalResult{ - Type: Boolean, - Value: left == right, - }, nil - default: - return nil, fmt.Errorf("unsupported type: %s", e.Type) - } -} - type Expr interface { GetType() ExprType Eval(variables map[string]map[string]any) (*EvalResult, error) } -type ConstExpr struct { - Type ExprType `json:"type"` - Value Value `json:"value"` - ValueType ValueType `json:"valueType"` +type ExprValueSelector struct { + Id string `json:"id"` + Name string `json:"name"` + Type ExprValueType `json:"type"` } -func (c ConstExpr) GetType() ExprType { return c.Type } +type ConstantExpr struct { + Type ExprType `json:"type"` + Value string `json:"value"` + ValueType ExprValueType `json:"valueType"` +} -func (c ConstExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { +func (c ConstantExpr) GetType() ExprType { return c.Type } + +func (c ConstantExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { return &EvalResult{ Type: c.ValueType, Value: c.Value, }, nil } -type VarExpr struct { - Type ExprType `json:"type"` - Selector WorkflowNodeIOValueSelector `json:"selector"` +type VariantExpr struct { + Type ExprType `json:"type"` + Selector ExprValueSelector `json:"selector"` } -func (v VarExpr) GetType() ExprType { return v.Type } +func (v VariantExpr) GetType() ExprType { return v.Type } -func (v VarExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { +func (v VariantExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { if v.Selector.Id == "" { return nil, fmt.Errorf("node id is empty") } @@ -391,16 +434,16 @@ func (v VarExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) }, nil } -type CompareExpr struct { - Type ExprType `json:"type"` // compare - Op ComparisonOperator `json:"op"` - Left Expr `json:"left"` - Right Expr `json:"right"` +type ComparisonExpr struct { + Type ExprType `json:"type"` // compare + Operator ExprComparisonOperator `json:"operator"` + Left Expr `json:"left"` + Right Expr `json:"right"` } -func (c CompareExpr) GetType() ExprType { return c.Type } +func (c ComparisonExpr) GetType() ExprType { return c.Type } -func (c CompareExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { +func (c ComparisonExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { left, err := c.Left.Eval(variables) if err != nil { return nil, err @@ -410,7 +453,7 @@ func (c CompareExpr) Eval(variables map[string]map[string]any) (*EvalResult, err return nil, err } - switch c.Op { + switch c.Operator { case GreaterThan: return left.GreaterThan(right) case LessThan: @@ -423,18 +466,16 @@ func (c CompareExpr) Eval(variables map[string]map[string]any) (*EvalResult, err return left.Equal(right) case NotEqual: return left.NotEqual(right) - case Is: - return left.Is(right) default: - return nil, fmt.Errorf("unknown operator: %s", c.Op) + return nil, fmt.Errorf("unknown expression operator: %s", c.Operator) } } type LogicalExpr struct { - Type ExprType `json:"type"` // logical - Op LogicalOperator `json:"op"` - Left Expr `json:"left"` - Right Expr `json:"right"` + Type ExprType `json:"type"` // logical + Operator ExprLogicalOperator `json:"operator"` + Left Expr `json:"left"` + Right Expr `json:"right"` } func (l LogicalExpr) GetType() ExprType { return l.Type } @@ -449,13 +490,13 @@ func (l LogicalExpr) Eval(variables map[string]map[string]any) (*EvalResult, err return nil, err } - switch l.Op { + switch l.Operator { case And: return left.And(right) case Or: return left.Or(right) default: - return nil, fmt.Errorf("unknown operator: %s", l.Op) + return nil, fmt.Errorf("unknown expression operator: %s", l.Operator) } } @@ -489,24 +530,24 @@ func UnmarshalExpr(data []byte) (Expr, error) { } switch typ.Type { - case ConstExprType: - var e ConstExpr + case ConstantExprType: + var e ConstantExpr if err := json.Unmarshal(data, &e); err != nil { return nil, err } return e, nil - case VarExprType: - var e VarExpr + case VariantExprType: + var e VariantExpr if err := json.Unmarshal(data, &e); err != nil { return nil, err } return e, nil - case CompareExprType: - var e CompareExprRaw + case ComparisonExprType: + var e ComparisonExprRaw if err := json.Unmarshal(data, &e); err != nil { return nil, err } - return e.ToCompareExpr() + return e.ToComparisonExpr() case LogicalExprType: var e LogicalExprRaw if err := json.Unmarshal(data, &e); err != nil { @@ -520,39 +561,39 @@ func UnmarshalExpr(data []byte) (Expr, error) { } return e.ToNotExpr() default: - return nil, fmt.Errorf("unknown expr type: %s", typ.Type) + return nil, fmt.Errorf("unknown expression type: %s", typ.Type) } } -type CompareExprRaw struct { - Type ExprType `json:"type"` - Op ComparisonOperator `json:"op"` - Left json.RawMessage `json:"left"` - Right json.RawMessage `json:"right"` +type ComparisonExprRaw struct { + Type ExprType `json:"type"` + Operator ExprComparisonOperator `json:"operator"` + Left json.RawMessage `json:"left"` + Right json.RawMessage `json:"right"` } -func (r CompareExprRaw) ToCompareExpr() (CompareExpr, error) { +func (r ComparisonExprRaw) ToComparisonExpr() (ComparisonExpr, error) { leftExpr, err := UnmarshalExpr(r.Left) if err != nil { - return CompareExpr{}, err + return ComparisonExpr{}, err } rightExpr, err := UnmarshalExpr(r.Right) if err != nil { - return CompareExpr{}, err + return ComparisonExpr{}, err } - return CompareExpr{ - Type: r.Type, - Op: r.Op, - Left: leftExpr, - Right: rightExpr, + return ComparisonExpr{ + Type: r.Type, + Operator: r.Operator, + Left: leftExpr, + Right: rightExpr, }, nil } type LogicalExprRaw struct { - Type ExprType `json:"type"` - Op LogicalOperator `json:"op"` - Left json.RawMessage `json:"left"` - Right json.RawMessage `json:"right"` + Type ExprType `json:"type"` + Operator ExprLogicalOperator `json:"operator"` + Left json.RawMessage `json:"left"` + Right json.RawMessage `json:"right"` } func (r LogicalExprRaw) ToLogicalExpr() (LogicalExpr, error) { @@ -565,10 +606,10 @@ func (r LogicalExprRaw) ToLogicalExpr() (LogicalExpr, error) { return LogicalExpr{}, err } return LogicalExpr{ - Type: r.Type, - Op: r.Op, - Left: left, - Right: right, + Type: r.Type, + Operator: r.Operator, + Left: left, + Right: right, }, nil } diff --git a/internal/domain/expr_test.go b/internal/domain/expr/expr_test.go similarity index 66% rename from internal/domain/expr_test.go rename to internal/domain/expr/expr_test.go index f0a34504..fb76d98c 100644 --- a/internal/domain/expr_test.go +++ b/internal/domain/expr/expr_test.go @@ -1,4 +1,4 @@ -package domain +package expr import ( "testing" @@ -7,15 +7,15 @@ import ( func TestLogicalEval(t *testing.T) { // 测试逻辑表达式 and logicalExpr := LogicalExpr{ - Left: ConstExpr{ + Left: ConstantExpr{ Type: "const", - Value: true, + Value: "true", ValueType: "boolean", }, - Op: And, - Right: ConstExpr{ + Operator: And, + Right: ConstantExpr{ Type: "const", - Value: true, + Value: "true", ValueType: "boolean", }, } @@ -29,15 +29,15 @@ func TestLogicalEval(t *testing.T) { // 测试逻辑表达式 or orExpr := LogicalExpr{ - Left: ConstExpr{ + Left: ConstantExpr{ Type: "const", - Value: true, + Value: "true", ValueType: "boolean", }, - Op: Or, - Right: ConstExpr{ + Operator: Or, + Right: ConstantExpr{ Type: "const", - Value: true, + Value: "true", ValueType: "boolean", }, } @@ -63,7 +63,7 @@ func TestUnmarshalExpr(t *testing.T) { { name: "test1", args: args{ - data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validated","type":"boolean"},"type":"var"},"op":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"compare"},"op":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"op":"==","right":{"type":"const","value":2,"valueType":"number"},"type":"compare"},"type":"logical"}`), + data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validity","type":"boolean"},"type":"var"},"operator":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"comparison"},"operator":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"operator":"eq","right":{"type":"const","value":2,"valueType":"number"},"type":"comparison"},"type":"logical"}`), }, }, } @@ -98,11 +98,11 @@ func TestExpr_Eval(t *testing.T) { args: args{ variables: map[string]map[string]any{ "ODnYSOXB6HQP2_vz6JcZE": { - "certificate.validated": true, - "certificate.daysLeft": 2, + "certificate.validity": true, + "certificate.daysLeft": 2, }, }, - data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validated","type":"boolean"},"type":"var"},"op":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"compare"},"op":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"op":"==","right":{"type":"const","value":2,"valueType":"number"},"type":"compare"},"type":"logical"}`), + data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validity","type":"boolean"},"type":"var"},"operator":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"comparison"},"operator":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"operator":"eq","right":{"type":"const","value":2,"valueType":"number"},"type":"comparison"},"type":"logical"}`), }, }, } diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 7d7355c5..02f8b671 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -4,6 +4,7 @@ import ( "encoding/json" "time" + "github.com/usual2970/certimate/internal/domain/expr" maputil "github.com/usual2970/certimate/internal/pkg/utils/map" ) @@ -114,7 +115,7 @@ type WorkflowNodeConfigForNotify struct { } type WorkflowNodeConfigForCondition struct { - Expression Expr `json:"expression"` // 条件表达式 + Expression expr.Expr `json:"expression"` // 条件表达式 } func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply { @@ -183,9 +184,8 @@ func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition { return WorkflowNodeConfigForCondition{} } - raw, _ := json.Marshal(expression) - - expr, err := UnmarshalExpr([]byte(raw)) + exprRaw, _ := json.Marshal(expression) + expr, err := expr.UnmarshalExpr([]byte(exprRaw)) if err != nil { return WorkflowNodeConfigForCondition{} } @@ -204,10 +204,6 @@ type WorkflowNodeIO struct { ValueSelector WorkflowNodeIOValueSelector `json:"valueSelector"` } -type WorkflowNodeIOValueSelector struct { - Id string `json:"id"` - Name string `json:"name"` - Type ValueType `json:"type"` -} +type WorkflowNodeIOValueSelector = expr.ExprValueSelector const WorkflowNodeIONameCertificate string = "certificate" diff --git a/internal/notify/notifier.go b/internal/notify/notifier.go index ee3fbd2f..5e957841 100644 --- a/internal/notify/notifier.go +++ b/internal/notify/notifier.go @@ -29,18 +29,18 @@ func NewWithWorkflowNode(config NotifierWithWorkflowNodeConfig) (Notifier, error return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeNotify)) } - nodeConfig := config.Node.GetConfigForNotify() + nodeCfg := config.Node.GetConfigForNotify() options := ¬ifierProviderOptions{ - Provider: domain.NotificationProviderType(nodeConfig.Provider), + Provider: domain.NotificationProviderType(nodeCfg.Provider), ProviderAccessConfig: make(map[string]any), - ProviderServiceConfig: nodeConfig.ProviderConfig, + ProviderServiceConfig: nodeCfg.ProviderConfig, } accessRepo := repository.NewAccessRepository() - if nodeConfig.ProviderAccessId != "" { - access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId) + if nodeCfg.ProviderAccessId != "" { + access, err := accessRepo.GetById(context.Background(), nodeCfg.ProviderAccessId) if err != nil { - return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) + return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err) } else { options.ProviderAccessConfig = access.Config } diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns/powerdns.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns/powerdns.go index b34516d4..7c87536c 100644 --- a/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns/powerdns.go +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns/powerdns.go @@ -29,6 +29,7 @@ func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, providerConfig.APIKey = config.ApiKey if config.AllowInsecureConnections { providerConfig.HTTPClient.Transport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, diff --git a/internal/pkg/core/deployer/providers/proxmoxve/proxmoxve.go b/internal/pkg/core/deployer/providers/proxmoxve/proxmoxve.go index 349c3a16..0295c7e2 100644 --- a/internal/pkg/core/deployer/providers/proxmoxve/proxmoxve.go +++ b/internal/pkg/core/deployer/providers/proxmoxve/proxmoxve.go @@ -13,6 +13,7 @@ import ( "github.com/luthermonson/go-proxmox" "github.com/usual2970/certimate/internal/pkg/core/deployer" + httputil "github.com/usual2970/certimate/internal/pkg/utils/http" ) type DeployerConfig struct { @@ -101,15 +102,16 @@ func createSdkClient(serverUrl, apiToken, apiTokenSecret string, skipTlsVerify b } httpClient := &http.Client{ - Transport: http.DefaultTransport, + Transport: httputil.NewDefaultTransport(), Timeout: http.DefaultClient.Timeout, } if skipTlsVerify { - httpClient.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, + transport := httputil.NewDefaultTransport() + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{} } + transport.TLSClientConfig.InsecureSkipVerify = true + httpClient.Transport = transport } client := proxmox.NewClient( strings.TrimRight(serverUrl, "/")+"/api2/json", diff --git a/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go b/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go index b512be09..38ddbd46 100644 --- a/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go +++ b/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go @@ -65,7 +65,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE } // 查询证书列表,避免重复上传 - // REF: https://www.wangsu.com/document/api-doc/26426 + // REF: https://www.wangsu.com/document/api-doc/22675?productCode=certificatemanagement listCertificatesResp, err := u.sdkClient.ListCertificates() u.logger.Debug("sdk request 'certificatemanagement.ListCertificates'", slog.Any("response", listCertificatesResp)) if err != nil { diff --git a/internal/pkg/utils/http/transport.go b/internal/pkg/utils/http/transport.go new file mode 100644 index 00000000..ff8c8804 --- /dev/null +++ b/internal/pkg/utils/http/transport.go @@ -0,0 +1,33 @@ +package httputil + +import ( + "net" + "net/http" + "time" +) + +// 创建并返回一个 [http.DefaultTransport] 对象副本。 +// +// 出参: +// - transport: [http.DefaultTransport] 对象副本。 +func NewDefaultTransport() *http.Transport { + if http.DefaultTransport != nil { + if t, ok := http.DefaultTransport.(*http.Transport); ok { + return t.Clone() + } + } + + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } +} diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 321d9fc8..8616fbd9 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -3,6 +3,7 @@ package nodeprocessor import ( "context" "fmt" + "strconv" "time" "golang.org/x/exp/maps" @@ -108,15 +109,15 @@ func (n *applyNode) Process(ctx context.Context) error { } } - // 添加中间结果 - n.outputs[outputCertificateValidatedKey] = "true" - n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(certificate.ExpireAt).Hours()/24)) + // 记录中间结果 + n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true) + n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(time.Until(certificate.ExpireAt).Hours()/24), 10) n.logger.Info("application completed") return nil } -func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (skip bool, reason string) { +func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) { if lastOutput != nil && lastOutput.Succeeded { // 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致 currentNodeConfig := n.node.GetConfigForApply() @@ -154,9 +155,12 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24 expirationTime := time.Until(lastCertificate.ExpireAt) if expirationTime > renewalInterval { - n.outputs[outputCertificateValidatedKey] = "true" - n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(expirationTime.Hours()/24)) - return true, fmt.Sprintf("the certificate has already been issued (expires in %d day(s), next renewal in %d day(s))", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) + 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, currentNodeConfig.SkipBeforeExpiryDays) } } } diff --git a/internal/workflow/node-processor/condition_node.go b/internal/workflow/node-processor/condition_node.go index d90811d9..d9e8126d 100644 --- a/internal/workflow/node-processor/condition_node.go +++ b/internal/workflow/node-processor/condition_node.go @@ -3,8 +3,10 @@ package nodeprocessor import ( "context" "errors" + "fmt" "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/domain/expr" ) type conditionNode struct { @@ -22,30 +24,29 @@ func NewConditionNode(node *domain.WorkflowNode) *conditionNode { } func (n *conditionNode) Process(ctx context.Context) error { - n.logger.Info("enter condition node: " + n.node.Name) - - nodeConfig := n.node.GetConfigForCondition() - if nodeConfig.Expression == nil { - n.logger.Info("no condition found, continue to next node") + nodeCfg := n.node.GetConfigForCondition() + if nodeCfg.Expression == nil { + n.logger.Info("without any conditions, enter this branch") return nil } - rs, err := n.eval(ctx, nodeConfig.Expression) + rs, err := n.evalExpr(ctx, nodeCfg.Expression) if err != nil { - n.logger.Warn("failed to eval expression: " + err.Error()) + 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") + return errors.New("condition not met") // TODO: 错误处理 + } else { + n.logger.Info("condition met, enter this branch") } - n.logger.Info("condition met, continue to next node") return nil } -func (n *conditionNode) eval(ctx context.Context, expression domain.Expr) (*domain.EvalResult, error) { +func (n *conditionNode) evalExpr(ctx context.Context, expression expr.Expr) (*expr.EvalResult, error) { variables := GetNodeOutputs(ctx) return expression.Eval(variables) } diff --git a/internal/workflow/node-processor/const.go b/internal/workflow/node-processor/const.go index c1af01c9..62d2d56b 100644 --- a/internal/workflow/node-processor/const.go +++ b/internal/workflow/node-processor/const.go @@ -1,6 +1,6 @@ package nodeprocessor const ( - outputCertificateValidatedKey = "certificate.validated" - outputCertificateDaysLeftKey = "certificate.daysLeft" + outputKeyForCertificateValidity = "certificate.validity" + outputKeyForCertificateDaysLeft = "certificate.daysLeft" ) diff --git a/internal/workflow/node-processor/context.go b/internal/workflow/node-processor/context.go index adceacf6..96c40487 100644 --- a/internal/workflow/node-processor/context.go +++ b/internal/workflow/node-processor/context.go @@ -35,7 +35,8 @@ func AddNodeOutput(ctx context.Context, nodeId string, output map[string]any) co container.Lock() defer container.Unlock() - // 创建输出的深拷贝以避免后续修改 + // 创建输出的深拷贝 + // TODO: 暂时使用浅拷贝,等后续值类型扩充后修改 outputCopy := make(map[string]any, len(output)) for k, v := range output { outputCopy[k] = v @@ -90,6 +91,7 @@ func GetNodeOutputs(ctx context.Context) map[string]map[string]any { 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)) diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index f0ded21d..f89f4a1f 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -42,8 +42,9 @@ func (n *deployNode) Process(ctx context.Context) error { } // 获取前序节点输出证书 + const DELIMITER = "#" previousNodeOutputCertificateSource := n.node.GetConfigForDeploy().Certificate - previousNodeOutputCertificateSourceSlice := strings.Split(previousNodeOutputCertificateSource, "#") + 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) @@ -99,7 +100,7 @@ func (n *deployNode) Process(ctx context.Context) error { return nil } -func (n *deployNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (skip bool, reason string) { +func (n *deployNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) { if lastOutput != nil && lastOutput.Succeeded { // 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致 currentNodeConfig := n.node.GetConfigForDeploy() diff --git a/internal/workflow/node-processor/monitor_node.go b/internal/workflow/node-processor/monitor_node.go index f8c1adae..4b875f26 100644 --- a/internal/workflow/node-processor/monitor_node.go +++ b/internal/workflow/node-processor/monitor_node.go @@ -6,13 +6,13 @@ import ( "crypto/x509" "fmt" "math" - "net" "net/http" "strconv" "strings" "time" "github.com/usual2970/certimate/internal/domain" + httputil "github.com/usual2970/certimate/internal/pkg/utils/http" ) type monitorNode struct { @@ -32,23 +32,23 @@ func NewMonitorNode(node *domain.WorkflowNode) *monitorNode { func (n *monitorNode) Process(ctx context.Context) error { n.logger.Info("ready to monitor certificate ...") - nodeConfig := n.node.GetConfigForMonitor() + nodeCfg := n.node.GetConfigForMonitor() - targetAddr := fmt.Sprintf("%s:%d", nodeConfig.Host, nodeConfig.Port) - if nodeConfig.Port == 0 { - targetAddr = fmt.Sprintf("%s:443", nodeConfig.Host) + targetAddr := fmt.Sprintf("%s:%d", nodeCfg.Host, nodeCfg.Port) + if nodeCfg.Port == 0 { + targetAddr = fmt.Sprintf("%s:443", nodeCfg.Host) } - targetDomain := nodeConfig.Domain + targetDomain := nodeCfg.Domain if targetDomain == "" { - targetDomain = nodeConfig.Host + 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 cert *x509.Certificate + var certs []*x509.Certificate var err error for attempt := 0; attempt < MAX_ATTEMPTS; attempt++ { if attempt > 0 { @@ -61,7 +61,7 @@ func (n *monitorNode) Process(ctx context.Context) error { } } - cert, err = n.tryRetrieveCert(ctx, targetAddr, targetDomain, nodeConfig.RequestPath) + certs, err = n.tryRetrievePeerCertificates(ctx, targetAddr, targetDomain, nodeCfg.RequestPath) if err == nil { break } @@ -71,15 +71,13 @@ func (n *monitorNode) Process(ctx context.Context) error { n.logger.Warn("failed to monitor certificate") return err } else { - if cert == nil { + if len(certs) == 0 { n.logger.Warn("no ssl certificates retrieved in http response") - outputs := map[string]any{ - outputCertificateValidatedKey: strconv.FormatBool(false), - outputCertificateDaysLeftKey: strconv.FormatInt(0, 10), - } - n.setOutputs(outputs) + 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), @@ -95,11 +93,8 @@ func (n *monitorNode) Process(ctx context.Context) error { validated := isCertPeriodValid && isCertHostMatched daysLeft := int(math.Floor(cert.NotAfter.Sub(now).Hours() / 24)) - outputs := map[string]any{ - outputCertificateValidatedKey: strconv.FormatBool(validated), - outputCertificateDaysLeftKey: strconv.FormatInt(int64(daysLeft), 10), - } - n.setOutputs(outputs) + 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)) @@ -113,52 +108,40 @@ func (n *monitorNode) Process(ctx context.Context) error { return nil } -func (n *monitorNode) tryRetrieveCert(ctx context.Context, addr, domain, requestPath string) (_cert *x509.Certificate, _err error) { - transport := &http.Transport{ - DialContext: (&net.Dialer{ - Timeout: 10 * time.Second, - }).DialContext, - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - ForceAttemptHTTP2: false, - DisableKeepAlives: true, - Proxy: http.ProxyFromEnvironment, +func (n *monitorNode) tryRetrievePeerCertificates(ctx context.Context, addr, domain, requestPath string) ([]*x509.Certificate, error) { + transport := httputil.NewDefaultTransport() + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{} } + transport.TLSClientConfig.InsecureSkipVerify = true client := &http.Client{ - Transport: transport, - Timeout: 15 * time.Second, 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(fmt.Sprintf("failed to create http request: %w", err)) - return nil, _err + err = fmt.Errorf("failed to create http request: %w", err) + n.logger.Warn(err.Error()) + return nil, err } 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(fmt.Sprintf("failed to send http request: %w", err)) - return nil, _err + 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 nil, _err + return make([]*x509.Certificate, 0), nil } - - _cert = resp.TLS.PeerCertificates[0] - return _cert, nil -} - -func (n *monitorNode) setOutputs(outputs map[string]any) { - n.outputs = outputs + return resp.TLS.PeerCertificates, nil } diff --git a/internal/workflow/node-processor/notify_node.go b/internal/workflow/node-processor/notify_node.go index f084cb4f..dabfd034 100644 --- a/internal/workflow/node-processor/notify_node.go +++ b/internal/workflow/node-processor/notify_node.go @@ -30,9 +30,9 @@ func NewNotifyNode(node *domain.WorkflowNode) *notifyNode { func (n *notifyNode) Process(ctx context.Context) error { n.logger.Info("ready to send notification ...") - nodeConfig := n.node.GetConfigForNotify() + nodeCfg := n.node.GetConfigForNotify() - if nodeConfig.Provider == "" { + if nodeCfg.Provider == "" { // Deprecated: v0.4.x 将废弃 // 兼容旧版本的通知渠道 n.logger.Warn("WARNING! you are using the notification channel from global settings, which will be deprecated in the future") @@ -44,14 +44,14 @@ func (n *notifyNode) Process(ctx context.Context) error { } // 获取通知渠道 - channelConfig, err := settings.GetNotifyChannelConfig(nodeConfig.Channel) + channelConfig, err := settings.GetNotifyChannelConfig(nodeCfg.Channel) if err != nil { return err } // 发送通知 - if err := notify.SendToChannel(nodeConfig.Subject, nodeConfig.Message, nodeConfig.Channel, channelConfig); err != nil { - n.logger.Warn("failed to send notification", slog.String("channel", nodeConfig.Channel)) + if err := notify.SendToChannel(nodeCfg.Subject, nodeCfg.Message, nodeCfg.Channel, channelConfig); err != nil { + n.logger.Warn("failed to send notification", slog.String("channel", nodeCfg.Channel)) return err } @@ -63,8 +63,8 @@ func (n *notifyNode) Process(ctx context.Context) error { deployer, err := notify.NewWithWorkflowNode(notify.NotifierWithWorkflowNodeConfig{ Node: n.node, Logger: n.logger, - Subject: nodeConfig.Subject, - Message: nodeConfig.Message, + Subject: nodeCfg.Subject, + Message: nodeCfg.Message, }) if err != nil { n.logger.Warn("failed to create notifier provider") diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go index 8e59b009..9431d31a 100644 --- a/internal/workflow/node-processor/upload_node.go +++ b/internal/workflow/node-processor/upload_node.go @@ -3,6 +3,7 @@ package nodeprocessor import ( "context" "fmt" + "strconv" "strings" "time" @@ -33,7 +34,7 @@ func NewUploadNode(node *domain.WorkflowNode) *uploadNode { func (n *uploadNode) Process(ctx context.Context) error { n.logger.Info("ready to upload certiticate ...") - nodeConfig := n.node.GetConfigForUpload() + nodeCfg := n.node.GetConfigForUpload() // 查询上次执行结果 lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id) @@ -53,7 +54,7 @@ func (n *uploadNode) Process(ctx context.Context) error { certificate := &domain.Certificate{ Source: domain.CertificateSourceTypeUpload, } - certificate.PopulateFromPEM(nodeConfig.Certificate, nodeConfig.PrivateKey) + certificate.PopulateFromPEM(nodeCfg.Certificate, nodeCfg.PrivateKey) // 保存执行结果 output := &domain.WorkflowOutput{ @@ -69,15 +70,15 @@ func (n *uploadNode) Process(ctx context.Context) error { return err } - n.outputs[outputCertificateValidatedKey] = "true" - n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(certificate.ExpireAt).Hours()/24)) + // 记录中间结果 + n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true) + n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(time.Until(certificate.ExpireAt).Hours()/24), 10) n.logger.Info("uploading completed") - return nil } -func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (skip bool, reason string) { +func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) { if lastOutput != nil && lastOutput.Succeeded { // 比较和上次上传时的关键配置(即影响证书上传的)参数是否一致 currentNodeConfig := n.node.GetConfigForUpload() @@ -91,8 +92,10 @@ func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workfl lastCertificate, _ := n.certRepo.GetByWorkflowRunId(ctx, lastOutput.RunId) if lastCertificate != nil { - n.outputs[outputCertificateValidatedKey] = "true" - n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(lastCertificate.ExpireAt).Hours()/24)) + daysLeft := int(time.Until(lastCertificate.ExpireAt).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" } } diff --git a/main.go b/main.go index 76f7f1c0..18e88bed 100644 --- a/main.go +++ b/main.go @@ -26,9 +26,7 @@ func main() { app := app.GetApp().(*pocketbase.PocketBase) var flagHttp string - var flagDir string flag.StringVar(&flagHttp, "http", "127.0.0.1:8090", "HTTP server address") - flag.StringVar(&flagDir, "dir", "/pb_data/database", "Pocketbase data directory") if len(os.Args) < 2 { slog.Error("[CERTIMATE] missing exec args") os.Exit(1) @@ -59,14 +57,17 @@ func main() { Priority: 999, }) + app.OnServe().BindFunc(func(e *core.ServeEvent) error { + slog.Info("[CERTIMATE] Visit the website: http://" + flagHttp) + return e.Next() + }) + app.OnTerminate().BindFunc(func(e *core.TerminateEvent) error { routes.Unregister() slog.Info("[CERTIMATE] Exit!") return e.Next() }) - slog.Info("[CERTIMATE] Visit the website: http://" + flagHttp) - if err := app.Start(); err != nil { slog.Error("[CERTIMATE] Start failed.", "err", err) } diff --git a/ui/src/components/MultipleInput.tsx b/ui/src/components/MultipleInput.tsx index d28db745..d8be12fa 100644 --- a/ui/src/components/MultipleInput.tsx +++ b/ui/src/components/MultipleInput.tsx @@ -152,10 +152,10 @@ const MultipleInput = ({ value={element} onBlur={() => handleInputBlur(index)} onChange={(val) => handleChange(index, val)} - onClickAdd={() => handleClickAdd(index)} - onClickDown={() => handleClickDown(index)} - onClickUp={() => handleClickUp(index)} - onClickRemove={() => handleClickRemove(index)} + onEntryAdd={() => handleClickAdd(index)} + onEntryDown={() => handleClickDown(index)} + onEntryUp={() => handleClickUp(index)} + onEntryRemove={() => handleClickRemove(index)} /> ); })} @@ -174,10 +174,10 @@ type MultipleInputItemProps = Omit< defaultValue?: string; value?: string; onChange?: (value: string) => void; - onClickAdd?: () => void; - onClickDown?: () => void; - onClickUp?: () => void; - onClickRemove?: () => void; + onEntryAdd?: () => void; + onEntryDown?: () => void; + onEntryUp?: () => void; + onEntryRemove?: () => void; }; type MultipleInputItemInstance = { @@ -197,10 +197,10 @@ const MultipleInputItem = forwardRef { if (!showSortButton) return null; - return diff --git a/ui/src/components/access/AccessSelect.tsx b/ui/src/components/access/AccessSelect.tsx index 0a570699..01f30249 100644 --- a/ui/src/components/access/AccessSelect.tsx +++ b/ui/src/components/access/AccessSelect.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; +import { Avatar, Select, type SelectProps, Space, Typography, theme } from "antd"; import { type AccessModel } from "@/domain/access"; import { accessProvidersMap } from "@/domain/provider"; @@ -14,6 +14,8 @@ export type AccessTypeSelectProps = Omit< }; const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => { + const { token: themeToken } = theme.useToken(); + const { accesses, loadedAtOnce, fetchAccesses } = useAccessesStore(useZustandShallowSelector(["accesses", "loadedAtOnce", "fetchAccesses"])); useEffect(() => { fetchAccesses(); @@ -65,12 +67,12 @@ const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => { const value = inputValue.toLowerCase(); return option.label.toLowerCase().includes(value); }} - labelRender={({ label, value }) => { - if (label) { + labelRender={({ value }) => { + if (value != null) { return renderOption(value as string); } - return {props.placeholder}; + return {props.placeholder}; }} loading={!loadedAtOnce} options={options} diff --git a/ui/src/components/provider/ACMEDns01ProviderSelect.tsx b/ui/src/components/provider/ACMEDns01ProviderSelect.tsx index e2408eeb..227bfcdd 100644 --- a/ui/src/components/provider/ACMEDns01ProviderSelect.tsx +++ b/ui/src/components/provider/ACMEDns01ProviderSelect.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; +import { Avatar, Select, type SelectProps, Space, Typography, theme } from "antd"; import { type ACMEDns01Provider, acmeDns01ProvidersMap } from "@/domain/provider"; @@ -14,6 +14,8 @@ export type ACMEDns01ProviderSelectProps = Omit< const ACMEDns01ProviderSelect = ({ filter, ...props }: ACMEDns01ProviderSelectProps) => { const { t } = useTranslation(); + const { token: themeToken } = theme.useToken(); + const [options, setOptions] = useState>([]); useEffect(() => { const allItems = Array.from(acmeDns01ProvidersMap.values()); @@ -49,12 +51,12 @@ const ACMEDns01ProviderSelect = ({ filter, ...props }: ACMEDns01ProviderSelectPr const value = inputValue.toLowerCase(); return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value); }} - labelRender={({ label, value }) => { - if (!label) { - return {props.placeholder}; + labelRender={({ value }) => { + if (value != null) { + return renderOption(value as string); } - return renderOption(value as string); + return {props.placeholder}; }} options={options} optionFilterProp={undefined} diff --git a/ui/src/components/provider/AccessProviderSelect.tsx b/ui/src/components/provider/AccessProviderSelect.tsx index bf4ff6e7..055b3ddc 100644 --- a/ui/src/components/provider/AccessProviderSelect.tsx +++ b/ui/src/components/provider/AccessProviderSelect.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Avatar, Select, type SelectProps, Space, Tag, Typography } from "antd"; +import { Avatar, Select, type SelectProps, Space, Tag, Typography, theme } from "antd"; import Show from "@/components/Show"; import { ACCESS_USAGES, type AccessProvider, type AccessUsageType, accessProvidersMap } from "@/domain/provider"; @@ -16,6 +16,8 @@ export type AccessProviderSelectProps = Omit< const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProviderSelectProps = { showOptionTags: true }) => { const { t } = useTranslation(); + const { token: themeToken } = theme.useToken(); + const [options, setOptions] = useState>([]); useEffect(() => { const allItems = Array.from(accessProvidersMap.values()); @@ -84,12 +86,12 @@ const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProvid const value = inputValue.toLowerCase(); return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value); }} - labelRender={({ label, value }) => { - if (!label) { - return {props.placeholder}; + labelRender={({ value }) => { + if (value != null) { + return renderOption(value as string); } - return renderOption(value as string); + return {props.placeholder}; }} options={options} optionFilterProp={undefined} diff --git a/ui/src/components/provider/CAProviderSelect.tsx b/ui/src/components/provider/CAProviderSelect.tsx index e5477c21..d1fdbba9 100644 --- a/ui/src/components/provider/CAProviderSelect.tsx +++ b/ui/src/components/provider/CAProviderSelect.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; +import { Avatar, Select, type SelectProps, Space, Typography, theme } from "antd"; import { type CAProvider, caProvidersMap } from "@/domain/provider"; @@ -14,6 +14,8 @@ export type CAProviderSelectProps = Omit< const CAProviderSelect = ({ filter, ...props }: CAProviderSelectProps) => { const { t } = useTranslation(); + const { token: themeToken } = theme.useToken(); + const [options, setOptions] = useState>([]); useEffect(() => { const allItems = Array.from(caProvidersMap.values()); @@ -65,12 +67,12 @@ const CAProviderSelect = ({ filter, ...props }: CAProviderSelectProps) => { const value = inputValue.toLowerCase(); return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value); }} - labelRender={({ label, value }) => { - if (!label) { - return {props.placeholder || t("provider.default_ca_provider.label")}; + labelRender={({ value }) => { + if (value != null) { + return renderOption(value as string); } - return renderOption(value as string); + return {props.placeholder}; }} options={options} optionFilterProp={undefined} diff --git a/ui/src/components/provider/DeploymentProviderSelect.tsx b/ui/src/components/provider/DeploymentProviderSelect.tsx index 89173243..07fa4577 100644 --- a/ui/src/components/provider/DeploymentProviderSelect.tsx +++ b/ui/src/components/provider/DeploymentProviderSelect.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; +import { Avatar, Select, type SelectProps, Space, Typography, theme } from "antd"; import { type DeploymentProvider, deploymentProvidersMap } from "@/domain/provider"; @@ -14,6 +14,8 @@ export type DeploymentProviderSelectProps = Omit< const DeploymentProviderSelect = ({ filter, ...props }: DeploymentProviderSelectProps) => { const { t } = useTranslation(); + const { token: themeToken } = theme.useToken(); + const [options, setOptions] = useState>([]); useEffect(() => { const allItems = Array.from(deploymentProvidersMap.values()); @@ -49,12 +51,12 @@ const DeploymentProviderSelect = ({ filter, ...props }: DeploymentProviderSelect const value = inputValue.toLowerCase(); return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value); }} - labelRender={({ label, value }) => { - if (!label) { - return {props.placeholder}; + labelRender={({ value }) => { + if (value != null) { + return renderOption(value as string); } - return renderOption(value as string); + return {props.placeholder}; }} options={options} optionFilterProp={undefined} diff --git a/ui/src/components/provider/NotificationProviderSelect.tsx b/ui/src/components/provider/NotificationProviderSelect.tsx index f30a8f6f..8b0dd353 100644 --- a/ui/src/components/provider/NotificationProviderSelect.tsx +++ b/ui/src/components/provider/NotificationProviderSelect.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; +import { Avatar, Select, type SelectProps, Space, Typography, theme } from "antd"; import { type NotificationProvider, notificationProvidersMap } from "@/domain/provider"; @@ -14,6 +14,8 @@ export type NotificationProviderSelectProps = Omit< const NotificationProviderSelect = ({ filter, ...props }: NotificationProviderSelectProps) => { const { t } = useTranslation(); + const { token: themeToken } = theme.useToken(); + const [options, setOptions] = useState>([]); useEffect(() => { const allItems = Array.from(notificationProvidersMap.values()); @@ -49,12 +51,12 @@ const NotificationProviderSelect = ({ filter, ...props }: NotificationProviderSe const value = inputValue.toLowerCase(); return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value); }} - labelRender={({ label, value }) => { - if (!label) { - return {props.placeholder}; + labelRender={({ value }) => { + if (value != null) { + return renderOption(value as string); } - return renderOption(value as string); + return {props.placeholder}; }} options={options} optionFilterProp={undefined} diff --git a/ui/src/components/workflow/WorkflowRunDetail.tsx b/ui/src/components/workflow/WorkflowRunDetail.tsx index 2d421880..746adb4c 100644 --- a/ui/src/components/workflow/WorkflowRunDetail.tsx +++ b/ui/src/components/workflow/WorkflowRunDetail.tsx @@ -36,7 +36,7 @@ import { ClientResponseError } from "pocketbase"; import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer"; import Show from "@/components/Show"; import { type CertificateModel } from "@/domain/certificate"; -import type { WorkflowLogModel } from "@/domain/workflowLog"; +import { type WorkflowLogModel } from "@/domain/workflowLog"; import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun"; import { useBrowserTheme } from "@/hooks"; import { listByWorkflowRunId as listCertificatesByWorkflowRunId } from "@/repository/certificate"; diff --git a/ui/src/components/workflow/node/AddNode.tsx b/ui/src/components/workflow/node/AddNode.tsx index 86a45134..207ec7c7 100644 --- a/ui/src/components/workflow/node/AddNode.tsx +++ b/ui/src/components/workflow/node/AddNode.tsx @@ -35,7 +35,14 @@ const AddNode = ({ node, disabled }: AddNodeProps) => { [WorkflowNodeType.ExecuteResultBranch, "workflow_node.execute_result_branch.label", ], ] .filter(([type]) => { - if (node.type !== WorkflowNodeType.Apply && node.type !== WorkflowNodeType.Deploy && node.type !== WorkflowNodeType.Notify) { + const hasExecuteResult = [ + WorkflowNodeType.Apply, + WorkflowNodeType.Upload, + WorkflowNodeType.Monitor, + WorkflowNodeType.Deploy, + WorkflowNodeType.Notify, + ].includes(node.type); + if (!hasExecuteResult) { return type !== WorkflowNodeType.ExecuteResultBranch; } diff --git a/ui/src/components/workflow/node/ApplyNode.tsx b/ui/src/components/workflow/node/ApplyNode.tsx index c250fd89..ff0d64bf 100644 --- a/ui/src/components/workflow/node/ApplyNode.tsx +++ b/ui/src/components/workflow/node/ApplyNode.tsx @@ -38,9 +38,9 @@ const ApplyNode = ({ node, disabled }: ApplyNodeProps) => { const formRef = useRef(null); const [formPending, setFormPending] = useState(false); + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForApply; const [drawerOpen, setDrawerOpen] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForApply; const handleDrawerConfirm = async () => { setFormPending(true); @@ -74,12 +74,12 @@ const ApplyNode = ({ node, disabled }: ApplyNodeProps) => { setDrawerOpen(open)} - getFormValues={() => formRef.current!.getFieldsValue()} > diff --git a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx index 7faa148e..ae56efc3 100644 --- a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx @@ -56,7 +56,7 @@ export type ApplyNodeConfigFormInstance = { validateFields: FormInstance["validateFields"]; }; -const MULTIPLE_INPUT_DELIMITER = ";"; +const MULTIPLE_INPUT_SEPARATOR = ";"; const initFormModel = (): ApplyNodeConfigFormFieldValues => { return { @@ -76,7 +76,7 @@ const ApplyNodeConfigForm = forwardRef { if (!v) return false; return String(v) - .split(MULTIPLE_INPUT_DELIMITER) + .split(MULTIPLE_INPUT_SEPARATOR) .every((e) => validDomainName(e, { allowWildcard: true })); }, t("common.errmsg.domain_invalid")), contactEmail: z.string({ message: t("workflow_node.apply.form.contact_email.placeholder") }).email(t("common.errmsg.email_invalid")), @@ -106,7 +106,7 @@ const ApplyNodeConfigForm = forwardRef { if (!v) return true; return String(v) - .split(MULTIPLE_INPUT_DELIMITER) + .split(MULTIPLE_INPUT_SEPARATOR) .every((e) => validIPv4Address(e) || validIPv6Address(e) || validDomainName(e)); }, t("common.errmsg.host_invalid")), dnsPropagationWait: z.preprocess( diff --git a/ui/src/components/workflow/node/ConditionNode.tsx b/ui/src/components/workflow/node/ConditionNode.tsx index bc5b5918..2c8b3d81 100644 --- a/ui/src/components/workflow/node/ConditionNode.tsx +++ b/ui/src/components/workflow/node/ConditionNode.tsx @@ -1,17 +1,14 @@ import { memo, useRef, useState } from "react"; -import { MoreOutlined as MoreOutlinedIcon } from "@ant-design/icons"; +import { FilterFilled as FilterFilledIcon, FilterOutlined as FilterOutlinedIcon, MoreOutlined as MoreOutlinedIcon } from "@ant-design/icons"; import { Button, Card, Popover } from "antd"; import { produce } from "immer"; -import type { Expr, WorkflowNodeIoValueType } from "@/domain/workflow"; -import { ExprType } from "@/domain/workflow"; import { useZustandShallowSelector } from "@/hooks"; import { useWorkflowStore } from "@/stores/workflow"; import SharedNode, { type SharedNodeProps } from "./_SharedNode"; import AddNode from "./AddNode"; -import type { ConditionItem, ConditionNodeConfigFormFieldValues, ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm"; -import ConditionNodeConfigForm from "./ConditionNodeConfigForm"; +import ConditionNodeConfigForm, { type ConditionNodeConfigFormFieldValues, type ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm"; export type ConditionNodeProps = SharedNodeProps & { branchId: string; @@ -23,55 +20,9 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP const [formPending, setFormPending] = useState(false); const formRef = useRef(null); - - const [drawerOpen, setDrawerOpen] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as ConditionNodeConfigFormFieldValues; - // 将表单值转换为表达式结构 - const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => { - // 创建单个条件的表达式 - const createComparisonExpr = (condition: ConditionItem): Expr => { - const selectors = condition.leftSelector.split("#"); - const t = selectors[2] as WorkflowNodeIoValueType; - const left: Expr = { - type: ExprType.Var, - selector: { - id: selectors[0], - name: selectors[1], - type: t, - }, - }; - - const right: Expr = { type: ExprType.Const, value: condition.rightValue, valueType: t }; - - return { - type: ExprType.Compare, - op: condition.operator, - left, - right, - }; - }; - - // 如果只有一个条件,直接返回比较表达式 - if (values.conditions.length === 1) { - return createComparisonExpr(values.conditions[0]); - } - - // 多个条件,通过逻辑运算符连接 - let expr: Expr = createComparisonExpr(values.conditions[0]); - - for (let i = 1; i < values.conditions.length; i++) { - expr = { - type: ExprType.Logical, - op: values.logicalOperator, - left: expr, - right: createComparisonExpr(values.conditions[i]), - }; - } - - return expr; - }; + const [drawerOpen, setDrawerOpen] = useState(false); const handleDrawerConfirm = async () => { setFormPending(true); @@ -84,10 +35,9 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP try { const newValues = getFormValues(); - const expression = formToExpression(newValues); const newNode = produce(node, (draft) => { draft.config = { - expression, + ...newValues, }; draft.validated = true; }); @@ -100,7 +50,7 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP return ( <> setDrawerOpen(true)}>
- +
e.stopPropagation()}> + +
setDrawerOpen(true)}> + {node.config?.expression ? ( +
+
- - setDrawerOpen(open)} - getFormValues={() => formRef.current!.getFieldsValue()} - > - -
+ setDrawerOpen(open)} + > + + + ); diff --git a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx index 9cbb56cc..3cd92d7b 100644 --- a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx @@ -1,36 +1,16 @@ -import { forwardRef, memo, useEffect, useImperativeHandle, useState } from "react"; -import { Button, Card, Form, Input, Select, Radio } from "antd"; -import { PlusOutlined, DeleteOutlined } from "@ant-design/icons"; -import i18n from "@/i18n"; - -import { - WorkflowNodeConfigForCondition, - Expr, - WorkflowNodeIOValueSelector, - ComparisonOperator, - LogicalOperator, - isConstExpr, - isVarExpr, - WorkflowNode, - workflowNodeIOOptions, - WorkflowNodeIoValueType, - ExprType, -} from "@/domain/workflow"; -import { FormInstance } from "antd"; -import { useZustandShallowSelector } from "@/hooks"; -import { useWorkflowStore } from "@/stores/workflow"; +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"; -// 表单内部使用的扁平结构 - 修改后只保留必要字段 -export interface ConditionItem { - leftSelector: string; - operator: ComparisonOperator; - rightValue: string; -} +import { type Expr, type WorkflowNodeConfigForCondition } from "@/domain/workflow"; +import { useAntdForm } from "@/hooks"; + +import ConditionNodeConfigFormExpressionEditor, { type ConditionNodeConfigFormExpressionEditorInstance } from "./ConditionNodeConfigFormExpressionEditor"; export type ConditionNodeConfigFormFieldValues = { - conditions: ConditionItem[]; - logicalOperator: LogicalOperator; + expression?: Expr | undefined; }; export type ConditionNodeConfigFormProps = { @@ -38,9 +18,8 @@ export type ConditionNodeConfigFormProps = { style?: React.CSSProperties; disabled?: boolean; initialValues?: Partial; - onValuesChange?: (values: WorkflowNodeConfigForCondition) => void; - availableSelectors?: WorkflowNodeIOValueSelector[]; nodeId: string; + onValuesChange?: (values: WorkflowNodeConfigForCondition) => void; }; export type ConditionNodeConfigFormInstance = { @@ -49,298 +28,49 @@ export type ConditionNodeConfigFormInstance = { validateFields: FormInstance["validateFields"]; }; -// 初始表单值 const initFormModel = (): ConditionNodeConfigFormFieldValues => { - return { - conditions: [ - { - leftSelector: "", - operator: "==", - rightValue: "", - }, - ], - logicalOperator: LogicalOperator.And, - }; -}; - -// 递归提取表达式中的条件项 -const expressionToForm = (expr?: Expr): ConditionNodeConfigFormFieldValues => { - if (!expr) return initFormModel(); - - const conditions: ConditionItem[] = []; - let logicalOp: LogicalOperator = LogicalOperator.And; - - const extractComparisons = (expr: Expr): void => { - if (expr.type === ExprType.Compare) { - // 确保左侧是变量,右侧是常量 - if (isVarExpr(expr.left) && isConstExpr(expr.right)) { - conditions.push({ - leftSelector: `${expr.left.selector.id}#${expr.left.selector.name}#${expr.left.selector.type}`, - operator: expr.op, - rightValue: String(expr.right.value), - }); - } - } else if (expr.type === ExprType.Logical) { - logicalOp = expr.op; - extractComparisons(expr.left); - extractComparisons(expr.right); - } - }; - - extractComparisons(expr); - - return { - conditions: conditions.length > 0 ? conditions : initFormModel().conditions, - logicalOperator: logicalOp, - }; -}; - -// 根据变量类型获取适当的操作符选项 -const getOperatorsByType = (type: string): { value: ComparisonOperator; label: string }[] => { - switch (type) { - case "number": - case "string": - return [ - { value: "==", label: i18n.t("workflow_node.condition.form.comparison.equal") }, - { value: "!=", label: i18n.t("workflow_node.condition.form.comparison.not_equal") }, - { value: ">", label: i18n.t("workflow_node.condition.form.comparison.greater_than") }, - { value: ">=", label: i18n.t("workflow_node.condition.form.comparison.greater_than_or_equal") }, - { value: "<", label: i18n.t("workflow_node.condition.form.comparison.less_than") }, - { value: "<=", label: i18n.t("workflow_node.condition.form.comparison.less_than_or_equal") }, - ]; - case "boolean": - return [{ value: "is", label: i18n.t("workflow_node.condition.form.comparison.is") }]; - default: - return []; - } -}; - -// 从选择器字符串中提取变量类型 -const getVariableTypeFromSelector = (selector: string): string => { - if (!selector) return "string"; - - // 假设选择器格式为 "id#name#type" - const parts = selector.split("#"); - if (parts.length >= 3) { - return parts[2].toLowerCase() || "string"; - } - return "string"; + return {}; }; const ConditionNodeConfigForm = forwardRef( - ({ className, style, disabled, initialValues, onValuesChange, nodeId }, ref) => { + ({ className, style, disabled, initialValues, nodeId, onValuesChange }, ref) => { const { t } = useTranslation(); - const prefix = "workflow_node.condition.form"; - const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"])); + const formSchema = z.object({ + expression: z.any().nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + const { form: formInst, formProps } = useAntdForm({ + name: "workflowNodeConditionConfigForm", + initialValues: initialValues ?? initFormModel(), + }); - const [form] = Form.useForm(); - const [formModel, setFormModel] = useState(initFormModel()); + const editorRef = useRef(null); - const [previousNodes, setPreviousNodes] = useState([]); - useEffect(() => { - const previousNodes = getWorkflowOuptutBeforeId(nodeId); - setPreviousNodes(previousNodes); - }, [nodeId]); - - // 初始化表单值 - useEffect(() => { - if (initialValues?.expression) { - const formValues = expressionToForm(initialValues.expression); - form.setFieldsValue(formValues); - setFormModel(formValues); - } - }, [form, initialValues]); - - // 公开表单方法 - useImperativeHandle( - ref, - () => ({ - getFieldsValue: form.getFieldsValue, - resetFields: form.resetFields, - validateFields: form.validateFields, - }), - [form] - ); - - // 表单值变更处理 - const handleFormChange = (_: undefined, values: ConditionNodeConfigFormFieldValues) => { - setFormModel(values); - - if (onValuesChange) { - // 将表单值转换为表达式 - const expression = formToExpression(values); - onValuesChange({ expression }); - } + 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 ( -
- - {(fields, { add, remove }) => ( - <> - {fields.map(({ key, name, ...restField }) => ( - 1 ? - - - )} - - - {formModel.conditions && formModel.conditions.length > 1 && ( - - - {t(`${prefix}.logical_operator.and`)} - {t(`${prefix}.logical_operator.or`)} - - - )} + + + +
); } ); -// 表单值转换为表达式结构 (需要添加) -const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => { - const createComparisonExpr = (condition: ConditionItem): Expr => { - const [id, name, typeStr] = condition.leftSelector.split("#"); - - const type = typeStr as WorkflowNodeIoValueType; - - const left: Expr = { - type: ExprType.Var, - selector: { id, name, type }, - }; - - const right: Expr = { - type: ExprType.Const, - value: condition.rightValue, - valueType: type, - }; - - return { - type: ExprType.Compare, - op: condition.operator, - left, - right, - }; - }; - - // 如果只有一个条件,直接返回比较表达式 - if (values.conditions.length === 1) { - return createComparisonExpr(values.conditions[0]); - } - - // 多个条件,通过逻辑运算符连接 - let expr: Expr = createComparisonExpr(values.conditions[0]); - - for (let i = 1; i < values.conditions.length; i++) { - expr = { - type: ExprType.Logical, - op: values.logicalOperator, - left: expr, - right: createComparisonExpr(values.conditions[i]), - }; - } - - return expr; -}; - export default memo(ConditionNodeConfigForm); diff --git a/ui/src/components/workflow/node/ConditionNodeConfigFormExpressionEditor.tsx b/ui/src/components/workflow/node/ConditionNodeConfigFormExpressionEditor.tsx new file mode 100644 index 00000000..1696d46c --- /dev/null +++ b/ui/src/components/workflow/node/ConditionNodeConfigFormExpressionEditor.tsx @@ -0,0 +1,400 @@ +import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { CloseOutlined as CloseOutlinedIcon, PlusOutlined } from "@ant-design/icons"; +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 type ConditionNodeConfigFormExpressionEditorProps = { + className?: string; + style?: React.CSSProperties; + defaultValue?: Expr; + disabled?: boolean; + nodeId: string; + value?: Expr; + onChange?: (value: Expr) => void; +}; + +export type 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 index b04516fb..f495c040 100644 --- a/ui/src/components/workflow/node/DeployNode.tsx +++ b/ui/src/components/workflow/node/DeployNode.tsx @@ -24,10 +24,10 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => { 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); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForDeploy; useEffect(() => { setDrawerFooterShow(!!(node.config as WorkflowNodeConfigForDeploy)?.provider); @@ -86,8 +86,9 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => { { setDrawerFooterShow(!!(node.config as WorkflowNodeConfigForDeploy)?.provider); setDrawerOpen(open); }} - getFormValues={() => formRef.current!.getFieldsValue()} > diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index 0443327e..33fefcf0 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -1,7 +1,7 @@ import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons"; -import { Button, Divider, Flex, Form, type FormInstance, Select, Switch, Tooltip, Typography } from "antd"; +import { Button, Divider, Flex, Form, type FormInstance, Select, Switch, Tooltip, Typography, theme } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; @@ -11,7 +11,7 @@ import DeploymentProviderPicker from "@/components/provider/DeploymentProviderPi import DeploymentProviderSelect from "@/components/provider/DeploymentProviderSelect.tsx"; import Show from "@/components/Show"; import { ACCESS_USAGES, DEPLOYMENT_PROVIDERS, accessProvidersMap, deploymentProvidersMap } from "@/domain/provider"; -import { type WorkflowNode, type WorkflowNodeConfigForDeploy } from "@/domain/workflow"; +import { type WorkflowNodeConfigForDeploy, WorkflowNodeType } from "@/domain/workflow"; import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks"; import { useWorkflowStore } from "@/stores/workflow"; @@ -125,14 +125,9 @@ const DeployNodeConfigForm = forwardRef { const { t } = useTranslation(); - const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"])); + const { token: themeToken } = theme.useToken(); - // TODO: 优化此处逻辑 - const [previousNodes, setPreviousNodes] = useState([]); - useEffect(() => { - const previousNodes = getWorkflowOuptutBeforeId(nodeId, "certificate"); - setPreviousNodes(previousNodes); - }, [nodeId]); + const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"])); const formSchema = z.object({ certificate: z @@ -170,6 +165,24 @@ const DeployNodeConfigForm = forwardRef { + 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(() => { @@ -487,17 +500,15 @@ const DeployNodeConfigForm = forwardRef} > ({ diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx index d64e6eba..36d663b5 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx @@ -18,7 +18,7 @@ export type DeployNodeConfigFormWangsuCDNConfigProps = { onValuesChange?: (values: DeployNodeConfigFormWangsuCDNConfigFieldValues) => void; }; -const MULTIPLE_INPUT_DELIMITER = ";"; +const MULTIPLE_INPUT_SEPARATOR = ";"; const initFormModel = (): DeployNodeConfigFormWangsuCDNConfigFieldValues => { return { @@ -42,7 +42,7 @@ const DeployNodeConfigFormWangsuCDNConfig = ({ .refine((v) => { if (!v) return false; return String(v) - .split(MULTIPLE_INPUT_DELIMITER) + .split(MULTIPLE_INPUT_SEPARATOR) .every((e) => validDomainName(e)); }, t("workflow_node.deploy.form.wangsu_cdn_domains.placeholder")), }); diff --git a/ui/src/components/workflow/node/MonitorNode.tsx b/ui/src/components/workflow/node/MonitorNode.tsx index 68feb842..39fb159e 100644 --- a/ui/src/components/workflow/node/MonitorNode.tsx +++ b/ui/src/components/workflow/node/MonitorNode.tsx @@ -23,9 +23,9 @@ const MonitorNode = ({ node, disabled }: MonitorNodeProps) => { const formRef = useRef(null); const [formPending, setFormPending] = useState(false); + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForMonitor; const [drawerOpen, setDrawerOpen] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForMonitor; const wrappedEl = useMemo(() => { if (node.type !== WorkflowNodeType.Monitor) { @@ -74,12 +74,12 @@ const MonitorNode = ({ node, disabled }: MonitorNodeProps) => { setDrawerOpen(open)} - getFormValues={() => formRef.current!.getFieldsValue()} > diff --git a/ui/src/components/workflow/node/NotifyNode.tsx b/ui/src/components/workflow/node/NotifyNode.tsx index da48552d..89326b50 100644 --- a/ui/src/components/workflow/node/NotifyNode.tsx +++ b/ui/src/components/workflow/node/NotifyNode.tsx @@ -25,9 +25,9 @@ const NotifyNode = ({ node, disabled }: NotifyNodeProps) => { const formRef = useRef(null); const [formPending, setFormPending] = useState(false); + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForNotify; const [drawerOpen, setDrawerOpen] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForNotify; const wrappedEl = useMemo(() => { if (node.type !== WorkflowNodeType.Notify) { @@ -82,12 +82,12 @@ const NotifyNode = ({ node, disabled }: NotifyNodeProps) => { setDrawerOpen(open)} - getFormValues={() => formRef.current!.getFieldsValue()} > diff --git a/ui/src/components/workflow/node/StartNode.tsx b/ui/src/components/workflow/node/StartNode.tsx index 900793fa..4b920bd9 100644 --- a/ui/src/components/workflow/node/StartNode.tsx +++ b/ui/src/components/workflow/node/StartNode.tsx @@ -23,9 +23,9 @@ const StartNode = ({ node, disabled }: StartNodeProps) => { const formRef = useRef(null); const [formPending, setFormPending] = useState(false); + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForStart; const [drawerOpen, setDrawerOpen] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForStart; const wrappedEl = useMemo(() => { if (node.type !== WorkflowNodeType.Start) { @@ -83,12 +83,12 @@ const StartNode = ({ node, disabled }: StartNodeProps) => { setDrawerOpen(open)} - getFormValues={() => formRef.current!.getFieldsValue()} > diff --git a/ui/src/components/workflow/node/UploadNode.tsx b/ui/src/components/workflow/node/UploadNode.tsx index 0197a8c4..6936f147 100644 --- a/ui/src/components/workflow/node/UploadNode.tsx +++ b/ui/src/components/workflow/node/UploadNode.tsx @@ -23,9 +23,9 @@ const UploadNode = ({ node, disabled }: UploadNodeProps) => { const formRef = useRef(null); const [formPending, setFormPending] = useState(false); + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForUpload; const [drawerOpen, setDrawerOpen] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForUpload; const wrappedEl = useMemo(() => { if (node.type !== WorkflowNodeType.Upload) { @@ -74,12 +74,12 @@ const UploadNode = ({ node, disabled }: UploadNodeProps) => { setDrawerOpen(open)} - getFormValues={() => formRef.current!.getFieldsValue()} > diff --git a/ui/src/components/workflow/node/_SharedNode.tsx b/ui/src/components/workflow/node/_SharedNode.tsx index 44c894ef..72f4b967 100644 --- a/ui/src/components/workflow/node/_SharedNode.tsx +++ b/ui/src/components/workflow/node/_SharedNode.tsx @@ -33,7 +33,7 @@ const SharedNodeTitle = ({ className, style, node, disabled }: SharedNodeTitlePr const handleBlur = (e: React.FocusEvent) => { const oldName = node.name; - const newName = e.target.innerText.trim().substring(0, 64) || oldName; + const newName = e.target.innerText.replaceAll("\r", "").replaceAll("\n", "").trim().substring(0, 64) || oldName; if (oldName === newName) { return; } @@ -45,9 +45,16 @@ const SharedNodeTitle = ({ className, style, node, disabled }: SharedNodeTitlePr ); }; + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.code === "Enter") { + e.preventDefault(); + e.currentTarget.blur(); + } + }; + return (
-
+
{node.name}
@@ -91,7 +98,7 @@ const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex, const handleRenameConfirm = async () => { const oldName = node.name; - const newName = nameRef.current?.trim()?.substring(0, 64) || oldName; + const newName = nameRef.current?.replaceAll("\r", "")?.replaceAll("\n", "").trim()?.substring(0, 64) || oldName; if (oldName === newName) { return; } @@ -195,7 +202,7 @@ const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex, }; // #endregion -// #region Wrapper +// #region Block type SharedNodeBlockProps = SharedNodeProps & { children: React.ReactNode; onClick?: (e: React.MouseEvent) => void; @@ -245,7 +252,7 @@ type SharedNodeEditDrawerProps = SharedNodeProps & { pending?: boolean; onOpenChange?: (open: boolean) => void; onConfirm: () => void | Promise; - getFormValues: () => NonNullable; + getConfigNewValues: () => NonNullable; // 用于获取节点配置的新值,以便在抽屉关闭前进行对比,决定是否提示保存 }; const SharedNodeConfigDrawer = ({ @@ -256,7 +263,7 @@ const SharedNodeConfigDrawer = ({ loading, pending, onConfirm, - getFormValues, + getConfigNewValues, ...props }: SharedNodeEditDrawerProps) => { const { t } = useTranslation(); @@ -284,7 +291,7 @@ const SharedNodeConfigDrawer = ({ if (pending) return; const oldValues = JSON.parse(JSON.stringify(node.config ?? {})); - const newValues = JSON.parse(JSON.stringify(getFormValues())); + const newValues = JSON.parse(JSON.stringify(getConfigNewValues())); const changed = !isEqual(oldValues, {}) && !isEqual(oldValues, newValues); const { promise, resolve, reject } = Promise.withResolvers(); diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 4dea7f64..d3b9f822 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -69,7 +69,7 @@ const workflowNodeTypeDefaultInputs: Map = n name: "certificate", type: "certificate", required: true, - label: i18n.t("workflow.variables.certificate.label"), + label: i18n.t("workflow.variables.type.certificate.label"), }, ], ], @@ -84,7 +84,7 @@ const workflowNodeTypeDefaultOutputs: Map = name: "certificate", type: "certificate", required: true, - label: i18n.t("workflow.variables.certificate.label"), + label: i18n.t("workflow.variables.type.certificate.label"), }, ], ], @@ -95,7 +95,7 @@ const workflowNodeTypeDefaultOutputs: Map = name: "certificate", type: "certificate", required: true, - label: i18n.t("workflow.variables.certificate.label"), + label: i18n.t("workflow.variables.type.certificate.label"), }, ], ], @@ -106,7 +106,7 @@ const workflowNodeTypeDefaultOutputs: Map = name: "certificate", type: "certificate", required: true, - label: i18n.t("workflow.variables.certificate.label"), + label: i18n.t("workflow.variables.type.certificate.label"), }, ], ], @@ -188,7 +188,7 @@ export type WorkflowNodeConfigForNotify = { }; export type WorkflowNodeConfigForCondition = { - expression: Expr; + expression?: Expr; }; export type WorkflowNodeConfigForBranch = never; @@ -204,96 +204,35 @@ export type WorkflowNodeIO = { valueSelector?: WorkflowNodeIOValueSelector; }; -export const VALUE_TYPES = Object.freeze({ - STRING: "string", - NUMBER: "number", - BOOLEAN: "boolean", -} as const); - -export type WorkflowNodeIoValueType = (typeof VALUE_TYPES)[keyof typeof VALUE_TYPES]; - -export type WorkflowNodeIOValueSelector = { - id: string; - name: string; - type: WorkflowNodeIoValueType; -}; - -type WorkflowNodeIOOptions = { - label: string; - value: string; -}; - -export const workflowNodeIOOptions = (node: WorkflowNode) => { - const rs = { - label: node.name, - options: Array(), - }; - - if (node.outputs) { - for (const output of node.outputs) { - switch (output.type) { - case "certificate": - rs.options.push({ - label: `${node.name} - ${output.label} - ${i18n.t("workflow.variables.is_validated.label")}`, - value: `${node.id}#${output.name}.validated#boolean`, - }); - - rs.options.push({ - label: `${node.name} - ${output.label} - ${i18n.t("workflow.variables.days_left.label")}`, - value: `${node.id}#${output.name}.daysLeft#number`, - }); - break; - default: - rs.options.push({ - label: `${node.name} - ${output.label}`, - value: `${node.id}#${output.name}#${output.type}`, - }); - break; - } - } - } - - return rs; -}; - +export type WorkflowNodeIOValueSelector = ExprValueSelector; // #endregion -// #region Condition expression - -export type Value = string | number | boolean; - -export type ComparisonOperator = ">" | "<" | ">=" | "<=" | "==" | "!=" | "is"; - -export enum LogicalOperator { - And = "and", - Or = "or", - Not = "not", -} - +// #region Expression export enum ExprType { - Const = "const", - Var = "var", - Compare = "compare", + Constant = "const", + Variant = "var", + Comparison = "comparison", Logical = "logical", Not = "not", } -export type ConstExpr = { type: ExprType.Const; value: string; valueType: WorkflowNodeIoValueType }; -export type VarExpr = { type: ExprType.Var; selector: WorkflowNodeIOValueSelector }; -export type CompareExpr = { type: ExprType.Compare; op: ComparisonOperator; left: Expr; right: Expr }; -export type LogicalExpr = { type: ExprType.Logical; op: LogicalOperator; left: Expr; right: Expr }; +export type ExprValue = string | number | boolean; +export type ExprValueType = "string" | "number" | "boolean"; +export type ExprValueSelector = { + id: string; + name: string; + type: ExprValueType; +}; + +export type ExprComparisonOperator = "gt" | "gte" | "lt" | "lte" | "eq" | "neq"; +export type ExprLogicalOperator = "and" | "or" | "not"; + +export type ConstantExpr = { type: ExprType.Constant; value: string; valueType: ExprValueType }; +export type VariantExpr = { type: ExprType.Variant; selector: ExprValueSelector }; +export type ComparisonExpr = { type: ExprType.Comparison; operator: ExprComparisonOperator; left: Expr; right: Expr }; +export type LogicalExpr = { type: ExprType.Logical; operator: ExprLogicalOperator; left: Expr; right: Expr }; export type NotExpr = { type: ExprType.Not; expr: Expr }; - -export type Expr = ConstExpr | VarExpr | CompareExpr | LogicalExpr | NotExpr; - -export const isConstExpr = (expr: Expr): expr is ConstExpr => { - return expr.type === ExprType.Const; -}; - -export const isVarExpr = (expr: Expr): expr is VarExpr => { - return expr.type === ExprType.Var; -}; - +export type Expr = ConstantExpr | VariantExpr | ComparisonExpr | LogicalExpr | NotExpr; // #endregion const isBranchLike = (node: WorkflowNode) => { @@ -352,8 +291,8 @@ export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions = {} switch (nodeType) { case WorkflowNodeType.Apply: case WorkflowNodeType.Upload: - case WorkflowNodeType.Deploy: case WorkflowNodeType.Monitor: + case WorkflowNodeType.Deploy: { node.inputs = workflowNodeTypeDefaultInputs.get(nodeType); node.outputs = workflowNodeTypeDefaultOutputs.get(nodeType); @@ -545,20 +484,24 @@ export const removeBranch = (node: WorkflowNode, branchNodeId: string, branchInd }); }; -const typeEqual = (a: WorkflowNodeIO, t: string) => { - if (t === "all") { - return true; - } - if (a.type === t) { - return true; - } - return false; -}; - -export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, type: string = "all"): WorkflowNode[] => { +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; @@ -567,10 +510,10 @@ export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, type: return true; } - if (current.type !== WorkflowNodeType.Branch && current.outputs && current.outputs.some((io) => typeEqual(io, type))) { + if (current.type !== WorkflowNodeType.Branch && current.outputs && current.outputs.some((io) => filter(io))) { output.push({ ...current, - outputs: current.outputs.filter((io) => typeEqual(io, type)), + outputs: current.outputs.filter((io) => filter(io)), }); } diff --git a/ui/src/domain/workflowExpr.ts b/ui/src/domain/workflowExpr.ts new file mode 100644 index 00000000..5f282702 --- /dev/null +++ b/ui/src/domain/workflowExpr.ts @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/i18n/locales/en/index.ts b/ui/src/i18n/locales/en/index.ts index f038efc7..4eaeced5 100644 --- a/ui/src/i18n/locales/en/index.ts +++ b/ui/src/i18n/locales/en/index.ts @@ -8,6 +8,7 @@ import nlsSettings from "./nls.settings.json"; import nlsWorkflow from "./nls.workflow.json"; import nlsWorkflowNodes from "./nls.workflow.nodes.json"; import nlsWorkflowRuns from "./nls.workflow.runs.json"; +import nlsWorkflowVars from "./nls.workflow.vars.json"; export default Object.freeze({ ...nlsCommon, @@ -16,8 +17,9 @@ export default Object.freeze({ ...nlsSettings, ...nlsProvider, ...nlsAccess, + ...nlsCertificate, ...nlsWorkflow, ...nlsWorkflowNodes, ...nlsWorkflowRuns, - ...nlsCertificate, + ...nlsWorkflowVars, }); diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index cdf722a0..b4f9d7e6 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -53,9 +53,5 @@ "workflow.detail.orchestration.action.run": "Run", "workflow.detail.orchestration.action.run.confirm": "You have unreleased changes. Do you really want to run this workflow based on the latest released version?", "workflow.detail.orchestration.action.run.prompt": "Running... Please check the history later", - "workflow.detail.runs.tab": "History runs", - - "workflow.variables.is_validated.label": "Is valid", - "workflow.variables.days_left.label": "Days left", - "workflow.variables.certificate.label": "Certificate" + "workflow.detail.runs.tab": "History runs" } diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 5b6c870c..626e9b68 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -871,31 +871,32 @@ "workflow_node.end.label": "End", "workflow_node.end.default_name": "End", - "workflow_node.branch.label": "Parallel branch", - "workflow_node.branch.default_name": "Parallel", + "workflow_node.branch.label": "Parallel/Conditional branch", + "workflow_node.branch.default_name": "Branch", "workflow_node.condition.label": "Branch", "workflow_node.condition.default_name": "Branch", - "workflow_node.condition.form.variable.placeholder": "Please select variable", - "workflow_node.condition.form.variable.errmsg": "Please select variable", - "workflow_node.condition.form.operator.errmsg": "Please select operator", - "workflow_node.condition.form.value.errmsg": "Please enter value", - "workflow_node.condition.form.value.string.placeholder": "Please enter value", - "workflow_node.condition.form.value.number.placeholder": "Please enter value", - "workflow_node.condition.form.value.boolean.placeholder": "Please select value", - "workflow_node.condition.form.value.boolean.true": "True", - "workflow_node.condition.form.value.boolean.false": "False", - "workflow_node.condition.form.add_condition.button": "Add condition", - "workflow_node.condition.form.logical_operator.label": "Logical operator", - "workflow_node.condition.form.logical_operator.and": "Meet all conditions (AND)", - "workflow_node.condition.form.logical_operator.or": "Meet any condition (OR)", - "workflow_node.condition.form.comparison.equal": "Equal", - "workflow_node.condition.form.comparison.not_equal": "Not equal", - "workflow_node.condition.form.comparison.greater_than": "Greater than", - "workflow_node.condition.form.comparison.greater_than_or_equal": "Greater than or equal", - "workflow_node.condition.form.comparison.less_than": "Less than", - "workflow_node.condition.form.comparison.less_than_or_equal": "Less than or equal", - "workflow_node.condition.form.comparison.is": "Is", + "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": "Execution result branch", diff --git a/ui/src/i18n/locales/en/nls.workflow.vars.json b/ui/src/i18n/locales/en/nls.workflow.vars.json new file mode 100644 index 00000000..a96d8ba5 --- /dev/null +++ b/ui/src/i18n/locales/en/nls.workflow.vars.json @@ -0,0 +1,6 @@ +{ + "workflow.variables.type.certificate.label": "Certificate", + + "workflow.variables.selector.validity.label": "Validity", + "workflow.variables.selector.days_left.label": "Days left" +} diff --git a/ui/src/i18n/locales/zh/index.ts b/ui/src/i18n/locales/zh/index.ts index f038efc7..4eaeced5 100644 --- a/ui/src/i18n/locales/zh/index.ts +++ b/ui/src/i18n/locales/zh/index.ts @@ -8,6 +8,7 @@ import nlsSettings from "./nls.settings.json"; import nlsWorkflow from "./nls.workflow.json"; import nlsWorkflowNodes from "./nls.workflow.nodes.json"; import nlsWorkflowRuns from "./nls.workflow.runs.json"; +import nlsWorkflowVars from "./nls.workflow.vars.json"; export default Object.freeze({ ...nlsCommon, @@ -16,8 +17,9 @@ export default Object.freeze({ ...nlsSettings, ...nlsProvider, ...nlsAccess, + ...nlsCertificate, ...nlsWorkflow, ...nlsWorkflowNodes, ...nlsWorkflowRuns, - ...nlsCertificate, + ...nlsWorkflowVars, }); diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index e86e796a..46cdc228 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -53,9 +53,5 @@ "workflow.detail.orchestration.action.run": "执行", "workflow.detail.orchestration.action.run.confirm": "你有尚未发布的更改。确定要以最近一次发布的版本继续执行吗?", "workflow.detail.orchestration.action.run.prompt": "执行中……请稍后查看执行历史", - "workflow.detail.runs.tab": "执行历史", - - "workflow.variables.is_validated.label": "是否有效", - "workflow.variables.days_left.label": "剩余天数", - "workflow.variables.certificate.label": "证书" + "workflow.detail.runs.tab": "执行历史" } diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 0d7ce68c..7710e386 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -3,7 +3,7 @@ "workflow_node.branch.add_node": "添加节点", "workflow_node.action.rename_node": "重命名", "workflow_node.action.remove_node": "删除节点", - "workflow_node.action.add_branch": "添加并行分支", + "workflow_node.action.add_branch": "添加分支", "workflow_node.action.rename_branch": "重命名", "workflow_node.action.remove_branch": "删除分支", @@ -707,7 +707,7 @@ "workflow_node.deploy.form.ucloud_us3_domain.label": "优刻得 US3 自定义域名", "workflow_node.deploy.form.ucloud_us3_domain.placeholder": "请输入优刻得 US3 自定义域名", "workflow_node.deploy.form.ucloud_us3_domain.tooltip": "这是什么?请参阅 https://console.ucloud.cn/ufile", - "workflow_node.deploy.form.unicloud_webhost.guide": "小贴士:由于 uniCloud 未提供相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇 uniCloud 接口变更,请到 GitHub 发起 Issue 告知。", + "workflow_node.deploy.form.unicloud_webhost.guide": "小贴士:由于 uniCloud 未公开相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇 uniCloud 接口变更,请到 GitHub 发起 Issue 告知。", "workflow_node.deploy.form.unicloud_webhost_space_provider.label": "uniCloud 服务空间提供商", "workflow_node.deploy.form.unicloud_webhost_space_provider.placeholder": "请选择 uniCloud 服务空间提供商", "workflow_node.deploy.form.unicloud_webhost_space_provider.option.aliyun.label": "阿里云", @@ -717,11 +717,11 @@ "workflow_node.deploy.form.unicloud_webhost_space_id.tooltip": "这是什么?请参阅 https://doc.dcloud.net.cn/uniCloud/concepts/space.html", "workflow_node.deploy.form.unicloud_webhost_domain.label": "uniCloud 前端网页托管网站域名", "workflow_node.deploy.form.unicloud_webhost_domain.placeholder": "请输入 uniCloud 前端网页托管网站域名", - "workflow_node.deploy.form.upyun_cdn.guide": "小贴士:由于又拍云未提供相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇又拍云接口变更,请到 GitHub 发起 Issue 告知。", + "workflow_node.deploy.form.upyun_cdn.guide": "小贴士:由于又拍云未公开相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇又拍云接口变更,请到 GitHub 发起 Issue 告知。", "workflow_node.deploy.form.upyun_cdn_domain.label": "又拍云 CDN 加速域名", "workflow_node.deploy.form.upyun_cdn_domain.placeholder": "请输入又拍云 CDN 加速域名(支持泛域名)", "workflow_node.deploy.form.upyun_cdn_domain.tooltip": "这是什么?请参阅 https://console.upyun.com/services/cdn/", - "workflow_node.deploy.form.upyun_file.guide": "小贴士:由于又拍云未提供相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇又拍云接口变更,请到 GitHub 发起 Issue 告知。", + "workflow_node.deploy.form.upyun_file.guide": "小贴士:由于又拍云未公开相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇又拍云接口变更,请到 GitHub 发起 Issue 告知。", "workflow_node.deploy.form.upyun_file_domain.label": "又拍云云存储加速域名", "workflow_node.deploy.form.upyun_file_domain.placeholder": "请输入又拍云云存储加速域名", "workflow_node.deploy.form.upyun_file_domain.tooltip": "这是什么?请参阅 https://console.upyun.com/services/file/", @@ -870,31 +870,32 @@ "workflow_node.end.label": "结束", "workflow_node.end.default_name": "结束", - "workflow_node.branch.label": "并行分支", - "workflow_node.branch.default_name": "并行", + "workflow_node.branch.label": "并行/条件分支", + "workflow_node.branch.default_name": "分支", "workflow_node.condition.label": "分支", "workflow_node.condition.default_name": "分支", - "workflow_node.condition.form.variable.placeholder": "选择变量", - "workflow_node.condition.form.variable.errmsg": "请选择变量", - "workflow_node.condition.form.operator.errmsg": "请选择操作符", - "workflow_node.condition.form.value.errmsg": "请输入值", - "workflow_node.condition.form.value.string.placeholder": "输入值", - "workflow_node.condition.form.value.number.placeholder": "输入数值", - "workflow_node.condition.form.value.boolean.placeholder": "选择值", - "workflow_node.condition.form.value.boolean.true": "是", - "workflow_node.condition.form.value.boolean.false": "否", - "workflow_node.condition.form.add_condition.button": "添加条件", - "workflow_node.condition.form.logical_operator.label": "条件逻辑", - "workflow_node.condition.form.logical_operator.and": "满足所有条件 (AND)", - "workflow_node.condition.form.logical_operator.or": "满足任一条件 (OR)", - "workflow_node.condition.form.comparison.equal": "等于", - "workflow_node.condition.form.comparison.not_equal": "不等于", - "workflow_node.condition.form.comparison.greater_than": "大于", - "workflow_node.condition.form.comparison.greater_than_or_equal": "大于等于", - "workflow_node.condition.form.comparison.less_than": "小于", - "workflow_node.condition.form.comparison.less_than_or_equal": "小于等于", - "workflow_node.condition.form.comparison.is": "为", + "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": "执行结果分支", diff --git a/ui/src/i18n/locales/zh/nls.workflow.vars.json b/ui/src/i18n/locales/zh/nls.workflow.vars.json new file mode 100644 index 00000000..eddfc585 --- /dev/null +++ b/ui/src/i18n/locales/zh/nls.workflow.vars.json @@ -0,0 +1,6 @@ +{ + "workflow.variables.type.certificate.label": "证书", + + "workflow.variables.selector.validity.label": "有效性", + "workflow.variables.selector.days_left.label": "剩余天数" +} diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx index 832269d0..91e8d746 100644 --- a/ui/src/pages/workflows/WorkflowDetail.tsx +++ b/ui/src/pages/workflows/WorkflowDetail.tsx @@ -265,7 +265,7 @@ const WorkflowDetail = () => { body: { position: "relative", height: "100%", - padding: 0, + padding: initialized ? 0 : undefined, }, }} loading={!initialized} diff --git a/ui/src/stores/workflow/index.ts b/ui/src/stores/workflow/index.ts index d20fec16..67bc25f9 100644 --- a/ui/src/stores/workflow/index.ts +++ b/ui/src/stores/workflow/index.ts @@ -32,7 +32,7 @@ export type WorkflowState = { addBranch: (branchId: string) => void; removeBranch: (branchId: string, index: number) => void; - getWorkflowOuptutBeforeId: (nodeId: string, type?: string) => WorkflowNode[]; + getWorkflowOuptutBeforeId: (nodeId: string, typeFilter?: string | string[]) => WorkflowNode[]; }; export const useWorkflowStore = create((set, get) => ({ @@ -243,7 +243,7 @@ export const useWorkflowStore = create((set, get) => ({ }); }, - getWorkflowOuptutBeforeId: (nodeId: string, type: string = "all") => { - return getOutputBeforeNodeId(get().workflow.draft as WorkflowNode, nodeId, type); + getWorkflowOuptutBeforeId: (nodeId: string, typeFilter?: string | string[]) => { + return getOutputBeforeNodeId(get().workflow.draft as WorkflowNode, nodeId, typeFilter); }, }));