Annotations: Use dashboard uids instead of dashboard ids (#106676)

pull/106737/head
Stephanie Hingtgen 2 weeks ago committed by GitHub
parent 47f3073ab8
commit a8886ad5ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      docs/sources/developers/http_api/annotations.md
  2. 111
      pkg/api/annotations.go
  3. 14
      pkg/api/annotations_test.go
  4. 19
      pkg/services/annotations/accesscontrol/accesscontrol.go
  5. 1
      pkg/services/annotations/annotationsimpl/annotations.go
  6. 27
      pkg/services/annotations/annotationsimpl/annotations_test.go
  7. 20
      pkg/services/annotations/annotationsimpl/cleanup_test.go
  8. 5
      pkg/services/annotations/annotationsimpl/loki/historian_store.go
  9. 27
      pkg/services/annotations/annotationsimpl/loki/historian_store_test.go
  10. 63
      pkg/services/annotations/annotationsimpl/xorm_store.go
  11. 189
      pkg/services/annotations/annotationsimpl/xorm_store_test.go
  12. 4
      pkg/services/annotations/annotationstest/fake.go
  13. 64
      pkg/services/annotations/models.go
  14. 3
      pkg/services/ngalert/state/historian/annotation_store.go
  15. 21
      pkg/services/publicdashboards/models/models.go
  16. 9
      pkg/services/publicdashboards/service/query.go
  17. 45
      pkg/services/sqlstore/migrations/annotation_mig.go
  18. 4
      public/api-enterprise-spec.json
  19. 4
      public/api-merged.json
  20. 4
      public/openapi3.json

@ -54,7 +54,7 @@ Query Parameters:
- `to`: epoch datetime in milliseconds. Optional. - `to`: epoch datetime in milliseconds. Optional.
- `limit`: number. Optional - default is 100. Max limit for results returned. - `limit`: number. Optional - default is 100. Max limit for results returned.
- `alertId`: number. Optional. Find annotations for a specified alert. - `alertId`: number. Optional. Find annotations for a specified alert.
- `dashboardId`: number. Optional. Find annotations that are scoped to a specific dashboard - `dashboardId`: Deprecated. Use dashboardUID instead.
- `dashboardUID`: string. Optional. Find annotations that are scoped to a specific dashboard, when dashboardUID presents, dashboardId would be ignored. - `dashboardUID`: string. Optional. Find annotations that are scoped to a specific dashboard, when dashboardUID presents, dashboardId would be ignored.
- `panelId`: number. Optional. Find annotations that are scoped to a specific panel - `panelId`: number. Optional. Find annotations that are scoped to a specific panel
- `userId`: number. Optional. Find annotations created by a specific user - `userId`: number. Optional. Find annotations created by a specific user
@ -113,7 +113,7 @@ Content-Type: application/json
## Create Annotation ## Create Annotation
Creates an annotation in the Grafana database. The `dashboardId` and `panelId` fields are optional. Creates an annotation in the Grafana database. The `dashboardUid` 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
the Grafana annotations data source. When creating a region annotation include the timeEnd property. the Grafana annotations data source. When creating a region annotation include the timeEnd property.

@ -61,34 +61,28 @@ func (hs *HTTPServer) GetAnnotations(c *contextmodel.ReqContext) response.Respon
if err != nil { if err != nil {
return response.Error(http.StatusBadRequest, "Invalid dashboard UID in annotation request", err) return response.Error(http.StatusBadRequest, "Invalid dashboard UID in annotation request", err)
} else { } else {
query.DashboardID = dqResult.ID query.DashboardID = dqResult.ID // nolint:staticcheck
} }
} }
if query.DashboardID != 0 && query.DashboardUID == "" { // nolint:staticcheck
dq := dashboards.GetDashboardQuery{ID: query.DashboardID, OrgID: c.GetOrgID()} // nolint:staticcheck
dqResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), &dq)
if err != nil {
return response.Error(http.StatusBadRequest, "Invalid dashboard ID in annotation request", err)
}
query.DashboardUID = dqResult.UID
}
items, err := hs.annotationsRepo.Find(c.Req.Context(), query) items, err := hs.annotationsRepo.Find(c.Req.Context(), query)
if err != nil { if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to get annotations", err) return response.Error(http.StatusInternalServerError, "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(hs.Cfg, item.Email) item.AvatarURL = dtos.GetGravatarUrl(hs.Cfg, item.Email)
} }
if item.DashboardID != 0 {
if val, ok := dashboardCache[item.DashboardID]; ok {
item.DashboardUID = val
} else {
query := dashboards.GetDashboardQuery{ID: item.DashboardID, OrgID: c.GetOrgID()}
queryResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), &query)
if err == nil && queryResult != nil {
item.DashboardUID = &queryResult.UID
dashboardCache[item.DashboardID] = &queryResult.UID
}
}
}
} }
return response.JSON(http.StatusOK, items) return response.JSON(http.StatusOK, items)
@ -131,7 +125,17 @@ func (hs *HTTPServer) PostAnnotation(c *contextmodel.ReqContext) response.Respon
} }
} }
if canSave, err := hs.canCreateAnnotation(c, cmd.DashboardId); err != nil || !canSave { // get dashboard uid if not provided
if cmd.DashboardId != 0 && cmd.DashboardUID == "" {
query := dashboards.GetDashboardQuery{OrgID: c.GetOrgID(), ID: cmd.DashboardId}
queryResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), &query)
if err != nil {
return response.Error(http.StatusBadRequest, "Invalid dashboard ID in annotation request", err)
}
cmd.DashboardUID = queryResult.UID
}
if canSave, err := hs.canCreateAnnotation(c, cmd.DashboardUID); err != nil || !canSave {
if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) { if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
return dashboardGuardianResponse(err) return dashboardGuardianResponse(err)
} else if err != nil { } else if err != nil {
@ -148,15 +152,16 @@ func (hs *HTTPServer) PostAnnotation(c *contextmodel.ReqContext) response.Respon
userID, _ := identity.UserIdentifier(c.GetID()) userID, _ := identity.UserIdentifier(c.GetID())
item := annotations.Item{ item := annotations.Item{
OrgID: c.GetOrgID(), OrgID: c.GetOrgID(),
UserID: userID, UserID: userID,
DashboardID: cmd.DashboardId, DashboardID: cmd.DashboardId,
PanelID: cmd.PanelId, DashboardUID: cmd.DashboardUID,
Epoch: cmd.Time, PanelID: cmd.PanelId,
EpochEnd: cmd.TimeEnd, Epoch: cmd.Time,
Text: cmd.Text, EpochEnd: cmd.TimeEnd,
Data: cmd.Data, Text: cmd.Text,
Tags: cmd.Tags, Data: cmd.Data,
Tags: cmd.Tags,
} }
if err := hs.annotationsRepo.Save(c.Req.Context(), &item); err != nil { if err := hs.annotationsRepo.Save(c.Req.Context(), &item); err != nil {
@ -402,6 +407,15 @@ func (hs *HTTPServer) MassDeleteAnnotations(c *contextmodel.ReqContext) response
} }
} }
if cmd.DashboardId != 0 && cmd.DashboardUID == "" {
query := dashboards.GetDashboardQuery{OrgID: c.GetOrgID(), ID: cmd.DashboardId}
queryResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), &query)
if err != nil {
return response.Error(http.StatusBadRequest, "Invalid dashboard ID in annotation request", err)
}
cmd.DashboardUID = queryResult.UID
}
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)
@ -411,28 +425,29 @@ func (hs *HTTPServer) MassDeleteAnnotations(c *contextmodel.ReqContext) response
// validations only for RBAC. A user can mass delete all annotations in a (dashboard + panel) or a specific annotation // validations only for RBAC. A user can mass delete all annotations in a (dashboard + panel) or a specific annotation
// if has access to that dashboard. // if has access to that dashboard.
var dashboardId int64 var dashboardUID string
if cmd.AnnotationId != 0 { if cmd.AnnotationId != 0 {
annotation, respErr := findAnnotationByID(c.Req.Context(), hs.annotationsRepo, cmd.AnnotationId, c.SignedInUser) annotation, respErr := findAnnotationByID(c.Req.Context(), hs.annotationsRepo, cmd.AnnotationId, c.SignedInUser)
if respErr != nil { if respErr != nil {
return respErr return respErr
} }
dashboardId = annotation.DashboardID dashboardUID = *annotation.DashboardUID
deleteParams = &annotations.DeleteParams{ deleteParams = &annotations.DeleteParams{
OrgID: c.GetOrgID(), OrgID: c.GetOrgID(),
ID: cmd.AnnotationId, ID: cmd.AnnotationId,
} }
} else { } else {
dashboardId = cmd.DashboardId dashboardUID = cmd.DashboardUID
deleteParams = &annotations.DeleteParams{ deleteParams = &annotations.DeleteParams{
OrgID: c.GetOrgID(), OrgID: c.GetOrgID(),
DashboardID: cmd.DashboardId, DashboardID: cmd.DashboardId,
PanelID: cmd.PanelId, DashboardUID: cmd.DashboardUID,
PanelID: cmd.PanelId,
} }
} }
canSave, err := hs.canMassDeleteAnnotations(c, dashboardId) canSave, err := hs.canMassDeleteAnnotations(c, dashboardUID)
if err != nil || !canSave { if err != nil || !canSave {
if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) { if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
return dashboardGuardianResponse(err) return dashboardGuardianResponse(err)
@ -519,14 +534,14 @@ func (hs *HTTPServer) DeleteAnnotationByID(c *contextmodel.ReqContext) response.
func (hs *HTTPServer) canSaveAnnotation(c *contextmodel.ReqContext, ac accesscontrol.AccessControl, annotation *annotations.ItemDTO) (bool, error) { func (hs *HTTPServer) canSaveAnnotation(c *contextmodel.ReqContext, ac accesscontrol.AccessControl, annotation *annotations.ItemDTO) (bool, error) {
if annotation.GetType() == annotations.Dashboard { if annotation.GetType() == annotations.Dashboard {
return canEditDashboard(c, ac, annotation.DashboardID) return canEditDashboard(c, ac, *annotation.DashboardUID)
} else { } else {
return true, nil return true, nil
} }
} }
func canEditDashboard(c *contextmodel.ReqContext, ac accesscontrol.AccessControl, dashboardID int64) (bool, error) { func canEditDashboard(c *contextmodel.ReqContext, ac accesscontrol.AccessControl, dashboardUID string) (bool, error) {
evaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScope(strconv.FormatInt(dashboardID, 10))) evaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashboardUID))
return ac.Evaluate(c.Req.Context(), c.SignedInUser, evaluator) return ac.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
} }
@ -630,11 +645,11 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, feature
} }
} }
if annotation.DashboardID == 0 { if annotation.DashboardUID == nil || *annotation.DashboardUID == "" {
return []string{accesscontrol.ScopeAnnotationsTypeOrganization}, nil return []string{accesscontrol.ScopeAnnotationsTypeOrganization}, nil
} else { } else {
return identity.WithServiceIdentityFn(ctx, orgID, func(ctx context.Context) ([]string, error) { return identity.WithServiceIdentityFn(ctx, orgID, func(ctx context.Context) ([]string, error) {
dashboard, err := dashSvc.GetDashboard(ctx, &dashboards.GetDashboardQuery{ID: annotation.DashboardID, OrgID: orgID}) dashboard, err := dashSvc.GetDashboard(ctx, &dashboards.GetDashboardQuery{UID: *annotation.DashboardUID, OrgID: orgID})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -656,10 +671,10 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, feature
}) })
} }
func (hs *HTTPServer) canCreateAnnotation(c *contextmodel.ReqContext, dashboardId int64) (bool, error) { func (hs *HTTPServer) canCreateAnnotation(c *contextmodel.ReqContext, dashboardUID string) (bool, error) {
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) { if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
if dashboardId != 0 { if dashboardUID != "" {
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, dashboards.ScopeDashboardsProvider.GetResourceScope(strconv.FormatInt(dashboardId, 10))) evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashboardUID))
return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator) return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
} else { // organization annotations } else { // organization annotations
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeOrganization) evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeOrganization)
@ -667,31 +682,31 @@ func (hs *HTTPServer) canCreateAnnotation(c *contextmodel.ReqContext, dashboardI
} }
} }
if dashboardId != 0 { if dashboardUID != "" {
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeDashboard) evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeDashboard)
if canSave, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator); err != nil || !canSave { if canSave, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator); err != nil || !canSave {
return canSave, err return canSave, err
} }
return canEditDashboard(c, hs.AccessControl, dashboardId) return canEditDashboard(c, hs.AccessControl, dashboardUID)
} else { // organization annotations } else { // organization annotations
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeOrganization) evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeOrganization)
return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator) return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
} }
} }
func (hs *HTTPServer) canMassDeleteAnnotations(c *contextmodel.ReqContext, dashboardID int64) (bool, error) { func (hs *HTTPServer) canMassDeleteAnnotations(c *contextmodel.ReqContext, dashboardUID string) (bool, error) {
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) { if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
if dashboardID == 0 { if dashboardUID == "" {
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, accesscontrol.ScopeAnnotationsTypeOrganization) evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, accesscontrol.ScopeAnnotationsTypeOrganization)
return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator) return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
} else { } else {
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, dashboards.ScopeDashboardsProvider.GetResourceScope(strconv.FormatInt(dashboardID, 10))) evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashboardUID))
return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator) return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
} }
} }
if dashboardID == 0 { if dashboardUID == "" {
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, accesscontrol.ScopeAnnotationsTypeOrganization) evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, accesscontrol.ScopeAnnotationsTypeOrganization)
return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator) return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
} else { } else {
@ -701,7 +716,7 @@ func (hs *HTTPServer) canMassDeleteAnnotations(c *contextmodel.ReqContext, dashb
return false, err return false, err
} }
canSave, err = canEditDashboard(c, hs.AccessControl, dashboardID) canSave, err = canEditDashboard(c, hs.AccessControl, dashboardUID)
if err != nil || !canSave { if err != nil || !canSave {
return false, err return false, err
} }

@ -399,8 +399,8 @@ func TestAPI_Annotations(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) { server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg() hs.Cfg = setting.NewCfg()
repo := annotationstest.NewFakeAnnotationsRepo() repo := annotationstest.NewFakeAnnotationsRepo()
_ = repo.Save(context.Background(), &annotations.Item{ID: 1, DashboardID: 0}) _ = repo.Save(context.Background(), &annotations.Item{ID: 1, DashboardID: 0, DashboardUID: ""})
_ = repo.Save(context.Background(), &annotations.Item{ID: 2, DashboardID: 1}) _ = repo.Save(context.Background(), &annotations.Item{ID: 2, DashboardID: 1, DashboardUID: "dashuid1"})
hs.annotationsRepo = repo hs.annotationsRepo = repo
hs.Features = featuremgmt.WithFeatures(tt.featureFlags...) hs.Features = featuremgmt.WithFeatures(tt.featureFlags...)
dashService := &dashboards.FakeDashboardService{} dashService := &dashboards.FakeDashboardService{}
@ -413,7 +413,7 @@ func TestAPI_Annotations(t *testing.T) {
hs.folderService = folderService hs.folderService = folderService
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo, hs.Features, dashService, folderService)) hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo, hs.Features, dashService, folderService))
hs.AccessControl.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(dashService, folderService)) hs.AccessControl.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(dashService, folderService))
}) })
var body io.Reader var body io.Reader
if tt.body != "" { if tt.body != "" {
@ -436,11 +436,11 @@ func TestService_AnnotationTypeScopeResolver(t *testing.T) {
dashSvc := &dashboards.FakeDashboardService{} dashSvc := &dashboards.FakeDashboardService{}
rootDash := &dashboards.Dashboard{ID: 1, OrgID: 1, UID: rootDashUID} rootDash := &dashboards.Dashboard{ID: 1, OrgID: 1, UID: rootDashUID}
folderDash := &dashboards.Dashboard{ID: 2, OrgID: 1, UID: folderDashUID, FolderUID: folderUID} folderDash := &dashboards.Dashboard{ID: 2, OrgID: 1, UID: folderDashUID, FolderUID: folderUID}
dashSvc.On("GetDashboard", mock.Anything, &dashboards.GetDashboardQuery{ID: rootDash.ID, OrgID: 1}).Return(rootDash, nil) dashSvc.On("GetDashboard", mock.Anything, &dashboards.GetDashboardQuery{UID: rootDash.UID, OrgID: 1}).Return(rootDash, nil)
dashSvc.On("GetDashboard", mock.Anything, &dashboards.GetDashboardQuery{ID: folderDash.ID, OrgID: 1}).Return(folderDash, nil) dashSvc.On("GetDashboard", mock.Anything, &dashboards.GetDashboardQuery{UID: folderDash.UID, OrgID: 1}).Return(folderDash, nil)
rootDashboardAnnotation := annotations.Item{ID: 1, DashboardID: rootDash.ID} rootDashboardAnnotation := annotations.Item{ID: 1, DashboardID: rootDash.ID, DashboardUID: rootDash.UID}
folderDashboardAnnotation := annotations.Item{ID: 3, DashboardID: folderDash.ID} folderDashboardAnnotation := annotations.Item{ID: 3, DashboardID: folderDash.ID, DashboardUID: folderDash.UID}
organizationAnnotation := annotations.Item{ID: 2} organizationAnnotation := annotations.Item{ID: 2}
fakeAnnoRepo := annotationstest.NewFakeAnnotationsRepo() fakeAnnoRepo := annotationstest.NewFakeAnnotationsRepo()

@ -63,11 +63,11 @@ func (authz *AuthService) Authorize(ctx context.Context, query annotations.ItemQ
var err error var err error
if canAccessDashAnnotations { if canAccessDashAnnotations {
if query.AnnotationID != 0 { if query.AnnotationID != 0 {
annotationDashboardID, err := authz.getAnnotationDashboard(ctx, query) annotationDashboardUID, err := authz.getAnnotationDashboard(ctx, query)
if err != nil { if err != nil {
return nil, ErrAccessControlInternal.Errorf("failed to fetch annotations: %w", err) return nil, ErrAccessControlInternal.Errorf("failed to fetch annotations: %w", err)
} }
query.DashboardID = annotationDashboardID query.DashboardUID = annotationDashboardUID
} }
visibleDashboards, err = authz.dashboardsWithVisibleAnnotations(ctx, query) visibleDashboards, err = authz.dashboardsWithVisibleAnnotations(ctx, query)
@ -83,7 +83,7 @@ func (authz *AuthService) Authorize(ctx context.Context, query annotations.ItemQ
}, nil }, nil
} }
func (authz *AuthService) getAnnotationDashboard(ctx context.Context, query annotations.ItemQuery) (int64, error) { func (authz *AuthService) getAnnotationDashboard(ctx context.Context, query annotations.ItemQuery) (string, error) {
var items []annotations.Item var items []annotations.Item
params := make([]any, 0) params := make([]any, 0)
err := authz.db.WithDbSession(ctx, func(sess *db.Session) error { err := authz.db.WithDbSession(ctx, func(sess *db.Session) error {
@ -91,7 +91,7 @@ func (authz *AuthService) getAnnotationDashboard(ctx context.Context, query anno
SELECT SELECT
a.id, a.id,
a.org_id, a.org_id,
a.dashboard_id a.dashboard_uid
FROM annotation as a FROM annotation as a
WHERE a.org_id = ? AND a.id = ? WHERE a.org_id = ? AND a.id = ?
` `
@ -100,13 +100,13 @@ func (authz *AuthService) getAnnotationDashboard(ctx context.Context, query anno
return sess.SQL(sql, params...).Find(&items) return sess.SQL(sql, params...).Find(&items)
}) })
if err != nil { if err != nil {
return 0, err return "", err
} }
if len(items) == 0 { if len(items) == 0 {
return 0, ErrAccessControlInternal.Errorf("annotation not found") return "", ErrAccessControlInternal.Errorf("annotation not found")
} }
return items[0].DashboardID, nil return items[0].DashboardUID, nil
} }
func (authz *AuthService) dashboardsWithVisibleAnnotations(ctx context.Context, query annotations.ItemQuery) (map[string]int64, error) { func (authz *AuthService) dashboardsWithVisibleAnnotations(ctx context.Context, query annotations.ItemQuery) (map[string]int64, error) {
@ -130,11 +130,6 @@ func (authz *AuthService) dashboardsWithVisibleAnnotations(ctx context.Context,
UIDs: []string{query.DashboardUID}, UIDs: []string{query.DashboardUID},
}) })
} }
if query.DashboardID != 0 {
filters = append(filters, searchstore.DashboardIDFilter{
IDs: []int64{query.DashboardID},
})
}
dashs, err := authz.dashSvc.SearchDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{ dashs, err := authz.dashSvc.SearchDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{
OrgId: query.SignedInUser.GetOrgID(), OrgId: query.SignedInUser.GetOrgID(),

@ -79,6 +79,7 @@ func (r *RepositoryImpl) Find(ctx context.Context, query *annotations.ItemQuery)
} }
// Search without dashboard UID filter is expensive, so check without access control first // Search without dashboard UID filter is expensive, so check without access control first
// nolint: staticcheck
if query.DashboardID == 0 && query.DashboardUID == "" { if query.DashboardID == 0 && query.DashboardUID == "" {
// Return early if no annotations found, it's not necessary to perform expensive access control filtering // Return early if no annotations found, it's not necessary to perform expensive access control filtering
res, err := r.reader.Get(ctx, *query, &accesscontrol.AccessResources{ res, err := r.reader.Get(ctx, *query, &accesscontrol.AccessResources{

@ -81,7 +81,7 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
}), }),
}) })
_ = testutil.CreateDashboard(t, sql, cfg, features, dashboards.SaveDashboardCommand{ dashboard2 := testutil.CreateDashboard(t, sql, cfg, features, dashboards.SaveDashboardCommand{
UserID: 1, UserID: 1,
OrgID: 1, OrgID: 1,
IsFolder: false, IsFolder: false,
@ -91,18 +91,20 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
}) })
dash1Annotation := &annotations.Item{ dash1Annotation := &annotations.Item{
OrgID: 1, OrgID: 1,
DashboardID: 1, DashboardID: 1, // nolint: staticcheck
Epoch: 10, DashboardUID: dashboard1.UID,
Epoch: 10,
} }
err = repo.Save(context.Background(), dash1Annotation) err = repo.Save(context.Background(), dash1Annotation)
require.NoError(t, err) require.NoError(t, err)
dash2Annotation := &annotations.Item{ dash2Annotation := &annotations.Item{
OrgID: 1, OrgID: 1,
DashboardID: 2, DashboardID: 2, // nolint: staticcheck
Epoch: 10, DashboardUID: dashboard2.UID,
Tags: []string{"foo:bar"}, Epoch: 10,
Tags: []string{"foo:bar"},
} }
err = repo.Save(context.Background(), dash2Annotation) err = repo.Save(context.Background(), dash2Annotation)
require.NoError(t, err) require.NoError(t, err)
@ -292,10 +294,11 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) {
annotationTxt := fmt.Sprintf("annotation %d", i) annotationTxt := fmt.Sprintf("annotation %d", i)
dash1Annotation := &annotations.Item{ dash1Annotation := &annotations.Item{
OrgID: orgID, OrgID: orgID,
DashboardID: dashboard.ID, DashboardID: dashboard.ID, // nolint: staticcheck
Epoch: 10, DashboardUID: dashboard.UID,
Text: annotationTxt, Epoch: 10,
Text: annotationTxt,
} }
err = store.Add(context.Background(), dash1Annotation) err = store.Add(context.Background(), dash1Annotation)
require.NoError(t, err) require.NoError(t, err)

@ -3,6 +3,7 @@ package annotationsimpl
import ( import (
"context" "context"
"errors" "errors"
"strconv"
"testing" "testing"
"time" "time"
@ -238,24 +239,27 @@ func createTestAnnotations(t *testing.T, store db.DB, expectedCount int, oldAnno
newAnnotationTags := make([]*annotationTag, 0, 2*expectedCount) newAnnotationTags := make([]*annotationTag, 0, 2*expectedCount)
for i := 0; i < expectedCount; i++ { for i := 0; i < expectedCount; i++ {
a := &annotations.Item{ a := &annotations.Item{
ID: int64(i + 1), ID: int64(i + 1),
DashboardID: 1, DashboardID: 1,
OrgID: 1, DashboardUID: "uid" + strconv.Itoa(i),
UserID: 1, OrgID: 1,
PanelID: 1, UserID: 1,
Text: "", PanelID: 1,
Text: "",
} }
// mark every third as an API annotation // mark every third as an API annotation
// that does not belong to a dashboard // that does not belong to a dashboard
if i%3 == 1 { if i%3 == 1 {
a.DashboardID = 0 a.DashboardID = 0 // nolint: staticcheck
a.DashboardUID = ""
} }
// mark every third annotation as an alert annotation // mark every third annotation as an alert annotation
if i%3 == 0 { if i%3 == 0 {
a.AlertID = 10 a.AlertID = 10
a.DashboardID = 2 a.DashboardID = 2 // nolint: staticcheck
a.DashboardUID = "dashboard2uid"
} }
// create epoch as int annotations.go line 40 // create epoch as int annotations.go line 40

@ -85,6 +85,7 @@ func (r *LokiHistorianStore) Get(ctx context.Context, query annotations.ItemQuer
// if the query is filtering on tags, but not on a specific dashboard, we shouldn't query loki // if the query is filtering on tags, but not on a specific dashboard, we shouldn't query loki
// since state history won't have tags for annotations // since state history won't have tags for annotations
// nolint: staticcheck
if len(query.Tags) > 0 && query.DashboardID == 0 && query.DashboardUID == "" { if len(query.Tags) > 0 && query.DashboardID == 0 && query.DashboardUID == "" {
return make([]*annotations.ItemDTO, 0), nil return make([]*annotations.ItemDTO, 0), nil
} }
@ -178,7 +179,7 @@ func (r *LokiHistorianStore) annotationsFromStream(stream historian.Stream, ac a
items = append(items, &annotations.ItemDTO{ items = append(items, &annotations.ItemDTO{
AlertID: entry.RuleID, AlertID: entry.RuleID,
DashboardID: ac.Dashboards[entry.DashboardUID], DashboardID: ac.Dashboards[entry.DashboardUID], // nolint: staticcheck
DashboardUID: &entry.DashboardUID, DashboardUID: &entry.DashboardUID,
PanelID: entry.PanelID, PanelID: entry.PanelID,
NewState: entry.Current, NewState: entry.Current,
@ -280,8 +281,10 @@ func buildHistoryQuery(query *annotations.ItemQuery, dashboards map[string]int64
RuleUID: ruleUID, RuleUID: ruleUID,
} }
// nolint: staticcheck
if historyQuery.DashboardUID == "" && query.DashboardID != 0 { if historyQuery.DashboardUID == "" && query.DashboardID != 0 {
for uid, id := range dashboards { for uid, id := range dashboards {
// nolint: staticcheck
if query.DashboardID == id { if query.DashboardID == id {
historyQuery.DashboardUID = uid historyQuery.DashboardUID = uid
break break

@ -193,7 +193,7 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) {
query := annotations.ItemQuery{ query := annotations.ItemQuery{
OrgID: 1, OrgID: 1,
DashboardID: dashboard1.ID, DashboardID: dashboard1.ID, // nolint: staticcheck
From: start.UnixMilli(), From: start.UnixMilli(),
To: start.Add(time.Second * time.Duration(numTransitions+1)).UnixMilli(), To: start.Add(time.Second * time.Duration(numTransitions+1)).UnixMilli(),
} }
@ -243,7 +243,7 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) {
query := annotations.ItemQuery{ query := annotations.ItemQuery{
OrgID: 1, OrgID: 1,
DashboardID: dashboard1.ID, DashboardID: dashboard1.ID, // nolint: staticcheck
From: start.Add(-2 * time.Second).UnixMilli(), From: start.Add(-2 * time.Second).UnixMilli(),
To: start.Add(-1 * time.Second).UnixMilli(), To: start.Add(-1 * time.Second).UnixMilli(),
} }
@ -273,7 +273,7 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) {
query := annotations.ItemQuery{ query := annotations.ItemQuery{
OrgID: 1, OrgID: 1,
DashboardID: dashboard1.ID, DashboardID: dashboard1.ID, // nolint: staticcheck
From: start.Add(-1 * time.Second).UnixMilli(), // should clamp to start From: start.Add(-1 * time.Second).UnixMilli(), // should clamp to start
To: start.Add(1 * time.Second).UnixMilli(), To: start.Add(1 * time.Second).UnixMilli(),
} }
@ -294,17 +294,17 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) {
fakeLokiClient.cfg.MaxQueryLength = oldMax fakeLokiClient.cfg.MaxQueryLength = oldMax
}) })
t.Run("should sort history by time", func(t *testing.T) { t.Run("should sort history by time and be able to query by dashboard uid", func(t *testing.T) {
fakeLokiClient.rangeQueryRes = []historian.Stream{ fakeLokiClient.rangeQueryRes = []historian.Stream{
historian.StatesToStream(ruleMetaFromRule(t, dashboardRules[dashboard1.UID][0]), transitions, map[string]string{}, log.NewNopLogger()), historian.StatesToStream(ruleMetaFromRule(t, dashboardRules[dashboard1.UID][0]), transitions, map[string]string{}, log.NewNopLogger()),
historian.StatesToStream(ruleMetaFromRule(t, dashboardRules[dashboard1.UID][1]), transitions, map[string]string{}, log.NewNopLogger()), historian.StatesToStream(ruleMetaFromRule(t, dashboardRules[dashboard1.UID][1]), transitions, map[string]string{}, log.NewNopLogger()),
} }
query := annotations.ItemQuery{ query := annotations.ItemQuery{
OrgID: 1, OrgID: 1,
DashboardID: dashboard1.ID, DashboardUID: dashboard1.UID,
From: start.UnixMilli(), From: start.UnixMilli(),
To: start.Add(time.Second * time.Duration(numTransitions+1)).UnixMilli(), To: start.Add(time.Second * time.Duration(numTransitions+1)).UnixMilli(),
} }
res, err := store.Get( res, err := store.Get(
context.Background(), context.Background(),
@ -393,7 +393,7 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) {
expected := &annotations.ItemDTO{ expected := &annotations.ItemDTO{
AlertID: rule.ID, AlertID: rule.ID,
DashboardID: dashboard1.ID, DashboardID: dashboard1.ID, // nolint: staticcheck
DashboardUID: &dashboard1.UID, DashboardUID: &dashboard1.UID,
PanelID: *rule.PanelID, PanelID: *rule.PanelID,
Time: transition.LastEvaluationTime.UnixMilli(), Time: transition.LastEvaluationTime.UnixMilli(),
@ -433,6 +433,7 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) {
require.Len(t, items, numTransitions) require.Len(t, items, numTransitions)
for _, item := range items { for _, item := range items {
// nolint: staticcheck
require.Equal(t, dashboard1.ID, item.DashboardID) require.Equal(t, dashboard1.ID, item.DashboardID)
require.Equal(t, dashboard1.UID, *item.DashboardUID) require.Equal(t, dashboard1.UID, *item.DashboardUID)
} }
@ -464,7 +465,7 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) {
for _, item := range items { for _, item := range items {
require.Zero(t, *item.DashboardUID) require.Zero(t, *item.DashboardUID)
require.Zero(t, item.DashboardID) require.Zero(t, item.DashboardID) // nolint: staticcheck
} }
}) })
}) })
@ -553,7 +554,7 @@ func TestBuildHistoryQuery(t *testing.T) {
t.Run("should set dashboard UID from dashboard ID if query does not contain UID", func(t *testing.T) { t.Run("should set dashboard UID from dashboard ID if query does not contain UID", func(t *testing.T) {
query := buildHistoryQuery( query := buildHistoryQuery(
&annotations.ItemQuery{ &annotations.ItemQuery{
DashboardID: 1, DashboardID: 1, // nolint: staticcheck
}, },
map[string]int64{ map[string]int64{
"dashboard-uid": 1, "dashboard-uid": 1,
@ -566,7 +567,7 @@ func TestBuildHistoryQuery(t *testing.T) {
t.Run("should skip dashboard UID if missing from query and dashboard map", func(t *testing.T) { t.Run("should skip dashboard UID if missing from query and dashboard map", func(t *testing.T) {
query := buildHistoryQuery( query := buildHistoryQuery(
&annotations.ItemQuery{ &annotations.ItemQuery{
DashboardID: 1, DashboardID: 1, // nolint: staticcheck
}, },
map[string]int64{ map[string]int64{
"other-dashboard-uid": 2, "other-dashboard-uid": 2,
@ -794,7 +795,7 @@ func compareAnnotationItem(t *testing.T, expected, actual *annotations.ItemDTO)
require.Equal(t, expected.PanelID, actual.PanelID) require.Equal(t, expected.PanelID, actual.PanelID)
} }
if expected.DashboardUID != nil { if expected.DashboardUID != nil {
require.Equal(t, expected.DashboardID, actual.DashboardID) require.Equal(t, expected.DashboardID, actual.DashboardID) // nolint: staticcheck
require.Equal(t, *expected.DashboardUID, *actual.DashboardUID) require.Equal(t, *expected.DashboardUID, *actual.DashboardUID)
} }
require.Equal(t, expected.NewState, actual.NewState) require.Equal(t, expected.NewState, actual.NewState)

@ -5,11 +5,11 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strconv"
"strings" "strings"
"time" "time"
"github.com/grafana/grafana/pkg/services/annotations/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations/accesscontrol"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/identity"
@ -46,6 +46,13 @@ type xormRepositoryImpl struct {
} }
func NewXormStore(cfg *setting.Cfg, l log.Logger, db db.DB, tagService tag.Service) *xormRepositoryImpl { func NewXormStore(cfg *setting.Cfg, l log.Logger, db db.DB, tagService tag.Service) *xormRepositoryImpl {
// populate dashboard_uid at startup, to ensure safe downgrades & upgrades after
// the initial migration occurs
err := migrations.RunDashboardUIDMigrations(db.GetEngine().NewSession(), db.GetEngine().DriverName())
if err != nil {
l.Error("failed to populate dashboard_uid for annotations", "error", err)
}
return &xormRepositoryImpl{ return &xormRepositoryImpl{
cfg: cfg, cfg: cfg,
db: db, db: db,
@ -255,6 +262,7 @@ func (r *xormRepositoryImpl) Get(ctx context.Context, query annotations.ItemQuer
annotation.id, annotation.id,
annotation.epoch as time, annotation.epoch as time,
annotation.epoch_end as time_end, annotation.epoch_end as time_end,
annotation.dashboard_uid,
annotation.dashboard_id, annotation.dashboard_id,
annotation.panel_id, annotation.panel_id,
annotation.new_state, annotation.new_state,
@ -292,11 +300,18 @@ func (r *xormRepositoryImpl) Get(ctx context.Context, query annotations.ItemQuer
params = append(params, query.AlertUID, query.OrgID) params = append(params, query.AlertUID, query.OrgID)
} }
// nolint: staticcheck
if query.DashboardID != 0 { if query.DashboardID != 0 {
sql.WriteString(` AND a.dashboard_id = ?`) sql.WriteString(` AND a.dashboard_id = ?`)
params = append(params, query.DashboardID) params = append(params, query.DashboardID)
} }
// note: orgID is already required above
if query.DashboardUID != "" {
sql.WriteString(` AND a.dashboard_uid = ?`)
params = append(params, query.DashboardUID)
}
if query.PanelID != 0 { if query.PanelID != 0 {
sql.WriteString(` AND a.panel_id = ?`) sql.WriteString(` AND a.panel_id = ?`)
params = append(params, query.PanelID) params = append(params, query.PanelID)
@ -351,13 +366,11 @@ func (r *xormRepositoryImpl) Get(ctx context.Context, query annotations.ItemQuer
} }
} }
acFilter, err := r.getAccessControlFilter(query.SignedInUser, accessResources) acFilter, acParams := r.getAccessControlFilter(query.SignedInUser, accessResources)
if err != nil {
return err
}
if acFilter != "" { if acFilter != "" {
sql.WriteString(fmt.Sprintf(" AND (%s)", acFilter)) sql.WriteString(fmt.Sprintf(" AND (%s)", acFilter))
} }
params = append(params, acParams...)
// order of ORDER BY arguments match the order of a sql index for performance // order of ORDER BY arguments match the order of a sql index for performance
orderBy := " ORDER BY a.org_id, a.epoch_end DESC, a.epoch DESC" orderBy := " ORDER BY a.org_id, a.epoch_end DESC, a.epoch DESC"
@ -377,41 +390,30 @@ func (r *xormRepositoryImpl) Get(ctx context.Context, query annotations.ItemQuer
return items, err return items, err
} }
func (r *xormRepositoryImpl) getAccessControlFilter(user identity.Requester, accessResources *accesscontrol.AccessResources) (string, error) { func (r *xormRepositoryImpl) getAccessControlFilter(user identity.Requester, accessResources *accesscontrol.AccessResources) (string, []any) {
if accessResources.SkipAccessControlFilter { if accessResources.SkipAccessControlFilter {
return "", nil return "", nil
} }
var filters []string var filters []string
var params []any
if accessResources.CanAccessOrgAnnotations { if accessResources.CanAccessOrgAnnotations {
filters = append(filters, "a.dashboard_id = 0") filters = append(filters, "a.dashboard_id = 0")
} }
if accessResources.CanAccessDashAnnotations { if accessResources.CanAccessDashAnnotations {
var dashboardIDs []int64 if len(accessResources.Dashboards) == 0 {
for _, id := range accessResources.Dashboards { filters = append(filters, "1=0") // empty set
dashboardIDs = append(dashboardIDs, id)
}
var inClause string
if len(dashboardIDs) == 0 {
inClause = "SELECT * FROM (SELECT 0 LIMIT 0) tt" // empty set
} else { } else {
b := make([]byte, 0, 3*len(dashboardIDs)) filters = append(filters, fmt.Sprintf("a.dashboard_uid IN (%s)", strings.Repeat("?,", len(accessResources.Dashboards)-1)+"?"))
for uid := range accessResources.Dashboards {
b = strconv.AppendInt(b, dashboardIDs[0], 10) params = append(params, uid)
for _, num := range dashboardIDs[1:] {
b = append(b, ',')
b = strconv.AppendInt(b, num, 10)
} }
inClause = string(b)
} }
filters = append(filters, fmt.Sprintf("a.dashboard_id IN (%s)", inClause))
} }
return strings.Join(filters, " OR "), nil return strings.Join(filters, " OR "), params
} }
func (r *xormRepositoryImpl) Delete(ctx context.Context, params *annotations.DeleteParams) error { func (r *xormRepositoryImpl) Delete(ctx context.Context, params *annotations.DeleteParams) error {
@ -433,14 +435,27 @@ func (r *xormRepositoryImpl) Delete(ctx context.Context, params *annotations.Del
if _, err := sess.Exec(sql, params.ID, params.OrgID); err != nil { if _, err := sess.Exec(sql, params.ID, params.OrgID); err != nil {
return err return err
} }
} else if params.DashboardUID != "" {
annoTagSQL = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE dashboard_uid = ? AND panel_id = ? AND org_id = ?)"
sql = "DELETE FROM annotation WHERE dashboard_uid = ? AND panel_id = ? AND org_id = ?"
if _, err := sess.Exec(annoTagSQL, params.DashboardUID, params.PanelID, params.OrgID); err != nil {
return err
}
if _, err := sess.Exec(sql, params.DashboardUID, params.PanelID, params.OrgID); err != nil {
return err
}
} else { } else {
annoTagSQL = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE dashboard_id = ? AND panel_id = ? AND org_id = ?)" annoTagSQL = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE dashboard_id = ? AND panel_id = ? AND org_id = ?)"
sql = "DELETE FROM annotation WHERE dashboard_id = ? AND panel_id = ? AND org_id = ?" sql = "DELETE FROM annotation WHERE dashboard_id = ? AND panel_id = ? AND org_id = ?"
// nolint: staticcheck
if _, err := sess.Exec(annoTagSQL, params.DashboardID, params.PanelID, params.OrgID); err != nil { if _, err := sess.Exec(annoTagSQL, params.DashboardID, params.PanelID, params.OrgID); err != nil {
return err return err
} }
// nolint: staticcheck
if _, err := sess.Exec(sql, params.DashboardID, params.PanelID, params.OrgID); err != nil { if _, err := sess.Exec(sql, params.DashboardID, params.PanelID, params.OrgID); err != nil {
return err return err
} }

@ -78,14 +78,15 @@ func TestIntegrationAnnotations(t *testing.T) {
var err error var err error
annotation := &annotations.Item{ annotation := &annotations.Item{
OrgID: 1, OrgID: 1,
UserID: 1, UserID: 1,
DashboardID: dashboard.ID, DashboardID: dashboard.ID, // nolint: staticcheck
Text: "hello", DashboardUID: dashboard.UID,
Type: "alert", Text: "hello",
Epoch: 10, Type: "alert",
Tags: []string{"outage", "error", "type:outage", "server:server-1"}, Epoch: 10,
Data: simplejson.NewFromAny(map[string]any{"data1": "I am a cool data", "data2": "I am another cool data"}), Tags: []string{"outage", "error", "type:outage", "server:server-1"},
Data: simplejson.NewFromAny(map[string]any{"data1": "I am a cool data", "data2": "I am another cool data"}),
} }
err = store.Add(context.Background(), annotation) err = store.Add(context.Background(), annotation)
require.NoError(t, err) require.NoError(t, err)
@ -93,14 +94,15 @@ func TestIntegrationAnnotations(t *testing.T) {
assert.Equal(t, annotation.Epoch, annotation.EpochEnd) assert.Equal(t, annotation.Epoch, annotation.EpochEnd)
annotation2 := &annotations.Item{ annotation2 := &annotations.Item{
OrgID: 1, OrgID: 1,
UserID: 1, UserID: 1,
DashboardID: dashboard2.ID, DashboardID: dashboard2.ID, // nolint: staticcheck
Text: "hello", DashboardUID: dashboard2.UID,
Type: "alert", Text: "hello",
Epoch: 21, // Should swap epoch & epochEnd Type: "alert",
EpochEnd: 20, Epoch: 21, // Should swap epoch & epochEnd
Tags: []string{"outage", "type:outage", "server:server-1", "error"}, EpochEnd: 20,
Tags: []string{"outage", "type:outage", "server:server-1", "error"},
} }
err = store.Add(context.Background(), annotation2) err = store.Add(context.Background(), annotation2)
require.NoError(t, err) require.NoError(t, err)
@ -135,7 +137,31 @@ func TestIntegrationAnnotations(t *testing.T) {
t.Run("Can query for annotation by dashboard id", func(t *testing.T) { t.Run("Can query for annotation by dashboard id", func(t *testing.T) {
items, err := store.Get(context.Background(), annotations.ItemQuery{ items, err := store.Get(context.Background(), annotations.ItemQuery{
OrgID: 1, OrgID: 1,
DashboardID: dashboard.ID, DashboardID: dashboard.ID, // nolint: staticcheck
From: 0,
To: 15,
SignedInUser: testUser,
}, &annotation_ac.AccessResources{
Dashboards: map[string]int64{
dashboard.UID: dashboard.ID,
},
CanAccessDashAnnotations: true,
})
require.NoError(t, err)
assert.Len(t, items, 1)
assert.Equal(t, []string{"outage", "error", "type:outage", "server:server-1"}, items[0].Tags)
assert.GreaterOrEqual(t, items[0].Created, int64(0))
assert.GreaterOrEqual(t, items[0].Updated, int64(0))
assert.Equal(t, items[0].Updated, items[0].Created)
})
t.Run("Can query for annotation by dashboard uid", func(t *testing.T) {
items, err := store.Get(context.Background(), annotations.ItemQuery{
OrgID: 1,
DashboardUID: dashboard.UID,
From: 0, From: 0,
To: 15, To: 15,
SignedInUser: testUser, SignedInUser: testUser,
@ -234,12 +260,13 @@ func TestIntegrationAnnotations(t *testing.T) {
t.Run("Should not find any when item is outside time range", func(t *testing.T) { t.Run("Should not find any when item is outside time range", func(t *testing.T) {
accRes := &annotation_ac.AccessResources{ accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{"foo": 1}, Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true, CanAccessDashAnnotations: true,
} }
items, err := store.Get(context.Background(), annotations.ItemQuery{ items, err := store.Get(context.Background(), annotations.ItemQuery{
OrgID: 1, OrgID: 1,
DashboardID: 1, DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 12, From: 12,
To: 15, To: 15,
SignedInUser: testUser, SignedInUser: testUser,
@ -250,12 +277,13 @@ func TestIntegrationAnnotations(t *testing.T) {
t.Run("Should not find one when tag filter does not match", func(t *testing.T) { t.Run("Should not find one when tag filter does not match", func(t *testing.T) {
accRes := &annotation_ac.AccessResources{ accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{"foo": 1}, Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true, CanAccessDashAnnotations: true,
} }
items, err := store.Get(context.Background(), annotations.ItemQuery{ items, err := store.Get(context.Background(), annotations.ItemQuery{
OrgID: 1, OrgID: 1,
DashboardID: 1, DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 1, From: 1,
To: 15, To: 15,
Tags: []string{"asd"}, Tags: []string{"asd"},
@ -267,12 +295,13 @@ func TestIntegrationAnnotations(t *testing.T) {
t.Run("Should not find one when type filter does not match", func(t *testing.T) { t.Run("Should not find one when type filter does not match", func(t *testing.T) {
accRes := &annotation_ac.AccessResources{ accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{"foo": 1}, Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true, CanAccessDashAnnotations: true,
} }
items, err := store.Get(context.Background(), annotations.ItemQuery{ items, err := store.Get(context.Background(), annotations.ItemQuery{
OrgID: 1, OrgID: 1,
DashboardID: 1, DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 1, From: 1,
To: 15, To: 15,
Type: "alert", Type: "alert",
@ -284,12 +313,13 @@ func TestIntegrationAnnotations(t *testing.T) {
t.Run("Should find one when all tag filters does match", func(t *testing.T) { t.Run("Should find one when all tag filters does match", func(t *testing.T) {
accRes := &annotation_ac.AccessResources{ accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{"foo": 1}, Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true, CanAccessDashAnnotations: true,
} }
items, err := store.Get(context.Background(), annotations.ItemQuery{ items, err := store.Get(context.Background(), annotations.ItemQuery{
OrgID: 1, OrgID: 1,
DashboardID: 1, DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 1, From: 1,
To: 15, // this will exclude the second test annotation To: 15, // this will exclude the second test annotation
Tags: []string{"outage", "error"}, Tags: []string{"outage", "error"},
@ -315,12 +345,13 @@ func TestIntegrationAnnotations(t *testing.T) {
t.Run("Should find one when all key value tag filters does match", func(t *testing.T) { t.Run("Should find one when all key value tag filters does match", func(t *testing.T) {
accRes := &annotation_ac.AccessResources{ accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{"foo": 1}, Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true, CanAccessDashAnnotations: true,
} }
items, err := store.Get(context.Background(), annotations.ItemQuery{ items, err := store.Get(context.Background(), annotations.ItemQuery{
OrgID: 1, OrgID: 1,
DashboardID: 1, DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 1, From: 1,
To: 15, To: 15,
Tags: []string{"type:outage", "server:server-1"}, Tags: []string{"type:outage", "server:server-1"},
@ -333,13 +364,14 @@ func TestIntegrationAnnotations(t *testing.T) {
t.Run("Can update annotation and remove all tags", func(t *testing.T) { t.Run("Can update annotation and remove all tags", func(t *testing.T) {
query := annotations.ItemQuery{ query := annotations.ItemQuery{
OrgID: 1, OrgID: 1,
DashboardID: 1, DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 0, From: 0,
To: 15, To: 15,
SignedInUser: testUser, SignedInUser: testUser,
} }
accRes := &annotation_ac.AccessResources{ accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{"foo": 1}, Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true, CanAccessDashAnnotations: true,
} }
items, err := store.Get(context.Background(), query, accRes) items, err := store.Get(context.Background(), query, accRes)
@ -368,13 +400,14 @@ func TestIntegrationAnnotations(t *testing.T) {
t.Run("Can update annotation with new tags", func(t *testing.T) { t.Run("Can update annotation with new tags", func(t *testing.T) {
query := annotations.ItemQuery{ query := annotations.ItemQuery{
OrgID: 1, OrgID: 1,
DashboardID: 1, DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 0, From: 0,
To: 15, To: 15,
SignedInUser: testUser, SignedInUser: testUser,
} }
accRes := &annotation_ac.AccessResources{ accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{"foo": 1}, Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true, CanAccessDashAnnotations: true,
} }
items, err := store.Get(context.Background(), query, accRes) items, err := store.Get(context.Background(), query, accRes)
@ -401,13 +434,14 @@ func TestIntegrationAnnotations(t *testing.T) {
t.Run("Can update annotation with additional tags", func(t *testing.T) { t.Run("Can update annotation with additional tags", func(t *testing.T) {
query := annotations.ItemQuery{ query := annotations.ItemQuery{
OrgID: 1, OrgID: 1,
DashboardID: 1, DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 0, From: 0,
To: 15, To: 15,
SignedInUser: testUser, SignedInUser: testUser,
} }
accRes := &annotation_ac.AccessResources{ accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{"foo": 1}, Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true, CanAccessDashAnnotations: true,
} }
items, err := store.Get(context.Background(), query, accRes) items, err := store.Get(context.Background(), query, accRes)
@ -434,13 +468,14 @@ func TestIntegrationAnnotations(t *testing.T) {
t.Run("Can update annotations with data", func(t *testing.T) { t.Run("Can update annotations with data", func(t *testing.T) {
query := annotations.ItemQuery{ query := annotations.ItemQuery{
OrgID: 1, OrgID: 1,
DashboardID: 1, DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 0, From: 0,
To: 15, To: 15,
SignedInUser: testUser, SignedInUser: testUser,
} }
accRes := &annotation_ac.AccessResources{ accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{"foo": 1}, Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true, CanAccessDashAnnotations: true,
} }
items, err := store.Get(context.Background(), query, accRes) items, err := store.Get(context.Background(), query, accRes)
@ -470,13 +505,14 @@ func TestIntegrationAnnotations(t *testing.T) {
t.Run("Can delete annotation", func(t *testing.T) { t.Run("Can delete annotation", func(t *testing.T) {
query := annotations.ItemQuery{ query := annotations.ItemQuery{
OrgID: 1, OrgID: 1,
DashboardID: 1, DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 0, From: 0,
To: 15, To: 15,
SignedInUser: testUser, SignedInUser: testUser,
} }
accRes := &annotation_ac.AccessResources{ accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{"foo": 1}, Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true, CanAccessDashAnnotations: true,
} }
items, err := store.Get(context.Background(), query, accRes) items, err := store.Get(context.Background(), query, accRes)
@ -493,14 +529,53 @@ func TestIntegrationAnnotations(t *testing.T) {
t.Run("Can delete annotation using dashboard id and panel id", func(t *testing.T) { t.Run("Can delete annotation using dashboard id and panel id", func(t *testing.T) {
annotation3 := &annotations.Item{ annotation3 := &annotations.Item{
OrgID: 1, OrgID: 1,
UserID: 1, UserID: 1,
DashboardID: dashboard2.ID, DashboardID: dashboard2.ID, // nolint: staticcheck
Text: "toBeDeletedWithPanelId", DashboardUID: dashboard2.UID,
Type: "alert", Text: "toBeDeletedWithPanelId",
Epoch: 11, Type: "alert",
Tags: []string{"test"}, Epoch: 11,
PanelID: 20, Tags: []string{"test"},
PanelID: 20,
}
err = store.Add(context.Background(), annotation3)
require.NoError(t, err)
accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{
dashboard2.UID: dashboard2.ID,
},
CanAccessDashAnnotations: true,
}
query := annotations.ItemQuery{
OrgID: 1,
AnnotationID: annotation3.ID,
SignedInUser: testUser,
}
items, err := store.Get(context.Background(), query, accRes)
require.NoError(t, err)
// nolint:staticcheck
err = store.Delete(context.Background(), &annotations.DeleteParams{DashboardID: items[0].DashboardID, PanelID: items[0].PanelID, OrgID: 1})
require.NoError(t, err)
items, err = store.Get(context.Background(), query, accRes)
require.NoError(t, err)
assert.Empty(t, items)
})
t.Run("Can delete annotation using dashboard uid and panel id", func(t *testing.T) {
annotation3 := &annotations.Item{
OrgID: 1,
UserID: 1,
DashboardUID: dashboard2.UID,
Text: "toBeDeletedWithPanelId",
Type: "alert",
Epoch: 11,
Tags: []string{"test"},
PanelID: 20,
} }
err = store.Add(context.Background(), annotation3) err = store.Add(context.Background(), annotation3)
require.NoError(t, err) require.NoError(t, err)
@ -520,9 +595,8 @@ func TestIntegrationAnnotations(t *testing.T) {
items, err := store.Get(context.Background(), query, accRes) items, err := store.Get(context.Background(), query, accRes)
require.NoError(t, err) require.NoError(t, err)
dashboardId := items[0].DashboardID // nolint:staticcheck
panelId := items[0].PanelID err = store.Delete(context.Background(), &annotations.DeleteParams{DashboardUID: *items[0].DashboardUID, PanelID: items[0].PanelID, OrgID: 1})
err = store.Delete(context.Background(), &annotations.DeleteParams{DashboardID: dashboardId, PanelID: panelId, OrgID: 1})
require.NoError(t, err) require.NoError(t, err)
items, err = store.Get(context.Background(), query, accRes) items, err = store.Get(context.Background(), query, accRes)
@ -635,15 +709,16 @@ func benchmarkFindTags(b *testing.B, numAnnotations int) {
require.NoError(b, err) require.NoError(b, err)
annotationWithTheTag := annotations.Item{ annotationWithTheTag := annotations.Item{
ID: int64(numAnnotations) + 1, ID: int64(numAnnotations) + 1,
OrgID: 1, OrgID: 1,
UserID: 1, UserID: 1,
DashboardID: int64(1), DashboardID: 1, // nolint: staticcheck
Text: "hello", DashboardUID: "uid",
Type: "alert", Text: "hello",
Epoch: 10, Type: "alert",
Tags: []string{"outage", "error", "type:outage", "server:server-1"}, Epoch: 10,
Data: simplejson.NewFromAny(map[string]any{"data1": "I am a cool data", "data2": "I am another cool data"}), Tags: []string{"outage", "error", "type:outage", "server:server-1"},
Data: simplejson.NewFromAny(map[string]any{"data1": "I am a cool data", "data2": "I am another cool data"}),
} }
err = store.Add(context.Background(), &annotationWithTheTag) err = store.Add(context.Background(), &annotationWithTheTag)
require.NoError(b, err) require.NoError(b, err)

@ -26,7 +26,7 @@ func (repo *fakeAnnotationsRepo) Delete(_ context.Context, params *annotations.D
delete(repo.annotations, params.ID) delete(repo.annotations, params.ID)
} else { } else {
for _, v := range repo.annotations { for _, v := range repo.annotations {
if params.DashboardID == v.DashboardID && params.PanelID == v.PanelID { if params.DashboardUID == v.DashboardUID && params.PanelID == v.PanelID {
delete(repo.annotations, v.ID) delete(repo.annotations, v.ID)
} }
} }
@ -70,7 +70,7 @@ func (repo *fakeAnnotationsRepo) Find(_ context.Context, query *annotations.Item
defer repo.mtx.Unlock() defer repo.mtx.Unlock()
if annotation, has := repo.annotations[query.AnnotationID]; has { if annotation, has := repo.annotations[query.AnnotationID]; has {
return []*annotations.ItemDTO{{ID: annotation.ID, DashboardID: annotation.DashboardID}}, nil return []*annotations.ItemDTO{{ID: annotation.ID, DashboardID: annotation.DashboardID, DashboardUID: &annotation.DashboardUID}}, nil // nolint: staticcheck
} }
annotations := []*annotations.ItemDTO{{ID: 1, DashboardID: 0}} annotations := []*annotations.ItemDTO{{ID: 1, DashboardID: 0}}
return annotations, nil return annotations, nil

@ -6,12 +6,13 @@ import (
) )
type ItemQuery struct { type ItemQuery struct {
OrgID int64 `json:"orgId"` OrgID int64 `json:"orgId"`
From int64 `json:"from"` From int64 `json:"from"`
To int64 `json:"to"` To int64 `json:"to"`
UserID int64 `json:"userId"` UserID int64 `json:"userId"`
AlertID int64 `json:"alertId"` AlertID int64 `json:"alertId"`
AlertUID string `json:"alertUID"` AlertUID string `json:"alertUID"`
// Deprecated: Use DashboardUID and OrgID instead
DashboardID int64 `json:"dashboardId"` DashboardID int64 `json:"dashboardId"`
DashboardUID string `json:"dashboardUID"` DashboardUID string `json:"dashboardUID"`
PanelID int64 `json:"panelId"` PanelID int64 `json:"panelId"`
@ -72,28 +73,32 @@ type GetAnnotationTagsResponse struct {
} }
type DeleteParams struct { type DeleteParams struct {
OrgID int64 OrgID int64
ID int64 ID int64
DashboardID int64 // Deprecated: Use DashboardUID and OrgID instead
PanelID int64 DashboardID int64
DashboardUID string
PanelID int64
} }
type Item struct { type Item struct {
ID int64 `json:"id" xorm:"pk autoincr 'id'"` ID int64 `json:"id" xorm:"pk autoincr 'id'"`
OrgID int64 `json:"orgId" xorm:"org_id"` OrgID int64 `json:"orgId" xorm:"org_id"`
UserID int64 `json:"userId" xorm:"user_id"` UserID int64 `json:"userId" xorm:"user_id"`
DashboardID int64 `json:"dashboardId" xorm:"dashboard_id"` // Deprecated: Use DashboardUID and OrgID instead
PanelID int64 `json:"panelId" xorm:"panel_id"` DashboardID int64 `json:"dashboardId" xorm:"dashboard_id"`
Text string `json:"text"` DashboardUID string `json:"dashboardUID" xorm:"dashboard_uid"`
AlertID int64 `json:"alertId" xorm:"alert_id"` PanelID int64 `json:"panelId" xorm:"panel_id"`
PrevState string `json:"prevState"` Text string `json:"text"`
NewState string `json:"newState"` AlertID int64 `json:"alertId" xorm:"alert_id"`
Epoch int64 `json:"epoch"` PrevState string `json:"prevState"`
EpochEnd int64 `json:"epochEnd"` NewState string `json:"newState"`
Created int64 `json:"created"` Epoch int64 `json:"epoch"`
Updated int64 `json:"updated"` EpochEnd int64 `json:"epochEnd"`
Tags []string `json:"tags"` Created int64 `json:"created"`
Data *simplejson.Json `json:"data"` Updated int64 `json:"updated"`
Tags []string `json:"tags"`
Data *simplejson.Json `json:"data"`
// needed until we remove it from db // needed until we remove it from db
Type string Type string
@ -106,9 +111,10 @@ func (i Item) TableName() string {
// swagger:model Annotation // swagger:model Annotation
type ItemDTO struct { type ItemDTO struct {
ID int64 `json:"id" xorm:"id"` ID int64 `json:"id" xorm:"id"`
AlertID int64 `json:"alertId" xorm:"alert_id"` AlertID int64 `json:"alertId" xorm:"alert_id"`
AlertName string `json:"alertName"` AlertName string `json:"alertName"`
// Deprecated: Use DashboardUID and OrgID instead
DashboardID int64 `json:"dashboardId" xorm:"dashboard_id"` DashboardID int64 `json:"dashboardId" xorm:"dashboard_id"`
DashboardUID *string `json:"dashboardUID" xorm:"dashboard_uid"` DashboardUID *string `json:"dashboardUID" xorm:"dashboard_uid"`
PanelID int64 `json:"panelId" xorm:"panel_id"` PanelID int64 `json:"panelId" xorm:"panel_id"`
@ -164,7 +170,7 @@ func (a annotationType) String() string {
} }
func (annotation *ItemDTO) GetType() annotationType { func (annotation *ItemDTO) GetType() annotationType {
if annotation.DashboardID != 0 { if annotation.DashboardUID != nil && *annotation.DashboardUID != "" {
return Dashboard return Dashboard
} }
return Organization return Organization

@ -38,7 +38,8 @@ func (s *AnnotationServiceStore) Save(ctx context.Context, panel *PanelKey, anno
} }
for i := range annotations { for i := range annotations {
annotations[i].DashboardID = dashID annotations[i].DashboardID = dashID // nolint: staticcheck
annotations[i].DashboardUID = panel.dashUID
annotations[i].PanelID = panel.panelID annotations[i].PanelID = panel.panelID
} }
} }

@ -80,16 +80,17 @@ type AnnotationsDto struct {
} }
type AnnotationEvent struct { type AnnotationEvent struct {
Id int64 `json:"id"` Id int64 `json:"id"`
DashboardId int64 `json:"dashboardId"` DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"` DashboardUID string `json:"dashboardUID"`
Tags []string `json:"tags"` PanelId int64 `json:"panelId"`
IsRegion bool `json:"isRegion"` Tags []string `json:"tags"`
Text string `json:"text"` IsRegion bool `json:"isRegion"`
Color string `json:"color"` Text string `json:"text"`
Time int64 `json:"time"` Color string `json:"color"`
TimeEnd int64 `json:"timeEnd"` Time int64 `json:"time"`
Source dashboard.AnnotationQuery `json:"source"` TimeEnd int64 `json:"timeEnd"`
Source dashboard.AnnotationQuery `json:"source"`
} }
func (pd PublicDashboard) TableName() string { func (pd PublicDashboard) TableName() string {

@ -55,7 +55,8 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT
annoQuery.Limit = anno.Target.Limit annoQuery.Limit = anno.Target.Limit
annoQuery.MatchAny = anno.Target.MatchAny annoQuery.MatchAny = anno.Target.MatchAny
if anno.Target.Type == "tags" { if anno.Target.Type == "tags" {
annoQuery.DashboardID = 0 annoQuery.DashboardID = 0 // nolint: staticcheck
annoQuery.DashboardUID = ""
annoQuery.Tags = anno.Target.Tags annoQuery.Tags = anno.Target.Tags
} }
} }
@ -68,7 +69,7 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT
for _, item := range annotationItems { for _, item := range annotationItems {
event := models.AnnotationEvent{ event := models.AnnotationEvent{
Id: item.ID, Id: item.ID,
DashboardId: item.DashboardID, DashboardId: item.DashboardID, // nolint: staticcheck
Tags: item.Tags, Tags: item.Tags,
IsRegion: item.TimeEnd > 0 && item.Time != item.TimeEnd, IsRegion: item.TimeEnd > 0 && item.Time != item.TimeEnd,
Text: item.Text, Text: item.Text,
@ -78,6 +79,10 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT
Source: anno, Source: anno,
} }
if item.DashboardUID != nil {
event.DashboardUID = *item.DashboardUID
}
// We want dashboard annotations to reference the panel they're for. If no panelId is provided, they'll show up on all panels // We want dashboard annotations to reference the panel they're for. If no panelId is provided, they'll show up on all panels
// which is only intended for tag and org annotations. // which is only intended for tag and org annotations.
if anno.Type != nil && *anno.Type == "dashboard" { if anno.Type != nil && *anno.Type == "dashboard" {

@ -1,6 +1,8 @@
package migrations package migrations
import ( import (
"fmt"
"github.com/grafana/grafana/pkg/util/xorm" "github.com/grafana/grafana/pkg/util/xorm"
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator" . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
@ -191,6 +193,12 @@ func addAnnotationMig(mg *Migrator) {
mg.AddMigration("Increase new_state column to length 40 not null", NewRawSQLMigration(""). mg.AddMigration("Increase new_state column to length 40 not null", NewRawSQLMigration("").
Postgres("ALTER TABLE annotation ALTER COLUMN new_state TYPE VARCHAR(40);"). // Does not modify nullability. Postgres("ALTER TABLE annotation ALTER COLUMN new_state TYPE VARCHAR(40);"). // Does not modify nullability.
Mysql("ALTER TABLE annotation MODIFY new_state VARCHAR(40) NOT NULL;")) Mysql("ALTER TABLE annotation MODIFY new_state VARCHAR(40) NOT NULL;"))
mg.AddMigration("Add dashboard_uid column to annotation table", NewAddColumnMigration(table, &Column{
Name: "dashboard_uid", Type: DB_NVarchar, Length: 40, Nullable: true,
}))
mg.AddMigration("Add missing dashboard_uid to annotation table", &SetDashboardUIDMigration{})
} }
type AddMakeRegionSingleRowMigration struct { type AddMakeRegionSingleRowMigration struct {
@ -225,3 +233,40 @@ func (m *AddMakeRegionSingleRowMigration) Exec(sess *xorm.Session, mg *Migrator)
_, err = sess.Exec("DELETE FROM annotation WHERE region_id > 0 AND id <> region_id") _, err = sess.Exec("DELETE FROM annotation WHERE region_id > 0 AND id <> region_id")
return err return err
} }
type SetDashboardUIDMigration struct {
MigrationBase
}
func (m *SetDashboardUIDMigration) SQL(dialect Dialect) string {
return "code migration"
}
func (m *SetDashboardUIDMigration) Exec(sess *xorm.Session, mg *Migrator) error {
return RunDashboardUIDMigrations(sess, mg.Dialect.DriverName())
}
func RunDashboardUIDMigrations(sess *xorm.Session, driverName string) error {
sql := `UPDATE annotation
SET dashboard_uid = (SELECT uid FROM dashboard WHERE dashboard.id = annotation.dashboard_id)
WHERE dashboard_uid IS NULL AND dashboard_id != 0 AND EXISTS (SELECT 1 FROM dashboard WHERE dashboard.id = annotation.dashboard_id);`
switch driverName {
case Postgres:
sql = `UPDATE annotation
SET dashboard_uid = dashboard.uid
FROM dashboard
WHERE annotation.dashboard_id = dashboard.id
AND annotation.dashboard_id != 0
AND annotation.dashboard_uid IS NULL;`
case MySQL:
sql = `UPDATE annotation
LEFT JOIN dashboard ON annotation.dashboard_id = dashboard.id
SET annotation.dashboard_uid = dashboard.uid
WHERE annotation.dashboard_uid IS NULL and annotation.dashboard_id != 0;`
}
if _, err := sess.Exec(sql); err != nil {
return fmt.Errorf("failed to set dashboard_uid for annotation: %w", err)
}
return nil
}

@ -2818,6 +2818,7 @@
"format": "int64" "format": "int64"
}, },
"dashboardId": { "dashboardId": {
"description": "Deprecated: Use DashboardUID and OrgID instead",
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
}, },
@ -2899,6 +2900,9 @@
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
}, },
"dashboardUID": {
"type": "string"
},
"id": { "id": {
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"

@ -13025,6 +13025,7 @@
"format": "int64" "format": "int64"
}, },
"dashboardId": { "dashboardId": {
"description": "Deprecated: Use DashboardUID and OrgID instead",
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
}, },
@ -13106,6 +13107,9 @@
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
}, },
"dashboardUID": {
"type": "string"
},
"id": { "id": {
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"

@ -3075,6 +3075,7 @@
"type": "integer" "type": "integer"
}, },
"dashboardId": { "dashboardId": {
"description": "Deprecated: Use DashboardUID and OrgID instead",
"format": "int64", "format": "int64",
"type": "integer" "type": "integer"
}, },
@ -3156,6 +3157,9 @@
"format": "int64", "format": "int64",
"type": "integer" "type": "integer"
}, },
"dashboardUID": {
"type": "string"
},
"id": { "id": {
"format": "int64", "format": "int64",
"type": "integer" "type": "integer"

Loading…
Cancel
Save