mirror of
https://github.com/grafana/grafana.git
synced 2026-08-13 06:34:55 -05:00
Chore: SQL store split for annotations (#55089)
* Chore: SQL store split for annotations * Apply suggestion from code review
This commit is contained in:
@@ -4,8 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -14,7 +12,7 @@ var (
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
Save(item *Item) error
|
||||
Save(ctx context.Context, item *Item) error
|
||||
Update(ctx context.Context, item *Item) error
|
||||
Find(ctx context.Context, query *ItemQuery) ([]*ItemDTO, error)
|
||||
Delete(ctx context.Context, params *DeleteParams) error
|
||||
@@ -26,63 +24,7 @@ type AnnotationCleaner interface {
|
||||
CleanAnnotations(ctx context.Context, cfg *setting.Cfg) (int64, int64, error)
|
||||
}
|
||||
|
||||
type ItemQuery struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
From int64 `json:"from"`
|
||||
To int64 `json:"to"`
|
||||
UserId int64 `json:"userId"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
DashboardUid string `json:"dashboardUID"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
AnnotationId int64 `json:"annotationId"`
|
||||
Tags []string `json:"tags"`
|
||||
Type string `json:"type"`
|
||||
MatchAny bool `json:"matchAny"`
|
||||
SignedInUser *user.SignedInUser
|
||||
|
||||
Limit int64 `json:"limit"`
|
||||
}
|
||||
|
||||
// TagsQuery is the query for a tags search.
|
||||
type TagsQuery struct {
|
||||
OrgID int64 `json:"orgId"`
|
||||
Tag string `json:"tag"`
|
||||
|
||||
Limit int64 `json:"limit"`
|
||||
}
|
||||
|
||||
// Tag is the DB result of a tags search.
|
||||
type Tag struct {
|
||||
Key string
|
||||
Value string
|
||||
Count int64
|
||||
}
|
||||
|
||||
// TagsDTO is the frontend DTO for Tag.
|
||||
type TagsDTO struct {
|
||||
Tag string `json:"tag"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
// FindTagsResult is the result of a tags search.
|
||||
type FindTagsResult struct {
|
||||
Tags []*TagsDTO `json:"tags"`
|
||||
}
|
||||
|
||||
// GetAnnotationTagsResponse is a response struct for FindTagsResult.
|
||||
type GetAnnotationTagsResponse struct {
|
||||
Result FindTagsResult `json:"result"`
|
||||
}
|
||||
|
||||
type DeleteParams struct {
|
||||
OrgId int64
|
||||
Id int64
|
||||
DashboardId int64
|
||||
PanelId int64
|
||||
}
|
||||
|
||||
var repositoryInstance Repository
|
||||
// var repositoryInstance Repository
|
||||
var cleanerInstance AnnotationCleaner
|
||||
|
||||
func GetAnnotationCleaner() AnnotationCleaner {
|
||||
@@ -92,84 +34,3 @@ func GetAnnotationCleaner() AnnotationCleaner {
|
||||
func SetAnnotationCleaner(rep AnnotationCleaner) {
|
||||
cleanerInstance = rep
|
||||
}
|
||||
|
||||
func GetRepository() Repository {
|
||||
return repositoryInstance
|
||||
}
|
||||
|
||||
func SetRepository(rep Repository) {
|
||||
repositoryInstance = rep
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
UserId int64 `json:"userId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
Text string `json:"text"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
PrevState string `json:"prevState"`
|
||||
NewState string `json:"newState"`
|
||||
Epoch int64 `json:"epoch"`
|
||||
EpochEnd int64 `json:"epochEnd"`
|
||||
Created int64 `json:"created"`
|
||||
Updated int64 `json:"updated"`
|
||||
Tags []string `json:"tags"`
|
||||
Data *simplejson.Json `json:"data"`
|
||||
|
||||
// needed until we remove it from db
|
||||
Type string
|
||||
Title string
|
||||
}
|
||||
|
||||
func (i Item) TableName() string {
|
||||
return "annotation"
|
||||
}
|
||||
|
||||
type ItemDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
AlertName string `json:"alertName"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
DashboardUID *string `json:"dashboardUID"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
UserId int64 `json:"userId"`
|
||||
NewState string `json:"newState"`
|
||||
PrevState string `json:"prevState"`
|
||||
Created int64 `json:"created"`
|
||||
Updated int64 `json:"updated"`
|
||||
Time int64 `json:"time"`
|
||||
TimeEnd int64 `json:"timeEnd"`
|
||||
Text string `json:"text"`
|
||||
Tags []string `json:"tags"`
|
||||
Login string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
Data *simplejson.Json `json:"data"`
|
||||
}
|
||||
|
||||
type annotationType int
|
||||
|
||||
const (
|
||||
Organization annotationType = iota
|
||||
Dashboard
|
||||
)
|
||||
|
||||
func (a annotationType) String() string {
|
||||
switch a {
|
||||
case Organization:
|
||||
return "organization"
|
||||
case Dashboard:
|
||||
return "dashboard"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (annotation *ItemDTO) GetType() annotationType {
|
||||
if annotation.DashboardId != 0 {
|
||||
return Dashboard
|
||||
}
|
||||
return Organization
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package annotationsimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/db"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type RepositoryImpl struct {
|
||||
store store
|
||||
}
|
||||
|
||||
func ProvideService(db db.DB, cfg *setting.Cfg) *RepositoryImpl {
|
||||
return &RepositoryImpl{
|
||||
store: &SQLAnnotationRepo{
|
||||
cfg: cfg,
|
||||
db: db,
|
||||
log: log.New("annotations"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RepositoryImpl) Save(ctx context.Context, item *annotations.Item) error {
|
||||
return r.store.Add(ctx, item)
|
||||
}
|
||||
|
||||
func (r *RepositoryImpl) Update(ctx context.Context, item *annotations.Item) error {
|
||||
return r.store.Update(ctx, item)
|
||||
}
|
||||
|
||||
func (r *RepositoryImpl) Find(ctx context.Context, query *annotations.ItemQuery) ([]*annotations.ItemDTO, error) {
|
||||
return r.store.Get(ctx, query)
|
||||
}
|
||||
|
||||
func (r *RepositoryImpl) Delete(ctx context.Context, params *annotations.DeleteParams) error {
|
||||
return r.store.Delete(ctx, params)
|
||||
}
|
||||
|
||||
func (r *RepositoryImpl) FindTags(ctx context.Context, query *annotations.TagsQuery) (annotations.FindTagsResult, error) {
|
||||
return r.store.GetTags(ctx, query)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package annotationsimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
)
|
||||
|
||||
type store interface {
|
||||
Add(ctx context.Context, item *annotations.Item) error
|
||||
Update(ctx context.Context, item *annotations.Item) error
|
||||
Get(ctx context.Context, query *annotations.ItemQuery) ([]*annotations.ItemDTO, error)
|
||||
Delete(ctx context.Context, params *annotations.DeleteParams) error
|
||||
GetTags(ctx context.Context, query *annotations.TagsQuery) (annotations.FindTagsResult, error)
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
package annotationsimpl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/db"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var timeNow = time.Now
|
||||
|
||||
// Update the item so that EpochEnd >= Epoch
|
||||
func validateTimeRange(item *annotations.Item) error {
|
||||
if item.EpochEnd == 0 {
|
||||
if item.Epoch == 0 {
|
||||
return annotations.ErrTimerangeMissing
|
||||
}
|
||||
item.EpochEnd = item.Epoch
|
||||
}
|
||||
if item.Epoch == 0 {
|
||||
item.Epoch = item.EpochEnd
|
||||
}
|
||||
if item.EpochEnd < item.Epoch {
|
||||
item.Epoch, item.EpochEnd = item.EpochEnd, item.Epoch
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SQLAnnotationRepo struct {
|
||||
cfg *setting.Cfg
|
||||
db db.DB
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (r *SQLAnnotationRepo) Add(ctx context.Context, item *annotations.Item) error {
|
||||
return r.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
tags := models.ParseTagPairs(item.Tags)
|
||||
item.Tags = models.JoinTagPairs(tags)
|
||||
item.Created = timeNow().UnixNano() / int64(time.Millisecond)
|
||||
item.Updated = item.Created
|
||||
if item.Epoch == 0 {
|
||||
item.Epoch = item.Created
|
||||
}
|
||||
if err := validateTimeRange(item); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sess.Table("annotation").Insert(item); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if item.Tags != nil {
|
||||
tags, err := sqlstore.EnsureTagsExist(sess, tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if _, err := sess.Exec("INSERT INTO annotation_tag (annotation_id, tag_id) VALUES(?,?)", item.Id, tag.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *SQLAnnotationRepo) Update(ctx context.Context, item *annotations.Item) error {
|
||||
return r.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
var (
|
||||
isExist bool
|
||||
err error
|
||||
)
|
||||
existing := new(annotations.Item)
|
||||
|
||||
isExist, err = sess.Table("annotation").Where("id=? AND org_id=?", item.Id, item.OrgId).Get(existing)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isExist {
|
||||
return errors.New("annotation not found")
|
||||
}
|
||||
|
||||
existing.Updated = timeNow().UnixNano() / int64(time.Millisecond)
|
||||
existing.Text = item.Text
|
||||
|
||||
if item.Epoch != 0 {
|
||||
existing.Epoch = item.Epoch
|
||||
}
|
||||
if item.EpochEnd != 0 {
|
||||
existing.EpochEnd = item.EpochEnd
|
||||
}
|
||||
|
||||
if err := validateTimeRange(existing); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if item.Tags != nil {
|
||||
tags, err := sqlstore.EnsureTagsExist(sess, models.ParseTagPairs(item.Tags))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := sess.Exec("DELETE FROM annotation_tag WHERE annotation_id = ?", existing.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if _, err := sess.Exec("INSERT INTO annotation_tag (annotation_id, tag_id) VALUES(?,?)", existing.Id, tag.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
existing.Tags = item.Tags
|
||||
|
||||
_, err = sess.Table("annotation").ID(existing.Id).Cols("epoch", "text", "epoch_end", "updated", "tags").Update(existing)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (r *SQLAnnotationRepo) Get(ctx context.Context, query *annotations.ItemQuery) ([]*annotations.ItemDTO, error) {
|
||||
var sql bytes.Buffer
|
||||
params := make([]interface{}, 0)
|
||||
items := make([]*annotations.ItemDTO, 0)
|
||||
err := r.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
sql.WriteString(`
|
||||
SELECT
|
||||
annotation.id,
|
||||
annotation.epoch as time,
|
||||
annotation.epoch_end as time_end,
|
||||
annotation.dashboard_id,
|
||||
annotation.panel_id,
|
||||
annotation.new_state,
|
||||
annotation.prev_state,
|
||||
annotation.alert_id,
|
||||
annotation.text,
|
||||
annotation.tags,
|
||||
annotation.data,
|
||||
annotation.created,
|
||||
annotation.updated,
|
||||
usr.email,
|
||||
usr.login,
|
||||
alert.name as alert_name
|
||||
FROM annotation
|
||||
LEFT OUTER JOIN ` + r.db.GetDialect().Quote("user") + ` as usr on usr.id = annotation.user_id
|
||||
LEFT OUTER JOIN alert on alert.id = annotation.alert_id
|
||||
INNER JOIN (
|
||||
SELECT a.id from annotation a
|
||||
`)
|
||||
|
||||
sql.WriteString(`WHERE a.org_id = ?`)
|
||||
params = append(params, query.OrgId)
|
||||
|
||||
if query.AnnotationId != 0 {
|
||||
// fmt.Print("annotation query")
|
||||
sql.WriteString(` AND a.id = ?`)
|
||||
params = append(params, query.AnnotationId)
|
||||
}
|
||||
|
||||
if query.AlertId != 0 {
|
||||
sql.WriteString(` AND a.alert_id = ?`)
|
||||
params = append(params, query.AlertId)
|
||||
}
|
||||
|
||||
if query.DashboardId != 0 {
|
||||
sql.WriteString(` AND a.dashboard_id = ?`)
|
||||
params = append(params, query.DashboardId)
|
||||
}
|
||||
|
||||
if query.PanelId != 0 {
|
||||
sql.WriteString(` AND a.panel_id = ?`)
|
||||
params = append(params, query.PanelId)
|
||||
}
|
||||
|
||||
if query.UserId != 0 {
|
||||
sql.WriteString(` AND a.user_id = ?`)
|
||||
params = append(params, query.UserId)
|
||||
}
|
||||
|
||||
if query.From > 0 && query.To > 0 {
|
||||
sql.WriteString(` AND a.epoch <= ? AND a.epoch_end >= ?`)
|
||||
params = append(params, query.To, query.From)
|
||||
}
|
||||
|
||||
if query.Type == "alert" {
|
||||
sql.WriteString(` AND a.alert_id > 0`)
|
||||
} else if query.Type == "annotation" {
|
||||
sql.WriteString(` AND a.alert_id = 0`)
|
||||
}
|
||||
|
||||
if len(query.Tags) > 0 {
|
||||
keyValueFilters := []string{}
|
||||
|
||||
tags := models.ParseTagPairs(query.Tags)
|
||||
for _, tag := range tags {
|
||||
if tag.Value == "" {
|
||||
keyValueFilters = append(keyValueFilters, "(tag."+r.db.GetDialect().Quote("key")+" = ?)")
|
||||
params = append(params, tag.Key)
|
||||
} else {
|
||||
keyValueFilters = append(keyValueFilters, "(tag."+r.db.GetDialect().Quote("key")+" = ? AND tag."+r.db.GetDialect().Quote("value")+" = ?)")
|
||||
params = append(params, tag.Key, tag.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if len(tags) > 0 {
|
||||
tagsSubQuery := fmt.Sprintf(`
|
||||
SELECT SUM(1) FROM annotation_tag at
|
||||
INNER JOIN tag on tag.id = at.tag_id
|
||||
WHERE at.annotation_id = a.id
|
||||
AND (
|
||||
%s
|
||||
)
|
||||
`, strings.Join(keyValueFilters, " OR "))
|
||||
|
||||
if query.MatchAny {
|
||||
sql.WriteString(fmt.Sprintf(" AND (%s) > 0 ", tagsSubQuery))
|
||||
} else {
|
||||
sql.WriteString(fmt.Sprintf(" AND (%s) = %d ", tagsSubQuery, len(tags)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !ac.IsDisabled(r.cfg) {
|
||||
acFilter, acArgs, err := getAccessControlFilter(query.SignedInUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sql.WriteString(fmt.Sprintf(" AND (%s)", acFilter))
|
||||
params = append(params, acArgs...)
|
||||
}
|
||||
|
||||
if query.Limit == 0 {
|
||||
query.Limit = 100
|
||||
}
|
||||
|
||||
// order of ORDER BY arguments match the order of a sql index for performance
|
||||
sql.WriteString(" ORDER BY a.org_id, a.epoch_end DESC, a.epoch DESC" + r.db.GetDialect().Limit(query.Limit) + " ) dt on dt.id = annotation.id")
|
||||
|
||||
spew.Dump(">>>> query: ", sql.String())
|
||||
if err := sess.SQL(sql.String(), params...).Find(&items); err != nil {
|
||||
items = nil
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
return items, err
|
||||
}
|
||||
|
||||
func getAccessControlFilter(user *user.SignedInUser) (string, []interface{}, error) {
|
||||
if user == nil || user.Permissions[user.OrgID] == nil {
|
||||
return "", nil, errors.New("missing permissions")
|
||||
}
|
||||
scopes, has := user.Permissions[user.OrgID][ac.ActionAnnotationsRead]
|
||||
if !has {
|
||||
return "", nil, errors.New("missing permissions")
|
||||
}
|
||||
types, hasWildcardScope := ac.ParseScopes(ac.ScopeAnnotationsProvider.GetResourceScopeType(""), scopes)
|
||||
if hasWildcardScope {
|
||||
types = map[interface{}]struct{}{annotations.Dashboard.String(): {}, annotations.Organization.String(): {}}
|
||||
}
|
||||
|
||||
var filters []string
|
||||
var params []interface{}
|
||||
for t := range types {
|
||||
// annotation read permission with scope annotations:type:organization allows listing annotations that are not associated with a dashboard
|
||||
if t == annotations.Organization.String() {
|
||||
filters = append(filters, "a.dashboard_id = 0")
|
||||
}
|
||||
// annotation read permission with scope annotations:type:dashboard allows listing annotations from dashboards which the user can view
|
||||
if t == annotations.Dashboard.String() {
|
||||
dashboardFilter, dashboardParams := permissions.NewAccessControlDashboardPermissionFilter(user, models.PERMISSION_VIEW, searchstore.TypeDashboard).Where()
|
||||
filter := fmt.Sprintf("a.dashboard_id IN(SELECT id FROM dashboard WHERE %s)", dashboardFilter)
|
||||
filters = append(filters, filter)
|
||||
params = dashboardParams
|
||||
}
|
||||
}
|
||||
return strings.Join(filters, " OR "), params, nil
|
||||
}
|
||||
|
||||
func (r *SQLAnnotationRepo) Delete(ctx context.Context, params *annotations.DeleteParams) error {
|
||||
return r.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
var (
|
||||
sql string
|
||||
annoTagSQL string
|
||||
)
|
||||
|
||||
r.log.Info("delete", "orgId", params.OrgId)
|
||||
if params.Id != 0 {
|
||||
annoTagSQL = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE id = ? AND org_id = ?)"
|
||||
sql = "DELETE FROM annotation WHERE id = ? AND org_id = ?"
|
||||
|
||||
if _, err := sess.Exec(annoTagSQL, params.Id, params.OrgId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sess.Exec(sql, params.Id, params.OrgId); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
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 = ?"
|
||||
|
||||
if _, err := sess.Exec(annoTagSQL, params.DashboardId, params.PanelId, params.OrgId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sess.Exec(sql, params.DashboardId, params.PanelId, params.OrgId); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *SQLAnnotationRepo) GetTags(ctx context.Context, query *annotations.TagsQuery) (annotations.FindTagsResult, error) {
|
||||
var items []*annotations.Tag
|
||||
err := r.db.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
||||
if query.Limit == 0 {
|
||||
query.Limit = 100
|
||||
}
|
||||
|
||||
var sql bytes.Buffer
|
||||
params := make([]interface{}, 0)
|
||||
tagKey := `tag.` + r.db.GetDialect().Quote("key")
|
||||
tagValue := `tag.` + r.db.GetDialect().Quote("value")
|
||||
|
||||
sql.WriteString(`
|
||||
SELECT
|
||||
` + tagKey + `,
|
||||
` + tagValue + `,
|
||||
count(*) as count
|
||||
FROM tag
|
||||
INNER JOIN annotation_tag ON tag.id = annotation_tag.tag_id
|
||||
`)
|
||||
|
||||
sql.WriteString(`WHERE EXISTS(SELECT 1 FROM annotation WHERE annotation.id = annotation_tag.annotation_id AND annotation.org_id = ?)`)
|
||||
params = append(params, query.OrgID)
|
||||
|
||||
sql.WriteString(` AND (` + tagKey + ` ` + r.db.GetDialect().LikeStr() + ` ? OR ` + tagValue + ` ` + r.db.GetDialect().LikeStr() + ` ?)`)
|
||||
params = append(params, `%`+query.Tag+`%`, `%`+query.Tag+`%`)
|
||||
|
||||
sql.WriteString(` GROUP BY ` + tagKey + `,` + tagValue)
|
||||
sql.WriteString(` ORDER BY ` + tagKey + `,` + tagValue)
|
||||
sql.WriteString(` ` + r.db.GetDialect().Limit(query.Limit))
|
||||
|
||||
err := dbSession.SQL(sql.String(), params...).Find(&items)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return annotations.FindTagsResult{Tags: []*annotations.TagsDTO{}}, err
|
||||
}
|
||||
tags := make([]*annotations.TagsDTO, 0)
|
||||
for _, item := range items {
|
||||
tag := item.Key
|
||||
if len(item.Value) > 0 {
|
||||
tag = item.Key + ":" + item.Value
|
||||
}
|
||||
tags = append(tags, &annotations.TagsDTO{
|
||||
Tag: tag,
|
||||
Count: item.Count,
|
||||
})
|
||||
}
|
||||
|
||||
return annotations.FindTagsResult{Tags: tags}, nil
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
package annotationsimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashboardstore "github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
)
|
||||
|
||||
func TestIntegrationAnnotations(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
sql := sqlstore.InitTestDB(t)
|
||||
repo := SQLAnnotationRepo{db: sql, cfg: setting.NewCfg(), log: log.New("annotation.test")}
|
||||
|
||||
testUser := &user.SignedInUser{
|
||||
OrgID: 1,
|
||||
Permissions: map[int64]map[string][]string{
|
||||
1: {
|
||||
accesscontrol.ActionAnnotationsRead: []string{accesscontrol.ScopeAnnotationsAll},
|
||||
dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("Testing annotation create, read, update and delete", func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
err := sql.WithDbSession(context.Background(), func(dbSession *sqlstore.DBSession) error {
|
||||
_, err := dbSession.Exec("DELETE FROM annotation WHERE 1=1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = dbSession.Exec("DELETE FROM annotation_tag WHERE 1=1")
|
||||
return err
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
dashboardStore := dashboardstore.ProvideDashboardStore(sql, featuremgmt.WithFeatures())
|
||||
|
||||
testDashboard1 := models.SaveDashboardCommand{
|
||||
UserId: 1,
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"title": "Dashboard 1",
|
||||
}),
|
||||
}
|
||||
dashboard, err := dashboardStore.SaveDashboard(testDashboard1)
|
||||
require.NoError(t, err)
|
||||
|
||||
testDashboard2 := models.SaveDashboardCommand{
|
||||
UserId: 1,
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"title": "Dashboard 2",
|
||||
}),
|
||||
}
|
||||
dashboard2, err := dashboardStore.SaveDashboard(testDashboard2)
|
||||
require.NoError(t, err)
|
||||
|
||||
annotation := &annotations.Item{
|
||||
OrgId: 1,
|
||||
UserId: 1,
|
||||
DashboardId: dashboard.Id,
|
||||
Text: "hello",
|
||||
Type: "alert",
|
||||
Epoch: 10,
|
||||
Tags: []string{"outage", "error", "type:outage", "server:server-1"},
|
||||
}
|
||||
err = repo.Add(context.Background(), annotation)
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, annotation.Id, int64(0))
|
||||
assert.Equal(t, annotation.Epoch, annotation.EpochEnd)
|
||||
|
||||
annotation2 := &annotations.Item{
|
||||
OrgId: 1,
|
||||
UserId: 1,
|
||||
DashboardId: dashboard2.Id,
|
||||
Text: "hello",
|
||||
Type: "alert",
|
||||
Epoch: 21, // Should swap epoch & epochEnd
|
||||
EpochEnd: 20,
|
||||
Tags: []string{"outage", "error", "type:outage", "server:server-1"},
|
||||
}
|
||||
err = repo.Add(context.Background(), annotation2)
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, annotation2.Id, int64(0))
|
||||
assert.Equal(t, int64(20), annotation2.Epoch)
|
||||
assert.Equal(t, int64(21), annotation2.EpochEnd)
|
||||
|
||||
organizationAnnotation1 := &annotations.Item{
|
||||
OrgId: 1,
|
||||
UserId: 1,
|
||||
Text: "deploy",
|
||||
Type: "",
|
||||
Epoch: 15,
|
||||
Tags: []string{"deploy"},
|
||||
}
|
||||
err = repo.Add(context.Background(), organizationAnnotation1)
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, organizationAnnotation1.Id, int64(0))
|
||||
|
||||
globalAnnotation2 := &annotations.Item{
|
||||
OrgId: 1,
|
||||
UserId: 1,
|
||||
Text: "rollback",
|
||||
Type: "",
|
||||
Epoch: 17,
|
||||
Tags: []string{"rollback"},
|
||||
}
|
||||
err = repo.Add(context.Background(), globalAnnotation2)
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, globalAnnotation2.Id, int64(0))
|
||||
t.Run("Can query for annotation by dashboard id", func(t *testing.T) {
|
||||
items, err := repo.Get(context.Background(), &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: dashboard.Id,
|
||||
From: 0,
|
||||
To: 15,
|
||||
SignedInUser: testUser,
|
||||
})
|
||||
|
||||
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 id", func(t *testing.T) {
|
||||
items, err := repo.Get(context.Background(), &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
AnnotationId: annotation2.Id,
|
||||
SignedInUser: testUser,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, items, 1)
|
||||
assert.Equal(t, annotation2.Id, items[0].Id)
|
||||
})
|
||||
|
||||
t.Run("Should not find any when item is outside time range", func(t *testing.T) {
|
||||
items, err := repo.Get(context.Background(), &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 12,
|
||||
To: 15,
|
||||
SignedInUser: testUser,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, items)
|
||||
})
|
||||
|
||||
t.Run("Should not find one when tag filter does not match", func(t *testing.T) {
|
||||
items, err := repo.Get(context.Background(), &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 1,
|
||||
To: 15,
|
||||
Tags: []string{"asd"},
|
||||
SignedInUser: testUser,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, items)
|
||||
})
|
||||
|
||||
t.Run("Should not find one when type filter does not match", func(t *testing.T) {
|
||||
items, err := repo.Get(context.Background(), &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 1,
|
||||
To: 15,
|
||||
Type: "alert",
|
||||
SignedInUser: testUser,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, items)
|
||||
})
|
||||
|
||||
t.Run("Should find one when all tag filters does match", func(t *testing.T) {
|
||||
items, err := repo.Get(context.Background(), &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 1,
|
||||
To: 15, // this will exclude the second test annotation
|
||||
Tags: []string{"outage", "error"},
|
||||
SignedInUser: testUser,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, items, 1)
|
||||
})
|
||||
|
||||
t.Run("Should find two annotations using partial match", func(t *testing.T) {
|
||||
items, err := repo.Get(context.Background(), &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
From: 1,
|
||||
To: 25,
|
||||
MatchAny: true,
|
||||
Tags: []string{"rollback", "deploy"},
|
||||
SignedInUser: testUser,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, items, 2)
|
||||
})
|
||||
|
||||
t.Run("Should find one when all key value tag filters does match", func(t *testing.T) {
|
||||
items, err := repo.Get(context.Background(), &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 1,
|
||||
To: 15,
|
||||
Tags: []string{"type:outage", "server:server-1"},
|
||||
SignedInUser: testUser,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, items, 1)
|
||||
})
|
||||
|
||||
t.Run("Can update annotation and remove all tags", func(t *testing.T) {
|
||||
query := &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 0,
|
||||
To: 15,
|
||||
SignedInUser: testUser,
|
||||
}
|
||||
items, err := repo.Get(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
|
||||
annotationId := items[0].Id
|
||||
err = repo.Update(context.Background(), &annotations.Item{
|
||||
Id: annotationId,
|
||||
OrgId: 1,
|
||||
Text: "something new",
|
||||
Tags: []string{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
items, err = repo.Get(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, annotationId, items[0].Id)
|
||||
assert.Empty(t, items[0].Tags)
|
||||
assert.Equal(t, "something new", items[0].Text)
|
||||
})
|
||||
|
||||
t.Run("Can update annotation with new tags", func(t *testing.T) {
|
||||
query := &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 0,
|
||||
To: 15,
|
||||
SignedInUser: testUser,
|
||||
}
|
||||
items, err := repo.Get(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
|
||||
annotationId := items[0].Id
|
||||
err = repo.Update(context.Background(), &annotations.Item{
|
||||
Id: annotationId,
|
||||
OrgId: 1,
|
||||
Text: "something new",
|
||||
Tags: []string{"newtag1", "newtag2"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
items, err = repo.Get(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, annotationId, items[0].Id)
|
||||
assert.Equal(t, []string{"newtag1", "newtag2"}, items[0].Tags)
|
||||
assert.Equal(t, "something new", items[0].Text)
|
||||
assert.Greater(t, items[0].Updated, items[0].Created)
|
||||
})
|
||||
|
||||
t.Run("Can delete annotation", func(t *testing.T) {
|
||||
query := &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 0,
|
||||
To: 15,
|
||||
SignedInUser: testUser,
|
||||
}
|
||||
items, err := repo.Get(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
|
||||
annotationId := items[0].Id
|
||||
err = repo.Delete(context.Background(), &annotations.DeleteParams{Id: annotationId, OrgId: 1})
|
||||
require.NoError(t, err)
|
||||
|
||||
items, err = repo.Get(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, items)
|
||||
})
|
||||
|
||||
t.Run("Can delete annotation using dashboard id and panel id", func(t *testing.T) {
|
||||
annotation3 := &annotations.Item{
|
||||
OrgId: 1,
|
||||
UserId: 1,
|
||||
DashboardId: dashboard2.Id,
|
||||
Text: "toBeDeletedWithPanelId",
|
||||
Type: "alert",
|
||||
Epoch: 11,
|
||||
Tags: []string{"test"},
|
||||
PanelId: 20,
|
||||
}
|
||||
err = repo.Add(context.Background(), annotation3)
|
||||
require.NoError(t, err)
|
||||
|
||||
query := &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
AnnotationId: annotation3.Id,
|
||||
SignedInUser: testUser,
|
||||
}
|
||||
items, err := repo.Get(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
|
||||
dashboardId := items[0].DashboardId
|
||||
panelId := items[0].PanelId
|
||||
err = repo.Delete(context.Background(), &annotations.DeleteParams{DashboardId: dashboardId, PanelId: panelId, OrgId: 1})
|
||||
require.NoError(t, err)
|
||||
|
||||
items, err = repo.Get(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, items)
|
||||
})
|
||||
|
||||
t.Run("Should find tags by key", func(t *testing.T) {
|
||||
result, err := repo.GetTags(context.Background(), &annotations.TagsQuery{
|
||||
OrgID: 1,
|
||||
Tag: "server",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.Tags, 1)
|
||||
require.Equal(t, "server:server-1", result.Tags[0].Tag)
|
||||
require.Equal(t, int64(1), result.Tags[0].Count)
|
||||
})
|
||||
|
||||
t.Run("Should find tags by value", func(t *testing.T) {
|
||||
result, err := repo.GetTags(context.Background(), &annotations.TagsQuery{
|
||||
OrgID: 1,
|
||||
Tag: "outage",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.Tags, 2)
|
||||
require.Equal(t, "outage", result.Tags[0].Tag)
|
||||
require.Equal(t, "type:outage", result.Tags[1].Tag)
|
||||
require.Equal(t, int64(1), result.Tags[0].Count)
|
||||
require.Equal(t, int64(1), result.Tags[1].Count)
|
||||
})
|
||||
|
||||
t.Run("Should not find tags in other org", func(t *testing.T) {
|
||||
result, err := repo.GetTags(context.Background(), &annotations.TagsQuery{
|
||||
OrgID: 0,
|
||||
Tag: "server-1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.Tags, 0)
|
||||
})
|
||||
|
||||
t.Run("Should not find tags that do not exist", func(t *testing.T) {
|
||||
result, err := repo.GetTags(context.Background(), &annotations.TagsQuery{
|
||||
OrgID: 0,
|
||||
Tag: "unknown:tag",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.Tags, 0)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
sql := sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{})
|
||||
repo := SQLAnnotationRepo{db: sql, cfg: setting.NewCfg(), log: log.New("annotation.test")}
|
||||
dashboardStore := dashboardstore.ProvideDashboardStore(sql, featuremgmt.WithFeatures())
|
||||
|
||||
testDashboard1 := models.SaveDashboardCommand{
|
||||
UserId: 1,
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"title": "Dashboard 1",
|
||||
}),
|
||||
}
|
||||
dashboard, err := dashboardStore.SaveDashboard(testDashboard1)
|
||||
require.NoError(t, err)
|
||||
dash1UID := dashboard.Uid
|
||||
|
||||
testDashboard2 := models.SaveDashboardCommand{
|
||||
UserId: 1,
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"title": "Dashboard 2",
|
||||
}),
|
||||
}
|
||||
_, err = dashboardStore.SaveDashboard(testDashboard2)
|
||||
require.NoError(t, err)
|
||||
|
||||
dash1Annotation := &annotations.Item{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
Epoch: 10,
|
||||
}
|
||||
err = repo.Add(context.Background(), dash1Annotation)
|
||||
require.NoError(t, err)
|
||||
|
||||
dash2Annotation := &annotations.Item{
|
||||
OrgId: 1,
|
||||
DashboardId: 2,
|
||||
Epoch: 10,
|
||||
}
|
||||
err = repo.Add(context.Background(), dash2Annotation)
|
||||
require.NoError(t, err)
|
||||
|
||||
organizationAnnotation := &annotations.Item{
|
||||
OrgId: 1,
|
||||
Epoch: 10,
|
||||
}
|
||||
err = repo.Add(context.Background(), organizationAnnotation)
|
||||
require.NoError(t, err)
|
||||
|
||||
user := &user.SignedInUser{
|
||||
UserID: 1,
|
||||
OrgID: 1,
|
||||
}
|
||||
|
||||
type testStruct struct {
|
||||
description string
|
||||
permissions map[string][]string
|
||||
expectedAnnotationIds []int64
|
||||
expectedError bool
|
||||
}
|
||||
|
||||
testCases := []testStruct{
|
||||
{
|
||||
description: "Should find all annotations when has permissions to list all annotations and read all dashboards",
|
||||
permissions: map[string][]string{
|
||||
accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsAll},
|
||||
dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll},
|
||||
},
|
||||
expectedAnnotationIds: []int64{dash1Annotation.Id, dash2Annotation.Id, organizationAnnotation.Id},
|
||||
},
|
||||
{
|
||||
description: "Should find all dashboard annotations",
|
||||
permissions: map[string][]string{
|
||||
accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsTypeDashboard},
|
||||
dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll},
|
||||
},
|
||||
expectedAnnotationIds: []int64{dash1Annotation.Id, dash2Annotation.Id},
|
||||
},
|
||||
{
|
||||
description: "Should find only annotations from dashboards that user can read",
|
||||
permissions: map[string][]string{
|
||||
accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsTypeDashboard},
|
||||
dashboards.ActionDashboardsRead: {fmt.Sprintf("dashboards:uid:%s", dash1UID)},
|
||||
},
|
||||
expectedAnnotationIds: []int64{dash1Annotation.Id},
|
||||
},
|
||||
{
|
||||
description: "Should find no annotations if user can't view dashboards or organization annotations",
|
||||
permissions: map[string][]string{
|
||||
accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsTypeDashboard},
|
||||
},
|
||||
expectedAnnotationIds: []int64{},
|
||||
},
|
||||
{
|
||||
description: "Should find only organization annotations",
|
||||
permissions: map[string][]string{
|
||||
accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsTypeOrganization},
|
||||
dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll},
|
||||
},
|
||||
expectedAnnotationIds: []int64{organizationAnnotation.Id},
|
||||
},
|
||||
{
|
||||
description: "Should error if user doesn't have annotation read permissions",
|
||||
permissions: map[string][]string{
|
||||
dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll},
|
||||
},
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
user.Permissions = map[int64]map[string][]string{1: tc.permissions}
|
||||
results, err := repo.Get(context.Background(), &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
SignedInUser: user,
|
||||
})
|
||||
if tc.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, results, len(tc.expectedAnnotationIds))
|
||||
for _, r := range results {
|
||||
assert.Contains(t, tc.expectedAnnotationIds, r.Id)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package annotationstest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
)
|
||||
|
||||
type fakeAnnotationsRepo struct {
|
||||
mtx sync.Mutex
|
||||
annotations map[int64]annotations.Item
|
||||
}
|
||||
|
||||
func NewFakeAnnotationsRepo() *fakeAnnotationsRepo {
|
||||
return &fakeAnnotationsRepo{
|
||||
annotations: map[int64]annotations.Item{},
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *fakeAnnotationsRepo) Delete(_ context.Context, params *annotations.DeleteParams) error {
|
||||
repo.mtx.Lock()
|
||||
defer repo.mtx.Unlock()
|
||||
|
||||
if params.Id != 0 {
|
||||
delete(repo.annotations, params.Id)
|
||||
} else {
|
||||
for _, v := range repo.annotations {
|
||||
if params.DashboardId == v.DashboardId && params.PanelId == v.PanelId {
|
||||
delete(repo.annotations, v.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *fakeAnnotationsRepo) Save(ctx context.Context, item *annotations.Item) error {
|
||||
repo.mtx.Lock()
|
||||
defer repo.mtx.Unlock()
|
||||
|
||||
if item.Id == 0 {
|
||||
item.Id = int64(len(repo.annotations) + 1)
|
||||
}
|
||||
repo.annotations[item.Id] = *item
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *fakeAnnotationsRepo) Update(_ context.Context, item *annotations.Item) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *fakeAnnotationsRepo) Find(_ context.Context, query *annotations.ItemQuery) ([]*annotations.ItemDTO, error) {
|
||||
repo.mtx.Lock()
|
||||
defer repo.mtx.Unlock()
|
||||
|
||||
if annotation, has := repo.annotations[query.AnnotationId]; has {
|
||||
return []*annotations.ItemDTO{{Id: annotation.Id, DashboardId: annotation.DashboardId}}, nil
|
||||
}
|
||||
annotations := []*annotations.ItemDTO{{Id: 1, DashboardId: 0}}
|
||||
return annotations, nil
|
||||
}
|
||||
|
||||
func (repo *fakeAnnotationsRepo) FindTags(_ context.Context, query *annotations.TagsQuery) (annotations.FindTagsResult, error) {
|
||||
result := annotations.FindTagsResult{
|
||||
Tags: []*annotations.TagsDTO{},
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (repo *fakeAnnotationsRepo) Len() int {
|
||||
repo.mtx.Lock()
|
||||
defer repo.mtx.Unlock()
|
||||
|
||||
return len(repo.annotations)
|
||||
}
|
||||
|
||||
func (repo *fakeAnnotationsRepo) Items() map[int64]annotations.Item {
|
||||
repo.mtx.Lock()
|
||||
defer repo.mtx.Unlock()
|
||||
|
||||
return repo.annotations
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package annotations
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
)
|
||||
|
||||
type ItemQuery struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
From int64 `json:"from"`
|
||||
To int64 `json:"to"`
|
||||
UserId int64 `json:"userId"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
DashboardUid string `json:"dashboardUID"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
AnnotationId int64 `json:"annotationId"`
|
||||
Tags []string `json:"tags"`
|
||||
Type string `json:"type"`
|
||||
MatchAny bool `json:"matchAny"`
|
||||
SignedInUser *user.SignedInUser
|
||||
|
||||
Limit int64 `json:"limit"`
|
||||
}
|
||||
|
||||
// TagsQuery is the query for a tags search.
|
||||
type TagsQuery struct {
|
||||
OrgID int64 `json:"orgId"`
|
||||
Tag string `json:"tag"`
|
||||
|
||||
Limit int64 `json:"limit"`
|
||||
}
|
||||
|
||||
// Tag is the DB result of a tags search.
|
||||
type Tag struct {
|
||||
Key string
|
||||
Value string
|
||||
Count int64
|
||||
}
|
||||
|
||||
// TagsDTO is the frontend DTO for Tag.
|
||||
type TagsDTO struct {
|
||||
Tag string `json:"tag"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
// FindTagsResult is the result of a tags search.
|
||||
type FindTagsResult struct {
|
||||
Tags []*TagsDTO `json:"tags"`
|
||||
}
|
||||
|
||||
// GetAnnotationTagsResponse is a response struct for FindTagsResult.
|
||||
type GetAnnotationTagsResponse struct {
|
||||
Result FindTagsResult `json:"result"`
|
||||
}
|
||||
|
||||
type DeleteParams struct {
|
||||
OrgId int64
|
||||
Id int64
|
||||
DashboardId int64
|
||||
PanelId int64
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
UserId int64 `json:"userId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
Text string `json:"text"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
PrevState string `json:"prevState"`
|
||||
NewState string `json:"newState"`
|
||||
Epoch int64 `json:"epoch"`
|
||||
EpochEnd int64 `json:"epochEnd"`
|
||||
Created int64 `json:"created"`
|
||||
Updated int64 `json:"updated"`
|
||||
Tags []string `json:"tags"`
|
||||
Data *simplejson.Json `json:"data"`
|
||||
|
||||
// needed until we remove it from db
|
||||
Type string
|
||||
Title string
|
||||
}
|
||||
|
||||
func (i Item) TableName() string {
|
||||
return "annotation"
|
||||
}
|
||||
|
||||
type ItemDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
AlertName string `json:"alertName"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
DashboardUID *string `json:"dashboardUID"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
UserId int64 `json:"userId"`
|
||||
NewState string `json:"newState"`
|
||||
PrevState string `json:"prevState"`
|
||||
Created int64 `json:"created"`
|
||||
Updated int64 `json:"updated"`
|
||||
Time int64 `json:"time"`
|
||||
TimeEnd int64 `json:"timeEnd"`
|
||||
Text string `json:"text"`
|
||||
Tags []string `json:"tags"`
|
||||
Login string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
Data *simplejson.Json `json:"data"`
|
||||
}
|
||||
|
||||
type annotationType int
|
||||
|
||||
const (
|
||||
Organization annotationType = iota
|
||||
Dashboard
|
||||
)
|
||||
|
||||
func (a annotationType) String() string {
|
||||
switch a {
|
||||
case Organization:
|
||||
return "organization"
|
||||
case Dashboard:
|
||||
return "dashboard"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (annotation *ItemDTO) GetType() annotationType {
|
||||
if annotation.DashboardId != 0 {
|
||||
return Dashboard
|
||||
}
|
||||
return Organization
|
||||
}
|
||||
Reference in New Issue
Block a user