mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Public Dashboards: Add audit table (#54508)
This PR adds an audit table for public dashboards allowing a user to view all public dashboards on an instance of grafana. The public dashboards team is working on a proposal for adding RBAC support to the audit table for 9.3 Co-authored-by: juanicabanas <juan.cabanas@grafana.com>
This commit is contained in:
parent
c7c640d903
commit
cc27214dca
@ -140,6 +140,10 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
}
|
||||
|
||||
if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) {
|
||||
// list public dashboards
|
||||
r.Get("/public-dashboards/list", reqSignedIn, hs.Index)
|
||||
|
||||
// anonymous view public dashboard
|
||||
r.Get("/public-dashboards/:accessToken",
|
||||
publicdashboardsapi.SetPublicDashboardFlag,
|
||||
publicdashboardsapi.SetPublicDashboardOrgIdOnContext(hs.PublicDashboardsApi.PublicDashboardService),
|
||||
|
@ -381,6 +381,15 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b
|
||||
Url: s.cfg.AppSubURL + "/library-panels",
|
||||
Icon: "library-panel",
|
||||
})
|
||||
|
||||
if s.features.IsEnabled(featuremgmt.FlagPublicDashboards) {
|
||||
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
|
||||
Text: "Public dashboards",
|
||||
Id: "dashboards/public",
|
||||
Url: s.cfg.AppSubURL + "/dashboard/public",
|
||||
Icon: "library-panel",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if s.features.IsEnabled(featuremgmt.FlagScenes) {
|
||||
|
@ -63,9 +63,11 @@ func (api *Api) RegisterAPIEndpoints() {
|
||||
api.RouteRegister.Get("/api/public/dashboards/:accessToken", routing.Wrap(api.GetPublicDashboard))
|
||||
api.RouteRegister.Post("/api/public/dashboards/:accessToken/panels/:panelId/query", routing.Wrap(api.QueryPublicDashboard))
|
||||
|
||||
// List Public Dashboards
|
||||
api.RouteRegister.Get("/api/dashboards/public", middleware.ReqSignedIn, routing.Wrap(api.ListPublicDashboards))
|
||||
|
||||
// Create/Update Public Dashboard
|
||||
uidScope := dashboards.ScopeDashboardsProvider.GetResourceScopeUID(accesscontrol.Parameter(":uid"))
|
||||
|
||||
api.RouteRegister.Get("/api/dashboards/uid/:uid/public-config",
|
||||
auth(middleware.ReqSignedIn, accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, uidScope)),
|
||||
routing.Wrap(api.GetPublicDashboardConfig))
|
||||
@ -111,6 +113,16 @@ func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response {
|
||||
return response.JSON(http.StatusOK, dto)
|
||||
}
|
||||
|
||||
// Gets list of public dashboards for an org
|
||||
// GET /api/dashboards/public
|
||||
func (api *Api) ListPublicDashboards(c *models.ReqContext) response.Response {
|
||||
resp, err := api.PublicDashboardService.ListPublicDashboards(c.Req.Context(), c.OrgID)
|
||||
if err != nil {
|
||||
return api.handleError(http.StatusInternalServerError, "failed to list public dashboards", err)
|
||||
}
|
||||
return response.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// Gets public dashboard configuration for dashboard
|
||||
// GET /api/dashboards/uid/:uid/public-config
|
||||
func (api *Api) GetPublicDashboardConfig(c *models.ReqContext) response.Response {
|
||||
|
@ -46,30 +46,129 @@ var userViewer = &user.SignedInUser{UserID: 3, OrgID: 1, OrgRole: org.RoleViewer
|
||||
var userViewerRBAC = &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleViewer, Login: "testViewerUserRBAC", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll}}}}
|
||||
var anonymousUser *user.SignedInUser
|
||||
|
||||
type JsonErrResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func TestAPIFeatureFlag(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Name string
|
||||
Method string
|
||||
Path string
|
||||
}{
|
||||
{
|
||||
Name: "API: Load Dashboard",
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/public/dashboards/acbc123",
|
||||
},
|
||||
{
|
||||
Name: "API: Query Dashboard",
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/public/dashboards/abc123/panels/2/query",
|
||||
},
|
||||
{
|
||||
Name: "API: List Dashboards",
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/dashboards/public",
|
||||
},
|
||||
{
|
||||
Name: "API: Get Public Dashboard Config",
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/dashboards/uid/abc123/public-config",
|
||||
},
|
||||
{
|
||||
Name: "API: Upate Public Dashboard",
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/dashboards/uid/abc123/public-config",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
cfg.RBACEnabled = false
|
||||
service := publicdashboards.NewFakePublicDashboardService(t)
|
||||
features := featuremgmt.WithFeatures()
|
||||
testServer := setupTestServer(t, cfg, features, service, nil, userAdmin)
|
||||
response := callAPI(testServer, test.Method, test.Path, nil, t)
|
||||
assert.Equal(t, http.StatusNotFound, response.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIListPublicDashboard(t *testing.T) {
|
||||
successResp := []PublicDashboardListResponse{
|
||||
{
|
||||
Uid: "1234asdfasdf",
|
||||
AccessToken: "asdfasdf",
|
||||
DashboardUid: "abc1234",
|
||||
IsEnabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
Name string
|
||||
User *user.SignedInUser
|
||||
Response []PublicDashboardListResponse
|
||||
ResponseErr error
|
||||
ExpectedHttpResponse int
|
||||
}{
|
||||
{
|
||||
Name: "Anonymous user cannot list dashboards",
|
||||
User: anonymousUser,
|
||||
Response: successResp,
|
||||
ResponseErr: nil,
|
||||
ExpectedHttpResponse: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
Name: "User viewer can see public dashboards",
|
||||
User: userViewer,
|
||||
Response: successResp,
|
||||
ResponseErr: nil,
|
||||
ExpectedHttpResponse: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "Handles Service error",
|
||||
User: userViewer,
|
||||
Response: nil,
|
||||
ResponseErr: errors.New("error, service broken"),
|
||||
ExpectedHttpResponse: http.StatusInternalServerError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
service := publicdashboards.NewFakePublicDashboardService(t)
|
||||
service.On("ListPublicDashboards", mock.Anything, mock.Anything).
|
||||
Return(test.Response, test.ResponseErr).Maybe()
|
||||
|
||||
cfg := setting.NewCfg()
|
||||
cfg.RBACEnabled = false
|
||||
features := featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)
|
||||
testServer := setupTestServer(t, cfg, features, service, nil, test.User)
|
||||
|
||||
response := callAPI(testServer, http.MethodGet, "/api/dashboards/public", nil, t)
|
||||
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
|
||||
|
||||
if test.ExpectedHttpResponse == http.StatusOK {
|
||||
var jsonResp []PublicDashboardListResponse
|
||||
err := json.Unmarshal(response.Body.Bytes(), &jsonResp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, jsonResp[0].Uid, "1234asdfasdf")
|
||||
}
|
||||
|
||||
if test.ResponseErr != nil {
|
||||
var errResp JsonErrResponse
|
||||
err := json.Unmarshal(response.Body.Bytes(), &errResp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "error, service broken", errResp.Error)
|
||||
service.AssertNotCalled(t, "ListPublicDashboards")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIGetPublicDashboard(t *testing.T) {
|
||||
t.Run("It should 404 if featureflag is not enabled", func(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
cfg.RBACEnabled = false
|
||||
service := publicdashboards.NewFakePublicDashboardService(t)
|
||||
service.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
|
||||
Return(&PublicDashboard{}, &models.Dashboard{}, nil).Maybe()
|
||||
service.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).
|
||||
Return(&PublicDashboard{}, nil).Maybe()
|
||||
|
||||
testServer := setupTestServer(t, cfg, featuremgmt.WithFeatures(), service, nil, anonymousUser)
|
||||
|
||||
response := callAPI(testServer, http.MethodGet, "/api/public/dashboards", nil, t)
|
||||
assert.Equal(t, http.StatusNotFound, response.Code)
|
||||
|
||||
response = callAPI(testServer, http.MethodGet, "/api/public/dashboards/asdf", nil, t)
|
||||
assert.Equal(t, http.StatusNotFound, response.Code)
|
||||
|
||||
// control set. make sure routes are mounted
|
||||
testServer = setupTestServer(t, cfg, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), service, nil, userAdmin)
|
||||
response = callAPI(testServer, http.MethodGet, "/api/public/dashboards/asdf", nil, t)
|
||||
assert.NotEqual(t, http.StatusNotFound, response.Code)
|
||||
})
|
||||
|
||||
DashboardUid := "dashboard-abcd1234"
|
||||
token, err := uuid.NewRandom()
|
||||
require.NoError(t, err)
|
||||
@ -138,9 +237,7 @@ func TestAPIGetPublicDashboard(t *testing.T) {
|
||||
assert.Equal(t, false, dashResp.Meta.CanDelete)
|
||||
assert.Equal(t, false, dashResp.Meta.CanSave)
|
||||
} else {
|
||||
var errResp struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
var errResp JsonErrResponse
|
||||
err := json.Unmarshal(response.Body.Bytes(), &errResp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.Err.Error(), errResp.Error)
|
||||
@ -435,12 +532,6 @@ func TestAPIQueryPublicDashboard(t *testing.T) {
|
||||
return testServer, service
|
||||
}
|
||||
|
||||
t.Run("Status code is 404 when feature toggle is disabled", func(t *testing.T) {
|
||||
server, _ := setup(false)
|
||||
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
|
||||
require.Equal(t, http.StatusNotFound, resp.Code)
|
||||
})
|
||||
|
||||
t.Run("Status code is 400 when the panel ID is invalid", func(t *testing.T) {
|
||||
server, _ := setup(true)
|
||||
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/notanumber/query", strings.NewReader("{}"), t)
|
||||
|
@ -37,6 +37,28 @@ func ProvideStore(sqlStore *sqlstore.SQLStore) *PublicDashboardStoreImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// Gets list of public dashboards by orgId
|
||||
func (d *PublicDashboardStoreImpl) ListPublicDashboards(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error) {
|
||||
resp := make([]PublicDashboardListResponse, 0)
|
||||
|
||||
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
sess.Table("dashboard_public").
|
||||
Join("LEFT", "dashboard", "dashboard.uid = dashboard_public.dashboard_uid AND dashboard.org_id = dashboard_public.org_id").
|
||||
Cols("dashboard_public.uid", "dashboard_public.access_token", "dashboard_public.dashboard_uid", "dashboard_public.is_enabled", "dashboard.title").
|
||||
Where("dashboard_public.org_id = ?", orgId).
|
||||
OrderBy("is_enabled DESC, dashboard.title ASC")
|
||||
|
||||
err := sess.Find(&resp)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (d *PublicDashboardStoreImpl) GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) {
|
||||
dashboard := &models.Dashboard{Uid: dashboardUid}
|
||||
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens"
|
||||
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
@ -29,6 +30,39 @@ func TestLogPrefix(t *testing.T) {
|
||||
assert.Equal(t, LogPrefix, "publicdashboards.store")
|
||||
}
|
||||
|
||||
func TestIntegrationListPublicDashboard(t *testing.T) {
|
||||
sqlStore := sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}})
|
||||
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
|
||||
publicdashboardStore := ProvideStore(sqlStore)
|
||||
|
||||
var orgId int64 = 1
|
||||
|
||||
aDash := insertTestDashboard(t, dashboardStore, "a", orgId, 0, true)
|
||||
bDash := insertTestDashboard(t, dashboardStore, "b", orgId, 0, true)
|
||||
cDash := insertTestDashboard(t, dashboardStore, "c", orgId, 0, true)
|
||||
|
||||
// these are in order of how they should be returned from ListPUblicDashboards
|
||||
a := insertPublicDashboard(t, publicdashboardStore, bDash.Uid, orgId, true)
|
||||
b := insertPublicDashboard(t, publicdashboardStore, cDash.Uid, orgId, true)
|
||||
c := insertPublicDashboard(t, publicdashboardStore, aDash.Uid, orgId, false)
|
||||
|
||||
// this is case that can happen as of now, however, postgres and mysql sort
|
||||
// null in the exact opposite fashion and there is no shared syntax to sort
|
||||
// nulls in the same way in all 3 db's.
|
||||
//d := insertPublicDashboard(t, publicdashboardStore, "missing", orgId, false)
|
||||
|
||||
// should not be included in response
|
||||
_ = insertPublicDashboard(t, publicdashboardStore, "wrongOrgId", 777, false)
|
||||
|
||||
resp, err := publicdashboardStore.ListPublicDashboards(context.Background(), orgId)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, resp, 3)
|
||||
assert.Equal(t, resp[0].Uid, a.Uid)
|
||||
assert.Equal(t, resp[1].Uid, b.Uid)
|
||||
assert.Equal(t, resp[2].Uid, c.Uid)
|
||||
}
|
||||
|
||||
func TestIntegrationGetDashboard(t *testing.T) {
|
||||
var sqlStore *sqlstore.SQLStore
|
||||
var dashboardStore *dashboardsDB.DashboardStore
|
||||
@ -506,7 +540,7 @@ func TestIntegrationGetPublicDashboardOrgId(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// helper function insertTestDashboard
|
||||
// helper function to insert a dashboard
|
||||
func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64,
|
||||
folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard {
|
||||
t.Helper()
|
||||
@ -527,3 +561,35 @@ func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardSto
|
||||
dash.Data.Set("uid", dash.Uid)
|
||||
return dash
|
||||
}
|
||||
|
||||
// helper function to insert a public dashboard
|
||||
func insertPublicDashboard(t *testing.T, publicdashboardStore *PublicDashboardStoreImpl, dashboardUid string, orgId int64, isEnabled bool) *PublicDashboard {
|
||||
ctx := context.Background()
|
||||
|
||||
uid, err := publicdashboardStore.GenerateNewPublicDashboardUid(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
accessToken, err := tokens.GenerateAccessToken()
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := SavePublicDashboardConfigCommand{
|
||||
PublicDashboard: PublicDashboard{
|
||||
Uid: uid,
|
||||
DashboardUid: dashboardUid,
|
||||
OrgId: orgId,
|
||||
IsEnabled: isEnabled,
|
||||
TimeSettings: &TimeSettings{},
|
||||
CreatedBy: 1,
|
||||
CreatedAt: time.Now(),
|
||||
AccessToken: accessToken,
|
||||
},
|
||||
}
|
||||
|
||||
err = publicdashboardStore.SavePublicDashboardConfig(ctx, cmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
pubdash, err := publicdashboardStore.GetPublicDashboardByUid(ctx, uid)
|
||||
require.NoError(t, err)
|
||||
|
||||
return pubdash
|
||||
}
|
||||
|
@ -81,6 +81,14 @@ func (pd PublicDashboard) TableName() string {
|
||||
return "dashboard_public"
|
||||
}
|
||||
|
||||
type PublicDashboardListResponse struct {
|
||||
Uid string `json:"uid" xorm:"uid"`
|
||||
AccessToken string `json:"accessToken" xorm:"access_token"`
|
||||
Title string `json:"title" xorm:"title"`
|
||||
DashboardUid string `json:"dashboardUid" xorm:"dashboard_uid"`
|
||||
IsEnabled bool `json:"isEnabled" xorm:"is_enabled"`
|
||||
}
|
||||
|
||||
type TimeSettings struct {
|
||||
From string `json:"from,omitempty"`
|
||||
To string `json:"to,omitempty"`
|
||||
|
@ -212,6 +212,29 @@ func (_m *FakePublicDashboardService) GetQueryDataResponse(ctx context.Context,
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ListPublicDashboards provides a mock function with given fields: ctx, orgId
|
||||
func (_m *FakePublicDashboardService) ListPublicDashboards(ctx context.Context, orgId int64) ([]publicdashboardsmodels.PublicDashboardListResponse, error) {
|
||||
ret := _m.Called(ctx, orgId)
|
||||
|
||||
var r0 []publicdashboardsmodels.PublicDashboardListResponse
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) []publicdashboardsmodels.PublicDashboardListResponse); ok {
|
||||
r0 = rf(ctx, orgId)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]publicdashboardsmodels.PublicDashboardListResponse)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, orgId)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// PublicDashboardEnabled provides a mock function with given fields: ctx, dashboardUid
|
||||
func (_m *FakePublicDashboardService) PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error) {
|
||||
ret := _m.Called(ctx, dashboardUid)
|
||||
|
@ -182,6 +182,29 @@ func (_m *FakePublicDashboardStore) GetPublicDashboardOrgId(ctx context.Context,
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ListPublicDashboards provides a mock function with given fields: ctx, orgId
|
||||
func (_m *FakePublicDashboardStore) ListPublicDashboards(ctx context.Context, orgId int64) ([]publicdashboardsmodels.PublicDashboardListResponse, error) {
|
||||
ret := _m.Called(ctx, orgId)
|
||||
|
||||
var r0 []publicdashboardsmodels.PublicDashboardListResponse
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) []publicdashboardsmodels.PublicDashboardListResponse); ok {
|
||||
r0 = rf(ctx, orgId)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]publicdashboardsmodels.PublicDashboardListResponse)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, orgId)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// PublicDashboardEnabled provides a mock function with given fields: ctx, dashboardUid
|
||||
func (_m *FakePublicDashboardStore) PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error) {
|
||||
ret := _m.Called(ctx, dashboardUid)
|
||||
|
@ -22,6 +22,7 @@ type Service interface {
|
||||
GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error)
|
||||
GetPublicDashboardOrgId(ctx context.Context, accessToken string) (int64, error)
|
||||
GetQueryDataResponse(ctx context.Context, skipCache bool, reqDTO PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error)
|
||||
ListPublicDashboards(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error)
|
||||
PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error)
|
||||
SavePublicDashboardConfig(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error)
|
||||
}
|
||||
@ -35,6 +36,7 @@ type Store interface {
|
||||
GetPublicDashboardByUid(ctx context.Context, uid string) (*PublicDashboard, error)
|
||||
GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error)
|
||||
GetPublicDashboardOrgId(ctx context.Context, accessToken string) (int64, error)
|
||||
ListPublicDashboards(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error)
|
||||
PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error)
|
||||
SavePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) error
|
||||
UpdatePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) error
|
||||
|
@ -54,6 +54,12 @@ func ProvideService(
|
||||
}
|
||||
}
|
||||
|
||||
// Gets a list of public dashboards by orgId
|
||||
func (pd *PublicDashboardServiceImpl) ListPublicDashboards(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error) {
|
||||
return pd.store.ListPublicDashboards(ctx, orgId)
|
||||
}
|
||||
|
||||
// Gets a dashboard by Uid
|
||||
func (pd *PublicDashboardServiceImpl) GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) {
|
||||
dashboard, err := pd.store.GetDashboard(ctx, dashboardUid)
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
import React, { FC, ReactNode, useContext, useEffect } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
useForceUpdate,
|
||||
Tag,
|
||||
ToolbarButtonRow,
|
||||
ModalsContext,
|
||||
} from '@grafana/ui';
|
||||
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
|
||||
import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbarSeparator';
|
||||
@ -51,6 +52,7 @@ export interface OwnProps {
|
||||
hideTimePicker: boolean;
|
||||
folderTitle?: string;
|
||||
title: string;
|
||||
shareModalActiveTab?: string;
|
||||
onAddPanel: () => void;
|
||||
}
|
||||
|
||||
@ -76,6 +78,7 @@ type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
export const DashNav = React.memo<Props>((props) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const { chrome } = useGrafana();
|
||||
const { showModal, hideModal } = useContext(ModalsContext);
|
||||
|
||||
// We don't really care about the event payload here only that it triggeres a re-render of this component
|
||||
useBusEvent(props.dashboard.events, DashboardMetaChangedEvent);
|
||||
@ -128,6 +131,25 @@ export const DashNav = React.memo<Props>((props) => {
|
||||
return playlistSrv.isPlaying;
|
||||
};
|
||||
|
||||
// Open/Close
|
||||
useEffect(() => {
|
||||
const dashboard = props.dashboard;
|
||||
const shareModalActiveTab = props.shareModalActiveTab;
|
||||
const { canShare } = dashboard.meta;
|
||||
|
||||
if (canShare && shareModalActiveTab) {
|
||||
// automagically open modal
|
||||
showModal(ShareModal, {
|
||||
dashboard,
|
||||
onDismiss: hideModal,
|
||||
activeTab: shareModalActiveTab,
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
hideModal();
|
||||
};
|
||||
}, [showModal, hideModal, props.dashboard, props.shareModalActiveTab]);
|
||||
|
||||
const renderLeftActions = () => {
|
||||
const { dashboard, kioskMode } = props;
|
||||
const { canStar, canShare, isStarred } = dashboard.meta;
|
||||
|
@ -28,15 +28,16 @@ export function addPanelShareTab(tab: ShareModalTabModel) {
|
||||
}
|
||||
|
||||
function getInitialState(props: Props): State {
|
||||
const tabs = getTabs(props);
|
||||
const { tabs, activeTab } = getTabs(props);
|
||||
|
||||
return {
|
||||
tabs,
|
||||
activeTab: tabs[0].value,
|
||||
activeTab,
|
||||
};
|
||||
}
|
||||
|
||||
function getTabs(props: Props) {
|
||||
const { panel } = props;
|
||||
const { panel, activeTab } = props;
|
||||
|
||||
const linkLabel = t('share-modal.tab-title.link', 'Link');
|
||||
const tabs: ShareModalTabModel[] = [{ label: linkLabel, value: 'link', component: ShareLink }];
|
||||
@ -65,12 +66,18 @@ function getTabs(props: Props) {
|
||||
tabs.push({ label: 'Public dashboard', value: 'share', component: SharePublicDashboard });
|
||||
}
|
||||
|
||||
return tabs;
|
||||
const at = tabs.find((t) => t.value === activeTab);
|
||||
|
||||
return {
|
||||
tabs,
|
||||
activeTab: at?.value ?? tabs[0].value,
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
dashboard: DashboardModel;
|
||||
panel?: PanelModel;
|
||||
activeTab?: string;
|
||||
|
||||
onDismiss(): void;
|
||||
}
|
||||
@ -95,7 +102,7 @@ export class ShareModal extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
getTabs() {
|
||||
return getTabs(this.props);
|
||||
return getTabs(this.props).tabs;
|
||||
}
|
||||
|
||||
getActiveTab() {
|
||||
@ -107,13 +114,12 @@ export class ShareModal extends React.Component<Props, State> {
|
||||
const { panel } = this.props;
|
||||
const { activeTab } = this.state;
|
||||
const title = panel ? t('share-modal.panel.title', 'Share Panel') : t('share-modal.dashboard.title', 'Share');
|
||||
const tabs = this.getTabs();
|
||||
|
||||
return (
|
||||
<ModalTabsHeader
|
||||
title={title}
|
||||
icon="share-alt"
|
||||
tabs={tabs}
|
||||
tabs={this.getTabs()}
|
||||
activeTab={activeTab}
|
||||
onChangeTab={this.onSelectTab}
|
||||
/>
|
||||
|
@ -18,7 +18,7 @@ import { configureStore } from 'app/store/configureStore';
|
||||
import { ShareModal } from '../ShareModal';
|
||||
|
||||
const server = setupServer(
|
||||
rest.get('/api/dashboards/uid/:uId/public-config', (req, res, ctx) => {
|
||||
rest.get('/api/dashboards/uid/:uId/public-config', (_, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
|
@ -4,8 +4,8 @@ import { VariableModel } from 'app/features/variables/types';
|
||||
import {
|
||||
PublicDashboard,
|
||||
dashboardHasTemplateVariables,
|
||||
generatePublicDashboardUrl,
|
||||
publicDashboardPersisted,
|
||||
generatePublicDashboardUrl,
|
||||
} from './SharePublicDashboardUtils';
|
||||
|
||||
describe('dashboardHasTemplateVariables', () => {
|
||||
|
@ -49,6 +49,7 @@ export type DashboardPageRouteSearchParams = {
|
||||
editPanel?: string;
|
||||
viewPanel?: string;
|
||||
editview?: string;
|
||||
shareView?: string;
|
||||
panelType?: string;
|
||||
inspect?: string;
|
||||
from?: string;
|
||||
@ -352,6 +353,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
onAddPanel={this.onAddPanel}
|
||||
kioskMode={kioskMode}
|
||||
hideTimePicker={dashboard.timepicker.hidden}
|
||||
shareModalActiveTab={this.props.queryParams.shareView}
|
||||
/>
|
||||
</header>
|
||||
);
|
||||
|
@ -6,6 +6,17 @@ import { DashboardRoutes } from '../../types';
|
||||
export const getPublicDashboardRoutes = (): RouteDescriptor[] => {
|
||||
if (config.featureToggles.publicDashboards) {
|
||||
return [
|
||||
{
|
||||
path: '/dashboard/public',
|
||||
pageClass: 'page-dashboard',
|
||||
routeName: DashboardRoutes.Public,
|
||||
component: SafeDynamicImport(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "ListPublicDashboardPage" */ '../../features/manage-dashboards/PublicDashboardListPage'
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/public-dashboards/:accessToken',
|
||||
pageClass: 'page-dashboard',
|
||||
|
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
import { ListPublicDashboardTable } from './components/PublicDashboardListTable';
|
||||
|
||||
export const ListPublicDashboardPage = ({}) => {
|
||||
return (
|
||||
<Page navId="dashboards/public">
|
||||
<Page.Contents>
|
||||
<ListPublicDashboardTable />
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListPublicDashboardPage;
|
@ -0,0 +1,54 @@
|
||||
import {
|
||||
LIST_PUBLIC_DASHBOARD_URL,
|
||||
viewPublicDashboardUrl,
|
||||
//ListPublicDashboardTable,
|
||||
} from './PublicDashboardListTable';
|
||||
|
||||
//import { render, screen, waitFor, act } from '@testing-library/react';
|
||||
//import React from 'react';
|
||||
|
||||
describe('listPublicDashboardsUrl', () => {
|
||||
it('has the correct url', () => {
|
||||
expect(LIST_PUBLIC_DASHBOARD_URL).toEqual('/api/dashboards/public');
|
||||
});
|
||||
});
|
||||
|
||||
describe('viewPublicDashboardUrl', () => {
|
||||
it('has the correct url', () => {
|
||||
expect(viewPublicDashboardUrl('abcd')).toEqual('public-dashboards/abcd');
|
||||
});
|
||||
});
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
||||
getBackendSrv: () => ({
|
||||
get: jest.fn().mockResolvedValue([
|
||||
{
|
||||
uid: 'SdZwuCZVz',
|
||||
accessToken: 'beeaf92f6ab3467f80b2be922c7741ab',
|
||||
title: 'New dashboardasdf',
|
||||
dashboardUid: 'iF36Qb6nz',
|
||||
isEnabled: false,
|
||||
},
|
||||
{
|
||||
uid: 'EuiEbd3nz',
|
||||
accessToken: '8687b0498ccf4babb2f92810d8563b33',
|
||||
title: 'New dashboard',
|
||||
dashboardUid: 'kFlxbd37k',
|
||||
isEnabled: true,
|
||||
},
|
||||
]),
|
||||
}),
|
||||
}));
|
||||
|
||||
//describe('ListPublicDashboardTable', () => {
|
||||
// test('renders properly', async() => {
|
||||
// act(() => {
|
||||
// render(<ListPublicDashboardTable />)
|
||||
// });
|
||||
|
||||
// //await waitFor(() => screen.getByRole('table'));
|
||||
// expect(screen.getByText("Dashboard")).toBeInTheDocument();
|
||||
// //expect(screen.getAllByRole("tr")).toHaveLength(2);
|
||||
// })
|
||||
//})
|
@ -0,0 +1,94 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useState } from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { Link, ButtonGroup, LinkButton, Icon, Tag, useStyles2 } from '@grafana/ui';
|
||||
import { getConfig } from 'app/core/config';
|
||||
|
||||
export interface ListPublicDashboardResponse {
|
||||
uid: string;
|
||||
accessToken: string;
|
||||
dashboardUid: string;
|
||||
title: string;
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
export const LIST_PUBLIC_DASHBOARD_URL = `/api/dashboards/public`;
|
||||
export const getPublicDashboards = async (): Promise<ListPublicDashboardResponse[]> => {
|
||||
return getBackendSrv().get(LIST_PUBLIC_DASHBOARD_URL);
|
||||
};
|
||||
|
||||
export const viewPublicDashboardUrl = (accessToken: string): string => {
|
||||
return `${getConfig().appUrl}public-dashboards/${accessToken}`;
|
||||
};
|
||||
|
||||
export const ListPublicDashboardTable = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [publicDashboards, setPublicDashboards] = useState<ListPublicDashboardResponse[]>([]);
|
||||
|
||||
useAsync(async () => {
|
||||
const publicDashboards = await getPublicDashboards();
|
||||
setPublicDashboards(publicDashboards);
|
||||
}, [setPublicDashboards]);
|
||||
|
||||
return (
|
||||
<div className="page-action-bar">
|
||||
<table className="filter-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Dashboard</th>
|
||||
<th>Public dashboard enabled</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{publicDashboards.map((pd) => (
|
||||
<tr key={pd.uid}>
|
||||
<td>
|
||||
<Link className={styles.link} href={`/d/${pd.dashboardUid}`}>
|
||||
{pd.title}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Tag name={pd.isEnabled ? 'enabled' : 'disabled'} colorIndex={pd.isEnabled ? 20 : 15} />
|
||||
</td>
|
||||
<td>
|
||||
<ButtonGroup>
|
||||
<LinkButton
|
||||
href={viewPublicDashboardUrl(pd.accessToken)}
|
||||
fill="text"
|
||||
title={pd.isEnabled ? 'View public dashboard' : 'Public dashboard is disabled'}
|
||||
target="_blank"
|
||||
disabled={!pd.isEnabled}
|
||||
>
|
||||
<Icon name="external-link-alt" />
|
||||
</LinkButton>
|
||||
|
||||
<LinkButton
|
||||
fill="text"
|
||||
href={`/d/${pd.dashboardUid}?shareView=share`}
|
||||
title="Configure public dashboard"
|
||||
>
|
||||
<Icon name="cog" />
|
||||
</LinkButton>
|
||||
</ButtonGroup>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
link: css`
|
||||
color: ${theme.colors.primary.text};
|
||||
text-decoration: underline;
|
||||
margin-right: ${theme.spacing()};
|
||||
`,
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user