mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PublicDashboards: Data discrepancy fix. Use real datasource plugin when it is a public dashboard. (#73708)
Co-authored-by: Torkel Ödegaard <torkel@grafana.com> Co-authored-by: Ezequiel Victorero <ezequiel.victorero@grafana.com> Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
@@ -2120,9 +2120,6 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||||
],
|
],
|
||||||
"public/app/features/dashboard/services/PublicDashboardDataSource.ts:5381": [
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
|
||||||
],
|
|
||||||
"public/app/features/dashboard/state/DashboardMigrator.test.ts:5381": [
|
"public/app/features/dashboard/state/DashboardMigrator.test.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||||
@@ -2754,6 +2751,9 @@ exports[`better eslint`] = {
|
|||||||
"public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.ts:5381": [
|
"public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
|
"public/app/features/query/state/DashboardQueryRunner/PublicAnnotationsDataSource.ts:5381": [
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
|
],
|
||||||
"public/app/features/query/state/DashboardQueryRunner/SnapshotWorker.ts:5381": [
|
"public/app/features/query/state/DashboardQueryRunner/SnapshotWorker.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ import {
|
|||||||
createDataFrame,
|
createDataFrame,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
|
import { config } from '../config';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DataSourceWithBackend,
|
DataSourceWithBackend,
|
||||||
isExpressionReference,
|
isExpressionReference,
|
||||||
standardStreamOptionsProvider,
|
standardStreamOptionsProvider,
|
||||||
toStreamingDataResponse,
|
toStreamingDataResponse,
|
||||||
} from './DataSourceWithBackend';
|
} from './DataSourceWithBackend';
|
||||||
|
import { publicDashboardQueryHandler } from './publicDashboardQueryHandler';
|
||||||
|
|
||||||
class MyDataSource extends DataSourceWithBackend<DataQuery, DataSourceJsonData> {
|
class MyDataSource extends DataSourceWithBackend<DataQuery, DataSourceJsonData> {
|
||||||
constructor(instanceSettings: DataSourceInstanceSettings<DataSourceJsonData>) {
|
constructor(instanceSettings: DataSourceInstanceSettings<DataSourceJsonData>) {
|
||||||
@@ -44,6 +47,7 @@ jest.mock('../services', () => ({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
jest.mock('./publicDashboardQueryHandler');
|
||||||
|
|
||||||
describe('DataSourceWithBackend', () => {
|
describe('DataSourceWithBackend', () => {
|
||||||
test('check the executed queries', () => {
|
test('check the executed queries', () => {
|
||||||
@@ -313,6 +317,43 @@ describe('DataSourceWithBackend', () => {
|
|||||||
expect(isExpressionReference(undefined)).toBeFalsy();
|
expect(isExpressionReference(undefined)).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('public dashboard scope', () => {
|
||||||
|
test("check public dashboard handler is not executed when it's not public dashboard scope", () => {
|
||||||
|
const { ds } = createMockDatasource();
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
maxDataPoints: 10,
|
||||||
|
intervalMs: 5000,
|
||||||
|
targets: [{ refId: 'A' }, { refId: 'B', datasource: { type: 'sample' } }],
|
||||||
|
dashboardUID: 'dashA',
|
||||||
|
panelId: 123,
|
||||||
|
queryGroupId: 'abc',
|
||||||
|
} as DataQueryRequest;
|
||||||
|
|
||||||
|
ds.query(request);
|
||||||
|
|
||||||
|
expect(publicDashboardQueryHandler).not.toHaveBeenCalledWith(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("check public dashboard handler is executed when it's public dashboard scope", () => {
|
||||||
|
config.publicDashboardAccessToken = 'abc123';
|
||||||
|
const { ds } = createMockDatasource();
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
maxDataPoints: 10,
|
||||||
|
intervalMs: 5000,
|
||||||
|
targets: [{ refId: 'A' }, { refId: 'B', datasource: { type: 'sample' } }],
|
||||||
|
dashboardUID: 'dashA',
|
||||||
|
panelId: 123,
|
||||||
|
queryGroupId: 'abc',
|
||||||
|
} as DataQueryRequest;
|
||||||
|
|
||||||
|
ds.query(request);
|
||||||
|
|
||||||
|
expect(publicDashboardQueryHandler).toHaveBeenCalledWith(request);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function createMockDatasource() {
|
function createMockDatasource() {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
StreamingFrameOptions,
|
StreamingFrameOptions,
|
||||||
} from '../services';
|
} from '../services';
|
||||||
|
|
||||||
|
import { publicDashboardQueryHandler } from './publicDashboardQueryHandler';
|
||||||
import { BackendDataSourceResponse, toDataQueryResponse } from './queryResponse';
|
import { BackendDataSourceResponse, toDataQueryResponse } from './queryResponse';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -123,6 +124,10 @@ class DataSourceWithBackend<
|
|||||||
* Ideally final -- any other implementation may not work as expected
|
* Ideally final -- any other implementation may not work as expected
|
||||||
*/
|
*/
|
||||||
query(request: DataQueryRequest<TQuery>): Observable<DataQueryResponse> {
|
query(request: DataQueryRequest<TQuery>): Observable<DataQueryResponse> {
|
||||||
|
if (config.publicDashboardAccessToken) {
|
||||||
|
return publicDashboardQueryHandler(request);
|
||||||
|
}
|
||||||
|
|
||||||
const { intervalMs, maxDataPoints, queryCachingTTL, range, requestId, hideFromInspector = false } = request;
|
const { intervalMs, maxDataPoints, queryCachingTTL, range, requestId, hideFromInspector = false } = request;
|
||||||
let targets = request.targets;
|
let targets = request.targets;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { catchError, Observable, of, switchMap } from 'rxjs';
|
||||||
|
|
||||||
|
import { DataQuery, DataQueryRequest, DataQueryResponse } from '@grafana/data';
|
||||||
|
|
||||||
|
import { config } from '../config';
|
||||||
|
import { getBackendSrv } from '../services/backendSrv';
|
||||||
|
|
||||||
|
import { BackendDataSourceResponse, toDataQueryResponse } from './queryResponse';
|
||||||
|
|
||||||
|
export function publicDashboardQueryHandler(request: DataQueryRequest<DataQuery>): Observable<DataQueryResponse> {
|
||||||
|
const {
|
||||||
|
intervalMs,
|
||||||
|
maxDataPoints,
|
||||||
|
requestId,
|
||||||
|
panelId,
|
||||||
|
queryCachingTTL,
|
||||||
|
range: { from: fromRange, to: toRange },
|
||||||
|
} = request;
|
||||||
|
// Return early if no queries exist
|
||||||
|
if (!request.targets.length) {
|
||||||
|
return of({ data: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
intervalMs,
|
||||||
|
maxDataPoints,
|
||||||
|
queryCachingTTL,
|
||||||
|
timeRange: {
|
||||||
|
from: fromRange.valueOf().toString(),
|
||||||
|
to: toRange.valueOf().toString(),
|
||||||
|
timezone: request.timezone,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return getBackendSrv()
|
||||||
|
.fetch<BackendDataSourceResponse>({
|
||||||
|
url: `/api/public/dashboards/${config.publicDashboardAccessToken!}/panels/${panelId}/query`,
|
||||||
|
method: 'POST',
|
||||||
|
data: body,
|
||||||
|
requestId,
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
switchMap((raw) => {
|
||||||
|
return of(toDataQueryResponse(raw, request.targets));
|
||||||
|
}),
|
||||||
|
catchError((err) => {
|
||||||
|
return of(toDataQueryResponse(err));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
import { of } from 'rxjs';
|
|
||||||
|
|
||||||
import { DataQueryRequest, DataSourceInstanceSettings, DataSourceRef, dateTime, TimeRange } from '@grafana/data';
|
|
||||||
import { BackendSrvRequest, BackendSrv, DataSourceWithBackend, config } from '@grafana/runtime';
|
|
||||||
import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
|
|
||||||
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
|
||||||
|
|
||||||
import { GRAFANA_DATASOURCE_NAME } from '../../alerting/unified/utils/datasource';
|
|
||||||
|
|
||||||
import { PublicDashboardDataSource, PUBLIC_DATASOURCE, DEFAULT_INTERVAL } from './PublicDashboardDataSource';
|
|
||||||
|
|
||||||
const mockDatasourceRequest = jest.fn();
|
|
||||||
|
|
||||||
const backendSrv = {
|
|
||||||
fetch: (options: BackendSrvRequest) => {
|
|
||||||
return of(mockDatasourceRequest(options));
|
|
||||||
},
|
|
||||||
get: (url: string, options?: Partial<BackendSrvRequest>) => {
|
|
||||||
return mockDatasourceRequest(url, options);
|
|
||||||
},
|
|
||||||
} as unknown as BackendSrv;
|
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
|
||||||
...jest.requireActual('@grafana/runtime'),
|
|
||||||
getBackendSrv: () => backendSrv,
|
|
||||||
getDataSourceSrv: () => {
|
|
||||||
return {
|
|
||||||
getInstanceSettings: (ref?: DataSourceRef) => ({ type: ref?.type ?? '?', uid: ref?.uid ?? '?' }),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('PublicDashboardDatasource', () => {
|
|
||||||
test('will add annotation query type to annotations', () => {
|
|
||||||
const ds = new PublicDashboardDataSource('public');
|
|
||||||
const annotationQuery = {
|
|
||||||
enable: true,
|
|
||||||
name: 'someName',
|
|
||||||
iconColor: 'red',
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const annotation = ds?.annotations.prepareQuery(annotationQuery);
|
|
||||||
|
|
||||||
expect(annotation?.queryType).toEqual(GrafanaQueryType.Annotations);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('fetches results from the pubdash annotations endpoint when it is an annotation query', async () => {
|
|
||||||
mockDatasourceRequest.mockReset();
|
|
||||||
mockDatasourceRequest.mockReturnValue(Promise.resolve([]));
|
|
||||||
|
|
||||||
const ds = new PublicDashboardDataSource('public');
|
|
||||||
const panelId = 1;
|
|
||||||
|
|
||||||
config.publicDashboardAccessToken = 'abc123';
|
|
||||||
|
|
||||||
await ds.query({
|
|
||||||
maxDataPoints: 10,
|
|
||||||
intervalMs: 5000,
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
refId: 'A',
|
|
||||||
datasource: { uid: GRAFANA_DATASOURCE_NAME, type: 'sample' },
|
|
||||||
queryType: GrafanaQueryType.Annotations,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
panelId,
|
|
||||||
range: { from: new Date().toLocaleString(), to: new Date().toLocaleString() } as unknown as TimeRange,
|
|
||||||
} as DataQueryRequest);
|
|
||||||
|
|
||||||
const mock = mockDatasourceRequest.mock;
|
|
||||||
|
|
||||||
expect(mock.calls.length).toBe(1);
|
|
||||||
expect(mock.lastCall[0]).toEqual(`/api/public/dashboards/abc123/annotations`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('fetches results from the pubdash query endpoint when not annotation query', () => {
|
|
||||||
mockDatasourceRequest.mockReset();
|
|
||||||
mockDatasourceRequest.mockReturnValue(Promise.resolve({}));
|
|
||||||
|
|
||||||
const ds = new PublicDashboardDataSource('public');
|
|
||||||
const panelId = 1;
|
|
||||||
config.publicDashboardAccessToken = 'abc123';
|
|
||||||
|
|
||||||
ds.query({
|
|
||||||
maxDataPoints: 10,
|
|
||||||
intervalMs: 5000,
|
|
||||||
targets: [{ refId: 'A' }, { refId: 'B', datasource: { type: 'sample' } }],
|
|
||||||
panelId,
|
|
||||||
range: {
|
|
||||||
from: dateTime('2022-01-01T15:55:00Z'),
|
|
||||||
to: dateTime('2022-07-12T15:55:00Z'),
|
|
||||||
raw: {
|
|
||||||
from: 'now-15m',
|
|
||||||
to: 'now',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as DataQueryRequest);
|
|
||||||
|
|
||||||
const mock = mockDatasourceRequest.mock;
|
|
||||||
|
|
||||||
expect(mock.calls.length).toBe(1);
|
|
||||||
expect(mock.lastCall[0].url).toEqual(`/api/public/dashboards/abc123/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');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('isMixedDatasource returns true when datasource is mixed', () => {
|
|
||||||
const datasource = new DataSourceWithBackend({ id: 1, uid: MIXED_DATASOURCE_NAME } as DataSourceInstanceSettings);
|
|
||||||
let ds = new PublicDashboardDataSource(datasource);
|
|
||||||
expect(ds.meta.mixed).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('isMixedDatasource returns false when datasource is not mixed', () => {
|
|
||||||
const datasource = new DataSourceWithBackend({ id: 1, uid: 'abc123' } as DataSourceInstanceSettings);
|
|
||||||
let ds = new PublicDashboardDataSource(datasource);
|
|
||||||
expect(ds.meta.mixed).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('isMixedDatasource returns false when datasource is a string', () => {
|
|
||||||
let ds = new PublicDashboardDataSource('abc123');
|
|
||||||
expect(ds.meta.mixed).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('isMixedDatasource returns false when datasource is null', () => {
|
|
||||||
let ds = new PublicDashboardDataSource(null);
|
|
||||||
expect(ds.meta.mixed).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns default datasource interval when datasource passed in is null', () => {
|
|
||||||
let ds = new PublicDashboardDataSource(null);
|
|
||||||
expect(ds.interval).toBe(DEFAULT_INTERVAL);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns default datasource interval when datasource passed in is a string', () => {
|
|
||||||
let ds = new PublicDashboardDataSource('theDatasourceUid');
|
|
||||||
expect(ds.interval).toBe(DEFAULT_INTERVAL);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns default datasource interval when datasource passed in is a DataSourceRef implementation', () => {
|
|
||||||
const datasource = { type: 'datasource', uid: 'abc123' };
|
|
||||||
let ds = new PublicDashboardDataSource(datasource);
|
|
||||||
expect(ds.interval).toBe(DEFAULT_INTERVAL);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns default datasource interval when datasource passed in is a DatasourceApi instance that has no interval', () => {
|
|
||||||
const settings: DataSourceInstanceSettings = { id: 1, uid: 'abc123' } as DataSourceInstanceSettings;
|
|
||||||
const datasource = new DataSourceWithBackend(settings);
|
|
||||||
let ds = new PublicDashboardDataSource(datasource);
|
|
||||||
expect(ds.interval).toBe(DEFAULT_INTERVAL);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns datasource interval when datasource passed in is a DatasourceApi instance that has interval', () => {
|
|
||||||
const settings: DataSourceInstanceSettings = { id: 1, uid: 'abc123' } as DataSourceInstanceSettings;
|
|
||||||
const datasource = new DataSourceWithBackend(settings);
|
|
||||||
datasource.interval = 'abc123';
|
|
||||||
let ds = new PublicDashboardDataSource(datasource);
|
|
||||||
expect(ds.interval).toBe('abc123');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
import { catchError, from, Observable, of, switchMap } from 'rxjs';
|
|
||||||
|
|
||||||
import {
|
|
||||||
AnnotationQuery,
|
|
||||||
DataQuery,
|
|
||||||
DataQueryRequest,
|
|
||||||
DataQueryResponse,
|
|
||||||
TestDataSourceResponse,
|
|
||||||
DataSourceApi,
|
|
||||||
DataSourceJsonData,
|
|
||||||
DataSourcePluginMeta,
|
|
||||||
DataSourceRef,
|
|
||||||
toDataFrame,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import { BackendDataSourceResponse, config, getBackendSrv, toDataQueryResponse } from '@grafana/runtime';
|
|
||||||
|
|
||||||
import { GrafanaQueryType } from '../../../plugins/datasource/grafana/types';
|
|
||||||
import { MIXED_DATASOURCE_NAME } from '../../../plugins/datasource/mixed/MixedDataSource';
|
|
||||||
import { GRAFANA_DATASOURCE_NAME } from '../../alerting/unified/utils/datasource';
|
|
||||||
|
|
||||||
export const PUBLIC_DATASOURCE = '-- Public --';
|
|
||||||
export const DEFAULT_INTERVAL = '1min';
|
|
||||||
|
|
||||||
export class PublicDashboardDataSource extends DataSourceApi<DataQuery, DataSourceJsonData, {}> {
|
|
||||||
constructor(datasource: DataSourceRef | string | DataSourceApi | null) {
|
|
||||||
let meta = {} as DataSourcePluginMeta;
|
|
||||||
if (PublicDashboardDataSource.isMixedDatasource(datasource)) {
|
|
||||||
meta.mixed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
super({
|
|
||||||
name: 'public-ds',
|
|
||||||
id: 0,
|
|
||||||
type: 'public-ds',
|
|
||||||
meta,
|
|
||||||
uid: PublicDashboardDataSource.resolveUid(datasource),
|
|
||||||
jsonData: {},
|
|
||||||
access: 'proxy',
|
|
||||||
readOnly: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.interval = PublicDashboardDataSource.resolveInterval(datasource);
|
|
||||||
|
|
||||||
this.annotations = {
|
|
||||||
prepareQuery(anno: AnnotationQuery): DataQuery | undefined {
|
|
||||||
return { ...anno, queryType: GrafanaQueryType.Annotations, refId: 'anno' };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static isMixedDatasource(datasource: DataSourceRef | string | DataSourceApi | null): boolean {
|
|
||||||
if (typeof datasource === 'string' || datasource == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return datasource?.uid === MIXED_DATASOURCE_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static resolveInterval(datasource: DataSourceRef | string | DataSourceApi | null): string {
|
|
||||||
if (typeof datasource === 'string' || datasource == null) {
|
|
||||||
return DEFAULT_INTERVAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
const interval = 'interval' in datasource ? datasource.interval : undefined;
|
|
||||||
|
|
||||||
return interval ?? DEFAULT_INTERVAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ideally final -- any other implementation may not work as expected
|
|
||||||
*/
|
|
||||||
query(request: DataQueryRequest<DataQuery>): Observable<DataQueryResponse> {
|
|
||||||
const {
|
|
||||||
intervalMs,
|
|
||||||
maxDataPoints,
|
|
||||||
requestId,
|
|
||||||
panelId,
|
|
||||||
queryCachingTTL,
|
|
||||||
range: { from: fromRange, to: toRange },
|
|
||||||
} = request;
|
|
||||||
// Return early if no queries exist
|
|
||||||
if (!request.targets.length) {
|
|
||||||
return of({ data: [] });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Its an annotations query
|
|
||||||
// Currently, annotations requests come in one at a time, so there will only be one target
|
|
||||||
const target = request.targets[0];
|
|
||||||
if (target.queryType === GrafanaQueryType.Annotations) {
|
|
||||||
if (target?.datasource?.uid === GRAFANA_DATASOURCE_NAME) {
|
|
||||||
return from(this.getAnnotations(request));
|
|
||||||
}
|
|
||||||
return of({ data: [] });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Its a datasource query
|
|
||||||
else {
|
|
||||||
const body = {
|
|
||||||
intervalMs,
|
|
||||||
maxDataPoints,
|
|
||||||
queryCachingTTL,
|
|
||||||
timeRange: {
|
|
||||||
from: fromRange.valueOf().toString(),
|
|
||||||
to: toRange.valueOf().toString(),
|
|
||||||
timezone: this.getBrowserTimezone(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return getBackendSrv()
|
|
||||||
.fetch<BackendDataSourceResponse>({
|
|
||||||
url: `/api/public/dashboards/${config.publicDashboardAccessToken!}/panels/${panelId}/query`,
|
|
||||||
method: 'POST',
|
|
||||||
data: body,
|
|
||||||
requestId,
|
|
||||||
})
|
|
||||||
.pipe(
|
|
||||||
switchMap((raw) => {
|
|
||||||
return of(toDataQueryResponse(raw, request.targets));
|
|
||||||
}),
|
|
||||||
catchError((err) => {
|
|
||||||
return of(toDataQueryResponse(err));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAnnotations(request: DataQueryRequest<DataQuery>): Promise<DataQueryResponse> {
|
|
||||||
const {
|
|
||||||
range: { to, from },
|
|
||||||
} = request;
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
from: from.valueOf(),
|
|
||||||
to: to.valueOf(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const annotations = await getBackendSrv().get(
|
|
||||||
`/api/public/dashboards/${config.publicDashboardAccessToken!}/annotations`,
|
|
||||||
params
|
|
||||||
);
|
|
||||||
|
|
||||||
return { data: [toDataFrame(annotations)] };
|
|
||||||
}
|
|
||||||
|
|
||||||
testDatasource(): Promise<TestDataSourceResponse> {
|
|
||||||
return Promise.resolve({ message: '', status: '' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to get the browser timezone otherwise return blank
|
|
||||||
getBrowserTimezone(): string {
|
|
||||||
return window.Intl?.DateTimeFormat().resolvedOptions()?.timeZone || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import { Subject, throwError } from 'rxjs';
|
|||||||
import { delay } from 'rxjs/operators';
|
import { delay } from 'rxjs/operators';
|
||||||
|
|
||||||
import { AnnotationQuery } from '@grafana/data';
|
import { AnnotationQuery } from '@grafana/data';
|
||||||
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
|
import { DataSourceSrv, setDataSourceSrv, config } from '@grafana/runtime';
|
||||||
import { DashboardModel } from 'app/features/dashboard/state';
|
import { DashboardModel } from 'app/features/dashboard/state';
|
||||||
|
|
||||||
import { silenceConsoleOutput } from '../../../../../test/core/utils/silenceConsoleOutput';
|
import { silenceConsoleOutput } from '../../../../../test/core/utils/silenceConsoleOutput';
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
DashboardQueryRunnerFactoryArgs,
|
DashboardQueryRunnerFactoryArgs,
|
||||||
setDashboardQueryRunnerFactory,
|
setDashboardQueryRunnerFactory,
|
||||||
} from './DashboardQueryRunner';
|
} from './DashboardQueryRunner';
|
||||||
|
import { PublicAnnotationsDataSource } from './PublicAnnotationsDataSource';
|
||||||
import { getDefaultOptions, LEGACY_DS_NAME, NEXT_GEN_DS_NAME, toAsyncOfResult } from './testHelpers';
|
import { getDefaultOptions, LEGACY_DS_NAME, NEXT_GEN_DS_NAME, toAsyncOfResult } from './testHelpers';
|
||||||
import { DashboardQueryRunnerOptions, DashboardQueryRunnerWorkerResult } from './types';
|
import { DashboardQueryRunnerOptions, DashboardQueryRunnerWorkerResult } from './types';
|
||||||
import { emptyResult } from './utils';
|
import { emptyResult } from './utils';
|
||||||
@@ -80,6 +81,8 @@ function expectOnResults(args: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jest.mock('./PublicAnnotationsDataSource');
|
||||||
|
|
||||||
describe('AnnotationsWorker', () => {
|
describe('AnnotationsWorker', () => {
|
||||||
const worker = new AnnotationsWorker();
|
const worker = new AnnotationsWorker();
|
||||||
|
|
||||||
@@ -297,4 +300,23 @@ describe('AnnotationsWorker', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('public dashboard scope', () => {
|
||||||
|
test('does not call PublicAnnotationsDataSource when it is not a public dashboard', async () => {
|
||||||
|
const { options, annotationQueryMock } = getTestContext();
|
||||||
|
await expect(worker.work(options)).toEmitValuesWith(() => {
|
||||||
|
expect(PublicAnnotationsDataSource).not.toHaveBeenCalled();
|
||||||
|
expect(annotationQueryMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calls PublicAnnotationsDataSource when it is a public dashboard', async () => {
|
||||||
|
config.publicDashboardAccessToken = 'abc123';
|
||||||
|
const { options, annotationQueryMock } = getTestContext(true);
|
||||||
|
await expect(worker.work(options)).toEmitValuesWith(() => {
|
||||||
|
expect(PublicAnnotationsDataSource).toHaveBeenCalledTimes(1);
|
||||||
|
expect(annotationQueryMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { catchError, filter, finalize, map, mergeAll, mergeMap, reduce, takeUnti
|
|||||||
|
|
||||||
import { AnnotationQuery, DataSourceApi } from '@grafana/data';
|
import { AnnotationQuery, DataSourceApi } from '@grafana/data';
|
||||||
import { config, getDataSourceSrv } from '@grafana/runtime';
|
import { config, getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
import { PublicAnnotationsDataSource } from 'app/features/query/state/DashboardQueryRunner/PublicAnnotationsDataSource';
|
||||||
|
|
||||||
import { AnnotationQueryFinished, AnnotationQueryStarted } from '../../../../types/events';
|
import { AnnotationQueryFinished, AnnotationQueryStarted } from '../../../../types/events';
|
||||||
import { PUBLIC_DATASOURCE, PublicDashboardDataSource } from '../../../dashboard/services/PublicDashboardDataSource';
|
|
||||||
|
|
||||||
import { AnnotationsQueryRunner } from './AnnotationsQueryRunner';
|
import { AnnotationsQueryRunner } from './AnnotationsQueryRunner';
|
||||||
import { getDashboardQueryRunner } from './DashboardQueryRunner';
|
import { getDashboardQueryRunner } from './DashboardQueryRunner';
|
||||||
@@ -50,7 +50,7 @@ export class AnnotationsWorker implements DashboardQueryRunnerWorker {
|
|||||||
let datasourceObservable;
|
let datasourceObservable;
|
||||||
|
|
||||||
if (config.publicDashboardAccessToken) {
|
if (config.publicDashboardAccessToken) {
|
||||||
const pubdashDatasource = new PublicDashboardDataSource(PUBLIC_DATASOURCE);
|
const pubdashDatasource = new PublicAnnotationsDataSource();
|
||||||
datasourceObservable = of(pubdashDatasource).pipe(catchError(handleDatasourceSrvError));
|
datasourceObservable = of(pubdashDatasource).pipe(catchError(handleDatasourceSrvError));
|
||||||
} else {
|
} else {
|
||||||
datasourceObservable = from(getDataSourceSrv().get(annotation.datasource)).pipe(
|
datasourceObservable = from(getDataSourceSrv().get(annotation.datasource)).pipe(
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
|
import { DataQueryRequest, DataSourceRef, TimeRange } from '@grafana/data';
|
||||||
|
import { BackendSrvRequest, BackendSrv, config } from '@grafana/runtime';
|
||||||
|
import { GRAFANA_DATASOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||||
|
import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
|
||||||
|
|
||||||
|
import { PublicAnnotationsDataSource } from './PublicAnnotationsDataSource';
|
||||||
|
|
||||||
|
const mockDatasourceRequest = jest.fn();
|
||||||
|
|
||||||
|
const backendSrv = {
|
||||||
|
fetch: (options: BackendSrvRequest) => {
|
||||||
|
return of(mockDatasourceRequest(options));
|
||||||
|
},
|
||||||
|
get: (url: string, options?: Partial<BackendSrvRequest>) => {
|
||||||
|
return mockDatasourceRequest(url, options);
|
||||||
|
},
|
||||||
|
} as unknown as BackendSrv;
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
...jest.requireActual('@grafana/runtime'),
|
||||||
|
getBackendSrv: () => backendSrv,
|
||||||
|
getDataSourceSrv: () => {
|
||||||
|
return {
|
||||||
|
getInstanceSettings: (ref?: DataSourceRef) => ({ type: ref?.type ?? '?', uid: ref?.uid ?? '?' }),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('PublicDashboardDatasource', () => {
|
||||||
|
test('will add annotation query type to annotations', () => {
|
||||||
|
const ds = new PublicAnnotationsDataSource();
|
||||||
|
const annotationQuery = {
|
||||||
|
enable: true,
|
||||||
|
name: 'someName',
|
||||||
|
iconColor: 'red',
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const annotation = ds?.annotations.prepareQuery(annotationQuery);
|
||||||
|
|
||||||
|
expect(annotation?.queryType).toEqual(GrafanaQueryType.Annotations);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fetches results from the pubdash annotations endpoint when it is an annotation query', async () => {
|
||||||
|
mockDatasourceRequest.mockReset();
|
||||||
|
mockDatasourceRequest.mockReturnValue(Promise.resolve([]));
|
||||||
|
|
||||||
|
const ds = new PublicAnnotationsDataSource();
|
||||||
|
const panelId = 1;
|
||||||
|
|
||||||
|
config.publicDashboardAccessToken = 'abc123';
|
||||||
|
|
||||||
|
await ds.query({
|
||||||
|
maxDataPoints: 10,
|
||||||
|
intervalMs: 5000,
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
refId: 'A',
|
||||||
|
datasource: { uid: GRAFANA_DATASOURCE_NAME, type: 'sample' },
|
||||||
|
queryType: GrafanaQueryType.Annotations,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
panelId,
|
||||||
|
range: { from: new Date().toLocaleString(), to: new Date().toLocaleString() } as unknown as TimeRange,
|
||||||
|
} as DataQueryRequest);
|
||||||
|
|
||||||
|
const mock = mockDatasourceRequest.mock;
|
||||||
|
|
||||||
|
expect(mock.calls.length).toBe(1);
|
||||||
|
expect(mock.lastCall[0]).toEqual(`/api/public/dashboards/abc123/annotations`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { from, Observable, of } from 'rxjs';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AnnotationQuery,
|
||||||
|
DataQuery,
|
||||||
|
DataQueryRequest,
|
||||||
|
DataQueryResponse,
|
||||||
|
TestDataSourceResponse,
|
||||||
|
DataSourceApi,
|
||||||
|
DataSourceJsonData,
|
||||||
|
DataSourcePluginMeta,
|
||||||
|
toDataFrame,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { config, getBackendSrv } from '@grafana/runtime';
|
||||||
|
import { GRAFANA_DATASOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||||
|
|
||||||
|
import { GrafanaQueryType } from '../../../../plugins/datasource/grafana/types';
|
||||||
|
|
||||||
|
export const PUBLIC_DATASOURCE = '-- Public --';
|
||||||
|
|
||||||
|
export class PublicAnnotationsDataSource extends DataSourceApi<DataQuery, DataSourceJsonData, {}> {
|
||||||
|
constructor() {
|
||||||
|
let meta = {} as DataSourcePluginMeta;
|
||||||
|
|
||||||
|
super({
|
||||||
|
name: 'public-ds',
|
||||||
|
id: 0,
|
||||||
|
type: 'public-ds',
|
||||||
|
meta,
|
||||||
|
uid: PUBLIC_DATASOURCE,
|
||||||
|
jsonData: {},
|
||||||
|
access: 'proxy',
|
||||||
|
readOnly: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.annotations = {
|
||||||
|
prepareQuery(anno: AnnotationQuery): DataQuery | undefined {
|
||||||
|
return { ...anno, queryType: GrafanaQueryType.Annotations, refId: 'anno' };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ideally final -- any other implementation may not work as expected
|
||||||
|
*/
|
||||||
|
query(request: DataQueryRequest<DataQuery>): Observable<DataQueryResponse> {
|
||||||
|
// Return early if no queries exist
|
||||||
|
if (!request.targets.length) {
|
||||||
|
return of({ data: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently, annotations requests come in one at a time, so there will only be one target
|
||||||
|
const target = request.targets[0];
|
||||||
|
|
||||||
|
if (target?.datasource?.uid === GRAFANA_DATASOURCE_NAME) {
|
||||||
|
return from(this.getAnnotations(request));
|
||||||
|
}
|
||||||
|
return of({ data: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAnnotations(request: DataQueryRequest<DataQuery>): Promise<DataQueryResponse> {
|
||||||
|
const {
|
||||||
|
range: { to, from },
|
||||||
|
} = request;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
from: from.valueOf(),
|
||||||
|
to: to.valueOf(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const annotations = await getBackendSrv().get(
|
||||||
|
`/api/public/dashboards/${config.publicDashboardAccessToken}/annotations`,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
|
return { data: [toDataFrame(annotations)] };
|
||||||
|
}
|
||||||
|
|
||||||
|
testDatasource(): Promise<TestDataSourceResponse> {
|
||||||
|
return Promise.resolve({ message: '', status: '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,14 +29,13 @@ import {
|
|||||||
toDataFrame,
|
toDataFrame,
|
||||||
transformDataFrame,
|
transformDataFrame,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { config, getTemplateSrv, toDataQueryError } from '@grafana/runtime';
|
import { getTemplateSrv, toDataQueryError } from '@grafana/runtime';
|
||||||
import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend';
|
import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend';
|
||||||
import { updatePanelDataWithASHFromLoki } from 'app/features/alerting/unified/components/rules/state-history/common';
|
import { updatePanelDataWithASHFromLoki } from 'app/features/alerting/unified/components/rules/state-history/common';
|
||||||
import { isStreamingDataFrame } from 'app/features/live/data/utils';
|
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';
|
||||||
@@ -415,14 +414,9 @@ async function getDataSource(
|
|||||||
datasource: DataSourceRef | string | DataSourceApi | null,
|
datasource: DataSourceRef | string | DataSourceApi | null,
|
||||||
scopedVars: ScopedVars
|
scopedVars: ScopedVars
|
||||||
): Promise<DataSourceApi> {
|
): Promise<DataSourceApi> {
|
||||||
if (!config.publicDashboardAccessToken && datasource && typeof datasource === 'object' && 'query' in datasource) {
|
if (datasource && typeof datasource === 'object' && 'query' in datasource) {
|
||||||
return datasource;
|
return datasource;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ds = await getDatasourceSrv().get(datasource, scopedVars);
|
return await getDatasourceSrv().get(datasource, scopedVars);
|
||||||
if (config.publicDashboardAccessToken) {
|
|
||||||
return new PublicDashboardDataSource(ds);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ds;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
PanelData,
|
PanelData,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { toDataQueryError } from '@grafana/runtime';
|
import { config, toDataQueryError } from '@grafana/runtime';
|
||||||
import { isExpressionReference } from '@grafana/runtime/src/utils/DataSourceWithBackend';
|
import { isExpressionReference } from '@grafana/runtime/src/utils/DataSourceWithBackend';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv';
|
import { backendSrv } from 'app/core/services/backend_srv';
|
||||||
import { queryIsEmpty } from 'app/core/utils/query';
|
import { queryIsEmpty } from 'app/core/utils/query';
|
||||||
@@ -194,7 +194,7 @@ export function callQueryMethod(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// If its a public datasource, just return the result. Expressions will be handled on the backend.
|
// If its a public datasource, just return the result. Expressions will be handled on the backend.
|
||||||
if (datasource.type === 'public-ds') {
|
if (config.publicDashboardAccessToken) {
|
||||||
return from(datasource.query(request));
|
return from(datasource.query(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user