From d19de0edc2d942aa0727ea3083205b1501284f1e Mon Sep 17 00:00:00 2001 From: naiba Date: Sat, 19 Dec 2020 22:14:36 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(notification):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E9=80=9A=E7=9F=A5=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +- cmd/dashboard/controller/member_api.go | 69 ++++++++++++ cmd/dashboard/controller/member_page.go | 11 ++ cmd/dashboard/main.go | 2 +- model/notification.go | 100 ++++++++++++++++++ model/rule.go | 10 ++ model/server.go | 5 +- resource/static/main.js | 23 +++- resource/static/theme-hotaru/css/darkmode.css | 21 +--- resource/template/common/menu.html | 7 +- resource/template/component/notification.html | 47 ++++++++ resource/template/component/rule.html | 29 +++++ resource/template/component/server.html | 2 +- resource/template/dashboard/notification.html | 88 +++++++++++++++ resource/template/dashboard/setting.html | 2 +- resource/template/theme-hotaru/home.html | 26 ----- 16 files changed, 391 insertions(+), 58 deletions(-) create mode 100644 model/notification.go create mode 100644 model/rule.go create mode 100644 resource/template/component/notification.html create mode 100644 resource/template/component/rule.html create mode 100644 resource/template/dashboard/notification.html diff --git a/README.md b/README.md index 8711dc1..618ec9e 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ sudo ./nezha.sh ``` -## 主题自定义 +## 使用说明 +### 自定义 CSS - 默认主题更改进度条颜色示例 @@ -42,6 +43,10 @@ } ``` +### 通知 + + + ## 常见问题 ### 数据备份恢复 diff --git a/cmd/dashboard/controller/member_api.go b/cmd/dashboard/controller/member_api.go index b5d5a3b..c5d38ed 100644 --- a/cmd/dashboard/controller/member_api.go +++ b/cmd/dashboard/controller/member_api.go @@ -1,6 +1,7 @@ package controller import ( + "encoding/json" "fmt" "net/http" "strconv" @@ -30,8 +31,10 @@ func (ma *memberAPI) serve() { mr.POST("/logout", ma.logout) mr.POST("/server", ma.addOrEditServer) + mr.POST("/notification", ma.addOrEditNotification) mr.POST("/setting", ma.updateSetting) mr.DELETE("/server/:id", ma.delete) + mr.DELETE("/notification/:id", ma.deleteNotification) } func (ma *memberAPI) delete(c *gin.Context) { @@ -58,6 +61,27 @@ func (ma *memberAPI) delete(c *gin.Context) { }) } +func (ma *memberAPI) deleteNotification(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + if id < 1 { + c.JSON(http.StatusOK, model.Response{ + Code: http.StatusBadRequest, + Message: "错误的 Notification ID", + }) + return + } + if err := dao.DB.Delete(&model.Notification{}, "id = ?", id).Error; err != nil { + c.JSON(http.StatusOK, model.Response{ + Code: http.StatusBadRequest, + Message: fmt.Sprintf("数据库错误:%s", err), + }) + return + } + c.JSON(http.StatusOK, model.Response{ + Code: http.StatusOK, + }) +} + type serverForm struct { ID uint64 Name string `binding:"required"` @@ -96,6 +120,51 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) { }) } +type notificationForm struct { + ID uint64 + Name string + URL string + RequestMethod int + RequestType int + RequestBody string + VerifySSL string +} + +func (ma *memberAPI) addOrEditNotification(c *gin.Context) { + var nf notificationForm + var n model.Notification + err := c.ShouldBindJSON(&nf) + if err == nil { + var data map[string]string + err = json.Unmarshal([]byte(nf.RequestBody), &data) + } + if err == nil { + n.Name = nf.Name + n.RequestMethod = nf.RequestMethod + n.RequestType = nf.RequestType + n.RequestBody = nf.RequestBody + n.URL = nf.URL + verifySSL := nf.VerifySSL == "on" + n.VerifySSL = &verifySSL + n.ID = nf.ID + if n.ID == 0 { + err = dao.DB.Create(&n).Error + } else { + err = dao.DB.Save(&n).Error + } + } + if err != nil { + c.JSON(http.StatusOK, model.Response{ + Code: http.StatusBadRequest, + Message: fmt.Sprintf("请求错误:%s", err), + }) + return + } + c.JSON(http.StatusOK, model.Response{ + Code: http.StatusOK, + }) +} + type logoutForm struct { ID uint64 } diff --git a/cmd/dashboard/controller/member_page.go b/cmd/dashboard/controller/member_page.go index 07dfaba..5dcadd1 100644 --- a/cmd/dashboard/controller/member_page.go +++ b/cmd/dashboard/controller/member_page.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/naiba/nezha/model" "github.com/naiba/nezha/pkg/mygin" "github.com/naiba/nezha/service/dao" ) @@ -22,6 +23,7 @@ func (mp *memberPage) serve() { Redirect: "/login", })) mr.GET("/server", mp.server) + mr.GET("/notification", mp.notification) mr.GET("/setting", mp.setting) } @@ -34,6 +36,15 @@ func (mp *memberPage) server(c *gin.Context) { })) } +func (mp *memberPage) notification(c *gin.Context) { + var nf []model.Notification + dao.DB.Find(&nf) + c.HTML(http.StatusOK, "dashboard/notification", mygin.CommonEnvironment(c, gin.H{ + "Title": "通知管理", + "Notifications": nf, + })) +} + func (mp *memberPage) setting(c *gin.Context) { c.HTML(http.StatusOK, "dashboard/setting", mygin.CommonEnvironment(c, gin.H{ "Title": "系统设置", diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index 5f3e22d..a615f10 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -34,7 +34,7 @@ func init() { } func initDB() { - dao.DB.AutoMigrate(model.Server{}, model.User{}) + dao.DB.AutoMigrate(model.Server{}, model.User{}, model.Notification{}) // load cache var servers []model.Server dao.DB.Find(&servers) diff --git a/model/notification.go b/model/notification.go new file mode 100644 index 0000000..2f12fcc --- /dev/null +++ b/model/notification.go @@ -0,0 +1,100 @@ +package model + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + "time" +) + +const ( + _ = iota + NotificationRequestTypeJSON + NotificationRequestTypeForm +) + +const ( + _ = iota + NotificationRequestMethodGET + NotificationRequestMethodPOST +) + +type NotificatonSender struct { + Rule *Rule + Server *Server + State *State +} + +type Notification struct { + Common + Name string + URL string + RequestMethod int + RequestType int + RequestBody string `gorm:"type:longtext" ` + VerifySSL *bool +} + +func (n *Notification) Send(sender *NotificatonSender, message string) { + var verifySSL bool + + if n.VerifySSL != nil && *n.VerifySSL { + verifySSL = true + } + + var err error + transCfg := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: verifySSL}, + } + client := &http.Client{Transport: transCfg, Timeout: time.Minute * 10} + var reqURL *url.URL + reqURL, err = url.Parse(n.URL) + var data map[string]string + if err == nil && (n.RequestMethod == NotificationRequestMethodGET || n.RequestType == NotificationRequestTypeForm) { + err = json.Unmarshal([]byte(n.RequestBody), &data) + } + + if err == nil { + if n.RequestMethod == NotificationRequestMethodGET { + for k, v := range data { + reqURL.Query().Set(k, replaceParamsInString(v, sender)) + } + client.Get(reqURL.String()) + } else { + if n.RequestType == NotificationRequestTypeForm { + params := url.Values{} + for k, v := range data { + params.Add(k, replaceParamsInString(v, sender)) + } + client.PostForm(reqURL.String(), params) + } else { + jsonValue := replaceParamsInJSON(n.RequestBody, sender) + if err == nil { + client.Post(reqURL.String(), "application/json", strings.NewReader(jsonValue)) + } + } + } + } +} + +func replaceParamsInString(str string, sender *NotificatonSender) string { + str = strings.ReplaceAll(str, "#CPU#", fmt.Sprintf("%2f%%", sender.State.CPU)) + return str +} + +func replaceParamsInJSON(str string, sender *NotificatonSender) string { + str = strings.ReplaceAll(str, "#CPU#", fmt.Sprintf("%2f%%", sender.State.CPU)) + return str +} + +func jsonEscape(raw interface{}) string { + b, _ := json.Marshal(raw) + strb := string(b) + if strings.HasPrefix(strb, "\"") { + return strb[1 : len(strb)-1] + } + return strb +} diff --git a/model/rule.go b/model/rule.go new file mode 100644 index 0000000..78b9979 --- /dev/null +++ b/model/rule.go @@ -0,0 +1,10 @@ +package model + +type Rule struct { + Common + Name string + Type string // 指标类型,cpu、memory、swap、disk、net_in、net_out、net_all、transfer_in、transfer_out、transfer_all、offline + Min uint64 // 最小阈值 + Max uint64 // 最大阈值 + Duration uint64 // 持续时间 +} diff --git a/model/server.go b/model/server.go index 643d1ea..284e39c 100644 --- a/model/server.go +++ b/model/server.go @@ -2,6 +2,7 @@ package model import ( "fmt" + "html/template" "time" pb "github.com/naiba/nezha/proto" @@ -21,6 +22,6 @@ type Server struct { StreamClose chan<- error `gorm:"-" json:"-"` } -func (s Server) Marshal() string { - return fmt.Sprintf(`{"ID":%d,"Name":"%s","Secret":"%s"}`, s.ID, s.Name, s.Secret) +func (s Server) Marshal() template.JS { + return template.JS(fmt.Sprintf(`{"ID":%d,"Name":"%s","Secret":"%s"}`, s.ID, s.Name, s.Secret)) } diff --git a/resource/static/main.js b/resource/static/main.js index 1353053..26cd723 100644 --- a/resource/static/main.js +++ b/resource/static/main.js @@ -42,7 +42,7 @@ function showFormModal(modelSelector, formID, URL, getData) { form.children('.message').remove() btn.toggleClass('loading') const data = getData ? getData() : $(formID).serializeArray().reduce(function (obj, item) { - obj[item.name] = (item.name.endsWith('_id') || item.name === 'id') ? parseInt(item.value) : item.value; + obj[item.name] = (item.name.endsWith('_id') || item.name === 'id' || item.name === 'ID' || item.name === 'RequestType' || item.name === 'RequestMethod') ? parseInt(item.value) : item.value; return obj; }, {}); $.post(URL, JSON.stringify(data)).done(function (resp) { @@ -70,11 +70,26 @@ function showFormModal(modelSelector, formID, URL, getData) { }).modal('show') } +function addOrEditNotification(notification) { + const modal = $('.notification.modal') + modal.children('.header').text((notification ? '修改' : '添加') + '通知方式') + modal.find('.positive.button').html(notification ? '修改' : '添加') + modal.find('input[name=ID]').val(notification ? notification.ID : null) + modal.find('input[name=Name]').val(notification ? notification.Name : null) + modal.find('input[name=URL]').val(notification ? notification.URL : null) + modal.find('textarea[name=RequestBody]').val(notification ? notification.RequestBody : null) + modal.find('select[name=RequestMethod]').val(notification ? notification.RequestMethod : 1) + modal.find('select[name=RequestType]').val(notification ? notification.RequestType : 1) + if (notification && notification.VerifySSL) { + modal.find('.ui.checkbox').checkbox('set checked') + } else { + modal.find('.ui.checkbox').checkbox('set unchecked') + } + showFormModal('.notification.modal', '#notificationForm', '/api/notification') +} + function addOrEditServer(server) { const modal = $('.server.modal') - if (server) { - server = JSON.parse(server) - } modal.children('.header').text((server ? '修改' : '添加') + '服务器') modal.find('.positive.button').html(server ? '修改' : '添加') modal.find('input[name=id]').val(server ? server.ID : null) diff --git a/resource/static/theme-hotaru/css/darkmode.css b/resource/static/theme-hotaru/css/darkmode.css index 7c01654..83a518a 100644 --- a/resource/static/theme-hotaru/css/darkmode.css +++ b/resource/static/theme-hotaru/css/darkmode.css @@ -1,21 +1,3 @@ -/* Dark mode */ - -#darkmodeButton { - background-color: black; - width: 3rem; - height: 3rem; - position: fixed; - bottom: 2rem; - right: 0.5rem; - z-index: 9999; - border-radius: 50%; - outline: 0; -} - -body.dark #darkmodeButton { - background-color: white; -} - body.dark { background: #263236; color: #aaa; @@ -38,7 +20,8 @@ body.dark .panel { color: #aaa; } -body.dark .panel h3, body.dark .panel span { +body.dark .panel h3, +body.dark .panel span { color: #aaa; } diff --git a/resource/template/common/menu.html b/resource/template/common/menu.html index eeed1bb..723b8d8 100644 --- a/resource/template/common/menu.html +++ b/resource/template/common/menu.html @@ -4,10 +4,11 @@
- 首页 + 首页 {{if .Admin}} - 服务器 - 设置 + 服务器 + 通知 + 设置 {{end}}
取消
-
diff --git a/resource/template/dashboard/notification.html b/resource/template/dashboard/notification.html new file mode 100644 index 0000000..8b3fdbc --- /dev/null +++ b/resource/template/dashboard/notification.html @@ -0,0 +1,88 @@ +{{define "dashboard/notification"}} +{{template "common/header" .}} +{{template "common/menu" .}} +
+
+
+
+ +
+
+ + + + + + + + + + + {{range $notification := .Notifications}} + + + + + + + {{end}} + +
ID备注URL管理
{{$notification.ID}}{{$notification.Name}}{{$notification.URL}} +
+ + +
+
+
+
+ +
+
+ + + + + + + + + + + {{range $notification := .Notifications}} + + + + + + + {{end}} + +
ID备注URL管理
{{$notification.ID}}{{$notification.Name}}{{$notification.URL}} +
+ + +
+
+
+ +
+{{template "component/notification"}} +{{template "common/footer" .}} + +{{end}} \ No newline at end of file diff --git a/resource/template/dashboard/setting.html b/resource/template/dashboard/setting.html index 5ac37c1..add9657 100644 --- a/resource/template/dashboard/setting.html +++ b/resource/template/dashboard/setting.html @@ -20,7 +20,7 @@
- +
diff --git a/resource/template/theme-hotaru/home.html b/resource/template/theme-hotaru/home.html index 26663c5..55cf093 100644 --- a/resource/template/theme-hotaru/home.html +++ b/resource/template/theme-hotaru/home.html @@ -134,8 +134,6 @@ - -