mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
SearchV2: instant local updates, folder events (#50001)
This commit is contained in:
parent
49d93fb67e
commit
d2868a1ce7
@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/guardian"
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||||
|
"github.com/grafana/grafana/pkg/services/store"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
@ -70,6 +71,14 @@ func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return apierrors.ToFolderErrorResponse(err)
|
return apierrors.ToFolderErrorResponse(err)
|
||||||
}
|
}
|
||||||
|
if hs.entityEventsService != nil {
|
||||||
|
if err := hs.entityEventsService.SaveEvent(c.Req.Context(), store.SaveEventCmd{
|
||||||
|
EntityId: store.CreateDatabaseEntityId(folder.Uid, c.OrgId, store.EntityTypeFolder),
|
||||||
|
EventType: store.EntityEventTypeCreate,
|
||||||
|
}); err != nil {
|
||||||
|
hs.log.Warn("failed to save folder entity event", "uid", folder.Uid, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
g := guardian.New(c.Req.Context(), folder.Id, c.OrgId, c.SignedInUser)
|
g := guardian.New(c.Req.Context(), folder.Id, c.OrgId, c.SignedInUser)
|
||||||
return response.JSON(http.StatusOK, hs.toFolderDto(c.Req.Context(), g, folder))
|
return response.JSON(http.StatusOK, hs.toFolderDto(c.Req.Context(), g, folder))
|
||||||
@ -84,6 +93,14 @@ func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return apierrors.ToFolderErrorResponse(err)
|
return apierrors.ToFolderErrorResponse(err)
|
||||||
}
|
}
|
||||||
|
if hs.entityEventsService != nil {
|
||||||
|
if err := hs.entityEventsService.SaveEvent(c.Req.Context(), store.SaveEventCmd{
|
||||||
|
EntityId: store.CreateDatabaseEntityId(cmd.Uid, c.OrgId, store.EntityTypeFolder),
|
||||||
|
EventType: store.EntityEventTypeUpdate,
|
||||||
|
}); err != nil {
|
||||||
|
hs.log.Warn("failed to save folder entity event", "uid", cmd.Uid, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
g := guardian.New(c.Req.Context(), cmd.Result.Id, c.OrgId, c.SignedInUser)
|
g := guardian.New(c.Req.Context(), cmd.Result.Id, c.OrgId, c.SignedInUser)
|
||||||
return response.JSON(http.StatusOK, hs.toFolderDto(c.Req.Context(), g, cmd.Result))
|
return response.JSON(http.StatusOK, hs.toFolderDto(c.Req.Context(), g, cmd.Result))
|
||||||
@ -98,10 +115,19 @@ func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { //
|
|||||||
return apierrors.ToFolderErrorResponse(err)
|
return apierrors.ToFolderErrorResponse(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := hs.folderService.DeleteFolder(c.Req.Context(), c.SignedInUser, c.OrgId, web.Params(c.Req)[":uid"], c.QueryBool("forceDeleteRules"))
|
uid := web.Params(c.Req)[":uid"]
|
||||||
|
f, err := hs.folderService.DeleteFolder(c.Req.Context(), c.SignedInUser, c.OrgId, uid, c.QueryBool("forceDeleteRules"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return apierrors.ToFolderErrorResponse(err)
|
return apierrors.ToFolderErrorResponse(err)
|
||||||
}
|
}
|
||||||
|
if hs.entityEventsService != nil {
|
||||||
|
if err := hs.entityEventsService.SaveEvent(c.Req.Context(), store.SaveEventCmd{
|
||||||
|
EntityId: store.CreateDatabaseEntityId(uid, c.OrgId, store.EntityTypeFolder),
|
||||||
|
EventType: store.EntityEventTypeDelete,
|
||||||
|
}); err != nil {
|
||||||
|
hs.log.Warn("failed to save folder entity event", "uid", uid, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return response.JSON(http.StatusOK, util.DynMap{
|
return response.JSON(http.StatusOK, util.DynMap{
|
||||||
"title": f.Title,
|
"title": f.Title,
|
||||||
|
@ -105,7 +105,10 @@ func initIndex(dashboards []dashboard, logger log.Logger, extendDoc ExtendDashbo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Index each panel in dashboard.
|
// Index each panel in dashboard.
|
||||||
location += "/" + dash.uid
|
if location != "" {
|
||||||
|
location += "/"
|
||||||
|
}
|
||||||
|
location += dash.uid
|
||||||
docs := getDashboardPanelDocs(dash, location)
|
docs := getDashboardPanelDocs(dash, location)
|
||||||
for _, panelDoc := range docs {
|
for _, panelDoc := range docs {
|
||||||
batch.Insert(panelDoc)
|
batch.Insert(panelDoc)
|
||||||
@ -194,7 +197,6 @@ func getDashboardPanelDocs(dash dashboard, location string) []*bluge.Document {
|
|||||||
purl := fmt.Sprintf("%s?viewPanel=%d", url, panel.ID)
|
purl := fmt.Sprintf("%s?viewPanel=%d", url, panel.ID)
|
||||||
|
|
||||||
doc := newSearchDocument(uid, panel.Title, panel.Description, purl).
|
doc := newSearchDocument(uid, panel.Title, panel.Description, purl).
|
||||||
AddField(bluge.NewKeywordField(documentFieldDSUID, dash.uid).StoreValue()).
|
|
||||||
AddField(bluge.NewKeywordField(documentFieldPanelType, panel.Type).Aggregatable().StoreValue()).
|
AddField(bluge.NewKeywordField(documentFieldPanelType, panel.Type).Aggregatable().StoreValue()).
|
||||||
AddField(bluge.NewKeywordField(documentFieldLocation, location).Aggregatable().StoreValue()).
|
AddField(bluge.NewKeywordField(documentFieldLocation, location).Aggregatable().StoreValue()).
|
||||||
AddField(bluge.NewKeywordField(documentFieldKind, string(entityKindPanel)).Aggregatable().StoreValue()) // likely want independent index for this
|
AddField(bluge.NewKeywordField(documentFieldKind, string(entityKindPanel)).Aggregatable().StoreValue()) // likely want independent index for this
|
||||||
@ -263,10 +265,10 @@ func newSearchDocument(uid string, name string, descr string, url string) *bluge
|
|||||||
return doc
|
return doc
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDashboardPanelIDs(reader *bluge.Reader, dashboardUID string) ([]string, error) {
|
func getDashboardPanelIDs(reader *bluge.Reader, panelLocation string) ([]string, error) {
|
||||||
var panelIDs []string
|
var panelIDs []string
|
||||||
fullQuery := bluge.NewBooleanQuery()
|
fullQuery := bluge.NewBooleanQuery()
|
||||||
fullQuery.AddMust(bluge.NewTermQuery(dashboardUID).SetField(documentFieldDSUID))
|
fullQuery.AddMust(bluge.NewTermQuery(panelLocation).SetField(documentFieldLocation))
|
||||||
fullQuery.AddMust(bluge.NewTermQuery(string(entityKindPanel)).SetField(documentFieldKind))
|
fullQuery.AddMust(bluge.NewTermQuery(string(entityKindPanel)).SetField(documentFieldKind))
|
||||||
req := bluge.NewAllMatches(fullQuery)
|
req := bluge.NewAllMatches(fullQuery)
|
||||||
documentMatchIterator, err := reader.Search(context.Background(), req)
|
documentMatchIterator, err := reader.Search(context.Background(), req)
|
||||||
@ -291,6 +293,64 @@ func getDashboardPanelIDs(reader *bluge.Reader, dashboardUID string) ([]string,
|
|||||||
return panelIDs, err
|
return panelIDs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getDocsIDsByLocationPrefix(reader *bluge.Reader, prefix string) ([]string, error) {
|
||||||
|
var ids []string
|
||||||
|
fullQuery := bluge.NewBooleanQuery()
|
||||||
|
fullQuery.AddMust(bluge.NewPrefixQuery(prefix).SetField(documentFieldLocation))
|
||||||
|
req := bluge.NewAllMatches(fullQuery)
|
||||||
|
documentMatchIterator, err := reader.Search(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
match, err := documentMatchIterator.Next()
|
||||||
|
for err == nil && match != nil {
|
||||||
|
// load the identifier for this match
|
||||||
|
err = match.VisitStoredFields(func(field string, value []byte) bool {
|
||||||
|
if field == documentFieldUID {
|
||||||
|
ids = append(ids, string(value))
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// load the next document match
|
||||||
|
match, err = documentMatchIterator.Next()
|
||||||
|
}
|
||||||
|
return ids, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDashboardLocation(reader *bluge.Reader, dashboardUID string) (string, bool, error) {
|
||||||
|
var dashboardLocation string
|
||||||
|
var found bool
|
||||||
|
fullQuery := bluge.NewBooleanQuery()
|
||||||
|
fullQuery.AddMust(bluge.NewTermQuery(dashboardUID).SetField(documentFieldUID))
|
||||||
|
fullQuery.AddMust(bluge.NewTermQuery(string(entityKindDashboard)).SetField(documentFieldKind))
|
||||||
|
req := bluge.NewAllMatches(fullQuery)
|
||||||
|
documentMatchIterator, err := reader.Search(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
match, err := documentMatchIterator.Next()
|
||||||
|
for err == nil && match != nil {
|
||||||
|
// load the identifier for this match
|
||||||
|
err = match.VisitStoredFields(func(field string, value []byte) bool {
|
||||||
|
if field == documentFieldLocation {
|
||||||
|
dashboardLocation = string(value)
|
||||||
|
found = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
// load the next document match
|
||||||
|
match, err = documentMatchIterator.Next()
|
||||||
|
}
|
||||||
|
return dashboardLocation, found, err
|
||||||
|
}
|
||||||
|
|
||||||
//nolint: gocyclo
|
//nolint: gocyclo
|
||||||
func doSearchQuery(
|
func doSearchQuery(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
@ -30,6 +30,7 @@ type dashboardLoader interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type eventStore interface {
|
type eventStore interface {
|
||||||
|
OnEvent(handler store.EventHandler)
|
||||||
GetLastEvent(ctx context.Context) (*store.EntityEvent, error)
|
GetLastEvent(ctx context.Context) (*store.EntityEvent, error)
|
||||||
GetAllEventsAfter(ctx context.Context, id int64) ([]*store.EntityEvent, error)
|
GetAllEventsAfter(ctx context.Context, id int64) ([]*store.EntityEvent, error)
|
||||||
}
|
}
|
||||||
@ -95,6 +96,8 @@ func (i *dashboardIndex) run(ctx context.Context) error {
|
|||||||
return fmt.Errorf("can't build initial dashboard search index: %w", err)
|
return fmt.Errorf("can't build initial dashboard search index: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i.eventStore.OnEvent(i.applyEventOnIndex)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-partialUpdateTicker.C:
|
case <-partialUpdateTicker.C:
|
||||||
@ -293,7 +296,6 @@ func (i *dashboardIndex) applyIndexUpdates(ctx context.Context, lastEventID int6
|
|||||||
}
|
}
|
||||||
started := time.Now()
|
started := time.Now()
|
||||||
for _, e := range events {
|
for _, e := range events {
|
||||||
i.logger.Debug("processing event", "event", e)
|
|
||||||
err := i.applyEventOnIndex(ctx, e)
|
err := i.applyEventOnIndex(ctx, e)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
i.logger.Error("can't apply event", "error", err)
|
i.logger.Error("can't apply event", "error", err)
|
||||||
@ -306,31 +308,30 @@ func (i *dashboardIndex) applyIndexUpdates(ctx context.Context, lastEventID int6
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *dashboardIndex) applyEventOnIndex(ctx context.Context, e *store.EntityEvent) error {
|
func (i *dashboardIndex) applyEventOnIndex(ctx context.Context, e *store.EntityEvent) error {
|
||||||
|
i.logger.Debug("processing event", "event", e)
|
||||||
|
|
||||||
if !strings.HasPrefix(e.EntityId, "database/") {
|
if !strings.HasPrefix(e.EntityId, "database/") {
|
||||||
i.logger.Warn("unknown storage", "entityId", e.EntityId)
|
i.logger.Warn("unknown storage", "entityId", e.EntityId)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
parts := strings.Split(strings.TrimPrefix(e.EntityId, "database/"), "/")
|
// database/org/entityType/path*
|
||||||
|
parts := strings.SplitN(strings.TrimPrefix(e.EntityId, "database/"), "/", 3)
|
||||||
if len(parts) != 3 {
|
if len(parts) != 3 {
|
||||||
i.logger.Error("can't parse entityId", "entityId", e.EntityId)
|
i.logger.Error("can't parse entityId", "entityId", e.EntityId)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
orgIDStr := parts[0]
|
orgIDStr := parts[0]
|
||||||
kind := parts[1]
|
orgID, err := strconv.ParseInt(orgIDStr, 10, 64)
|
||||||
dashboardUID := parts[2]
|
|
||||||
if kind != "dashboard" {
|
|
||||||
i.logger.Error("unknown kind in entityId", "entityId", e.EntityId)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
orgID, err := strconv.Atoi(orgIDStr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
i.logger.Error("can't extract org ID", "entityId", e.EntityId)
|
i.logger.Error("can't extract org ID", "entityId", e.EntityId)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return i.applyDashboardEvent(ctx, int64(orgID), dashboardUID, e.EventType)
|
kind := store.EntityType(parts[1])
|
||||||
|
uid := parts[2]
|
||||||
|
return i.applyEvent(ctx, orgID, kind, uid, e.EventType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *dashboardIndex) applyDashboardEvent(ctx context.Context, orgID int64, dashboardUID string, _ store.EntityEventType) error {
|
func (i *dashboardIndex) applyEvent(ctx context.Context, orgID int64, kind store.EntityType, uid string, _ store.EntityEventType) error {
|
||||||
i.mu.Lock()
|
i.mu.Lock()
|
||||||
_, ok := i.perOrgWriter[orgID]
|
_, ok := i.perOrgWriter[orgID]
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -340,7 +341,8 @@ func (i *dashboardIndex) applyDashboardEvent(ctx context.Context, orgID int64, d
|
|||||||
}
|
}
|
||||||
i.mu.Unlock()
|
i.mu.Unlock()
|
||||||
|
|
||||||
dbDashboards, err := i.loader.LoadDashboards(ctx, orgID, dashboardUID)
|
// Both dashboard and folder share same DB table.
|
||||||
|
dbDashboards, err := i.loader.LoadDashboards(ctx, orgID, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -364,7 +366,14 @@ func (i *dashboardIndex) applyDashboardEvent(ctx context.Context, orgID int64, d
|
|||||||
|
|
||||||
// In the future we can rely on operation types to reduce work here.
|
// In the future we can rely on operation types to reduce work here.
|
||||||
if len(dbDashboards) == 0 {
|
if len(dbDashboards) == 0 {
|
||||||
newReader, err = i.removeDashboard(ctx, writer, reader, dashboardUID)
|
switch kind {
|
||||||
|
case store.EntityTypeDashboard:
|
||||||
|
newReader, err = i.removeDashboard(ctx, writer, reader, uid)
|
||||||
|
case store.EntityTypeFolder:
|
||||||
|
newReader, err = i.removeFolder(ctx, writer, reader, uid)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
newReader, err = i.updateDashboard(ctx, orgID, writer, reader, dbDashboards[0])
|
newReader, err = i.updateDashboard(ctx, orgID, writer, reader, dbDashboards[0])
|
||||||
}
|
}
|
||||||
@ -377,8 +386,21 @@ func (i *dashboardIndex) applyDashboardEvent(ctx context.Context, orgID int64, d
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *dashboardIndex) removeDashboard(_ context.Context, writer *bluge.Writer, reader *bluge.Reader, dashboardUID string) (*bluge.Reader, error) {
|
func (i *dashboardIndex) removeDashboard(_ context.Context, writer *bluge.Writer, reader *bluge.Reader, dashboardUID string) (*bluge.Reader, error) {
|
||||||
|
dashboardLocation, ok, err := getDashboardLocation(reader, dashboardUID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
// No dashboard, nothing to remove.
|
||||||
|
return reader, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Find all panel docs to remove with dashboard.
|
// Find all panel docs to remove with dashboard.
|
||||||
panelIDs, err := getDashboardPanelIDs(reader, dashboardUID)
|
panelLocation := dashboardUID
|
||||||
|
if dashboardLocation != "" {
|
||||||
|
panelLocation = dashboardLocation + "/" + dashboardUID
|
||||||
|
}
|
||||||
|
panelIDs, err := getDocsIDsByLocationPrefix(reader, panelLocation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -397,6 +419,23 @@ func (i *dashboardIndex) removeDashboard(_ context.Context, writer *bluge.Writer
|
|||||||
return writer.Reader()
|
return writer.Reader()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *dashboardIndex) removeFolder(_ context.Context, writer *bluge.Writer, reader *bluge.Reader, folderUID string) (*bluge.Reader, error) {
|
||||||
|
ids, err := getDocsIDsByLocationPrefix(reader, folderUID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
batch := bluge.NewBatch()
|
||||||
|
batch.Delete(bluge.NewDocument(folderUID).ID())
|
||||||
|
for _, id := range ids {
|
||||||
|
batch.Delete(bluge.NewDocument(id).ID())
|
||||||
|
}
|
||||||
|
err = writer.Batch(batch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return writer.Reader()
|
||||||
|
}
|
||||||
|
|
||||||
func stringInSlice(str string, slice []string) bool {
|
func stringInSlice(str string, slice []string) bool {
|
||||||
for _, s := range slice {
|
for _, s := range slice {
|
||||||
if s == str {
|
if s == str {
|
||||||
@ -437,14 +476,17 @@ func (i *dashboardIndex) updateDashboard(ctx context.Context, orgID int64, write
|
|||||||
|
|
||||||
var actualPanelIDs []string
|
var actualPanelIDs []string
|
||||||
|
|
||||||
location += "/" + dash.uid
|
if location != "" {
|
||||||
|
location += "/"
|
||||||
|
}
|
||||||
|
location += dash.uid
|
||||||
panelDocs := getDashboardPanelDocs(dash, location)
|
panelDocs := getDashboardPanelDocs(dash, location)
|
||||||
for _, panelDoc := range panelDocs {
|
for _, panelDoc := range panelDocs {
|
||||||
actualPanelIDs = append(actualPanelIDs, string(panelDoc.ID().Term()))
|
actualPanelIDs = append(actualPanelIDs, string(panelDoc.ID().Term()))
|
||||||
batch.Update(panelDoc.ID(), panelDoc)
|
batch.Update(panelDoc.ID(), panelDoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
indexedPanelIDs, err := getDashboardPanelIDs(reader, dash.uid)
|
indexedPanelIDs, err := getDashboardPanelIDs(reader, location)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,17 @@ package searchV2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/searchV2/extract"
|
"github.com/grafana/grafana/pkg/services/searchV2/extract"
|
||||||
"github.com/grafana/grafana/pkg/services/store"
|
"github.com/grafana/grafana/pkg/services/store"
|
||||||
|
|
||||||
"github.com/blugelabs/bluge"
|
"github.com/blugelabs/bluge"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/experimental"
|
"github.com/grafana/grafana-plugin-sdk-go/experimental"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@ -370,3 +372,142 @@ func TestDashboardIndex_MultipleTokensScattered(t *testing.T) {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var dashboardsWithFolders = []dashboard{
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
uid: "1",
|
||||||
|
isFolder: true,
|
||||||
|
info: &extract.DashboardInfo{
|
||||||
|
Title: "My folder",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
uid: "2",
|
||||||
|
folderID: 1,
|
||||||
|
info: &extract.DashboardInfo{
|
||||||
|
Title: "Dashboard in folder 1",
|
||||||
|
Panels: []extract.PanelInfo{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Title: "Panel 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
Title: "Panel 2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
uid: "3",
|
||||||
|
folderID: 1,
|
||||||
|
info: &extract.DashboardInfo{
|
||||||
|
Title: "Dashboard in folder 2",
|
||||||
|
Panels: []extract.PanelInfo{
|
||||||
|
{
|
||||||
|
ID: 3,
|
||||||
|
Title: "Panel 3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
uid: "4",
|
||||||
|
info: &extract.DashboardInfo{
|
||||||
|
Title: "One more dash",
|
||||||
|
Panels: []extract.PanelInfo{
|
||||||
|
{
|
||||||
|
ID: 3,
|
||||||
|
Title: "Panel 4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDashboardIndex_Folders(t *testing.T) {
|
||||||
|
t.Run("folders-indexed", func(t *testing.T) {
|
||||||
|
_, reader, _ := initTestIndexFromDashes(t, dashboardsWithFolders)
|
||||||
|
checkSearchResponse(t, filepath.Base(t.Name()), reader, testAllowAllFilter,
|
||||||
|
DashboardQuery{Query: "My folder", Kind: []string{string(entityKindFolder)}},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.Run("folders-dashboard-has-folder", func(t *testing.T) {
|
||||||
|
_, reader, _ := initTestIndexFromDashes(t, dashboardsWithFolders)
|
||||||
|
// TODO: golden file compare does not work here.
|
||||||
|
resp := doSearchQuery(context.Background(), testLogger, reader, testAllowAllFilter,
|
||||||
|
DashboardQuery{Query: "Dashboard in folder", Kind: []string{string(entityKindDashboard)}},
|
||||||
|
&NoopQueryExtender{}, "")
|
||||||
|
custom, ok := resp.Frames[0].Meta.Custom.(*customMeta)
|
||||||
|
require.Equal(t, uint64(2), custom.Count)
|
||||||
|
require.True(t, ok, fmt.Sprintf("actual type: %T", resp.Frames[0].Meta.Custom))
|
||||||
|
require.Equal(t, "/dashboards/f/1/", custom.Locations["1"].URL)
|
||||||
|
})
|
||||||
|
t.Run("folders-dashboard-removed-on-folder-removed", func(t *testing.T) {
|
||||||
|
index, reader, writer := initTestIndexFromDashes(t, dashboardsWithFolders)
|
||||||
|
newReader, err := index.removeFolder(context.Background(), writer, reader, "1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
// In response we expect one dashboard which does not belong to removed folder.
|
||||||
|
checkSearchResponse(t, filepath.Base(t.Name()), newReader, testAllowAllFilter,
|
||||||
|
DashboardQuery{Query: "dash", Kind: []string{string(entityKindDashboard)}},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.Run("folders-panels-removed-on-folder-removed", func(t *testing.T) {
|
||||||
|
index, reader, writer := initTestIndexFromDashes(t, dashboardsWithFolders)
|
||||||
|
newReader, err := index.removeFolder(context.Background(), writer, reader, "1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp := doSearchQuery(context.Background(), testLogger, newReader, testAllowAllFilter,
|
||||||
|
DashboardQuery{Query: "Panel", Kind: []string{string(entityKindPanel)}},
|
||||||
|
&NoopQueryExtender{}, "")
|
||||||
|
custom, ok := resp.Frames[0].Meta.Custom.(*customMeta)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, uint64(1), custom.Count) // 1 panel which does not belong to dashboards in removed folder.
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var dashboardsWithPanels = []dashboard{
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
uid: "1",
|
||||||
|
info: &extract.DashboardInfo{
|
||||||
|
Title: "My Dash",
|
||||||
|
Panels: []extract.PanelInfo{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Title: "Panel 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
Title: "Panel 2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDashboardIndex_Panels(t *testing.T) {
|
||||||
|
t.Run("panels-indexed", func(t *testing.T) {
|
||||||
|
_, reader, _ := initTestIndexFromDashes(t, dashboardsWithPanels)
|
||||||
|
// TODO: golden file compare does not work here.
|
||||||
|
resp := doSearchQuery(
|
||||||
|
context.Background(), testLogger, reader, testAllowAllFilter,
|
||||||
|
DashboardQuery{Query: "Panel", Kind: []string{string(entityKindPanel)}},
|
||||||
|
&NoopQueryExtender{}, "")
|
||||||
|
custom, ok := resp.Frames[0].Meta.Custom.(*customMeta)
|
||||||
|
require.True(t, ok, fmt.Sprintf("actual type: %T", resp.Frames[0].Meta.Custom))
|
||||||
|
require.Equal(t, uint64(2), custom.Count)
|
||||||
|
require.Equal(t, "/d/1/", custom.Locations["1"].URL)
|
||||||
|
})
|
||||||
|
t.Run("panels-panel-removed-on-dashboard-removed", func(t *testing.T) {
|
||||||
|
index, reader, writer := initTestIndexFromDashes(t, dashboardsWithPanels)
|
||||||
|
newReader, err := index.removeDashboard(context.Background(), writer, reader, "1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkSearchResponse(t, filepath.Base(t.Name()), newReader, testAllowAllFilter,
|
||||||
|
DashboardQuery{Query: "Panel", Kind: []string{string(entityKindPanel)}},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
131
pkg/services/searchV2/testdata/folders-dashboard-removed-on-folder-removed.jsonc
vendored
Normal file
131
pkg/services/searchV2/testdata/folders-dashboard-removed-on-folder-removed.jsonc
vendored
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// 🌟 This was machine generated. Do not edit. 🌟
|
||||||
|
//
|
||||||
|
// Frame[0] {
|
||||||
|
// "type": "search-results",
|
||||||
|
// "custom": {
|
||||||
|
// "count": 1
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Name: Query results
|
||||||
|
// Dimensions: 8 Fields by 1 Rows
|
||||||
|
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
|
||||||
|
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
|
||||||
|
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
|
||||||
|
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
// | dashboard | 4 | One more dash | | /pfix/d/4/ | null | null | |
|
||||||
|
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// 🌟 This was machine generated. Do not edit. 🌟
|
||||||
|
{
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"schema": {
|
||||||
|
"name": "Query results",
|
||||||
|
"meta": {
|
||||||
|
"type": "search-results",
|
||||||
|
"custom": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "kind",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "uid",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "panel_type",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "url",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"title": "link",
|
||||||
|
"url": "${__value.text}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tags",
|
||||||
|
"type": "other",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "json.RawMessage",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ds_uid",
|
||||||
|
"type": "other",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "json.RawMessage",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "location",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"values": [
|
||||||
|
[
|
||||||
|
"dashboard"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"4"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"One more dash"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
""
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/pfix/d/4/"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
null
|
||||||
|
],
|
||||||
|
[
|
||||||
|
null
|
||||||
|
],
|
||||||
|
[
|
||||||
|
""
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
131
pkg/services/searchV2/testdata/folders-indexed.jsonc
vendored
Normal file
131
pkg/services/searchV2/testdata/folders-indexed.jsonc
vendored
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// 🌟 This was machine generated. Do not edit. 🌟
|
||||||
|
//
|
||||||
|
// Frame[0] {
|
||||||
|
// "type": "search-results",
|
||||||
|
// "custom": {
|
||||||
|
// "count": 1
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Name: Query results
|
||||||
|
// Dimensions: 8 Fields by 1 Rows
|
||||||
|
// +----------------+----------------+----------------+------------------+-----------------------+--------------------------+--------------------------+----------------+
|
||||||
|
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
|
||||||
|
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
|
||||||
|
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
|
||||||
|
// +----------------+----------------+----------------+------------------+-----------------------+--------------------------+--------------------------+----------------+
|
||||||
|
// | folder | 1 | My folder | | /pfix/dashboards/f/1/ | null | null | |
|
||||||
|
// +----------------+----------------+----------------+------------------+-----------------------+--------------------------+--------------------------+----------------+
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// 🌟 This was machine generated. Do not edit. 🌟
|
||||||
|
{
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"schema": {
|
||||||
|
"name": "Query results",
|
||||||
|
"meta": {
|
||||||
|
"type": "search-results",
|
||||||
|
"custom": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "kind",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "uid",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "panel_type",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "url",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"title": "link",
|
||||||
|
"url": "${__value.text}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tags",
|
||||||
|
"type": "other",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "json.RawMessage",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ds_uid",
|
||||||
|
"type": "other",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "json.RawMessage",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "location",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"values": [
|
||||||
|
[
|
||||||
|
"folder"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"1"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"My folder"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
""
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/pfix/dashboards/f/1/"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
null
|
||||||
|
],
|
||||||
|
[
|
||||||
|
null
|
||||||
|
],
|
||||||
|
[
|
||||||
|
""
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
114
pkg/services/searchV2/testdata/panels-panel-removed-on-dashboard-removed.jsonc
vendored
Normal file
114
pkg/services/searchV2/testdata/panels-panel-removed-on-dashboard-removed.jsonc
vendored
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// 🌟 This was machine generated. Do not edit. 🌟
|
||||||
|
//
|
||||||
|
// Frame[0] {
|
||||||
|
// "type": "search-results",
|
||||||
|
// "custom": {
|
||||||
|
// "count": 0
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Name: Query results
|
||||||
|
// Dimensions: 8 Fields by 0 Rows
|
||||||
|
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
|
||||||
|
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
|
||||||
|
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
|
||||||
|
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// 🌟 This was machine generated. Do not edit. 🌟
|
||||||
|
{
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"schema": {
|
||||||
|
"name": "Query results",
|
||||||
|
"meta": {
|
||||||
|
"type": "search-results",
|
||||||
|
"custom": {
|
||||||
|
"count": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "kind",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "uid",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "panel_type",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "url",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"title": "link",
|
||||||
|
"url": "${__value.text}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tags",
|
||||||
|
"type": "other",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "json.RawMessage",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ds_uid",
|
||||||
|
"type": "other",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "json.RawMessage",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "location",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"values": [
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -24,6 +24,7 @@ type EntityType string
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
EntityTypeDashboard EntityType = "dashboard"
|
EntityTypeDashboard EntityType = "dashboard"
|
||||||
|
EntityTypeFolder EntityType = "folder"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateDatabaseEntityId creates entityId for entities stored in the existing SQL tables
|
// CreateDatabaseEntityId creates entityId for entities stored in the existing SQL tables
|
||||||
@ -51,6 +52,8 @@ type SaveEventCmd struct {
|
|||||||
EventType EntityEventType
|
EventType EntityEventType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EventHandler func(ctx context.Context, e *EntityEvent) error
|
||||||
|
|
||||||
// EntityEventsService is a temporary solution to support change notifications in an HA setup
|
// EntityEventsService is a temporary solution to support change notifications in an HA setup
|
||||||
// With this service each system can query for any events that have happened since a fixed time
|
// With this service each system can query for any events that have happened since a fixed time
|
||||||
//go:generate mockery --name EntityEventsService --structname MockEntityEventsService --inpackage --filename entity_events_mock.go
|
//go:generate mockery --name EntityEventsService --structname MockEntityEventsService --inpackage --filename entity_events_mock.go
|
||||||
@ -60,6 +63,7 @@ type EntityEventsService interface {
|
|||||||
SaveEvent(ctx context.Context, cmd SaveEventCmd) error
|
SaveEvent(ctx context.Context, cmd SaveEventCmd) error
|
||||||
GetLastEvent(ctx context.Context) (*EntityEvent, error)
|
GetLastEvent(ctx context.Context) (*EntityEvent, error)
|
||||||
GetAllEventsAfter(ctx context.Context, id int64) ([]*EntityEvent, error)
|
GetAllEventsAfter(ctx context.Context, id int64) ([]*EntityEvent, error)
|
||||||
|
OnEvent(handler EventHandler)
|
||||||
|
|
||||||
deleteEventsOlderThan(ctx context.Context, duration time.Duration) error
|
deleteEventsOlderThan(ctx context.Context, duration time.Duration) error
|
||||||
}
|
}
|
||||||
@ -70,27 +74,48 @@ func ProvideEntityEventsService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, f
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &entityEventService{
|
return &entityEventService{
|
||||||
sql: sqlStore,
|
sql: sqlStore,
|
||||||
features: features,
|
features: features,
|
||||||
log: log.New("entity-events"),
|
log: log.New("entity-events"),
|
||||||
|
eventHandlers: make([]EventHandler, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type entityEventService struct {
|
type entityEventService struct {
|
||||||
sql *sqlstore.SQLStore
|
sql *sqlstore.SQLStore
|
||||||
log log.Logger
|
log log.Logger
|
||||||
features featuremgmt.FeatureToggles
|
features featuremgmt.FeatureToggles
|
||||||
|
eventHandlers []EventHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *entityEventService) SaveEvent(ctx context.Context, cmd SaveEventCmd) error {
|
func (e *entityEventService) SaveEvent(ctx context.Context, cmd SaveEventCmd) error {
|
||||||
return e.sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
entityEvent := &EntityEvent{
|
||||||
_, err := sess.Insert(&EntityEvent{
|
EventType: cmd.EventType,
|
||||||
EventType: cmd.EventType,
|
EntityId: cmd.EntityId,
|
||||||
EntityId: cmd.EntityId,
|
Created: time.Now().Unix(),
|
||||||
Created: time.Now().Unix(),
|
}
|
||||||
})
|
err := e.sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||||
|
_, err := sess.Insert(entityEvent)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.broadcastEvent(ctx, entityEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *entityEventService) broadcastEvent(ctx context.Context, event *EntityEvent) error {
|
||||||
|
for _, h := range e.eventHandlers {
|
||||||
|
err := h(ctx, event)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *entityEventService) OnEvent(handler EventHandler) {
|
||||||
|
e.eventHandlers = append(e.eventHandlers, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *entityEventService) GetLastEvent(ctx context.Context) (*EntityEvent, error) {
|
func (e *entityEventService) GetLastEvent(ctx context.Context) (*EntityEvent, error) {
|
||||||
@ -164,6 +189,9 @@ func (d dummyEntityEventsService) SaveEvent(ctx context.Context, cmd SaveEventCm
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d dummyEntityEventsService) OnEvent(handler EventHandler) {
|
||||||
|
}
|
||||||
|
|
||||||
func (d dummyEntityEventsService) GetLastEvent(ctx context.Context) (*EntityEvent, error) {
|
func (d dummyEntityEventsService) GetLastEvent(ctx context.Context) (*EntityEvent, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Code generated by mockery v2.10.6. DO NOT EDIT.
|
// Code generated by mockery v2.12.3. DO NOT EDIT.
|
||||||
|
|
||||||
package store
|
package store
|
||||||
|
|
||||||
@ -74,6 +74,11 @@ func (_m *MockEntityEventsService) IsDisabled() bool {
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OnEvent provides a mock function with given fields: handler
|
||||||
|
func (_m *MockEntityEventsService) OnEvent(handler EventHandler) {
|
||||||
|
_m.Called(handler)
|
||||||
|
}
|
||||||
|
|
||||||
// Run provides a mock function with given fields: ctx
|
// Run provides a mock function with given fields: ctx
|
||||||
func (_m *MockEntityEventsService) Run(ctx context.Context) error {
|
func (_m *MockEntityEventsService) Run(ctx context.Context) error {
|
||||||
ret := _m.Called(ctx)
|
ret := _m.Called(ctx)
|
||||||
@ -115,3 +120,18 @@ func (_m *MockEntityEventsService) deleteEventsOlderThan(ctx context.Context, du
|
|||||||
|
|
||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NewMockEntityEventsServiceT interface {
|
||||||
|
mock.TestingT
|
||||||
|
Cleanup(func())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockEntityEventsService creates a new instance of MockEntityEventsService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
func NewMockEntityEventsService(t NewMockEntityEventsServiceT) *MockEntityEventsService {
|
||||||
|
mock := &MockEntityEventsService{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user