mirror of https://github.com/grafana/grafana
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>pull/74110/head
parent
5038137662
commit
969ef5282c
@ -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 || ''; |
||||
} |
||||
} |
@ -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: '' }); |
||||
} |
||||
} |
Loading…
Reference in new issue