Public Dashboards: Query Caching (#51403)

* passes id and uid to PublicDashboardDatasource

* betterer results

* If for a public dashboard, return the PublicDashboardDataSource first or else getDatasourceSrv.get() will fail bc of no authed user.

Added some unit tests for resolving the uid from the many possible datasource types.

* updates betterer

* Exports DashboardService. Adds method to DashboardService to build anonymous user for use with public dashboards where there is no authed user. Adds method on dashboard_queries to get all dashboard uids from a dashboard.

* refactors to get unique datasource uids

* Adds tests for getting all unique datasource uids off a dashboard

* adds test for building anonymous user with read and query actions that are scoped to each datasource uid in the dashboard

* updates casing of DashboardService

* updates test case to have additional panel with a different datasource

* gives default interval to public dashboard data source
pull/51844/head
owensmallwood 3 years ago committed by GitHub
parent 9941e06e22
commit 0b4af38bfa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 56
      .betterer.results
  2. 1
      packages/grafana-data/src/types/datasource.ts
  3. 34
      packages/grafana-runtime/src/utils/PublicDashboardDataSource.test.ts
  4. 6
      pkg/api/annotations.go
  5. 4
      pkg/api/annotations_test.go
  6. 2
      pkg/api/api.go
  7. 2
      pkg/api/common_test.go
  8. 18
      pkg/api/dashboard.go
  9. 2
      pkg/api/dashboard_permission.go
  10. 2
      pkg/api/dashboard_permission_test.go
  11. 12
      pkg/api/dashboard_public.go
  12. 12
      pkg/api/dashboard_public_test.go
  13. 14
      pkg/api/dashboard_test.go
  14. 2
      pkg/api/dtos/models.go
  15. 2
      pkg/api/folder_permission.go
  16. 2
      pkg/api/folder_permission_test.go
  17. 4
      pkg/api/http_server.go
  18. 4
      pkg/api/index.go
  19. 2
      pkg/api/playlist_play.go
  20. 8
      pkg/api/preferences.go
  21. 4
      pkg/api/preferences_test.go
  22. 2
      pkg/api/stars.go
  23. 18
      pkg/models/dashboard_queries.go
  24. 103
      pkg/models/dashboard_queries_test.go
  25. 1
      pkg/services/dashboards/dashboard.go
  26. 23
      pkg/services/dashboards/dashboard_service_mock.go
  27. 23
      pkg/services/dashboards/service/dashboard_public.go
  28. 26
      pkg/services/dashboards/service/dashboard_public_test.go
  29. 30
      public/app/features/dashboard/services/PublicDashboardDataSource.ts
  30. 2
      public/app/features/query/state/PanelQueryRunner.ts

@ -1,5 +1,5 @@
// BETTERER RESULTS V2.
//
//
// If this file contains merge conflicts, use `betterer merge` to automatically resolve them:
// https://phenomnomnominal.github.io/betterer/docs/results-file/#merge
//
@ -131,6 +131,37 @@ exports[`better eslint`] = {
"e2e/dashboards-suite/dashboard-templating.spec.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"packages/grafana-data/src/types/datasource.ts:1730680024": [
[24, 85, 3, "Unexpected any. Specify a different type.", "193409811"],
[27, 73, 3, "Unexpected any. Specify a different type.", "193409811"],
[46, 28, 3, "Unexpected any. Specify a different type.", "193409811"],
[51, 26, 3, "Unexpected any. Specify a different type.", "193409811"],
[56, 47, 3, "Unexpected any. Specify a different type.", "193409811"],
[99, 46, 3, "Unexpected any. Specify a different type.", "193409811"],
[109, 48, 3, "Unexpected any. Specify a different type.", "193409811"],
[151, 14, 3, "Unexpected any. Specify a different type.", "193409811"],
[152, 25, 3, "Unexpected any. Specify a different type.", "193409811"],
[153, 24, 3, "Unexpected any. Specify a different type.", "193409811"],
[172, 72, 3, "Unexpected any. Specify a different type.", "193409811"],
[246, 37, 3, "Unexpected any. Specify a different type.", "193409811"],
[260, 41, 3, "Unexpected any. Specify a different type.", "193409811"],
[260, 57, 3, "Unexpected any. Specify a different type.", "193409811"],
[270, 26, 3, "Unexpected any. Specify a different type.", "193409811"],
[270, 41, 3, "Unexpected any. Specify a different type.", "193409811"],
[275, 24, 3, "Unexpected any. Specify a different type.", "193409811"],
[280, 25, 3, "Unexpected any. Specify a different type.", "193409811"],
[317, 21, 3, "Unexpected any. Specify a different type.", "193409811"],
[319, 32, 3, "Unexpected any. Specify a different type.", "193409811"],
[382, 14, 3, "Unexpected any. Specify a different type.", "193409811"],
[408, 14, 3, "Unexpected any. Specify a different type.", "193409811"],
[414, 58, 3, "Unexpected any. Specify a different type.", "193409811"],
[608, 13, 3, "Unexpected any. Specify a different type.", "193409811"],
[618, 37, 3, "Unexpected any. Specify a different type.", "193409811"],
[618, 42, 3, "Unexpected any. Specify a different type.", "193409811"],
[619, 43, 3, "Unexpected any. Specify a different type.", "193409811"],
[619, 59, 3, "Unexpected any. Specify a different type.", "193409811"],
[625, 46, 3, "Unexpected any. Specify a different type.", "193409811"],
[626, 22, 3, "Unexpected any. Specify a different type.", "193409811"]
"e2e/dashboards-suite/textbox-variables.spec.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
@ -693,6 +724,11 @@ exports[`better eslint`] = {
"packages/grafana-data/src/types/flot.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"packages/grafana-runtime/src/utils/PublicDashboardDataSource.test.ts:2092121707": [
[14, 19, 123, "Do not use any type assertions.", "3028355264"],
[14, 19, 109, "Do not use any type assertions.", "4248357345"],
[39, 13, 206, "Do not use any type assertions.", "1200376833"],
[72, 49, 54, "Do not use any type assertions.", "114713672"]
"packages/grafana-data/src/types/graph.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
@ -4333,6 +4369,13 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
],
"public/app/features/dashboard/services/PublicDashboardDataSource.ts:102072381": [
[14, 61, 3, "Unexpected any. Specify a different type.", "193409811"],
[20, 12, 16, "Do not use any type assertions.", "1747412709"],
[43, 34, 3, "Unexpected any. Specify a different type.", "193409811"],
[61, 16, 3, "Unexpected any. Specify a different type.", "193409811"],
[78, 45, 22, "Do not use any type assertions.", "1838499175"],
[86, 28, 3, "Unexpected any. Specify a different type.", "193409811"]
"public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
@ -5639,6 +5682,17 @@ exports[`better eslint`] = {
"public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/query/state/PanelQueryRunner.ts:3311920590": [
[110, 39, 118, "Do not use any type assertions.", "2873233307"],
[189, 46, 3, "Unexpected any. Specify a different type.", "193409811"],
[238, 5, 14, "Do not use any type assertions.", "4095749936"],
[238, 16, 3, "Unexpected any. Specify a different type.", "193409811"],
[285, 20, 46, "Do not use any type assertions.", "1712789723"],
[285, 20, 32, "Do not use any type assertions.", "2220885232"],
[367, 21, 17, "Do not use any type assertions.", "1733699692"],
[367, 35, 3, "Unexpected any. Specify a different type.", "193409811"],
[368, 11, 27, "Do not use any type assertions.", "2133479311"],
[371, 38, 20, "Do not use any type assertions.", "340150831"]
"public/app/features/query/state/DashboardQueryRunner/LegacyAnnotationQueryRunner.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],

@ -483,7 +483,6 @@ export interface DataQueryRequest<TQuery extends DataQuery = DataQuery> {
timeInfo?: string; // The query time description (blue text in the upper right)
panelId?: number;
dashboardId?: number;
// Temporary prop for public dashboards, to be replaced by publicAccessKey
publicDashboardAccessToken?: string;
// Request Timing

@ -1,9 +1,14 @@
import { of } from 'rxjs';
import { BackendSrv, BackendSrvRequest } from 'src/services';
import { DataQueryRequest, DataSourceRef } from '@grafana/data';
import { DataQueryRequest, DataSourceInstanceSettings, DataSourceRef } from '@grafana/data';
import { PublicDashboardDataSource } from '../../../../public/app/features/dashboard/services/PublicDashboardDataSource';
import {
PUBLIC_DATASOURCE,
PublicDashboardDataSource,
} from '../../../../public/app/features/dashboard/services/PublicDashboardDataSource';
import { DataSourceWithBackend } from './DataSourceWithBackend';
const mockDatasourceRequest = jest.fn();
@ -28,7 +33,7 @@ describe('PublicDashboardDatasource', () => {
mockDatasourceRequest.mockReset();
mockDatasourceRequest.mockReturnValue(Promise.resolve({}));
const ds = new PublicDashboardDataSource();
const ds = new PublicDashboardDataSource('public');
const panelId = 1;
const publicDashboardAccessToken = 'abc123';
@ -47,4 +52,27 @@ describe('PublicDashboardDatasource', () => {
`/api/public/dashboards/${publicDashboardAccessToken}/panels/${panelId}/query`
);
});
test('returns public datasource uid when datasource passed in is null', () => {
let ds = new PublicDashboardDataSource(null);
expect(ds.uid).toBe(PUBLIC_DATASOURCE);
});
test('returns datasource when datasource passed in is a string', () => {
let ds = new PublicDashboardDataSource('theDatasourceUid');
expect(ds.uid).toBe('theDatasourceUid');
});
test('returns datasource uid when datasource passed in is a DataSourceRef implementation', () => {
const datasource = { type: 'datasource', uid: 'abc123' };
let ds = new PublicDashboardDataSource(datasource);
expect(ds.uid).toBe('abc123');
});
test('returns datasource uid when datasource passed in is a DatasourceApi instance', () => {
const settings: DataSourceInstanceSettings = { id: 1, uid: 'abc123' } as DataSourceInstanceSettings;
const datasource = new DataSourceWithBackend(settings);
let ds = new PublicDashboardDataSource(datasource);
expect(ds.uid).toBe('abc123');
});
});

@ -53,7 +53,7 @@ func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response {
item.DashboardUID = val
} else {
query := models.GetDashboardQuery{Id: item.DashboardId, OrgId: c.OrgId}
err := hs.dashboardService.GetDashboard(c.Req.Context(), &query)
err := hs.DashboardService.GetDashboard(c.Req.Context(), &query)
if err == nil && query.Result != nil {
item.DashboardUID = &query.Result.Uid
dashboardCache[item.DashboardId] = &query.Result.Uid
@ -82,7 +82,7 @@ func (hs *HTTPServer) PostAnnotation(c *models.ReqContext) response.Response {
// overwrite dashboardId when dashboardUID is not empty
if cmd.DashboardUID != "" {
query := models.GetDashboardQuery{OrgId: c.OrgId, Uid: cmd.DashboardUID}
err := hs.dashboardService.GetDashboard(c.Req.Context(), &query)
err := hs.DashboardService.GetDashboard(c.Req.Context(), &query)
if err == nil {
cmd.DashboardId = query.Result.Id
}
@ -291,7 +291,7 @@ func (hs *HTTPServer) MassDeleteAnnotations(c *models.ReqContext) response.Respo
if cmd.DashboardUID != "" {
query := models.GetDashboardQuery{OrgId: c.OrgId, Uid: cmd.DashboardUID}
err := hs.dashboardService.GetDashboard(c.Req.Context(), &query)
err := hs.DashboardService.GetDashboard(c.Req.Context(), &query)
if err == nil {
cmd.DashboardId = query.Result.Id
}

@ -343,7 +343,7 @@ func postAnnotationScenario(t *testing.T, desc string, url string, routePattern
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
hs := setupSimpleHTTPServer(nil)
hs.SQLStore = store
hs.dashboardService = dashSvc
hs.DashboardService = dashSvc
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
@ -428,7 +428,7 @@ func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePatte
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
hs := setupSimpleHTTPServer(nil)
hs.SQLStore = store
hs.dashboardService = dashSvc
hs.DashboardService = dashSvc
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {

@ -28,7 +28,7 @@ func (hs *HTTPServer) registerRoutes() {
reqGrafanaAdmin := middleware.ReqGrafanaAdmin
reqEditorRole := middleware.ReqEditorRole
reqOrgAdmin := middleware.ReqOrgAdmin
reqOrgAdminDashOrFolderAdminOrTeamAdmin := middleware.OrgAdminDashOrFolderAdminOrTeamAdmin(hs.SQLStore, hs.dashboardService)
reqOrgAdminDashOrFolderAdminOrTeamAdmin := middleware.OrgAdminDashOrFolderAdminOrTeamAdmin(hs.SQLStore, hs.DashboardService)
reqCanAccessTeams := middleware.AdminOrEditorAndFeatureEnabled(hs.Cfg.EditorsCanAdmin)
reqSnapshotPublicModeOrSignedIn := middleware.SnapshotPublicModeOrSignedIn(hs.Cfg)
redirectFromLegacyPanelEditURL := middleware.RedirectFromLegacyPanelEditURL(hs.Cfg)

@ -396,7 +396,7 @@ func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessCo
AccessControl: ac,
teamPermissionsService: teamPermissionService,
searchUsersService: searchusers.ProvideUsersService(db, filters.ProvideOSSSearchUserFilter()),
dashboardService: dashboardservice.ProvideDashboardService(
DashboardService: dashboardservice.ProvideDashboardService(
cfg, dashboardsStore, nil, features,
accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.NewMockedPermissionsService(), ac,
),

@ -144,7 +144,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
// lookup folder title
if dash.FolderId > 0 {
query := models.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId}
if err := hs.dashboardService.GetDashboard(c.Req.Context(), &query); err != nil {
if err := hs.DashboardService.GetDashboard(c.Req.Context(), &query); err != nil {
if errors.Is(err, dashboards.ErrFolderNotFound) {
return response.Error(404, "Folder not found", err)
}
@ -235,7 +235,7 @@ func (hs *HTTPServer) getDashboardHelper(ctx context.Context, orgID int64, id in
query = models.GetDashboardQuery{Id: id, OrgId: orgID}
}
if err := hs.dashboardService.GetDashboard(ctx, &query); err != nil {
if err := hs.DashboardService.GetDashboard(ctx, &query); err != nil {
return nil, response.Error(404, "Dashboard not found", err)
}
@ -262,7 +262,7 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response {
hs.log.Error("Failed to disconnect library elements", "dashboard", dash.Id, "user", c.SignedInUser.UserId, "error", err)
}
err = hs.dashboardService.DeleteDashboard(c.Req.Context(), dash.Id, c.OrgId)
err = hs.DashboardService.DeleteDashboard(c.Req.Context(), dash.Id, c.OrgId)
if err != nil {
var dashboardErr dashboards.DashboardErr
if ok := errors.As(err, &dashboardErr); ok {
@ -387,7 +387,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
Overwrite: cmd.Overwrite,
}
dashboard, err := hs.dashboardService.SaveDashboard(alerting.WithUAEnabled(ctx, hs.Cfg.UnifiedAlerting.IsEnabled()), dashItem, allowUiUpdate)
dashboard, err := hs.DashboardService.SaveDashboard(alerting.WithUAEnabled(ctx, hs.Cfg.UnifiedAlerting.IsEnabled()), dashItem, allowUiUpdate)
if dashboard != nil && hs.entityEventsService != nil {
if err := hs.entityEventsService.SaveEvent(ctx, store.SaveEventCmd{
@ -460,7 +460,7 @@ func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response {
if preference.HomeDashboardID != 0 {
slugQuery := models.GetDashboardRefByIdQuery{Id: preference.HomeDashboardID}
err := hs.dashboardService.GetDashboardUIDById(c.Req.Context(), &slugQuery)
err := hs.DashboardService.GetDashboardUIDById(c.Req.Context(), &slugQuery)
if err == nil {
url := models.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug)
dashRedirect := dtos.DashboardRedirect{RedirectUri: url}
@ -546,7 +546,7 @@ func (hs *HTTPServer) GetDashboardVersions(c *models.ReqContext) response.Respon
OrgId: c.SignedInUser.OrgId,
Uid: dashUID,
}
if err := hs.dashboardService.GetDashboard(c.Req.Context(), &q); err != nil {
if err := hs.DashboardService.GetDashboard(c.Req.Context(), &q); err != nil {
return response.Error(http.StatusBadRequest, "failed to get dashboard by UID", err)
}
dashID = q.Result.Id
@ -605,7 +605,7 @@ func (hs *HTTPServer) GetDashboardVersion(c *models.ReqContext) response.Respons
OrgId: c.SignedInUser.OrgId,
Uid: dashUID,
}
if err := hs.dashboardService.GetDashboard(c.Req.Context(), &q); err != nil {
if err := hs.DashboardService.GetDashboard(c.Req.Context(), &q); err != nil {
return response.Error(http.StatusBadRequest, "failed to get dashboard by UID", err)
}
dashID = q.Result.Id
@ -782,7 +782,7 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *models.ReqContext) response.Res
func (hs *HTTPServer) GetDashboardTags(c *models.ReqContext) {
query := models.GetDashboardTagsQuery{OrgId: c.OrgId}
err := hs.dashboardService.GetDashboardTags(c.Req.Context(), &query)
err := hs.DashboardService.GetDashboardTags(c.Req.Context(), &query)
if err != nil {
c.JsonApiErr(500, "Failed to get tags from database", err)
return
@ -803,7 +803,7 @@ func (hs *HTTPServer) GetDashboardUIDs(c *models.ReqContext) {
continue
}
q.Id = id
err = hs.dashboardService.GetDashboardUIDById(c.Req.Context(), q)
err = hs.DashboardService.GetDashboardUIDById(c.Req.Context(), q)
if err != nil {
continue
}

@ -143,7 +143,7 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response.
return response.Success("Dashboard permissions updated")
}
if err := hs.dashboardService.UpdateDashboardACL(c.Req.Context(), dashID, items); err != nil {
if err := hs.DashboardService.UpdateDashboardACL(c.Req.Context(), dashID, items); err != nil {
if errors.Is(err, models.ErrDashboardAclInfoMissing) ||
errors.Is(err, models.ErrDashboardPermissionDashboardEmpty) {
return response.Error(409, err.Error(), err)

@ -39,7 +39,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) {
Cfg: settings,
SQLStore: mockSQLStore,
Features: features,
dashboardService: dashboardservice.ProvideDashboardService(
DashboardService: dashboardservice.ProvideDashboardService(
settings, dashboardStore, nil, features, folderPermissions, dashboardPermissions, ac,
),
AccessControl: accesscontrolmock.New().WithDisabled(),

@ -19,7 +19,7 @@ import (
func (hs *HTTPServer) GetPublicDashboard(c *models.ReqContext) response.Response {
accessToken := web.Params(c.Req)[":accessToken"]
dash, err := hs.dashboardService.GetPublicDashboard(c.Req.Context(), accessToken)
dash, err := hs.DashboardService.GetPublicDashboard(c.Req.Context(), accessToken)
if err != nil {
return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard", err)
}
@ -47,7 +47,7 @@ func (hs *HTTPServer) GetPublicDashboard(c *models.ReqContext) response.Response
// gets public dashboard configuration for dashboard
func (hs *HTTPServer) GetPublicDashboardConfig(c *models.ReqContext) response.Response {
pdc, err := hs.dashboardService.GetPublicDashboardConfig(c.Req.Context(), c.OrgId, web.Params(c.Req)[":uid"])
pdc, err := hs.DashboardService.GetPublicDashboardConfig(c.Req.Context(), c.OrgId, web.Params(c.Req)[":uid"])
if err != nil {
return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard config", err)
}
@ -71,7 +71,7 @@ func (hs *HTTPServer) SavePublicDashboardConfig(c *models.ReqContext) response.R
PublicDashboard: pubdash,
}
pubdash, 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)
}
@ -87,17 +87,17 @@ func (hs *HTTPServer) QueryPublicDashboard(c *models.ReqContext) response.Respon
return response.Error(http.StatusBadRequest, "invalid panel ID", err)
}
dashboard, err := hs.dashboardService.GetPublicDashboard(c.Req.Context(), web.Params(c.Req)[":accessToken"])
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)
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(
reqDTO, err := hs.DashboardService.BuildPublicDashboardMetricRequest(
c.Req.Context(),
dashboard,
publicDashboard,

@ -38,7 +38,7 @@ func TestAPIGetPublicDashboard(t *testing.T) {
dashSvc := dashboards.NewFakeDashboardService(t)
dashSvc.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
Return(&models.Dashboard{}, nil).Maybe()
sc.hs.dashboardService = dashSvc
sc.hs.DashboardService = dashSvc
setInitCtxSignedInViewer(sc.initCtx)
response := callAPI(
@ -97,7 +97,7 @@ func TestAPIGetPublicDashboard(t *testing.T) {
dashSvc := dashboards.NewFakeDashboardService(t)
dashSvc.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
Return(test.publicDashboardResult, test.publicDashboardErr)
sc.hs.dashboardService = dashSvc
sc.hs.DashboardService = dashSvc
setInitCtxSignedInViewer(sc.initCtx)
response := callAPI(
@ -170,7 +170,7 @@ func TestAPIGetPublicDashboardConfig(t *testing.T) {
dashSvc := dashboards.NewFakeDashboardService(t)
dashSvc.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).
Return(test.PublicDashboardResult, test.PublicDashboardError)
sc.hs.dashboardService = dashSvc
sc.hs.DashboardService = dashSvc
setInitCtxSignedInViewer(sc.initCtx)
response := callAPI(
@ -229,7 +229,7 @@ func TestApiSavePublicDashboardConfig(t *testing.T) {
dashSvc := dashboards.NewFakeDashboardService(t)
dashSvc.On("SavePublicDashboardConfig", mock.Anything, mock.AnythingOfType("*dashboards.SavePublicDashboardConfigDTO")).
Return(&models.PublicDashboard{IsEnabled: true}, test.saveDashboardError)
sc.hs.dashboardService = dashSvc
sc.hs.DashboardService = dashSvc
setInitCtxSignedInViewer(sc.initCtx)
response := callAPI(
@ -298,7 +298,7 @@ func TestAPIQueryPublicDashboard(t *testing.T) {
return SetupAPITestServer(t, func(hs *HTTPServer) {
hs.queryDataService = qds
hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards, enabled)
hs.dashboardService = fakeDashboardService
hs.DashboardService = fakeDashboardService
}), fakeDashboardService
}
@ -593,7 +593,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T)
},
}
pubdash, err := scenario.hs.dashboardService.SavePublicDashboardConfig(context.Background(), savePubDashboardCmd)
pubdash, err := scenario.hs.DashboardService.SavePublicDashboardConfig(context.Background(), savePubDashboardCmd)
require.NoError(t, err)
response := callAPI(

@ -139,7 +139,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
SQLStore: mockSQLStore,
AccessControl: accesscontrolmock.New(),
Features: featuremgmt.WithFeatures(),
dashboardService: dashboardService,
DashboardService: dashboardService,
dashboardVersionService: fakeDashboardVersionService,
}
hs.CoremodelStaticRegistry, hs.CoremodelRegistry = setupDashboardCoremodel(t)
@ -259,7 +259,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
LibraryElementService: &mockLibraryElementService{},
SQLStore: mockSQLStore,
AccessControl: accesscontrolmock.New(),
dashboardService: dashboardService,
DashboardService: dashboardService,
dashboardVersionService: fakeDashboardVersionService,
}
hs.CoremodelStaticRegistry, hs.CoremodelRegistry = setupDashboardCoremodel(t)
@ -900,7 +900,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
dashboardProvisioningService: mockDashboardProvisioningService{},
SQLStore: mockSQLStore,
AccessControl: accesscontrolmock.New(),
dashboardService: dashboardService,
DashboardService: dashboardService,
}
hs.CoremodelStaticRegistry, hs.CoremodelRegistry = setupDashboardCoremodel(t)
hs.callGetDashboard(sc)
@ -954,7 +954,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
cfg, dashboardStore, nil, features,
folderPermissions, dashboardPermissions, ac,
),
dashboardService: dashboardService,
DashboardService: dashboardService,
}
hs.CoremodelStaticRegistry, hs.CoremodelRegistry = setupDashboardCoremodel(t)
@ -986,7 +986,7 @@ func (hs *HTTPServer) callGetDashboardVersions(sc *scenarioContext) {
func (hs *HTTPServer) callDeleteDashboardByUID(t *testing.T,
sc *scenarioContext, mockDashboard *dashboards.FakeDashboardService) {
hs.dashboardService = mockDashboard
hs.DashboardService = mockDashboard
sc.handlerFunc = hs.DeleteDashboardByUID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
}
@ -1018,7 +1018,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
pluginStore: &fakePluginStore{},
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
dashboardService: dashboardService,
DashboardService: dashboardService,
folderService: folderService,
Features: featuremgmt.WithFeatures(),
}
@ -1088,7 +1088,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
QuotaService: &quota.QuotaService{Cfg: cfg},
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
dashboardService: mock,
DashboardService: mock,
SQLStore: sqlStore,
Features: featuremgmt.WithFeatures(),
dashboardVersionService: fakeDashboardVersionService,

@ -70,6 +70,8 @@ type MetricRequest struct {
// required: false
Debug bool `json:"debug"`
PublicDashboardAccessToken string `json:"publicDashboardAccessToken"`
HTTPRequest *http.Request `json:"-"`
}

@ -126,7 +126,7 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext) response.Res
return response.Success("Dashboard permissions updated")
}
if err := hs.dashboardService.UpdateDashboardACL(c.Req.Context(), folder.Id, items); err != nil {
if err := hs.DashboardService.UpdateDashboardACL(c.Req.Context(), folder.Id, items); err != nil {
if errors.Is(err, models.ErrDashboardAclInfoMissing) {
err = models.ErrFolderAclInfoMissing
}

@ -42,7 +42,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
folderService: folderService,
folderPermissionsService: folderPermissions,
dashboardPermissionsService: dashboardPermissions,
dashboardService: service.ProvideDashboardService(
DashboardService: service.ProvideDashboardService(
settings, dashboardStore, nil, features, folderPermissions, dashboardPermissions, ac,
),
AccessControl: accesscontrolmock.New().WithDisabled(),

@ -148,7 +148,7 @@ type HTTPServer struct {
authenticator loginpkg.Authenticator
teamPermissionsService accesscontrol.TeamPermissionsService
NotificationService *notifications.NotificationService
dashboardService dashboards.DashboardService
DashboardService dashboards.DashboardService
dashboardProvisioningService dashboards.DashboardProvisioningService
folderService dashboards.FolderService
DatasourcePermissionsService permissions.DatasourcePermissionsService
@ -267,7 +267,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
authInfoService: authInfoService,
authenticator: authenticator,
NotificationService: notificationService,
dashboardService: dashboardService,
DashboardService: dashboardService,
dashboardProvisioningService: dashboardProvisioningService,
folderService: folderService,
DatasourcePermissionsService: datasourcePermissionsService,

@ -407,7 +407,7 @@ func (hs *HTTPServer) buildStarredItemsNavLinks(c *models.ReqContext, prefs *pre
Id: dashboardId,
OrgId: c.OrgId,
}
err := hs.dashboardService.GetDashboard(c.Req.Context(), query)
err := hs.DashboardService.GetDashboard(c.Req.Context(), query)
if err == nil {
starredDashboards = append(starredDashboards, query.Result)
}
@ -674,7 +674,7 @@ func (hs *HTTPServer) buildAdminNavLinks(c *models.ReqContext) []*dtos.NavLink {
func (hs *HTTPServer) editorInAnyFolder(c *models.ReqContext) bool {
hasEditPermissionInFoldersQuery := models.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser}
if err := hs.dashboardService.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery); err != nil {
if err := hs.DashboardService.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery); err != nil {
return false
}
return hasEditPermissionInFoldersQuery.Result

@ -16,7 +16,7 @@ func (hs *HTTPServer) populateDashboardsByID(ctx context.Context, dashboardByIDs
if len(dashboardByIDs) > 0 {
dashboardQuery := models.GetDashboardsQuery{DashboardIds: dashboardByIDs}
if err := hs.dashboardService.GetDashboards(ctx, &dashboardQuery); err != nil {
if err := hs.DashboardService.GetDashboards(ctx, &dashboardQuery); err != nil {
return result, err
}

@ -31,7 +31,7 @@ func (hs *HTTPServer) SetHomeDashboard(c *models.ReqContext) response.Response {
dashboardID := cmd.HomeDashboardID
if cmd.HomeDashboardUID != nil {
query := models.GetDashboardQuery{Uid: *cmd.HomeDashboardUID}
err := hs.dashboardService.GetDashboard(c.Req.Context(), &query)
err := hs.DashboardService.GetDashboard(c.Req.Context(), &query)
if err != nil {
return response.Error(404, "Dashboard not found", err)
}
@ -65,7 +65,7 @@ func (hs *HTTPServer) getPreferencesFor(ctx context.Context, orgID, userID, team
// when homedashboardID is 0, that means it is the default home dashboard, no UID would be returned in the response
if preference.HomeDashboardID != 0 {
query := models.GetDashboardQuery{Id: preference.HomeDashboardID, OrgId: orgID}
err = hs.dashboardService.GetDashboard(ctx, &query)
err = hs.DashboardService.GetDashboard(ctx, &query)
if err == nil {
dashboardUID = query.Result.Uid
}
@ -105,7 +105,7 @@ func (hs *HTTPServer) updatePreferencesFor(ctx context.Context, orgID, userID, t
dashboardID := dtoCmd.HomeDashboardID
if dtoCmd.HomeDashboardUID != nil {
query := models.GetDashboardQuery{Uid: *dtoCmd.HomeDashboardUID, OrgId: orgID}
err := hs.dashboardService.GetDashboard(ctx, &query)
err := hs.DashboardService.GetDashboard(ctx, &query)
if err != nil {
return response.Error(404, "Dashboard not found", err)
}
@ -151,7 +151,7 @@ func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, te
dashboardID := dtoCmd.HomeDashboardID
if dtoCmd.HomeDashboardUID != nil {
query := models.GetDashboardQuery{Uid: *dtoCmd.HomeDashboardUID, OrgId: orgID}
err := hs.dashboardService.GetDashboard(ctx, &query)
err := hs.DashboardService.GetDashboard(ctx, &query)
if err != nil {
return response.Error(404, "Dashboard not found", err)
}

@ -40,7 +40,7 @@ func TestAPIEndpoint_GetCurrentOrgPreferences_LegacyAccessControl(t *testing.T)
q.Result = &models.Dashboard{Uid: "home", Id: 1}
}).Return(nil)
sc.hs.dashboardService = dashSvc
sc.hs.DashboardService = dashSvc
prefService := preftest.NewPreferenceServiceFake()
prefService.ExpectedPreference = &pref.Preference{HomeDashboardID: 1, Theme: "dark"}
@ -169,7 +169,7 @@ func TestAPIEndpoint_PatchUserPreferences(t *testing.T) {
q := args.Get(1).(*models.GetDashboardQuery)
q.Result = &models.Dashboard{Uid: "home", Id: 1}
}).Return(nil)
sc.hs.dashboardService = dashSvc
sc.hs.DashboardService = dashSvc
t.Run("Returns 200 on success", func(t *testing.T) {
response := callAPI(sc.server, http.MethodPatch, patchUserPreferencesUrl, input, t)
assert.Equal(t, http.StatusOK, response.Code)

@ -26,7 +26,7 @@ func (hs *HTTPServer) GetStars(c *models.ReqContext) response.Response {
Id: dashboardId,
OrgId: c.OrgId,
}
err := hs.dashboardService.GetDashboard(c.Req.Context(), query)
err := hs.DashboardService.GetDashboard(c.Req.Context(), query)
// Grafana admin users may have starred dashboards in multiple orgs. This will avoid returning errors when the dashboard is in another org
if err == nil {

@ -4,7 +4,23 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
)
func GetQueriesFromDashboard(dashboard *simplejson.Json) map[int64][]*simplejson.Json {
func GetUniqueDashboardDatasourceUids(dashboard *simplejson.Json) []string {
var datasourceUids []string
exists := map[string]bool{}
for _, panelObj := range dashboard.Get("panels").MustArray() {
panel := simplejson.NewFromAny(panelObj)
uid := panel.Get("datasource").Get("uid").MustString()
if _, ok := exists[uid]; !ok {
datasourceUids = append(datasourceUids, uid)
exists[uid] = true
}
}
return datasourceUids
}
func GroupQueriesByPanelId(dashboard *simplejson.Json) map[int64][]*simplejson.Json {
result := make(map[int64][]*simplejson.Json)
for _, panelObj := range dashboard.Get("panels").MustArray() {

@ -56,6 +56,79 @@ const (
"schemaVersion": 35
}`
dashboardWithDuplicateDatasources = `
{
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "abc123"
},
"id": 1,
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "abc123"
},
"exemplar": true,
"expr": "go_goroutines{job=\"$job\"}",
"interval": "",
"legendFormat": "",
"refId": "A"
}
],
"title": "Panel Title",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "_yxMP8Ynk"
},
"id": 2,
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "_yxMP8Ynk"
},
"exemplar": true,
"expr": "go_goroutines{job=\"$job\"}",
"interval": "",
"legendFormat": "",
"refId": "A"
}
],
"title": "Panel Title",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "_yxMP8Ynk"
},
"id": 3,
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "_yxMP8Ynk"
},
"exemplar": true,
"expr": "go_goroutines{job=\"$job\"}",
"interval": "",
"legendFormat": "",
"refId": "A"
}
],
"title": "Panel Title",
"type": "timeseries"
}
],
"schemaVersion": 35
}`
oldStyleDashboard = `
{
"panels": [
@ -79,12 +152,32 @@ const (
}`
)
func TestGetQueriesFromDashboard(t *testing.T) {
func TestGetUniqueDashboardDatasourceUids(t *testing.T) {
t.Run("can get unique datasource ids from dashboard", func(t *testing.T) {
json, err := simplejson.NewJson([]byte(dashboardWithDuplicateDatasources))
require.NoError(t, err)
uids := GetUniqueDashboardDatasourceUids(json)
require.Len(t, uids, 2)
require.Equal(t, "abc123", uids[0])
require.Equal(t, "_yxMP8Ynk", uids[1])
})
t.Run("can get no datasource uids from empty dashboard", func(t *testing.T) {
json, err := simplejson.NewJson([]byte(`{"panels": {}}`))
require.NoError(t, err)
uids := GetUniqueDashboardDatasourceUids(json)
require.Len(t, uids, 0)
})
}
func TestGroupQueriesByPanelId(t *testing.T) {
t.Run("can extract no queries from empty dashboard", func(t *testing.T) {
json, err := simplejson.NewJson([]byte(`{"panels": {}}`))
require.NoError(t, err)
queries := GetQueriesFromDashboard(json)
queries := GroupQueriesByPanelId(json)
require.Len(t, queries, 0)
})
@ -92,7 +185,7 @@ func TestGetQueriesFromDashboard(t *testing.T) {
json, err := simplejson.NewJson([]byte(dashboardWithNoQueries))
require.NoError(t, err)
queries := GetQueriesFromDashboard(json)
queries := GroupQueriesByPanelId(json)
require.Len(t, queries, 1)
require.Contains(t, queries, int64(2))
require.Len(t, queries[2], 0)
@ -102,7 +195,7 @@ func TestGetQueriesFromDashboard(t *testing.T) {
json, err := simplejson.NewJson([]byte(dashboardWithQueries))
require.NoError(t, err)
queries := GetQueriesFromDashboard(json)
queries := GroupQueriesByPanelId(json)
require.Len(t, queries, 1)
require.Contains(t, queries, int64(2))
require.Len(t, queries[2], 2)
@ -138,7 +231,7 @@ func TestGetQueriesFromDashboard(t *testing.T) {
json, err := simplejson.NewJson([]byte(oldStyleDashboard))
require.NoError(t, err)
queries := GetQueriesFromDashboard(json)
queries := GroupQueriesByPanelId(json)
require.Len(t, queries, 1)
require.Contains(t, queries, int64(2))
require.Len(t, queries[2], 1)

@ -11,6 +11,7 @@ import (
// DashboardService is a service for operating on dashboards.
type DashboardService interface {
BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *models.PublicDashboard, panelId int64) (dtos.MetricRequest, error)
BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*models.SignedInUser, 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)

@ -18,6 +18,29 @@ type FakeDashboardService struct {
mock.Mock
}
// BuildAnonymousUser provides a mock function with given fields: ctx, dashboard
func (_m *FakeDashboardService) BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*models.SignedInUser, error) {
ret := _m.Called(ctx, dashboard)
var r0 *models.SignedInUser
if rf, ok := ret.Get(0).(func(context.Context, *models.Dashboard) *models.SignedInUser); ok {
r0 = rf(ctx, dashboard)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.SignedInUser)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *models.Dashboard) error); ok {
r1 = rf(ctx, dashboard)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// BuildPublicDashboardMetricRequest provides a mock function with given fields: ctx, dashboard, publicDashboard, panelId
func (_m *FakeDashboardService) BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *models.PublicDashboard, panelId int64) (dtos.MetricRequest, error) {
ret := _m.Called(ctx, dashboard, publicDashboard, panelId)

@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
)
// Gets public dashboard via access token
@ -124,7 +125,7 @@ func (dr *DashboardServiceImpl) BuildPublicDashboardMetricRequest(ctx context.Co
return dtos.MetricRequest{}, dashboards.ErrPublicDashboardNotFound
}
queriesByPanel := models.GetQueriesFromDashboard(dashboard.Data)
queriesByPanel := models.GroupQueriesByPanelId(dashboard.Data)
if _, ok := queriesByPanel[panelId]; !ok {
return dtos.MetricRequest{}, dashboards.ErrPublicDashboardPanelNotFound
@ -139,6 +140,26 @@ func (dr *DashboardServiceImpl) BuildPublicDashboardMetricRequest(ctx context.Co
}, nil
}
// BuildAnonymousUser creates a user with permissions to read from all datasources used in the dashboard
func (dr *DashboardServiceImpl) BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*models.SignedInUser, error) {
datasourceUids := models.GetUniqueDashboardDatasourceUids(dashboard.Data)
// 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)
queryScopes := make([]string, 0)
readScopes := make([]string, 0)
for _, uid := range datasourceUids {
queryScopes = append(queryScopes, fmt.Sprintf("datasources:uid:%s", uid))
readScopes = append(readScopes, fmt.Sprintf("datasources:uid:%s", uid))
}
permissions[datasources.ActionQuery] = queryScopes
permissions[datasources.ActionRead] = readScopes
anonymousUser.Permissions[dashboard.OrgId] = permissions
return anonymousUser, nil
}
// generates a uuid formatted without dashes to use as access token
func GenerateAccessToken() (string, error) {
token, err := uuid.NewRandom()

@ -314,6 +314,26 @@ func TestUpdatePublicDashboard(t *testing.T) {
})
}
func TestBuildAnonymousUser(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,
}
t.Run("will add datasource read and query permissions to user for each datasource in dashboard", func(t *testing.T) {
user, err := service.BuildAnonymousUser(context.Background(), dashboard)
require.NoError(t, err)
require.Equal(t, dashboard.OrgId, user.OrgId)
require.Equal(t, "datasources:uid:ds1", user.Permissions[user.OrgId]["datasources:query"][0])
require.Equal(t, "datasources:uid:ds3", user.Permissions[user.OrgId]["datasources:query"][1])
require.Equal(t, "datasources:uid:ds1", user.Permissions[user.OrgId]["datasources:read"][0])
require.Equal(t, "datasources:uid:ds3", user.Permissions[user.OrgId]["datasources:read"][1])
})
}
func TestBuildPublicDashboardMetricRequest(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := database.ProvideDashboardStore(sqlStore)
@ -425,6 +445,9 @@ func insertTestDashboard(t *testing.T, dashboardStore *database.DashboardStore,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"datasource": map[string]interface{}{
"uid": "ds1",
},
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
@ -444,6 +467,9 @@ func insertTestDashboard(t *testing.T, dashboardStore *database.DashboardStore,
},
map[string]interface{}{
"id": 2,
"datasource": map[string]interface{}{
"uid": "ds3",
},
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{

@ -1,19 +1,41 @@
import { catchError, Observable, of, switchMap } from 'rxjs';
import { DataQuery, DataQueryRequest, DataQueryResponse, DataSourceApi, PluginMeta } from '@grafana/data';
import {
DataQuery,
DataQueryRequest,
DataQueryResponse,
DataSourceApi,
DataSourceRef,
PluginMeta,
} from '@grafana/data';
import { BackendDataSourceResponse, getBackendSrv, toDataQueryResponse } from '@grafana/runtime';
export const PUBLIC_DATASOURCE = '-- Public --';
export class PublicDashboardDataSource extends DataSourceApi<any> {
constructor() {
constructor(datasource: DataSourceRef | string | DataSourceApi | null) {
super({
name: 'public-ds',
id: 1,
id: 0,
type: 'public-ds',
meta: {} as PluginMeta,
uid: '1',
uid: PublicDashboardDataSource.resolveUid(datasource),
jsonData: {},
access: 'proxy',
});
this.interval = '1min';
}
/**
* Get the datasource uid based on the many types a datasource can be.
*/
private static resolveUid(datasource: DataSourceRef | string | DataSourceApi | null): string {
if (typeof datasource === 'string') {
return datasource;
}
return datasource?.uid ?? PUBLIC_DATASOURCE;
}
/**

@ -362,7 +362,7 @@ async function getDataSource(
publicDashboardAccessToken?: string
): Promise<DataSourceApi> {
if (publicDashboardAccessToken) {
return new PublicDashboardDataSource();
return new PublicDashboardDataSource(datasource);
}
if (datasource && (datasource as any).query) {

Loading…
Cancel
Save