mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Publicdasboards: Add annotations support (#56413)
adds annotations support for public dashboards
This commit is contained in:
parent
cc6245df8e
commit
b2408dd7c5
@ -40,7 +40,12 @@ export interface AnnotationQuery {
|
|||||||
*/
|
*/
|
||||||
rawQuery?: string;
|
rawQuery?: string;
|
||||||
showIn: number;
|
showIn: number;
|
||||||
target?: Record<string, unknown>;
|
target?: {
|
||||||
|
limit: number;
|
||||||
|
matchAny: boolean;
|
||||||
|
tags: Array<string>;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,6 +84,13 @@ seqs: [
|
|||||||
///////////////////////////////////////
|
///////////////////////////////////////
|
||||||
// Definitions (referenced above) are declared below
|
// Definitions (referenced above) are declared below
|
||||||
|
|
||||||
|
#AnnotationTarget: {
|
||||||
|
limit: int64
|
||||||
|
matchAny: bool
|
||||||
|
tags: [...string]
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
// TODO docs
|
// TODO docs
|
||||||
// FROM: AnnotationQuery in grafana-data/src/types/annotations.ts
|
// FROM: AnnotationQuery in grafana-data/src/types/annotations.ts
|
||||||
#AnnotationQuery: {
|
#AnnotationQuery: {
|
||||||
@ -106,7 +113,7 @@ seqs: [
|
|||||||
// Query for annotation data.
|
// Query for annotation data.
|
||||||
rawQuery?: string @grafanamaturity(NeedsExpertReview)
|
rawQuery?: string @grafanamaturity(NeedsExpertReview)
|
||||||
showIn: uint8 | *0 @grafanamaturity(NeedsExpertReview)
|
showIn: uint8 | *0 @grafanamaturity(NeedsExpertReview)
|
||||||
target?: #Target @grafanamaturity(NeedsExpertReview) // TODO currently a generic in AnnotationQuery
|
target?: #AnnotationTarget @grafanamaturity(NeedsExpertReview)
|
||||||
} @cuetsy(kind="interface")
|
} @cuetsy(kind="interface")
|
||||||
|
|
||||||
// FROM: packages/grafana-data/src/types/templateVars.ts
|
// FROM: packages/grafana-data/src/types/templateVars.ts
|
||||||
|
@ -343,18 +343,21 @@ type AnnotationQuery struct {
|
|||||||
Name *string `json:"name,omitempty"`
|
Name *string `json:"name,omitempty"`
|
||||||
|
|
||||||
// Query for annotation data.
|
// Query for annotation data.
|
||||||
RawQuery *string `json:"rawQuery,omitempty"`
|
RawQuery *string `json:"rawQuery,omitempty"`
|
||||||
ShowIn int `json:"showIn"`
|
ShowIn int `json:"showIn"`
|
||||||
|
Target *AnnotationTarget `json:"target,omitempty"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
// Schema for panel targets is specified by datasource
|
// AnnotationTarget is the Go representation of a dashboard.AnnotationTarget.
|
||||||
// plugins. We use a placeholder definition, which the Go
|
//
|
||||||
// schema loader either left open/as-is with the Base
|
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
|
||||||
// variant of the Model and Panel families, or filled
|
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
|
||||||
// with types derived from plugins in the Instance variant.
|
type AnnotationTarget struct {
|
||||||
// When working directly from CUE, importers can extend this
|
Limit int64 `json:"limit"`
|
||||||
// type directly to achieve the same effect.
|
MatchAny bool `json:"matchAny"`
|
||||||
Target *Target `json:"target,omitempty"`
|
Tags []string `json:"tags"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 0 for no shared crosshair or tooltip (default).
|
// 0 for no shared crosshair or tooltip (default).
|
||||||
|
@ -13,6 +13,7 @@ var (
|
|||||||
ErrBaseTagLimitExceeded = errutil.NewBase(errutil.StatusBadRequest, "annotations.tag-limit-exceeded", errutil.WithPublicMessage("Tags length exceeds the maximum allowed."))
|
ErrBaseTagLimitExceeded = errutil.NewBase(errutil.StatusBadRequest, "annotations.tag-limit-exceeded", errutil.WithPublicMessage("Tags length exceeds the maximum allowed."))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:generate mockery --name Repository --structname FakeAnnotationsRepo --inpackage --filename annotations_repository_mock.go
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
Save(ctx context.Context, item *Item) error
|
Save(ctx context.Context, item *Item) error
|
||||||
Update(ctx context.Context, item *Item) error
|
Update(ctx context.Context, item *Item) error
|
||||||
|
111
pkg/services/annotations/annotations_repository_mock.go
Normal file
111
pkg/services/annotations/annotations_repository_mock.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
// Code generated by mockery v2.12.1. DO NOT EDIT.
|
||||||
|
|
||||||
|
package annotations
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
testing "testing"
|
||||||
|
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FakeAnnotationsRepo is an autogenerated mock type for the Repository type
|
||||||
|
type FakeAnnotationsRepo struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete provides a mock function with given fields: ctx, params
|
||||||
|
func (_m *FakeAnnotationsRepo) Delete(ctx context.Context, params *DeleteParams) error {
|
||||||
|
ret := _m.Called(ctx, params)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *DeleteParams) error); ok {
|
||||||
|
r0 = rf(ctx, params)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find provides a mock function with given fields: ctx, query
|
||||||
|
func (_m *FakeAnnotationsRepo) Find(ctx context.Context, query *ItemQuery) ([]*ItemDTO, error) {
|
||||||
|
ret := _m.Called(ctx, query)
|
||||||
|
|
||||||
|
var r0 []*ItemDTO
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *ItemQuery) []*ItemDTO); ok {
|
||||||
|
r0 = rf(ctx, query)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*ItemDTO)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, *ItemQuery) error); ok {
|
||||||
|
r1 = rf(ctx, query)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindTags provides a mock function with given fields: ctx, query
|
||||||
|
func (_m *FakeAnnotationsRepo) FindTags(ctx context.Context, query *TagsQuery) (FindTagsResult, error) {
|
||||||
|
ret := _m.Called(ctx, query)
|
||||||
|
|
||||||
|
var r0 FindTagsResult
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *TagsQuery) FindTagsResult); ok {
|
||||||
|
r0 = rf(ctx, query)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(FindTagsResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, *TagsQuery) error); ok {
|
||||||
|
r1 = rf(ctx, query)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save provides a mock function with given fields: ctx, item
|
||||||
|
func (_m *FakeAnnotationsRepo) Save(ctx context.Context, item *Item) error {
|
||||||
|
ret := _m.Called(ctx, item)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *Item) error); ok {
|
||||||
|
r0 = rf(ctx, item)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update provides a mock function with given fields: ctx, item
|
||||||
|
func (_m *FakeAnnotationsRepo) Update(ctx context.Context, item *Item) error {
|
||||||
|
ret := _m.Called(ctx, item)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *Item) error); ok {
|
||||||
|
r0 = rf(ctx, item)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFakeAnnotationsRepo creates a new instance of FakeAnnotationsRepo. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
func NewFakeAnnotationsRepo(t testing.TB) *FakeAnnotationsRepo {
|
||||||
|
mock := &FakeAnnotationsRepo{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
@ -63,6 +63,7 @@ func (api *Api) RegisterAPIEndpoints() {
|
|||||||
// public endpoints
|
// public endpoints
|
||||||
api.RouteRegister.Get("/api/public/dashboards/:accessToken", routing.Wrap(api.GetPublicDashboard))
|
api.RouteRegister.Get("/api/public/dashboards/:accessToken", routing.Wrap(api.GetPublicDashboard))
|
||||||
api.RouteRegister.Post("/api/public/dashboards/:accessToken/panels/:panelId/query", routing.Wrap(api.QueryPublicDashboard))
|
api.RouteRegister.Post("/api/public/dashboards/:accessToken/panels/:panelId/query", routing.Wrap(api.QueryPublicDashboard))
|
||||||
|
api.RouteRegister.Get("/api/public/dashboards/:accessToken/annotations", routing.Wrap(api.GetAnnotations))
|
||||||
|
|
||||||
// List Public Dashboards
|
// List Public Dashboards
|
||||||
api.RouteRegister.Get("/api/dashboards/public", middleware.ReqSignedIn, routing.Wrap(api.ListPublicDashboards))
|
api.RouteRegister.Get("/api/dashboards/public", middleware.ReqSignedIn, routing.Wrap(api.ListPublicDashboards))
|
||||||
@ -187,6 +188,21 @@ func (api *Api) QueryPublicDashboard(c *models.ReqContext) response.Response {
|
|||||||
return toJsonStreamingResponse(api.Features, resp)
|
return toJsonStreamingResponse(api.Features, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *Api) GetAnnotations(c *models.ReqContext) response.Response {
|
||||||
|
reqDTO := AnnotationsQueryDTO{
|
||||||
|
From: c.QueryInt64("from"),
|
||||||
|
To: c.QueryInt64("to"),
|
||||||
|
}
|
||||||
|
|
||||||
|
annotations, err := api.PublicDashboardService.GetAnnotations(c.Req.Context(), reqDTO, web.Params(c.Req)[":accessToken"])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return api.handleError(c.Req.Context(), http.StatusInternalServerError, "error getting public dashboard annotations", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(http.StatusOK, annotations)
|
||||||
|
}
|
||||||
|
|
||||||
// util to help us unpack dashboard and publicdashboard errors or use default http code and message
|
// util to help us unpack dashboard and publicdashboard errors or use default http code and message
|
||||||
// we should look to do some future refactoring of these errors as publicdashboard err is the same as a dashboarderr, just defined in a
|
// we should look to do some future refactoring of these errors as publicdashboard err is the same as a dashboarderr, just defined in a
|
||||||
// different package.
|
// different package.
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database"
|
dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
@ -49,6 +50,56 @@ type JsonErrResponse struct {
|
|||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAPIGetAnnotations(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
Name string
|
||||||
|
ExpectedHttpResponse int
|
||||||
|
Annotations []AnnotationEvent
|
||||||
|
ServiceError error
|
||||||
|
From string
|
||||||
|
To string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "will return success when there is no error and to and from are provided",
|
||||||
|
ExpectedHttpResponse: http.StatusOK,
|
||||||
|
Annotations: []AnnotationEvent{{Id: 1}},
|
||||||
|
ServiceError: nil,
|
||||||
|
From: "123",
|
||||||
|
To: "123",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "will return 500 when service returns an error",
|
||||||
|
ExpectedHttpResponse: http.StatusInternalServerError,
|
||||||
|
Annotations: nil,
|
||||||
|
ServiceError: errors.New("an error happened"),
|
||||||
|
From: "123",
|
||||||
|
To: "123",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.Name, func(t *testing.T) {
|
||||||
|
cfg := setting.NewCfg()
|
||||||
|
cfg.RBACEnabled = false
|
||||||
|
service := publicdashboards.NewFakePublicDashboardService(t)
|
||||||
|
service.On("GetAnnotations", mock.Anything, mock.Anything, mock.AnythingOfType("string")).
|
||||||
|
Return(test.Annotations, test.ServiceError).Once()
|
||||||
|
testServer := setupTestServer(t, cfg, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), service, nil, anonymousUser)
|
||||||
|
|
||||||
|
path := fmt.Sprintf("/api/public/dashboards/abc123/annotations?from=%s&to=%s", test.From, test.To)
|
||||||
|
response := callAPI(testServer, http.MethodGet, path, nil, t)
|
||||||
|
|
||||||
|
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
|
||||||
|
|
||||||
|
if test.ExpectedHttpResponse == http.StatusOK {
|
||||||
|
var items []AnnotationEvent
|
||||||
|
err := json.Unmarshal(response.Body.Bytes(), &items)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, items, test.Annotations)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAPIFeatureFlag(t *testing.T) {
|
func TestAPIFeatureFlag(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
Name string
|
Name string
|
||||||
@ -630,11 +681,13 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T)
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
annotationsService := annotationstest.NewFakeAnnotationsRepo()
|
||||||
|
|
||||||
// create public dashboard
|
// create public dashboard
|
||||||
store := publicdashboardsStore.ProvideStore(db)
|
store := publicdashboardsStore.ProvideStore(db)
|
||||||
cfg := setting.NewCfg()
|
cfg := setting.NewCfg()
|
||||||
cfg.RBACEnabled = false
|
cfg.RBACEnabled = false
|
||||||
service := publicdashboardsService.ProvideService(cfg, store, qds)
|
service := publicdashboardsService.ProvideService(cfg, store, qds, annotationsService)
|
||||||
pubdash, err := service.SavePublicDashboardConfig(context.Background(), &user.SignedInUser{}, savePubDashboardCmd)
|
pubdash, err := service.SavePublicDashboardConfig(context.Background(), &user.SignedInUser{}, savePubDashboardCmd)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/coremodel/dashboard"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/legacydata"
|
"github.com/grafana/grafana/pkg/tsdb/legacydata"
|
||||||
)
|
)
|
||||||
@ -77,6 +78,28 @@ type PublicDashboard struct {
|
|||||||
UpdatedAt time.Time `json:"updatedAt" xorm:"updated_at"`
|
UpdatedAt time.Time `json:"updatedAt" xorm:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Alias the generated type
|
||||||
|
type DashAnnotation = dashboard.AnnotationQuery
|
||||||
|
|
||||||
|
type AnnotationsDto struct {
|
||||||
|
Annotations struct {
|
||||||
|
List []DashAnnotation `json:"list"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnnotationEvent struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
DashboardId int64 `json:"dashboardId"`
|
||||||
|
PanelId int64 `json:"panelId"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
IsRegion bool `json:"isRegion"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
Time int64 `json:"time"`
|
||||||
|
TimeEnd int64 `json:"timeEnd"`
|
||||||
|
Source dashboard.AnnotationQuery `json:"source"`
|
||||||
|
}
|
||||||
|
|
||||||
func (pd PublicDashboard) TableName() string {
|
func (pd PublicDashboard) TableName() string {
|
||||||
return "dashboard_public"
|
return "dashboard_public"
|
||||||
}
|
}
|
||||||
@ -135,6 +158,11 @@ type PublicDashboardQueryDTO struct {
|
|||||||
MaxDataPoints int64
|
MaxDataPoints int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AnnotationsQueryDTO struct {
|
||||||
|
From int64
|
||||||
|
To int64
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// COMMANDS
|
// COMMANDS
|
||||||
//
|
//
|
||||||
|
@ -62,6 +62,29 @@ func (_m *FakePublicDashboardService) BuildAnonymousUser(ctx context.Context, da
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAnnotations provides a mock function with given fields: ctx, reqDTO, accessToken
|
||||||
|
func (_m *FakePublicDashboardService) GetAnnotations(ctx context.Context, reqDTO publicdashboardsmodels.AnnotationsQueryDTO, accessToken string) ([]publicdashboardsmodels.AnnotationEvent, error) {
|
||||||
|
ret := _m.Called(ctx, reqDTO, accessToken)
|
||||||
|
|
||||||
|
var r0 []publicdashboardsmodels.AnnotationEvent
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, publicdashboardsmodels.AnnotationsQueryDTO, string) []publicdashboardsmodels.AnnotationEvent); ok {
|
||||||
|
r0 = rf(ctx, reqDTO, accessToken)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]publicdashboardsmodels.AnnotationEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, publicdashboardsmodels.AnnotationsQueryDTO, string) error); ok {
|
||||||
|
r1 = rf(ctx, reqDTO, accessToken)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// GetDashboard provides a mock function with given fields: ctx, dashboardUid
|
// GetDashboard provides a mock function with given fields: ctx, dashboardUid
|
||||||
func (_m *FakePublicDashboardService) GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) {
|
func (_m *FakePublicDashboardService) GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) {
|
||||||
ret := _m.Called(ctx, dashboardUid)
|
ret := _m.Called(ctx, dashboardUid)
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
type Service interface {
|
type Service interface {
|
||||||
AccessTokenExists(ctx context.Context, accessToken string) (bool, error)
|
AccessTokenExists(ctx context.Context, accessToken string) (bool, error)
|
||||||
BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) *user.SignedInUser
|
BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) *user.SignedInUser
|
||||||
|
GetAnnotations(ctx context.Context, reqDTO AnnotationsQueryDTO, accessToken string) ([]AnnotationEvent, error)
|
||||||
GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error)
|
GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error)
|
||||||
GetMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64, reqDTO PublicDashboardQueryDTO) (dtos.MetricRequest, error)
|
GetMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64, reqDTO PublicDashboardQueryDTO) (dtos.MetricRequest, error)
|
||||||
GetPublicDashboard(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error)
|
GetPublicDashboard(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error)
|
||||||
|
22
pkg/services/publicdashboards/service/annotations.go
Normal file
22
pkg/services/publicdashboards/service/annotations.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
"github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UnmarshalDashboardAnnotations(sj *simplejson.Json) (*models.AnnotationsDto, error) {
|
||||||
|
bytes, err := sj.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dto := &models.AnnotationsDto{}
|
||||||
|
err = json.Unmarshal(bytes, dto)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto, err
|
||||||
|
}
|
@ -2,13 +2,15 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"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"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||||
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||||
@ -17,6 +19,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/query"
|
"github.com/grafana/grafana/pkg/services/query"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb/grafanads"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/intervalv2"
|
"github.com/grafana/grafana/pkg/tsdb/intervalv2"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/legacydata"
|
"github.com/grafana/grafana/pkg/tsdb/legacydata"
|
||||||
)
|
)
|
||||||
@ -29,6 +32,7 @@ type PublicDashboardServiceImpl struct {
|
|||||||
store publicdashboards.Store
|
store publicdashboards.Store
|
||||||
intervalCalculator intervalv2.Calculator
|
intervalCalculator intervalv2.Calculator
|
||||||
QueryDataService *query.Service
|
QueryDataService *query.Service
|
||||||
|
AnnotationsRepo annotations.Repository
|
||||||
}
|
}
|
||||||
|
|
||||||
var LogPrefix = "publicdashboards.service"
|
var LogPrefix = "publicdashboards.service"
|
||||||
@ -43,6 +47,7 @@ func ProvideService(
|
|||||||
cfg *setting.Cfg,
|
cfg *setting.Cfg,
|
||||||
store publicdashboards.Store,
|
store publicdashboards.Store,
|
||||||
qds *query.Service,
|
qds *query.Service,
|
||||||
|
anno annotations.Repository,
|
||||||
) *PublicDashboardServiceImpl {
|
) *PublicDashboardServiceImpl {
|
||||||
return &PublicDashboardServiceImpl{
|
return &PublicDashboardServiceImpl{
|
||||||
log: log.New(LogPrefix),
|
log: log.New(LogPrefix),
|
||||||
@ -50,6 +55,7 @@ func ProvideService(
|
|||||||
store: store,
|
store: store,
|
||||||
intervalCalculator: intervalv2.NewCalculator(),
|
intervalCalculator: intervalv2.NewCalculator(),
|
||||||
QueryDataService: qds,
|
QueryDataService: qds,
|
||||||
|
AnnotationsRepo: anno,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,6 +258,81 @@ func (pd *PublicDashboardServiceImpl) GetMetricRequest(ctx context.Context, dash
|
|||||||
return metricReqDTO, nil
|
return metricReqDTO, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pd *PublicDashboardServiceImpl) GetAnnotations(ctx context.Context, reqDTO AnnotationsQueryDTO, accessToken string) ([]AnnotationEvent, error) {
|
||||||
|
_, dash, err := pd.GetPublicDashboard(ctx, accessToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
annoDto, err := UnmarshalDashboardAnnotations(dash.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
anonymousUser := pd.BuildAnonymousUser(ctx, dash)
|
||||||
|
|
||||||
|
uniqueEvents := make(map[int64]AnnotationEvent, 0)
|
||||||
|
for _, anno := range annoDto.Annotations.List {
|
||||||
|
// skip annotations that are not enabled or are not a grafana datasource
|
||||||
|
if !anno.Enable || (*anno.Datasource.Uid != grafanads.DatasourceUID && *anno.Datasource.Uid != grafanads.DatasourceName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
annoQuery := &annotations.ItemQuery{
|
||||||
|
From: reqDTO.From,
|
||||||
|
To: reqDTO.To,
|
||||||
|
OrgId: dash.OrgId,
|
||||||
|
DashboardId: dash.Id,
|
||||||
|
DashboardUid: dash.Uid,
|
||||||
|
Limit: anno.Target.Limit,
|
||||||
|
MatchAny: anno.Target.MatchAny,
|
||||||
|
SignedInUser: anonymousUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
if anno.Target.Type == "tags" {
|
||||||
|
annoQuery.DashboardId = 0
|
||||||
|
annoQuery.Tags = anno.Target.Tags
|
||||||
|
}
|
||||||
|
|
||||||
|
annotationItems, err := pd.AnnotationsRepo.Find(ctx, annoQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range annotationItems {
|
||||||
|
event := AnnotationEvent{
|
||||||
|
Id: item.Id,
|
||||||
|
DashboardId: item.DashboardId,
|
||||||
|
Tags: item.Tags,
|
||||||
|
IsRegion: item.TimeEnd > 0 && item.Time != item.TimeEnd,
|
||||||
|
Text: item.Text,
|
||||||
|
Color: *anno.IconColor,
|
||||||
|
Time: item.Time,
|
||||||
|
TimeEnd: item.TimeEnd,
|
||||||
|
Source: anno,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
if anno.Type == "dashboard" {
|
||||||
|
event.PanelId = item.PanelId
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want events from tag queries to overwrite existing events
|
||||||
|
_, has := uniqueEvents[event.Id]
|
||||||
|
if !has || (has && anno.Target.Type == "tags") {
|
||||||
|
uniqueEvents[event.Id] = event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []AnnotationEvent
|
||||||
|
for _, result := range uniqueEvents {
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
// buildMetricRequest merges public dashboard parameters with
|
// buildMetricRequest merges public dashboard parameters with
|
||||||
// dashboard and returns a metrics request to be sent to query backend
|
// dashboard and returns a metrics request to be sent to query backend
|
||||||
func (pd *PublicDashboardServiceImpl) buildMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64, reqDTO PublicDashboardQueryDTO) (dtos.MetricRequest, error) {
|
func (pd *PublicDashboardServiceImpl) buildMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64, reqDTO PublicDashboardQueryDTO) (dtos.MetricRequest, error) {
|
||||||
@ -282,17 +363,30 @@ func (pd *PublicDashboardServiceImpl) buildMetricRequest(ctx context.Context, da
|
|||||||
func (pd *PublicDashboardServiceImpl) BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) *user.SignedInUser {
|
func (pd *PublicDashboardServiceImpl) BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) *user.SignedInUser {
|
||||||
datasourceUids := queries.GetUniqueDashboardDatasourceUids(dashboard.Data)
|
datasourceUids := queries.GetUniqueDashboardDatasourceUids(dashboard.Data)
|
||||||
|
|
||||||
// Create a temp user with read-only datasource permissions
|
// Create a user with blank permissions
|
||||||
anonymousUser := &user.SignedInUser{OrgID: dashboard.OrgId, Permissions: make(map[int64]map[string][]string)}
|
anonymousUser := &user.SignedInUser{OrgID: dashboard.OrgId, Permissions: make(map[int64]map[string][]string)}
|
||||||
permissions := make(map[string][]string)
|
|
||||||
|
// Scopes needed for Annotation queries
|
||||||
|
annotationScopes := []string{accesscontrol.ScopeAnnotationsTypeDashboard}
|
||||||
|
// Need to access all dashboards since tags annotations span across all dashboards
|
||||||
|
dashboardScopes := []string{dashboards.ScopeDashboardsProvider.GetResourceAllScope()}
|
||||||
|
|
||||||
|
// Scopes needed for datasource queries
|
||||||
queryScopes := make([]string, 0)
|
queryScopes := make([]string, 0)
|
||||||
readScopes := make([]string, 0)
|
readScopes := make([]string, 0)
|
||||||
for _, uid := range datasourceUids {
|
for _, uid := range datasourceUids {
|
||||||
queryScopes = append(queryScopes, fmt.Sprintf("datasources:uid:%s", uid))
|
scope := datasources.ScopeProvider.GetResourceScopeUID(uid)
|
||||||
readScopes = append(readScopes, fmt.Sprintf("datasources:uid:%s", uid))
|
queryScopes = append(queryScopes, scope)
|
||||||
|
readScopes = append(readScopes, scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply all scopes to the actions we need the user to be able to perform
|
||||||
|
permissions := make(map[string][]string)
|
||||||
permissions[datasources.ActionQuery] = queryScopes
|
permissions[datasources.ActionQuery] = queryScopes
|
||||||
permissions[datasources.ActionRead] = readScopes
|
permissions[datasources.ActionRead] = readScopes
|
||||||
|
permissions[accesscontrol.ActionAnnotationsRead] = annotationScopes
|
||||||
|
permissions[dashboards.ActionDashboardsRead] = dashboardScopes
|
||||||
|
|
||||||
anonymousUser.Permissions[dashboard.OrgId] = permissions
|
anonymousUser.Permissions[dashboard.OrgId] = permissions
|
||||||
|
|
||||||
return anonymousUser
|
return anonymousUser
|
||||||
|
@ -2,13 +2,19 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
dashboard2 "github.com/grafana/grafana/pkg/coremodel/dashboard"
|
||||||
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
|
"github.com/grafana/grafana/pkg/services/annotations/annotationsimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/publicdashboards/internal"
|
"github.com/grafana/grafana/pkg/services/publicdashboards/internal"
|
||||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -35,6 +41,289 @@ func TestLogPrefix(t *testing.T) {
|
|||||||
assert.Equal(t, LogPrefix, "publicdashboards.service")
|
assert.Equal(t, LogPrefix, "publicdashboards.service")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetAnnotations(t *testing.T) {
|
||||||
|
t.Run("will build anonymous user with correct permissions to get annotations", func(t *testing.T) {
|
||||||
|
sqlStore := sqlstore.InitTestDB(t)
|
||||||
|
config := setting.NewCfg()
|
||||||
|
tagService := tagimpl.ProvideService(sqlStore, sqlStore.Cfg)
|
||||||
|
annotationsRepo := annotationsimpl.ProvideService(sqlStore, config, tagService)
|
||||||
|
fakeStore := FakePublicDashboardStore{}
|
||||||
|
service := &PublicDashboardServiceImpl{
|
||||||
|
log: log.New("test.logger"),
|
||||||
|
store: &fakeStore,
|
||||||
|
AnnotationsRepo: annotationsRepo,
|
||||||
|
}
|
||||||
|
fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
|
||||||
|
Return(&PublicDashboard{Uid: "uid1", IsEnabled: true}, models.NewDashboard("dash1"), nil)
|
||||||
|
reqDTO := AnnotationsQueryDTO{
|
||||||
|
From: 1,
|
||||||
|
To: 2,
|
||||||
|
}
|
||||||
|
dash := models.NewDashboard("testDashboard")
|
||||||
|
|
||||||
|
items, _ := service.GetAnnotations(context.Background(), reqDTO, "abc123")
|
||||||
|
anonUser := service.BuildAnonymousUser(context.Background(), dash)
|
||||||
|
|
||||||
|
assert.Equal(t, "dashboards:*", anonUser.Permissions[0]["dashboards:read"][0])
|
||||||
|
assert.Len(t, items, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Test events from tag queries overwrite built-in annotation queries and duplicate events are not returned", func(t *testing.T) {
|
||||||
|
dash := models.NewDashboard("test")
|
||||||
|
color := "red"
|
||||||
|
name := "annoName"
|
||||||
|
grafanaAnnotation := DashAnnotation{
|
||||||
|
Datasource: CreateDatasource("grafana", "grafana"),
|
||||||
|
Enable: true,
|
||||||
|
Name: &name,
|
||||||
|
IconColor: &color,
|
||||||
|
Target: &dashboard2.AnnotationTarget{
|
||||||
|
Limit: 100,
|
||||||
|
MatchAny: false,
|
||||||
|
Tags: nil,
|
||||||
|
Type: "dashboard",
|
||||||
|
},
|
||||||
|
Type: "dashboard",
|
||||||
|
}
|
||||||
|
grafanaTagAnnotation := DashAnnotation{
|
||||||
|
Datasource: CreateDatasource("grafana", "grafana"),
|
||||||
|
Enable: true,
|
||||||
|
Name: &name,
|
||||||
|
IconColor: &color,
|
||||||
|
Target: &dashboard2.AnnotationTarget{
|
||||||
|
Limit: 100,
|
||||||
|
MatchAny: false,
|
||||||
|
Tags: []string{"tag1"},
|
||||||
|
Type: "tags",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
annos := []DashAnnotation{grafanaAnnotation, grafanaTagAnnotation}
|
||||||
|
dashboard := AddAnnotationsToDashboard(t, dash, annos)
|
||||||
|
|
||||||
|
annotationsRepo := annotations.FakeAnnotationsRepo{}
|
||||||
|
fakeStore := FakePublicDashboardStore{}
|
||||||
|
service := &PublicDashboardServiceImpl{
|
||||||
|
log: log.New("test.logger"),
|
||||||
|
store: &fakeStore,
|
||||||
|
AnnotationsRepo: &annotationsRepo,
|
||||||
|
}
|
||||||
|
pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid}
|
||||||
|
fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dashboard, nil)
|
||||||
|
annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{
|
||||||
|
{
|
||||||
|
Id: 1,
|
||||||
|
DashboardId: 1,
|
||||||
|
PanelId: 1,
|
||||||
|
Tags: []string{"tag1"},
|
||||||
|
TimeEnd: 2,
|
||||||
|
Time: 2,
|
||||||
|
Text: "text",
|
||||||
|
},
|
||||||
|
}, nil).Maybe()
|
||||||
|
|
||||||
|
items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123")
|
||||||
|
|
||||||
|
expected := AnnotationEvent{
|
||||||
|
Id: 1,
|
||||||
|
DashboardId: 1,
|
||||||
|
PanelId: 0,
|
||||||
|
Tags: []string{"tag1"},
|
||||||
|
IsRegion: false,
|
||||||
|
Text: "text",
|
||||||
|
Color: color,
|
||||||
|
Time: 2,
|
||||||
|
TimeEnd: 2,
|
||||||
|
Source: grafanaTagAnnotation,
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, items, 1)
|
||||||
|
assert.Equal(t, expected, items[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Test panelId set to zero when annotation event is for a tags query", func(t *testing.T) {
|
||||||
|
dash := models.NewDashboard("test")
|
||||||
|
color := "red"
|
||||||
|
name := "annoName"
|
||||||
|
grafanaAnnotation := DashAnnotation{
|
||||||
|
Datasource: CreateDatasource("grafana", "grafana"),
|
||||||
|
Enable: true,
|
||||||
|
Name: &name,
|
||||||
|
IconColor: &color,
|
||||||
|
Target: &dashboard2.AnnotationTarget{
|
||||||
|
Limit: 100,
|
||||||
|
MatchAny: false,
|
||||||
|
Tags: []string{"tag1"},
|
||||||
|
Type: "tags",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
annos := []DashAnnotation{grafanaAnnotation}
|
||||||
|
dashboard := AddAnnotationsToDashboard(t, dash, annos)
|
||||||
|
|
||||||
|
annotationsRepo := annotations.FakeAnnotationsRepo{}
|
||||||
|
fakeStore := FakePublicDashboardStore{}
|
||||||
|
service := &PublicDashboardServiceImpl{
|
||||||
|
log: log.New("test.logger"),
|
||||||
|
store: &fakeStore,
|
||||||
|
AnnotationsRepo: &annotationsRepo,
|
||||||
|
}
|
||||||
|
pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid}
|
||||||
|
fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dashboard, nil)
|
||||||
|
annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{
|
||||||
|
{
|
||||||
|
Id: 1,
|
||||||
|
DashboardId: 1,
|
||||||
|
PanelId: 1,
|
||||||
|
Tags: []string{},
|
||||||
|
TimeEnd: 1,
|
||||||
|
Time: 2,
|
||||||
|
Text: "text",
|
||||||
|
},
|
||||||
|
}, nil).Maybe()
|
||||||
|
|
||||||
|
items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123")
|
||||||
|
|
||||||
|
expected := AnnotationEvent{
|
||||||
|
Id: 1,
|
||||||
|
DashboardId: 1,
|
||||||
|
PanelId: 0,
|
||||||
|
Tags: []string{},
|
||||||
|
IsRegion: true,
|
||||||
|
Text: "text",
|
||||||
|
Color: color,
|
||||||
|
Time: 2,
|
||||||
|
TimeEnd: 1,
|
||||||
|
Source: grafanaAnnotation,
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, items, 1)
|
||||||
|
assert.Equal(t, expected, items[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Test can get grafana annotations and will skip annotation queries and disabled annotations", func(t *testing.T) {
|
||||||
|
dash := models.NewDashboard("test")
|
||||||
|
color := "red"
|
||||||
|
name := "annoName"
|
||||||
|
disabledGrafanaAnnotation := DashAnnotation{
|
||||||
|
Datasource: CreateDatasource("grafana", "grafana"),
|
||||||
|
Enable: false,
|
||||||
|
Name: &name,
|
||||||
|
IconColor: &color,
|
||||||
|
}
|
||||||
|
grafanaAnnotation := DashAnnotation{
|
||||||
|
Datasource: CreateDatasource("grafana", "grafana"),
|
||||||
|
Enable: true,
|
||||||
|
Name: &name,
|
||||||
|
IconColor: &color,
|
||||||
|
Target: &dashboard2.AnnotationTarget{
|
||||||
|
Limit: 100,
|
||||||
|
MatchAny: true,
|
||||||
|
Tags: nil,
|
||||||
|
Type: "dashboard",
|
||||||
|
},
|
||||||
|
Type: "dashboard",
|
||||||
|
}
|
||||||
|
queryAnnotation := DashAnnotation{
|
||||||
|
Datasource: CreateDatasource("prometheus", "abc123"),
|
||||||
|
Enable: true,
|
||||||
|
Name: &name,
|
||||||
|
}
|
||||||
|
annos := []DashAnnotation{grafanaAnnotation, queryAnnotation, disabledGrafanaAnnotation}
|
||||||
|
dashboard := AddAnnotationsToDashboard(t, dash, annos)
|
||||||
|
|
||||||
|
annotationsRepo := annotations.FakeAnnotationsRepo{}
|
||||||
|
fakeStore := FakePublicDashboardStore{}
|
||||||
|
service := &PublicDashboardServiceImpl{
|
||||||
|
log: log.New("test.logger"),
|
||||||
|
store: &fakeStore,
|
||||||
|
AnnotationsRepo: &annotationsRepo,
|
||||||
|
}
|
||||||
|
pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid}
|
||||||
|
fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dashboard, nil)
|
||||||
|
annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{
|
||||||
|
{
|
||||||
|
Id: 1,
|
||||||
|
DashboardId: 1,
|
||||||
|
PanelId: 1,
|
||||||
|
Tags: []string{},
|
||||||
|
TimeEnd: 1,
|
||||||
|
Time: 2,
|
||||||
|
Text: "text",
|
||||||
|
},
|
||||||
|
}, nil).Maybe()
|
||||||
|
|
||||||
|
items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123")
|
||||||
|
|
||||||
|
expected := AnnotationEvent{
|
||||||
|
Id: 1,
|
||||||
|
DashboardId: 1,
|
||||||
|
PanelId: 1,
|
||||||
|
Tags: []string{},
|
||||||
|
IsRegion: true,
|
||||||
|
Text: "text",
|
||||||
|
Color: color,
|
||||||
|
Time: 2,
|
||||||
|
TimeEnd: 1,
|
||||||
|
Source: grafanaAnnotation,
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, items, 1)
|
||||||
|
assert.Equal(t, expected, items[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("test will return nothing when dashboard has no annotations", func(t *testing.T) {
|
||||||
|
annotationsRepo := annotations.FakeAnnotationsRepo{}
|
||||||
|
fakeStore := FakePublicDashboardStore{}
|
||||||
|
service := &PublicDashboardServiceImpl{
|
||||||
|
log: log.New("test.logger"),
|
||||||
|
store: &fakeStore,
|
||||||
|
AnnotationsRepo: &annotationsRepo,
|
||||||
|
}
|
||||||
|
dashboard := models.NewDashboard("dashWithNoAnnotations")
|
||||||
|
pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid}
|
||||||
|
fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dashboard, nil)
|
||||||
|
|
||||||
|
items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123")
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, items)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("test will error when annotations repo returns an error", func(t *testing.T) {
|
||||||
|
annotationsRepo := annotations.FakeAnnotationsRepo{}
|
||||||
|
fakeStore := FakePublicDashboardStore{}
|
||||||
|
service := &PublicDashboardServiceImpl{
|
||||||
|
log: log.New("test.logger"),
|
||||||
|
store: &fakeStore,
|
||||||
|
AnnotationsRepo: &annotationsRepo,
|
||||||
|
}
|
||||||
|
dash := models.NewDashboard("test")
|
||||||
|
color := "red"
|
||||||
|
name := "annoName"
|
||||||
|
grafanaAnnotation := DashAnnotation{
|
||||||
|
Datasource: CreateDatasource("grafana", "grafana"),
|
||||||
|
Enable: true,
|
||||||
|
Name: &name,
|
||||||
|
IconColor: &color,
|
||||||
|
Target: &dashboard2.AnnotationTarget{
|
||||||
|
Limit: 100,
|
||||||
|
MatchAny: false,
|
||||||
|
Tags: []string{"tag1"},
|
||||||
|
Type: "tags",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
annos := []DashAnnotation{grafanaAnnotation}
|
||||||
|
dash = AddAnnotationsToDashboard(t, dash, annos)
|
||||||
|
pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dash.Uid}
|
||||||
|
fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dash, nil)
|
||||||
|
annotationsRepo.On("Find", mock.Anything, mock.Anything).Return(nil, errors.New("failed")).Maybe()
|
||||||
|
|
||||||
|
items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123")
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, items)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetPublicDashboard(t *testing.T) {
|
func TestGetPublicDashboard(t *testing.T) {
|
||||||
type storeResp struct {
|
type storeResp struct {
|
||||||
pd *PublicDashboard
|
pd *PublicDashboard
|
||||||
@ -374,12 +663,20 @@ func TestBuildAnonymousUser(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("will add datasource read and query permissions to user for each datasource in dashboard", func(t *testing.T) {
|
t.Run("will add datasource read and query permissions to user for each datasource in dashboard", func(t *testing.T) {
|
||||||
user := service.BuildAnonymousUser(context.Background(), dashboard)
|
user := service.BuildAnonymousUser(context.Background(), dashboard)
|
||||||
|
|
||||||
require.Equal(t, dashboard.OrgId, user.OrgID)
|
require.Equal(t, dashboard.OrgId, user.OrgID)
|
||||||
require.Equal(t, "datasources:uid:ds1", user.Permissions[user.OrgID]["datasources:query"][0])
|
require.Equal(t, "datasources:uid:ds1", user.Permissions[user.OrgID]["datasources:query"][0])
|
||||||
require.Equal(t, "datasources:uid:ds3", user.Permissions[user.OrgID]["datasources:query"][1])
|
require.Equal(t, "datasources:uid:ds3", user.Permissions[user.OrgID]["datasources:query"][1])
|
||||||
require.Equal(t, "datasources:uid:ds1", user.Permissions[user.OrgID]["datasources:read"][0])
|
require.Equal(t, "datasources:uid:ds1", user.Permissions[user.OrgID]["datasources:read"][0])
|
||||||
require.Equal(t, "datasources:uid:ds3", user.Permissions[user.OrgID]["datasources:read"][1])
|
require.Equal(t, "datasources:uid:ds3", user.Permissions[user.OrgID]["datasources:read"][1])
|
||||||
})
|
})
|
||||||
|
t.Run("will add dashboard and annotation permissions needed for getting annotations", func(t *testing.T) {
|
||||||
|
user := service.BuildAnonymousUser(context.Background(), dashboard)
|
||||||
|
|
||||||
|
require.Equal(t, dashboard.OrgId, user.OrgID)
|
||||||
|
require.Equal(t, "annotations:type:dashboard", user.Permissions[user.OrgID]["annotations:read"][0])
|
||||||
|
require.Equal(t, "dashboards:*", user.Permissions[user.OrgID]["dashboards:read"][0])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetMetricRequest(t *testing.T) {
|
func TestGetMetricRequest(t *testing.T) {
|
||||||
@ -863,3 +1160,33 @@ func TestDashboardEnabledChanged(t *testing.T) {
|
|||||||
assert.True(t, publicDashboardIsEnabledChanged(&PublicDashboard{IsEnabled: false}, &PublicDashboard{IsEnabled: true}))
|
assert.True(t, publicDashboardIsEnabledChanged(&PublicDashboard{IsEnabled: false}, &PublicDashboard{IsEnabled: true}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CreateDatasource(dsType string, uid string) struct {
|
||||||
|
Type *string `json:"type,omitempty"`
|
||||||
|
Uid *string `json:"uid,omitempty"`
|
||||||
|
} {
|
||||||
|
return struct {
|
||||||
|
Type *string `json:"type,omitempty"`
|
||||||
|
Uid *string `json:"uid,omitempty"`
|
||||||
|
}{
|
||||||
|
Type: &dsType,
|
||||||
|
Uid: &uid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddAnnotationsToDashboard(t *testing.T, dash *models.Dashboard, annotations []DashAnnotation) *models.Dashboard {
|
||||||
|
type annotationsDto struct {
|
||||||
|
List []DashAnnotation `json:"list"`
|
||||||
|
}
|
||||||
|
annos := annotationsDto{}
|
||||||
|
annos.List = annotations
|
||||||
|
annoJSON, err := json.Marshal(annos)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dashAnnos, err := simplejson.NewJson(annoJSON)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dash.Data.Set("annotations", dashAnnos)
|
||||||
|
|
||||||
|
return dash
|
||||||
|
}
|
||||||
|
@ -53,6 +53,7 @@ export function executeAnnotationQuery(
|
|||||||
scopedVars,
|
scopedVars,
|
||||||
...interval,
|
...interval,
|
||||||
app: CoreApp.Dashboard,
|
app: CoreApp.Dashboard,
|
||||||
|
publicDashboardAccessToken: options.dashboard.meta.publicDashboardAccessToken,
|
||||||
|
|
||||||
timezone: options.dashboard.timezone,
|
timezone: options.dashboard.timezone,
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
KeyValue,
|
KeyValue,
|
||||||
standardTransformers,
|
standardTransformers,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
import { config } from 'app/core/config';
|
||||||
|
|
||||||
export const standardAnnotationSupport: AnnotationSupport = {
|
export const standardAnnotationSupport: AnnotationSupport = {
|
||||||
/**
|
/**
|
||||||
@ -112,9 +113,22 @@ export const annotationEventNames: AnnotationFieldInfo[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const publicDashboardEventNames: AnnotationFieldInfo[] = [
|
||||||
|
{
|
||||||
|
key: 'color',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'isRegion',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'source',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Given legacy infrastructure, alert events are passed though the same annotation
|
// Given legacy infrastructure, alert events are passed though the same annotation
|
||||||
// pipeline, but include fields that should not be exposed generally
|
// pipeline, but include fields that should not be exposed generally
|
||||||
const alertEventAndAnnotationFields: AnnotationFieldInfo[] = [
|
const alertEventAndAnnotationFields: AnnotationFieldInfo[] = [
|
||||||
|
...(config.isPublicDashboardView ? publicDashboardEventNames : []),
|
||||||
...annotationEventNames,
|
...annotationEventNames,
|
||||||
{ key: 'userId' },
|
{ key: 'userId' },
|
||||||
{ key: 'login' },
|
{ key: 'login' },
|
||||||
@ -185,7 +199,8 @@ export function getAnnotationsFromData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!hasTime || !hasText) {
|
if (!hasTime || !hasText) {
|
||||||
return []; // throw an error?
|
console.error('Cannot process annotation fields. No time or text present.');
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add each value to the string
|
// Add each value to the string
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
import { DataQueryRequest, DataSourceInstanceSettings, DataSourceRef } from '@grafana/data';
|
import { DataQueryRequest, DataSourceInstanceSettings, DataSourceRef, TimeRange } from '@grafana/data';
|
||||||
import { BackendSrvRequest, BackendSrv, DataSourceWithBackend } from '@grafana/runtime';
|
import { BackendSrvRequest, BackendSrv, DataSourceWithBackend } from '@grafana/runtime';
|
||||||
|
import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
|
||||||
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||||
|
|
||||||
|
import { GRAFANA_DATASOURCE_NAME } from '../../alerting/unified/utils/datasource';
|
||||||
|
|
||||||
import { PublicDashboardDataSource, PUBLIC_DATASOURCE, DEFAULT_INTERVAL } from './PublicDashboardDataSource';
|
import { PublicDashboardDataSource, PUBLIC_DATASOURCE, DEFAULT_INTERVAL } from './PublicDashboardDataSource';
|
||||||
|
|
||||||
const mockDatasourceRequest = jest.fn();
|
const mockDatasourceRequest = jest.fn();
|
||||||
@ -12,6 +15,9 @@ const backendSrv = {
|
|||||||
fetch: (options: BackendSrvRequest) => {
|
fetch: (options: BackendSrvRequest) => {
|
||||||
return of(mockDatasourceRequest(options));
|
return of(mockDatasourceRequest(options));
|
||||||
},
|
},
|
||||||
|
get: (url: string, options?: Partial<BackendSrvRequest>) => {
|
||||||
|
return mockDatasourceRequest(url, options);
|
||||||
|
},
|
||||||
} as unknown as BackendSrv;
|
} as unknown as BackendSrv;
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
@ -25,7 +31,50 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('PublicDashboardDatasource', () => {
|
describe('PublicDashboardDatasource', () => {
|
||||||
test('Fetches results from the pubdash query endpoint', () => {
|
test('will add annotation query type to annotations', () => {
|
||||||
|
const ds = new PublicDashboardDataSource('public');
|
||||||
|
const annotationQuery = {
|
||||||
|
enable: true,
|
||||||
|
name: 'someName',
|
||||||
|
iconColor: 'red',
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const annotation = ds?.annotations.prepareQuery(annotationQuery);
|
||||||
|
|
||||||
|
expect(annotation?.queryType).toEqual(GrafanaQueryType.Annotations);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fetches results from the pubdash annotations endpoint when it is an annotation query', async () => {
|
||||||
|
mockDatasourceRequest.mockReset();
|
||||||
|
mockDatasourceRequest.mockReturnValue(Promise.resolve([]));
|
||||||
|
|
||||||
|
const ds = new PublicDashboardDataSource('public');
|
||||||
|
const panelId = 1;
|
||||||
|
const publicDashboardAccessToken = 'abc123';
|
||||||
|
|
||||||
|
await ds.query({
|
||||||
|
maxDataPoints: 10,
|
||||||
|
intervalMs: 5000,
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
refId: 'A',
|
||||||
|
datasource: { uid: GRAFANA_DATASOURCE_NAME, type: 'sample' },
|
||||||
|
queryType: GrafanaQueryType.Annotations,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
panelId,
|
||||||
|
publicDashboardAccessToken,
|
||||||
|
range: { from: new Date().toLocaleString(), to: new Date().toLocaleString() } as unknown as TimeRange,
|
||||||
|
} as DataQueryRequest);
|
||||||
|
|
||||||
|
const mock = mockDatasourceRequest.mock;
|
||||||
|
|
||||||
|
expect(mock.calls.length).toBe(1);
|
||||||
|
expect(mock.lastCall[0]).toEqual(`/api/public/dashboards/${publicDashboardAccessToken}/annotations`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fetches results from the pubdash query endpoint when not annotation query', () => {
|
||||||
mockDatasourceRequest.mockReset();
|
mockDatasourceRequest.mockReset();
|
||||||
mockDatasourceRequest.mockReturnValue(Promise.resolve({}));
|
mockDatasourceRequest.mockReturnValue(Promise.resolve({}));
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { catchError, Observable, of, switchMap } from 'rxjs';
|
import { catchError, from, Observable, of, switchMap } from 'rxjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AnnotationQuery,
|
||||||
DataQuery,
|
DataQuery,
|
||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
@ -8,10 +9,13 @@ import {
|
|||||||
DataSourceJsonData,
|
DataSourceJsonData,
|
||||||
DataSourcePluginMeta,
|
DataSourcePluginMeta,
|
||||||
DataSourceRef,
|
DataSourceRef,
|
||||||
|
toDataFrame,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { BackendDataSourceResponse, getBackendSrv, toDataQueryResponse } from '@grafana/runtime';
|
import { BackendDataSourceResponse, getBackendSrv, toDataQueryResponse } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { GrafanaQueryType } from '../../../plugins/datasource/grafana/types';
|
||||||
import { MIXED_DATASOURCE_NAME } from '../../../plugins/datasource/mixed/MixedDataSource';
|
import { MIXED_DATASOURCE_NAME } from '../../../plugins/datasource/mixed/MixedDataSource';
|
||||||
|
import { GRAFANA_DATASOURCE_NAME } from '../../alerting/unified/utils/datasource';
|
||||||
|
|
||||||
export const PUBLIC_DATASOURCE = '-- Public --';
|
export const PUBLIC_DATASOURCE = '-- Public --';
|
||||||
export const DEFAULT_INTERVAL = '1min';
|
export const DEFAULT_INTERVAL = '1min';
|
||||||
@ -35,6 +39,12 @@ export class PublicDashboardDataSource extends DataSourceApi<DataQuery, DataSour
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.interval = PublicDashboardDataSource.resolveInterval(datasource);
|
this.interval = PublicDashboardDataSource.resolveInterval(datasource);
|
||||||
|
|
||||||
|
this.annotations = {
|
||||||
|
prepareQuery(anno: AnnotationQuery): DataQuery | undefined {
|
||||||
|
return { ...anno, queryType: GrafanaQueryType.Annotations, refId: 'anno' };
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,23 +88,51 @@ export class PublicDashboardDataSource extends DataSourceApi<DataQuery, DataSour
|
|||||||
return of({ data: [] });
|
return of({ data: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const body: any = { intervalMs, maxDataPoints };
|
// Its an annotations query
|
||||||
|
// Currently, annotations requests come in one at a time, so there will only be one target
|
||||||
|
const target = request.targets[0];
|
||||||
|
if (target.queryType === GrafanaQueryType.Annotations) {
|
||||||
|
if (target?.datasource?.uid === GRAFANA_DATASOURCE_NAME) {
|
||||||
|
return from(this.getAnnotations(request));
|
||||||
|
}
|
||||||
|
return of({ data: [] });
|
||||||
|
}
|
||||||
|
|
||||||
return getBackendSrv()
|
// Its a datasource query
|
||||||
.fetch<BackendDataSourceResponse>({
|
else {
|
||||||
url: `/api/public/dashboards/${publicDashboardAccessToken}/panels/${panelId}/query`,
|
const body: any = { intervalMs, maxDataPoints };
|
||||||
method: 'POST',
|
|
||||||
data: body,
|
return getBackendSrv()
|
||||||
requestId,
|
.fetch<BackendDataSourceResponse>({
|
||||||
})
|
url: `/api/public/dashboards/${publicDashboardAccessToken}/panels/${panelId}/query`,
|
||||||
.pipe(
|
method: 'POST',
|
||||||
switchMap((raw) => {
|
data: body,
|
||||||
return of(toDataQueryResponse(raw, queries));
|
requestId,
|
||||||
}),
|
|
||||||
catchError((err) => {
|
|
||||||
return of(toDataQueryResponse(err));
|
|
||||||
})
|
})
|
||||||
);
|
.pipe(
|
||||||
|
switchMap((raw) => {
|
||||||
|
return of(toDataQueryResponse(raw, queries));
|
||||||
|
}),
|
||||||
|
catchError((err) => {
|
||||||
|
return of(toDataQueryResponse(err));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAnnotations(request: DataQueryRequest<DataQuery>): Promise<DataQueryResponse> {
|
||||||
|
const {
|
||||||
|
publicDashboardAccessToken: accessToken,
|
||||||
|
range: { to, from },
|
||||||
|
} = request;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
from: from.valueOf(),
|
||||||
|
to: to.valueOf(),
|
||||||
|
};
|
||||||
|
const annotations = await getBackendSrv().get(`/api/public/dashboards/${accessToken}/annotations`, params);
|
||||||
|
|
||||||
|
return { data: [toDataFrame(annotations)] };
|
||||||
}
|
}
|
||||||
|
|
||||||
testDatasource(): Promise<any> {
|
testDatasource(): Promise<any> {
|
||||||
|
@ -91,7 +91,7 @@ describe('AnnotationsWorker', () => {
|
|||||||
const options = getDefaultOptions();
|
const options = getDefaultOptions();
|
||||||
options.dashboard.meta.publicDashboardAccessToken = 'accessTokenString';
|
options.dashboard.meta.publicDashboardAccessToken = 'accessTokenString';
|
||||||
|
|
||||||
expect(worker.canWork(options)).toBe(false);
|
expect(worker.canWork(options)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import { AnnotationQuery, DataSourceApi } from '@grafana/data';
|
|||||||
import { getDataSourceSrv } from '@grafana/runtime';
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
import { AnnotationQueryFinished, AnnotationQueryStarted } from '../../../../types/events';
|
import { AnnotationQueryFinished, AnnotationQueryStarted } from '../../../../types/events';
|
||||||
|
import { PUBLIC_DATASOURCE, PublicDashboardDataSource } from '../../../dashboard/services/PublicDashboardDataSource';
|
||||||
import { DashboardModel } from '../../../dashboard/state';
|
import { DashboardModel } from '../../../dashboard/state';
|
||||||
|
|
||||||
import { AnnotationsQueryRunner } from './AnnotationsQueryRunner';
|
import { AnnotationsQueryRunner } from './AnnotationsQueryRunner';
|
||||||
@ -29,8 +30,8 @@ export class AnnotationsWorker implements DashboardQueryRunnerWorker {
|
|||||||
|
|
||||||
canWork({ dashboard }: DashboardQueryRunnerOptions): boolean {
|
canWork({ dashboard }: DashboardQueryRunnerOptions): boolean {
|
||||||
const annotations = dashboard.annotations.list.find(AnnotationsWorker.getAnnotationsToProcessFilter);
|
const annotations = dashboard.annotations.list.find(AnnotationsWorker.getAnnotationsToProcessFilter);
|
||||||
// We shouldn't return annotations for public dashboards v1
|
|
||||||
return Boolean(annotations) && !this.publicDashboardViewMode(dashboard);
|
return Boolean(annotations);
|
||||||
}
|
}
|
||||||
|
|
||||||
work(options: DashboardQueryRunnerOptions): Observable<DashboardQueryRunnerWorkerResult> {
|
work(options: DashboardQueryRunnerOptions): Observable<DashboardQueryRunnerWorkerResult> {
|
||||||
@ -39,11 +40,22 @@ export class AnnotationsWorker implements DashboardQueryRunnerWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { dashboard, range } = options;
|
const { dashboard, range } = options;
|
||||||
const annotations = dashboard.annotations.list.filter(AnnotationsWorker.getAnnotationsToProcessFilter);
|
let annotations = dashboard.annotations.list.filter(AnnotationsWorker.getAnnotationsToProcessFilter);
|
||||||
|
// We only want to create a single PublicDashboardDatasource. This will get all annotations in one request.
|
||||||
|
if (dashboard.meta.publicDashboardAccessToken && annotations.length > 0) {
|
||||||
|
annotations = [annotations[0]];
|
||||||
|
}
|
||||||
const observables = annotations.map((annotation) => {
|
const observables = annotations.map((annotation) => {
|
||||||
const datasourceObservable = from(getDataSourceSrv().get(annotation.datasource)).pipe(
|
let datasourceObservable;
|
||||||
catchError(handleDatasourceSrvError) // because of the reduce all observables need to be completed, so an erroneous observable wont do
|
if (dashboard.meta.publicDashboardAccessToken !== '') {
|
||||||
);
|
const pubdashDatasource = new PublicDashboardDataSource(PUBLIC_DATASOURCE);
|
||||||
|
datasourceObservable = of(pubdashDatasource).pipe(catchError(handleDatasourceSrvError));
|
||||||
|
} else {
|
||||||
|
datasourceObservable = from(getDataSourceSrv().get(annotation.datasource)).pipe(
|
||||||
|
catchError(handleDatasourceSrvError) // because of the reduce all observables need to be completed, so an erroneous observable wont do
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return datasourceObservable.pipe(
|
return datasourceObservable.pipe(
|
||||||
mergeMap((datasource?: DataSourceApi) => {
|
mergeMap((datasource?: DataSourceApi) => {
|
||||||
const runner = this.runners.find((r) => r.canRun(datasource));
|
const runner = this.runners.find((r) => r.canRun(datasource));
|
||||||
@ -65,7 +77,11 @@ export class AnnotationsWorker implements DashboardQueryRunnerWorker {
|
|||||||
annotation.snapshotData = cloneDeep(results);
|
annotation.snapshotData = cloneDeep(results);
|
||||||
}
|
}
|
||||||
// translate result
|
// translate result
|
||||||
return translateQueryResult(annotation, results);
|
if (dashboard.meta.publicDashboardAccessToken) {
|
||||||
|
return results;
|
||||||
|
} else {
|
||||||
|
return translateQueryResult(annotation, results);
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
finalize(() => {
|
finalize(() => {
|
||||||
dashboard.events.publish(new AnnotationQueryFinished(annotation));
|
dashboard.events.publish(new AnnotationQueryFinished(annotation));
|
||||||
|
@ -19,7 +19,7 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
function getDefaultOptions(): DashboardQueryRunnerOptions {
|
function getDefaultOptions(): DashboardQueryRunnerOptions {
|
||||||
const dashboard: any = { id: 'an id', uid: 'a uid' };
|
const dashboard: any = { id: 'an id', uid: 'a uid', meta: { publicDashboardAccessToken: '' } };
|
||||||
const range = getDefaultTimeRange();
|
const range = getDefaultTimeRange();
|
||||||
|
|
||||||
return { dashboard, range };
|
return { dashboard, range };
|
||||||
@ -49,6 +49,15 @@ describe('UnifiedAlertStatesWorker', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when canWork is called on a public dashboard view', () => {
|
||||||
|
it('then it should return false', () => {
|
||||||
|
const options = getDefaultOptions();
|
||||||
|
options.dashboard.meta.publicDashboardAccessToken = 'abc123';
|
||||||
|
|
||||||
|
expect(worker.canWork(options)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('when canWork is called with no dashboard id', () => {
|
describe('when canWork is called with no dashboard id', () => {
|
||||||
it('then it should return false', () => {
|
it('then it should return false', () => {
|
||||||
const dashboard: any = {};
|
const dashboard: any = {};
|
||||||
@ -114,6 +123,7 @@ describe('UnifiedAlertStatesWorker', () => {
|
|||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
it('then it should return the correct results', async () => {
|
it('then it should return the correct results', async () => {
|
||||||
const getResults: PromRulesResponse = {
|
const getResults: PromRulesResponse = {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
|
@ -23,6 +23,11 @@ export class UnifiedAlertStatesWorker implements DashboardQueryRunnerWorker {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cannot fetch rules while on a public dashboard since it's unauthenticated
|
||||||
|
if (dashboard.meta.publicDashboardAccessToken) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (range.raw.to !== 'now') {
|
if (range.raw.to !== 'now') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user