mirror of
https://github.com/grafana/grafana.git
synced 2025-12-20 19:44:55 +08:00
Compare commits
30 Commits
KD/lazy-lo
...
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"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "5.0.0",
|
||||
"version": "5.0.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#! /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
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ func (hs *HttpServer) registerRoutes() {
|
||||
r.Get("/plugins/:id/page/:page", 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/script/*", reqSignedIn, Index)
|
||||
r.Get("/dashboard-solo/snapshot/*", Index)
|
||||
@@ -150,11 +151,11 @@ func (hs *HttpServer) registerRoutes() {
|
||||
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
|
||||
teamsRoute.Get("/:teamId", wrap(GetTeamById))
|
||||
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.Delete("/:teamId", wrap(DeleteTeamById))
|
||||
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))
|
||||
}, reqOrgAdmin)
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -72,7 +72,9 @@ func RenderToPng(params *RenderOpts) (string, error) {
|
||||
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))
|
||||
scriptPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "render.js"))
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
@@ -36,9 +37,14 @@ func RedirectFromLegacyDashboardUrl() macaron.Handler {
|
||||
func RedirectFromLegacyDashboardSoloUrl() macaron.Handler {
|
||||
return func(c *Context) {
|
||||
slug := c.Params("slug")
|
||||
renderRequest := c.QueryBool("render")
|
||||
|
||||
if slug != "" {
|
||||
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 = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery)
|
||||
c.Redirect(url, 301)
|
||||
|
||||
@@ -113,6 +113,18 @@ type SessionWrapper struct {
|
||||
}
|
||||
|
||||
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
|
||||
s.session, err = s.manager.Start(c.Context)
|
||||
return err
|
||||
|
||||
@@ -80,7 +80,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
|
||||
User: cmd.User,
|
||||
}
|
||||
|
||||
savedDash, err := dashboards.NewService().SaveDashboard(dto)
|
||||
savedDash, err := dashboards.NewService().ImportDashboard(dto)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -74,6 +74,21 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
||||
|
||||
for _, panelObj := range jsonWithPanels.Get("panels").MustArray() {
|
||||
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")
|
||||
|
||||
if !hasAlert {
|
||||
@@ -143,10 +158,15 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
||||
|
||||
// validate
|
||||
_, err = NewRuleFromDBAlert(alert)
|
||||
if err == nil && alert.ValidToSave() {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if alert.ValidToSave() {
|
||||
alerts = append(alerts, alert)
|
||||
} 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}
|
||||
graphite2Ds := &m.DataSource{Id: 15, OrgId: 1, Name: "graphite2"}
|
||||
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 {
|
||||
query.Result = []*m.DataSource{defaultDs, graphite2Ds}
|
||||
@@ -38,6 +39,10 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
if query.Name == influxDBDs.Name {
|
||||
query.Result = influxDBDs
|
||||
}
|
||||
if query.Name == prom.Name {
|
||||
query.Result = prom
|
||||
}
|
||||
|
||||
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() {
|
||||
json, err := ioutil.ReadFile("./test-data/v5-dashboard.json")
|
||||
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"
|
||||
)
|
||||
|
||||
const (
|
||||
captionLengthLimit = 200
|
||||
)
|
||||
|
||||
var (
|
||||
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 {
|
||||
var imageFile *os.File
|
||||
var err error
|
||||
|
||||
if sendImageInline {
|
||||
imageFile, err = os.Open(evalContext.ImageOnDiskPath)
|
||||
defer imageFile.Close()
|
||||
if err != nil {
|
||||
sendImageInline = false // fall back to text message
|
||||
cmd, err := this.buildMessageInlineImage(evalContext)
|
||||
if err == nil {
|
||||
return cmd
|
||||
} else {
|
||||
this.log.Error("Could not generate Telegram message with inline image.", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
message := ""
|
||||
return this.buildMessageLinkedImage(evalContext)
|
||||
}
|
||||
|
||||
if sendImageInline {
|
||||
// Telegram's API does not allow HTML formatting for image captions.
|
||||
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)
|
||||
}
|
||||
func (this *TelegramNotifier) buildMessageLinkedImage(evalContext *alerting.EvalContext) *m.SendWebhookSync {
|
||||
message := fmt.Sprintf("<b>%s</b>\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message)
|
||||
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err == nil {
|
||||
message = message + fmt.Sprintf("URL: %s\n", ruleUrl)
|
||||
}
|
||||
|
||||
if !sendImageInline {
|
||||
// only attach this if we are not sending it inline.
|
||||
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
|
||||
}
|
||||
if evalContext.ImagePublicUrl != "" {
|
||||
message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl)
|
||||
}
|
||||
|
||||
metrics := generateMetricsMessage(evalContext)
|
||||
if metrics != "" {
|
||||
if sendImageInline {
|
||||
// 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)
|
||||
}
|
||||
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)
|
||||
|
||||
fw, _ := w.CreateFormField("chat_id")
|
||||
fw.Write([]byte(this.ChatID))
|
||||
|
||||
if sendImageInline {
|
||||
fw, _ = w.CreateFormField("caption")
|
||||
fw.Write([]byte(message))
|
||||
fw, _ = w.CreateFormField(messageField)
|
||||
fw.Write([]byte(message))
|
||||
|
||||
fw, _ = w.CreateFormFile("photo", evalContext.ImageOnDiskPath)
|
||||
io.Copy(fw, imageFile)
|
||||
} else {
|
||||
fw, _ = w.CreateFormField("text")
|
||||
fw.Write([]byte(message))
|
||||
|
||||
fw, _ = w.CreateFormField("parse_mode")
|
||||
fw.Write([]byte("html"))
|
||||
}
|
||||
extraConf(w)
|
||||
|
||||
w.Close()
|
||||
|
||||
apiMethod := ""
|
||||
if sendImageInline {
|
||||
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"
|
||||
}
|
||||
this.log.Info("Sending telegram notification", "chat_id", this.ChatID, "bot_token", this.BotToken, "apiAction", apiAction)
|
||||
url := fmt.Sprintf(telegramApiUrl, this.BotToken, apiAction)
|
||||
|
||||
url := fmt.Sprintf(telegramApiUrl, this.BotToken, apiMethod)
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: url,
|
||||
Body: body.String(),
|
||||
@@ -175,6 +172,50 @@ func (this *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, se
|
||||
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 {
|
||||
return defaultShouldNotify(context)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
@@ -50,6 +51,71 @@ func TestTelegramNotifier(t *testing.T) {
|
||||
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
|
||||
type DashboardService interface {
|
||||
SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
|
||||
ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
|
||||
}
|
||||
|
||||
// DashboardProvisioningService service for operating on provisioned dashboards
|
||||
@@ -214,6 +215,20 @@ func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Da
|
||||
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 {
|
||||
SaveDashboardResult *models.Dashboard
|
||||
SaveDashboardError error
|
||||
@@ -230,6 +245,10 @@ func (s *FakeDashboardService) SaveDashboard(dto *SaveDashboardDTO) (*models.Das
|
||||
return s.SaveDashboardResult, s.SaveDashboardError
|
||||
}
|
||||
|
||||
func (s *FakeDashboardService) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
|
||||
return s.SaveDashboard(dto)
|
||||
}
|
||||
|
||||
func MockDashboardService(mock *FakeDashboardService) {
|
||||
NewService = func() DashboardService {
|
||||
return mock
|
||||
|
||||
@@ -180,6 +180,7 @@ type UserInfoJson struct {
|
||||
|
||||
func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
|
||||
var data UserInfoJson
|
||||
var err error
|
||||
|
||||
if s.extractToken(&data, token) != true {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
name := s.extractName(&data)
|
||||
|
||||
email := s.extractEmail(&data)
|
||||
if email == "" {
|
||||
email, err = s.FetchPrivateEmail(client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
email, err := s.extractEmail(data, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
login, err := s.extractLogin(data, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
login := s.extractLogin(&data, email)
|
||||
|
||||
userInfo := &BasicUserInfo{
|
||||
Name: name,
|
||||
@@ -251,49 +249,55 @@ func (s *SocialGenericOAuth) extractToken(data *UserInfoJson, token *oauth2.Toke
|
||||
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)
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *SocialGenericOAuth) extractEmail(data UserInfoJson, client *http.Client) (string, error) {
|
||||
func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson) string {
|
||||
if data.Email != "" {
|
||||
return data.Email, nil
|
||||
return data.Email
|
||||
}
|
||||
|
||||
if data.Attributes["email:primary"] != nil {
|
||||
return data.Attributes["email:primary"][0], nil
|
||||
return data.Attributes["email:primary"][0]
|
||||
}
|
||||
|
||||
if data.Upn != "" {
|
||||
emailAddr, emailErr := mail.ParseAddress(data.Upn)
|
||||
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 != "" {
|
||||
return data.Login, nil
|
||||
return data.Login
|
||||
}
|
||||
|
||||
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 != "" {
|
||||
return data.Name, nil
|
||||
return data.Name
|
||||
}
|
||||
|
||||
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")
|
||||
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) {
|
||||
|
||||
@@ -8,6 +8,7 @@ export function geminiScrollbar() {
|
||||
link: function(scope, elem, attrs) {
|
||||
let scrollbar = new PerfectScrollbar(elem[0], {
|
||||
wheelPropagation: true,
|
||||
wheelSpeed: 3,
|
||||
});
|
||||
let lastPos = 0;
|
||||
|
||||
|
||||
@@ -1,73 +1,78 @@
|
||||
<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 class="sidemenu__logo_small_breakpoint" ng-click="ctrl.toggleSideMenuSmallBreakpoint()">
|
||||
<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>
|
||||
|
||||
<div class="sidemenu__top">
|
||||
<div ng-repeat="item in ::ctrl.mainLinks" class="sidemenu-item dropdown">
|
||||
<a href="{{::item.url}}" class="sidemenu-link" target="{{::item.target}}">
|
||||
<span class="icon-circle sidemenu-icon">
|
||||
<i class="{{::item.icon}}" ng-show="::item.icon"></i>
|
||||
<img ng-src="{{::item.img}}" ng-show="::item.img">
|
||||
</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu" ng-if="::item.children">
|
||||
<li class="side-menu-header">
|
||||
<span class="sidemenu-item-text">{{::item.text}}</span>
|
||||
</li>
|
||||
<li ng-repeat="child in ::item.children" ng-class="{divider: child.divider}">
|
||||
<a href="{{::child.url}}">
|
||||
<i class="{{::child.icon}}" ng-show="::child.icon"></i>
|
||||
{{::child.text}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div ng-repeat="item in ::ctrl.mainLinks" class="sidemenu-item dropdown">
|
||||
<a href="{{::item.url}}" class="sidemenu-link" target="{{::item.target}}">
|
||||
<span class="icon-circle sidemenu-icon">
|
||||
<i class="{{::item.icon}}" ng-show="::item.icon"></i>
|
||||
<img ng-src="{{::item.img}}" ng-show="::item.img">
|
||||
</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu" ng-if="::item.children">
|
||||
<li class="side-menu-header">
|
||||
<span class="sidemenu-item-text">{{::item.text}}</span>
|
||||
</li>
|
||||
<li ng-repeat="child in ::item.children" ng-class="{divider: child.divider}">
|
||||
<a href="{{::child.url}}">
|
||||
<i class="{{::child.icon}}" ng-show="::child.icon"></i>
|
||||
{{::child.text}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidemenu__bottom">
|
||||
<div ng-show="::!ctrl.isSignedIn" class="sidemenu-item">
|
||||
<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>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
||||
<li class="side-menu-header">
|
||||
<span class="sidemenu-item-text">Sign In</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div ng-show="::!ctrl.isSignedIn" class="sidemenu-item">
|
||||
<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>
|
||||
</a>
|
||||
<a href="{{ctrl.loginUrl}}">
|
||||
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
||||
<li class="side-menu-header">
|
||||
<span class="sidemenu-item-text">Sign In</span>
|
||||
</li>
|
||||
</ul>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div ng-repeat="item in ::ctrl.bottomNav" class="sidemenu-item dropdown dropup">
|
||||
<a href="{{::item.url}}" class="sidemenu-link" target="{{::item.target}}">
|
||||
<span class="icon-circle sidemenu-icon">
|
||||
<i class="{{::item.icon}}" ng-show="::item.icon"></i>
|
||||
<img ng-src="{{::item.img}}" ng-show="::item.img">
|
||||
</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
||||
<li ng-if="item.showOrgSwitcher" class="sidemenu-org-switcher">
|
||||
<a ng-click="ctrl.switchOrg()">
|
||||
<div>
|
||||
<div class="sidemenu-org-switcher__org-name">{{ctrl.contextSrv.user.orgName}}</div>
|
||||
<div class="sidemenu-org-switcher__org-current">Current Org:</div>
|
||||
</div>
|
||||
<div class="sidemenu-org-switcher__switch"><i class="fa fa-fw fa-random"></i>Switch</div>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-repeat="child in ::item.children" ng-class="{divider: child.divider}" ng-hide="::child.hideFromMenu">
|
||||
<a href="{{::child.url}}" target="{{::child.target}}" ng-click="ctrl.itemClicked(child, $event)">
|
||||
<i class="{{::child.icon}}" ng-show="::child.icon"></i>
|
||||
{{::child.text}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="side-menu-header">
|
||||
<span class="sidemenu-item-text">{{::item.text}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div ng-repeat="item in ::ctrl.bottomNav" class="sidemenu-item dropdown dropup">
|
||||
<a href="{{::item.url}}" class="sidemenu-link" target="{{::item.target}}">
|
||||
<span class="icon-circle sidemenu-icon">
|
||||
<i class="{{::item.icon}}" ng-show="::item.icon"></i>
|
||||
<img ng-src="{{::item.img}}" ng-show="::item.img">
|
||||
</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
||||
<li ng-if="item.showOrgSwitcher" class="sidemenu-org-switcher">
|
||||
<a ng-click="ctrl.switchOrg()">
|
||||
<div>
|
||||
<div class="sidemenu-org-switcher__org-name">{{ctrl.contextSrv.user.orgName}}</div>
|
||||
<div class="sidemenu-org-switcher__org-current">Current Org:</div>
|
||||
</div>
|
||||
<div class="sidemenu-org-switcher__switch">
|
||||
<i class="fa fa-fw fa-random"></i>Switch</div>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-repeat="child in ::item.children" ng-class="{divider: child.divider}" ng-hide="::child.hideFromMenu">
|
||||
<a href="{{::child.url}}" target="{{::child.target}}" ng-click="ctrl.itemClicked(child, $event)">
|
||||
<i class="{{::child.icon}}" ng-show="::child.icon"></i>
|
||||
{{::child.text}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="side-menu-header">
|
||||
<span class="sidemenu-item-text">{{::item.text}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PanelModel } from '../panel_model';
|
||||
import { PanelContainer } from './PanelContainer';
|
||||
import templateSrv from 'app/features/templating/template_srv';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import config from 'app/core/config';
|
||||
|
||||
export interface DashboardRowProps {
|
||||
panel: PanelModel;
|
||||
@@ -94,14 +95,16 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
{title}
|
||||
<span className="dashboard-row__panel_count">({hiddenPanels} hidden panels)</span>
|
||||
</a>
|
||||
<div className="dashboard-row__actions">
|
||||
<a className="pointer" onClick={this.openSettings}>
|
||||
<i className="fa fa-cog" />
|
||||
</a>
|
||||
<a className="pointer" onClick={this.delete}>
|
||||
<i className="fa fa-trash" />
|
||||
</a>
|
||||
</div>
|
||||
{config.bootData.user.orgRole !== 'Viewer' && (
|
||||
<div className="dashboard-row__actions">
|
||||
<a className="pointer" onClick={this.openSettings}>
|
||||
<i className="fa fa-cog" />
|
||||
</a>
|
||||
<a className="pointer" onClick={this.delete}>
|
||||
<i className="fa fa-trash" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<div className="dashboard-row__drag grid-drag-handle" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,19 +2,26 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { DashboardRow } from '../dashgrid/DashboardRow';
|
||||
import { PanelModel } from '../panel_model';
|
||||
import config from '../../../core/config';
|
||||
|
||||
describe('DashboardRow', () => {
|
||||
let wrapper, panel, getPanelContainer, dashboardMock;
|
||||
|
||||
beforeEach(() => {
|
||||
dashboardMock = {toggleRow: jest.fn()};
|
||||
dashboardMock = { toggleRow: jest.fn() };
|
||||
|
||||
config.bootData = {
|
||||
user: {
|
||||
orgRole: 'Admin',
|
||||
},
|
||||
};
|
||||
|
||||
getPanelContainer = jest.fn().mockReturnValue({
|
||||
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} />);
|
||||
});
|
||||
|
||||
@@ -30,4 +37,14 @@ describe('DashboardRow', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
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() {
|
||||
dash.rows[0].collapse = true;
|
||||
expect(tracker.hasChanges()).to.be(false);
|
||||
|
||||
@@ -97,6 +97,9 @@ export class Tracker {
|
||||
dash.refresh = 0;
|
||||
dash.schemaVersion = 0;
|
||||
|
||||
// ignore iteration property
|
||||
delete dash.iteration;
|
||||
|
||||
// filter row and panels properties that should be ignored
|
||||
dash.rows = _.filter(dash.rows, function(row) {
|
||||
if (row.repeatRowId) {
|
||||
|
||||
@@ -348,7 +348,7 @@
|
||||
"tableColumn": "",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "tsdb_wal_corruptions_total{job=\"prometheus\"}",
|
||||
"expr": "prometheus_tsdb_wal_corruptions_total{job=\"prometheus\"}",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 2,
|
||||
"legendFormat": "",
|
||||
@@ -1048,7 +1048,7 @@
|
||||
"steppedLine": false,
|
||||
"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",
|
||||
"interval": "",
|
||||
"intervalFactor": 2,
|
||||
@@ -1060,7 +1060,7 @@
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Rule Eval Duration",
|
||||
"title": "Rule Group Eval Duration",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
@@ -1124,7 +1124,7 @@
|
||||
"steppedLine": false,
|
||||
"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",
|
||||
"intervalFactor": 2,
|
||||
"legendFormat": "missed",
|
||||
@@ -1132,15 +1132,7 @@
|
||||
"step": 10
|
||||
},
|
||||
{
|
||||
"expr": "rate(prometheus_evaluator_iterations_skipped_total{job=\"prometheus\"}[5m])",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 2,
|
||||
"legendFormat": "skipped",
|
||||
"refId": "C",
|
||||
"step": 10
|
||||
},
|
||||
{
|
||||
"expr": "rate(prometheus_evaluator_iterations_total{job=\"prometheus\"}[5m])",
|
||||
"expr": "rate(prometheus_rule_group_iterations_total{job=\"prometheus\"}[5m])",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 2,
|
||||
"legendFormat": "iterations",
|
||||
@@ -1151,7 +1143,7 @@
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Rule Eval Activity",
|
||||
"title": "Rule Group Eval Activity",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
|
||||
@@ -22,6 +22,12 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
reloadOnSearch: false,
|
||||
pageClass: 'page-dashboard',
|
||||
})
|
||||
.when('/d/:uid', {
|
||||
templateUrl: 'public/app/partials/dashboard.html',
|
||||
controller: 'LoadDashboardCtrl',
|
||||
reloadOnSearch: false,
|
||||
pageClass: 'page-dashboard',
|
||||
})
|
||||
.when('/dashboard/:type/:slug', {
|
||||
templateUrl: 'public/app/partials/dashboard.html',
|
||||
controller: 'LoadDashboardCtrl',
|
||||
@@ -98,6 +104,11 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
controller: 'FolderDashboardsCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
})
|
||||
.when('/dashboards/f/:uid', {
|
||||
templateUrl: 'public/app/features/dashboard/partials/folder_dashboards.html',
|
||||
controller: 'FolderDashboardsCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
})
|
||||
.when('/org', {
|
||||
templateUrl: 'public/app/features/org/partials/orgDetails.html',
|
||||
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
|
||||
// -------------------------
|
||||
$side-menu-bg: $black;
|
||||
$side-menu-bg-mobile: $side-menu-bg;
|
||||
$side-menu-item-hover-bg: $dark-2;
|
||||
$side-menu-shadow: 0 0 20px black;
|
||||
$side-menu-link-color: $link-color;
|
||||
|
||||
@@ -200,6 +200,7 @@ $input-invalid-border-color: lighten($red, 5%);
|
||||
// Sidemenu
|
||||
// -------------------------
|
||||
$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-shadow: 5px 0px 10px -5px $gray-1;
|
||||
$side-menu-link-color: $gray-6;
|
||||
|
||||
@@ -44,6 +44,11 @@
|
||||
border-right: 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 {
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
// important to overlap it otherwise it can be hidden
|
||||
// again by the mouse getting outside the hover space
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -193,9 +193,13 @@ li.sidemenu-org-switcher {
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
.sidemenu-open--xs {
|
||||
li {
|
||||
font-size: $font-size-lg;
|
||||
}
|
||||
|
||||
.sidemenu {
|
||||
width: 100%;
|
||||
background: $side-menu-bg;
|
||||
background: $side-menu-bg-mobile;
|
||||
position: initial;
|
||||
height: auto;
|
||||
box-shadow: $side-menu-shadow;
|
||||
@@ -214,6 +218,9 @@ li.sidemenu-org-switcher {
|
||||
.sidemenu__bottom {
|
||||
display: block;
|
||||
}
|
||||
.sidemenu-item {
|
||||
border-right: 2px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.sidemenu {
|
||||
|
||||
Reference in New Issue
Block a user