Chore : Replace dashboardid with dashboardUID in annotation API (#48481)

* replace dashboardid with dashboardUID in annotation API

* add some tests

* modify some docs and add uid into get endpoint

* rebase with main

* add map for avoiding too much retrieve on dashboards
This commit is contained in:
ying-jeanne
2022-05-02 11:35:36 +02:00
committed by GitHub
parent b8460051a6
commit bde368be55
7 changed files with 191 additions and 67 deletions

View File

@@ -55,6 +55,7 @@ Content-Type: application/json
> Starting in Grafana v6.4 regions annotations are now returned in one entity that now includes the timeEnd property. > Starting in Grafana v6.4 regions annotations are now returned in one entity that now includes the timeEnd property.
## Create Annotation
Creates an annotation in the Grafana database. The `dashboardId` and `panelId` fields are optional. Creates an annotation in the Grafana database. The `dashboardId` and `panelId` fields are optional.
If they are not specified then an organization annotation is created and can be queried in any dashboard that adds If they are not specified then an organization annotation is created and can be queried in any dashboard that adds
@@ -74,6 +75,7 @@ Content-Type: application/json
**Example Request**: **Example Request**:
```http
POST /api/annotations HTTP/1.1 POST /api/annotations HTTP/1.1
Accept: application/json Accept: application/json
Content-Type: application/json Content-Type: application/json
@@ -119,7 +121,7 @@ Accept: application/json
**Example Response**: **Example Response**:
```http ```http
HTTP/1.1 200
Content-Type: application/json Content-Type: application/json
``` ```

View File

@@ -40,10 +40,25 @@ func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response {
return response.Error(500, "Failed to get annotations", err) 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 { for _, item := range items {
if item.Email != "" { if item.Email != "" {
item.AvatarUrl = dtos.GetGravatarUrl(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) 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) 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 { if canSave, err := hs.canCreateAnnotation(c, cmd.DashboardId); err != nil || !canSave {
return dashboardGuardianResponse(err) 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) 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) { 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"} err := &AnnotationError{message: "DashboardId and PanelId are both required for mass delete"}
return response.Error(http.StatusBadRequest, "bad request data", err) return response.Error(http.StatusBadRequest, "bad request data", err)

View File

@@ -50,7 +50,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
role := models.ROLE_VIEWER role := models.ROLE_VIEWER
t.Run("Should not be allowed to save an annotation", func(t *testing.T) { 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, 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() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 403, sc.resp.Code) assert.Equal(t, 403, sc.resp.Code)
}) })
@@ -83,7 +83,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
role := models.ROLE_EDITOR role := models.ROLE_EDITOR
t.Run("Should be able to save an annotation", func(t *testing.T) { t.Run("Should be able to save an annotation", func(t *testing.T) {
postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, 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() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code) assert.Equal(t, 200, sc.resp.Code)
}) })
@@ -119,6 +119,14 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
PanelId: 1, PanelId: 1,
} }
dashboardUIDCmd := dtos.PostAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
DashboardUID: "home",
PanelId: 1,
}
updateCmd := dtos.UpdateAnnotationsCmd{ updateCmd := dtos.UpdateAnnotationsCmd{
Time: 1000, Time: 1000,
Text: "annotation text", Text: "annotation text",
@@ -138,10 +146,15 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
PanelId: 1, PanelId: 1,
} }
deleteWithDashboardUIDCmd := dtos.MassDeleteAnnotationsCmd{
DashboardUID: "home",
PanelId: 1,
}
t.Run("When user is an Org Viewer", func(t *testing.T) { t.Run("When user is an Org Viewer", func(t *testing.T) {
role := models.ROLE_VIEWER role := models.ROLE_VIEWER
t.Run("Should not be allowed to save an annotation", func(t *testing.T) { 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() setUpACL()
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 403, sc.resp.Code) 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) { t.Run("When user is an Org Editor", func(t *testing.T) {
role := models.ROLE_EDITOR role := models.ROLE_EDITOR
t.Run("Should be able to save an annotation", func(t *testing.T) { 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() setUpACL()
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code) 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) { t.Run("When user is an Admin", func(t *testing.T) {
role := models.ROLE_ADMIN 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) { 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() setUpACL()
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code) 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", 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() setUpACL()
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code) assert.Equal(t, 200, sc.resp.Code)
@@ -290,11 +323,9 @@ func (repo *fakeAnnotationsRepo) LoadItems() {
var fakeAnnoRepo *fakeAnnotationsRepo var fakeAnnoRepo *fakeAnnotationsRepo
func postAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, 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) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
hs := setupSimpleHTTPServer(nil) hs := setupSimpleHTTPServer(nil)
store := sqlstore.InitTestDB(t)
store.Cfg = hs.Cfg
hs.SQLStore = store hs.SQLStore = store
sc := setupScenarioContext(t, url) 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, 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) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
hs := setupSimpleHTTPServer(nil) hs := setupSimpleHTTPServer(nil)
store := sqlstore.InitTestDB(t)
store.Cfg = hs.Cfg
hs.SQLStore = store hs.SQLStore = store
sc := setupScenarioContext(t, url) sc := setupScenarioContext(t, url)

View File

@@ -3,13 +3,14 @@ package dtos
import "github.com/grafana/grafana/pkg/components/simplejson" import "github.com/grafana/grafana/pkg/components/simplejson"
type PostAnnotationsCmd struct { type PostAnnotationsCmd struct {
DashboardId int64 `json:"dashboardId"` DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"` DashboardUID string `json:"dashboardUID,omitempty"`
Time int64 `json:"time"` PanelId int64 `json:"panelId"`
TimeEnd int64 `json:"timeEnd,omitempty"` // Optional Time int64 `json:"time"`
Text string `json:"text"` TimeEnd int64 `json:"timeEnd,omitempty"` // Optional
Tags []string `json:"tags"` Text string `json:"text"`
Data *simplejson.Json `json:"data"` Tags []string `json:"tags"`
Data *simplejson.Json `json:"data"`
} }
type UpdateAnnotationsCmd struct { type UpdateAnnotationsCmd struct {
@@ -29,9 +30,10 @@ type PatchAnnotationsCmd struct {
} }
type MassDeleteAnnotationsCmd struct { type MassDeleteAnnotationsCmd struct {
DashboardId int64 `json:"dashboardId"` DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"` PanelId int64 `json:"panelId"`
AnnotationId int64 `json:"annotationId"` AnnotationId int64 `json:"annotationId"`
DashboardUID string `json:"dashboardUID,omitempty"`
} }
type PostGraphiteAnnotationsCmd struct { type PostGraphiteAnnotationsCmd struct {

View File

@@ -127,24 +127,25 @@ func (i Item) TableName() string {
} }
type ItemDTO struct { type ItemDTO struct {
Id int64 `json:"id"` Id int64 `json:"id"`
AlertId int64 `json:"alertId"` AlertId int64 `json:"alertId"`
AlertName string `json:"alertName"` AlertName string `json:"alertName"`
DashboardId int64 `json:"dashboardId"` DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"` DashboardUID *string `json:"dashboardUID"`
UserId int64 `json:"userId"` PanelId int64 `json:"panelId"`
NewState string `json:"newState"` UserId int64 `json:"userId"`
PrevState string `json:"prevState"` NewState string `json:"newState"`
Created int64 `json:"created"` PrevState string `json:"prevState"`
Updated int64 `json:"updated"` Created int64 `json:"created"`
Time int64 `json:"time"` Updated int64 `json:"updated"`
TimeEnd int64 `json:"timeEnd"` Time int64 `json:"time"`
Text string `json:"text"` TimeEnd int64 `json:"timeEnd"`
Tags []string `json:"tags"` Text string `json:"text"`
Login string `json:"login"` Tags []string `json:"tags"`
Email string `json:"email"` Login string `json:"login"`
AvatarUrl string `json:"avatarUrl"` Email string `json:"email"`
Data *simplejson.Json `json:"data"` AvatarUrl string `json:"avatarUrl"`
Data *simplejson.Json `json:"data"`
} }
type annotationType int type annotationType int

View File

@@ -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": { "/recording-rules": {
"get": { "get": {
"tags": ["recording_rules", "enterprise"], "tags": ["recording_rules", "enterprise"],
@@ -8225,6 +8258,14 @@
"summary": "Add External Group.", "summary": "Add External Group.",
"operationId": "addTeamGroupApi", "operationId": "addTeamGroupApi",
"parameters": [ "parameters": [
{
"type": "integer",
"format": "int64",
"x-go-name": "TeamID",
"name": "teamId",
"in": "path",
"required": true
},
{ {
"x-go-name": "Body", "x-go-name": "Body",
"name": "body", "name": "body",
@@ -8233,14 +8274,6 @@
"schema": { "schema": {
"$ref": "#/definitions/TeamGroupMapping" "$ref": "#/definitions/TeamGroupMapping"
} }
},
{
"type": "integer",
"format": "int64",
"x-go-name": "TeamID",
"name": "teamId",
"in": "path",
"required": true
} }
], ],
"responses": { "responses": {
@@ -8274,16 +8307,16 @@
{ {
"type": "integer", "type": "integer",
"format": "int64", "format": "int64",
"x-go-name": "GroupID", "x-go-name": "TeamID",
"name": "groupId", "name": "teamId",
"in": "path", "in": "path",
"required": true "required": true
}, },
{ {
"type": "integer", "type": "integer",
"format": "int64", "format": "int64",
"x-go-name": "TeamID", "x-go-name": "GroupID",
"name": "teamId", "name": "groupId",
"in": "path", "in": "path",
"required": true "required": true
} }
@@ -13109,6 +13142,10 @@
"format": "int64", "format": "int64",
"x-go-name": "DashboardId" "x-go-name": "DashboardId"
}, },
"dashboardUID": {
"type": "string",
"x-go-name": "DashboardUID"
},
"data": { "data": {
"$ref": "#/definitions/Json" "$ref": "#/definitions/Json"
}, },
@@ -13443,6 +13480,10 @@
"format": "int64", "format": "int64",
"x-go-name": "DashboardId" "x-go-name": "DashboardId"
}, },
"dashboardUID": {
"type": "string",
"x-go-name": "DashboardUID"
},
"panelId": { "panelId": {
"type": "integer", "type": "integer",
"format": "int64", "format": "int64",
@@ -14215,6 +14256,10 @@
"format": "int64", "format": "int64",
"x-go-name": "DashboardId" "x-go-name": "DashboardId"
}, },
"dashboardUID": {
"type": "string",
"x-go-name": "DashboardUID"
},
"data": { "data": {
"$ref": "#/definitions/Json" "$ref": "#/definitions/Json"
}, },
@@ -17636,7 +17681,6 @@
} }
}, },
"receiver": { "receiver": {
"description": "Receiver receiver",
"type": "object", "type": "object",
"required": ["name"], "required": ["name"],
"properties": { "properties": {
@@ -17645,7 +17689,9 @@
"type": "string", "type": "string",
"x-go-name": "Name" "x-go-name": "Name"
} }
} },
"x-go-name": "Receiver",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
}, },
"silence": { "silence": {
"description": "Silence silence", "description": "Silence silence",

View File

@@ -6667,6 +6667,14 @@
"summary": "Add External Group.", "summary": "Add External Group.",
"operationId": "addTeamGroupApi", "operationId": "addTeamGroupApi",
"parameters": [ "parameters": [
{
"type": "integer",
"format": "int64",
"x-go-name": "TeamID",
"name": "teamId",
"in": "path",
"required": true
},
{ {
"x-go-name": "Body", "x-go-name": "Body",
"name": "body", "name": "body",
@@ -6675,14 +6683,6 @@
"schema": { "schema": {
"$ref": "#/definitions/TeamGroupMapping" "$ref": "#/definitions/TeamGroupMapping"
} }
},
{
"type": "integer",
"format": "int64",
"x-go-name": "TeamID",
"name": "teamId",
"in": "path",
"required": true
} }
], ],
"responses": { "responses": {
@@ -6716,16 +6716,16 @@
{ {
"type": "integer", "type": "integer",
"format": "int64", "format": "int64",
"x-go-name": "GroupID", "x-go-name": "TeamID",
"name": "groupId", "name": "teamId",
"in": "path", "in": "path",
"required": true "required": true
}, },
{ {
"type": "integer", "type": "integer",
"format": "int64", "format": "int64",
"x-go-name": "TeamID", "x-go-name": "GroupID",
"name": "teamId", "name": "groupId",
"in": "path", "in": "path",
"required": true "required": true
} }
@@ -10354,6 +10354,10 @@
"format": "int64", "format": "int64",
"x-go-name": "DashboardId" "x-go-name": "DashboardId"
}, },
"dashboardUID": {
"type": "string",
"x-go-name": "DashboardUID"
},
"data": { "data": {
"$ref": "#/definitions/Json" "$ref": "#/definitions/Json"
}, },
@@ -10643,6 +10647,10 @@
"format": "int64", "format": "int64",
"x-go-name": "DashboardId" "x-go-name": "DashboardId"
}, },
"dashboardUID": {
"type": "string",
"x-go-name": "DashboardUID"
},
"panelId": { "panelId": {
"type": "integer", "type": "integer",
"format": "int64", "format": "int64",
@@ -11054,6 +11062,10 @@
"format": "int64", "format": "int64",
"x-go-name": "DashboardId" "x-go-name": "DashboardId"
}, },
"dashboardUID": {
"type": "string",
"x-go-name": "DashboardUID"
},
"data": { "data": {
"$ref": "#/definitions/Json" "$ref": "#/definitions/Json"
}, },