Public Dashboards: Pubdash panels get data from pubdash api (#50556)

* Public dashboard query API

* Create new API on service for building metric request

* Flesh out testing, implement BuildPublicDashboardMetricRequest

* Test for errors and missing panels

* WIP: Test for multiple datasources

* Refactor tests, add supporting code for multiple datasources

* Gets the panel data from the pubdash query api

* Adds tests to make sure we get the correct api url from retrieving panel data

* Public dashboard query API

* Create new API on service for building metric request

* Flesh out testing, implement BuildPublicDashboardMetricRequest

* Test for errors and missing panels

* WIP: Test for multiple datasources

* Refactor tests, add supporting code for multiple datasources

* Handle queries from multiple datasources

* Replace dashboard time range with pubdash time range settings

* Fix comments from review, build failure

* removes changes to DataSourceWithBackend.ts regarding getting the pubdash panel query url. Going to do this in a new class, PublicDashboardDataSource.ts

* Include pubdash Uid in dashboard meta

* Creates new PublicDashboardDataSource.ts and adds test

* Passes pubdash uid down to PanelQueryRunner.ts to a PublicDashboardDatasource can be chosen when were looking at a public dashboard

* removes comment

* checks for error when unmarshalling json

* Only replace dashboard time settings with pubdash time settings when pubdash time settings exist

* formatting and added comment

Co-authored-by: Jesse Weaver <jesse.weaver@grafana.com>
Co-authored-by: Jeff Levin <jeff@levinology.com>
This commit is contained in:
owensmallwood 2022-06-13 18:03:43 -06:00 committed by GitHub
parent 0371884cdd
commit 1bb2d2599c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 199 additions and 20 deletions

View File

@ -477,6 +477,8 @@ 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
publicDashboardUid?: string;
// Request Timing
startTime: number;

View File

@ -0,0 +1,48 @@
import { of } from 'rxjs';
import { BackendSrv, BackendSrvRequest } from 'src/services';
import { DataQueryRequest, DataSourceRef } from '@grafana/data';
import { PublicDashboardDataSource } from '../../../../public/app/features/dashboard/services/PublicDashboardDataSource';
const mockDatasourceRequest = jest.fn();
const backendSrv = {
fetch: (options: BackendSrvRequest) => {
return of(mockDatasourceRequest(options));
},
} as unknown as BackendSrv;
jest.mock('../services', () => ({
...(jest.requireActual('../services') as any),
getBackendSrv: () => backendSrv,
getDataSourceSrv: () => {
return {
getInstanceSettings: (ref?: DataSourceRef) => ({ type: ref?.type ?? '?', uid: ref?.uid ?? '?' }),
};
},
}));
describe('PublicDashboardDatasource', () => {
test('Fetches results from the pubdash query endpoint', () => {
mockDatasourceRequest.mockReset();
mockDatasourceRequest.mockReturnValue(Promise.resolve({}));
const ds = new PublicDashboardDataSource();
const panelId = 1;
const publicDashboardUid = 'abc123';
ds.query({
maxDataPoints: 10,
intervalMs: 5000,
targets: [{ refId: 'A' }, { refId: 'B', datasource: { type: 'sample' } }],
panelId,
publicDashboardUid,
} 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`);
});
});

View File

@ -14,25 +14,28 @@ import (
// gets public dashboard
func (hs *HTTPServer) GetPublicDashboard(c *models.ReqContext) response.Response {
dash, err := hs.dashboardService.GetPublicDashboard(c.Req.Context(), web.Params(c.Req)[":uid"])
publicDashboardUid := web.Params(c.Req)[":uid"]
dash, err := hs.dashboardService.GetPublicDashboard(c.Req.Context(), publicDashboardUid)
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,
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,
}
dto := dtos.DashboardFullWithMeta{Meta: meta, Dashboard: dash.Data}
@ -88,6 +91,7 @@ func (hs *HTTPServer) QueryPublicDashboard(c *models.ReqContext) response.Respon
}
resp, err := hs.queryDataService.QueryDataMultipleSources(c.Req.Context(), nil, c.SkipCache, reqDTO, true)
if err != nil {
return hs.handleQueryMetricsError(err)
}

View File

@ -34,6 +34,7 @@ type DashboardMeta struct {
ProvisionedExternalId string `json:"provisionedExternalId"`
AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"`
IsPublic bool `json:"isPublic"`
PublicDashboardUid string `json:"publicDashboardUid"`
}
type AnnotationPermission struct {
Dashboard AnnotationActions `json:"dashboard"`

View File

@ -25,7 +25,16 @@ func (dr *DashboardServiceImpl) GetPublicDashboard(ctx context.Context, dashboar
return nil, models.ErrPublicDashboardNotFound
}
// FIXME maybe insert logic to substitute pdc.TimeSettings into d
// 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)
}
return d, nil
}

View File

@ -36,6 +36,19 @@ func TestGetPublicDashboard(t *testing.T) {
errResp: nil,
dashResp: &models.Dashboard{IsPublic: true},
},
{
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: "returns ErrPublicDashboardNotFound when isPublic is false",
uid: "abc123",
@ -224,6 +237,7 @@ func TestBuildPublicDashboardMetricRequest(t *testing.T) {
pdc.PublicDashboard.Uid,
49,
)
require.ErrorContains(t, err, "Panel not found")
})

View File

@ -354,6 +354,10 @@ func (dr *DashboardServiceImpl) DeleteDashboard(ctx context.Context, dashboardId
return dr.deleteDashboard(ctx, dashboardId, orgId, true)
}
func (dr *DashboardServiceImpl) GetDashboardByPublicUid(ctx context.Context, dashboardPublicUid string) (*models.Dashboard, error) {
return nil, nil
}
func (dr *DashboardServiceImpl) MakeUserAdmin(ctx context.Context, orgID int64, userID int64, dashboardID int64, setViewAndEditPermissions bool) error {
rtEditor := models.ROLE_EDITOR
rtViewer := models.ROLE_VIEWER

View File

@ -444,6 +444,10 @@ func (s *dashboardServiceMock) DeleteDashboard(_ context.Context, dashboardId in
return nil
}
func (s *dashboardServiceMock) GetDashboardByPublicUid(ctx context.Context, dashboardPublicUid string) (*models.Dashboard, error) {
return nil, nil
}
type scenarioInput struct {
storedPluginSettings []*pluginsettings.DTO
installedPlugins []plugins.PluginDTO

View File

@ -338,7 +338,7 @@ export class PanelChrome extends PureComponent<Props, State> {
}
onRefresh = () => {
const { panel, isInView, width } = this.props;
const { dashboard, panel, isInView, width } = this.props;
if (!isInView) {
this.setState({ refreshWhenInView: true });
@ -356,7 +356,13 @@ export class PanelChrome extends PureComponent<Props, State> {
if (this.state.refreshWhenInView) {
this.setState({ refreshWhenInView: false });
}
panel.runAllPanelQueries(this.props.dashboard.id, this.props.dashboard.getTimezone(), timeData, width);
panel.runAllPanelQueries(
dashboard.id,
dashboard.getTimezone(),
timeData,
width,
dashboard.meta.publicDashboardUid
);
} else {
// The panel should render on refresh as well if it doesn't have a query, like clock panel
this.setState({

View File

@ -0,0 +1,68 @@
import { catchError, Observable, of, switchMap } from 'rxjs';
import { DataQuery, DataQueryRequest, DataQueryResponse, DataSourceApi, PluginMeta } from '@grafana/data';
import { BackendDataSourceResponse, getBackendSrv, toDataQueryResponse } from '@grafana/runtime';
export class PublicDashboardDataSource extends DataSourceApi<any> {
constructor() {
super({
name: 'public-ds',
id: 1,
type: 'public-ds',
meta: {} as PluginMeta,
uid: '1',
jsonData: {},
access: 'proxy',
});
}
/**
* Ideally final -- any other implementation may not work as expected
*/
query(request: DataQueryRequest<any>): Observable<DataQueryResponse> {
const { intervalMs, maxDataPoints, range, requestId, publicDashboardUid, panelId } = request;
let targets = request.targets;
const queries = targets.map((q) => {
return {
...q,
publicDashboardUid,
intervalMs,
maxDataPoints,
};
});
// Return early if no queries exist
if (!queries.length) {
return of({ data: [] });
}
const body: any = { queries, publicDashboardUid, panelId };
if (range) {
body.range = range;
body.from = range.from.valueOf().toString();
body.to = range.to.valueOf().toString();
}
return getBackendSrv()
.fetch<BackendDataSourceResponse>({
url: `/api/public/dashboards/${publicDashboardUid}/panels/${panelId}/query`,
method: 'POST',
data: body,
requestId,
})
.pipe(
switchMap((raw) => {
return of(toDataQueryResponse(raw, queries as DataQuery[]));
}),
catchError((err) => {
return of(toDataQueryResponse(err));
})
);
}
testDatasource(): Promise<any> {
return Promise.resolve(null);
}
}

View File

@ -304,12 +304,19 @@ export class PanelModel implements DataConfigSource, IPanelModel {
this.gridPos.h = newPos.h;
}
runAllPanelQueries(dashboardId: number, dashboardTimezone: string, timeData: TimeOverrideResult, width: number) {
runAllPanelQueries(
dashboardId: number,
dashboardTimezone: string,
timeData: TimeOverrideResult,
width: number,
publicDashboardUid?: string
) {
this.getQueryRunner().run({
datasource: this.datasource,
queries: this.targets,
panelId: this.id,
dashboardId: dashboardId,
publicDashboardUid,
timezone: dashboardTimezone,
timeRange: timeData.timeRange,
timeInfo: timeData.timeInfo,

View File

@ -32,6 +32,7 @@ import { isStreamingDataFrame } from 'app/features/live/data/utils';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { isSharedDashboardQuery, runSharedRequest } from '../../../plugins/datasource/dashboard';
import { PublicDashboardDataSource } from '../../dashboard/services/PublicDashboardDataSource';
import { PanelModel } from '../../dashboard/state';
import { getDashboardQueryRunner } from './DashboardQueryRunner/DashboardQueryRunner';
@ -46,6 +47,7 @@ export interface QueryRunnerOptions<
queries: TQuery[];
panelId?: number;
dashboardId?: number;
publicDashboardUid?: string;
timezone: TimeZone;
timeRange: TimeRange;
timeInfo?: string; // String description of time range for display
@ -201,6 +203,7 @@ export class PanelQueryRunner {
datasource,
panelId,
dashboardId,
publicDashboardUid,
timeRange,
timeInfo,
cacheTimeout,
@ -220,6 +223,7 @@ export class PanelQueryRunner {
timezone,
panelId,
dashboardId,
publicDashboardUid,
range: timeRange,
timeInfo,
interval: '',
@ -235,8 +239,9 @@ export class PanelQueryRunner {
(request as any).rangeRaw = timeRange.raw;
try {
const ds = await getDataSource(datasource, request.scopedVars);
const ds = await getDataSource(datasource, request.scopedVars, publicDashboardUid);
const isMixedDS = ds.meta?.mixed;
// Attach the data source to each query
request.targets = request.targets.map((query) => {
const isExpressionQuery = query.datasource?.type === ExpressionDatasourceRef.type;
@ -353,10 +358,16 @@ export class PanelQueryRunner {
async function getDataSource(
datasource: DataSourceRef | string | DataSourceApi | null,
scopedVars: ScopedVars
scopedVars: ScopedVars,
publicDashboardUid?: string
): Promise<DataSourceApi> {
if (publicDashboardUid) {
return new PublicDashboardDataSource();
}
if (datasource && (datasource as any).query) {
return datasource as DataSourceApi;
}
return await getDatasourceSrv().get(datasource as string, scopedVars);
}

View File

@ -39,6 +39,7 @@ export interface DashboardMeta {
hasUnsavedFolderChange?: boolean;
annotationsPermissions?: AnnotationsPermissions;
isPublic?: boolean;
publicDashboardUid?: string;
}
export interface AnnotationActions {