mirror of
https://github.com/woodchen-ink/certimate.git
synced 2025-07-18 17:31:55 +08:00
workflow data save
This commit is contained in:
commit
35c0ed2ba5
2
go.mod
2
go.mod
@ -113,7 +113,7 @@ require (
|
|||||||
github.com/cloudflare/cloudflare-go v0.104.0 // indirect
|
github.com/cloudflare/cloudflare-go v0.104.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/disintegration/imaging v1.6.2 // indirect
|
github.com/disintegration/imaging v1.6.2 // indirect
|
||||||
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
|
github.com/domodwyer/mailyak/v3 v3.6.2
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/fatih/color v1.17.0 // indirect
|
github.com/fatih/color v1.17.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
|
||||||
|
@ -3,6 +3,8 @@ package domain
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/usual2970/certimate/internal/pkg/utils/maps"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ApplyConfig struct {
|
type ApplyConfig struct {
|
||||||
@ -29,7 +31,7 @@ type DeployConfig struct {
|
|||||||
// 出参:
|
// 出参:
|
||||||
// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回空字符串。
|
// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回空字符串。
|
||||||
func (dc *DeployConfig) GetConfigAsString(key string) string {
|
func (dc *DeployConfig) GetConfigAsString(key string) string {
|
||||||
return dc.GetConfigOrDefaultAsString(key, "")
|
return maps.GetValueAsString(dc.Config, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 以字符串形式获取配置项。
|
// 以字符串形式获取配置项。
|
||||||
@ -41,17 +43,7 @@ func (dc *DeployConfig) GetConfigAsString(key string) string {
|
|||||||
// 出参:
|
// 出参:
|
||||||
// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回默认值。
|
// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回默认值。
|
||||||
func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue string) string {
|
func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue string) string {
|
||||||
if dc.Config == nil {
|
return maps.GetValueOrDefaultAsString(dc.Config, key, defaultValue)
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
if value, ok := dc.Config[key]; ok {
|
|
||||||
if result, ok := value.(string); ok {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultValue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 以 32 位整数形式获取配置项。
|
// 以 32 位整数形式获取配置项。
|
||||||
@ -62,7 +54,7 @@ func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue stri
|
|||||||
// 出参:
|
// 出参:
|
||||||
// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回 0。
|
// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回 0。
|
||||||
func (dc *DeployConfig) GetConfigAsInt32(key string) int32 {
|
func (dc *DeployConfig) GetConfigAsInt32(key string) int32 {
|
||||||
return dc.GetConfigOrDefaultAsInt32(key, 0)
|
return maps.GetValueAsInt32(dc.Config, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 以 32 位整数形式获取配置项。
|
// 以 32 位整数形式获取配置项。
|
||||||
@ -74,17 +66,7 @@ func (dc *DeployConfig) GetConfigAsInt32(key string) int32 {
|
|||||||
// 出参:
|
// 出参:
|
||||||
// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回默认值。
|
// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回默认值。
|
||||||
func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32) int32 {
|
func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32) int32 {
|
||||||
if dc.Config == nil {
|
return maps.GetValueOrDefaultAsInt32(dc.Config, key, defaultValue)
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
if value, ok := dc.Config[key]; ok {
|
|
||||||
if result, ok := value.(int32); ok {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultValue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 以布尔形式获取配置项。
|
// 以布尔形式获取配置项。
|
||||||
@ -95,7 +77,7 @@ func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32
|
|||||||
// 出参:
|
// 出参:
|
||||||
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回 false。
|
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回 false。
|
||||||
func (dc *DeployConfig) GetConfigAsBool(key string) bool {
|
func (dc *DeployConfig) GetConfigAsBool(key string) bool {
|
||||||
return dc.GetConfigOrDefaultAsBool(key, false)
|
return maps.GetValueAsBool(dc.Config, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 以布尔形式获取配置项。
|
// 以布尔形式获取配置项。
|
||||||
@ -107,17 +89,7 @@ func (dc *DeployConfig) GetConfigAsBool(key string) bool {
|
|||||||
// 出参:
|
// 出参:
|
||||||
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回默认值。
|
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回默认值。
|
||||||
func (dc *DeployConfig) GetConfigOrDefaultAsBool(key string, defaultValue bool) bool {
|
func (dc *DeployConfig) GetConfigOrDefaultAsBool(key string, defaultValue bool) bool {
|
||||||
if dc.Config == nil {
|
return maps.GetValueOrDefaultAsBool(dc.Config, key, defaultValue)
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
if value, ok := dc.Config[key]; ok {
|
|
||||||
if result, ok := value.(bool); ok {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultValue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 以变量字典形式获取配置项。
|
// 以变量字典形式获取配置项。
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
package domain
|
package domain
|
||||||
|
|
||||||
const (
|
const (
|
||||||
NotifyChannelDingtalk = "dingtalk"
|
NotifyChannelEmail = "email"
|
||||||
NotifyChannelWebhook = "webhook"
|
NotifyChannelWebhook = "webhook"
|
||||||
NotifyChannelTelegram = "telegram"
|
NotifyChannelDingtalk = "dingtalk"
|
||||||
NotifyChannelLark = "lark"
|
NotifyChannelLark = "lark"
|
||||||
|
NotifyChannelTelegram = "telegram"
|
||||||
NotifyChannelServerChan = "serverchan"
|
NotifyChannelServerChan = "serverchan"
|
||||||
NotifyChannelMail = "mail"
|
|
||||||
NotifyChannelBark = "bark"
|
NotifyChannelBark = "bark"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ func (s *Setting) GetChannelContent(channel string) (map[string]any, error) {
|
|||||||
|
|
||||||
v, ok := (*conf)[channel]
|
v, ok := (*conf)[channel]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("channel %s not found", channel)
|
return nil, fmt.Errorf("channel \"%s\" not found", channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
return v, nil
|
return v, nil
|
||||||
|
@ -12,19 +12,13 @@ import (
|
|||||||
"github.com/usual2970/certimate/internal/utils/xtime"
|
"github.com/usual2970/certimate/internal/utils/xtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
type msg struct {
|
|
||||||
subject string
|
|
||||||
message string
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultExpireSubject = "您有{COUNT}张证书即将过期"
|
defaultExpireSubject = "您有 {COUNT} 张证书即将过期"
|
||||||
defaultExpireMsg = "有{COUNT}张证书即将过期,域名分别为{DOMAINS},请保持关注!"
|
defaultExpireMessage = "有 {COUNT} 张证书即将过期,域名分别为 {DOMAINS},请保持关注!"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PushExpireMsg() {
|
func PushExpireMsg() {
|
||||||
// 查询即将过期的证书
|
// 查询即将过期的证书
|
||||||
|
|
||||||
records, err := app.GetApp().Dao().FindRecordsByFilter("domains", "expiredAt<{:time}&&certUrl!=''", "-created", 500, 0,
|
records, err := app.GetApp().Dao().FindRecordsByFilter("domains", "expiredAt<{:time}&&certUrl!=''", "-created", 500, 0,
|
||||||
dbx.Params{"time": xtime.GetTimeAfter(24 * time.Hour * 15)})
|
dbx.Params{"time": xtime.GetTimeAfter(24 * time.Hour * 15)})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -34,12 +28,12 @@ func PushExpireMsg() {
|
|||||||
|
|
||||||
// 组装消息
|
// 组装消息
|
||||||
msg := buildMsg(records)
|
msg := buildMsg(records)
|
||||||
|
|
||||||
if msg == nil {
|
if msg == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := Send(msg.subject, msg.message); err != nil {
|
// 发送通知
|
||||||
|
if err := SendToAllChannels(msg.Subject, msg.Message); err != nil {
|
||||||
app.GetApp().Logger().Error("send expire msg", "error", err)
|
app.GetApp().Logger().Error("send expire msg", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,22 +47,27 @@ type notifyTemplate struct {
|
|||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildMsg(records []*models.Record) *msg {
|
type notifyMessage struct {
|
||||||
|
Subject string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMsg(records []*models.Record) *notifyMessage {
|
||||||
if len(records) == 0 {
|
if len(records) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询模板信息
|
// 查询模板信息
|
||||||
templateRecord, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='templates'")
|
templateRecord, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='templates'")
|
||||||
title := defaultExpireSubject
|
subject := defaultExpireSubject
|
||||||
content := defaultExpireMsg
|
message := defaultExpireMessage
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
var templates *notifyTemplates
|
var templates *notifyTemplates
|
||||||
templateRecord.UnmarshalJSONField("content", templates)
|
templateRecord.UnmarshalJSONField("content", templates)
|
||||||
if templates != nil && len(templates.NotifyTemplates) > 0 {
|
if templates != nil && len(templates.NotifyTemplates) > 0 {
|
||||||
title = templates.NotifyTemplates[0].Title
|
subject = templates.NotifyTemplates[0].Title
|
||||||
content = templates.NotifyTemplates[0].Content
|
message = templates.NotifyTemplates[0].Content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,17 +80,17 @@ func buildMsg(records []*models.Record) *msg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
countStr := strconv.Itoa(count)
|
countStr := strconv.Itoa(count)
|
||||||
domainStr := strings.Join(domains, ",")
|
domainStr := strings.Join(domains, ";")
|
||||||
|
|
||||||
title = strings.ReplaceAll(title, "{COUNT}", countStr)
|
subject = strings.ReplaceAll(subject, "{COUNT}", countStr)
|
||||||
title = strings.ReplaceAll(title, "{DOMAINS}", domainStr)
|
subject = strings.ReplaceAll(subject, "{DOMAINS}", domainStr)
|
||||||
|
|
||||||
content = strings.ReplaceAll(content, "{COUNT}", countStr)
|
message = strings.ReplaceAll(message, "{COUNT}", countStr)
|
||||||
content = strings.ReplaceAll(content, "{DOMAINS}", domainStr)
|
message = strings.ReplaceAll(message, "{DOMAINS}", domainStr)
|
||||||
|
|
||||||
// 返回消息
|
// 返回消息
|
||||||
return &msg{
|
return ¬ifyMessage{
|
||||||
subject: title,
|
Subject: subject,
|
||||||
message: content,
|
Message: message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
66
internal/notify/factory.go
Normal file
66
internal/notify/factory.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package notify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/usual2970/certimate/internal/domain"
|
||||||
|
"github.com/usual2970/certimate/internal/pkg/core/notifier"
|
||||||
|
notifierBark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark"
|
||||||
|
notifierDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
|
||||||
|
notifierEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
|
||||||
|
notifierLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
|
||||||
|
notifierServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
|
||||||
|
notifierTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
|
||||||
|
notifierWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
|
||||||
|
"github.com/usual2970/certimate/internal/pkg/utils/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createNotifier(channel string, channelConfig map[string]any) (notifier.Notifier, error) {
|
||||||
|
switch channel {
|
||||||
|
case domain.NotifyChannelEmail:
|
||||||
|
return notifierEmail.New(¬ifierEmail.EmailNotifierConfig{
|
||||||
|
SmtpHost: maps.GetValueAsString(channelConfig, "smtpHost"),
|
||||||
|
SmtpPort: maps.GetValueAsInt32(channelConfig, "smtpPort"),
|
||||||
|
SmtpTLS: maps.GetValueOrDefaultAsBool(channelConfig, "smtpTLS", true),
|
||||||
|
Username: maps.GetValueOrDefaultAsString(channelConfig, "username", maps.GetValueAsString(channelConfig, "senderAddress")),
|
||||||
|
Password: maps.GetValueAsString(channelConfig, "password"),
|
||||||
|
SenderAddress: maps.GetValueAsString(channelConfig, "senderAddress"),
|
||||||
|
ReceiverAddress: maps.GetValueAsString(channelConfig, "receiverAddress"),
|
||||||
|
})
|
||||||
|
|
||||||
|
case domain.NotifyChannelWebhook:
|
||||||
|
return notifierWebhook.New(¬ifierWebhook.WebhookNotifierConfig{
|
||||||
|
Url: maps.GetValueAsString(channelConfig, "url"),
|
||||||
|
})
|
||||||
|
|
||||||
|
case domain.NotifyChannelDingtalk:
|
||||||
|
return notifierDingTalk.New(¬ifierDingTalk.DingTalkNotifierConfig{
|
||||||
|
AccessToken: maps.GetValueAsString(channelConfig, "accessToken"),
|
||||||
|
Secret: maps.GetValueAsString(channelConfig, "secret"),
|
||||||
|
})
|
||||||
|
|
||||||
|
case domain.NotifyChannelLark:
|
||||||
|
return notifierLark.New(¬ifierLark.LarkNotifierConfig{
|
||||||
|
WebhookUrl: maps.GetValueAsString(channelConfig, "webhookUrl"),
|
||||||
|
})
|
||||||
|
|
||||||
|
case domain.NotifyChannelTelegram:
|
||||||
|
return notifierTelegram.New(¬ifierTelegram.TelegramNotifierConfig{
|
||||||
|
ApiToken: maps.GetValueAsString(channelConfig, "apiToken"),
|
||||||
|
ChatId: maps.GetValueAsInt64(channelConfig, "chatId"),
|
||||||
|
})
|
||||||
|
|
||||||
|
case domain.NotifyChannelServerChan:
|
||||||
|
return notifierServerChan.New(¬ifierServerChan.ServerChanNotifierConfig{
|
||||||
|
Url: maps.GetValueAsString(channelConfig, "url"),
|
||||||
|
})
|
||||||
|
|
||||||
|
case domain.NotifyChannelBark:
|
||||||
|
return notifierBark.New(¬ifierBark.BarkNotifierConfig{
|
||||||
|
DeviceKey: maps.GetValueAsString(channelConfig, "deviceKey"),
|
||||||
|
ServerUrl: maps.GetValueAsString(channelConfig, "serverUrl"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("unsupported notifier channel")
|
||||||
|
}
|
@ -1,56 +0,0 @@
|
|||||||
package notify
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/mail"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase/tools/mailer"
|
|
||||||
)
|
|
||||||
|
|
||||||
const defaultSmtpHostPort = "25"
|
|
||||||
|
|
||||||
type Mail struct {
|
|
||||||
username string
|
|
||||||
to string
|
|
||||||
client *mailer.SmtpClient
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMail(senderAddress, receiverAddresses, smtpHostAddr, smtpHostPort, password string) (*Mail, error) {
|
|
||||||
if smtpHostPort == "" {
|
|
||||||
smtpHostPort = defaultSmtpHostPort
|
|
||||||
}
|
|
||||||
|
|
||||||
port, err := strconv.Atoi(smtpHostPort)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid smtp port: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
client := mailer.SmtpClient{
|
|
||||||
Host: smtpHostAddr,
|
|
||||||
Port: port,
|
|
||||||
Username: senderAddress,
|
|
||||||
Password: password,
|
|
||||||
Tls: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Mail{
|
|
||||||
username: senderAddress,
|
|
||||||
client: &client,
|
|
||||||
to: receiverAddresses,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Mail) Send(ctx context.Context, subject, content string) error {
|
|
||||||
message := &mailer.Message{
|
|
||||||
From: mail.Address{
|
|
||||||
Address: m.username,
|
|
||||||
},
|
|
||||||
To: []mail.Address{{Address: m.to}},
|
|
||||||
Subject: subject,
|
|
||||||
Text: content,
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.client.Send(message)
|
|
||||||
}
|
|
@ -3,24 +3,16 @@ package notify
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
stdhttp "net/http"
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
"github.com/usual2970/certimate/internal/domain"
|
"github.com/usual2970/certimate/internal/pkg/core/notifier"
|
||||||
|
"github.com/usual2970/certimate/internal/pkg/utils/maps"
|
||||||
"github.com/usual2970/certimate/internal/utils/app"
|
"github.com/usual2970/certimate/internal/utils/app"
|
||||||
|
|
||||||
notifyPackage "github.com/nikoksr/notify"
|
|
||||||
"github.com/nikoksr/notify/service/bark"
|
|
||||||
"github.com/nikoksr/notify/service/dingding"
|
|
||||||
"github.com/nikoksr/notify/service/http"
|
|
||||||
"github.com/nikoksr/notify/service/lark"
|
|
||||||
"github.com/nikoksr/notify/service/telegram"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Send(title, content string) error {
|
func SendToAllChannels(subject, message string) error {
|
||||||
// 获取所有的推送渠道
|
notifiers, err := getEnabledNotifiers()
|
||||||
notifiers, err := getNotifiers()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -28,184 +20,56 @@ func Send(title, content string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
n := notifyPackage.New()
|
var eg errgroup.Group
|
||||||
// 添加推送渠道
|
for _, n := range notifiers {
|
||||||
n.UseServices(notifiers...)
|
if n == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// 发送消息
|
eg.Go(func() error {
|
||||||
return n.Send(context.Background(), title, content)
|
_, err := n.Notify(context.Background(), subject, message)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = eg.Wait()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
type sendTestParam struct {
|
func SendToChannel(subject, message string, channel string, channelConfig map[string]any) error {
|
||||||
Title string `json:"title"`
|
notifier, err := createNotifier(channel, channelConfig)
|
||||||
Content string `json:"content"`
|
|
||||||
Channel string `json:"channel"`
|
|
||||||
Conf map[string]any `json:"conf"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func SendTest(param *sendTestParam) error {
|
|
||||||
notifier, err := getNotifier(param.Channel, param.Conf)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
n := notifyPackage.New()
|
_, err = notifier.Notify(context.Background(), subject, message)
|
||||||
|
return err
|
||||||
// 添加推送渠道
|
|
||||||
n.UseServices(notifier)
|
|
||||||
|
|
||||||
// 发送消息
|
|
||||||
return n.Send(context.Background(), param.Title, param.Content)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getNotifiers() ([]notifyPackage.Notifier, error) {
|
func getEnabledNotifiers() ([]notifier.Notifier, error) {
|
||||||
resp, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='notifyChannels'")
|
settings, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='notifyChannels'")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("find notifyChannels error: %w", err)
|
return nil, fmt.Errorf("find notifyChannels error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
notifiers := make([]notifyPackage.Notifier, 0)
|
|
||||||
|
|
||||||
rs := make(map[string]map[string]any)
|
rs := make(map[string]map[string]any)
|
||||||
|
if err := settings.UnmarshalJSONField("content", &rs); err != nil {
|
||||||
if err := resp.UnmarshalJSONField("content", &rs); err != nil {
|
|
||||||
return nil, fmt.Errorf("unmarshal notifyChannels error: %w", err)
|
return nil, fmt.Errorf("unmarshal notifyChannels error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifiers := make([]notifier.Notifier, 0)
|
||||||
for k, v := range rs {
|
for k, v := range rs {
|
||||||
|
if !maps.GetValueAsBool(v, "enabled") {
|
||||||
if !getBool(v, "enabled") {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
notifier, err := getNotifier(k, v)
|
notifier, err := createNotifier(k, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
notifiers = append(notifiers, notifier)
|
notifiers = append(notifiers, notifier)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return notifiers, nil
|
return notifiers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getNotifier(channel string, conf map[string]any) (notifyPackage.Notifier, error) {
|
|
||||||
switch channel {
|
|
||||||
case domain.NotifyChannelTelegram:
|
|
||||||
temp := getTelegramNotifier(conf)
|
|
||||||
if temp == nil {
|
|
||||||
return nil, fmt.Errorf("telegram notifier config error")
|
|
||||||
}
|
|
||||||
|
|
||||||
return temp, nil
|
|
||||||
case domain.NotifyChannelDingtalk:
|
|
||||||
return getDingTalkNotifier(conf), nil
|
|
||||||
case domain.NotifyChannelLark:
|
|
||||||
return getLarkNotifier(conf), nil
|
|
||||||
case domain.NotifyChannelWebhook:
|
|
||||||
return getWebhookNotifier(conf), nil
|
|
||||||
case domain.NotifyChannelServerChan:
|
|
||||||
return getServerChanNotifier(conf), nil
|
|
||||||
case domain.NotifyChannelMail:
|
|
||||||
return getMailNotifier(conf)
|
|
||||||
case domain.NotifyChannelBark:
|
|
||||||
return getBarkNotifier(conf), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("notifier not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getWebhookNotifier(conf map[string]any) notifyPackage.Notifier {
|
|
||||||
rs := http.New()
|
|
||||||
|
|
||||||
rs.AddReceiversURLs(getString(conf, "url"))
|
|
||||||
|
|
||||||
return rs
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTelegramNotifier(conf map[string]any) notifyPackage.Notifier {
|
|
||||||
rs, err := telegram.New(getString(conf, "apiToken"))
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
chatId := getString(conf, "chatId")
|
|
||||||
|
|
||||||
id, err := strconv.ParseInt(chatId, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
rs.AddReceivers(id)
|
|
||||||
return rs
|
|
||||||
}
|
|
||||||
|
|
||||||
func getServerChanNotifier(conf map[string]any) notifyPackage.Notifier {
|
|
||||||
rs := http.New()
|
|
||||||
|
|
||||||
rs.AddReceivers(&http.Webhook{
|
|
||||||
URL: getString(conf, "url"),
|
|
||||||
Header: stdhttp.Header{},
|
|
||||||
ContentType: "application/json",
|
|
||||||
Method: stdhttp.MethodPost,
|
|
||||||
BuildPayload: func(subject, message string) (payload any) {
|
|
||||||
return map[string]string{
|
|
||||||
"text": subject,
|
|
||||||
"desp": message,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return rs
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBarkNotifier(conf map[string]any) notifyPackage.Notifier {
|
|
||||||
deviceKey := getString(conf, "deviceKey")
|
|
||||||
serverURL := getString(conf, "serverUrl")
|
|
||||||
if serverURL == "" {
|
|
||||||
return bark.New(deviceKey)
|
|
||||||
}
|
|
||||||
return bark.NewWithServers(deviceKey, serverURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDingTalkNotifier(conf map[string]any) notifyPackage.Notifier {
|
|
||||||
return dingding.New(&dingding.Config{
|
|
||||||
Token: getString(conf, "accessToken"),
|
|
||||||
Secret: getString(conf, "secret"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLarkNotifier(conf map[string]any) notifyPackage.Notifier {
|
|
||||||
return lark.NewWebhookService(getString(conf, "webhookUrl"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMailNotifier(conf map[string]any) (notifyPackage.Notifier, error) {
|
|
||||||
rs, err := NewMail(getString(conf, "senderAddress"),
|
|
||||||
getString(conf, "receiverAddresses"),
|
|
||||||
getString(conf, "smtpHostAddr"),
|
|
||||||
getString(conf, "smtpHostPort"),
|
|
||||||
getString(conf, "password"),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return rs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getString(conf map[string]any, key string) string {
|
|
||||||
if _, ok := conf[key]; !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return conf[key].(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBool(conf map[string]any, key string) bool {
|
|
||||||
if _, ok := conf[key]; !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return conf[key].(bool)
|
|
||||||
}
|
|
||||||
|
@ -29,18 +29,13 @@ func NewNotifyService(settingRepo SettingRepository) *NotifyService {
|
|||||||
func (n *NotifyService) Test(ctx context.Context, req *domain.NotifyTestPushReq) error {
|
func (n *NotifyService) Test(ctx context.Context, req *domain.NotifyTestPushReq) error {
|
||||||
setting, err := n.settingRepo.GetByName(ctx, "notifyChannels")
|
setting, err := n.settingRepo.GetByName(ctx, "notifyChannels")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get notify channels setting failed: %w", err)
|
return fmt.Errorf("failed to get notify channels settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
conf, err := setting.GetChannelContent(req.Channel)
|
channelConfig, err := setting.GetChannelContent(req.Channel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get notify channel %s config failed: %w", req.Channel, err)
|
return fmt.Errorf("failed to get notify channel \"%s\" config: %w", req.Channel, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return SendTest(&sendTestParam{
|
return SendToChannel(notifyTestTitle, notifyTestBody, req.Channel, channelConfig)
|
||||||
Title: notifyTestTitle,
|
|
||||||
Content: notifyTestBody,
|
|
||||||
Channel: req.Channel,
|
|
||||||
Conf: conf,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
23
internal/pkg/core/notifier/notifier.go
Normal file
23
internal/pkg/core/notifier/notifier.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package notifier
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// 表示定义消息通知器的抽象类型接口。
|
||||||
|
type Notifier interface {
|
||||||
|
// 发送通知。
|
||||||
|
//
|
||||||
|
// 入参:
|
||||||
|
// - ctx:上下文。
|
||||||
|
// - subject:通知主题。
|
||||||
|
// - message:通知内容。
|
||||||
|
//
|
||||||
|
// 出参:
|
||||||
|
// - res:发送结果。
|
||||||
|
// - err: 错误。
|
||||||
|
Notify(ctx context.Context, subject string, message string) (res *NotifyResult, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表示通知发送结果的数据结构。
|
||||||
|
type NotifyResult struct {
|
||||||
|
NotificationData map[string]any `json:"notificationData,omitempty"`
|
||||||
|
}
|
48
internal/pkg/core/notifier/providers/bark/bark.go
Normal file
48
internal/pkg/core/notifier/providers/bark/bark.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package bark
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/nikoksr/notify"
|
||||||
|
"github.com/nikoksr/notify/service/bark"
|
||||||
|
|
||||||
|
"github.com/usual2970/certimate/internal/pkg/core/notifier"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BarkNotifierConfig struct {
|
||||||
|
ServerUrl string `json:"serverUrl"`
|
||||||
|
DeviceKey string `json:"deviceKey"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BarkNotifier struct {
|
||||||
|
config *BarkNotifierConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ notifier.Notifier = (*BarkNotifier)(nil)
|
||||||
|
|
||||||
|
func New(config *BarkNotifierConfig) (*BarkNotifier, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BarkNotifier{
|
||||||
|
config: config,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *BarkNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
|
||||||
|
var srv notify.Notifier
|
||||||
|
if n.config.ServerUrl == "" {
|
||||||
|
srv = bark.New(n.config.DeviceKey)
|
||||||
|
} else {
|
||||||
|
srv = bark.NewWithServers(n.config.DeviceKey, n.config.ServerUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = srv.Send(ctx, subject, message)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ¬ifier.NotifyResult{}, nil
|
||||||
|
}
|
45
internal/pkg/core/notifier/providers/dingtalk/dingtalk.go
Normal file
45
internal/pkg/core/notifier/providers/dingtalk/dingtalk.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package dingtalk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/nikoksr/notify/service/dingding"
|
||||||
|
|
||||||
|
"github.com/usual2970/certimate/internal/pkg/core/notifier"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DingTalkNotifierConfig struct {
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DingTalkNotifier struct {
|
||||||
|
config *DingTalkNotifierConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ notifier.Notifier = (*DingTalkNotifier)(nil)
|
||||||
|
|
||||||
|
func New(config *DingTalkNotifierConfig) (*DingTalkNotifier, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DingTalkNotifier{
|
||||||
|
config: config,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *DingTalkNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
|
||||||
|
srv := dingding.New(&dingding.Config{
|
||||||
|
Token: n.config.AccessToken,
|
||||||
|
Secret: n.config.Secret,
|
||||||
|
})
|
||||||
|
|
||||||
|
err = srv.Send(ctx, subject, message)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ¬ifier.NotifyResult{}, nil
|
||||||
|
}
|
95
internal/pkg/core/notifier/providers/email/email.go
Normal file
95
internal/pkg/core/notifier/providers/email/email.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package email
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/smtp"
|
||||||
|
|
||||||
|
"github.com/domodwyer/mailyak/v3"
|
||||||
|
|
||||||
|
"github.com/usual2970/certimate/internal/pkg/core/notifier"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmailNotifierConfig struct {
|
||||||
|
SmtpHost string `json:"smtpHost"`
|
||||||
|
SmtpPort int32 `json:"smtpPort"`
|
||||||
|
SmtpTLS bool `json:"smtpTLS"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
SenderAddress string `json:"senderAddress"`
|
||||||
|
ReceiverAddress string `json:"receiverAddress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmailNotifier struct {
|
||||||
|
config *EmailNotifierConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ notifier.Notifier = (*EmailNotifier)(nil)
|
||||||
|
|
||||||
|
func New(config *EmailNotifierConfig) (*EmailNotifier, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &EmailNotifier{
|
||||||
|
config: config,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *EmailNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
|
||||||
|
var smtpAuth smtp.Auth
|
||||||
|
if n.config.Username != "" || n.config.Password != "" {
|
||||||
|
smtpAuth = smtp.PlainAuth("", n.config.Username, n.config.Password, n.config.SmtpHost)
|
||||||
|
}
|
||||||
|
|
||||||
|
var smtpAddr string
|
||||||
|
if n.config.SmtpPort == 0 {
|
||||||
|
if n.config.SmtpTLS {
|
||||||
|
smtpAddr = fmt.Sprintf("%s:465", n.config.SmtpHost)
|
||||||
|
} else {
|
||||||
|
smtpAddr = fmt.Sprintf("%s:25", n.config.SmtpHost)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
smtpAddr = fmt.Sprintf("%s:%d", n.config.SmtpHost, n.config.SmtpPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
var yak *mailyak.MailYak
|
||||||
|
if n.config.SmtpTLS {
|
||||||
|
yak, err = mailyak.NewWithTLS(smtpAddr, smtpAuth, newTlsConfig())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
yak = mailyak.New(smtpAddr, smtpAuth)
|
||||||
|
}
|
||||||
|
|
||||||
|
yak.From(n.config.SenderAddress)
|
||||||
|
yak.To(n.config.ReceiverAddress)
|
||||||
|
yak.Subject(subject)
|
||||||
|
yak.Plain().Set(message)
|
||||||
|
|
||||||
|
err = yak.Send()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ¬ifier.NotifyResult{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTlsConfig() *tls.Config {
|
||||||
|
var suiteIds []uint16
|
||||||
|
for _, suite := range tls.CipherSuites() {
|
||||||
|
suiteIds = append(suiteIds, suite.ID)
|
||||||
|
}
|
||||||
|
for _, suite := range tls.InsecureCipherSuites() {
|
||||||
|
suiteIds = append(suiteIds, suite.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为兼容国内部分低版本 TLS 的 SMTP 服务商
|
||||||
|
return &tls.Config{
|
||||||
|
MinVersion: tls.VersionTLS10,
|
||||||
|
CipherSuites: suiteIds,
|
||||||
|
}
|
||||||
|
}
|
51
internal/pkg/core/notifier/providers/email/email_test.go
Normal file
51
internal/pkg/core/notifier/providers/email/email_test.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package email_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
notifierEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Shell command to run this test:
|
||||||
|
|
||||||
|
CERTIMATE_NOTIFIER_EMAIL_SMTPPORT=465 \
|
||||||
|
CERTIMATE_NOTIFIER_EMAIL_SMTPTLS=true \
|
||||||
|
CERTIMATE_NOTIFIER_EMAIL_SMTPHOST="smtp.example.com" \
|
||||||
|
CERTIMATE_NOTIFIER_EMAIL_USERNAME="your-username" \
|
||||||
|
CERTIMATE_NOTIFIER_EMAIL_PASSWORD="your-password" \
|
||||||
|
CERTIMATE_NOTIFIER_EMAIL_SENDERADDRESS="sender@example.com" \
|
||||||
|
CERTIMATE_NOTIFIER_EMAIL_RECEIVERADDRESS="receiver@example.com" \
|
||||||
|
go test -v -run TestNotify email_test.go
|
||||||
|
*/
|
||||||
|
func TestNotify(t *testing.T) {
|
||||||
|
smtpPort, err := strconv.ParseInt(os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SMTPPORT"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("invalid envvar: %+v", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
smtpTLS, err := strconv.ParseBool(os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SMTPTLS"))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("invalid envvar: %+v", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := notifierEmail.New(¬ifierEmail.EmailNotifierConfig{
|
||||||
|
SmtpHost: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SMTPHOST"),
|
||||||
|
SmtpPort: int32(smtpPort),
|
||||||
|
SmtpTLS: smtpTLS,
|
||||||
|
Username: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_USERNAME"),
|
||||||
|
Password: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_PASSWORD"),
|
||||||
|
SenderAddress: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SENDERADDRESS"),
|
||||||
|
ReceiverAddress: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_RECEIVERADDRESS"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("invalid envvar: %+v", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("notify result: %v", res)
|
||||||
|
}
|
41
internal/pkg/core/notifier/providers/lark/lark.go
Normal file
41
internal/pkg/core/notifier/providers/lark/lark.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package lark
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/nikoksr/notify/service/lark"
|
||||||
|
|
||||||
|
"github.com/usual2970/certimate/internal/pkg/core/notifier"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LarkNotifierConfig struct {
|
||||||
|
WebhookUrl string `json:"webhookUrl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LarkNotifier struct {
|
||||||
|
config *LarkNotifierConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ notifier.Notifier = (*LarkNotifier)(nil)
|
||||||
|
|
||||||
|
func New(config *LarkNotifierConfig) (*LarkNotifier, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &LarkNotifier{
|
||||||
|
config: config,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *LarkNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
|
||||||
|
srv := lark.NewWebhookService(n.config.WebhookUrl)
|
||||||
|
|
||||||
|
err = srv.Send(ctx, subject, message)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ¬ifier.NotifyResult{}, nil
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
package serverchan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
notifyHttp "github.com/nikoksr/notify/service/http"
|
||||||
|
|
||||||
|
"github.com/usual2970/certimate/internal/pkg/core/notifier"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerChanNotifierConfig struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerChanNotifier struct {
|
||||||
|
config *ServerChanNotifierConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ notifier.Notifier = (*ServerChanNotifier)(nil)
|
||||||
|
|
||||||
|
func New(config *ServerChanNotifierConfig) (*ServerChanNotifier, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ServerChanNotifier{
|
||||||
|
config: config,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ServerChanNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
|
||||||
|
srv := notifyHttp.New()
|
||||||
|
|
||||||
|
srv.AddReceivers(¬ifyHttp.Webhook{
|
||||||
|
URL: n.config.Url,
|
||||||
|
Header: http.Header{},
|
||||||
|
ContentType: "application/json",
|
||||||
|
Method: http.MethodPost,
|
||||||
|
BuildPayload: func(subject, message string) (payload any) {
|
||||||
|
return map[string]string{
|
||||||
|
"text": subject,
|
||||||
|
"desp": message,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
err = srv.Send(ctx, subject, message)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ¬ifier.NotifyResult{}, nil
|
||||||
|
}
|
47
internal/pkg/core/notifier/providers/telegram/telegram.go
Normal file
47
internal/pkg/core/notifier/providers/telegram/telegram.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/nikoksr/notify/service/telegram"
|
||||||
|
|
||||||
|
"github.com/usual2970/certimate/internal/pkg/core/notifier"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TelegramNotifierConfig struct {
|
||||||
|
ApiToken string `json:"apiToken"`
|
||||||
|
ChatId int64 `json:"chatId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TelegramNotifier struct {
|
||||||
|
config *TelegramNotifierConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ notifier.Notifier = (*TelegramNotifier)(nil)
|
||||||
|
|
||||||
|
func New(config *TelegramNotifierConfig) (*TelegramNotifier, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TelegramNotifier{
|
||||||
|
config: config,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *TelegramNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
|
||||||
|
srv, err := telegram.New(n.config.ApiToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.AddReceivers(n.config.ChatId)
|
||||||
|
|
||||||
|
err = srv.Send(ctx, subject, message)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ¬ifier.NotifyResult{}, nil
|
||||||
|
}
|
43
internal/pkg/core/notifier/providers/webhook/webhook.go
Normal file
43
internal/pkg/core/notifier/providers/webhook/webhook.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/nikoksr/notify/service/http"
|
||||||
|
|
||||||
|
"github.com/usual2970/certimate/internal/pkg/core/notifier"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebhookNotifierConfig struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebhookNotifier struct {
|
||||||
|
config *WebhookNotifierConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ notifier.Notifier = (*WebhookNotifier)(nil)
|
||||||
|
|
||||||
|
func New(config *WebhookNotifierConfig) (*WebhookNotifier, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &WebhookNotifier{
|
||||||
|
config: config,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *WebhookNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
|
||||||
|
srv := http.New()
|
||||||
|
|
||||||
|
srv.AddReceiversURLs(n.config.Url)
|
||||||
|
|
||||||
|
err = srv.Send(ctx, subject, message)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ¬ifier.NotifyResult{}, nil
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -26,7 +27,13 @@ type AliyunCASUploader struct {
|
|||||||
sdkClient *aliyunCas.Client
|
sdkClient *aliyunCas.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ uploader.Uploader = (*AliyunCASUploader)(nil)
|
||||||
|
|
||||||
func New(config *AliyunCASUploaderConfig) (*AliyunCASUploader, error) {
|
func New(config *AliyunCASUploaderConfig) (*AliyunCASUploader, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
client, err := createSdkClient(
|
client, err := createSdkClient(
|
||||||
config.AccessKeyId,
|
config.AccessKeyId,
|
||||||
config.AccessKeySecret,
|
config.AccessKeySecret,
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -28,7 +29,13 @@ type AliyunSLBUploader struct {
|
|||||||
sdkClient *aliyunSlb.Client
|
sdkClient *aliyunSlb.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ uploader.Uploader = (*AliyunSLBUploader)(nil)
|
||||||
|
|
||||||
func New(config *AliyunSLBUploaderConfig) (*AliyunSLBUploader, error) {
|
func New(config *AliyunSLBUploaderConfig) (*AliyunSLBUploader, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
client, err := createSdkClient(
|
client, err := createSdkClient(
|
||||||
config.AccessKeyId,
|
config.AccessKeyId,
|
||||||
config.AccessKeySecret,
|
config.AccessKeySecret,
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -21,7 +22,13 @@ type DogeCloudUploader struct {
|
|||||||
sdkClient *doge.Client
|
sdkClient *doge.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ uploader.Uploader = (*DogeCloudUploader)(nil)
|
||||||
|
|
||||||
func New(config *DogeCloudUploaderConfig) (*DogeCloudUploader, error) {
|
func New(config *DogeCloudUploaderConfig) (*DogeCloudUploader, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
client, err := createSdkClient(
|
client, err := createSdkClient(
|
||||||
config.AccessKey,
|
config.AccessKey,
|
||||||
config.SecretKey,
|
config.SecretKey,
|
||||||
|
@ -32,7 +32,13 @@ type HuaweiCloudELBUploader struct {
|
|||||||
sdkClient *hcElb.ElbClient
|
sdkClient *hcElb.ElbClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ uploader.Uploader = (*HuaweiCloudELBUploader)(nil)
|
||||||
|
|
||||||
func New(config *HuaweiCloudELBUploaderConfig) (*HuaweiCloudELBUploader, error) {
|
func New(config *HuaweiCloudELBUploaderConfig) (*HuaweiCloudELBUploader, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
client, err := createSdkClient(
|
client, err := createSdkClient(
|
||||||
config.AccessKeyId,
|
config.AccessKeyId,
|
||||||
config.SecretAccessKey,
|
config.SecretAccessKey,
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -27,7 +28,13 @@ type HuaweiCloudSCMUploader struct {
|
|||||||
sdkClient *hcScm.ScmClient
|
sdkClient *hcScm.ScmClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ uploader.Uploader = (*HuaweiCloudSCMUploader)(nil)
|
||||||
|
|
||||||
func New(config *HuaweiCloudSCMUploaderConfig) (*HuaweiCloudSCMUploader, error) {
|
func New(config *HuaweiCloudSCMUploaderConfig) (*HuaweiCloudSCMUploader, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
client, err := createSdkClient(
|
client, err := createSdkClient(
|
||||||
config.AccessKeyId,
|
config.AccessKeyId,
|
||||||
config.SecretAccessKey,
|
config.SecretAccessKey,
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -23,7 +24,13 @@ type QiniuSSLCertUploader struct {
|
|||||||
sdkClient *qiniuEx.Client
|
sdkClient *qiniuEx.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ uploader.Uploader = (*QiniuSSLCertUploader)(nil)
|
||||||
|
|
||||||
func New(config *QiniuSSLCertUploaderConfig) (*QiniuSSLCertUploader, error) {
|
func New(config *QiniuSSLCertUploaderConfig) (*QiniuSSLCertUploader, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
client, err := createSdkClient(
|
client, err := createSdkClient(
|
||||||
config.AccessKey,
|
config.AccessKey,
|
||||||
config.SecretKey,
|
config.SecretKey,
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
xerrors "github.com/pkg/errors"
|
xerrors "github.com/pkg/errors"
|
||||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
|
||||||
@ -21,7 +22,13 @@ type TencentCloudSSLUploader struct {
|
|||||||
sdkClient *tcSsl.Client
|
sdkClient *tcSsl.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ uploader.Uploader = (*TencentCloudSSLUploader)(nil)
|
||||||
|
|
||||||
func New(config *TencentCloudSSLUploaderConfig) (*TencentCloudSSLUploader, error) {
|
func New(config *TencentCloudSSLUploaderConfig) (*TencentCloudSSLUploader, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
client, err := createSdkClient(
|
client, err := createSdkClient(
|
||||||
config.SecretId,
|
config.SecretId,
|
||||||
config.SecretKey,
|
config.SecretKey,
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import "context"
|
import "context"
|
||||||
|
|
||||||
// 表示定义证书上传者的抽象类型接口。
|
// 表示定义证书上传器的抽象类型接口。
|
||||||
// 云服务商通常会提供 SSL 证书管理服务,可供用户集中管理证书。
|
// 云服务商通常会提供 SSL 证书管理服务,可供用户集中管理证书。
|
||||||
// 注意与 `Deployer` 区分,“上传”通常为“部署”的前置操作。
|
// 注意与 `Deployer` 区分,“上传”通常为“部署”的前置操作。
|
||||||
type Uploader interface {
|
type Uploader interface {
|
||||||
|
164
internal/pkg/utils/maps/maps.go
Normal file
164
internal/pkg/utils/maps/maps.go
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
package maps
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
// 以字符串形式从字典中获取指定键的值。
|
||||||
|
//
|
||||||
|
// 入参:
|
||||||
|
// - dict: 字典。
|
||||||
|
// - key: 键。
|
||||||
|
//
|
||||||
|
// 出参:
|
||||||
|
// - 字典中键对应的值。如果指定键不存在或者值的类型不是字符串,则返回空字符串。
|
||||||
|
func GetValueAsString(dict map[string]any, key string) string {
|
||||||
|
return GetValueOrDefaultAsString(dict, key, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 以字符串形式从字典中获取指定键的值。
|
||||||
|
//
|
||||||
|
// 入参:
|
||||||
|
// - dict: 字典。
|
||||||
|
// - key: 键。
|
||||||
|
// - defaultValue: 默认值。
|
||||||
|
//
|
||||||
|
// 出参:
|
||||||
|
// - 字典中键对应的值。如果指定键不存在或者值的类型不是字符串,则返回默认值。
|
||||||
|
func GetValueOrDefaultAsString(dict map[string]any, key string, defaultValue string) string {
|
||||||
|
if dict == nil {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := dict[key]; ok {
|
||||||
|
if result, ok := value.(string); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 以 32 位整数形式从字典中获取指定键的值。
|
||||||
|
//
|
||||||
|
// 入参:
|
||||||
|
// - dict: 字典。
|
||||||
|
// - key: 键。
|
||||||
|
//
|
||||||
|
// 出参:
|
||||||
|
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 32 位整数,则返回 0。
|
||||||
|
func GetValueAsInt32(dict map[string]any, key string) int32 {
|
||||||
|
return GetValueOrDefaultAsInt32(dict, key, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 以 32 位整数形式从字典中获取指定键的值。
|
||||||
|
//
|
||||||
|
// 入参:
|
||||||
|
// - dict: 字典。
|
||||||
|
// - key: 键。
|
||||||
|
// - defaultValue: 默认值。
|
||||||
|
//
|
||||||
|
// 出参:
|
||||||
|
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 32 位整数,则返回默认值。
|
||||||
|
func GetValueOrDefaultAsInt32(dict map[string]any, key string, defaultValue int32) int32 {
|
||||||
|
if dict == nil {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := dict[key]; ok {
|
||||||
|
if result, ok := value.(int32); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容字符串类型的值
|
||||||
|
if str, ok := value.(string); ok {
|
||||||
|
if result, err := strconv.ParseInt(str, 10, 32); err == nil {
|
||||||
|
return int32(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 以 64 位整数形式从字典中获取指定键的值。
|
||||||
|
//
|
||||||
|
// 入参:
|
||||||
|
// - dict: 字典。
|
||||||
|
// - key: 键。
|
||||||
|
//
|
||||||
|
// 出参:
|
||||||
|
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 64 位整数,则返回 0。
|
||||||
|
func GetValueAsInt64(dict map[string]any, key string) int64 {
|
||||||
|
return GetValueOrDefaultAsInt64(dict, key, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 以 64 位整数形式从字典中获取指定键的值。
|
||||||
|
//
|
||||||
|
// 入参:
|
||||||
|
// - dict: 字典。
|
||||||
|
// - key: 键。
|
||||||
|
// - defaultValue: 默认值。
|
||||||
|
//
|
||||||
|
// 出参:
|
||||||
|
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 64 位整数,则返回默认值。
|
||||||
|
func GetValueOrDefaultAsInt64(dict map[string]any, key string, defaultValue int64) int64 {
|
||||||
|
if dict == nil {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := dict[key]; ok {
|
||||||
|
if result, ok := value.(int64); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容字符串类型的值
|
||||||
|
if str, ok := value.(string); ok {
|
||||||
|
if result, err := strconv.ParseInt(str, 10, 64); err == nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 以布尔形式从字典中获取指定键的值。
|
||||||
|
//
|
||||||
|
// 入参:
|
||||||
|
// - dict: 字典。
|
||||||
|
// - key: 键。
|
||||||
|
//
|
||||||
|
// 出参:
|
||||||
|
// - 字典中键对应的值。如果指定键不存在或者值的类型不是布尔,则返回 false。
|
||||||
|
func GetValueAsBool(dict map[string]any, key string) bool {
|
||||||
|
return GetValueOrDefaultAsBool(dict, key, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 以布尔形式从字典中获取指定键的值。
|
||||||
|
//
|
||||||
|
// 入参:
|
||||||
|
// - dict: 字典。
|
||||||
|
// - key: 键。
|
||||||
|
// - defaultValue: 默认值。
|
||||||
|
//
|
||||||
|
// 出参:
|
||||||
|
// - 字典中键对应的值。如果指定键不存在或者值的类型不是布尔,则返回默认值。
|
||||||
|
func GetValueOrDefaultAsBool(dict map[string]any, key string, defaultValue bool) bool {
|
||||||
|
if dict == nil {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := dict[key]; ok {
|
||||||
|
if result, ok := value.(bool); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容字符串类型的值
|
||||||
|
if str, ok := value.(string); ok {
|
||||||
|
if result, err := strconv.ParseBool(str); err == nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
@ -19,4 +19,3 @@ export const notifyTest = async (channel: string) => {
|
|||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -123,22 +123,29 @@ const Bark = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [testing, setTesting] = useState<boolean>(false);
|
||||||
const handlePushTestClick = async () => {
|
const handlePushTestClick = async () => {
|
||||||
|
if (testing) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setTesting(true);
|
||||||
|
|
||||||
await notifyTest("bark");
|
await notifyTest("bark");
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("settings.notification.config.push.test.message.success.message"),
|
title: t("settings.notification.push_test_message.succeeded.message"),
|
||||||
description: t("settings.notification.config.push.test.message.success.message"),
|
description: t("settings.notification.push_test_message.succeeded.message"),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = getErrMessage(e);
|
const msg = getErrMessage(e);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("settings.notification.config.push.test.message.failed.message"),
|
title: t("settings.notification.push_test_message.failed.message"),
|
||||||
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
|
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setTesting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -177,67 +184,76 @@ const Bark = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col space-y-4">
|
||||||
<Input
|
<div>
|
||||||
placeholder={t("settings.notification.bark.serverUrl.placeholder")}
|
<Label>{t("settings.notification.bark.server_url.label")}</Label>
|
||||||
value={bark.data.serverUrl}
|
<Input
|
||||||
onChange={(e) => {
|
placeholder={t("settings.notification.bark.server_url.placeholder")}
|
||||||
const newData = {
|
value={bark.data.serverUrl}
|
||||||
...bark,
|
onChange={(e) => {
|
||||||
data: {
|
const newData = {
|
||||||
...bark.data,
|
...bark,
|
||||||
serverUrl: e.target.value,
|
data: {
|
||||||
},
|
...bark.data,
|
||||||
};
|
serverUrl: e.target.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
checkChanged(newData.data);
|
checkChanged(newData.data);
|
||||||
setBark(newData);
|
setBark(newData);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
|
||||||
placeholder={t("settings.notification.bark.deviceKey.placeholder")}
|
|
||||||
value={bark.data.deviceKey}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newData = {
|
|
||||||
...bark,
|
|
||||||
data: {
|
|
||||||
...bark.data,
|
|
||||||
deviceKey: e.target.value,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
checkChanged(newData.data);
|
|
||||||
setBark(newData);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-1 mt-2">
|
|
||||||
<Switch id="airplane-mode" checked={bark.data.enabled} onCheckedChange={handleSwitchChange} />
|
|
||||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end mt-2">
|
<div>
|
||||||
<Show when={changed}>
|
<Label>{t("settings.notification.bark.device_key.label")}</Label>
|
||||||
<Button
|
<Input
|
||||||
onClick={() => {
|
placeholder={t("settings.notification.bark.device_key.placeholder")}
|
||||||
handleSaveClick();
|
value={bark.data.deviceKey}
|
||||||
}}
|
onChange={(e) => {
|
||||||
>
|
const newData = {
|
||||||
{t("common.save")}
|
...bark,
|
||||||
</Button>
|
data: {
|
||||||
</Show>
|
...bark.data,
|
||||||
|
deviceKey: e.target.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
<Show when={!changed && bark.id != ""}>
|
checkChanged(newData.data);
|
||||||
<Button
|
setBark(newData);
|
||||||
variant="secondary"
|
}}
|
||||||
onClick={() => {
|
/>
|
||||||
handlePushTestClick();
|
</div>
|
||||||
}}
|
|
||||||
>
|
<div className="flex justify-between gap-4">
|
||||||
{t("settings.notification.config.push.test.message")}
|
<div className="flex items-center space-x-1">
|
||||||
</Button>
|
<Switch id="airplane-mode" checked={bark.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||||
</Show>
|
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<Show when={changed}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
handleSaveClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("common.save")}
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={!changed && bark.id != ""}>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
loading={testing}
|
||||||
|
onClick={() => {
|
||||||
|
handlePushTestClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("settings.notification.push_test_message")}
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -120,23 +120,30 @@ const DingTalk = () => {
|
|||||||
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
|
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setTesting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [testing, setTesting] = useState<boolean>(false);
|
||||||
const handlePushTestClick = async () => {
|
const handlePushTestClick = async () => {
|
||||||
|
if (testing) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setTesting(true);
|
||||||
|
|
||||||
await notifyTest("dingtalk");
|
await notifyTest("dingtalk");
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("settings.notification.config.push.test.message.success.message"),
|
title: t("settings.notification.push_test_message.succeeded.message"),
|
||||||
description: t("settings.notification.config.push.test.message.success.message"),
|
description: t("settings.notification.push_test_message.succeeded.message"),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = getErrMessage(e);
|
const msg = getErrMessage(e);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("settings.notification.config.push.test.message.failed.message"),
|
title: t("settings.notification.push_test_message.failed.message"),
|
||||||
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
|
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -177,64 +184,74 @@ const DingTalk = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col space-y-4">
|
||||||
<Input
|
<div>
|
||||||
placeholder="AccessToken"
|
<Label>{t("settings.notification.dingtalk.access_token.label")}</Label>
|
||||||
value={dingtalk.data.accessToken}
|
<Input
|
||||||
onChange={(e) => {
|
placeholder={t("settings.notification.dingtalk.access_token.placeholder")}
|
||||||
const newData = {
|
value={dingtalk.data.accessToken}
|
||||||
...dingtalk,
|
onChange={(e) => {
|
||||||
data: {
|
const newData = {
|
||||||
...dingtalk.data,
|
...dingtalk,
|
||||||
accessToken: e.target.value,
|
data: {
|
||||||
},
|
...dingtalk.data,
|
||||||
};
|
accessToken: e.target.value,
|
||||||
checkChanged(newData.data);
|
},
|
||||||
setDingtalk(newData);
|
};
|
||||||
}}
|
checkChanged(newData.data);
|
||||||
/>
|
setDingtalk(newData);
|
||||||
<Input
|
}}
|
||||||
placeholder={t("settings.notification.dingtalk.secret.placeholder")}
|
/>
|
||||||
className="mt-2"
|
|
||||||
value={dingtalk.data.secret}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newData = {
|
|
||||||
...dingtalk,
|
|
||||||
data: {
|
|
||||||
...dingtalk.data,
|
|
||||||
secret: e.target.value,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
checkChanged(newData.data);
|
|
||||||
setDingtalk(newData);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center space-x-1 mt-2">
|
|
||||||
<Switch id="airplane-mode" checked={dingtalk.data.enabled} onCheckedChange={handleSwitchChange} />
|
|
||||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end mt-2">
|
<div>
|
||||||
<Show when={changed}>
|
<Label>{t("settings.notification.dingtalk.secret.label")}</Label>
|
||||||
<Button
|
<Input
|
||||||
onClick={() => {
|
placeholder={t("settings.notification.dingtalk.secret.placeholder")}
|
||||||
handleSaveClick();
|
value={dingtalk.data.secret}
|
||||||
}}
|
onChange={(e) => {
|
||||||
>
|
const newData = {
|
||||||
{t("common.save")}
|
...dingtalk,
|
||||||
</Button>
|
data: {
|
||||||
</Show>
|
...dingtalk.data,
|
||||||
|
secret: e.target.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
checkChanged(newData.data);
|
||||||
|
setDingtalk(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Show when={!changed && dingtalk.id != ""}>
|
<div className="flex justify-between gap-4">
|
||||||
<Button
|
<div className="flex items-center space-x-1">
|
||||||
variant="secondary"
|
<Switch id="airplane-mode" checked={dingtalk.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||||
onClick={() => {
|
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||||
handlePushTestClick();
|
</div>
|
||||||
}}
|
|
||||||
>
|
<div className="flex items-center space-x-1">
|
||||||
{t("settings.notification.config.push.test.message")}
|
<Show when={changed}>
|
||||||
</Button>
|
<Button
|
||||||
</Show>
|
onClick={() => {
|
||||||
|
handleSaveClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("common.save")}
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={!changed && dingtalk.id != ""}>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
loading={testing}
|
||||||
|
onClick={() => {
|
||||||
|
handlePushTestClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("settings.notification.push_test_message")}
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
384
ui/src/components/notify/Email.tsx
Normal file
384
ui/src/components/notify/Email.tsx
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import { getErrMessage } from "@/lib/error";
|
||||||
|
import { NotifyChannelEmail, NotifyChannels } from "@/domain/settings";
|
||||||
|
import { useNotifyContext } from "@/providers/notify";
|
||||||
|
import { update } from "@/repository/settings";
|
||||||
|
import Show from "@/components/Show";
|
||||||
|
import { notifyTest } from "@/api/notify";
|
||||||
|
|
||||||
|
type EmailSetting = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
data: NotifyChannelEmail;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Mail = () => {
|
||||||
|
const { config, setChannels } = useNotifyContext();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [changed, setChanged] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [mail, setMail] = useState<EmailSetting>({
|
||||||
|
id: config.id ?? "",
|
||||||
|
name: "notifyChannels",
|
||||||
|
data: {
|
||||||
|
smtpHost: "",
|
||||||
|
smtpPort: 465,
|
||||||
|
smtpTLS: true,
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
senderAddress: "",
|
||||||
|
receiverAddress: "",
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [originMail, setOriginMail] = useState<EmailSetting>({
|
||||||
|
id: config.id ?? "",
|
||||||
|
name: "notifyChannels",
|
||||||
|
data: {
|
||||||
|
smtpHost: "",
|
||||||
|
smtpPort: 465,
|
||||||
|
smtpTLS: true,
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
senderAddress: "",
|
||||||
|
receiverAddress: "",
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setChanged(false);
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const data = getDetailMail();
|
||||||
|
setOriginMail({
|
||||||
|
id: config.id ?? "",
|
||||||
|
name: "email",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const data = getDetailMail();
|
||||||
|
setMail({
|
||||||
|
id: config.id ?? "",
|
||||||
|
name: "email",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const getDetailMail = () => {
|
||||||
|
const df: NotifyChannelEmail = {
|
||||||
|
smtpHost: "smtp.example.com",
|
||||||
|
smtpPort: 465,
|
||||||
|
smtpTLS: true,
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
senderAddress: "",
|
||||||
|
receiverAddress: "",
|
||||||
|
enabled: false,
|
||||||
|
};
|
||||||
|
if (!config.content) {
|
||||||
|
return df;
|
||||||
|
}
|
||||||
|
const chanels = config.content as NotifyChannels;
|
||||||
|
if (!chanels.email) {
|
||||||
|
return df;
|
||||||
|
}
|
||||||
|
|
||||||
|
return chanels.email as NotifyChannelEmail;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkChanged = (data: NotifyChannelEmail) => {
|
||||||
|
if (
|
||||||
|
data.smtpHost !== originMail.data.smtpHost ||
|
||||||
|
data.smtpPort !== originMail.data.smtpPort ||
|
||||||
|
data.smtpTLS !== originMail.data.smtpTLS ||
|
||||||
|
data.username !== originMail.data.username ||
|
||||||
|
data.password !== originMail.data.password ||
|
||||||
|
data.senderAddress !== originMail.data.senderAddress ||
|
||||||
|
data.receiverAddress !== originMail.data.receiverAddress
|
||||||
|
) {
|
||||||
|
setChanged(true);
|
||||||
|
} else {
|
||||||
|
setChanged(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveClick = async () => {
|
||||||
|
try {
|
||||||
|
const resp = await update({
|
||||||
|
...config,
|
||||||
|
name: "notifyChannels",
|
||||||
|
content: {
|
||||||
|
...config.content,
|
||||||
|
email: {
|
||||||
|
...mail.data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setChannels(resp);
|
||||||
|
toast({
|
||||||
|
title: t("common.save.succeeded.message"),
|
||||||
|
description: t("settings.notification.config.saved.message"),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
const msg = getErrMessage(e);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: t("common.save.failed.message"),
|
||||||
|
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [testing, setTesting] = useState<boolean>(false);
|
||||||
|
const handlePushTestClick = async () => {
|
||||||
|
if (testing) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setTesting(true);
|
||||||
|
|
||||||
|
await notifyTest("email");
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: t("settings.notification.push_test_message.succeeded.message"),
|
||||||
|
description: t("settings.notification.push_test_message.succeeded.message"),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
const msg = getErrMessage(e);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: t("settings.notification.push_test_message.failed.message"),
|
||||||
|
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setTesting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSwitchChange = async () => {
|
||||||
|
const newData = {
|
||||||
|
...mail,
|
||||||
|
data: {
|
||||||
|
...mail.data,
|
||||||
|
enabled: !mail.data.enabled,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setMail(newData);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await update({
|
||||||
|
...config,
|
||||||
|
name: "notifyChannels",
|
||||||
|
content: {
|
||||||
|
...config.content,
|
||||||
|
email: {
|
||||||
|
...newData.data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setChannels(resp);
|
||||||
|
} catch (e) {
|
||||||
|
const msg = getErrMessage(e);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: t("common.save.failed.message"),
|
||||||
|
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<div className="w-2/5">
|
||||||
|
<Label>{t("settings.notification.email.smtp_host.label")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("settings.notification.email.smtp_host.placeholder")}
|
||||||
|
value={mail.data.smtpHost}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = {
|
||||||
|
...mail,
|
||||||
|
data: {
|
||||||
|
...mail.data,
|
||||||
|
smtpHost: e.target.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
checkChanged(newData.data);
|
||||||
|
setMail(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-2/5">
|
||||||
|
<Label>{t("settings.notification.email.smtp_port.label")}</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder={t("settings.notification.email.smtp_port.placeholder")}
|
||||||
|
value={mail.data.smtpPort}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = {
|
||||||
|
...mail,
|
||||||
|
data: {
|
||||||
|
...mail.data,
|
||||||
|
smtpPort: +e.target.value || 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
checkChanged(newData.data);
|
||||||
|
setMail(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-1/5">
|
||||||
|
<Label>{t("settings.notification.email.smtp_tls.label")}</Label>
|
||||||
|
<Switch
|
||||||
|
className="block mt-2"
|
||||||
|
checked={mail.data.smtpTLS}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
const newData = {
|
||||||
|
...mail,
|
||||||
|
data: {
|
||||||
|
...mail.data,
|
||||||
|
smtpPort: e && mail.data.smtpPort === 25 ? 465 : !e && mail.data.smtpPort === 465 ? 25 : mail.data.smtpPort,
|
||||||
|
smtpTLS: e,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
checkChanged(newData.data);
|
||||||
|
setMail(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<div className="w-1/2">
|
||||||
|
<Label>{t("settings.notification.email.username.label")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("settings.notification.email.username.placeholder")}
|
||||||
|
value={mail.data.username}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = {
|
||||||
|
...mail,
|
||||||
|
data: {
|
||||||
|
...mail.data,
|
||||||
|
username: e.target.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
checkChanged(newData.data);
|
||||||
|
setMail(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-1/2">
|
||||||
|
<Label>{t("settings.notification.email.password.label")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("settings.notification.email.password.placeholder")}
|
||||||
|
value={mail.data.password}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = {
|
||||||
|
...mail,
|
||||||
|
data: {
|
||||||
|
...mail.data,
|
||||||
|
password: e.target.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
checkChanged(newData.data);
|
||||||
|
setMail(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>{t("settings.notification.email.sender_address.label")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("settings.notification.email.sender_address.placeholder")}
|
||||||
|
value={mail.data.senderAddress}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = {
|
||||||
|
...mail,
|
||||||
|
data: {
|
||||||
|
...mail.data,
|
||||||
|
senderAddress: e.target.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
checkChanged(newData.data);
|
||||||
|
setMail(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>{t("settings.notification.email.receiver_address.label")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("settings.notification.email.receiver_address.placeholder")}
|
||||||
|
value={mail.data.receiverAddress}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = {
|
||||||
|
...mail,
|
||||||
|
data: {
|
||||||
|
...mail.data,
|
||||||
|
receiverAddress: e.target.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
checkChanged(newData.data);
|
||||||
|
setMail(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between gap-4">
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<Switch id="airplane-mode" checked={mail.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||||
|
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<Show when={changed}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
handleSaveClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("common.save")}
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={!changed && mail.id != ""}>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
loading={testing}
|
||||||
|
onClick={() => {
|
||||||
|
handlePushTestClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("settings.notification.push_test_message")}
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Mail;
|
@ -116,23 +116,30 @@ const Lark = () => {
|
|||||||
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
|
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setTesting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [testing, setTesting] = useState<boolean>(false);
|
||||||
const handlePushTestClick = async () => {
|
const handlePushTestClick = async () => {
|
||||||
|
if (testing) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setTesting(true);
|
||||||
|
|
||||||
await notifyTest("lark");
|
await notifyTest("lark");
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("settings.notification.config.push.test.message.success.message"),
|
title: t("settings.notification.push_test_message.succeeded.message"),
|
||||||
description: t("settings.notification.config.push.test.message.success.message"),
|
description: t("settings.notification.push_test_message.succeeded.message"),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = getErrMessage(e);
|
const msg = getErrMessage(e);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("settings.notification.config.push.test.message.failed.message"),
|
title: t("settings.notification.push_test_message.failed.message"),
|
||||||
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
|
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -173,49 +180,56 @@ const Lark = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col space-y-4">
|
||||||
<Input
|
<div>
|
||||||
placeholder="Webhook Url"
|
<Label>{t("settings.notification.lark.webhook_url.label")}</Label>
|
||||||
value={lark.data.webhookUrl}
|
<Input
|
||||||
onChange={(e) => {
|
placeholder={t("settings.notification.lark.webhook_url.placeholder")}
|
||||||
const newData = {
|
value={lark.data.webhookUrl}
|
||||||
...lark,
|
onChange={(e) => {
|
||||||
data: {
|
const newData = {
|
||||||
...lark.data,
|
...lark,
|
||||||
webhookUrl: e.target.value,
|
data: {
|
||||||
},
|
...lark.data,
|
||||||
};
|
webhookUrl: e.target.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
checkChanged(newData.data);
|
checkChanged(newData.data);
|
||||||
setLark(newData);
|
setLark(newData);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center space-x-1 mt-2">
|
|
||||||
<Switch id="airplane-mode" checked={lark.data.enabled} onCheckedChange={handleSwitchChange} />
|
|
||||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end mt-2">
|
<div className="flex justify-between gap-4">
|
||||||
<Show when={changed}>
|
<div className="flex items-center space-x-1">
|
||||||
<Button
|
<Switch id="airplane-mode" checked={lark.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||||
onClick={() => {
|
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||||
handleSaveClick();
|
</div>
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("common.save")}
|
|
||||||
</Button>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={!changed && lark.id != ""}>
|
<div className="flex items-center space-x-1">
|
||||||
<Button
|
<Show when={changed}>
|
||||||
variant="secondary"
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePushTestClick();
|
handleSaveClick();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("settings.notification.config.push.test.message")}
|
{t("common.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={!changed && lark.id != ""}>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
loading={testing}
|
||||||
|
onClick={() => {
|
||||||
|
handlePushTestClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("settings.notification.push_test_message")}
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -97,7 +97,7 @@ const ServerChan = () => {
|
|||||||
if (!isValidURL(serverchan.data.url)) {
|
if (!isValidURL(serverchan.data.url)) {
|
||||||
toast({
|
toast({
|
||||||
title: t("common.save.failed.message"),
|
title: t("common.save.failed.message"),
|
||||||
description: t("settings.notification.url.errmsg.invalid"),
|
description: t("common.errmsg.url_invalid"),
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -130,22 +130,29 @@ const ServerChan = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [testing, setTesting] = useState<boolean>(false);
|
||||||
const handlePushTestClick = async () => {
|
const handlePushTestClick = async () => {
|
||||||
|
if (testing) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setTesting(true);
|
||||||
|
|
||||||
await notifyTest("serverchan");
|
await notifyTest("serverchan");
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("settings.notification.config.push.test.message.success.message"),
|
title: t("settings.notification.push_test_message.succeeded.message"),
|
||||||
description: t("settings.notification.config.push.test.message.success.message"),
|
description: t("settings.notification.push_test_message.succeeded.message"),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = getErrMessage(e);
|
const msg = getErrMessage(e);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("settings.notification.config.push.test.message.failed.message"),
|
title: t("settings.notification.push_test_message.failed.message"),
|
||||||
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
|
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setTesting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -184,50 +191,56 @@ const ServerChan = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col space-y-4">
|
||||||
<Input
|
<div>
|
||||||
placeholder={t("settings.notification.serverchan.url.placeholder")}
|
<Label>{t("settings.notification.serverchan.url.label")}</Label>
|
||||||
value={serverchan.data.url}
|
<Input
|
||||||
onChange={(e) => {
|
placeholder={t("settings.notification.serverchan.url.placeholder")}
|
||||||
const newData = {
|
value={serverchan.data.url}
|
||||||
...serverchan,
|
onChange={(e) => {
|
||||||
data: {
|
const newData = {
|
||||||
...serverchan.data,
|
...serverchan,
|
||||||
url: e.target.value,
|
data: {
|
||||||
},
|
...serverchan.data,
|
||||||
};
|
url: e.target.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
checkChanged(newData.data);
|
checkChanged(newData.data);
|
||||||
setServerChan(newData);
|
setServerChan(newData);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center space-x-1 mt-2">
|
|
||||||
<Switch id="airplane-mode" checked={serverchan.data.enabled} onCheckedChange={handleSwitchChange} />
|
|
||||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end mt-2">
|
<div className="flex justify-between gap-4">
|
||||||
<Show when={changed}>
|
<div className="flex items-center space-x-1">
|
||||||
<Button
|
<Switch id="airplane-mode" checked={serverchan.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||||
onClick={() => {
|
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||||
handleSaveClick();
|
</div>
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("common.save")}
|
|
||||||
</Button>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={!changed && serverchan.id != ""}>
|
<div className="flex items-center space-x-1">
|
||||||
<Button
|
<Show when={changed}>
|
||||||
variant="secondary"
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePushTestClick();
|
handleSaveClick();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("settings.notification.config.push.test.message")}
|
{t("common.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={!changed && serverchan.id != ""}>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
loading={testing}
|
||||||
|
onClick={() => {
|
||||||
|
handlePushTestClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("settings.notification.push_test_message")}
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -123,22 +123,29 @@ const Telegram = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [testing, setTesting] = useState<boolean>(false);
|
||||||
const handlePushTestClick = async () => {
|
const handlePushTestClick = async () => {
|
||||||
|
if (testing) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setTesting(true);
|
||||||
|
|
||||||
await notifyTest("telegram");
|
await notifyTest("telegram");
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("settings.notification.config.push.test.message.success.message"),
|
title: t("settings.notification.push_test_message.succeeded.message"),
|
||||||
description: t("settings.notification.config.push.test.message.success.message"),
|
description: t("settings.notification.push_test_message.succeeded.message"),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = getErrMessage(e);
|
const msg = getErrMessage(e);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("settings.notification.config.push.test.message.failed.message"),
|
title: t("settings.notification.push_test_message.failed.message"),
|
||||||
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
|
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setTesting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -177,67 +184,76 @@ const Telegram = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col space-y-4">
|
||||||
<Input
|
<div>
|
||||||
placeholder="ApiToken"
|
<Label>{t("settings.notification.telegram.api_token.label")}</Label>
|
||||||
value={telegram.data.apiToken}
|
<Input
|
||||||
onChange={(e) => {
|
placeholder={t("settings.notification.telegram.api_token.placeholder")}
|
||||||
const newData = {
|
value={telegram.data.apiToken}
|
||||||
...telegram,
|
onChange={(e) => {
|
||||||
data: {
|
const newData = {
|
||||||
...telegram.data,
|
...telegram,
|
||||||
apiToken: e.target.value,
|
data: {
|
||||||
},
|
...telegram.data,
|
||||||
};
|
apiToken: e.target.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
checkChanged(newData.data);
|
checkChanged(newData.data);
|
||||||
setTelegram(newData);
|
setTelegram(newData);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
|
||||||
placeholder="ChatId"
|
|
||||||
value={telegram.data.chatId}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newData = {
|
|
||||||
...telegram,
|
|
||||||
data: {
|
|
||||||
...telegram.data,
|
|
||||||
chatId: e.target.value,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
checkChanged(newData.data);
|
|
||||||
setTelegram(newData);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-1 mt-2">
|
|
||||||
<Switch id="airplane-mode" checked={telegram.data.enabled} onCheckedChange={handleSwitchChange} />
|
|
||||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end mt-2">
|
<div>
|
||||||
<Show when={changed}>
|
<Label>{t("settings.notification.telegram.chat_id.label")}</Label>
|
||||||
<Button
|
<Input
|
||||||
onClick={() => {
|
placeholder={t("settings.notification.telegram.chat_id.placeholder")}
|
||||||
handleSaveClick();
|
value={telegram.data.chatId}
|
||||||
}}
|
onChange={(e) => {
|
||||||
>
|
const newData = {
|
||||||
{t("common.save")}
|
...telegram,
|
||||||
</Button>
|
data: {
|
||||||
</Show>
|
...telegram.data,
|
||||||
|
chatId: e.target.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
<Show when={!changed && telegram.id != ""}>
|
checkChanged(newData.data);
|
||||||
<Button
|
setTelegram(newData);
|
||||||
variant="secondary"
|
}}
|
||||||
onClick={() => {
|
/>
|
||||||
handlePushTestClick();
|
</div>
|
||||||
}}
|
|
||||||
>
|
<div className="flex justify-between gap-4">
|
||||||
{t("settings.notification.config.push.test.message")}
|
<div className="flex items-center space-x-1">
|
||||||
</Button>
|
<Switch id="airplane-mode" checked={telegram.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||||
</Show>
|
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<Show when={changed}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
handleSaveClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("common.save")}
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={!changed && telegram.id != ""}>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
loading={testing}
|
||||||
|
onClick={() => {
|
||||||
|
handlePushTestClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("settings.notification.push_test_message")}
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -97,7 +97,7 @@ const Webhook = () => {
|
|||||||
if (!isValidURL(webhook.data.url)) {
|
if (!isValidURL(webhook.data.url)) {
|
||||||
toast({
|
toast({
|
||||||
title: t("common.save.failed.message"),
|
title: t("common.save.failed.message"),
|
||||||
description: t("settings.notification.url.errmsg.invalid"),
|
description: t("common.errmsg.url_invalid"),
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -130,22 +130,29 @@ const Webhook = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [testing, setTesting] = useState<boolean>(false);
|
||||||
const handlePushTestClick = async () => {
|
const handlePushTestClick = async () => {
|
||||||
|
if (testing) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setTesting(true);
|
||||||
|
|
||||||
await notifyTest("webhook");
|
await notifyTest("webhook");
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("settings.notification.config.push.test.message.success.message"),
|
title: t("settings.notification.push_test_message.succeeded.message"),
|
||||||
description: t("settings.notification.config.push.test.message.success.message"),
|
description: t("settings.notification.push_test_message.succeeded.message"),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = getErrMessage(e);
|
const msg = getErrMessage(e);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("settings.notification.config.push.test.message.failed.message"),
|
title: t("settings.notification.push_test_message.failed.message"),
|
||||||
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
|
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setTesting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -184,50 +191,56 @@ const Webhook = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col space-y-4">
|
||||||
<Input
|
<div>
|
||||||
placeholder="Url"
|
<Label>{t("settings.notification.webhook.url.label")}</Label>
|
||||||
value={webhook.data.url}
|
<Input
|
||||||
onChange={(e) => {
|
placeholder={t("settings.notification.webhook.url.placeholder")}
|
||||||
const newData = {
|
value={webhook.data.url}
|
||||||
...webhook,
|
onChange={(e) => {
|
||||||
data: {
|
const newData = {
|
||||||
...webhook.data,
|
...webhook,
|
||||||
url: e.target.value,
|
data: {
|
||||||
},
|
...webhook.data,
|
||||||
};
|
url: e.target.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
checkChanged(newData.data);
|
checkChanged(newData.data);
|
||||||
setWebhook(newData);
|
setWebhook(newData);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center space-x-1 mt-2">
|
|
||||||
<Switch id="airplane-mode" checked={webhook.data.enabled} onCheckedChange={handleSwitchChange} />
|
|
||||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end mt-2">
|
<div className="flex justify-between gap-4">
|
||||||
<Show when={changed}>
|
<div className="flex items-center space-x-1">
|
||||||
<Button
|
<Switch id="airplane-mode" checked={webhook.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||||
onClick={() => {
|
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||||
handleSaveClick();
|
</div>
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("common.save")}
|
|
||||||
</Button>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={!changed && webhook.id != ""}>
|
<div className="flex items-center space-x-1">
|
||||||
<Button
|
<Show when={changed}>
|
||||||
variant="secondary"
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePushTestClick();
|
handleSaveClick();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("settings.notification.config.push.test.message")}
|
{t("common.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={!changed && webhook.id != ""}>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
loading={testing}
|
||||||
|
onClick={() => {
|
||||||
|
handlePushTestClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("settings.notification.push_test_message")}
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@ -32,11 +33,38 @@ const buttonVariants = cva(
|
|||||||
|
|
||||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, asChild = false, ...props }, ref) => {
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, loading, asChild = false, children, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "button";
|
if (asChild) {
|
||||||
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
|
return (
|
||||||
|
<Slot ref={ref} {...props}>
|
||||||
|
<>
|
||||||
|
{React.Children.map(children as React.ReactElement, (child: React.ReactElement) => {
|
||||||
|
return React.cloneElement(child, {
|
||||||
|
className: cn(buttonVariants({ variant, size }), className),
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
{loading && <Loader2 className={cn("h-4 w-4 animate-spin", children && "mr-2")} />}
|
||||||
|
{child.props.children}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
</Slot>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={cn(buttonVariants({ variant, size, className }))} disabled={loading} ref={ref} {...props}>
|
||||||
|
<>
|
||||||
|
{loading && <Loader2 className={cn("h-4 w-4 animate-spin", children && "mr-2")} />}
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
Button.displayName = "Button";
|
Button.displayName = "Button";
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ const BranchNode = memo(({ data }: BrandNodeProps) => {
|
|||||||
}}
|
}}
|
||||||
size={"sm"}
|
size={"sm"}
|
||||||
variant={"outline"}
|
variant={"outline"}
|
||||||
className="text-xs px-2 h-6 rounded-full absolute -top-3 left-[50%] -translate-x-1/2 z-10"
|
className="text-xs px-2 h-6 rounded-full absolute -top-3 left-[50%] -translate-x-1/2 z-10 dark:text-stone-200"
|
||||||
>
|
>
|
||||||
{t("workflow.node.addBranch.label")}
|
{t("workflow.node.addBranch.label")}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -34,7 +34,7 @@ const ConditionNode = ({ data, branchId, branchIndex }: NodeProps) => {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<div className="w-[261px] flex flex-col justify-center text-foreground rounded-md bg-white px-5 py-5">
|
<div className="w-[261px] flex flex-col justify-center text-foreground rounded-md bg-white px-5 py-5">
|
||||||
<div contentEditable suppressContentEditableWarning onBlur={handleNameBlur} className="text-center outline-slate-200">
|
<div contentEditable suppressContentEditableWarning onBlur={handleNameBlur} className="text-center outline-slate-200 dark:text-stone-600">
|
||||||
{data.name}
|
{data.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,7 +24,7 @@ export type DeployFormProps = {
|
|||||||
defaultProivder?: string;
|
defaultProivder?: string;
|
||||||
};
|
};
|
||||||
const DeployForm = ({ data, defaultProivder }: DeployFormProps) => {
|
const DeployForm = ({ data, defaultProivder }: DeployFormProps) => {
|
||||||
return getForm(data, defaultProivder);
|
return <div className="dark:text-stone-200">{getForm(data, defaultProivder)}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(DeployForm);
|
export default memo(DeployForm);
|
||||||
|
@ -39,7 +39,6 @@ const Node = ({ data }: NodeProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getSetting = () => {
|
const getSetting = () => {
|
||||||
console.log(data);
|
|
||||||
if (!data.validated) {
|
if (!data.validated) {
|
||||||
return <>{t(`${i18nPrefix}.setting.label`)}</>;
|
return <>{t(`${i18nPrefix}.setting.label`)}</>;
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,7 @@ const NotifyForm = ({ data }: NotifyFormProps) => {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
form.handleSubmit(onSubmit)(e);
|
form.handleSubmit(onSubmit)(e);
|
||||||
}}
|
}}
|
||||||
className="space-y-8"
|
className="space-y-8 dark:text-stone-200"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
@ -22,12 +22,12 @@ export type NotifyChannels = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type NotifyChannel =
|
export type NotifyChannel =
|
||||||
|
| NotifyChannelEmail
|
||||||
|
| NotifyChannelWebhook
|
||||||
| NotifyChannelDingTalk
|
| NotifyChannelDingTalk
|
||||||
| NotifyChannelLark
|
| NotifyChannelLark
|
||||||
| NotifyChannelTelegram
|
| NotifyChannelTelegram
|
||||||
| NotifyChannelWebhook
|
|
||||||
| NotifyChannelServerChan
|
| NotifyChannelServerChan
|
||||||
| NotifyChannelMail
|
|
||||||
| NotifyChannelBark;
|
| NotifyChannelBark;
|
||||||
|
|
||||||
type ChannelLabel = {
|
type ChannelLabel = {
|
||||||
@ -66,6 +66,21 @@ export const channels: ChannelLabel[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const channelLabelMap: Map<string, ChannelLabel> = new Map(channels.map((item) => [item.name, item]));
|
export const channelLabelMap: Map<string, ChannelLabel> = new Map(channels.map((item) => [item.name, item]));
|
||||||
|
export type NotifyChannelEmail = {
|
||||||
|
smtpHost: string;
|
||||||
|
smtpPort: number;
|
||||||
|
smtpTLS: boolean;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
senderAddress: string;
|
||||||
|
receiverAddress: string;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NotifyChannelWebhook = {
|
||||||
|
url: string;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type NotifyChannelDingTalk = {
|
export type NotifyChannelDingTalk = {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
@ -84,26 +99,11 @@ export type NotifyChannelTelegram = {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NotifyChannelWebhook = {
|
|
||||||
url: string;
|
|
||||||
enabled: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NotifyChannelServerChan = {
|
export type NotifyChannelServerChan = {
|
||||||
url: string;
|
url: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NotifyChannelMail = {
|
|
||||||
senderAddress: string;
|
|
||||||
receiverAddresses: string;
|
|
||||||
smtpHostAddr: string;
|
|
||||||
smtpHostPort: string;
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
enabled: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NotifyChannelBark = {
|
export type NotifyChannelBark = {
|
||||||
deviceKey: string;
|
deviceKey: string;
|
||||||
serverUrl: string;
|
serverUrl: string;
|
||||||
|
@ -3,6 +3,20 @@ import { nanoid } from "nanoid";
|
|||||||
import i18n from "@/i18n";
|
import i18n from "@/i18n";
|
||||||
import { deployTargets, KVType } from "./domain";
|
import { deployTargets, KVType } from "./domain";
|
||||||
|
|
||||||
|
export type Workflow = {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
type: string;
|
||||||
|
crontab?: string;
|
||||||
|
content?: WorkflowNode;
|
||||||
|
draft?: WorkflowNode;
|
||||||
|
enabled?: boolean;
|
||||||
|
hasDraft?: boolean;
|
||||||
|
created?: string;
|
||||||
|
updated?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export enum WorkflowNodeType {
|
export enum WorkflowNodeType {
|
||||||
Start = "start",
|
Start = "start",
|
||||||
End = "end",
|
End = "end",
|
||||||
@ -94,6 +108,31 @@ type NewWorkflowNodeOptions = {
|
|||||||
providerType?: string;
|
providerType?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initWorkflow = (): Workflow => {
|
||||||
|
// 开始节点
|
||||||
|
const rs = newWorkflowNode(WorkflowNodeType.Start, {});
|
||||||
|
let root = rs;
|
||||||
|
|
||||||
|
// 申请节点
|
||||||
|
root.next = newWorkflowNode(WorkflowNodeType.Apply, {});
|
||||||
|
root = root.next;
|
||||||
|
|
||||||
|
// 部署节点
|
||||||
|
root.next = newWorkflowNode(WorkflowNodeType.Deploy, {});
|
||||||
|
root = root.next;
|
||||||
|
|
||||||
|
// 通知节点
|
||||||
|
root.next = newWorkflowNode(WorkflowNodeType.Notify, {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: i18n.t("workflow.default.name"),
|
||||||
|
type: "auto",
|
||||||
|
crontab: "0 0 * * *",
|
||||||
|
enabled: false,
|
||||||
|
draft: rs,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const newWorkflowNode = (type: WorkflowNodeType, options: NewWorkflowNodeOptions): WorkflowNode | WorkflowBranchNode => {
|
export const newWorkflowNode = (type: WorkflowNodeType, options: NewWorkflowNodeOptions): WorkflowNode | WorkflowBranchNode => {
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
const typeName = workflowNodeTypeDefaultName.get(type) || "";
|
const typeName = workflowNodeTypeDefaultName.get(type) || "";
|
||||||
|
@ -83,12 +83,12 @@
|
|||||||
"common.provider.local": "Local Deployment",
|
"common.provider.local": "Local Deployment",
|
||||||
"common.provider.ssh": "SSH Deployment",
|
"common.provider.ssh": "SSH Deployment",
|
||||||
"common.provider.webhook": "Webhook",
|
"common.provider.webhook": "Webhook",
|
||||||
"common.provider.serverchan": "ServerChan",
|
|
||||||
"common.provider.kubernetes": "Kubernetes",
|
"common.provider.kubernetes": "Kubernetes",
|
||||||
"common.provider.kubernetes.secret": "Kubernetes - Secret",
|
"common.provider.kubernetes.secret": "Kubernetes - Secret",
|
||||||
|
"common.provider.email": "Email",
|
||||||
"common.provider.dingtalk": "DingTalk",
|
"common.provider.dingtalk": "DingTalk",
|
||||||
"common.provider.telegram": "Telegram",
|
|
||||||
"common.provider.lark": "Lark",
|
"common.provider.lark": "Lark",
|
||||||
"common.provider.mail": "Mail",
|
"common.provider.telegram": "Telegram",
|
||||||
|
"common.provider.serverchan": "ServerChan",
|
||||||
"common.provider.bark": "Bark"
|
"common.provider.bark": "Bark"
|
||||||
}
|
}
|
||||||
|
@ -30,20 +30,40 @@
|
|||||||
"settings.notification.config.enable": "Enable",
|
"settings.notification.config.enable": "Enable",
|
||||||
"settings.notification.config.saved.message": "Configuration saved successfully",
|
"settings.notification.config.saved.message": "Configuration saved successfully",
|
||||||
"settings.notification.config.failed.message": "Configuration save failed",
|
"settings.notification.config.failed.message": "Configuration save failed",
|
||||||
"settings.notification.config.push.test.message": "Send test notification",
|
"settings.notification.push_test_message": "Send test notification",
|
||||||
"settings.notification.config.push.test.message.failed.message": "Send test notification failed",
|
"settings.notification.push_test_message.succeeded.message": "Send test notification successfully",
|
||||||
"settings.notification.config.push.test.message.success.message": "Send test notification successfully",
|
"settings.notification.push_test_message.failed.message": "Send test notification failed",
|
||||||
"settings.notification.dingtalk.secret.placeholder": "Signature for signed addition",
|
"settings.notification.email.smtp_host.label": "SMTP Host",
|
||||||
"settings.notification.url.errmsg.invalid": "Invalid Url format",
|
"settings.notification.email.smtp_host.placeholder": "Please enter SMTP host",
|
||||||
"settings.notification.serverchan.url.placeholder": "Url, e.g. https://sctapi.ftqq.com/****************.send",
|
"settings.notification.email.smtp_port.label": "SMTP Port",
|
||||||
"settings.notification.mail.sender_address.placeholder": "Sender email address",
|
"settings.notification.email.smtp_port.placeholder": "Please enter SMTP port",
|
||||||
"settings.notification.mail.receiver_address.placeholder": "Receiver email address",
|
"settings.notification.email.smtp_tls.label": "Use TLS/SSL",
|
||||||
"settings.notification.mail.smtp_host.placeholder": "SMTP server address",
|
"settings.notification.email.username.label": "Username",
|
||||||
"settings.notification.mail.smtp_port.placeholder": "SMTP server port, if not set, default is 25",
|
"settings.notification.email.username.placeholder": "please enter username",
|
||||||
"settings.notification.mail.username.placeholder": "username",
|
"settings.notification.email.password.label": "Password",
|
||||||
"settings.notification.mail.password.placeholder": "password",
|
"settings.notification.email.password.placeholder": "please enter password",
|
||||||
"settings.notification.bark.serverUrl.placeholder": "Server URL, e.g. https://your-bark-server.com, leave it blank to use the bark default server",
|
"settings.notification.email.sender_address.label": "Sender Email Address",
|
||||||
"settings.notification.bark.deviceKey.placeholder": "Device Key,e.g. XXXXXXXXXXXXXXXXXXXX",
|
"settings.notification.email.sender_address.placeholder": "Please enter sender email address",
|
||||||
|
"settings.notification.email.receiver_address.label": "Receiver Email Address",
|
||||||
|
"settings.notification.email.receiver_address.placeholder": "Please enter receiver email address",
|
||||||
|
"settings.notification.webhook.url.label": "Webhook URL",
|
||||||
|
"settings.notification.webhook.url.placeholder": "Please enter Webhook URL",
|
||||||
|
"settings.notification.dingtalk.access_token.label": "AccessToken",
|
||||||
|
"settings.notification.dingtalk.access_token.placeholder": "Please enter access token",
|
||||||
|
"settings.notification.dingtalk.secret.label": "Secret",
|
||||||
|
"settings.notification.dingtalk.secret.placeholder": "Please enter secret",
|
||||||
|
"settings.notification.lark.webhook_url.label": "Webhook URL",
|
||||||
|
"settings.notification.lark.webhook_url.placeholder": "Please enter Webhook URL",
|
||||||
|
"settings.notification.telegram.api_token.label": "API Token",
|
||||||
|
"settings.notification.telegram.api_token.placeholder": "Please enter API token",
|
||||||
|
"settings.notification.telegram.chat_id.label": "Chat ID",
|
||||||
|
"settings.notification.telegram.chat_id.placeholder": "Please enter Telegram chat ID",
|
||||||
|
"settings.notification.serverchan.url.label": "Server URL",
|
||||||
|
"settings.notification.serverchan.url.placeholder": "Please enter server URL (e.g. https://sctapi.ftqq.com/*****.send)",
|
||||||
|
"settings.notification.bark.server_url.label": "Server URL",
|
||||||
|
"settings.notification.bark.server_url.placeholder": "Please enter server URL (e.g. https://your-bark-server.com. Leave it blank to use the bark default server)",
|
||||||
|
"settings.notification.bark.device_key.label": "Device Key",
|
||||||
|
"settings.notification.bark.device_key.placeholder": "Please enter device key",
|
||||||
|
|
||||||
"settings.ca.tab": "Certificate Authority",
|
"settings.ca.tab": "Certificate Authority",
|
||||||
"settings.ca.provider.errmsg.empty": "Please select a Certificate Authority",
|
"settings.ca.provider.errmsg.empty": "Please select a Certificate Authority",
|
||||||
|
@ -83,12 +83,12 @@
|
|||||||
"common.provider.local": "本地部署",
|
"common.provider.local": "本地部署",
|
||||||
"common.provider.ssh": "SSH 部署",
|
"common.provider.ssh": "SSH 部署",
|
||||||
"common.provider.webhook": "Webhook",
|
"common.provider.webhook": "Webhook",
|
||||||
"common.provider.serverchan": "Server酱",
|
|
||||||
"common.provider.kubernetes": "Kubernetes",
|
"common.provider.kubernetes": "Kubernetes",
|
||||||
"common.provider.kubernetes.secret": "Kubernetes - Secret",
|
"common.provider.kubernetes.secret": "Kubernetes - Secret",
|
||||||
|
"common.provider.email": "电子邮件",
|
||||||
"common.provider.dingtalk": "钉钉",
|
"common.provider.dingtalk": "钉钉",
|
||||||
"common.provider.telegram": "Telegram",
|
|
||||||
"common.provider.lark": "飞书",
|
"common.provider.lark": "飞书",
|
||||||
"common.provider.mail": "电子邮件",
|
"common.provider.telegram": "Telegram",
|
||||||
|
"common.provider.serverchan": "Server酱",
|
||||||
"common.provider.bark": "Bark"
|
"common.provider.bark": "Bark"
|
||||||
}
|
}
|
||||||
|
@ -30,20 +30,40 @@
|
|||||||
"settings.notification.config.enable": "是否启用",
|
"settings.notification.config.enable": "是否启用",
|
||||||
"settings.notification.config.saved.message": "配置保存成功",
|
"settings.notification.config.saved.message": "配置保存成功",
|
||||||
"settings.notification.config.failed.message": "配置保存失败",
|
"settings.notification.config.failed.message": "配置保存失败",
|
||||||
"settings.notification.config.push.test.message": "推送测试消息",
|
"settings.notification.push_test_message": "推送测试消息",
|
||||||
"settings.notification.config.push.test.message.failed.message": "推送测试消息失败",
|
"settings.notification.push_test_message.failed.message": "推送测试消息失败",
|
||||||
"settings.notification.config.push.test.message.success.message": "推送测试消息成功",
|
"settings.notification.push_test_message.succeeded.message": "推送测试消息成功",
|
||||||
"settings.notification.dingtalk.secret.placeholder": "加签的签名",
|
"settings.notification.email.smtp_host.label": "SMTP 服务器地址",
|
||||||
"settings.notification.url.errmsg.invalid": "URL 格式不正确",
|
"settings.notification.email.smtp_host.placeholder": "请输入 SMTP 服务器地址",
|
||||||
"settings.notification.serverchan.url.placeholder": "Url, 形如: https://sctapi.ftqq.com/****************.send",
|
"settings.notification.email.smtp_port.label": "SMTP 服务器端口",
|
||||||
"settings.notification.mail.sender_address.placeholder": "发送邮箱地址",
|
"settings.notification.email.smtp_port.placeholder": "请输入 SMTP 服务器端口",
|
||||||
"settings.notification.mail.receiver_address.placeholder": "接收邮箱地址",
|
"settings.notification.email.smtp_tls.label": "TLS/SSL 连接",
|
||||||
"settings.notification.mail.smtp_host.placeholder": "SMTP服务器地址",
|
"settings.notification.email.username.label": "用户名",
|
||||||
"settings.notification.mail.smtp_port.placeholder": "SMTP服务器端口, 如果未设置, 默认为25",
|
"settings.notification.email.username.placeholder": "请输入用户名",
|
||||||
"settings.notification.mail.username.placeholder": "用于登录到邮件服务器的用户名",
|
"settings.notification.email.password.label": "密码",
|
||||||
"settings.notification.mail.password.placeholder": "用于登录到邮件服务器的密码",
|
"settings.notification.email.password.placeholder": "请输入密码",
|
||||||
"settings.notification.bark.serverUrl.placeholder": "服务器URL,形如: https://your-bark-server.com, 留空则使用 Bark 默认服务器",
|
"settings.notification.email.sender_address.label": "发送邮箱地址",
|
||||||
"settings.notification.bark.deviceKey.placeholder": "设备密钥,形如: XXXXXXXXXXXXXXXXXXXX",
|
"settings.notification.email.sender_address.placeholder": "请输入发送邮箱地址",
|
||||||
|
"settings.notification.email.receiver_address.label": "接收邮箱地址",
|
||||||
|
"settings.notification.email.receiver_address.placeholder": "请输入接收邮箱地址",
|
||||||
|
"settings.notification.webhook.url.label": "Webhook 回调地址",
|
||||||
|
"settings.notification.webhook.url.placeholder": "请输入 Webhook 回调地址",
|
||||||
|
"settings.notification.dingtalk.access_token.label": "AccessToken",
|
||||||
|
"settings.notification.dingtalk.access_token.placeholder": "请输入 AccessToken",
|
||||||
|
"settings.notification.dingtalk.secret.label": "签名密钥",
|
||||||
|
"settings.notification.dingtalk.secret.placeholder": "请输入签名密钥",
|
||||||
|
"settings.notification.lark.webhook_url.label": "Webhook URL",
|
||||||
|
"settings.notification.lark.webhook_url.placeholder": "请输入 Webhook URL",
|
||||||
|
"settings.notification.telegram.api_token.label": "API Token",
|
||||||
|
"settings.notification.telegram.api_token.placeholder": "请输入 API token",
|
||||||
|
"settings.notification.telegram.chat_id.label": "会话 ID",
|
||||||
|
"settings.notification.telegram.chat_id.placeholder": "请输入 Telegram 会话 ID",
|
||||||
|
"settings.notification.serverchan.url.label": "服务器 URL",
|
||||||
|
"settings.notification.serverchan.url.placeholder": "请输入服务器 URL(形如: https://sctapi.ftqq.com/*****.send)",
|
||||||
|
"settings.notification.bark.server_url.label": "服务器 URL",
|
||||||
|
"settings.notification.bark.server_url.placeholder": "请输入服务器 URL(形如: https://your-bark-server.com;留空则使用 Bark 默认服务器)",
|
||||||
|
"settings.notification.bark.device_key.label": "设备密钥",
|
||||||
|
"settings.notification.bark.device_key.placeholder": "请输入设备密钥",
|
||||||
|
|
||||||
"settings.ca.tab": "证书颁发机构(CA)",
|
"settings.ca.tab": "证书颁发机构(CA)",
|
||||||
"settings.ca.provider.errmsg.empty": "请选择证书分发机构",
|
"settings.ca.provider.errmsg.empty": "请选择证书分发机构",
|
||||||
|
@ -7,7 +7,7 @@ import NotifyTemplate from "@/components/notify/NotifyTemplate";
|
|||||||
import Telegram from "@/components/notify/Telegram";
|
import Telegram from "@/components/notify/Telegram";
|
||||||
import Webhook from "@/components/notify/Webhook";
|
import Webhook from "@/components/notify/Webhook";
|
||||||
import ServerChan from "@/components/notify/ServerChan";
|
import ServerChan from "@/components/notify/ServerChan";
|
||||||
import Mail from "@/components/notify/Mail";
|
import Email from "@/components/notify/Email";
|
||||||
import Bark from "@/components/notify/Bark";
|
import Bark from "@/components/notify/Bark";
|
||||||
import { NotifyProvider } from "@/providers/notify";
|
import { NotifyProvider } from "@/providers/notify";
|
||||||
|
|
||||||
@ -27,51 +27,52 @@ const Notify = () => {
|
|||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border rounded-md p-5 mt-7 shadow-lg">
|
<div className="border rounded-md p-5 mt-7 shadow-lg">
|
||||||
<Accordion type={"single"} className="dark:text-stone-200">
|
<Accordion type={"single"} className="dark:text-stone-200">
|
||||||
<AccordionItem value="item-2" className="dark:border-stone-200">
|
<AccordionItem value="item-email" className="dark:border-stone-200">
|
||||||
<AccordionTrigger>{t("common.provider.dingtalk")}</AccordionTrigger>
|
<AccordionTrigger>{t("common.provider.email")}</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<DingTalk />
|
<Email />
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|
||||||
<AccordionItem value="item-3" className="dark:border-stone-200">
|
<AccordionItem value="item-webhook" className="dark:border-stone-200">
|
||||||
<AccordionTrigger>{t("common.provider.lark")}</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
<Lark />
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="item-4" className="dark:border-stone-200">
|
|
||||||
<AccordionTrigger>{t("common.provider.telegram")}</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
<Telegram />
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="item-5" className="dark:border-stone-200">
|
|
||||||
<AccordionTrigger>{t("common.provider.webhook")}</AccordionTrigger>
|
<AccordionTrigger>{t("common.provider.webhook")}</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<Webhook />
|
<Webhook />
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|
||||||
<AccordionItem value="item-6" className="dark:border-stone-200">
|
<AccordionItem value="item-dingtalk" className="dark:border-stone-200">
|
||||||
|
<AccordionTrigger>{t("common.provider.dingtalk")}</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<DingTalk />
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
<AccordionItem value="item-lark" className="dark:border-stone-200">
|
||||||
|
<AccordionTrigger>{t("common.provider.lark")}</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<Lark />
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
<AccordionItem value="item-telegram" className="dark:border-stone-200">
|
||||||
|
<AccordionTrigger>{t("common.provider.telegram")}</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<Telegram />
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
<AccordionItem value="item-serverchan" className="dark:border-stone-200">
|
||||||
<AccordionTrigger>{t("common.provider.serverchan")}</AccordionTrigger>
|
<AccordionTrigger>{t("common.provider.serverchan")}</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<ServerChan />
|
<ServerChan />
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|
||||||
<AccordionItem value="item-7" className="dark:border-stone-200">
|
<AccordionItem value="item-bark" className="dark:border-stone-200">
|
||||||
<AccordionTrigger>{t("common.provider.mail")}</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
<Mail />
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="item-8" className="dark:border-stone-200">
|
|
||||||
<AccordionTrigger>{t("common.provider.bark")}</AccordionTrigger>
|
<AccordionTrigger>{t("common.provider.bark")}</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<Bark />
|
<Bark />
|
||||||
|
102
ui/src/pages/workflow/WorkflowDetail.tsx
Normal file
102
ui/src/pages/workflow/WorkflowDetail.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import Show from "@/components/Show";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import End from "@/components/workflow/End";
|
||||||
|
import NodeRender from "@/components/workflow/NodeRender";
|
||||||
|
|
||||||
|
import WorkflowProvider from "@/components/workflow/WorkflowProvider";
|
||||||
|
import { WorkflowNode } from "@/domain/workflow";
|
||||||
|
import { useWorkflowStore, WorkflowState } from "@/providers/workflow";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
|
import { useShallow } from "zustand/shallow";
|
||||||
|
|
||||||
|
const selectState = (state: WorkflowState) => ({
|
||||||
|
workflow: state.workflow,
|
||||||
|
init: state.init,
|
||||||
|
switchEnable: state.switchEnable,
|
||||||
|
save: state.save,
|
||||||
|
});
|
||||||
|
|
||||||
|
const WorkflowDetail = () => {
|
||||||
|
// 3. 使用正确的选择器和 shallow 比较
|
||||||
|
const { workflow, init, switchEnable, save } = useWorkflowStore(useShallow(selectState));
|
||||||
|
|
||||||
|
// 从 url 中获取 workflowId
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const id = searchParams.get("id");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(id);
|
||||||
|
init(id ?? "");
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const elements = useMemo(() => {
|
||||||
|
let current = workflow.draft as WorkflowNode;
|
||||||
|
|
||||||
|
const elements: JSX.Element[] = [];
|
||||||
|
|
||||||
|
while (current) {
|
||||||
|
// 处理普通节点
|
||||||
|
elements.push(<NodeRender data={current} key={current.id} />);
|
||||||
|
current = current.next as WorkflowNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.push(<End key="workflow-end" />);
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}, [workflow]);
|
||||||
|
|
||||||
|
const handleBackClick = () => {
|
||||||
|
navigate("/workflow");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnableChange = () => {
|
||||||
|
switchEnable();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWorkflowSaveClick = () => {
|
||||||
|
save();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WorkflowProvider>
|
||||||
|
<ScrollArea className="h-[100vh] w-full relative bg-background">
|
||||||
|
<div className="h-16 sticky top-0 left-0 z-20 shadow-md bg-muted/40 flex justify-between items-center">
|
||||||
|
<div className="px-5 text-stone-700 dark:text-stone-200 flex items-center space-x-2">
|
||||||
|
<ArrowLeft className="cursor-pointer" onClick={handleBackClick} />
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
<div className="">工作流</div>
|
||||||
|
<div className="text-sm text-muted-foreground">工作流详情</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-5 flex items-center space-x-3">
|
||||||
|
<Show when={!!workflow.enabled}>
|
||||||
|
<Show when={!!workflow.hasDraft} fallback={<Button variant={"secondary"}>立即执行</Button>}>
|
||||||
|
<Button variant={"secondary"} onClick={handleWorkflowSaveClick}>
|
||||||
|
保存变更
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Switch className="dark:data-[state=unchecked]:bg-stone-400" checked={workflow.enabled ?? false} onCheckedChange={handleEnableChange} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className=" flex flex-col items-center mt-8">{elements}</div>
|
||||||
|
|
||||||
|
<ScrollBar orientation="vertical" />
|
||||||
|
<ScrollBar orientation="horizontal" />
|
||||||
|
</ScrollArea>
|
||||||
|
</WorkflowProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkflowDetail;
|
@ -1,50 +1,21 @@
|
|||||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
import { Button } from "@/components/ui/button";
|
||||||
import End from "@/components/workflow/End";
|
import { Plus } from "lucide-react";
|
||||||
import NodeRender from "@/components/workflow/NodeRender";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import WorkflowProvider from "@/components/workflow/WorkflowProvider";
|
|
||||||
import { WorkflowNode } from "@/domain/workflow";
|
|
||||||
import { useWorkflowStore, WorkflowState } from "@/providers/workflow";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
|
|
||||||
import { useShallow } from "zustand/shallow";
|
|
||||||
|
|
||||||
const selectState = (state: WorkflowState) => ({
|
|
||||||
root: state.root,
|
|
||||||
});
|
|
||||||
|
|
||||||
const Workflow = () => {
|
const Workflow = () => {
|
||||||
// 3. 使用正确的选择器和 shallow 比较
|
const navigate = useNavigate();
|
||||||
const { root } = useWorkflowStore(useShallow(selectState));
|
const handleCreateClick = () => {
|
||||||
|
navigate("/workflow/detail");
|
||||||
const elements = useMemo(() => {
|
};
|
||||||
let current = root;
|
|
||||||
|
|
||||||
const elements: JSX.Element[] = [];
|
|
||||||
|
|
||||||
while (current) {
|
|
||||||
// 处理普通节点
|
|
||||||
elements.push(<NodeRender data={current} key={current.id} />);
|
|
||||||
current = current.next as WorkflowNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
elements.push(<End key="workflow-end" />);
|
|
||||||
|
|
||||||
return elements;
|
|
||||||
}, [root]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<WorkflowProvider>
|
<div className="flex justify-between items-center">
|
||||||
<ScrollArea className="h-[100vh] w-full relative bg-background">
|
<div className="text-muted-foreground">工作流</div>
|
||||||
<div className="h-16 sticky top-0 left-0 z-20 shadow-md bg-muted/40"></div>
|
<Button onClick={handleCreateClick}>
|
||||||
|
<Plus size={16} />
|
||||||
<div className=" flex flex-col items-center mt-8">{elements}</div>
|
新建工作流
|
||||||
|
</Button>
|
||||||
<ScrollBar orientation="vertical" />
|
</div>
|
||||||
<ScrollBar orientation="horizontal" />
|
|
||||||
</ScrollArea>
|
|
||||||
</WorkflowProvider>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,99 +2,183 @@ import {
|
|||||||
addBranch,
|
addBranch,
|
||||||
addNode,
|
addNode,
|
||||||
getWorkflowOutputBeforeId,
|
getWorkflowOutputBeforeId,
|
||||||
|
initWorkflow,
|
||||||
removeBranch,
|
removeBranch,
|
||||||
removeNode,
|
removeNode,
|
||||||
updateNode,
|
updateNode,
|
||||||
|
Workflow,
|
||||||
WorkflowBranchNode,
|
WorkflowBranchNode,
|
||||||
WorkflowNode,
|
WorkflowNode,
|
||||||
WorkflowNodeType,
|
WorkflowNodeType,
|
||||||
} from "@/domain/workflow";
|
} from "@/domain/workflow";
|
||||||
|
import { save, get as getWrokflow } from "@/repository/workflow";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
export type WorkflowState = {
|
export type WorkflowState = {
|
||||||
root: WorkflowNode;
|
workflow: Workflow;
|
||||||
|
initialized: boolean;
|
||||||
updateNode: (node: WorkflowNode) => void;
|
updateNode: (node: WorkflowNode) => void;
|
||||||
addNode: (node: WorkflowNode, preId: string) => void;
|
addNode: (node: WorkflowNode, preId: string) => void;
|
||||||
addBranch: (branchId: string) => void;
|
addBranch: (branchId: string) => void;
|
||||||
removeNode: (nodeId: string) => void;
|
removeNode: (nodeId: string) => void;
|
||||||
removeBranch: (branchId: string, index: number) => void;
|
removeBranch: (branchId: string, index: number) => void;
|
||||||
getWorkflowOuptutBeforeId: (id: string, type: string) => WorkflowNode[];
|
getWorkflowOuptutBeforeId: (id: string, type: string) => WorkflowNode[];
|
||||||
|
switchEnable(): void;
|
||||||
|
save(): void;
|
||||||
|
init(id?: string): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||||
root: {
|
workflow: {
|
||||||
id: "1",
|
id: "root",
|
||||||
name: "开始",
|
name: "placeholder",
|
||||||
type: WorkflowNodeType.Start,
|
type: WorkflowNodeType.Start,
|
||||||
next: {
|
|
||||||
id: "2",
|
|
||||||
name: "分支",
|
|
||||||
type: WorkflowNodeType.Branch,
|
|
||||||
branches: [
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
name: "条件1",
|
|
||||||
type: WorkflowNodeType.Condition,
|
|
||||||
next: {
|
|
||||||
id: "4",
|
|
||||||
name: "条件2",
|
|
||||||
type: WorkflowNodeType.Apply,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "5",
|
|
||||||
name: "条件2",
|
|
||||||
type: WorkflowNodeType.Condition,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
updateNode: (node: WorkflowNode | WorkflowBranchNode) => {
|
initialized: false,
|
||||||
|
init: async (id?: string) => {
|
||||||
|
let data = {
|
||||||
|
name: "placeholder",
|
||||||
|
type: "auto",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
data = initWorkflow();
|
||||||
|
} else {
|
||||||
|
data = await getWrokflow(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
set({
|
||||||
|
workflow: data,
|
||||||
|
initialized: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
switchEnable: async () => {
|
||||||
|
const resp = await save({
|
||||||
|
id: (get().workflow.id as string) ?? "",
|
||||||
|
content: get().workflow.draft as WorkflowNode,
|
||||||
|
enabled: !get().workflow.enabled,
|
||||||
|
hasDraft: false,
|
||||||
|
});
|
||||||
set((state: WorkflowState) => {
|
set((state: WorkflowState) => {
|
||||||
const newRoot = updateNode(state.root, node);
|
|
||||||
console.log(newRoot);
|
|
||||||
return {
|
return {
|
||||||
root: newRoot,
|
workflow: {
|
||||||
|
...state.workflow,
|
||||||
|
id: resp.id,
|
||||||
|
content: resp.content,
|
||||||
|
enabled: resp.enabled,
|
||||||
|
hasDraft: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
addNode: (node: WorkflowNode | WorkflowBranchNode, preId: string) =>
|
save: async () => {
|
||||||
|
const resp = await save({
|
||||||
|
id: (get().workflow.id as string) ?? "",
|
||||||
|
content: get().workflow.draft as WorkflowNode,
|
||||||
|
hasDraft: false,
|
||||||
|
});
|
||||||
set((state: WorkflowState) => {
|
set((state: WorkflowState) => {
|
||||||
const newRoot = addNode(state.root, preId, node);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
root: newRoot,
|
workflow: {
|
||||||
|
...state.workflow,
|
||||||
|
id: resp.id,
|
||||||
|
content: resp.content,
|
||||||
|
hasDraft: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}),
|
});
|
||||||
addBranch: (branchId: string) =>
|
},
|
||||||
|
updateNode: async (node: WorkflowNode | WorkflowBranchNode) => {
|
||||||
|
const newRoot = updateNode(get().workflow.draft as WorkflowNode, node);
|
||||||
|
const resp = await save({
|
||||||
|
id: (get().workflow.id as string) ?? "",
|
||||||
|
draft: newRoot,
|
||||||
|
hasDraft: true,
|
||||||
|
});
|
||||||
set((state: WorkflowState) => {
|
set((state: WorkflowState) => {
|
||||||
const newRoot = addBranch(state.root, branchId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
root: newRoot,
|
workflow: {
|
||||||
|
...state.workflow,
|
||||||
|
draft: newRoot,
|
||||||
|
id: resp.id,
|
||||||
|
hasDraft: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
removeBranch: (branchId: string, index: number) =>
|
addNode: async (node: WorkflowNode | WorkflowBranchNode, preId: string) => {
|
||||||
|
const newRoot = addNode(get().workflow.draft as WorkflowNode, preId, node);
|
||||||
|
const resp = await save({
|
||||||
|
id: (get().workflow.id as string) ?? "",
|
||||||
|
draft: newRoot,
|
||||||
|
hasDraft: true,
|
||||||
|
});
|
||||||
set((state: WorkflowState) => {
|
set((state: WorkflowState) => {
|
||||||
const newRoot = removeBranch(state.root, branchId, index);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
root: newRoot,
|
workflow: {
|
||||||
|
...state.workflow,
|
||||||
|
draft: newRoot,
|
||||||
|
id: resp.id,
|
||||||
|
hasDraft: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
removeNode: (nodeId: string) =>
|
addBranch: async (branchId: string) => {
|
||||||
|
const newRoot = addBranch(get().workflow.draft as WorkflowNode, branchId);
|
||||||
|
const resp = await save({
|
||||||
|
id: (get().workflow.id as string) ?? "",
|
||||||
|
draft: newRoot,
|
||||||
|
hasDraft: true,
|
||||||
|
});
|
||||||
set((state: WorkflowState) => {
|
set((state: WorkflowState) => {
|
||||||
const newRoot = removeNode(state.root, nodeId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
root: newRoot,
|
workflow: {
|
||||||
|
...state.workflow,
|
||||||
|
draft: newRoot,
|
||||||
|
id: resp.id,
|
||||||
|
hasDraft: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
|
removeBranch: async (branchId: string, index: number) => {
|
||||||
|
const newRoot = removeBranch(get().workflow.draft as WorkflowNode, branchId, index);
|
||||||
|
const resp = await save({
|
||||||
|
id: (get().workflow.id as string) ?? "",
|
||||||
|
draft: newRoot,
|
||||||
|
hasDraft: true,
|
||||||
|
});
|
||||||
|
set((state: WorkflowState) => {
|
||||||
|
return {
|
||||||
|
workflow: {
|
||||||
|
...state.workflow,
|
||||||
|
draft: newRoot,
|
||||||
|
id: resp.id,
|
||||||
|
hasDraft: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
removeNode: async (nodeId: string) => {
|
||||||
|
const newRoot = removeNode(get().workflow.draft as WorkflowNode, nodeId);
|
||||||
|
const resp = await save({
|
||||||
|
id: (get().workflow.id as string) ?? "",
|
||||||
|
draft: newRoot,
|
||||||
|
hasDraft: true,
|
||||||
|
});
|
||||||
|
set((state: WorkflowState) => {
|
||||||
|
return {
|
||||||
|
workflow: {
|
||||||
|
...state.workflow,
|
||||||
|
draft: newRoot,
|
||||||
|
id: resp.id,
|
||||||
|
hasDraft: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
getWorkflowOuptutBeforeId: (id: string, type: string) => {
|
getWorkflowOuptutBeforeId: (id: string, type: string) => {
|
||||||
return getWorkflowOutputBeforeId(get().root, id, type);
|
return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
16
ui/src/repository/workflow.ts
Normal file
16
ui/src/repository/workflow.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Workflow, WorkflowNode } from "@/domain/workflow";
|
||||||
|
import { getPb } from "./api";
|
||||||
|
|
||||||
|
export const get = async (id: string) => {
|
||||||
|
const response = await getPb().collection("workflow").getOne<Workflow>(id);
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const save = async (data: Record<string, string | boolean | WorkflowNode>) => {
|
||||||
|
if (data.id) {
|
||||||
|
return await getPb()
|
||||||
|
.collection("workflow")
|
||||||
|
.update<Workflow>(data.id as string, data);
|
||||||
|
}
|
||||||
|
return await getPb().collection("workflow").create<Workflow>(data);
|
||||||
|
};
|
@ -14,6 +14,7 @@ import Account from "./pages/setting/Account";
|
|||||||
import Notify from "./pages/setting/Notify";
|
import Notify from "./pages/setting/Notify";
|
||||||
import SSLProvider from "./pages/setting/SSLProvider";
|
import SSLProvider from "./pages/setting/SSLProvider";
|
||||||
import Workflow from "./pages/workflow";
|
import Workflow from "./pages/workflow";
|
||||||
|
import WorkflowDetail from "./pages/workflow/WorkflowDetail";
|
||||||
|
|
||||||
export const router = createHashRouter([
|
export const router = createHashRouter([
|
||||||
{
|
{
|
||||||
@ -40,6 +41,10 @@ export const router = createHashRouter([
|
|||||||
path: "/history",
|
path: "/history",
|
||||||
element: <History />,
|
element: <History />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/workflow",
|
||||||
|
element: <Workflow />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/setting",
|
path: "/setting",
|
||||||
element: <SettingLayout />,
|
element: <SettingLayout />,
|
||||||
@ -75,7 +80,7 @@ export const router = createHashRouter([
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/about",
|
path: "/workflow/detail",
|
||||||
element: <Workflow />,
|
element: <WorkflowDetail />,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user