mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-07 21:19:51 +00:00
Merge pull request #712 from usual2970/feat/condition
workflow conditional branch & monitoring node
This commit is contained in:
commit
722c3a0e83
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
做个人产品或者在中小企业里负责运维的同学,会遇到要管理多个域名的情况,需要给域名申请证书。但是手动申请证书有以下缺点:
|
做个人产品或者在中小企业里负责运维的同学,会遇到要管理多个域名的情况,需要给域名申请证书。但是手动申请证书有以下缺点:
|
||||||
|
|
||||||
- 😱 麻烦:申请证书并部署到服务的流程虽不复杂,但也挺麻烦的,犹其是你有多个域名需要维护的时候。
|
- 😱 麻烦:申请证书并部署到服务的流程虽不复杂,但也挺麻烦的,尤其是你有多个域名需要维护的时候。
|
||||||
- 😭 易忘:另外当前免费证书的有效期只有 90 天,这就要求你定期的操作,增加了工作量的同时,你也很容易忘掉续期,从而导致网站访问不了。
|
- 😭 易忘:另外当前免费证书的有效期只有 90 天,这就要求你定期的操作,增加了工作量的同时,你也很容易忘掉续期,从而导致网站访问不了。
|
||||||
|
|
||||||
Certimate 就是为了解决上述问题而产生的,它具有以下优势:
|
Certimate 就是为了解决上述问题而产生的,它具有以下优势:
|
||||||
|
@ -53,35 +53,35 @@ func NewWithWorkflowNode(config ApplicantWithWorkflowNodeConfig) (Applicant, err
|
|||||||
return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeApply))
|
return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeApply))
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeConfig := config.Node.GetConfigForApply()
|
nodeCfg := config.Node.GetConfigForApply()
|
||||||
options := &applicantProviderOptions{
|
options := &applicantProviderOptions{
|
||||||
Domains: sliceutil.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }),
|
Domains: sliceutil.Filter(strings.Split(nodeCfg.Domains, ";"), func(s string) bool { return s != "" }),
|
||||||
ContactEmail: nodeConfig.ContactEmail,
|
ContactEmail: nodeCfg.ContactEmail,
|
||||||
Provider: domain.ACMEDns01ProviderType(nodeConfig.Provider),
|
Provider: domain.ACMEDns01ProviderType(nodeCfg.Provider),
|
||||||
ProviderAccessConfig: make(map[string]any),
|
ProviderAccessConfig: make(map[string]any),
|
||||||
ProviderServiceConfig: nodeConfig.ProviderConfig,
|
ProviderServiceConfig: nodeCfg.ProviderConfig,
|
||||||
CAProvider: domain.CAProviderType(nodeConfig.CAProvider),
|
CAProvider: domain.CAProviderType(nodeCfg.CAProvider),
|
||||||
CAProviderAccessConfig: make(map[string]any),
|
CAProviderAccessConfig: make(map[string]any),
|
||||||
CAProviderServiceConfig: nodeConfig.CAProviderConfig,
|
CAProviderServiceConfig: nodeCfg.CAProviderConfig,
|
||||||
KeyAlgorithm: nodeConfig.KeyAlgorithm,
|
KeyAlgorithm: nodeCfg.KeyAlgorithm,
|
||||||
Nameservers: sliceutil.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }),
|
Nameservers: sliceutil.Filter(strings.Split(nodeCfg.Nameservers, ";"), func(s string) bool { return s != "" }),
|
||||||
DnsPropagationWait: nodeConfig.DnsPropagationWait,
|
DnsPropagationWait: nodeCfg.DnsPropagationWait,
|
||||||
DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout,
|
DnsPropagationTimeout: nodeCfg.DnsPropagationTimeout,
|
||||||
DnsTTL: nodeConfig.DnsTTL,
|
DnsTTL: nodeCfg.DnsTTL,
|
||||||
DisableFollowCNAME: nodeConfig.DisableFollowCNAME,
|
DisableFollowCNAME: nodeCfg.DisableFollowCNAME,
|
||||||
}
|
}
|
||||||
|
|
||||||
accessRepo := repository.NewAccessRepository()
|
accessRepo := repository.NewAccessRepository()
|
||||||
if nodeConfig.ProviderAccessId != "" {
|
if nodeCfg.ProviderAccessId != "" {
|
||||||
if access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId); err != nil {
|
if access, err := accessRepo.GetById(context.Background(), nodeCfg.ProviderAccessId); 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 {
|
} else {
|
||||||
options.ProviderAccessConfig = access.Config
|
options.ProviderAccessConfig = access.Config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if nodeConfig.CAProviderAccessId != "" {
|
if nodeCfg.CAProviderAccessId != "" {
|
||||||
if access, err := accessRepo.GetById(context.Background(), nodeConfig.CAProviderAccessId); err != nil {
|
if access, err := accessRepo.GetById(context.Background(), nodeCfg.CAProviderAccessId); err != nil {
|
||||||
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.CAProviderAccessId, err)
|
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.CAProviderAccessId, err)
|
||||||
} else {
|
} else {
|
||||||
options.CAProviderAccessId = access.Id
|
options.CAProviderAccessId = access.Id
|
||||||
options.CAProviderAccessConfig = access.Config
|
options.CAProviderAccessConfig = access.Config
|
||||||
|
@ -29,18 +29,18 @@ func NewWithWorkflowNode(config DeployerWithWorkflowNodeConfig) (Deployer, error
|
|||||||
return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeDeploy))
|
return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeDeploy))
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeConfig := config.Node.GetConfigForDeploy()
|
nodeCfg := config.Node.GetConfigForDeploy()
|
||||||
options := &deployerProviderOptions{
|
options := &deployerProviderOptions{
|
||||||
Provider: domain.DeploymentProviderType(nodeConfig.Provider),
|
Provider: domain.DeploymentProviderType(nodeCfg.Provider),
|
||||||
ProviderAccessConfig: make(map[string]any),
|
ProviderAccessConfig: make(map[string]any),
|
||||||
ProviderServiceConfig: nodeConfig.ProviderConfig,
|
ProviderServiceConfig: nodeCfg.ProviderConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
accessRepo := repository.NewAccessRepository()
|
accessRepo := repository.NewAccessRepository()
|
||||||
if nodeConfig.ProviderAccessId != "" {
|
if nodeCfg.ProviderAccessId != "" {
|
||||||
access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId)
|
access, err := accessRepo.GetById(context.Background(), nodeCfg.ProviderAccessId)
|
||||||
if err != nil {
|
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 {
|
} else {
|
||||||
options.ProviderAccessConfig = access.Config
|
options.ProviderAccessConfig = access.Config
|
||||||
}
|
}
|
||||||
|
630
internal/domain/expr/expr.go
Normal file
630
internal/domain/expr/expr.go
Normal file
@ -0,0 +1,630 @@
|
|||||||
|
package expr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
ExprType string
|
||||||
|
ExprComparisonOperator string
|
||||||
|
ExprLogicalOperator string
|
||||||
|
ExprValueType string
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GreaterThan ExprComparisonOperator = "gt"
|
||||||
|
GreaterOrEqual ExprComparisonOperator = "gte"
|
||||||
|
LessThan ExprComparisonOperator = "lt"
|
||||||
|
LessOrEqual ExprComparisonOperator = "lte"
|
||||||
|
Equal ExprComparisonOperator = "eq"
|
||||||
|
NotEqual ExprComparisonOperator = "neq"
|
||||||
|
|
||||||
|
And ExprLogicalOperator = "and"
|
||||||
|
Or ExprLogicalOperator = "or"
|
||||||
|
Not ExprLogicalOperator = "not"
|
||||||
|
|
||||||
|
Number ExprValueType = "number"
|
||||||
|
String ExprValueType = "string"
|
||||||
|
Boolean ExprValueType = "boolean"
|
||||||
|
|
||||||
|
ConstantExprType ExprType = "const"
|
||||||
|
VariantExprType ExprType = "var"
|
||||||
|
ComparisonExprType ExprType = "comparison"
|
||||||
|
LogicalExprType ExprType = "logical"
|
||||||
|
NotExprType ExprType = "not"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EvalResult struct {
|
||||||
|
Type ExprValueType
|
||||||
|
Value any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EvalResult) GetFloat64() (float64, error) {
|
||||||
|
if e.Type != Number {
|
||||||
|
return 0, fmt.Errorf("type mismatch: %s", e.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
stringValue, ok := e.Value.(string)
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("value is not a string: %v", e.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
floatValue, err := strconv.ParseFloat(stringValue, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to parse float64: %v", err)
|
||||||
|
}
|
||||||
|
return floatValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EvalResult) GetBool() (bool, error) {
|
||||||
|
if e.Type != Boolean {
|
||||||
|
return false, fmt.Errorf("type mismatch: %s", e.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
strValue, ok := e.Value.(string)
|
||||||
|
if ok {
|
||||||
|
if strValue == "true" {
|
||||||
|
return true, nil
|
||||||
|
} else if strValue == "false" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("value is not a boolean: %v", e.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
boolValue, ok := e.Value.(bool)
|
||||||
|
if !ok {
|
||||||
|
return false, fmt.Errorf("value is not a boolean: %v", e.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return boolValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 value type: %s", e.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 value type: %s", e.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 value type: %s", e.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 value type: %s", e.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 value type: %s", e.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 value type: %s", e.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 value type: %s", e.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 value type: %s", e.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
type Expr interface {
|
||||||
|
GetType() ExprType
|
||||||
|
Eval(variables map[string]map[string]any) (*EvalResult, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExprValueSelector struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type ExprValueType `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConstantExpr struct {
|
||||||
|
Type ExprType `json:"type"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
ValueType ExprValueType `json:"valueType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 VariantExpr struct {
|
||||||
|
Type ExprType `json:"type"`
|
||||||
|
Selector ExprValueSelector `json:"selector"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v VariantExpr) GetType() ExprType { return v.Type }
|
||||||
|
|
||||||
|
func (v VariantExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||||
|
if v.Selector.Id == "" {
|
||||||
|
return nil, fmt.Errorf("node id is empty")
|
||||||
|
}
|
||||||
|
if v.Selector.Name == "" {
|
||||||
|
return nil, fmt.Errorf("name is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := variables[v.Selector.Id]; !ok {
|
||||||
|
return nil, fmt.Errorf("node %s not found", v.Selector.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := variables[v.Selector.Id][v.Selector.Name]; !ok {
|
||||||
|
return nil, fmt.Errorf("variable %s not found in node %s", v.Selector.Name, v.Selector.Id)
|
||||||
|
}
|
||||||
|
return &EvalResult{
|
||||||
|
Type: v.Selector.Type,
|
||||||
|
Value: variables[v.Selector.Id][v.Selector.Name],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComparisonExpr struct {
|
||||||
|
Type ExprType `json:"type"` // compare
|
||||||
|
Operator ExprComparisonOperator `json:"operator"`
|
||||||
|
Left Expr `json:"left"`
|
||||||
|
Right Expr `json:"right"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ComparisonExpr) GetType() ExprType { return c.Type }
|
||||||
|
|
||||||
|
func (c ComparisonExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||||
|
left, err := c.Left.Eval(variables)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
right, err := c.Right.Eval(variables)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c.Operator {
|
||||||
|
case GreaterThan:
|
||||||
|
return left.GreaterThan(right)
|
||||||
|
case LessThan:
|
||||||
|
return left.LessThan(right)
|
||||||
|
case GreaterOrEqual:
|
||||||
|
return left.GreaterOrEqual(right)
|
||||||
|
case LessOrEqual:
|
||||||
|
return left.LessOrEqual(right)
|
||||||
|
case Equal:
|
||||||
|
return left.Equal(right)
|
||||||
|
case NotEqual:
|
||||||
|
return left.NotEqual(right)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown expression operator: %s", c.Operator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogicalExpr struct {
|
||||||
|
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 }
|
||||||
|
|
||||||
|
func (l LogicalExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||||
|
left, err := l.Left.Eval(variables)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
right, err := l.Right.Eval(variables)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch l.Operator {
|
||||||
|
case And:
|
||||||
|
return left.And(right)
|
||||||
|
case Or:
|
||||||
|
return left.Or(right)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown expression operator: %s", l.Operator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotExpr struct {
|
||||||
|
Type ExprType `json:"type"` // not
|
||||||
|
Expr Expr `json:"expr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n NotExpr) GetType() ExprType { return n.Type }
|
||||||
|
|
||||||
|
func (n NotExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||||
|
inner, err := n.Expr.Eval(variables)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return inner.Not()
|
||||||
|
}
|
||||||
|
|
||||||
|
type rawExpr struct {
|
||||||
|
Type ExprType `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func MarshalExpr(e Expr) ([]byte, error) {
|
||||||
|
return json.Marshal(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnmarshalExpr(data []byte) (Expr, error) {
|
||||||
|
var typ rawExpr
|
||||||
|
if err := json.Unmarshal(data, &typ); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch typ.Type {
|
||||||
|
case ConstantExprType:
|
||||||
|
var e ConstantExpr
|
||||||
|
if err := json.Unmarshal(data, &e); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return e, nil
|
||||||
|
case VariantExprType:
|
||||||
|
var e VariantExpr
|
||||||
|
if err := json.Unmarshal(data, &e); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return e, nil
|
||||||
|
case ComparisonExprType:
|
||||||
|
var e ComparisonExprRaw
|
||||||
|
if err := json.Unmarshal(data, &e); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return e.ToComparisonExpr()
|
||||||
|
case LogicalExprType:
|
||||||
|
var e LogicalExprRaw
|
||||||
|
if err := json.Unmarshal(data, &e); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return e.ToLogicalExpr()
|
||||||
|
case NotExprType:
|
||||||
|
var e NotExprRaw
|
||||||
|
if err := json.Unmarshal(data, &e); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return e.ToNotExpr()
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown expression type: %s", typ.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComparisonExprRaw struct {
|
||||||
|
Type ExprType `json:"type"`
|
||||||
|
Operator ExprComparisonOperator `json:"operator"`
|
||||||
|
Left json.RawMessage `json:"left"`
|
||||||
|
Right json.RawMessage `json:"right"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ComparisonExprRaw) ToComparisonExpr() (ComparisonExpr, error) {
|
||||||
|
leftExpr, err := UnmarshalExpr(r.Left)
|
||||||
|
if err != nil {
|
||||||
|
return ComparisonExpr{}, err
|
||||||
|
}
|
||||||
|
rightExpr, err := UnmarshalExpr(r.Right)
|
||||||
|
if err != nil {
|
||||||
|
return ComparisonExpr{}, err
|
||||||
|
}
|
||||||
|
return ComparisonExpr{
|
||||||
|
Type: r.Type,
|
||||||
|
Operator: r.Operator,
|
||||||
|
Left: leftExpr,
|
||||||
|
Right: rightExpr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogicalExprRaw struct {
|
||||||
|
Type ExprType `json:"type"`
|
||||||
|
Operator ExprLogicalOperator `json:"operator"`
|
||||||
|
Left json.RawMessage `json:"left"`
|
||||||
|
Right json.RawMessage `json:"right"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r LogicalExprRaw) ToLogicalExpr() (LogicalExpr, error) {
|
||||||
|
left, err := UnmarshalExpr(r.Left)
|
||||||
|
if err != nil {
|
||||||
|
return LogicalExpr{}, err
|
||||||
|
}
|
||||||
|
right, err := UnmarshalExpr(r.Right)
|
||||||
|
if err != nil {
|
||||||
|
return LogicalExpr{}, err
|
||||||
|
}
|
||||||
|
return LogicalExpr{
|
||||||
|
Type: r.Type,
|
||||||
|
Operator: r.Operator,
|
||||||
|
Left: left,
|
||||||
|
Right: right,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotExprRaw struct {
|
||||||
|
Type ExprType `json:"type"`
|
||||||
|
Expr json.RawMessage `json:"expr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r NotExprRaw) ToNotExpr() (NotExpr, error) {
|
||||||
|
inner, err := UnmarshalExpr(r.Expr)
|
||||||
|
if err != nil {
|
||||||
|
return NotExpr{}, err
|
||||||
|
}
|
||||||
|
return NotExpr{
|
||||||
|
Type: r.Type,
|
||||||
|
Expr: inner,
|
||||||
|
}, nil
|
||||||
|
}
|
127
internal/domain/expr/expr_test.go
Normal file
127
internal/domain/expr/expr_test.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package expr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLogicalEval(t *testing.T) {
|
||||||
|
// 测试逻辑表达式 and
|
||||||
|
logicalExpr := LogicalExpr{
|
||||||
|
Left: ConstantExpr{
|
||||||
|
Type: "const",
|
||||||
|
Value: "true",
|
||||||
|
ValueType: "boolean",
|
||||||
|
},
|
||||||
|
Operator: And,
|
||||||
|
Right: ConstantExpr{
|
||||||
|
Type: "const",
|
||||||
|
Value: "true",
|
||||||
|
ValueType: "boolean",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result, err := logicalExpr.Eval(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to evaluate logical expression: %v", err)
|
||||||
|
}
|
||||||
|
if result.Value != true {
|
||||||
|
t.Errorf("expected true, got %v", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试逻辑表达式 or
|
||||||
|
orExpr := LogicalExpr{
|
||||||
|
Left: ConstantExpr{
|
||||||
|
Type: "const",
|
||||||
|
Value: "true",
|
||||||
|
ValueType: "boolean",
|
||||||
|
},
|
||||||
|
Operator: Or,
|
||||||
|
Right: ConstantExpr{
|
||||||
|
Type: "const",
|
||||||
|
Value: "true",
|
||||||
|
ValueType: "boolean",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result, err = orExpr.Eval(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to evaluate logical expression: %v", err)
|
||||||
|
}
|
||||||
|
if result.Value != true {
|
||||||
|
t.Errorf("expected true, got %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalExpr(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want Expr
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test1",
|
||||||
|
args: args{
|
||||||
|
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"}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := UnmarshalExpr(tt.args.data)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("UnmarshalExpr() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got == nil {
|
||||||
|
t.Errorf("UnmarshalExpr() got = nil, want %v", tt.want)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpr_Eval(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
variables map[string]map[string]any
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *EvalResult
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test1",
|
||||||
|
args: args{
|
||||||
|
variables: map[string]map[string]any{
|
||||||
|
"ODnYSOXB6HQP2_vz6JcZE": {
|
||||||
|
"certificate.validity": true,
|
||||||
|
"certificate.daysLeft": 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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"}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c, err := UnmarshalExpr(tt.args.data)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("UnmarshalExpr() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
got, err := c.Eval(tt.args.variables)
|
||||||
|
t.Log("got:", got)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ConstExpr.Eval() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got.Value != true {
|
||||||
|
t.Errorf("ConstExpr.Eval() got = %v, want %v", got.Value, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,10 @@
|
|||||||
package domain
|
package domain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/usual2970/certimate/internal/domain/expr"
|
||||||
maputil "github.com/usual2970/certimate/internal/pkg/utils/map"
|
maputil "github.com/usual2970/certimate/internal/pkg/utils/map"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -30,6 +32,7 @@ const (
|
|||||||
WorkflowNodeTypeEnd = WorkflowNodeType("end")
|
WorkflowNodeTypeEnd = WorkflowNodeType("end")
|
||||||
WorkflowNodeTypeApply = WorkflowNodeType("apply")
|
WorkflowNodeTypeApply = WorkflowNodeType("apply")
|
||||||
WorkflowNodeTypeUpload = WorkflowNodeType("upload")
|
WorkflowNodeTypeUpload = WorkflowNodeType("upload")
|
||||||
|
WorkflowNodeTypeMonitor = WorkflowNodeType("monitor")
|
||||||
WorkflowNodeTypeDeploy = WorkflowNodeType("deploy")
|
WorkflowNodeTypeDeploy = WorkflowNodeType("deploy")
|
||||||
WorkflowNodeTypeNotify = WorkflowNodeType("notify")
|
WorkflowNodeTypeNotify = WorkflowNodeType("notify")
|
||||||
WorkflowNodeTypeBranch = WorkflowNodeType("branch")
|
WorkflowNodeTypeBranch = WorkflowNodeType("branch")
|
||||||
@ -82,9 +85,16 @@ type WorkflowNodeConfigForApply struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type WorkflowNodeConfigForUpload struct {
|
type WorkflowNodeConfigForUpload struct {
|
||||||
Certificate string `json:"certificate"`
|
Certificate string `json:"certificate"` // 证书 PEM 内容
|
||||||
PrivateKey string `json:"privateKey"`
|
PrivateKey string `json:"privateKey"` // 私钥 PEM 内容
|
||||||
Domains string `json:"domains"`
|
Domains string `json:"domains,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkflowNodeConfigForMonitor struct {
|
||||||
|
Host string `json:"host"` // 主机地址
|
||||||
|
Port int32 `json:"port,omitempty"` // 端口(零值时默认值 443)
|
||||||
|
Domain string `json:"domain,omitempty"` // 域名(零值时默认值 [Host])
|
||||||
|
RequestPath string `json:"requestPath,omitempty"` // 请求路径
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkflowNodeConfigForDeploy struct {
|
type WorkflowNodeConfigForDeploy struct {
|
||||||
@ -104,6 +114,10 @@ type WorkflowNodeConfigForNotify struct {
|
|||||||
Message string `json:"message"` // 通知内容
|
Message string `json:"message"` // 通知内容
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WorkflowNodeConfigForCondition struct {
|
||||||
|
Expression expr.Expr `json:"expression"` // 条件表达式
|
||||||
|
}
|
||||||
|
|
||||||
func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply {
|
func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply {
|
||||||
return WorkflowNodeConfigForApply{
|
return WorkflowNodeConfigForApply{
|
||||||
Domains: maputil.GetString(n.Config, "domains"),
|
Domains: maputil.GetString(n.Config, "domains"),
|
||||||
@ -133,6 +147,16 @@ func (n *WorkflowNode) GetConfigForUpload() WorkflowNodeConfigForUpload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *WorkflowNode) GetConfigForMonitor() WorkflowNodeConfigForMonitor {
|
||||||
|
host := maputil.GetString(n.Config, "host")
|
||||||
|
return WorkflowNodeConfigForMonitor{
|
||||||
|
Host: host,
|
||||||
|
Port: maputil.GetOrDefaultInt32(n.Config, "port", 443),
|
||||||
|
Domain: maputil.GetOrDefaultString(n.Config, "domain", host),
|
||||||
|
RequestPath: maputil.GetString(n.Config, "path"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (n *WorkflowNode) GetConfigForDeploy() WorkflowNodeConfigForDeploy {
|
func (n *WorkflowNode) GetConfigForDeploy() WorkflowNodeConfigForDeploy {
|
||||||
return WorkflowNodeConfigForDeploy{
|
return WorkflowNodeConfigForDeploy{
|
||||||
Certificate: maputil.GetString(n.Config, "certificate"),
|
Certificate: maputil.GetString(n.Config, "certificate"),
|
||||||
@ -154,6 +178,23 @@ func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition {
|
||||||
|
expression := n.Config["expression"]
|
||||||
|
if expression == nil {
|
||||||
|
return WorkflowNodeConfigForCondition{}
|
||||||
|
}
|
||||||
|
|
||||||
|
exprRaw, _ := json.Marshal(expression)
|
||||||
|
expr, err := expr.UnmarshalExpr([]byte(exprRaw))
|
||||||
|
if err != nil {
|
||||||
|
return WorkflowNodeConfigForCondition{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return WorkflowNodeConfigForCondition{
|
||||||
|
Expression: expr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type WorkflowNodeIO struct {
|
type WorkflowNodeIO struct {
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -163,9 +204,6 @@ type WorkflowNodeIO struct {
|
|||||||
ValueSelector WorkflowNodeIOValueSelector `json:"valueSelector"`
|
ValueSelector WorkflowNodeIOValueSelector `json:"valueSelector"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkflowNodeIOValueSelector struct {
|
type WorkflowNodeIOValueSelector = expr.ExprValueSelector
|
||||||
Id string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const WorkflowNodeIONameCertificate string = "certificate"
|
const WorkflowNodeIONameCertificate string = "certificate"
|
||||||
|
@ -29,18 +29,18 @@ func NewWithWorkflowNode(config NotifierWithWorkflowNodeConfig) (Notifier, error
|
|||||||
return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeNotify))
|
return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeNotify))
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeConfig := config.Node.GetConfigForNotify()
|
nodeCfg := config.Node.GetConfigForNotify()
|
||||||
options := ¬ifierProviderOptions{
|
options := ¬ifierProviderOptions{
|
||||||
Provider: domain.NotificationProviderType(nodeConfig.Provider),
|
Provider: domain.NotificationProviderType(nodeCfg.Provider),
|
||||||
ProviderAccessConfig: make(map[string]any),
|
ProviderAccessConfig: make(map[string]any),
|
||||||
ProviderServiceConfig: nodeConfig.ProviderConfig,
|
ProviderServiceConfig: nodeCfg.ProviderConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
accessRepo := repository.NewAccessRepository()
|
accessRepo := repository.NewAccessRepository()
|
||||||
if nodeConfig.ProviderAccessId != "" {
|
if nodeCfg.ProviderAccessId != "" {
|
||||||
access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId)
|
access, err := accessRepo.GetById(context.Background(), nodeCfg.ProviderAccessId)
|
||||||
if err != nil {
|
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 {
|
} else {
|
||||||
options.ProviderAccessConfig = access.Config
|
options.ProviderAccessConfig = access.Config
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider,
|
|||||||
providerConfig.APIKey = config.ApiKey
|
providerConfig.APIKey = config.ApiKey
|
||||||
if config.AllowInsecureConnections {
|
if config.AllowInsecureConnections {
|
||||||
providerConfig.HTTPClient.Transport = &http.Transport{
|
providerConfig.HTTPClient.Transport = &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
},
|
},
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/luthermonson/go-proxmox"
|
"github.com/luthermonson/go-proxmox"
|
||||||
|
|
||||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||||
|
httputil "github.com/usual2970/certimate/internal/pkg/utils/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DeployerConfig struct {
|
type DeployerConfig struct {
|
||||||
@ -101,15 +102,16 @@ func createSdkClient(serverUrl, apiToken, apiTokenSecret string, skipTlsVerify b
|
|||||||
}
|
}
|
||||||
|
|
||||||
httpClient := &http.Client{
|
httpClient := &http.Client{
|
||||||
Transport: http.DefaultTransport,
|
Transport: httputil.NewDefaultTransport(),
|
||||||
Timeout: http.DefaultClient.Timeout,
|
Timeout: http.DefaultClient.Timeout,
|
||||||
}
|
}
|
||||||
if skipTlsVerify {
|
if skipTlsVerify {
|
||||||
httpClient.Transport = &http.Transport{
|
transport := httputil.NewDefaultTransport()
|
||||||
TLSClientConfig: &tls.Config{
|
if transport.TLSClientConfig == nil {
|
||||||
InsecureSkipVerify: true,
|
transport.TLSClientConfig = &tls.Config{}
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
transport.TLSClientConfig.InsecureSkipVerify = true
|
||||||
|
httpClient.Transport = transport
|
||||||
}
|
}
|
||||||
client := proxmox.NewClient(
|
client := proxmox.NewClient(
|
||||||
strings.TrimRight(serverUrl, "/")+"/api2/json",
|
strings.TrimRight(serverUrl, "/")+"/api2/json",
|
||||||
|
@ -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()
|
listCertificatesResp, err := u.sdkClient.ListCertificates()
|
||||||
u.logger.Debug("sdk request 'certificatemanagement.ListCertificates'", slog.Any("response", listCertificatesResp))
|
u.logger.Debug("sdk request 'certificatemanagement.ListCertificates'", slog.Any("response", listCertificatesResp))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
33
internal/pkg/utils/http/transport.go
Normal file
33
internal/pkg/utils/http/transport.go
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
@ -98,16 +98,26 @@ func (w *workflowInvoker) processNode(ctx context.Context, node *domain.Workflow
|
|||||||
|
|
||||||
procErr = processor.Process(ctx)
|
procErr = processor.Process(ctx)
|
||||||
if procErr != nil {
|
if procErr != nil {
|
||||||
processor.GetLogger().Error(procErr.Error())
|
if current.Type != domain.WorkflowNodeTypeCondition {
|
||||||
|
processor.GetLogger().Error(procErr.Error())
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nodeOutputs := processor.GetOutputs()
|
||||||
|
if len(nodeOutputs) > 0 {
|
||||||
|
ctx = nodes.AddNodeOutput(ctx, current.Id, nodeOutputs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 优化可读性
|
// TODO: 优化可读性
|
||||||
if procErr != nil && current.Next != nil && current.Next.Type != domain.WorkflowNodeTypeExecuteResultBranch {
|
if procErr != nil && current.Type == domain.WorkflowNodeTypeCondition {
|
||||||
|
current = nil
|
||||||
|
procErr = nil
|
||||||
|
return nil
|
||||||
|
} else if procErr != nil && current.Next != nil && current.Next.Type != domain.WorkflowNodeTypeExecuteResultBranch {
|
||||||
return procErr
|
return procErr
|
||||||
} else if procErr != nil && current.Next != nil && current.Next.Type == domain.WorkflowNodeTypeExecuteResultBranch {
|
} else if procErr != nil && current.Next != nil && current.Next.Type == domain.WorkflowNodeTypeExecuteResultBranch {
|
||||||
current = w.getBranchByType(current.Next.Branches, domain.WorkflowNodeTypeExecuteFailure)
|
current = w.getBranchByType(current.Next.Branches, domain.WorkflowNodeTypeExecuteFailure)
|
||||||
|
@ -3,6 +3,7 @@ package nodeprocessor
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
@ -16,6 +17,7 @@ import (
|
|||||||
type applyNode struct {
|
type applyNode struct {
|
||||||
node *domain.WorkflowNode
|
node *domain.WorkflowNode
|
||||||
*nodeProcessor
|
*nodeProcessor
|
||||||
|
*nodeOutputer
|
||||||
|
|
||||||
certRepo certificateRepository
|
certRepo certificateRepository
|
||||||
outputRepo workflowOutputRepository
|
outputRepo workflowOutputRepository
|
||||||
@ -25,6 +27,7 @@ func NewApplyNode(node *domain.WorkflowNode) *applyNode {
|
|||||||
return &applyNode{
|
return &applyNode{
|
||||||
node: node,
|
node: node,
|
||||||
nodeProcessor: newNodeProcessor(node),
|
nodeProcessor: newNodeProcessor(node),
|
||||||
|
nodeOutputer: newNodeOutputer(),
|
||||||
|
|
||||||
certRepo: repository.NewCertificateRepository(),
|
certRepo: repository.NewCertificateRepository(),
|
||||||
outputRepo: repository.NewWorkflowOutputRepository(),
|
outputRepo: repository.NewWorkflowOutputRepository(),
|
||||||
@ -32,7 +35,7 @@ func NewApplyNode(node *domain.WorkflowNode) *applyNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *applyNode) Process(ctx context.Context) error {
|
func (n *applyNode) Process(ctx context.Context) error {
|
||||||
n.logger.Info("ready to apply ...")
|
n.logger.Info("ready to obtain certificiate ...")
|
||||||
|
|
||||||
// 查询上次执行结果
|
// 查询上次执行结果
|
||||||
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
|
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
|
||||||
@ -61,7 +64,7 @@ func (n *applyNode) Process(ctx context.Context) error {
|
|||||||
// 申请证书
|
// 申请证书
|
||||||
applyResult, err := applicant.Apply(ctx)
|
applyResult, err := applicant.Apply(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
n.logger.Warn("failed to apply")
|
n.logger.Warn("failed to obtain certificiate")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,6 +74,7 @@ func (n *applyNode) Process(ctx context.Context) error {
|
|||||||
n.logger.Warn("failed to parse certificate, may be the CA responded error")
|
n.logger.Warn("failed to parse certificate, may be the CA responded error")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
certificate := &domain.Certificate{
|
certificate := &domain.Certificate{
|
||||||
Source: domain.CertificateSourceTypeWorkflow,
|
Source: domain.CertificateSourceTypeWorkflow,
|
||||||
Certificate: applyResult.FullChainCertificate,
|
Certificate: applyResult.FullChainCertificate,
|
||||||
@ -105,12 +109,15 @@ func (n *applyNode) Process(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
n.logger.Info("apply completed")
|
// 记录中间结果
|
||||||
|
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
|
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 {
|
if lastOutput != nil && lastOutput.Succeeded {
|
||||||
// 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致
|
// 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致
|
||||||
currentNodeConfig := n.node.GetConfigForApply()
|
currentNodeConfig := n.node.GetConfigForApply()
|
||||||
@ -148,7 +155,12 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo
|
|||||||
renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24
|
renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24
|
||||||
expirationTime := time.Until(lastCertificate.ExpireAt)
|
expirationTime := time.Until(lastCertificate.ExpireAt)
|
||||||
if expirationTime > renewalInterval {
|
if expirationTime > renewalInterval {
|
||||||
return true, fmt.Sprintf("the certificate has already been issued (expires in %dd, next renewal in %dd)", 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,23 +2,51 @@ package nodeprocessor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/usual2970/certimate/internal/domain"
|
"github.com/usual2970/certimate/internal/domain"
|
||||||
|
"github.com/usual2970/certimate/internal/domain/expr"
|
||||||
)
|
)
|
||||||
|
|
||||||
type conditionNode struct {
|
type conditionNode struct {
|
||||||
node *domain.WorkflowNode
|
node *domain.WorkflowNode
|
||||||
*nodeProcessor
|
*nodeProcessor
|
||||||
|
*nodeOutputer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConditionNode(node *domain.WorkflowNode) *conditionNode {
|
func NewConditionNode(node *domain.WorkflowNode) *conditionNode {
|
||||||
return &conditionNode{
|
return &conditionNode{
|
||||||
node: node,
|
node: node,
|
||||||
nodeProcessor: newNodeProcessor(node),
|
nodeProcessor: newNodeProcessor(node),
|
||||||
|
nodeOutputer: newNodeOutputer(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *conditionNode) Process(ctx context.Context) error {
|
func (n *conditionNode) Process(ctx context.Context) error {
|
||||||
// 此类型节点不需要执行任何操作,直接返回
|
nodeCfg := n.node.GetConfigForCondition()
|
||||||
|
if nodeCfg.Expression == nil {
|
||||||
|
n.logger.Info("without any conditions, enter this branch")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rs, err := n.evalExpr(ctx, nodeCfg.Expression)
|
||||||
|
if err != nil {
|
||||||
|
n.logger.Warn(fmt.Sprintf("failed to eval condition expression: %w", err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rs.Value == false {
|
||||||
|
n.logger.Info("condition not met, skip this branch")
|
||||||
|
return errors.New("condition not met") // TODO: 错误处理
|
||||||
|
} else {
|
||||||
|
n.logger.Info("condition met, enter this branch")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *conditionNode) evalExpr(ctx context.Context, expression expr.Expr) (*expr.EvalResult, error) {
|
||||||
|
variables := GetNodeOutputs(ctx)
|
||||||
|
return expression.Eval(variables)
|
||||||
|
}
|
||||||
|
6
internal/workflow/node-processor/const.go
Normal file
6
internal/workflow/node-processor/const.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package nodeprocessor
|
||||||
|
|
||||||
|
const (
|
||||||
|
outputKeyForCertificateValidity = "certificate.validity"
|
||||||
|
outputKeyForCertificateDaysLeft = "certificate.daysLeft"
|
||||||
|
)
|
128
internal/workflow/node-processor/context.go
Normal file
128
internal/workflow/node-processor/context.go
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
package nodeprocessor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 定义上下文键类型,避免键冲突
|
||||||
|
type workflowContextKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
nodeOutputsKey workflowContextKey = "node_outputs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 带互斥锁的节点输出容器
|
||||||
|
type nodeOutputsContainer struct {
|
||||||
|
sync.RWMutex
|
||||||
|
outputs map[string]map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的并发安全的节点输出容器
|
||||||
|
func newNodeOutputsContainer() *nodeOutputsContainer {
|
||||||
|
return &nodeOutputsContainer{
|
||||||
|
outputs: make(map[string]map[string]any),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加节点输出到上下文
|
||||||
|
func AddNodeOutput(ctx context.Context, nodeId string, output map[string]any) context.Context {
|
||||||
|
container := getNodeOutputsContainer(ctx)
|
||||||
|
if container == nil {
|
||||||
|
container = newNodeOutputsContainer()
|
||||||
|
}
|
||||||
|
|
||||||
|
container.Lock()
|
||||||
|
defer container.Unlock()
|
||||||
|
|
||||||
|
// 创建输出的深拷贝
|
||||||
|
// TODO: 暂时使用浅拷贝,等后续值类型扩充后修改
|
||||||
|
outputCopy := make(map[string]any, len(output))
|
||||||
|
for k, v := range output {
|
||||||
|
outputCopy[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
container.outputs[nodeId] = outputCopy
|
||||||
|
return context.WithValue(ctx, nodeOutputsKey, container)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从上下文获取节点输出
|
||||||
|
func GetNodeOutput(ctx context.Context, nodeId string) map[string]any {
|
||||||
|
container := getNodeOutputsContainer(ctx)
|
||||||
|
if container == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
container.RLock()
|
||||||
|
defer container.RUnlock()
|
||||||
|
|
||||||
|
output, exists := container.outputs[nodeId]
|
||||||
|
if !exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
outputCopy := make(map[string]any, len(output))
|
||||||
|
for k, v := range output {
|
||||||
|
outputCopy[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取特定节点的特定输出项
|
||||||
|
func GetNodeOutputValue(ctx context.Context, nodeId string, key string) (any, bool) {
|
||||||
|
output := GetNodeOutput(ctx, nodeId)
|
||||||
|
if output == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
value, exists := output[key]
|
||||||
|
return value, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有节点输出
|
||||||
|
func GetNodeOutputs(ctx context.Context) map[string]map[string]any {
|
||||||
|
container := getNodeOutputsContainer(ctx)
|
||||||
|
if container == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
container.RLock()
|
||||||
|
defer container.RUnlock()
|
||||||
|
|
||||||
|
// 创建所有输出的深拷贝
|
||||||
|
// TODO: 暂时使用浅拷贝,等后续值类型扩充后修改
|
||||||
|
allOutputs := make(map[string]map[string]any, len(container.outputs))
|
||||||
|
for nodeId, output := range container.outputs {
|
||||||
|
nodeCopy := make(map[string]any, len(output))
|
||||||
|
for k, v := range output {
|
||||||
|
nodeCopy[k] = v
|
||||||
|
}
|
||||||
|
allOutputs[nodeId] = nodeCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
return allOutputs
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取节点输出容器
|
||||||
|
func getNodeOutputsContainer(ctx context.Context) *nodeOutputsContainer {
|
||||||
|
value := ctx.Value(nodeOutputsKey)
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return value.(*nodeOutputsContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查节点是否有输出
|
||||||
|
func HasNodeOutput(ctx context.Context, nodeId string) bool {
|
||||||
|
container := getNodeOutputsContainer(ctx)
|
||||||
|
if container == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
container.RLock()
|
||||||
|
defer container.RUnlock()
|
||||||
|
|
||||||
|
_, exists := container.outputs[nodeId]
|
||||||
|
return exists
|
||||||
|
}
|
@ -15,6 +15,7 @@ import (
|
|||||||
type deployNode struct {
|
type deployNode struct {
|
||||||
node *domain.WorkflowNode
|
node *domain.WorkflowNode
|
||||||
*nodeProcessor
|
*nodeProcessor
|
||||||
|
*nodeOutputer
|
||||||
|
|
||||||
certRepo certificateRepository
|
certRepo certificateRepository
|
||||||
outputRepo workflowOutputRepository
|
outputRepo workflowOutputRepository
|
||||||
@ -24,6 +25,7 @@ func NewDeployNode(node *domain.WorkflowNode) *deployNode {
|
|||||||
return &deployNode{
|
return &deployNode{
|
||||||
node: node,
|
node: node,
|
||||||
nodeProcessor: newNodeProcessor(node),
|
nodeProcessor: newNodeProcessor(node),
|
||||||
|
nodeOutputer: newNodeOutputer(),
|
||||||
|
|
||||||
certRepo: repository.NewCertificateRepository(),
|
certRepo: repository.NewCertificateRepository(),
|
||||||
outputRepo: repository.NewWorkflowOutputRepository(),
|
outputRepo: repository.NewWorkflowOutputRepository(),
|
||||||
@ -31,7 +33,7 @@ func NewDeployNode(node *domain.WorkflowNode) *deployNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *deployNode) Process(ctx context.Context) error {
|
func (n *deployNode) Process(ctx context.Context) error {
|
||||||
n.logger.Info("ready to deploy ...")
|
n.logger.Info("ready to deploy certificate ...")
|
||||||
|
|
||||||
// 查询上次执行结果
|
// 查询上次执行结果
|
||||||
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
|
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
|
||||||
@ -40,8 +42,9 @@ func (n *deployNode) Process(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取前序节点输出证书
|
// 获取前序节点输出证书
|
||||||
|
const DELIMITER = "#"
|
||||||
previousNodeOutputCertificateSource := n.node.GetConfigForDeploy().Certificate
|
previousNodeOutputCertificateSource := n.node.GetConfigForDeploy().Certificate
|
||||||
previousNodeOutputCertificateSourceSlice := strings.Split(previousNodeOutputCertificateSource, "#")
|
previousNodeOutputCertificateSourceSlice := strings.Split(previousNodeOutputCertificateSource, DELIMITER)
|
||||||
if len(previousNodeOutputCertificateSourceSlice) != 2 {
|
if len(previousNodeOutputCertificateSourceSlice) != 2 {
|
||||||
n.logger.Warn("invalid certificate source", slog.String("certificate.source", previousNodeOutputCertificateSource))
|
n.logger.Warn("invalid certificate source", slog.String("certificate.source", previousNodeOutputCertificateSource))
|
||||||
return fmt.Errorf("invalid certificate source: %s", previousNodeOutputCertificateSource)
|
return fmt.Errorf("invalid certificate source: %s", previousNodeOutputCertificateSource)
|
||||||
@ -76,7 +79,7 @@ func (n *deployNode) Process(ctx context.Context) error {
|
|||||||
|
|
||||||
// 部署证书
|
// 部署证书
|
||||||
if err := deployer.Deploy(ctx); err != nil {
|
if err := deployer.Deploy(ctx); err != nil {
|
||||||
n.logger.Warn("failed to deploy")
|
n.logger.Warn("failed to deploy certificate")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,12 +96,11 @@ func (n *deployNode) Process(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
n.logger.Info("deploy completed")
|
n.logger.Info("deployment completed")
|
||||||
|
|
||||||
return nil
|
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 {
|
if lastOutput != nil && lastOutput.Succeeded {
|
||||||
// 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致
|
// 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致
|
||||||
currentNodeConfig := n.node.GetConfigForDeploy()
|
currentNodeConfig := n.node.GetConfigForDeploy()
|
||||||
|
@ -9,12 +9,14 @@ import (
|
|||||||
type executeFailureNode struct {
|
type executeFailureNode struct {
|
||||||
node *domain.WorkflowNode
|
node *domain.WorkflowNode
|
||||||
*nodeProcessor
|
*nodeProcessor
|
||||||
|
*nodeOutputer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExecuteFailureNode(node *domain.WorkflowNode) *executeFailureNode {
|
func NewExecuteFailureNode(node *domain.WorkflowNode) *executeFailureNode {
|
||||||
return &executeFailureNode{
|
return &executeFailureNode{
|
||||||
node: node,
|
node: node,
|
||||||
nodeProcessor: newNodeProcessor(node),
|
nodeProcessor: newNodeProcessor(node),
|
||||||
|
nodeOutputer: newNodeOutputer(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,12 +9,14 @@ import (
|
|||||||
type executeSuccessNode struct {
|
type executeSuccessNode struct {
|
||||||
node *domain.WorkflowNode
|
node *domain.WorkflowNode
|
||||||
*nodeProcessor
|
*nodeProcessor
|
||||||
|
*nodeOutputer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExecuteSuccessNode(node *domain.WorkflowNode) *executeSuccessNode {
|
func NewExecuteSuccessNode(node *domain.WorkflowNode) *executeSuccessNode {
|
||||||
return &executeSuccessNode{
|
return &executeSuccessNode{
|
||||||
node: node,
|
node: node,
|
||||||
nodeProcessor: newNodeProcessor(node),
|
nodeProcessor: newNodeProcessor(node),
|
||||||
|
nodeOutputer: newNodeOutputer(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
147
internal/workflow/node-processor/monitor_node.go
Normal file
147
internal/workflow/node-processor/monitor_node.go
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
package nodeprocessor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/usual2970/certimate/internal/domain"
|
||||||
|
httputil "github.com/usual2970/certimate/internal/pkg/utils/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type monitorNode struct {
|
||||||
|
node *domain.WorkflowNode
|
||||||
|
*nodeProcessor
|
||||||
|
*nodeOutputer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMonitorNode(node *domain.WorkflowNode) *monitorNode {
|
||||||
|
return &monitorNode{
|
||||||
|
node: node,
|
||||||
|
nodeProcessor: newNodeProcessor(node),
|
||||||
|
nodeOutputer: newNodeOutputer(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *monitorNode) Process(ctx context.Context) error {
|
||||||
|
n.logger.Info("ready to monitor certificate ...")
|
||||||
|
|
||||||
|
nodeCfg := n.node.GetConfigForMonitor()
|
||||||
|
|
||||||
|
targetAddr := fmt.Sprintf("%s:%d", nodeCfg.Host, nodeCfg.Port)
|
||||||
|
if nodeCfg.Port == 0 {
|
||||||
|
targetAddr = fmt.Sprintf("%s:443", nodeCfg.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetDomain := nodeCfg.Domain
|
||||||
|
if targetDomain == "" {
|
||||||
|
targetDomain = nodeCfg.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
n.logger.Info(fmt.Sprintf("retrieving certificate at %s (domain: %s)", targetAddr, targetDomain))
|
||||||
|
|
||||||
|
const MAX_ATTEMPTS = 3
|
||||||
|
const RETRY_INTERVAL = 2 * time.Second
|
||||||
|
var certs []*x509.Certificate
|
||||||
|
var err error
|
||||||
|
for attempt := 0; attempt < MAX_ATTEMPTS; attempt++ {
|
||||||
|
if attempt > 0 {
|
||||||
|
n.logger.Info(fmt.Sprintf("retry %d time(s) ...", attempt, targetAddr))
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-time.After(RETRY_INTERVAL):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
certs, err = n.tryRetrievePeerCertificates(ctx, targetAddr, targetDomain, nodeCfg.RequestPath)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
n.logger.Warn("failed to monitor certificate")
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
if len(certs) == 0 {
|
||||||
|
n.logger.Warn("no ssl certificates retrieved in http response")
|
||||||
|
|
||||||
|
n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(false)
|
||||||
|
n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(0, 10)
|
||||||
|
} else {
|
||||||
|
cert := certs[0] // 只取证书链中的第一个证书,即服务器证书
|
||||||
|
n.logger.Info(fmt.Sprintf("ssl certificate retrieved (serial='%s', subject='%s', issuer='%s', not_before='%s', not_after='%s', sans='%s')",
|
||||||
|
cert.SerialNumber, cert.Subject.String(), cert.Issuer.String(),
|
||||||
|
cert.NotBefore.Format(time.RFC3339), cert.NotAfter.Format(time.RFC3339),
|
||||||
|
strings.Join(cert.DNSNames, ";")),
|
||||||
|
)
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
isCertPeriodValid := now.Before(cert.NotAfter) && now.After(cert.NotBefore)
|
||||||
|
isCertHostMatched := true
|
||||||
|
if err := cert.VerifyHostname(targetDomain); err != nil {
|
||||||
|
isCertHostMatched = false
|
||||||
|
}
|
||||||
|
|
||||||
|
validated := isCertPeriodValid && isCertHostMatched
|
||||||
|
daysLeft := int(math.Floor(cert.NotAfter.Sub(now).Hours() / 24))
|
||||||
|
n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(validated)
|
||||||
|
n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(daysLeft), 10)
|
||||||
|
|
||||||
|
if validated {
|
||||||
|
n.logger.Info(fmt.Sprintf("the certificate is valid, and will expire in %d day(s)", daysLeft))
|
||||||
|
} else {
|
||||||
|
n.logger.Warn(fmt.Sprintf("the certificate is invalid", validated))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n.logger.Info("monitoring completed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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{
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
},
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
Transport: transport,
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("https://%s/%s", addr, strings.TrimLeft(requestPath, "/"))
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("failed to create http request: %w", err)
|
||||||
|
n.logger.Warn(err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", "certimate")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("failed to send http request: %w", err)
|
||||||
|
n.logger.Warn(err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 {
|
||||||
|
return make([]*x509.Certificate, 0), nil
|
||||||
|
}
|
||||||
|
return resp.TLS.PeerCertificates, nil
|
||||||
|
}
|
28
internal/workflow/node-processor/monitor_node_test.go
Normal file
28
internal/workflow/node-processor/monitor_node_test.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package nodeprocessor_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/usual2970/certimate/internal/domain"
|
||||||
|
nodeprocessor "github.com/usual2970/certimate/internal/workflow/node-processor"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_MonitorNode(t *testing.T) {
|
||||||
|
t.Run("Monitor", func(t *testing.T) {
|
||||||
|
node := nodeprocessor.NewMonitorNode(&domain.WorkflowNode{
|
||||||
|
Id: "test",
|
||||||
|
Type: domain.WorkflowNodeTypeMonitor,
|
||||||
|
Name: "test",
|
||||||
|
Config: map[string]any{
|
||||||
|
"host": "baidu.com",
|
||||||
|
"port": 443,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
node.SetLogger(slog.Default())
|
||||||
|
if err := node.Process(context.Background()); err != nil {
|
||||||
|
t.Errorf("err: %+v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -12,6 +12,7 @@ import (
|
|||||||
type notifyNode struct {
|
type notifyNode struct {
|
||||||
node *domain.WorkflowNode
|
node *domain.WorkflowNode
|
||||||
*nodeProcessor
|
*nodeProcessor
|
||||||
|
*nodeOutputer
|
||||||
|
|
||||||
settingsRepo settingsRepository
|
settingsRepo settingsRepository
|
||||||
}
|
}
|
||||||
@ -20,17 +21,18 @@ func NewNotifyNode(node *domain.WorkflowNode) *notifyNode {
|
|||||||
return ¬ifyNode{
|
return ¬ifyNode{
|
||||||
node: node,
|
node: node,
|
||||||
nodeProcessor: newNodeProcessor(node),
|
nodeProcessor: newNodeProcessor(node),
|
||||||
|
nodeOutputer: newNodeOutputer(),
|
||||||
|
|
||||||
settingsRepo: repository.NewSettingsRepository(),
|
settingsRepo: repository.NewSettingsRepository(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifyNode) Process(ctx context.Context) error {
|
func (n *notifyNode) Process(ctx context.Context) error {
|
||||||
n.logger.Info("ready to notify ...")
|
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 将废弃
|
// Deprecated: v0.4.x 将废弃
|
||||||
// 兼容旧版本的通知渠道
|
// 兼容旧版本的通知渠道
|
||||||
n.logger.Warn("WARNING! you are using the notification channel from global settings, which will be deprecated in the future")
|
n.logger.Warn("WARNING! you are using the notification channel from global settings, which will be deprecated in the future")
|
||||||
@ -42,18 +44,18 @@ func (n *notifyNode) Process(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取通知渠道
|
// 获取通知渠道
|
||||||
channelConfig, err := settings.GetNotifyChannelConfig(nodeConfig.Channel)
|
channelConfig, err := settings.GetNotifyChannelConfig(nodeCfg.Channel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送通知
|
// 发送通知
|
||||||
if err := notify.SendToChannel(nodeConfig.Subject, nodeConfig.Message, nodeConfig.Channel, channelConfig); err != nil {
|
if err := notify.SendToChannel(nodeCfg.Subject, nodeCfg.Message, nodeCfg.Channel, channelConfig); err != nil {
|
||||||
n.logger.Warn("failed to notify", slog.String("channel", nodeConfig.Channel))
|
n.logger.Warn("failed to send notification", slog.String("channel", nodeCfg.Channel))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
n.logger.Info("notify completed")
|
n.logger.Info("notification completed")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,8 +63,8 @@ func (n *notifyNode) Process(ctx context.Context) error {
|
|||||||
deployer, err := notify.NewWithWorkflowNode(notify.NotifierWithWorkflowNodeConfig{
|
deployer, err := notify.NewWithWorkflowNode(notify.NotifierWithWorkflowNodeConfig{
|
||||||
Node: n.node,
|
Node: n.node,
|
||||||
Logger: n.logger,
|
Logger: n.logger,
|
||||||
Subject: nodeConfig.Subject,
|
Subject: nodeCfg.Subject,
|
||||||
Message: nodeConfig.Message,
|
Message: nodeCfg.Message,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
n.logger.Warn("failed to create notifier provider")
|
n.logger.Warn("failed to create notifier provider")
|
||||||
@ -71,9 +73,10 @@ func (n *notifyNode) Process(ctx context.Context) error {
|
|||||||
|
|
||||||
// 推送通知
|
// 推送通知
|
||||||
if err := deployer.Notify(ctx); err != nil {
|
if err := deployer.Notify(ctx); err != nil {
|
||||||
n.logger.Warn("failed to notify")
|
n.logger.Warn("failed to send notification")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n.logger.Info("notification completed")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ type NodeProcessor interface {
|
|||||||
SetLogger(*slog.Logger)
|
SetLogger(*slog.Logger)
|
||||||
|
|
||||||
Process(ctx context.Context) error
|
Process(ctx context.Context) error
|
||||||
|
|
||||||
|
GetOutputs() map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
type nodeProcessor struct {
|
type nodeProcessor struct {
|
||||||
@ -32,6 +34,20 @@ func (n *nodeProcessor) SetLogger(logger *slog.Logger) {
|
|||||||
n.logger = logger
|
n.logger = logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type nodeOutputer struct {
|
||||||
|
outputs map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNodeOutputer() *nodeOutputer {
|
||||||
|
return &nodeOutputer{
|
||||||
|
outputs: make(map[string]any),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *nodeOutputer) GetOutputs() map[string]any {
|
||||||
|
return n.outputs
|
||||||
|
}
|
||||||
|
|
||||||
type certificateRepository interface {
|
type certificateRepository interface {
|
||||||
GetByWorkflowNodeId(ctx context.Context, workflowNodeId string) (*domain.Certificate, error)
|
GetByWorkflowNodeId(ctx context.Context, workflowNodeId string) (*domain.Certificate, error)
|
||||||
GetByWorkflowRunId(ctx context.Context, workflowRunId string) (*domain.Certificate, error)
|
GetByWorkflowRunId(ctx context.Context, workflowRunId string) (*domain.Certificate, error)
|
||||||
@ -58,23 +74,25 @@ func GetProcessor(node *domain.WorkflowNode) (NodeProcessor, error) {
|
|||||||
switch node.Type {
|
switch node.Type {
|
||||||
case domain.WorkflowNodeTypeStart:
|
case domain.WorkflowNodeTypeStart:
|
||||||
return NewStartNode(node), nil
|
return NewStartNode(node), nil
|
||||||
case domain.WorkflowNodeTypeCondition:
|
|
||||||
return NewConditionNode(node), nil
|
|
||||||
case domain.WorkflowNodeTypeApply:
|
case domain.WorkflowNodeTypeApply:
|
||||||
return NewApplyNode(node), nil
|
return NewApplyNode(node), nil
|
||||||
case domain.WorkflowNodeTypeUpload:
|
case domain.WorkflowNodeTypeUpload:
|
||||||
return NewUploadNode(node), nil
|
return NewUploadNode(node), nil
|
||||||
|
case domain.WorkflowNodeTypeMonitor:
|
||||||
|
return NewMonitorNode(node), nil
|
||||||
case domain.WorkflowNodeTypeDeploy:
|
case domain.WorkflowNodeTypeDeploy:
|
||||||
return NewDeployNode(node), nil
|
return NewDeployNode(node), nil
|
||||||
case domain.WorkflowNodeTypeNotify:
|
case domain.WorkflowNodeTypeNotify:
|
||||||
return NewNotifyNode(node), nil
|
return NewNotifyNode(node), nil
|
||||||
|
case domain.WorkflowNodeTypeCondition:
|
||||||
|
return NewConditionNode(node), nil
|
||||||
case domain.WorkflowNodeTypeExecuteSuccess:
|
case domain.WorkflowNodeTypeExecuteSuccess:
|
||||||
return NewExecuteSuccessNode(node), nil
|
return NewExecuteSuccessNode(node), nil
|
||||||
case domain.WorkflowNodeTypeExecuteFailure:
|
case domain.WorkflowNodeTypeExecuteFailure:
|
||||||
return NewExecuteFailureNode(node), nil
|
return NewExecuteFailureNode(node), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("supported node type: %s", string(node.Type))
|
return nil, fmt.Errorf("unsupported node type: %s", string(node.Type))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getContextWorkflowId(ctx context.Context) string {
|
func getContextWorkflowId(ctx context.Context) string {
|
||||||
|
@ -9,12 +9,14 @@ import (
|
|||||||
type startNode struct {
|
type startNode struct {
|
||||||
node *domain.WorkflowNode
|
node *domain.WorkflowNode
|
||||||
*nodeProcessor
|
*nodeProcessor
|
||||||
|
*nodeOutputer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStartNode(node *domain.WorkflowNode) *startNode {
|
func NewStartNode(node *domain.WorkflowNode) *startNode {
|
||||||
return &startNode{
|
return &startNode{
|
||||||
node: node,
|
node: node,
|
||||||
nodeProcessor: newNodeProcessor(node),
|
nodeProcessor: newNodeProcessor(node),
|
||||||
|
nodeOutputer: newNodeOutputer(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,9 @@ package nodeprocessor
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/usual2970/certimate/internal/domain"
|
"github.com/usual2970/certimate/internal/domain"
|
||||||
"github.com/usual2970/certimate/internal/repository"
|
"github.com/usual2970/certimate/internal/repository"
|
||||||
@ -12,6 +14,7 @@ import (
|
|||||||
type uploadNode struct {
|
type uploadNode struct {
|
||||||
node *domain.WorkflowNode
|
node *domain.WorkflowNode
|
||||||
*nodeProcessor
|
*nodeProcessor
|
||||||
|
*nodeOutputer
|
||||||
|
|
||||||
certRepo certificateRepository
|
certRepo certificateRepository
|
||||||
outputRepo workflowOutputRepository
|
outputRepo workflowOutputRepository
|
||||||
@ -21,6 +24,7 @@ func NewUploadNode(node *domain.WorkflowNode) *uploadNode {
|
|||||||
return &uploadNode{
|
return &uploadNode{
|
||||||
node: node,
|
node: node,
|
||||||
nodeProcessor: newNodeProcessor(node),
|
nodeProcessor: newNodeProcessor(node),
|
||||||
|
nodeOutputer: newNodeOutputer(),
|
||||||
|
|
||||||
certRepo: repository.NewCertificateRepository(),
|
certRepo: repository.NewCertificateRepository(),
|
||||||
outputRepo: repository.NewWorkflowOutputRepository(),
|
outputRepo: repository.NewWorkflowOutputRepository(),
|
||||||
@ -28,9 +32,9 @@ func NewUploadNode(node *domain.WorkflowNode) *uploadNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *uploadNode) Process(ctx context.Context) error {
|
func (n *uploadNode) Process(ctx context.Context) error {
|
||||||
n.logger.Info("ready to upload ...")
|
n.logger.Info("ready to upload certiticate ...")
|
||||||
|
|
||||||
nodeConfig := n.node.GetConfigForUpload()
|
nodeCfg := n.node.GetConfigForUpload()
|
||||||
|
|
||||||
// 查询上次执行结果
|
// 查询上次执行结果
|
||||||
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
|
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
|
||||||
@ -40,7 +44,7 @@ func (n *uploadNode) Process(ctx context.Context) error {
|
|||||||
|
|
||||||
// 检测是否可以跳过本次执行
|
// 检测是否可以跳过本次执行
|
||||||
if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable {
|
if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable {
|
||||||
n.logger.Info(fmt.Sprintf("skip this upload, because %s", reason))
|
n.logger.Info(fmt.Sprintf("skip this uploading, because %s", reason))
|
||||||
return nil
|
return nil
|
||||||
} else if reason != "" {
|
} else if reason != "" {
|
||||||
n.logger.Info(fmt.Sprintf("re-upload, because %s", reason))
|
n.logger.Info(fmt.Sprintf("re-upload, because %s", reason))
|
||||||
@ -50,7 +54,7 @@ func (n *uploadNode) Process(ctx context.Context) error {
|
|||||||
certificate := &domain.Certificate{
|
certificate := &domain.Certificate{
|
||||||
Source: domain.CertificateSourceTypeUpload,
|
Source: domain.CertificateSourceTypeUpload,
|
||||||
}
|
}
|
||||||
certificate.PopulateFromPEM(nodeConfig.Certificate, nodeConfig.PrivateKey)
|
certificate.PopulateFromPEM(nodeCfg.Certificate, nodeCfg.PrivateKey)
|
||||||
|
|
||||||
// 保存执行结果
|
// 保存执行结果
|
||||||
output := &domain.WorkflowOutput{
|
output := &domain.WorkflowOutput{
|
||||||
@ -66,12 +70,15 @@ func (n *uploadNode) Process(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
n.logger.Info("upload completed")
|
// 记录中间结果
|
||||||
|
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
|
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 {
|
if lastOutput != nil && lastOutput.Succeeded {
|
||||||
// 比较和上次上传时的关键配置(即影响证书上传的)参数是否一致
|
// 比较和上次上传时的关键配置(即影响证书上传的)参数是否一致
|
||||||
currentNodeConfig := n.node.GetConfigForUpload()
|
currentNodeConfig := n.node.GetConfigForUpload()
|
||||||
@ -85,6 +92,10 @@ func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workfl
|
|||||||
|
|
||||||
lastCertificate, _ := n.certRepo.GetByWorkflowRunId(ctx, lastOutput.RunId)
|
lastCertificate, _ := n.certRepo.GetByWorkflowRunId(ctx, lastOutput.RunId)
|
||||||
if lastCertificate != nil {
|
if lastCertificate != nil {
|
||||||
|
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"
|
return true, "the certificate has already been uploaded"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
main.go
9
main.go
@ -26,9 +26,7 @@ func main() {
|
|||||||
app := app.GetApp().(*pocketbase.PocketBase)
|
app := app.GetApp().(*pocketbase.PocketBase)
|
||||||
|
|
||||||
var flagHttp string
|
var flagHttp string
|
||||||
var flagDir string
|
|
||||||
flag.StringVar(&flagHttp, "http", "127.0.0.1:8090", "HTTP server address")
|
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 {
|
if len(os.Args) < 2 {
|
||||||
slog.Error("[CERTIMATE] missing exec args")
|
slog.Error("[CERTIMATE] missing exec args")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -59,14 +57,17 @@ func main() {
|
|||||||
Priority: 999,
|
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 {
|
app.OnTerminate().BindFunc(func(e *core.TerminateEvent) error {
|
||||||
routes.Unregister()
|
routes.Unregister()
|
||||||
slog.Info("[CERTIMATE] Exit!")
|
slog.Info("[CERTIMATE] Exit!")
|
||||||
return e.Next()
|
return e.Next()
|
||||||
})
|
})
|
||||||
|
|
||||||
slog.Info("[CERTIMATE] Visit the website: http://" + flagHttp)
|
|
||||||
|
|
||||||
if err := app.Start(); err != nil {
|
if err := app.Start(); err != nil {
|
||||||
slog.Error("[CERTIMATE] Start failed.", "err", err)
|
slog.Error("[CERTIMATE] Start failed.", "err", err)
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 9.0 KiB |
BIN
ui/public/imgs/workflow/tpl-certtest.png
Normal file
BIN
ui/public/imgs/workflow/tpl-certtest.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 20 KiB |
@ -152,10 +152,10 @@ const MultipleInput = ({
|
|||||||
value={element}
|
value={element}
|
||||||
onBlur={() => handleInputBlur(index)}
|
onBlur={() => handleInputBlur(index)}
|
||||||
onChange={(val) => handleChange(index, val)}
|
onChange={(val) => handleChange(index, val)}
|
||||||
onClickAdd={() => handleClickAdd(index)}
|
onEntryAdd={() => handleClickAdd(index)}
|
||||||
onClickDown={() => handleClickDown(index)}
|
onEntryDown={() => handleClickDown(index)}
|
||||||
onClickUp={() => handleClickUp(index)}
|
onEntryUp={() => handleClickUp(index)}
|
||||||
onClickRemove={() => handleClickRemove(index)}
|
onEntryRemove={() => handleClickRemove(index)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -174,10 +174,10 @@ type MultipleInputItemProps = Omit<
|
|||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
onClickAdd?: () => void;
|
onEntryAdd?: () => void;
|
||||||
onClickDown?: () => void;
|
onEntryDown?: () => void;
|
||||||
onClickUp?: () => void;
|
onEntryUp?: () => void;
|
||||||
onClickRemove?: () => void;
|
onEntryRemove?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MultipleInputItemInstance = {
|
type MultipleInputItemInstance = {
|
||||||
@ -197,10 +197,10 @@ const MultipleInputItem = forwardRef<MultipleInputItemInstance, MultipleInputIte
|
|||||||
disabled,
|
disabled,
|
||||||
showSortButton,
|
showSortButton,
|
||||||
size,
|
size,
|
||||||
onClickAdd,
|
onEntryAdd,
|
||||||
onClickDown,
|
onEntryDown,
|
||||||
onClickUp,
|
onEntryUp,
|
||||||
onClickRemove,
|
onEntryRemove,
|
||||||
...props
|
...props
|
||||||
}: MultipleInputItemProps,
|
}: MultipleInputItemProps,
|
||||||
ref
|
ref
|
||||||
@ -216,18 +216,18 @@ const MultipleInputItem = forwardRef<MultipleInputItemInstance, MultipleInputIte
|
|||||||
|
|
||||||
const upBtn = useMemo(() => {
|
const upBtn = useMemo(() => {
|
||||||
if (!showSortButton) return null;
|
if (!showSortButton) return null;
|
||||||
return <Button icon={<ArrowUpOutlinedIcon />} color="default" disabled={disabled || !allowUp} type="text" onClick={onClickUp} />;
|
return <Button icon={<ArrowUpOutlinedIcon />} color="default" disabled={disabled || !allowUp} type="text" onClick={onEntryUp} />;
|
||||||
}, [allowUp, disabled, showSortButton, onClickUp]);
|
}, [allowUp, disabled, showSortButton, onEntryUp]);
|
||||||
const downBtn = useMemo(() => {
|
const downBtn = useMemo(() => {
|
||||||
if (!showSortButton) return null;
|
if (!showSortButton) return null;
|
||||||
return <Button icon={<ArrowDownOutlinedIcon />} color="default" disabled={disabled || !allowDown} type="text" onClick={onClickDown} />;
|
return <Button icon={<ArrowDownOutlinedIcon />} color="default" disabled={disabled || !allowDown} type="text" onClick={onEntryDown} />;
|
||||||
}, [allowDown, disabled, showSortButton, onClickDown]);
|
}, [allowDown, disabled, showSortButton, onEntryDown]);
|
||||||
const removeBtn = useMemo(() => {
|
const removeBtn = useMemo(() => {
|
||||||
return <Button icon={<MinusOutlinedIcon />} color="default" disabled={disabled || !allowRemove} type="text" onClick={onClickRemove} />;
|
return <Button icon={<MinusOutlinedIcon />} color="default" disabled={disabled || !allowRemove} type="text" onClick={onEntryRemove} />;
|
||||||
}, [allowRemove, disabled, onClickRemove]);
|
}, [allowRemove, disabled, onEntryRemove]);
|
||||||
const addBtn = useMemo(() => {
|
const addBtn = useMemo(() => {
|
||||||
return <Button icon={<PlusOutlinedIcon />} color="default" disabled={disabled || !allowAdd} type="text" onClick={onClickAdd} />;
|
return <Button icon={<PlusOutlinedIcon />} color="default" disabled={disabled || !allowAdd} type="text" onClick={onEntryAdd} />;
|
||||||
}, [allowAdd, disabled, onClickAdd]);
|
}, [allowAdd, disabled, onEntryAdd]);
|
||||||
|
|
||||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setValue(e.target.value);
|
setValue(e.target.value);
|
||||||
|
@ -15,24 +15,24 @@ type SplitOptions = {
|
|||||||
|
|
||||||
export type MultipleSplitValueInputProps = Omit<InputProps, "count" | "defaultValue" | "showCount" | "value" | "onChange"> & {
|
export type MultipleSplitValueInputProps = Omit<InputProps, "count" | "defaultValue" | "showCount" | "value" | "onChange"> & {
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
delimiter?: string;
|
|
||||||
maxCount?: number;
|
maxCount?: number;
|
||||||
minCount?: number;
|
minCount?: number;
|
||||||
modalTitle?: string;
|
modalTitle?: string;
|
||||||
modalWidth?: number;
|
modalWidth?: number;
|
||||||
placeholderInModal?: string;
|
placeholderInModal?: string;
|
||||||
showSortButton?: boolean;
|
showSortButton?: boolean;
|
||||||
|
separator?: string;
|
||||||
splitOptions?: SplitOptions;
|
splitOptions?: SplitOptions;
|
||||||
value?: string[];
|
value?: string[];
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_DELIMITER = ";";
|
const DEFAULT_SEPARATOR = ";";
|
||||||
|
|
||||||
const MultipleSplitValueInput = ({
|
const MultipleSplitValueInput = ({
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
delimiter = DEFAULT_DELIMITER,
|
separator: delimiter = DEFAULT_SEPARATOR,
|
||||||
disabled,
|
disabled,
|
||||||
maxCount,
|
maxCount,
|
||||||
minCount,
|
minCount,
|
||||||
|
@ -120,7 +120,7 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
|
|||||||
|
|
||||||
<div className="w-1/3">
|
<div className="w-1/3">
|
||||||
<Form.Item name="port" label={t("access.form.ssh_port.label")} rules={[formRule]}>
|
<Form.Item name="port" label={t("access.form.ssh_port.label")} rules={[formRule]}>
|
||||||
<InputNumber className="w-full" placeholder={t("access.form.ssh_port.placeholder")} min={1} max={65535} />
|
<InputNumber className="w-full" min={1} max={65535} placeholder={t("access.form.ssh_port.placeholder")} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -265,7 +265,7 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<Button type="dashed" className="w-full" icon={<PlusOutlined />} onClick={() => add()}>
|
<Button className="w-full" type="dashed" icon={<PlusOutlined />} onClick={() => add()}>
|
||||||
{t("access.form.ssh_jump_servers.add")}
|
{t("access.form.ssh_jump_servers.add")}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
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 { type AccessModel } from "@/domain/access";
|
||||||
import { accessProvidersMap } from "@/domain/provider";
|
import { accessProvidersMap } from "@/domain/provider";
|
||||||
@ -14,6 +14,8 @@ export type AccessTypeSelectProps = Omit<
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => {
|
const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => {
|
||||||
|
const { token: themeToken } = theme.useToken();
|
||||||
|
|
||||||
const { accesses, loadedAtOnce, fetchAccesses } = useAccessesStore(useZustandShallowSelector(["accesses", "loadedAtOnce", "fetchAccesses"]));
|
const { accesses, loadedAtOnce, fetchAccesses } = useAccessesStore(useZustandShallowSelector(["accesses", "loadedAtOnce", "fetchAccesses"]));
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAccesses();
|
fetchAccesses();
|
||||||
@ -37,7 +39,7 @@ const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => {
|
|||||||
if (!access) {
|
if (!access) {
|
||||||
return (
|
return (
|
||||||
<Space className="max-w-full grow truncate" size={4}>
|
<Space className="max-w-full grow truncate" size={4}>
|
||||||
<Avatar size="small" />
|
<Avatar shape="square" size="small" />
|
||||||
<Typography.Text className="leading-loose" ellipsis>
|
<Typography.Text className="leading-loose" ellipsis>
|
||||||
{key}
|
{key}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@ -48,7 +50,7 @@ const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => {
|
|||||||
const provider = accessProvidersMap.get(access.provider);
|
const provider = accessProvidersMap.get(access.provider);
|
||||||
return (
|
return (
|
||||||
<Space className="max-w-full grow truncate" size={4}>
|
<Space className="max-w-full grow truncate" size={4}>
|
||||||
<Avatar src={provider?.icon} size="small" />
|
<Avatar shape="square" src={provider?.icon} size="small" />
|
||||||
<Typography.Text className="leading-loose" ellipsis>
|
<Typography.Text className="leading-loose" ellipsis>
|
||||||
{access.name}
|
{access.name}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@ -65,12 +67,12 @@ const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => {
|
|||||||
const value = inputValue.toLowerCase();
|
const value = inputValue.toLowerCase();
|
||||||
return option.label.toLowerCase().includes(value);
|
return option.label.toLowerCase().includes(value);
|
||||||
}}
|
}}
|
||||||
labelRender={({ label, value }) => {
|
labelRender={({ value }) => {
|
||||||
if (label) {
|
if (value != null) {
|
||||||
return renderOption(value as string);
|
return renderOption(value as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Typography.Text type="secondary">{props.placeholder}</Typography.Text>;
|
return <span style={{ color: themeToken.colorTextPlaceholder }}>{props.placeholder}</span>;
|
||||||
}}
|
}}
|
||||||
loading={!loadedAtOnce}
|
loading={!loadedAtOnce}
|
||||||
options={options}
|
options={options}
|
||||||
|
@ -67,7 +67,7 @@ const ACMEDns01ProviderPicker = ({ className, style, autoFocus, filter, placehol
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex className="size-full overflow-hidden" align="center" gap={8}>
|
<Flex className="size-full overflow-hidden" align="center" gap={8}>
|
||||||
<Avatar src={provider.icon} size="small" />
|
<Avatar shape="square" src={provider.icon} size="small" />
|
||||||
<Typography.Text className="line-clamp-2 flex-1">{t(provider.name)}</Typography.Text>
|
<Typography.Text className="line-clamp-2 flex-1">{t(provider.name)}</Typography.Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { memo, useEffect, useState } from "react";
|
import { memo, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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";
|
import { type ACMEDns01Provider, acmeDns01ProvidersMap } from "@/domain/provider";
|
||||||
|
|
||||||
@ -14,6 +14,8 @@ export type ACMEDns01ProviderSelectProps = Omit<
|
|||||||
const ACMEDns01ProviderSelect = ({ filter, ...props }: ACMEDns01ProviderSelectProps) => {
|
const ACMEDns01ProviderSelect = ({ filter, ...props }: ACMEDns01ProviderSelectProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { token: themeToken } = theme.useToken();
|
||||||
|
|
||||||
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: ACMEDns01Provider }>>([]);
|
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: ACMEDns01Provider }>>([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const allItems = Array.from(acmeDns01ProvidersMap.values());
|
const allItems = Array.from(acmeDns01ProvidersMap.values());
|
||||||
@ -32,7 +34,7 @@ const ACMEDns01ProviderSelect = ({ filter, ...props }: ACMEDns01ProviderSelectPr
|
|||||||
const provider = acmeDns01ProvidersMap.get(key);
|
const provider = acmeDns01ProvidersMap.get(key);
|
||||||
return (
|
return (
|
||||||
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
|
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
|
||||||
<Avatar src={provider?.icon} size="small" />
|
<Avatar shape="square" src={provider?.icon} size="small" />
|
||||||
<Typography.Text className="leading-loose" ellipsis>
|
<Typography.Text className="leading-loose" ellipsis>
|
||||||
{t(provider?.name ?? "")}
|
{t(provider?.name ?? "")}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@ -49,12 +51,12 @@ const ACMEDns01ProviderSelect = ({ filter, ...props }: ACMEDns01ProviderSelectPr
|
|||||||
const value = inputValue.toLowerCase();
|
const value = inputValue.toLowerCase();
|
||||||
return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value);
|
return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value);
|
||||||
}}
|
}}
|
||||||
labelRender={({ label, value }) => {
|
labelRender={({ value }) => {
|
||||||
if (!label) {
|
if (value != null) {
|
||||||
return <Typography.Text type="secondary">{props.placeholder}</Typography.Text>;
|
return renderOption(value as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderOption(value as string);
|
return <span style={{ color: themeToken.colorTextPlaceholder }}>{props.placeholder}</span>;
|
||||||
}}
|
}}
|
||||||
options={options}
|
options={options}
|
||||||
optionFilterProp={undefined}
|
optionFilterProp={undefined}
|
||||||
|
@ -86,12 +86,12 @@ const AccessProviderPicker = ({ className, style, autoFocus, filter, placeholder
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex className="size-full overflow-hidden" align="center" gap={8}>
|
<Flex className="size-full overflow-hidden" align="center" gap={8}>
|
||||||
<Avatar src={provider.icon} size="small" />
|
<Avatar shape="square" src={provider.icon} size="small" />
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<Typography.Text className="mb-1 line-clamp-1" type={provider.builtin ? "secondary" : undefined}>
|
<Typography.Text className="mb-1 line-clamp-1" type={provider.builtin ? "secondary" : undefined}>
|
||||||
{t(provider.name)}
|
{t(provider.name)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<div className="origin-left scale-[80%]">
|
<div className="origin-left scale-[75%]">
|
||||||
<Show when={provider.builtin}>
|
<Show when={provider.builtin}>
|
||||||
<Tag>{t("access.props.provider.builtin")}</Tag>
|
<Tag>{t("access.props.provider.builtin")}</Tag>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { memo, useEffect, useMemo, useState } from "react";
|
import { memo, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 Show from "@/components/Show";
|
||||||
import { ACCESS_USAGES, type AccessProvider, type AccessUsageType, accessProvidersMap } from "@/domain/provider";
|
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 AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProviderSelectProps = { showOptionTags: true }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { token: themeToken } = theme.useToken();
|
||||||
|
|
||||||
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: AccessProvider }>>([]);
|
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: AccessProvider }>>([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const allItems = Array.from(accessProvidersMap.values());
|
const allItems = Array.from(accessProvidersMap.values());
|
||||||
@ -49,12 +51,12 @@ const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProvid
|
|||||||
return (
|
return (
|
||||||
<div className="flex max-w-full items-center justify-between gap-4 overflow-hidden">
|
<div className="flex max-w-full items-center justify-between gap-4 overflow-hidden">
|
||||||
<Space className="max-w-full grow truncate" size={4}>
|
<Space className="max-w-full grow truncate" size={4}>
|
||||||
<Avatar src={provider.icon} size="small" />
|
<Avatar shape="square" src={provider.icon} size="small" />
|
||||||
<Typography.Text className="leading-loose" type={provider.builtin ? "secondary" : undefined} ellipsis>
|
<Typography.Text className="leading-loose" type={provider.builtin ? "secondary" : undefined} ellipsis>
|
||||||
{t(provider.name)}
|
{t(provider.name)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Space>
|
</Space>
|
||||||
<div className="origin-right scale-[80%]">
|
<div className="origin-right scale-[75%]">
|
||||||
<Show when={provider.builtin}>
|
<Show when={provider.builtin}>
|
||||||
<Tag>{t("access.props.provider.builtin")}</Tag>
|
<Tag>{t("access.props.provider.builtin")}</Tag>
|
||||||
</Show>
|
</Show>
|
||||||
@ -84,12 +86,12 @@ const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProvid
|
|||||||
const value = inputValue.toLowerCase();
|
const value = inputValue.toLowerCase();
|
||||||
return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value);
|
return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value);
|
||||||
}}
|
}}
|
||||||
labelRender={({ label, value }) => {
|
labelRender={({ value }) => {
|
||||||
if (!label) {
|
if (value != null) {
|
||||||
return <Typography.Text type="secondary">{props.placeholder}</Typography.Text>;
|
return renderOption(value as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderOption(value as string);
|
return <span style={{ color: themeToken.colorTextPlaceholder }}>{props.placeholder}</span>;
|
||||||
}}
|
}}
|
||||||
options={options}
|
options={options}
|
||||||
optionFilterProp={undefined}
|
optionFilterProp={undefined}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { memo, useEffect, useState } from "react";
|
import { memo, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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";
|
import { type CAProvider, caProvidersMap } from "@/domain/provider";
|
||||||
|
|
||||||
@ -14,6 +14,8 @@ export type CAProviderSelectProps = Omit<
|
|||||||
const CAProviderSelect = ({ filter, ...props }: CAProviderSelectProps) => {
|
const CAProviderSelect = ({ filter, ...props }: CAProviderSelectProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { token: themeToken } = theme.useToken();
|
||||||
|
|
||||||
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: CAProvider }>>([]);
|
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: CAProvider }>>([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const allItems = Array.from(caProvidersMap.values());
|
const allItems = Array.from(caProvidersMap.values());
|
||||||
@ -48,7 +50,7 @@ const CAProviderSelect = ({ filter, ...props }: CAProviderSelectProps) => {
|
|||||||
const provider = caProvidersMap.get(key);
|
const provider = caProvidersMap.get(key);
|
||||||
return (
|
return (
|
||||||
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
|
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
|
||||||
<Avatar src={provider?.icon} size="small" />
|
<Avatar shape="square" src={provider?.icon} size="small" />
|
||||||
<Typography.Text className="leading-loose" ellipsis>
|
<Typography.Text className="leading-loose" ellipsis>
|
||||||
{t(provider?.name ?? "")}
|
{t(provider?.name ?? "")}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@ -65,12 +67,12 @@ const CAProviderSelect = ({ filter, ...props }: CAProviderSelectProps) => {
|
|||||||
const value = inputValue.toLowerCase();
|
const value = inputValue.toLowerCase();
|
||||||
return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value);
|
return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value);
|
||||||
}}
|
}}
|
||||||
labelRender={({ label, value }) => {
|
labelRender={({ value }) => {
|
||||||
if (!label) {
|
if (value != null) {
|
||||||
return <Typography.Text type="secondary">{props.placeholder || t("provider.default_ca_provider.label")}</Typography.Text>;
|
return renderOption(value as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderOption(value as string);
|
return <span style={{ color: themeToken.colorTextPlaceholder }}>{props.placeholder}</span>;
|
||||||
}}
|
}}
|
||||||
options={options}
|
options={options}
|
||||||
optionFilterProp={undefined}
|
optionFilterProp={undefined}
|
||||||
|
@ -104,7 +104,7 @@ const DeploymentProviderPicker = ({ className, style, autoFocus, filter, placeho
|
|||||||
>
|
>
|
||||||
<Tooltip title={t(provider.name)} mouseEnterDelay={1}>
|
<Tooltip title={t(provider.name)} mouseEnterDelay={1}>
|
||||||
<Flex className="size-full overflow-hidden" align="center" gap={8}>
|
<Flex className="size-full overflow-hidden" align="center" gap={8}>
|
||||||
<Avatar src={provider.icon} size="small" />
|
<Avatar shape="square" src={provider.icon} size="small" />
|
||||||
<Typography.Text className="line-clamp-2 flex-1">{t(provider.name)}</Typography.Text>
|
<Typography.Text className="line-clamp-2 flex-1">{t(provider.name)}</Typography.Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { memo, useEffect, useState } from "react";
|
import { memo, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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";
|
import { type DeploymentProvider, deploymentProvidersMap } from "@/domain/provider";
|
||||||
|
|
||||||
@ -14,6 +14,8 @@ export type DeploymentProviderSelectProps = Omit<
|
|||||||
const DeploymentProviderSelect = ({ filter, ...props }: DeploymentProviderSelectProps) => {
|
const DeploymentProviderSelect = ({ filter, ...props }: DeploymentProviderSelectProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { token: themeToken } = theme.useToken();
|
||||||
|
|
||||||
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: DeploymentProvider }>>([]);
|
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: DeploymentProvider }>>([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const allItems = Array.from(deploymentProvidersMap.values());
|
const allItems = Array.from(deploymentProvidersMap.values());
|
||||||
@ -32,7 +34,7 @@ const DeploymentProviderSelect = ({ filter, ...props }: DeploymentProviderSelect
|
|||||||
const provider = deploymentProvidersMap.get(key);
|
const provider = deploymentProvidersMap.get(key);
|
||||||
return (
|
return (
|
||||||
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
|
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
|
||||||
<Avatar src={provider?.icon} size="small" />
|
<Avatar shape="square" src={provider?.icon} size="small" />
|
||||||
<Typography.Text className="leading-loose" ellipsis>
|
<Typography.Text className="leading-loose" ellipsis>
|
||||||
{t(provider?.name ?? "")}
|
{t(provider?.name ?? "")}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@ -49,12 +51,12 @@ const DeploymentProviderSelect = ({ filter, ...props }: DeploymentProviderSelect
|
|||||||
const value = inputValue.toLowerCase();
|
const value = inputValue.toLowerCase();
|
||||||
return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value);
|
return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value);
|
||||||
}}
|
}}
|
||||||
labelRender={({ label, value }) => {
|
labelRender={({ value }) => {
|
||||||
if (!label) {
|
if (value != null) {
|
||||||
return <Typography.Text type="secondary">{props.placeholder}</Typography.Text>;
|
return renderOption(value as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderOption(value as string);
|
return <span style={{ color: themeToken.colorTextPlaceholder }}>{props.placeholder}</span>;
|
||||||
}}
|
}}
|
||||||
options={options}
|
options={options}
|
||||||
optionFilterProp={undefined}
|
optionFilterProp={undefined}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { memo, useEffect, useState } from "react";
|
import { memo, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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";
|
import { type NotificationProvider, notificationProvidersMap } from "@/domain/provider";
|
||||||
|
|
||||||
@ -14,6 +14,8 @@ export type NotificationProviderSelectProps = Omit<
|
|||||||
const NotificationProviderSelect = ({ filter, ...props }: NotificationProviderSelectProps) => {
|
const NotificationProviderSelect = ({ filter, ...props }: NotificationProviderSelectProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { token: themeToken } = theme.useToken();
|
||||||
|
|
||||||
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: NotificationProvider }>>([]);
|
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: NotificationProvider }>>([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const allItems = Array.from(notificationProvidersMap.values());
|
const allItems = Array.from(notificationProvidersMap.values());
|
||||||
@ -32,7 +34,7 @@ const NotificationProviderSelect = ({ filter, ...props }: NotificationProviderSe
|
|||||||
const provider = notificationProvidersMap.get(key);
|
const provider = notificationProvidersMap.get(key);
|
||||||
return (
|
return (
|
||||||
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
|
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
|
||||||
<Avatar src={provider?.icon} size="small" />
|
<Avatar shape="square" src={provider?.icon} size="small" />
|
||||||
<Typography.Text className="leading-loose" ellipsis>
|
<Typography.Text className="leading-loose" ellipsis>
|
||||||
{t(provider?.name ?? "")}
|
{t(provider?.name ?? "")}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@ -49,12 +51,12 @@ const NotificationProviderSelect = ({ filter, ...props }: NotificationProviderSe
|
|||||||
const value = inputValue.toLowerCase();
|
const value = inputValue.toLowerCase();
|
||||||
return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value);
|
return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value);
|
||||||
}}
|
}}
|
||||||
labelRender={({ label, value }) => {
|
labelRender={({ value }) => {
|
||||||
if (!label) {
|
if (value != null) {
|
||||||
return <Typography.Text type="secondary">{props.placeholder}</Typography.Text>;
|
return renderOption(value as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderOption(value as string);
|
return <span style={{ color: themeToken.colorTextPlaceholder }}>{props.placeholder}</span>;
|
||||||
}}
|
}}
|
||||||
options={options}
|
options={options}
|
||||||
optionFilterProp={undefined}
|
optionFilterProp={undefined}
|
||||||
|
@ -9,8 +9,10 @@ import DeployNode from "./node/DeployNode";
|
|||||||
import EndNode from "./node/EndNode";
|
import EndNode from "./node/EndNode";
|
||||||
import ExecuteResultBranchNode from "./node/ExecuteResultBranchNode";
|
import ExecuteResultBranchNode from "./node/ExecuteResultBranchNode";
|
||||||
import ExecuteResultNode from "./node/ExecuteResultNode";
|
import ExecuteResultNode from "./node/ExecuteResultNode";
|
||||||
|
import MonitorNode from "./node/MonitorNode";
|
||||||
import NotifyNode from "./node/NotifyNode";
|
import NotifyNode from "./node/NotifyNode";
|
||||||
import StartNode from "./node/StartNode";
|
import StartNode from "./node/StartNode";
|
||||||
|
import UnknownNode from "./node/UnknownNode";
|
||||||
import UploadNode from "./node/UploadNode";
|
import UploadNode from "./node/UploadNode";
|
||||||
|
|
||||||
export type WorkflowElementProps = {
|
export type WorkflowElementProps = {
|
||||||
@ -32,6 +34,9 @@ const WorkflowElement = ({ node, disabled, branchId, branchIndex }: WorkflowElem
|
|||||||
case WorkflowNodeType.Upload:
|
case WorkflowNodeType.Upload:
|
||||||
return <UploadNode node={node} disabled={disabled} />;
|
return <UploadNode node={node} disabled={disabled} />;
|
||||||
|
|
||||||
|
case WorkflowNodeType.Monitor:
|
||||||
|
return <MonitorNode node={node} disabled={disabled} />;
|
||||||
|
|
||||||
case WorkflowNodeType.Deploy:
|
case WorkflowNodeType.Deploy:
|
||||||
return <DeployNode node={node} disabled={disabled} />;
|
return <DeployNode node={node} disabled={disabled} />;
|
||||||
|
|
||||||
@ -56,7 +61,7 @@ const WorkflowElement = ({ node, disabled, branchId, branchIndex }: WorkflowElem
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn(`[certimate] unsupported workflow node type: ${node.type}`);
|
console.warn(`[certimate] unsupported workflow node type: ${node.type}`);
|
||||||
return <></>;
|
return <UnknownNode node={node} />;
|
||||||
}
|
}
|
||||||
}, [node, disabled, branchId, branchIndex]);
|
}, [node, disabled, branchId, branchIndex]);
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ import { ClientResponseError } from "pocketbase";
|
|||||||
import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer";
|
import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer";
|
||||||
import Show from "@/components/Show";
|
import Show from "@/components/Show";
|
||||||
import { type CertificateModel } from "@/domain/certificate";
|
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 { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
|
||||||
import { useBrowserTheme } from "@/hooks";
|
import { useBrowserTheme } from "@/hooks";
|
||||||
import { listByWorkflowRunId as listCertificatesByWorkflowRunId } from "@/repository/certificate";
|
import { listByWorkflowRunId as listCertificatesByWorkflowRunId } from "@/repository/certificate";
|
||||||
|
@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import {
|
import {
|
||||||
CloudUploadOutlined as CloudUploadOutlinedIcon,
|
CloudUploadOutlined as CloudUploadOutlinedIcon,
|
||||||
DeploymentUnitOutlined as DeploymentUnitOutlinedIcon,
|
DeploymentUnitOutlined as DeploymentUnitOutlinedIcon,
|
||||||
|
MonitorOutlined as MonitorOutlinedIcon,
|
||||||
PlusOutlined as PlusOutlinedIcon,
|
PlusOutlined as PlusOutlinedIcon,
|
||||||
SendOutlined as SendOutlinedIcon,
|
SendOutlined as SendOutlinedIcon,
|
||||||
SisternodeOutlined as SisternodeOutlinedIcon,
|
SisternodeOutlined as SisternodeOutlinedIcon,
|
||||||
@ -27,13 +28,21 @@ const AddNode = ({ node, disabled }: AddNodeProps) => {
|
|||||||
return [
|
return [
|
||||||
[WorkflowNodeType.Apply, "workflow_node.apply.label", <SolutionOutlinedIcon />],
|
[WorkflowNodeType.Apply, "workflow_node.apply.label", <SolutionOutlinedIcon />],
|
||||||
[WorkflowNodeType.Upload, "workflow_node.upload.label", <CloudUploadOutlinedIcon />],
|
[WorkflowNodeType.Upload, "workflow_node.upload.label", <CloudUploadOutlinedIcon />],
|
||||||
|
[WorkflowNodeType.Monitor, "workflow_node.monitor.label", <MonitorOutlinedIcon />],
|
||||||
[WorkflowNodeType.Deploy, "workflow_node.deploy.label", <DeploymentUnitOutlinedIcon />],
|
[WorkflowNodeType.Deploy, "workflow_node.deploy.label", <DeploymentUnitOutlinedIcon />],
|
||||||
[WorkflowNodeType.Notify, "workflow_node.notify.label", <SendOutlinedIcon />],
|
[WorkflowNodeType.Notify, "workflow_node.notify.label", <SendOutlinedIcon />],
|
||||||
[WorkflowNodeType.Branch, "workflow_node.branch.label", <SisternodeOutlinedIcon />],
|
[WorkflowNodeType.Branch, "workflow_node.branch.label", <SisternodeOutlinedIcon />],
|
||||||
[WorkflowNodeType.ExecuteResultBranch, "workflow_node.execute_result_branch.label", <SisternodeOutlinedIcon />],
|
[WorkflowNodeType.ExecuteResultBranch, "workflow_node.execute_result_branch.label", <SisternodeOutlinedIcon />],
|
||||||
]
|
]
|
||||||
.filter(([type]) => {
|
.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;
|
return type !== WorkflowNodeType.ExecuteResultBranch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,9 +38,9 @@ const ApplyNode = ({ node, disabled }: ApplyNodeProps) => {
|
|||||||
|
|
||||||
const formRef = useRef<ApplyNodeConfigFormInstance>(null);
|
const formRef = useRef<ApplyNodeConfigFormInstance>(null);
|
||||||
const [formPending, setFormPending] = useState(false);
|
const [formPending, setFormPending] = useState(false);
|
||||||
|
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForApply;
|
||||||
|
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForApply;
|
|
||||||
|
|
||||||
const handleDrawerConfirm = async () => {
|
const handleDrawerConfirm = async () => {
|
||||||
setFormPending(true);
|
setFormPending(true);
|
||||||
@ -74,12 +74,12 @@ const ApplyNode = ({ node, disabled }: ApplyNodeProps) => {
|
|||||||
</SharedNode.Block>
|
</SharedNode.Block>
|
||||||
|
|
||||||
<SharedNode.ConfigDrawer
|
<SharedNode.ConfigDrawer
|
||||||
|
getConfigNewValues={getFormValues}
|
||||||
node={node}
|
node={node}
|
||||||
open={drawerOpen}
|
open={drawerOpen}
|
||||||
pending={formPending}
|
pending={formPending}
|
||||||
onConfirm={handleDrawerConfirm}
|
onConfirm={handleDrawerConfirm}
|
||||||
onOpenChange={(open) => setDrawerOpen(open)}
|
onOpenChange={(open) => setDrawerOpen(open)}
|
||||||
getFormValues={() => formRef.current!.getFieldsValue()}
|
|
||||||
>
|
>
|
||||||
<ApplyNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} />
|
<ApplyNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} />
|
||||||
</SharedNode.ConfigDrawer>
|
</SharedNode.ConfigDrawer>
|
||||||
|
@ -56,7 +56,7 @@ export type ApplyNodeConfigFormInstance = {
|
|||||||
validateFields: FormInstance<ApplyNodeConfigFormFieldValues>["validateFields"];
|
validateFields: FormInstance<ApplyNodeConfigFormFieldValues>["validateFields"];
|
||||||
};
|
};
|
||||||
|
|
||||||
const MULTIPLE_INPUT_DELIMITER = ";";
|
const MULTIPLE_INPUT_SEPARATOR = ";";
|
||||||
|
|
||||||
const initFormModel = (): ApplyNodeConfigFormFieldValues => {
|
const initFormModel = (): ApplyNodeConfigFormFieldValues => {
|
||||||
return {
|
return {
|
||||||
@ -76,7 +76,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
|||||||
domains: z.string({ message: t("workflow_node.apply.form.domains.placeholder") }).refine((v) => {
|
domains: z.string({ message: t("workflow_node.apply.form.domains.placeholder") }).refine((v) => {
|
||||||
if (!v) return false;
|
if (!v) return false;
|
||||||
return String(v)
|
return String(v)
|
||||||
.split(MULTIPLE_INPUT_DELIMITER)
|
.split(MULTIPLE_INPUT_SEPARATOR)
|
||||||
.every((e) => validDomainName(e, { allowWildcard: true }));
|
.every((e) => validDomainName(e, { allowWildcard: true }));
|
||||||
}, t("common.errmsg.domain_invalid")),
|
}, t("common.errmsg.domain_invalid")),
|
||||||
contactEmail: z.string({ message: t("workflow_node.apply.form.contact_email.placeholder") }).email(t("common.errmsg.email_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<ApplyNodeConfigFormInstance, ApplyNodeCon
|
|||||||
.refine((v) => {
|
.refine((v) => {
|
||||||
if (!v) return true;
|
if (!v) return true;
|
||||||
return String(v)
|
return String(v)
|
||||||
.split(MULTIPLE_INPUT_DELIMITER)
|
.split(MULTIPLE_INPUT_SEPARATOR)
|
||||||
.every((e) => validIPv4Address(e) || validIPv6Address(e) || validDomainName(e));
|
.every((e) => validIPv4Address(e) || validIPv6Address(e) || validDomainName(e));
|
||||||
}, t("common.errmsg.host_invalid")),
|
}, t("common.errmsg.host_invalid")),
|
||||||
dnsPropagationWait: z.preprocess(
|
dnsPropagationWait: z.preprocess(
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
import { memo } from "react";
|
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 { Button, Card, Popover } from "antd";
|
||||||
|
import { produce } from "immer";
|
||||||
|
|
||||||
|
import { useZustandShallowSelector } from "@/hooks";
|
||||||
|
import { useWorkflowStore } from "@/stores/workflow";
|
||||||
|
|
||||||
import SharedNode, { type SharedNodeProps } from "./_SharedNode";
|
import SharedNode, { type SharedNodeProps } from "./_SharedNode";
|
||||||
import AddNode from "./AddNode";
|
import AddNode from "./AddNode";
|
||||||
|
import ConditionNodeConfigForm, { type ConditionNodeConfigFormFieldValues, type ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm";
|
||||||
|
|
||||||
export type ConditionNodeProps = SharedNodeProps & {
|
export type ConditionNodeProps = SharedNodeProps & {
|
||||||
branchId: string;
|
branchId: string;
|
||||||
@ -11,12 +16,41 @@ export type ConditionNodeProps = SharedNodeProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeProps) => {
|
const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeProps) => {
|
||||||
// TODO: 条件分支
|
const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"]));
|
||||||
|
|
||||||
|
const [formPending, setFormPending] = useState(false);
|
||||||
|
const formRef = useRef<ConditionNodeConfigFormInstance>(null);
|
||||||
|
const getFormValues = () => formRef.current!.getFieldsValue() as ConditionNodeConfigFormFieldValues;
|
||||||
|
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleDrawerConfirm = async () => {
|
||||||
|
setFormPending(true);
|
||||||
|
try {
|
||||||
|
await formRef.current!.validateFields();
|
||||||
|
} catch (err) {
|
||||||
|
setFormPending(false);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newValues = getFormValues();
|
||||||
|
const newNode = produce(node, (draft) => {
|
||||||
|
draft.config = {
|
||||||
|
...newValues,
|
||||||
|
};
|
||||||
|
draft.validated = true;
|
||||||
|
});
|
||||||
|
await updateNode(newNode);
|
||||||
|
} finally {
|
||||||
|
setFormPending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popover
|
<Popover
|
||||||
classNames={{ root: "shadow-md" }}
|
classNames={{ root: "mt-20 shadow-md" }}
|
||||||
styles={{ body: { padding: 0 } }}
|
styles={{ body: { padding: 0 } }}
|
||||||
arrow={false}
|
arrow={false}
|
||||||
content={
|
content={
|
||||||
@ -30,17 +64,37 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP
|
|||||||
}
|
}
|
||||||
placement="rightTop"
|
placement="rightTop"
|
||||||
>
|
>
|
||||||
<Card className="relative z-[1] mt-10 w-[256px] shadow-md" styles={{ body: { padding: 0 } }} hoverable>
|
<Card className="relative z-[1] mt-10 w-[256px] shadow-md" styles={{ body: { padding: 0 } }} hoverable onClick={() => setDrawerOpen(true)}>
|
||||||
<div className="flex h-[48px] flex-col items-center justify-center truncate px-4 py-2">
|
<div className="flex h-[48px] flex-col items-center justify-center truncate px-4 py-2">
|
||||||
<SharedNode.Title
|
<div className="relative w-full overflow-hidden" onClick={(e) => e.stopPropagation()}>
|
||||||
className="focus:bg-background focus:text-foreground overflow-hidden outline-slate-200 focus:rounded-sm"
|
<SharedNode.Title
|
||||||
node={node}
|
className="focus:bg-background focus:text-foreground overflow-hidden outline-slate-200 focus:rounded-sm"
|
||||||
disabled={disabled}
|
node={node}
|
||||||
/>
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<div className="absolute right-0 top-1/2 -translate-y-1/2" onClick={() => setDrawerOpen(true)}>
|
||||||
|
{node.config?.expression ? (
|
||||||
|
<Button color="primary" icon={<FilterFilledIcon />} variant="link" />
|
||||||
|
) : (
|
||||||
|
<Button color="default" icon={<FilterOutlinedIcon />} variant="link" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
|
<SharedNode.ConfigDrawer
|
||||||
|
getConfigNewValues={getFormValues}
|
||||||
|
node={node}
|
||||||
|
open={drawerOpen}
|
||||||
|
pending={formPending}
|
||||||
|
onConfirm={handleDrawerConfirm}
|
||||||
|
onOpenChange={(open) => setDrawerOpen(open)}
|
||||||
|
>
|
||||||
|
<ConditionNodeConfigForm nodeId={node.id} ref={formRef} disabled={disabled} initialValues={node.config} />
|
||||||
|
</SharedNode.ConfigDrawer>
|
||||||
|
|
||||||
<AddNode node={node} disabled={disabled} />
|
<AddNode node={node} disabled={disabled} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
76
ui/src/components/workflow/node/ConditionNodeConfigForm.tsx
Normal file
76
ui/src/components/workflow/node/ConditionNodeConfigForm.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { forwardRef, memo, useImperativeHandle, useRef } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Form, type FormInstance } from "antd";
|
||||||
|
import { createSchemaFieldRule } from "antd-zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { type Expr, type WorkflowNodeConfigForCondition } from "@/domain/workflow";
|
||||||
|
import { useAntdForm } from "@/hooks";
|
||||||
|
|
||||||
|
import ConditionNodeConfigFormExpressionEditor, { type ConditionNodeConfigFormExpressionEditorInstance } from "./ConditionNodeConfigFormExpressionEditor";
|
||||||
|
|
||||||
|
export type ConditionNodeConfigFormFieldValues = {
|
||||||
|
expression?: Expr | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConditionNodeConfigFormProps = {
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
disabled?: boolean;
|
||||||
|
initialValues?: Partial<WorkflowNodeConfigForCondition>;
|
||||||
|
nodeId: string;
|
||||||
|
onValuesChange?: (values: WorkflowNodeConfigForCondition) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConditionNodeConfigFormInstance = {
|
||||||
|
getFieldsValue: () => ReturnType<FormInstance<ConditionNodeConfigFormFieldValues>["getFieldsValue"]>;
|
||||||
|
resetFields: FormInstance<ConditionNodeConfigFormFieldValues>["resetFields"];
|
||||||
|
validateFields: FormInstance<ConditionNodeConfigFormFieldValues>["validateFields"];
|
||||||
|
};
|
||||||
|
|
||||||
|
const initFormModel = (): ConditionNodeConfigFormFieldValues => {
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConditionNodeConfigForm = forwardRef<ConditionNodeConfigFormInstance, ConditionNodeConfigFormProps>(
|
||||||
|
({ className, style, disabled, initialValues, nodeId, onValuesChange }, ref) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
expression: z.any().nullish(),
|
||||||
|
});
|
||||||
|
const formRule = createSchemaFieldRule(formSchema);
|
||||||
|
const { form: formInst, formProps } = useAntdForm({
|
||||||
|
name: "workflowNodeConditionConfigForm",
|
||||||
|
initialValues: initialValues ?? initFormModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const editorRef = useRef<ConditionNodeConfigFormExpressionEditorInstance>(null);
|
||||||
|
|
||||||
|
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
|
||||||
|
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 (
|
||||||
|
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
|
||||||
|
<Form.Item name="expression" label={t("workflow_node.condition.form.expression.label")} rules={[formRule]}>
|
||||||
|
<ConditionNodeConfigFormExpressionEditor ref={editorRef} nodeId={nodeId} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default memo(ConditionNodeConfigForm);
|
@ -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<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 表单内部使用的扁平结构
|
||||||
|
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<ConditionNodeConfigFormExpressionEditorInstance, ConditionNodeConfigFormExpressionEditorProps>(
|
||||||
|
({ className, style, disabled, nodeId, ...props }, ref) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { token: themeToken } = theme.useToken();
|
||||||
|
|
||||||
|
const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"]));
|
||||||
|
|
||||||
|
const [value, setValue] = useControllableValue<Expr | undefined>(props, {
|
||||||
|
valuePropName: "value",
|
||||||
|
defaultValuePropName: "defaultValue",
|
||||||
|
trigger: "onChange",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [formInst] = Form.useForm<ConditionFormValues>();
|
||||||
|
const formName = useAntdFormName({ form: formInst, name: "workflowNodeConditionConfigFormExpressionEditorForm" });
|
||||||
|
const [formModel, setFormModel] = useState<ConditionFormValues>(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 (
|
||||||
|
<Form
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
form={formInst}
|
||||||
|
disabled={disabled}
|
||||||
|
initialValues={formModel}
|
||||||
|
layout="vertical"
|
||||||
|
name={formName}
|
||||||
|
onValuesChange={handleFormChange}
|
||||||
|
>
|
||||||
|
<Show when={formModel.conditions?.length > 1}>
|
||||||
|
<Form.Item
|
||||||
|
className="mb-2"
|
||||||
|
name="logicalOperator"
|
||||||
|
rules={[{ required: true, message: t("workflow_node.condition.form.expression.logical_operator.errmsg") }]}
|
||||||
|
>
|
||||||
|
<Radio.Group block>
|
||||||
|
<Radio.Button value="and">{t("workflow_node.condition.form.expression.logical_operator.option.and.label")}</Radio.Button>
|
||||||
|
<Radio.Button value="or">{t("workflow_node.condition.form.expression.logical_operator.option.or.label")}</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Form.List name="conditions">
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{fields.map(({ key, name: index, ...rest }) => (
|
||||||
|
<div key={key} className="flex gap-2">
|
||||||
|
{/* 左:变量选择器 */}
|
||||||
|
<Form.Item
|
||||||
|
className="mb-0 flex-1"
|
||||||
|
name={[index, "leftSelector"]}
|
||||||
|
rules={[{ required: true, message: t("workflow_node.condition.form.expression.variable.errmsg") }]}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
labelRender={({ label, value }) => {
|
||||||
|
if (value != null) {
|
||||||
|
const group = ciSelectorCandidates.find((group) => group.options.some((option) => option.value === value));
|
||||||
|
return `${group?.label} - ${label}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span style={{ color: themeToken.colorTextPlaceholder }}>{t("workflow_node.condition.form.expression.variable.placeholder")}</span>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
options={ciSelectorCandidates}
|
||||||
|
placeholder={t("workflow_node.condition.form.expression.variable.placeholder")}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* 中:运算符选择器,根据变量类型决定选项 */}
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
shouldUpdate={(prevValues, currentValues) => {
|
||||||
|
return prevValues.conditions?.[index]?.leftSelector !== currentValues.conditions?.[index]?.leftSelector;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ getFieldValue }) => {
|
||||||
|
const leftSelector = getFieldValue(["conditions", index, "leftSelector"]);
|
||||||
|
const operators = getOperatorsBySelector(leftSelector);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
className="mb-0 w-36"
|
||||||
|
name={[index, "operator"]}
|
||||||
|
rules={[{ required: true, message: t("workflow_node.condition.form.expression.operator.errmsg") }]}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
open={operators.length === 0 ? false : undefined}
|
||||||
|
options={operators}
|
||||||
|
placeholder={t("workflow_node.condition.form.expression.operator.placeholder")}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* 右:输入控件,根据变量类型决定组件 */}
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
shouldUpdate={(prevValues, currentValues) => {
|
||||||
|
return prevValues.conditions?.[index]?.leftSelector !== currentValues.conditions?.[index]?.leftSelector;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ getFieldValue }) => {
|
||||||
|
const leftSelector = getFieldValue(["conditions", index, "leftSelector"]);
|
||||||
|
const valueType = getValueTypeBySelector(leftSelector);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
className="mb-0 w-36"
|
||||||
|
name={[index, "rightValue"]}
|
||||||
|
rules={[{ required: true, message: t("workflow_node.condition.form.expression.value.errmsg") }]}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{valueType === "string" ? (
|
||||||
|
<Input placeholder={t("workflow_node.condition.form.expression.value.placeholder")} />
|
||||||
|
) : valueType === "number" ? (
|
||||||
|
<Input type="number" placeholder={t("workflow_node.condition.form.expression.value.placeholder")} />
|
||||||
|
) : valueType === "boolean" ? (
|
||||||
|
<Select placeholder={t("workflow_node.condition.form.expression.value.placeholder")}>
|
||||||
|
<Select.Option value="true">{t("workflow_node.condition.form.expression.value.option.true.label")}</Select.Option>
|
||||||
|
<Select.Option value="false">{t("workflow_node.condition.form.expression.value.option.false.label")}</Select.Option>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<Input readOnly placeholder={t("workflow_node.condition.form.expression.value.placeholder")} />
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="my-1"
|
||||||
|
color="default"
|
||||||
|
disabled={disabled}
|
||||||
|
icon={<CloseOutlinedIcon />}
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="dashed" block icon={<PlusOutlined />} onClick={() => add({})}>
|
||||||
|
{t("workflow_node.condition.form.expression.add_condition.button")}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ConditionNodeConfigFormExpressionEditor;
|
@ -24,10 +24,10 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => {
|
|||||||
|
|
||||||
const formRef = useRef<DeployNodeConfigFormInstance>(null);
|
const formRef = useRef<DeployNodeConfigFormInstance>(null);
|
||||||
const [formPending, setFormPending] = useState(false);
|
const [formPending, setFormPending] = useState(false);
|
||||||
|
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForDeploy;
|
||||||
|
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const [drawerFooterShow, setDrawerFooterShow] = useState(true);
|
const [drawerFooterShow, setDrawerFooterShow] = useState(true);
|
||||||
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForDeploy;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDrawerFooterShow(!!(node.config as WorkflowNodeConfigForDeploy)?.provider);
|
setDrawerFooterShow(!!(node.config as WorkflowNodeConfigForDeploy)?.provider);
|
||||||
@ -46,7 +46,7 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => {
|
|||||||
const provider = deploymentProvidersMap.get(config.provider);
|
const provider = deploymentProvidersMap.get(config.provider);
|
||||||
return (
|
return (
|
||||||
<Flex className="size-full overflow-hidden" align="center" gap={8}>
|
<Flex className="size-full overflow-hidden" align="center" gap={8}>
|
||||||
<Avatar src={provider?.icon} size="small" />
|
<Avatar shape="square" src={provider?.icon} size="small" />
|
||||||
<Typography.Text className="flex-1 truncate">{t(provider?.name ?? "")}</Typography.Text>
|
<Typography.Text className="flex-1 truncate">{t(provider?.name ?? "")}</Typography.Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
@ -86,8 +86,9 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => {
|
|||||||
</SharedNode.Block>
|
</SharedNode.Block>
|
||||||
|
|
||||||
<SharedNode.ConfigDrawer
|
<SharedNode.ConfigDrawer
|
||||||
node={node}
|
|
||||||
footer={drawerFooterShow}
|
footer={drawerFooterShow}
|
||||||
|
getConfigNewValues={getFormValues}
|
||||||
|
node={node}
|
||||||
open={drawerOpen}
|
open={drawerOpen}
|
||||||
pending={formPending}
|
pending={formPending}
|
||||||
onConfirm={handleDrawerConfirm}
|
onConfirm={handleDrawerConfirm}
|
||||||
@ -95,7 +96,6 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => {
|
|||||||
setDrawerFooterShow(!!(node.config as WorkflowNodeConfigForDeploy)?.provider);
|
setDrawerFooterShow(!!(node.config as WorkflowNodeConfigForDeploy)?.provider);
|
||||||
setDrawerOpen(open);
|
setDrawerOpen(open);
|
||||||
}}
|
}}
|
||||||
getFormValues={() => formRef.current!.getFieldsValue()}
|
|
||||||
>
|
>
|
||||||
<DeployNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} nodeId={node.id} onValuesChange={handleFormValuesChange} />
|
<DeployNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} nodeId={node.id} onValuesChange={handleFormValuesChange} />
|
||||||
</SharedNode.ConfigDrawer>
|
</SharedNode.ConfigDrawer>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } from "react";
|
import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons";
|
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 { createSchemaFieldRule } from "antd-zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ import DeploymentProviderPicker from "@/components/provider/DeploymentProviderPi
|
|||||||
import DeploymentProviderSelect from "@/components/provider/DeploymentProviderSelect.tsx";
|
import DeploymentProviderSelect from "@/components/provider/DeploymentProviderSelect.tsx";
|
||||||
import Show from "@/components/Show";
|
import Show from "@/components/Show";
|
||||||
import { ACCESS_USAGES, DEPLOYMENT_PROVIDERS, accessProvidersMap, deploymentProvidersMap } from "@/domain/provider";
|
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 { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks";
|
||||||
import { useWorkflowStore } from "@/stores/workflow";
|
import { useWorkflowStore } from "@/stores/workflow";
|
||||||
|
|
||||||
@ -125,14 +125,9 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
|
|||||||
({ className, style, disabled, initialValues, nodeId, onValuesChange }, ref) => {
|
({ className, style, disabled, initialValues, nodeId, onValuesChange }, ref) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"]));
|
const { token: themeToken } = theme.useToken();
|
||||||
|
|
||||||
// TODO: 优化此处逻辑
|
const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"]));
|
||||||
const [previousNodes, setPreviousNodes] = useState<WorkflowNode[]>([]);
|
|
||||||
useEffect(() => {
|
|
||||||
const previousNodes = getWorkflowOuptutBeforeId(nodeId, "certificate");
|
|
||||||
setPreviousNodes(previousNodes);
|
|
||||||
}, [nodeId]);
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
certificate: z
|
certificate: z
|
||||||
@ -170,6 +165,24 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
|
|||||||
}
|
}
|
||||||
}, [fieldProvider]);
|
}, [fieldProvider]);
|
||||||
|
|
||||||
|
const certificateCandidates = useMemo(() => {
|
||||||
|
const previousNodes = getWorkflowOuptutBeforeId(nodeId, "certificate");
|
||||||
|
return previousNodes
|
||||||
|
.filter((node) => node.type === WorkflowNodeType.Apply || node.type === WorkflowNodeType.Upload)
|
||||||
|
.map((item) => {
|
||||||
|
return {
|
||||||
|
label: item.name,
|
||||||
|
options: (item.outputs ?? [])?.map((output) => {
|
||||||
|
return {
|
||||||
|
label: output.label,
|
||||||
|
value: `${item.id}#${output.name}`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((group) => group.options.length > 0);
|
||||||
|
}, [nodeId]);
|
||||||
|
|
||||||
const [nestedFormInst] = Form.useForm();
|
const [nestedFormInst] = Form.useForm();
|
||||||
const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "workflowNodeDeployConfigFormProviderConfigForm" });
|
const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "workflowNodeDeployConfigFormProviderConfigForm" });
|
||||||
const nestedFormEl = useMemo(() => {
|
const nestedFormEl = useMemo(() => {
|
||||||
@ -487,17 +500,15 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
|
|||||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.certificate.tooltip") }}></span>}
|
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.certificate.tooltip") }}></span>}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
options={previousNodes.map((item) => {
|
labelRender={({ label, value }) => {
|
||||||
return {
|
if (value != null) {
|
||||||
label: item.name,
|
const group = certificateCandidates.find((group) => group.options.some((option) => option.value === value));
|
||||||
options: item.outputs?.map((output) => {
|
return `${group?.label} - ${label}`;
|
||||||
return {
|
}
|
||||||
label: `${item.name} - ${output.label}`,
|
|
||||||
value: `${item.id}#${output.name}`,
|
return <span style={{ color: themeToken.colorTextPlaceholder }}>{t("workflow_node.deploy.form.certificate.placeholder")}</span>;
|
||||||
};
|
}}
|
||||||
}),
|
options={certificateCandidates}
|
||||||
};
|
|
||||||
})}
|
|
||||||
placeholder={t("workflow_node.deploy.form.certificate.placeholder")}
|
placeholder={t("workflow_node.deploy.form.certificate.placeholder")}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
@ -19,7 +19,7 @@ export type DeployNodeConfigFormAliyunCASDeployConfigProps = {
|
|||||||
onValuesChange?: (values: DeployNodeConfigFormAliyunCASDeployConfigFieldValues) => void;
|
onValuesChange?: (values: DeployNodeConfigFormAliyunCASDeployConfigFieldValues) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MULTIPLE_INPUT_DELIMITER = ";";
|
const MULTIPLE_INPUT_SEPARATOR = ";";
|
||||||
|
|
||||||
const initFormModel = (): DeployNodeConfigFormAliyunCASDeployConfigFieldValues => {
|
const initFormModel = (): DeployNodeConfigFormAliyunCASDeployConfigFieldValues => {
|
||||||
return {};
|
return {};
|
||||||
@ -42,7 +42,7 @@ const DeployNodeConfigFormAliyunCASDeployConfig = ({
|
|||||||
resourceIds: z.string({ message: t("workflow_node.deploy.form.aliyun_cas_deploy_resource_ids.placeholder") }).refine((v) => {
|
resourceIds: z.string({ message: t("workflow_node.deploy.form.aliyun_cas_deploy_resource_ids.placeholder") }).refine((v) => {
|
||||||
if (!v) return false;
|
if (!v) return false;
|
||||||
return String(v)
|
return String(v)
|
||||||
.split(MULTIPLE_INPUT_DELIMITER)
|
.split(MULTIPLE_INPUT_SEPARATOR)
|
||||||
.every((e) => /^[1-9]\d*$/.test(e));
|
.every((e) => /^[1-9]\d*$/.test(e));
|
||||||
}, t("workflow_node.deploy.form.aliyun_cas_deploy_resource_ids.errmsg.invalid")),
|
}, t("workflow_node.deploy.form.aliyun_cas_deploy_resource_ids.errmsg.invalid")),
|
||||||
contactIds: z
|
contactIds: z
|
||||||
@ -51,7 +51,7 @@ const DeployNodeConfigFormAliyunCASDeployConfig = ({
|
|||||||
.refine((v) => {
|
.refine((v) => {
|
||||||
if (!v) return true;
|
if (!v) return true;
|
||||||
return String(v)
|
return String(v)
|
||||||
.split(MULTIPLE_INPUT_DELIMITER)
|
.split(MULTIPLE_INPUT_SEPARATOR)
|
||||||
.every((e) => /^[1-9]\d*$/.test(e));
|
.every((e) => /^[1-9]\d*$/.test(e));
|
||||||
}, t("workflow_node.deploy.form.aliyun_cas_deploy_contact_ids.errmsg.invalid")),
|
}, t("workflow_node.deploy.form.aliyun_cas_deploy_contact_ids.errmsg.invalid")),
|
||||||
});
|
});
|
||||||
|
@ -23,7 +23,7 @@ export type DeployNodeConfigFormBaotaPanelSiteConfigProps = {
|
|||||||
const SITE_TYPE_PHP = "php";
|
const SITE_TYPE_PHP = "php";
|
||||||
const SITE_TYPE_OTHER = "other";
|
const SITE_TYPE_OTHER = "other";
|
||||||
|
|
||||||
const MULTIPLE_INPUT_DELIMITER = ";";
|
const MULTIPLE_INPUT_SEPARATOR = ";";
|
||||||
|
|
||||||
const initFormModel = (): DeployNodeConfigFormBaotaPanelSiteConfigFieldValues => {
|
const initFormModel = (): DeployNodeConfigFormBaotaPanelSiteConfigFieldValues => {
|
||||||
return {
|
return {
|
||||||
@ -60,7 +60,7 @@ const DeployNodeConfigFormBaotaPanelSiteConfig = ({
|
|||||||
if (fieldSiteType !== SITE_TYPE_OTHER) return true;
|
if (fieldSiteType !== SITE_TYPE_OTHER) return true;
|
||||||
if (!v) return false;
|
if (!v) return false;
|
||||||
return String(v)
|
return String(v)
|
||||||
.split(MULTIPLE_INPUT_DELIMITER)
|
.split(MULTIPLE_INPUT_SEPARATOR)
|
||||||
.every((e) => !!e.trim());
|
.every((e) => !!e.trim());
|
||||||
}, t("workflow_node.deploy.form.baotapanel_site_names.placeholder")),
|
}, t("workflow_node.deploy.form.baotapanel_site_names.placeholder")),
|
||||||
});
|
});
|
||||||
|
@ -19,7 +19,7 @@ export type DeployNodeConfigFormTencentCloudSSLDeployConfigProps = {
|
|||||||
onValuesChange?: (values: DeployNodeConfigFormTencentCloudSSLDeployConfigFieldValues) => void;
|
onValuesChange?: (values: DeployNodeConfigFormTencentCloudSSLDeployConfigFieldValues) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MULTIPLE_INPUT_DELIMITER = ";";
|
const MULTIPLE_INPUT_SEPARATOR = ";";
|
||||||
|
|
||||||
const initFormModel = (): DeployNodeConfigFormTencentCloudSSLDeployConfigFieldValues => {
|
const initFormModel = (): DeployNodeConfigFormTencentCloudSSLDeployConfigFieldValues => {
|
||||||
return {};
|
return {};
|
||||||
@ -46,7 +46,7 @@ const DeployNodeConfigFormTencentCloudSSLDeployConfig = ({
|
|||||||
resourceIds: z.string({ message: t("workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.placeholder") }).refine((v) => {
|
resourceIds: z.string({ message: t("workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.placeholder") }).refine((v) => {
|
||||||
if (!v) return false;
|
if (!v) return false;
|
||||||
return String(v)
|
return String(v)
|
||||||
.split(MULTIPLE_INPUT_DELIMITER)
|
.split(MULTIPLE_INPUT_SEPARATOR)
|
||||||
.every((e) => /^[A-Za-z0-9*._-|]+$/.test(e));
|
.every((e) => /^[A-Za-z0-9*._-|]+$/.test(e));
|
||||||
}, t("workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.errmsg.invalid")),
|
}, t("workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.errmsg.invalid")),
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Form, type FormInstance, Input, Select } from "antd";
|
import { Alert, Form, type FormInstance, Input, Select } from "antd";
|
||||||
import { createSchemaFieldRule } from "antd-zod";
|
import { createSchemaFieldRule } from "antd-zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@ -56,6 +56,10 @@ const DeployNodeConfigFormUniCloudWebHostConfig = ({
|
|||||||
name={formName}
|
name={formName}
|
||||||
onValuesChange={handleFormChange}
|
onValuesChange={handleFormChange}
|
||||||
>
|
>
|
||||||
|
<Form.Item>
|
||||||
|
<Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.unicloud_webhost.guide") }}></span>} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="spaceProvider" label={t("workflow_node.deploy.form.unicloud_webhost_space_provider.label")} rules={[formRule]}>
|
<Form.Item name="spaceProvider" label={t("workflow_node.deploy.form.unicloud_webhost_space_provider.label")} rules={[formRule]}>
|
||||||
<Select
|
<Select
|
||||||
options={["aliyun", "tencent"].map((s) => ({
|
options={["aliyun", "tencent"].map((s) => ({
|
||||||
|
@ -18,7 +18,7 @@ export type DeployNodeConfigFormWangsuCDNConfigProps = {
|
|||||||
onValuesChange?: (values: DeployNodeConfigFormWangsuCDNConfigFieldValues) => void;
|
onValuesChange?: (values: DeployNodeConfigFormWangsuCDNConfigFieldValues) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MULTIPLE_INPUT_DELIMITER = ";";
|
const MULTIPLE_INPUT_SEPARATOR = ";";
|
||||||
|
|
||||||
const initFormModel = (): DeployNodeConfigFormWangsuCDNConfigFieldValues => {
|
const initFormModel = (): DeployNodeConfigFormWangsuCDNConfigFieldValues => {
|
||||||
return {
|
return {
|
||||||
@ -42,7 +42,7 @@ const DeployNodeConfigFormWangsuCDNConfig = ({
|
|||||||
.refine((v) => {
|
.refine((v) => {
|
||||||
if (!v) return false;
|
if (!v) return false;
|
||||||
return String(v)
|
return String(v)
|
||||||
.split(MULTIPLE_INPUT_DELIMITER)
|
.split(MULTIPLE_INPUT_SEPARATOR)
|
||||||
.every((e) => validDomainName(e));
|
.every((e) => validDomainName(e));
|
||||||
}, t("workflow_node.deploy.form.wangsu_cdn_domains.placeholder")),
|
}, t("workflow_node.deploy.form.wangsu_cdn_domains.placeholder")),
|
||||||
});
|
});
|
||||||
|
90
ui/src/components/workflow/node/MonitorNode.tsx
Normal file
90
ui/src/components/workflow/node/MonitorNode.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { memo, useMemo, useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Flex, Typography } from "antd";
|
||||||
|
import { produce } from "immer";
|
||||||
|
|
||||||
|
import { type WorkflowNodeConfigForMonitor, WorkflowNodeType } from "@/domain/workflow";
|
||||||
|
import { useZustandShallowSelector } from "@/hooks";
|
||||||
|
import { useWorkflowStore } from "@/stores/workflow";
|
||||||
|
|
||||||
|
import SharedNode, { type SharedNodeProps } from "./_SharedNode";
|
||||||
|
import MonitorNodeConfigForm, { type MonitorNodeConfigFormInstance } from "./MonitorNodeConfigForm";
|
||||||
|
|
||||||
|
export type MonitorNodeProps = SharedNodeProps;
|
||||||
|
|
||||||
|
const MonitorNode = ({ node, disabled }: MonitorNodeProps) => {
|
||||||
|
if (node.type !== WorkflowNodeType.Monitor) {
|
||||||
|
console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Monitor}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"]));
|
||||||
|
|
||||||
|
const formRef = useRef<MonitorNodeConfigFormInstance>(null);
|
||||||
|
const [formPending, setFormPending] = useState(false);
|
||||||
|
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForMonitor;
|
||||||
|
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
|
||||||
|
const wrappedEl = useMemo(() => {
|
||||||
|
if (node.type !== WorkflowNodeType.Monitor) {
|
||||||
|
console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Monitor}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node.validated) {
|
||||||
|
return <Typography.Link>{t("workflow_node.action.configure_node")}</Typography.Link>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = (node.config as WorkflowNodeConfigForMonitor) ?? {};
|
||||||
|
return (
|
||||||
|
<Flex className="size-full overflow-hidden" align="center" gap={8}>
|
||||||
|
<Typography.Text className="truncate">{config.domain || config.host || ""}</Typography.Text>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}, [node]);
|
||||||
|
|
||||||
|
const handleDrawerConfirm = async () => {
|
||||||
|
setFormPending(true);
|
||||||
|
try {
|
||||||
|
await formRef.current!.validateFields();
|
||||||
|
} catch (err) {
|
||||||
|
setFormPending(false);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newValues = getFormValues();
|
||||||
|
const newNode = produce(node, (draft) => {
|
||||||
|
draft.config = {
|
||||||
|
...newValues,
|
||||||
|
};
|
||||||
|
draft.validated = true;
|
||||||
|
});
|
||||||
|
await updateNode(newNode);
|
||||||
|
} finally {
|
||||||
|
setFormPending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SharedNode.Block node={node} disabled={disabled} onClick={() => setDrawerOpen(true)}>
|
||||||
|
{wrappedEl}
|
||||||
|
</SharedNode.Block>
|
||||||
|
|
||||||
|
<SharedNode.ConfigDrawer
|
||||||
|
getConfigNewValues={getFormValues}
|
||||||
|
node={node}
|
||||||
|
open={drawerOpen}
|
||||||
|
pending={formPending}
|
||||||
|
onConfirm={handleDrawerConfirm}
|
||||||
|
onOpenChange={(open) => setDrawerOpen(open)}
|
||||||
|
>
|
||||||
|
<MonitorNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} />
|
||||||
|
</SharedNode.ConfigDrawer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(MonitorNode);
|
115
ui/src/components/workflow/node/MonitorNodeConfigForm.tsx
Normal file
115
ui/src/components/workflow/node/MonitorNodeConfigForm.tsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import { forwardRef, memo, useImperativeHandle } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Alert, Form, type FormInstance, Input, InputNumber } from "antd";
|
||||||
|
import { createSchemaFieldRule } from "antd-zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { type WorkflowNodeConfigForMonitor } from "@/domain/workflow";
|
||||||
|
import { useAntdForm } from "@/hooks";
|
||||||
|
import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators";
|
||||||
|
|
||||||
|
type MonitorNodeConfigFormFieldValues = Partial<WorkflowNodeConfigForMonitor>;
|
||||||
|
|
||||||
|
export type MonitorNodeConfigFormProps = {
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
disabled?: boolean;
|
||||||
|
initialValues?: MonitorNodeConfigFormFieldValues;
|
||||||
|
onValuesChange?: (values: MonitorNodeConfigFormFieldValues) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MonitorNodeConfigFormInstance = {
|
||||||
|
getFieldsValue: () => ReturnType<FormInstance<MonitorNodeConfigFormFieldValues>["getFieldsValue"]>;
|
||||||
|
resetFields: FormInstance<MonitorNodeConfigFormFieldValues>["resetFields"];
|
||||||
|
validateFields: FormInstance<MonitorNodeConfigFormFieldValues>["validateFields"];
|
||||||
|
};
|
||||||
|
|
||||||
|
const initFormModel = (): MonitorNodeConfigFormFieldValues => {
|
||||||
|
return {
|
||||||
|
host: "",
|
||||||
|
port: 443,
|
||||||
|
requestPath: "/",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const MonitorNodeConfigForm = forwardRef<MonitorNodeConfigFormInstance, MonitorNodeConfigFormProps>(
|
||||||
|
({ className, style, disabled, initialValues, onValuesChange }, ref) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
host: z.string().refine((v) => {
|
||||||
|
return validDomainName(v) || validIPv4Address(v) || validIPv6Address(v);
|
||||||
|
}, t("common.errmsg.host_invalid")),
|
||||||
|
port: z.preprocess(
|
||||||
|
(v) => Number(v),
|
||||||
|
z
|
||||||
|
.number()
|
||||||
|
.int(t("workflow_node.monitor.form.port.placeholder"))
|
||||||
|
.refine((v) => validPortNumber(v), t("common.errmsg.port_invalid"))
|
||||||
|
),
|
||||||
|
domain: z
|
||||||
|
.string()
|
||||||
|
.nullish()
|
||||||
|
.refine((v) => {
|
||||||
|
if (!v) return true;
|
||||||
|
return validDomainName(v);
|
||||||
|
}, t("common.errmsg.domain_invalid")),
|
||||||
|
requestPath: z.string().nullish(),
|
||||||
|
});
|
||||||
|
const formRule = createSchemaFieldRule(formSchema);
|
||||||
|
const { form: formInst, formProps } = useAntdForm({
|
||||||
|
name: "workflowNodeMonitorConfigForm",
|
||||||
|
initialValues: initialValues ?? initFormModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
|
||||||
|
onValuesChange?.(values as MonitorNodeConfigFormFieldValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => {
|
||||||
|
return {
|
||||||
|
getFieldsValue: () => {
|
||||||
|
return formInst.getFieldsValue(true);
|
||||||
|
},
|
||||||
|
resetFields: (fields) => {
|
||||||
|
return formInst.resetFields(fields as (keyof MonitorNodeConfigFormFieldValues)[]);
|
||||||
|
},
|
||||||
|
validateFields: (nameList, config) => {
|
||||||
|
return formInst.validateFields(nameList, config);
|
||||||
|
},
|
||||||
|
} as MonitorNodeConfigFormInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
|
||||||
|
<Form.Item>
|
||||||
|
<Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.monitor.form.guide") }}></span>} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<div className="w-2/3">
|
||||||
|
<Form.Item name="host" label={t("workflow_node.monitor.form.host.label")} rules={[formRule]}>
|
||||||
|
<Input placeholder={t("workflow_node.monitor.form.host.placeholder")} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-1/3">
|
||||||
|
<Form.Item name="port" label={t("workflow_node.monitor.form.port.label")} rules={[formRule]}>
|
||||||
|
<InputNumber className="w-full" min={1} max={65535} placeholder={t("workflow_node.monitor.form.port.placeholder")} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form.Item name="domain" label={t("workflow_node.monitor.form.domain.label")} rules={[formRule]}>
|
||||||
|
<Input placeholder={t("workflow_node.monitor.form.domain.placeholder")} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="requestPath" label={t("workflow_node.monitor.form.request_path.label")} rules={[formRule]}>
|
||||||
|
<Input placeholder={t("workflow_node.monitor.form.request_path.placeholder")} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default memo(MonitorNodeConfigForm);
|
@ -25,9 +25,9 @@ const NotifyNode = ({ node, disabled }: NotifyNodeProps) => {
|
|||||||
|
|
||||||
const formRef = useRef<NotifyNodeConfigFormInstance>(null);
|
const formRef = useRef<NotifyNodeConfigFormInstance>(null);
|
||||||
const [formPending, setFormPending] = useState(false);
|
const [formPending, setFormPending] = useState(false);
|
||||||
|
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForNotify;
|
||||||
|
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForNotify;
|
|
||||||
|
|
||||||
const wrappedEl = useMemo(() => {
|
const wrappedEl = useMemo(() => {
|
||||||
if (node.type !== WorkflowNodeType.Notify) {
|
if (node.type !== WorkflowNodeType.Notify) {
|
||||||
@ -43,7 +43,7 @@ const NotifyNode = ({ node, disabled }: NotifyNodeProps) => {
|
|||||||
const provider = notificationProvidersMap.get(config.provider);
|
const provider = notificationProvidersMap.get(config.provider);
|
||||||
return (
|
return (
|
||||||
<Flex className="size-full overflow-hidden" align="center" gap={8}>
|
<Flex className="size-full overflow-hidden" align="center" gap={8}>
|
||||||
<Avatar src={provider?.icon} size="small" />
|
<Avatar shape="square" src={provider?.icon} size="small" />
|
||||||
<Typography.Text className="flex-1 truncate">{t(channel?.name ?? provider?.name ?? " ")}</Typography.Text>
|
<Typography.Text className="flex-1 truncate">{t(channel?.name ?? provider?.name ?? " ")}</Typography.Text>
|
||||||
<Typography.Text className="truncate" type="secondary">
|
<Typography.Text className="truncate" type="secondary">
|
||||||
{config.subject ?? ""}
|
{config.subject ?? ""}
|
||||||
@ -82,12 +82,12 @@ const NotifyNode = ({ node, disabled }: NotifyNodeProps) => {
|
|||||||
</SharedNode.Block>
|
</SharedNode.Block>
|
||||||
|
|
||||||
<SharedNode.ConfigDrawer
|
<SharedNode.ConfigDrawer
|
||||||
|
getConfigNewValues={getFormValues}
|
||||||
node={node}
|
node={node}
|
||||||
open={drawerOpen}
|
open={drawerOpen}
|
||||||
pending={formPending}
|
pending={formPending}
|
||||||
onConfirm={handleDrawerConfirm}
|
onConfirm={handleDrawerConfirm}
|
||||||
onOpenChange={(open) => setDrawerOpen(open)}
|
onOpenChange={(open) => setDrawerOpen(open)}
|
||||||
getFormValues={() => formRef.current!.getFieldsValue()}
|
|
||||||
>
|
>
|
||||||
<NotifyNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} />
|
<NotifyNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} />
|
||||||
</SharedNode.ConfigDrawer>
|
</SharedNode.ConfigDrawer>
|
||||||
|
@ -23,9 +23,9 @@ const StartNode = ({ node, disabled }: StartNodeProps) => {
|
|||||||
|
|
||||||
const formRef = useRef<StartNodeConfigFormInstance>(null);
|
const formRef = useRef<StartNodeConfigFormInstance>(null);
|
||||||
const [formPending, setFormPending] = useState(false);
|
const [formPending, setFormPending] = useState(false);
|
||||||
|
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForStart;
|
||||||
|
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForStart;
|
|
||||||
|
|
||||||
const wrappedEl = useMemo(() => {
|
const wrappedEl = useMemo(() => {
|
||||||
if (node.type !== WorkflowNodeType.Start) {
|
if (node.type !== WorkflowNodeType.Start) {
|
||||||
@ -83,12 +83,12 @@ const StartNode = ({ node, disabled }: StartNodeProps) => {
|
|||||||
</SharedNode.Block>
|
</SharedNode.Block>
|
||||||
|
|
||||||
<SharedNode.ConfigDrawer
|
<SharedNode.ConfigDrawer
|
||||||
|
getConfigNewValues={getFormValues}
|
||||||
node={node}
|
node={node}
|
||||||
open={drawerOpen}
|
open={drawerOpen}
|
||||||
pending={formPending}
|
pending={formPending}
|
||||||
onConfirm={handleDrawerConfirm}
|
onConfirm={handleDrawerConfirm}
|
||||||
onOpenChange={(open) => setDrawerOpen(open)}
|
onOpenChange={(open) => setDrawerOpen(open)}
|
||||||
getFormValues={() => formRef.current!.getFieldsValue()}
|
|
||||||
>
|
>
|
||||||
<StartNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} />
|
<StartNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} />
|
||||||
</SharedNode.ConfigDrawer>
|
</SharedNode.ConfigDrawer>
|
||||||
|
45
ui/src/components/workflow/node/UnknownNode.tsx
Normal file
45
ui/src/components/workflow/node/UnknownNode.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { memo } from "react";
|
||||||
|
import { CloseCircleOutlined as CloseCircleOutlinedIcon } from "@ant-design/icons";
|
||||||
|
import { Alert, Button, Card } from "antd";
|
||||||
|
|
||||||
|
import { useZustandShallowSelector } from "@/hooks";
|
||||||
|
import { useWorkflowStore } from "@/stores/workflow";
|
||||||
|
|
||||||
|
import { type SharedNodeProps } from "./_SharedNode";
|
||||||
|
import AddNode from "./AddNode";
|
||||||
|
|
||||||
|
export type MonitorNodeProps = SharedNodeProps;
|
||||||
|
|
||||||
|
const UnknownNode = ({ node, disabled }: MonitorNodeProps) => {
|
||||||
|
const { removeNode } = useWorkflowStore(useZustandShallowSelector(["removeNode"]));
|
||||||
|
|
||||||
|
const handleClickRemove = () => {
|
||||||
|
removeNode(node.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card className="relative w-[256px] overflow-hidden shadow-md" styles={{ body: { padding: 0 } }} hoverable variant="borderless">
|
||||||
|
<div className="cursor-pointer ">
|
||||||
|
<Alert
|
||||||
|
type="error"
|
||||||
|
message={
|
||||||
|
<div className="flex items-center justify-between gap-4 overflow-hidden">
|
||||||
|
<div className="flex-1 text-center text-xs">
|
||||||
|
INVALID NODE
|
||||||
|
<br />
|
||||||
|
PLEASE REMOVE
|
||||||
|
</div>
|
||||||
|
<Button color="primary" icon={<CloseCircleOutlinedIcon />} variant="text" onClick={handleClickRemove} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<AddNode node={node} disabled={disabled} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(UnknownNode);
|
@ -23,9 +23,9 @@ const UploadNode = ({ node, disabled }: UploadNodeProps) => {
|
|||||||
|
|
||||||
const formRef = useRef<UploadNodeConfigFormInstance>(null);
|
const formRef = useRef<UploadNodeConfigFormInstance>(null);
|
||||||
const [formPending, setFormPending] = useState(false);
|
const [formPending, setFormPending] = useState(false);
|
||||||
|
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForUpload;
|
||||||
|
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForUpload;
|
|
||||||
|
|
||||||
const wrappedEl = useMemo(() => {
|
const wrappedEl = useMemo(() => {
|
||||||
if (node.type !== WorkflowNodeType.Upload) {
|
if (node.type !== WorkflowNodeType.Upload) {
|
||||||
@ -74,12 +74,12 @@ const UploadNode = ({ node, disabled }: UploadNodeProps) => {
|
|||||||
</SharedNode.Block>
|
</SharedNode.Block>
|
||||||
|
|
||||||
<SharedNode.ConfigDrawer
|
<SharedNode.ConfigDrawer
|
||||||
|
getConfigNewValues={getFormValues}
|
||||||
node={node}
|
node={node}
|
||||||
open={drawerOpen}
|
open={drawerOpen}
|
||||||
pending={formPending}
|
pending={formPending}
|
||||||
onConfirm={handleDrawerConfirm}
|
onConfirm={handleDrawerConfirm}
|
||||||
onOpenChange={(open) => setDrawerOpen(open)}
|
onOpenChange={(open) => setDrawerOpen(open)}
|
||||||
getFormValues={() => formRef.current!.getFieldsValue()}
|
|
||||||
>
|
>
|
||||||
<UploadNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} />
|
<UploadNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} />
|
||||||
</SharedNode.ConfigDrawer>
|
</SharedNode.ConfigDrawer>
|
||||||
|
@ -33,7 +33,7 @@ const SharedNodeTitle = ({ className, style, node, disabled }: SharedNodeTitlePr
|
|||||||
|
|
||||||
const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
|
const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
|
||||||
const oldName = node.name;
|
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) {
|
if (oldName === newName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -45,9 +45,16 @@ const SharedNodeTitle = ({ className, style, node, disabled }: SharedNodeTitlePr
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (e.code === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
e.currentTarget.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full cursor-text overflow-hidden text-center">
|
<div className="w-full cursor-text overflow-hidden text-center">
|
||||||
<div className={className} style={style} contentEditable={!disabled} suppressContentEditableWarning onBlur={handleBlur}>
|
<div className={className} style={style} contentEditable={!disabled} suppressContentEditableWarning onBlur={handleBlur} onKeyDown={handleKeyDown}>
|
||||||
{node.name}
|
{node.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -91,7 +98,7 @@ const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex,
|
|||||||
|
|
||||||
const handleRenameConfirm = async () => {
|
const handleRenameConfirm = async () => {
|
||||||
const oldName = node.name;
|
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) {
|
if (oldName === newName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -195,7 +202,7 @@ const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex,
|
|||||||
};
|
};
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Wrapper
|
// #region Block
|
||||||
type SharedNodeBlockProps = SharedNodeProps & {
|
type SharedNodeBlockProps = SharedNodeProps & {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
onClick?: (e: React.MouseEvent) => void;
|
onClick?: (e: React.MouseEvent) => void;
|
||||||
@ -245,7 +252,7 @@ type SharedNodeEditDrawerProps = SharedNodeProps & {
|
|||||||
pending?: boolean;
|
pending?: boolean;
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void;
|
||||||
onConfirm: () => void | Promise<unknown>;
|
onConfirm: () => void | Promise<unknown>;
|
||||||
getFormValues: () => NonNullable<unknown>;
|
getConfigNewValues: () => NonNullable<unknown>; // 用于获取节点配置的新值,以便在抽屉关闭前进行对比,决定是否提示保存
|
||||||
};
|
};
|
||||||
|
|
||||||
const SharedNodeConfigDrawer = ({
|
const SharedNodeConfigDrawer = ({
|
||||||
@ -256,7 +263,7 @@ const SharedNodeConfigDrawer = ({
|
|||||||
loading,
|
loading,
|
||||||
pending,
|
pending,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
getFormValues,
|
getConfigNewValues,
|
||||||
...props
|
...props
|
||||||
}: SharedNodeEditDrawerProps) => {
|
}: SharedNodeEditDrawerProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -284,7 +291,7 @@ const SharedNodeConfigDrawer = ({
|
|||||||
if (pending) return;
|
if (pending) return;
|
||||||
|
|
||||||
const oldValues = JSON.parse(JSON.stringify(node.config ?? {}));
|
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 changed = !isEqual(oldValues, {}) && !isEqual(oldValues, newValues);
|
||||||
|
|
||||||
const { promise, resolve, reject } = Promise.withResolvers();
|
const { promise, resolve, reject } = Promise.withResolvers();
|
||||||
|
@ -553,6 +553,14 @@ export const deploymentProvidersMap: Map<DeploymentProvider["type"] | string, De
|
|||||||
[DEPLOYMENT_PROVIDERS.UCLOUD_UCDN, "provider.ucloud.ucdn", DEPLOYMENT_CATEGORIES.CDN],
|
[DEPLOYMENT_PROVIDERS.UCLOUD_UCDN, "provider.ucloud.ucdn", DEPLOYMENT_CATEGORIES.CDN],
|
||||||
[DEPLOYMENT_PROVIDERS.RAINYUN_RCDN, "provider.rainyun.rcdn", DEPLOYMENT_CATEGORIES.CDN],
|
[DEPLOYMENT_PROVIDERS.RAINYUN_RCDN, "provider.rainyun.rcdn", DEPLOYMENT_CATEGORIES.CDN],
|
||||||
[DEPLOYMENT_PROVIDERS.UNICLOUD_WEBHOST, "provider.unicloud.webhost", DEPLOYMENT_CATEGORIES.WEBSITE],
|
[DEPLOYMENT_PROVIDERS.UNICLOUD_WEBHOST, "provider.unicloud.webhost", DEPLOYMENT_CATEGORIES.WEBSITE],
|
||||||
|
[DEPLOYMENT_PROVIDERS.AWS_CLOUDFRONT, "provider.aws.cloudfront", DEPLOYMENT_CATEGORIES.CDN],
|
||||||
|
[DEPLOYMENT_PROVIDERS.AWS_ACM, "provider.aws.acm", DEPLOYMENT_CATEGORIES.SSL],
|
||||||
|
[DEPLOYMENT_PROVIDERS.AZURE_KEYVAULT, "provider.azure.keyvault", DEPLOYMENT_CATEGORIES.SSL],
|
||||||
|
[DEPLOYMENT_PROVIDERS.BUNNY_CDN, "provider.bunny.cdn", DEPLOYMENT_CATEGORIES.CDN],
|
||||||
|
[DEPLOYMENT_PROVIDERS.CACHEFLY, "provider.cachefly", DEPLOYMENT_CATEGORIES.CDN],
|
||||||
|
[DEPLOYMENT_PROVIDERS.EDGIO_APPLICATIONS, "provider.edgio.applications", DEPLOYMENT_CATEGORIES.WEBSITE],
|
||||||
|
[DEPLOYMENT_PROVIDERS.GCORE_CDN, "provider.gcore.cdn", DEPLOYMENT_CATEGORIES.CDN],
|
||||||
|
[DEPLOYMENT_PROVIDERS.NETLIFY_SITE, "provider.netlify.site", DEPLOYMENT_CATEGORIES.WEBSITE],
|
||||||
[DEPLOYMENT_PROVIDERS.CDNFLY, "provider.cdnfly", DEPLOYMENT_CATEGORIES.CDN],
|
[DEPLOYMENT_PROVIDERS.CDNFLY, "provider.cdnfly", DEPLOYMENT_CATEGORIES.CDN],
|
||||||
[DEPLOYMENT_PROVIDERS.FLEXCDN, "provider.flexcdn", DEPLOYMENT_CATEGORIES.CDN],
|
[DEPLOYMENT_PROVIDERS.FLEXCDN, "provider.flexcdn", DEPLOYMENT_CATEGORIES.CDN],
|
||||||
[DEPLOYMENT_PROVIDERS.GOEDGE, "provider.goedge", DEPLOYMENT_CATEGORIES.CDN],
|
[DEPLOYMENT_PROVIDERS.GOEDGE, "provider.goedge", DEPLOYMENT_CATEGORIES.CDN],
|
||||||
@ -566,14 +574,6 @@ export const deploymentProvidersMap: Map<DeploymentProvider["type"] | string, De
|
|||||||
[DEPLOYMENT_PROVIDERS.BAOTAWAF_SITE, "provider.baotawaf.site", DEPLOYMENT_CATEGORIES.FIREWALL],
|
[DEPLOYMENT_PROVIDERS.BAOTAWAF_SITE, "provider.baotawaf.site", DEPLOYMENT_CATEGORIES.FIREWALL],
|
||||||
[DEPLOYMENT_PROVIDERS.BAOTAWAF_CONSOLE, "provider.baotawaf.console", DEPLOYMENT_CATEGORIES.OTHER],
|
[DEPLOYMENT_PROVIDERS.BAOTAWAF_CONSOLE, "provider.baotawaf.console", DEPLOYMENT_CATEGORIES.OTHER],
|
||||||
[DEPLOYMENT_PROVIDERS.SAFELINE, "provider.safeline", DEPLOYMENT_CATEGORIES.FIREWALL],
|
[DEPLOYMENT_PROVIDERS.SAFELINE, "provider.safeline", DEPLOYMENT_CATEGORIES.FIREWALL],
|
||||||
[DEPLOYMENT_PROVIDERS.AWS_CLOUDFRONT, "provider.aws.cloudfront", DEPLOYMENT_CATEGORIES.CDN],
|
|
||||||
[DEPLOYMENT_PROVIDERS.AWS_ACM, "provider.aws.acm", DEPLOYMENT_CATEGORIES.SSL],
|
|
||||||
[DEPLOYMENT_PROVIDERS.AZURE_KEYVAULT, "provider.azure.keyvault", DEPLOYMENT_CATEGORIES.SSL],
|
|
||||||
[DEPLOYMENT_PROVIDERS.BUNNY_CDN, "provider.bunny.cdn", DEPLOYMENT_CATEGORIES.CDN],
|
|
||||||
[DEPLOYMENT_PROVIDERS.CACHEFLY, "provider.cachefly", DEPLOYMENT_CATEGORIES.CDN],
|
|
||||||
[DEPLOYMENT_PROVIDERS.EDGIO_APPLICATIONS, "provider.edgio.applications", DEPLOYMENT_CATEGORIES.WEBSITE],
|
|
||||||
[DEPLOYMENT_PROVIDERS.GCORE_CDN, "provider.gcore.cdn", DEPLOYMENT_CATEGORIES.CDN],
|
|
||||||
[DEPLOYMENT_PROVIDERS.NETLIFY_SITE, "provider.netlify.site", DEPLOYMENT_CATEGORIES.WEBSITE],
|
|
||||||
[DEPLOYMENT_PROVIDERS.PROXMOXVE, "provider.proxmoxve", DEPLOYMENT_CATEGORIES.NAS],
|
[DEPLOYMENT_PROVIDERS.PROXMOXVE, "provider.proxmoxve", DEPLOYMENT_CATEGORIES.NAS],
|
||||||
].map(([type, name, category, builtin]) => [
|
].map(([type, name, category, builtin]) => [
|
||||||
type,
|
type,
|
||||||
|
@ -31,6 +31,7 @@ export enum WorkflowNodeType {
|
|||||||
End = "end",
|
End = "end",
|
||||||
Apply = "apply",
|
Apply = "apply",
|
||||||
Upload = "upload",
|
Upload = "upload",
|
||||||
|
Monitor = "monitor",
|
||||||
Deploy = "deploy",
|
Deploy = "deploy",
|
||||||
Notify = "notify",
|
Notify = "notify",
|
||||||
Branch = "branch",
|
Branch = "branch",
|
||||||
@ -42,22 +43,24 @@ export enum WorkflowNodeType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const workflowNodeTypeDefaultNames: Map<WorkflowNodeType, string> = new Map([
|
const workflowNodeTypeDefaultNames: Map<WorkflowNodeType, string> = new Map([
|
||||||
[WorkflowNodeType.Start, i18n.t("workflow_node.start.label")],
|
[WorkflowNodeType.Start, i18n.t("workflow_node.start.default_name")],
|
||||||
[WorkflowNodeType.End, i18n.t("workflow_node.end.label")],
|
[WorkflowNodeType.End, i18n.t("workflow_node.end.default_name")],
|
||||||
[WorkflowNodeType.Apply, i18n.t("workflow_node.apply.label")],
|
[WorkflowNodeType.Apply, i18n.t("workflow_node.apply.default_name")],
|
||||||
[WorkflowNodeType.Upload, i18n.t("workflow_node.upload.label")],
|
[WorkflowNodeType.Upload, i18n.t("workflow_node.upload.default_name")],
|
||||||
[WorkflowNodeType.Deploy, i18n.t("workflow_node.deploy.label")],
|
[WorkflowNodeType.Monitor, i18n.t("workflow_node.monitor.default_name")],
|
||||||
[WorkflowNodeType.Notify, i18n.t("workflow_node.notify.label")],
|
[WorkflowNodeType.Deploy, i18n.t("workflow_node.deploy.default_name")],
|
||||||
[WorkflowNodeType.Branch, i18n.t("workflow_node.branch.label")],
|
[WorkflowNodeType.Notify, i18n.t("workflow_node.notify.default_name")],
|
||||||
[WorkflowNodeType.Condition, i18n.t("workflow_node.condition.label")],
|
[WorkflowNodeType.Branch, i18n.t("workflow_node.branch.default_name")],
|
||||||
[WorkflowNodeType.ExecuteResultBranch, i18n.t("workflow_node.execute_result_branch.label")],
|
[WorkflowNodeType.Condition, i18n.t("workflow_node.condition.default_name")],
|
||||||
[WorkflowNodeType.ExecuteSuccess, i18n.t("workflow_node.execute_success.label")],
|
[WorkflowNodeType.ExecuteResultBranch, i18n.t("workflow_node.execute_result_branch.default_name")],
|
||||||
[WorkflowNodeType.ExecuteFailure, i18n.t("workflow_node.execute_failure.label")],
|
[WorkflowNodeType.ExecuteSuccess, i18n.t("workflow_node.execute_success.default_name")],
|
||||||
[WorkflowNodeType.Custom, i18n.t("workflow_node.custom.title")],
|
[WorkflowNodeType.ExecuteFailure, i18n.t("workflow_node.execute_failure.default_name")],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const workflowNodeTypeDefaultInputs: Map<WorkflowNodeType, WorkflowNodeIO[]> = new Map([
|
const workflowNodeTypeDefaultInputs: Map<WorkflowNodeType, WorkflowNodeIO[]> = new Map([
|
||||||
[WorkflowNodeType.Apply, []],
|
[WorkflowNodeType.Apply, []],
|
||||||
|
[WorkflowNodeType.Upload, []],
|
||||||
|
[WorkflowNodeType.Monitor, []],
|
||||||
[
|
[
|
||||||
WorkflowNodeType.Deploy,
|
WorkflowNodeType.Deploy,
|
||||||
[
|
[
|
||||||
@ -65,7 +68,7 @@ const workflowNodeTypeDefaultInputs: Map<WorkflowNodeType, WorkflowNodeIO[]> = n
|
|||||||
name: "certificate",
|
name: "certificate",
|
||||||
type: "certificate",
|
type: "certificate",
|
||||||
required: true,
|
required: true,
|
||||||
label: "证书",
|
label: i18n.t("workflow.variables.type.certificate.label"),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -80,7 +83,7 @@ const workflowNodeTypeDefaultOutputs: Map<WorkflowNodeType, WorkflowNodeIO[]> =
|
|||||||
name: "certificate",
|
name: "certificate",
|
||||||
type: "certificate",
|
type: "certificate",
|
||||||
required: true,
|
required: true,
|
||||||
label: "证书",
|
label: i18n.t("workflow.variables.type.certificate.label"),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -91,7 +94,18 @@ const workflowNodeTypeDefaultOutputs: Map<WorkflowNodeType, WorkflowNodeIO[]> =
|
|||||||
name: "certificate",
|
name: "certificate",
|
||||||
type: "certificate",
|
type: "certificate",
|
||||||
required: true,
|
required: true,
|
||||||
label: "证书",
|
label: i18n.t("workflow.variables.type.certificate.label"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
WorkflowNodeType.Monitor,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "certificate",
|
||||||
|
type: "certificate",
|
||||||
|
required: true,
|
||||||
|
label: i18n.t("workflow.variables.type.certificate.label"),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -145,6 +159,13 @@ export type WorkflowNodeConfigForUpload = {
|
|||||||
privateKey: string;
|
privateKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkflowNodeConfigForMonitor = {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
domain?: string;
|
||||||
|
requestPath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowNodeConfigForDeploy = {
|
export type WorkflowNodeConfigForDeploy = {
|
||||||
certificate: string;
|
certificate: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
@ -165,6 +186,10 @@ export type WorkflowNodeConfigForNotify = {
|
|||||||
providerConfig?: Record<string, unknown>;
|
providerConfig?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkflowNodeConfigForCondition = {
|
||||||
|
expression?: Expr;
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowNodeConfigForBranch = never;
|
export type WorkflowNodeConfigForBranch = never;
|
||||||
|
|
||||||
export type WorkflowNodeConfigForEnd = never;
|
export type WorkflowNodeConfigForEnd = never;
|
||||||
@ -178,11 +203,35 @@ export type WorkflowNodeIO = {
|
|||||||
valueSelector?: WorkflowNodeIOValueSelector;
|
valueSelector?: WorkflowNodeIOValueSelector;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowNodeIOValueSelector = {
|
export type WorkflowNodeIOValueSelector = ExprValueSelector;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Expression
|
||||||
|
export enum ExprType {
|
||||||
|
Constant = "const",
|
||||||
|
Variant = "var",
|
||||||
|
Comparison = "comparison",
|
||||||
|
Logical = "logical",
|
||||||
|
Not = "not",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExprValue = string | number | boolean;
|
||||||
|
export type ExprValueType = "string" | "number" | "boolean";
|
||||||
|
export type ExprValueSelector = {
|
||||||
id: string;
|
id: string;
|
||||||
name: 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 = ConstantExpr | VariantExpr | ComparisonExpr | LogicalExpr | NotExpr;
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
const isBranchLike = (node: WorkflowNode) => {
|
const isBranchLike = (node: WorkflowNode) => {
|
||||||
@ -190,25 +239,153 @@ const isBranchLike = (node: WorkflowNode) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type InitWorkflowOptions = {
|
type InitWorkflowOptions = {
|
||||||
template?: "standard";
|
template?: "standard" | "certtest";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel => {
|
export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel => {
|
||||||
const root = newNode(WorkflowNodeType.Start, {}) as WorkflowNode;
|
const root = newNode(WorkflowNodeType.Start, {}) as WorkflowNode;
|
||||||
root.config = { trigger: WORKFLOW_TRIGGERS.MANUAL };
|
root.config = { trigger: WORKFLOW_TRIGGERS.MANUAL };
|
||||||
|
|
||||||
if (options.template === "standard") {
|
switch (options.template) {
|
||||||
let current = root;
|
case "standard":
|
||||||
current.next = newNode(WorkflowNodeType.Apply, {});
|
{
|
||||||
|
let current = root;
|
||||||
|
|
||||||
current = current.next;
|
const applyNode = newNode(WorkflowNodeType.Apply);
|
||||||
current.next = newNode(WorkflowNodeType.Deploy, {});
|
current.next = applyNode;
|
||||||
|
|
||||||
current = current.next;
|
current = current.next;
|
||||||
current.next = newNode(WorkflowNodeType.ExecuteResultBranch, {});
|
current.next = newNode(WorkflowNodeType.ExecuteResultBranch);
|
||||||
|
|
||||||
current = current.next!.branches![1];
|
current = current.next!.branches![1];
|
||||||
current.next = newNode(WorkflowNodeType.Notify, {});
|
current.next = newNode(WorkflowNodeType.Notify, {
|
||||||
|
nodeConfig: {
|
||||||
|
subject: "[Certimate] Workflow Failure Alert!",
|
||||||
|
message: "Your workflow run for the certificate application has failed. Please check the details.",
|
||||||
|
} as WorkflowNodeConfigForNotify,
|
||||||
|
});
|
||||||
|
|
||||||
|
current = applyNode.next!.branches![0];
|
||||||
|
current.next = newNode(WorkflowNodeType.Deploy, {
|
||||||
|
nodeConfig: {
|
||||||
|
certificate: `${applyNode.id}#certificate`,
|
||||||
|
skipOnLastSucceeded: true,
|
||||||
|
} as WorkflowNodeConfigForDeploy,
|
||||||
|
});
|
||||||
|
|
||||||
|
current = current.next;
|
||||||
|
current.next = newNode(WorkflowNodeType.ExecuteResultBranch);
|
||||||
|
|
||||||
|
current = current.next!.branches![1];
|
||||||
|
current.next = newNode(WorkflowNodeType.Notify, {
|
||||||
|
nodeConfig: {
|
||||||
|
subject: "[Certimate] Workflow Failure Alert!",
|
||||||
|
message: "Your workflow run for the certificate deployment has failed. Please check the details.",
|
||||||
|
} as WorkflowNodeConfigForNotify,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "certtest":
|
||||||
|
{
|
||||||
|
let current = root;
|
||||||
|
|
||||||
|
const monitorNode = newNode(WorkflowNodeType.Monitor);
|
||||||
|
current.next = monitorNode;
|
||||||
|
|
||||||
|
current = current.next;
|
||||||
|
current.next = newNode(WorkflowNodeType.ExecuteResultBranch);
|
||||||
|
|
||||||
|
current = current.next!.branches![1];
|
||||||
|
current.next = newNode(WorkflowNodeType.Notify, {
|
||||||
|
nodeConfig: {
|
||||||
|
subject: "[Certimate] Workflow Failure Alert!",
|
||||||
|
message: "Your workflow run for the certificate monitoring has failed. Please check the details.",
|
||||||
|
} as WorkflowNodeConfigForNotify,
|
||||||
|
});
|
||||||
|
|
||||||
|
current = monitorNode.next!.branches![0];
|
||||||
|
const branchNode = newNode(WorkflowNodeType.Branch);
|
||||||
|
current.next = branchNode;
|
||||||
|
|
||||||
|
current = branchNode.branches![0];
|
||||||
|
current.name = i18n.t("workflow_node.condition.default_name.template_certtest_on_expire_soon");
|
||||||
|
current.config = {
|
||||||
|
expression: {
|
||||||
|
left: {
|
||||||
|
left: {
|
||||||
|
selector: {
|
||||||
|
id: monitorNode.id,
|
||||||
|
name: "certificate.validity",
|
||||||
|
type: "boolean",
|
||||||
|
},
|
||||||
|
type: "var",
|
||||||
|
},
|
||||||
|
operator: "eq",
|
||||||
|
right: {
|
||||||
|
type: "const",
|
||||||
|
value: "true",
|
||||||
|
valueType: "boolean",
|
||||||
|
},
|
||||||
|
type: "comparison",
|
||||||
|
},
|
||||||
|
operator: "and",
|
||||||
|
right: {
|
||||||
|
left: {
|
||||||
|
selector: {
|
||||||
|
id: monitorNode.id,
|
||||||
|
name: "certificate.daysLeft",
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
type: "var",
|
||||||
|
},
|
||||||
|
operator: "lte",
|
||||||
|
right: {
|
||||||
|
type: "const",
|
||||||
|
value: "30",
|
||||||
|
valueType: "number",
|
||||||
|
},
|
||||||
|
type: "comparison",
|
||||||
|
},
|
||||||
|
type: "logical",
|
||||||
|
},
|
||||||
|
} as WorkflowNodeConfigForCondition;
|
||||||
|
current.next = newNode(WorkflowNodeType.Notify, {
|
||||||
|
nodeConfig: {
|
||||||
|
subject: "[Certimate] Certificate Expiry Alert!",
|
||||||
|
message: "The certificate will expire soon. Please pay attention to your website.",
|
||||||
|
} as WorkflowNodeConfigForNotify,
|
||||||
|
});
|
||||||
|
|
||||||
|
current = branchNode.branches![1];
|
||||||
|
current.name = i18n.t("workflow_node.condition.default_name.template_certtest_on_expired");
|
||||||
|
current.config = {
|
||||||
|
expression: {
|
||||||
|
left: {
|
||||||
|
selector: {
|
||||||
|
id: monitorNode.id,
|
||||||
|
name: "certificate.validity",
|
||||||
|
type: "boolean",
|
||||||
|
},
|
||||||
|
type: "var",
|
||||||
|
},
|
||||||
|
operator: "eq",
|
||||||
|
right: {
|
||||||
|
type: "const",
|
||||||
|
value: "false",
|
||||||
|
valueType: "boolean",
|
||||||
|
},
|
||||||
|
type: "comparison",
|
||||||
|
},
|
||||||
|
} as WorkflowNodeConfigForCondition;
|
||||||
|
current.next = newNode(WorkflowNodeType.Notify, {
|
||||||
|
nodeConfig: {
|
||||||
|
subject: "[Certimate] Certificate Expiry Alert!",
|
||||||
|
message: "The certificate has already expired. Please pay attention to your website.",
|
||||||
|
} as WorkflowNodeConfigForNotify,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -225,6 +402,8 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel =
|
|||||||
};
|
};
|
||||||
|
|
||||||
type NewNodeOptions = {
|
type NewNodeOptions = {
|
||||||
|
nodeName?: string;
|
||||||
|
nodeConfig?: Record<string, unknown>;
|
||||||
branchIndex?: number;
|
branchIndex?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -234,13 +413,15 @@ export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions = {}
|
|||||||
|
|
||||||
const node: WorkflowNode = {
|
const node: WorkflowNode = {
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
name: nodeName,
|
name: options.nodeName ?? nodeName,
|
||||||
type: nodeType,
|
type: nodeType,
|
||||||
|
config: options.nodeConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (nodeType) {
|
switch (nodeType) {
|
||||||
case WorkflowNodeType.Apply:
|
case WorkflowNodeType.Apply:
|
||||||
case WorkflowNodeType.Upload:
|
case WorkflowNodeType.Upload:
|
||||||
|
case WorkflowNodeType.Monitor:
|
||||||
case WorkflowNodeType.Deploy:
|
case WorkflowNodeType.Deploy:
|
||||||
{
|
{
|
||||||
node.inputs = workflowNodeTypeDefaultInputs.get(nodeType);
|
node.inputs = workflowNodeTypeDefaultInputs.get(nodeType);
|
||||||
@ -433,10 +614,24 @@ export const removeBranch = (node: WorkflowNode, branchNodeId: string, branchInd
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, type: string): WorkflowNode[] => {
|
export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, typeFilter?: string | string[]): WorkflowNode[] => {
|
||||||
// 某个分支的节点,不应该能获取到相邻分支上节点的输出
|
// 某个分支的节点,不应该能获取到相邻分支上节点的输出
|
||||||
const outputs: 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[]) => {
|
const traverse = (current: WorkflowNode, output: WorkflowNode[]) => {
|
||||||
if (!current) {
|
if (!current) {
|
||||||
return false;
|
return false;
|
||||||
@ -445,10 +640,10 @@ export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, type:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current.type !== WorkflowNodeType.Branch && current.outputs && current.outputs.some((io) => io.type === type)) {
|
if (current.type !== WorkflowNodeType.Branch && current.outputs && current.outputs.some((io) => filter(io))) {
|
||||||
output.push({
|
output.push({
|
||||||
...current,
|
...current,
|
||||||
outputs: current.outputs.filter((io) => io.type === type),
|
outputs: current.outputs.filter((io) => filter(io)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import nlsSettings from "./nls.settings.json";
|
|||||||
import nlsWorkflow from "./nls.workflow.json";
|
import nlsWorkflow from "./nls.workflow.json";
|
||||||
import nlsWorkflowNodes from "./nls.workflow.nodes.json";
|
import nlsWorkflowNodes from "./nls.workflow.nodes.json";
|
||||||
import nlsWorkflowRuns from "./nls.workflow.runs.json";
|
import nlsWorkflowRuns from "./nls.workflow.runs.json";
|
||||||
|
import nlsWorkflowVars from "./nls.workflow.vars.json";
|
||||||
|
|
||||||
export default Object.freeze({
|
export default Object.freeze({
|
||||||
...nlsCommon,
|
...nlsCommon,
|
||||||
@ -16,8 +17,9 @@ export default Object.freeze({
|
|||||||
...nlsSettings,
|
...nlsSettings,
|
||||||
...nlsProvider,
|
...nlsProvider,
|
||||||
...nlsAccess,
|
...nlsAccess,
|
||||||
|
...nlsCertificate,
|
||||||
...nlsWorkflow,
|
...nlsWorkflow,
|
||||||
...nlsWorkflowNodes,
|
...nlsWorkflowNodes,
|
||||||
...nlsWorkflowRuns,
|
...nlsWorkflowRuns,
|
||||||
...nlsCertificate,
|
...nlsWorkflowVars,
|
||||||
});
|
});
|
||||||
|
@ -30,6 +30,8 @@
|
|||||||
"workflow.new.templates.title": "Choose a Workflow Template",
|
"workflow.new.templates.title": "Choose a Workflow Template",
|
||||||
"workflow.new.templates.template.standard.title": "Standard template",
|
"workflow.new.templates.template.standard.title": "Standard template",
|
||||||
"workflow.new.templates.template.standard.description": "A standard operating procedure that includes application, deployment, and notification steps.",
|
"workflow.new.templates.template.standard.description": "A standard operating procedure that includes application, deployment, and notification steps.",
|
||||||
|
"workflow.new.templates.template.certtest.title": "Monitoring template",
|
||||||
|
"workflow.new.templates.template.certtest.description": "A monitoring operating procedure that includes monitoring, and notification steps.",
|
||||||
"workflow.new.templates.template.blank.title": "Blank template",
|
"workflow.new.templates.template.blank.title": "Blank template",
|
||||||
"workflow.new.templates.template.blank.description": "Customize all the contents of the workflow from the beginning.",
|
"workflow.new.templates.template.blank.description": "Customize all the contents of the workflow from the beginning.",
|
||||||
"workflow.new.modal.title": "Create workflow",
|
"workflow.new.modal.title": "Create workflow",
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"workflow_node.unsaved_changes.confirm": "You have unsaved changes. Do you really want to close the panel and drop those changes?",
|
"workflow_node.unsaved_changes.confirm": "You have unsaved changes. Do you really want to close the panel and drop those changes?",
|
||||||
|
|
||||||
"workflow_node.start.label": "Start",
|
"workflow_node.start.label": "Start",
|
||||||
|
"workflow_node.start.default_name": "Start",
|
||||||
"workflow_node.start.form.trigger.label": "Trigger",
|
"workflow_node.start.form.trigger.label": "Trigger",
|
||||||
"workflow_node.start.form.trigger.placeholder": "Please select trigger",
|
"workflow_node.start.form.trigger.placeholder": "Please select trigger",
|
||||||
"workflow_node.start.form.trigger.tooltip": "Auto: Time triggered based on cron expression.<br>Manual: Manually triggered.",
|
"workflow_node.start.form.trigger.tooltip": "Auto: Time triggered based on cron expression.<br>Manual: Manually triggered.",
|
||||||
@ -22,7 +23,8 @@
|
|||||||
"workflow_node.start.form.trigger_cron.extra": "Expected execution time for the last 5 times:",
|
"workflow_node.start.form.trigger_cron.extra": "Expected execution time for the last 5 times:",
|
||||||
"workflow_node.start.form.trigger_cron.guide": "Tips: If you have multiple workflows, it is recommended to set them to run at multiple times of the day instead of always running at specific times. Don't always set it to midnight every day to avoid spikes in traffic.<br><br>Reference links:<br>1. <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">Let’s Encrypt rate limits</a><br>2. <a href=\"https://letsencrypt.org/docs/faq/#why-should-my-let-s-encrypt-acme-client-run-at-a-random-time\" target=\"_blank\">Why should my Let’s Encrypt (ACME) client run at a random time?</a>",
|
"workflow_node.start.form.trigger_cron.guide": "Tips: If you have multiple workflows, it is recommended to set them to run at multiple times of the day instead of always running at specific times. Don't always set it to midnight every day to avoid spikes in traffic.<br><br>Reference links:<br>1. <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">Let’s Encrypt rate limits</a><br>2. <a href=\"https://letsencrypt.org/docs/faq/#why-should-my-let-s-encrypt-acme-client-run-at-a-random-time\" target=\"_blank\">Why should my Let’s Encrypt (ACME) client run at a random time?</a>",
|
||||||
|
|
||||||
"workflow_node.apply.label": "Application",
|
"workflow_node.apply.label": "Obtain certificate",
|
||||||
|
"workflow_node.apply.default_name": "Application",
|
||||||
"workflow_node.apply.form.domains.label": "Domains",
|
"workflow_node.apply.form.domains.label": "Domains",
|
||||||
"workflow_node.apply.form.domains.placeholder": "Please enter domains (separated by semicolons)",
|
"workflow_node.apply.form.domains.placeholder": "Please enter domains (separated by semicolons)",
|
||||||
"workflow_node.apply.form.domains.tooltip": "Wildcard domain: *.example.com",
|
"workflow_node.apply.form.domains.tooltip": "Wildcard domain: *.example.com",
|
||||||
@ -97,7 +99,17 @@
|
|||||||
"workflow_node.apply.form.skip_before_expiry_days.unit": "days",
|
"workflow_node.apply.form.skip_before_expiry_days.unit": "days",
|
||||||
"workflow_node.apply.form.skip_before_expiry_days.tooltip": "Be careful not to exceed the validity period limit of the issued certificate, otherwise the certificate may never be renewed.",
|
"workflow_node.apply.form.skip_before_expiry_days.tooltip": "Be careful not to exceed the validity period limit of the issued certificate, otherwise the certificate may never be renewed.",
|
||||||
|
|
||||||
"workflow_node.deploy.label": "Deployment",
|
"workflow_node.upload.label": "Upload certificate",
|
||||||
|
"workflow_node.upload.default_name": "Uploading",
|
||||||
|
"workflow_node.upload.form.domains.label": "Domains",
|
||||||
|
"workflow_node.upload.form.domains.placholder": "Please select certificate file",
|
||||||
|
"workflow_node.upload.form.certificate.label": "Certificate (PEM format)",
|
||||||
|
"workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
|
||||||
|
"workflow_node.upload.form.private_key.label": "Private key (PEM format)",
|
||||||
|
"workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
|
||||||
|
|
||||||
|
"workflow_node.deploy.label": "Deploy certificate",
|
||||||
|
"workflow_node.deploy.default_name": "Deployment",
|
||||||
"workflow_node.deploy.form.provider.label": "Deploy target",
|
"workflow_node.deploy.form.provider.label": "Deploy target",
|
||||||
"workflow_node.deploy.form.provider.placeholder": "Please select deploy target",
|
"workflow_node.deploy.form.provider.placeholder": "Please select deploy target",
|
||||||
"workflow_node.deploy.form.provider.search.placeholder": "Search deploy target ...",
|
"workflow_node.deploy.form.provider.search.placeholder": "Search deploy target ...",
|
||||||
@ -805,15 +817,20 @@
|
|||||||
"workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "skip",
|
"workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "skip",
|
||||||
"workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "not skip",
|
"workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "not skip",
|
||||||
|
|
||||||
"workflow_node.upload.label": "Upload",
|
"workflow_node.monitor.label": "Monitor certificate",
|
||||||
"workflow_node.upload.form.domains.label": "Domains",
|
"workflow_node.monitor.default_name": "Monitoring",
|
||||||
"workflow_node.upload.form.domains.placholder": "Please select certificate file",
|
"workflow_node.monitor.form.guide": "Tips: Certimate will send a HEAD request to the target address to obtain the certificate. Please ensure that the address is accessible through HTTPS protocol.",
|
||||||
"workflow_node.upload.form.certificate.label": "Certificate (PEM format)",
|
"workflow_node.monitor.form.host.label": "Host",
|
||||||
"workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
|
"workflow_node.monitor.form.host.placeholder": "Please enter host",
|
||||||
"workflow_node.upload.form.private_key.label": "Private key (PEM format)",
|
"workflow_node.monitor.form.port.label": "Port",
|
||||||
"workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
|
"workflow_node.monitor.form.port.placeholder": "Please enter port",
|
||||||
|
"workflow_node.monitor.form.domain.label": "Domain (Optional)",
|
||||||
|
"workflow_node.monitor.form.domain.placeholder": "Please enter domain name",
|
||||||
|
"workflow_node.monitor.form.request_path.label": "Request path (Optional)",
|
||||||
|
"workflow_node.monitor.form.request_path.placeholder": "Please enter request path",
|
||||||
|
|
||||||
"workflow_node.notify.label": "Notification",
|
"workflow_node.notify.label": "Send notification",
|
||||||
|
"workflow_node.notify.default_name": "Notification",
|
||||||
"workflow_node.notify.form.subject.label": "Subject",
|
"workflow_node.notify.form.subject.label": "Subject",
|
||||||
"workflow_node.notify.form.subject.placeholder": "Please enter subject",
|
"workflow_node.notify.form.subject.placeholder": "Please enter subject",
|
||||||
"workflow_node.notify.form.message.label": "Message",
|
"workflow_node.notify.form.message.label": "Message",
|
||||||
@ -852,14 +869,43 @@
|
|||||||
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string",
|
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string",
|
||||||
|
|
||||||
"workflow_node.end.label": "End",
|
"workflow_node.end.label": "End",
|
||||||
|
"workflow_node.end.default_name": "End",
|
||||||
|
|
||||||
"workflow_node.branch.label": "Parallel branch",
|
"workflow_node.branch.label": "Parallel/Conditional branch",
|
||||||
|
"workflow_node.branch.default_name": "Branch",
|
||||||
|
|
||||||
"workflow_node.condition.label": "Branch",
|
"workflow_node.condition.label": "Branch",
|
||||||
|
"workflow_node.condition.default_name": "Branch",
|
||||||
|
"workflow_node.condition.default_name.template_certtest_on_expire_soon": "If the certificate will expire soon ...",
|
||||||
|
"workflow_node.condition.default_name.template_certtest_on_expired": "If the certificate has expired ...",
|
||||||
|
"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.label": "Execution result branch",
|
||||||
|
"workflow_node.execute_result_branch.default_name": "Execution result branch",
|
||||||
|
|
||||||
"workflow_node.execute_success.label": "If the previous node succeeded ...",
|
"workflow_node.execute_success.label": "If the previous node succeeded ...",
|
||||||
|
"workflow_node.execute_success.default_name": "If the previous node succeeded ...",
|
||||||
|
|
||||||
"workflow_node.execute_failure.label": "If the previous node failed ..."
|
"workflow_node.execute_failure.label": "If the previous node failed ...",
|
||||||
|
"workflow_node.execute_failure.default_name": "If the previous node failed ..."
|
||||||
}
|
}
|
||||||
|
6
ui/src/i18n/locales/en/nls.workflow.vars.json
Normal file
6
ui/src/i18n/locales/en/nls.workflow.vars.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"workflow.variables.type.certificate.label": "Certificate",
|
||||||
|
|
||||||
|
"workflow.variables.selector.validity.label": "Validity",
|
||||||
|
"workflow.variables.selector.days_left.label": "Days left"
|
||||||
|
}
|
@ -8,6 +8,7 @@ import nlsSettings from "./nls.settings.json";
|
|||||||
import nlsWorkflow from "./nls.workflow.json";
|
import nlsWorkflow from "./nls.workflow.json";
|
||||||
import nlsWorkflowNodes from "./nls.workflow.nodes.json";
|
import nlsWorkflowNodes from "./nls.workflow.nodes.json";
|
||||||
import nlsWorkflowRuns from "./nls.workflow.runs.json";
|
import nlsWorkflowRuns from "./nls.workflow.runs.json";
|
||||||
|
import nlsWorkflowVars from "./nls.workflow.vars.json";
|
||||||
|
|
||||||
export default Object.freeze({
|
export default Object.freeze({
|
||||||
...nlsCommon,
|
...nlsCommon,
|
||||||
@ -16,8 +17,9 @@ export default Object.freeze({
|
|||||||
...nlsSettings,
|
...nlsSettings,
|
||||||
...nlsProvider,
|
...nlsProvider,
|
||||||
...nlsAccess,
|
...nlsAccess,
|
||||||
|
...nlsCertificate,
|
||||||
...nlsWorkflow,
|
...nlsWorkflow,
|
||||||
...nlsWorkflowNodes,
|
...nlsWorkflowNodes,
|
||||||
...nlsWorkflowRuns,
|
...nlsWorkflowRuns,
|
||||||
...nlsCertificate,
|
...nlsWorkflowVars,
|
||||||
});
|
});
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
"access.form.name.placeholder": "请输入授权名称",
|
"access.form.name.placeholder": "请输入授权名称",
|
||||||
"access.form.provider.label": "提供商",
|
"access.form.provider.label": "提供商",
|
||||||
"access.form.provider.placeholder": "请选择提供商",
|
"access.form.provider.placeholder": "请选择提供商",
|
||||||
"access.form.provider.tooltip": "提供商分为两种类型:<br>【DNS 提供商】你的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理您的域名解析记录。<br>【主机提供商】你的服务器或云服务的托管方,用于部署签发的证书。<br><br>该字段保存后不可修改。",
|
"access.form.provider.tooltip": "提供商分为两种类型:<br>【DNS 提供商】你的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理你的域名解析记录。<br>【主机提供商】你的服务器或云服务的托管方,用于部署签发的证书。<br><br>该字段保存后不可修改。",
|
||||||
"access.form.provider.search.placeholder": "搜索提供商……",
|
"access.form.provider.search.placeholder": "搜索提供商……",
|
||||||
"access.form.certificate_authority.label": "证书颁发机构",
|
"access.form.certificate_authority.label": "证书颁发机构",
|
||||||
"access.form.certificate_authority.placeholder": "请选择证书颁发机构",
|
"access.form.certificate_authority.placeholder": "请选择证书颁发机构",
|
||||||
|
@ -29,7 +29,9 @@
|
|||||||
"workflow.new.subtitle": "使用工作流来申请证书、部署上传和发送通知",
|
"workflow.new.subtitle": "使用工作流来申请证书、部署上传和发送通知",
|
||||||
"workflow.new.templates.title": "选择工作流模板",
|
"workflow.new.templates.title": "选择工作流模板",
|
||||||
"workflow.new.templates.template.standard.title": "标准模板",
|
"workflow.new.templates.template.standard.title": "标准模板",
|
||||||
"workflow.new.templates.template.standard.description": "一个包含申请 + 部署 + 通知步骤的标准工作流程。",
|
"workflow.new.templates.template.standard.description": "一个包含证书申请 + 证书部署 + 消息通知步骤的工作流程。",
|
||||||
|
"workflow.new.templates.template.certtest.title": "监控模板",
|
||||||
|
"workflow.new.templates.template.certtest.description": "一个包含证书监控 + 消息通知步骤的工作流程。",
|
||||||
"workflow.new.templates.template.blank.title": "空白模板",
|
"workflow.new.templates.template.blank.title": "空白模板",
|
||||||
"workflow.new.templates.template.blank.description": "从零开始自定义工作流的任务内容。",
|
"workflow.new.templates.template.blank.description": "从零开始自定义工作流的任务内容。",
|
||||||
"workflow.new.modal.title": "新建工作流",
|
"workflow.new.modal.title": "新建工作流",
|
||||||
|
@ -3,13 +3,14 @@
|
|||||||
"workflow_node.branch.add_node": "添加节点",
|
"workflow_node.branch.add_node": "添加节点",
|
||||||
"workflow_node.action.rename_node": "重命名",
|
"workflow_node.action.rename_node": "重命名",
|
||||||
"workflow_node.action.remove_node": "删除节点",
|
"workflow_node.action.remove_node": "删除节点",
|
||||||
"workflow_node.action.add_branch": "添加并行分支",
|
"workflow_node.action.add_branch": "添加分支",
|
||||||
"workflow_node.action.rename_branch": "重命名",
|
"workflow_node.action.rename_branch": "重命名",
|
||||||
"workflow_node.action.remove_branch": "删除分支",
|
"workflow_node.action.remove_branch": "删除分支",
|
||||||
|
|
||||||
"workflow_node.unsaved_changes.confirm": "你有尚未保存的更改。确定要关闭面板吗?",
|
"workflow_node.unsaved_changes.confirm": "你有尚未保存的更改。确定要关闭面板吗?",
|
||||||
|
|
||||||
"workflow_node.start.label": "开始",
|
"workflow_node.start.label": "开始",
|
||||||
|
"workflow_node.start.default_name": "开始",
|
||||||
"workflow_node.start.form.trigger.label": "触发方式",
|
"workflow_node.start.form.trigger.label": "触发方式",
|
||||||
"workflow_node.start.form.trigger.placeholder": "请选择触发方式",
|
"workflow_node.start.form.trigger.placeholder": "请选择触发方式",
|
||||||
"workflow_node.start.form.trigger.tooltip": "自动触发:基于 Cron 表达式定时触发。<br>手动触发:手动点击执行触发。",
|
"workflow_node.start.form.trigger.tooltip": "自动触发:基于 Cron 表达式定时触发。<br>手动触发:手动点击执行触发。",
|
||||||
@ -22,7 +23,8 @@
|
|||||||
"workflow_node.start.form.trigger_cron.extra": "预计最近 5 次执行时间:",
|
"workflow_node.start.form.trigger_cron.extra": "预计最近 5 次执行时间:",
|
||||||
"workflow_node.start.form.trigger_cron.guide": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。也不要总是设置为每日零时,以免遭遇证书颁发机构的流量高峰。<br><br>参考链接:<br>1. <a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">Let’s Encrypt 速率限制</a><br>2. <a href=\"https://letsencrypt.org/zh-cn/docs/faq/#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E7%9A%84-let-s-encrypt-acme-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%E5%BA%94%E5%BD%93%E9%9A%8F%E6%9C%BA\" target=\"_blank\">为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?</a>",
|
"workflow_node.start.form.trigger_cron.guide": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。也不要总是设置为每日零时,以免遭遇证书颁发机构的流量高峰。<br><br>参考链接:<br>1. <a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">Let’s Encrypt 速率限制</a><br>2. <a href=\"https://letsencrypt.org/zh-cn/docs/faq/#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E7%9A%84-let-s-encrypt-acme-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%E5%BA%94%E5%BD%93%E9%9A%8F%E6%9C%BA\" target=\"_blank\">为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?</a>",
|
||||||
|
|
||||||
"workflow_node.apply.label": "申请证书",
|
"workflow_node.apply.label": "申请签发证书",
|
||||||
|
"workflow_node.apply.default_name": "申请",
|
||||||
"workflow_node.apply.form.domains.label": "域名",
|
"workflow_node.apply.form.domains.label": "域名",
|
||||||
"workflow_node.apply.form.domains.placeholder": "请输入域名(多个值请用半角分号隔开)",
|
"workflow_node.apply.form.domains.placeholder": "请输入域名(多个值请用半角分号隔开)",
|
||||||
"workflow_node.apply.form.domains.tooltip": "泛域名表示形式为:*.example.com",
|
"workflow_node.apply.form.domains.tooltip": "泛域名表示形式为:*.example.com",
|
||||||
@ -96,7 +98,17 @@
|
|||||||
"workflow_node.apply.form.skip_before_expiry_days.unit": "天",
|
"workflow_node.apply.form.skip_before_expiry_days.unit": "天",
|
||||||
"workflow_node.apply.form.skip_before_expiry_days.tooltip": "注意不要超过颁发的证书最大有效期,否则证书可能永远不会续期。",
|
"workflow_node.apply.form.skip_before_expiry_days.tooltip": "注意不要超过颁发的证书最大有效期,否则证书可能永远不会续期。",
|
||||||
|
|
||||||
"workflow_node.deploy.label": "部署证书",
|
"workflow_node.upload.label": "上传自有证书",
|
||||||
|
"workflow_node.upload.default_name": "上传",
|
||||||
|
"workflow_node.upload.form.domains.label": "域名",
|
||||||
|
"workflow_node.upload.form.domains.placeholder": "上传证书文件后显示",
|
||||||
|
"workflow_node.upload.form.certificate.label": "证书文件(PEM 格式)",
|
||||||
|
"workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
|
||||||
|
"workflow_node.upload.form.private_key.label": "私钥文件(PEM 格式)",
|
||||||
|
"workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
|
||||||
|
|
||||||
|
"workflow_node.deploy.label": "部署证书到 ...",
|
||||||
|
"workflow_node.deploy.default_name": "部署",
|
||||||
"workflow_node.deploy.form.provider.label": "部署目标",
|
"workflow_node.deploy.form.provider.label": "部署目标",
|
||||||
"workflow_node.deploy.form.provider.placeholder": "请选择部署目标",
|
"workflow_node.deploy.form.provider.placeholder": "请选择部署目标",
|
||||||
"workflow_node.deploy.form.provider.search.placeholder": "搜索部署目标……",
|
"workflow_node.deploy.form.provider.search.placeholder": "搜索部署目标……",
|
||||||
@ -695,7 +707,7 @@
|
|||||||
"workflow_node.deploy.form.ucloud_us3_domain.label": "优刻得 US3 自定义域名",
|
"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.placeholder": "请输入优刻得 US3 自定义域名",
|
||||||
"workflow_node.deploy.form.ucloud_us3_domain.tooltip": "这是什么?请参阅 <a href=\"https://console.ucloud.cn/ufile\" target=\"_blank\">https://console.ucloud.cn/ufile</a>",
|
"workflow_node.deploy.form.ucloud_us3_domain.tooltip": "这是什么?请参阅 <a href=\"https://console.ucloud.cn/ufile\" target=\"_blank\">https://console.ucloud.cn/ufile</a>",
|
||||||
"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.label": "uniCloud 服务空间提供商",
|
||||||
"workflow_node.deploy.form.unicloud_webhost_space_provider.placeholder": "请选择 uniCloud 服务空间提供商",
|
"workflow_node.deploy.form.unicloud_webhost_space_provider.placeholder": "请选择 uniCloud 服务空间提供商",
|
||||||
"workflow_node.deploy.form.unicloud_webhost_space_provider.option.aliyun.label": "阿里云",
|
"workflow_node.deploy.form.unicloud_webhost_space_provider.option.aliyun.label": "阿里云",
|
||||||
@ -705,11 +717,11 @@
|
|||||||
"workflow_node.deploy.form.unicloud_webhost_space_id.tooltip": "这是什么?请参阅 <a href=\"https://doc.dcloud.net.cn/uniCloud/concepts/space.html\" target=\"_blank\">https://doc.dcloud.net.cn/uniCloud/concepts/space.html</a>",
|
"workflow_node.deploy.form.unicloud_webhost_space_id.tooltip": "这是什么?请参阅 <a href=\"https://doc.dcloud.net.cn/uniCloud/concepts/space.html\" target=\"_blank\">https://doc.dcloud.net.cn/uniCloud/concepts/space.html</a>",
|
||||||
"workflow_node.deploy.form.unicloud_webhost_domain.label": "uniCloud 前端网页托管网站域名",
|
"workflow_node.deploy.form.unicloud_webhost_domain.label": "uniCloud 前端网页托管网站域名",
|
||||||
"workflow_node.deploy.form.unicloud_webhost_domain.placeholder": "请输入 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.label": "又拍云 CDN 加速域名",
|
||||||
"workflow_node.deploy.form.upyun_cdn_domain.placeholder": "请输入又拍云 CDN 加速域名(支持泛域名)",
|
"workflow_node.deploy.form.upyun_cdn_domain.placeholder": "请输入又拍云 CDN 加速域名(支持泛域名)",
|
||||||
"workflow_node.deploy.form.upyun_cdn_domain.tooltip": "这是什么?请参阅 <a href=\"https://console.upyun.com/services/cdn/\" target=\"_blank\">https://console.upyun.com/services/cdn/</a>",
|
"workflow_node.deploy.form.upyun_cdn_domain.tooltip": "这是什么?请参阅 <a href=\"https://console.upyun.com/services/cdn/\" target=\"_blank\">https://console.upyun.com/services/cdn/</a>",
|
||||||
"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.label": "又拍云云存储加速域名",
|
||||||
"workflow_node.deploy.form.upyun_file_domain.placeholder": "请输入又拍云云存储加速域名",
|
"workflow_node.deploy.form.upyun_file_domain.placeholder": "请输入又拍云云存储加速域名",
|
||||||
"workflow_node.deploy.form.upyun_file_domain.tooltip": "这是什么?请参阅 <a href=\"https://console.upyun.com/services/file/\" target=\"_blank\">https://console.upyun.com/services/file/</a>",
|
"workflow_node.deploy.form.upyun_file_domain.tooltip": "这是什么?请参阅 <a href=\"https://console.upyun.com/services/file/\" target=\"_blank\">https://console.upyun.com/services/file/</a>",
|
||||||
@ -804,15 +816,20 @@
|
|||||||
"workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "跳过",
|
"workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "跳过",
|
||||||
"workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "不跳过",
|
"workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "不跳过",
|
||||||
|
|
||||||
"workflow_node.upload.label": "上传证书",
|
"workflow_node.monitor.label": "监控网站证书",
|
||||||
"workflow_node.upload.form.domains.label": "域名",
|
"workflow_node.monitor.default_name": "监控",
|
||||||
"workflow_node.upload.form.domains.placeholder": "上传证书文件后显示",
|
"workflow_node.monitor.form.guide": "小贴士:Certimate 将向目标地址发送一个 HEAD 请求来获取相应的域名证书,请确保该地址可通过 HTTPS 协议访问。",
|
||||||
"workflow_node.upload.form.certificate.label": "证书文件(PEM 格式)",
|
"workflow_node.monitor.form.host.label": "主机地址",
|
||||||
"workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
|
"workflow_node.monitor.form.host.placeholder": "请输入主机地址(可以是域名或 IP)",
|
||||||
"workflow_node.upload.form.private_key.label": "私钥文件(PEM 格式)",
|
"workflow_node.monitor.form.port.label": "主机端口",
|
||||||
"workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
|
"workflow_node.monitor.form.port.placeholder": "请输入主机端口",
|
||||||
|
"workflow_node.monitor.form.domain.label": "域名(可选)",
|
||||||
|
"workflow_node.monitor.form.domain.placeholder": "请输入域名(仅当主机地址为 IP 时可选)",
|
||||||
|
"workflow_node.monitor.form.request_path.label": "请求路径(可选)",
|
||||||
|
"workflow_node.monitor.form.request_path.placeholder": "请输入请求路径",
|
||||||
|
|
||||||
"workflow_node.notify.label": "推送通知",
|
"workflow_node.notify.label": "推送通知",
|
||||||
|
"workflow_node.notify.default_name": "通知",
|
||||||
"workflow_node.notify.form.subject.label": "通知主题",
|
"workflow_node.notify.form.subject.label": "通知主题",
|
||||||
"workflow_node.notify.form.subject.placeholder": "请输入通知主题",
|
"workflow_node.notify.form.subject.placeholder": "请输入通知主题",
|
||||||
"workflow_node.notify.form.message.label": "通知内容",
|
"workflow_node.notify.form.message.label": "通知内容",
|
||||||
@ -851,14 +868,43 @@
|
|||||||
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串",
|
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串",
|
||||||
|
|
||||||
"workflow_node.end.label": "结束",
|
"workflow_node.end.label": "结束",
|
||||||
|
"workflow_node.end.default_name": "结束",
|
||||||
|
|
||||||
"workflow_node.branch.label": "并行分支",
|
"workflow_node.branch.label": "并行/条件分支",
|
||||||
|
"workflow_node.branch.default_name": "分支",
|
||||||
|
|
||||||
"workflow_node.condition.label": "分支",
|
"workflow_node.condition.label": "分支",
|
||||||
|
"workflow_node.condition.default_name": "分支",
|
||||||
|
"workflow_node.condition.default_name.template_certtest_on_expire_soon": "若网站证书即将到期 ...",
|
||||||
|
"workflow_node.condition.default_name.template_certtest_on_expired": "若网站证书已到期 ...",
|
||||||
|
"workflow_node.condition.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.label": "执行结果分支",
|
||||||
|
"workflow_node.execute_result_branch.default_name": "执行结果分支",
|
||||||
|
|
||||||
"workflow_node.execute_success.label": "若前序节点执行成功…",
|
"workflow_node.execute_success.label": "若上一节点执行成功…",
|
||||||
|
"workflow_node.execute_success.default_name": "若上一节点执行成功…",
|
||||||
|
|
||||||
"workflow_node.execute_failure.label": "若前序节点执行失败…"
|
"workflow_node.execute_failure.label": "若上一节点执行失败…",
|
||||||
|
"workflow_node.execute_failure.default_name": "若上一节点执行失败…"
|
||||||
}
|
}
|
||||||
|
6
ui/src/i18n/locales/zh/nls.workflow.vars.json
Normal file
6
ui/src/i18n/locales/zh/nls.workflow.vars.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"workflow.variables.type.certificate.label": "证书",
|
||||||
|
|
||||||
|
"workflow.variables.selector.validity.label": "有效性",
|
||||||
|
"workflow.variables.selector.days_left.label": "剩余天数"
|
||||||
|
}
|
@ -56,7 +56,7 @@ const AccessList = () => {
|
|||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
return (
|
return (
|
||||||
<Space className="max-w-full truncate" size={4}>
|
<Space className="max-w-full truncate" size={4}>
|
||||||
<Avatar src={accessProvidersMap.get(record.provider)?.icon} size="small" />
|
<Avatar shape="square" src={accessProvidersMap.get(record.provider)?.icon} size="small" />
|
||||||
<Typography.Text ellipsis>{t(accessProvidersMap.get(record.provider)?.name ?? "")}</Typography.Text>
|
<Typography.Text ellipsis>{t(accessProvidersMap.get(record.provider)?.name ?? "")}</Typography.Text>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
@ -265,7 +265,7 @@ const WorkflowDetail = () => {
|
|||||||
body: {
|
body: {
|
||||||
position: "relative",
|
position: "relative",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
padding: 0,
|
padding: initialized ? 0 : undefined,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
loading={!initialized}
|
loading={!initialized}
|
||||||
|
@ -12,9 +12,10 @@ import { useAntdForm } from "@/hooks";
|
|||||||
import { save as saveWorkflow } from "@/repository/workflow";
|
import { save as saveWorkflow } from "@/repository/workflow";
|
||||||
import { getErrMsg } from "@/utils/error";
|
import { getErrMsg } from "@/utils/error";
|
||||||
|
|
||||||
const TEMPLATE_KEY_BLANK = "blank" as const;
|
|
||||||
const TEMPLATE_KEY_STANDARD = "standard" as const;
|
const TEMPLATE_KEY_STANDARD = "standard" as const;
|
||||||
type TemplateKeys = typeof TEMPLATE_KEY_BLANK | typeof TEMPLATE_KEY_STANDARD;
|
const TEMPLATE_KEY_CERTTEST = "monitor" as const;
|
||||||
|
const TEMPLATE_KEY_BLANK = "blank" as const;
|
||||||
|
type TemplateKeys = typeof TEMPLATE_KEY_BLANK | typeof TEMPLATE_KEY_CERTTEST | typeof TEMPLATE_KEY_STANDARD;
|
||||||
|
|
||||||
const WorkflowNew = () => {
|
const WorkflowNew = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -27,8 +28,8 @@ const WorkflowNew = () => {
|
|||||||
xs: { flex: "100%" },
|
xs: { flex: "100%" },
|
||||||
md: { flex: "100%" },
|
md: { flex: "100%" },
|
||||||
lg: { flex: "50%" },
|
lg: { flex: "50%" },
|
||||||
xl: { flex: "50%" },
|
xl: { flex: "33.3333%" },
|
||||||
xxl: { flex: "50%" },
|
xxl: { flex: "33.3333%" },
|
||||||
};
|
};
|
||||||
const [templateSelectKey, setTemplateSelectKey] = useState<TemplateKeys>();
|
const [templateSelectKey, setTemplateSelectKey] = useState<TemplateKeys>();
|
||||||
|
|
||||||
@ -64,6 +65,10 @@ const WorkflowNew = () => {
|
|||||||
workflow = initWorkflow({ template: "standard" });
|
workflow = initWorkflow({ template: "standard" });
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case TEMPLATE_KEY_CERTTEST:
|
||||||
|
workflow = initWorkflow({ template: "certtest" });
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw "Invalid state: `templateSelectKey`";
|
throw "Invalid state: `templateSelectKey`";
|
||||||
}
|
}
|
||||||
@ -116,7 +121,7 @@ const WorkflowNew = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="mx-auto max-w-[960px] px-2">
|
<div className="mx-auto max-w-[1600px] px-2">
|
||||||
<Typography.Text type="secondary">
|
<Typography.Text type="secondary">
|
||||||
<div className="mb-8 mt-4 text-xl">{t("workflow.new.templates.title")}</div>
|
<div className="mb-8 mt-4 text-xl">{t("workflow.new.templates.title")}</div>
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@ -139,6 +144,25 @@ const WorkflowNew = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
|
<Col {...templateGridSpans}>
|
||||||
|
<Card
|
||||||
|
className="size-full"
|
||||||
|
cover={<img className="min-h-[120px] object-contain" src="/imgs/workflow/tpl-certtest.png" />}
|
||||||
|
hoverable
|
||||||
|
onClick={() => handleTemplateClick(TEMPLATE_KEY_CERTTEST)}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center gap-4">
|
||||||
|
<Card.Meta
|
||||||
|
className="grow"
|
||||||
|
title={t("workflow.new.templates.template.certtest.title")}
|
||||||
|
description={t("workflow.new.templates.template.certtest.description")}
|
||||||
|
/>
|
||||||
|
<Spin spinning={templateSelectKey === TEMPLATE_KEY_CERTTEST} />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<Col {...templateGridSpans}>
|
<Col {...templateGridSpans}>
|
||||||
<Card
|
<Card
|
||||||
className="size-full"
|
className="size-full"
|
||||||
|
@ -32,7 +32,7 @@ export type WorkflowState = {
|
|||||||
addBranch: (branchId: string) => void;
|
addBranch: (branchId: string) => void;
|
||||||
removeBranch: (branchId: string, index: number) => void;
|
removeBranch: (branchId: string, index: number) => void;
|
||||||
|
|
||||||
getWorkflowOuptutBeforeId: (nodeId: string, type: string) => WorkflowNode[];
|
getWorkflowOuptutBeforeId: (nodeId: string, typeFilter?: string | string[]) => WorkflowNode[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||||
@ -243,7 +243,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getWorkflowOuptutBeforeId: (nodeId: string, type: string) => {
|
getWorkflowOuptutBeforeId: (nodeId: string, typeFilter?: string | string[]) => {
|
||||||
return getOutputBeforeNodeId(get().workflow.draft as WorkflowNode, nodeId, type);
|
return getOutputBeforeNodeId(get().workflow.draft as WorkflowNode, nodeId, typeFilter);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user