mirror of
https://github.com/usual2970/certimate.git
synced 2025-07-07 19:49:55 +00:00
Compare commits
11 Commits
5f213b5f51
...
c0386b153e
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c0386b153e | ||
![]() |
5cabceb08e | ||
![]() |
b67049f9aa | ||
![]() |
7a2fc5e2fd | ||
![]() |
0f945881a1 | ||
![]() |
bee4ba10cb | ||
![]() |
7e0f575e0a | ||
![]() |
79c1da6d14 | ||
![]() |
8dc86209df | ||
![]() |
c61b2d2d3f | ||
![]() |
d5568608f5 |
28
go.mod
28
go.mod
@ -18,14 +18,14 @@ require (
|
||||
github.com/baidubce/bce-sdk-go v0.9.214
|
||||
github.com/byteplus-sdk/byteplus-sdk-golang v1.0.40
|
||||
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/huaweicloud/huaweicloud-sdk-go-v3 v0.1.132
|
||||
github.com/nikoksr/notify v1.3.0
|
||||
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0
|
||||
github.com/pkg/sftp v1.13.7
|
||||
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/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn 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/go-cmp v0.6.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/kylelemons/godebug v1.1.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/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // 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/structured-merge-diff/v4 v4.5.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/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/config v1.29.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.53 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.1 // 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/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/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/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/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/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/sso v1.24.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.8 // 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.11 // 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.9 // indirect
|
||||
github.com/aws/smithy-go v1.22.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
@ -192,8 +188,8 @@ require (
|
||||
google.golang.org/protobuf v1.36.3 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // 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/memory v1.8.2 // indirect
|
||||
modernc.org/sqlite v1.34.4 // indirect
|
||||
modernc.org/sqlite v1.34.5 // indirect
|
||||
)
|
||||
|
72
go.sum
72
go.sum
@ -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/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/config v1.29.0 h1:Vk/u4jof33or1qAQLdofpjKV7mQQT7DcUpnYx8kdmxY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.0/go.mod h1:iXAZK3Gxvpq3tA+B9WaDYpZis7M8KFgdrDPMmHrgbJM=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.53 h1:lwrVhiEDW5yXsuVKlFVUnR2R50zt2DklhOyeLETqDuE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.53/go.mod h1:CkqM1bIw/xjEpBMhBnvqUXYZbpCFuj6dnCAyDk2AtAY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.1 h1:JZhGawAyZ/EuJeBtbQYnaoftczcb2drR2Iq36Wgz4sQ=
|
||||
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.54 h1:4UmqeOqJPvdvASZWrKlhzpRahAulBfyTJQUaYy4+hEI=
|
||||
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/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.51/go.mod h1:B9sW5/AD5bStKdTyUdz1xWRKOwnyUwJ4eJ4olQBtZo0=
|
||||
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.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/go.mod h1:3So8EA/aAYm36L7XIvCVwLa0s5N0P7o2b1oqnx/2R4g=
|
||||
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/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/checksum v1.5.1 h1:mJ9FRktB8v1Ihpqwfk0AWvYEd0FgQtLsshc2Qb2TVc8=
|
||||
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 h1:e6um6+DWYQP1XCa+E9YVtG/9v1qk5lyAOelMOVwSyO8=
|
||||
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/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/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/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.1/go.mod h1:K+0a0kWDHAUXBH8GvYGS3cQRwIuRjO9bMWUz6vpNCaU=
|
||||
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.10/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.9/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.8/go.mod h1:f6vjfZER1M17Fokn0IzssOTMT2N8ZSq+7jnNF0tArvw=
|
||||
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.2/go.mod h1:jGJ/v7FIi7Ys9t54tmEFnrxuaWeJLpwNgKp2DXAVhOU=
|
||||
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.11/go.mod h1:Ro744S4fKiCCuZECXgOi760TiYylUM8ZBf6OGiZzJtY=
|
||||
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.10/go.mod h1:Fzsj6lZEb8AkTE5S68OhcbBqeWPsR8RnGuKPr8Todl8=
|
||||
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.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.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
|
||||
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.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-resty/resty/v2 v2.16.3 h1:zacNT7lt4b8M/io2Ahj6yPypL7bqx9n1iprfQuodV+E=
|
||||
github.com/go-resty/resty/v2 v2.16.3/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||
github.com/go-resty/resty/v2 v2.16.4 h1:81IjtszQKwbz7dot4LLYGwhJNUsNwECD2O7nru5q60E=
|
||||
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.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
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.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/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/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
|
||||
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/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
|
||||
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.3/go.mod h1:EfXV/8RUY76jA6g1RPNHjOuW7wTd2bz0QlvAI/RU8YY=
|
||||
github.com/pocketbase/pocketbase v0.24.4 h1:kw/c23HccoxMV/19U9QlDcvNJgQ66vlUrxGQDZicWKM=
|
||||
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.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
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/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0=
|
||||
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.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
|
||||
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
|
||||
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
||||
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.23.13 h1:PFiaemQwE/jdwi8XEHyEV+qYWoIuikLP3T4rvDeJb00=
|
||||
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.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
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.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/gc/v3 v3.0.0-20250105121824-520be1a3aee6 h1:JoKwHjIFumiKrjMbp1cNbC5E9UyCgA/ZcID0xOWQ2N8=
|
||||
modernc.org/gc/v3 v3.0.0-20250105121824-520be1a3aee6/go.mod h1:LG5UO1Ran4OO0JRKz2oNiXhR5nNrgz0PzH7UKhz0aMU=
|
||||
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/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8=
|
||||
modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/libc v1.61.9 h1:PLSBXVkifXGELtJ5BOnBUyAHr7lsatNwFU/RRo4kfJM=
|
||||
modernc.org/libc v1.61.9/go.mod h1:61xrnzk/aR8gr5bR7Uj/lLFLuXu2/zMpIjcry63Eumk=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
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/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||
modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
|
||||
modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
|
||||
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/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
@ -82,9 +82,9 @@ type acmeAccountRepository interface {
|
||||
|
||||
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) {
|
||||
return register(client, sslProviderConfig, user)
|
||||
return registerAcmeUser(client, sslProviderConfig, user)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@ -94,7 +94,7 @@ func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderCon
|
||||
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 err error
|
||||
switch sslProviderConfig.Provider {
|
||||
@ -123,7 +123,6 @@ func register(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, use
|
||||
}
|
||||
|
||||
repo := repository.NewAcmeAccountRepository()
|
||||
|
||||
resp, err := repo.GetByCAAndEmail(sslProviderConfig.Provider, user.GetEmail())
|
||||
if err == nil {
|
||||
user.privkey = resp.Key
|
||||
|
@ -159,7 +159,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
|
||||
|
||||
// New users need to register first
|
||||
if !acmeUser.hasRegistration() {
|
||||
reg, err := registerAcmeUser(client, sslProviderConfig, acmeUser)
|
||||
reg, err := registerAcmeUserWithSingleFlight(client, sslProviderConfig, acmeUser)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to register: %w", err)
|
||||
}
|
||||
|
@ -25,8 +25,8 @@ const (
|
||||
)
|
||||
|
||||
type certificateRepository interface {
|
||||
GetById(ctx context.Context, id string) (*domain.Certificate, error)
|
||||
ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error)
|
||||
GetById(ctx context.Context, id string) (*domain.Certificate, error)
|
||||
}
|
||||
|
||||
type CertificateService struct {
|
||||
|
@ -5,11 +5,6 @@ type CertificateArchiveFileReq struct {
|
||||
Format string `json:"format"`
|
||||
}
|
||||
|
||||
type CertificateArchiveFileResp struct {
|
||||
Certificate string `json:"certificate"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
}
|
||||
|
||||
type CertificateValidateCertificateReq struct {
|
||||
Certificate string `json:"certificate"`
|
||||
}
|
||||
|
@ -2,7 +2,12 @@
|
||||
|
||||
import "github.com/usual2970/certimate/internal/domain"
|
||||
|
||||
type WorkflowRunReq struct {
|
||||
type WorkflowStartRunReq struct {
|
||||
WorkflowId string `json:"-"`
|
||||
Trigger domain.WorkflowTriggerType `json:"trigger"`
|
||||
}
|
||||
|
||||
type WorkflowCancelRunReq struct {
|
||||
WorkflowId string `json:"-"`
|
||||
RunId string `json:"-"`
|
||||
}
|
||||
|
@ -69,11 +69,11 @@ type WorkflowNodeConfigForApply struct {
|
||||
ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置
|
||||
KeyAlgorithm string `json:"keyAlgorithm"` // 密钥算法
|
||||
Nameservers string `json:"nameservers"` // DNS 服务器列表,以半角逗号分隔
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout"` // DNS 传播超时时间(默认取决于提供商)
|
||||
DnsTTL int32 `json:"dnsTTL"` // DNS TTL(默认取决于提供商)
|
||||
DisableFollowCNAME bool `json:"disableFollowCNAME"` // 是否禁用 CNAME 跟随
|
||||
DisableARI bool `json:"disableARI"` // 是否禁用 ARI
|
||||
SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays"` // 证书到期前多少天前跳过续期(默认值:30)
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout"` // DNS 传播超时时间(零值取决于提供商的默认值)
|
||||
DnsTTL int32 `json:"dnsTTL"` // DNS TTL(零值取决于提供商的默认值)
|
||||
DisableFollowCNAME bool `json:"disableFollowCNAME"` // 是否关闭 CNAME 跟随
|
||||
DisableARI bool `json:"disableARI"` // 是否关闭 ARI
|
||||
SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays"` // 证书到期前多少天前跳过续期(零值将使用默认值 30)
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForUpload struct {
|
||||
|
@ -1,6 +1,9 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const CollectionNameWorkflowRun = "workflow_run"
|
||||
|
||||
@ -22,6 +25,7 @@ const (
|
||||
WorkflowRunStatusTypeRunning WorkflowRunStatusType = "running"
|
||||
WorkflowRunStatusTypeSucceeded WorkflowRunStatusType = "succeeded"
|
||||
WorkflowRunStatusTypeFailed WorkflowRunStatusType = "failed"
|
||||
WorkflowRunStatusTypeCanceled WorkflowRunStatusType = "canceled"
|
||||
)
|
||||
|
||||
type WorkflowRunLog struct {
|
||||
@ -40,12 +44,13 @@ type WorkflowRunLogOutput struct {
|
||||
|
||||
type WorkflowRunLogs []WorkflowRunLog
|
||||
|
||||
func (r WorkflowRunLogs) FirstError() string {
|
||||
func (r WorkflowRunLogs) ErrorString() string {
|
||||
var builder strings.Builder
|
||||
for _, log := range r {
|
||||
if log.Error != "" {
|
||||
return log.Error
|
||||
builder.WriteString(log.Error)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
return builder.String()
|
||||
}
|
||||
|
@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/types"
|
||||
)
|
||||
|
||||
// 表示默认的日志记录器类型。
|
||||
@ -21,7 +23,7 @@ func (l *DefaultLogger) Logt(tag string, data ...any) {
|
||||
temp[0] = tag
|
||||
for i, v := range data {
|
||||
s := ""
|
||||
if v == nil {
|
||||
if types.IsNil(v) {
|
||||
s = "<nil>"
|
||||
} else {
|
||||
switch reflect.ValueOf(v).Kind() {
|
||||
|
@ -1,12 +1,14 @@
|
||||
package certs
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
xerrors "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -34,6 +36,19 @@ func ParseCertificateFromPEM(certPem string) (cert *x509.Certificate, err error)
|
||||
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 对象。
|
||||
//
|
||||
// 入参:
|
||||
|
@ -2,8 +2,6 @@
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"time"
|
||||
@ -28,23 +26,9 @@ func TransformCertificateFromPEMToPFX(certPem string, privkeyPem string, pfxPass
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var privkey interface{}
|
||||
switch cert.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
{
|
||||
privkey, err = ParsePKCS1PrivateKeyFromPEM(privkeyPem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
case *ecdsa.PublicKey:
|
||||
{
|
||||
privkey, err = ParseECPrivateKeyFromPEM(privkeyPem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
privkey, err := ParsePrivateKeyFromPEM(privkeyPem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pfxData, err := pkcs12.LegacyRC2.Encode(privkey, cert, nil, pfxPassword)
|
||||
|
@ -3,6 +3,7 @@
|
||||
import "reflect"
|
||||
|
||||
// 判断对象是否为 nil。
|
||||
// 与直接使用 `obj == nil` 不同,该函数会正确判断接口类型对象的真实值是否为空。
|
||||
//
|
||||
// 入参:
|
||||
// - value:待判断的对象。
|
||||
|
@ -19,11 +19,11 @@ func NewCertificateRepository() *CertificateRepository {
|
||||
}
|
||||
|
||||
func (r *CertificateRepository) ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error) {
|
||||
records, err := app.GetApp().FindRecordsByFilter(
|
||||
records, err := app.GetApp().FindAllRecords(
|
||||
domain.CollectionNameCertificate,
|
||||
"expireAt>DATETIME('now') && expireAt<DATETIME('now', '+20 days') && deleted=null",
|
||||
"-created",
|
||||
0, 0,
|
||||
dbx.NewExp("expireAt>DATETIME('now')"),
|
||||
dbx.NewExp("expireAt<DATETIME('now', '+20 days')"),
|
||||
dbx.NewExp("deleted=null"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -62,7 +62,8 @@ func (r *CertificateRepository) GetByWorkflowNodeId(ctx context.Context, workflo
|
||||
records, err := app.GetApp().FindRecordsByFilter(
|
||||
domain.CollectionNameCertificate,
|
||||
"workflowNodeId={:workflowNodeId} && deleted=null",
|
||||
"-created", 1, 0,
|
||||
"-created",
|
||||
1, 0,
|
||||
dbx.Params{"workflowNodeId": workflowNodeId},
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -55,10 +55,10 @@ func (r *WorkflowRepository) GetById(ctx context.Context, id string) (*domain.Wo
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
return workflow, err
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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("lastRunTime", workflow.LastRunTime)
|
||||
|
||||
return app.GetApp().Save(record)
|
||||
if err := app.GetApp().Save(record); err != nil {
|
||||
return workflow, err
|
||||
}
|
||||
|
||||
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) error {
|
||||
func (r *WorkflowRepository) SaveRun(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) {
|
||||
collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowRun)
|
||||
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 {
|
||||
workflowRunRecord := core.NewRecord(collection)
|
||||
workflowRunRecord.Id = workflowRun.Id
|
||||
workflowRunRecord.Set("workflowId", workflowRun.WorkflowId)
|
||||
workflowRunRecord.Set("trigger", string(workflowRun.Trigger))
|
||||
workflowRunRecord.Set("status", string(workflowRun.Status))
|
||||
@ -115,6 +133,7 @@ func (r *WorkflowRepository) SaveRun(ctx context.Context, workflowRun *domain.Wo
|
||||
return err
|
||||
}
|
||||
|
||||
workflowRecord.IgnoreUnchangedFields(true)
|
||||
workflowRecord.Set("lastRunId", workflowRunRecord.Id)
|
||||
workflowRecord.Set("lastRunStatus", workflowRunRecord.GetString("status"))
|
||||
workflowRecord.Set("lastRunTime", workflowRunRecord.GetString("startedAt"))
|
||||
@ -123,13 +142,17 @@ func (r *WorkflowRepository) SaveRun(ctx context.Context, workflowRun *domain.Wo
|
||||
return err
|
||||
}
|
||||
|
||||
workflowRun.Id = workflowRunRecord.Id
|
||||
workflowRun.CreatedAt = workflowRunRecord.GetDateTime("created").Time()
|
||||
workflowRun.UpdatedAt = workflowRunRecord.GetDateTime("updated").Time()
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return workflowRun, err
|
||||
}
|
||||
|
||||
return nil
|
||||
return workflowRun, nil
|
||||
}
|
||||
|
||||
func (r *WorkflowRepository) castRecordToModel(record *core.Record) (*domain.Workflow, error) {
|
||||
|
@ -26,16 +26,31 @@ func NewCertificateHandler(router *router.RouterGroup[*core.RequestEvent], servi
|
||||
}
|
||||
|
||||
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/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 {
|
||||
req := &dtos.CertificateValidateCertificateReq{}
|
||||
if err := e.BindBody(req); err != nil {
|
||||
return resp.Err(e, err)
|
||||
}
|
||||
|
||||
if rs, err := handler.service.ValidateCertificate(e.Request.Context(), req); err != nil {
|
||||
return resp.Err(e, err)
|
||||
} else {
|
||||
@ -48,23 +63,10 @@ func (handler *CertificateHandler) validatePrivateKey(e *core.RequestEvent) erro
|
||||
if err := e.BindBody(req); err != nil {
|
||||
return resp.Err(e, err)
|
||||
}
|
||||
|
||||
if err := handler.service.ValidatePrivateKey(e.Request.Context(), req); err != nil {
|
||||
return resp.Err(e, err)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,8 @@ import (
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -25,17 +26,30 @@ func NewWorkflowHandler(router *router.RouterGroup[*core.RequestEvent], service
|
||||
}
|
||||
|
||||
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 {
|
||||
req := &dtos.WorkflowRunReq{}
|
||||
req.WorkflowId = e.Request.PathValue("id")
|
||||
req := &dtos.WorkflowStartRunReq{}
|
||||
req.WorkflowId = e.Request.PathValue("workflowId")
|
||||
if err := e.BindBody(req); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
NewWorkflowService(repository.NewWorkflowRepository()).Run(ctx, &dtos.WorkflowRunReq{
|
||||
NewWorkflowService(repository.NewWorkflowRepository()).StartRun(ctx, &dtos.WorkflowStartRunReq{
|
||||
WorkflowId: workflowId,
|
||||
Trigger: domain.WorkflowTriggerTypeAuto,
|
||||
})
|
||||
|
@ -134,7 +134,7 @@ func (a *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo
|
||||
renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24
|
||||
expirationTime := time.Until(lastCertificate.ExpireAt)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
ctx = setContextWorkflowId(ctx, w.workflow.Id)
|
||||
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 {
|
||||
current := node
|
||||
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 runErr error
|
||||
for {
|
||||
if current.Type != domain.WorkflowNodeTypeBranch && current.Type != domain.WorkflowNodeTypeExecuteResultBranch {
|
||||
processor, runErr = nodes.GetProcessor(current)
|
||||
@ -49,7 +49,6 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl
|
||||
}
|
||||
|
||||
runErr = processor.Run(ctx)
|
||||
|
||||
log := processor.Log(ctx)
|
||||
if log != nil {
|
||||
w.logs = append(w.logs, *log)
|
||||
@ -58,6 +57,7 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@ -70,8 +70,8 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl
|
||||
} else {
|
||||
current = current.Next
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -79,10 +79,6 @@ func setContextWorkflowId(ctx context.Context, id string) context.Context {
|
||||
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 {
|
||||
for _, branch := range branches {
|
||||
if branch.Type == nodeType {
|
||||
|
@ -23,8 +23,8 @@ type workflowRunData struct {
|
||||
type workflowRepository interface {
|
||||
ListEnabledAuto(ctx context.Context) ([]*domain.Workflow, error)
|
||||
GetById(ctx context.Context, id string) (*domain.Workflow, error)
|
||||
Save(ctx context.Context, workflow *domain.Workflow) error
|
||||
SaveRun(ctx context.Context, workflowRun *domain.WorkflowRun) error
|
||||
Save(ctx context.Context, workflow *domain.Workflow) (*domain.Workflow, error)
|
||||
SaveRun(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error)
|
||||
}
|
||||
|
||||
type WorkflowService struct {
|
||||
@ -35,35 +35,20 @@ type WorkflowService struct {
|
||||
}
|
||||
|
||||
func NewWorkflowService(repo workflowRepository) *WorkflowService {
|
||||
rs := &WorkflowService{
|
||||
srv := &WorkflowService{
|
||||
repo: repo,
|
||||
ch: make(chan *workflowRunData, 1),
|
||||
}
|
||||
|
||||
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++ {
|
||||
go rs.process(ctx)
|
||||
go srv.run(ctx)
|
||||
}
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
return srv
|
||||
}
|
||||
|
||||
func (s *WorkflowService) InitSchedule(ctx context.Context) error {
|
||||
@ -75,7 +60,7 @@ func (s *WorkflowService) InitSchedule(ctx context.Context) error {
|
||||
scheduler := app.GetScheduler()
|
||||
for _, workflow := range workflows {
|
||||
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,
|
||||
Trigger: domain.WorkflowTriggerTypeAuto,
|
||||
})
|
||||
@ -89,8 +74,7 @@ func (s *WorkflowService) InitSchedule(ctx context.Context) error {
|
||||
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)
|
||||
if err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
// set last run
|
||||
workflow.LastRunTime = time.Now()
|
||||
workflow.LastRunStatus = domain.WorkflowRunStatusTypeRunning
|
||||
workflow.LastRunStatus = domain.WorkflowRunStatusTypePending
|
||||
workflow.LastRunId = ""
|
||||
|
||||
if err := s.repo.Save(ctx, workflow); err != nil {
|
||||
if resp, err := s.repo.Save(ctx, workflow); err != nil {
|
||||
return err
|
||||
} else {
|
||||
workflow = resp
|
||||
}
|
||||
|
||||
s.ch <- &workflowRunData{
|
||||
@ -118,51 +102,70 @@ func (s *WorkflowService) Run(ctx context.Context, req *dtos.WorkflowRunReq) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WorkflowService) run(ctx context.Context, runData *workflowRunData) error {
|
||||
// 执行
|
||||
workflow := runData.Workflow
|
||||
run := &domain.WorkflowRun{
|
||||
WorkflowId: workflow.Id,
|
||||
Status: domain.WorkflowRunStatusTypeRunning,
|
||||
Trigger: runData.RunTrigger,
|
||||
StartedAt: time.Now(),
|
||||
EndedAt: time.Now(),
|
||||
}
|
||||
func (s *WorkflowService) CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) error {
|
||||
// TODO: 取消运行,防止因为某些原因意外挂起(如进程被杀死)导致工作流一直处于 running 状态无法重新运行
|
||||
|
||||
processor := processor.NewWorkflowProcessor(workflow)
|
||||
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
|
||||
return errors.New("TODO: 尚未实现")
|
||||
}
|
||||
|
||||
func (s *WorkflowService) Stop(ctx context.Context) {
|
||||
s.cancel()
|
||||
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
|
||||
}
|
||||
|
65
migrations/1737479489_updated_workflow.go
Normal file
65
migrations/1737479489_updated_workflow.go
Normal 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)
|
||||
})
|
||||
}
|
65
migrations/1737479538_updated_workflow_run.go
Normal file
65
migrations/1737479538_updated_workflow_run.go
Normal 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
19
ui/package-lock.json
generated
@ -13,6 +13,7 @@
|
||||
"ahooks": "^3.8.4",
|
||||
"antd": "^5.23.1",
|
||||
"antd-zod": "^6.0.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cron-parser": "^4.9.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^24.2.1",
|
||||
@ -27,6 +28,7 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-router-dom": "^7.1.3",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"zod": "^3.24.1",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
@ -4124,6 +4126,14 @@
|
||||
"resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz",
|
||||
"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": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz",
|
||||
@ -8563,6 +8573,15 @@
|
||||
"integrity": "sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==",
|
||||
"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": {
|
||||
"version": "3.4.17",
|
||||
"resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||
|
@ -15,6 +15,7 @@
|
||||
"ahooks": "^3.8.4",
|
||||
"antd": "^5.23.1",
|
||||
"antd-zod": "^6.0.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cron-parser": "^4.9.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^24.2.1",
|
||||
@ -29,6 +30,7 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-router-dom": "^7.1.3",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"zod": "^3.24.1",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
|
@ -3,10 +3,10 @@ import { ClientResponseError } from "pocketbase";
|
||||
import { type CertificateFormatType } from "@/domain/certificate";
|
||||
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 resp = await pb.send<BaseResponse>(`/api/certificates/${encodeURIComponent(id)}/archive`, {
|
||||
const resp = await pb.send<BaseResponse<string>>(`/api/certificates/${encodeURIComponent(certificateId)}/archive`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@ -38,9 +38,11 @@ export const validateCertificate = async (certificate: string) => {
|
||||
certificate: certificate,
|
||||
},
|
||||
});
|
||||
|
||||
if (resp.code != 0) {
|
||||
throw new Error(resp.msg);
|
||||
throw new ClientResponseError({ status: resp.code, response: resp, data: {} });
|
||||
}
|
||||
|
||||
return resp;
|
||||
};
|
||||
|
||||
@ -55,8 +57,10 @@ export const validatePrivateKey = async (privateKey: string) => {
|
||||
privateKey: privateKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (resp.code != 0) {
|
||||
throw new Error(resp.msg);
|
||||
throw new ClientResponseError({ status: resp.code, response: resp, data: {} });
|
||||
}
|
||||
|
||||
return resp;
|
||||
};
|
||||
|
@ -3,10 +3,10 @@ import { ClientResponseError } from "pocketbase";
|
||||
import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
|
||||
import { getPocketBase } from "@/repository/_pocketbase";
|
||||
|
||||
export const run = async (id: string) => {
|
||||
export const startRun = async (workflowId: string) => {
|
||||
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",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@ -22,3 +22,20 @@ export const run = async (id: string) => {
|
||||
|
||||
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;
|
||||
};
|
||||
|
@ -49,9 +49,7 @@ const DrawerForm = <T extends NonNullable<unknown> = any>({
|
||||
|
||||
const triggerEl = useTriggerElement(trigger, {
|
||||
onClick: () => {
|
||||
console.log("click");
|
||||
setOpen(true);
|
||||
console.log(open);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -27,7 +27,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
|
||||
const blob = new Blob([u8arr], { type: "application/zip" });
|
||||
saveAs(blob, `${data.id}-${data.subjectAltNames}.zip`);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
messageApi.warning(t("common.text.operation_failed"));
|
||||
}
|
||||
};
|
||||
|
41
ui/src/components/workflow/WorkflowElementsContainer.tsx
Normal file
41
ui/src/components/workflow/WorkflowElementsContainer.tsx
Normal 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;
|
@ -41,11 +41,11 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR
|
||||
</Show>
|
||||
|
||||
<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) => {
|
||||
return (
|
||||
<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">
|
||||
{item.outputs?.map((output, j) => {
|
||||
return (
|
||||
|
@ -4,17 +4,21 @@ import {
|
||||
CheckCircleOutlined as CheckCircleOutlinedIcon,
|
||||
ClockCircleOutlined as ClockCircleOutlinedIcon,
|
||||
CloseCircleOutlined as CloseCircleOutlinedIcon,
|
||||
DeleteOutlined as DeleteOutlinedIcon,
|
||||
PauseCircleOutlined as PauseCircleOutlinedIcon,
|
||||
PauseOutlined as PauseOutlinedIcon,
|
||||
SelectOutlined as SelectOutlinedIcon,
|
||||
SyncOutlined as SyncOutlinedIcon,
|
||||
} from "@ant-design/icons";
|
||||
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 { ClientResponseError } from "pocketbase";
|
||||
|
||||
import { cancelRun as cancelWorkflowRun } from "@/api/workflows";
|
||||
import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
|
||||
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 WorkflowRunDetailDrawer from "./WorkflowRunDetailDrawer";
|
||||
|
||||
@ -27,6 +31,7 @@ export type WorkflowRunsProps = {
|
||||
const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [modalApi, ModelContextHolder] = Modal.useModal();
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
|
||||
const tableColumns: TableProps<WorkflowRunModel>["columns"] = [
|
||||
@ -68,6 +73,12 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
{t("workflow_run.props.status.failed")}
|
||||
</Tag>
|
||||
);
|
||||
} else if (record.status === WORKFLOW_RUN_STATUSES.CANCELED) {
|
||||
return (
|
||||
<Tag icon={<PauseCircleOutlinedIcon />} color="warning">
|
||||
{t("workflow_run.props.status.canceled")}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
@ -116,11 +127,51 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
align: "end",
|
||||
fixed: "right",
|
||||
width: 120,
|
||||
render: (_, record) => (
|
||||
<Button.Group>
|
||||
<WorkflowRunDetailDrawer data={record} trigger={<Button color="primary" icon={<SelectOutlinedIcon />} variant="text" />} />
|
||||
</Button.Group>
|
||||
),
|
||||
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>
|
||||
<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>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
const [tableData, setTableData] = useState<WorkflowRunModel[]>([]);
|
||||
@ -129,7 +180,11 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [pageSize, setPageSize] = useState<number>(10);
|
||||
|
||||
const { loading, error: loadedError } = useRequest(
|
||||
const {
|
||||
loading,
|
||||
error: loadedError,
|
||||
run: refreshData,
|
||||
} = useRequest(
|
||||
() => {
|
||||
return listWorkflowRuns({
|
||||
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 (
|
||||
<>
|
||||
{ModelContextHolder}
|
||||
{NotificationContextHolder}
|
||||
|
||||
<div className={className} style={style}>
|
||||
|
@ -2,11 +2,11 @@ import { memo, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
CloudUploadOutlined as CloudUploadOutlinedIcon,
|
||||
DeploymentUnitOutlined as DeploymentUnitOutlinedIcon,
|
||||
PlusOutlined as PlusOutlinedIcon,
|
||||
SendOutlined as SendOutlinedIcon,
|
||||
SisternodeOutlined as SisternodeOutlinedIcon,
|
||||
SolutionOutlined as SolutionOutlinedIcon,
|
||||
SafetyOutlined as SafetyOutlinedIcon,
|
||||
} from "@ant-design/icons";
|
||||
import { Dropdown } from "antd";
|
||||
|
||||
@ -26,15 +26,15 @@ const AddNode = ({ node, disabled }: AddNodeProps) => {
|
||||
const dropdownMenus = useMemo(() => {
|
||||
return [
|
||||
[WorkflowNodeType.Apply, "workflow_node.apply.label", <SolutionOutlinedIcon />],
|
||||
[WorkflowNodeType.Upload, "workflow_node.upload.label", <SafetyOutlinedIcon />],
|
||||
[WorkflowNodeType.Deploy, "workflow_node.deploy.label", <CloudUploadOutlinedIcon />],
|
||||
[WorkflowNodeType.Upload, "workflow_node.upload.label", <CloudUploadOutlinedIcon />],
|
||||
[WorkflowNodeType.Deploy, "workflow_node.deploy.label", <DeploymentUnitOutlinedIcon />],
|
||||
[WorkflowNodeType.Notify, "workflow_node.notify.label", <SendOutlinedIcon />],
|
||||
[WorkflowNodeType.Branch, "workflow_node.branch.label", <SisternodeOutlinedIcon />],
|
||||
[WorkflowNodeType.ExecuteResultBranch, "workflow_node.execute_result_branch.label", <SisternodeOutlinedIcon />],
|
||||
[WorkflowNodeType.Notify, "workflow_node.notify.label", <SendOutlinedIcon />],
|
||||
]
|
||||
.filter(([type]) => {
|
||||
if (node.type !== WorkflowNodeType.Apply && node.type !== WorkflowNodeType.Deploy && type === WorkflowNodeType.ExecuteResultBranch) {
|
||||
return false;
|
||||
if (node.type !== WorkflowNodeType.Apply && node.type !== WorkflowNodeType.Deploy && node.type !== WorkflowNodeType.Notify) {
|
||||
return type !== WorkflowNodeType.ExecuteResultBranch;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -16,6 +16,8 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP
|
||||
return (
|
||||
<>
|
||||
<Popover
|
||||
classNames={{ root: "shadow-md" }}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
arrow={false}
|
||||
content={
|
||||
<SharedNode.Menu
|
||||
@ -26,8 +28,6 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP
|
||||
trigger={<Button color="primary" icon={<MoreOutlinedIcon />} variant="text" />}
|
||||
/>
|
||||
}
|
||||
overlayClassName="shadow-md"
|
||||
overlayInnerStyle={{ padding: 0 }}
|
||||
placement="rightTop"
|
||||
>
|
||||
<Card className="relative z-[1] mt-10 w-[256px] shadow-md" styles={{ body: { padding: 0 } }} hoverable>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons";
|
||||
import { Button, Divider, Flex, Form, type FormInstance, Select, Switch, Tooltip, Typography } from "antd";
|
||||
import { Alert, Button, Divider, Flex, Form, type FormInstance, Select, Switch, Tooltip, Typography } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
@ -310,6 +310,15 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
|
||||
</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
|
||||
name="certificate"
|
||||
label={t("workflow_node.deploy.form.certificate.label")}
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { memo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MoreOutlined as MoreOutlinedIcon } from "@ant-design/icons";
|
||||
import { Button, Card, Popover } from "antd";
|
||||
import {
|
||||
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 AddNode from "./AddNode";
|
||||
import SharedNode, { type SharedNodeProps } from "./_SharedNode";
|
||||
@ -16,9 +19,13 @@ export type ConditionNodeProps = SharedNodeProps & {
|
||||
const ExecuteResultNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { token: themeToken } = theme.useToken();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover
|
||||
classNames={{ root: "shadow-md" }}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
arrow={false}
|
||||
content={
|
||||
<SharedNode.Menu
|
||||
@ -29,8 +36,6 @@ const ExecuteResultNode = ({ node, disabled, branchId, branchIndex }: ConditionN
|
||||
trigger={<Button color="primary" icon={<MoreOutlinedIcon />} variant="text" />}
|
||||
/>
|
||||
}
|
||||
overlayClassName="shadow-md"
|
||||
overlayInnerStyle={{ padding: 0 }}
|
||||
placement="rightTop"
|
||||
>
|
||||
<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">
|
||||
{node.type === WorkflowNodeType.ExecuteSuccess ? (
|
||||
<>
|
||||
<CheckCircleIcon size={18} className="text-green-500" />
|
||||
<CheckCircleOutlinedIcon style={{ color: themeToken.colorSuccess }} />
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
@ -136,7 +136,7 @@ const StartNodeConfigForm = forwardRef<StartNodeConfigFormInstance, StartNodeCon
|
||||
|
||||
<Show when={fieldTrigger === WORKFLOW_TRIGGERS.AUTO}>
|
||||
<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>
|
||||
</Show>
|
||||
</Form>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { forwardRef, memo, useEffect, useImperativeHandle } from "react";
|
||||
import { forwardRef, memo, useImperativeHandle } from "react";
|
||||
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 { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
@ -35,8 +36,7 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
certificateId: z.string().optional(),
|
||||
domains: z.string().optional(),
|
||||
domains: z.string().nullish(),
|
||||
certificate: z
|
||||
.string({ message: 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(),
|
||||
});
|
||||
|
||||
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>) => {
|
||||
onValuesChange?.(values as UploadNodeConfigFormFieldValues);
|
||||
};
|
||||
@ -86,18 +77,21 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
|
||||
try {
|
||||
const resp = await validateCertificate(certificate);
|
||||
formInst.setFields([
|
||||
{
|
||||
name: "certificate",
|
||||
value: certificate,
|
||||
errors: [],
|
||||
},
|
||||
{
|
||||
name: "domains",
|
||||
value: resp.data.domains,
|
||||
},
|
||||
{
|
||||
name: "certificate",
|
||||
value: certificate,
|
||||
},
|
||||
]);
|
||||
} catch (e) {
|
||||
formInst.setFields([
|
||||
{
|
||||
name: "domains",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "certificate",
|
||||
value: "",
|
||||
@ -108,25 +102,27 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
|
||||
} else {
|
||||
formInst.setFieldValue("certificate", "");
|
||||
}
|
||||
|
||||
onValuesChange?.(formInst.getFieldsValue(true));
|
||||
};
|
||||
|
||||
const handlePrivateKeyFileChange: UploadProps["onChange"] = async ({ file }) => {
|
||||
if (file && file.status !== "removed") {
|
||||
const privateKey = await readFileContent(file.originFileObj ?? (file as unknown as File));
|
||||
|
||||
try {
|
||||
await validatePrivateKey(privateKey);
|
||||
formInst.setFields([
|
||||
{
|
||||
name: "privateKey",
|
||||
value: privateKey,
|
||||
errors: [],
|
||||
},
|
||||
]);
|
||||
} catch (e) {
|
||||
formInst.setFields([
|
||||
{
|
||||
name: "privateKey",
|
||||
value: "",
|
||||
errors: [getErrMsg(e)],
|
||||
},
|
||||
]);
|
||||
@ -141,35 +137,27 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
|
||||
return (
|
||||
<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]}>
|
||||
<Input readOnly />
|
||||
<Input placeholder={t("workflow_node.upload.form.domains.placeholder")} readOnly />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="certificate" label={t("workflow_node.upload.form.certificate.label")} rules={[formRule]}>
|
||||
<Input.TextArea
|
||||
readOnly
|
||||
autoSize={{ minRows: 5, maxRows: 10 }}
|
||||
placeholder={t("workflow_node.upload.form.certificate.placeholder")}
|
||||
value={certificate}
|
||||
/>
|
||||
<div className="mt-2 text-right">
|
||||
<Upload beforeUpload={() => false} maxCount={1} onChange={handleCertificateFileChange}>
|
||||
<Button>{t("workflow_node.upload.form.certificate.button")}</Button>
|
||||
</Upload>
|
||||
</div>
|
||||
<Input.TextArea readOnly autoSize={{ minRows: 5, maxRows: 10 }} placeholder={t("workflow_node.upload.form.certificate.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Upload beforeUpload={() => false} maxCount={1} onChange={handleCertificateFileChange}>
|
||||
<Button icon={<UploadOutlinedIcon />}>{t("workflow_node.upload.form.certificate.button")}</Button>
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="privateKey" label={t("workflow_node.upload.form.private_key.label")} rules={[formRule]}>
|
||||
<Input.TextArea
|
||||
readOnly
|
||||
autoSize={{ minRows: 5, maxRows: 10 }}
|
||||
placeholder={t("workflow_node.upload.form.private_key.placeholder")}
|
||||
value={privateKey}
|
||||
/>
|
||||
<div className="mt-2 text-right">
|
||||
<Upload beforeUpload={() => false} maxCount={1} onChange={handlePrivateKeyFileChange}>
|
||||
<Button>{t("workflow_node.upload.form.private_key.button")}</Button>
|
||||
</Upload>
|
||||
</div>
|
||||
<Input.TextArea readOnly autoSize={{ minRows: 5, maxRows: 10 }} placeholder={t("workflow_node.upload.form.private_key.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Upload beforeUpload={() => false} maxCount={1} onChange={handlePrivateKeyFileChange}>
|
||||
<Button icon={<UploadOutlinedIcon />}>{t("workflow_node.upload.form.private_key.button")}</Button>
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
|
@ -64,6 +64,16 @@ type SharedNodeMenuProps = SharedNodeProps & {
|
||||
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 { t } = useTranslation();
|
||||
|
||||
@ -91,13 +101,7 @@ const SharedNodeMenu = ({ trigger, node, disabled, branchId, branchIndex, afterU
|
||||
};
|
||||
|
||||
const handleDeleteClick = async () => {
|
||||
if (
|
||||
node.type === WorkflowNodeType.Branch ||
|
||||
node.type === WorkflowNodeType.Condition ||
|
||||
node.type === WorkflowNodeType.ExecuteResultBranch ||
|
||||
node.type === WorkflowNodeType.ExecuteSuccess ||
|
||||
node.type === WorkflowNodeType.ExecuteFailure
|
||||
) {
|
||||
if (isBranchingNode(node)) {
|
||||
await removeBranch(branchId!, branchIndex!);
|
||||
} else {
|
||||
await removeNode(node.id);
|
||||
@ -116,19 +120,13 @@ const SharedNodeMenu = ({ trigger, node, disabled, branchId, branchIndex, afterU
|
||||
{
|
||||
key: "rename",
|
||||
disabled: disabled,
|
||||
label:
|
||||
node.type === WorkflowNodeType.Branch || node.type === WorkflowNodeType.Condition
|
||||
? t("workflow_node.action.rename_branch")
|
||||
: t("workflow_node.action.rename_node"),
|
||||
label: isBranchingNode(node) ? t("workflow_node.action.rename_branch") : t("workflow_node.action.rename_node"),
|
||||
icon: <FormOutlinedIcon />,
|
||||
onClick: () => {
|
||||
nameRef.current = node.name;
|
||||
|
||||
const dialog = modalApi.confirm({
|
||||
title:
|
||||
node.type === WorkflowNodeType.Branch || node.type === WorkflowNodeType.Condition
|
||||
? t("workflow_node.action.rename_branch")
|
||||
: t("workflow_node.action.rename_node"),
|
||||
title: isBranchingNode(node) ? t("workflow_node.action.rename_branch") : t("workflow_node.action.rename_node"),
|
||||
content: (
|
||||
<div className="pb-2 pt-4">
|
||||
<Input
|
||||
@ -156,14 +154,7 @@ const SharedNodeMenu = ({ trigger, node, disabled, branchId, branchIndex, afterU
|
||||
{
|
||||
key: "remove",
|
||||
disabled: disabled || node.type === WorkflowNodeType.Start,
|
||||
label:
|
||||
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"),
|
||||
label: isBranchingNode(node) ? t("workflow_node.action.remove_branch") : t("workflow_node.action.remove_node"),
|
||||
icon: <CloseCircleOutlinedIcon />,
|
||||
danger: true,
|
||||
onClick: handleDeleteClick,
|
||||
@ -193,10 +184,10 @@ const SharedNodeBlock = ({ children, node, disabled, onClick }: SharedNodeBlockP
|
||||
return (
|
||||
<>
|
||||
<Popover
|
||||
classNames={{ root: "shadow-md" }}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
arrow={false}
|
||||
content={<SharedNodeMenu node={node} disabled={disabled} trigger={<Button color="primary" icon={<MoreOutlinedIcon />} variant="text" />} />}
|
||||
overlayClassName="shadow-md"
|
||||
overlayInnerStyle={{ padding: 0 }}
|
||||
placement="rightTop"
|
||||
>
|
||||
<Card className="relative w-[256px] overflow-hidden shadow-md" styles={{ body: { padding: 0 } }} hoverable>
|
||||
|
@ -29,30 +29,30 @@ export type WorkflowTriggerType = (typeof WORKFLOW_TRIGGERS)[keyof typeof WORKFL
|
||||
export enum WorkflowNodeType {
|
||||
Start = "start",
|
||||
End = "end",
|
||||
Branch = "branch",
|
||||
ExecuteResultBranch = "execute_result_branch",
|
||||
ExecuteSuccess = "execute_success",
|
||||
ExecuteFailure = "execute_failure",
|
||||
Condition = "condition",
|
||||
Apply = "apply",
|
||||
Upload = "upload",
|
||||
Deploy = "deploy",
|
||||
Notify = "notify",
|
||||
Branch = "branch",
|
||||
Condition = "condition",
|
||||
ExecuteResultBranch = "execute_result_branch",
|
||||
ExecuteSuccess = "execute_success",
|
||||
ExecuteFailure = "execute_failure",
|
||||
Custom = "custom",
|
||||
}
|
||||
|
||||
const workflowNodeTypeDefaultNames: Map<WorkflowNodeType, string> = new Map([
|
||||
[WorkflowNodeType.Start, i18n.t("workflow_node.start.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.Upload, i18n.t("workflow_node.upload.label")],
|
||||
[WorkflowNodeType.Deploy, i18n.t("workflow_node.deploy.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")],
|
||||
]);
|
||||
|
||||
|
@ -32,6 +32,7 @@ export const WORKFLOW_RUN_STATUSES = Object.freeze({
|
||||
RUNNING: "running",
|
||||
SUCCEEDED: "succeeded",
|
||||
FAILED: "failed",
|
||||
CANCELED: "canceled",
|
||||
} as const);
|
||||
|
||||
export type WorkflorRunStatusType = (typeof WORKFLOW_RUN_STATUSES)[keyof typeof WORKFLOW_RUN_STATUSES];
|
||||
|
@ -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.rename_node": "Rename 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.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_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\">Let’s Encrypt rate limits</a><br>2. <a href=\"https://letsencrypt.org/docs/faq/#why-should-my-let-s-encrypt-acme-client-run-at-a-random-time\" target=\"_blank\">Why should my Let’s Encrypt (ACME) client run at a random time?</a>",
|
||||
"workflow_node.start.form.trigger_cron.guide": "Tips: If you have multiple workflows, it is recommended to set them to run at multiple times of the day instead of always running at specific times.<br><br>Reference links:<br>1. <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">Let’s Encrypt rate limits</a><br>2. <a href=\"https://letsencrypt.org/docs/faq/#why-should-my-let-s-encrypt-acme-client-run-at-a-random-time\" target=\"_blank\">Why should my Let’s Encrypt (ACME) client run at a random time?</a>",
|
||||
|
||||
"workflow_node.apply.label": "Application",
|
||||
"workflow_node.apply.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.tooltip": "Used to deploy certificates.",
|
||||
"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.placeholder": "Please select certificate",
|
||||
"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.button": "Configure",
|
||||
|
||||
"workflow_node.upload.label": "Upload certificate",
|
||||
"workflow_node.upload.label": "Upload",
|
||||
"workflow_node.upload.form.domains.label": "Domains",
|
||||
"workflow_node.upload.form.certificate.label": "Certificate",
|
||||
"workflow_node.upload.form.certificate.placeholder": "The certificate format begins with \"-----BEGIN CERTIFICATE-----\" and ends with \"-----END CERTIFICATE-----\"",
|
||||
"workflow_node.upload.form.certificate.button": "Upload",
|
||||
"workflow_node.upload.form.private_key.label": "Private key",
|
||||
"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.button": "Upload",
|
||||
"workflow_node.upload.form.domains.placholder": "Please select certificate file",
|
||||
"workflow_node.upload.form.certificate.label": "Certificate (PEM format)",
|
||||
"workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
|
||||
"workflow_node.upload.form.certificate.button": "Choose file ...",
|
||||
"workflow_node.upload.form.private_key.label": "Private key (PEM format)",
|
||||
"workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
|
||||
"workflow_node.upload.form.private_key.button": "Choose file ...",
|
||||
|
||||
"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 ..."
|
||||
}
|
||||
|
||||
|
@ -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.status": "Status",
|
||||
"workflow_run.props.status.pending": "Pending",
|
||||
"workflow_run.props.status.running": "Running",
|
||||
"workflow_run.props.status.succeeded": "Succeeded",
|
||||
"workflow_run.props.status.failed": "Failed",
|
||||
"workflow_run.props.status.canceled": "Canceled",
|
||||
"workflow_run.props.trigger": "Trigger",
|
||||
"workflow_run.props.trigger.auto": "Timing",
|
||||
"workflow_run.props.trigger.manual": "Manual",
|
||||
|
@ -3,7 +3,7 @@
|
||||
"workflow_node.branch.add_node": "添加节点",
|
||||
"workflow_node.action.rename_node": "重命名",
|
||||
"workflow_node.action.remove_node": "删除节点",
|
||||
"workflow_node.action.add_branch": "添加分支",
|
||||
"workflow_node.action.add_branch": "添加并行分支",
|
||||
"workflow_node.action.rename_branch": "重命名",
|
||||
"workflow_node.action.remove_branch": "删除分支",
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
"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.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\">Let’s Encrypt 速率限制</a><br>2. <a href=\"https://letsencrypt.org/zh-cn/docs/faq/#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E7%9A%84-let-s-encrypt-acme-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%E5%BA%94%E5%BD%93%E9%9A%8F%E6%9C%BA\" target=\"_blank\">为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?</a>",
|
||||
"workflow_node.start.form.trigger_cron.guide": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。<br><br>参考链接:<br>1. <a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">Let’s Encrypt 速率限制</a><br>2. <a href=\"https://letsencrypt.org/zh-cn/docs/faq/#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E7%9A%84-let-s-encrypt-acme-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%E5%BA%94%E5%BD%93%E9%9A%8F%E6%9C%BA\" target=\"_blank\">为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?</a>",
|
||||
|
||||
"workflow_node.apply.label": "申请",
|
||||
"workflow_node.apply.form.domains.label": "域名",
|
||||
@ -37,6 +37,7 @@
|
||||
"workflow_node.apply.form.provider_access.placeholder": "请选择 DNS 提供商授权",
|
||||
"workflow_node.apply.form.provider_access.tooltip": "用于 ACME DNS-01 认证时操作域名解析记录,注意与部署阶段所需的主机提供商相区分。",
|
||||
"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.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>",
|
||||
@ -377,25 +378,25 @@
|
||||
"workflow_node.notify.form.channel.placeholder": "请选择通知渠道",
|
||||
"workflow_node.notify.form.channel.button": "去配置",
|
||||
|
||||
"workflow_node.upload.label": "上传证书",
|
||||
"workflow_node.upload.form.domains.label": "证书域名",
|
||||
"workflow_node.upload.form.certificate.label": "证书文件",
|
||||
"workflow_node.upload.form.certificate.placeholder": "证书格式以\"-----BEGIN CERTIFICATE-----\"开头,以\"-----END CERTIFICATE-----\"结尾。",
|
||||
"workflow_node.upload.form.certificate.button": "上传",
|
||||
"workflow_node.upload.form.private_key.label": "证书私钥",
|
||||
"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.upload.label": "上传",
|
||||
"workflow_node.upload.form.domains.label": "域名",
|
||||
"workflow_node.upload.form.domains.placeholder": "上传证书文件后显示",
|
||||
"workflow_node.upload.form.certificate.label": "证书文件(PEM 格式)",
|
||||
"workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
|
||||
"workflow_node.upload.form.certificate.button": "选择文件",
|
||||
"workflow_node.upload.form.private_key.label": "私钥文件(PEM 格式)",
|
||||
"workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
|
||||
"workflow_node.upload.form.private_key.button": "选择文件",
|
||||
|
||||
"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_success.label": "执行成功",
|
||||
"workflow_node.execute_success.label": "若前序节点执行成功…",
|
||||
|
||||
"workflow_node.execute_failure.label": "执行失败",
|
||||
|
||||
"workflow_node.condition.label": "条件"
|
||||
"workflow_node.execute_failure.label": "若前序节点执行失败…"
|
||||
}
|
||||
|
||||
|
@ -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.status": "状态",
|
||||
"workflow_run.props.status.pending": "等待执行",
|
||||
"workflow_run.props.status.running": "执行中",
|
||||
"workflow_run.props.status.succeeded": "成功",
|
||||
"workflow_run.props.status.failed": "失败",
|
||||
"workflow_run.props.status.succeeded": "已成功",
|
||||
"workflow_run.props.status.failed": "已失败",
|
||||
"workflow_run.props.status.canceled": "已取消",
|
||||
"workflow_run.props.trigger": "执行方式",
|
||||
"workflow_run.props.trigger.auto": "定时执行",
|
||||
"workflow_run.props.trigger.manual": "手动执行",
|
||||
|
@ -41,7 +41,7 @@ const ConsoleLayout = () => {
|
||||
}
|
||||
|
||||
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">
|
||||
<div className="flex size-full flex-col items-center justify-between overflow-hidden">
|
||||
<div className="w-full">
|
||||
@ -53,8 +53,8 @@ const ConsoleLayout = () => {
|
||||
</div>
|
||||
</Layout.Sider>
|
||||
|
||||
<Layout className="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 className="flex flex-col overflow-hidden pl-[256px] max-md:pl-0">
|
||||
<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 items-center gap-4">
|
||||
<SiderMenuDrawer trigger={<Button className="md:hidden" icon={<MenuOutlinedIcon />} size="large" />} />
|
||||
@ -76,7 +76,7 @@ const ConsoleLayout = () => {
|
||||
</div>
|
||||
</Layout.Header>
|
||||
|
||||
<Layout.Content style={{ overflow: "initial" }}>
|
||||
<Layout.Content className="flex-1 overflow-y-auto overflow-x-hidden">
|
||||
<Outlet />
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
|
@ -2,15 +2,16 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
ApiOutlined,
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
LockOutlined,
|
||||
PlusOutlined,
|
||||
SelectOutlined,
|
||||
SendOutlined,
|
||||
SyncOutlined,
|
||||
ApiOutlined as ApiOutlinedIcon,
|
||||
CheckCircleOutlined as CheckCircleOutlinedIcon,
|
||||
ClockCircleOutlined as ClockCircleOutlinedIcon,
|
||||
CloseCircleOutlined as CloseCircleOutlinedIcon,
|
||||
LockOutlined as LockOutlinedIcon,
|
||||
PauseCircleOutlined as PauseCircleOutlinedIcon,
|
||||
PlusOutlined as PlusOutlinedIcon,
|
||||
SelectOutlined as SelectOutlinedIcon,
|
||||
SendOutlined as SendOutlinedIcon,
|
||||
SyncOutlined as SyncOutlinedIcon,
|
||||
} from "@ant-design/icons";
|
||||
import { PageHeader } from "@ant-design/pro-components";
|
||||
import { useRequest } from "ahooks";
|
||||
@ -84,14 +85,22 @@ const Dashboard = () => {
|
||||
key: "name",
|
||||
title: t("workflow.props.name"),
|
||||
ellipsis: true,
|
||||
render: (_, record) => (
|
||||
<Space className="max-w-full" direction="vertical" size={4}>
|
||||
<Typography.Text ellipsis>{record.expand?.workflowId?.name}</Typography.Text>
|
||||
<Typography.Text type="secondary" ellipsis>
|
||||
{record.expand?.workflowId?.description}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
),
|
||||
render: (_, record) => {
|
||||
const workflow = record.expand?.workflowId;
|
||||
return (
|
||||
<Typography.Link
|
||||
type="secondary"
|
||||
ellipsis
|
||||
onClick={() => {
|
||||
if (workflow) {
|
||||
navigate(`/workflows/${workflow.id}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{workflow?.name ?? <span className="font-mono">{t(`#${record.workflowId}`)}</span>}
|
||||
</Typography.Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
@ -99,25 +108,31 @@ const Dashboard = () => {
|
||||
ellipsis: true,
|
||||
render: (_, record) => {
|
||||
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) {
|
||||
return (
|
||||
<Tag icon={<SyncOutlined spin />} color="processing">
|
||||
<Tag icon={<SyncOutlinedIcon spin />} color="processing">
|
||||
{t("workflow_run.props.status.running")}
|
||||
</Tag>
|
||||
);
|
||||
} else if (record.status === WORKFLOW_RUN_STATUSES.SUCCEEDED) {
|
||||
return (
|
||||
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||
<Tag icon={<CheckCircleOutlinedIcon />} color="success">
|
||||
{t("workflow_run.props.status.succeeded")}
|
||||
</Tag>
|
||||
);
|
||||
} else if (record.status === WORKFLOW_RUN_STATUSES.FAILED) {
|
||||
return (
|
||||
<Tag icon={<CloseCircleOutlined />} color="error">
|
||||
<Tag icon={<CloseCircleOutlinedIcon />} color="error">
|
||||
{t("workflow_run.props.status.failed")}
|
||||
</Tag>
|
||||
);
|
||||
} else if (record.status === WORKFLOW_RUN_STATUSES.CANCELED) {
|
||||
return (
|
||||
<Tag icon={<PauseCircleOutlinedIcon />} color="warning">
|
||||
{t("workflow_run.props.status.canceled")}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
@ -153,7 +168,7 @@ const Dashboard = () => {
|
||||
width: 120,
|
||||
render: (_, record) => (
|
||||
<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>
|
||||
),
|
||||
},
|
||||
@ -248,16 +263,16 @@ const Dashboard = () => {
|
||||
<Flex justify="stretch" vertical={!breakpoints.lg} gap={16}>
|
||||
<Card className="max-lg:flex-1 lg:w-[360px]" title={t("dashboard.quick_actions")}>
|
||||
<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")}
|
||||
</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")}
|
||||
</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")}
|
||||
</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")}
|
||||
</Button>
|
||||
</Space>
|
||||
|
@ -8,9 +8,6 @@ import {
|
||||
DownOutlined as DownOutlinedIcon,
|
||||
EllipsisOutlined as EllipsisOutlinedIcon,
|
||||
HistoryOutlined as HistoryOutlinedIcon,
|
||||
MinusOutlined,
|
||||
PlusCircleOutlined,
|
||||
ReloadOutlined,
|
||||
UndoOutlined as UndoOutlinedIcon,
|
||||
} from "@ant-design/icons";
|
||||
import { PageHeader } from "@ant-design/pro-components";
|
||||
@ -19,10 +16,10 @@ import { createSchemaFieldRule } from "antd-zod";
|
||||
import { isEqual } from "radash";
|
||||
import { z } from "zod";
|
||||
|
||||
import { run as runWorkflow } from "@/api/workflows";
|
||||
import { startRun as startWorkflowRun } from "@/api/workflows";
|
||||
import ModalForm from "@/components/ModalForm";
|
||||
import Show from "@/components/Show";
|
||||
import WorkflowElements from "@/components/workflow/WorkflowElements";
|
||||
import WorkflowElementsContainer from "@/components/workflow/WorkflowElementsContainer";
|
||||
import WorkflowRuns from "@/components/workflow/WorkflowRuns";
|
||||
import { isAllNodesValidated } from "@/domain/workflow";
|
||||
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
|
||||
@ -40,8 +37,6 @@ const WorkflowDetail = () => {
|
||||
const [modalApi, ModalContextHolder] = Modal.useModal();
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
|
||||
const [scale, setScale] = useState(1);
|
||||
|
||||
const { id: workflowId } = useParams();
|
||||
const { workflow, initialized, ...workflowState } = useWorkflowStore(
|
||||
useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setEnabled", "release", "discard"])
|
||||
@ -58,15 +53,12 @@ const WorkflowDetail = () => {
|
||||
const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration");
|
||||
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const lastRunStatus = useMemo(() => workflow.lastRunStatus, [workflow]);
|
||||
|
||||
const [allowDiscard, setAllowDiscard] = useState(false);
|
||||
const [allowRelease, setAllowRelease] = useState(false);
|
||||
const [allowRun, setAllowRun] = useState(false);
|
||||
|
||||
const lastRunStatus = useMemo(() => {
|
||||
return workflow.lastRunStatus;
|
||||
}, [workflow]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsRunning(lastRunStatus == WORKFLOW_RUN_STATUSES.RUNNING);
|
||||
}, [lastRunStatus]);
|
||||
@ -192,7 +184,7 @@ const WorkflowDetail = () => {
|
||||
}
|
||||
});
|
||||
|
||||
await runWorkflow(workflowId!);
|
||||
await startWorkflowRun(workflowId!);
|
||||
|
||||
messageApi.info(t("workflow.detail.orchestration.action.run.prompt"));
|
||||
} catch (err) {
|
||||
@ -206,123 +198,129 @@ const WorkflowDetail = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex size-full flex-col">
|
||||
{MessageContextHolder}
|
||||
{ModalContextHolder}
|
||||
{NotificationContextHolder}
|
||||
|
||||
<Card styles={{ body: { padding: "0.5rem", paddingBottom: 0 } }}>
|
||||
<PageHeader
|
||||
style={{ paddingBottom: 0 }}
|
||||
title={workflow.name}
|
||||
extra={
|
||||
initialized
|
||||
? [
|
||||
<WorkflowBaseInfoModal key="edit" trigger={<Button>{t("common.button.edit")}</Button>} />,
|
||||
<div>
|
||||
<Card styles={{ body: { padding: "0.5rem", paddingBottom: 0 } }} style={{ borderRadius: 0 }}>
|
||||
<PageHeader
|
||||
style={{ paddingBottom: 0 }}
|
||||
title={workflow.name}
|
||||
extra={
|
||||
initialized
|
||||
? [
|
||||
<WorkflowBaseInfoModal key="edit" trigger={<Button>{t("common.button.edit")}</Button>} />,
|
||||
|
||||
<Button key="enable" onClick={handleEnableChange}>
|
||||
{workflow.enabled ? t("workflow.action.disable") : t("workflow.action.enable")}
|
||||
</Button>,
|
||||
<Button key="enable" onClick={handleEnableChange}>
|
||||
{workflow.enabled ? t("workflow.action.disable") : t("workflow.action.enable")}
|
||||
</Button>,
|
||||
|
||||
<Dropdown
|
||||
key="more"
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "delete",
|
||||
label: t("workflow.action.delete"),
|
||||
danger: true,
|
||||
icon: <DeleteOutlinedIcon />,
|
||||
onClick: () => {
|
||||
handleDeleteClick();
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button icon={<DownOutlinedIcon />} iconPosition="end">
|
||||
{t("common.button.more")}
|
||||
</Button>
|
||||
</Dropdown>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
>
|
||||
<Typography.Paragraph type="secondary">{workflow.description}</Typography.Paragraph>
|
||||
<Tabs
|
||||
activeKey={tabValue}
|
||||
defaultActiveKey="orchestration"
|
||||
items={[
|
||||
{ key: "orchestration", label: t("workflow.detail.orchestration.tab"), icon: <ApartmentOutlinedIcon /> },
|
||||
{ key: "runs", label: t("workflow.detail.runs.tab"), icon: <HistoryOutlinedIcon /> },
|
||||
]}
|
||||
renderTabBar={(props, DefaultTabBar) => <DefaultTabBar {...props} style={{ margin: 0 }} />}
|
||||
tabBarStyle={{ border: "none" }}
|
||||
onChange={(key) => setTabValue(key as typeof tabValue)}
|
||||
/>
|
||||
</PageHeader>
|
||||
</Card>
|
||||
|
||||
<div className="p-4">
|
||||
<Card loading={!initialized}>
|
||||
<Show when={tabValue === "orchestration"}>
|
||||
<div className="relative">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Show when={workflow.hasDraft!}>
|
||||
<Alert banner message={<div className="truncate">{t("workflow.detail.orchestration.draft.alert")}</div>} type="warning" />
|
||||
</Show>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Space>
|
||||
<Button disabled={!allowRun} icon={<CaretRightOutlinedIcon />} loading={isRunning} type="primary" onClick={handleRunClick}>
|
||||
{t("workflow.detail.orchestration.action.run")}
|
||||
</Button>
|
||||
|
||||
<Button.Group>
|
||||
<Button color="primary" disabled={!allowRelease} variant="outlined" onClick={handleReleaseClick}>
|
||||
{t("workflow.detail.orchestration.action.release")}
|
||||
</Button>
|
||||
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "discard",
|
||||
disabled: !allowDiscard,
|
||||
label: t("workflow.detail.orchestration.action.discard"),
|
||||
icon: <UndoOutlinedIcon />,
|
||||
onClick: handleDiscardClick,
|
||||
<Dropdown
|
||||
key="more"
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "delete",
|
||||
label: t("workflow.action.delete"),
|
||||
danger: true,
|
||||
icon: <DeleteOutlinedIcon />,
|
||||
onClick: () => {
|
||||
handleDeleteClick();
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button color="primary" disabled={!allowDiscard} icon={<EllipsisOutlinedIcon />} variant="outlined" />
|
||||
</Dropdown>
|
||||
</Button.Group>
|
||||
</Space>
|
||||
</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})` }}>
|
||||
<WorkflowElements />
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={tabValue === "runs"}>
|
||||
<WorkflowRuns workflowId={workflowId!} />
|
||||
</Show>
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button icon={<DownOutlinedIcon />} iconPosition="end">
|
||||
{t("common.button.more")}
|
||||
</Button>
|
||||
</Dropdown>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
>
|
||||
<Typography.Paragraph type="secondary">{workflow.description}</Typography.Paragraph>
|
||||
<Tabs
|
||||
activeKey={tabValue}
|
||||
defaultActiveKey="orchestration"
|
||||
items={[
|
||||
{ key: "orchestration", label: t("workflow.detail.orchestration.tab"), icon: <ApartmentOutlinedIcon /> },
|
||||
{ key: "runs", label: t("workflow.detail.runs.tab"), icon: <HistoryOutlinedIcon /> },
|
||||
]}
|
||||
renderTabBar={(props, DefaultTabBar) => <DefaultTabBar {...props} style={{ margin: 0 }} />}
|
||||
tabBarStyle={{ border: "none" }}
|
||||
onChange={(key) => setTabValue(key as typeof tabValue)}
|
||||
/>
|
||||
</PageHeader>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Show when={tabValue === "orchestration"}>
|
||||
<div className="min-h-[360px] flex-1 overflow-hidden p-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">
|
||||
<Show when={workflow.hasDraft!}>
|
||||
<Alert banner message={<div className="truncate">{t("workflow.detail.orchestration.draft.alert")}</div>} type="warning" />
|
||||
</Show>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Space>
|
||||
<Button disabled={!allowRun} icon={<CaretRightOutlinedIcon />} loading={isRunning} type="primary" onClick={handleRunClick}>
|
||||
{t("workflow.detail.orchestration.action.run")}
|
||||
</Button>
|
||||
|
||||
<Button.Group>
|
||||
<Button color="primary" disabled={!allowRelease} variant="outlined" onClick={handleReleaseClick}>
|
||||
{t("workflow.detail.orchestration.action.release")}
|
||||
</Button>
|
||||
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "discard",
|
||||
disabled: !allowDiscard,
|
||||
label: t("workflow.detail.orchestration.action.discard"),
|
||||
icon: <UndoOutlinedIcon />,
|
||||
onClick: handleDiscardClick,
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button color="primary" disabled={!allowDiscard} icon={<EllipsisOutlinedIcon />} variant="outlined" />
|
||||
</Dropdown>
|
||||
</Button.Group>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WorkflowElementsContainer className="pt-16" />
|
||||
</Card>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={tabValue === "runs"}>
|
||||
<div className="p-4">
|
||||
<Card loading={!initialized}>
|
||||
<WorkflowRuns workflowId={workflowId!} />
|
||||
</Card>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -3,9 +3,11 @@ import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
CheckCircleOutlined as CheckCircleOutlinedIcon,
|
||||
ClockCircleOutlined as ClockCircleOutlinedIcon,
|
||||
CloseCircleOutlined as CloseCircleOutlinedIcon,
|
||||
DeleteOutlined as DeleteOutlinedIcon,
|
||||
EditOutlined as EditOutlinedIcon,
|
||||
PauseCircleOutlined as PauseCircleOutlinedIcon,
|
||||
PlusOutlined as PlusOutlinedIcon,
|
||||
SyncOutlined as SyncOutlinedIcon,
|
||||
} from "@ant-design/icons";
|
||||
@ -13,7 +15,6 @@ import {
|
||||
import { PageHeader } from "@ant-design/pro-components";
|
||||
import { useRequest } from "ahooks";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Divider,
|
||||
Empty,
|
||||
@ -159,32 +160,25 @@ const WorkflowList = () => {
|
||||
key: "lastRun",
|
||||
title: t("workflow.props.last_run_at"),
|
||||
render: (_, record) => {
|
||||
if (record.lastRunId) {
|
||||
if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.RUNNING) {
|
||||
return (
|
||||
<Space>
|
||||
<Badge status="processing" count={<SyncOutlinedIcon style={{ color: themeToken.colorInfo }} />} />
|
||||
<Typography.Text>{dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss")}</Typography.Text>
|
||||
</Space>
|
||||
);
|
||||
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.SUCCEEDED) {
|
||||
return (
|
||||
<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) {
|
||||
return (
|
||||
<Space>
|
||||
<Badge status="error" count={<CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} />} />
|
||||
<Typography.Text>{dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss")}</Typography.Text>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
let icon = <></>;
|
||||
if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.PENDING) {
|
||||
icon = <ClockCircleOutlinedIcon style={{ color: themeToken.colorTextSecondary }} />;
|
||||
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.RUNNING) {
|
||||
icon = <SyncOutlinedIcon style={{ color: themeToken.colorInfo }} spin />;
|
||||
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.SUCCEEDED) {
|
||||
icon = <CheckCircleOutlinedIcon style={{ color: themeToken.colorSuccess }} />;
|
||||
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.FAILED) {
|
||||
icon = <CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} />;
|
||||
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.CANCELED) {
|
||||
icon = <PauseCircleOutlinedIcon style={{ color: themeToken.colorWarning }} />;
|
||||
}
|
||||
|
||||
return <></>;
|
||||
return (
|
||||
<Space>
|
||||
{icon}
|
||||
<Typography.Text>{record.lastRunTime ? dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss") : ""}</Typography.Text>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -109,7 +109,7 @@ const WorkflowNew = () => {
|
||||
<div>
|
||||
{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")}>
|
||||
<Typography.Paragraph type="secondary">{t("workflow.new.subtitle")}</Typography.Paragraph>
|
||||
</PageHeader>
|
||||
|
@ -14,7 +14,7 @@ export type ListWorkflowRunsRequest = {
|
||||
export const list = async (request: ListWorkflowRunsRequest) => {
|
||||
const page = request.page || 1;
|
||||
const perPage = request.perPage || 10;
|
||||
console.log("request.workflowId", request.workflowId);
|
||||
|
||||
let filter = "";
|
||||
const params: Record<string, string> = {};
|
||||
if (request.workflowId) {
|
||||
@ -31,3 +31,7 @@ export const list = async (request: ListWorkflowRunsRequest) => {
|
||||
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
6
ui/src/utils/css.ts
Normal 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));
|
||||
};
|
@ -1,5 +1,9 @@
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
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;
|
||||
} else if (typeof error === "object" && error != null) {
|
||||
if ("message" in error) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user