diff --git a/docs/sources/http_api/annotations.md b/docs/sources/http_api/annotations.md index 67d31126a5f..8cb2711f917 100644 --- a/docs/sources/http_api/annotations.md +++ b/docs/sources/http_api/annotations.md @@ -55,6 +55,7 @@ Content-Type: application/json "id": 1124, "alertId": 0, "dashboardId": 468, + "dashboardUID": "uGlb_lG7z", "panelId": 2, "userId": 1, "userName": "", @@ -74,6 +75,7 @@ Content-Type: application/json "id": 1123, "alertId": 0, "dashboardId": 468, + "dashboardUID": "jcIIG-07z", "panelId": 2, "userId": 1, "userName": "", @@ -119,7 +121,7 @@ Accept: application/json Content-Type: application/json { - "dashboardId":468, + "dashboardUID":"jcIIG-07z", "panelId":1, "time":1507037197339, "timeEnd":1507180805056, diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 762d64e1e29..24a34e77e08 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -40,10 +40,25 @@ func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response { return response.Error(500, "Failed to get annotations", err) } + // since there are several annotations per dashboard, we can cache dashboard uid + dashboardCache := make(map[int64]*string) for _, item := range items { if item.Email != "" { item.AvatarUrl = dtos.GetGravatarUrl(item.Email) } + + if item.DashboardId != 0 { + if val, ok := dashboardCache[item.DashboardId]; ok { + item.DashboardUID = val + } else { + query := models.GetDashboardQuery{Id: item.DashboardId, OrgId: c.OrgId} + err := hs.SQLStore.GetDashboard(c.Req.Context(), &query) + if err == nil && query.Result != nil { + item.DashboardUID = &query.Result.Uid + dashboardCache[item.DashboardId] = &query.Result.Uid + } + } + } } return response.JSON(http.StatusOK, items) @@ -63,6 +78,15 @@ func (hs *HTTPServer) PostAnnotation(c *models.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "bad request data", err) } + // overwrite dashboardId when dashboardUID is not empty + if cmd.DashboardUID != "" { + query := models.GetDashboardQuery{OrgId: c.OrgId, Uid: cmd.DashboardUID} + err := hs.SQLStore.GetDashboard(c.Req.Context(), &query) + if err == nil { + cmd.DashboardId = query.Result.Id + } + } + if canSave, err := hs.canCreateAnnotation(c, cmd.DashboardId); err != nil || !canSave { return dashboardGuardianResponse(err) } @@ -264,6 +288,14 @@ func (hs *HTTPServer) MassDeleteAnnotations(c *models.ReqContext) response.Respo return response.Error(http.StatusBadRequest, "bad request data", err) } + if cmd.DashboardUID != "" { + query := models.GetDashboardQuery{OrgId: c.OrgId, Uid: cmd.DashboardUID} + err := hs.SQLStore.GetDashboard(c.Req.Context(), &query) + if err == nil { + cmd.DashboardId = query.Result.Id + } + } + if (cmd.DashboardId != 0 && cmd.PanelId == 0) || (cmd.PanelId != 0 && cmd.DashboardId == 0) { err := &AnnotationError{message: "DashboardId and PanelId are both required for mass delete"} return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 603972b06f4..32e1e817e6e 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -50,7 +50,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { role := models.ROLE_VIEWER t.Run("Should not be allowed to save an annotation", func(t *testing.T) { postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, - cmd, func(sc *scenarioContext) { + cmd, store, func(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 403, sc.resp.Code) }) @@ -83,7 +83,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { role := models.ROLE_EDITOR t.Run("Should be able to save an annotation", func(t *testing.T) { postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, - cmd, func(sc *scenarioContext) { + cmd, store, func(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) }) @@ -119,6 +119,14 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { PanelId: 1, } + dashboardUIDCmd := dtos.PostAnnotationsCmd{ + Time: 1000, + Text: "annotation text", + Tags: []string{"tag1", "tag2"}, + DashboardUID: "home", + PanelId: 1, + } + updateCmd := dtos.UpdateAnnotationsCmd{ Time: 1000, Text: "annotation text", @@ -138,10 +146,15 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { PanelId: 1, } + deleteWithDashboardUIDCmd := dtos.MassDeleteAnnotationsCmd{ + DashboardUID: "home", + PanelId: 1, + } + t.Run("When user is an Org Viewer", func(t *testing.T) { role := models.ROLE_VIEWER t.Run("Should not be allowed to save an annotation", func(t *testing.T) { - postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) { + postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, store, func(sc *scenarioContext) { setUpACL() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 403, sc.resp.Code) @@ -174,7 +187,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { t.Run("When user is an Org Editor", func(t *testing.T) { role := models.ROLE_EDITOR t.Run("Should be able to save an annotation", func(t *testing.T) { - postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) { + postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, store, func(sc *scenarioContext) { setUpACL() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) @@ -206,8 +219,21 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { t.Run("When user is an Admin", func(t *testing.T) { role := models.ROLE_ADMIN + + mock := mockstore.NewSQLStoreMock() + mock.ExpectedDashboard = &models.Dashboard{ + Id: 1, + Uid: "home", + } + t.Run("Should be able to do anything", func(t *testing.T) { - postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) { + postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, store, func(sc *scenarioContext) { + setUpACL() + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + assert.Equal(t, 200, sc.resp.Code) + }) + + postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, dashboardUIDCmd, mock, func(sc *scenarioContext) { setUpACL() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) @@ -226,7 +252,14 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { }) deleteAnnotationsScenario(t, "When calling POST on", "/api/annotations/mass-delete", - "/api/annotations/mass-delete", role, deleteCmd, func(sc *scenarioContext) { + "/api/annotations/mass-delete", role, deleteCmd, store, func(sc *scenarioContext) { + setUpACL() + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + assert.Equal(t, 200, sc.resp.Code) + }) + + deleteAnnotationsScenario(t, "When calling POST with dashboardUID on", "/api/annotations/mass-delete", + "/api/annotations/mass-delete", role, deleteWithDashboardUIDCmd, mock, func(sc *scenarioContext) { setUpACL() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) @@ -290,11 +323,9 @@ func (repo *fakeAnnotationsRepo) LoadItems() { var fakeAnnoRepo *fakeAnnotationsRepo func postAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, - cmd dtos.PostAnnotationsCmd, fn scenarioFunc) { + cmd dtos.PostAnnotationsCmd, store sqlstore.Store, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { hs := setupSimpleHTTPServer(nil) - store := sqlstore.InitTestDB(t) - store.Cfg = hs.Cfg hs.SQLStore = store sc := setupScenarioContext(t, url) @@ -376,11 +407,9 @@ func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern } func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, - cmd dtos.MassDeleteAnnotationsCmd, fn scenarioFunc) { + cmd dtos.MassDeleteAnnotationsCmd, store sqlstore.Store, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { hs := setupSimpleHTTPServer(nil) - store := sqlstore.InitTestDB(t) - store.Cfg = hs.Cfg hs.SQLStore = store sc := setupScenarioContext(t, url) diff --git a/pkg/api/dtos/annotations.go b/pkg/api/dtos/annotations.go index 15cbc48838d..1f480334494 100644 --- a/pkg/api/dtos/annotations.go +++ b/pkg/api/dtos/annotations.go @@ -3,13 +3,14 @@ package dtos import "github.com/grafana/grafana/pkg/components/simplejson" type PostAnnotationsCmd struct { - DashboardId int64 `json:"dashboardId"` - PanelId int64 `json:"panelId"` - Time int64 `json:"time"` - TimeEnd int64 `json:"timeEnd,omitempty"` // Optional - Text string `json:"text"` - Tags []string `json:"tags"` - Data *simplejson.Json `json:"data"` + DashboardId int64 `json:"dashboardId"` + DashboardUID string `json:"dashboardUID,omitempty"` + PanelId int64 `json:"panelId"` + Time int64 `json:"time"` + TimeEnd int64 `json:"timeEnd,omitempty"` // Optional + Text string `json:"text"` + Tags []string `json:"tags"` + Data *simplejson.Json `json:"data"` } type UpdateAnnotationsCmd struct { @@ -29,9 +30,10 @@ type PatchAnnotationsCmd struct { } type MassDeleteAnnotationsCmd struct { - DashboardId int64 `json:"dashboardId"` - PanelId int64 `json:"panelId"` - AnnotationId int64 `json:"annotationId"` + DashboardId int64 `json:"dashboardId"` + PanelId int64 `json:"panelId"` + AnnotationId int64 `json:"annotationId"` + DashboardUID string `json:"dashboardUID,omitempty"` } type PostGraphiteAnnotationsCmd struct { diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go index 5dea39e5ccc..60130735b29 100644 --- a/pkg/services/annotations/annotations.go +++ b/pkg/services/annotations/annotations.go @@ -127,24 +127,25 @@ func (i Item) TableName() string { } type ItemDTO struct { - Id int64 `json:"id"` - AlertId int64 `json:"alertId"` - AlertName string `json:"alertName"` - DashboardId int64 `json:"dashboardId"` - PanelId int64 `json:"panelId"` - UserId int64 `json:"userId"` - NewState string `json:"newState"` - PrevState string `json:"prevState"` - Created int64 `json:"created"` - Updated int64 `json:"updated"` - Time int64 `json:"time"` - TimeEnd int64 `json:"timeEnd"` - Text string `json:"text"` - Tags []string `json:"tags"` - Login string `json:"login"` - Email string `json:"email"` - AvatarUrl string `json:"avatarUrl"` - Data *simplejson.Json `json:"data"` + Id int64 `json:"id"` + AlertId int64 `json:"alertId"` + AlertName string `json:"alertName"` + DashboardId int64 `json:"dashboardId"` + DashboardUID *string `json:"dashboardUID"` + PanelId int64 `json:"panelId"` + UserId int64 `json:"userId"` + NewState string `json:"newState"` + PrevState string `json:"prevState"` + Created int64 `json:"created"` + Updated int64 `json:"updated"` + Time int64 `json:"time"` + TimeEnd int64 `json:"timeEnd"` + Text string `json:"text"` + Tags []string `json:"tags"` + Login string `json:"login"` + Email string `json:"email"` + AvatarUrl string `json:"avatarUrl"` + Data *simplejson.Json `json:"data"` } type annotationType int diff --git a/public/api-merged.json b/public/api-merged.json index 1095292f0d4..4fa6dd8353d 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -6806,6 +6806,39 @@ } } }, + "/provisioning/templates": { + "get": { + "tags": ["provisioning"], + "summary": "Get all message templates.", + "operationId": "RouteGetTemplates", + "responses": { + "200": { + "$ref": "#/responses/MessageTemplate" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + } + }, + "/provisioning/templates/{ID}": { + "get": { + "tags": ["provisioning"], + "summary": "Get a message template.", + "operationId": "RouteGetTemplate", + "responses": { + "200": { + "$ref": "#/responses/MessageTemplate" + }, + "404": { + "$ref": "#/responses/NotFound" + } + } + } + }, "/recording-rules": { "get": { "tags": ["recording_rules", "enterprise"], @@ -8225,6 +8258,14 @@ "summary": "Add External Group.", "operationId": "addTeamGroupApi", "parameters": [ + { + "type": "integer", + "format": "int64", + "x-go-name": "TeamID", + "name": "teamId", + "in": "path", + "required": true + }, { "x-go-name": "Body", "name": "body", @@ -8233,14 +8274,6 @@ "schema": { "$ref": "#/definitions/TeamGroupMapping" } - }, - { - "type": "integer", - "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", - "in": "path", - "required": true } ], "responses": { @@ -8274,16 +8307,16 @@ { "type": "integer", "format": "int64", - "x-go-name": "GroupID", - "name": "groupId", + "x-go-name": "TeamID", + "name": "teamId", "in": "path", "required": true }, { "type": "integer", "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", + "x-go-name": "GroupID", + "name": "groupId", "in": "path", "required": true } @@ -13109,6 +13142,10 @@ "format": "int64", "x-go-name": "DashboardId" }, + "dashboardUID": { + "type": "string", + "x-go-name": "DashboardUID" + }, "data": { "$ref": "#/definitions/Json" }, @@ -13443,6 +13480,10 @@ "format": "int64", "x-go-name": "DashboardId" }, + "dashboardUID": { + "type": "string", + "x-go-name": "DashboardUID" + }, "panelId": { "type": "integer", "format": "int64", @@ -14215,6 +14256,10 @@ "format": "int64", "x-go-name": "DashboardId" }, + "dashboardUID": { + "type": "string", + "x-go-name": "DashboardUID" + }, "data": { "$ref": "#/definitions/Json" }, @@ -17636,7 +17681,6 @@ } }, "receiver": { - "description": "Receiver receiver", "type": "object", "required": ["name"], "properties": { @@ -17645,7 +17689,9 @@ "type": "string", "x-go-name": "Name" } - } + }, + "x-go-name": "Receiver", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "silence": { "description": "Silence silence", diff --git a/public/api-spec.json b/public/api-spec.json index 22753102183..5f993fcd6da 100644 --- a/public/api-spec.json +++ b/public/api-spec.json @@ -6667,6 +6667,14 @@ "summary": "Add External Group.", "operationId": "addTeamGroupApi", "parameters": [ + { + "type": "integer", + "format": "int64", + "x-go-name": "TeamID", + "name": "teamId", + "in": "path", + "required": true + }, { "x-go-name": "Body", "name": "body", @@ -6675,14 +6683,6 @@ "schema": { "$ref": "#/definitions/TeamGroupMapping" } - }, - { - "type": "integer", - "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", - "in": "path", - "required": true } ], "responses": { @@ -6716,16 +6716,16 @@ { "type": "integer", "format": "int64", - "x-go-name": "GroupID", - "name": "groupId", + "x-go-name": "TeamID", + "name": "teamId", "in": "path", "required": true }, { "type": "integer", "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", + "x-go-name": "GroupID", + "name": "groupId", "in": "path", "required": true } @@ -10354,6 +10354,10 @@ "format": "int64", "x-go-name": "DashboardId" }, + "dashboardUID": { + "type": "string", + "x-go-name": "DashboardUID" + }, "data": { "$ref": "#/definitions/Json" }, @@ -10643,6 +10647,10 @@ "format": "int64", "x-go-name": "DashboardId" }, + "dashboardUID": { + "type": "string", + "x-go-name": "DashboardUID" + }, "panelId": { "type": "integer", "format": "int64", @@ -11054,6 +11062,10 @@ "format": "int64", "x-go-name": "DashboardId" }, + "dashboardUID": { + "type": "string", + "x-go-name": "DashboardUID" + }, "data": { "$ref": "#/definitions/Json" },