mirror of https://github.com/grafana/grafana.git
Public Dashboards: use intervalMs and maxDataPoints from request (#53613)
This commit is contained in:
parent
fcea9ac913
commit
722aca5c53
|
|
@ -4145,8 +4145,7 @@ exports[`better eslint`] = {
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
|
|
||||||
],
|
],
|
||||||
"public/app/features/dashboard/services/TimeSrv.test.ts:5381": [
|
"public/app/features/dashboard/services/TimeSrv.test.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,11 @@ import (
|
||||||
"github.com/grafana/grafana/pkg/api/routing"
|
"github.com/grafana/grafana/pkg/api/routing"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"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"
|
||||||
"github.com/grafana/grafana/pkg/services/query"
|
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
|
|
@ -27,7 +24,6 @@ type Api struct {
|
||||||
PublicDashboardService publicdashboards.Service
|
PublicDashboardService publicdashboards.Service
|
||||||
RouteRegister routing.RouteRegister
|
RouteRegister routing.RouteRegister
|
||||||
AccessControl accesscontrol.AccessControl
|
AccessControl accesscontrol.AccessControl
|
||||||
QueryDataService *query.Service
|
|
||||||
Features *featuremgmt.FeatureManager
|
Features *featuremgmt.FeatureManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,14 +31,12 @@ func ProvideApi(
|
||||||
pd publicdashboards.Service,
|
pd publicdashboards.Service,
|
||||||
rr routing.RouteRegister,
|
rr routing.RouteRegister,
|
||||||
ac accesscontrol.AccessControl,
|
ac accesscontrol.AccessControl,
|
||||||
qds *query.Service,
|
|
||||||
features *featuremgmt.FeatureManager,
|
features *featuremgmt.FeatureManager,
|
||||||
) *Api {
|
) *Api {
|
||||||
api := &Api{
|
api := &Api{
|
||||||
PublicDashboardService: pd,
|
PublicDashboardService: pd,
|
||||||
RouteRegister: rr,
|
RouteRegister: rr,
|
||||||
AccessControl: ac,
|
AccessControl: ac,
|
||||||
QueryDataService: qds,
|
|
||||||
Features: features,
|
Features: features,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,35 +151,16 @@ func (api *Api) QueryPublicDashboard(c *models.ReqContext) response.Response {
|
||||||
return response.Error(http.StatusBadRequest, "invalid panel ID", err)
|
return response.Error(http.StatusBadRequest, "invalid panel ID", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the dashboard
|
reqDTO := &PublicDashboardQueryDTO{}
|
||||||
pubdash, dashboard, err := api.PublicDashboardService.GetPublicDashboard(c.Req.Context(), web.Params(c.Req)[":accessToken"])
|
if err = web.Bind(c.Req, reqDTO); err != nil {
|
||||||
if err != nil {
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||||
return response.Error(http.StatusInternalServerError, "could not fetch dashboard", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the request data objecct
|
resp, err := api.PublicDashboardService.GetQueryDataResponse(c.Req.Context(), c.SkipCache, reqDTO, panelId, web.Params(c.Req)[":accessToken"])
|
||||||
reqDTO, err := api.PublicDashboardService.BuildPublicDashboardMetricRequest(
|
|
||||||
c.Req.Context(),
|
|
||||||
dashboard,
|
|
||||||
pubdash,
|
|
||||||
panelId,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return handleDashboardErr(http.StatusInternalServerError, "Failed to get queries for public dashboard", err)
|
return handlePublicDashboardErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build anonymous user for the request
|
|
||||||
anonymousUser, err := api.PublicDashboardService.BuildAnonymousUser(c.Req.Context(), dashboard)
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(http.StatusInternalServerError, "could not create anonymous user", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make the request
|
|
||||||
resp, err := api.QueryDataService.QueryDataMultipleSources(c.Req.Context(), anonymousUser, c.SkipCache, reqDTO, true)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return handleQueryMetricsError(err)
|
|
||||||
}
|
|
||||||
return toJsonStreamingResponse(api.Features, resp)
|
return toJsonStreamingResponse(api.Features, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,24 +184,8 @@ func handleDashboardErr(defaultCode int, defaultMsg string, err error) response.
|
||||||
return response.Error(defaultCode, defaultMsg, err)
|
return response.Error(defaultCode, defaultMsg, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copied from pkg/api/metrics.go
|
func handlePublicDashboardErr(err error) response.Response {
|
||||||
func handleQueryMetricsError(err error) *response.NormalResponse {
|
return handleDashboardErr(http.StatusInternalServerError, "Unexpected Error", err)
|
||||||
if errors.Is(err, datasources.ErrDataSourceAccessDenied) {
|
|
||||||
return response.Error(http.StatusForbidden, "Access denied to data source", err)
|
|
||||||
}
|
|
||||||
if errors.Is(err, datasources.ErrDataSourceNotFound) {
|
|
||||||
return response.Error(http.StatusNotFound, "Data source not found", err)
|
|
||||||
}
|
|
||||||
var badQuery *query.ErrBadQuery
|
|
||||||
if errors.As(err, &badQuery) {
|
|
||||||
return response.Error(http.StatusBadRequest, util.Capitalize(badQuery.Message), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.Is(err, backendplugin.ErrPluginNotRegistered) {
|
|
||||||
return response.Error(http.StatusNotFound, "Plugin not found", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.Error(http.StatusInternalServerError, "Query data error", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copied from pkg/api/metrics.go
|
// Copied from pkg/api/metrics.go
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
|
|
@ -23,7 +24,7 @@ import (
|
||||||
"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"
|
||||||
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
|
||||||
"github.com/grafana/grafana/pkg/services/datasources/service"
|
"github.com/grafana/grafana/pkg/services/datasources/service"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||||
|
|
@ -40,11 +41,13 @@ func TestAPIGetPublicDashboard(t *testing.T) {
|
||||||
t.Run("It should 404 if featureflag is not enabled", func(t *testing.T) {
|
t.Run("It should 404 if featureflag is not enabled", func(t *testing.T) {
|
||||||
cfg := setting.NewCfg()
|
cfg := setting.NewCfg()
|
||||||
cfg.RBACEnabled = false
|
cfg.RBACEnabled = false
|
||||||
qs := buildQueryDataService(t, nil, nil, nil)
|
|
||||||
service := publicdashboards.NewFakePublicDashboardService(t)
|
service := publicdashboards.NewFakePublicDashboardService(t)
|
||||||
service.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
|
service.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
|
||||||
Return(&PublicDashboard{}, &models.Dashboard{}, nil).Maybe()
|
Return(&PublicDashboard{}, &models.Dashboard{}, nil).Maybe()
|
||||||
testServer := setupTestServer(t, cfg, qs, featuremgmt.WithFeatures(), service, nil)
|
service.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).
|
||||||
|
Return(&PublicDashboard{}, nil).Maybe()
|
||||||
|
|
||||||
|
testServer := setupTestServer(t, cfg, featuremgmt.WithFeatures(), service, nil)
|
||||||
|
|
||||||
response := callAPI(testServer, http.MethodGet, "/api/public/dashboards", nil, t)
|
response := callAPI(testServer, http.MethodGet, "/api/public/dashboards", nil, t)
|
||||||
assert.Equal(t, http.StatusNotFound, response.Code)
|
assert.Equal(t, http.StatusNotFound, response.Code)
|
||||||
|
|
@ -53,7 +56,7 @@ func TestAPIGetPublicDashboard(t *testing.T) {
|
||||||
assert.Equal(t, http.StatusNotFound, response.Code)
|
assert.Equal(t, http.StatusNotFound, response.Code)
|
||||||
|
|
||||||
// control set. make sure routes are mounted
|
// control set. make sure routes are mounted
|
||||||
testServer = setupTestServer(t, cfg, qs, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), service, nil)
|
testServer = setupTestServer(t, cfg, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), service, nil)
|
||||||
response = callAPI(testServer, http.MethodGet, "/api/public/dashboards/asdf", nil, t)
|
response = callAPI(testServer, http.MethodGet, "/api/public/dashboards/asdf", nil, t)
|
||||||
assert.NotEqual(t, http.StatusNotFound, response.Code)
|
assert.NotEqual(t, http.StatusNotFound, response.Code)
|
||||||
})
|
})
|
||||||
|
|
@ -102,7 +105,6 @@ func TestAPIGetPublicDashboard(t *testing.T) {
|
||||||
testServer := setupTestServer(
|
testServer := setupTestServer(
|
||||||
t,
|
t,
|
||||||
cfg,
|
cfg,
|
||||||
buildQueryDataService(t, nil, nil, nil),
|
|
||||||
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
|
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
|
||||||
service,
|
service,
|
||||||
nil,
|
nil,
|
||||||
|
|
@ -182,7 +184,6 @@ func TestAPIGetPublicDashboardConfig(t *testing.T) {
|
||||||
testServer := setupTestServer(
|
testServer := setupTestServer(
|
||||||
t,
|
t,
|
||||||
cfg,
|
cfg,
|
||||||
buildQueryDataService(t, nil, nil, nil),
|
|
||||||
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
|
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
|
||||||
service,
|
service,
|
||||||
nil,
|
nil,
|
||||||
|
|
@ -249,7 +250,6 @@ func TestApiSavePublicDashboardConfig(t *testing.T) {
|
||||||
testServer := setupTestServer(
|
testServer := setupTestServer(
|
||||||
t,
|
t,
|
||||||
cfg,
|
cfg,
|
||||||
buildQueryDataService(t, nil, nil, nil),
|
|
||||||
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
|
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
|
||||||
service,
|
service,
|
||||||
nil,
|
nil,
|
||||||
|
|
@ -277,40 +277,56 @@ func TestApiSavePublicDashboardConfig(t *testing.T) {
|
||||||
|
|
||||||
// `/public/dashboards/:uid/query`` endpoint test
|
// `/public/dashboards/:uid/query`` endpoint test
|
||||||
func TestAPIQueryPublicDashboard(t *testing.T) {
|
func TestAPIQueryPublicDashboard(t *testing.T) {
|
||||||
cacheService := &fakeDatasources.FakeCacheService{
|
mockedResponse := &backend.QueryDataResponse{
|
||||||
DataSources: []*datasources.DataSource{
|
Responses: map[string]backend.DataResponse{
|
||||||
{Uid: "mysqlds"},
|
"test": {
|
||||||
{Uid: "promds"},
|
Frames: data.Frames{
|
||||||
{Uid: "promds2"},
|
&data.Frame{
|
||||||
},
|
Name: "anyDataFrame",
|
||||||
}
|
Fields: []*data.Field{
|
||||||
|
data.NewField("anyGroupName", nil, []*string{
|
||||||
// used to determine whether fakePluginClient returns an error
|
aws.String("group_a"), aws.String("group_b"), aws.String("group_c"),
|
||||||
queryReturnsError := false
|
}),
|
||||||
|
|
||||||
fakePluginClient := &fakePluginClient{
|
|
||||||
QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
|
||||||
if queryReturnsError {
|
|
||||||
return nil, errors.New("error")
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := backend.Responses{}
|
|
||||||
|
|
||||||
for _, query := range req.Queries {
|
|
||||||
resp[query.RefID] = backend.DataResponse{
|
|
||||||
Frames: []*data.Frame{
|
|
||||||
{
|
|
||||||
RefID: query.RefID,
|
|
||||||
Name: "query-" + query.RefID,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
Error: nil,
|
||||||
return &backend.QueryDataResponse{Responses: resp}, nil
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
qds := buildQueryDataService(t, cacheService, fakePluginClient, nil)
|
expectedResponse := `{
|
||||||
|
"results": {
|
||||||
|
"test": {
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"schema": {
|
||||||
|
"name": "anyDataFrame",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "anyGroupName",
|
||||||
|
"type": "string",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "string",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"values": [
|
||||||
|
[
|
||||||
|
"group_a",
|
||||||
|
"group_b",
|
||||||
|
"group_c"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
setup := func(enabled bool) (*web.Mux, *publicdashboards.FakePublicDashboardService) {
|
setup := func(enabled bool) (*web.Mux, *publicdashboards.FakePublicDashboardService) {
|
||||||
service := publicdashboards.NewFakePublicDashboardService(t)
|
service := publicdashboards.NewFakePublicDashboardService(t)
|
||||||
|
|
@ -320,7 +336,6 @@ func TestAPIQueryPublicDashboard(t *testing.T) {
|
||||||
testServer := setupTestServer(
|
testServer := setupTestServer(
|
||||||
t,
|
t,
|
||||||
cfg,
|
cfg,
|
||||||
qds,
|
|
||||||
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards, enabled),
|
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards, enabled),
|
||||||
service,
|
service,
|
||||||
nil,
|
nil,
|
||||||
|
|
@ -341,51 +356,29 @@ func TestAPIQueryPublicDashboard(t *testing.T) {
|
||||||
require.Equal(t, http.StatusBadRequest, resp.Code)
|
require.Equal(t, http.StatusBadRequest, resp.Code)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Status code is 400 when the intervalMS is lesser than 0", func(t *testing.T) {
|
||||||
|
server, fakeDashboardService := setup(true)
|
||||||
|
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), "abc123").Return(&backend.QueryDataResponse{}, ErrPublicDashboardBadRequest)
|
||||||
|
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader(`{"intervalMs":-100,"maxDataPoints":1000}`), t)
|
||||||
|
require.Equal(t, http.StatusBadRequest, resp.Code)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Status code is 400 when the maxDataPoints is lesser than 0", func(t *testing.T) {
|
||||||
|
server, fakeDashboardService := setup(true)
|
||||||
|
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), "abc123").Return(&backend.QueryDataResponse{}, ErrPublicDashboardBadRequest)
|
||||||
|
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader(`{"intervalMs":100,"maxDataPoints":-1000}`), t)
|
||||||
|
require.Equal(t, http.StatusBadRequest, resp.Code)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Returns query data when feature toggle is enabled", func(t *testing.T) {
|
t.Run("Returns query data when feature toggle is enabled", func(t *testing.T) {
|
||||||
server, fakeDashboardService := setup(true)
|
server, fakeDashboardService := setup(true)
|
||||||
|
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), "abc123").Return(mockedResponse, nil)
|
||||||
fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&PublicDashboard{}, &models.Dashboard{}, nil)
|
|
||||||
fakeDashboardService.On("BuildAnonymousUser", mock.Anything, mock.Anything, mock.Anything).Return(&user.SignedInUser{}, nil)
|
|
||||||
fakeDashboardService.On("BuildPublicDashboardMetricRequest", mock.Anything, mock.Anything, mock.Anything, int64(2)).Return(dtos.MetricRequest{
|
|
||||||
Queries: []*simplejson.Json{
|
|
||||||
simplejson.MustJson([]byte(`
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "promds"
|
|
||||||
},
|
|
||||||
"exemplar": true,
|
|
||||||
"expr": "query_2_A",
|
|
||||||
"interval": "",
|
|
||||||
"legendFormat": "",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
`)),
|
|
||||||
},
|
|
||||||
}, nil)
|
|
||||||
|
|
||||||
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
|
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
|
||||||
|
|
||||||
require.JSONEq(
|
require.JSONEq(
|
||||||
t,
|
t,
|
||||||
`{
|
expectedResponse,
|
||||||
"results": {
|
|
||||||
"A": {
|
|
||||||
"frames": [
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"values": []
|
|
||||||
},
|
|
||||||
"schema": {
|
|
||||||
"fields": [],
|
|
||||||
"refId": "A",
|
|
||||||
"name": "query-A"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
resp.Body.String(),
|
resp.Body.String(),
|
||||||
)
|
)
|
||||||
require.Equal(t, http.StatusOK, resp.Code)
|
require.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
|
@ -393,107 +386,10 @@ func TestAPIQueryPublicDashboard(t *testing.T) {
|
||||||
|
|
||||||
t.Run("Status code is 500 when the query fails", func(t *testing.T) {
|
t.Run("Status code is 500 when the query fails", func(t *testing.T) {
|
||||||
server, fakeDashboardService := setup(true)
|
server, fakeDashboardService := setup(true)
|
||||||
|
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), "abc123").Return(&backend.QueryDataResponse{}, fmt.Errorf("error"))
|
||||||
|
|
||||||
fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&PublicDashboard{}, &models.Dashboard{}, nil)
|
|
||||||
fakeDashboardService.On("BuildAnonymousUser", mock.Anything, mock.Anything, mock.Anything).Return(&user.SignedInUser{}, nil)
|
|
||||||
fakeDashboardService.On("BuildPublicDashboardMetricRequest", mock.Anything, mock.Anything, mock.Anything, int64(2)).Return(dtos.MetricRequest{
|
|
||||||
Queries: []*simplejson.Json{
|
|
||||||
simplejson.MustJson([]byte(`
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "promds"
|
|
||||||
},
|
|
||||||
"exemplar": true,
|
|
||||||
"expr": "query_2_A",
|
|
||||||
"interval": "",
|
|
||||||
"legendFormat": "",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
`)),
|
|
||||||
},
|
|
||||||
}, nil)
|
|
||||||
|
|
||||||
queryReturnsError = true
|
|
||||||
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
|
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
|
||||||
require.Equal(t, http.StatusInternalServerError, resp.Code)
|
require.Equal(t, http.StatusInternalServerError, resp.Code)
|
||||||
queryReturnsError = false
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Status code is 200 when a panel has queries from multiple datasources", func(t *testing.T) {
|
|
||||||
server, fakeDashboardService := setup(true)
|
|
||||||
|
|
||||||
fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&PublicDashboard{}, &models.Dashboard{}, nil)
|
|
||||||
fakeDashboardService.On("BuildAnonymousUser", mock.Anything, mock.Anything, mock.Anything).Return(&user.SignedInUser{}, nil)
|
|
||||||
fakeDashboardService.On("BuildPublicDashboardMetricRequest", mock.Anything, mock.Anything, mock.Anything, int64(2)).Return(dtos.MetricRequest{
|
|
||||||
Queries: []*simplejson.Json{
|
|
||||||
simplejson.MustJson([]byte(`
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "promds"
|
|
||||||
},
|
|
||||||
"exemplar": true,
|
|
||||||
"expr": "query_2_A",
|
|
||||||
"interval": "",
|
|
||||||
"legendFormat": "",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
`)),
|
|
||||||
simplejson.MustJson([]byte(`
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "promds2"
|
|
||||||
},
|
|
||||||
"exemplar": true,
|
|
||||||
"expr": "query_2_B",
|
|
||||||
"interval": "",
|
|
||||||
"legendFormat": "",
|
|
||||||
"refId": "B"
|
|
||||||
}
|
|
||||||
`)),
|
|
||||||
},
|
|
||||||
}, nil)
|
|
||||||
|
|
||||||
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
|
|
||||||
require.JSONEq(
|
|
||||||
t,
|
|
||||||
`{
|
|
||||||
"results": {
|
|
||||||
"A": {
|
|
||||||
"frames": [
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"values": []
|
|
||||||
},
|
|
||||||
"schema": {
|
|
||||||
"fields": [],
|
|
||||||
"refId": "A",
|
|
||||||
"name": "query-A"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"B": {
|
|
||||||
"frames": [
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"values": []
|
|
||||||
},
|
|
||||||
"schema": {
|
|
||||||
"fields": [],
|
|
||||||
"refId": "B",
|
|
||||||
"name": "query-B"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
resp.Body.String(),
|
|
||||||
)
|
|
||||||
require.Equal(t, http.StatusOK, resp.Code)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -557,14 +453,13 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T)
|
||||||
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)
|
service := publicdashboardsService.ProvideService(cfg, store, qds)
|
||||||
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)
|
||||||
|
|
||||||
// setup test server
|
// setup test server
|
||||||
server := setupTestServer(t,
|
server := setupTestServer(t,
|
||||||
cfg,
|
cfg,
|
||||||
qds,
|
|
||||||
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
|
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
|
||||||
service,
|
service,
|
||||||
db,
|
db,
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ type Server struct {
|
||||||
func setupTestServer(
|
func setupTestServer(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
cfg *setting.Cfg,
|
cfg *setting.Cfg,
|
||||||
qs *query.Service,
|
|
||||||
features *featuremgmt.FeatureManager,
|
features *featuremgmt.FeatureManager,
|
||||||
service publicdashboards.Service,
|
service publicdashboards.Service,
|
||||||
db *sqlstore.SQLStore,
|
db *sqlstore.SQLStore,
|
||||||
|
|
@ -85,7 +84,7 @@ func setupTestServer(
|
||||||
|
|
||||||
// build api, this will mount the routes at the same time if
|
// build api, this will mount the routes at the same time if
|
||||||
// featuremgmt.FlagPublicDashboard is enabled
|
// featuremgmt.FlagPublicDashboard is enabled
|
||||||
ProvideApi(service, rr, ac, qs, features)
|
ProvideApi(service, rr, ac, features)
|
||||||
|
|
||||||
// connect routes to mux
|
// connect routes to mux
|
||||||
rr.Register(m.Router)
|
rr.Register(m.Router)
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
||||||
|
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
||||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||||
publicdashboardsService "github.com/grafana/grafana/pkg/services/publicdashboards/service"
|
publicdashboardsService "github.com/grafana/grafana/pkg/services/publicdashboards/service"
|
||||||
|
"github.com/grafana/grafana/pkg/services/query"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
|
|
@ -57,7 +61,27 @@ func TestRequiresValidAccessToken(t *testing.T) {
|
||||||
func mockAccessTokenExistsResponse(returnArguments ...interface{}) *publicdashboardsService.PublicDashboardServiceImpl {
|
func mockAccessTokenExistsResponse(returnArguments ...interface{}) *publicdashboardsService.PublicDashboardServiceImpl {
|
||||||
fakeStore := &publicdashboards.FakePublicDashboardStore{}
|
fakeStore := &publicdashboards.FakePublicDashboardStore{}
|
||||||
fakeStore.On("AccessTokenExists", mock.Anything, mock.Anything).Return(returnArguments[0], returnArguments[1])
|
fakeStore.On("AccessTokenExists", mock.Anything, mock.Anything).Return(returnArguments[0], returnArguments[1])
|
||||||
return publicdashboardsService.ProvideService(setting.NewCfg(), fakeStore)
|
|
||||||
|
qds := query.ProvideService(
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
&fakePluginRequestValidator{},
|
||||||
|
&fakeDatasources.FakeDataSourceService{},
|
||||||
|
&fakePluginClient{
|
||||||
|
QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||||
|
resp := backend.Responses{
|
||||||
|
"A": backend.DataResponse{
|
||||||
|
Error: fmt.Errorf("query failed"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return &backend.QueryDataResponse{Responses: resp}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&fakeOAuthTokenService{},
|
||||||
|
)
|
||||||
|
|
||||||
|
return publicdashboardsService.ProvideService(setting.NewCfg(), fakeStore, qds)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runMiddleware(request *http.Request, pubdashService *publicdashboardsService.PublicDashboardServiceImpl) *httptest.ResponseRecorder {
|
func runMiddleware(request *http.Request, pubdashService *publicdashboardsService.PublicDashboardServiceImpl) *httptest.ResponseRecorder {
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,10 @@ var (
|
||||||
Reason: "Public dashboard has template variables",
|
Reason: "Public dashboard has template variables",
|
||||||
StatusCode: 422,
|
StatusCode: 422,
|
||||||
}
|
}
|
||||||
|
ErrPublicDashboardBadRequest = PublicDashboardErr{
|
||||||
|
Reason: "Bad Request",
|
||||||
|
StatusCode: 400,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type PublicDashboard struct {
|
type PublicDashboard struct {
|
||||||
|
|
@ -108,6 +112,11 @@ type SavePublicDashboardConfigDTO struct {
|
||||||
PublicDashboard *PublicDashboard
|
PublicDashboard *PublicDashboard
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PublicDashboardQueryDTO struct {
|
||||||
|
IntervalMs int64
|
||||||
|
MaxDataPoints int64
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// COMMANDS
|
// COMMANDS
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,13 @@ package publicdashboards
|
||||||
|
|
||||||
import (
|
import (
|
||||||
context "context"
|
context "context"
|
||||||
|
|
||||||
dtos "github.com/grafana/grafana/pkg/api/dtos"
|
|
||||||
mock "github.com/stretchr/testify/mock"
|
|
||||||
|
|
||||||
models "github.com/grafana/grafana/pkg/models"
|
|
||||||
|
|
||||||
publicdashboardsmodels "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
|
||||||
|
|
||||||
testing "testing"
|
testing "testing"
|
||||||
|
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
models "github.com/grafana/grafana/pkg/models"
|
||||||
|
publicdashboardsmodels "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
|
||||||
user "github.com/grafana/grafana/pkg/services/user"
|
user "github.com/grafana/grafana/pkg/services/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -66,20 +63,20 @@ func (_m *FakePublicDashboardService) BuildAnonymousUser(ctx context.Context, da
|
||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildPublicDashboardMetricRequest provides a mock function with given fields: ctx, dashboard, publicDashboard, panelId
|
// GetQueryDataResponse provides a mock function with given fields: ctx, skipCache, reqDTO, panelId, accessToken
|
||||||
func (_m *FakePublicDashboardService) BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *publicdashboardsmodels.PublicDashboard, panelId int64) (dtos.MetricRequest, error) {
|
func (_m *FakePublicDashboardService) GetQueryDataResponse(ctx context.Context, skipCache bool, reqDTO *publicdashboardsmodels.PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error) {
|
||||||
ret := _m.Called(ctx, dashboard, publicDashboard, panelId)
|
ret := _m.Called(ctx, skipCache, reqDTO, panelId, accessToken)
|
||||||
|
|
||||||
var r0 dtos.MetricRequest
|
var r0 *backend.QueryDataResponse
|
||||||
if rf, ok := ret.Get(0).(func(context.Context, *models.Dashboard, *publicdashboardsmodels.PublicDashboard, int64) dtos.MetricRequest); ok {
|
if rf, ok := ret.Get(0).(func(context.Context, bool, *publicdashboardsmodels.PublicDashboardQueryDTO, int64, string) *backend.QueryDataResponse); ok {
|
||||||
r0 = rf(ctx, dashboard, publicDashboard, panelId)
|
r0 = rf(ctx, skipCache, reqDTO, panelId, accessToken)
|
||||||
} else {
|
} else {
|
||||||
r0 = ret.Get(0).(dtos.MetricRequest)
|
r0 = ret.Get(0).(*backend.QueryDataResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
var r1 error
|
var r1 error
|
||||||
if rf, ok := ret.Get(1).(func(context.Context, *models.Dashboard, *publicdashboardsmodels.PublicDashboard, int64) error); ok {
|
if rf, ok := ret.Get(1).(func(context.Context, bool, *publicdashboardsmodels.PublicDashboardQueryDTO, int64, string) error); ok {
|
||||||
r1 = rf(ctx, dashboard, publicDashboard, panelId)
|
r1 = rf(ctx, skipCache, reqDTO, panelId, accessToken)
|
||||||
} else {
|
} else {
|
||||||
r1 = ret.Error(1)
|
r1 = ret.Error(1)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package publicdashboards
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
|
|
@ -18,7 +18,7 @@ type Service interface {
|
||||||
GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error)
|
GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error)
|
||||||
GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error)
|
GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error)
|
||||||
SavePublicDashboardConfig(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error)
|
SavePublicDashboardConfig(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error)
|
||||||
BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64) (dtos.MetricRequest, error)
|
GetQueryDataResponse(ctx context.Context, skipCache bool, reqDTO *PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error)
|
||||||
PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error)
|
PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error)
|
||||||
AccessTokenExists(ctx context.Context, accessToken string) (bool, error)
|
AccessTokenExists(ctx context.Context, accessToken string) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"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/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
|
@ -14,16 +15,21 @@ import (
|
||||||
"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"
|
||||||
"github.com/grafana/grafana/pkg/services/publicdashboards/validation"
|
"github.com/grafana/grafana/pkg/services/publicdashboards/validation"
|
||||||
|
"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/intervalv2"
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb/legacydata"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Define the Service Implementation. We're generating mock implementation
|
// Define the Service Implementation. We're generating mock implementation
|
||||||
// automatically
|
// automatically
|
||||||
type PublicDashboardServiceImpl struct {
|
type PublicDashboardServiceImpl struct {
|
||||||
log log.Logger
|
log log.Logger
|
||||||
cfg *setting.Cfg
|
cfg *setting.Cfg
|
||||||
store publicdashboards.Store
|
store publicdashboards.Store
|
||||||
|
intervalCalculator intervalv2.Calculator
|
||||||
|
QueryDataService *query.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
var LogPrefix = "publicdashboards.service"
|
var LogPrefix = "publicdashboards.service"
|
||||||
|
|
@ -37,11 +43,14 @@ var _ publicdashboards.Service = (*PublicDashboardServiceImpl)(nil)
|
||||||
func ProvideService(
|
func ProvideService(
|
||||||
cfg *setting.Cfg,
|
cfg *setting.Cfg,
|
||||||
store publicdashboards.Store,
|
store publicdashboards.Store,
|
||||||
|
qds *query.Service,
|
||||||
) *PublicDashboardServiceImpl {
|
) *PublicDashboardServiceImpl {
|
||||||
return &PublicDashboardServiceImpl{
|
return &PublicDashboardServiceImpl{
|
||||||
log: log.New(LogPrefix),
|
log: log.New(LogPrefix),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
store: store,
|
store: store,
|
||||||
|
intervalCalculator: intervalv2.NewCalculator(),
|
||||||
|
QueryDataService: qds,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,25 +189,58 @@ func (pd *PublicDashboardServiceImpl) updatePublicDashboardConfig(ctx context.Co
|
||||||
return dto.PublicDashboard.Uid, pd.store.UpdatePublicDashboardConfig(ctx, cmd)
|
return dto.PublicDashboard.Uid, pd.store.UpdatePublicDashboardConfig(ctx, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildPublicDashboardMetricRequest merges public dashboard parameters with
|
func (pd *PublicDashboardServiceImpl) GetQueryDataResponse(ctx context.Context, skipCache bool, reqDTO *PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error) {
|
||||||
// dashboard and returns a metrics request to be sent to query backend
|
if err := validation.ValidateQueryPublicDashboardRequest(reqDTO); err != nil {
|
||||||
func (pd *PublicDashboardServiceImpl) BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64) (dtos.MetricRequest, error) {
|
return nil, ErrPublicDashboardBadRequest
|
||||||
if !publicDashboard.IsEnabled {
|
|
||||||
return dtos.MetricRequest{}, ErrPublicDashboardNotFound
|
|
||||||
}
|
}
|
||||||
|
|
||||||
queriesByPanel := models.GroupQueriesByPanelId(dashboard.Data)
|
publicDashboard, dashboard, err := pd.GetPublicDashboard(ctx, accessToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if _, ok := queriesByPanel[panelId]; !ok {
|
metricReqDTO, err := pd.buildPublicDashboardMetricRequest(
|
||||||
|
ctx,
|
||||||
|
dashboard,
|
||||||
|
publicDashboard,
|
||||||
|
panelId,
|
||||||
|
reqDTO,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
anonymousUser, err := pd.BuildAnonymousUser(ctx, dashboard)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pd.QueryDataService.QueryDataMultipleSources(ctx, anonymousUser, skipCache, metricReqDTO, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildPublicDashboardMetricRequest merges public dashboard parameters with
|
||||||
|
// dashboard and returns a metrics request to be sent to query backend
|
||||||
|
func (pd *PublicDashboardServiceImpl) buildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64, reqDTO *PublicDashboardQueryDTO) (dtos.MetricRequest, error) {
|
||||||
|
// group queries by panel
|
||||||
|
queriesByPanel := models.GroupQueriesByPanelId(dashboard.Data)
|
||||||
|
queries, ok := queriesByPanel[panelId]
|
||||||
|
if !ok {
|
||||||
return dtos.MetricRequest{}, ErrPublicDashboardPanelNotFound
|
return dtos.MetricRequest{}, ErrPublicDashboardPanelNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
ts := publicDashboard.BuildTimeSettings(dashboard)
|
ts := publicDashboard.BuildTimeSettings(dashboard)
|
||||||
|
|
||||||
|
// determine safe resolution to query data at
|
||||||
|
safeInterval, safeResolution := pd.getSafeIntervalAndMaxDataPoints(reqDTO, ts)
|
||||||
|
for i := range queries {
|
||||||
|
queries[i].Set("intervalMs", safeInterval)
|
||||||
|
queries[i].Set("maxDataPoints", safeResolution)
|
||||||
|
}
|
||||||
|
|
||||||
return dtos.MetricRequest{
|
return dtos.MetricRequest{
|
||||||
From: ts.From,
|
From: ts.From,
|
||||||
To: ts.To,
|
To: ts.To,
|
||||||
Queries: queriesByPanel[panelId],
|
Queries: queries,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,6 +282,35 @@ func GenerateAccessToken() (string, error) {
|
||||||
return fmt.Sprintf("%x", token[:]), nil
|
return fmt.Sprintf("%x", token[:]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// intervalMS and maxQueryData values are being calculated on the frontend for regular dashboards
|
||||||
|
// we are doing the same for public dashboards but because this access would be public, we need a way to keep this
|
||||||
|
// values inside reasonable bounds to avoid an attack that could hit data sources with a small interval and a big
|
||||||
|
// time range and perform big calculations
|
||||||
|
// this is an additional validation, all data sources implements QueryData interface and should have proper validations
|
||||||
|
// of these limits
|
||||||
|
// for the maxDataPoints we took a hard limit from prometheus which is 11000
|
||||||
|
func (pd *PublicDashboardServiceImpl) getSafeIntervalAndMaxDataPoints(reqDTO *PublicDashboardQueryDTO, ts *TimeSettings) (int64, int64) {
|
||||||
|
// arbitrary max value for all data sources, it is actually a hard limit defined in prometheus
|
||||||
|
safeResolution := int64(11000)
|
||||||
|
|
||||||
|
// interval calculated on the frontend
|
||||||
|
interval := time.Duration(reqDTO.IntervalMs) * time.Millisecond
|
||||||
|
|
||||||
|
// calculate a safe interval with time range from dashboard and safeResolution
|
||||||
|
dataTimeRange := legacydata.NewDataTimeRange(ts.From, ts.To)
|
||||||
|
tr := backend.TimeRange{
|
||||||
|
From: dataTimeRange.GetFromAsTimeUTC(),
|
||||||
|
To: dataTimeRange.GetToAsTimeUTC(),
|
||||||
|
}
|
||||||
|
safeInterval := pd.intervalCalculator.CalculateSafeInterval(tr, safeResolution)
|
||||||
|
|
||||||
|
if interval > safeInterval.Value {
|
||||||
|
return reqDTO.IntervalMs, reqDTO.MaxDataPoints
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeInterval.Value.Milliseconds(), safeResolution
|
||||||
|
}
|
||||||
|
|
||||||
// Log when PublicDashboard.IsEnabled changed
|
// Log when PublicDashboard.IsEnabled changed
|
||||||
func (pd *PublicDashboardServiceImpl) logIsEnabledChanged(existingPubdash *PublicDashboard, newPubdash *PublicDashboard, u *user.SignedInUser) {
|
func (pd *PublicDashboardServiceImpl) logIsEnabledChanged(existingPubdash *PublicDashboard, newPubdash *PublicDashboard, u *user.SignedInUser) {
|
||||||
if publicDashboardIsEnabledChanged(existingPubdash, newPubdash) {
|
if publicDashboardIsEnabledChanged(existingPubdash, newPubdash) {
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,12 @@ import (
|
||||||
database "github.com/grafana/grafana/pkg/services/publicdashboards/database"
|
database "github.com/grafana/grafana/pkg/services/publicdashboards/database"
|
||||||
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb/intervalv2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var timeSettings, _ = simplejson.NewJson([]byte(`{"from": "now-12", "to": "now"}`))
|
var timeSettings, _ = simplejson.NewJson([]byte(`{"from": "now-12h", "to": "now"}`))
|
||||||
var defaultPubdashTimeSettings, _ = simplejson.NewJson([]byte(`{}`))
|
var defaultPubdashTimeSettings, _ = simplejson.NewJson([]byte(`{}`))
|
||||||
var dashboardData = simplejson.NewFromAny(map[string]interface{}{"time": map[string]interface{}{"from": "now-8", "to": "now"}})
|
var dashboardData = simplejson.NewFromAny(map[string]interface{}{"time": map[string]interface{}{"from": "now-8h", "to": "now"}})
|
||||||
var SignedInUser = &user.SignedInUser{UserID: 1234, Login: "user@login.com"}
|
var SignedInUser = &user.SignedInUser{UserID: 1234, Login: "user@login.com"}
|
||||||
|
|
||||||
func TestLogPrefix(t *testing.T) {
|
func TestLogPrefix(t *testing.T) {
|
||||||
|
|
@ -360,8 +361,14 @@ func TestBuildPublicDashboardMetricRequest(t *testing.T) {
|
||||||
nonPublicDashboard := insertTestDashboard(t, dashboardStore, "testNonPublicDashie", 1, 0, true, []map[string]interface{}{})
|
nonPublicDashboard := insertTestDashboard(t, dashboardStore, "testNonPublicDashie", 1, 0, true, []map[string]interface{}{})
|
||||||
|
|
||||||
service := &PublicDashboardServiceImpl{
|
service := &PublicDashboardServiceImpl{
|
||||||
log: log.New("test.logger"),
|
log: log.New("test.logger"),
|
||||||
store: publicdashboardStore,
|
store: publicdashboardStore,
|
||||||
|
intervalCalculator: intervalv2.NewCalculator(),
|
||||||
|
}
|
||||||
|
|
||||||
|
publicDashboardQueryDTO := &PublicDashboardQueryDTO{
|
||||||
|
IntervalMs: int64(10000000),
|
||||||
|
MaxDataPoints: int64(200),
|
||||||
}
|
}
|
||||||
|
|
||||||
dto := &SavePublicDashboardConfigDTO{
|
dto := &SavePublicDashboardConfigDTO{
|
||||||
|
|
@ -389,65 +396,69 @@ func TestBuildPublicDashboardMetricRequest(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
nonPublicDashboardPD, err := service.SavePublicDashboardConfig(context.Background(), SignedInUser, nonPublicDto)
|
_, err = service.SavePublicDashboardConfig(context.Background(), SignedInUser, nonPublicDto)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("extracts queries from provided dashboard", func(t *testing.T) {
|
t.Run("extracts queries from provided dashboard", func(t *testing.T) {
|
||||||
reqDTO, err := service.BuildPublicDashboardMetricRequest(
|
reqDTO, err := service.buildPublicDashboardMetricRequest(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
publicDashboard,
|
publicDashboard,
|
||||||
publicDashboardPD,
|
publicDashboardPD,
|
||||||
1,
|
1,
|
||||||
|
publicDashboardQueryDTO,
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, timeSettings.Get("from").MustString(), reqDTO.From)
|
require.Equal(t, timeSettings.Get("from").MustString(), reqDTO.From)
|
||||||
require.Equal(t, timeSettings.Get("to").MustString(), reqDTO.To)
|
require.Equal(t, timeSettings.Get("to").MustString(), reqDTO.To)
|
||||||
|
|
||||||
|
for i := range reqDTO.Queries {
|
||||||
|
require.Equal(t, publicDashboardQueryDTO.IntervalMs, reqDTO.Queries[i].Get("intervalMs").MustInt64())
|
||||||
|
require.Equal(t, publicDashboardQueryDTO.MaxDataPoints, reqDTO.Queries[i].Get("maxDataPoints").MustInt64())
|
||||||
|
}
|
||||||
|
|
||||||
require.Len(t, reqDTO.Queries, 2)
|
require.Len(t, reqDTO.Queries, 2)
|
||||||
|
|
||||||
require.Equal(
|
require.Equal(
|
||||||
t,
|
t,
|
||||||
simplejson.MustJson([]byte(`{
|
simplejson.NewFromAny(map[string]interface{}{
|
||||||
"datasource": {
|
"datasource": map[string]interface{}{
|
||||||
"type": "mysql",
|
"type": "mysql",
|
||||||
"uid": "ds1"
|
"uid": "ds1",
|
||||||
},
|
},
|
||||||
"refId": "A"
|
"intervalMs": int64(10000000),
|
||||||
}`)),
|
"maxDataPoints": int64(200),
|
||||||
|
"refId": "A",
|
||||||
|
}),
|
||||||
reqDTO.Queries[0],
|
reqDTO.Queries[0],
|
||||||
)
|
)
|
||||||
|
|
||||||
require.Equal(
|
require.Equal(
|
||||||
t,
|
t,
|
||||||
simplejson.MustJson([]byte(`{
|
simplejson.NewFromAny(map[string]interface{}{
|
||||||
"datasource": {
|
"datasource": map[string]interface{}{
|
||||||
"type": "prometheus",
|
"type": "prometheus",
|
||||||
"uid": "ds2"
|
"uid": "ds2",
|
||||||
},
|
},
|
||||||
"refId": "B"
|
"intervalMs": int64(10000000),
|
||||||
}`)),
|
"maxDataPoints": int64(200),
|
||||||
|
"refId": "B",
|
||||||
|
}),
|
||||||
reqDTO.Queries[1],
|
reqDTO.Queries[1],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("returns an error when panel missing", func(t *testing.T) {
|
t.Run("returns an error when panel missing", func(t *testing.T) {
|
||||||
_, err := service.BuildPublicDashboardMetricRequest(
|
_, err := service.buildPublicDashboardMetricRequest(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
publicDashboard,
|
publicDashboard,
|
||||||
publicDashboardPD,
|
publicDashboardPD,
|
||||||
49,
|
49,
|
||||||
|
publicDashboardQueryDTO,
|
||||||
)
|
)
|
||||||
|
|
||||||
require.ErrorContains(t, err, "Panel not found")
|
require.ErrorContains(t, err, "Panel not found")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("returns an error when dashboard not public", func(t *testing.T) {
|
|
||||||
_, err := service.BuildPublicDashboardMetricRequest(
|
|
||||||
context.Background(),
|
|
||||||
nonPublicDashboard,
|
|
||||||
nonPublicDashboardPD,
|
|
||||||
2,
|
|
||||||
)
|
|
||||||
require.ErrorContains(t, err, "Public dashboard not found")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64,
|
func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64,
|
||||||
|
|
@ -513,6 +524,87 @@ func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardSto
|
||||||
return dash
|
return dash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPublicDashboardServiceImpl_getSafeIntervalAndMaxDataPoints(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
reqDTO *PublicDashboardQueryDTO
|
||||||
|
ts *TimeSettings
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantSafeInterval int64
|
||||||
|
wantSafeMaxDataPoints int64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "return original interval",
|
||||||
|
args: args{
|
||||||
|
reqDTO: &PublicDashboardQueryDTO{
|
||||||
|
IntervalMs: 10000,
|
||||||
|
MaxDataPoints: 300,
|
||||||
|
},
|
||||||
|
ts: &TimeSettings{
|
||||||
|
From: "now-3h",
|
||||||
|
To: "now",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantSafeInterval: 10000,
|
||||||
|
wantSafeMaxDataPoints: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "return safe interval because of a small interval",
|
||||||
|
args: args{
|
||||||
|
reqDTO: &PublicDashboardQueryDTO{
|
||||||
|
IntervalMs: 1000,
|
||||||
|
MaxDataPoints: 300,
|
||||||
|
},
|
||||||
|
ts: &TimeSettings{
|
||||||
|
From: "now-6h",
|
||||||
|
To: "now",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantSafeInterval: 2000,
|
||||||
|
wantSafeMaxDataPoints: 11000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "return safe interval for long time range",
|
||||||
|
args: args{
|
||||||
|
reqDTO: &PublicDashboardQueryDTO{
|
||||||
|
IntervalMs: 100,
|
||||||
|
MaxDataPoints: 300,
|
||||||
|
},
|
||||||
|
ts: &TimeSettings{
|
||||||
|
From: "now-90d",
|
||||||
|
To: "now",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantSafeInterval: 600000,
|
||||||
|
wantSafeMaxDataPoints: 11000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "return safe interval when reqDTO is empty",
|
||||||
|
args: args{
|
||||||
|
reqDTO: &PublicDashboardQueryDTO{},
|
||||||
|
ts: &TimeSettings{
|
||||||
|
From: "now-90d",
|
||||||
|
To: "now",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantSafeInterval: 600000,
|
||||||
|
wantSafeMaxDataPoints: 11000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
pd := &PublicDashboardServiceImpl{
|
||||||
|
intervalCalculator: intervalv2.NewCalculator(),
|
||||||
|
}
|
||||||
|
got, got1 := pd.getSafeIntervalAndMaxDataPoints(tt.args.reqDTO, tt.args.ts)
|
||||||
|
assert.Equalf(t, tt.wantSafeInterval, got, "getSafeIntervalAndMaxDataPoints(%v, %v)", tt.args.reqDTO, tt.args.ts)
|
||||||
|
assert.Equalf(t, tt.wantSafeMaxDataPoints, got1, "getSafeIntervalAndMaxDataPoints(%v, %v)", tt.args.reqDTO, tt.args.ts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDashboardEnabledChanged(t *testing.T) {
|
func TestDashboardEnabledChanged(t *testing.T) {
|
||||||
t.Run("created isEnabled: false", func(t *testing.T) {
|
t.Run("created isEnabled: false", func(t *testing.T) {
|
||||||
assert.False(t, publicDashboardIsEnabledChanged(nil, &PublicDashboard{IsEnabled: false}))
|
assert.False(t, publicDashboardIsEnabledChanged(nil, &PublicDashboard{IsEnabled: false}))
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package validation
|
package validation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
publicDashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
publicDashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||||
)
|
)
|
||||||
|
|
@ -18,3 +20,15 @@ func hasTemplateVariables(dashboard *models.Dashboard) bool {
|
||||||
|
|
||||||
return len(templateVariables) > 0
|
return len(templateVariables) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ValidateQueryPublicDashboardRequest(req *publicDashboardModels.PublicDashboardQueryDTO) error {
|
||||||
|
if req.IntervalMs < 0 {
|
||||||
|
return fmt.Errorf("intervalMS should be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.MaxDataPoints < 0 {
|
||||||
|
return fmt.Errorf("maxDataPoints should be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@ func roundInterval(interval time.Duration) time.Duration {
|
||||||
// 12.5m
|
// 12.5m
|
||||||
case interval <= 750000*time.Millisecond:
|
case interval <= 750000*time.Millisecond:
|
||||||
return time.Millisecond * 600000 // 10m
|
return time.Millisecond * 600000 // 10m
|
||||||
// 12.5m
|
// 17.5m
|
||||||
case interval <= 1050000*time.Millisecond:
|
case interval <= 1050000*time.Millisecond:
|
||||||
return time.Millisecond * 900000 // 15m
|
return time.Millisecond * 900000 // 15m
|
||||||
// 25m
|
// 25m
|
||||||
|
|
|
||||||
|
|
@ -58,30 +58,15 @@ export class PublicDashboardDataSource extends DataSourceApi<any> {
|
||||||
* Ideally final -- any other implementation may not work as expected
|
* Ideally final -- any other implementation may not work as expected
|
||||||
*/
|
*/
|
||||||
query(request: DataQueryRequest<any>): Observable<DataQueryResponse> {
|
query(request: DataQueryRequest<any>): Observable<DataQueryResponse> {
|
||||||
const { intervalMs, maxDataPoints, range, requestId, publicDashboardAccessToken, panelId } = request;
|
const { intervalMs, maxDataPoints, requestId, publicDashboardAccessToken, panelId } = request;
|
||||||
let targets = request.targets;
|
let queries: DataQuery[];
|
||||||
|
|
||||||
const queries = targets.map((q) => {
|
|
||||||
return {
|
|
||||||
...q,
|
|
||||||
publicDashboardAccessToken,
|
|
||||||
intervalMs,
|
|
||||||
maxDataPoints,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return early if no queries exist
|
// Return early if no queries exist
|
||||||
if (!queries.length) {
|
if (!request.targets.length) {
|
||||||
return of({ data: [] });
|
return of({ data: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const body: any = { queries, publicDashboardAccessToken, panelId };
|
const body: any = { intervalMs, maxDataPoints };
|
||||||
|
|
||||||
if (range) {
|
|
||||||
body.range = range;
|
|
||||||
body.from = range.from.valueOf().toString();
|
|
||||||
body.to = range.to.valueOf().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return getBackendSrv()
|
return getBackendSrv()
|
||||||
.fetch<BackendDataSourceResponse>({
|
.fetch<BackendDataSourceResponse>({
|
||||||
|
|
@ -92,7 +77,7 @@ export class PublicDashboardDataSource extends DataSourceApi<any> {
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((raw) => {
|
switchMap((raw) => {
|
||||||
return of(toDataQueryResponse(raw, queries as DataQuery[]));
|
return of(toDataQueryResponse(raw, queries));
|
||||||
}),
|
}),
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
return of(toDataQueryResponse(err));
|
return of(toDataQueryResponse(err));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue