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.
## Create Annotation
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
@@ -74,6 +75,7 @@ Content-Type: application/json
**Example Request**:
```http
POST /api/annotations HTTP/1.1
Accept: application/json
Content-Type: application/json
@@ -119,7 +121,7 @@ Accept: application/json
**Example Response**:
```http
HTTP/1.1 200
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)
}
// 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)

View File

@@ -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)

View File

@@ -4,6 +4,7 @@ import "github.com/grafana/grafana/pkg/components/simplejson"
type PostAnnotationsCmd struct {
DashboardId int64 `json:"dashboardId"`
DashboardUID string `json:"dashboardUID,omitempty"`
PanelId int64 `json:"panelId"`
Time int64 `json:"time"`
TimeEnd int64 `json:"timeEnd,omitempty"` // Optional
@@ -32,6 +33,7 @@ type MassDeleteAnnotationsCmd struct {
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
AnnotationId int64 `json:"annotationId"`
DashboardUID string `json:"dashboardUID,omitempty"`
}
type PostGraphiteAnnotationsCmd struct {

View File

@@ -131,6 +131,7 @@ type ItemDTO struct {
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"`

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": {
"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",

View File

@@ -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"
},