mirror of
https://github.com/grafana/grafana.git
synced 2025-07-28 01:42:07 +08:00
Annotations: Use dashboard uids instead of dashboard ids (#106676)
This commit is contained in:

committed by
GitHub

parent
47f3073ab8
commit
a8886ad5ec
@ -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.
|
||||||
|
|
||||||
|
@ -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,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)
|
||||||
|
@ -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"
|
||||||
|
Reference in New Issue
Block a user