mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -477,6 +477,8 @@ export interface DataQueryRequest<TQuery extends DataQuery = DataQuery> {
|
|||||||
timeInfo?: string; // The query time description (blue text in the upper right)
|
timeInfo?: string; // The query time description (blue text in the upper right)
|
||||||
panelId?: number;
|
panelId?: number;
|
||||||
dashboardId?: number;
|
dashboardId?: number;
|
||||||
|
// Temporary prop for public dashboards, to be replaced by publicAccessKey
|
||||||
|
publicDashboardUid?: string;
|
||||||
|
|
||||||
// Request Timing
|
// Request Timing
|
||||||
startTime: number;
|
startTime: number;
|
||||||
|
|||||||
@@ -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`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,25 +14,28 @@ import (
|
|||||||
|
|
||||||
// gets public dashboard
|
// gets public dashboard
|
||||||
func (hs *HTTPServer) GetPublicDashboard(c *models.ReqContext) response.Response {
|
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 {
|
if err != nil {
|
||||||
return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard", err)
|
return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
meta := dtos.DashboardMeta{
|
meta := dtos.DashboardMeta{
|
||||||
Slug: dash.Slug,
|
Slug: dash.Slug,
|
||||||
Type: models.DashTypeDB,
|
Type: models.DashTypeDB,
|
||||||
CanStar: false,
|
CanStar: false,
|
||||||
CanSave: false,
|
CanSave: false,
|
||||||
CanEdit: false,
|
CanEdit: false,
|
||||||
CanAdmin: false,
|
CanAdmin: false,
|
||||||
CanDelete: false,
|
CanDelete: false,
|
||||||
Created: dash.Created,
|
Created: dash.Created,
|
||||||
Updated: dash.Updated,
|
Updated: dash.Updated,
|
||||||
Version: dash.Version,
|
Version: dash.Version,
|
||||||
IsFolder: false,
|
IsFolder: false,
|
||||||
FolderId: dash.FolderId,
|
FolderId: dash.FolderId,
|
||||||
IsPublic: dash.IsPublic,
|
IsPublic: dash.IsPublic,
|
||||||
|
PublicDashboardUid: publicDashboardUid,
|
||||||
}
|
}
|
||||||
|
|
||||||
dto := dtos.DashboardFullWithMeta{Meta: meta, Dashboard: dash.Data}
|
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)
|
resp, err := hs.queryDataService.QueryDataMultipleSources(c.Req.Context(), nil, c.SkipCache, reqDTO, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return hs.handleQueryMetricsError(err)
|
return hs.handleQueryMetricsError(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ type DashboardMeta struct {
|
|||||||
ProvisionedExternalId string `json:"provisionedExternalId"`
|
ProvisionedExternalId string `json:"provisionedExternalId"`
|
||||||
AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"`
|
AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"`
|
||||||
IsPublic bool `json:"isPublic"`
|
IsPublic bool `json:"isPublic"`
|
||||||
|
PublicDashboardUid string `json:"publicDashboardUid"`
|
||||||
}
|
}
|
||||||
type AnnotationPermission struct {
|
type AnnotationPermission struct {
|
||||||
Dashboard AnnotationActions `json:"dashboard"`
|
Dashboard AnnotationActions `json:"dashboard"`
|
||||||
|
|||||||
@@ -25,7 +25,16 @@ func (dr *DashboardServiceImpl) GetPublicDashboard(ctx context.Context, dashboar
|
|||||||
return nil, models.ErrPublicDashboardNotFound
|
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
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,19 @@ func TestGetPublicDashboard(t *testing.T) {
|
|||||||
errResp: nil,
|
errResp: nil,
|
||||||
dashResp: &models.Dashboard{IsPublic: true},
|
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",
|
name: "returns ErrPublicDashboardNotFound when isPublic is false",
|
||||||
uid: "abc123",
|
uid: "abc123",
|
||||||
@@ -224,6 +237,7 @@ func TestBuildPublicDashboardMetricRequest(t *testing.T) {
|
|||||||
pdc.PublicDashboard.Uid,
|
pdc.PublicDashboard.Uid,
|
||||||
49,
|
49,
|
||||||
)
|
)
|
||||||
|
|
||||||
require.ErrorContains(t, err, "Panel not found")
|
require.ErrorContains(t, err, "Panel not found")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -354,6 +354,10 @@ func (dr *DashboardServiceImpl) DeleteDashboard(ctx context.Context, dashboardId
|
|||||||
return dr.deleteDashboard(ctx, dashboardId, orgId, true)
|
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 {
|
func (dr *DashboardServiceImpl) MakeUserAdmin(ctx context.Context, orgID int64, userID int64, dashboardID int64, setViewAndEditPermissions bool) error {
|
||||||
rtEditor := models.ROLE_EDITOR
|
rtEditor := models.ROLE_EDITOR
|
||||||
rtViewer := models.ROLE_VIEWER
|
rtViewer := models.ROLE_VIEWER
|
||||||
|
|||||||
@@ -444,6 +444,10 @@ func (s *dashboardServiceMock) DeleteDashboard(_ context.Context, dashboardId in
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *dashboardServiceMock) GetDashboardByPublicUid(ctx context.Context, dashboardPublicUid string) (*models.Dashboard, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
type scenarioInput struct {
|
type scenarioInput struct {
|
||||||
storedPluginSettings []*pluginsettings.DTO
|
storedPluginSettings []*pluginsettings.DTO
|
||||||
installedPlugins []plugins.PluginDTO
|
installedPlugins []plugins.PluginDTO
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onRefresh = () => {
|
onRefresh = () => {
|
||||||
const { panel, isInView, width } = this.props;
|
const { dashboard, panel, isInView, width } = this.props;
|
||||||
|
|
||||||
if (!isInView) {
|
if (!isInView) {
|
||||||
this.setState({ refreshWhenInView: true });
|
this.setState({ refreshWhenInView: true });
|
||||||
@@ -356,7 +356,13 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
if (this.state.refreshWhenInView) {
|
if (this.state.refreshWhenInView) {
|
||||||
this.setState({ refreshWhenInView: false });
|
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 {
|
} else {
|
||||||
// The panel should render on refresh as well if it doesn't have a query, like clock panel
|
// The panel should render on refresh as well if it doesn't have a query, like clock panel
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -304,12 +304,19 @@ export class PanelModel implements DataConfigSource, IPanelModel {
|
|||||||
this.gridPos.h = newPos.h;
|
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({
|
this.getQueryRunner().run({
|
||||||
datasource: this.datasource,
|
datasource: this.datasource,
|
||||||
queries: this.targets,
|
queries: this.targets,
|
||||||
panelId: this.id,
|
panelId: this.id,
|
||||||
dashboardId: dashboardId,
|
dashboardId: dashboardId,
|
||||||
|
publicDashboardUid,
|
||||||
timezone: dashboardTimezone,
|
timezone: dashboardTimezone,
|
||||||
timeRange: timeData.timeRange,
|
timeRange: timeData.timeRange,
|
||||||
timeInfo: timeData.timeInfo,
|
timeInfo: timeData.timeInfo,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { isStreamingDataFrame } from 'app/features/live/data/utils';
|
|||||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
|
|
||||||
import { isSharedDashboardQuery, runSharedRequest } from '../../../plugins/datasource/dashboard';
|
import { isSharedDashboardQuery, runSharedRequest } from '../../../plugins/datasource/dashboard';
|
||||||
|
import { PublicDashboardDataSource } from '../../dashboard/services/PublicDashboardDataSource';
|
||||||
import { PanelModel } from '../../dashboard/state';
|
import { PanelModel } from '../../dashboard/state';
|
||||||
|
|
||||||
import { getDashboardQueryRunner } from './DashboardQueryRunner/DashboardQueryRunner';
|
import { getDashboardQueryRunner } from './DashboardQueryRunner/DashboardQueryRunner';
|
||||||
@@ -46,6 +47,7 @@ export interface QueryRunnerOptions<
|
|||||||
queries: TQuery[];
|
queries: TQuery[];
|
||||||
panelId?: number;
|
panelId?: number;
|
||||||
dashboardId?: number;
|
dashboardId?: number;
|
||||||
|
publicDashboardUid?: string;
|
||||||
timezone: TimeZone;
|
timezone: TimeZone;
|
||||||
timeRange: TimeRange;
|
timeRange: TimeRange;
|
||||||
timeInfo?: string; // String description of time range for display
|
timeInfo?: string; // String description of time range for display
|
||||||
@@ -201,6 +203,7 @@ export class PanelQueryRunner {
|
|||||||
datasource,
|
datasource,
|
||||||
panelId,
|
panelId,
|
||||||
dashboardId,
|
dashboardId,
|
||||||
|
publicDashboardUid,
|
||||||
timeRange,
|
timeRange,
|
||||||
timeInfo,
|
timeInfo,
|
||||||
cacheTimeout,
|
cacheTimeout,
|
||||||
@@ -220,6 +223,7 @@ export class PanelQueryRunner {
|
|||||||
timezone,
|
timezone,
|
||||||
panelId,
|
panelId,
|
||||||
dashboardId,
|
dashboardId,
|
||||||
|
publicDashboardUid,
|
||||||
range: timeRange,
|
range: timeRange,
|
||||||
timeInfo,
|
timeInfo,
|
||||||
interval: '',
|
interval: '',
|
||||||
@@ -235,8 +239,9 @@ export class PanelQueryRunner {
|
|||||||
(request as any).rangeRaw = timeRange.raw;
|
(request as any).rangeRaw = timeRange.raw;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ds = await getDataSource(datasource, request.scopedVars);
|
const ds = await getDataSource(datasource, request.scopedVars, publicDashboardUid);
|
||||||
const isMixedDS = ds.meta?.mixed;
|
const isMixedDS = ds.meta?.mixed;
|
||||||
|
|
||||||
// Attach the data source to each query
|
// Attach the data source to each query
|
||||||
request.targets = request.targets.map((query) => {
|
request.targets = request.targets.map((query) => {
|
||||||
const isExpressionQuery = query.datasource?.type === ExpressionDatasourceRef.type;
|
const isExpressionQuery = query.datasource?.type === ExpressionDatasourceRef.type;
|
||||||
@@ -353,10 +358,16 @@ export class PanelQueryRunner {
|
|||||||
|
|
||||||
async function getDataSource(
|
async function getDataSource(
|
||||||
datasource: DataSourceRef | string | DataSourceApi | null,
|
datasource: DataSourceRef | string | DataSourceApi | null,
|
||||||
scopedVars: ScopedVars
|
scopedVars: ScopedVars,
|
||||||
|
publicDashboardUid?: string
|
||||||
): Promise<DataSourceApi> {
|
): Promise<DataSourceApi> {
|
||||||
|
if (publicDashboardUid) {
|
||||||
|
return new PublicDashboardDataSource();
|
||||||
|
}
|
||||||
|
|
||||||
if (datasource && (datasource as any).query) {
|
if (datasource && (datasource as any).query) {
|
||||||
return datasource as DataSourceApi;
|
return datasource as DataSourceApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await getDatasourceSrv().get(datasource as string, scopedVars);
|
return await getDatasourceSrv().get(datasource as string, scopedVars);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export interface DashboardMeta {
|
|||||||
hasUnsavedFolderChange?: boolean;
|
hasUnsavedFolderChange?: boolean;
|
||||||
annotationsPermissions?: AnnotationsPermissions;
|
annotationsPermissions?: AnnotationsPermissions;
|
||||||
isPublic?: boolean;
|
isPublic?: boolean;
|
||||||
|
publicDashboardUid?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnnotationActions {
|
export interface AnnotationActions {
|
||||||
|
|||||||
Reference in New Issue
Block a user