Merge pull request #430 from fudiwei/feat/new-workflow

feat: enhance workflow
This commit is contained in:
Yoan.liu 2025-01-23 09:37:43 +08:00 committed by GitHub
commit c0386b153e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 910 additions and 538 deletions

28
go.mod
View File

@ -18,14 +18,14 @@ require (
github.com/baidubce/bce-sdk-go v0.9.214 github.com/baidubce/bce-sdk-go v0.9.214
github.com/byteplus-sdk/byteplus-sdk-golang v1.0.40 github.com/byteplus-sdk/byteplus-sdk-golang v1.0.40
github.com/go-acme/lego/v4 v4.21.0 github.com/go-acme/lego/v4 v4.21.0
github.com/go-resty/resty/v2 v2.16.3 github.com/go-resty/resty/v2 v2.16.4
github.com/go-viper/mapstructure/v2 v2.2.1 github.com/go-viper/mapstructure/v2 v2.2.1
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.132 github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.132
github.com/nikoksr/notify v1.3.0 github.com/nikoksr/notify v1.3.0
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0
github.com/pkg/sftp v1.13.7 github.com/pkg/sftp v1.13.7
github.com/pocketbase/dbx v1.11.0 github.com/pocketbase/dbx v1.11.0
github.com/pocketbase/pocketbase v0.24.3 github.com/pocketbase/pocketbase v0.24.4
github.com/qiniu/go-sdk/v7 v7.25.2 github.com/qiniu/go-sdk/v7 v7.25.2
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1084 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1084
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1084 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1084
@ -73,7 +73,6 @@ require (
github.com/google/gnostic-models v0.6.9 // indirect github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect github.com/google/gofuzz v1.2.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect
@ -92,9 +91,6 @@ require (
k8s.io/klog/v2 v2.130.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect
k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect
modernc.org/gc/v3 v3.0.0-20250105121824-520be1a3aee6 // indirect
modernc.org/strutil v1.2.1 // indirect
modernc.org/token v1.1.0 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect
@ -115,22 +111,22 @@ require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2 v1.33.0 // indirect github.com/aws/aws-sdk-go-v2 v1.33.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.0 // indirect github.com/aws/aws-sdk-go-v2/config v1.29.1 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.53 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.54 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.51 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.52 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.28 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.28 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.9 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.9 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.73.1 // indirect github.com/aws/aws-sdk-go-v2/service/s3 v1.73.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.10 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.24.11 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.9 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.8 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 // indirect
github.com/aws/smithy-go v1.22.1 // indirect github.com/aws/smithy-go v1.22.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect
@ -192,8 +188,8 @@ require (
google.golang.org/protobuf v1.36.3 // indirect google.golang.org/protobuf v1.36.3 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.55.3 // indirect modernc.org/libc v1.61.9 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect modernc.org/memory v1.8.2 // indirect
modernc.org/sqlite v1.34.4 // indirect modernc.org/sqlite v1.34.5 // indirect
) )

72
go.sum
View File

@ -214,14 +214,14 @@ github.com/aws/aws-sdk-go-v2 v1.33.0 h1:Evgm4DI9imD81V0WwD+TN4DCwjUMdc94TrduMLbg
github.com/aws/aws-sdk-go-v2 v1.33.0/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= github.com/aws/aws-sdk-go-v2 v1.33.0/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc=
github.com/aws/aws-sdk-go-v2/config v1.29.0 h1:Vk/u4jof33or1qAQLdofpjKV7mQQT7DcUpnYx8kdmxY= github.com/aws/aws-sdk-go-v2/config v1.29.1 h1:JZhGawAyZ/EuJeBtbQYnaoftczcb2drR2Iq36Wgz4sQ=
github.com/aws/aws-sdk-go-v2/config v1.29.0/go.mod h1:iXAZK3Gxvpq3tA+B9WaDYpZis7M8KFgdrDPMmHrgbJM= github.com/aws/aws-sdk-go-v2/config v1.29.1/go.mod h1:7bR2YD5euaxBhzt2y/oDkt3uNRb6tjFp98GlTFueRwk=
github.com/aws/aws-sdk-go-v2/credentials v1.17.53 h1:lwrVhiEDW5yXsuVKlFVUnR2R50zt2DklhOyeLETqDuE= github.com/aws/aws-sdk-go-v2/credentials v1.17.54 h1:4UmqeOqJPvdvASZWrKlhzpRahAulBfyTJQUaYy4+hEI=
github.com/aws/aws-sdk-go-v2/credentials v1.17.53/go.mod h1:CkqM1bIw/xjEpBMhBnvqUXYZbpCFuj6dnCAyDk2AtAY= github.com/aws/aws-sdk-go-v2/credentials v1.17.54/go.mod h1:RTdfo0P0hbbTxIhmQrOsC/PquBZGabEPnCaxxKRPSnI=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 h1:5grmdTdMsovn9kPZPI23Hhvp0ZyNm5cRO+IZFIYiAfw= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 h1:5grmdTdMsovn9kPZPI23Hhvp0ZyNm5cRO+IZFIYiAfw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24/go.mod h1:zqi7TVKTswH3Ozq28PkmBmgzG1tona7mo9G2IJg4Cis= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24/go.mod h1:zqi7TVKTswH3Ozq28PkmBmgzG1tona7mo9G2IJg4Cis=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.51 h1:Q0FNHs6JTGuoBWNQycD5LRSf+/WVHWEl+FwJ0tEDZUE= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.52 h1:6kI83R98XOnnyzHv9g9KTYXFawMyeQq8NeEERWMAwJk=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.51/go.mod h1:B9sW5/AD5bStKdTyUdz1xWRKOwnyUwJ4eJ4olQBtZo0= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.52/go.mod h1:Juj7unpf3CIrWpEyJZhRJ6rJl9IYX7Hd8HOlwaZq/LE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 h1:igORFSiH3bfq4lxKFkTSYDhJEUCYo6C8VKiWJjYwQuQ= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 h1:igORFSiH3bfq4lxKFkTSYDhJEUCYo6C8VKiWJjYwQuQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28/go.mod h1:3So8EA/aAYm36L7XIvCVwLa0s5N0P7o2b1oqnx/2R4g= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28/go.mod h1:3So8EA/aAYm36L7XIvCVwLa0s5N0P7o2b1oqnx/2R4g=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 h1:1mOW9zAUMhTSrMDssEHS/ajx8JcAj/IcftzcmNlmVLI= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 h1:1mOW9zAUMhTSrMDssEHS/ajx8JcAj/IcftzcmNlmVLI=
@ -233,22 +233,22 @@ github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.28/go.mod h1:pyaOYEdp1MJWgtXLy6q8
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.1 h1:mJ9FRktB8v1Ihpqwfk0AWvYEd0FgQtLsshc2Qb2TVc8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.2 h1:e6um6+DWYQP1XCa+E9YVtG/9v1qk5lyAOelMOVwSyO8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.1/go.mod h1:dIW8puxSbYLSPv/ju0d9A3CpwXdtqvJtYKDMVmPLOWE= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.2/go.mod h1:dIW8puxSbYLSPv/ju0d9A3CpwXdtqvJtYKDMVmPLOWE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 h1:TQmKDyETFGiXVhZfQ/I0cCFziqqX58pi4tKJGYGFSz0= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 h1:TQmKDyETFGiXVhZfQ/I0cCFziqqX58pi4tKJGYGFSz0=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9/go.mod h1:HVLPK2iHQBUx7HfZeOQSEu3v2ubZaAY2YPbAm5/WUyY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9/go.mod h1:HVLPK2iHQBUx7HfZeOQSEu3v2ubZaAY2YPbAm5/WUyY=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.9 h1:2aInXbh02XsbO0KobPGMNXyv2QP73VDKsWPNJARj/+4= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.9 h1:2aInXbh02XsbO0KobPGMNXyv2QP73VDKsWPNJARj/+4=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.9/go.mod h1:dgXS1i+HgWnYkPXqNoPIPKeUsUUYHaUbThC90aDnNiE= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.9/go.mod h1:dgXS1i+HgWnYkPXqNoPIPKeUsUUYHaUbThC90aDnNiE=
github.com/aws/aws-sdk-go-v2/service/route53 v1.48.1 h1:njgAP7Rtt4DGdTGFPhJ4gaZXCD1CDj/SZDa5W4ZgSTs= github.com/aws/aws-sdk-go-v2/service/route53 v1.48.1 h1:njgAP7Rtt4DGdTGFPhJ4gaZXCD1CDj/SZDa5W4ZgSTs=
github.com/aws/aws-sdk-go-v2/service/route53 v1.48.1/go.mod h1:TN4PcCL0lvqmYcv+AV8iZFC4Sd0FM06QDaoBXrFEftU= github.com/aws/aws-sdk-go-v2/service/route53 v1.48.1/go.mod h1:TN4PcCL0lvqmYcv+AV8iZFC4Sd0FM06QDaoBXrFEftU=
github.com/aws/aws-sdk-go-v2/service/s3 v1.73.1 h1:OzmyfYGiMCOIAq5pa0KWcaZoA9F8FqajOJevh+hhFdY= github.com/aws/aws-sdk-go-v2/service/s3 v1.73.2 h1:F3h8VYq9ZLBXYurmwrT8W0SPhgCcU0q+0WZJfT1dFt0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.73.1/go.mod h1:K+0a0kWDHAUXBH8GvYGS3cQRwIuRjO9bMWUz6vpNCaU= github.com/aws/aws-sdk-go-v2/service/s3 v1.73.2/go.mod h1:jGJ/v7FIi7Ys9t54tmEFnrxuaWeJLpwNgKp2DXAVhOU=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.10 h1:DyZUj3xSw3FR3TXSwDhPhuZkkT14QHBiacdbUVcD0Dg= github.com/aws/aws-sdk-go-v2/service/sso v1.24.11 h1:kuIyu4fTT38Kj7YCC7ouNbVZSSpqkZ+LzIfhCr6Dg+I=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.10/go.mod h1:Ro744S4fKiCCuZECXgOi760TiYylUM8ZBf6OGiZzJtY= github.com/aws/aws-sdk-go-v2/service/sso v1.24.11/go.mod h1:Ro744S4fKiCCuZECXgOi760TiYylUM8ZBf6OGiZzJtY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.9 h1:I1TsPEs34vbpOnR81GIcAq4/3Ud+jRHVGwx6qLQUHLs= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 h1:l+dgv/64iVlQ3WsBbnn+JSbkj01jIi+SM0wYsj3y/hY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.9/go.mod h1:Fzsj6lZEb8AkTE5S68OhcbBqeWPsR8RnGuKPr8Todl8= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10/go.mod h1:Fzsj6lZEb8AkTE5S68OhcbBqeWPsR8RnGuKPr8Todl8=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.8 h1:pqEJQtlKWvnv3B6VRt60ZmsHy3SotlEBvfUBPB1KVcM= github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 h1:BRVDbewN6VZcwr+FBOszDKvYeXY1kJ+GGMCcpghlw0U=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.8/go.mod h1:f6vjfZER1M17Fokn0IzssOTMT2N8ZSq+7jnNF0tArvw= github.com/aws/aws-sdk-go-v2/service/sts v1.33.9/go.mod h1:f6vjfZER1M17Fokn0IzssOTMT2N8ZSq+7jnNF0tArvw=
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
@ -393,8 +393,8 @@ github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.7.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= github.com/go-playground/validator/v10 v10.7.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk=
github.com/go-resty/resty/v2 v2.16.3 h1:zacNT7lt4b8M/io2Ahj6yPypL7bqx9n1iprfQuodV+E= github.com/go-resty/resty/v2 v2.16.4 h1:81IjtszQKwbz7dot4LLYGwhJNUsNwECD2O7nru5q60E=
github.com/go-resty/resty/v2 v2.16.3/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-resty/resty/v2 v2.16.4/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
@ -542,8 +542,6 @@ github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
@ -723,8 +721,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU= github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs= github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/pocketbase/pocketbase v0.24.3 h1:WUrzW11ijCySlDsVRHon3HXdtiratWv+ODK26/k6cI8= github.com/pocketbase/pocketbase v0.24.4 h1:kw/c23HccoxMV/19U9QlDcvNJgQ66vlUrxGQDZicWKM=
github.com/pocketbase/pocketbase v0.24.3/go.mod h1:EfXV/8RUY76jA6g1RPNHjOuW7wTd2bz0QlvAI/RU8YY= github.com/pocketbase/pocketbase v0.24.4/go.mod h1:EfXV/8RUY76jA6g1RPNHjOuW7wTd2bz0QlvAI/RU8YY=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@ -1385,29 +1383,27 @@ k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8X
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas= k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas=
k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0=
k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= modernc.org/ccgo/v4 v4.23.13 h1:PFiaemQwE/jdwi8XEHyEV+qYWoIuikLP3T4rvDeJb00=
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= modernc.org/ccgo/v4 v4.23.13/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0=
modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= modernc.org/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.0.0-20250105121824-520be1a3aee6 h1:JoKwHjIFumiKrjMbp1cNbC5E9UyCgA/ZcID0xOWQ2N8= modernc.org/libc v1.61.9 h1:PLSBXVkifXGELtJ5BOnBUyAHr7lsatNwFU/RRo4kfJM=
modernc.org/gc/v3 v3.0.0-20250105121824-520be1a3aee6/go.mod h1:LG5UO1Ran4OO0JRKz2oNiXhR5nNrgz0PzH7UKhz0aMU= modernc.org/libc v1.61.9/go.mod h1:61xrnzk/aR8gr5bR7Uj/lLFLuXu2/zMpIjcry63Eumk=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8= modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk= modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@ -82,9 +82,9 @@ type acmeAccountRepository interface {
var registerGroup singleflight.Group var registerGroup singleflight.Group
func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) { func registerAcmeUserWithSingleFlight(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) {
resp, err, _ := registerGroup.Do(fmt.Sprintf("register_acme_user_%s_%s", sslProviderConfig.Provider, user.GetEmail()), func() (interface{}, error) { resp, err, _ := registerGroup.Do(fmt.Sprintf("register_acme_user_%s_%s", sslProviderConfig.Provider, user.GetEmail()), func() (interface{}, error) {
return register(client, sslProviderConfig, user) return registerAcmeUser(client, sslProviderConfig, user)
}) })
if err != nil { if err != nil {
@ -94,7 +94,7 @@ func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderCon
return resp.(*registration.Resource), nil return resp.(*registration.Resource), nil
} }
func register(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) { func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) {
var reg *registration.Resource var reg *registration.Resource
var err error var err error
switch sslProviderConfig.Provider { switch sslProviderConfig.Provider {
@ -123,7 +123,6 @@ func register(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, use
} }
repo := repository.NewAcmeAccountRepository() repo := repository.NewAcmeAccountRepository()
resp, err := repo.GetByCAAndEmail(sslProviderConfig.Provider, user.GetEmail()) resp, err := repo.GetByCAAndEmail(sslProviderConfig.Provider, user.GetEmail())
if err == nil { if err == nil {
user.privkey = resp.Key user.privkey = resp.Key

View File

@ -159,7 +159,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
// New users need to register first // New users need to register first
if !acmeUser.hasRegistration() { if !acmeUser.hasRegistration() {
reg, err := registerAcmeUser(client, sslProviderConfig, acmeUser) reg, err := registerAcmeUserWithSingleFlight(client, sslProviderConfig, acmeUser)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to register: %w", err) return nil, fmt.Errorf("failed to register: %w", err)
} }

View File

@ -25,8 +25,8 @@ const (
) )
type certificateRepository interface { type certificateRepository interface {
GetById(ctx context.Context, id string) (*domain.Certificate, error)
ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error) ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error)
GetById(ctx context.Context, id string) (*domain.Certificate, error)
} }
type CertificateService struct { type CertificateService struct {

View File

@ -5,11 +5,6 @@ type CertificateArchiveFileReq struct {
Format string `json:"format"` Format string `json:"format"`
} }
type CertificateArchiveFileResp struct {
Certificate string `json:"certificate"`
PrivateKey string `json:"privateKey"`
}
type CertificateValidateCertificateReq struct { type CertificateValidateCertificateReq struct {
Certificate string `json:"certificate"` Certificate string `json:"certificate"`
} }

View File

@ -2,7 +2,12 @@
import "github.com/usual2970/certimate/internal/domain" import "github.com/usual2970/certimate/internal/domain"
type WorkflowRunReq struct { type WorkflowStartRunReq struct {
WorkflowId string `json:"-"` WorkflowId string `json:"-"`
Trigger domain.WorkflowTriggerType `json:"trigger"` Trigger domain.WorkflowTriggerType `json:"trigger"`
} }
type WorkflowCancelRunReq struct {
WorkflowId string `json:"-"`
RunId string `json:"-"`
}

View File

@ -69,11 +69,11 @@ type WorkflowNodeConfigForApply struct {
ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置 ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置
KeyAlgorithm string `json:"keyAlgorithm"` // 密钥算法 KeyAlgorithm string `json:"keyAlgorithm"` // 密钥算法
Nameservers string `json:"nameservers"` // DNS 服务器列表,以半角逗号分隔 Nameservers string `json:"nameservers"` // DNS 服务器列表,以半角逗号分隔
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout"` // DNS 传播超时时间(默认取决于提供商 DnsPropagationTimeout int32 `json:"dnsPropagationTimeout"` // DNS 传播超时时间(零值取决于提供商的默认值
DnsTTL int32 `json:"dnsTTL"` // DNS TTL默认取决于提供商 DnsTTL int32 `json:"dnsTTL"` // DNS TTL零值取决于提供商的默认值
DisableFollowCNAME bool `json:"disableFollowCNAME"` // 是否禁用 CNAME 跟随 DisableFollowCNAME bool `json:"disableFollowCNAME"` // 是否关闭 CNAME 跟随
DisableARI bool `json:"disableARI"` // 是否禁用 ARI DisableARI bool `json:"disableARI"` // 是否关闭 ARI
SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays"` // 证书到期前多少天前跳过续期(默认值:30 SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays"` // 证书到期前多少天前跳过续期(零值将使用默认值 30
} }
type WorkflowNodeConfigForUpload struct { type WorkflowNodeConfigForUpload struct {

View File

@ -1,6 +1,9 @@
package domain package domain
import "time" import (
"strings"
"time"
)
const CollectionNameWorkflowRun = "workflow_run" const CollectionNameWorkflowRun = "workflow_run"
@ -22,6 +25,7 @@ const (
WorkflowRunStatusTypeRunning WorkflowRunStatusType = "running" WorkflowRunStatusTypeRunning WorkflowRunStatusType = "running"
WorkflowRunStatusTypeSucceeded WorkflowRunStatusType = "succeeded" WorkflowRunStatusTypeSucceeded WorkflowRunStatusType = "succeeded"
WorkflowRunStatusTypeFailed WorkflowRunStatusType = "failed" WorkflowRunStatusTypeFailed WorkflowRunStatusType = "failed"
WorkflowRunStatusTypeCanceled WorkflowRunStatusType = "canceled"
) )
type WorkflowRunLog struct { type WorkflowRunLog struct {
@ -40,12 +44,13 @@ type WorkflowRunLogOutput struct {
type WorkflowRunLogs []WorkflowRunLog type WorkflowRunLogs []WorkflowRunLog
func (r WorkflowRunLogs) FirstError() string { func (r WorkflowRunLogs) ErrorString() string {
var builder strings.Builder
for _, log := range r { for _, log := range r {
if log.Error != "" { if log.Error != "" {
return log.Error builder.WriteString(log.Error)
builder.WriteString("\n")
} }
} }
return builder.String()
return ""
} }

View File

@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"strings" "strings"
"github.com/usual2970/certimate/internal/pkg/utils/types"
) )
// 表示默认的日志记录器类型。 // 表示默认的日志记录器类型。
@ -21,7 +23,7 @@ func (l *DefaultLogger) Logt(tag string, data ...any) {
temp[0] = tag temp[0] = tag
for i, v := range data { for i, v := range data {
s := "" s := ""
if v == nil { if types.IsNil(v) {
s = "<nil>" s = "<nil>"
} else { } else {
switch reflect.ValueOf(v).Kind() { switch reflect.ValueOf(v).Kind() {

View File

@ -1,12 +1,14 @@
package certs package certs
import ( import (
"crypto"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"errors" "errors"
"github.com/go-acme/lego/v4/certcrypto"
xerrors "github.com/pkg/errors" xerrors "github.com/pkg/errors"
) )
@ -34,6 +36,19 @@ func ParseCertificateFromPEM(certPem string) (cert *x509.Certificate, err error)
return cert, nil return cert, nil
} }
// 从 PEM 编码的私钥字符串解析并返回一个 crypto.PrivateKey 对象。
//
// 入参:
// - privkeyPem: 私钥 PEM 内容。
//
// 出参:
// - privkey: crypto.PrivateKey 对象,可能是 rsa.PrivateKey、ecdsa.PrivateKey 或 ed25519.PrivateKey。
// - err: 错误。
func ParsePrivateKeyFromPEM(privkeyPem string) (privkey crypto.PrivateKey, err error) {
pemData := []byte(privkeyPem)
return certcrypto.ParsePEMPrivateKey(pemData)
}
// 从 PEM 编码的私钥字符串解析并返回一个 ecdsa.PrivateKey 对象。 // 从 PEM 编码的私钥字符串解析并返回一个 ecdsa.PrivateKey 对象。
// //
// 入参: // 入参:

View File

@ -2,8 +2,6 @@
import ( import (
"bytes" "bytes"
"crypto/ecdsa"
"crypto/rsa"
"encoding/pem" "encoding/pem"
"errors" "errors"
"time" "time"
@ -28,24 +26,10 @@ func TransformCertificateFromPEMToPFX(certPem string, privkeyPem string, pfxPass
return nil, err return nil, err
} }
var privkey interface{} privkey, err := ParsePrivateKeyFromPEM(privkeyPem)
switch cert.PublicKey.(type) {
case *rsa.PublicKey:
{
privkey, err = ParsePKCS1PrivateKeyFromPEM(privkeyPem)
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
case *ecdsa.PublicKey:
{
privkey, err = ParseECPrivateKeyFromPEM(privkeyPem)
if err != nil {
return nil, err
}
}
}
pfxData, err := pkcs12.LegacyRC2.Encode(privkey, cert, nil, pfxPassword) pfxData, err := pkcs12.LegacyRC2.Encode(privkey, cert, nil, pfxPassword)
if err != nil { if err != nil {

View File

@ -3,6 +3,7 @@
import "reflect" import "reflect"
// 判断对象是否为 nil。 // 判断对象是否为 nil。
// 与直接使用 `obj == nil` 不同,该函数会正确判断接口类型对象的真实值是否为空。
// //
// 入参: // 入参:
// - value待判断的对象。 // - value待判断的对象。

View File

@ -19,11 +19,11 @@ func NewCertificateRepository() *CertificateRepository {
} }
func (r *CertificateRepository) ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error) { func (r *CertificateRepository) ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error) {
records, err := app.GetApp().FindRecordsByFilter( records, err := app.GetApp().FindAllRecords(
domain.CollectionNameCertificate, domain.CollectionNameCertificate,
"expireAt>DATETIME('now') && expireAt<DATETIME('now', '+20 days') && deleted=null", dbx.NewExp("expireAt>DATETIME('now')"),
"-created", dbx.NewExp("expireAt<DATETIME('now', '+20 days')"),
0, 0, dbx.NewExp("deleted=null"),
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -62,7 +62,8 @@ func (r *CertificateRepository) GetByWorkflowNodeId(ctx context.Context, workflo
records, err := app.GetApp().FindRecordsByFilter( records, err := app.GetApp().FindRecordsByFilter(
domain.CollectionNameCertificate, domain.CollectionNameCertificate,
"workflowNodeId={:workflowNodeId} && deleted=null", "workflowNodeId={:workflowNodeId} && deleted=null",
"-created", 1, 0, "-created",
1, 0,
dbx.Params{"workflowNodeId": workflowNodeId}, dbx.Params{"workflowNodeId": workflowNodeId},
) )
if err != nil { if err != nil {

View File

@ -55,10 +55,10 @@ func (r *WorkflowRepository) GetById(ctx context.Context, id string) (*domain.Wo
return r.castRecordToModel(record) return r.castRecordToModel(record)
} }
func (r *WorkflowRepository) Save(ctx context.Context, workflow *domain.Workflow) error { func (r *WorkflowRepository) Save(ctx context.Context, workflow *domain.Workflow) (*domain.Workflow, error) {
collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflow) collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflow)
if err != nil { if err != nil {
return err return workflow, err
} }
var record *core.Record var record *core.Record
@ -68,9 +68,9 @@ func (r *WorkflowRepository) Save(ctx context.Context, workflow *domain.Workflow
record, err = app.GetApp().FindRecordById(domain.CollectionNameWorkflow, workflow.Id) record, err = app.GetApp().FindRecordById(domain.CollectionNameWorkflow, workflow.Id)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return domain.ErrRecordNotFound return workflow, domain.ErrRecordNotFound
} }
return err return workflow, err
} }
} }
@ -86,18 +86,36 @@ func (r *WorkflowRepository) Save(ctx context.Context, workflow *domain.Workflow
record.Set("lastRunStatus", string(workflow.LastRunStatus)) record.Set("lastRunStatus", string(workflow.LastRunStatus))
record.Set("lastRunTime", workflow.LastRunTime) record.Set("lastRunTime", workflow.LastRunTime)
return app.GetApp().Save(record) if err := app.GetApp().Save(record); err != nil {
return workflow, err
} }
func (r *WorkflowRepository) SaveRun(ctx context.Context, workflowRun *domain.WorkflowRun) error { workflow.Id = record.Id
workflow.CreatedAt = record.GetDateTime("created").Time()
workflow.UpdatedAt = record.GetDateTime("updated").Time()
return workflow, nil
}
func (r *WorkflowRepository) SaveRun(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) {
collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowRun) collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowRun)
if err != nil { if err != nil {
return err return workflowRun, err
}
var workflowRunRecord *core.Record
if workflowRun.Id == "" {
workflowRunRecord = core.NewRecord(collection)
} else {
workflowRunRecord, err = app.GetApp().FindRecordById(domain.CollectionNameWorkflowRun, workflowRun.Id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return workflowRun, err
}
workflowRunRecord = core.NewRecord(collection)
}
} }
err = app.GetApp().RunInTransaction(func(txApp core.App) error { err = app.GetApp().RunInTransaction(func(txApp core.App) error {
workflowRunRecord := core.NewRecord(collection)
workflowRunRecord.Id = workflowRun.Id
workflowRunRecord.Set("workflowId", workflowRun.WorkflowId) workflowRunRecord.Set("workflowId", workflowRun.WorkflowId)
workflowRunRecord.Set("trigger", string(workflowRun.Trigger)) workflowRunRecord.Set("trigger", string(workflowRun.Trigger))
workflowRunRecord.Set("status", string(workflowRun.Status)) workflowRunRecord.Set("status", string(workflowRun.Status))
@ -115,6 +133,7 @@ func (r *WorkflowRepository) SaveRun(ctx context.Context, workflowRun *domain.Wo
return err return err
} }
workflowRecord.IgnoreUnchangedFields(true)
workflowRecord.Set("lastRunId", workflowRunRecord.Id) workflowRecord.Set("lastRunId", workflowRunRecord.Id)
workflowRecord.Set("lastRunStatus", workflowRunRecord.GetString("status")) workflowRecord.Set("lastRunStatus", workflowRunRecord.GetString("status"))
workflowRecord.Set("lastRunTime", workflowRunRecord.GetString("startedAt")) workflowRecord.Set("lastRunTime", workflowRunRecord.GetString("startedAt"))
@ -123,13 +142,17 @@ func (r *WorkflowRepository) SaveRun(ctx context.Context, workflowRun *domain.Wo
return err return err
} }
workflowRun.Id = workflowRunRecord.Id
workflowRun.CreatedAt = workflowRunRecord.GetDateTime("created").Time()
workflowRun.UpdatedAt = workflowRunRecord.GetDateTime("updated").Time()
return nil return nil
}) })
if err != nil { if err != nil {
return err return workflowRun, err
} }
return nil return workflowRun, nil
} }
func (r *WorkflowRepository) castRecordToModel(record *core.Record) (*domain.Workflow, error) { func (r *WorkflowRepository) castRecordToModel(record *core.Record) (*domain.Workflow, error) {

View File

@ -26,16 +26,31 @@ func NewCertificateHandler(router *router.RouterGroup[*core.RequestEvent], servi
} }
group := router.Group("/certificates") group := router.Group("/certificates")
group.POST("/{id}/archive", handler.run) group.POST("/{certificateId}/archive", handler.archiveFile)
group.POST("/validate/certificate", handler.validateCertificate) group.POST("/validate/certificate", handler.validateCertificate)
group.POST("/validate/private-key", handler.validatePrivateKey) group.POST("/validate/private-key", handler.validatePrivateKey)
} }
func (handler *CertificateHandler) archiveFile(e *core.RequestEvent) error {
req := &dtos.CertificateArchiveFileReq{}
req.CertificateId = e.Request.PathValue("certificateId")
if err := e.BindBody(req); err != nil {
return resp.Err(e, err)
}
if bt, err := handler.service.ArchiveFile(e.Request.Context(), req); err != nil {
return resp.Err(e, err)
} else {
return resp.Ok(e, bt)
}
}
func (handler *CertificateHandler) validateCertificate(e *core.RequestEvent) error { func (handler *CertificateHandler) validateCertificate(e *core.RequestEvent) error {
req := &dtos.CertificateValidateCertificateReq{} req := &dtos.CertificateValidateCertificateReq{}
if err := e.BindBody(req); err != nil { if err := e.BindBody(req); err != nil {
return resp.Err(e, err) return resp.Err(e, err)
} }
if rs, err := handler.service.ValidateCertificate(e.Request.Context(), req); err != nil { if rs, err := handler.service.ValidateCertificate(e.Request.Context(), req); err != nil {
return resp.Err(e, err) return resp.Err(e, err)
} else { } else {
@ -48,23 +63,10 @@ func (handler *CertificateHandler) validatePrivateKey(e *core.RequestEvent) erro
if err := e.BindBody(req); err != nil { if err := e.BindBody(req); err != nil {
return resp.Err(e, err) return resp.Err(e, err)
} }
if err := handler.service.ValidatePrivateKey(e.Request.Context(), req); err != nil { if err := handler.service.ValidatePrivateKey(e.Request.Context(), req); err != nil {
return resp.Err(e, err) return resp.Err(e, err)
} else { } else {
return resp.Ok(e, nil) return resp.Ok(e, nil)
} }
} }
func (handler *CertificateHandler) run(e *core.RequestEvent) error {
req := &dtos.CertificateArchiveFileReq{}
req.CertificateId = e.Request.PathValue("id")
if err := e.BindBody(req); err != nil {
return resp.Err(e, err)
}
if bt, err := handler.service.ArchiveFile(e.Request.Context(), req); err != nil {
return resp.Err(e, err)
} else {
return resp.Ok(e, bt)
}
}

View File

@ -11,7 +11,8 @@ import (
) )
type workflowService interface { type workflowService interface {
Run(ctx context.Context, req *dtos.WorkflowRunReq) error StartRun(ctx context.Context, req *dtos.WorkflowStartRunReq) error
CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) error
Stop(ctx context.Context) Stop(ctx context.Context)
} }
@ -25,17 +26,30 @@ func NewWorkflowHandler(router *router.RouterGroup[*core.RequestEvent], service
} }
group := router.Group("/workflows") group := router.Group("/workflows")
group.POST("/{id}/run", handler.run) group.POST("/{workflowId}/runs", handler.run)
group.POST("/{workflowId}/runs/{runId}/cancel", handler.cancel)
} }
func (handler *WorkflowHandler) run(e *core.RequestEvent) error { func (handler *WorkflowHandler) run(e *core.RequestEvent) error {
req := &dtos.WorkflowRunReq{} req := &dtos.WorkflowStartRunReq{}
req.WorkflowId = e.Request.PathValue("id") req.WorkflowId = e.Request.PathValue("workflowId")
if err := e.BindBody(req); err != nil { if err := e.BindBody(req); err != nil {
return resp.Err(e, err) return resp.Err(e, err)
} }
if err := handler.service.Run(e.Request.Context(), req); err != nil { if err := handler.service.StartRun(e.Request.Context(), req); err != nil {
return resp.Err(e, err)
}
return resp.Ok(e, nil)
}
func (handler *WorkflowHandler) cancel(e *core.RequestEvent) error {
req := &dtos.WorkflowCancelRunReq{}
req.WorkflowId = e.Request.PathValue("workflowId")
req.RunId = e.Request.PathValue("runId")
if err := handler.service.CancelRun(e.Request.Context(), req); err != nil {
return resp.Err(e, err) return resp.Err(e, err)
} }

View File

@ -65,7 +65,7 @@ func onWorkflowRecordCreateOrUpdate(ctx context.Context, record *core.Record) er
// 反之,重新添加定时任务 // 反之,重新添加定时任务
err := scheduler.Add(fmt.Sprintf("workflow#%s", workflowId), record.GetString("triggerCron"), func() { err := scheduler.Add(fmt.Sprintf("workflow#%s", workflowId), record.GetString("triggerCron"), func() {
NewWorkflowService(repository.NewWorkflowRepository()).Run(ctx, &dtos.WorkflowRunReq{ NewWorkflowService(repository.NewWorkflowRepository()).StartRun(ctx, &dtos.WorkflowStartRunReq{
WorkflowId: workflowId, WorkflowId: workflowId,
Trigger: domain.WorkflowTriggerTypeAuto, Trigger: domain.WorkflowTriggerTypeAuto,
}) })

View File

@ -134,7 +134,7 @@ func (a *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 lastCertificate != nil && expirationTime > renewalInterval { if lastCertificate != nil && expirationTime > renewalInterval {
return true, fmt.Sprintf("已申请过证书,且证书尚未临近过期(到期尚余 %d 天,距 %d 天时续期)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) return true, fmt.Sprintf("已申请过证书,且证书尚未临近过期(到期尚余 %d 天,预计距 %d 天时续期)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays)
} }
} }

View File

@ -19,15 +19,15 @@ func NewWorkflowProcessor(workflow *domain.Workflow) *workflowProcessor {
} }
} }
func (w *workflowProcessor) Log(ctx context.Context) []domain.WorkflowRunLog {
return w.logs
}
func (w *workflowProcessor) Run(ctx context.Context) error { func (w *workflowProcessor) Run(ctx context.Context) error {
ctx = setContextWorkflowId(ctx, w.workflow.Id) ctx = setContextWorkflowId(ctx, w.workflow.Id)
return w.processNode(ctx, w.workflow.Content) return w.processNode(ctx, w.workflow.Content)
} }
func (w *workflowProcessor) GetRunLogs() []domain.WorkflowRunLog {
return w.logs
}
func (w *workflowProcessor) processNode(ctx context.Context, node *domain.WorkflowNode) error { func (w *workflowProcessor) processNode(ctx context.Context, node *domain.WorkflowNode) error {
current := node current := node
for current != nil { for current != nil {
@ -39,8 +39,8 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl
} }
} }
var runErr error
var processor nodes.NodeProcessor var processor nodes.NodeProcessor
var runErr error
for { for {
if current.Type != domain.WorkflowNodeTypeBranch && current.Type != domain.WorkflowNodeTypeExecuteResultBranch { if current.Type != domain.WorkflowNodeTypeBranch && current.Type != domain.WorkflowNodeTypeExecuteResultBranch {
processor, runErr = nodes.GetProcessor(current) processor, runErr = nodes.GetProcessor(current)
@ -49,7 +49,6 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl
} }
runErr = processor.Run(ctx) runErr = processor.Run(ctx)
log := processor.Log(ctx) log := processor.Log(ctx)
if log != nil { if log != nil {
w.logs = append(w.logs, *log) w.logs = append(w.logs, *log)
@ -58,6 +57,7 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl
break break
} }
} }
break break
} }
@ -70,8 +70,8 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl
} else { } else {
current = current.Next current = current.Next
} }
} }
return nil return nil
} }
@ -79,10 +79,6 @@ func setContextWorkflowId(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, "workflow_id", id) return context.WithValue(ctx, "workflow_id", id)
} }
func GetWorkflowId(ctx context.Context) string {
return ctx.Value("workflow_id").(string)
}
func getBranchByType(branches []domain.WorkflowNode, nodeType domain.WorkflowNodeType) *domain.WorkflowNode { func getBranchByType(branches []domain.WorkflowNode, nodeType domain.WorkflowNodeType) *domain.WorkflowNode {
for _, branch := range branches { for _, branch := range branches {
if branch.Type == nodeType { if branch.Type == nodeType {

View File

@ -23,8 +23,8 @@ type workflowRunData struct {
type workflowRepository interface { type workflowRepository interface {
ListEnabledAuto(ctx context.Context) ([]*domain.Workflow, error) ListEnabledAuto(ctx context.Context) ([]*domain.Workflow, error)
GetById(ctx context.Context, id string) (*domain.Workflow, error) GetById(ctx context.Context, id string) (*domain.Workflow, error)
Save(ctx context.Context, workflow *domain.Workflow) error Save(ctx context.Context, workflow *domain.Workflow) (*domain.Workflow, error)
SaveRun(ctx context.Context, workflowRun *domain.WorkflowRun) error SaveRun(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error)
} }
type WorkflowService struct { type WorkflowService struct {
@ -35,35 +35,20 @@ type WorkflowService struct {
} }
func NewWorkflowService(repo workflowRepository) *WorkflowService { func NewWorkflowService(repo workflowRepository) *WorkflowService {
rs := &WorkflowService{ srv := &WorkflowService{
repo: repo, repo: repo,
ch: make(chan *workflowRunData, 1), ch: make(chan *workflowRunData, 1),
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
rs.cancel = cancel srv.cancel = cancel
rs.wg.Add(defaultRoutines) srv.wg.Add(defaultRoutines)
for i := 0; i < defaultRoutines; i++ { for i := 0; i < defaultRoutines; i++ {
go rs.process(ctx) go srv.run(ctx)
} }
return rs return srv
}
func (s *WorkflowService) process(ctx context.Context) {
defer s.wg.Done()
for {
select {
case data := <-s.ch:
// 执行
if err := s.run(ctx, data); err != nil {
app.GetLogger().Error("failed to run workflow", "id", data.Workflow.Id, "err", err)
}
case <-ctx.Done():
return
}
}
} }
func (s *WorkflowService) InitSchedule(ctx context.Context) error { func (s *WorkflowService) InitSchedule(ctx context.Context) error {
@ -75,7 +60,7 @@ func (s *WorkflowService) InitSchedule(ctx context.Context) error {
scheduler := app.GetScheduler() scheduler := app.GetScheduler()
for _, workflow := range workflows { for _, workflow := range workflows {
err := scheduler.Add(fmt.Sprintf("workflow#%s", workflow.Id), workflow.TriggerCron, func() { err := scheduler.Add(fmt.Sprintf("workflow#%s", workflow.Id), workflow.TriggerCron, func() {
s.Run(ctx, &dtos.WorkflowRunReq{ s.StartRun(ctx, &dtos.WorkflowStartRunReq{
WorkflowId: workflow.Id, WorkflowId: workflow.Id,
Trigger: domain.WorkflowTriggerTypeAuto, Trigger: domain.WorkflowTriggerTypeAuto,
}) })
@ -89,8 +74,7 @@ func (s *WorkflowService) InitSchedule(ctx context.Context) error {
return nil return nil
} }
func (s *WorkflowService) Run(ctx context.Context, req *dtos.WorkflowRunReq) error { func (s *WorkflowService) StartRun(ctx context.Context, req *dtos.WorkflowStartRunReq) error {
// 查询
workflow, err := s.repo.GetById(ctx, req.WorkflowId) workflow, err := s.repo.GetById(ctx, req.WorkflowId)
if err != nil { if err != nil {
app.GetLogger().Error("failed to get workflow", "id", req.WorkflowId, "err", err) app.GetLogger().Error("failed to get workflow", "id", req.WorkflowId, "err", err)
@ -101,13 +85,13 @@ func (s *WorkflowService) Run(ctx context.Context, req *dtos.WorkflowRunReq) err
return errors.New("workflow is running") return errors.New("workflow is running")
} }
// set last run
workflow.LastRunTime = time.Now() workflow.LastRunTime = time.Now()
workflow.LastRunStatus = domain.WorkflowRunStatusTypeRunning workflow.LastRunStatus = domain.WorkflowRunStatusTypePending
workflow.LastRunId = "" workflow.LastRunId = ""
if resp, err := s.repo.Save(ctx, workflow); err != nil {
if err := s.repo.Save(ctx, workflow); err != nil {
return err return err
} else {
workflow = resp
} }
s.ch <- &workflowRunData{ s.ch <- &workflowRunData{
@ -118,51 +102,70 @@ func (s *WorkflowService) Run(ctx context.Context, req *dtos.WorkflowRunReq) err
return nil return nil
} }
func (s *WorkflowService) run(ctx context.Context, runData *workflowRunData) error { func (s *WorkflowService) CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) error {
// 执行 // TODO: 取消运行,防止因为某些原因意外挂起(如进程被杀死)导致工作流一直处于 running 状态无法重新运行
workflow := runData.Workflow
run := &domain.WorkflowRun{
WorkflowId: workflow.Id,
Status: domain.WorkflowRunStatusTypeRunning,
Trigger: runData.RunTrigger,
StartedAt: time.Now(),
EndedAt: time.Now(),
}
processor := processor.NewWorkflowProcessor(workflow) return errors.New("TODO: 尚未实现")
if err := processor.Run(ctx); err != nil {
run.Status = domain.WorkflowRunStatusTypeFailed
run.EndedAt = time.Now()
run.Logs = processor.Log(ctx)
run.Error = err.Error()
if err := s.repo.SaveRun(ctx, run); err != nil {
app.GetLogger().Error("failed to save workflow run", "err", err)
}
return fmt.Errorf("failed to run workflow: %w", err)
}
// 保存日志
logs := processor.Log(ctx)
runStatus := domain.WorkflowRunStatusTypeSucceeded
runError := domain.WorkflowRunLogs(logs).FirstError()
if runError != "" {
runStatus = domain.WorkflowRunStatusTypeFailed
}
run.Status = runStatus
run.EndedAt = time.Now()
run.Logs = processor.Log(ctx)
run.Error = runError
if err := s.repo.SaveRun(ctx, run); err != nil {
app.GetLogger().Error("failed to save workflow run", "err", err)
return err
}
return nil
} }
func (s *WorkflowService) Stop(ctx context.Context) { func (s *WorkflowService) Stop(ctx context.Context) {
s.cancel() s.cancel()
s.wg.Wait() s.wg.Wait()
} }
func (s *WorkflowService) run(ctx context.Context) {
defer s.wg.Done()
for {
select {
case data := <-s.ch:
if err := s.runWithData(ctx, data); err != nil {
app.GetLogger().Error("failed to run workflow", "id", data.Workflow.Id, "err", err)
}
case <-ctx.Done():
return
}
}
}
func (s *WorkflowService) runWithData(ctx context.Context, runData *workflowRunData) error {
workflow := runData.Workflow
run := &domain.WorkflowRun{
WorkflowId: workflow.Id,
Status: domain.WorkflowRunStatusTypeRunning,
Trigger: runData.RunTrigger,
StartedAt: time.Now(),
}
if resp, err := s.repo.SaveRun(ctx, run); err != nil {
return err
} else {
run = resp
}
processor := processor.NewWorkflowProcessor(workflow)
if runErr := processor.Run(ctx); runErr != nil {
run.Status = domain.WorkflowRunStatusTypeFailed
run.EndedAt = time.Now()
run.Logs = processor.GetRunLogs()
run.Error = runErr.Error()
if _, err := s.repo.SaveRun(ctx, run); err != nil {
app.GetLogger().Error("failed to save workflow run", "err", err)
}
return fmt.Errorf("failed to run workflow: %w", runErr)
}
run.EndedAt = time.Now()
run.Logs = processor.GetRunLogs()
run.Error = domain.WorkflowRunLogs(run.Logs).ErrorString()
if run.Error == "" {
run.Status = domain.WorkflowRunStatusTypeSucceeded
} else {
run.Status = domain.WorkflowRunStatusTypeFailed
}
if _, err := s.repo.SaveRun(ctx, run); err != nil {
app.GetLogger().Error("failed to save workflow run", "err", err)
return err
}
return nil
}

View File

@ -0,0 +1,65 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(10, []byte(`{
"hidden": false,
"id": "zivdxh23",
"maxSelect": 1,
"name": "lastRunStatus",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"pending",
"running",
"succeeded",
"failed",
"canceled"
]
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(10, []byte(`{
"hidden": false,
"id": "zivdxh23",
"maxSelect": 1,
"name": "lastRunStatus",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"pending",
"running",
"succeeded",
"failed"
]
}`)); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@ -0,0 +1,65 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{
"hidden": false,
"id": "qldmh0tw",
"maxSelect": 1,
"name": "status",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"pending",
"running",
"succeeded",
"failed",
"canceled"
]
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{
"hidden": false,
"id": "qldmh0tw",
"maxSelect": 1,
"name": "status",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"pending",
"running",
"succeeded",
"failed"
]
}`)); err != nil {
return err
}
return app.Save(collection)
})
}

19
ui/package-lock.json generated
View File

@ -13,6 +13,7 @@
"ahooks": "^3.8.4", "ahooks": "^3.8.4",
"antd": "^5.23.1", "antd": "^5.23.1",
"antd-zod": "^6.0.1", "antd-zod": "^6.0.1",
"clsx": "^2.1.1",
"cron-parser": "^4.9.0", "cron-parser": "^4.9.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"i18next": "^24.2.1", "i18next": "^24.2.1",
@ -27,6 +28,7 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-i18next": "^15.4.0", "react-i18next": "^15.4.0",
"react-router-dom": "^7.1.3", "react-router-dom": "^7.1.3",
"tailwind-merge": "^2.6.0",
"zod": "^3.24.1", "zod": "^3.24.1",
"zustand": "^5.0.3" "zustand": "^5.0.3"
}, },
@ -4124,6 +4126,14 @@
"resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz", "resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
}, },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"engines": {
"node": ">=6"
}
},
"node_modules/commander": { "node_modules/commander": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz",
@ -8563,6 +8573,15 @@
"integrity": "sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==", "integrity": "sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==",
"dev": true "dev": true
}, },
"node_modules/tailwind-merge": {
"version": "2.6.0",
"resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
"integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.4.17", "version": "3.4.17",
"resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.17.tgz", "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.17.tgz",

View File

@ -15,6 +15,7 @@
"ahooks": "^3.8.4", "ahooks": "^3.8.4",
"antd": "^5.23.1", "antd": "^5.23.1",
"antd-zod": "^6.0.1", "antd-zod": "^6.0.1",
"clsx": "^2.1.1",
"cron-parser": "^4.9.0", "cron-parser": "^4.9.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"i18next": "^24.2.1", "i18next": "^24.2.1",
@ -29,6 +30,7 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-i18next": "^15.4.0", "react-i18next": "^15.4.0",
"react-router-dom": "^7.1.3", "react-router-dom": "^7.1.3",
"tailwind-merge": "^2.6.0",
"zod": "^3.24.1", "zod": "^3.24.1",
"zustand": "^5.0.3" "zustand": "^5.0.3"
}, },

View File

@ -3,10 +3,10 @@ import { ClientResponseError } from "pocketbase";
import { type CertificateFormatType } from "@/domain/certificate"; import { type CertificateFormatType } from "@/domain/certificate";
import { getPocketBase } from "@/repository/_pocketbase"; import { getPocketBase } from "@/repository/_pocketbase";
export const archive = async (id: string, format?: CertificateFormatType) => { export const archive = async (certificateId: string, format?: CertificateFormatType) => {
const pb = getPocketBase(); const pb = getPocketBase();
const resp = await pb.send<BaseResponse>(`/api/certificates/${encodeURIComponent(id)}/archive`, { const resp = await pb.send<BaseResponse<string>>(`/api/certificates/${encodeURIComponent(certificateId)}/archive`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -38,9 +38,11 @@ export const validateCertificate = async (certificate: string) => {
certificate: certificate, certificate: certificate,
}, },
}); });
if (resp.code != 0) { if (resp.code != 0) {
throw new Error(resp.msg); throw new ClientResponseError({ status: resp.code, response: resp, data: {} });
} }
return resp; return resp;
}; };
@ -55,8 +57,10 @@ export const validatePrivateKey = async (privateKey: string) => {
privateKey: privateKey, privateKey: privateKey,
}, },
}); });
if (resp.code != 0) { if (resp.code != 0) {
throw new Error(resp.msg); throw new ClientResponseError({ status: resp.code, response: resp, data: {} });
} }
return resp; return resp;
}; };

View File

@ -3,10 +3,10 @@ import { ClientResponseError } from "pocketbase";
import { WORKFLOW_TRIGGERS } from "@/domain/workflow"; import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
import { getPocketBase } from "@/repository/_pocketbase"; import { getPocketBase } from "@/repository/_pocketbase";
export const run = async (id: string) => { export const startRun = async (workflowId: string) => {
const pb = getPocketBase(); const pb = getPocketBase();
const resp = await pb.send<BaseResponse>(`/api/workflows/${encodeURIComponent(id)}/run`, { const resp = await pb.send<BaseResponse>(`/api/workflows/${encodeURIComponent(workflowId)}/runs`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -22,3 +22,20 @@ export const run = async (id: string) => {
return resp; return resp;
}; };
export const cancelRun = async (workflowId: string, runId: string) => {
const pb = getPocketBase();
const resp = await pb.send<BaseResponse>(`/api/workflows/${encodeURIComponent(workflowId)}/runs/${encodeURIComponent(runId)}/cancel`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
if (resp.code != 0) {
throw new ClientResponseError({ status: resp.code, response: resp, data: {} });
}
return resp;
};

View File

@ -49,9 +49,7 @@ const DrawerForm = <T extends NonNullable<unknown> = any>({
const triggerEl = useTriggerElement(trigger, { const triggerEl = useTriggerElement(trigger, {
onClick: () => { onClick: () => {
console.log("click");
setOpen(true); setOpen(true);
console.log(open);
}, },
}); });

View File

@ -27,7 +27,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
const blob = new Blob([u8arr], { type: "application/zip" }); const blob = new Blob([u8arr], { type: "application/zip" });
saveAs(blob, `${data.id}-${data.subjectAltNames}.zip`); saveAs(blob, `${data.id}-${data.subjectAltNames}.zip`);
} catch (err) { } catch (err) {
console.log(err); console.error(err);
messageApi.warning(t("common.text.operation_failed")); messageApi.warning(t("common.text.operation_failed"));
} }
}; };

View File

@ -0,0 +1,41 @@
import { useState } from "react";
import { ExpandOutlined as ExpandOutlinedIcon, MinusOutlined as MinusOutlinedIcon, PlusOutlined as PlusOutlinedIcon } from "@ant-design/icons";
import { Button, Card, Typography } from "antd";
import WorkflowElements from "@/components/workflow/WorkflowElements";
import { mergeCls } from "@/utils/css";
export type WorkflowElementsProps = {
className?: string;
style?: React.CSSProperties;
disabled?: boolean;
};
const WorkflowElementsContainer = ({ className, style, disabled }: WorkflowElementsProps) => {
const [scale, setScale] = useState(1);
return (
<div className={mergeCls("relative size-full overflow-hidden", className)} style={style}>
<div className="size-full overflow-auto">
<div className="relative z-[1]">
<div className="origin-center transition-transform duration-300" style={{ zoom: `${scale}` }}>
<div className="p-4">
<WorkflowElements disabled={disabled} />
</div>
</div>
</div>
</div>
<Card className="absolute bottom-4 right-6 z-[2] rounded-lg p-2 shadow-lg" styles={{ body: { padding: 0 } }}>
<div className="flex items-center gap-2">
<Button icon={<MinusOutlinedIcon />} disabled={scale <= 0.5} onClick={() => setScale((s) => Math.max(0.5, s - 0.1))} />
<Typography.Text className="min-w-[3em] text-center">{Math.round(scale * 100)}%</Typography.Text>
<Button icon={<PlusOutlinedIcon />} disabled={scale >= 2} onClick={() => setScale((s) => Math.min(2, s + 0.1))} />
<Button icon={<ExpandOutlinedIcon />} onClick={() => setScale(1)} />
</div>
</Card>
</div>
);
};
export default WorkflowElementsContainer;

View File

@ -41,11 +41,11 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR
</Show> </Show>
<div className="mt-4 rounded-md bg-black p-4 text-stone-200"> <div className="mt-4 rounded-md bg-black p-4 text-stone-200">
<div className="flex flex-col space-y-3"> <div className="flex flex-col space-y-4">
{data!.logs?.map((item, i) => { {data!.logs?.map((item, i) => {
return ( return (
<div key={i} className="flex flex-col space-y-2"> <div key={i} className="flex flex-col space-y-2">
<div>{item.nodeName}</div> <div className="font-semibold">{item.nodeName}</div>
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
{item.outputs?.map((output, j) => { {item.outputs?.map((output, j) => {
return ( return (

View File

@ -4,17 +4,21 @@ import {
CheckCircleOutlined as CheckCircleOutlinedIcon, CheckCircleOutlined as CheckCircleOutlinedIcon,
ClockCircleOutlined as ClockCircleOutlinedIcon, ClockCircleOutlined as ClockCircleOutlinedIcon,
CloseCircleOutlined as CloseCircleOutlinedIcon, CloseCircleOutlined as CloseCircleOutlinedIcon,
DeleteOutlined as DeleteOutlinedIcon,
PauseCircleOutlined as PauseCircleOutlinedIcon,
PauseOutlined as PauseOutlinedIcon,
SelectOutlined as SelectOutlinedIcon, SelectOutlined as SelectOutlinedIcon,
SyncOutlined as SyncOutlinedIcon, SyncOutlined as SyncOutlinedIcon,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useRequest } from "ahooks"; import { useRequest } from "ahooks";
import { Button, Empty, Table, type TableProps, Tag, notification } from "antd"; import { Button, Empty, Modal, Table, type TableProps, Tag, Tooltip, notification } from "antd";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase"; import { ClientResponseError } from "pocketbase";
import { cancelRun as cancelWorkflowRun } from "@/api/workflows";
import { WORKFLOW_TRIGGERS } from "@/domain/workflow"; import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun"; import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
import { list as listWorkflowRuns } from "@/repository/workflowRun"; import { list as listWorkflowRuns, remove as removeWorkflowRun } from "@/repository/workflowRun";
import { getErrMsg } from "@/utils/error"; import { getErrMsg } from "@/utils/error";
import WorkflowRunDetailDrawer from "./WorkflowRunDetailDrawer"; import WorkflowRunDetailDrawer from "./WorkflowRunDetailDrawer";
@ -27,6 +31,7 @@ export type WorkflowRunsProps = {
const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => { const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [modalApi, ModelContextHolder] = Modal.useModal();
const [notificationApi, NotificationContextHolder] = notification.useNotification(); const [notificationApi, NotificationContextHolder] = notification.useNotification();
const tableColumns: TableProps<WorkflowRunModel>["columns"] = [ const tableColumns: TableProps<WorkflowRunModel>["columns"] = [
@ -68,6 +73,12 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
{t("workflow_run.props.status.failed")} {t("workflow_run.props.status.failed")}
</Tag> </Tag>
); );
} else if (record.status === WORKFLOW_RUN_STATUSES.CANCELED) {
return (
<Tag icon={<PauseCircleOutlinedIcon />} color="warning">
{t("workflow_run.props.status.canceled")}
</Tag>
);
} }
return <></>; return <></>;
@ -116,11 +127,51 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
align: "end", align: "end",
fixed: "right", fixed: "right",
width: 120, width: 120,
render: (_, record) => ( render: (_, record) => {
const allowCancel = record.status === WORKFLOW_RUN_STATUSES.PENDING || record.status === WORKFLOW_RUN_STATUSES.RUNNING;
const aloowDelete =
record.status === WORKFLOW_RUN_STATUSES.SUCCEEDED ||
record.status === WORKFLOW_RUN_STATUSES.FAILED ||
record.status === WORKFLOW_RUN_STATUSES.CANCELED;
return (
<Button.Group> <Button.Group>
<WorkflowRunDetailDrawer data={record} trigger={<Button color="primary" icon={<SelectOutlinedIcon />} variant="text" />} /> <WorkflowRunDetailDrawer
data={record}
trigger={
<Tooltip title={t("workflow_run.action.view")}>
<Button color="primary" icon={<SelectOutlinedIcon />} variant="text" />
</Tooltip>
}
/>
<Tooltip title={t("workflow_run.action.cancel")}>
<Button
color="default"
disabled={!allowCancel}
icon={<PauseOutlinedIcon />}
variant="text"
onClick={() => {
handleCancelClick(record);
}}
/>
</Tooltip>
<Tooltip title={t("workflow_run.action.delete")}>
<Button
color="danger"
danger
disabled={!aloowDelete}
icon={<DeleteOutlinedIcon />}
variant="text"
onClick={() => {
handleDeleteClick(record);
}}
/>
</Tooltip>
</Button.Group> </Button.Group>
), );
},
}, },
]; ];
const [tableData, setTableData] = useState<WorkflowRunModel[]>([]); const [tableData, setTableData] = useState<WorkflowRunModel[]>([]);
@ -129,7 +180,11 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
const [page, setPage] = useState<number>(1); const [page, setPage] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10); const [pageSize, setPageSize] = useState<number>(10);
const { loading, error: loadedError } = useRequest( const {
loading,
error: loadedError,
run: refreshData,
} = useRequest(
() => { () => {
return listWorkflowRuns({ return listWorkflowRuns({
workflowId: workflowId, workflowId: workflowId,
@ -156,8 +211,46 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
} }
); );
const handleCancelClick = (workflowRun: WorkflowRunModel) => {
modalApi.confirm({
title: t("workflow_run.action.cancel"),
content: t("workflow_run.action.cancel.confirm"),
onOk: async () => {
try {
const resp = await cancelWorkflowRun(workflowId, workflowRun.id);
if (resp) {
refreshData();
}
} catch (err) {
console.error(err);
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
}
},
});
};
const handleDeleteClick = (workflowRun: WorkflowRunModel) => {
modalApi.confirm({
title: t("workflow_run.action.delete"),
content: t("workflow_run.action.delete.confirm"),
onOk: async () => {
try {
const resp = await removeWorkflowRun(workflowRun);
if (resp) {
setTableData((prev) => prev.filter((item) => item.id !== workflowRun.id));
refreshData();
}
} catch (err) {
console.error(err);
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
}
},
});
};
return ( return (
<> <>
{ModelContextHolder}
{NotificationContextHolder} {NotificationContextHolder}
<div className={className} style={style}> <div className={className} style={style}>

View File

@ -2,11 +2,11 @@ import { memo, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
CloudUploadOutlined as CloudUploadOutlinedIcon, CloudUploadOutlined as CloudUploadOutlinedIcon,
DeploymentUnitOutlined as DeploymentUnitOutlinedIcon,
PlusOutlined as PlusOutlinedIcon, PlusOutlined as PlusOutlinedIcon,
SendOutlined as SendOutlinedIcon, SendOutlined as SendOutlinedIcon,
SisternodeOutlined as SisternodeOutlinedIcon, SisternodeOutlined as SisternodeOutlinedIcon,
SolutionOutlined as SolutionOutlinedIcon, SolutionOutlined as SolutionOutlinedIcon,
SafetyOutlined as SafetyOutlinedIcon,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Dropdown } from "antd"; import { Dropdown } from "antd";
@ -26,15 +26,15 @@ const AddNode = ({ node, disabled }: AddNodeProps) => {
const dropdownMenus = useMemo(() => { const dropdownMenus = useMemo(() => {
return [ return [
[WorkflowNodeType.Apply, "workflow_node.apply.label", <SolutionOutlinedIcon />], [WorkflowNodeType.Apply, "workflow_node.apply.label", <SolutionOutlinedIcon />],
[WorkflowNodeType.Upload, "workflow_node.upload.label", <SafetyOutlinedIcon />], [WorkflowNodeType.Upload, "workflow_node.upload.label", <CloudUploadOutlinedIcon />],
[WorkflowNodeType.Deploy, "workflow_node.deploy.label", <CloudUploadOutlinedIcon />], [WorkflowNodeType.Deploy, "workflow_node.deploy.label", <DeploymentUnitOutlinedIcon />],
[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 />],
[WorkflowNodeType.Notify, "workflow_node.notify.label", <SendOutlinedIcon />],
] ]
.filter(([type]) => { .filter(([type]) => {
if (node.type !== WorkflowNodeType.Apply && node.type !== WorkflowNodeType.Deploy && type === WorkflowNodeType.ExecuteResultBranch) { if (node.type !== WorkflowNodeType.Apply && node.type !== WorkflowNodeType.Deploy && node.type !== WorkflowNodeType.Notify) {
return false; return type !== WorkflowNodeType.ExecuteResultBranch;
} }
return true; return true;

View File

@ -16,6 +16,8 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP
return ( return (
<> <>
<Popover <Popover
classNames={{ root: "shadow-md" }}
styles={{ body: { padding: 0 } }}
arrow={false} arrow={false}
content={ content={
<SharedNode.Menu <SharedNode.Menu
@ -26,8 +28,6 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP
trigger={<Button color="primary" icon={<MoreOutlinedIcon />} variant="text" />} trigger={<Button color="primary" icon={<MoreOutlinedIcon />} variant="text" />}
/> />
} }
overlayClassName="shadow-md"
overlayInnerStyle={{ padding: 0 }}
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>

View File

@ -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 { Alert, Button, Divider, Flex, Form, type FormInstance, Select, Switch, Tooltip, Typography } from "antd";
import { createSchemaFieldRule } from "antd-zod"; import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod"; import { z } from "zod";
@ -310,6 +310,15 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
</Form.Item> </Form.Item>
</Form.Item> </Form.Item>
<Show when={fieldProvider === DEPLOY_PROVIDERS.LOCAL}>
<Form.Item>
<Alert
type="info"
message={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.provider_access.guide_for_local") }}></span>}
/>
</Form.Item>
</Show>
<Form.Item <Form.Item
name="certificate" name="certificate"
label={t("workflow_node.deploy.form.certificate.label")} label={t("workflow_node.deploy.form.certificate.label")}

View File

@ -1,9 +1,12 @@
import { memo } from "react"; import { memo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { MoreOutlined as MoreOutlinedIcon } from "@ant-design/icons"; import {
import { Button, Card, Popover } from "antd"; CheckCircleOutlined as CheckCircleOutlinedIcon,
CloseCircleOutlined as CloseCircleOutlinedIcon,
MoreOutlined as MoreOutlinedIcon,
} from "@ant-design/icons";
import { Button, Card, Popover, theme } from "antd";
import { CheckCircleIcon, XCircleIcon } from "lucide-react";
import { WorkflowNodeType } from "@/domain/workflow"; import { WorkflowNodeType } from "@/domain/workflow";
import AddNode from "./AddNode"; import AddNode from "./AddNode";
import SharedNode, { type SharedNodeProps } from "./_SharedNode"; import SharedNode, { type SharedNodeProps } from "./_SharedNode";
@ -16,9 +19,13 @@ export type ConditionNodeProps = SharedNodeProps & {
const ExecuteResultNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeProps) => { const ExecuteResultNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { token: themeToken } = theme.useToken();
return ( return (
<> <>
<Popover <Popover
classNames={{ root: "shadow-md" }}
styles={{ body: { padding: 0 } }}
arrow={false} arrow={false}
content={ content={
<SharedNode.Menu <SharedNode.Menu
@ -29,8 +36,6 @@ const ExecuteResultNode = ({ node, disabled, branchId, branchIndex }: ConditionN
trigger={<Button color="primary" icon={<MoreOutlinedIcon />} variant="text" />} trigger={<Button color="primary" icon={<MoreOutlinedIcon />} variant="text" />}
/> />
} }
overlayClassName="shadow-md"
overlayInnerStyle={{ padding: 0 }}
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>
@ -38,12 +43,12 @@ const ExecuteResultNode = ({ node, disabled, branchId, branchIndex }: ConditionN
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{node.type === WorkflowNodeType.ExecuteSuccess ? ( {node.type === WorkflowNodeType.ExecuteSuccess ? (
<> <>
<CheckCircleIcon size={18} className="text-green-500" /> <CheckCircleOutlinedIcon style={{ color: themeToken.colorSuccess }} />
<div>{t("workflow_node.execute_success.label")}</div> <div>{t("workflow_node.execute_success.label")}</div>
</> </>
) : ( ) : (
<> <>
<XCircleIcon size={18} className="text-red-500" /> <CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} />
<div>{t("workflow_node.execute_failure.label")}</div> <div>{t("workflow_node.execute_failure.label")}</div>
</> </>
)} )}

View File

@ -136,7 +136,7 @@ const StartNodeConfigForm = forwardRef<StartNodeConfigFormInstance, StartNodeCon
<Show when={fieldTrigger === WORKFLOW_TRIGGERS.AUTO}> <Show when={fieldTrigger === WORKFLOW_TRIGGERS.AUTO}>
<Form.Item> <Form.Item>
<Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.start.form.trigger_cron_alert.content") }}></span>} /> <Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.start.form.trigger_cron.guide") }}></span>} />
</Form.Item> </Form.Item>
</Show> </Show>
</Form> </Form>

View File

@ -1,5 +1,6 @@
import { forwardRef, memo, useEffect, useImperativeHandle } from "react"; import { forwardRef, memo, useImperativeHandle } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UploadOutlined as UploadOutlinedIcon } from "@ant-design/icons";
import { Button, Form, type FormInstance, Input, Upload, type UploadProps } from "antd"; import { Button, Form, type FormInstance, Input, Upload, type UploadProps } from "antd";
import { createSchemaFieldRule } from "antd-zod"; import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod"; import { z } from "zod";
@ -35,8 +36,7 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
const { t } = useTranslation(); const { t } = useTranslation();
const formSchema = z.object({ const formSchema = z.object({
certificateId: z.string().optional(), domains: z.string().nullish(),
domains: z.string().optional(),
certificate: z certificate: z
.string({ message: t("workflow_node.upload.form.certificate.placeholder") }) .string({ message: t("workflow_node.upload.form.certificate.placeholder") })
.min(1, t("workflow_node.upload.form.certificate.placeholder")) .min(1, t("workflow_node.upload.form.certificate.placeholder"))
@ -52,15 +52,6 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
initialValues: initialValues ?? initFormModel(), initialValues: initialValues ?? initFormModel(),
}); });
const certificate = Form.useWatch("certificate", formInst);
const privateKey = Form.useWatch("privateKey", formInst);
useEffect(() => {
if (certificate && privateKey) {
formInst.validateFields(["certificate", "privateKey"]);
}
}, [certificate, privateKey]);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => { const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values as UploadNodeConfigFormFieldValues); onValuesChange?.(values as UploadNodeConfigFormFieldValues);
}; };
@ -86,18 +77,21 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
try { try {
const resp = await validateCertificate(certificate); const resp = await validateCertificate(certificate);
formInst.setFields([ formInst.setFields([
{
name: "certificate",
value: certificate,
errors: [],
},
{ {
name: "domains", name: "domains",
value: resp.data.domains, value: resp.data.domains,
}, },
{
name: "certificate",
value: certificate,
},
]); ]);
} catch (e) { } catch (e) {
formInst.setFields([ formInst.setFields([
{
name: "domains",
value: "",
},
{ {
name: "certificate", name: "certificate",
value: "", value: "",
@ -108,25 +102,27 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
} else { } else {
formInst.setFieldValue("certificate", ""); formInst.setFieldValue("certificate", "");
} }
onValuesChange?.(formInst.getFieldsValue(true)); onValuesChange?.(formInst.getFieldsValue(true));
}; };
const handlePrivateKeyFileChange: UploadProps["onChange"] = async ({ file }) => { const handlePrivateKeyFileChange: UploadProps["onChange"] = async ({ file }) => {
if (file && file.status !== "removed") { if (file && file.status !== "removed") {
const privateKey = await readFileContent(file.originFileObj ?? (file as unknown as File)); const privateKey = await readFileContent(file.originFileObj ?? (file as unknown as File));
try { try {
await validatePrivateKey(privateKey); await validatePrivateKey(privateKey);
formInst.setFields([ formInst.setFields([
{ {
name: "privateKey", name: "privateKey",
value: privateKey, value: privateKey,
errors: [],
}, },
]); ]);
} catch (e) { } catch (e) {
formInst.setFields([ formInst.setFields([
{ {
name: "privateKey", name: "privateKey",
value: "",
errors: [getErrMsg(e)], errors: [getErrMsg(e)],
}, },
]); ]);
@ -141,35 +137,27 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
return ( return (
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}> <Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
<Form.Item name="domains" label={t("workflow_node.upload.form.domains.label")} rules={[formRule]}> <Form.Item name="domains" label={t("workflow_node.upload.form.domains.label")} rules={[formRule]}>
<Input readOnly /> <Input placeholder={t("workflow_node.upload.form.domains.placeholder")} readOnly />
</Form.Item> </Form.Item>
<Form.Item name="certificate" label={t("workflow_node.upload.form.certificate.label")} rules={[formRule]}> <Form.Item name="certificate" label={t("workflow_node.upload.form.certificate.label")} rules={[formRule]}>
<Input.TextArea <Input.TextArea readOnly autoSize={{ minRows: 5, maxRows: 10 }} placeholder={t("workflow_node.upload.form.certificate.placeholder")} />
readOnly </Form.Item>
autoSize={{ minRows: 5, maxRows: 10 }}
placeholder={t("workflow_node.upload.form.certificate.placeholder")} <Form.Item>
value={certificate}
/>
<div className="mt-2 text-right">
<Upload beforeUpload={() => false} maxCount={1} onChange={handleCertificateFileChange}> <Upload beforeUpload={() => false} maxCount={1} onChange={handleCertificateFileChange}>
<Button>{t("workflow_node.upload.form.certificate.button")}</Button> <Button icon={<UploadOutlinedIcon />}>{t("workflow_node.upload.form.certificate.button")}</Button>
</Upload> </Upload>
</div>
</Form.Item> </Form.Item>
<Form.Item name="privateKey" label={t("workflow_node.upload.form.private_key.label")} rules={[formRule]}> <Form.Item name="privateKey" label={t("workflow_node.upload.form.private_key.label")} rules={[formRule]}>
<Input.TextArea <Input.TextArea readOnly autoSize={{ minRows: 5, maxRows: 10 }} placeholder={t("workflow_node.upload.form.private_key.placeholder")} />
readOnly </Form.Item>
autoSize={{ minRows: 5, maxRows: 10 }}
placeholder={t("workflow_node.upload.form.private_key.placeholder")} <Form.Item>
value={privateKey}
/>
<div className="mt-2 text-right">
<Upload beforeUpload={() => false} maxCount={1} onChange={handlePrivateKeyFileChange}> <Upload beforeUpload={() => false} maxCount={1} onChange={handlePrivateKeyFileChange}>
<Button>{t("workflow_node.upload.form.private_key.button")}</Button> <Button icon={<UploadOutlinedIcon />}>{t("workflow_node.upload.form.private_key.button")}</Button>
</Upload> </Upload>
</div>
</Form.Item> </Form.Item>
</Form> </Form>
); );

View File

@ -64,6 +64,16 @@ type SharedNodeMenuProps = SharedNodeProps & {
afterDelete?: () => void; afterDelete?: () => void;
}; };
const isBranchingNode = (node: WorkflowNode) => {
return (
node.type === WorkflowNodeType.Branch ||
node.type === WorkflowNodeType.Condition ||
node.type === WorkflowNodeType.ExecuteResultBranch ||
node.type === WorkflowNodeType.ExecuteSuccess ||
node.type === WorkflowNodeType.ExecuteFailure
);
};
const SharedNodeMenu = ({ trigger, node, disabled, branchId, branchIndex, afterUpdate, afterDelete }: SharedNodeMenuProps) => { const SharedNodeMenu = ({ trigger, node, disabled, branchId, branchIndex, afterUpdate, afterDelete }: SharedNodeMenuProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -91,13 +101,7 @@ const SharedNodeMenu = ({ trigger, node, disabled, branchId, branchIndex, afterU
}; };
const handleDeleteClick = async () => { const handleDeleteClick = async () => {
if ( if (isBranchingNode(node)) {
node.type === WorkflowNodeType.Branch ||
node.type === WorkflowNodeType.Condition ||
node.type === WorkflowNodeType.ExecuteResultBranch ||
node.type === WorkflowNodeType.ExecuteSuccess ||
node.type === WorkflowNodeType.ExecuteFailure
) {
await removeBranch(branchId!, branchIndex!); await removeBranch(branchId!, branchIndex!);
} else { } else {
await removeNode(node.id); await removeNode(node.id);
@ -116,19 +120,13 @@ const SharedNodeMenu = ({ trigger, node, disabled, branchId, branchIndex, afterU
{ {
key: "rename", key: "rename",
disabled: disabled, disabled: disabled,
label: label: isBranchingNode(node) ? t("workflow_node.action.rename_branch") : t("workflow_node.action.rename_node"),
node.type === WorkflowNodeType.Branch || node.type === WorkflowNodeType.Condition
? t("workflow_node.action.rename_branch")
: t("workflow_node.action.rename_node"),
icon: <FormOutlinedIcon />, icon: <FormOutlinedIcon />,
onClick: () => { onClick: () => {
nameRef.current = node.name; nameRef.current = node.name;
const dialog = modalApi.confirm({ const dialog = modalApi.confirm({
title: title: isBranchingNode(node) ? t("workflow_node.action.rename_branch") : t("workflow_node.action.rename_node"),
node.type === WorkflowNodeType.Branch || node.type === WorkflowNodeType.Condition
? t("workflow_node.action.rename_branch")
: t("workflow_node.action.rename_node"),
content: ( content: (
<div className="pb-2 pt-4"> <div className="pb-2 pt-4">
<Input <Input
@ -156,14 +154,7 @@ const SharedNodeMenu = ({ trigger, node, disabled, branchId, branchIndex, afterU
{ {
key: "remove", key: "remove",
disabled: disabled || node.type === WorkflowNodeType.Start, disabled: disabled || node.type === WorkflowNodeType.Start,
label: label: isBranchingNode(node) ? t("workflow_node.action.remove_branch") : t("workflow_node.action.remove_node"),
node.type === WorkflowNodeType.Branch ||
node.type === WorkflowNodeType.Condition ||
node.type === WorkflowNodeType.ExecuteResultBranch ||
node.type === WorkflowNodeType.ExecuteSuccess ||
node.type === WorkflowNodeType.ExecuteFailure
? t("workflow_node.action.remove_branch")
: t("workflow_node.action.remove_node"),
icon: <CloseCircleOutlinedIcon />, icon: <CloseCircleOutlinedIcon />,
danger: true, danger: true,
onClick: handleDeleteClick, onClick: handleDeleteClick,
@ -193,10 +184,10 @@ const SharedNodeBlock = ({ children, node, disabled, onClick }: SharedNodeBlockP
return ( return (
<> <>
<Popover <Popover
classNames={{ root: "shadow-md" }}
styles={{ body: { padding: 0 } }}
arrow={false} arrow={false}
content={<SharedNodeMenu node={node} disabled={disabled} trigger={<Button color="primary" icon={<MoreOutlinedIcon />} variant="text" />} />} content={<SharedNodeMenu node={node} disabled={disabled} trigger={<Button color="primary" icon={<MoreOutlinedIcon />} variant="text" />} />}
overlayClassName="shadow-md"
overlayInnerStyle={{ padding: 0 }}
placement="rightTop" placement="rightTop"
> >
<Card className="relative w-[256px] overflow-hidden shadow-md" styles={{ body: { padding: 0 } }} hoverable> <Card className="relative w-[256px] overflow-hidden shadow-md" styles={{ body: { padding: 0 } }} hoverable>

View File

@ -29,30 +29,30 @@ export type WorkflowTriggerType = (typeof WORKFLOW_TRIGGERS)[keyof typeof WORKFL
export enum WorkflowNodeType { export enum WorkflowNodeType {
Start = "start", Start = "start",
End = "end", End = "end",
Branch = "branch",
ExecuteResultBranch = "execute_result_branch",
ExecuteSuccess = "execute_success",
ExecuteFailure = "execute_failure",
Condition = "condition",
Apply = "apply", Apply = "apply",
Upload = "upload", Upload = "upload",
Deploy = "deploy", Deploy = "deploy",
Notify = "notify", Notify = "notify",
Branch = "branch",
Condition = "condition",
ExecuteResultBranch = "execute_result_branch",
ExecuteSuccess = "execute_success",
ExecuteFailure = "execute_failure",
Custom = "custom", Custom = "custom",
} }
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.label")],
[WorkflowNodeType.End, i18n.t("workflow_node.end.label")], [WorkflowNodeType.End, i18n.t("workflow_node.end.label")],
[WorkflowNodeType.Branch, i18n.t("workflow_node.branch.label")],
[WorkflowNodeType.ExecuteResultBranch, i18n.t("workflow_node.execute_result_branch.label")],
[WorkflowNodeType.ExecuteSuccess, i18n.t("workflow_node.execute_success.label")],
[WorkflowNodeType.ExecuteFailure, i18n.t("workflow_node.execute_failure.label")],
[WorkflowNodeType.Condition, i18n.t("workflow_node.condition.label")],
[WorkflowNodeType.Apply, i18n.t("workflow_node.apply.label")], [WorkflowNodeType.Apply, i18n.t("workflow_node.apply.label")],
[WorkflowNodeType.Upload, i18n.t("workflow_node.upload.label")], [WorkflowNodeType.Upload, i18n.t("workflow_node.upload.label")],
[WorkflowNodeType.Deploy, i18n.t("workflow_node.deploy.label")], [WorkflowNodeType.Deploy, i18n.t("workflow_node.deploy.label")],
[WorkflowNodeType.Notify, i18n.t("workflow_node.notify.label")], [WorkflowNodeType.Notify, i18n.t("workflow_node.notify.label")],
[WorkflowNodeType.Branch, i18n.t("workflow_node.branch.label")],
[WorkflowNodeType.Condition, i18n.t("workflow_node.condition.label")],
[WorkflowNodeType.ExecuteResultBranch, i18n.t("workflow_node.execute_result_branch.label")],
[WorkflowNodeType.ExecuteSuccess, i18n.t("workflow_node.execute_success.label")],
[WorkflowNodeType.ExecuteFailure, i18n.t("workflow_node.execute_failure.label")],
[WorkflowNodeType.Custom, i18n.t("workflow_node.custom.title")], [WorkflowNodeType.Custom, i18n.t("workflow_node.custom.title")],
]); ]);

View File

@ -32,6 +32,7 @@ export const WORKFLOW_RUN_STATUSES = Object.freeze({
RUNNING: "running", RUNNING: "running",
SUCCEEDED: "succeeded", SUCCEEDED: "succeeded",
FAILED: "failed", FAILED: "failed",
CANCELED: "canceled",
} as const); } as const);
export type WorkflorRunStatusType = (typeof WORKFLOW_RUN_STATUSES)[keyof typeof WORKFLOW_RUN_STATUSES]; export type WorkflorRunStatusType = (typeof WORKFLOW_RUN_STATUSES)[keyof typeof WORKFLOW_RUN_STATUSES];

View File

@ -1,5 +1,5 @@
{ {
"workflow_node.action.configure_node": "Configure", "workflow_node.action.configure_node": "Configure node",
"workflow_node.action.add_node": "Add node", "workflow_node.action.add_node": "Add node",
"workflow_node.action.rename_node": "Rename node", "workflow_node.action.rename_node": "Rename node",
"workflow_node.action.remove_node": "Delete node", "workflow_node.action.remove_node": "Delete node",
@ -20,7 +20,7 @@
"workflow_node.start.form.trigger_cron.errmsg.invalid": "Please enter a valid cron expression", "workflow_node.start.form.trigger_cron.errmsg.invalid": "Please enter a valid cron expression",
"workflow_node.start.form.trigger_cron.tooltip": "Time zone is based on the server.", "workflow_node.start.form.trigger_cron.tooltip": "Time zone is based on the server.",
"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_alert.content": "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.<br><br>Reference links:<br>1. <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">Lets 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 Lets 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.<br><br>Reference links:<br>1. <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">Lets 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 Lets Encrypt (ACME) client run at a random time?</a>",
"workflow_node.apply.label": "Application", "workflow_node.apply.label": "Application",
"workflow_node.apply.form.domains.label": "Domains", "workflow_node.apply.form.domains.label": "Domains",
@ -82,6 +82,7 @@
"workflow_node.deploy.form.provider_access.placeholder": "Please select an authorization of host provider", "workflow_node.deploy.form.provider_access.placeholder": "Please select an authorization of host provider",
"workflow_node.deploy.form.provider_access.tooltip": "Used to deploy certificates.", "workflow_node.deploy.form.provider_access.tooltip": "Used to deploy certificates.",
"workflow_node.deploy.form.provider_access.button": "Create", "workflow_node.deploy.form.provider_access.button": "Create",
"workflow_node.deploy.form.provider_access.guide_for_local": "Tips: Due to the form validations, youe need to select an authorization for local deployment also, even if it means nothing.",
"workflow_node.deploy.form.certificate.label": "Certificate", "workflow_node.deploy.form.certificate.label": "Certificate",
"workflow_node.deploy.form.certificate.placeholder": "Please select certificate", "workflow_node.deploy.form.certificate.placeholder": "Please select certificate",
"workflow_node.deploy.form.certificate.tooltip": "The certificate to be deployed comes from the previous application stage node.", "workflow_node.deploy.form.certificate.tooltip": "The certificate to be deployed comes from the previous application stage node.",
@ -377,25 +378,25 @@
"workflow_node.notify.form.channel.placeholder": "Please select channel", "workflow_node.notify.form.channel.placeholder": "Please select channel",
"workflow_node.notify.form.channel.button": "Configure", "workflow_node.notify.form.channel.button": "Configure",
"workflow_node.upload.label": "Upload certificate", "workflow_node.upload.label": "Upload",
"workflow_node.upload.form.domains.label": "Domains", "workflow_node.upload.form.domains.label": "Domains",
"workflow_node.upload.form.certificate.label": "Certificate", "workflow_node.upload.form.domains.placholder": "Please select certificate file",
"workflow_node.upload.form.certificate.placeholder": "The certificate format begins with \"-----BEGIN CERTIFICATE-----\" and ends with \"-----END CERTIFICATE-----\"", "workflow_node.upload.form.certificate.label": "Certificate (PEM format)",
"workflow_node.upload.form.certificate.button": "Upload", "workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
"workflow_node.upload.form.private_key.label": "Private key", "workflow_node.upload.form.certificate.button": "Choose file ...",
"workflow_node.upload.form.private_key.placeholder": "The private key begins with \"-----BEGIN (RSA|EC) PRIVATE KEY-----\" and ends with \"-----END(RSA|EC) PRIVATE KEY-----\"", "workflow_node.upload.form.private_key.label": "Private key (PEM format)",
"workflow_node.upload.form.private_key.button": "Upload", "workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
"workflow_node.upload.form.private_key.button": "Choose file ...",
"workflow_node.end.label": "End", "workflow_node.end.label": "End",
"workflow_node.branch.label": "Branch", "workflow_node.branch.label": "Parallel branch",
"workflow_node.execute_result_branch.label": "Execute result branch", "workflow_node.condition.label": "Branch",
"workflow_node.execute_success.label": "Execute success", "workflow_node.execute_result_branch.label": "Execution result branch",
"workflow_node.execute_failure.label": "Execute failure", "workflow_node.execute_success.label": "If the previous node succeeded ...",
"workflow_node.condition.label": "Condition" "workflow_node.execute_failure.label": "If the previous node failed ..."
} }

View File

@ -1,10 +1,17 @@
{ {
"workflow_run.action.view": "View detail",
"workflow_run.action.cancel": "Cancel run",
"workflow_run.action.cancel.confirm": "Are you sure to cancel this run?",
"workflow_run.action.delete": "Delete run",
"workflow_run.action.delete.confirm": "Are you sure to delete this run?",
"workflow_run.props.id": "ID", "workflow_run.props.id": "ID",
"workflow_run.props.status": "Status", "workflow_run.props.status": "Status",
"workflow_run.props.status.pending": "Pending", "workflow_run.props.status.pending": "Pending",
"workflow_run.props.status.running": "Running", "workflow_run.props.status.running": "Running",
"workflow_run.props.status.succeeded": "Succeeded", "workflow_run.props.status.succeeded": "Succeeded",
"workflow_run.props.status.failed": "Failed", "workflow_run.props.status.failed": "Failed",
"workflow_run.props.status.canceled": "Canceled",
"workflow_run.props.trigger": "Trigger", "workflow_run.props.trigger": "Trigger",
"workflow_run.props.trigger.auto": "Timing", "workflow_run.props.trigger.auto": "Timing",
"workflow_run.props.trigger.manual": "Manual", "workflow_run.props.trigger.manual": "Manual",

View File

@ -3,7 +3,7 @@
"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": "删除分支",
@ -20,7 +20,7 @@
"workflow_node.start.form.trigger_cron.errmsg.invalid": "请输入正确的 Cron 表达式", "workflow_node.start.form.trigger_cron.errmsg.invalid": "请输入正确的 Cron 表达式",
"workflow_node.start.form.trigger_cron.tooltip": "支持使用任意值(即 <strong>*</strong>)、值列表分隔符(即 <strong>,</strong>)、值的范围(即 <strong>-</strong>)、步骤值(即 <strong>/</strong>)等四种表达式,时区以服务器设置为准。", "workflow_node.start.form.trigger_cron.tooltip": "支持使用任意值(即 <strong>*</strong>)、值列表分隔符(即 <strong>,</strong>)、值的范围(即 <strong>-</strong>)、步骤值(即 <strong>/</strong>)等四种表达式,时区以服务器设置为准。",
"workflow_node.start.form.trigger_cron.extra": "预计最近 5 次执行时间:", "workflow_node.start.form.trigger_cron.extra": "预计最近 5 次执行时间:",
"workflow_node.start.form.trigger_cron_alert.content": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。<br><br>参考链接:<br>1. <a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">Lets 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\">为什么我的 Lets 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\">Lets 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\">为什么我的 Lets Encrypt (ACME) 客户端启动时间应当随机?</a>",
"workflow_node.apply.label": "申请", "workflow_node.apply.label": "申请",
"workflow_node.apply.form.domains.label": "域名", "workflow_node.apply.form.domains.label": "域名",
@ -37,6 +37,7 @@
"workflow_node.apply.form.provider_access.placeholder": "请选择 DNS 提供商授权", "workflow_node.apply.form.provider_access.placeholder": "请选择 DNS 提供商授权",
"workflow_node.apply.form.provider_access.tooltip": "用于 ACME DNS-01 认证时操作域名解析记录,注意与部署阶段所需的主机提供商相区分。", "workflow_node.apply.form.provider_access.tooltip": "用于 ACME DNS-01 认证时操作域名解析记录,注意与部署阶段所需的主机提供商相区分。",
"workflow_node.apply.form.provider_access.button": "新建", "workflow_node.apply.form.provider_access.button": "新建",
"workflow_node.deploy.form.provider_access.guide_for_local": "小贴士:由于表单限制,你同样需要为本地部署选择一个授权 —— 即使它是空白的。",
"workflow_node.apply.form.aws_route53_region.label": "AWS Route53 区域", "workflow_node.apply.form.aws_route53_region.label": "AWS Route53 区域",
"workflow_node.apply.form.aws_route53_region.placeholder": "请输入 AWS Route53 区域例如us-east-1", "workflow_node.apply.form.aws_route53_region.placeholder": "请输入 AWS Route53 区域例如us-east-1",
"workflow_node.apply.form.aws_route53_region.tooltip": "这是什么?请参阅 <a href=\"https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints\" tworkflow_node.applyank\">https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints</a>", "workflow_node.apply.form.aws_route53_region.tooltip": "这是什么?请参阅 <a href=\"https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints\" tworkflow_node.applyank\">https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints</a>",
@ -377,25 +378,25 @@
"workflow_node.notify.form.channel.placeholder": "请选择通知渠道", "workflow_node.notify.form.channel.placeholder": "请选择通知渠道",
"workflow_node.notify.form.channel.button": "去配置", "workflow_node.notify.form.channel.button": "去配置",
"workflow_node.upload.label": "上传证书", "workflow_node.upload.label": "上传",
"workflow_node.upload.form.domains.label": "证书域名", "workflow_node.upload.form.domains.label": "域名",
"workflow_node.upload.form.certificate.label": "证书文件", "workflow_node.upload.form.domains.placeholder": "上传证书文件后显示",
"workflow_node.upload.form.certificate.placeholder": "证书格式以\"-----BEGIN CERTIFICATE-----\"开头,以\"-----END CERTIFICATE-----\"结尾。", "workflow_node.upload.form.certificate.label": "证书文件PEM 格式)",
"workflow_node.upload.form.certificate.button": "上传", "workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
"workflow_node.upload.form.private_key.label": "证书私钥", "workflow_node.upload.form.certificate.button": "选择文件",
"workflow_node.upload.form.private_key.placeholder": "证书私钥格式以\"-----BEGIN (RSA|EC) PRIVATE KEY-----\"开头,以\"-----END(RSA|EC) PRIVATE KEY-----\"结尾。", "workflow_node.upload.form.private_key.label": "私钥文件PEM 格式)",
"workflow_node.upload.form.private_key.button": "上传", "workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
"workflow_node.upload.form.private_key.button": "选择文件",
"workflow_node.end.label": "结束", "workflow_node.end.label": "结束",
"workflow_node.branch.label": "分支", "workflow_node.branch.label": "并行分支",
"workflow_node.condition.label": "分支",
"workflow_node.execute_result_branch.label": "执行结果分支", "workflow_node.execute_result_branch.label": "执行结果分支",
"workflow_node.execute_success.label": "执行成功", "workflow_node.execute_success.label": "若前序节点执行成功",
"workflow_node.execute_failure.label": "执行失败", "workflow_node.execute_failure.label": "若前序节点执行失败…"
"workflow_node.condition.label": "条件"
} }

View File

@ -1,10 +1,17 @@
{ {
"workflow_run.action.view": "查看详情",
"workflow_run.action.cancel": "取消执行",
"workflow_run.action.cancel.confirm": "确定要取消此执行吗?请注意此操作仅中止流程,但不会回滚已执行的节点。",
"workflow_run.action.delete": "删除执行",
"workflow_run.action.delete.confirm": "确定要删除此执行吗?请注意此操作仅清除日志历史,但不会影响各节点的执行结果和签发的证书。",
"workflow_run.props.id": "ID", "workflow_run.props.id": "ID",
"workflow_run.props.status": "状态", "workflow_run.props.status": "状态",
"workflow_run.props.status.pending": "等待执行", "workflow_run.props.status.pending": "等待执行",
"workflow_run.props.status.running": "执行中", "workflow_run.props.status.running": "执行中",
"workflow_run.props.status.succeeded": "成功", "workflow_run.props.status.succeeded": "已成功",
"workflow_run.props.status.failed": "失败", "workflow_run.props.status.failed": "已失败",
"workflow_run.props.status.canceled": "已取消",
"workflow_run.props.trigger": "执行方式", "workflow_run.props.trigger": "执行方式",
"workflow_run.props.trigger.auto": "定时执行", "workflow_run.props.trigger.auto": "定时执行",
"workflow_run.props.trigger.manual": "手动执行", "workflow_run.props.trigger.manual": "手动执行",

View File

@ -41,7 +41,7 @@ const ConsoleLayout = () => {
} }
return ( return (
<Layout className="min-h-screen" hasSider> <Layout className="h-screen" hasSider>
<Layout.Sider className="fixed left-0 top-0 z-20 h-full max-md:static max-md:hidden" width="256px" theme="light"> <Layout.Sider className="fixed left-0 top-0 z-20 h-full max-md:static max-md:hidden" width="256px" theme="light">
<div className="flex size-full flex-col items-center justify-between overflow-hidden"> <div className="flex size-full flex-col items-center justify-between overflow-hidden">
<div className="w-full"> <div className="w-full">
@ -53,8 +53,8 @@ const ConsoleLayout = () => {
</div> </div>
</Layout.Sider> </Layout.Sider>
<Layout className="pl-[256px] max-md:pl-0"> <Layout className="flex flex-col overflow-hidden pl-[256px] max-md:pl-0">
<Layout.Header className="sticky inset-x-0 top-0 z-[19] p-0 shadow-sm" style={{ background: themeToken.colorBgContainer }}> <Layout.Header className="p-0 shadow-sm" style={{ background: themeToken.colorBgContainer }}>
<div className="flex size-full items-center justify-between overflow-hidden px-4"> <div className="flex size-full items-center justify-between overflow-hidden px-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<SiderMenuDrawer trigger={<Button className="md:hidden" icon={<MenuOutlinedIcon />} size="large" />} /> <SiderMenuDrawer trigger={<Button className="md:hidden" icon={<MenuOutlinedIcon />} size="large" />} />
@ -76,7 +76,7 @@ const ConsoleLayout = () => {
</div> </div>
</Layout.Header> </Layout.Header>
<Layout.Content style={{ overflow: "initial" }}> <Layout.Content className="flex-1 overflow-y-auto overflow-x-hidden">
<Outlet /> <Outlet />
</Layout.Content> </Layout.Content>
</Layout> </Layout>

View File

@ -2,15 +2,16 @@ import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { import {
ApiOutlined, ApiOutlined as ApiOutlinedIcon,
CheckCircleOutlined, CheckCircleOutlined as CheckCircleOutlinedIcon,
ClockCircleOutlined, ClockCircleOutlined as ClockCircleOutlinedIcon,
CloseCircleOutlined, CloseCircleOutlined as CloseCircleOutlinedIcon,
LockOutlined, LockOutlined as LockOutlinedIcon,
PlusOutlined, PauseCircleOutlined as PauseCircleOutlinedIcon,
SelectOutlined, PlusOutlined as PlusOutlinedIcon,
SendOutlined, SelectOutlined as SelectOutlinedIcon,
SyncOutlined, SendOutlined as SendOutlinedIcon,
SyncOutlined as SyncOutlinedIcon,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-components"; import { PageHeader } from "@ant-design/pro-components";
import { useRequest } from "ahooks"; import { useRequest } from "ahooks";
@ -84,14 +85,22 @@ const Dashboard = () => {
key: "name", key: "name",
title: t("workflow.props.name"), title: t("workflow.props.name"),
ellipsis: true, ellipsis: true,
render: (_, record) => ( render: (_, record) => {
<Space className="max-w-full" direction="vertical" size={4}> const workflow = record.expand?.workflowId;
<Typography.Text ellipsis>{record.expand?.workflowId?.name}</Typography.Text> return (
<Typography.Text type="secondary" ellipsis> <Typography.Link
{record.expand?.workflowId?.description} type="secondary"
</Typography.Text> ellipsis
</Space> onClick={() => {
), if (workflow) {
navigate(`/workflows/${workflow.id}`);
}
}}
>
{workflow?.name ?? <span className="font-mono">{t(`#${record.workflowId}`)}</span>}
</Typography.Link>
);
},
}, },
{ {
key: "status", key: "status",
@ -99,25 +108,31 @@ const Dashboard = () => {
ellipsis: true, ellipsis: true,
render: (_, record) => { render: (_, record) => {
if (record.status === WORKFLOW_RUN_STATUSES.PENDING) { if (record.status === WORKFLOW_RUN_STATUSES.PENDING) {
return <Tag icon={<ClockCircleOutlined />}>{t("workflow_run.props.status.pending")}</Tag>; return <Tag icon={<ClockCircleOutlinedIcon />}>{t("workflow_run.props.status.pending")}</Tag>;
} else if (record.status === WORKFLOW_RUN_STATUSES.RUNNING) { } else if (record.status === WORKFLOW_RUN_STATUSES.RUNNING) {
return ( return (
<Tag icon={<SyncOutlined spin />} color="processing"> <Tag icon={<SyncOutlinedIcon spin />} color="processing">
{t("workflow_run.props.status.running")} {t("workflow_run.props.status.running")}
</Tag> </Tag>
); );
} else if (record.status === WORKFLOW_RUN_STATUSES.SUCCEEDED) { } else if (record.status === WORKFLOW_RUN_STATUSES.SUCCEEDED) {
return ( return (
<Tag icon={<CheckCircleOutlined />} color="success"> <Tag icon={<CheckCircleOutlinedIcon />} color="success">
{t("workflow_run.props.status.succeeded")} {t("workflow_run.props.status.succeeded")}
</Tag> </Tag>
); );
} else if (record.status === WORKFLOW_RUN_STATUSES.FAILED) { } else if (record.status === WORKFLOW_RUN_STATUSES.FAILED) {
return ( return (
<Tag icon={<CloseCircleOutlined />} color="error"> <Tag icon={<CloseCircleOutlinedIcon />} color="error">
{t("workflow_run.props.status.failed")} {t("workflow_run.props.status.failed")}
</Tag> </Tag>
); );
} else if (record.status === WORKFLOW_RUN_STATUSES.CANCELED) {
return (
<Tag icon={<PauseCircleOutlinedIcon />} color="warning">
{t("workflow_run.props.status.canceled")}
</Tag>
);
} }
return <></>; return <></>;
@ -153,7 +168,7 @@ const Dashboard = () => {
width: 120, width: 120,
render: (_, record) => ( render: (_, record) => (
<Button.Group> <Button.Group>
<WorkflowRunDetailDrawer data={record} trigger={<Button color="primary" icon={<SelectOutlined />} variant="text" />} /> <WorkflowRunDetailDrawer data={record} trigger={<Button color="primary" icon={<SelectOutlinedIcon />} variant="text" />} />
</Button.Group> </Button.Group>
), ),
}, },
@ -248,16 +263,16 @@ const Dashboard = () => {
<Flex justify="stretch" vertical={!breakpoints.lg} gap={16}> <Flex justify="stretch" vertical={!breakpoints.lg} gap={16}>
<Card className="max-lg:flex-1 lg:w-[360px]" title={t("dashboard.quick_actions")}> <Card className="max-lg:flex-1 lg:w-[360px]" title={t("dashboard.quick_actions")}>
<Space className="w-full" direction="vertical" size="large"> <Space className="w-full" direction="vertical" size="large">
<Button block type="primary" size="large" icon={<PlusOutlined />} onClick={() => navigate("/workflows/new")}> <Button block type="primary" size="large" icon={<PlusOutlinedIcon />} onClick={() => navigate("/workflows/new")}>
{t("dashboard.quick_actions.create_workflow")} {t("dashboard.quick_actions.create_workflow")}
</Button> </Button>
<Button block size="large" icon={<LockOutlined />} onClick={() => navigate("/settings/password")}> <Button block size="large" icon={<LockOutlinedIcon />} onClick={() => navigate("/settings/password")}>
{t("dashboard.quick_actions.change_login_password")} {t("dashboard.quick_actions.change_login_password")}
</Button> </Button>
<Button block size="large" icon={<SendOutlined />} onClick={() => navigate("/settings/notification")}> <Button block size="large" icon={<SendOutlinedIcon />} onClick={() => navigate("/settings/notification")}>
{t("dashboard.quick_actions.cofigure_notification")} {t("dashboard.quick_actions.cofigure_notification")}
</Button> </Button>
<Button block size="large" icon={<ApiOutlined />} onClick={() => navigate("/settings/ssl-provider")}> <Button block size="large" icon={<ApiOutlinedIcon />} onClick={() => navigate("/settings/ssl-provider")}>
{t("dashboard.quick_actions.configure_ca")} {t("dashboard.quick_actions.configure_ca")}
</Button> </Button>
</Space> </Space>

View File

@ -8,9 +8,6 @@ import {
DownOutlined as DownOutlinedIcon, DownOutlined as DownOutlinedIcon,
EllipsisOutlined as EllipsisOutlinedIcon, EllipsisOutlined as EllipsisOutlinedIcon,
HistoryOutlined as HistoryOutlinedIcon, HistoryOutlined as HistoryOutlinedIcon,
MinusOutlined,
PlusCircleOutlined,
ReloadOutlined,
UndoOutlined as UndoOutlinedIcon, UndoOutlined as UndoOutlinedIcon,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-components"; import { PageHeader } from "@ant-design/pro-components";
@ -19,10 +16,10 @@ import { createSchemaFieldRule } from "antd-zod";
import { isEqual } from "radash"; import { isEqual } from "radash";
import { z } from "zod"; import { z } from "zod";
import { run as runWorkflow } from "@/api/workflows"; import { startRun as startWorkflowRun } from "@/api/workflows";
import ModalForm from "@/components/ModalForm"; import ModalForm from "@/components/ModalForm";
import Show from "@/components/Show"; import Show from "@/components/Show";
import WorkflowElements from "@/components/workflow/WorkflowElements"; import WorkflowElementsContainer from "@/components/workflow/WorkflowElementsContainer";
import WorkflowRuns from "@/components/workflow/WorkflowRuns"; import WorkflowRuns from "@/components/workflow/WorkflowRuns";
import { isAllNodesValidated } from "@/domain/workflow"; import { isAllNodesValidated } from "@/domain/workflow";
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun"; import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
@ -40,8 +37,6 @@ const WorkflowDetail = () => {
const [modalApi, ModalContextHolder] = Modal.useModal(); const [modalApi, ModalContextHolder] = Modal.useModal();
const [notificationApi, NotificationContextHolder] = notification.useNotification(); const [notificationApi, NotificationContextHolder] = notification.useNotification();
const [scale, setScale] = useState(1);
const { id: workflowId } = useParams(); const { id: workflowId } = useParams();
const { workflow, initialized, ...workflowState } = useWorkflowStore( const { workflow, initialized, ...workflowState } = useWorkflowStore(
useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setEnabled", "release", "discard"]) useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setEnabled", "release", "discard"])
@ -58,15 +53,12 @@ const WorkflowDetail = () => {
const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration"); const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration");
const [isRunning, setIsRunning] = useState(false); const [isRunning, setIsRunning] = useState(false);
const lastRunStatus = useMemo(() => workflow.lastRunStatus, [workflow]);
const [allowDiscard, setAllowDiscard] = useState(false); const [allowDiscard, setAllowDiscard] = useState(false);
const [allowRelease, setAllowRelease] = useState(false); const [allowRelease, setAllowRelease] = useState(false);
const [allowRun, setAllowRun] = useState(false); const [allowRun, setAllowRun] = useState(false);
const lastRunStatus = useMemo(() => {
return workflow.lastRunStatus;
}, [workflow]);
useEffect(() => { useEffect(() => {
setIsRunning(lastRunStatus == WORKFLOW_RUN_STATUSES.RUNNING); setIsRunning(lastRunStatus == WORKFLOW_RUN_STATUSES.RUNNING);
}, [lastRunStatus]); }, [lastRunStatus]);
@ -192,7 +184,7 @@ const WorkflowDetail = () => {
} }
}); });
await runWorkflow(workflowId!); await startWorkflowRun(workflowId!);
messageApi.info(t("workflow.detail.orchestration.action.run.prompt")); messageApi.info(t("workflow.detail.orchestration.action.run.prompt"));
} catch (err) { } catch (err) {
@ -206,12 +198,13 @@ const WorkflowDetail = () => {
}; };
return ( return (
<div> <div className="flex size-full flex-col">
{MessageContextHolder} {MessageContextHolder}
{ModalContextHolder} {ModalContextHolder}
{NotificationContextHolder} {NotificationContextHolder}
<Card styles={{ body: { padding: "0.5rem", paddingBottom: 0 } }}> <div>
<Card styles={{ body: { padding: "0.5rem", paddingBottom: 0 } }} style={{ borderRadius: 0 }}>
<PageHeader <PageHeader
style={{ paddingBottom: 0 }} style={{ paddingBottom: 0 }}
title={workflow.name} title={workflow.name}
@ -263,12 +256,22 @@ const WorkflowDetail = () => {
/> />
</PageHeader> </PageHeader>
</Card> </Card>
</div>
<div className="p-4">
<Card loading={!initialized}>
<Show when={tabValue === "orchestration"}> <Show when={tabValue === "orchestration"}>
<div className="relative"> <div className="min-h-[360px] flex-1 overflow-hidden p-4">
<div className="flex items-center justify-between gap-4"> <Card
className="size-full overflow-hidden"
styles={{
body: {
position: "relative",
height: "100%",
padding: 0,
},
}}
loading={!initialized}
>
<div className="absolute inset-x-6 top-4 z-[2] flex items-center justify-between gap-4">
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
<Show when={workflow.hasDraft!}> <Show when={workflow.hasDraft!}>
<Alert banner message={<div className="truncate">{t("workflow.detail.orchestration.draft.alert")}</div>} type="warning" /> <Alert banner message={<div className="truncate">{t("workflow.detail.orchestration.draft.alert")}</div>} type="warning" />
@ -305,24 +308,19 @@ const WorkflowDetail = () => {
</Space> </Space>
</div> </div>
</div> </div>
<div className="fixed bottom-8 right-8 z-10 flex items-center gap-2 rounded-lg bg-white p-2 shadow-lg">
<Button icon={<MinusOutlined />} disabled={scale <= 0.5} onClick={() => setScale((s) => Math.max(0.5, s - 0.1))} />
<Typography.Text className="min-w-[3em] text-center">{Math.round(scale * 100)}%</Typography.Text>
<Button icon={<PlusCircleOutlined />} disabled={scale >= 2} onClick={() => setScale((s) => Math.min(2, s + 0.1))} />
<Button icon={<ReloadOutlined />} onClick={() => setScale(1)} />
</div>
<div className="size-full origin-top px-12 py-8 transition-transform duration-300 max-md:px-4" style={{ transform: `scale(${scale})` }}> <WorkflowElementsContainer className="pt-16" />
<WorkflowElements /> </Card>
</div>
</div> </div>
</Show> </Show>
<Show when={tabValue === "runs"}> <Show when={tabValue === "runs"}>
<div className="p-4">
<Card loading={!initialized}>
<WorkflowRuns workflowId={workflowId!} /> <WorkflowRuns workflowId={workflowId!} />
</Show>
</Card> </Card>
</div> </div>
</Show>
</div> </div>
); );
}; };

View File

@ -3,9 +3,11 @@ import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import { import {
CheckCircleOutlined as CheckCircleOutlinedIcon, CheckCircleOutlined as CheckCircleOutlinedIcon,
ClockCircleOutlined as ClockCircleOutlinedIcon,
CloseCircleOutlined as CloseCircleOutlinedIcon, CloseCircleOutlined as CloseCircleOutlinedIcon,
DeleteOutlined as DeleteOutlinedIcon, DeleteOutlined as DeleteOutlinedIcon,
EditOutlined as EditOutlinedIcon, EditOutlined as EditOutlinedIcon,
PauseCircleOutlined as PauseCircleOutlinedIcon,
PlusOutlined as PlusOutlinedIcon, PlusOutlined as PlusOutlinedIcon,
SyncOutlined as SyncOutlinedIcon, SyncOutlined as SyncOutlinedIcon,
} from "@ant-design/icons"; } from "@ant-design/icons";
@ -13,7 +15,6 @@ import {
import { PageHeader } from "@ant-design/pro-components"; import { PageHeader } from "@ant-design/pro-components";
import { useRequest } from "ahooks"; import { useRequest } from "ahooks";
import { import {
Badge,
Button, Button,
Divider, Divider,
Empty, Empty,
@ -159,32 +160,25 @@ const WorkflowList = () => {
key: "lastRun", key: "lastRun",
title: t("workflow.props.last_run_at"), title: t("workflow.props.last_run_at"),
render: (_, record) => { render: (_, record) => {
if (record.lastRunId) { let icon = <></>;
if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.RUNNING) { if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.PENDING) {
return ( icon = <ClockCircleOutlinedIcon style={{ color: themeToken.colorTextSecondary }} />;
<Space> } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.RUNNING) {
<Badge status="processing" count={<SyncOutlinedIcon style={{ color: themeToken.colorInfo }} />} /> icon = <SyncOutlinedIcon style={{ color: themeToken.colorInfo }} spin />;
<Typography.Text>{dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss")}</Typography.Text>
</Space>
);
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.SUCCEEDED) { } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.SUCCEEDED) {
return ( icon = <CheckCircleOutlinedIcon style={{ color: themeToken.colorSuccess }} />;
<Space>
<Badge status="success" count={<CheckCircleOutlinedIcon style={{ color: themeToken.colorSuccess }} />} />
<Typography.Text>{dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss")}</Typography.Text>
</Space>
);
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.FAILED) { } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.FAILED) {
return ( icon = <CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} />;
<Space> } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.CANCELED) {
<Badge status="error" count={<CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} />} /> icon = <PauseCircleOutlinedIcon style={{ color: themeToken.colorWarning }} />;
<Typography.Text>{dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss")}</Typography.Text>
</Space>
);
}
} }
return <></>; return (
<Space>
{icon}
<Typography.Text>{record.lastRunTime ? dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss") : ""}</Typography.Text>
</Space>
);
}, },
}, },
{ {

View File

@ -109,7 +109,7 @@ const WorkflowNew = () => {
<div> <div>
{NotificationContextHolder} {NotificationContextHolder}
<Card styles={{ body: { padding: "0.5rem", paddingBottom: 0 } }}> <Card styles={{ body: { padding: "0.5rem", paddingBottom: 0 } }} style={{ borderRadius: 0 }}>
<PageHeader title={t("workflow.new.title")}> <PageHeader title={t("workflow.new.title")}>
<Typography.Paragraph type="secondary">{t("workflow.new.subtitle")}</Typography.Paragraph> <Typography.Paragraph type="secondary">{t("workflow.new.subtitle")}</Typography.Paragraph>
</PageHeader> </PageHeader>

View File

@ -14,7 +14,7 @@ export type ListWorkflowRunsRequest = {
export const list = async (request: ListWorkflowRunsRequest) => { export const list = async (request: ListWorkflowRunsRequest) => {
const page = request.page || 1; const page = request.page || 1;
const perPage = request.perPage || 10; const perPage = request.perPage || 10;
console.log("request.workflowId", request.workflowId);
let filter = ""; let filter = "";
const params: Record<string, string> = {}; const params: Record<string, string> = {};
if (request.workflowId) { if (request.workflowId) {
@ -31,3 +31,7 @@ export const list = async (request: ListWorkflowRunsRequest) => {
expand: request.expand ? "workflowId" : undefined, expand: request.expand ? "workflowId" : undefined,
}); });
}; };
export const remove = async (record: MaybeModelRecordWithId<WorkflowRunModel>) => {
return await getPocketBase().collection(COLLECTION_NAME).delete(record.id);
};

6
ui/src/utils/css.ts Normal file
View File

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export const mergeCls = (...inputs: ClassValue[]) => {
return twMerge(clsx(inputs));
};

View File

@ -1,5 +1,9 @@
import { ClientResponseError } from "pocketbase";
export const getErrMsg = (error: unknown): string => { export const getErrMsg = (error: unknown): string => {
if (error instanceof Error) { if (error instanceof ClientResponseError) {
return error.response != null ? getErrMsg(error.response) : error.message;
} else if (error instanceof Error) {
return error.message; return error.message;
} else if (typeof error === "object" && error != null) { } else if (typeof error === "object" && error != null) {
if ("message" in error) { if ("message" in error) {