mirror of https://github.com/grafana/grafana
public dashboards: move into into its own service (#51358)
This PR moves public dashboards into its own self contained service including API, Service, Database, and Models. Routes are mounted on the Grafana HTTPServer by the API service at injection time with wire.go. The main route that loads the frontend for public dashboards is still handled by the API package. Co-authored-by: Jesse Weaver <jesse.weaver@grafana.com> Co-authored-by: Owen Smallwood <owen.smallwood@grafana.com>pull/51358/merge
parent
ba2d8cd838
commit
eacee08135
@ -1,141 +0,0 @@ |
||||
package api |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"net/http" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos" |
||||
"github.com/grafana/grafana/pkg/api/response" |
||||
"github.com/grafana/grafana/pkg/models" |
||||
"github.com/grafana/grafana/pkg/services/dashboards" |
||||
"github.com/grafana/grafana/pkg/services/datasources" |
||||
"github.com/grafana/grafana/pkg/web" |
||||
) |
||||
|
||||
// gets public dashboard
|
||||
func (hs *HTTPServer) GetPublicDashboard(c *models.ReqContext) response.Response { |
||||
accessToken := web.Params(c.Req)[":accessToken"] |
||||
|
||||
dash, err := hs.DashboardService.GetPublicDashboard(c.Req.Context(), accessToken) |
||||
if err != nil { |
||||
return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard", err) |
||||
} |
||||
|
||||
meta := dtos.DashboardMeta{ |
||||
Slug: dash.Slug, |
||||
Type: models.DashTypeDB, |
||||
CanStar: false, |
||||
CanSave: false, |
||||
CanEdit: false, |
||||
CanAdmin: false, |
||||
CanDelete: false, |
||||
Created: dash.Created, |
||||
Updated: dash.Updated, |
||||
Version: dash.Version, |
||||
IsFolder: false, |
||||
FolderId: dash.FolderId, |
||||
PublicDashboardAccessToken: accessToken, |
||||
} |
||||
|
||||
dto := dtos.DashboardFullWithMeta{Meta: meta, Dashboard: dash.Data} |
||||
|
||||
return response.JSON(http.StatusOK, dto) |
||||
} |
||||
|
||||
// gets public dashboard configuration for dashboard
|
||||
func (hs *HTTPServer) GetPublicDashboardConfig(c *models.ReqContext) response.Response { |
||||
pdc, err := hs.DashboardService.GetPublicDashboardConfig(c.Req.Context(), c.OrgId, web.Params(c.Req)[":uid"]) |
||||
if err != nil { |
||||
return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard config", err) |
||||
} |
||||
return response.JSON(http.StatusOK, pdc) |
||||
} |
||||
|
||||
// sets public dashboard configuration for dashboard
|
||||
func (hs *HTTPServer) SavePublicDashboardConfig(c *models.ReqContext) response.Response { |
||||
pubdash := &models.PublicDashboard{} |
||||
if err := web.Bind(c.Req, pubdash); err != nil { |
||||
return response.Error(http.StatusBadRequest, "bad request data", err) |
||||
} |
||||
|
||||
// Always set the org id to the current auth session orgId
|
||||
pubdash.OrgId = c.OrgId |
||||
|
||||
dto := dashboards.SavePublicDashboardConfigDTO{ |
||||
OrgId: c.OrgId, |
||||
DashboardUid: web.Params(c.Req)[":uid"], |
||||
UserId: c.UserId, |
||||
PublicDashboard: pubdash, |
||||
} |
||||
|
||||
pubdash, err := hs.DashboardService.SavePublicDashboardConfig(c.Req.Context(), &dto) |
||||
if err != nil { |
||||
return handleDashboardErr(http.StatusInternalServerError, "Failed to save public dashboard configuration", err) |
||||
} |
||||
|
||||
return response.JSON(http.StatusOK, pubdash) |
||||
} |
||||
|
||||
// QueryPublicDashboard returns all results for a given panel on a public dashboard
|
||||
// POST /api/public/dashboard/:accessToken/panels/:panelId/query
|
||||
func (hs *HTTPServer) QueryPublicDashboard(c *models.ReqContext) response.Response { |
||||
panelId, err := strconv.ParseInt(web.Params(c.Req)[":panelId"], 10, 64) |
||||
if err != nil { |
||||
return response.Error(http.StatusBadRequest, "invalid panel ID", err) |
||||
} |
||||
|
||||
dashboard, err := hs.DashboardService.GetPublicDashboard(c.Req.Context(), web.Params(c.Req)[":accessToken"]) |
||||
if err != nil { |
||||
return response.Error(http.StatusInternalServerError, "could not fetch dashboard", err) |
||||
} |
||||
|
||||
publicDashboard, err := hs.DashboardService.GetPublicDashboardConfig(c.Req.Context(), dashboard.OrgId, dashboard.Uid) |
||||
if err != nil { |
||||
return response.Error(http.StatusInternalServerError, "could not fetch public dashboard", err) |
||||
} |
||||
|
||||
reqDTO, err := hs.DashboardService.BuildPublicDashboardMetricRequest( |
||||
c.Req.Context(), |
||||
dashboard, |
||||
publicDashboard, |
||||
panelId, |
||||
) |
||||
if err != nil { |
||||
return handleDashboardErr(http.StatusInternalServerError, "Failed to get queries for public dashboard", err) |
||||
} |
||||
|
||||
// Get all needed datasource UIDs from queries
|
||||
var uids []string |
||||
for _, query := range reqDTO.Queries { |
||||
uids = append(uids, query.Get("datasource").Get("uid").MustString()) |
||||
} |
||||
|
||||
// Create a temp user with read-only datasource permissions
|
||||
anonymousUser := &models.SignedInUser{OrgId: dashboard.OrgId, Permissions: make(map[int64]map[string][]string)} |
||||
permissions := make(map[string][]string) |
||||
datasourceScope := fmt.Sprintf("datasources:uid:%s", strings.Join(uids, ",")) |
||||
permissions[datasources.ActionQuery] = []string{datasourceScope} |
||||
permissions[datasources.ActionRead] = []string{datasourceScope} |
||||
anonymousUser.Permissions[dashboard.OrgId] = permissions |
||||
|
||||
resp, err := hs.queryDataService.QueryDataMultipleSources(c.Req.Context(), anonymousUser, c.SkipCache, reqDTO, true) |
||||
|
||||
if err != nil { |
||||
return hs.handleQueryMetricsError(err) |
||||
} |
||||
return hs.toJsonStreamingResponse(resp) |
||||
} |
||||
|
||||
// util to help us unpack a dashboard err or use default http code and message
|
||||
func handleDashboardErr(defaultCode int, defaultMsg string, err error) response.Response { |
||||
var dashboardErr dashboards.DashboardErr |
||||
|
||||
if ok := errors.As(err, &dashboardErr); ok { |
||||
return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), dashboardErr) |
||||
} |
||||
|
||||
return response.Error(defaultCode, defaultMsg, err) |
||||
} |
@ -1,630 +0,0 @@ |
||||
package api |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"net/http" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/google/uuid" |
||||
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/mock" |
||||
"github.com/stretchr/testify/require" |
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos" |
||||
"github.com/grafana/grafana/pkg/components/simplejson" |
||||
"github.com/grafana/grafana/pkg/infra/localcache" |
||||
"github.com/grafana/grafana/pkg/models" |
||||
"github.com/grafana/grafana/pkg/services/dashboards" |
||||
"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/featuremgmt" |
||||
"github.com/grafana/grafana/pkg/services/query" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
"github.com/grafana/grafana/pkg/web/webtest" |
||||
) |
||||
|
||||
func TestAPIGetPublicDashboard(t *testing.T) { |
||||
t.Run("It should 404 if featureflag is not enabled", func(t *testing.T) { |
||||
sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures()) |
||||
dashSvc := dashboards.NewFakeDashboardService(t) |
||||
dashSvc.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")). |
||||
Return(&models.Dashboard{}, nil).Maybe() |
||||
sc.hs.DashboardService = dashSvc |
||||
|
||||
setInitCtxSignedInViewer(sc.initCtx) |
||||
response := callAPI( |
||||
sc.server, |
||||
http.MethodGet, |
||||
"/api/public/dashboards", |
||||
nil, |
||||
t, |
||||
) |
||||
assert.Equal(t, http.StatusNotFound, response.Code) |
||||
response = callAPI( |
||||
sc.server, |
||||
http.MethodGet, |
||||
"/api/public/dashboards/asdf", |
||||
nil, |
||||
t, |
||||
) |
||||
assert.Equal(t, http.StatusNotFound, response.Code) |
||||
}) |
||||
|
||||
DashboardUid := "dashboard-abcd1234" |
||||
token, err := uuid.NewRandom() |
||||
require.NoError(t, err) |
||||
accessToken := fmt.Sprintf("%x", token) |
||||
|
||||
testCases := []struct { |
||||
Name string |
||||
AccessToken string |
||||
ExpectedHttpResponse int |
||||
publicDashboardResult *models.Dashboard |
||||
publicDashboardErr error |
||||
}{ |
||||
{ |
||||
Name: "It gets a public dashboard", |
||||
AccessToken: accessToken, |
||||
ExpectedHttpResponse: http.StatusOK, |
||||
publicDashboardResult: &models.Dashboard{ |
||||
Data: simplejson.NewFromAny(map[string]interface{}{ |
||||
"Uid": DashboardUid, |
||||
}), |
||||
}, |
||||
publicDashboardErr: nil, |
||||
}, |
||||
{ |
||||
Name: "It should return 404 if isPublicDashboard is false", |
||||
AccessToken: accessToken, |
||||
ExpectedHttpResponse: http.StatusNotFound, |
||||
publicDashboardResult: nil, |
||||
publicDashboardErr: dashboards.ErrPublicDashboardNotFound, |
||||
}, |
||||
} |
||||
|
||||
for _, test := range testCases { |
||||
t.Run(test.Name, func(t *testing.T) { |
||||
sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)) |
||||
dashSvc := dashboards.NewFakeDashboardService(t) |
||||
dashSvc.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")). |
||||
Return(test.publicDashboardResult, test.publicDashboardErr) |
||||
sc.hs.DashboardService = dashSvc |
||||
|
||||
setInitCtxSignedInViewer(sc.initCtx) |
||||
response := callAPI( |
||||
sc.server, |
||||
http.MethodGet, |
||||
fmt.Sprintf("/api/public/dashboards/%s", test.AccessToken), |
||||
nil, |
||||
t, |
||||
) |
||||
|
||||
assert.Equal(t, test.ExpectedHttpResponse, response.Code) |
||||
|
||||
if test.publicDashboardErr == nil { |
||||
var dashResp dtos.DashboardFullWithMeta |
||||
err := json.Unmarshal(response.Body.Bytes(), &dashResp) |
||||
require.NoError(t, err) |
||||
|
||||
assert.Equal(t, DashboardUid, dashResp.Dashboard.Get("Uid").MustString()) |
||||
assert.Equal(t, false, dashResp.Meta.CanEdit) |
||||
assert.Equal(t, false, dashResp.Meta.CanDelete) |
||||
assert.Equal(t, false, dashResp.Meta.CanSave) |
||||
} else { |
||||
var errResp struct { |
||||
Error string `json:"error"` |
||||
} |
||||
err := json.Unmarshal(response.Body.Bytes(), &errResp) |
||||
require.NoError(t, err) |
||||
assert.Equal(t, test.publicDashboardErr.Error(), errResp.Error) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestAPIGetPublicDashboardConfig(t *testing.T) { |
||||
pubdash := &models.PublicDashboard{IsEnabled: true} |
||||
|
||||
testCases := []struct { |
||||
Name string |
||||
DashboardUid string |
||||
ExpectedHttpResponse int |
||||
PublicDashboardResult *models.PublicDashboard |
||||
PublicDashboardError error |
||||
}{ |
||||
{ |
||||
Name: "retrieves public dashboard config when dashboard is found", |
||||
DashboardUid: "1", |
||||
ExpectedHttpResponse: http.StatusOK, |
||||
PublicDashboardResult: pubdash, |
||||
PublicDashboardError: nil, |
||||
}, |
||||
{ |
||||
Name: "returns 404 when dashboard not found", |
||||
DashboardUid: "77777", |
||||
ExpectedHttpResponse: http.StatusNotFound, |
||||
PublicDashboardResult: nil, |
||||
PublicDashboardError: dashboards.ErrDashboardNotFound, |
||||
}, |
||||
{ |
||||
Name: "returns 500 when internal server error", |
||||
DashboardUid: "1", |
||||
ExpectedHttpResponse: http.StatusInternalServerError, |
||||
PublicDashboardResult: nil, |
||||
PublicDashboardError: errors.New("database broken"), |
||||
}, |
||||
} |
||||
|
||||
for _, test := range testCases { |
||||
t.Run(test.Name, func(t *testing.T) { |
||||
sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)) |
||||
dashSvc := dashboards.NewFakeDashboardService(t) |
||||
dashSvc.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")). |
||||
Return(test.PublicDashboardResult, test.PublicDashboardError) |
||||
sc.hs.DashboardService = dashSvc |
||||
|
||||
setInitCtxSignedInViewer(sc.initCtx) |
||||
response := callAPI( |
||||
sc.server, |
||||
http.MethodGet, |
||||
"/api/dashboards/uid/1/public-config", |
||||
nil, |
||||
t, |
||||
) |
||||
|
||||
assert.Equal(t, test.ExpectedHttpResponse, response.Code) |
||||
|
||||
if response.Code == http.StatusOK { |
||||
var pdcResp models.PublicDashboard |
||||
err := json.Unmarshal(response.Body.Bytes(), &pdcResp) |
||||
require.NoError(t, err) |
||||
assert.Equal(t, test.PublicDashboardResult, &pdcResp) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestApiSavePublicDashboardConfig(t *testing.T) { |
||||
testCases := []struct { |
||||
Name string |
||||
DashboardUid string |
||||
publicDashboardConfig *models.PublicDashboard |
||||
ExpectedHttpResponse int |
||||
saveDashboardError error |
||||
}{ |
||||
{ |
||||
Name: "returns 200 when update persists", |
||||
DashboardUid: "1", |
||||
publicDashboardConfig: &models.PublicDashboard{IsEnabled: true}, |
||||
ExpectedHttpResponse: http.StatusOK, |
||||
saveDashboardError: nil, |
||||
}, |
||||
{ |
||||
Name: "returns 500 when not persisted", |
||||
ExpectedHttpResponse: http.StatusInternalServerError, |
||||
publicDashboardConfig: &models.PublicDashboard{}, |
||||
saveDashboardError: errors.New("backend failed to save"), |
||||
}, |
||||
{ |
||||
Name: "returns 404 when dashboard not found", |
||||
ExpectedHttpResponse: http.StatusNotFound, |
||||
publicDashboardConfig: &models.PublicDashboard{}, |
||||
saveDashboardError: dashboards.ErrDashboardNotFound, |
||||
}, |
||||
} |
||||
|
||||
for _, test := range testCases { |
||||
t.Run(test.Name, func(t *testing.T) { |
||||
sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)) |
||||
|
||||
dashSvc := dashboards.NewFakeDashboardService(t) |
||||
dashSvc.On("SavePublicDashboardConfig", mock.Anything, mock.AnythingOfType("*dashboards.SavePublicDashboardConfigDTO")). |
||||
Return(&models.PublicDashboard{IsEnabled: true}, test.saveDashboardError) |
||||
sc.hs.DashboardService = dashSvc |
||||
|
||||
setInitCtxSignedInViewer(sc.initCtx) |
||||
response := callAPI( |
||||
sc.server, |
||||
http.MethodPost, |
||||
"/api/dashboards/uid/1/public-config", |
||||
strings.NewReader(`{ "isPublic": true }`), |
||||
t, |
||||
) |
||||
|
||||
assert.Equal(t, test.ExpectedHttpResponse, response.Code) |
||||
|
||||
// check the result if it's a 200
|
||||
if response.Code == http.StatusOK { |
||||
val, err := json.Marshal(test.publicDashboardConfig) |
||||
require.NoError(t, err) |
||||
assert.Equal(t, string(val), response.Body.String()) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
// `/public/dashboards/:uid/query`` endpoint test
|
||||
func TestAPIQueryPublicDashboard(t *testing.T) { |
||||
queryReturnsError := false |
||||
|
||||
qds := query.ProvideService( |
||||
nil, |
||||
&fakeDatasources.FakeCacheService{ |
||||
DataSources: []*datasources.DataSource{ |
||||
{Uid: "mysqlds"}, |
||||
{Uid: "promds"}, |
||||
{Uid: "promds2"}, |
||||
}, |
||||
}, |
||||
nil, |
||||
&fakePluginRequestValidator{}, |
||||
&fakeDatasources.FakeDataSourceService{}, |
||||
&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, |
||||
}, |
||||
}, |
||||
} |
||||
} |
||||
return &backend.QueryDataResponse{Responses: resp}, nil |
||||
}, |
||||
}, |
||||
&fakeOAuthTokenService{}, |
||||
) |
||||
|
||||
setup := func(enabled bool) (*webtest.Server, *dashboards.FakeDashboardService) { |
||||
fakeDashboardService := &dashboards.FakeDashboardService{} |
||||
|
||||
return SetupAPITestServer(t, func(hs *HTTPServer) { |
||||
hs.queryDataService = qds |
||||
hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards, enabled) |
||||
hs.DashboardService = fakeDashboardService |
||||
}), fakeDashboardService |
||||
} |
||||
|
||||
t.Run("Status code is 404 when feature toggle is disabled", func(t *testing.T) { |
||||
server, _ := setup(false) |
||||
|
||||
req := server.NewPostRequest( |
||||
"/api/public/dashboards/abc123/panels/2/query", |
||||
strings.NewReader("{}"), |
||||
) |
||||
resp, err := server.SendJSON(req) |
||||
require.NoError(t, err) |
||||
require.NoError(t, resp.Body.Close()) |
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode) |
||||
}) |
||||
|
||||
t.Run("Status code is 400 when the panel ID is invalid", func(t *testing.T) { |
||||
server, _ := setup(true) |
||||
|
||||
req := server.NewPostRequest( |
||||
"/api/public/dashboards/abc123/panels/notanumber/query", |
||||
strings.NewReader("{}"), |
||||
) |
||||
resp, err := server.SendJSON(req) |
||||
require.NoError(t, err) |
||||
require.NoError(t, resp.Body.Close()) |
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode) |
||||
}) |
||||
|
||||
t.Run("Returns query data when feature toggle is enabled", func(t *testing.T) { |
||||
server, fakeDashboardService := setup(true) |
||||
|
||||
fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil) |
||||
fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&models.PublicDashboard{}, 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) |
||||
req := server.NewPostRequest( |
||||
"/api/public/dashboards/abc123/panels/2/query", |
||||
strings.NewReader("{}"), |
||||
) |
||||
resp, err := server.SendJSON(req) |
||||
require.NoError(t, err) |
||||
bodyBytes, err := ioutil.ReadAll(resp.Body) |
||||
require.NoError(t, err) |
||||
require.JSONEq( |
||||
t, |
||||
`{ |
||||
"results": { |
||||
"A": { |
||||
"frames": [ |
||||
{ |
||||
"data": { |
||||
"values": [] |
||||
}, |
||||
"schema": { |
||||
"fields": [], |
||||
"refId": "A", |
||||
"name": "query-A" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
} |
||||
}`, |
||||
string(bodyBytes), |
||||
) |
||||
require.NoError(t, resp.Body.Close()) |
||||
require.Equal(t, http.StatusOK, resp.StatusCode) |
||||
}) |
||||
|
||||
t.Run("Status code is 500 when the query fails", func(t *testing.T) { |
||||
server, fakeDashboardService := setup(true) |
||||
|
||||
fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil) |
||||
fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&models.PublicDashboard{}, 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) |
||||
req := server.NewPostRequest( |
||||
"/api/public/dashboards/abc123/panels/2/query", |
||||
strings.NewReader("{}"), |
||||
) |
||||
queryReturnsError = true |
||||
resp, err := server.SendJSON(req) |
||||
require.NoError(t, err) |
||||
require.NoError(t, resp.Body.Close()) |
||||
require.Equal(t, http.StatusInternalServerError, resp.StatusCode) |
||||
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(&models.Dashboard{}, nil) |
||||
fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&models.PublicDashboard{}, 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) |
||||
req := server.NewPostRequest( |
||||
"/api/public/dashboards/abc123/panels/2/query", |
||||
strings.NewReader("{}"), |
||||
) |
||||
resp, err := server.SendJSON(req) |
||||
require.NoError(t, err) |
||||
bodyBytes, err := ioutil.ReadAll(resp.Body) |
||||
require.NoError(t, err) |
||||
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" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
} |
||||
}`, |
||||
string(bodyBytes), |
||||
) |
||||
require.NoError(t, resp.Body.Close()) |
||||
require.Equal(t, http.StatusOK, resp.StatusCode) |
||||
}) |
||||
} |
||||
|
||||
func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) { |
||||
config := setting.NewCfg() |
||||
db := sqlstore.InitTestDB(t) |
||||
scenario := setupHTTPServerWithCfgDb(t, false, false, config, db, db, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)) |
||||
scenario.initCtx.SkipCache = true |
||||
cacheService := service.ProvideCacheService(localcache.ProvideService(), db) |
||||
qds := query.ProvideService( |
||||
nil, |
||||
cacheService, |
||||
nil, |
||||
&fakePluginRequestValidator{}, |
||||
&fakeDatasources.FakeDataSourceService{}, |
||||
&fakePluginClient{ |
||||
QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { |
||||
resp := backend.Responses{ |
||||
"A": backend.DataResponse{ |
||||
Frames: []*data.Frame{{}}, |
||||
}, |
||||
} |
||||
return &backend.QueryDataResponse{Responses: resp}, nil |
||||
}, |
||||
}, |
||||
&fakeOAuthTokenService{}, |
||||
) |
||||
scenario.hs.queryDataService = qds |
||||
|
||||
_ = db.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ |
||||
Uid: "ds1", |
||||
OrgId: 1, |
||||
Name: "laban", |
||||
Type: datasources.DS_MYSQL, |
||||
Access: datasources.DS_ACCESS_DIRECT, |
||||
Url: "http://test", |
||||
Database: "site", |
||||
ReadOnly: true, |
||||
}) |
||||
|
||||
// Create Dashboard
|
||||
saveDashboardCmd := models.SaveDashboardCommand{ |
||||
OrgId: 1, |
||||
FolderId: 1, |
||||
IsFolder: false, |
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{ |
||||
"id": nil, |
||||
"title": "test", |
||||
"panels": []map[string]interface{}{ |
||||
{ |
||||
"id": 1, |
||||
"targets": []map[string]interface{}{ |
||||
{ |
||||
"datasource": map[string]string{ |
||||
"type": "mysql", |
||||
"uid": "ds1", |
||||
}, |
||||
"refId": "A", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}), |
||||
} |
||||
dashboard, _ := scenario.dashboardsStore.SaveDashboard(saveDashboardCmd) |
||||
|
||||
// Create public dashboard
|
||||
savePubDashboardCmd := &dashboards.SavePublicDashboardConfigDTO{ |
||||
DashboardUid: dashboard.Uid, |
||||
OrgId: dashboard.OrgId, |
||||
PublicDashboard: &models.PublicDashboard{ |
||||
IsEnabled: true, |
||||
}, |
||||
} |
||||
|
||||
pubdash, err := scenario.hs.DashboardService.SavePublicDashboardConfig(context.Background(), savePubDashboardCmd) |
||||
require.NoError(t, err) |
||||
|
||||
response := callAPI( |
||||
scenario.server, |
||||
http.MethodPost, |
||||
fmt.Sprintf("/api/public/dashboards/%s/panels/1/query", pubdash.AccessToken), |
||||
strings.NewReader(`{}`), |
||||
t, |
||||
) |
||||
|
||||
require.Equal(t, http.StatusOK, response.Code) |
||||
bodyBytes, err := ioutil.ReadAll(response.Body) |
||||
require.NoError(t, err) |
||||
require.JSONEq( |
||||
t, |
||||
`{ |
||||
"results": { |
||||
"A": { |
||||
"frames": [ |
||||
{ |
||||
"data": { |
||||
"values": [] |
||||
}, |
||||
"schema": { |
||||
"fields": [] |
||||
} |
||||
} |
||||
] |
||||
} |
||||
} |
||||
}`, |
||||
string(bodyBytes), |
||||
) |
||||
} |
@ -0,0 +1,234 @@ |
||||
package api |
||||
|
||||
import ( |
||||
"errors" |
||||
"net/http" |
||||
"strconv" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
"github.com/grafana/grafana/pkg/api/dtos" |
||||
"github.com/grafana/grafana/pkg/api/response" |
||||
"github.com/grafana/grafana/pkg/api/routing" |
||||
"github.com/grafana/grafana/pkg/middleware" |
||||
"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/dashboards" |
||||
"github.com/grafana/grafana/pkg/services/datasources" |
||||
"github.com/grafana/grafana/pkg/services/featuremgmt" |
||||
"github.com/grafana/grafana/pkg/services/publicdashboards" |
||||
. "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/web" |
||||
) |
||||
|
||||
type Api struct { |
||||
PublicDashboardService publicdashboards.Service |
||||
RouteRegister routing.RouteRegister |
||||
AccessControl accesscontrol.AccessControl |
||||
QueryDataService *query.Service |
||||
Features *featuremgmt.FeatureManager |
||||
} |
||||
|
||||
func ProvideApi( |
||||
pd publicdashboards.Service, |
||||
rr routing.RouteRegister, |
||||
ac accesscontrol.AccessControl, |
||||
qds *query.Service, |
||||
features *featuremgmt.FeatureManager, |
||||
) *Api { |
||||
api := &Api{ |
||||
PublicDashboardService: pd, |
||||
RouteRegister: rr, |
||||
AccessControl: ac, |
||||
QueryDataService: qds, |
||||
Features: features, |
||||
} |
||||
|
||||
// attach api if PublicDashboards feature flag is enabled
|
||||
if features.IsEnabled(featuremgmt.FlagPublicDashboards) { |
||||
api.RegisterAPIEndpoints() |
||||
} |
||||
|
||||
return api |
||||
} |
||||
|
||||
func (api *Api) RegisterAPIEndpoints() { |
||||
auth := accesscontrol.Middleware(api.AccessControl) |
||||
reqSignedIn := middleware.ReqSignedIn |
||||
|
||||
// Anonymous access to public dashboard route is configured in pkg/api/api.go
|
||||
// because it is deeply dependent on the HTTPServer.Index() method and would result in a
|
||||
// circular dependency
|
||||
|
||||
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)) |
||||
|
||||
// Create/Update Public Dashboard
|
||||
api.RouteRegister.Get("/api/dashboards/uid/:uid/public-config", auth(reqSignedIn, accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(api.GetPublicDashboardConfig)) |
||||
api.RouteRegister.Post("/api/dashboards/uid/:uid/public-config", auth(reqSignedIn, accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(api.SavePublicDashboardConfig)) |
||||
} |
||||
|
||||
// gets public dashboard
|
||||
func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response { |
||||
accessToken := web.Params(c.Req)[":accessToken"] |
||||
|
||||
dash, err := api.PublicDashboardService.GetPublicDashboard(c.Req.Context(), accessToken) |
||||
if err != nil { |
||||
return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard", err) |
||||
} |
||||
|
||||
meta := dtos.DashboardMeta{ |
||||
Slug: dash.Slug, |
||||
Type: models.DashTypeDB, |
||||
CanStar: false, |
||||
CanSave: false, |
||||
CanEdit: false, |
||||
CanAdmin: false, |
||||
CanDelete: false, |
||||
Created: dash.Created, |
||||
Updated: dash.Updated, |
||||
Version: dash.Version, |
||||
IsFolder: false, |
||||
FolderId: dash.FolderId, |
||||
PublicDashboardAccessToken: accessToken, |
||||
} |
||||
|
||||
dto := dtos.DashboardFullWithMeta{Meta: meta, Dashboard: dash.Data} |
||||
|
||||
return response.JSON(http.StatusOK, dto) |
||||
} |
||||
|
||||
// gets public dashboard configuration for dashboard
|
||||
func (api *Api) GetPublicDashboardConfig(c *models.ReqContext) response.Response { |
||||
pdc, err := api.PublicDashboardService.GetPublicDashboardConfig(c.Req.Context(), c.OrgId, web.Params(c.Req)[":uid"]) |
||||
if err != nil { |
||||
return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard config", err) |
||||
} |
||||
return response.JSON(http.StatusOK, pdc) |
||||
} |
||||
|
||||
// sets public dashboard configuration for dashboard
|
||||
func (api *Api) SavePublicDashboardConfig(c *models.ReqContext) response.Response { |
||||
pubdash := &PublicDashboard{} |
||||
if err := web.Bind(c.Req, pubdash); err != nil { |
||||
return response.Error(http.StatusBadRequest, "bad request data", err) |
||||
} |
||||
|
||||
// Always set the org id to the current auth session orgId
|
||||
pubdash.OrgId = c.OrgId |
||||
|
||||
dto := SavePublicDashboardConfigDTO{ |
||||
OrgId: c.OrgId, |
||||
DashboardUid: web.Params(c.Req)[":uid"], |
||||
UserId: c.UserId, |
||||
PublicDashboard: pubdash, |
||||
} |
||||
|
||||
pubdash, err := api.PublicDashboardService.SavePublicDashboardConfig(c.Req.Context(), &dto) |
||||
if err != nil { |
||||
return handleDashboardErr(http.StatusInternalServerError, "Failed to save public dashboard configuration", err) |
||||
} |
||||
|
||||
return response.JSON(http.StatusOK, pubdash) |
||||
} |
||||
|
||||
// QueryPublicDashboard returns all results for a given panel on a public dashboard
|
||||
// POST /api/public/dashboard/:accessToken/panels/:panelId/query
|
||||
func (api *Api) QueryPublicDashboard(c *models.ReqContext) response.Response { |
||||
panelId, err := strconv.ParseInt(web.Params(c.Req)[":panelId"], 10, 64) |
||||
if err != nil { |
||||
return response.Error(http.StatusBadRequest, "invalid panel ID", err) |
||||
} |
||||
|
||||
dashboard, err := api.PublicDashboardService.GetPublicDashboard(c.Req.Context(), web.Params(c.Req)[":accessToken"]) |
||||
if err != nil { |
||||
return response.Error(http.StatusInternalServerError, "could not fetch dashboard", err) |
||||
} |
||||
|
||||
publicDashboard, err := api.PublicDashboardService.GetPublicDashboardConfig(c.Req.Context(), dashboard.OrgId, dashboard.Uid) |
||||
if err != nil { |
||||
return response.Error(http.StatusInternalServerError, "could not fetch public dashboard", err) |
||||
} |
||||
|
||||
reqDTO, err := api.PublicDashboardService.BuildPublicDashboardMetricRequest( |
||||
c.Req.Context(), |
||||
dashboard, |
||||
publicDashboard, |
||||
panelId, |
||||
) |
||||
if err != nil { |
||||
return handleDashboardErr(http.StatusInternalServerError, "Failed to get queries for public dashboard", err) |
||||
} |
||||
|
||||
anonymousUser, err := api.PublicDashboardService.BuildAnonymousUser(c.Req.Context(), dashboard) |
||||
|
||||
if err != nil { |
||||
return response.Error(http.StatusInternalServerError, "could not create anonymous user", err) |
||||
} |
||||
|
||||
resp, err := api.QueryDataService.QueryDataMultipleSources(c.Req.Context(), anonymousUser, c.SkipCache, reqDTO, true) |
||||
|
||||
if err != nil { |
||||
return handleQueryMetricsError(err) |
||||
} |
||||
return toJsonStreamingResponse(api.Features, resp) |
||||
} |
||||
|
||||
// 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
|
||||
// different package.
|
||||
func handleDashboardErr(defaultCode int, defaultMsg string, err error) response.Response { |
||||
var publicDashboardErr PublicDashboardErr |
||||
|
||||
// handle public dashboard er
|
||||
if ok := errors.As(err, &publicDashboardErr); ok { |
||||
return response.Error(publicDashboardErr.StatusCode, publicDashboardErr.Error(), publicDashboardErr) |
||||
} |
||||
|
||||
// handle dashboard errors as well
|
||||
var dashboardErr dashboards.DashboardErr |
||||
if ok := errors.As(err, &dashboardErr); ok { |
||||
return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), dashboardErr) |
||||
} |
||||
|
||||
return response.Error(defaultCode, defaultMsg, err) |
||||
} |
||||
|
||||
// Copied from pkg/api/metrics.go
|
||||
func handleQueryMetricsError(err error) *response.NormalResponse { |
||||
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
|
||||
func toJsonStreamingResponse(features *featuremgmt.FeatureManager, qdr *backend.QueryDataResponse) response.Response { |
||||
statusWhenError := http.StatusBadRequest |
||||
if features.IsEnabled(featuremgmt.FlagDatasourceQueryMultiStatus) { |
||||
statusWhenError = http.StatusMultiStatus |
||||
} |
||||
|
||||
statusCode := http.StatusOK |
||||
for _, res := range qdr.Responses { |
||||
if res.Error != nil { |
||||
statusCode = statusWhenError |
||||
} |
||||
} |
||||
|
||||
return response.JSONStreaming(statusCode, qdr) |
||||
} |
@ -0,0 +1,589 @@ |
||||
package api |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"net/http" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/google/uuid" |
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/mock" |
||||
"github.com/stretchr/testify/require" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
"github.com/grafana/grafana/pkg/api/dtos" |
||||
"github.com/grafana/grafana/pkg/components/simplejson" |
||||
"github.com/grafana/grafana/pkg/infra/localcache" |
||||
"github.com/grafana/grafana/pkg/models" |
||||
"github.com/grafana/grafana/pkg/services/dashboards" |
||||
dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database" |
||||
"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/featuremgmt" |
||||
"github.com/grafana/grafana/pkg/services/publicdashboards" |
||||
publicdashboardsStore "github.com/grafana/grafana/pkg/services/publicdashboards/database" |
||||
. "github.com/grafana/grafana/pkg/services/publicdashboards/models" |
||||
publicdashboardsService "github.com/grafana/grafana/pkg/services/publicdashboards/service" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
"github.com/grafana/grafana/pkg/web" |
||||
) |
||||
|
||||
func TestAPIGetPublicDashboard(t *testing.T) { |
||||
t.Run("It should 404 if featureflag is not enabled", func(t *testing.T) { |
||||
cfg := setting.NewCfg() |
||||
qs := buildQueryDataService(t, nil, nil, nil) |
||||
service := publicdashboards.NewFakePublicDashboardService(t) |
||||
service.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")). |
||||
Return(&models.Dashboard{}, nil).Maybe() |
||||
|
||||
testServer := setupTestServer(t, cfg, qs, featuremgmt.WithFeatures(), service, nil) |
||||
|
||||
response := callAPI(testServer, http.MethodGet, "/api/public/dashboards", nil, t) |
||||
assert.Equal(t, http.StatusNotFound, response.Code) |
||||
|
||||
response = callAPI(testServer, http.MethodGet, "/api/public/dashboards/asdf", nil, t) |
||||
assert.Equal(t, http.StatusNotFound, response.Code) |
||||
|
||||
// control set. make sure routes are mounted
|
||||
testServer = setupTestServer(t, cfg, qs, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), service, nil) |
||||
response = callAPI(testServer, http.MethodGet, "/api/public/dashboards/asdf", nil, t) |
||||
assert.NotEqual(t, http.StatusNotFound, response.Code) |
||||
}) |
||||
|
||||
DashboardUid := "dashboard-abcd1234" |
||||
token, err := uuid.NewRandom() |
||||
require.NoError(t, err) |
||||
accessToken := fmt.Sprintf("%x", token) |
||||
|
||||
testCases := []struct { |
||||
Name string |
||||
AccessToken string |
||||
ExpectedHttpResponse int |
||||
PublicDashboardResult *models.Dashboard |
||||
PublicDashboardErr error |
||||
}{ |
||||
{ |
||||
Name: "It gets a public dashboard", |
||||
AccessToken: accessToken, |
||||
ExpectedHttpResponse: http.StatusOK, |
||||
PublicDashboardResult: &models.Dashboard{ |
||||
Data: simplejson.NewFromAny(map[string]interface{}{ |
||||
"Uid": DashboardUid, |
||||
}), |
||||
}, |
||||
PublicDashboardErr: nil, |
||||
}, |
||||
{ |
||||
Name: "It should return 404 if no public dashboard", |
||||
AccessToken: accessToken, |
||||
ExpectedHttpResponse: http.StatusNotFound, |
||||
PublicDashboardResult: nil, |
||||
PublicDashboardErr: ErrPublicDashboardNotFound, |
||||
}, |
||||
} |
||||
|
||||
for _, test := range testCases { |
||||
t.Run(test.Name, func(t *testing.T) { |
||||
service := publicdashboards.NewFakePublicDashboardService(t) |
||||
service.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")). |
||||
Return(test.PublicDashboardResult, test.PublicDashboardErr).Maybe() |
||||
|
||||
testServer := setupTestServer( |
||||
t, |
||||
setting.NewCfg(), |
||||
buildQueryDataService(t, nil, nil, nil), |
||||
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), |
||||
service, |
||||
nil, |
||||
) |
||||
|
||||
response := callAPI(testServer, http.MethodGet, |
||||
fmt.Sprintf("/api/public/dashboards/%s", test.AccessToken), |
||||
nil, |
||||
t, |
||||
) |
||||
|
||||
assert.Equal(t, test.ExpectedHttpResponse, response.Code) |
||||
|
||||
if test.PublicDashboardErr == nil { |
||||
var dashResp dtos.DashboardFullWithMeta |
||||
err := json.Unmarshal(response.Body.Bytes(), &dashResp) |
||||
require.NoError(t, err) |
||||
|
||||
assert.Equal(t, DashboardUid, dashResp.Dashboard.Get("Uid").MustString()) |
||||
assert.Equal(t, false, dashResp.Meta.CanEdit) |
||||
assert.Equal(t, false, dashResp.Meta.CanDelete) |
||||
assert.Equal(t, false, dashResp.Meta.CanSave) |
||||
} else { |
||||
var errResp struct { |
||||
Error string `json:"error"` |
||||
} |
||||
err := json.Unmarshal(response.Body.Bytes(), &errResp) |
||||
require.NoError(t, err) |
||||
assert.Equal(t, test.PublicDashboardErr.Error(), errResp.Error) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestAPIGetPublicDashboardConfig(t *testing.T) { |
||||
pubdash := &PublicDashboard{IsEnabled: true} |
||||
|
||||
testCases := []struct { |
||||
Name string |
||||
DashboardUid string |
||||
ExpectedHttpResponse int |
||||
PublicDashboardResult *PublicDashboard |
||||
PublicDashboardErr error |
||||
}{ |
||||
{ |
||||
Name: "retrieves public dashboard config when dashboard is found", |
||||
DashboardUid: "1", |
||||
ExpectedHttpResponse: http.StatusOK, |
||||
PublicDashboardResult: pubdash, |
||||
PublicDashboardErr: nil, |
||||
}, |
||||
{ |
||||
Name: "returns 404 when dashboard not found", |
||||
DashboardUid: "77777", |
||||
ExpectedHttpResponse: http.StatusNotFound, |
||||
PublicDashboardResult: nil, |
||||
PublicDashboardErr: dashboards.ErrDashboardNotFound, |
||||
}, |
||||
{ |
||||
Name: "returns 500 when internal server error", |
||||
DashboardUid: "1", |
||||
ExpectedHttpResponse: http.StatusInternalServerError, |
||||
PublicDashboardResult: nil, |
||||
PublicDashboardErr: errors.New("database broken"), |
||||
}, |
||||
} |
||||
|
||||
for _, test := range testCases { |
||||
t.Run(test.Name, func(t *testing.T) { |
||||
service := publicdashboards.NewFakePublicDashboardService(t) |
||||
service.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")). |
||||
Return(test.PublicDashboardResult, test.PublicDashboardErr) |
||||
|
||||
testServer := setupTestServer( |
||||
t, |
||||
setting.NewCfg(), |
||||
buildQueryDataService(t, nil, nil, nil), |
||||
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), |
||||
service, |
||||
nil, |
||||
) |
||||
|
||||
response := callAPI( |
||||
testServer, |
||||
http.MethodGet, |
||||
"/api/dashboards/uid/1/public-config", |
||||
nil, |
||||
t, |
||||
) |
||||
|
||||
assert.Equal(t, test.ExpectedHttpResponse, response.Code) |
||||
|
||||
if response.Code == http.StatusOK { |
||||
var pdcResp PublicDashboard |
||||
err := json.Unmarshal(response.Body.Bytes(), &pdcResp) |
||||
require.NoError(t, err) |
||||
assert.Equal(t, test.PublicDashboardResult, &pdcResp) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestApiSavePublicDashboardConfig(t *testing.T) { |
||||
testCases := []struct { |
||||
Name string |
||||
DashboardUid string |
||||
publicDashboardConfig *PublicDashboard |
||||
ExpectedHttpResponse int |
||||
SaveDashboardErr error |
||||
}{ |
||||
{ |
||||
Name: "returns 200 when update persists", |
||||
DashboardUid: "1", |
||||
publicDashboardConfig: &PublicDashboard{IsEnabled: true}, |
||||
ExpectedHttpResponse: http.StatusOK, |
||||
SaveDashboardErr: nil, |
||||
}, |
||||
{ |
||||
Name: "returns 500 when not persisted", |
||||
ExpectedHttpResponse: http.StatusInternalServerError, |
||||
publicDashboardConfig: &PublicDashboard{}, |
||||
SaveDashboardErr: errors.New("backend failed to save"), |
||||
}, |
||||
{ |
||||
Name: "returns 404 when dashboard not found", |
||||
ExpectedHttpResponse: http.StatusNotFound, |
||||
publicDashboardConfig: &PublicDashboard{}, |
||||
SaveDashboardErr: dashboards.ErrDashboardNotFound, |
||||
}, |
||||
} |
||||
|
||||
for _, test := range testCases { |
||||
t.Run(test.Name, func(t *testing.T) { |
||||
service := publicdashboards.NewFakePublicDashboardService(t) |
||||
service.On("SavePublicDashboardConfig", mock.Anything, mock.AnythingOfType("*models.SavePublicDashboardConfigDTO")). |
||||
Return(&PublicDashboard{IsEnabled: true}, test.SaveDashboardErr) |
||||
|
||||
testServer := setupTestServer( |
||||
t, |
||||
setting.NewCfg(), |
||||
buildQueryDataService(t, nil, nil, nil), |
||||
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), |
||||
service, |
||||
nil, |
||||
) |
||||
|
||||
response := callAPI( |
||||
testServer, |
||||
http.MethodPost, |
||||
"/api/dashboards/uid/1/public-config", |
||||
strings.NewReader(`{ "isPublic": true }`), |
||||
t, |
||||
) |
||||
|
||||
assert.Equal(t, test.ExpectedHttpResponse, response.Code) |
||||
|
||||
//check the result if it's a 200
|
||||
if response.Code == http.StatusOK { |
||||
val, err := json.Marshal(test.publicDashboardConfig) |
||||
require.NoError(t, err) |
||||
assert.Equal(t, string(val), response.Body.String()) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
// `/public/dashboards/:uid/query`` endpoint test
|
||||
func TestAPIQueryPublicDashboard(t *testing.T) { |
||||
cacheService := &fakeDatasources.FakeCacheService{ |
||||
DataSources: []*datasources.DataSource{ |
||||
{Uid: "mysqlds"}, |
||||
{Uid: "promds"}, |
||||
{Uid: "promds2"}, |
||||
}, |
||||
} |
||||
|
||||
// used to determine whether fakePluginClient returns an error
|
||||
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, |
||||
}, |
||||
}, |
||||
} |
||||
} |
||||
return &backend.QueryDataResponse{Responses: resp}, nil |
||||
}, |
||||
} |
||||
|
||||
qds := buildQueryDataService(t, cacheService, fakePluginClient, nil) |
||||
|
||||
setup := func(enabled bool) (*web.Mux, *publicdashboards.FakePublicDashboardService) { |
||||
service := publicdashboards.NewFakePublicDashboardService(t) |
||||
|
||||
testServer := setupTestServer( |
||||
t, |
||||
setting.NewCfg(), |
||||
qds, |
||||
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards, enabled), |
||||
service, |
||||
nil, |
||||
) |
||||
|
||||
return testServer, service |
||||
} |
||||
|
||||
t.Run("Status code is 404 when feature toggle is disabled", func(t *testing.T) { |
||||
server, _ := setup(false) |
||||
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t) |
||||
require.Equal(t, http.StatusNotFound, resp.Code) |
||||
}) |
||||
|
||||
t.Run("Status code is 400 when the panel ID is invalid", func(t *testing.T) { |
||||
server, _ := setup(true) |
||||
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/notanumber/query", strings.NewReader("{}"), t) |
||||
require.Equal(t, http.StatusBadRequest, resp.Code) |
||||
}) |
||||
|
||||
t.Run("Returns query data when feature toggle is enabled", func(t *testing.T) { |
||||
server, fakeDashboardService := setup(true) |
||||
|
||||
fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil) |
||||
fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&PublicDashboard{}, nil) |
||||
fakeDashboardService.On("BuildAnonymousUser", mock.Anything, mock.Anything, mock.Anything).Return(&models.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) |
||||
|
||||
require.JSONEq( |
||||
t, |
||||
`{ |
||||
"results": { |
||||
"A": { |
||||
"frames": [ |
||||
{ |
||||
"data": { |
||||
"values": [] |
||||
}, |
||||
"schema": { |
||||
"fields": [], |
||||
"refId": "A", |
||||
"name": "query-A" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
} |
||||
}`, |
||||
resp.Body.String(), |
||||
) |
||||
require.Equal(t, http.StatusOK, resp.Code) |
||||
}) |
||||
|
||||
t.Run("Status code is 500 when the query fails", func(t *testing.T) { |
||||
server, fakeDashboardService := setup(true) |
||||
|
||||
fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil) |
||||
fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&PublicDashboard{}, nil) |
||||
fakeDashboardService.On("BuildAnonymousUser", mock.Anything, mock.Anything, mock.Anything).Return(&models.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) |
||||
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(&models.Dashboard{}, nil) |
||||
fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&PublicDashboard{}, nil) |
||||
fakeDashboardService.On("BuildAnonymousUser", mock.Anything, mock.Anything, mock.Anything).Return(&models.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) |
||||
}) |
||||
} |
||||
|
||||
func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) { |
||||
db := sqlstore.InitTestDB(t) |
||||
|
||||
cacheService := service.ProvideCacheService(localcache.ProvideService(), db) |
||||
qds := buildQueryDataService(t, cacheService, nil, db) |
||||
|
||||
_ = db.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ |
||||
Uid: "ds1", |
||||
OrgId: 1, |
||||
Name: "laban", |
||||
Type: datasources.DS_MYSQL, |
||||
Access: datasources.DS_ACCESS_DIRECT, |
||||
Url: "http://test", |
||||
Database: "site", |
||||
ReadOnly: true, |
||||
}) |
||||
|
||||
// Create Dashboard
|
||||
saveDashboardCmd := models.SaveDashboardCommand{ |
||||
OrgId: 1, |
||||
FolderId: 1, |
||||
IsFolder: false, |
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{ |
||||
"id": nil, |
||||
"title": "test", |
||||
"panels": []map[string]interface{}{ |
||||
{ |
||||
"id": 1, |
||||
"targets": []map[string]interface{}{ |
||||
{ |
||||
"datasource": map[string]string{ |
||||
"type": "mysql", |
||||
"uid": "ds1", |
||||
}, |
||||
"refId": "A", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}), |
||||
} |
||||
|
||||
// create dashboard
|
||||
dashboardStore := dashboardStore.ProvideDashboardStore(db) |
||||
dashboard, err := dashboardStore.SaveDashboard(saveDashboardCmd) |
||||
require.NoError(t, err) |
||||
|
||||
// Create public dashboard
|
||||
savePubDashboardCmd := &SavePublicDashboardConfigDTO{ |
||||
DashboardUid: dashboard.Uid, |
||||
OrgId: dashboard.OrgId, |
||||
PublicDashboard: &PublicDashboard{ |
||||
IsEnabled: true, |
||||
}, |
||||
} |
||||
|
||||
// create public dashboard
|
||||
store := publicdashboardsStore.ProvideStore(db) |
||||
service := publicdashboardsService.ProvideService(setting.NewCfg(), store) |
||||
pubdash, err := service.SavePublicDashboardConfig(context.Background(), savePubDashboardCmd) |
||||
require.NoError(t, err) |
||||
|
||||
// setup test server
|
||||
server := setupTestServer(t, |
||||
setting.NewCfg(), |
||||
qds, |
||||
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), |
||||
service, |
||||
db, |
||||
) |
||||
|
||||
resp := callAPI(server, http.MethodPost, |
||||
fmt.Sprintf("/api/public/dashboards/%s/panels/1/query", pubdash.AccessToken), |
||||
strings.NewReader(`{}`), |
||||
t, |
||||
) |
||||
require.Equal(t, http.StatusOK, resp.Code) |
||||
require.NoError(t, err) |
||||
require.JSONEq( |
||||
t, |
||||
`{ |
||||
"results": { |
||||
"A": { |
||||
"frames": [ |
||||
{ |
||||
"data": { |
||||
"values": [] |
||||
}, |
||||
"schema": { |
||||
"fields": [] |
||||
} |
||||
} |
||||
] |
||||
} |
||||
} |
||||
}`, |
||||
resp.Body.String(), |
||||
) |
||||
} |
@ -0,0 +1,174 @@ |
||||
package api |
||||
|
||||
import ( |
||||
"context" |
||||
"io" |
||||
"net/http" |
||||
"net/http/httptest" |
||||
"testing" |
||||
|
||||
"golang.org/x/oauth2" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
"github.com/grafana/grafana/pkg/api/routing" |
||||
"github.com/grafana/grafana/pkg/infra/localcache" |
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/models" |
||||
"github.com/grafana/grafana/pkg/plugins" |
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/database" |
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" |
||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" |
||||
"github.com/grafana/grafana/pkg/services/datasources" |
||||
"github.com/grafana/grafana/pkg/services/featuremgmt" |
||||
"github.com/grafana/grafana/pkg/services/publicdashboards" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
|
||||
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" |
||||
datasourceService "github.com/grafana/grafana/pkg/services/datasources/service" |
||||
"github.com/grafana/grafana/pkg/services/query" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
"github.com/grafana/grafana/pkg/web" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
type Server struct { |
||||
Mux *web.Mux |
||||
RouteRegister routing.RouteRegister |
||||
TestServer *httptest.Server |
||||
} |
||||
|
||||
func setupTestServer( |
||||
t *testing.T, |
||||
cfg *setting.Cfg, |
||||
qs *query.Service, |
||||
features *featuremgmt.FeatureManager, |
||||
service publicdashboards.Service, |
||||
db *sqlstore.SQLStore, |
||||
) *web.Mux { |
||||
// build router to register routes
|
||||
rr := routing.NewRouteRegister() |
||||
|
||||
// build access control - FIXME we should be able to mock this, but to get
|
||||
// tests going, we're going to instantiate full accesscontrol
|
||||
//ac := accesscontrolmock.New()
|
||||
//ac.WithDisabled()
|
||||
|
||||
// create a sqlstore for access control.
|
||||
if db == nil { |
||||
db = sqlstore.InitTestDB(t) |
||||
} |
||||
|
||||
var err error |
||||
ac, err := ossaccesscontrol.ProvideService(features, cfg, database.ProvideService(db), rr) |
||||
require.NoError(t, err) |
||||
|
||||
// build mux
|
||||
m := web.New() |
||||
|
||||
// set initial context
|
||||
m.Use(func(c *web.Context) { |
||||
ctx := &models.ReqContext{ |
||||
Context: c, |
||||
IsSignedIn: true, // FIXME need to be able to change this for tests
|
||||
SkipCache: true, // hardcoded to make sure query service doesnt hit the cache
|
||||
Logger: log.New("publicdashboards-test"), |
||||
|
||||
// Set signed in user. We might not actually need to do this.
|
||||
SignedInUser: &models.SignedInUser{UserId: 1, OrgId: 1, OrgRole: models.ROLE_ADMIN, Login: "testUser"}, |
||||
} |
||||
c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), ctx)) |
||||
}) |
||||
|
||||
// build api, this will mount the routes at the same time if
|
||||
// featuremgmt.FlagPublicDashboard is enabled
|
||||
ProvideApi(service, rr, ac, qs, features) |
||||
|
||||
// connect routes to mux
|
||||
rr.Register(m.Router) |
||||
|
||||
return m |
||||
} |
||||
|
||||
func callAPI(server *web.Mux, method, path string, body io.Reader, t *testing.T) *httptest.ResponseRecorder { |
||||
req, err := http.NewRequest(method, path, body) |
||||
require.NoError(t, err) |
||||
req.Header.Set("Content-Type", "application/json") |
||||
recorder := httptest.NewRecorder() |
||||
server.ServeHTTP(recorder, req) |
||||
return recorder |
||||
} |
||||
|
||||
// helper to query.Service
|
||||
// allows us to stub the cache and plugin clients
|
||||
func buildQueryDataService(t *testing.T, cs datasources.CacheService, fpc *fakePluginClient, store *sqlstore.SQLStore) *query.Service { |
||||
// build database if we need one
|
||||
if store == nil { |
||||
store = sqlstore.InitTestDB(t) |
||||
} |
||||
|
||||
// default cache service
|
||||
if cs == nil { |
||||
cs = datasourceService.ProvideCacheService(localcache.ProvideService(), store) |
||||
} |
||||
|
||||
// default fakePluginClient
|
||||
if fpc == nil { |
||||
fpc = &fakePluginClient{ |
||||
QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { |
||||
resp := backend.Responses{ |
||||
"A": backend.DataResponse{ |
||||
Frames: []*data.Frame{{}}, |
||||
}, |
||||
} |
||||
return &backend.QueryDataResponse{Responses: resp}, nil |
||||
}, |
||||
} |
||||
} |
||||
|
||||
return query.ProvideService( |
||||
nil, |
||||
cs, |
||||
nil, |
||||
&fakePluginRequestValidator{}, |
||||
&fakeDatasources.FakeDataSourceService{}, |
||||
fpc, |
||||
&fakeOAuthTokenService{}, |
||||
) |
||||
} |
||||
|
||||
//copied from pkg/api/metrics_test.go
|
||||
type fakePluginRequestValidator struct { |
||||
err error |
||||
} |
||||
|
||||
func (rv *fakePluginRequestValidator) Validate(dsURL string, req *http.Request) error { |
||||
return rv.err |
||||
} |
||||
|
||||
type fakeOAuthTokenService struct { |
||||
passThruEnabled bool |
||||
token *oauth2.Token |
||||
} |
||||
|
||||
func (ts *fakeOAuthTokenService) GetCurrentOAuthToken(context.Context, *models.SignedInUser) *oauth2.Token { |
||||
return ts.token |
||||
} |
||||
|
||||
func (ts *fakeOAuthTokenService) IsOAuthPassThruEnabled(*datasources.DataSource) bool { |
||||
return ts.passThruEnabled |
||||
} |
||||
|
||||
// copied from pkg/api/plugins_test.go
|
||||
type fakePluginClient struct { |
||||
plugins.Client |
||||
backend.QueryDataHandlerFunc |
||||
} |
||||
|
||||
func (c *fakePluginClient) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { |
||||
if c.QueryDataHandlerFunc != nil { |
||||
return c.QueryDataHandlerFunc.QueryData(ctx, req) |
||||
} |
||||
|
||||
return backend.NewQueryDataResponse(), nil |
||||
} |
@ -1,8 +1,6 @@ |
||||
package middleware |
||||
package api |
||||
|
||||
import ( |
||||
"github.com/grafana/grafana/pkg/models" |
||||
) |
||||
import "github.com/grafana/grafana/pkg/models" |
||||
|
||||
func SetPublicDashboardFlag() func(c *models.ReqContext) { |
||||
return func(c *models.ReqContext) { |
@ -0,0 +1,144 @@ |
||||
// Code generated by mockery v2.12.2. DO NOT EDIT.
|
||||
|
||||
package publicdashboards |
||||
|
||||
import ( |
||||
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" |
||||
) |
||||
|
||||
// FakePublicDashboardService is an autogenerated mock type for the Service type
|
||||
type FakePublicDashboardService struct { |
||||
mock.Mock |
||||
} |
||||
|
||||
// BuildAnonymousUser provides a mock function with given fields: ctx, dashboard
|
||||
func (_m *FakePublicDashboardService) BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*models.SignedInUser, error) { |
||||
ret := _m.Called(ctx, dashboard) |
||||
|
||||
var r0 *models.SignedInUser |
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.Dashboard) *models.SignedInUser); ok { |
||||
r0 = rf(ctx, dashboard) |
||||
} else { |
||||
if ret.Get(0) != nil { |
||||
r0 = ret.Get(0).(*models.SignedInUser) |
||||
} |
||||
} |
||||
|
||||
var r1 error |
||||
if rf, ok := ret.Get(1).(func(context.Context, *models.Dashboard) error); ok { |
||||
r1 = rf(ctx, dashboard) |
||||
} else { |
||||
r1 = ret.Error(1) |
||||
} |
||||
|
||||
return r0, r1 |
||||
} |
||||
|
||||
// BuildPublicDashboardMetricRequest provides a mock function with given fields: ctx, dashboard, publicDashboard, panelId
|
||||
func (_m *FakePublicDashboardService) BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *publicdashboardsmodels.PublicDashboard, panelId int64) (dtos.MetricRequest, error) { |
||||
ret := _m.Called(ctx, dashboard, publicDashboard, panelId) |
||||
|
||||
var r0 dtos.MetricRequest |
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.Dashboard, *publicdashboardsmodels.PublicDashboard, int64) dtos.MetricRequest); ok { |
||||
r0 = rf(ctx, dashboard, publicDashboard, panelId) |
||||
} else { |
||||
r0 = ret.Get(0).(dtos.MetricRequest) |
||||
} |
||||
|
||||
var r1 error |
||||
if rf, ok := ret.Get(1).(func(context.Context, *models.Dashboard, *publicdashboardsmodels.PublicDashboard, int64) error); ok { |
||||
r1 = rf(ctx, dashboard, publicDashboard, panelId) |
||||
} else { |
||||
r1 = ret.Error(1) |
||||
} |
||||
|
||||
return r0, r1 |
||||
} |
||||
|
||||
// GetPublicDashboard provides a mock function with given fields: ctx, accessToken
|
||||
func (_m *FakePublicDashboardService) GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) { |
||||
ret := _m.Called(ctx, accessToken) |
||||
|
||||
var r0 *models.Dashboard |
||||
if rf, ok := ret.Get(0).(func(context.Context, string) *models.Dashboard); ok { |
||||
r0 = rf(ctx, accessToken) |
||||
} else { |
||||
if ret.Get(0) != nil { |
||||
r0 = ret.Get(0).(*models.Dashboard) |
||||
} |
||||
} |
||||
|
||||
var r1 error |
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { |
||||
r1 = rf(ctx, accessToken) |
||||
} else { |
||||
r1 = ret.Error(1) |
||||
} |
||||
|
||||
return r0, r1 |
||||
} |
||||
|
||||
// GetPublicDashboardConfig provides a mock function with given fields: ctx, orgId, dashboardUid
|
||||
func (_m *FakePublicDashboardService) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*publicdashboardsmodels.PublicDashboard, error) { |
||||
ret := _m.Called(ctx, orgId, dashboardUid) |
||||
|
||||
var r0 *publicdashboardsmodels.PublicDashboard |
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *publicdashboardsmodels.PublicDashboard); ok { |
||||
r0 = rf(ctx, orgId, dashboardUid) |
||||
} else { |
||||
if ret.Get(0) != nil { |
||||
r0 = ret.Get(0).(*publicdashboardsmodels.PublicDashboard) |
||||
} |
||||
} |
||||
|
||||
var r1 error |
||||
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok { |
||||
r1 = rf(ctx, orgId, dashboardUid) |
||||
} else { |
||||
r1 = ret.Error(1) |
||||
} |
||||
|
||||
return r0, r1 |
||||
} |
||||
|
||||
// SavePublicDashboardConfig provides a mock function with given fields: ctx, dto
|
||||
func (_m *FakePublicDashboardService) SavePublicDashboardConfig(ctx context.Context, dto *publicdashboardsmodels.SavePublicDashboardConfigDTO) (*publicdashboardsmodels.PublicDashboard, error) { |
||||
ret := _m.Called(ctx, dto) |
||||
|
||||
var r0 *publicdashboardsmodels.PublicDashboard |
||||
if rf, ok := ret.Get(0).(func(context.Context, *publicdashboardsmodels.SavePublicDashboardConfigDTO) *publicdashboardsmodels.PublicDashboard); ok { |
||||
r0 = rf(ctx, dto) |
||||
} else { |
||||
if ret.Get(0) != nil { |
||||
r0 = ret.Get(0).(*publicdashboardsmodels.PublicDashboard) |
||||
} |
||||
} |
||||
|
||||
var r1 error |
||||
if rf, ok := ret.Get(1).(func(context.Context, *publicdashboardsmodels.SavePublicDashboardConfigDTO) error); ok { |
||||
r1 = rf(ctx, dto) |
||||
} else { |
||||
r1 = ret.Error(1) |
||||
} |
||||
|
||||
return r0, r1 |
||||
} |
||||
|
||||
// NewFakePublicDashboardService creates a new instance of FakePublicDashboardService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewFakePublicDashboardService(t testing.TB) *FakePublicDashboardService { |
||||
mock := &FakePublicDashboardService{} |
||||
mock.Mock.Test(t) |
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) }) |
||||
|
||||
return mock |
||||
} |
@ -0,0 +1,142 @@ |
||||
// Code generated by mockery v2.12.2. DO NOT EDIT.
|
||||
|
||||
package publicdashboards |
||||
|
||||
import ( |
||||
context "context" |
||||
|
||||
models "github.com/grafana/grafana/pkg/services/publicdashboards/models" |
||||
mock "github.com/stretchr/testify/mock" |
||||
|
||||
pkgmodels "github.com/grafana/grafana/pkg/models" |
||||
|
||||
testing "testing" |
||||
) |
||||
|
||||
// FakePublicDashboardStore is an autogenerated mock type for the Store type
|
||||
type FakePublicDashboardStore struct { |
||||
mock.Mock |
||||
} |
||||
|
||||
// GenerateNewPublicDashboardUid provides a mock function with given fields: ctx
|
||||
func (_m *FakePublicDashboardStore) GenerateNewPublicDashboardUid(ctx context.Context) (string, error) { |
||||
ret := _m.Called(ctx) |
||||
|
||||
var r0 string |
||||
if rf, ok := ret.Get(0).(func(context.Context) string); ok { |
||||
r0 = rf(ctx) |
||||
} else { |
||||
r0 = ret.Get(0).(string) |
||||
} |
||||
|
||||
var r1 error |
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok { |
||||
r1 = rf(ctx) |
||||
} else { |
||||
r1 = ret.Error(1) |
||||
} |
||||
|
||||
return r0, r1 |
||||
} |
||||
|
||||
// GetPublicDashboard provides a mock function with given fields: ctx, accessToken
|
||||
func (_m *FakePublicDashboardStore) GetPublicDashboard(ctx context.Context, accessToken string) (*models.PublicDashboard, *pkgmodels.Dashboard, error) { |
||||
ret := _m.Called(ctx, accessToken) |
||||
|
||||
var r0 *models.PublicDashboard |
||||
if rf, ok := ret.Get(0).(func(context.Context, string) *models.PublicDashboard); ok { |
||||
r0 = rf(ctx, accessToken) |
||||
} else { |
||||
if ret.Get(0) != nil { |
||||
r0 = ret.Get(0).(*models.PublicDashboard) |
||||
} |
||||
} |
||||
|
||||
var r1 *pkgmodels.Dashboard |
||||
if rf, ok := ret.Get(1).(func(context.Context, string) *pkgmodels.Dashboard); ok { |
||||
r1 = rf(ctx, accessToken) |
||||
} else { |
||||
if ret.Get(1) != nil { |
||||
r1 = ret.Get(1).(*pkgmodels.Dashboard) |
||||
} |
||||
} |
||||
|
||||
var r2 error |
||||
if rf, ok := ret.Get(2).(func(context.Context, string) error); ok { |
||||
r2 = rf(ctx, accessToken) |
||||
} else { |
||||
r2 = ret.Error(2) |
||||
} |
||||
|
||||
return r0, r1, r2 |
||||
} |
||||
|
||||
// GetPublicDashboardConfig provides a mock function with given fields: ctx, orgId, dashboardUid
|
||||
func (_m *FakePublicDashboardStore) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) { |
||||
ret := _m.Called(ctx, orgId, dashboardUid) |
||||
|
||||
var r0 *models.PublicDashboard |
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *models.PublicDashboard); ok { |
||||
r0 = rf(ctx, orgId, dashboardUid) |
||||
} else { |
||||
if ret.Get(0) != nil { |
||||
r0 = ret.Get(0).(*models.PublicDashboard) |
||||
} |
||||
} |
||||
|
||||
var r1 error |
||||
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok { |
||||
r1 = rf(ctx, orgId, dashboardUid) |
||||
} else { |
||||
r1 = ret.Error(1) |
||||
} |
||||
|
||||
return r0, r1 |
||||
} |
||||
|
||||
// SavePublicDashboardConfig provides a mock function with given fields: ctx, cmd
|
||||
func (_m *FakePublicDashboardStore) SavePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboard, error) { |
||||
ret := _m.Called(ctx, cmd) |
||||
|
||||
var r0 *models.PublicDashboard |
||||
if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardConfigCommand) *models.PublicDashboard); ok { |
||||
r0 = rf(ctx, cmd) |
||||
} else { |
||||
if ret.Get(0) != nil { |
||||
r0 = ret.Get(0).(*models.PublicDashboard) |
||||
} |
||||
} |
||||
|
||||
var r1 error |
||||
if rf, ok := ret.Get(1).(func(context.Context, models.SavePublicDashboardConfigCommand) error); ok { |
||||
r1 = rf(ctx, cmd) |
||||
} else { |
||||
r1 = ret.Error(1) |
||||
} |
||||
|
||||
return r0, r1 |
||||
} |
||||
|
||||
// UpdatePublicDashboardConfig provides a mock function with given fields: ctx, cmd
|
||||
func (_m *FakePublicDashboardStore) UpdatePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) error { |
||||
ret := _m.Called(ctx, cmd) |
||||
|
||||
var r0 error |
||||
if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardConfigCommand) error); ok { |
||||
r0 = rf(ctx, cmd) |
||||
} else { |
||||
r0 = ret.Error(0) |
||||
} |
||||
|
||||
return r0 |
||||
} |
||||
|
||||
// NewFakePublicDashboardStore creates a new instance of FakePublicDashboardStore. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewFakePublicDashboardStore(t testing.TB) *FakePublicDashboardStore { |
||||
mock := &FakePublicDashboardStore{} |
||||
mock.Mock.Test(t) |
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) }) |
||||
|
||||
return mock |
||||
} |
@ -0,0 +1,29 @@ |
||||
package publicdashboards |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos" |
||||
"github.com/grafana/grafana/pkg/models" |
||||
. "github.com/grafana/grafana/pkg/services/publicdashboards/models" |
||||
) |
||||
|
||||
// These are the api contracts. The API should match the underlying service and store
|
||||
|
||||
//go:generate mockery --name Service --structname FakePublicDashboardService --inpackage --filename public_dashboard_service_mock.go
|
||||
type Service interface { |
||||
BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*models.SignedInUser, error) |
||||
GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) |
||||
GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) |
||||
SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error) |
||||
BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64) (dtos.MetricRequest, error) |
||||
} |
||||
|
||||
//go:generate mockery --name Store --structname FakePublicDashboardStore --inpackage --filename public_dashboard_store_mock.go
|
||||
type Store interface { |
||||
GetPublicDashboard(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error) |
||||
GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) |
||||
GenerateNewPublicDashboardUid(ctx context.Context) (string, error) |
||||
SavePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) (*PublicDashboard, error) |
||||
UpdatePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) error |
||||
} |
Loading…
Reference in new issue