diff --git a/.betterer.results b/.betterer.results index fe00c7886fd..429de56bad5 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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.", "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": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [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": [ [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": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts index b4048689c98..e0b5d4f19db 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts @@ -11,12 +11,15 @@ import { createDataFrame, } from '@grafana/data'; +import { config } from '../config'; + import { DataSourceWithBackend, isExpressionReference, standardStreamOptionsProvider, toStreamingDataResponse, } from './DataSourceWithBackend'; +import { publicDashboardQueryHandler } from './publicDashboardQueryHandler'; class MyDataSource extends DataSourceWithBackend { constructor(instanceSettings: DataSourceInstanceSettings) { @@ -44,6 +47,7 @@ jest.mock('../services', () => ({ }; }, })); +jest.mock('./publicDashboardQueryHandler'); describe('DataSourceWithBackend', () => { test('check the executed queries', () => { @@ -313,6 +317,43 @@ describe('DataSourceWithBackend', () => { 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() { diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts index 5e9c758c2a3..86502103a21 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts @@ -29,6 +29,7 @@ import { StreamingFrameOptions, } from '../services'; +import { publicDashboardQueryHandler } from './publicDashboardQueryHandler'; import { BackendDataSourceResponse, toDataQueryResponse } from './queryResponse'; /** @@ -123,6 +124,10 @@ class DataSourceWithBackend< * Ideally final -- any other implementation may not work as expected */ query(request: DataQueryRequest): Observable { + if (config.publicDashboardAccessToken) { + return publicDashboardQueryHandler(request); + } + const { intervalMs, maxDataPoints, queryCachingTTL, range, requestId, hideFromInspector = false } = request; let targets = request.targets; diff --git a/packages/grafana-runtime/src/utils/publicDashboardQueryHandler.ts b/packages/grafana-runtime/src/utils/publicDashboardQueryHandler.ts new file mode 100644 index 00000000000..f7b84b5fdd4 --- /dev/null +++ b/packages/grafana-runtime/src/utils/publicDashboardQueryHandler.ts @@ -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): Observable { + 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({ + 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)); + }) + ); +} diff --git a/public/app/features/dashboard/services/PublicDashboardDataSource.test.ts b/public/app/features/dashboard/services/PublicDashboardDataSource.test.ts deleted file mode 100644 index dd31afc7b2f..00000000000 --- a/public/app/features/dashboard/services/PublicDashboardDataSource.test.ts +++ /dev/null @@ -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) => { - 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'); - }); -}); diff --git a/public/app/features/dashboard/services/PublicDashboardDataSource.ts b/public/app/features/dashboard/services/PublicDashboardDataSource.ts deleted file mode 100644 index 05abe8a45fe..00000000000 --- a/public/app/features/dashboard/services/PublicDashboardDataSource.ts +++ /dev/null @@ -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 { - 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): Observable { - 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({ - 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): Promise { - 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 { - return Promise.resolve({ message: '', status: '' }); - } - - // Try to get the browser timezone otherwise return blank - getBrowserTimezone(): string { - return window.Intl?.DateTimeFormat().resolvedOptions()?.timeZone || ''; - } -} diff --git a/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.test.ts b/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.test.ts index 7f0a6558550..c71989ffac4 100644 --- a/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.test.ts +++ b/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.test.ts @@ -2,7 +2,7 @@ import { Subject, throwError } from 'rxjs'; import { delay } from 'rxjs/operators'; 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 { silenceConsoleOutput } from '../../../../../test/core/utils/silenceConsoleOutput'; @@ -14,6 +14,7 @@ import { DashboardQueryRunnerFactoryArgs, setDashboardQueryRunnerFactory, } from './DashboardQueryRunner'; +import { PublicAnnotationsDataSource } from './PublicAnnotationsDataSource'; import { getDefaultOptions, LEGACY_DS_NAME, NEXT_GEN_DS_NAME, toAsyncOfResult } from './testHelpers'; import { DashboardQueryRunnerOptions, DashboardQueryRunnerWorkerResult } from './types'; import { emptyResult } from './utils'; @@ -80,6 +81,8 @@ function expectOnResults(args: { }); } +jest.mock('./PublicAnnotationsDataSource'); + describe('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(); + }); + }); + }); }); diff --git a/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.ts b/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.ts index b31b0fb0fbc..55ce5a23a04 100644 --- a/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.ts +++ b/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.ts @@ -4,9 +4,9 @@ import { catchError, filter, finalize, map, mergeAll, mergeMap, reduce, takeUnti import { AnnotationQuery, DataSourceApi } from '@grafana/data'; import { config, getDataSourceSrv } from '@grafana/runtime'; +import { PublicAnnotationsDataSource } from 'app/features/query/state/DashboardQueryRunner/PublicAnnotationsDataSource'; import { AnnotationQueryFinished, AnnotationQueryStarted } from '../../../../types/events'; -import { PUBLIC_DATASOURCE, PublicDashboardDataSource } from '../../../dashboard/services/PublicDashboardDataSource'; import { AnnotationsQueryRunner } from './AnnotationsQueryRunner'; import { getDashboardQueryRunner } from './DashboardQueryRunner'; @@ -50,7 +50,7 @@ export class AnnotationsWorker implements DashboardQueryRunnerWorker { let datasourceObservable; if (config.publicDashboardAccessToken) { - const pubdashDatasource = new PublicDashboardDataSource(PUBLIC_DATASOURCE); + const pubdashDatasource = new PublicAnnotationsDataSource(); datasourceObservable = of(pubdashDatasource).pipe(catchError(handleDatasourceSrvError)); } else { datasourceObservable = from(getDataSourceSrv().get(annotation.datasource)).pipe( diff --git a/public/app/features/query/state/DashboardQueryRunner/PublicAnnotationsDataSource.test.ts b/public/app/features/query/state/DashboardQueryRunner/PublicAnnotationsDataSource.test.ts new file mode 100644 index 00000000000..3aea3552b04 --- /dev/null +++ b/public/app/features/query/state/DashboardQueryRunner/PublicAnnotationsDataSource.test.ts @@ -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) => { + 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`); + }); +}); diff --git a/public/app/features/query/state/DashboardQueryRunner/PublicAnnotationsDataSource.ts b/public/app/features/query/state/DashboardQueryRunner/PublicAnnotationsDataSource.ts new file mode 100644 index 00000000000..3cc202d20b1 --- /dev/null +++ b/public/app/features/query/state/DashboardQueryRunner/PublicAnnotationsDataSource.ts @@ -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 { + 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): Observable { + // 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): Promise { + 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 { + return Promise.resolve({ message: '', status: '' }); + } +} diff --git a/public/app/features/query/state/PanelQueryRunner.ts b/public/app/features/query/state/PanelQueryRunner.ts index 23ac379b56a..a625f0a1662 100644 --- a/public/app/features/query/state/PanelQueryRunner.ts +++ b/public/app/features/query/state/PanelQueryRunner.ts @@ -29,14 +29,13 @@ import { toDataFrame, transformDataFrame, } 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 { updatePanelDataWithASHFromLoki } from 'app/features/alerting/unified/components/rules/state-history/common'; 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'; @@ -415,14 +414,9 @@ async function getDataSource( datasource: DataSourceRef | string | DataSourceApi | null, scopedVars: ScopedVars ): Promise { - if (!config.publicDashboardAccessToken && datasource && typeof datasource === 'object' && 'query' in datasource) { + if (datasource && typeof datasource === 'object' && 'query' in datasource) { return datasource; } - const ds = await getDatasourceSrv().get(datasource, scopedVars); - if (config.publicDashboardAccessToken) { - return new PublicDashboardDataSource(ds); - } - - return ds; + return await getDatasourceSrv().get(datasource, scopedVars); } diff --git a/public/app/features/query/state/runRequest.ts b/public/app/features/query/state/runRequest.ts index 1c4ace55276..cf389aff076 100644 --- a/public/app/features/query/state/runRequest.ts +++ b/public/app/features/query/state/runRequest.ts @@ -18,7 +18,7 @@ import { PanelData, TimeRange, } from '@grafana/data'; -import { toDataQueryError } from '@grafana/runtime'; +import { config, toDataQueryError } from '@grafana/runtime'; import { isExpressionReference } from '@grafana/runtime/src/utils/DataSourceWithBackend'; import { backendSrv } from 'app/core/services/backend_srv'; 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 (datasource.type === 'public-ds') { + if (config.publicDashboardAccessToken) { return from(datasource.query(request)); }