CloudMonitoring: Support request cancellation properly (#28847)

This commit is contained in:
Hugo Häggmark 2020-11-19 14:09:08 +01:00 committed by GitHub
parent 294770f411
commit f9281742d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 335 additions and 309 deletions

View File

@ -1,6 +1,8 @@
import { of } from 'rxjs';
import Api from './api'; import Api from './api';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
import { SelectableValue } from '@grafana/data'; import { createFetchResponse } from 'test/helpers/createFetchResponse';
jest.mock('@grafana/runtime', () => ({ jest.mock('@grafana/runtime', () => ({
...((jest.requireActual('@grafana/runtime') as unknown) as object), ...((jest.requireActual('@grafana/runtime') as unknown) as object),
@ -12,58 +14,60 @@ const response = [
{ label: 'test2', value: 'test2' }, { label: 'test2', value: 'test2' },
]; ];
describe('api', () => { type Args = { path?: string; options?: any; response?: any; cache?: any };
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
beforeEach(() => { async function getTestContext({ path = 'some-resource', options = {}, response = {}, cache }: Args = {}) {
datasourceRequestMock.mockImplementation((options: any) => { jest.clearAllMocks();
const data = { [options.url.match(/([^\/]*)\/*$/)[1]]: response };
return Promise.resolve({ data, status: 200 }); const fetchMock = jest.spyOn(backendSrv, 'fetch');
});
fetchMock.mockImplementation((options: any) => {
const data = { [options.url.match(/([^\/]*)\/*$/)[1]]: response };
return of(createFetchResponse(data));
}); });
describe('when resource was cached', () => { const api = new Api('/cloudmonitoring/');
let api: Api;
let res: Array<SelectableValue<string>>; if (cache) {
beforeEach(async () => { api.cache[path] = cache;
api = new Api('/cloudmonitoring/'); }
api.cache['some-resource'] = response;
res = await api.get('some-resource'); const res = await api.get(path, options);
});
return { res, api, fetchMock };
}
describe('api', () => {
describe('when resource was cached', () => {
it('should return cached value and not load from source', async () => {
const path = 'some-resource';
const { res, api, fetchMock } = await getTestContext({ path, cache: response });
it('should return cached value and not load from source', () => {
expect(res).toEqual(response); expect(res).toEqual(response);
expect(api.cache['some-resource']).toEqual(response); expect(api.cache[path]).toEqual(response);
expect(datasourceRequestMock).not.toHaveBeenCalled(); expect(fetchMock).not.toHaveBeenCalled();
}); });
}); });
describe('when resource was not cached', () => { describe('when resource was not cached', () => {
let api: Api; it('should return from source and not from cache', async () => {
let res: Array<SelectableValue<string>>; const path = 'some-resource';
beforeEach(async () => { const { res, api, fetchMock } = await getTestContext({ path, response });
api = new Api('/cloudmonitoring/');
res = await api.get('some-resource');
});
it('should return cached value and not load from source', () => {
expect(res).toEqual(response); expect(res).toEqual(response);
expect(api.cache['some-resource']).toEqual(response); expect(api.cache[path]).toEqual(response);
expect(datasourceRequestMock).toHaveBeenCalled(); expect(fetchMock).toHaveBeenCalled();
}); });
}); });
describe('when cache should be bypassed', () => { describe('when cache should be bypassed', () => {
let api: Api; it('should return from source and not from cache', async () => {
let res: Array<SelectableValue<string>>; const options = { useCache: false };
beforeEach(async () => { const path = 'some-resource';
api = new Api('/cloudmonitoring/'); const { res, fetchMock } = await getTestContext({ path, response, cache: response, options });
api.cache['some-resource'] = response;
res = await api.get('some-resource', { useCache: false });
});
it('should return cached value and not load from source', () => {
expect(res).toEqual(response); expect(res).toEqual(response);
expect(datasourceRequestMock).toHaveBeenCalled(); expect(fetchMock).toHaveBeenCalled();
}); });
}); });
}); });

View File

@ -1,11 +1,17 @@
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { SelectableValue } from '@grafana/data';
import { FetchResponse, getBackendSrv } from '@grafana/runtime';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { CoreEvents } from 'app/types'; import { CoreEvents } from 'app/types';
import { SelectableValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { formatCloudMonitoringError } from './functions'; import { formatCloudMonitoringError } from './functions';
import { MetricDescriptor } from './types'; import { MetricDescriptor } from './types';
export interface PostResponse {
results: Record<string, any>;
}
interface Options { interface Options {
responseMap?: (res: any) => SelectableValue<string> | MetricDescriptor; responseMap?: (res: any) => SelectableValue<string> | MetricDescriptor;
baseUrl?: string; baseUrl?: string;
@ -25,48 +31,56 @@ export default class Api {
}; };
} }
async get(path: string, options?: Options): Promise<Array<SelectableValue<string>> | MetricDescriptor[]> { get(path: string, options?: Options): Promise<Array<SelectableValue<string>> | MetricDescriptor[]> {
try { const { useCache, responseMap, baseUrl } = { ...this.defaultOptions, ...options };
const { useCache, responseMap, baseUrl } = { ...this.defaultOptions, ...options };
if (useCache && this.cache[path]) { if (useCache && this.cache[path]) {
return this.cache[path]; return Promise.resolve(this.cache[path]);
} }
const response = await getBackendSrv().datasourceRequest({ return getBackendSrv()
.fetch<Record<string, any>>({
url: baseUrl + path, url: baseUrl + path,
method: 'GET', method: 'GET',
}); })
.pipe(
map(response => {
const responsePropName = path.match(/([^\/]*)\/*$/)![1];
let res = [];
if (response && response.data && response.data[responsePropName]) {
res = response.data[responsePropName].map(responseMap);
}
const responsePropName = path.match(/([^\/]*)\/*$/)![1]; if (useCache) {
let res = []; this.cache[path] = res;
if (response && response.data && response.data[responsePropName]) { }
res = response.data[responsePropName].map(responseMap);
}
if (useCache) { return res;
this.cache[path] = res; }),
} catchError(error => {
appEvents.emit(CoreEvents.dsRequestError, {
return res; error: { data: { error: formatCloudMonitoringError(error) } },
} catch (error) { });
appEvents.emit(CoreEvents.dsRequestError, { error: { data: { error: formatCloudMonitoringError(error) } } }); return of([]);
return []; })
} )
.toPromise();
} }
async post(data: { [key: string]: any }) { post(data: Record<string, any>): Observable<FetchResponse<PostResponse>> {
return getBackendSrv().datasourceRequest({ return getBackendSrv().fetch<PostResponse>({
url: '/api/tsdb/query', url: '/api/tsdb/query',
method: 'POST', method: 'POST',
data, data,
}); });
} }
async test(projectName: string) { test(projectName: string) {
return getBackendSrv().datasourceRequest({ return getBackendSrv()
url: `${this.baseUrl}${projectName}/metricDescriptors`, .fetch<any>({
method: 'GET', url: `${this.baseUrl}${projectName}/metricDescriptors`,
}); method: 'GET',
})
.toPromise();
} }
} }

View File

@ -14,8 +14,10 @@ import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { CloudMonitoringOptions, CloudMonitoringQuery, Filter, MetricDescriptor, QueryType } from './types'; import { CloudMonitoringOptions, CloudMonitoringQuery, Filter, MetricDescriptor, QueryType } from './types';
import { cloudMonitoringUnitMappings } from './constants'; import { cloudMonitoringUnitMappings } from './constants';
import API from './api'; import API, { PostResponse } from './api';
import { CloudMonitoringVariableSupport } from './variables'; import { CloudMonitoringVariableSupport } from './variables';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { from, Observable, of, throwError } from 'rxjs';
export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonitoringQuery, CloudMonitoringOptions> { export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonitoringQuery, CloudMonitoringOptions> {
api: API; api: API;
@ -37,45 +39,52 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
return this.templateSrv.getVariables().map(v => `$${v.name}`); return this.templateSrv.getVariables().map(v => `$${v.name}`);
} }
async query(options: DataQueryRequest<CloudMonitoringQuery>): Promise<DataQueryResponseData> { query(options: DataQueryRequest<CloudMonitoringQuery>): Observable<DataQueryResponseData> {
const result: DataQueryResponseData[] = []; return this.getTimeSeries(options).pipe(
const data = await this.getTimeSeries(options); map(data => {
if (data.results) { if (!data.results) {
Object.values(data.results).forEach((queryRes: any) => { return { data: [] };
if (!queryRes.series) {
return;
} }
const unit = this.resolvePanelUnitFromTargets(options.targets);
queryRes.series.forEach((series: any) => {
let timeSerie: any = {
target: series.name,
datapoints: series.points,
refId: queryRes.refId,
meta: queryRes.meta,
};
if (unit) {
timeSerie = { ...timeSerie, unit };
}
const df = toDataFrame(timeSerie);
for (const field of df.fields) { const result: DataQueryResponseData[] = [];
if (queryRes.meta?.deepLink && queryRes.meta?.deepLink.length > 0) { const values = Object.values(data.results);
field.config.links = [ for (const queryRes of values) {
{ if (!queryRes.series) {
url: queryRes.meta?.deepLink, continue;
title: 'View in Metrics Explorer',
targetBlank: true,
},
];
}
} }
result.push(df);
}); const unit = this.resolvePanelUnitFromTargets(options.targets);
});
return { data: result }; for (const series of queryRes.series) {
} else { let timeSerie: any = {
return { data: [] }; target: series.name,
} datapoints: series.points,
refId: queryRes.refId,
meta: queryRes.meta,
};
if (unit) {
timeSerie = { ...timeSerie, unit };
}
const df = toDataFrame(timeSerie);
for (const field of df.fields) {
if (queryRes.meta?.deepLink && queryRes.meta?.deepLink.length > 0) {
field.config.links = [
{
url: queryRes.meta?.deepLink,
title: 'View in Metrics Explorer',
targetBlank: true,
},
];
}
}
result.push(df);
}
}
return { data: result };
})
);
} }
async annotationQuery(options: any) { async annotationQuery(options: any) {
@ -101,47 +110,57 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
}, },
]; ];
const { data } = await this.api.post({ return this.api
from: options.range.from.valueOf().toString(), .post({
to: options.range.to.valueOf().toString(), from: options.range.from.valueOf().toString(),
queries, to: options.range.to.valueOf().toString(),
}); queries,
})
.pipe(
map(({ data }) => {
const results = data.results['annotationQuery'].tables[0].rows.map((v: any) => {
return {
annotation: annotation,
time: Date.parse(v[0]),
title: v[1],
tags: [],
text: v[3],
} as any;
});
const results = data.results['annotationQuery'].tables[0].rows.map((v: any) => { return results;
return { })
annotation: annotation, )
time: Date.parse(v[0]), .toPromise();
title: v[1],
tags: [],
text: v[3],
} as any;
});
return results;
} }
async getTimeSeries(options: DataQueryRequest<CloudMonitoringQuery>) { getTimeSeries(options: DataQueryRequest<CloudMonitoringQuery>): Observable<PostResponse> {
await this.ensureGCEDefaultProject();
const queries = options.targets const queries = options.targets
.map(this.migrateQuery) .map(this.migrateQuery)
.filter(this.shouldRunQuery) .filter(this.shouldRunQuery)
.map(q => this.prepareTimeSeriesQuery(q, options.scopedVars)) .map(q => this.prepareTimeSeriesQuery(q, options.scopedVars))
.map(q => ({ ...q, intervalMs: options.intervalMs, type: 'timeSeriesQuery' })); .map(q => ({ ...q, intervalMs: options.intervalMs, type: 'timeSeriesQuery' }));
if (queries.length > 0) { if (!queries.length) {
const { data } = await this.api.post({ return of({ results: [] });
from: options.range.from.valueOf().toString(),
to: options.range.to.valueOf().toString(),
queries,
});
return data;
} else {
return { results: [] };
} }
return from(this.ensureGCEDefaultProject()).pipe(
mergeMap(() => {
return this.api.post({
from: options.range.from.valueOf().toString(),
to: options.range.to.valueOf().toString(),
queries,
});
}),
map(({ data }) => {
return data;
})
);
} }
async getLabels(metricType: string, refId: string, projectName: string, groupBys?: string[]) { async getLabels(metricType: string, refId: string, projectName: string, groupBys?: string[]) {
const response = await this.getTimeSeries({ return this.getTimeSeries({
targets: [ targets: [
{ {
refId, refId,
@ -157,9 +176,14 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
}, },
], ],
range: this.timeSrv.timeRange(), range: this.timeSrv.timeRange(),
} as DataQueryRequest<CloudMonitoringQuery>); } as DataQueryRequest<CloudMonitoringQuery>)
const result = response.results[refId]; .pipe(
return result && result.meta ? result.meta.labels : {}; map(response => {
const result = response.results[refId];
return result && result.meta ? result.meta.labels : {};
})
)
.toPromise();
} }
async testDatasource() { async testDatasource() {
@ -205,14 +229,17 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
}, },
], ],
}) })
.then(({ data }) => { .pipe(
return data && data.results && data.results.getGCEDefaultProject && data.results.getGCEDefaultProject.meta map(({ data }) => {
? data.results.getGCEDefaultProject.meta.defaultProject return data && data.results && data.results.getGCEDefaultProject && data.results.getGCEDefaultProject.meta
: ''; ? data.results.getGCEDefaultProject.meta.defaultProject
}) : '';
.catch(err => { }),
throw err.data.error; catchError(err => {
}); return throwError(err.data.error);
})
)
.toPromise();
} }
getDefaultProject(): string { getDefaultProject(): string {
@ -272,7 +299,7 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
}); });
} }
async getProjects() { getProjects() {
return this.api.get(`projects`, { return this.api.get(`projects`, {
responseMap: ({ projectId, name }: { projectId: string; name: string }) => ({ responseMap: ({ projectId, name }: { projectId: string; name: string }) => ({
value: projectId, value: projectId,

View File

@ -1,86 +1,79 @@
import { of, throwError } from 'rxjs';
import { DataSourceInstanceSettings, observableTester, toUtc } from '@grafana/data';
import CloudMonitoringDataSource from '../datasource'; import CloudMonitoringDataSource from '../datasource';
import { metricDescriptors } from './testData'; import { metricDescriptors } from './testData';
import { TemplateSrv } from 'app/features/templating/template_srv'; import { TemplateSrv } from 'app/features/templating/template_srv';
import { DataSourceInstanceSettings, toUtc } from '@grafana/data';
import { CloudMonitoringOptions } from '../types'; import { CloudMonitoringOptions } from '../types';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { CustomVariableModel } from '../../../../features/variables/types'; import { CustomVariableModel } from '../../../../features/variables/types';
import { initialCustomVariableModelState } from '../../../../features/variables/custom/reducer'; import { initialCustomVariableModelState } from '../../../../features/variables/custom/reducer';
import { createFetchResponse } from 'test/helpers/createFetchResponse';
jest.mock('@grafana/runtime', () => ({ jest.mock('@grafana/runtime', () => ({
...((jest.requireActual('@grafana/runtime') as unknown) as object), ...((jest.requireActual('@grafana/runtime') as unknown) as object),
getBackendSrv: () => backendSrv, getBackendSrv: () => backendSrv,
})); }));
interface Result { type Args = { response?: any; throws?: boolean; templateSrv?: TemplateSrv };
status: any;
message?: any; function getTestcontext({ response = {}, throws = false, templateSrv = new TemplateSrv() }: Args = {}) {
} jest.clearAllMocks();
describe('CloudMonitoringDataSource', () => {
const instanceSettings = ({ const instanceSettings = ({
jsonData: { jsonData: {
defaultProject: 'testproject', defaultProject: 'testproject',
}, },
} as unknown) as DataSourceInstanceSettings<CloudMonitoringOptions>; } as unknown) as DataSourceInstanceSettings<CloudMonitoringOptions>;
const templateSrv = new TemplateSrv();
const timeSrv = {} as TimeSrv; const timeSrv = {} as TimeSrv;
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
beforeEach(() => { const fetchMock = jest.spyOn(backendSrv, 'fetch');
jest.clearAllMocks();
datasourceRequestMock.mockImplementation(jest.fn());
});
throws
? fetchMock.mockImplementation(() => throwError(response))
: fetchMock.mockImplementation(() => of(createFetchResponse(response)));
const ds = new CloudMonitoringDataSource(instanceSettings, templateSrv, timeSrv);
return { ds };
}
describe('CloudMonitoringDataSource', () => {
describe('when performing testDataSource', () => { describe('when performing testDataSource', () => {
describe('and call to cloud monitoring api succeeds', () => { describe('and call to cloud monitoring api succeeds', () => {
let ds; it('should return successfully', async () => {
let result: Result; const { ds } = getTestcontext();
beforeEach(async () => {
datasourceRequestMock.mockImplementation(() => Promise.resolve({ status: 200 })); const result = await ds.testDatasource();
ds = new CloudMonitoringDataSource(instanceSettings, templateSrv, timeSrv);
result = await ds.testDatasource();
});
it('should return successfully', () => {
expect(result.status).toBe('success'); expect(result.status).toBe('success');
}); });
}); });
describe('and a list of metricDescriptors are returned', () => { describe('and a list of metricDescriptors are returned', () => {
let ds; it('should return status success', async () => {
let result: Result; const { ds } = getTestcontext({ response: metricDescriptors });
beforeEach(async () => {
datasourceRequestMock.mockImplementation(() => Promise.resolve({ status: 200, data: metricDescriptors }));
ds = new CloudMonitoringDataSource(instanceSettings, templateSrv, timeSrv); const result = await ds.testDatasource();
result = await ds.testDatasource();
});
it('should return status success', () => {
expect(result.status).toBe('success'); expect(result.status).toBe('success');
}); });
}); });
describe('and call to cloud monitoring api fails with 400 error', () => { describe('and call to cloud monitoring api fails with 400 error', () => {
let ds; it('should return error status and a detailed error message', async () => {
let result: Result; const response = {
beforeEach(async () => { statusText: 'Bad Request',
datasourceRequestMock.mockImplementation(() => data: {
Promise.reject({ error: { code: 400, message: 'Field interval.endTime had an invalid value' },
statusText: 'Bad Request', },
data: { };
error: { code: 400, message: 'Field interval.endTime had an invalid value' }, const { ds } = getTestcontext({ response, throws: true });
},
})
);
ds = new CloudMonitoringDataSource(instanceSettings, templateSrv, timeSrv); const result = await ds.testDatasource();
result = await ds.testDatasource();
});
it('should return error status and a detailed error message', () => {
expect(result.status).toEqual('error'); expect(result.status).toEqual('error');
expect(result.message).toBe( expect(result.message).toBe(
'Google Cloud Monitoring: Bad Request: 400. Field interval.endTime had an invalid value' 'Google Cloud Monitoring: Bad Request: 400. Field interval.endTime had an invalid value'
@ -90,154 +83,133 @@ describe('CloudMonitoringDataSource', () => {
}); });
describe('When performing query', () => { describe('When performing query', () => {
const options = {
range: {
from: toUtc('2017-08-22T20:00:00Z'),
to: toUtc('2017-08-22T23:59:00Z'),
},
rangeRaw: {
from: 'now-4h',
to: 'now',
},
targets: [
{
refId: 'A',
},
],
};
describe('and no time series data is returned', () => { describe('and no time series data is returned', () => {
let ds: CloudMonitoringDataSource; it('should return a list of datapoints', done => {
const response: any = { const options = {
results: { range: {
A: { from: toUtc('2017-08-22T20:00:00Z'),
refId: 'A', to: toUtc('2017-08-22T23:59:00Z'),
meta: {
rawQuery: 'arawquerystring',
},
series: null,
tables: null,
}, },
}, rangeRaw: {
}; from: 'now-4h',
to: 'now',
},
targets: [
{
refId: 'A',
},
],
};
beforeEach(() => { const response: any = {
datasourceRequestMock.mockImplementation(() => Promise.resolve({ status: 200, data: response })); results: {
ds = new CloudMonitoringDataSource(instanceSettings, templateSrv, timeSrv); A: {
}); refId: 'A',
meta: {
rawQuery: 'arawquerystring',
},
series: null,
tables: null,
},
},
};
it('should return a list of datapoints', () => { const { ds } = getTestcontext({ response });
return ds.query(options as any).then(results => {
expect(results.data.length).toBe(0); observableTester().subscribeAndExpectOnNext({
expect: results => {
expect(results.data.length).toBe(0);
},
observable: ds.query(options as any),
done,
}); });
}); });
}); });
}); });
describe('when performing getMetricTypes', () => { describe('when performing getMetricTypes', () => {
describe('and call to cloud monitoring api succeeds', () => {}); describe('and call to cloud monitoring api succeeds', () => {
let ds; it('should return successfully', async () => {
let result: any; const response = {
beforeEach(async () => { metricDescriptors: [
datasourceRequestMock.mockImplementation(() => {
Promise.resolve({ displayName: 'test metric name 1',
data: { type: 'compute.googleapis.com/instance/cpu/test-metric-type-1',
metricDescriptors: [ description: 'A description',
{ },
displayName: 'test metric name 1', {
type: 'compute.googleapis.com/instance/cpu/test-metric-type-1', type: 'logging.googleapis.com/user/logbased-metric-with-no-display-name',
description: 'A description', },
}, ],
{ };
type: 'logging.googleapis.com/user/logbased-metric-with-no-display-name', const { ds } = getTestcontext({ response });
},
],
},
})
);
ds = new CloudMonitoringDataSource(instanceSettings, templateSrv, timeSrv); const result = await ds.getMetricTypes('proj');
// @ts-ignore
result = await ds.getMetricTypes('proj');
});
it('should return successfully', () => { expect(result.length).toBe(2);
expect(result.length).toBe(2); expect(result[0].service).toBe('compute.googleapis.com');
expect(result[0].service).toBe('compute.googleapis.com'); expect(result[0].serviceShortName).toBe('compute');
expect(result[0].serviceShortName).toBe('compute'); expect(result[0].type).toBe('compute.googleapis.com/instance/cpu/test-metric-type-1');
expect(result[0].type).toBe('compute.googleapis.com/instance/cpu/test-metric-type-1'); expect(result[0].displayName).toBe('test metric name 1');
expect(result[0].displayName).toBe('test metric name 1'); expect(result[0].description).toBe('A description');
expect(result[0].description).toBe('A description'); expect(result[1].type).toBe('logging.googleapis.com/user/logbased-metric-with-no-display-name');
expect(result[1].type).toBe('logging.googleapis.com/user/logbased-metric-with-no-display-name'); expect(result[1].displayName).toBe('logging.googleapis.com/user/logbased-metric-with-no-display-name');
expect(result[1].displayName).toBe('logging.googleapis.com/user/logbased-metric-with-no-display-name'); });
}); });
}); });
describe('when interpolating a template variable for the filter', () => { describe('when interpolating a template variable for the filter', () => {
let interpolated: any[];
describe('and is single value variable', () => { describe('and is single value variable', () => {
beforeEach(() => {
const filterTemplateSrv = initTemplateSrv('filtervalue1');
const ds = new CloudMonitoringDataSource(instanceSettings, filterTemplateSrv, timeSrv);
interpolated = ds.interpolateFilters(['resource.label.zone', '=~', '${test}'], {});
});
it('should replace the variable with the value', () => { it('should replace the variable with the value', () => {
const templateSrv = initTemplateSrv('filtervalue1');
const { ds } = getTestcontext({ templateSrv });
const interpolated = ds.interpolateFilters(['resource.label.zone', '=~', '${test}'], {});
expect(interpolated.length).toBe(3); expect(interpolated.length).toBe(3);
expect(interpolated[2]).toBe('filtervalue1'); expect(interpolated[2]).toBe('filtervalue1');
}); });
}); });
describe('and is single value variable for the label part', () => { describe('and is single value variable for the label part', () => {
beforeEach(() => {
const filterTemplateSrv = initTemplateSrv('resource.label.zone');
const ds = new CloudMonitoringDataSource(instanceSettings, filterTemplateSrv, timeSrv);
interpolated = ds.interpolateFilters(['${test}', '=~', 'europe-north-1a'], {});
});
it('should replace the variable with the value and not with regex formatting', () => { it('should replace the variable with the value and not with regex formatting', () => {
const templateSrv = initTemplateSrv('resource.label.zone');
const { ds } = getTestcontext({ templateSrv });
const interpolated = ds.interpolateFilters(['${test}', '=~', 'europe-north-1a'], {});
expect(interpolated.length).toBe(3); expect(interpolated.length).toBe(3);
expect(interpolated[0]).toBe('resource.label.zone'); expect(interpolated[0]).toBe('resource.label.zone');
}); });
}); });
describe('and is multi value variable', () => { describe('and is multi value variable', () => {
beforeEach(() => {
const filterTemplateSrv = initTemplateSrv(['filtervalue1', 'filtervalue2'], true);
const ds = new CloudMonitoringDataSource(instanceSettings, filterTemplateSrv, timeSrv);
interpolated = ds.interpolateFilters(['resource.label.zone', '=~', '[[test]]'], {});
});
it('should replace the variable with a regex expression', () => { it('should replace the variable with a regex expression', () => {
const templateSrv = initTemplateSrv(['filtervalue1', 'filtervalue2'], true);
const { ds } = getTestcontext({ templateSrv });
const interpolated = ds.interpolateFilters(['resource.label.zone', '=~', '[[test]]'], {});
expect(interpolated[2]).toBe('(filtervalue1|filtervalue2)'); expect(interpolated[2]).toBe('(filtervalue1|filtervalue2)');
}); });
}); });
}); });
describe('when interpolating a template variable for group bys', () => { describe('when interpolating a template variable for group bys', () => {
let interpolated: any[];
describe('and is single value variable', () => { describe('and is single value variable', () => {
beforeEach(() => {
const groupByTemplateSrv = initTemplateSrv('groupby1');
const ds = new CloudMonitoringDataSource(instanceSettings, groupByTemplateSrv, timeSrv);
interpolated = ds.interpolateGroupBys(['[[test]]'], {});
});
it('should replace the variable with the value', () => { it('should replace the variable with the value', () => {
const templateSrv = initTemplateSrv('groupby1');
const { ds } = getTestcontext({ templateSrv });
const interpolated = ds.interpolateGroupBys(['[[test]]'], {});
expect(interpolated.length).toBe(1); expect(interpolated.length).toBe(1);
expect(interpolated[0]).toBe('groupby1'); expect(interpolated[0]).toBe('groupby1');
}); });
}); });
describe('and is multi value variable', () => { describe('and is multi value variable', () => {
beforeEach(() => {
const groupByTemplateSrv = initTemplateSrv(['groupby1', 'groupby2'], true);
const ds = new CloudMonitoringDataSource(instanceSettings, groupByTemplateSrv, timeSrv);
interpolated = ds.interpolateGroupBys(['[[test]]'], {});
});
it('should replace the variable with an array of group bys', () => { it('should replace the variable with an array of group bys', () => {
const templateSrv = initTemplateSrv(['groupby1', 'groupby2'], true);
const { ds } = getTestcontext({ templateSrv });
const interpolated = ds.interpolateGroupBys(['[[test]]'], {});
expect(interpolated.length).toBe(2); expect(interpolated.length).toBe(2);
expect(interpolated[0]).toBe('groupby1'); expect(interpolated[0]).toBe('groupby1');
expect(interpolated[1]).toBe('groupby2'); expect(interpolated[1]).toBe('groupby2');
@ -246,24 +218,20 @@ describe('CloudMonitoringDataSource', () => {
}); });
describe('unit parsing', () => { describe('unit parsing', () => {
let ds: CloudMonitoringDataSource, res: any; const { ds } = getTestcontext();
beforeEach(() => {
ds = new CloudMonitoringDataSource(instanceSettings, templateSrv, timeSrv);
});
describe('when theres only one target', () => { describe('when theres only one target', () => {
describe('and the cloud monitoring unit does nott have a corresponding grafana unit', () => { describe('and the cloud monitoring unit does nott have a corresponding grafana unit', () => {
beforeEach(() => {
res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }]);
});
it('should return undefined', () => { it('should return undefined', () => {
const res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }]);
expect(res).toBeUndefined(); expect(res).toBeUndefined();
}); });
}); });
describe('and the cloud monitoring unit has a corresponding grafana unit', () => { describe('and the cloud monitoring unit has a corresponding grafana unit', () => {
beforeEach(() => {
res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }]);
});
it('should return bits', () => { it('should return bits', () => {
const res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }]);
expect(res).toEqual('bits'); expect(res).toEqual('bits');
}); });
}); });
@ -271,32 +239,30 @@ describe('CloudMonitoringDataSource', () => {
describe('when theres more than one target', () => { describe('when theres more than one target', () => {
describe('and all target units are the same', () => { describe('and all target units are the same', () => {
beforeEach(() => {
res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }, { unit: 'bit' }]);
});
it('should return bits', () => { it('should return bits', () => {
const res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }, { unit: 'bit' }]);
expect(res).toEqual('bits'); expect(res).toEqual('bits');
}); });
}); });
describe('and all target units are the same but does not have grafana mappings', () => { describe('and all target units are the same but does not have grafana mappings', () => {
beforeEach(() => {
res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }, { unit: 'megaseconds' }]);
});
it('should return the default value of undefined', () => { it('should return the default value of undefined', () => {
const res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }, { unit: 'megaseconds' }]);
expect(res).toBeUndefined(); expect(res).toBeUndefined();
}); });
}); });
describe('and all target units are not the same', () => { describe('and all target units are not the same', () => {
beforeEach(() => {
res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }, { unit: 'min' }]);
});
it('should return the default value of undefined', () => { it('should return the default value of undefined', () => {
const res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }, { unit: 'min' }]);
expect(res).toBeUndefined(); expect(res).toBeUndefined();
}); });
}); });
}); });
}); });
}); });
function initTemplateSrv(values: any, multi = false) { function initTemplateSrv(values: any, multi = false) {
const templateSrv = new TemplateSrv(); const templateSrv = new TemplateSrv();
const test: CustomVariableModel = { const test: CustomVariableModel = {

View File

@ -0,0 +1,15 @@
import { FetchResponse } from '@grafana/runtime';
export function createFetchResponse<T>(data: T): FetchResponse<T> {
return {
data,
status: 200,
url: 'http://localhost:3000/api/query',
config: { url: 'http://localhost:3000/api/query' },
type: 'basic',
statusText: 'Ok',
redirected: false,
headers: ({} as unknown) as Headers,
ok: true,
};
}