mirror of
https://github.com/woodchen-ink/certimate.git
synced 2025-07-18 01:11:55 +08:00
commit
b60e30bb8c
52
.goreleaser.linux.yml
Normal file
52
.goreleaser.linux.yml
Normal file
@ -0,0 +1,52 @@
|
||||
# .goreleaser.linux.yml
|
||||
project_name: certimate
|
||||
|
||||
dist: .builds/linux
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
- id: build_linux
|
||||
main: ./
|
||||
binary: certimate
|
||||
ldflags:
|
||||
- -s -w -X github.com/usual2970/certimate.Version={{ .Version }}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
- arm
|
||||
goarm:
|
||||
- 7
|
||||
|
||||
release:
|
||||
draft: true
|
||||
ids:
|
||||
- linux
|
||||
|
||||
archives:
|
||||
- id: archive_linux
|
||||
builds: [build_linux]
|
||||
format: "zip"
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
files:
|
||||
- CHANGELOG.md
|
||||
- LICENSE.md
|
||||
- README.md
|
||||
|
||||
checksum:
|
||||
name_template: "checksums_linux.txt"
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^ui:"
|
49
.goreleaser.macos.yml
Normal file
49
.goreleaser.macos.yml
Normal file
@ -0,0 +1,49 @@
|
||||
# .goreleaser.macos.yml
|
||||
project_name: certimate
|
||||
|
||||
dist: .builds/macos
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
- id: build_macos
|
||||
main: ./
|
||||
binary: certimate
|
||||
ldflags:
|
||||
- -s -w -X github.com/usual2970/certimate.Version={{ .Version }}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
|
||||
release:
|
||||
draft: true
|
||||
ids:
|
||||
- macos
|
||||
|
||||
archives:
|
||||
- id: archive_macos
|
||||
builds: [build_macos]
|
||||
format: "zip"
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
files:
|
||||
- CHANGELOG.md
|
||||
- LICENSE.md
|
||||
- README.md
|
||||
|
||||
checksum:
|
||||
name_template: "checksums_macos.txt"
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^ui:"
|
52
.goreleaser.windows.yml
Normal file
52
.goreleaser.windows.yml
Normal file
@ -0,0 +1,52 @@
|
||||
# .goreleaser.windows.yml
|
||||
project_name: certimate
|
||||
|
||||
dist: .builds/windows
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
- id: build_windows
|
||||
main: ./
|
||||
binary: certimate
|
||||
ldflags:
|
||||
- -s -w -X github.com/usual2970/certimate.Version={{ .Version }}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
|
||||
release:
|
||||
draft: true
|
||||
ids:
|
||||
- windows
|
||||
|
||||
archives:
|
||||
- id: archive_windows
|
||||
builds: [build_windows]
|
||||
format: "zip"
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
files:
|
||||
- CHANGELOG.md
|
||||
- LICENSE.md
|
||||
- README.md
|
||||
|
||||
checksum:
|
||||
name_template: "checksums_windows.txt"
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^ui:"
|
@ -106,12 +106,13 @@ type WorkflowNodeConfigForDeploy struct {
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForNotify struct {
|
||||
Channel string `json:"channel,omitempty"` // Deprecated: v0.4.x 将废弃
|
||||
Provider string `json:"provider"` // 通知提供商
|
||||
ProviderAccessId string `json:"providerAccessId"` // 通知提供商授权记录 ID
|
||||
ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 通知提供商额外配置
|
||||
Subject string `json:"subject"` // 通知主题
|
||||
Message string `json:"message"` // 通知内容
|
||||
Channel string `json:"channel,omitempty"` // Deprecated: v0.4.x 将废弃
|
||||
Provider string `json:"provider"` // 通知提供商
|
||||
ProviderAccessId string `json:"providerAccessId"` // 通知提供商授权记录 ID
|
||||
ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 通知提供商额外配置
|
||||
Subject string `json:"subject"` // 通知主题
|
||||
Message string `json:"message"` // 通知内容
|
||||
SkipOnAllPrevSkipped bool `json:"skipOnAllPrevSkipped"` // 前序节点均已跳过时是否跳过
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForCondition struct {
|
||||
@ -128,7 +129,7 @@ func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply {
|
||||
CAProvider: maputil.GetString(n.Config, "caProvider"),
|
||||
CAProviderAccessId: maputil.GetString(n.Config, "caProviderAccessId"),
|
||||
CAProviderConfig: maputil.GetKVMapAny(n.Config, "caProviderConfig"),
|
||||
KeyAlgorithm: maputil.GetString(n.Config, "keyAlgorithm"),
|
||||
KeyAlgorithm: maputil.GetOrDefaultString(n.Config, "keyAlgorithm", string(CertificateKeyAlgorithmTypeRSA2048)),
|
||||
Nameservers: maputil.GetString(n.Config, "nameservers"),
|
||||
DnsPropagationWait: maputil.GetInt32(n.Config, "dnsPropagationWait"),
|
||||
DnsPropagationTimeout: maputil.GetInt32(n.Config, "dnsPropagationTimeout"),
|
||||
@ -169,12 +170,13 @@ func (n *WorkflowNode) GetConfigForDeploy() WorkflowNodeConfigForDeploy {
|
||||
|
||||
func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify {
|
||||
return WorkflowNodeConfigForNotify{
|
||||
Channel: maputil.GetString(n.Config, "channel"),
|
||||
Provider: maputil.GetString(n.Config, "provider"),
|
||||
ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"),
|
||||
ProviderConfig: maputil.GetKVMapAny(n.Config, "providerConfig"),
|
||||
Subject: maputil.GetString(n.Config, "subject"),
|
||||
Message: maputil.GetString(n.Config, "message"),
|
||||
Channel: maputil.GetString(n.Config, "channel"),
|
||||
Provider: maputil.GetString(n.Config, "provider"),
|
||||
ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"),
|
||||
ProviderConfig: maputil.GetKVMapAny(n.Config, "providerConfig"),
|
||||
Subject: maputil.GetString(n.Config, "subject"),
|
||||
Message: maputil.GetString(n.Config, "message"),
|
||||
SkipOnAllPrevSkipped: maputil.GetBool(n.Config, "skipOnAllPrevSkipped"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -285,6 +285,7 @@ func (d *DeployerProvider) updateHttpsListenerCertificate(ctx context.Context, c
|
||||
ClientToken: generateClientToken(),
|
||||
ListenerPort: uint16(cloudHttpsListenerPort),
|
||||
Scheduler: describeAppHTTPSListenersResp.ListenerList[0].Scheduler,
|
||||
CertIds: describeAppHTTPSListenersResp.ListenerList[0].CertIds,
|
||||
AdditionalCertDomains: sliceutil.Map(describeAppHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceappblb.AdditionalCertDomainsModel) bceappblb.AdditionalCertDomainsModel {
|
||||
if domain.Host == d.config.Domain {
|
||||
return bceappblb.AdditionalCertDomainsModel{
|
||||
|
@ -283,6 +283,7 @@ func (d *DeployerProvider) updateHttpsListenerCertificate(ctx context.Context, c
|
||||
updateHTTPSListenerReq := &bceblb.UpdateHTTPSListenerArgs{
|
||||
ClientToken: generateClientToken(),
|
||||
ListenerPort: uint16(cloudHttpsListenerPort),
|
||||
CertIds: describeHTTPSListenersResp.ListenerList[0].CertIds,
|
||||
AdditionalCertDomains: sliceutil.Map(describeHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceblb.AdditionalCertDomainsModel) bceblb.AdditionalCertDomainsModel {
|
||||
if domain.Host == d.config.Domain {
|
||||
return bceblb.AdditionalCertDomainsModel{
|
||||
|
@ -136,7 +136,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
|
||||
}
|
||||
|
||||
// 循环获取部署任务详情,等待任务状态变更
|
||||
// REF: https://cloud.tencent.com.cn/document/api/400/91658
|
||||
// REF: https://cloud.tencent.com/document/api/400/91658
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
@ -153,7 +153,7 @@ func (d *DeployerProvider) deployViaSslService(ctx context.Context, cloudCertId
|
||||
}
|
||||
|
||||
// 循环获取部署任务详情,等待任务状态变更
|
||||
// REF: https://cloud.tencent.com.cn/document/api/400/91658
|
||||
// REF: https://cloud.tencent.com/document/api/400/91658
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
@ -104,7 +104,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
|
||||
}
|
||||
|
||||
// 循环获取部署任务详情,等待任务状态变更
|
||||
// REF: https://cloud.tencent.com.cn/document/api/400/91658
|
||||
// REF: https://cloud.tencent.com/document/api/400/91658
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
@ -119,7 +119,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
|
||||
}
|
||||
|
||||
// 循环获取部署任务详情,等待任务状态变更
|
||||
// REF: https://cloud.tencent.com.cn/document/api/400/91658
|
||||
// REF: https://cloud.tencent.com/document/api/400/91658
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
@ -106,7 +106,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
|
||||
}
|
||||
|
||||
// 循环获取部署任务详情,等待任务状态变更
|
||||
// REF: https://cloud.tencent.com.cn/document/api/400/91658
|
||||
// REF: https://cloud.tencent.com/document/api/400/91658
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
@ -6,11 +6,13 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/wangsu-certificate"
|
||||
wangsusdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/cdn"
|
||||
sliceutil "github.com/usual2970/certimate/internal/pkg/utils/slice"
|
||||
)
|
||||
|
||||
type DeployerConfig struct {
|
||||
@ -18,7 +20,7 @@ type DeployerConfig struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 网宿云 AccessKeySecret。
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
// 加速域名数组。
|
||||
// 加速域名数组(支持泛域名)。
|
||||
Domains []string `json:"domains"`
|
||||
}
|
||||
|
||||
@ -80,7 +82,10 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
|
||||
certId, _ := strconv.ParseInt(upres.CertId, 10, 64)
|
||||
batchUpdateCertificateConfigReq := &wangsusdk.BatchUpdateCertificateConfigRequest{
|
||||
CertificateId: certId,
|
||||
DomainNames: d.config.Domains,
|
||||
DomainNames: sliceutil.Map(d.config.Domains, func(domain string) string {
|
||||
// "*.example.com" → ".example.com",适配网宿云 CDN 要求的泛域名格式
|
||||
return strings.TrimPrefix(domain, "*")
|
||||
}),
|
||||
}
|
||||
batchUpdateCertificateConfigResp, err := d.sdkClient.BatchUpdateCertificateConfig(batchUpdateCertificateConfigReq)
|
||||
d.logger.Debug("sdk request 'cdn.BatchUpdateCertificateConfig'", slog.Any("request", batchUpdateCertificateConfigReq), slog.Any("response", batchUpdateCertificateConfigResp))
|
||||
|
@ -77,13 +77,14 @@ func (r *CertificateRepository) GetByWorkflowNodeId(ctx context.Context, workflo
|
||||
return r.castRecordToModel(records[0])
|
||||
}
|
||||
|
||||
func (r *CertificateRepository) GetByWorkflowRunId(ctx context.Context, workflowRunId string) (*domain.Certificate, error) {
|
||||
func (r *CertificateRepository) GetByWorkflowRunIdAndNodeId(ctx context.Context, workflowRunId string, workflowNodeId string) (*domain.Certificate, error) {
|
||||
records, err := app.GetApp().FindRecordsByFilter(
|
||||
domain.CollectionNameCertificate,
|
||||
"workflowRunId={:workflowRunId} && deleted=null",
|
||||
"workflowRunId={:workflowRunId} && workflowNodeId={:workflowNodeId} && deleted=null",
|
||||
"-created",
|
||||
1, 0,
|
||||
dbx.Params{"workflowRunId": workflowRunId},
|
||||
dbx.Params{"workflowNodeId": workflowNodeId},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -112,6 +112,7 @@ func (w *workflowInvoker) processNode(ctx context.Context, node *domain.Workflow
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
// TODO: 优化可读性
|
||||
if procErr != nil && current.Type == domain.WorkflowNodeTypeCondition {
|
||||
current = nil
|
||||
|
@ -3,6 +3,7 @@ package nodeprocessor
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@ -35,7 +36,8 @@ func NewApplyNode(node *domain.WorkflowNode) *applyNode {
|
||||
}
|
||||
|
||||
func (n *applyNode) Process(ctx context.Context) error {
|
||||
n.logger.Info("ready to obtain certificiate ...")
|
||||
nodeCfg := n.node.GetConfigForApply()
|
||||
n.logger.Info("ready to obtain certificiate ...", slog.Any("config", nodeCfg))
|
||||
|
||||
// 查询上次执行结果
|
||||
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
|
||||
@ -45,6 +47,7 @@ func (n *applyNode) Process(ctx context.Context) error {
|
||||
|
||||
// 检测是否可以跳过本次执行
|
||||
if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable {
|
||||
n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(true)
|
||||
n.logger.Info(fmt.Sprintf("skip this application, because %s", reason))
|
||||
return nil
|
||||
} else if reason != "" {
|
||||
@ -101,8 +104,8 @@ func (n *applyNode) Process(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// 保存 ARI 记录
|
||||
if applyResult.ARIReplaced {
|
||||
lastCertificate, _ := n.certRepo.GetByWorkflowRunId(ctx, lastOutput.RunId)
|
||||
if applyResult.ARIReplaced && lastOutput != nil {
|
||||
lastCertificate, _ := n.certRepo.GetByWorkflowRunIdAndNodeId(ctx, lastOutput.RunId, lastOutput.NodeId)
|
||||
if lastCertificate != nil {
|
||||
lastCertificate.ACMERenewed = true
|
||||
n.certRepo.Save(ctx, lastCertificate)
|
||||
@ -110,6 +113,7 @@ func (n *applyNode) Process(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// 记录中间结果
|
||||
n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(false)
|
||||
n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true)
|
||||
n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(time.Until(certificate.ExpireAt).Hours()/24), 10)
|
||||
|
||||
@ -120,39 +124,40 @@ func (n *applyNode) Process(ctx context.Context) error {
|
||||
func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) {
|
||||
if lastOutput != nil && lastOutput.Succeeded {
|
||||
// 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致
|
||||
currentNodeConfig := n.node.GetConfigForApply()
|
||||
lastNodeConfig := lastOutput.Node.GetConfigForApply()
|
||||
if currentNodeConfig.Domains != lastNodeConfig.Domains {
|
||||
thisNodeCfg := n.node.GetConfigForApply()
|
||||
lastNodeCfg := lastOutput.Node.GetConfigForApply()
|
||||
|
||||
if thisNodeCfg.Domains != lastNodeCfg.Domains {
|
||||
return false, "the configuration item 'Domains' changed"
|
||||
}
|
||||
if currentNodeConfig.ContactEmail != lastNodeConfig.ContactEmail {
|
||||
if thisNodeCfg.ContactEmail != lastNodeCfg.ContactEmail {
|
||||
return false, "the configuration item 'ContactEmail' changed"
|
||||
}
|
||||
if currentNodeConfig.Provider != lastNodeConfig.Provider {
|
||||
if thisNodeCfg.Provider != lastNodeCfg.Provider {
|
||||
return false, "the configuration item 'Provider' changed"
|
||||
}
|
||||
if currentNodeConfig.ProviderAccessId != lastNodeConfig.ProviderAccessId {
|
||||
if thisNodeCfg.ProviderAccessId != lastNodeCfg.ProviderAccessId {
|
||||
return false, "the configuration item 'ProviderAccessId' changed"
|
||||
}
|
||||
if !maps.Equal(currentNodeConfig.ProviderConfig, lastNodeConfig.ProviderConfig) {
|
||||
if !maps.Equal(thisNodeCfg.ProviderConfig, lastNodeCfg.ProviderConfig) {
|
||||
return false, "the configuration item 'ProviderConfig' changed"
|
||||
}
|
||||
if currentNodeConfig.CAProvider != lastNodeConfig.CAProvider {
|
||||
if thisNodeCfg.CAProvider != lastNodeCfg.CAProvider {
|
||||
return false, "the configuration item 'CAProvider' changed"
|
||||
}
|
||||
if currentNodeConfig.CAProviderAccessId != lastNodeConfig.CAProviderAccessId {
|
||||
if thisNodeCfg.CAProviderAccessId != lastNodeCfg.CAProviderAccessId {
|
||||
return false, "the configuration item 'CAProviderAccessId' changed"
|
||||
}
|
||||
if !maps.Equal(currentNodeConfig.CAProviderConfig, lastNodeConfig.CAProviderConfig) {
|
||||
if !maps.Equal(thisNodeCfg.CAProviderConfig, lastNodeCfg.CAProviderConfig) {
|
||||
return false, "the configuration item 'CAProviderConfig' changed"
|
||||
}
|
||||
if currentNodeConfig.KeyAlgorithm != lastNodeConfig.KeyAlgorithm {
|
||||
if thisNodeCfg.KeyAlgorithm != lastNodeCfg.KeyAlgorithm {
|
||||
return false, "the configuration item 'KeyAlgorithm' changed"
|
||||
}
|
||||
|
||||
lastCertificate, _ := n.certRepo.GetByWorkflowRunId(ctx, lastOutput.RunId)
|
||||
lastCertificate, _ := n.certRepo.GetByWorkflowRunIdAndNodeId(ctx, lastOutput.RunId, lastOutput.NodeId)
|
||||
if lastCertificate != nil {
|
||||
renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24
|
||||
renewalInterval := time.Duration(thisNodeCfg.SkipBeforeExpiryDays) * time.Hour * 24
|
||||
expirationTime := time.Until(lastCertificate.ExpireAt)
|
||||
if expirationTime > renewalInterval {
|
||||
daysLeft := int(expirationTime.Hours() / 24)
|
||||
@ -160,7 +165,7 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo
|
||||
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)
|
||||
return true, fmt.Sprintf("the certificate has already been issued (expires in %d day(s), next renewal in %d day(s))", daysLeft, thisNodeCfg.SkipBeforeExpiryDays)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,6 @@ func (n *conditionNode) Process(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (n *conditionNode) evalExpr(ctx context.Context, expression expr.Expr) (*expr.EvalResult, error) {
|
||||
variables := GetNodeOutputs(ctx)
|
||||
variables := GetAllNodeOutputs(ctx)
|
||||
return expression.Eval(variables)
|
||||
}
|
||||
|
@ -3,4 +3,5 @@ package nodeprocessor
|
||||
const (
|
||||
outputKeyForCertificateValidity = "certificate.validity"
|
||||
outputKeyForCertificateDaysLeft = "certificate.daysLeft"
|
||||
outputKeyForNodeSkipped = "node.skipped"
|
||||
)
|
||||
|
@ -25,6 +25,15 @@ func newNodeOutputsContainer() *nodeOutputsContainer {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取节点输出容器
|
||||
func getNodeOutputsContainer(ctx context.Context) *nodeOutputsContainer {
|
||||
value := ctx.Value(nodeOutputsKey)
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return value.(*nodeOutputsContainer)
|
||||
}
|
||||
|
||||
// 添加节点输出到上下文
|
||||
func AddNodeOutput(ctx context.Context, nodeId string, output map[string]any) context.Context {
|
||||
container := getNodeOutputsContainer(ctx)
|
||||
@ -50,7 +59,7 @@ func AddNodeOutput(ctx context.Context, nodeId string, output map[string]any) co
|
||||
func GetNodeOutput(ctx context.Context, nodeId string) map[string]any {
|
||||
container := getNodeOutputsContainer(ctx)
|
||||
if container == nil {
|
||||
return nil
|
||||
container = newNodeOutputsContainer()
|
||||
}
|
||||
|
||||
container.RLock()
|
||||
@ -69,22 +78,11 @@ func GetNodeOutput(ctx context.Context, nodeId string) map[string]any {
|
||||
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 {
|
||||
func GetAllNodeOutputs(ctx context.Context) map[string]map[string]any {
|
||||
container := getNodeOutputsContainer(ctx)
|
||||
if container == nil {
|
||||
return nil
|
||||
container = newNodeOutputsContainer()
|
||||
}
|
||||
|
||||
container.RLock()
|
||||
@ -103,26 +101,3 @@ func GetNodeOutputs(ctx context.Context) map[string]map[string]any {
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/usual2970/certimate/internal/deployer"
|
||||
@ -33,7 +34,8 @@ func NewDeployNode(node *domain.WorkflowNode) *deployNode {
|
||||
}
|
||||
|
||||
func (n *deployNode) Process(ctx context.Context) error {
|
||||
n.logger.Info("ready to deploy certificate ...")
|
||||
nodeCfg := n.node.GetConfigForDeploy()
|
||||
n.logger.Info("ready to deploy certificate ...", slog.Any("config", nodeCfg))
|
||||
|
||||
// 查询上次执行结果
|
||||
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
|
||||
@ -58,6 +60,7 @@ func (n *deployNode) Process(ctx context.Context) error {
|
||||
// 检测是否可以跳过本次执行
|
||||
if lastOutput != nil && certificate.CreatedAt.Before(lastOutput.UpdatedAt) {
|
||||
if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable {
|
||||
n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(true)
|
||||
n.logger.Info(fmt.Sprintf("skip this deployment, because %s", reason))
|
||||
return nil
|
||||
} else if reason != "" {
|
||||
@ -96,6 +99,9 @@ func (n *deployNode) Process(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录中间结果
|
||||
n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(false)
|
||||
|
||||
n.logger.Info("deployment completed")
|
||||
return nil
|
||||
}
|
||||
@ -103,16 +109,17 @@ func (n *deployNode) Process(ctx context.Context) error {
|
||||
func (n *deployNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) {
|
||||
if lastOutput != nil && lastOutput.Succeeded {
|
||||
// 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致
|
||||
currentNodeConfig := n.node.GetConfigForDeploy()
|
||||
lastNodeConfig := lastOutput.Node.GetConfigForDeploy()
|
||||
if currentNodeConfig.ProviderAccessId != lastNodeConfig.ProviderAccessId {
|
||||
thisNodeCfg := n.node.GetConfigForDeploy()
|
||||
lastNodeCfg := lastOutput.Node.GetConfigForDeploy()
|
||||
|
||||
if thisNodeCfg.ProviderAccessId != lastNodeCfg.ProviderAccessId {
|
||||
return false, "the configuration item 'ProviderAccessId' changed"
|
||||
}
|
||||
if !maps.Equal(currentNodeConfig.ProviderConfig, lastNodeConfig.ProviderConfig) {
|
||||
if !maps.Equal(thisNodeCfg.ProviderConfig, lastNodeCfg.ProviderConfig) {
|
||||
return false, "the configuration item 'ProviderConfig' changed"
|
||||
}
|
||||
|
||||
if currentNodeConfig.SkipOnLastSucceeded {
|
||||
if thisNodeCfg.SkipOnLastSucceeded {
|
||||
return true, "the certificate has already been deployed"
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,9 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -30,13 +32,12 @@ func NewMonitorNode(node *domain.WorkflowNode) *monitorNode {
|
||||
}
|
||||
|
||||
func (n *monitorNode) Process(ctx context.Context) error {
|
||||
n.logger.Info("ready to monitor certificate ...")
|
||||
|
||||
nodeCfg := n.node.GetConfigForMonitor()
|
||||
n.logger.Info("ready to monitor certificate ...", slog.Any("config", nodeCfg))
|
||||
|
||||
targetAddr := fmt.Sprintf("%s:%d", nodeCfg.Host, nodeCfg.Port)
|
||||
targetAddr := net.JoinHostPort(nodeCfg.Host, fmt.Sprintf("%d", nodeCfg.Port))
|
||||
if nodeCfg.Port == 0 {
|
||||
targetAddr = fmt.Sprintf("%s:443", nodeCfg.Host)
|
||||
targetAddr = net.JoinHostPort(nodeCfg.Host, "443")
|
||||
}
|
||||
|
||||
targetDomain := nodeCfg.Domain
|
||||
|
@ -2,7 +2,9 @@ package nodeprocessor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/notify"
|
||||
@ -28,9 +30,8 @@ func NewNotifyNode(node *domain.WorkflowNode) *notifyNode {
|
||||
}
|
||||
|
||||
func (n *notifyNode) Process(ctx context.Context) error {
|
||||
n.logger.Info("ready to send notification ...")
|
||||
|
||||
nodeCfg := n.node.GetConfigForNotify()
|
||||
n.logger.Info("ready to send notification ...", slog.Any("config", nodeCfg))
|
||||
|
||||
if nodeCfg.Provider == "" {
|
||||
// Deprecated: v0.4.x 将废弃
|
||||
@ -59,6 +60,12 @@ func (n *notifyNode) Process(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检测是否可以跳过本次执行
|
||||
if skippable := n.checkCanSkip(ctx); skippable {
|
||||
n.logger.Info(fmt.Sprintf("skip this notification, because all the previous nodes have been skipped"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 初始化通知器
|
||||
deployer, err := notify.NewWithWorkflowNode(notify.NotifierWithWorkflowNodeConfig{
|
||||
Node: n.node,
|
||||
@ -80,3 +87,21 @@ func (n *notifyNode) Process(ctx context.Context) error {
|
||||
n.logger.Info("notification completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *notifyNode) checkCanSkip(ctx context.Context) (_skip bool) {
|
||||
thisNodeCfg := n.node.GetConfigForNotify()
|
||||
if !thisNodeCfg.SkipOnAllPrevSkipped {
|
||||
return false
|
||||
}
|
||||
|
||||
prevNodeOutputs := GetAllNodeOutputs(ctx)
|
||||
for _, nodeOutput := range prevNodeOutputs {
|
||||
if nodeOutput[outputKeyForNodeSkipped] != nil {
|
||||
if nodeOutput[outputKeyForNodeSkipped].(string) != strconv.FormatBool(true) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ func (n *nodeOutputer) GetOutputs() map[string]any {
|
||||
|
||||
type certificateRepository interface {
|
||||
GetByWorkflowNodeId(ctx context.Context, workflowNodeId string) (*domain.Certificate, error)
|
||||
GetByWorkflowRunId(ctx context.Context, workflowRunId string) (*domain.Certificate, error)
|
||||
GetByWorkflowRunIdAndNodeId(ctx context.Context, workflowRunId string, workflowNodeId string) (*domain.Certificate, error)
|
||||
Save(ctx context.Context, certificate *domain.Certificate) (*domain.Certificate, error)
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ package nodeprocessor
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -32,9 +33,8 @@ func NewUploadNode(node *domain.WorkflowNode) *uploadNode {
|
||||
}
|
||||
|
||||
func (n *uploadNode) Process(ctx context.Context) error {
|
||||
n.logger.Info("ready to upload certiticate ...")
|
||||
|
||||
nodeCfg := n.node.GetConfigForUpload()
|
||||
n.logger.Info("ready to upload certiticate ...", slog.Any("config", nodeCfg))
|
||||
|
||||
// 查询上次执行结果
|
||||
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
|
||||
@ -44,6 +44,7 @@ func (n *uploadNode) Process(ctx context.Context) error {
|
||||
|
||||
// 检测是否可以跳过本次执行
|
||||
if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable {
|
||||
n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(true)
|
||||
n.logger.Info(fmt.Sprintf("skip this uploading, because %s", reason))
|
||||
return nil
|
||||
} else if reason != "" {
|
||||
@ -71,6 +72,7 @@ func (n *uploadNode) Process(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// 记录中间结果
|
||||
n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(false)
|
||||
n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true)
|
||||
n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(time.Until(certificate.ExpireAt).Hours()/24), 10)
|
||||
|
||||
@ -81,16 +83,17 @@ func (n *uploadNode) Process(ctx context.Context) error {
|
||||
func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) {
|
||||
if lastOutput != nil && lastOutput.Succeeded {
|
||||
// 比较和上次上传时的关键配置(即影响证书上传的)参数是否一致
|
||||
currentNodeConfig := n.node.GetConfigForUpload()
|
||||
lastNodeConfig := lastOutput.Node.GetConfigForUpload()
|
||||
if strings.TrimSpace(currentNodeConfig.Certificate) != strings.TrimSpace(lastNodeConfig.Certificate) {
|
||||
thisNodeCfg := n.node.GetConfigForUpload()
|
||||
lastNodeCfg := lastOutput.Node.GetConfigForUpload()
|
||||
|
||||
if strings.TrimSpace(thisNodeCfg.Certificate) != strings.TrimSpace(lastNodeCfg.Certificate) {
|
||||
return false, "the configuration item 'Certificate' changed"
|
||||
}
|
||||
if strings.TrimSpace(currentNodeConfig.PrivateKey) != strings.TrimSpace(lastNodeConfig.PrivateKey) {
|
||||
if strings.TrimSpace(thisNodeCfg.PrivateKey) != strings.TrimSpace(lastNodeCfg.PrivateKey) {
|
||||
return false, "the configuration item 'PrivateKey' changed"
|
||||
}
|
||||
|
||||
lastCertificate, _ := n.certRepo.GetByWorkflowRunId(ctx, lastOutput.RunId)
|
||||
lastCertificate, _ := n.certRepo.GetByWorkflowRunIdAndNodeId(ctx, lastOutput.RunId, lastOutput.NodeId)
|
||||
if lastCertificate != nil {
|
||||
daysLeft := int(time.Until(lastCertificate.ExpireAt).Hours() / 24)
|
||||
n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(daysLeft > 0)
|
||||
|
@ -28,7 +28,7 @@ import ACMEDns01ProviderSelect from "@/components/provider/ACMEDns01ProviderSele
|
||||
import CAProviderSelect from "@/components/provider/CAProviderSelect";
|
||||
import Show from "@/components/Show";
|
||||
import { ACCESS_USAGES, ACME_DNS01_PROVIDERS, accessProvidersMap, acmeDns01ProvidersMap, caProvidersMap } from "@/domain/provider";
|
||||
import { type WorkflowNodeConfigForApply } from "@/domain/workflow";
|
||||
import { type WorkflowNodeConfigForApply, defaultNodeConfigForApply } from "@/domain/workflow";
|
||||
import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks";
|
||||
import { useAccessesStore } from "@/stores/access";
|
||||
import { useContactEmailsStore } from "@/stores/contact";
|
||||
@ -59,11 +59,7 @@ export type ApplyNodeConfigFormInstance = {
|
||||
const MULTIPLE_INPUT_SEPARATOR = ";";
|
||||
|
||||
const initFormModel = (): ApplyNodeConfigFormFieldValues => {
|
||||
return {
|
||||
challengeType: "dns-01",
|
||||
keyAlgorithm: "RSA2048",
|
||||
skipBeforeExpiryDays: 30,
|
||||
};
|
||||
return defaultNodeConfigForApply();
|
||||
};
|
||||
|
||||
const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeConfigFormProps>(
|
||||
|
@ -4,7 +4,7 @@ import { Form, type FormInstance } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { type Expr, type WorkflowNodeConfigForCondition } from "@/domain/workflow";
|
||||
import { type Expr, type WorkflowNodeConfigForCondition, defaultNodeConfigForCondition } from "@/domain/workflow";
|
||||
import { useAntdForm } from "@/hooks";
|
||||
|
||||
import ConditionNodeConfigFormExpressionEditor, { type ConditionNodeConfigFormExpressionEditorInstance } from "./ConditionNodeConfigFormExpressionEditor";
|
||||
@ -29,7 +29,7 @@ export type ConditionNodeConfigFormInstance = {
|
||||
};
|
||||
|
||||
const initFormModel = (): ConditionNodeConfigFormFieldValues => {
|
||||
return {};
|
||||
return defaultNodeConfigForCondition();
|
||||
};
|
||||
|
||||
const ConditionNodeConfigForm = forwardRef<ConditionNodeConfigFormInstance, ConditionNodeConfigFormProps>(
|
||||
|
@ -11,7 +11,7 @@ import DeploymentProviderPicker from "@/components/provider/DeploymentProviderPi
|
||||
import DeploymentProviderSelect from "@/components/provider/DeploymentProviderSelect.tsx";
|
||||
import Show from "@/components/Show";
|
||||
import { ACCESS_USAGES, DEPLOYMENT_PROVIDERS, accessProvidersMap, deploymentProvidersMap } from "@/domain/provider";
|
||||
import { type WorkflowNodeConfigForDeploy, WorkflowNodeType } from "@/domain/workflow";
|
||||
import { type WorkflowNodeConfigForDeploy, WorkflowNodeType, defaultNodeConfigForDeploy } from "@/domain/workflow";
|
||||
import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks";
|
||||
import { useWorkflowStore } from "@/stores/workflow";
|
||||
|
||||
@ -117,9 +117,7 @@ export type DeployNodeConfigFormInstance = {
|
||||
};
|
||||
|
||||
const initFormModel = (): DeployNodeConfigFormFieldValues => {
|
||||
return {
|
||||
skipOnLastSucceeded: true,
|
||||
};
|
||||
return defaultNodeConfigForDeploy();
|
||||
};
|
||||
|
||||
const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNodeConfigFormProps>(
|
||||
|
@ -43,7 +43,7 @@ const DeployNodeConfigFormWangsuCDNConfig = ({
|
||||
if (!v) return false;
|
||||
return String(v)
|
||||
.split(MULTIPLE_INPUT_SEPARATOR)
|
||||
.every((e) => validDomainName(e));
|
||||
.every((e) => validDomainName(e, { allowWildcard: true }));
|
||||
}, t("workflow_node.deploy.form.wangsu_cdn_domains.placeholder")),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
@ -4,7 +4,7 @@ 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 { type WorkflowNodeConfigForMonitor, defaultNodeConfigForMonitor } from "@/domain/workflow";
|
||||
import { useAntdForm } from "@/hooks";
|
||||
import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators";
|
||||
|
||||
@ -25,11 +25,7 @@ export type MonitorNodeConfigFormInstance = {
|
||||
};
|
||||
|
||||
const initFormModel = (): MonitorNodeConfigFormFieldValues => {
|
||||
return {
|
||||
host: "",
|
||||
port: 443,
|
||||
requestPath: "/",
|
||||
};
|
||||
return defaultNodeConfigForMonitor();
|
||||
};
|
||||
|
||||
const MonitorNodeConfigForm = forwardRef<MonitorNodeConfigFormInstance, MonitorNodeConfigFormProps>(
|
||||
|
@ -2,7 +2,7 @@ import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } f
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
import { PlusOutlined as PlusOutlinedIcon, RightOutlined as RightOutlinedIcon } from "@ant-design/icons";
|
||||
import { Button, Divider, Form, type FormInstance, Input, Select, Typography } from "antd";
|
||||
import { Button, Divider, Flex, Form, type FormInstance, Input, Select, Switch, Typography } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
@ -12,7 +12,7 @@ import NotificationProviderSelect from "@/components/provider/NotificationProvid
|
||||
import Show from "@/components/Show";
|
||||
import { ACCESS_USAGES, NOTIFICATION_PROVIDERS, accessProvidersMap, notificationProvidersMap } from "@/domain/provider";
|
||||
import { notifyChannelsMap } from "@/domain/settings";
|
||||
import { type WorkflowNodeConfigForNotify } from "@/domain/workflow";
|
||||
import { type WorkflowNodeConfigForNotify, defaultNodeConfigForNotify } from "@/domain/workflow";
|
||||
import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks";
|
||||
import { useAccessesStore } from "@/stores/access";
|
||||
import { useNotifyChannelsStore } from "@/stores/notify";
|
||||
@ -41,7 +41,7 @@ export type NotifyNodeConfigFormInstance = {
|
||||
};
|
||||
|
||||
const initFormModel = (): NotifyNodeConfigFormFieldValues => {
|
||||
return {};
|
||||
return defaultNodeConfigForNotify();
|
||||
};
|
||||
|
||||
const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNodeConfigFormProps>(
|
||||
@ -74,6 +74,7 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
|
||||
.string({ message: t("workflow_node.notify.form.provider_access.placeholder") })
|
||||
.nonempty(t("workflow_node.notify.form.provider_access.placeholder")),
|
||||
providerConfig: z.any().nullish(),
|
||||
skipOnAllPrevSkipped: z.boolean().nullish(),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
const { form: formInst, formProps } = useAntdForm({
|
||||
@ -281,6 +282,27 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
|
||||
|
||||
{nestedFormEl}
|
||||
</Show>
|
||||
|
||||
<Divider size="small">
|
||||
<Typography.Text className="text-xs font-normal" type="secondary">
|
||||
{t("workflow_node.notify.form.strategy_config.label")}
|
||||
</Typography.Text>
|
||||
</Divider>
|
||||
|
||||
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
|
||||
<Form.Item label={t("workflow_node.notify.form.skip_on_all_prev_skipped.label")}>
|
||||
<Flex align="center" gap={8} wrap="wrap">
|
||||
<div>{t("workflow_node.notify.form.skip_on_all_prev_skipped.prefix")}</div>
|
||||
<Form.Item name="skipOnAllPrevSkipped" noStyle rules={[formRule]}>
|
||||
<Switch
|
||||
checkedChildren={t("workflow_node.notify.form.skip_on_all_prev_skipped.switch.on")}
|
||||
unCheckedChildren={t("workflow_node.notify.form.skip_on_all_prev_skipped.switch.off")}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div>{t("workflow_node.notify.form.skip_on_all_prev_skipped.suffix")}</div>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import dayjs from "dayjs";
|
||||
import { z } from "zod";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
import { WORKFLOW_TRIGGERS, type WorkflowNodeConfigForStart, type WorkflowTriggerType } from "@/domain/workflow";
|
||||
import { WORKFLOW_TRIGGERS, type WorkflowNodeConfigForStart, type WorkflowTriggerType, defaultNodeConfigForStart } from "@/domain/workflow";
|
||||
import { useAntdForm } from "@/hooks";
|
||||
import { getNextCronExecutions, validCronExpression } from "@/utils/cron";
|
||||
|
||||
@ -27,10 +27,7 @@ export type StartNodeConfigFormInstance = {
|
||||
};
|
||||
|
||||
const initFormModel = (): StartNodeConfigFormFieldValues => {
|
||||
return {
|
||||
trigger: WORKFLOW_TRIGGERS.AUTO,
|
||||
triggerCron: "0 0 * * *",
|
||||
};
|
||||
return defaultNodeConfigForStart();
|
||||
};
|
||||
|
||||
const StartNodeConfigForm = forwardRef<StartNodeConfigFormInstance, StartNodeConfigFormProps>(
|
||||
|
@ -6,7 +6,7 @@ import { z } from "zod";
|
||||
|
||||
import { validateCertificate, validatePrivateKey } from "@/api/certificates";
|
||||
import TextFileInput from "@/components/TextFileInput";
|
||||
import { type WorkflowNodeConfigForUpload } from "@/domain/workflow";
|
||||
import { type WorkflowNodeConfigForUpload, defaultNodeConfigForUpload } from "@/domain/workflow";
|
||||
import { useAntdForm } from "@/hooks";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
@ -27,7 +27,7 @@ export type UploadNodeConfigFormInstance = {
|
||||
};
|
||||
|
||||
const initFormModel = (): UploadNodeConfigFormFieldValues => {
|
||||
return {};
|
||||
return defaultNodeConfigForUpload();
|
||||
};
|
||||
|
||||
const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNodeConfigFormProps>(
|
||||
|
@ -1 +1 @@
|
||||
export const version = "v0.3.16";
|
||||
export const version = "v0.3.17";
|
||||
|
@ -133,6 +133,13 @@ export type WorkflowNodeConfigForStart = {
|
||||
triggerCron?: string;
|
||||
};
|
||||
|
||||
export const defaultNodeConfigForStart = (): Partial<WorkflowNodeConfigForStart> => {
|
||||
return {
|
||||
trigger: WORKFLOW_TRIGGERS.AUTO,
|
||||
triggerCron: "0 0 * * *",
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkflowNodeConfigForApply = {
|
||||
domains: string;
|
||||
contactEmail: string;
|
||||
@ -152,6 +159,14 @@ export type WorkflowNodeConfigForApply = {
|
||||
skipBeforeExpiryDays: number;
|
||||
};
|
||||
|
||||
export const defaultNodeConfigForApply = (): Partial<WorkflowNodeConfigForApply> => {
|
||||
return {
|
||||
challengeType: "dns-01",
|
||||
keyAlgorithm: "RSA2048",
|
||||
skipBeforeExpiryDays: 30,
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkflowNodeConfigForUpload = {
|
||||
certificateId: string;
|
||||
domains: string;
|
||||
@ -159,6 +174,10 @@ export type WorkflowNodeConfigForUpload = {
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
export const defaultNodeConfigForUpload = (): Partial<WorkflowNodeConfigForUpload> => {
|
||||
return {};
|
||||
};
|
||||
|
||||
export type WorkflowNodeConfigForMonitor = {
|
||||
host: string;
|
||||
port: number;
|
||||
@ -166,6 +185,13 @@ export type WorkflowNodeConfigForMonitor = {
|
||||
requestPath?: string;
|
||||
};
|
||||
|
||||
export const defaultNodeConfigForMonitor = (): Partial<WorkflowNodeConfigForMonitor> => {
|
||||
return {
|
||||
port: 443,
|
||||
requestPath: "/",
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkflowNodeConfigForDeploy = {
|
||||
certificate: string;
|
||||
provider: string;
|
||||
@ -174,6 +200,12 @@ export type WorkflowNodeConfigForDeploy = {
|
||||
skipOnLastSucceeded: boolean;
|
||||
};
|
||||
|
||||
export const defaultNodeConfigForDeploy = (): Partial<WorkflowNodeConfigForDeploy> => {
|
||||
return {
|
||||
skipOnLastSucceeded: true,
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkflowNodeConfigForNotify = {
|
||||
subject: string;
|
||||
message: string;
|
||||
@ -184,12 +216,21 @@ export type WorkflowNodeConfigForNotify = {
|
||||
provider: string;
|
||||
providerAccessId: string;
|
||||
providerConfig?: Record<string, unknown>;
|
||||
skipOnAllPrevSkipped?: boolean;
|
||||
};
|
||||
|
||||
export const defaultNodeConfigForNotify = (): Partial<WorkflowNodeConfigForNotify> => {
|
||||
return {};
|
||||
};
|
||||
|
||||
export type WorkflowNodeConfigForCondition = {
|
||||
expression?: Expr;
|
||||
};
|
||||
|
||||
export const defaultNodeConfigForCondition = (): Partial<WorkflowNodeConfigForCondition> => {
|
||||
return {};
|
||||
};
|
||||
|
||||
export type WorkflowNodeConfigForBranch = never;
|
||||
|
||||
export type WorkflowNodeConfigForEnd = never;
|
||||
@ -243,15 +284,18 @@ type InitWorkflowOptions = {
|
||||
};
|
||||
|
||||
export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel => {
|
||||
const root = newNode(WorkflowNodeType.Start, {}) as WorkflowNode;
|
||||
root.config = { trigger: WORKFLOW_TRIGGERS.MANUAL };
|
||||
const root = newNode(WorkflowNodeType.Start, {
|
||||
nodeConfig: { trigger: WORKFLOW_TRIGGERS.MANUAL },
|
||||
});
|
||||
|
||||
switch (options.template) {
|
||||
case "standard":
|
||||
{
|
||||
let current = root;
|
||||
|
||||
const applyNode = newNode(WorkflowNodeType.Apply);
|
||||
const applyNode = newNode(WorkflowNodeType.Apply, {
|
||||
nodeConfig: defaultNodeConfigForApply(),
|
||||
});
|
||||
current.next = applyNode;
|
||||
|
||||
current = current.next;
|
||||
@ -260,6 +304,7 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel =
|
||||
current = current.next!.branches![1];
|
||||
current.next = newNode(WorkflowNodeType.Notify, {
|
||||
nodeConfig: {
|
||||
...defaultNodeConfigForNotify(),
|
||||
subject: "[Certimate] Workflow Failure Alert!",
|
||||
message: "Your workflow run for the certificate application has failed. Please check the details.",
|
||||
} as WorkflowNodeConfigForNotify,
|
||||
@ -268,8 +313,8 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel =
|
||||
current = applyNode.next!.branches![0];
|
||||
current.next = newNode(WorkflowNodeType.Deploy, {
|
||||
nodeConfig: {
|
||||
...defaultNodeConfigForDeploy(),
|
||||
certificate: `${applyNode.id}#certificate`,
|
||||
skipOnLastSucceeded: true,
|
||||
} as WorkflowNodeConfigForDeploy,
|
||||
});
|
||||
|
||||
@ -279,6 +324,7 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel =
|
||||
current = current.next!.branches![1];
|
||||
current.next = newNode(WorkflowNodeType.Notify, {
|
||||
nodeConfig: {
|
||||
...defaultNodeConfigForNotify(),
|
||||
subject: "[Certimate] Workflow Failure Alert!",
|
||||
message: "Your workflow run for the certificate deployment has failed. Please check the details.",
|
||||
} as WorkflowNodeConfigForNotify,
|
||||
@ -290,7 +336,9 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel =
|
||||
{
|
||||
let current = root;
|
||||
|
||||
const monitorNode = newNode(WorkflowNodeType.Monitor);
|
||||
const monitorNode = newNode(WorkflowNodeType.Monitor, {
|
||||
nodeConfig: defaultNodeConfigForMonitor(),
|
||||
});
|
||||
current.next = monitorNode;
|
||||
|
||||
current = current.next;
|
||||
@ -299,6 +347,7 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel =
|
||||
current = current.next!.branches![1];
|
||||
current.next = newNode(WorkflowNodeType.Notify, {
|
||||
nodeConfig: {
|
||||
...defaultNodeConfigForNotify(),
|
||||
subject: "[Certimate] Workflow Failure Alert!",
|
||||
message: "Your workflow run for the certificate monitoring has failed. Please check the details.",
|
||||
} as WorkflowNodeConfigForNotify,
|
||||
@ -352,6 +401,7 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel =
|
||||
} as WorkflowNodeConfigForCondition;
|
||||
current.next = newNode(WorkflowNodeType.Notify, {
|
||||
nodeConfig: {
|
||||
...defaultNodeConfigForNotify(),
|
||||
subject: "[Certimate] Certificate Expiry Alert!",
|
||||
message: "The certificate will expire soon. Please pay attention to your website.",
|
||||
} as WorkflowNodeConfigForNotify,
|
||||
@ -380,6 +430,7 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel =
|
||||
} as WorkflowNodeConfigForCondition;
|
||||
current.next = newNode(WorkflowNodeType.Notify, {
|
||||
nodeConfig: {
|
||||
...defaultNodeConfigForNotify(),
|
||||
subject: "[Certimate] Certificate Expiry Alert!",
|
||||
message: "The certificate has already expired. Please pay attention to your website.",
|
||||
} as WorkflowNodeConfigForNotify,
|
||||
@ -458,18 +509,22 @@ export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions = {}
|
||||
return node;
|
||||
};
|
||||
|
||||
export const cloneNode = (sourceNode: WorkflowNode): WorkflowNode => {
|
||||
type CloneNodeOptions = {
|
||||
withCopySuffix?: boolean;
|
||||
};
|
||||
|
||||
export const cloneNode = (sourceNode: WorkflowNode, { withCopySuffix }: CloneNodeOptions = { withCopySuffix: true }): WorkflowNode => {
|
||||
const { produce } = new Immer({ autoFreeze: false });
|
||||
const deepClone = (node: WorkflowNode): WorkflowNode => {
|
||||
return produce(node, (draft) => {
|
||||
draft.id = nanoid();
|
||||
|
||||
if (draft.next) {
|
||||
draft.next = cloneNode(draft.next);
|
||||
draft.next = cloneNode(draft.next, { withCopySuffix });
|
||||
}
|
||||
|
||||
if (draft.branches) {
|
||||
draft.branches = draft.branches.map((branch) => cloneNode(branch));
|
||||
draft.branches = draft.branches.map((branch) => cloneNode(branch, { withCopySuffix }));
|
||||
}
|
||||
|
||||
return draft;
|
||||
@ -477,7 +532,7 @@ export const cloneNode = (sourceNode: WorkflowNode): WorkflowNode => {
|
||||
};
|
||||
|
||||
const copyNode = produce(sourceNode, (draft) => {
|
||||
draft.name = `${draft.name}-copy`;
|
||||
draft.name = withCopySuffix ? `${draft.name}-copy` : draft.name;
|
||||
});
|
||||
return deepClone(copyNode);
|
||||
};
|
||||
|
@ -7,6 +7,8 @@
|
||||
|
||||
"workflow.action.create": "Create workflow",
|
||||
"workflow.action.edit": "Edit workflow",
|
||||
"workflow.action.duplicate": "Duplicate workflow",
|
||||
"workflow.action.duplicate.confirm": "Are you sure to duplicate this workflow?",
|
||||
"workflow.action.delete": "Delete workflow",
|
||||
"workflow.action.delete.confirm": "Are you sure to delete this workflow?",
|
||||
"workflow.action.enable": "Enable",
|
||||
|
@ -682,11 +682,11 @@
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_region.tooltip": "For more information, see <a href=\"https://www.tencentcloud.com/document/product/1007/36573\" target=\"_blank\">https://www.tencentcloud.com/document/product/1007/36573</a>",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_type.label": "Tencent Cloud resource type",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_type.placeholder": "Please enter Tencent Cloud resource type",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_type.tooltip": "For more information, see <a href=\"https://cloud.tencent.com.cn/document/product/400/91667\" target=\"_blank\">https://cloud.tencent.com.cn/document/product/400/91667</a>",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_type.tooltip": "For more information, see <a href=\"https://cloud.tencent.com/document/product/400/91667\" target=\"_blank\">https://cloud.tencent.com/document/product/400/91667</a>",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.label": "Tencent Cloud resource IDs",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.placeholder": "Please enter Tencent Cloud resource IDs (separated by semicolons)",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.errmsg.invalid": "Please enter a valid Tencent Cloud resource ID",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.tooltip": "For more information, see <a href=\"https://cloud.tencent.com.cn/document/product/400/91667\" target=\"_blank\">https://cloud.tencent.com.cn/document/product/400/91667</a>",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.tooltip": "For more information, see <a href=\"https://cloud.tencent.com/document/product/400/91667\" target=\"_blank\">https://cloud.tencent.com/document/product/400/91667</a>",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.multiple_input_modal.title": "Change Tencent Cloud resource IDs",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.multiple_input_modal.placeholder": "Please enter Tencent Cloud resouce ID",
|
||||
"workflow_node.deploy.form.tencentcloud_vod_sub_app_id.label": "Tencent Cloud VOD App ID",
|
||||
@ -878,6 +878,12 @@
|
||||
"workflow_node.notify.form.webhook_data.tooltip": "Leave it blank to use the default Webhook data provided by the authorization.",
|
||||
"workflow_node.notify.form.webhook_data.guide": "<details><summary>Supported variables: </summary><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${SUBJECT}</strong>: The subject of notification.</li><li><strong>${MESSAGE}</strong>: The message of notification.</li></ol></details><br>Please visit the authorization management page for addtional notes.",
|
||||
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string",
|
||||
"workflow_node.notify.form.strategy_config.label": "Strategy settings",
|
||||
"workflow_node.notify.form.skip_on_all_prev_skipped.label": "Silent behavior",
|
||||
"workflow_node.notify.form.skip_on_all_prev_skipped.prefix": "If all the previous nodes were skipped, ",
|
||||
"workflow_node.notify.form.skip_on_all_prev_skipped.suffix": " to notify.",
|
||||
"workflow_node.notify.form.skip_on_all_prev_skipped.switch.on": "skip",
|
||||
"workflow_node.notify.form.skip_on_all_prev_skipped.switch.off": "not skip",
|
||||
|
||||
"workflow_node.end.label": "End",
|
||||
"workflow_node.end.default_name": "End",
|
||||
|
@ -7,6 +7,8 @@
|
||||
|
||||
"workflow.action.create": "新建工作流",
|
||||
"workflow.action.edit": "编辑工作流",
|
||||
"workflow.action.duplicate": "复制工作流",
|
||||
"workflow.action.duplicate.confirm": "确定要复制此工作流吗?",
|
||||
"workflow.action.delete": "删除工作流",
|
||||
"workflow.action.delete.confirm": "确定要删除此工作流吗?",
|
||||
"workflow.action.enable": "启用",
|
||||
|
@ -678,14 +678,14 @@
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy.guide": "小贴士:由于腾讯云证书部署任务是异步的,此节点若执行成功仅代表已创建部署任务,实际部署结果需要你自行前往腾讯云控制台查询。",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_region.label": "腾讯云云产品地域",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_region.placeholder": "请输入腾讯云云产品地域(例如:ap-guangzhou)",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_region.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com.cn/document/product/400/41659\" target=\"_blank\">https://cloud.tencent.com.cn/document/product/400/41659</a>",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_region.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/400/41659\" target=\"_blank\">https://cloud.tencent.com/document/product/400/41659</a>",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_type.label": "腾讯云云产品资源类型",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_type.placeholder": "请输入腾讯云产品资源类型",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_type.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com.cn/document/product/400/91667\" target=\"_blank\">https://cloud.tencent.com.cn/document/product/400/91667</a>",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_type.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/400/91667\" target=\"_blank\">https://cloud.tencent.com/document/product/400/91667</a>",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.label": "腾讯云云产品资源 ID",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.placeholder": "请输入腾讯云云产品资源 ID(多个值请用半角分号隔开)",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.errmsg.invalid": "请输入正确的腾讯云云产品资源 ID",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com.cn/document/product/400/91667\" target=\"_blank\">https://cloud.tencent.com.cn/document/product/400/91667</a><br><br>注意与各产品本身的实例 ID 区分。",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/400/91667\" target=\"_blank\">https://cloud.tencent.com/document/product/400/91667</a><br><br>注意与各产品本身的实例 ID 区分。",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.multiple_input_modal.title": "修改腾讯云云产品资源 ID",
|
||||
"workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.multiple_input_modal.placeholder": "请输入腾讯云云产品资源 ID",
|
||||
"workflow_node.deploy.form.tencentcloud_vod_sub_app_id.label": "腾讯云云点播应用 ID",
|
||||
@ -795,7 +795,7 @@
|
||||
"workflow_node.deploy.form.volcengine_tos_domain.placeholder": "请输入火山引擎 TOS 自定义域名",
|
||||
"workflow_node.deploy.form.volcengine_tos_domain.tooltip": "这是什么?请参阅 see <a href=\"https://console.volcengine.com/tos\" target=\"_blank\">https://console.volcengine.com/tos</a>",
|
||||
"workflow_node.deploy.form.wangsu_cdn_domains.label": "网宿云 CDN 加速域名",
|
||||
"workflow_node.deploy.form.wangsu_cdn_domains.placeholder": "请输入网宿云 CDN 加速域名(多个值请用半角分号隔开)",
|
||||
"workflow_node.deploy.form.wangsu_cdn_domains.placeholder": "请输入网宿云 CDN 加速域名(支持泛域名;多个值请用半角分号隔开)",
|
||||
"workflow_node.deploy.form.wangsu_cdn_domains.tooltip": "这是什么?请参阅 <a href=\"https://cdn.console.wangsu.com/v2/index/#/property/list\" target=\"_blank\">https://cdn.console.wangsu.com/v2/index/#/property/list</a>",
|
||||
"workflow_node.deploy.form.wangsu_cdn_domains.multiple_input_modal.title": "修改网宿云 CDN 加速域名",
|
||||
"workflow_node.deploy.form.wangsu_cdn_domains.multiple_input_modal.placeholder": "请输入网宿云 CDN 加速域名",
|
||||
@ -845,7 +845,7 @@
|
||||
"workflow_node.notify.form.subject.placeholder": "请输入通知主题",
|
||||
"workflow_node.notify.form.message.label": "通知内容",
|
||||
"workflow_node.notify.form.message.placeholder": "请输入通知内容",
|
||||
"workflow_node.notify.form.channel.label": "通知渠道(已废弃,请使用「通知渠道授权」字段)",
|
||||
"workflow_node.notify.form.channel.label": "通知渠道(即将废弃,请使用「通知渠道授权」字段)",
|
||||
"workflow_node.notify.form.channel.placeholder": "请选择通知渠道",
|
||||
"workflow_node.notify.form.channel.button": "设置",
|
||||
"workflow_node.notify.form.provider.label": "通知渠道",
|
||||
@ -877,6 +877,12 @@
|
||||
"workflow_node.notify.form.webhook_data.tooltip": "不填写时,将使用所选部署目标授权的默认 Webhook 回调数据。",
|
||||
"workflow_node.notify.form.webhook_data.guide": "<details><summary>支持的变量:</summary><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${SUBJECT}</strong>:通知主题。</li><li><strong>${MESSAGE}</strong>:通知内容。</ol></details><br>其他注意事项请前往授权管理页面查看。",
|
||||
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串",
|
||||
"workflow_node.notify.form.strategy_config.label": "执行策略",
|
||||
"workflow_node.notify.form.skip_on_all_prev_skipped.label": "静默行为",
|
||||
"workflow_node.notify.form.skip_on_all_prev_skipped.prefix": "当前序申请、上传、部署等节点均已跳过执行时,",
|
||||
"workflow_node.notify.form.skip_on_all_prev_skipped.suffix": "此通知节点。",
|
||||
"workflow_node.notify.form.skip_on_all_prev_skipped.switch.on": "跳过",
|
||||
"workflow_node.notify.form.skip_on_all_prev_skipped.switch.off": "不跳过",
|
||||
|
||||
"workflow_node.end.label": "结束",
|
||||
"workflow_node.end.default_name": "结束",
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
EditOutlined as EditOutlinedIcon,
|
||||
PlusOutlined as PlusOutlinedIcon,
|
||||
ReloadOutlined as ReloadOutlinedIcon,
|
||||
SnippetsOutlined as SnippetsOutlinedIcon,
|
||||
StopOutlined as StopOutlinedIcon,
|
||||
SyncOutlined as SyncOutlinedIcon,
|
||||
} from "@ant-design/icons";
|
||||
@ -39,7 +40,7 @@ import {
|
||||
import dayjs from "dayjs";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import { WORKFLOW_TRIGGERS, type WorkflowModel, isAllNodesValidated } from "@/domain/workflow";
|
||||
import { WORKFLOW_TRIGGERS, type WorkflowModel, cloneNode, initWorkflow, isAllNodesValidated } from "@/domain/workflow";
|
||||
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
|
||||
import { list as listWorkflows, remove as removeWorkflow, save as saveWorkflow } from "@/repository/workflow";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
@ -219,6 +220,17 @@ const WorkflowList = () => {
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={t("workflow.action.duplicate")}>
|
||||
<Button
|
||||
color="primary"
|
||||
icon={<SnippetsOutlinedIcon />}
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
handleDuplicateClick(record);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={t("workflow.action.delete")}>
|
||||
<Button
|
||||
color="danger"
|
||||
@ -321,6 +333,36 @@ const WorkflowList = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDuplicateClick = (workflow: WorkflowModel) => {
|
||||
modalApi.confirm({
|
||||
title: t("workflow.action.duplicate"),
|
||||
content: t("workflow.action.duplicate.confirm"),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const workflowCopy = {
|
||||
name: `${workflow.name}-copy`,
|
||||
description: workflow.description,
|
||||
trigger: workflow.trigger,
|
||||
triggerCron: workflow.triggerCron,
|
||||
draft: workflow.content
|
||||
? cloneNode(workflow.content, { withCopySuffix: false })
|
||||
: workflow.draft
|
||||
? cloneNode(workflow.draft, { withCopySuffix: false })
|
||||
: initWorkflow().draft,
|
||||
hasDraft: true,
|
||||
} as WorkflowModel;
|
||||
const resp = await saveWorkflow(workflowCopy);
|
||||
if (resp) {
|
||||
refreshData();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteClick = (workflow: WorkflowModel) => {
|
||||
modalApi.confirm({
|
||||
title: t("workflow.action.delete"),
|
||||
|
Loading…
x
Reference in New Issue
Block a user