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

This commit is contained in:
Stephanie Hingtgen
2025-06-13 13:59:24 -05:00
committed by GitHub
parent 47f3073ab8
commit a8886ad5ec
20 changed files with 410 additions and 228 deletions

View File

@ -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
``` ```
> The response for this HTTP request is slightly different in versions prior to v6.4. In prior versions you would > The response for this HTTP request is slightly different in versions prior to v6.4. In prior versions you would
> also get an endId if you where creating a region. But in 6.4 regions are represented using a single event with time and > also get an endId if you where creating a region. But in 6.4 regions are represented using a single event with time and
> timeEnd properties. > timeEnd properties.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,15 @@ 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) err = store.Add(context.Background(), annotation3)
require.NoError(t, err) require.NoError(t, err)
@ -520,9 +557,46 @@ 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{DashboardID: items[0].DashboardID, PanelID: items[0].PanelID, OrgID: 1})
err = store.Delete(context.Background(), &annotations.DeleteParams{DashboardID: dashboardId, PanelID: 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)
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{DashboardUID: *items[0].DashboardUID, PanelID: items[0].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)

View File

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

View File

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

View File

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

View File

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

View File

@ -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" {

View File

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

View File

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

View File

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

View File

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