mirror of
https://github.com/grafana/grafana.git
synced 2025-12-22 20:54:34 +08:00
Compare commits
30 Commits
sriram/pos
...
v5.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f14ad7445f | ||
|
|
946a6c7d59 | ||
|
|
990168c2af | ||
|
|
95ce4725fb | ||
|
|
7133cc1013 | ||
|
|
3600f0ec0b | ||
|
|
80c717cf6d | ||
|
|
9d58257be6 | ||
|
|
e1b554c61e | ||
|
|
a747ec349e | ||
|
|
d9f3c0931a | ||
|
|
f736e7aeb2 | ||
|
|
84d6e67e6b | ||
|
|
5ad76a29a0 | ||
|
|
5c6fd4f202 | ||
|
|
f13ffa3a52 | ||
|
|
55809ee92a | ||
|
|
a1db5561e0 | ||
|
|
3591e573f1 | ||
|
|
b29c2da7ef | ||
|
|
0f601dc4b1 | ||
|
|
5742c4d603 | ||
|
|
2226506a1d | ||
|
|
dcafc29bf2 | ||
|
|
1b16a1ae81 | ||
|
|
45e22da955 | ||
|
|
bc660ff3d5 | ||
|
|
426ae8bdd3 | ||
|
|
5bab016837 | ||
|
|
a7fcadd7c9 |
@@ -4,7 +4,7 @@
|
|||||||
"company": "Grafana Labs"
|
"company": "Grafana Labs"
|
||||||
},
|
},
|
||||||
"name": "grafana",
|
"name": "grafana",
|
||||||
"version": "5.0.0",
|
"version": "5.0.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "http://github.com/grafana/grafana.git"
|
"url": "http://github.com/grafana/grafana.git"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#! /usr/bin/env bash
|
#! /usr/bin/env bash
|
||||||
version=4.6.3
|
version=5.0.0
|
||||||
|
|
||||||
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${version}_amd64.deb
|
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${version}_amd64.deb
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ func (hs *HttpServer) registerRoutes() {
|
|||||||
r.Get("/plugins/:id/page/:page", reqSignedIn, Index)
|
r.Get("/plugins/:id/page/:page", reqSignedIn, Index)
|
||||||
|
|
||||||
r.Get("/d/:uid/:slug", reqSignedIn, Index)
|
r.Get("/d/:uid/:slug", reqSignedIn, Index)
|
||||||
|
r.Get("/d/:uid", reqSignedIn, Index)
|
||||||
r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardUrl, Index)
|
r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardUrl, Index)
|
||||||
r.Get("/dashboard/script/*", reqSignedIn, Index)
|
r.Get("/dashboard/script/*", reqSignedIn, Index)
|
||||||
r.Get("/dashboard-solo/snapshot/*", Index)
|
r.Get("/dashboard-solo/snapshot/*", Index)
|
||||||
@@ -150,11 +151,11 @@ func (hs *HttpServer) registerRoutes() {
|
|||||||
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
|
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
|
||||||
teamsRoute.Get("/:teamId", wrap(GetTeamById))
|
teamsRoute.Get("/:teamId", wrap(GetTeamById))
|
||||||
teamsRoute.Get("/search", wrap(SearchTeams))
|
teamsRoute.Get("/search", wrap(SearchTeams))
|
||||||
teamsRoute.Post("/", quota("teams"), bind(m.CreateTeamCommand{}), wrap(CreateTeam))
|
teamsRoute.Post("/", bind(m.CreateTeamCommand{}), wrap(CreateTeam))
|
||||||
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
|
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
|
||||||
teamsRoute.Delete("/:teamId", wrap(DeleteTeamById))
|
teamsRoute.Delete("/:teamId", wrap(DeleteTeamById))
|
||||||
teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers))
|
teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers))
|
||||||
teamsRoute.Post("/:teamId/members", quota("teams"), bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember))
|
teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember))
|
||||||
teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
|
teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
|
||||||
}, reqOrgAdmin)
|
}, reqOrgAdmin)
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if setting.DisableGravatar {
|
if setting.DisableGravatar {
|
||||||
data.User.GravatarUrl = setting.AppSubUrl + "/public/img/transparent.png"
|
data.User.GravatarUrl = setting.AppSubUrl + "/public/img/user_profile.png"
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(data.User.Name) == 0 {
|
if len(data.User.Name) == 0 {
|
||||||
|
|||||||
@@ -72,7 +72,9 @@ func RenderToPng(params *RenderOpts) (string, error) {
|
|||||||
localDomain = setting.HttpAddr
|
localDomain = setting.HttpAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
url := fmt.Sprintf("%s://%s:%s/%s", setting.Protocol, localDomain, setting.HttpPort, params.Path)
|
// &render=1 signals to the legacy redirect layer to
|
||||||
|
// avoid redirect these requests.
|
||||||
|
url := fmt.Sprintf("%s://%s:%s/%s&render=1", setting.Protocol, localDomain, setting.HttpPort, params.Path)
|
||||||
|
|
||||||
binPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, executable))
|
binPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, executable))
|
||||||
scriptPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "render.js"))
|
scriptPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "render.js"))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"gopkg.in/macaron.v1"
|
"gopkg.in/macaron.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,9 +37,14 @@ func RedirectFromLegacyDashboardUrl() macaron.Handler {
|
|||||||
func RedirectFromLegacyDashboardSoloUrl() macaron.Handler {
|
func RedirectFromLegacyDashboardSoloUrl() macaron.Handler {
|
||||||
return func(c *Context) {
|
return func(c *Context) {
|
||||||
slug := c.Params("slug")
|
slug := c.Params("slug")
|
||||||
|
renderRequest := c.QueryBool("render")
|
||||||
|
|
||||||
if slug != "" {
|
if slug != "" {
|
||||||
if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil {
|
if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil {
|
||||||
|
if renderRequest && strings.Contains(url, setting.AppSubUrl) {
|
||||||
|
url = strings.Replace(url, setting.AppSubUrl, "", 1)
|
||||||
|
}
|
||||||
|
|
||||||
url = strings.Replace(url, "/d/", "/d-solo/", 1)
|
url = strings.Replace(url, "/d/", "/d-solo/", 1)
|
||||||
url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery)
|
url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery)
|
||||||
c.Redirect(url, 301)
|
c.Redirect(url, 301)
|
||||||
|
|||||||
@@ -113,6 +113,18 @@ type SessionWrapper struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SessionWrapper) Start(c *Context) error {
|
func (s *SessionWrapper) Start(c *Context) error {
|
||||||
|
// See https://github.com/grafana/grafana/issues/11155 for details on why
|
||||||
|
// a recover and retry is needed
|
||||||
|
defer func() error {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
var retryErr error
|
||||||
|
s.session, retryErr = s.manager.Start(c.Context)
|
||||||
|
return retryErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
s.session, err = s.manager.Start(c.Context)
|
s.session, err = s.manager.Start(c.Context)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
|
|||||||
User: cmd.User,
|
User: cmd.User,
|
||||||
}
|
}
|
||||||
|
|
||||||
savedDash, err := dashboards.NewService().SaveDashboard(dto)
|
savedDash, err := dashboards.NewService().ImportDashboard(dto)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -74,6 +74,21 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
|||||||
|
|
||||||
for _, panelObj := range jsonWithPanels.Get("panels").MustArray() {
|
for _, panelObj := range jsonWithPanels.Get("panels").MustArray() {
|
||||||
panel := simplejson.NewFromAny(panelObj)
|
panel := simplejson.NewFromAny(panelObj)
|
||||||
|
|
||||||
|
collapsedJson, collapsed := panel.CheckGet("collapsed")
|
||||||
|
// check if the panel is collapsed
|
||||||
|
if collapsed && collapsedJson.MustBool() {
|
||||||
|
|
||||||
|
// extract alerts from sub panels for collapsed panels
|
||||||
|
als, err := e.GetAlertFromPanels(panel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
alerts = append(alerts, als...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
jsonAlert, hasAlert := panel.CheckGet("alert")
|
jsonAlert, hasAlert := panel.CheckGet("alert")
|
||||||
|
|
||||||
if !hasAlert {
|
if !hasAlert {
|
||||||
@@ -143,10 +158,15 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
|||||||
|
|
||||||
// validate
|
// validate
|
||||||
_, err = NewRuleFromDBAlert(alert)
|
_, err = NewRuleFromDBAlert(alert)
|
||||||
if err == nil && alert.ValidToSave() {
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if alert.ValidToSave() {
|
||||||
alerts = append(alerts, alert)
|
alerts = append(alerts, alert)
|
||||||
} else {
|
} else {
|
||||||
return nil, err
|
e.log.Debug("Invalid Alert Data. Dashboard, Org or Panel ID is not correct", "alertName", alert.Name, "panelId", alert.PanelId)
|
||||||
|
return nil, m.ErrDashboardContainsInvalidAlertData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
defaultDs := &m.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true}
|
defaultDs := &m.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true}
|
||||||
graphite2Ds := &m.DataSource{Id: 15, OrgId: 1, Name: "graphite2"}
|
graphite2Ds := &m.DataSource{Id: 15, OrgId: 1, Name: "graphite2"}
|
||||||
influxDBDs := &m.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB"}
|
influxDBDs := &m.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB"}
|
||||||
|
prom := &m.DataSource{Id: 17, OrgId: 1, Name: "Prometheus"}
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetDataSourcesQuery) error {
|
bus.AddHandler("test", func(query *m.GetDataSourcesQuery) error {
|
||||||
query.Result = []*m.DataSource{defaultDs, graphite2Ds}
|
query.Result = []*m.DataSource{defaultDs, graphite2Ds}
|
||||||
@@ -38,6 +39,10 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
if query.Name == influxDBDs.Name {
|
if query.Name == influxDBDs.Name {
|
||||||
query.Result = influxDBDs
|
query.Result = influxDBDs
|
||||||
}
|
}
|
||||||
|
if query.Name == prom.Name {
|
||||||
|
query.Result = prom
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -150,6 +155,22 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Panel with id set to zero should return error", func() {
|
||||||
|
panelWithIdZero, err := ioutil.ReadFile("./test-data/panel-with-id-0.json")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
dashJson, err := simplejson.NewJson([]byte(panelWithIdZero))
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
dash := m.NewDashboardFromJson(dashJson)
|
||||||
|
extractor := NewDashAlertExtractor(dash, 1)
|
||||||
|
|
||||||
|
_, err = extractor.GetAlerts()
|
||||||
|
|
||||||
|
Convey("panel with id 0 should return error", func() {
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
Convey("Parse alerts from dashboard without rows", func() {
|
Convey("Parse alerts from dashboard without rows", func() {
|
||||||
json, err := ioutil.ReadFile("./test-data/v5-dashboard.json")
|
json, err := ioutil.ReadFile("./test-data/v5-dashboard.json")
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
@@ -198,5 +219,26 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Should be able to extract collapsed panels", func() {
|
||||||
|
json, err := ioutil.ReadFile("./test-data/collapsed-panels.json")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
dashJson, err := simplejson.NewJson(json)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
dash := m.NewDashboardFromJson(dashJson)
|
||||||
|
extractor := NewDashAlertExtractor(dash, 1)
|
||||||
|
|
||||||
|
alerts, err := extractor.GetAlerts()
|
||||||
|
|
||||||
|
Convey("Get rules without error", func() {
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("should be able to extract collapsed alerts", func() {
|
||||||
|
So(len(alerts), ShouldEqual, 4)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
captionLengthLimit = 200
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
telegramApiUrl string = "https://api.telegram.org/bot%s/%s"
|
telegramApiUrl string = "https://api.telegram.org/bot%s/%s"
|
||||||
)
|
)
|
||||||
@@ -82,88 +86,81 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (this *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, sendImageInline bool) *m.SendWebhookSync {
|
func (this *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, sendImageInline bool) *m.SendWebhookSync {
|
||||||
var imageFile *os.File
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if sendImageInline {
|
if sendImageInline {
|
||||||
imageFile, err = os.Open(evalContext.ImageOnDiskPath)
|
cmd, err := this.buildMessageInlineImage(evalContext)
|
||||||
defer imageFile.Close()
|
if err == nil {
|
||||||
if err != nil {
|
return cmd
|
||||||
sendImageInline = false // fall back to text message
|
} else {
|
||||||
|
this.log.Error("Could not generate Telegram message with inline image.", "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message := ""
|
return this.buildMessageLinkedImage(evalContext)
|
||||||
|
}
|
||||||
|
|
||||||
if sendImageInline {
|
func (this *TelegramNotifier) buildMessageLinkedImage(evalContext *alerting.EvalContext) *m.SendWebhookSync {
|
||||||
// Telegram's API does not allow HTML formatting for image captions.
|
message := fmt.Sprintf("<b>%s</b>\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message)
|
||||||
message = fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message)
|
|
||||||
} else {
|
|
||||||
message = fmt.Sprintf("<b>%s</b>\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
ruleUrl, err := evalContext.GetRuleUrl()
|
ruleUrl, err := evalContext.GetRuleUrl()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
message = message + fmt.Sprintf("URL: %s\n", ruleUrl)
|
message = message + fmt.Sprintf("URL: %s\n", ruleUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !sendImageInline {
|
if evalContext.ImagePublicUrl != "" {
|
||||||
// only attach this if we are not sending it inline.
|
message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl)
|
||||||
if evalContext.ImagePublicUrl != "" {
|
|
||||||
message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
metrics := ""
|
|
||||||
fieldLimitCount := 4
|
|
||||||
for index, evt := range evalContext.EvalMatches {
|
|
||||||
metrics += fmt.Sprintf("\n%s: %s", evt.Metric, evt.Value)
|
|
||||||
if index > fieldLimitCount {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
metrics := generateMetricsMessage(evalContext)
|
||||||
if metrics != "" {
|
if metrics != "" {
|
||||||
if sendImageInline {
|
message = message + fmt.Sprintf("\n<i>Metrics:</i>%s", metrics)
|
||||||
// Telegram's API does not allow HTML formatting for image captions.
|
|
||||||
message = message + fmt.Sprintf("\nMetrics:%s", metrics)
|
|
||||||
} else {
|
|
||||||
message = message + fmt.Sprintf("\n<i>Metrics:</i>%s", metrics)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body bytes.Buffer
|
cmd := this.generateTelegramCmd(message, "text", "sendMessage", func(w *multipart.Writer) {
|
||||||
|
fw, _ := w.CreateFormField("parse_mode")
|
||||||
|
fw.Write([]byte("html"))
|
||||||
|
})
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.EvalContext) (*m.SendWebhookSync, error) {
|
||||||
|
var imageFile *os.File
|
||||||
|
var err error
|
||||||
|
|
||||||
|
imageFile, err = os.Open(evalContext.ImageOnDiskPath)
|
||||||
|
defer imageFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleUrl, err := evalContext.GetRuleUrl()
|
||||||
|
|
||||||
|
metrics := generateMetricsMessage(evalContext)
|
||||||
|
message := generateImageCaption(evalContext, ruleUrl, metrics)
|
||||||
|
|
||||||
|
cmd := this.generateTelegramCmd(message, "caption", "sendPhoto", func(w *multipart.Writer) {
|
||||||
|
fw, _ := w.CreateFormFile("photo", evalContext.ImageOnDiskPath)
|
||||||
|
io.Copy(fw, imageFile)
|
||||||
|
})
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *TelegramNotifier) generateTelegramCmd(message string, messageField string, apiAction string, extraConf func(writer *multipart.Writer)) *m.SendWebhookSync {
|
||||||
|
var body bytes.Buffer
|
||||||
w := multipart.NewWriter(&body)
|
w := multipart.NewWriter(&body)
|
||||||
|
|
||||||
fw, _ := w.CreateFormField("chat_id")
|
fw, _ := w.CreateFormField("chat_id")
|
||||||
fw.Write([]byte(this.ChatID))
|
fw.Write([]byte(this.ChatID))
|
||||||
|
|
||||||
if sendImageInline {
|
fw, _ = w.CreateFormField(messageField)
|
||||||
fw, _ = w.CreateFormField("caption")
|
fw.Write([]byte(message))
|
||||||
fw.Write([]byte(message))
|
|
||||||
|
|
||||||
fw, _ = w.CreateFormFile("photo", evalContext.ImageOnDiskPath)
|
extraConf(w)
|
||||||
io.Copy(fw, imageFile)
|
|
||||||
} else {
|
|
||||||
fw, _ = w.CreateFormField("text")
|
|
||||||
fw.Write([]byte(message))
|
|
||||||
|
|
||||||
fw, _ = w.CreateFormField("parse_mode")
|
|
||||||
fw.Write([]byte("html"))
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Close()
|
w.Close()
|
||||||
|
|
||||||
apiMethod := ""
|
this.log.Info("Sending telegram notification", "chat_id", this.ChatID, "bot_token", this.BotToken, "apiAction", apiAction)
|
||||||
if sendImageInline {
|
url := fmt.Sprintf(telegramApiUrl, this.BotToken, apiAction)
|
||||||
this.log.Info("Sending telegram image notification", "photo", evalContext.ImageOnDiskPath, "chat_id", this.ChatID, "bot_token", this.BotToken)
|
|
||||||
apiMethod = "sendPhoto"
|
|
||||||
} else {
|
|
||||||
this.log.Info("Sending telegram text notification", "chat_id", this.ChatID, "bot_token", this.BotToken)
|
|
||||||
apiMethod = "sendMessage"
|
|
||||||
}
|
|
||||||
|
|
||||||
url := fmt.Sprintf(telegramApiUrl, this.BotToken, apiMethod)
|
|
||||||
cmd := &m.SendWebhookSync{
|
cmd := &m.SendWebhookSync{
|
||||||
Url: url,
|
Url: url,
|
||||||
Body: body.String(),
|
Body: body.String(),
|
||||||
@@ -175,6 +172,50 @@ func (this *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, se
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateMetricsMessage(evalContext *alerting.EvalContext) string {
|
||||||
|
metrics := ""
|
||||||
|
fieldLimitCount := 4
|
||||||
|
for index, evt := range evalContext.EvalMatches {
|
||||||
|
metrics += fmt.Sprintf("\n%s: %s", evt.Metric, evt.Value)
|
||||||
|
if index > fieldLimitCount {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateImageCaption(evalContext *alerting.EvalContext, ruleUrl string, metrics string) string {
|
||||||
|
message := evalContext.GetNotificationTitle()
|
||||||
|
|
||||||
|
if len(evalContext.Rule.Message) > 0 {
|
||||||
|
message = fmt.Sprintf("%s\nMessage: %s", message, evalContext.Rule.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(message) > captionLengthLimit {
|
||||||
|
message = message[0:captionLengthLimit]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ruleUrl) > 0 {
|
||||||
|
urlLine := fmt.Sprintf("\nURL: %s", ruleUrl)
|
||||||
|
message = appendIfPossible(message, urlLine, captionLengthLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if metrics != "" {
|
||||||
|
metricsLines := fmt.Sprintf("\n\nMetrics:%s", metrics)
|
||||||
|
message = appendIfPossible(message, metricsLines, captionLengthLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
func appendIfPossible(message string, extra string, sizeLimit int) string {
|
||||||
|
if len(extra)+len(message) <= sizeLimit {
|
||||||
|
return message + extra
|
||||||
|
}
|
||||||
|
log.Debug("Line too long for image caption.", "value", extra)
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
func (this *TelegramNotifier) ShouldNotify(context *alerting.EvalContext) bool {
|
func (this *TelegramNotifier) ShouldNotify(context *alerting.EvalContext) bool {
|
||||||
return defaultShouldNotify(context)
|
return defaultShouldNotify(context)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,6 +51,71 @@ func TestTelegramNotifier(t *testing.T) {
|
|||||||
So(telegramNotifier.ChatID, ShouldEqual, "-1234567890")
|
So(telegramNotifier.ChatID, ShouldEqual, "-1234567890")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("generateCaption should generate a message with all pertinent details", func() {
|
||||||
|
evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
|
||||||
|
Name: "This is an alarm",
|
||||||
|
Message: "Some kind of message.",
|
||||||
|
State: m.AlertStateOK,
|
||||||
|
})
|
||||||
|
|
||||||
|
caption := generateImageCaption(evalContext, "http://grafa.url/abcdef", "")
|
||||||
|
So(len(caption), ShouldBeLessThanOrEqualTo, 200)
|
||||||
|
So(caption, ShouldContainSubstring, "Some kind of message.")
|
||||||
|
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
|
||||||
|
So(caption, ShouldContainSubstring, "http://grafa.url/abcdef")
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When generating a message", func() {
|
||||||
|
|
||||||
|
Convey("URL should be skipped if it's too long", func() {
|
||||||
|
evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
|
||||||
|
Name: "This is an alarm",
|
||||||
|
Message: "Some kind of message.",
|
||||||
|
State: m.AlertStateOK,
|
||||||
|
})
|
||||||
|
|
||||||
|
caption := generateImageCaption(evalContext,
|
||||||
|
"http://grafa.url/abcdefaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"foo bar")
|
||||||
|
So(len(caption), ShouldBeLessThanOrEqualTo, 200)
|
||||||
|
So(caption, ShouldContainSubstring, "Some kind of message.")
|
||||||
|
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
|
||||||
|
So(caption, ShouldContainSubstring, "foo bar")
|
||||||
|
So(caption, ShouldNotContainSubstring, "http")
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Message should be trimmed if it's too long", func() {
|
||||||
|
evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
|
||||||
|
Name: "This is an alarm",
|
||||||
|
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it.",
|
||||||
|
State: m.AlertStateOK,
|
||||||
|
})
|
||||||
|
|
||||||
|
caption := generateImageCaption(evalContext,
|
||||||
|
"http://grafa.url/foo",
|
||||||
|
"")
|
||||||
|
So(len(caption), ShouldBeLessThanOrEqualTo, 200)
|
||||||
|
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
|
||||||
|
So(caption, ShouldNotContainSubstring, "http")
|
||||||
|
So(caption, ShouldContainSubstring, "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise ")
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Metrics should be skipped if they dont fit", func() {
|
||||||
|
evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
|
||||||
|
Name: "This is an alarm",
|
||||||
|
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I ",
|
||||||
|
State: m.AlertStateOK,
|
||||||
|
})
|
||||||
|
|
||||||
|
caption := generateImageCaption(evalContext,
|
||||||
|
"http://grafa.url/foo",
|
||||||
|
"foo bar long song")
|
||||||
|
So(len(caption), ShouldBeLessThanOrEqualTo, 200)
|
||||||
|
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
|
||||||
|
So(caption, ShouldNotContainSubstring, "http")
|
||||||
|
So(caption, ShouldNotContainSubstring, "foo bar")
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
597
pkg/services/alerting/test-data/collapsed-panels.json
Normal file
597
pkg/services/alerting/test-data/collapsed-panels.json
Normal file
@@ -0,0 +1,597 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": "-- Grafana --",
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"gnetId": null,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": 127,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"gridPos": {
|
||||||
|
"h": 1,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 9,
|
||||||
|
"title": "Row title",
|
||||||
|
"type": "row"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alert": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"evaluator": {
|
||||||
|
"params": [
|
||||||
|
200
|
||||||
|
],
|
||||||
|
"type": "gt"
|
||||||
|
},
|
||||||
|
"operator": {
|
||||||
|
"type": "and"
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"params": [
|
||||||
|
"A",
|
||||||
|
"5m",
|
||||||
|
"now"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reducer": {
|
||||||
|
"params": [],
|
||||||
|
"type": "avg"
|
||||||
|
},
|
||||||
|
"type": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"executionErrorState": "alerting",
|
||||||
|
"frequency": "10s",
|
||||||
|
"handler": 1,
|
||||||
|
"name": "Panel Title alert",
|
||||||
|
"noDataState": "no_data",
|
||||||
|
"notifications": []
|
||||||
|
},
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 1
|
||||||
|
},
|
||||||
|
"id": 10,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "go_goroutines",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "{{job}}",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [
|
||||||
|
{
|
||||||
|
"colorMode": "critical",
|
||||||
|
"fill": true,
|
||||||
|
"line": true,
|
||||||
|
"op": "gt",
|
||||||
|
"value": 200
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Panel Title",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 1
|
||||||
|
},
|
||||||
|
"id": 14,
|
||||||
|
"limit": 10,
|
||||||
|
"links": [],
|
||||||
|
"onlyAlertsOnDashboard": true,
|
||||||
|
"show": "current",
|
||||||
|
"sortOrder": 1,
|
||||||
|
"stateFilter": [],
|
||||||
|
"title": "Panel Title",
|
||||||
|
"type": "alertlist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsed": true,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 1,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 10
|
||||||
|
},
|
||||||
|
"id": 6,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"alert": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"evaluator": {
|
||||||
|
"params": [
|
||||||
|
200
|
||||||
|
],
|
||||||
|
"type": "gt"
|
||||||
|
},
|
||||||
|
"operator": {
|
||||||
|
"type": "and"
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"params": [
|
||||||
|
"A",
|
||||||
|
"5m",
|
||||||
|
"now"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reducer": {
|
||||||
|
"params": [],
|
||||||
|
"type": "avg"
|
||||||
|
},
|
||||||
|
"type": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"executionErrorState": "alerting",
|
||||||
|
"frequency": "10s",
|
||||||
|
"handler": 1,
|
||||||
|
"name": "Panel 2 alert",
|
||||||
|
"noDataState": "no_data",
|
||||||
|
"notifications": []
|
||||||
|
},
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 11
|
||||||
|
},
|
||||||
|
"id": 11,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "go_goroutines",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "{{job}}",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [
|
||||||
|
{
|
||||||
|
"colorMode": "critical",
|
||||||
|
"fill": true,
|
||||||
|
"line": true,
|
||||||
|
"op": "gt",
|
||||||
|
"value": 200
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Panel 2",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alert": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"evaluator": {
|
||||||
|
"params": [
|
||||||
|
200
|
||||||
|
],
|
||||||
|
"type": "gt"
|
||||||
|
},
|
||||||
|
"operator": {
|
||||||
|
"type": "and"
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"params": [
|
||||||
|
"A",
|
||||||
|
"5m",
|
||||||
|
"now"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reducer": {
|
||||||
|
"params": [],
|
||||||
|
"type": "avg"
|
||||||
|
},
|
||||||
|
"type": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"executionErrorState": "alerting",
|
||||||
|
"frequency": "10s",
|
||||||
|
"handler": 1,
|
||||||
|
"name": "Panel 4 alert",
|
||||||
|
"noDataState": "no_data",
|
||||||
|
"notifications": []
|
||||||
|
},
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 11
|
||||||
|
},
|
||||||
|
"id": 15,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "go_goroutines",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "{{job}}",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [
|
||||||
|
{
|
||||||
|
"colorMode": "critical",
|
||||||
|
"fill": true,
|
||||||
|
"line": true,
|
||||||
|
"op": "gt",
|
||||||
|
"value": 200
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Panel 4",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Row title",
|
||||||
|
"type": "row"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"gridPos": {
|
||||||
|
"h": 1,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 11
|
||||||
|
},
|
||||||
|
"id": 4,
|
||||||
|
"title": "Row title",
|
||||||
|
"type": "row"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alert": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"evaluator": {
|
||||||
|
"params": [
|
||||||
|
200
|
||||||
|
],
|
||||||
|
"type": "gt"
|
||||||
|
},
|
||||||
|
"operator": {
|
||||||
|
"type": "and"
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"params": [
|
||||||
|
"A",
|
||||||
|
"5m",
|
||||||
|
"now"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reducer": {
|
||||||
|
"params": [],
|
||||||
|
"type": "avg"
|
||||||
|
},
|
||||||
|
"type": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"executionErrorState": "alerting",
|
||||||
|
"frequency": "10s",
|
||||||
|
"handler": 1,
|
||||||
|
"name": "Panel 3 alert",
|
||||||
|
"noDataState": "no_data",
|
||||||
|
"notifications": []
|
||||||
|
},
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 12
|
||||||
|
},
|
||||||
|
"id": 12,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "go_goroutines",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "{{job}}",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [
|
||||||
|
{
|
||||||
|
"colorMode": "critical",
|
||||||
|
"fill": true,
|
||||||
|
"line": true,
|
||||||
|
"op": "gt",
|
||||||
|
"value": 200
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Panel 3",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemaVersion": 16,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": [],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {
|
||||||
|
"refresh_intervals": [
|
||||||
|
"5s",
|
||||||
|
"10s",
|
||||||
|
"30s",
|
||||||
|
"1m",
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"30m",
|
||||||
|
"1h",
|
||||||
|
"2h",
|
||||||
|
"1d"
|
||||||
|
],
|
||||||
|
"time_options": [
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"1h",
|
||||||
|
"6h",
|
||||||
|
"12h",
|
||||||
|
"24h",
|
||||||
|
"2d",
|
||||||
|
"7d",
|
||||||
|
"30d"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timezone": "",
|
||||||
|
"title": "New dashboard Copy",
|
||||||
|
"uid": "6v5pg36zk",
|
||||||
|
"version": 17
|
||||||
|
}
|
||||||
63
pkg/services/alerting/test-data/panel-with-id-0.json
Normal file
63
pkg/services/alerting/test-data/panel-with-id-0.json
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"id": 57,
|
||||||
|
"title": "Graphite 4",
|
||||||
|
"originalTitle": "Graphite 4",
|
||||||
|
"tags": ["graphite"],
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"title": "Active desktop users",
|
||||||
|
"id": 0,
|
||||||
|
"editable": true,
|
||||||
|
"type": "graph",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"datasource": null,
|
||||||
|
"alert": {
|
||||||
|
"name": "name1",
|
||||||
|
"message": "desc1",
|
||||||
|
"handler": 1,
|
||||||
|
"frequency": "60s",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"type": "query",
|
||||||
|
"query": {"params": ["A", "5m", "now"]},
|
||||||
|
"reducer": {"type": "avg", "params": []},
|
||||||
|
"evaluator": {"type": ">", "params": [100]}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Active mobile users",
|
||||||
|
"id": 4,
|
||||||
|
"targets": [
|
||||||
|
{"refId": "A", "target": ""},
|
||||||
|
{"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
|
||||||
|
],
|
||||||
|
"datasource": "graphite2",
|
||||||
|
"alert": {
|
||||||
|
"name": "name2",
|
||||||
|
"message": "desc2",
|
||||||
|
"handler": 0,
|
||||||
|
"frequency": "60s",
|
||||||
|
"severity": "warning",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"type": "query",
|
||||||
|
"query": {"params": ["B", "5m", "now"]},
|
||||||
|
"reducer": {"type": "avg", "params": []},
|
||||||
|
"evaluator": {"type": ">", "params": [100]}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
// DashboardService service for operating on dashboards
|
// DashboardService service for operating on dashboards
|
||||||
type DashboardService interface {
|
type DashboardService interface {
|
||||||
SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
|
SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
|
||||||
|
ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DashboardProvisioningService service for operating on provisioned dashboards
|
// DashboardProvisioningService service for operating on provisioned dashboards
|
||||||
@@ -214,6 +215,20 @@ func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Da
|
|||||||
return cmd.Result, nil
|
return cmd.Result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
|
||||||
|
cmd, err := dr.buildSaveDashboardCommand(dto, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bus.Dispatch(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.Result, nil
|
||||||
|
}
|
||||||
|
|
||||||
type FakeDashboardService struct {
|
type FakeDashboardService struct {
|
||||||
SaveDashboardResult *models.Dashboard
|
SaveDashboardResult *models.Dashboard
|
||||||
SaveDashboardError error
|
SaveDashboardError error
|
||||||
@@ -230,6 +245,10 @@ func (s *FakeDashboardService) SaveDashboard(dto *SaveDashboardDTO) (*models.Das
|
|||||||
return s.SaveDashboardResult, s.SaveDashboardError
|
return s.SaveDashboardResult, s.SaveDashboardError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *FakeDashboardService) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
|
||||||
|
return s.SaveDashboard(dto)
|
||||||
|
}
|
||||||
|
|
||||||
func MockDashboardService(mock *FakeDashboardService) {
|
func MockDashboardService(mock *FakeDashboardService) {
|
||||||
NewService = func() DashboardService {
|
NewService = func() DashboardService {
|
||||||
return mock
|
return mock
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ type UserInfoJson struct {
|
|||||||
|
|
||||||
func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
|
func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
|
||||||
var data UserInfoJson
|
var data UserInfoJson
|
||||||
|
var err error
|
||||||
|
|
||||||
if s.extractToken(&data, token) != true {
|
if s.extractToken(&data, token) != true {
|
||||||
response, err := HttpGet(client, s.apiUrl)
|
response, err := HttpGet(client, s.apiUrl)
|
||||||
@@ -193,20 +194,17 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
name, err := s.extractName(data)
|
name := s.extractName(&data)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
email := s.extractEmail(&data)
|
||||||
|
if email == "" {
|
||||||
|
email, err = s.FetchPrivateEmail(client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
email, err := s.extractEmail(data, client)
|
login := s.extractLogin(&data, email)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
login, err := s.extractLogin(data, email)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
userInfo := &BasicUserInfo{
|
userInfo := &BasicUserInfo{
|
||||||
Name: name,
|
Name: name,
|
||||||
@@ -251,49 +249,55 @@ func (s *SocialGenericOAuth) extractToken(data *UserInfoJson, token *oauth2.Toke
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
email := s.extractEmail(data)
|
||||||
|
if email == "" {
|
||||||
|
s.log.Debug("No email found in id_token", "json", string(payload), "data", data)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
s.log.Debug("Received id_token", "json", string(payload), "data", data)
|
s.log.Debug("Received id_token", "json", string(payload), "data", data)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SocialGenericOAuth) extractEmail(data UserInfoJson, client *http.Client) (string, error) {
|
func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson) string {
|
||||||
if data.Email != "" {
|
if data.Email != "" {
|
||||||
return data.Email, nil
|
return data.Email
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.Attributes["email:primary"] != nil {
|
if data.Attributes["email:primary"] != nil {
|
||||||
return data.Attributes["email:primary"][0], nil
|
return data.Attributes["email:primary"][0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.Upn != "" {
|
if data.Upn != "" {
|
||||||
emailAddr, emailErr := mail.ParseAddress(data.Upn)
|
emailAddr, emailErr := mail.ParseAddress(data.Upn)
|
||||||
if emailErr == nil {
|
if emailErr == nil {
|
||||||
return emailAddr.Address, nil
|
return emailAddr.Address
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.FetchPrivateEmail(client)
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SocialGenericOAuth) extractLogin(data UserInfoJson, email string) (string, error) {
|
func (s *SocialGenericOAuth) extractLogin(data *UserInfoJson, email string) string {
|
||||||
if data.Login != "" {
|
if data.Login != "" {
|
||||||
return data.Login, nil
|
return data.Login
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.Username != "" {
|
if data.Username != "" {
|
||||||
return data.Username, nil
|
return data.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
return email, nil
|
return email
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SocialGenericOAuth) extractName(data UserInfoJson) (string, error) {
|
func (s *SocialGenericOAuth) extractName(data *UserInfoJson) string {
|
||||||
if data.Name != "" {
|
if data.Name != "" {
|
||||||
return data.Name, nil
|
return data.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.DisplayName != "" {
|
if data.DisplayName != "" {
|
||||||
return data.DisplayName, nil
|
return data.DisplayName
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", nil
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ func generateConnectionString(datasource *models.DataSource) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sslmode := datasource.JsonData.Get("sslmode").MustString("verify-full")
|
sslmode := datasource.JsonData.Get("sslmode").MustString("verify-full")
|
||||||
return fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", url.PathEscape(datasource.User), url.PathEscape(password), url.PathEscape(datasource.Url), url.PathEscape(datasource.Database), url.QueryEscape(sslmode))
|
u := &url.URL{Scheme: "postgres", User: url.UserPassword(datasource.User, password), Host: datasource.Url, Path: datasource.Database, RawQuery: "sslmode=" + sslmode}
|
||||||
|
return u.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *PostgresQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
func (e *PostgresQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export function geminiScrollbar() {
|
|||||||
link: function(scope, elem, attrs) {
|
link: function(scope, elem, attrs) {
|
||||||
let scrollbar = new PerfectScrollbar(elem[0], {
|
let scrollbar = new PerfectScrollbar(elem[0], {
|
||||||
wheelPropagation: true,
|
wheelPropagation: true,
|
||||||
|
wheelSpeed: 3,
|
||||||
});
|
});
|
||||||
let lastPos = 0;
|
let lastPos = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -1,73 +1,78 @@
|
|||||||
<a class="sidemenu__logo" ng-click="ctrl.toggleSideMenu()">
|
<a class="sidemenu__logo" ng-click="ctrl.toggleSideMenu()">
|
||||||
<img src="public/img/grafana_icon.svg"></img>
|
<img src="public/img/grafana_icon.svg"></img>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="sidemenu__logo_small_breakpoint" ng-click="ctrl.toggleSideMenuSmallBreakpoint()">
|
<a class="sidemenu__logo_small_breakpoint" ng-click="ctrl.toggleSideMenuSmallBreakpoint()">
|
||||||
<i class="fa fa-bars"></i>
|
<i class="fa fa-bars"></i>
|
||||||
<span class="sidemenu__close"><i class="fa fa-times"></i> Close</span>
|
<span class="sidemenu__close">
|
||||||
|
<i class="fa fa-times"></i> Close</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="sidemenu__top">
|
<div class="sidemenu__top">
|
||||||
<div ng-repeat="item in ::ctrl.mainLinks" class="sidemenu-item dropdown">
|
<div ng-repeat="item in ::ctrl.mainLinks" class="sidemenu-item dropdown">
|
||||||
<a href="{{::item.url}}" class="sidemenu-link" target="{{::item.target}}">
|
<a href="{{::item.url}}" class="sidemenu-link" target="{{::item.target}}">
|
||||||
<span class="icon-circle sidemenu-icon">
|
<span class="icon-circle sidemenu-icon">
|
||||||
<i class="{{::item.icon}}" ng-show="::item.icon"></i>
|
<i class="{{::item.icon}}" ng-show="::item.icon"></i>
|
||||||
<img ng-src="{{::item.img}}" ng-show="::item.img">
|
<img ng-src="{{::item.img}}" ng-show="::item.img">
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu" ng-if="::item.children">
|
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu" ng-if="::item.children">
|
||||||
<li class="side-menu-header">
|
<li class="side-menu-header">
|
||||||
<span class="sidemenu-item-text">{{::item.text}}</span>
|
<span class="sidemenu-item-text">{{::item.text}}</span>
|
||||||
</li>
|
</li>
|
||||||
<li ng-repeat="child in ::item.children" ng-class="{divider: child.divider}">
|
<li ng-repeat="child in ::item.children" ng-class="{divider: child.divider}">
|
||||||
<a href="{{::child.url}}">
|
<a href="{{::child.url}}">
|
||||||
<i class="{{::child.icon}}" ng-show="::child.icon"></i>
|
<i class="{{::child.icon}}" ng-show="::child.icon"></i>
|
||||||
{{::child.text}}
|
{{::child.text}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidemenu__bottom">
|
<div class="sidemenu__bottom">
|
||||||
<div ng-show="::!ctrl.isSignedIn" class="sidemenu-item">
|
<div ng-show="::!ctrl.isSignedIn" class="sidemenu-item">
|
||||||
<a href="{{ctrl.loginUrl}}" class="sidemenu-link" target="_self">
|
<a href="{{ctrl.loginUrl}}" class="sidemenu-link" target="_self">
|
||||||
<span class="icon-circle sidemenu-icon"><i class="fa fa-fw fa-sign-in"></i></span>
|
<span class="icon-circle sidemenu-icon">
|
||||||
</a>
|
<i class="fa fa-fw fa-sign-in"></i>
|
||||||
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
</span>
|
||||||
<li class="side-menu-header">
|
</a>
|
||||||
<span class="sidemenu-item-text">Sign In</span>
|
<a href="{{ctrl.loginUrl}}">
|
||||||
</li>
|
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
||||||
</ul>
|
<li class="side-menu-header">
|
||||||
</div>
|
<span class="sidemenu-item-text">Sign In</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div ng-repeat="item in ::ctrl.bottomNav" class="sidemenu-item dropdown dropup">
|
<div ng-repeat="item in ::ctrl.bottomNav" class="sidemenu-item dropdown dropup">
|
||||||
<a href="{{::item.url}}" class="sidemenu-link" target="{{::item.target}}">
|
<a href="{{::item.url}}" class="sidemenu-link" target="{{::item.target}}">
|
||||||
<span class="icon-circle sidemenu-icon">
|
<span class="icon-circle sidemenu-icon">
|
||||||
<i class="{{::item.icon}}" ng-show="::item.icon"></i>
|
<i class="{{::item.icon}}" ng-show="::item.icon"></i>
|
||||||
<img ng-src="{{::item.img}}" ng-show="::item.img">
|
<img ng-src="{{::item.img}}" ng-show="::item.img">
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
||||||
<li ng-if="item.showOrgSwitcher" class="sidemenu-org-switcher">
|
<li ng-if="item.showOrgSwitcher" class="sidemenu-org-switcher">
|
||||||
<a ng-click="ctrl.switchOrg()">
|
<a ng-click="ctrl.switchOrg()">
|
||||||
<div>
|
<div>
|
||||||
<div class="sidemenu-org-switcher__org-name">{{ctrl.contextSrv.user.orgName}}</div>
|
<div class="sidemenu-org-switcher__org-name">{{ctrl.contextSrv.user.orgName}}</div>
|
||||||
<div class="sidemenu-org-switcher__org-current">Current Org:</div>
|
<div class="sidemenu-org-switcher__org-current">Current Org:</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidemenu-org-switcher__switch"><i class="fa fa-fw fa-random"></i>Switch</div>
|
<div class="sidemenu-org-switcher__switch">
|
||||||
</a>
|
<i class="fa fa-fw fa-random"></i>Switch</div>
|
||||||
</li>
|
</a>
|
||||||
<li ng-repeat="child in ::item.children" ng-class="{divider: child.divider}" ng-hide="::child.hideFromMenu">
|
</li>
|
||||||
<a href="{{::child.url}}" target="{{::child.target}}" ng-click="ctrl.itemClicked(child, $event)">
|
<li ng-repeat="child in ::item.children" ng-class="{divider: child.divider}" ng-hide="::child.hideFromMenu">
|
||||||
<i class="{{::child.icon}}" ng-show="::child.icon"></i>
|
<a href="{{::child.url}}" target="{{::child.target}}" ng-click="ctrl.itemClicked(child, $event)">
|
||||||
{{::child.text}}
|
<i class="{{::child.icon}}" ng-show="::child.icon"></i>
|
||||||
</a>
|
{{::child.text}}
|
||||||
</li>
|
</a>
|
||||||
<li class="side-menu-header">
|
</li>
|
||||||
<span class="sidemenu-item-text">{{::item.text}}</span>
|
<li class="side-menu-header">
|
||||||
</li>
|
<span class="sidemenu-item-text">{{::item.text}}</span>
|
||||||
</ul>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { PanelModel } from '../panel_model';
|
|||||||
import { PanelContainer } from './PanelContainer';
|
import { PanelContainer } from './PanelContainer';
|
||||||
import templateSrv from 'app/features/templating/template_srv';
|
import templateSrv from 'app/features/templating/template_srv';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
|
||||||
export interface DashboardRowProps {
|
export interface DashboardRowProps {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
@@ -94,14 +95,16 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
|||||||
{title}
|
{title}
|
||||||
<span className="dashboard-row__panel_count">({hiddenPanels} hidden panels)</span>
|
<span className="dashboard-row__panel_count">({hiddenPanels} hidden panels)</span>
|
||||||
</a>
|
</a>
|
||||||
<div className="dashboard-row__actions">
|
{config.bootData.user.orgRole !== 'Viewer' && (
|
||||||
<a className="pointer" onClick={this.openSettings}>
|
<div className="dashboard-row__actions">
|
||||||
<i className="fa fa-cog" />
|
<a className="pointer" onClick={this.openSettings}>
|
||||||
</a>
|
<i className="fa fa-cog" />
|
||||||
<a className="pointer" onClick={this.delete}>
|
</a>
|
||||||
<i className="fa fa-trash" />
|
<a className="pointer" onClick={this.delete}>
|
||||||
</a>
|
<i className="fa fa-trash" />
|
||||||
</div>
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="dashboard-row__drag grid-drag-handle" />
|
<div className="dashboard-row__drag grid-drag-handle" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,19 +2,26 @@ import React from 'react';
|
|||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { DashboardRow } from '../dashgrid/DashboardRow';
|
import { DashboardRow } from '../dashgrid/DashboardRow';
|
||||||
import { PanelModel } from '../panel_model';
|
import { PanelModel } from '../panel_model';
|
||||||
|
import config from '../../../core/config';
|
||||||
|
|
||||||
describe('DashboardRow', () => {
|
describe('DashboardRow', () => {
|
||||||
let wrapper, panel, getPanelContainer, dashboardMock;
|
let wrapper, panel, getPanelContainer, dashboardMock;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dashboardMock = {toggleRow: jest.fn()};
|
dashboardMock = { toggleRow: jest.fn() };
|
||||||
|
|
||||||
|
config.bootData = {
|
||||||
|
user: {
|
||||||
|
orgRole: 'Admin',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
getPanelContainer = jest.fn().mockReturnValue({
|
getPanelContainer = jest.fn().mockReturnValue({
|
||||||
getDashboard: jest.fn().mockReturnValue(dashboardMock),
|
getDashboard: jest.fn().mockReturnValue(dashboardMock),
|
||||||
getPanelLoader: jest.fn()
|
getPanelLoader: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
panel = new PanelModel({collapsed: false});
|
panel = new PanelModel({ collapsed: false });
|
||||||
wrapper = shallow(<DashboardRow panel={panel} getPanelContainer={getPanelContainer} />);
|
wrapper = shallow(<DashboardRow panel={panel} getPanelContainer={getPanelContainer} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -30,4 +37,14 @@ describe('DashboardRow', () => {
|
|||||||
expect(dashboardMock.toggleRow.mock.calls).toHaveLength(1);
|
expect(dashboardMock.toggleRow.mock.calls).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should have two actions as admin', () => {
|
||||||
|
expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have zero actions as viewer', () => {
|
||||||
|
config.bootData.user.orgRole = 'Viewer';
|
||||||
|
panel = new PanelModel({ collapsed: false });
|
||||||
|
wrapper = shallow(<DashboardRow panel={panel} getPanelContainer={getPanelContainer} />);
|
||||||
|
expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ describe('unsavedChangesSrv', function() {
|
|||||||
expect(tracker.hasChanges()).to.be(false);
|
expect(tracker.hasChanges()).to.be(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should ignore .iteration changes', () => {
|
||||||
|
dash.iteration = new Date().getTime() + 1;
|
||||||
|
expect(tracker.hasChanges()).to.be(false);
|
||||||
|
});
|
||||||
|
|
||||||
it.skip('Should ignore row collapse change', function() {
|
it.skip('Should ignore row collapse change', function() {
|
||||||
dash.rows[0].collapse = true;
|
dash.rows[0].collapse = true;
|
||||||
expect(tracker.hasChanges()).to.be(false);
|
expect(tracker.hasChanges()).to.be(false);
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ export class Tracker {
|
|||||||
dash.refresh = 0;
|
dash.refresh = 0;
|
||||||
dash.schemaVersion = 0;
|
dash.schemaVersion = 0;
|
||||||
|
|
||||||
|
// ignore iteration property
|
||||||
|
delete dash.iteration;
|
||||||
|
|
||||||
// filter row and panels properties that should be ignored
|
// filter row and panels properties that should be ignored
|
||||||
dash.rows = _.filter(dash.rows, function(row) {
|
dash.rows = _.filter(dash.rows, function(row) {
|
||||||
if (row.repeatRowId) {
|
if (row.repeatRowId) {
|
||||||
|
|||||||
@@ -348,7 +348,7 @@
|
|||||||
"tableColumn": "",
|
"tableColumn": "",
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"expr": "tsdb_wal_corruptions_total{job=\"prometheus\"}",
|
"expr": "prometheus_tsdb_wal_corruptions_total{job=\"prometheus\"}",
|
||||||
"format": "time_series",
|
"format": "time_series",
|
||||||
"intervalFactor": 2,
|
"intervalFactor": 2,
|
||||||
"legendFormat": "",
|
"legendFormat": "",
|
||||||
@@ -1048,7 +1048,7 @@
|
|||||||
"steppedLine": false,
|
"steppedLine": false,
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"expr": "max(prometheus_evaluator_duration_seconds{job=\"prometheus\", quantile!=\"0.01\", quantile!=\"0.05\"}) by (quantile)",
|
"expr": "max(prometheus_rule_group_duration_seconds{job=\"prometheus\"}) by (quantile)",
|
||||||
"format": "time_series",
|
"format": "time_series",
|
||||||
"interval": "",
|
"interval": "",
|
||||||
"intervalFactor": 2,
|
"intervalFactor": 2,
|
||||||
@@ -1060,7 +1060,7 @@
|
|||||||
"thresholds": [],
|
"thresholds": [],
|
||||||
"timeFrom": null,
|
"timeFrom": null,
|
||||||
"timeShift": null,
|
"timeShift": null,
|
||||||
"title": "Rule Eval Duration",
|
"title": "Rule Group Eval Duration",
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"shared": true,
|
"shared": true,
|
||||||
"sort": 0,
|
"sort": 0,
|
||||||
@@ -1124,7 +1124,7 @@
|
|||||||
"steppedLine": false,
|
"steppedLine": false,
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"expr": "rate(prometheus_evaluator_iterations_missed_total{job=\"prometheus\"}[5m])",
|
"expr": "rate(prometheus_rule_group_iterations_missed_total{job=\"prometheus\"}[5m])",
|
||||||
"format": "time_series",
|
"format": "time_series",
|
||||||
"intervalFactor": 2,
|
"intervalFactor": 2,
|
||||||
"legendFormat": "missed",
|
"legendFormat": "missed",
|
||||||
@@ -1132,15 +1132,7 @@
|
|||||||
"step": 10
|
"step": 10
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"expr": "rate(prometheus_evaluator_iterations_skipped_total{job=\"prometheus\"}[5m])",
|
"expr": "rate(prometheus_rule_group_iterations_total{job=\"prometheus\"}[5m])",
|
||||||
"format": "time_series",
|
|
||||||
"intervalFactor": 2,
|
|
||||||
"legendFormat": "skipped",
|
|
||||||
"refId": "C",
|
|
||||||
"step": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expr": "rate(prometheus_evaluator_iterations_total{job=\"prometheus\"}[5m])",
|
|
||||||
"format": "time_series",
|
"format": "time_series",
|
||||||
"intervalFactor": 2,
|
"intervalFactor": 2,
|
||||||
"legendFormat": "iterations",
|
"legendFormat": "iterations",
|
||||||
@@ -1151,7 +1143,7 @@
|
|||||||
"thresholds": [],
|
"thresholds": [],
|
||||||
"timeFrom": null,
|
"timeFrom": null,
|
||||||
"timeShift": null,
|
"timeShift": null,
|
||||||
"title": "Rule Eval Activity",
|
"title": "Rule Group Eval Activity",
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"shared": true,
|
"shared": true,
|
||||||
"sort": 0,
|
"sort": 0,
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
|||||||
reloadOnSearch: false,
|
reloadOnSearch: false,
|
||||||
pageClass: 'page-dashboard',
|
pageClass: 'page-dashboard',
|
||||||
})
|
})
|
||||||
|
.when('/d/:uid', {
|
||||||
|
templateUrl: 'public/app/partials/dashboard.html',
|
||||||
|
controller: 'LoadDashboardCtrl',
|
||||||
|
reloadOnSearch: false,
|
||||||
|
pageClass: 'page-dashboard',
|
||||||
|
})
|
||||||
.when('/dashboard/:type/:slug', {
|
.when('/dashboard/:type/:slug', {
|
||||||
templateUrl: 'public/app/partials/dashboard.html',
|
templateUrl: 'public/app/partials/dashboard.html',
|
||||||
controller: 'LoadDashboardCtrl',
|
controller: 'LoadDashboardCtrl',
|
||||||
@@ -98,6 +104,11 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
|||||||
controller: 'FolderDashboardsCtrl',
|
controller: 'FolderDashboardsCtrl',
|
||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
})
|
})
|
||||||
|
.when('/dashboards/f/:uid', {
|
||||||
|
templateUrl: 'public/app/features/dashboard/partials/folder_dashboards.html',
|
||||||
|
controller: 'FolderDashboardsCtrl',
|
||||||
|
controllerAs: 'ctrl',
|
||||||
|
})
|
||||||
.when('/org', {
|
.when('/org', {
|
||||||
templateUrl: 'public/app/features/org/partials/orgDetails.html',
|
templateUrl: 'public/app/features/org/partials/orgDetails.html',
|
||||||
controller: 'OrgDetailsCtrl',
|
controller: 'OrgDetailsCtrl',
|
||||||
|
|||||||
11
public/img/resize-handle-white.svg
Normal file
11
public/img/resize-handle-white.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!-- Generator: Adobe Fireworks CS6, Export SVG Extension by Aaron Beall (http://fireworks.abeall.com) . Version: 0.6.1 -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg id="Untitled-Page%201" viewBox="0 0 6 6" style="background-color:#ffffff00" version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
|
||||||
|
x="0px" y="0px" width="6px" height="6px"
|
||||||
|
>
|
||||||
|
<g opacity="0.302">
|
||||||
|
<path d="M 6 6 L 0 6 L 0 4.2 L 4 4.2 L 4.2 4.2 L 4.2 0 L 6 0 L 6 6 L 6 6 Z" fill="#FFFFFF"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 630 B |
@@ -259,6 +259,7 @@ $navbar-button-border: #2f2f32;
|
|||||||
// Sidemenu
|
// Sidemenu
|
||||||
// -------------------------
|
// -------------------------
|
||||||
$side-menu-bg: $black;
|
$side-menu-bg: $black;
|
||||||
|
$side-menu-bg-mobile: $side-menu-bg;
|
||||||
$side-menu-item-hover-bg: $dark-2;
|
$side-menu-item-hover-bg: $dark-2;
|
||||||
$side-menu-shadow: 0 0 20px black;
|
$side-menu-shadow: 0 0 20px black;
|
||||||
$side-menu-link-color: $link-color;
|
$side-menu-link-color: $link-color;
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ $input-invalid-border-color: lighten($red, 5%);
|
|||||||
// Sidemenu
|
// Sidemenu
|
||||||
// -------------------------
|
// -------------------------
|
||||||
$side-menu-bg: $dark-2;
|
$side-menu-bg: $dark-2;
|
||||||
|
$side-menu-bg-mobile: rgba(0, 0, 0, 0); //$gray-6;
|
||||||
$side-menu-item-hover-bg: $gray-1;
|
$side-menu-item-hover-bg: $gray-1;
|
||||||
$side-menu-shadow: 5px 0px 10px -5px $gray-1;
|
$side-menu-shadow: 5px 0px 10px -5px $gray-1;
|
||||||
$side-menu-link-color: $gray-6;
|
$side-menu-link-color: $gray-6;
|
||||||
|
|||||||
@@ -44,6 +44,11 @@
|
|||||||
border-right: 2px solid $gray-1;
|
border-right: 2px solid $gray-1;
|
||||||
border-bottom: 2px solid $gray-1;
|
border-bottom: 2px solid $gray-1;
|
||||||
}
|
}
|
||||||
|
// temp fix since we use old commit of grid component
|
||||||
|
// this can be removed when we revert to non fork grid component
|
||||||
|
.react-grid-item > .react-resizable-handle {
|
||||||
|
background-image: url('../img/resize-handle-white.svg');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-light {
|
.theme-light {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
// important to overlap it otherwise it can be hidden
|
// important to overlap it otherwise it can be hidden
|
||||||
// again by the mouse getting outside the hover space
|
// again by the mouse getting outside the hover space
|
||||||
left: $side-menu-width - 2px;
|
left: $side-menu-width - 2px;
|
||||||
@include animation("dropdown-anim 150ms ease-in-out 100ms forwards");
|
@include animation('dropdown-anim 150ms ease-in-out 100ms forwards');
|
||||||
z-index: $zindex-sidemenu;
|
z-index: $zindex-sidemenu;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,9 +193,13 @@ li.sidemenu-org-switcher {
|
|||||||
|
|
||||||
@include media-breakpoint-down(xs) {
|
@include media-breakpoint-down(xs) {
|
||||||
.sidemenu-open--xs {
|
.sidemenu-open--xs {
|
||||||
|
li {
|
||||||
|
font-size: $font-size-lg;
|
||||||
|
}
|
||||||
|
|
||||||
.sidemenu {
|
.sidemenu {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: $side-menu-bg;
|
background: $side-menu-bg-mobile;
|
||||||
position: initial;
|
position: initial;
|
||||||
height: auto;
|
height: auto;
|
||||||
box-shadow: $side-menu-shadow;
|
box-shadow: $side-menu-shadow;
|
||||||
@@ -214,6 +218,9 @@ li.sidemenu-org-switcher {
|
|||||||
.sidemenu__bottom {
|
.sidemenu__bottom {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
.sidemenu-item {
|
||||||
|
border-right: 2px solid transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidemenu {
|
.sidemenu {
|
||||||
|
|||||||
Reference in New Issue
Block a user