public dashboards: finalize db schema & v1 feature complete (#50467)

This PR completes public dashboards v1 functionality and simplifies public dashboard conventions. It exists as a large PR so that we are not making constant changes to the database schema.

models.PublicDashboardConfig model replaced with models.PublicDashboard directly
dashboard_public_config table renamed to dashboard_public
models.Dashboard.IsPublic removed from the dashboard and replaced with models.PublicDashboard.isEnabled
Routing now uses a uuid v4 as an access token for viewing a public dashboard anonymously, PublicDashboard.Uid only used as database identifier
Frontend utilizes uuid for auth'd operations and access token for anonymous access
Default to time range defined on dashboard when viewing public dashboard
Add audit fields to public dashboard

Co-authored-by: Owen Smallwood <owen.smallwood@grafana.com>, Ezequiel Victorero <ezequiel.victorero@grafana.com>, Jesse Weaver <jesse.weaver@grafana.com>
This commit is contained in:
Jeff Levin
2022-06-22 13:58:52 -08:00
committed by GitHub
parent 773c269084
commit d076bedb5e
37 changed files with 1326 additions and 645 deletions

View File

@@ -484,7 +484,7 @@ export interface DataQueryRequest<TQuery extends DataQuery = DataQuery> {
panelId?: number;
dashboardId?: number;
// Temporary prop for public dashboards, to be replaced by publicAccessKey
publicDashboardUid?: string;
publicDashboardAccessToken?: string;
// Request Timing
startTime: number;

View File

@@ -30,19 +30,21 @@ describe('PublicDashboardDatasource', () => {
const ds = new PublicDashboardDataSource();
const panelId = 1;
const publicDashboardUid = 'abc123';
const publicDashboardAccessToken = 'abc123';
ds.query({
maxDataPoints: 10,
intervalMs: 5000,
targets: [{ refId: 'A' }, { refId: 'B', datasource: { type: 'sample' } }],
panelId,
publicDashboardUid,
publicDashboardAccessToken,
} as DataQueryRequest);
const mock = mockDatasourceRequest.mock;
expect(mock.calls.length).toBe(1);
expect(mock.lastCall[0].url).toEqual(`/api/public/dashboards/${publicDashboardUid}/panels/${panelId}/query`);
expect(mock.lastCall[0].url).toEqual(
`/api/public/dashboards/${publicDashboardAccessToken}/panels/${panelId}/query`
);
});
});

View File

@@ -89,11 +89,6 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/a/:id/*", reqSignedIn, hs.Index) // App Root Page
r.Get("/a/:id", reqSignedIn, hs.Index)
//pubdash
if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) {
r.Get("/public-dashboards/:uid", middleware.SetPublicDashboardFlag(), hs.Index)
}
r.Get("/d/:uid/:slug", reqSignedIn, redirectFromLegacyPanelEditURL, hs.Index)
r.Get("/d/:uid", reqSignedIn, redirectFromLegacyPanelEditURL, hs.Index)
r.Get("/dashboard/script/*", reqSignedIn, hs.Index)
@@ -612,8 +607,9 @@ func (hs *HTTPServer) registerRoutes() {
// Public API
if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) {
r.Get("/api/public/dashboards/:uid", routing.Wrap(hs.GetPublicDashboard))
r.Post("/api/public/dashboards/:uid/panels/:panelId/query", routing.Wrap(hs.QueryPublicDashboard))
r.Get("/public-dashboards/:accessToken", middleware.SetPublicDashboardFlag(), hs.Index)
r.Get("/api/public/dashboards/:accessToken", routing.Wrap(hs.GetPublicDashboard))
r.Post("/api/public/dashboards/:accessToken/panels/:panelId/query", routing.Wrap(hs.QueryPublicDashboard))
}
// Frontend logs

View File

@@ -139,7 +139,6 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
Url: dash.GetUrl(),
FolderTitle: "General",
AnnotationsPermissions: annotationPermissions,
IsPublic: dash.IsPublic,
}
// lookup folder title

View File

@@ -2,40 +2,42 @@ 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 {
publicDashboardUid := web.Params(c.Req)[":uid"]
accessToken := web.Params(c.Req)[":accessToken"]
dash, err := hs.dashboardService.GetPublicDashboard(c.Req.Context(), publicDashboardUid)
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,
IsPublic: dash.IsPublic,
PublicDashboardUid: publicDashboardUid,
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}
@@ -54,43 +56,72 @@ func (hs *HTTPServer) GetPublicDashboardConfig(c *models.ReqContext) response.Re
// sets public dashboard configuration for dashboard
func (hs *HTTPServer) SavePublicDashboardConfig(c *models.ReqContext) response.Response {
pdc := &models.PublicDashboardConfig{}
if err := web.Bind(c.Req, pdc); err != nil {
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"],
PublicDashboardConfig: pdc,
OrgId: c.OrgId,
DashboardUid: web.Params(c.Req)[":uid"],
UserId: c.UserId,
PublicDashboard: pubdash,
}
pdc, err := hs.dashboardService.SavePublicDashboardConfig(c.Req.Context(), &dto)
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, pdc)
return response.JSON(http.StatusOK, pubdash)
}
// QueryPublicDashboard returns all results for a given panel on a public dashboard
// POST /api/public/dashboard/:uid/panels/:panelId/query
// 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(),
web.Params(c.Req)[":uid"],
dashboard,
publicDashboard,
panelId,
)
if err != nil {
return handleDashboardErr(http.StatusInternalServerError, "Failed to get queries for public dashboard", err)
}
resp, err := hs.queryDataService.QueryDataMultipleSources(c.Req.Context(), nil, c.SkipCache, reqDTO, true)
// 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)

View File

@@ -10,6 +10,7 @@ import (
"strings"
"testing"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@@ -18,10 +19,14 @@ import (
"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"
"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"
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
@@ -54,39 +59,40 @@ func TestAPIGetPublicDashboard(t *testing.T) {
assert.Equal(t, http.StatusNotFound, response.Code)
})
dashboardUid := "dashboard-abcd1234"
pubdashUid := "pubdash-abcd1234"
DashboardUid := "dashboard-abcd1234"
token, err := uuid.NewV4()
require.NoError(t, err)
accessToken := fmt.Sprintf("%x", token)
testCases := []struct {
name string
uid string
expectedHttpResponse int
Name string
AccessToken string
ExpectedHttpResponse int
publicDashboardResult *models.Dashboard
publicDashboardErr error
}{
{
name: "It gets a public dashboard",
uid: pubdashUid,
expectedHttpResponse: http.StatusOK,
Name: "It gets a public dashboard",
AccessToken: accessToken,
ExpectedHttpResponse: http.StatusOK,
publicDashboardResult: &models.Dashboard{
Data: simplejson.NewFromAny(map[string]interface{}{
"Uid": dashboardUid,
"Uid": DashboardUid,
}),
IsPublic: true,
},
publicDashboardErr: nil,
},
{
name: "It should return 404 if isPublicDashboard is false",
uid: pubdashUid,
expectedHttpResponse: http.StatusNotFound,
Name: "It should return 404 if isPublicDashboard is false",
AccessToken: accessToken,
ExpectedHttpResponse: http.StatusNotFound,
publicDashboardResult: nil,
publicDashboardErr: models.ErrPublicDashboardNotFound,
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
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")).
@@ -97,20 +103,19 @@ func TestAPIGetPublicDashboard(t *testing.T) {
response := callAPI(
sc.server,
http.MethodGet,
fmt.Sprintf("/api/public/dashboards/%v", test.uid),
fmt.Sprintf("/api/public/dashboards/%s", test.AccessToken),
nil,
t,
)
assert.Equal(t, test.expectedHttpResponse, response.Code)
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, true, dashResp.Meta.IsPublic)
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)
@@ -127,44 +132,44 @@ func TestAPIGetPublicDashboard(t *testing.T) {
}
func TestAPIGetPublicDashboardConfig(t *testing.T) {
pdc := &models.PublicDashboardConfig{IsPublic: true}
pubdash := &models.PublicDashboard{IsEnabled: true}
testCases := []struct {
name string
dashboardUid string
expectedHttpResponse int
publicDashboardConfigResult *models.PublicDashboardConfig
publicDashboardConfigError error
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,
publicDashboardConfigResult: pdc,
publicDashboardConfigError: nil,
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,
publicDashboardConfigResult: nil,
publicDashboardConfigError: models.ErrDashboardNotFound,
Name: "returns 404 when dashboard not found",
DashboardUid: "77777",
ExpectedHttpResponse: http.StatusNotFound,
PublicDashboardResult: nil,
PublicDashboardError: models.ErrDashboardNotFound,
},
{
name: "returns 500 when internal server error",
dashboardUid: "1",
expectedHttpResponse: http.StatusInternalServerError,
publicDashboardConfigResult: nil,
publicDashboardConfigError: errors.New("database broken"),
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) {
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.publicDashboardConfigResult, test.publicDashboardConfigError)
Return(test.PublicDashboardResult, test.PublicDashboardError)
sc.hs.dashboardService = dashSvc
setInitCtxSignedInViewer(sc.initCtx)
@@ -176,13 +181,13 @@ func TestAPIGetPublicDashboardConfig(t *testing.T) {
t,
)
assert.Equal(t, test.expectedHttpResponse, response.Code)
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
if response.Code == http.StatusOK {
var pdcResp models.PublicDashboardConfig
var pdcResp models.PublicDashboard
err := json.Unmarshal(response.Body.Bytes(), &pdcResp)
require.NoError(t, err)
assert.Equal(t, test.publicDashboardConfigResult, &pdcResp)
assert.Equal(t, test.PublicDashboardResult, &pdcResp)
}
})
}
@@ -190,40 +195,40 @@ func TestAPIGetPublicDashboardConfig(t *testing.T) {
func TestApiSavePublicDashboardConfig(t *testing.T) {
testCases := []struct {
name string
dashboardUid string
publicDashboardConfig *models.PublicDashboardConfig
expectedHttpResponse int
Name string
DashboardUid string
publicDashboardConfig *models.PublicDashboard
ExpectedHttpResponse int
saveDashboardError error
}{
{
name: "returns 200 when update persists",
dashboardUid: "1",
publicDashboardConfig: &models.PublicDashboardConfig{IsPublic: true},
expectedHttpResponse: http.StatusOK,
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.PublicDashboardConfig{},
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.PublicDashboardConfig{},
Name: "returns 404 when dashboard not found",
ExpectedHttpResponse: http.StatusNotFound,
publicDashboardConfig: &models.PublicDashboard{},
saveDashboardError: models.ErrDashboardNotFound,
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
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.PublicDashboardConfig{IsPublic: true}, test.saveDashboardError)
Return(&models.PublicDashboard{IsEnabled: true}, test.saveDashboardError)
sc.hs.dashboardService = dashSvc
setInitCtxSignedInViewer(sc.initCtx)
@@ -235,7 +240,7 @@ func TestApiSavePublicDashboardConfig(t *testing.T) {
t,
)
assert.Equal(t, test.expectedHttpResponse, response.Code)
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
// check the result if it's a 200
if response.Code == http.StatusOK {
@@ -326,10 +331,14 @@ func TestAPIQueryPublicDashboard(t *testing.T) {
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,
"abc123",
mock.Anything,
mock.Anything,
int64(2),
).Return(dtos.MetricRequest{
Queries: []*simplejson.Json{
@@ -385,10 +394,13 @@ func TestAPIQueryPublicDashboard(t *testing.T) {
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,
"abc123",
mock.Anything,
mock.Anything,
int64(2),
).Return(dtos.MetricRequest{
Queries: []*simplejson.Json{
@@ -422,10 +434,13 @@ func TestAPIQueryPublicDashboard(t *testing.T) {
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,
"abc123",
mock.Anything,
mock.Anything,
int64(2),
).Return(dtos.MetricRequest{
Queries: []*simplejson.Json{
@@ -505,3 +520,111 @@ func TestAPIQueryPublicDashboard(t *testing.T) {
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(), &models.AddDataSourceCommand{
Uid: "ds1",
OrgId: 1,
Name: "laban",
Type: models.DS_MYSQL,
Access: models.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),
)
}

View File

@@ -7,34 +7,33 @@ import (
)
type DashboardMeta struct {
IsStarred bool `json:"isStarred,omitempty"`
IsHome bool `json:"isHome,omitempty"`
IsSnapshot bool `json:"isSnapshot,omitempty"`
Type string `json:"type,omitempty"`
CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"`
CanAdmin bool `json:"canAdmin"`
CanStar bool `json:"canStar"`
CanDelete bool `json:"canDelete"`
Slug string `json:"slug"`
Url string `json:"url"`
Expires time.Time `json:"expires"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
UpdatedBy string `json:"updatedBy"`
CreatedBy string `json:"createdBy"`
Version int `json:"version"`
HasAcl bool `json:"hasAcl"`
IsFolder bool `json:"isFolder"`
FolderId int64 `json:"folderId"`
FolderUid string `json:"folderUid"`
FolderTitle string `json:"folderTitle"`
FolderUrl string `json:"folderUrl"`
Provisioned bool `json:"provisioned"`
ProvisionedExternalId string `json:"provisionedExternalId"`
AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"`
IsPublic bool `json:"isPublic"`
PublicDashboardUid string `json:"publicDashboardUid"`
IsStarred bool `json:"isStarred,omitempty"`
IsHome bool `json:"isHome,omitempty"`
IsSnapshot bool `json:"isSnapshot,omitempty"`
Type string `json:"type,omitempty"`
CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"`
CanAdmin bool `json:"canAdmin"`
CanStar bool `json:"canStar"`
CanDelete bool `json:"canDelete"`
Slug string `json:"slug"`
Url string `json:"url"`
Expires time.Time `json:"expires"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
UpdatedBy string `json:"updatedBy"`
CreatedBy string `json:"createdBy"`
Version int `json:"version"`
HasAcl bool `json:"hasAcl"`
IsFolder bool `json:"isFolder"`
FolderId int64 `json:"folderId"`
FolderUid string `json:"folderUid"`
FolderTitle string `json:"folderTitle"`
FolderUrl string `json:"folderUrl"`
Provisioned bool `json:"provisioned"`
ProvisionedExternalId string `json:"provisionedExternalId"`
AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"`
PublicDashboardAccessToken string `json:"publicDashboardAccessToken"`
}
type AnnotationPermission struct {
Dashboard AnnotationActions `json:"dashboard"`

View File

@@ -200,7 +200,6 @@ type Dashboard struct {
FolderId int64
IsFolder bool
HasAcl bool
IsPublic bool
Title string
Data *simplejson.Json

View File

@@ -1,8 +1,18 @@
package models
import (
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
)
var (
ErrPublicDashboardFailedGenerateUniqueUid = DashboardErr{
Reason: "Failed to generate unique dashboard id",
Reason: "Failed to generate unique public dashboard id",
StatusCode: 500,
}
ErrPublicDashboardFailedGenerateAccesstoken = DashboardErr{
Reason: "Failed to public dashboard access token",
StatusCode: 500,
}
ErrPublicDashboardNotFound = DashboardErr{
@@ -21,20 +31,51 @@ var (
}
)
type PublicDashboardConfig struct {
IsPublic bool `json:"isPublic"`
PublicDashboard PublicDashboard `json:"publicDashboard"`
}
type PublicDashboard struct {
Uid string `json:"uid" xorm:"uid"`
DashboardUid string `json:"dashboardUid" xorm:"dashboard_uid"`
OrgId int64 `json:"orgId" xorm:"org_id"`
TimeSettings string `json:"timeSettings" xorm:"time_settings"`
Uid string `json:"uid" xorm:"uid"`
DashboardUid string `json:"dashboardUid" xorm:"dashboard_uid"`
OrgId int64 `json:"-" xorm:"org_id"` // Don't ever marshal orgId to Json
TimeSettings *simplejson.Json `json:"timeSettings" xorm:"time_settings"`
IsEnabled bool `json:"isEnabled" xorm:"is_enabled"`
AccessToken string `json:"accessToken" xorm:"access_token"`
CreatedBy int64 `json:"createdBy" xorm:"created_by"`
UpdatedBy int64 `json:"updatedBy" xorm:"updated_by"`
CreatedAt time.Time `json:"createdAt" xorm:"created_at"`
UpdatedAt time.Time `json:"updatedAt" xorm:"updated_at"`
}
func (pd PublicDashboard) TableName() string {
return "dashboard_public_config"
return "dashboard_public"
}
type TimeSettings struct {
From string `json:"from"`
To string `json:"to"`
}
// build time settings object from json on public dashboard. If empty, use
// defaults on the dashboard
func (pd PublicDashboard) BuildTimeSettings(dashboard *Dashboard) *TimeSettings {
ts := &TimeSettings{
From: dashboard.Data.GetPath("time", "from").MustString(),
To: dashboard.Data.GetPath("time", "to").MustString(),
}
if pd.TimeSettings == nil {
return ts
}
// merge time settings from public dashboard
to := pd.TimeSettings.Get("to").MustString("")
from := pd.TimeSettings.Get("from").MustString("")
if to != "" && from != "" {
ts.From = from
ts.To = to
}
return ts
}
//
@@ -42,7 +83,7 @@ func (pd PublicDashboard) TableName() string {
//
type SavePublicDashboardConfigCommand struct {
DashboardUid string
OrgId int64
PublicDashboardConfig PublicDashboardConfig
DashboardUid string
OrgId int64
PublicDashboard PublicDashboard
}

View File

@@ -0,0 +1,56 @@
package models
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/stretchr/testify/assert"
)
func TestPublicDashboardTableName(t *testing.T) {
assert.Equal(t, "dashboard_public", PublicDashboard{}.TableName())
}
func TestBuildTimeSettings(t *testing.T) {
var dashboardData = simplejson.NewFromAny(map[string]interface{}{"time": map[string]interface{}{"from": "now-8", "to": "now"}})
testCases := []struct {
name string
dashboard *Dashboard
pubdash *PublicDashboard
timeResult *TimeSettings
}{
{
name: "should use dashboard time if pubdash time empty",
dashboard: &Dashboard{Data: dashboardData},
pubdash: &PublicDashboard{},
timeResult: &TimeSettings{
From: "now-8",
To: "now",
},
},
{
name: "should use dashboard time if pubdash to/from empty",
dashboard: &Dashboard{Data: dashboardData},
pubdash: &PublicDashboard{},
timeResult: &TimeSettings{
From: "now-8",
To: "now",
},
},
{
name: "should use pubdash time",
dashboard: &Dashboard{Data: dashboardData},
pubdash: &PublicDashboard{TimeSettings: simplejson.NewFromAny(map[string]interface{}{"from": "now-12", "to": "now"})},
timeResult: &TimeSettings{
From: "now-12",
To: "now",
},
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.timeResult, test.pubdash.BuildTimeSettings(test.dashboard))
})
}
}

View File

@@ -10,7 +10,7 @@ import (
//go:generate mockery --name DashboardService --structname FakeDashboardService --inpackage --filename dashboard_service_mock.go
// DashboardService is a service for operating on dashboards.
type DashboardService interface {
BuildPublicDashboardMetricRequest(ctx context.Context, publicDashboardUid string, panelId int64) (dtos.MetricRequest, error)
BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *models.PublicDashboard, panelId int64) (dtos.MetricRequest, error)
BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error)
DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error
FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error)
@@ -19,14 +19,14 @@ type DashboardService interface {
GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error
GetDashboardTags(ctx context.Context, query *models.GetDashboardTagsQuery) error
GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error
GetPublicDashboard(ctx context.Context, publicDashboardUid string) (*models.Dashboard, error)
GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error)
GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error)
GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error)
HasAdminPermissionInFolders(ctx context.Context, query *models.HasAdminPermissionInFoldersQuery) error
HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error
ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error)
MakeUserAdmin(ctx context.Context, orgID int64, userID, dashboardID int64, setViewAndEditPermissions bool) error
SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error)
SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*models.PublicDashboardConfig, error)
SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*models.PublicDashboard, error)
SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error
UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error
}
@@ -65,16 +65,18 @@ type Store interface {
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error)
GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error)
GetPublicDashboardConfig(orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error)
GetPublicDashboard(uid string) (*models.PublicDashboard, *models.Dashboard, error)
GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error)
GetPublicDashboard(ctx context.Context, accessToken string) (*models.PublicDashboard, *models.Dashboard, error)
GenerateNewPublicDashboardUid(ctx context.Context) (string, error)
HasAdminPermissionInFolders(ctx context.Context, query *models.HasAdminPermissionInFoldersQuery) error
HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error
// SaveAlerts saves dashboard alerts.
SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error
SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error)
SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
SavePublicDashboardConfig(cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboardConfig, error)
SavePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboard, error)
UnprovisionDashboard(ctx context.Context, id int64) error
UpdatePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) error
UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error
// ValidateDashboardBeforeSave validates a dashboard before save.
ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error)

View File

@@ -1,4 +1,4 @@
// Code generated by mockery v2.12.2. DO NOT EDIT.
// Code generated by mockery v2.12.1. DO NOT EDIT.
package dashboards
@@ -18,20 +18,20 @@ type FakeDashboardService struct {
mock.Mock
}
// BuildPublicDashboardMetricRequest provides a mock function with given fields: ctx, publicDashboardUid, panelId
func (_m *FakeDashboardService) BuildPublicDashboardMetricRequest(ctx context.Context, publicDashboardUid string, panelId int64) (dtos.MetricRequest, error) {
ret := _m.Called(ctx, publicDashboardUid, panelId)
// BuildPublicDashboardMetricRequest provides a mock function with given fields: ctx, dashboard, publicDashboard, panelId
func (_m *FakeDashboardService) BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *models.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, string, int64) dtos.MetricRequest); ok {
r0 = rf(ctx, publicDashboardUid, panelId)
if rf, ok := ret.Get(0).(func(context.Context, *models.Dashboard, *models.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, string, int64) error); ok {
r1 = rf(ctx, publicDashboardUid, panelId)
if rf, ok := ret.Get(1).(func(context.Context, *models.Dashboard, *models.PublicDashboard, int64) error); ok {
r1 = rf(ctx, dashboard, publicDashboard, panelId)
} else {
r1 = ret.Error(1)
}
@@ -169,13 +169,13 @@ func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *models
return r0
}
// GetPublicDashboard provides a mock function with given fields: ctx, publicDashboardUid
func (_m *FakeDashboardService) GetPublicDashboard(ctx context.Context, publicDashboardUid string) (*models.Dashboard, error) {
ret := _m.Called(ctx, publicDashboardUid)
// GetPublicDashboard provides a mock function with given fields: ctx, accessToken
func (_m *FakeDashboardService) 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, publicDashboardUid)
r0 = rf(ctx, accessToken)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Dashboard)
@@ -184,7 +184,7 @@ func (_m *FakeDashboardService) GetPublicDashboard(ctx context.Context, publicDa
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, publicDashboardUid)
r1 = rf(ctx, accessToken)
} else {
r1 = ret.Error(1)
}
@@ -193,15 +193,15 @@ func (_m *FakeDashboardService) GetPublicDashboard(ctx context.Context, publicDa
}
// GetPublicDashboardConfig provides a mock function with given fields: ctx, orgId, dashboardUid
func (_m *FakeDashboardService) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) {
func (_m *FakeDashboardService) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) {
ret := _m.Called(ctx, orgId, dashboardUid)
var r0 *models.PublicDashboardConfig
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *models.PublicDashboardConfig); ok {
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.PublicDashboardConfig)
r0 = ret.Get(0).(*models.PublicDashboard)
}
}
@@ -304,15 +304,15 @@ func (_m *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDash
}
// SavePublicDashboardConfig provides a mock function with given fields: ctx, dto
func (_m *FakeDashboardService) SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*models.PublicDashboardConfig, error) {
func (_m *FakeDashboardService) SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) {
ret := _m.Called(ctx, dto)
var r0 *models.PublicDashboardConfig
if rf, ok := ret.Get(0).(func(context.Context, *SavePublicDashboardConfigDTO) *models.PublicDashboardConfig); ok {
var r0 *models.PublicDashboard
if rf, ok := ret.Get(0).(func(context.Context, *SavePublicDashboardConfigDTO) *models.PublicDashboard); ok {
r0 = rf(ctx, dto)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.PublicDashboardConfig)
r0 = ret.Get(0).(*models.PublicDashboard)
}
}

View File

@@ -2,7 +2,6 @@ package database
import (
"context"
"fmt"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
@@ -10,14 +9,14 @@ import (
)
// retrieves public dashboard configuration
func (d *DashboardStore) GetPublicDashboard(uid string) (*models.PublicDashboard, *models.Dashboard, error) {
if uid == "" {
func (d *DashboardStore) GetPublicDashboard(ctx context.Context, accessToken string) (*models.PublicDashboard, *models.Dashboard, error) {
if accessToken == "" {
return nil, nil, models.ErrPublicDashboardIdentifierNotSet
}
// get public dashboard
pdRes := &models.PublicDashboard{Uid: uid}
err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
pdRes := &models.PublicDashboard{AccessToken: accessToken}
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
has, err := sess.Get(pdRes)
if err != nil {
return err
@@ -34,7 +33,7 @@ func (d *DashboardStore) GetPublicDashboard(uid string) (*models.PublicDashboard
// find dashboard
dashRes := &models.Dashboard{OrgId: pdRes.OrgId, Uid: pdRes.DashboardUid}
err = d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
err = d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
has, err := sess.Get(dashRes)
if err != nil {
return err
@@ -53,44 +52,43 @@ func (d *DashboardStore) GetPublicDashboard(uid string) (*models.PublicDashboard
}
// generates a new unique uid to retrieve a public dashboard
func generateNewPublicDashboardUid(sess *sqlstore.DBSession) (string, error) {
for i := 0; i < 3; i++ {
uid := util.GenerateShortUID()
func (d *DashboardStore) GenerateNewPublicDashboardUid(ctx context.Context) (string, error) {
var uid string
exists, err := sess.Get(&models.PublicDashboard{Uid: uid})
if err != nil {
return "", err
err := d.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
for i := 0; i < 3; i++ {
uid = util.GenerateShortUID()
exists, err := sess.Get(&models.PublicDashboard{Uid: uid})
if err != nil {
return err
}
if !exists {
return nil
}
}
if !exists {
return uid, nil
}
return models.ErrPublicDashboardFailedGenerateUniqueUid
})
if err != nil {
return "", err
}
return "", models.ErrPublicDashboardFailedGenerateUniqueUid
return uid, nil
}
// retrieves public dashboard configuration
func (d *DashboardStore) GetPublicDashboardConfig(orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) {
func (d *DashboardStore) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) {
if dashboardUid == "" {
return nil, models.ErrDashboardIdentifierNotSet
}
// get dashboard and publicDashboard
dashRes := &models.Dashboard{OrgId: orgId, Uid: dashboardUid}
pdRes := &models.PublicDashboard{OrgId: orgId, DashboardUid: dashboardUid}
err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
// dashboard
has, err := sess.Get(dashRes)
if err != nil {
return err
}
if !has {
return models.ErrDashboardNotFound
}
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
// publicDashboard
_, err = sess.Get(pdRes)
_, err := sess.Get(pdRes)
if err != nil {
return err
}
@@ -102,46 +100,13 @@ func (d *DashboardStore) GetPublicDashboardConfig(orgId int64, dashboardUid stri
return nil, err
}
pdc := &models.PublicDashboardConfig{
IsPublic: dashRes.IsPublic,
PublicDashboard: *pdRes,
}
return pdc, err
return pdRes, err
}
// persists public dashboard configuration
func (d *DashboardStore) SavePublicDashboardConfig(cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboardConfig, error) {
if len(cmd.PublicDashboardConfig.PublicDashboard.DashboardUid) == 0 {
return nil, models.ErrDashboardIdentifierNotSet
}
err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
// update isPublic on dashboard entry
affectedRowCount, err := sess.Table("dashboard").Where("org_id = ? AND uid = ?", cmd.OrgId, cmd.DashboardUid).Update(map[string]interface{}{"is_public": cmd.PublicDashboardConfig.IsPublic})
if err != nil {
return err
}
if affectedRowCount == 0 {
return models.ErrDashboardNotFound
}
// update dashboard_public_config
// if we have a uid, public dashboard config exists. delete it otherwise generate a uid
if cmd.PublicDashboardConfig.PublicDashboard.Uid != "" {
if _, err = sess.Exec("DELETE FROM dashboard_public_config WHERE uid=?", cmd.PublicDashboardConfig.PublicDashboard.Uid); err != nil {
return err
}
} else {
uid, err := generateNewPublicDashboardUid(sess)
if err != nil {
return fmt.Errorf("failed to generate UID for public dashboard: %w", err)
}
cmd.PublicDashboardConfig.PublicDashboard.Uid = uid
}
_, err = sess.Insert(&cmd.PublicDashboardConfig.PublicDashboard)
func (d *DashboardStore) SavePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboard, error) {
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
_, err := sess.UseBool("is_enabled").Insert(&cmd.PublicDashboard)
if err != nil {
return err
}
@@ -153,5 +118,19 @@ func (d *DashboardStore) SavePublicDashboardConfig(cmd models.SavePublicDashboar
return nil, err
}
return &cmd.PublicDashboardConfig, nil
return &cmd.PublicDashboard, nil
}
// updates existing public dashboard configuration
func (d *DashboardStore) UpdatePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) error {
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
_, err := sess.UseBool("is_enabled").Update(&cmd.PublicDashboard)
if err != nil {
return err
}
return nil
})
return err
}

View File

@@ -1,8 +1,11 @@
package database
import (
"context"
"testing"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
@@ -11,6 +14,12 @@ import (
"github.com/stretchr/testify/require"
)
// This is what the db sets empty time settings to
var DefaultTimeSettings, _ = simplejson.NewJson([]byte(`{}`))
// Default time to pass in with seconds rounded
var DefaultTime = time.Now().UTC().Round(time.Second)
// GetPublicDashboard
func TestIntegrationGetPublicDashboard(t *testing.T) {
var sqlStore *sqlstore.SQLStore
@@ -25,54 +34,55 @@ func TestIntegrationGetPublicDashboard(t *testing.T) {
t.Run("returns PublicDashboard and Dashboard", func(t *testing.T) {
setup()
pdc, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
Uid: "abc1234",
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
},
pubdash, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
PublicDashboard: models.PublicDashboard{
IsEnabled: true,
Uid: "abc1234",
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
TimeSettings: DefaultTimeSettings,
CreatedAt: DefaultTime,
CreatedBy: 7,
AccessToken: "NOTAREALUUID",
},
})
require.NoError(t, err)
pd, d, err := dashboardStore.GetPublicDashboard("abc1234")
pd, d, err := dashboardStore.GetPublicDashboard(context.Background(), "NOTAREALUUID")
require.NoError(t, err)
assert.Equal(t, pd, &pdc.PublicDashboard)
assert.Equal(t, d.Uid, pdc.PublicDashboard.DashboardUid)
assert.Equal(t, pd, pubdash)
assert.Equal(t, d.Uid, pubdash.DashboardUid)
})
t.Run("returns ErrPublicDashboardNotFound with empty uid", func(t *testing.T) {
setup()
_, _, err := dashboardStore.GetPublicDashboard("")
_, _, err := dashboardStore.GetPublicDashboard(context.Background(), "")
require.Error(t, models.ErrPublicDashboardIdentifierNotSet, err)
})
t.Run("returns ErrPublicDashboardNotFound when PublicDashboard not found", func(t *testing.T) {
setup()
_, _, err := dashboardStore.GetPublicDashboard("zzzzzz")
_, _, err := dashboardStore.GetPublicDashboard(context.Background(), "zzzzzz")
require.Error(t, models.ErrPublicDashboardNotFound, err)
})
t.Run("returns ErrDashboardNotFound when Dashboard not found", func(t *testing.T) {
setup()
_, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
_, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
Uid: "abc1234",
DashboardUid: "nevergonnafindme",
OrgId: savedDashboard.OrgId,
},
PublicDashboard: models.PublicDashboard{
IsEnabled: true,
Uid: "abc1234",
DashboardUid: "nevergonnafindme",
OrgId: savedDashboard.OrgId,
CreatedAt: DefaultTime,
CreatedBy: 7,
},
})
require.NoError(t, err)
_, _, err = dashboardStore.GetPublicDashboard("abc1234")
_, _, err = dashboardStore.GetPublicDashboard(context.Background(), "abc1234")
require.Error(t, models.ErrDashboardNotFound, err)
})
}
@@ -91,38 +101,40 @@ func TestIntegrationGetPublicDashboardConfig(t *testing.T) {
t.Run("returns isPublic and set dashboardUid and orgId", func(t *testing.T) {
setup()
pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid)
pubdash, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
assert.Equal(t, &models.PublicDashboardConfig{IsPublic: false, PublicDashboard: models.PublicDashboard{DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId}}, pdc)
assert.Equal(t, &models.PublicDashboard{IsEnabled: false, DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId}, pubdash)
})
t.Run("returns dashboard errDashboardIdentifierNotSet", func(t *testing.T) {
setup()
_, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, "")
_, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, "")
require.Error(t, models.ErrDashboardIdentifierNotSet, err)
})
t.Run("returns isPublic along with public dashboard when exists", func(t *testing.T) {
setup()
// insert test public dashboard
resp, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
resp, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
Uid: "pubdash-uid",
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
TimeSettings: "{from: now, to: then}",
},
PublicDashboard: models.PublicDashboard{
IsEnabled: true,
Uid: "pubdash-uid",
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
TimeSettings: DefaultTimeSettings,
CreatedAt: DefaultTime,
CreatedBy: 7,
},
})
require.NoError(t, err)
pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid)
pubdash, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
assert.Equal(t, resp, pdc)
assert.True(t, assert.ObjectsAreEqualValues(resp, pubdash))
assert.True(t, assert.ObjectsAreEqual(resp, pubdash))
})
}
@@ -142,89 +154,92 @@ func TestIntegrationSavePublicDashboardConfig(t *testing.T) {
t.Run("saves new public dashboard", func(t *testing.T) {
setup()
resp, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
resp, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
Uid: "pubdash-uid",
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
},
PublicDashboard: models.PublicDashboard{
IsEnabled: true,
Uid: "pubdash-uid",
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
TimeSettings: DefaultTimeSettings,
CreatedAt: DefaultTime,
CreatedBy: 7,
AccessToken: "NOTAREALUUID",
},
})
require.NoError(t, err)
pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid)
pubdash, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
//verify saved response and queried response are the same
assert.Equal(t, resp, pdc)
assert.Equal(t, resp, pubdash)
// verify we have a valid uid
assert.True(t, util.IsValidShortUID(pdc.PublicDashboard.Uid))
assert.True(t, util.IsValidShortUID(pubdash.Uid))
// verify we didn't update all dashboards
pdc2, err := dashboardStore.GetPublicDashboardConfig(savedDashboard2.OrgId, savedDashboard2.Uid)
pubdash2, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard2.OrgId, savedDashboard2.Uid)
require.NoError(t, err)
assert.False(t, pdc2.IsPublic)
})
t.Run("returns ErrDashboardIdentifierNotSet", func(t *testing.T) {
setup()
_, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
DashboardUid: "",
OrgId: savedDashboard.OrgId,
},
},
})
require.Error(t, models.ErrDashboardIdentifierNotSet, err)
})
t.Run("overwrites existing public dashboard", func(t *testing.T) {
setup()
pdUid := util.GenerateShortUID()
// insert initial record
_, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
Uid: pdUid,
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
},
},
})
require.NoError(t, err)
// update initial record
resp, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: false,
PublicDashboard: models.PublicDashboard{
Uid: pdUid,
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
TimeSettings: "{}",
},
},
})
require.NoError(t, err)
pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
assert.Equal(t, resp, pdc)
assert.False(t, pubdash2.IsEnabled)
})
}
func TestIntegrationUpdatePublicDashboard(t *testing.T) {
var sqlStore *sqlstore.SQLStore
var dashboardStore *DashboardStore
var savedDashboard *models.Dashboard
setup := func() {
sqlStore = sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}})
dashboardStore = ProvideDashboardStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
t.Run("updates an existing dashboard", func(t *testing.T) {
setup()
pdUid := "asdf1234"
_, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboard: models.PublicDashboard{
Uid: pdUid,
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
IsEnabled: true,
CreatedAt: DefaultTime,
CreatedBy: 7,
AccessToken: "NOTAREALUUID",
},
})
require.NoError(t, err)
updatedPublicDashboard := models.PublicDashboard{
Uid: pdUid,
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
IsEnabled: false,
TimeSettings: simplejson.NewFromAny(map[string]interface{}{"from": "now-8", "to": "now"}),
UpdatedAt: time.Now().UTC().Round(time.Second),
UpdatedBy: 8,
}
// update initial record
err = dashboardStore.UpdatePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboard: updatedPublicDashboard,
})
require.NoError(t, err)
pdRetrieved, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
assert.Equal(t, updatedPublicDashboard.UpdatedAt, pdRetrieved.UpdatedAt)
// make sure we're correctly updated IsEnabled because we have to call
// UseBool with xorm
assert.Equal(t, updatedPublicDashboard.IsEnabled, pdRetrieved.IsEnabled)
})
}

View File

@@ -16,9 +16,10 @@ type SaveDashboardDTO struct {
}
type SavePublicDashboardConfigDTO struct {
DashboardUid string
OrgId int64
PublicDashboardConfig *models.PublicDashboardConfig
DashboardUid string
OrgId int64
UserId int64
PublicDashboard *models.PublicDashboard
}
type DashboardSearchProjection struct {

View File

@@ -2,46 +2,42 @@ package service
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/gofrs/uuid"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
)
// Gets public dashboard via generated Uid
func (dr *DashboardServiceImpl) GetPublicDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) {
pdc, d, err := dr.dashboardStore.GetPublicDashboard(dashboardUid)
// Gets public dashboard via access token
func (dr *DashboardServiceImpl) GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) {
pubdash, d, err := dr.dashboardStore.GetPublicDashboard(ctx, accessToken)
if err != nil {
return nil, err
}
if pdc == nil || d == nil {
if pubdash == nil || d == nil {
return nil, models.ErrPublicDashboardNotFound
}
if !d.IsPublic {
if !pubdash.IsEnabled {
return nil, models.ErrPublicDashboardNotFound
}
// Replace dashboard time range with pubdash time range
if pdc.TimeSettings != "" {
var pdcTimeSettings map[string]interface{}
err = json.Unmarshal([]byte(pdc.TimeSettings), &pdcTimeSettings)
if err != nil {
return nil, err
}
d.Data.Set("time", pdcTimeSettings)
}
ts := pubdash.BuildTimeSettings(d)
d.Data.SetPath([]string{"time", "from"}, ts.From)
d.Data.SetPath([]string{"time", "to"}, ts.To)
return d, nil
}
// GetPublicDashboardConfig is a helper method to retrieve the public dashboard configuration for a given dashboard from the database
func (dr *DashboardServiceImpl) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) {
pdc, err := dr.dashboardStore.GetPublicDashboardConfig(orgId, dashboardUid)
func (dr *DashboardServiceImpl) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) {
pdc, err := dr.dashboardStore.GetPublicDashboardConfig(ctx, orgId, dashboardUid)
if err != nil {
return nil, err
}
@@ -51,53 +47,108 @@ func (dr *DashboardServiceImpl) GetPublicDashboardConfig(ctx context.Context, or
// SavePublicDashboardConfig is a helper method to persist the sharing config
// to the database. It handles validations for sharing config and persistence
func (dr *DashboardServiceImpl) SavePublicDashboardConfig(ctx context.Context, dto *dashboards.SavePublicDashboardConfigDTO) (*models.PublicDashboardConfig, error) {
cmd := models.SavePublicDashboardConfigCommand{
DashboardUid: dto.DashboardUid,
OrgId: dto.OrgId,
PublicDashboardConfig: *dto.PublicDashboardConfig,
func (dr *DashboardServiceImpl) SavePublicDashboardConfig(ctx context.Context, dto *dashboards.SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) {
if len(dto.DashboardUid) == 0 {
return nil, models.ErrDashboardIdentifierNotSet
}
// Eventually we want this to propagate to array of public dashboards
cmd.PublicDashboardConfig.PublicDashboard.OrgId = dto.OrgId
cmd.PublicDashboardConfig.PublicDashboard.DashboardUid = dto.DashboardUid
// set default value for time settings
if dto.PublicDashboard.TimeSettings == nil {
json, err := simplejson.NewJson([]byte("{}"))
if err != nil {
return nil, err
}
dto.PublicDashboard.TimeSettings = json
}
pdc, err := dr.dashboardStore.SavePublicDashboardConfig(cmd)
if dto.PublicDashboard.Uid == "" {
return dr.savePublicDashboardConfig(ctx, dto)
}
return dr.updatePublicDashboardConfig(ctx, dto)
}
func (dr *DashboardServiceImpl) savePublicDashboardConfig(ctx context.Context, dto *dashboards.SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) {
uid, err := dr.dashboardStore.GenerateNewPublicDashboardUid(ctx)
if err != nil {
return nil, err
}
return pdc, nil
accessToken, err := GenerateAccessToken()
if err != nil {
return nil, err
}
cmd := models.SavePublicDashboardConfigCommand{
DashboardUid: dto.DashboardUid,
OrgId: dto.OrgId,
PublicDashboard: models.PublicDashboard{
Uid: uid,
DashboardUid: dto.DashboardUid,
OrgId: dto.OrgId,
IsEnabled: dto.PublicDashboard.IsEnabled,
TimeSettings: dto.PublicDashboard.TimeSettings,
CreatedBy: dto.UserId,
CreatedAt: time.Now(),
AccessToken: accessToken,
},
}
return dr.dashboardStore.SavePublicDashboardConfig(ctx, cmd)
}
func (dr *DashboardServiceImpl) BuildPublicDashboardMetricRequest(ctx context.Context, publicDashboardUid string, panelId int64) (dtos.MetricRequest, error) {
publicDashboardConfig, dashboard, err := dr.dashboardStore.GetPublicDashboard(publicDashboardUid)
if err != nil {
return dtos.MetricRequest{}, err
func (dr *DashboardServiceImpl) updatePublicDashboardConfig(ctx context.Context, dto *dashboards.SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) {
cmd := models.SavePublicDashboardConfigCommand{
PublicDashboard: models.PublicDashboard{
Uid: dto.PublicDashboard.Uid,
IsEnabled: dto.PublicDashboard.IsEnabled,
TimeSettings: dto.PublicDashboard.TimeSettings,
UpdatedBy: dto.UserId,
UpdatedAt: time.Now(),
},
}
if !dashboard.IsPublic {
err := dr.dashboardStore.UpdatePublicDashboardConfig(ctx, cmd)
if err != nil {
return nil, err
}
publicDashboard, err := dr.dashboardStore.GetPublicDashboardConfig(ctx, dto.OrgId, dto.DashboardUid)
if err != nil {
return nil, err
}
return publicDashboard, nil
}
// BuildPublicDashboardMetricRequest merges public dashboard parameters with
// dashboard and returns a metrics request to be sent to query backend
func (dr *DashboardServiceImpl) BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *models.PublicDashboard, panelId int64) (dtos.MetricRequest, error) {
if !publicDashboard.IsEnabled {
return dtos.MetricRequest{}, models.ErrPublicDashboardNotFound
}
var timeSettings struct {
From string `json:"from"`
To string `json:"to"`
}
err = json.Unmarshal([]byte(publicDashboardConfig.TimeSettings), &timeSettings)
if err != nil {
return dtos.MetricRequest{}, err
}
queriesByPanel := models.GetQueriesFromDashboard(dashboard.Data)
if _, ok := queriesByPanel[panelId]; !ok {
return dtos.MetricRequest{}, models.ErrPublicDashboardPanelNotFound
}
ts := publicDashboard.BuildTimeSettings(dashboard)
return dtos.MetricRequest{
From: timeSettings.From,
To: timeSettings.To,
From: ts.From,
To: ts.To,
Queries: queriesByPanel[panelId],
}, nil
}
// generates a uuid formatted without dashes to use as access token
func GenerateAccessToken() (string, error) {
token, err := uuid.NewV4()
if err != nil {
return "", err
}
return fmt.Sprintf("%x", token), nil
}

View File

@@ -3,7 +3,9 @@ package service
import (
"context"
"testing"
"time"
"github.com/gofrs/uuid"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
@@ -15,6 +17,11 @@ import (
"github.com/stretchr/testify/require"
)
var timeSettings, _ = simplejson.NewJson([]byte(`{"from": "now-12", "to": "now"}`))
var defaultPubdashTimeSettings, _ = simplejson.NewJson([]byte(`{}`))
var dashboardData = simplejson.NewFromAny(map[string]interface{}{"time": map[string]interface{}{"from": "now-8", "to": "now"}})
var mergedDashboardData = simplejson.NewFromAny(map[string]interface{}{"time": map[string]interface{}{"from": "now-12", "to": "now"}})
func TestGetPublicDashboard(t *testing.T) {
type storeResp struct {
pd *models.PublicDashboard
@@ -23,78 +30,90 @@ func TestGetPublicDashboard(t *testing.T) {
}
testCases := []struct {
name string
uid string
storeResp *storeResp
errResp error
dashResp *models.Dashboard
Name string
AccessToken string
StoreResp *storeResp
ErrResp error
DashResp *models.Dashboard
}{
{
name: "returns a dashboard",
uid: "abc123",
storeResp: &storeResp{pd: &models.PublicDashboard{}, d: &models.Dashboard{IsPublic: true}, err: nil},
errResp: nil,
dashResp: &models.Dashboard{IsPublic: true},
Name: "returns a dashboard",
AccessToken: "abc123",
StoreResp: &storeResp{
pd: &models.PublicDashboard{IsEnabled: true},
d: &models.Dashboard{Uid: "mydashboard", Data: dashboardData},
err: nil,
},
ErrResp: nil,
DashResp: &models.Dashboard{Uid: "mydashboard", Data: dashboardData},
},
{
name: "puts pubdash time settings into dashboard",
uid: "abc123",
storeResp: &storeResp{
pd: &models.PublicDashboard{TimeSettings: `{"from": "now-8", "to": "now"}`},
d: &models.Dashboard{
IsPublic: true,
Data: simplejson.NewFromAny(map[string]interface{}{"time": map[string]interface{}{"from": "abc", "to": "123"}}),
},
err: nil},
errResp: nil,
dashResp: &models.Dashboard{IsPublic: true, Data: simplejson.NewFromAny(map[string]interface{}{"time": map[string]interface{}{"from": "now-8", "to": "now"}})},
Name: "puts pubdash time settings into dashboard",
AccessToken: "abc123",
StoreResp: &storeResp{
pd: &models.PublicDashboard{IsEnabled: true, TimeSettings: timeSettings},
d: &models.Dashboard{Data: dashboardData},
err: nil,
},
ErrResp: nil,
DashResp: &models.Dashboard{Data: mergedDashboardData},
},
{
name: "returns ErrPublicDashboardNotFound when isPublic is false",
uid: "abc123",
storeResp: &storeResp{pd: &models.PublicDashboard{}, d: &models.Dashboard{IsPublic: false}, err: nil},
errResp: models.ErrPublicDashboardNotFound,
dashResp: nil,
Name: "returns ErrPublicDashboardNotFound when isEnabled is false",
AccessToken: "abc123",
StoreResp: &storeResp{
pd: &models.PublicDashboard{IsEnabled: false},
d: &models.Dashboard{Uid: "mydashboard"},
err: nil,
},
ErrResp: models.ErrPublicDashboardNotFound,
DashResp: nil,
},
{
name: "returns ErrPublicDashboardNotFound if PublicDashboard missing",
uid: "abc123",
storeResp: &storeResp{pd: nil, d: nil, err: nil},
errResp: models.ErrPublicDashboardNotFound,
dashResp: nil,
Name: "returns ErrPublicDashboardNotFound if PublicDashboard missing",
AccessToken: "abc123",
StoreResp: &storeResp{pd: nil, d: nil, err: nil},
ErrResp: models.ErrPublicDashboardNotFound,
DashResp: nil,
},
{
name: "returns ErrPublicDashboardNotFound if Dashboard missing",
uid: "abc123",
storeResp: &storeResp{pd: nil, d: nil, err: nil},
errResp: models.ErrPublicDashboardNotFound,
dashResp: nil,
Name: "returns ErrPublicDashboardNotFound if Dashboard missing",
AccessToken: "abc123",
StoreResp: &storeResp{pd: nil, d: nil, err: nil},
ErrResp: models.ErrPublicDashboardNotFound,
DashResp: nil,
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
t.Run(test.Name, func(t *testing.T) {
fakeStore := dashboards.FakeDashboardStore{}
service := &DashboardServiceImpl{
log: log.New("test.logger"),
dashboardStore: &fakeStore,
}
fakeStore.On("GetPublicDashboard", mock.Anything).
Return(test.storeResp.pd, test.storeResp.d, test.storeResp.err)
dashboard, err := service.GetPublicDashboard(context.Background(), test.uid)
if test.errResp != nil {
assert.Error(t, test.errResp, err)
fakeStore.On("GetPublicDashboard", mock.Anything, mock.Anything).
Return(test.StoreResp.pd, test.StoreResp.d, test.StoreResp.err)
dashboard, err := service.GetPublicDashboard(context.Background(), test.AccessToken)
if test.ErrResp != nil {
assert.Error(t, test.ErrResp, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, test.dashResp, dashboard)
assert.Equal(t, test.DashResp, dashboard)
if test.DashResp != nil {
assert.NotNil(t, dashboard.CreatedBy)
}
})
}
}
func TestSavePublicDashboard(t *testing.T) {
t.Run("gets PublicDashboard.orgId and PublicDashboard.DashboardUid set from SavePublicDashboardConfigDTO", func(t *testing.T) {
t.Run("Saving public dashboard", func(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := database.ProvideDashboardStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
@@ -107,56 +126,197 @@ func TestSavePublicDashboard(t *testing.T) {
dto := &dashboards.SavePublicDashboardConfigDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
PublicDashboardConfig: &models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
},
UserId: 7,
PublicDashboard: &models.PublicDashboard{
IsEnabled: true,
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
TimeSettings: timeSettings,
},
}
pdc, err := service.SavePublicDashboardConfig(context.Background(), dto)
_, err := service.SavePublicDashboardConfig(context.Background(), dto)
require.NoError(t, err)
assert.Equal(t, dashboard.Uid, pdc.PublicDashboard.DashboardUid)
assert.Equal(t, dashboard.OrgId, pdc.PublicDashboard.OrgId)
pubdash, err := service.GetPublicDashboardConfig(context.Background(), dashboard.OrgId, dashboard.Uid)
require.NoError(t, err)
// DashboardUid/OrgId/CreatedBy set by the command, not parameters
assert.Equal(t, dashboard.Uid, pubdash.DashboardUid)
assert.Equal(t, dashboard.OrgId, pubdash.OrgId)
assert.Equal(t, dto.UserId, pubdash.CreatedBy)
// IsEnabled set by parameters
assert.Equal(t, dto.PublicDashboard.IsEnabled, pubdash.IsEnabled)
// CreatedAt set to non-zero time
assert.NotEqual(t, &time.Time{}, pubdash.CreatedAt)
// Time settings set by db
assert.Equal(t, timeSettings, pubdash.TimeSettings)
// accessToken is valid uuid
_, err = uuid.FromString(pubdash.AccessToken)
require.NoError(t, err)
})
t.Run("PLACEHOLDER - dashboard with template variables cannot be saved", func(t *testing.T) {
//sqlStore := sqlstore.InitTestDB(t)
//dashboardStore := database.ProvideDashboardStore(sqlStore)
//dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
t.Run("Validate pubdash has default time setting value", func(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := database.ProvideDashboardStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
//service := &DashboardServiceImpl{
//log: log.New("test.logger"),
//dashboardStore: dashboardStore,
//}
service := &DashboardServiceImpl{
log: log.New("test.logger"),
dashboardStore: dashboardStore,
}
//dto := &dashboards.SavePublicDashboardConfigDTO{
//DashboardUid: dashboard.Uid,
//OrgId: dashboard.OrgId,
//PublicDashboardConfig: &models.PublicDashboardConfig{
//IsPublic: true,
//PublicDashboard: models.PublicDashboard{
//DashboardUid: "NOTTHESAME",
//OrgId: 9999999,
//},
//},
//}
dto := &dashboards.SavePublicDashboardConfigDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
UserId: 7,
PublicDashboard: &models.PublicDashboard{
IsEnabled: true,
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
},
}
//pdc, err := service.SavePublicDashboardConfig(context.Background(), dto)
//require.NoError(t, err)
_, err := service.SavePublicDashboardConfig(context.Background(), dto)
require.NoError(t, err)
//assert.Equal(t, dashboard.Uid, pdc.PublicDashboard.DashboardUid)
//assert.Equal(t, dashboard.OrgId, pdc.PublicDashboard.OrgId)
pubdash, err := service.GetPublicDashboardConfig(context.Background(), dashboard.OrgId, dashboard.Uid)
require.NoError(t, err)
assert.Equal(t, defaultPubdashTimeSettings, pubdash.TimeSettings)
})
t.Run("PLACEHOLDER - dashboard with template variables cannot be saved", func(t *testing.T) {})
}
func TestUpdatePublicDashboard(t *testing.T) {
t.Run("Updating public dashboard", func(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := database.ProvideDashboardStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
service := &DashboardServiceImpl{
log: log.New("test.logger"),
dashboardStore: dashboardStore,
}
dto := &dashboards.SavePublicDashboardConfigDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
UserId: 7,
PublicDashboard: &models.PublicDashboard{
IsEnabled: true,
TimeSettings: timeSettings,
},
}
_, err := service.SavePublicDashboardConfig(context.Background(), dto)
require.NoError(t, err)
savedPubdash, err := service.GetPublicDashboardConfig(context.Background(), dashboard.OrgId, dashboard.Uid)
require.NoError(t, err)
// attempt to overwrite settings
dto = &dashboards.SavePublicDashboardConfigDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
UserId: 8,
PublicDashboard: &models.PublicDashboard{
Uid: savedPubdash.Uid,
OrgId: 9,
DashboardUid: "abc1234",
CreatedBy: 9,
CreatedAt: time.Time{},
IsEnabled: true,
TimeSettings: timeSettings,
AccessToken: "NOTAREALUUID",
},
}
// Since the dto.PublicDashboard has a uid, this will call
// service.updatePublicDashboardConfig
_, err = service.SavePublicDashboardConfig(context.Background(), dto)
require.NoError(t, err)
updatedPubdash, err := service.GetPublicDashboardConfig(context.Background(), dashboard.OrgId, dashboard.Uid)
require.NoError(t, err)
// don't get updated
assert.Equal(t, savedPubdash.DashboardUid, updatedPubdash.DashboardUid)
assert.Equal(t, savedPubdash.OrgId, updatedPubdash.OrgId)
assert.Equal(t, savedPubdash.CreatedAt, updatedPubdash.CreatedAt)
assert.Equal(t, savedPubdash.CreatedBy, updatedPubdash.CreatedBy)
assert.Equal(t, savedPubdash.AccessToken, updatedPubdash.AccessToken)
// gets updated
assert.Equal(t, dto.PublicDashboard.IsEnabled, updatedPubdash.IsEnabled)
assert.Equal(t, dto.PublicDashboard.TimeSettings, updatedPubdash.TimeSettings)
assert.Equal(t, dto.UserId, updatedPubdash.UpdatedBy)
assert.NotEqual(t, &time.Time{}, updatedPubdash.UpdatedAt)
})
t.Run("Updating set empty time settings", func(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := database.ProvideDashboardStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
service := &DashboardServiceImpl{
log: log.New("test.logger"),
dashboardStore: dashboardStore,
}
dto := &dashboards.SavePublicDashboardConfigDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
UserId: 7,
PublicDashboard: &models.PublicDashboard{
IsEnabled: true,
TimeSettings: timeSettings,
},
}
// Since the dto.PublicDashboard has a uid, this will call
// service.updatePublicDashboardConfig
_, err := service.SavePublicDashboardConfig(context.Background(), dto)
require.NoError(t, err)
savedPubdash, err := service.GetPublicDashboardConfig(context.Background(), dashboard.OrgId, dashboard.Uid)
require.NoError(t, err)
// attempt to overwrite settings
dto = &dashboards.SavePublicDashboardConfigDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
UserId: 8,
PublicDashboard: &models.PublicDashboard{
Uid: savedPubdash.Uid,
OrgId: 9,
DashboardUid: "abc1234",
CreatedBy: 9,
CreatedAt: time.Time{},
IsEnabled: true,
AccessToken: "NOTAREALUUID",
},
}
_, err = service.SavePublicDashboardConfig(context.Background(), dto)
require.NoError(t, err)
updatedPubdash, err := service.GetPublicDashboardConfig(context.Background(), dashboard.OrgId, dashboard.Uid)
require.NoError(t, err)
timeSettings, err := simplejson.NewJson([]byte("{}"))
require.NoError(t, err)
assert.Equal(t, timeSettings, updatedPubdash.TimeSettings)
})
}
func TestBuildPublicDashboardMetricRequest(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := database.ProvideDashboardStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
publicDashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
nonPublicDashboard := insertTestDashboard(t, dashboardStore, "testNonPublicDashie", 1, 0, true)
service := &DashboardServiceImpl{
@@ -165,47 +325,44 @@ func TestBuildPublicDashboardMetricRequest(t *testing.T) {
}
dto := &dashboards.SavePublicDashboardConfigDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
PublicDashboardConfig: &models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
TimeSettings: `{"from": "FROM", "to": "TO"}`,
},
DashboardUid: publicDashboard.Uid,
OrgId: publicDashboard.OrgId,
PublicDashboard: &models.PublicDashboard{
IsEnabled: true,
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
TimeSettings: timeSettings,
},
}
pdc, err := service.SavePublicDashboardConfig(context.Background(), dto)
publicDashboardPD, err := service.SavePublicDashboardConfig(context.Background(), dto)
require.NoError(t, err)
nonPublicDto := &dashboards.SavePublicDashboardConfigDTO{
DashboardUid: nonPublicDashboard.Uid,
OrgId: nonPublicDashboard.OrgId,
PublicDashboardConfig: &models.PublicDashboardConfig{
IsPublic: false,
PublicDashboard: models.PublicDashboard{
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
TimeSettings: `{"from": "FROM", "to": "TO"}`,
},
PublicDashboard: &models.PublicDashboard{
IsEnabled: false,
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
TimeSettings: defaultPubdashTimeSettings,
},
}
nonPublicPdc, err := service.SavePublicDashboardConfig(context.Background(), nonPublicDto)
nonPublicDashboardPD, err := service.SavePublicDashboardConfig(context.Background(), nonPublicDto)
require.NoError(t, err)
t.Run("extracts queries from provided dashboard", func(t *testing.T) {
reqDTO, err := service.BuildPublicDashboardMetricRequest(
context.Background(),
pdc.PublicDashboard.Uid,
publicDashboard,
publicDashboardPD,
1,
)
require.NoError(t, err)
require.Equal(t, "FROM", reqDTO.From)
require.Equal(t, "TO", reqDTO.To)
require.Equal(t, timeSettings.Get("from").MustString(), reqDTO.From)
require.Equal(t, timeSettings.Get("to").MustString(), reqDTO.To)
require.Len(t, reqDTO.Queries, 2)
require.Equal(
t,
@@ -234,7 +391,8 @@ func TestBuildPublicDashboardMetricRequest(t *testing.T) {
t.Run("returns an error when panel missing", func(t *testing.T) {
_, err := service.BuildPublicDashboardMetricRequest(
context.Background(),
pdc.PublicDashboard.Uid,
publicDashboard,
publicDashboardPD,
49,
)
@@ -244,7 +402,8 @@ func TestBuildPublicDashboardMetricRequest(t *testing.T) {
t.Run("returns an error when dashboard not public", func(t *testing.T) {
_, err := service.BuildPublicDashboardMetricRequest(
context.Background(),
nonPublicPdc.PublicDashboard.Uid,
nonPublicDashboard,
nonPublicDashboardPD,
2,
)
require.ErrorContains(t, err, "Public dashboard not found")
@@ -262,19 +421,19 @@ func insertTestDashboard(t *testing.T, dashboardStore *database.DashboardStore,
"id": nil,
"title": title,
"tags": tags,
"panels": []map[string]interface{}{
{
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"targets": []map[string]interface{}{
{
"datasource": map[string]string{
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "mysql",
"uid": "ds1",
},
"refId": "A",
},
{
"datasource": map[string]string{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "ds2",
},
@@ -282,11 +441,11 @@ func insertTestDashboard(t *testing.T, dashboardStore *database.DashboardStore,
},
},
},
{
map[string]interface{}{
"id": 2,
"targets": []map[string]interface{}{
{
"datasource": map[string]string{
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "mysql",
"uid": "ds3",
},

View File

@@ -67,6 +67,27 @@ func (_m *FakeDashboardStore) FindDashboards(ctx context.Context, query *models.
return r0, r1
}
// GenerateNewPublicDashboardUid provides a mock function with given fields: ctx
func (_m *FakeDashboardStore) 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
}
// GetDashboard provides a mock function with given fields: ctx, query
func (_m *FakeDashboardStore) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) (*models.Dashboard, error) {
ret := _m.Called(ctx, query)
@@ -298,13 +319,13 @@ func (_m *FakeDashboardStore) GetProvisionedDataByDashboardUID(orgID int64, dash
return r0, r1
}
// GetPublicDashboard provides a mock function with given fields: uid
func (_m *FakeDashboardStore) GetPublicDashboard(uid string) (*models.PublicDashboard, *models.Dashboard, error) {
ret := _m.Called(uid)
// GetPublicDashboard provides a mock function with given fields: ctx, accessToken
func (_m *FakeDashboardStore) GetPublicDashboard(ctx context.Context, accessToken string) (*models.PublicDashboard, *models.Dashboard, error) {
ret := _m.Called(ctx, accessToken)
var r0 *models.PublicDashboard
if rf, ok := ret.Get(0).(func(string) *models.PublicDashboard); ok {
r0 = rf(uid)
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)
@@ -312,8 +333,8 @@ func (_m *FakeDashboardStore) GetPublicDashboard(uid string) (*models.PublicDash
}
var r1 *models.Dashboard
if rf, ok := ret.Get(1).(func(string) *models.Dashboard); ok {
r1 = rf(uid)
if rf, ok := ret.Get(1).(func(context.Context, string) *models.Dashboard); ok {
r1 = rf(ctx, accessToken)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*models.Dashboard)
@@ -321,8 +342,8 @@ func (_m *FakeDashboardStore) GetPublicDashboard(uid string) (*models.PublicDash
}
var r2 error
if rf, ok := ret.Get(2).(func(string) error); ok {
r2 = rf(uid)
if rf, ok := ret.Get(2).(func(context.Context, string) error); ok {
r2 = rf(ctx, accessToken)
} else {
r2 = ret.Error(2)
}
@@ -330,22 +351,22 @@ func (_m *FakeDashboardStore) GetPublicDashboard(uid string) (*models.PublicDash
return r0, r1, r2
}
// GetPublicDashboardConfig provides a mock function with given fields: orgId, dashboardUid
func (_m *FakeDashboardStore) GetPublicDashboardConfig(orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) {
ret := _m.Called(orgId, dashboardUid)
// GetPublicDashboardConfig provides a mock function with given fields: ctx, orgId, dashboardUid
func (_m *FakeDashboardStore) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) {
ret := _m.Called(ctx, orgId, dashboardUid)
var r0 *models.PublicDashboardConfig
if rf, ok := ret.Get(0).(func(int64, string) *models.PublicDashboardConfig); ok {
r0 = rf(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.PublicDashboardConfig)
r0 = ret.Get(0).(*models.PublicDashboard)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, string) error); ok {
r1 = rf(orgId, dashboardUid)
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok {
r1 = rf(ctx, orgId, dashboardUid)
} else {
r1 = ret.Error(1)
}
@@ -441,22 +462,22 @@ func (_m *FakeDashboardStore) SaveProvisionedDashboard(cmd models.SaveDashboardC
return r0, r1
}
// SavePublicDashboardConfig provides a mock function with given fields: cmd
func (_m *FakeDashboardStore) SavePublicDashboardConfig(cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboardConfig, error) {
ret := _m.Called(cmd)
// SavePublicDashboardConfig provides a mock function with given fields: ctx, cmd
func (_m *FakeDashboardStore) SavePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboard, error) {
ret := _m.Called(ctx, cmd)
var r0 *models.PublicDashboardConfig
if rf, ok := ret.Get(0).(func(models.SavePublicDashboardConfigCommand) *models.PublicDashboardConfig); ok {
r0 = rf(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.PublicDashboardConfig)
r0 = ret.Get(0).(*models.PublicDashboard)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(models.SavePublicDashboardConfigCommand) error); ok {
r1 = rf(cmd)
if rf, ok := ret.Get(1).(func(context.Context, models.SavePublicDashboardConfigCommand) error); ok {
r1 = rf(ctx, cmd)
} else {
r1 = ret.Error(1)
}
@@ -492,6 +513,20 @@ func (_m *FakeDashboardStore) UpdateDashboardACL(ctx context.Context, uid int64,
return r0
}
// UpdatePublicDashboardConfig provides a mock function with given fields: ctx, cmd
func (_m *FakeDashboardStore) 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
}
// ValidateDashboardBeforeSave provides a mock function with given fields: dashboard, overwrite
func (_m *FakeDashboardStore) ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) {
ret := _m.Called(dashboard, overwrite)

View File

@@ -98,4 +98,4 @@
}
}
]
}
}

View File

@@ -1,33 +0,0 @@
package migrations
import (
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)
func addPublicDashboardMigration(mg *Migrator) {
var dashboardPublicCfgV1 = Table{
Name: "dashboard_public_config",
Columns: []*Column{
{Name: "uid", Type: DB_NVarchar, Length: 40, IsPrimaryKey: true},
{Name: "dashboard_uid", Type: DB_NVarchar, Length: 40, Nullable: false},
{Name: "org_id", Type: DB_BigInt, Nullable: false},
{Name: "time_settings", Type: DB_Text, Nullable: false},
{Name: "refresh_rate", Type: DB_Int, Nullable: false, Default: "30"},
{Name: "template_variables", Type: DB_MediumText, Nullable: true},
},
Indices: []*Index{
{Cols: []string{"uid"}, Type: UniqueIndex},
{Cols: []string{"org_id", "dashboard_uid"}},
},
}
mg.AddMigration("create dashboard public config v1", NewAddTableMigration(dashboardPublicCfgV1))
// table has no dependencies and was created with incorrect pkey type.
// drop then recreate with correct values
addDropAllIndicesMigrations(mg, "v1", dashboardPublicCfgV1)
mg.AddMigration("Drop old dashboard public config table", NewDropTableMigration("dashboard_public_config"))
// recreate table with proper primary key type
mg.AddMigration("recreate dashboard public config v1", NewAddTableMigration(dashboardPublicCfgV1))
addTableIndicesMigrations(mg, "v1", dashboardPublicCfgV1)
}

View File

@@ -0,0 +1,52 @@
package migrations
import (
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)
func addPublicDashboardMigration(mg *Migrator) {
var dashboardPublicCfgV1 = Table{
Name: "dashboard_public_config",
Columns: []*Column{
{Name: "uid", Type: DB_NVarchar, Length: 40, IsPrimaryKey: true},
{Name: "dashboard_uid", Type: DB_NVarchar, Length: 40, Nullable: false},
{Name: "org_id", Type: DB_BigInt, Nullable: false},
{Name: "time_settings", Type: DB_Text, Nullable: true},
{Name: "template_variables", Type: DB_MediumText, Nullable: true},
{Name: "access_token", Type: DB_NVarchar, Length: 32, Nullable: false},
{Name: "created_by", Type: DB_Int, Nullable: false},
{Name: "updated_by", Type: DB_Int, Nullable: true},
{Name: "created_at", Type: DB_DateTime, Nullable: false},
{Name: "updated_at", Type: DB_DateTime, Nullable: true},
{Name: "is_enabled", Type: DB_Bool, Nullable: false, Default: "0"},
},
Indices: []*Index{
{Cols: []string{"uid"}, Type: UniqueIndex},
{Cols: []string{"org_id", "dashboard_uid"}},
{Cols: []string{"access_token"}, Type: UniqueIndex},
},
}
// initial create table
mg.AddMigration("create dashboard public config v1", NewAddTableMigration(dashboardPublicCfgV1))
// recreate table - no dependencies and was created with incorrect pkey type
addDropAllIndicesMigrations(mg, "v1", dashboardPublicCfgV1)
mg.AddMigration("Drop old dashboard public config table", NewDropTableMigration("dashboard_public_config"))
mg.AddMigration("recreate dashboard public config v1", NewAddTableMigration(dashboardPublicCfgV1))
addTableIndicesMigrations(mg, "v1", dashboardPublicCfgV1)
// recreate table - schema finalized for public dashboards v1
addDropAllIndicesMigrations(mg, "v2", dashboardPublicCfgV1)
mg.AddMigration("Drop public config table", NewDropTableMigration("dashboard_public_config"))
mg.AddMigration("Recreate dashboard public config v2", NewAddTableMigration(dashboardPublicCfgV1))
addTableIndicesMigrations(mg, "v2", dashboardPublicCfgV1)
// rename table
addTableRenameMigration(mg, "dashboard_public_config", "dashboard_public", "v2")
}

View File

@@ -10423,7 +10423,7 @@
"provisionedExternalId": {
"type": "string"
},
"publicDashboardUid": {
"publicDashboardAccessToken": {
"type": "string"
},
"slug": {

View File

@@ -9465,7 +9465,7 @@
"provisionedExternalId": {
"type": "string"
},
"publicDashboardUid": {
"publicDashboardAccessToken": {
"type": "string"
},
"slug": {

View File

@@ -6,12 +6,14 @@ import config from 'app/core/config';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { ShareModal } from './ShareModal';
import { PublicDashboardConfig } from './SharePublicDashboardUtils';
import { PublicDashboard } from './SharePublicDashboardUtils';
// Mock api request
const publicDashboardconfigResp: PublicDashboardConfig = {
isPublic: true,
publicDashboard: { uid: '', dashboardUid: '' },
const publicDashboardconfigResp: PublicDashboard = {
isEnabled: true,
uid: '',
dashboardUid: '',
accessToken: '',
};
const backendSrv = {
@@ -88,6 +90,13 @@ describe('SharePublic', () => {
fireEvent.click(screen.getByText('Public Dashboard'));
await waitFor(() => screen.getByText('Enabled'));
await screen.findByText('Welcome to Grafana public dashboards alpha!');
});
// test when checkboxes show up
// test checkboxes hidden
// test url hidden
// test url shows up
//
// test checking if current version of dashboard in state is persisted to db
});

View File

@@ -1,34 +1,46 @@
import React, { useState, useEffect } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Button, Field, Switch, Alert } from '@grafana/ui';
import { AppEvents } from '@grafana/data';
import { Alert, Button, Checkbox, ClipboardButton, Field, FieldSet, Icon, Input, Switch } from '@grafana/ui';
import { notifyApp } from 'app/core/actions';
import { createErrorNotification } from 'app/core/copy/appNotification';
import { VariableModel } from 'app/features/variables/types';
import { appEvents } from 'app/core/core';
import { dispatch } from 'app/store/store';
import {
dashboardHasTemplateVariables,
generatePublicDashboardUrl,
getPublicDashboardConfig,
PublicDashboard,
publicDashboardPersisted,
savePublicDashboardConfig,
PublicDashboardConfig,
} from './SharePublicDashboardUtils';
import { ShareModalTabProps } from './types';
interface Props extends ShareModalTabProps {}
interface Acknowledgements {
public: boolean;
datasources: boolean;
usage: boolean;
}
export const SharePublicDashboard = (props: Props) => {
const dashboardUid = props.dashboard.uid;
const [publicDashboardConfig, setPublicDashboardConfig] = useState<PublicDashboardConfig>({
isPublic: false,
publicDashboard: { uid: '', dashboardUid },
const dashboardVariables = props.dashboard.getVariables();
const [publicDashboard, setPublicDashboardConfig] = useState<PublicDashboard>({
isEnabled: false,
uid: '',
dashboardUid: props.dashboard.uid,
});
const [acknowledgements, setAcknowledgements] = useState<Acknowledgements>({
public: false,
datasources: false,
usage: false,
});
const [dashboardVariables, setDashboardVariables] = useState<VariableModel[]>([]);
useEffect(() => {
setDashboardVariables(props.dashboard.getVariables());
getPublicDashboardConfig(dashboardUid, setPublicDashboardConfig).catch();
}, [props, dashboardUid]);
getPublicDashboardConfig(props.dashboard.uid, setPublicDashboardConfig).catch();
}, [props.dashboard.uid]);
const onSavePublicConfig = () => {
if (dashboardHasTemplateVariables(dashboardVariables)) {
@@ -38,32 +50,143 @@ export const SharePublicDashboard = (props: Props) => {
return;
}
savePublicDashboardConfig(props.dashboard.uid, publicDashboardConfig, setPublicDashboardConfig).catch();
savePublicDashboardConfig(props.dashboard.uid, publicDashboard, setPublicDashboardConfig).catch();
};
const onShareUrlCopy = () => {
appEvents.emit(AppEvents.alertSuccess, ['Content copied to clipboard']);
};
const onAcknowledge = useCallback(
(field: string, checked: boolean) => {
setAcknowledgements({ ...acknowledgements, [field]: checked });
},
[acknowledgements]
);
// check if all conditions have been acknowledged
const acknowledged = () => {
return acknowledgements.public && acknowledgements.datasources && acknowledgements.usage;
};
return (
<>
{dashboardHasTemplateVariables(dashboardVariables) && (
<p>Welcome to Grafana public dashboards alpha!</p>
{dashboardHasTemplateVariables(dashboardVariables) ? (
<Alert severity="warning" title="dashboard cannot be public">
This dashboard cannot be made public because it has template variables
</Alert>
) : (
<>
<p>
To allow the current dashboard to be published publicly, toggle the switch. For now we do not support
template variables or frontend datasources.
</p>
We&apos;d love your feedback. To share, please comment on this{' '}
<a
href="https://github.com/grafana/grafana/discussions/49253"
target="_blank"
rel="noreferrer"
className="text-link"
>
github discussion
</a>
<hr />
{!publicDashboardPersisted(publicDashboard) && (
<div>
Before you click Save, please acknowledge the following information: <br />
<FieldSet>
<br />
<div>
<Checkbox
label="Your entire dashboard will be public"
value={acknowledgements.public}
onChange={(e) => onAcknowledge('public', e.currentTarget.checked)}
/>
</div>
<br />
<div>
<Checkbox
label="Publishing currently only works with a subset of datasources"
value={acknowledgements.datasources}
description="Learn more about public datasources"
onChange={(e) => onAcknowledge('datasources', e.currentTarget.checked)}
/>
</div>
<br />
<Checkbox
label="Making your dashboard public will cause queries to run each time the dashboard is viewed which may increase costs"
value={acknowledgements.usage}
description="Learn more about query caching"
onChange={(e) => onAcknowledge('usage', e.currentTarget.checked)}
/>
<br />
<br />
</FieldSet>
</div>
)}
{(publicDashboardPersisted(publicDashboard) || acknowledged()) && (
<div>
<h4 className="share-modal-info-text">Public Dashboard Configuration</h4>
<FieldSet>
Time Range
<br />
<div style={{ padding: '5px' }}>
<Input
value={props.dashboard.time.from}
disabled={true}
addonBefore={
<span style={{ width: '50px', display: 'flex', alignItems: 'center', padding: '5px' }}>
From:
</span>
}
/>
<Input
value={props.dashboard.time.to}
disabled={true}
addonBefore={
<span style={{ width: '50px', display: 'flex', alignItems: 'center', padding: '5px' }}>To:</span>
}
/>
</div>
<br />
<Field label="Enabled" description="Configures whether current dashboard can be available publicly">
<Switch
disabled={dashboardHasTemplateVariables(dashboardVariables)}
value={publicDashboard?.isEnabled}
onChange={() =>
setPublicDashboardConfig({
...publicDashboard,
isEnabled: !publicDashboard.isEnabled,
})
}
/>
</Field>
{publicDashboardPersisted(publicDashboard) && publicDashboard.isEnabled && (
<Field label="Link URL">
<Input
value={generatePublicDashboardUrl(publicDashboard)}
readOnly
addonAfter={
<ClipboardButton
variant="primary"
getText={() => {
return generatePublicDashboardUrl(publicDashboard);
}}
onClipboardCopy={onShareUrlCopy}
>
<Icon name="copy" /> Copy
</ClipboardButton>
}
/>
</Field>
)}
</FieldSet>
<Button onClick={onSavePublicConfig}>Save Sharing Configuration</Button>
</div>
)}
</>
)}
<p className="share-modal-info-text">Public Dashboard Configuration</p>
<Field label="Enabled" description="Configures whether current dashboard can be available publicly">
<Switch
id="share-current-time-range"
disabled={dashboardHasTemplateVariables(dashboardVariables)}
value={publicDashboardConfig?.isPublic}
onChange={() =>
setPublicDashboardConfig((state) => {
return { ...state, isPublic: !state.isPublic };
})
}
/>
</Field>
<Button onClick={onSavePublicConfig}>Save Sharing Configuration</Button>
</>
);
};

View File

@@ -1,6 +1,11 @@
import { VariableModel } from 'app/features/variables/types';
import { dashboardHasTemplateVariables } from './SharePublicDashboardUtils';
import {
PublicDashboard,
dashboardHasTemplateVariables,
generatePublicDashboardUrl,
publicDashboardPersisted,
} from './SharePublicDashboardUtils';
describe('dashboardHasTemplateVariables', () => {
it('false', () => {
@@ -14,3 +19,24 @@ describe('dashboardHasTemplateVariables', () => {
expect(dashboardHasTemplateVariables(variables)).toBe(true);
});
});
describe('generatePublicDashboardUrl', () => {
it('has the right uid', () => {
let pubdash = { accessToken: 'abcd1234' } as PublicDashboard;
expect(generatePublicDashboardUrl(pubdash)).toEqual(`${window.location.origin}/public-dashboards/abcd1234`);
});
});
describe('publicDashboardPersisted', () => {
it('true', () => {
let pubdash = { uid: 'abcd1234' } as PublicDashboard;
expect(publicDashboardPersisted(pubdash)).toBe(true);
});
it('false', () => {
let pubdash = { uid: '' } as PublicDashboard;
expect(publicDashboardPersisted(pubdash)).toBe(false);
pubdash = {} as PublicDashboard;
expect(publicDashboardPersisted(pubdash)).toBe(false);
});
});

View File

@@ -5,39 +5,53 @@ import { VariableModel } from 'app/features/variables/types';
import { dispatch } from 'app/store/store';
import { DashboardDataDTO, DashboardMeta } from 'app/types/dashboard';
export interface PublicDashboardConfig {
isPublic: boolean;
publicDashboard: {
uid: string;
dashboardUid: string;
timeSettings?: object;
};
export interface PublicDashboard {
accessToken?: string;
isEnabled: boolean;
uid: string;
dashboardUid: string;
timeSettings?: object;
}
export interface DashboardResponse {
dashboard: DashboardDataDTO;
meta: DashboardMeta;
}
export const dashboardHasTemplateVariables = (variables: VariableModel[]): boolean => {
return variables.length > 0;
};
export const getPublicDashboardConfig = async (
dashboardUid: string,
setPublicDashboardConfig: React.Dispatch<React.SetStateAction<PublicDashboardConfig>>
setPublicDashboard: React.Dispatch<React.SetStateAction<PublicDashboard>>
) => {
const url = `/api/dashboards/uid/${dashboardUid}/public-config`;
const pdResp: PublicDashboardConfig = await getBackendSrv().get(url);
setPublicDashboardConfig(pdResp);
const pdResp: PublicDashboard = await getBackendSrv().get(url);
setPublicDashboard(pdResp);
};
export const savePublicDashboardConfig = async (
dashboardUid: string,
publicDashboardConfig: PublicDashboardConfig,
setPublicDashboardConfig: Function
publicDashboardConfig: PublicDashboard,
setPublicDashboard: React.Dispatch<React.SetStateAction<PublicDashboard>>
) => {
const url = `/api/dashboards/uid/${dashboardUid}/public-config`;
const pdResp: PublicDashboardConfig = await getBackendSrv().post(url, publicDashboardConfig);
const pdResp: PublicDashboard = await getBackendSrv().post(url, publicDashboardConfig);
// Never allow a user to send the orgId
// @ts-ignore
delete pdResp.orgId;
dispatch(notifyApp(createSuccessNotification('Dashboard sharing configuration saved')));
setPublicDashboardConfig(pdResp);
setPublicDashboard(pdResp);
};
// Instance methods
export const dashboardHasTemplateVariables = (variables: VariableModel[]): boolean => {
return variables.length > 0;
};
export const publicDashboardPersisted = (publicDashboard: PublicDashboard): boolean => {
return publicDashboard.uid !== '' && publicDashboard.uid !== undefined;
};
export const generatePublicDashboardUrl = (publicDashboard: PublicDashboard): string => {
return `${window.location.origin}/public-dashboards/${publicDashboard.accessToken}`;
};

View File

@@ -37,6 +37,7 @@ export interface DashboardPageRouteParams {
uid?: string;
type?: string;
slug?: string;
accessToken?: string;
}
export type DashboardPageRouteSearchParams = {
@@ -130,6 +131,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
urlFolderId: queryParams.folderId,
routeName: this.props.route.routeName,
fixUrl: !isPublic,
accessToken: match.params.accessToken,
});
// small delay to start live updates

View File

@@ -361,7 +361,7 @@ export class PanelChrome extends PureComponent<Props, State> {
dashboard.getTimezone(),
timeData,
width,
dashboard.meta.publicDashboardUid
dashboard.meta.publicDashboardAccessToken
);
} else {
// The panel should render on refresh as well if it doesn't have a query, like clock panel

View File

@@ -16,7 +16,7 @@ let panelModel = new PanelModel({
let panelData = createEmptyQueryResponse();
describe('Panel Header', () => {
const dashboardModel = new DashboardModel({}, { isPublic: true });
const dashboardModel = new DashboardModel({}, { publicDashboardAccessToken: 'abc123' });
it('will render header title but not render dropdown icon when dashboard is being viewed publicly', () => {
window.history.pushState({}, 'Test Title', '/public-dashboards/abc123');
@@ -29,7 +29,7 @@ describe('Panel Header', () => {
});
it('will render header title and dropdown icon when dashboard is not being viewed publicly', () => {
const dashboardModel = new DashboardModel({}, { isPublic: false });
const dashboardModel = new DashboardModel({}, { publicDashboardAccessToken: '' });
window.history.pushState({}, 'Test Title', '/d/abc/123');
render(

View File

@@ -59,7 +59,7 @@ export const PanelHeader: FC<Props> = ({ panel, error, isViewing, isEditing, dat
/>
) : null}
<h2 className={styles.titleText}>{title}</h2>
{!dashboard.meta.isPublic && (
{!dashboard.meta.publicDashboardAccessToken && (
<div data-testid="panel-dropdown">
<Icon name="angle-down" className="panel-menu-toggle" />
<PanelHeaderMenuWrapper

View File

@@ -7,7 +7,7 @@ export const getPublicDashboardRoutes = (): RouteDescriptor[] => {
if (config.featureToggles.publicDashboards) {
return [
{
path: '/public-dashboards/:uid',
path: '/public-dashboards/:accessToken',
pageClass: 'page-dashboard',
routeName: DashboardRoutes.Public,
component: SafeDynamicImport(

View File

@@ -20,13 +20,13 @@ export class PublicDashboardDataSource extends DataSourceApi<any> {
* Ideally final -- any other implementation may not work as expected
*/
query(request: DataQueryRequest<any>): Observable<DataQueryResponse> {
const { intervalMs, maxDataPoints, range, requestId, publicDashboardUid, panelId } = request;
const { intervalMs, maxDataPoints, range, requestId, publicDashboardAccessToken, panelId } = request;
let targets = request.targets;
const queries = targets.map((q) => {
return {
...q,
publicDashboardUid,
publicDashboardAccessToken,
intervalMs,
maxDataPoints,
};
@@ -37,7 +37,7 @@ export class PublicDashboardDataSource extends DataSourceApi<any> {
return of({ data: [] });
}
const body: any = { queries, publicDashboardUid, panelId };
const body: any = { queries, publicDashboardAccessToken, panelId };
if (range) {
body.range = range;
@@ -47,7 +47,7 @@ export class PublicDashboardDataSource extends DataSourceApi<any> {
return getBackendSrv()
.fetch<BackendDataSourceResponse>({
url: `/api/public/dashboards/${publicDashboardUid}/panels/${panelId}/query`,
url: `/api/public/dashboards/${publicDashboardAccessToken}/panels/${panelId}/query`,
method: 'POST',
data: body,
requestId,

View File

@@ -319,14 +319,14 @@ export class PanelModel implements DataConfigSource, IPanelModel {
dashboardTimezone: string,
timeData: TimeOverrideResult,
width: number,
publicDashboardUid?: string
publicDashboardAccessToken?: string
) {
this.getQueryRunner().run({
datasource: this.datasource,
queries: this.targets,
panelId: this.id,
dashboardId: dashboardId,
publicDashboardUid,
publicDashboardAccessToken,
timezone: dashboardTimezone,
timeRange: timeData.timeRange,
timeInfo: timeData.timeInfo,

View File

@@ -25,6 +25,7 @@ export interface InitDashboardArgs {
urlSlug?: string;
urlType?: string;
urlFolderId?: string | null;
accessToken?: string;
routeName?: string;
fixUrl: boolean;
}
@@ -61,7 +62,7 @@ async function fetchDashboard(
return dashDTO;
}
case DashboardRoutes.Public: {
return await dashboardLoaderSrv.loadDashboard('public', args.urlSlug, args.urlUid);
return await dashboardLoaderSrv.loadDashboard('public', args.urlSlug, args.accessToken);
}
case DashboardRoutes.Normal: {
const dashDTO: DashboardDTO = await dashboardLoaderSrv.loadDashboard(args.urlType, args.urlSlug, args.urlUid);

View File

@@ -47,7 +47,7 @@ export interface QueryRunnerOptions<
queries: TQuery[];
panelId?: number;
dashboardId?: number;
publicDashboardUid?: string;
publicDashboardAccessToken?: string;
timezone: TimeZone;
timeRange: TimeRange;
timeInfo?: string; // String description of time range for display
@@ -203,7 +203,7 @@ export class PanelQueryRunner {
datasource,
panelId,
dashboardId,
publicDashboardUid,
publicDashboardAccessToken,
timeRange,
timeInfo,
cacheTimeout,
@@ -223,7 +223,7 @@ export class PanelQueryRunner {
timezone,
panelId,
dashboardId,
publicDashboardUid,
publicDashboardAccessToken,
range: timeRange,
timeInfo,
interval: '',
@@ -239,7 +239,7 @@ export class PanelQueryRunner {
(request as any).rangeRaw = timeRange.raw;
try {
const ds = await getDataSource(datasource, request.scopedVars, publicDashboardUid);
const ds = await getDataSource(datasource, request.scopedVars, publicDashboardAccessToken);
const isMixedDS = ds.meta?.mixed;
// Attach the data source to each query
@@ -359,9 +359,9 @@ export class PanelQueryRunner {
async function getDataSource(
datasource: DataSourceRef | string | DataSourceApi | null,
scopedVars: ScopedVars,
publicDashboardUid?: string
publicDashboardAccessToken?: string
): Promise<DataSourceApi> {
if (publicDashboardUid) {
if (publicDashboardAccessToken) {
return new PublicDashboardDataSource();
}

View File

@@ -38,8 +38,7 @@ export interface DashboardMeta {
fromFile?: boolean;
hasUnsavedFolderChange?: boolean;
annotationsPermissions?: AnnotationsPermissions;
isPublic?: boolean;
publicDashboardUid?: string;
publicDashboardAccessToken?: string;
}
export interface AnnotationActions {