Chore: Remove angular dependency from backendSrv (#20999)

* Chore: Remove angular dependency from backendSrv

* Refactor: Naive soultion for logging out unauthorized users

* Refactor: Restructures to different streams

* Refactor: Restructures datasourceRequest

* Refactor: Flipped back if statement

* Refactor: Extracted getFromFetchStream

* Refactor: Extracts toFailureStream operation

* Refactor: Fixes issue when options.params contains arrays

* Refactor: Fixes broken test (but we need a lot more)

* Refactor: Adds explaining comments

* Refactor: Adds latest RxJs version so cancellations work

* Refactor: Cleans up the takeUntil code

* Refactor: Adds tests for request function

* Refactor: Separates into smaller functions

* Refactor: Adds last error tests

* Started to changed so we require getBackendSrv from the @grafana-runtime when applicable.

* Using the getBackendSrv from @grafana/runtime.

* Changed so we use the getBackendSrv from the @grafana-runtime when possible.

* Fixed so Server Admin -> Orgs works again.

* Removed unused dependency.

* Fixed digest issues on the Server Admin -> Users page.

* Fix: Fixes digest problems in Playlists

* Fix: Fixes digest issues in VersionHistory

* Tests: Fixes broken tests

* Fix: Fixes digest issues in Alerting => Notification channels

* Fixed digest issues on the Intive page.

* Fixed so we run digest after password reset email sent.

* Fixed digest issue when trying to sign up account.

* Fixed so the Server Admin -> Edit Org works with backendSrv

* Fixed so Server Admin -> Users works with backend srv.

* Fixed digest issues in Server Admin -> Orgs

* Fix: Fixes digest issues in DashList plugin

* Fixed digest issues on Server Admin -> users.

* Fix: Fixes digest issues with Snapshots

* Fixed digest issue when deleting a user.

* Fix: Fixes digest issues with dashLink

* Chore: Changes RxJs version to 6.5.4 which includes the same cancellation fix

* Fix: Fixes digest issue when toggling folder in manage dashboards

* Fix: Fixes bug in executeInOrder

* Fix: Fixes digest issue with CreateFolderCtrl and FolderDashboardsCtrl

* Fix: Fixes tslint error in test

* Refactor: Changes default behaviour for emitted messages as before migration

* Fix: Fixes various digest issues when saving, starring or deleting dashboards

* Fix: Fixes digest issues with FolderPickerCtrl

* Fixed digest issue.

* Fixed digest issues.

* Fixed issues with angular digest.

* Removed the this.digest pattern.

Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com>
Co-authored-by: Marcus Andersson <systemvetaren@gmail.com>
This commit is contained in:
kay delaney 2020-01-21 09:08:07 +00:00 committed by Hugo Häggmark
parent 6ff315a299
commit cf2cc71393
122 changed files with 2856 additions and 2016 deletions

View File

@ -259,7 +259,7 @@
"redux-thunk": "2.3.0",
"reselect": "4.0.0",
"rst2html": "github:thoward/rst2html#990cb89",
"rxjs": "6.4.0",
"rxjs": "6.5.4",
"search-query-parser": "1.5.2",
"slate": "0.47.8",
"slate-plain-serializer": "0.7.10",

View File

@ -39,7 +39,7 @@
"rollup-plugin-terser": "4.0.4",
"rollup-plugin-typescript2": "0.19.3",
"rollup-plugin-visualizer": "0.9.2",
"rxjs": "6.4.0",
"rxjs": "6.5.4",
"sinon": "1.17.6",
"typescript": "3.7.2"
}

View File

@ -43,10 +43,8 @@ export interface BackendSrv {
let singletonInstance: BackendSrv;
export function setBackendSrv(instance: BackendSrv) {
export const setBackendSrv = (instance: BackendSrv) => {
singletonInstance = instance;
}
};
export function getBackendSrv(): BackendSrv {
return singletonInstance;
}
export const getBackendSrv = (): BackendSrv => singletonInstance;

View File

@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { AsyncSelect } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { debounce } from 'lodash';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv';
import { DashboardSearchHit, DashboardDTO } from 'app/types';
export interface Props {
@ -33,20 +33,16 @@ export class DashboardPicker extends PureComponent<Props, State> {
getDashboards = (query = '') => {
this.setState({ isLoading: true });
return getBackendSrv()
.search({ type: 'dash-db', query })
.then((result: DashboardSearchHit[]) => {
const dashboards = result.map((item: DashboardSearchHit) => {
return {
id: item.id,
value: item.id,
label: `${item.folderTitle ? item.folderTitle : 'General'}/${item.title}`,
};
});
return backendSrv.search({ type: 'dash-db', query }).then((result: DashboardSearchHit[]) => {
const dashboards = result.map((item: DashboardSearchHit) => ({
id: item.id,
value: item.id,
label: `${item.folderTitle ? item.folderTitle : 'General'}/${item.title}`,
}));
this.setState({ isLoading: false });
return dashboards;
});
this.setState({ isLoading: false });
return dashboards;
});
};
render() {

View File

@ -3,7 +3,7 @@ import React from 'react';
import renderer from 'react-test-renderer';
import { TeamPicker } from './TeamPicker';
jest.mock('app/core/services/backend_srv', () => ({
jest.mock('@grafana/runtime', () => ({
getBackendSrv: () => {
return {
get: () => {

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import _ from 'lodash';
import { AsyncSelect } from '@grafana/ui';
import { debounce } from 'lodash';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
export interface Team {
id: number;
@ -35,27 +35,28 @@ export class TeamPicker extends Component<Props, State> {
}
search(query?: string) {
const backendSrv = getBackendSrv();
this.setState({ isLoading: true });
if (_.isNil(query)) {
query = '';
}
return backendSrv.get(`/api/teams/search?perpage=100&page=1&query=${query}`).then((result: any) => {
const teams = result.teams.map((team: any) => {
return {
id: team.id,
value: team.id,
label: team.name,
name: team.name,
imgUrl: team.avatarUrl,
};
});
return getBackendSrv()
.get(`/api/teams/search?perpage=100&page=1&query=${query}`)
.then((result: any) => {
const teams = result.teams.map((team: any) => {
return {
id: team.id,
value: team.id,
label: team.name,
name: team.name,
imgUrl: team.avatarUrl,
};
});
this.setState({ isLoading: false });
return teams;
});
this.setState({ isLoading: false });
return teams;
});
}
render() {

View File

@ -3,14 +3,8 @@ import React from 'react';
import renderer from 'react-test-renderer';
import { UserPicker } from './UserPicker';
jest.mock('app/core/services/backend_srv', () => ({
getBackendSrv: () => {
return {
get: () => {
return Promise.resolve([]);
},
};
},
jest.mock('@grafana/runtime', () => ({
getBackendSrv: () => ({ get: jest.fn().mockResolvedValue([]) }),
}));
describe('UserPicker', () => {

View File

@ -7,7 +7,7 @@ import { AsyncSelect } from '@grafana/ui';
// Utils & Services
import { debounce } from 'lodash';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
// Types
import { User } from 'app/types';
@ -36,14 +36,13 @@ export class UserPicker extends Component<Props, State> {
}
search(query?: string) {
const backendSrv = getBackendSrv();
this.setState({ isLoading: true });
if (_.isNil(query)) {
query = '';
}
return backendSrv
return getBackendSrv()
.get(`/api/org/users/lookup?query=${query}&limit=10`)
.then((result: any) => {
return result.map((user: any) => ({

View File

@ -3,7 +3,7 @@ import React, { PureComponent } from 'react';
import { FormLabel, Select } from '@grafana/ui';
import { DashboardSearchHit, DashboardSearchHitType } from 'app/types';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv';
export interface Props {
resourceUri: string;
@ -29,7 +29,7 @@ const timezones = [
];
export class SharedPreferences extends PureComponent<Props, State> {
backendSrv = getBackendSrv();
backendSrv = backendSrv;
constructor(props: Props) {
super(props);
@ -43,8 +43,8 @@ export class SharedPreferences extends PureComponent<Props, State> {
}
async componentDidMount() {
const prefs = await this.backendSrv.get(`/api/${this.props.resourceUri}/preferences`);
const dashboards = await this.backendSrv.search({ starred: true });
const prefs = await backendSrv.get(`/api/${this.props.resourceUri}/preferences`);
const dashboards = await backendSrv.search({ starred: true });
const defaultDashboardHit: DashboardSearchHit = {
id: 0,
title: 'Default',
@ -62,7 +62,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
};
if (prefs.homeDashboardId > 0 && !dashboards.find(d => d.id === prefs.homeDashboardId)) {
const missing = await this.backendSrv.search({ dashboardIds: [prefs.homeDashboardId] });
const missing = await backendSrv.search({ dashboardIds: [prefs.homeDashboardId] });
if (missing && missing.length > 0) {
dashboards.push(missing[0]);
}
@ -81,7 +81,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
const { homeDashboardId, theme, timezone } = this.state;
await this.backendSrv.put(`/api/${this.props.resourceUri}/preferences`, {
await backendSrv.put(`/api/${this.props.resourceUri}/preferences`, {
homeDashboardId,
theme,
timezone,

View File

@ -1,5 +1,5 @@
import coreModule from 'app/core/core_module';
import { BackendSrv } from '../services/backend_srv';
import { backendSrv } from '../services/backend_srv';
const template = `
<select class="gf-form-input" ng-model="ctrl.model" ng-options="f.value as f.text for f in ctrl.options"></select>
@ -9,13 +9,10 @@ export class DashboardSelectorCtrl {
model: any;
options: any;
/** @ngInject */
constructor(private backendSrv: BackendSrv) {}
$onInit() {
this.options = [{ value: 0, text: 'Default' }];
return this.backendSrv.search({ starred: true }).then(res => {
return backendSrv.search({ starred: true }).then(res => {
res.forEach(dash => {
this.options.push({ value: dash.id, text: dash.title });
});

View File

@ -3,9 +3,10 @@ import _ from 'lodash';
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import { SearchSrv } from 'app/core/services/search_srv';
import { BackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv';
import { ContextSrv } from 'app/core/services/context_srv';
import { CoreEvents } from 'app/types';
import { promiseToDigest } from '../../utils/promiseToDigest';
export interface Section {
id: number;
@ -69,12 +70,7 @@ export class ManageDashboardsCtrl {
hasEditPermissionInFolders: boolean;
/** @ngInject */
constructor(
private $scope: IScope,
private backendSrv: BackendSrv,
private searchSrv: SearchSrv,
private contextSrv: ContextSrv
) {
constructor(private $scope: IScope, private searchSrv: SearchSrv, private contextSrv: ContextSrv) {
this.isEditor = this.contextSrv.isEditor;
this.hasEditPermissionInFolders = this.contextSrv.hasEditPermissionInFolders;
@ -108,10 +104,10 @@ export class ManageDashboardsCtrl {
.then(() => {
if (!this.folderUid) {
this.$scope.$digest();
return;
return undefined;
}
return this.backendSrv.getFolderByUid(this.folderUid).then((folder: any) => {
return backendSrv.getFolderByUid(this.folderUid).then((folder: any) => {
this.canSave = folder.canSave;
if (!this.canSave) {
this.hasEditPermissionInFolders = false;
@ -216,9 +212,11 @@ export class ManageDashboardsCtrl {
}
private deleteFoldersAndDashboards(folderUids: string[], dashboardUids: string[]) {
this.backendSrv.deleteFoldersAndDashboards(folderUids, dashboardUids).then(() => {
this.refreshList();
});
promiseToDigest(this.$scope)(
backendSrv.deleteFoldersAndDashboards(folderUids, dashboardUids).then(() => {
this.refreshList();
})
);
}
getDashboardsToMove() {

View File

@ -1,9 +1,11 @@
import _ from 'lodash';
import { ILocationService, IScope } from 'angular';
import { e2e } from '@grafana/e2e';
import coreModule from '../../core_module';
import appEvents from 'app/core/app_events';
import { CoreEvents } from 'app/types';
import { promiseToDigest } from '../../utils/promiseToDigest';
export class SearchResultsCtrl {
results: any;
@ -14,7 +16,7 @@ export class SearchResultsCtrl {
selectors: typeof e2e.pages.Dashboards.selectors;
/** @ngInject */
constructor(private $location: any) {
constructor(private $location: ILocationService, private $scope: IScope) {
this.selectors = e2e.pages.Dashboards.selectors;
}
@ -24,19 +26,21 @@ export class SearchResultsCtrl {
this.onFolderExpanding();
}
section.toggle(section).then((f: any) => {
if (this.editable && f.expanded) {
if (f.items) {
_.each(f.items, i => {
i.checked = f.checked;
});
promiseToDigest(this.$scope)(
section.toggle(section).then((f: any) => {
if (this.editable && f.expanded) {
if (f.items) {
_.each(f.items, i => {
i.checked = f.checked;
});
if (this.onSelectionChanged) {
this.onSelectionChanged();
if (this.onSelectionChanged) {
this.onSelectionChanged();
}
}
}
}
});
})
);
}
}

View File

@ -1,9 +1,11 @@
import coreModule from '../core_module';
import config from 'app/core/config';
import { getBackendSrv } from '@grafana/runtime';
import { promiseToDigest } from '../utils/promiseToDigest';
export class InvitedCtrl {
/** @ngInject */
constructor($scope: any, $routeParams: any, contextSrv: any, backendSrv: any) {
constructor($scope: any, $routeParams: any, contextSrv: any) {
contextSrv.sidemenu = false;
$scope.formModel = {};
@ -17,15 +19,19 @@ export class InvitedCtrl {
};
$scope.init = () => {
backendSrv.get('/api/user/invite/' + $routeParams.code).then((invite: any) => {
$scope.formModel.name = invite.name;
$scope.formModel.email = invite.email;
$scope.formModel.username = invite.email;
$scope.formModel.inviteCode = $routeParams.code;
promiseToDigest($scope)(
getBackendSrv()
.get('/api/user/invite/' + $routeParams.code)
.then((invite: any) => {
$scope.formModel.name = invite.name;
$scope.formModel.email = invite.email;
$scope.formModel.username = invite.email;
$scope.formModel.inviteCode = $routeParams.code;
$scope.greeting = invite.name || invite.email || invite.username;
$scope.invitedBy = invite.invitedBy;
});
$scope.greeting = invite.name || invite.email || invite.username;
$scope.invitedBy = invite.invitedBy;
})
);
};
$scope.submit = () => {
@ -33,9 +39,11 @@ export class InvitedCtrl {
return;
}
backendSrv.post('/api/user/invite/complete', $scope.formModel).then(() => {
window.location.href = config.appSubUrl + '/';
});
getBackendSrv()
.post('/api/user/invite/complete', $scope.formModel)
.then(() => {
window.location.href = config.appSubUrl + '/';
});
};
$scope.init();

View File

@ -1,11 +1,12 @@
import coreModule from '../core_module';
import config from 'app/core/config';
import { BackendSrv } from '../services/backend_srv';
import { AppEvents } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { promiseToDigest } from '../utils/promiseToDigest';
export class ResetPasswordCtrl {
/** @ngInject */
constructor($scope: any, backendSrv: BackendSrv, $location: any) {
constructor($scope: any, $location: any) {
$scope.formModel = {};
$scope.mode = 'send';
$scope.ldapEnabled = config.ldapEnabled;
@ -31,9 +32,14 @@ export class ResetPasswordCtrl {
if (!$scope.sendResetForm.$valid) {
return;
}
backendSrv.post('/api/user/password/send-reset-email', $scope.formModel).then(() => {
$scope.mode = 'email-sent';
});
promiseToDigest($scope)(
getBackendSrv()
.post('/api/user/password/send-reset-email', $scope.formModel)
.then(() => {
$scope.mode = 'email-sent';
})
);
};
$scope.submitReset = () => {
@ -46,9 +52,11 @@ export class ResetPasswordCtrl {
return;
}
backendSrv.post('/api/user/password/reset', $scope.formModel).then(() => {
$location.path('login');
});
getBackendSrv()
.post('/api/user/password/reset', $scope.formModel)
.then(() => {
$location.path('login');
});
};
}
}

View File

@ -1,9 +1,11 @@
import config from 'app/core/config';
import coreModule from '../core_module';
import { getBackendSrv } from '@grafana/runtime/src/services';
import { promiseToDigest } from '../utils/promiseToDigest';
export class SignUpCtrl {
/** @ngInject */
constructor(private $scope: any, private backendSrv: any, $location: any, contextSrv: any) {
constructor(private $scope: any, $location: any, contextSrv: any) {
contextSrv.sidemenu = false;
$scope.ctrl = this;
@ -34,10 +36,14 @@ export class SignUpCtrl {
},
};
backendSrv.get('/api/user/signup/options').then((options: any) => {
$scope.verifyEmailEnabled = options.verifyEmailEnabled;
$scope.autoAssignOrg = options.autoAssignOrg;
});
promiseToDigest($scope)(
getBackendSrv()
.get('/api/user/signup/options')
.then((options: any) => {
$scope.verifyEmailEnabled = options.verifyEmailEnabled;
$scope.autoAssignOrg = options.autoAssignOrg;
})
);
}
submit() {
@ -45,13 +51,15 @@ export class SignUpCtrl {
return;
}
this.backendSrv.post('/api/user/signup/step2', this.$scope.formModel).then((rsp: any) => {
if (rsp.code === 'redirect-to-select-org') {
window.location.href = config.appSubUrl + '/profile/select-org?signup=1';
} else {
window.location.href = config.appSubUrl + '/';
}
});
getBackendSrv()
.post('/api/user/signup/step2', this.$scope.formModel)
.then((rsp: any) => {
if (rsp.code === 'redirect-to-select-org') {
window.location.href = config.appSubUrl + '/profile/select-org?signup=1';
} else {
window.location.href = config.appSubUrl + '/';
}
});
}
}

View File

@ -1,57 +1,119 @@
import _ from 'lodash';
import angular from 'angular';
import coreModule from 'app/core/core_module';
import omitBy from 'lodash/omitBy';
import { from, merge, MonoTypeOperatorFunction, Observable, Subject, throwError } from 'rxjs';
import { catchError, filter, map, mergeMap, retryWhen, share, takeUntil, tap } from 'rxjs/operators';
import { fromFetch } from 'rxjs/fetch';
import { BackendSrv as BackendService, BackendSrvRequest } from '@grafana/runtime';
import { AppEvents } from '@grafana/data';
import appEvents from 'app/core/app_events';
import config from 'app/core/config';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { DashboardSearchHit } from 'app/types/search';
import { ContextSrv } from './context_srv';
import { FolderInfo, DashboardDTO, CoreEvents } from 'app/types';
import { BackendSrv as BackendService, getBackendSrv as getBackendService, BackendSrvRequest } from '@grafana/runtime';
import { AppEvents } from '@grafana/data';
import { CoreEvents, DashboardDTO, FolderInfo } from 'app/types';
import { ContextSrv, contextSrv } from './context_srv';
import { coreModule } from 'app/core/core_module';
import { Emitter } from '../utils/emitter';
export interface DatasourceRequestOptions {
retry?: number;
method?: string;
requestId?: string;
timeout?: angular.IPromise<any>;
timeout?: Promise<any>;
url?: string;
headers?: { [key: string]: any };
headers?: Record<string, any>;
silent?: boolean;
data?: { [key: string]: any };
data?: Record<string, any>;
}
interface FetchResponseProps {
message?: string;
}
interface ErrorResponseProps extends FetchResponseProps {
status?: string;
error?: string | any;
}
export interface FetchResponse<T extends FetchResponseProps = any> {
status: number;
statusText: string;
ok: boolean;
data: T;
}
interface SuccessResponse extends FetchResponseProps, Record<any, any> {}
interface DataSourceSuccessResponse<T extends {} = any> {
data: T;
}
interface ErrorResponse<T extends ErrorResponseProps = any> {
status: number;
statusText?: string;
isHandled?: boolean;
data: T | string;
cancelled?: boolean;
}
function serializeParams(data: Record<string, any>): string {
return Object.keys(data)
.map(key => {
const value = data[key];
if (Array.isArray(value)) {
return value.map(arrayValue => `${encodeURIComponent(key)}=${encodeURIComponent(arrayValue)}`).join('&');
}
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
})
.join('&');
}
export interface BackendSrvDependencies {
fromFetch: (input: string | Request, init?: RequestInit) => Observable<Response>;
appEvents: Emitter;
contextSrv: ContextSrv;
logout: () => void;
}
export class BackendSrv implements BackendService {
private inFlightRequests: { [key: string]: Array<angular.IDeferred<any>> } = {};
private inFlightRequests: Subject<string> = new Subject<string>();
private HTTP_REQUEST_CANCELED = -1;
private noBackendCache: boolean;
private dependencies: BackendSrvDependencies = {
fromFetch: fromFetch,
appEvents: appEvents,
contextSrv: contextSrv,
logout: () => {
window.location.href = config.appSubUrl + '/logout';
},
};
/** @ngInject */
constructor(
private $http: any,
private $q: angular.IQService,
private $timeout: angular.ITimeoutService,
private contextSrv: ContextSrv
) {}
get(url: string, params?: any) {
return this.request({ method: 'GET', url, params });
constructor(deps?: BackendSrvDependencies) {
if (deps) {
this.dependencies = {
...this.dependencies,
...deps,
};
}
}
delete(url: string) {
return this.request({ method: 'DELETE', url });
async get(url: string, params?: any) {
return await this.request({ method: 'GET', url, params });
}
post(url: string, data?: any) {
return this.request({ method: 'POST', url, data });
async delete(url: string) {
return await this.request({ method: 'DELETE', url });
}
patch(url: string, data: any) {
return this.request({ method: 'PATCH', url, data });
async post(url: string, data?: any) {
return await this.request({ method: 'POST', url, data });
}
put(url: string, data: any) {
return this.request({ method: 'PUT', url, data });
async patch(url: string, data: any) {
return await this.request({ method: 'PATCH', url, data });
}
async put(url: string, data: any) {
return await this.request({ method: 'PUT', url, data });
}
withNoBackendCache(callback: any) {
@ -61,18 +123,18 @@ export class BackendSrv implements BackendService {
});
}
requestErrorHandler(err: any) {
requestErrorHandler = (err: ErrorResponse) => {
if (err.isHandled) {
return;
}
let data = err.data || { message: 'Unexpected error' };
if (_.isString(data)) {
let data = err.data ?? { message: 'Unexpected error' };
if (typeof data === 'string') {
data = { message: data };
}
if (err.status === 422) {
appEvents.emit(AppEvents.alertWarning, ['Validation failed', data.message]);
this.dependencies.appEvents.emit(AppEvents.alertWarning, ['Validation failed', data.message]);
throw data;
}
@ -84,170 +146,133 @@ export class BackendSrv implements BackendService {
message = 'Error';
}
appEvents.emit(err.status < 500 ? AppEvents.alertWarning : AppEvents.alertError, [message, description]);
this.dependencies.appEvents.emit(err.status < 500 ? AppEvents.alertWarning : AppEvents.alertError, [
message,
description,
]);
}
throw data;
}
};
request(options: BackendSrvRequest) {
options.retry = options.retry || 0;
const requestIsLocal = !options.url.match(/^http/);
const firstAttempt = options.retry === 0;
async request(options: BackendSrvRequest): Promise<any> {
options = this.parseRequestOptions(options, this.dependencies.contextSrv.user?.orgId);
if (requestIsLocal) {
if (this.contextSrv.user && this.contextSrv.user.orgId) {
options.headers = options.headers || {};
options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId;
}
if (options.url.indexOf('/') === 0) {
options.url = options.url.substring(1);
}
}
return this.$http(options).then(
(results: any) => {
if (options.method !== 'GET') {
if (results && results.data.message) {
if (options.showSuccessAlert !== false) {
appEvents.emit(AppEvents.alertSuccess, [results.data.message]);
}
}
const fromFetchStream = this.getFromFetchStream(options);
const failureStream = fromFetchStream.pipe(this.toFailureStream(options));
const successStream = fromFetchStream.pipe(
filter(response => response.ok === true),
map(response => {
const fetchSuccessResponse: SuccessResponse = response.data;
return fetchSuccessResponse;
}),
tap(response => {
if (options.method !== 'GET' && response?.message && options.showSuccessAlert !== false) {
this.dependencies.appEvents.emit(AppEvents.alertSuccess, [response.message]);
}
return results.data;
},
(err: any) => {
// handle unauthorized
if (err.status === 401 && this.contextSrv.user.isSignedIn && firstAttempt) {
return this.loginPing()
.then(() => {
options.retry = 1;
return this.request(options);
})
.catch((err: any) => {
if (err.status === 401) {
window.location.href = config.appSubUrl + '/logout';
throw err;
}
});
}
this.$timeout(this.requestErrorHandler.bind(this, err), 50);
throw err;
}
})
);
}
addCanceler(requestId: string, canceler: angular.IDeferred<any>) {
if (requestId in this.inFlightRequests) {
this.inFlightRequests[requestId].push(canceler);
} else {
this.inFlightRequests[requestId] = [canceler];
}
return merge(successStream, failureStream)
.pipe(
catchError((err: ErrorResponse) => {
if (err.status === 401) {
this.dependencies.logout();
return throwError(err);
}
// this setTimeout hack enables any caller catching this err to set isHandled to true
setTimeout(() => this.requestErrorHandler(err), 50);
return throwError(err);
})
)
.toPromise();
}
resolveCancelerIfExists(requestId: string) {
const cancelers = this.inFlightRequests[requestId];
if (!_.isUndefined(cancelers) && cancelers.length) {
cancelers[0].resolve();
}
this.inFlightRequests.next(requestId);
}
datasourceRequest(options: BackendSrvRequest) {
let canceler: angular.IDeferred<any> = null;
options.retry = options.retry || 0;
// A requestID is provided by the datasource as a unique identifier for a
// particular query. If the requestID exists, the promise it is keyed to
// is canceled, canceling the previous datasource request if it is still
// in-flight.
const requestId = options.requestId;
if (requestId) {
this.resolveCancelerIfExists(requestId);
// create new canceler
canceler = this.$q.defer();
options.timeout = canceler.promise;
this.addCanceler(requestId, canceler);
async datasourceRequest(options: BackendSrvRequest): Promise<any> {
// A requestId is provided by the datasource as a unique identifier for a
// particular query. Every observable below has a takeUntil that subscribes to this.inFlightRequests and
// will cancel/unsubscribe that observable when a new datasourceRequest with the same requestId is made
if (options.requestId) {
this.inFlightRequests.next(options.requestId);
}
const requestIsLocal = !options.url.match(/^http/);
const firstAttempt = options.retry === 0;
options = this.parseDataSourceRequestOptions(
options,
this.dependencies.contextSrv.user?.orgId,
this.noBackendCache
);
if (requestIsLocal) {
if (this.contextSrv.user && this.contextSrv.user.orgId) {
options.headers = options.headers || {};
options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId;
}
if (options.url.indexOf('/') === 0) {
options.url = options.url.substring(1);
}
if (options.headers && options.headers.Authorization) {
options.headers['X-DS-Authorization'] = options.headers.Authorization;
delete options.headers.Authorization;
}
if (this.noBackendCache) {
options.headers['X-Grafana-NoCache'] = 'true';
}
}
return this.$http(options)
.then((response: any) => {
const fromFetchStream = this.getFromFetchStream(options);
const failureStream = fromFetchStream.pipe(this.toFailureStream(options));
const successStream = fromFetchStream.pipe(
filter(response => response.ok === true),
map(response => {
const { data } = response;
const fetchSuccessResponse: DataSourceSuccessResponse = { data };
return fetchSuccessResponse;
}),
tap(res => {
if (!options.silent) {
appEvents.emit(CoreEvents.dsRequestResponse, response);
this.dependencies.appEvents.emit(CoreEvents.dsRequestResponse, res);
}
return response;
})
.catch((err: any) => {
if (err.status === this.HTTP_REQUEST_CANCELED) {
throw { err, cancelled: true };
}
);
// handle unauthorized for backend requests
if (requestIsLocal && firstAttempt && err.status === 401) {
return this.loginPing()
.then(() => {
options.retry = 1;
if (canceler) {
canceler.resolve();
}
return this.datasourceRequest(options);
})
.catch((err: any) => {
if (err.status === 401) {
window.location.href = config.appSubUrl + '/logout';
throw err;
}
return merge(successStream, failureStream)
.pipe(
catchError((err: ErrorResponse) => {
if (err.status === this.HTTP_REQUEST_CANCELED) {
return throwError({
err,
cancelled: true,
});
}
}
// populate error obj on Internal Error
if (_.isString(err.data) && err.status === 500) {
err.data = {
error: err.statusText,
response: err.data,
};
}
if (err.status === 401) {
this.dependencies.logout();
return throwError(err);
}
// for Prometheus
if (err.data && !err.data.message && _.isString(err.data.error)) {
err.data.message = err.data.error;
}
if (!options.silent) {
appEvents.emit(CoreEvents.dsRequestError, err);
}
throw err;
})
.finally(() => {
// clean up
if (options.requestId) {
this.inFlightRequests[options.requestId].shift();
}
});
// populate error obj on Internal Error
if (typeof err.data === 'string' && err.status === 500) {
err.data = {
error: err.statusText,
response: err.data,
};
}
// for Prometheus
if (err.data && !err.data.message && typeof err.data.error === 'string') {
err.data.message = err.data.error;
}
if (!options.silent) {
this.dependencies.appEvents.emit(CoreEvents.dsRequestError, err);
}
return throwError(err);
}),
takeUntil(
this.inFlightRequests.pipe(
filter(requestId => {
let cancelRequest = false;
if (options && options.requestId && options.requestId === requestId) {
// when a new requestId is started it will be published to inFlightRequests
// if a previous long running request that hasn't finished yet has the same requestId
// we need to cancel that request
cancelRequest = true;
}
return cancelRequest;
})
)
)
)
.toPromise();
}
loginPing() {
@ -309,7 +334,7 @@ export class BackendSrv implements BackendService {
tasks.push(this.createTask(this.deleteDashboard.bind(this), true, dashboardUid, true));
}
return this.executeInOrder(tasks, []);
return this.executeInOrder(tasks);
}
moveDashboards(dashboardUids: string[], toFolder: FolderInfo) {
@ -319,82 +344,184 @@ export class BackendSrv implements BackendService {
tasks.push(this.createTask(this.moveDashboard.bind(this), true, uid, toFolder));
}
return this.executeInOrder(tasks, []).then((result: any) => {
return this.executeInOrder(tasks).then((result: any) => {
return {
totalCount: result.length,
successCount: _.filter(result, { succeeded: true }).length,
alreadyInFolderCount: _.filter(result, { alreadyInFolder: true }).length,
successCount: result.filter((res: any) => res.succeeded).length,
alreadyInFolderCount: result.filter((res: any) => res.alreadyInFolder).length,
};
});
}
private moveDashboard(uid: string, toFolder: FolderInfo) {
const deferred = this.$q.defer();
private async moveDashboard(uid: string, toFolder: FolderInfo) {
const fullDash: DashboardDTO = await this.getDashboardByUid(uid);
const model = new DashboardModel(fullDash.dashboard, fullDash.meta);
this.getDashboardByUid(uid).then((fullDash: DashboardDTO) => {
const model = new DashboardModel(fullDash.dashboard, fullDash.meta);
if ((!fullDash.meta.folderId && toFolder.id === 0) || fullDash.meta.folderId === toFolder.id) {
return { alreadyInFolder: true };
}
if ((!fullDash.meta.folderId && toFolder.id === 0) || fullDash.meta.folderId === toFolder.id) {
deferred.resolve({ alreadyInFolder: true });
return;
const clone = model.getSaveModelClone();
const options = {
folderId: toFolder.id,
overwrite: false,
};
try {
await this.saveDashboard(clone, options);
return { succeeded: true };
} catch (err) {
if (err.data?.status !== 'plugin-dashboard') {
return { succeeded: false };
}
const clone = model.getSaveModelClone();
const options = {
folderId: toFolder.id,
overwrite: false,
};
err.isHandled = true;
options.overwrite = true;
this.saveDashboard(clone, options)
.then(() => {
deferred.resolve({ succeeded: true });
})
.catch((err: any) => {
if (err.data && err.data.status === 'plugin-dashboard') {
err.isHandled = true;
options.overwrite = true;
this.saveDashboard(clone, options)
.then(() => {
deferred.resolve({ succeeded: true });
})
.catch((err: any) => {
deferred.resolve({ succeeded: false });
});
} else {
deferred.resolve({ succeeded: false });
}
});
});
return deferred.promise;
try {
await this.saveDashboard(clone, options);
return { succeeded: true };
} catch (e) {
return { succeeded: false };
}
}
}
private createTask(fn: Function, ignoreRejections: boolean, ...args: any[]) {
return (result: any) => {
return fn
.apply(null, args)
.then((res: any) => {
return Array.prototype.concat(result, [res]);
})
.catch((err: any) => {
if (ignoreRejections) {
return result;
}
private createTask(fn: (...args: any[]) => Promise<any>, ignoreRejections: boolean, ...args: any[]) {
return async (result: any) => {
try {
const res = await fn(...args);
return Array.prototype.concat(result, [res]);
} catch (err) {
if (ignoreRejections) {
return result;
}
throw err;
});
throw err;
}
};
}
private executeInOrder(tasks: any[], initialValue: any[]) {
return tasks.reduce(this.$q.when, initialValue);
private executeInOrder(tasks: any[]) {
return tasks.reduce((acc, task) => {
return Promise.resolve(acc).then(task);
}, []);
}
private parseRequestOptions = (options: BackendSrvRequest, orgId?: number): BackendSrvRequest => {
options.retry = options.retry ?? 0;
const requestIsLocal = !options.url.match(/^http/);
if (requestIsLocal) {
if (orgId) {
options.headers = options.headers ?? {};
options.headers['X-Grafana-Org-Id'] = orgId;
}
if (options.url.startsWith('/')) {
options.url = options.url.substring(1);
}
if (options.url.endsWith('/')) {
options.url = options.url.slice(0, -1);
}
}
return options;
};
private parseDataSourceRequestOptions = (
options: BackendSrvRequest,
orgId?: number,
noBackendCache?: boolean
): BackendSrvRequest => {
options.retry = options.retry ?? 0;
const requestIsLocal = !options.url.match(/^http/);
if (requestIsLocal) {
if (orgId) {
options.headers = options.headers || {};
options.headers['X-Grafana-Org-Id'] = orgId;
}
if (options.url.startsWith('/')) {
options.url = options.url.substring(1);
}
if (options.headers?.Authorization) {
options.headers['X-DS-Authorization'] = options.headers.Authorization;
delete options.headers.Authorization;
}
if (noBackendCache) {
options.headers['X-Grafana-NoCache'] = 'true';
}
}
return options;
};
private parseUrlFromOptions = (options: BackendSrvRequest): string => {
const cleanParams = omitBy(options.params, v => v === undefined || (v && v.length === 0));
const serializedParams = serializeParams(cleanParams);
return options.params && serializedParams.length ? `${options.url}?${serializedParams}` : options.url;
};
private parseInitFromOptions = (options: BackendSrvRequest): RequestInit => ({
method: options.method,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, text/plain, */*',
...options.headers,
},
body: JSON.stringify(options.data),
});
private getFromFetchStream = (options: BackendSrvRequest) => {
const url = this.parseUrlFromOptions(options);
const init = this.parseInitFromOptions(options);
return this.dependencies.fromFetch(url, init).pipe(
mergeMap(async response => {
const { status, statusText, ok } = response;
const textData = await response.text(); // this could be just a string, prometheus requests for instance
let data;
try {
data = JSON.parse(textData); // majority of the requests this will be something that can be parsed
} catch {
data = textData;
}
const fetchResponse: FetchResponse = { status, statusText, ok, data };
return fetchResponse;
}),
share() // sharing this so we can split into success and failure and then merge back
);
};
private toFailureStream = (options: BackendSrvRequest): MonoTypeOperatorFunction<FetchResponse> => inputStream =>
inputStream.pipe(
filter(response => response.ok === false),
mergeMap(response => {
const { status, statusText, data } = response;
const fetchErrorResponse: ErrorResponse = { status, statusText, data };
return throwError(fetchErrorResponse);
}),
retryWhen((attempts: Observable<any>) =>
attempts.pipe(
mergeMap((error, i) => {
const firstAttempt = i === 0 && options.retry === 0;
if (error.status === 401 && this.dependencies.contextSrv.user.isSignedIn && firstAttempt) {
return from(this.loginPing());
}
return throwError(error);
})
)
)
);
}
coreModule.service('backendSrv', BackendSrv);
coreModule.factory('backendSrv', () => backendSrv);
// Used for testing and things that really need BackendSrv
export function getBackendSrv(): BackendSrv {
return getBackendService() as BackendSrv;
}
export const backendSrv = new BackendSrv();
export const getBackendSrv = (): BackendSrv => backendSrv;

View File

@ -43,7 +43,7 @@ export class PerformanceBackend implements EchoBackend<PerformanceEvent, Perform
// TODO: Enable backend request when we have metrics API
// if (this.options.url) {
// getBackendSrv().post(this.options.url, result);
// backendSrv.post(this.options.url, result);
// }
};
}

View File

@ -4,7 +4,7 @@ import coreModule from 'app/core/core_module';
import impressionSrv from 'app/core/services/impression_srv';
import store from 'app/core/store';
import { contextSrv } from 'app/core/services/context_srv';
import { BackendSrv } from './backend_srv';
import { backendSrv } from './backend_srv';
import { Section } from '../components/manage_dashboards/manage_dashboards';
import { DashboardSearchHit } from 'app/types/search';
@ -16,8 +16,7 @@ export class SearchSrv {
recentIsOpen: boolean;
starredIsOpen: boolean;
/** @ngInject */
constructor(private backendSrv: BackendSrv) {
constructor() {
this.recentIsOpen = store.getBool('search.sections.recent', true);
this.starredIsOpen = store.getBool('search.sections.starred', true);
}
@ -44,7 +43,7 @@ export class SearchSrv {
return Promise.resolve([]);
}
return this.backendSrv.search({ dashboardIds: dashIds }).then(result => {
return backendSrv.search({ dashboardIds: dashIds }).then(result => {
return dashIds
.map(orderId => {
return _.find(result, { id: orderId });
@ -78,7 +77,7 @@ export class SearchSrv {
return Promise.resolve();
}
return this.backendSrv.search({ starred: true, limit: 30 }).then(result => {
return backendSrv.search({ starred: true, limit: 30 }).then(result => {
if (result.length > 0) {
sections['starred'] = {
title: 'Starred',
@ -116,7 +115,7 @@ export class SearchSrv {
}
promises.push(
this.backendSrv.search(query).then(results => {
backendSrv.search(query).then(results => {
return this.handleSearchResult(sections, results);
})
);
@ -197,14 +196,14 @@ export class SearchSrv {
folderIds: [section.id],
};
return this.backendSrv.search(query).then(results => {
return backendSrv.search(query).then(results => {
section.items = results;
return Promise.resolve(section);
});
}
getDashboardTags() {
return this.backendSrv.get('/api/dashboards/tags');
return backendSrv.get('/api/dashboards/tags');
}
}

View File

@ -1,16 +1,13 @@
import React from 'react';
// @ts-ignore
import { getBackendSrv } from '@grafana/runtime/src/services/backendSrv';
import { OrgSwitcher } from '../components/OrgSwitcher';
import { shallow } from 'enzyme';
import { OrgRole } from '@grafana/data';
const getMock = jest.fn(() => Promise.resolve([]));
const postMock = jest.fn();
const postMock = jest.fn().mockImplementation(jest.fn());
jest.mock('@grafana/runtime/src/services/backendSrv', () => ({
jest.mock('@grafana/runtime', () => ({
getBackendSrv: () => ({
get: getMock,
get: jest.fn().mockResolvedValue([]),
post: postMock,
}),
}));

View File

@ -1,32 +1,532 @@
import angular from 'angular';
import { BackendSrv } from 'app/core/services/backend_srv';
import { ContextSrv } from '../services/context_srv';
jest.mock('app/core/store');
import { BackendSrv, getBackendSrv } from '../services/backend_srv';
import { Emitter } from '../utils/emitter';
import { ContextSrv, User } from '../services/context_srv';
import { Observable, of } from 'rxjs';
import { AppEvents } from '@grafana/data';
import { CoreEvents } from '../../types';
import { delay } from 'rxjs/operators';
describe('backend_srv', () => {
const _httpBackend = (options: any) => {
if (options.url === 'gateway-error') {
return Promise.reject({ status: 502 });
}
return Promise.resolve({});
const getTestContext = (overides?: object) => {
const defaults = {
data: { test: 'hello world' },
ok: true,
status: 200,
statusText: 'Ok',
isSignedIn: true,
orgId: 1337,
};
const props = { ...defaults, ...overides };
const textMock = jest.fn().mockResolvedValue(JSON.stringify(props.data));
const fromFetchMock = jest.fn().mockImplementation(() => {
return of({
ok: props.ok,
status: props.status,
statusText: props.statusText,
text: textMock,
});
});
const appEventsMock: Emitter = ({
emit: jest.fn(),
} as any) as Emitter;
const user: User = ({
isSignedIn: props.isSignedIn,
orgId: props.orgId,
} as any) as User;
const contextSrvMock: ContextSrv = ({
user,
} as any) as ContextSrv;
const logoutMock = jest.fn();
const parseRequestOptionsMock = jest.fn().mockImplementation(options => options);
const parseDataSourceRequestOptionsMock = jest.fn().mockImplementation(options => options);
const parseUrlFromOptionsMock = jest.fn().mockImplementation(() => 'parseUrlFromOptionsMock');
const parseInitFromOptionsMock = jest.fn().mockImplementation(() => 'parseInitFromOptionsMock');
const backendSrv = new BackendSrv({
fromFetch: fromFetchMock,
appEvents: appEventsMock,
contextSrv: contextSrvMock,
logout: logoutMock,
});
backendSrv['parseRequestOptions'] = parseRequestOptionsMock;
backendSrv['parseDataSourceRequestOptions'] = parseDataSourceRequestOptionsMock;
backendSrv['parseUrlFromOptions'] = parseUrlFromOptionsMock;
backendSrv['parseInitFromOptions'] = parseInitFromOptionsMock;
const expectCallChain = (options: any) => {
expect(parseUrlFromOptionsMock).toHaveBeenCalledTimes(1);
expect(parseUrlFromOptionsMock).toHaveBeenCalledWith(options);
expect(parseInitFromOptionsMock).toHaveBeenCalledTimes(1);
expect(parseInitFromOptionsMock).toHaveBeenCalledWith(options);
expect(fromFetchMock).toHaveBeenCalledTimes(1);
expect(fromFetchMock).toHaveBeenCalledWith('parseUrlFromOptionsMock', 'parseInitFromOptionsMock');
};
const _backendSrv = new BackendSrv(
_httpBackend,
{} as angular.IQService,
{} as angular.ITimeoutService,
{} as ContextSrv
);
const expectRequestCallChain = (options: any) => {
expect(parseRequestOptionsMock).toHaveBeenCalledTimes(1);
expect(parseRequestOptionsMock).toHaveBeenCalledWith(options, 1337);
expectCallChain(options);
};
describe('when handling errors', () => {
it('should return the http status code', async () => {
try {
await _backendSrv.datasourceRequest({
url: 'gateway-error',
});
} catch (err) {
expect(err.status).toBe(502);
const expectDataSourceRequestCallChain = (options: any) => {
expect(parseDataSourceRequestOptionsMock).toHaveBeenCalledTimes(1);
expect(parseDataSourceRequestOptionsMock).toHaveBeenCalledWith(options, 1337, undefined);
expectCallChain(options);
};
return {
backendSrv,
fromFetchMock,
appEventsMock,
contextSrvMock,
textMock,
logoutMock,
parseRequestOptionsMock,
parseDataSourceRequestOptionsMock,
parseUrlFromOptionsMock,
parseInitFromOptionsMock,
expectRequestCallChain,
expectDataSourceRequestCallChain,
};
};
describe('backendSrv', () => {
describe('parseRequestOptions', () => {
it.each`
retry | url | orgId | expected
${undefined} | ${'http://localhost:3000/api/dashboard'} | ${undefined} | ${{ retry: 0, url: 'http://localhost:3000/api/dashboard' }}
${1} | ${'http://localhost:3000/api/dashboard'} | ${1} | ${{ retry: 1, url: 'http://localhost:3000/api/dashboard' }}
${undefined} | ${'api/dashboard'} | ${undefined} | ${{ retry: 0, url: 'api/dashboard' }}
${undefined} | ${'/api/dashboard'} | ${undefined} | ${{ retry: 0, url: 'api/dashboard' }}
${undefined} | ${'/api/dashboard/'} | ${undefined} | ${{ retry: 0, url: 'api/dashboard' }}
${1} | ${'/api/dashboard/'} | ${undefined} | ${{ retry: 1, url: 'api/dashboard' }}
${undefined} | ${'/api/dashboard/'} | ${1} | ${{ retry: 0, url: 'api/dashboard', headers: { 'X-Grafana-Org-Id': 1 } }}
${1} | ${'/api/dashboard/'} | ${1} | ${{ retry: 1, url: 'api/dashboard', headers: { 'X-Grafana-Org-Id': 1 } }}
`(
"when called with retry: '$retry', url: '$url' and orgId: '$orgId' then result should be '$expected'",
({ retry, url, orgId, expected }) => {
expect(getBackendSrv()['parseRequestOptions']({ retry, url }, orgId)).toEqual(expected);
}
);
});
describe('parseDataSourceRequestOptions', () => {
it.each`
retry | url | headers | orgId | noBackendCache | expected
${undefined} | ${'http://localhost:3000/api/dashboard'} | ${undefined} | ${undefined} | ${undefined} | ${{ retry: 0, url: 'http://localhost:3000/api/dashboard' }}
${1} | ${'http://localhost:3000/api/dashboard'} | ${{ Authorization: 'Some Auth' }} | ${1} | ${true} | ${{ retry: 1, url: 'http://localhost:3000/api/dashboard', headers: { Authorization: 'Some Auth' } }}
${undefined} | ${'api/dashboard'} | ${undefined} | ${undefined} | ${undefined} | ${{ retry: 0, url: 'api/dashboard' }}
${undefined} | ${'/api/dashboard'} | ${undefined} | ${undefined} | ${undefined} | ${{ retry: 0, url: 'api/dashboard' }}
${undefined} | ${'/api/dashboard/'} | ${undefined} | ${undefined} | ${undefined} | ${{ retry: 0, url: 'api/dashboard/' }}
${undefined} | ${'/api/dashboard/'} | ${{ Authorization: 'Some Auth' }} | ${undefined} | ${undefined} | ${{ retry: 0, url: 'api/dashboard/', headers: { 'X-DS-Authorization': 'Some Auth' } }}
${undefined} | ${'/api/dashboard/'} | ${{ Authorization: 'Some Auth' }} | ${1} | ${undefined} | ${{ retry: 0, url: 'api/dashboard/', headers: { 'X-DS-Authorization': 'Some Auth', 'X-Grafana-Org-Id': 1 } }}
${undefined} | ${'/api/dashboard/'} | ${{ Authorization: 'Some Auth' }} | ${1} | ${true} | ${{ retry: 0, url: 'api/dashboard/', headers: { 'X-DS-Authorization': 'Some Auth', 'X-Grafana-Org-Id': 1, 'X-Grafana-NoCache': 'true' } }}
${1} | ${'/api/dashboard/'} | ${undefined} | ${undefined} | ${undefined} | ${{ retry: 1, url: 'api/dashboard/' }}
${1} | ${'/api/dashboard/'} | ${{ Authorization: 'Some Auth' }} | ${undefined} | ${undefined} | ${{ retry: 1, url: 'api/dashboard/', headers: { 'X-DS-Authorization': 'Some Auth' } }}
${1} | ${'/api/dashboard/'} | ${{ Authorization: 'Some Auth' }} | ${1} | ${undefined} | ${{ retry: 1, url: 'api/dashboard/', headers: { 'X-DS-Authorization': 'Some Auth', 'X-Grafana-Org-Id': 1 } }}
${1} | ${'/api/dashboard/'} | ${{ Authorization: 'Some Auth' }} | ${1} | ${true} | ${{ retry: 1, url: 'api/dashboard/', headers: { 'X-DS-Authorization': 'Some Auth', 'X-Grafana-Org-Id': 1, 'X-Grafana-NoCache': 'true' } }}
`(
"when called with retry: '$retry', url: '$url', headers: '$headers', orgId: '$orgId' and noBackendCache: '$noBackendCache' then result should be '$expected'",
({ retry, url, headers, orgId, noBackendCache, expected }) => {
expect(
getBackendSrv()['parseDataSourceRequestOptions']({ retry, url, headers }, orgId, noBackendCache)
).toEqual(expected);
}
);
});
describe('parseUrlFromOptions', () => {
it.each`
params | url | expected
${undefined} | ${'api/dashboard'} | ${'api/dashboard'}
${{ key: 'value' }} | ${'api/dashboard'} | ${'api/dashboard?key=value'}
${{ key: undefined }} | ${'api/dashboard'} | ${'api/dashboard'}
${{ firstKey: 'first value', secondValue: 'second value' }} | ${'api/dashboard'} | ${'api/dashboard?firstKey=first%20value&secondValue=second%20value'}
${{ firstKey: 'first value', secondValue: undefined }} | ${'api/dashboard'} | ${'api/dashboard?firstKey=first%20value'}
${{ id: [1, 2, 3] }} | ${'api/dashboard'} | ${'api/dashboard?id=1&id=2&id=3'}
${{ id: [] }} | ${'api/dashboard'} | ${'api/dashboard'}
`(
"when called with params: '$params' and url: '$url' then result should be '$expected'",
({ params, url, expected }) => {
expect(getBackendSrv()['parseUrlFromOptions']({ params, url })).toEqual(expected);
}
);
});
describe('parseInitFromOptions', () => {
it.each`
method | headers | data | expected
${undefined} | ${undefined} | ${undefined} | ${{ method: undefined, headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*' }, body: undefined }}
${'GET'} | ${undefined} | ${undefined} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*' }, body: undefined }}
${'GET'} | ${{ Auth: 'Some Auth' }} | ${undefined} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: undefined }}
${'GET'} | ${{ Auth: 'Some Auth' }} | ${{ data: { test: 'Some data' } }} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: '{"data":{"test":"Some data"}}' }}
${'GET'} | ${{ Auth: 'Some Auth' }} | ${'some data'} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: '"some data"' }}
`(
"when called with method: '$method', headers: '$headers' and data: '$data' then result should be '$expected'",
({ method, headers, data, expected }) => {
expect(getBackendSrv()['parseInitFromOptions']({ method, headers, data, url: '' })).toEqual(expected);
}
);
});
describe('request', () => {
describe('when making a successful call and conditions for showSuccessAlert are not favorable', () => {
it('then it should return correct result and not emit anything', async () => {
const { backendSrv, appEventsMock, expectRequestCallChain } = getTestContext({
data: { message: 'A message' },
});
const url = '/api/dashboard/';
const result = await backendSrv.request({ url, method: 'DELETE', showSuccessAlert: false });
expect(result).toEqual({ message: 'A message' });
expect(appEventsMock.emit).not.toHaveBeenCalled();
expectRequestCallChain({ url, method: 'DELETE', showSuccessAlert: false });
});
});
describe('when making a successful call and conditions for showSuccessAlert are favorable', () => {
it('then it should emit correct message', async () => {
const { backendSrv, appEventsMock, expectRequestCallChain } = getTestContext({
data: { message: 'A message' },
});
const url = '/api/dashboard/';
const result = await backendSrv.request({ url, method: 'DELETE', showSuccessAlert: true });
expect(result).toEqual({ message: 'A message' });
expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
expect(appEventsMock.emit).toHaveBeenCalledWith(AppEvents.alertSuccess, ['A message']);
expectRequestCallChain({ url, method: 'DELETE', showSuccessAlert: true });
});
});
describe('when making an unsuccessful call and conditions for retry are favorable and loginPing does not throw', () => {
it('then it should retry', async () => {
jest.useFakeTimers();
const { backendSrv, appEventsMock, logoutMock, expectRequestCallChain } = getTestContext({
ok: false,
status: 401,
statusText: 'UnAuthorized',
data: { message: 'UnAuthorized' },
});
backendSrv.loginPing = jest
.fn()
.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', data: { message: 'Ok' } });
const url = '/api/dashboard/';
// it would be better if we could simulate that after the call to loginPing everything is successful but as
// our fromFetchMock returns ok:false the second time this retries it will still be ok:false going into the
// mergeMap in toFailureStream
await backendSrv.request({ url, method: 'GET', retry: 0 }).catch(error => {
expect(error.status).toBe(401);
expect(error.statusText).toBe('UnAuthorized');
expect(error.data).toEqual({ message: 'UnAuthorized' });
expect(appEventsMock.emit).not.toHaveBeenCalled();
expect(backendSrv.loginPing).toHaveBeenCalledTimes(1);
expect(logoutMock).toHaveBeenCalledTimes(1);
expectRequestCallChain({ url, method: 'GET', retry: 0 });
jest.advanceTimersByTime(50);
expect(appEventsMock.emit).not.toHaveBeenCalled();
});
});
});
describe('when making an unsuccessful call and conditions for retry are favorable and retry throws', () => {
it('then it throw error', async () => {
jest.useFakeTimers();
const { backendSrv, appEventsMock, logoutMock, expectRequestCallChain } = getTestContext({
ok: false,
status: 401,
statusText: 'UnAuthorized',
data: { message: 'UnAuthorized' },
});
backendSrv.loginPing = jest
.fn()
.mockRejectedValue({ status: 403, statusText: 'Forbidden', data: { message: 'Forbidden' } });
const url = '/api/dashboard/';
await backendSrv
.request({ url, method: 'GET', retry: 0 })
.catch(error => {
expect(error.status).toBe(403);
expect(error.statusText).toBe('Forbidden');
expect(error.data).toEqual({ message: 'Forbidden' });
expect(appEventsMock.emit).not.toHaveBeenCalled();
expect(backendSrv.loginPing).toHaveBeenCalledTimes(1);
expect(logoutMock).not.toHaveBeenCalled();
expectRequestCallChain({ url, method: 'GET', retry: 0 });
jest.advanceTimersByTime(50);
})
.catch(error => {
expect(error).toEqual({ message: 'Forbidden' });
expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
expect(appEventsMock.emit).toHaveBeenCalledWith(AppEvents.alertWarning, ['Forbidden', '']);
});
});
});
describe('when making an unsuccessful 422 call', () => {
it('then it should emit Validation failed message', async () => {
jest.useFakeTimers();
const { backendSrv, appEventsMock, logoutMock, expectRequestCallChain } = getTestContext({
ok: false,
status: 422,
statusText: 'Unprocessable Entity',
data: { message: 'Unprocessable Entity' },
});
const url = '/api/dashboard/';
await backendSrv
.request({ url, method: 'GET' })
.catch(error => {
expect(error.status).toBe(422);
expect(error.statusText).toBe('Unprocessable Entity');
expect(error.data).toEqual({ message: 'Unprocessable Entity' });
expect(appEventsMock.emit).not.toHaveBeenCalled();
expect(logoutMock).not.toHaveBeenCalled();
expectRequestCallChain({ url, method: 'GET' });
jest.advanceTimersByTime(50);
})
.catch(error => {
expect(error).toEqual({ message: 'Unprocessable Entity' });
expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
expect(appEventsMock.emit).toHaveBeenCalledWith(AppEvents.alertWarning, [
'Validation failed',
'Unprocessable Entity',
]);
});
});
});
describe('when making an unsuccessful call and we handle the error', () => {
it('then it should not emit message', async () => {
jest.useFakeTimers();
const { backendSrv, appEventsMock, logoutMock, expectRequestCallChain } = getTestContext({
ok: false,
status: 404,
statusText: 'Not found',
data: { message: 'Not found' },
});
const url = '/api/dashboard/';
await backendSrv.request({ url, method: 'GET' }).catch(error => {
expect(error.status).toBe(404);
expect(error.statusText).toBe('Not found');
expect(error.data).toEqual({ message: 'Not found' });
expect(appEventsMock.emit).not.toHaveBeenCalled();
expect(logoutMock).not.toHaveBeenCalled();
expectRequestCallChain({ url, method: 'GET' });
error.isHandled = true;
jest.advanceTimersByTime(50);
expect(appEventsMock.emit).not.toHaveBeenCalled();
});
});
});
});
describe('datasourceRequest', () => {
describe('when making a successful call and silent is true', () => {
it('then it should not emit message', async () => {
const { backendSrv, appEventsMock, expectDataSourceRequestCallChain } = getTestContext();
const url = 'http://www.some.url.com/';
const result = await backendSrv.datasourceRequest({ url, silent: true });
expect(result).toEqual({ data: { test: 'hello world' } });
expect(appEventsMock.emit).not.toHaveBeenCalled();
expectDataSourceRequestCallChain({ url, silent: true });
});
});
describe('when making a successful call and silent is not defined', () => {
it('then it should not emit message', async () => {
const { backendSrv, appEventsMock, expectDataSourceRequestCallChain } = getTestContext();
const url = 'http://www.some.url.com/';
const result = await backendSrv.datasourceRequest({ url });
expect(result).toEqual({ data: { test: 'hello world' } });
expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
expect(appEventsMock.emit).toHaveBeenCalledWith(CoreEvents.dsRequestResponse, {
data: { test: 'hello world' },
});
expectDataSourceRequestCallChain({ url });
});
});
describe('when called with the same requestId twice', () => {
it('then it should cancel the first call and the first call should be unsubscribed', async () => {
const { backendSrv, fromFetchMock } = getTestContext();
const unsubscribe = jest.fn();
const slowData = { message: 'Slow Request' };
const slowFetch = new Observable(subscriber => {
subscriber.next({
ok: true,
status: 200,
statusText: 'Ok',
text: () => Promise.resolve(JSON.stringify(slowData)),
});
return unsubscribe;
}).pipe(delay(10000));
const fastData = { message: 'Fast Request' };
const fastFetch = of({
ok: true,
status: 200,
statusText: 'Ok',
text: () => Promise.resolve(JSON.stringify(fastData)),
});
fromFetchMock.mockImplementationOnce(() => slowFetch);
fromFetchMock.mockImplementation(() => fastFetch);
const options = {
url: '/api/dashboard/',
requestId: 'A',
};
const slowRequest = backendSrv.datasourceRequest(options);
const fastResponse = await backendSrv.datasourceRequest(options);
expect(fastResponse).toEqual({ data: { message: 'Fast Request' } });
const slowResponse = await slowRequest;
expect(slowResponse).toEqual(undefined);
expect(unsubscribe).toHaveBeenCalledTimes(1);
});
});
describe('when making an unsuccessful call and conditions for retry are favorable and loginPing does not throw', () => {
it('then it should retry', async () => {
const { backendSrv, appEventsMock, logoutMock, expectDataSourceRequestCallChain } = getTestContext({
ok: false,
status: 401,
statusText: 'UnAuthorized',
data: { message: 'UnAuthorized' },
});
backendSrv.loginPing = jest
.fn()
.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', data: { message: 'Ok' } });
const url = '/api/dashboard/';
// it would be better if we could simulate that after the call to loginPing everything is successful but as
// our fromFetchMock returns ok:false the second time this retries it will still be ok:false going into the
// mergeMap in toFailureStream
await backendSrv.datasourceRequest({ url, method: 'GET', retry: 0 }).catch(error => {
expect(error.status).toBe(401);
expect(error.statusText).toBe('UnAuthorized');
expect(error.data).toEqual({ message: 'UnAuthorized' });
expect(appEventsMock.emit).not.toHaveBeenCalled();
expect(backendSrv.loginPing).toHaveBeenCalledTimes(1);
expect(logoutMock).toHaveBeenCalledTimes(1);
expectDataSourceRequestCallChain({ url, method: 'GET', retry: 0 });
});
});
});
describe('when making an unsuccessful call and conditions for retry are favorable and retry throws', () => {
it('then it throw error', async () => {
const { backendSrv, appEventsMock, logoutMock, expectDataSourceRequestCallChain } = getTestContext({
ok: false,
status: 401,
statusText: 'UnAuthorized',
data: { message: 'UnAuthorized' },
});
backendSrv.loginPing = jest
.fn()
.mockRejectedValue({ status: 403, statusText: 'Forbidden', data: { message: 'Forbidden' } });
const url = '/api/dashboard/';
await backendSrv.datasourceRequest({ url, method: 'GET', retry: 0 }).catch(error => {
expect(error.status).toBe(403);
expect(error.statusText).toBe('Forbidden');
expect(error.data).toEqual({ message: 'Forbidden' });
expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
expect(appEventsMock.emit).toHaveBeenCalledWith(CoreEvents.dsRequestError, {
data: { message: 'Forbidden' },
status: 403,
statusText: 'Forbidden',
});
expect(backendSrv.loginPing).toHaveBeenCalledTimes(1);
expect(logoutMock).not.toHaveBeenCalled();
expectDataSourceRequestCallChain({ url, method: 'GET', retry: 0 });
});
});
});
describe('when making a HTTP_REQUEST_CANCELED call', () => {
it('then it should throw cancelled error', async () => {
const { backendSrv, appEventsMock, logoutMock, expectDataSourceRequestCallChain } = getTestContext({
ok: false,
status: -1,
statusText: 'HTTP_REQUEST_CANCELED',
data: { message: 'HTTP_REQUEST_CANCELED' },
});
const url = '/api/dashboard/';
await backendSrv.datasourceRequest({ url, method: 'GET' }).catch(error => {
expect(error).toEqual({
err: {
status: -1,
statusText: 'HTTP_REQUEST_CANCELED',
data: { message: 'HTTP_REQUEST_CANCELED' },
},
cancelled: true,
});
expect(appEventsMock.emit).not.toHaveBeenCalled();
expect(logoutMock).not.toHaveBeenCalled();
expectDataSourceRequestCallChain({ url, method: 'GET' });
});
});
});
describe('when making an Internal Error call', () => {
it('then it should throw cancelled error', async () => {
const { backendSrv, appEventsMock, logoutMock, expectDataSourceRequestCallChain } = getTestContext({
ok: false,
status: 500,
statusText: 'Internal Server Error',
data: 'Internal Server Error',
});
const url = '/api/dashboard/';
await backendSrv.datasourceRequest({ url, method: 'GET' }).catch(error => {
expect(error).toEqual({
status: 500,
statusText: 'Internal Server Error',
data: {
error: 'Internal Server Error',
response: 'Internal Server Error',
message: 'Internal Server Error',
},
});
expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
expect(appEventsMock.emit).toHaveBeenCalledWith(CoreEvents.dsRequestError, {
status: 500,
statusText: 'Internal Server Error',
data: {
error: 'Internal Server Error',
response: 'Internal Server Error',
message: 'Internal Server Error',
},
});
expect(logoutMock).not.toHaveBeenCalled();
expectDataSourceRequestCallChain({ url, method: 'GET' });
});
});
});
describe('when formatting prometheus error', () => {
it('then it should throw cancelled error', async () => {
const { backendSrv, appEventsMock, logoutMock, expectDataSourceRequestCallChain } = getTestContext({
ok: false,
status: 403,
statusText: 'Forbidden',
data: { error: 'Forbidden' },
});
const url = '/api/dashboard/';
await backendSrv.datasourceRequest({ url, method: 'GET' }).catch(error => {
expect(error).toEqual({
status: 403,
statusText: 'Forbidden',
data: {
error: 'Forbidden',
message: 'Forbidden',
},
});
expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
expect(appEventsMock.emit).toHaveBeenCalledWith(CoreEvents.dsRequestError, {
status: 403,
statusText: 'Forbidden',
data: {
error: 'Forbidden',
message: 'Forbidden',
},
});
expect(logoutMock).not.toHaveBeenCalled();
expectDataSourceRequestCallChain({ url, method: 'GET' });
});
});
});
});
});

View File

@ -6,7 +6,6 @@ import {
FoldersAndDashboardUids,
} from 'app/core/components/manage_dashboards/manage_dashboards';
import { SearchSrv } from 'app/core/services/search_srv';
import { BackendSrv } from '../services/backend_srv';
import { ContextSrv } from '../services/context_srv';
const mockSection = (overides?: object): Section => {
@ -593,7 +592,6 @@ function createCtrlWithStubs(searchResponse: any, tags?: any) {
return new ManageDashboardsCtrl(
{ $digest: jest.fn() } as any,
{} as BackendSrv,
searchSrvStub as SearchSrv,
{ isEditor: true } as ContextSrv
);

View File

@ -1,5 +1,7 @@
import { ILocationService, IScope } from 'angular';
import { SearchResultsCtrl } from '../components/search/search_results';
import { beforeEach, afterEach } from 'test/lib/common';
import { afterEach, beforeEach } from 'test/lib/common';
import appEvents from 'app/core/app_events';
import { CoreEvents } from 'app/types';
@ -11,13 +13,15 @@ jest.mock('app/core/app_events', () => {
describe('SearchResultsCtrl', () => {
let ctrl: any;
const $location = {} as ILocationService;
const $scope = ({ $evalAsync: jest.fn() } as any) as IScope;
describe('when checking an item that is not checked', () => {
const item = { checked: false };
let selectionChanged = false;
beforeEach(() => {
ctrl = new SearchResultsCtrl({});
ctrl = new SearchResultsCtrl($location, $scope);
ctrl.onSelectionChanged = () => (selectionChanged = true);
ctrl.toggleSelection(item);
});
@ -36,7 +40,7 @@ describe('SearchResultsCtrl', () => {
let selectionChanged = false;
beforeEach(() => {
ctrl = new SearchResultsCtrl({});
ctrl = new SearchResultsCtrl($location, $scope);
ctrl.onSelectionChanged = () => (selectionChanged = true);
ctrl.toggleSelection(item);
});
@ -54,7 +58,7 @@ describe('SearchResultsCtrl', () => {
let selectedTag: any = null;
beforeEach(() => {
ctrl = new SearchResultsCtrl({});
ctrl = new SearchResultsCtrl($location, $scope);
ctrl.onTagSelected = (tag: any) => (selectedTag = tag);
ctrl.selectTag('tag-test');
});
@ -68,7 +72,7 @@ describe('SearchResultsCtrl', () => {
let folderExpanded = false;
beforeEach(() => {
ctrl = new SearchResultsCtrl({});
ctrl = new SearchResultsCtrl($location, $scope);
ctrl.onFolderExpanding = () => {
folderExpanded = true;
};
@ -90,7 +94,7 @@ describe('SearchResultsCtrl', () => {
let folderExpanded = false;
beforeEach(() => {
ctrl = new SearchResultsCtrl({});
ctrl = new SearchResultsCtrl($location, $scope);
ctrl.onFolderExpanding = () => {
folderExpanded = true;
};
@ -110,12 +114,12 @@ describe('SearchResultsCtrl', () => {
describe('when clicking on a link in search result', () => {
const dashPath = 'dashboard/path';
const $location = { path: () => dashPath };
const $location = ({ path: () => dashPath } as any) as ILocationService;
const appEventsMock = appEvents as any;
describe('with the same url as current path', () => {
beforeEach(() => {
ctrl = new SearchResultsCtrl($location);
ctrl = new SearchResultsCtrl($location, $scope);
const item = { url: dashPath };
ctrl.onItemClick(item);
});
@ -128,7 +132,7 @@ describe('SearchResultsCtrl', () => {
describe('with a different url than current path', () => {
beforeEach(() => {
ctrl = new SearchResultsCtrl($location);
ctrl = new SearchResultsCtrl($location, $scope);
const item = { url: 'another/path' };
ctrl.onItemClick(item);
});

View File

@ -1,9 +1,8 @@
import { SearchSrv } from 'app/core/services/search_srv';
import { BackendSrvMock } from 'test/mocks/backend_srv';
import impressionSrv from 'app/core/services/impression_srv';
import { contextSrv } from 'app/core/services/context_srv';
import { beforeEach } from 'test/lib/common';
import { BackendSrv } from '../services/backend_srv';
import { backendSrv } from '../services/backend_srv';
jest.mock('app/core/store', () => {
return {
@ -19,29 +18,32 @@ jest.mock('app/core/services/impression_srv', () => {
});
describe('SearchSrv', () => {
let searchSrv: SearchSrv, backendSrvMock: BackendSrvMock;
let searchSrv: SearchSrv;
const searchMock = jest.spyOn(backendSrv, 'search'); // will use the mock in __mocks__
beforeEach(() => {
backendSrvMock = new BackendSrvMock();
searchSrv = new SearchSrv(backendSrvMock as BackendSrv);
searchSrv = new SearchSrv();
contextSrv.isSignedIn = true;
impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([]);
jest.clearAllMocks();
});
describe('With recent dashboards', () => {
let results: any;
beforeEach(() => {
backendSrvMock.search = jest
.fn()
.mockReturnValueOnce(
Promise.resolve([
{ id: 2, title: 'second but first' },
{ id: 1, title: 'first but second' },
])
)
.mockReturnValue(Promise.resolve([]));
searchMock.mockImplementation(
jest
.fn()
.mockReturnValueOnce(
Promise.resolve([
{ id: 2, title: 'second but first' },
{ id: 1, title: 'first but second' },
])
)
.mockReturnValue(Promise.resolve([]))
);
impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([1, 2]);
@ -63,15 +65,17 @@ describe('SearchSrv', () => {
let results: any;
beforeEach(() => {
backendSrvMock.search = jest
.fn()
.mockReturnValueOnce(
Promise.resolve([
{ id: 2, title: 'two' },
{ id: 1, title: 'one' },
])
)
.mockReturnValue(Promise.resolve([]));
searchMock.mockImplementation(
jest
.fn()
.mockReturnValueOnce(
Promise.resolve([
{ id: 2, title: 'two' },
{ id: 1, title: 'one' },
])
)
.mockReturnValue(Promise.resolve([]))
);
impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([4, 5, 1, 2, 3]);
@ -92,7 +96,7 @@ describe('SearchSrv', () => {
let results: any;
beforeEach(() => {
backendSrvMock.search = jest.fn().mockReturnValue(Promise.resolve([{ id: 1, title: 'starred' }]));
searchMock.mockImplementation(jest.fn().mockReturnValue(Promise.resolve([{ id: 1, title: 'starred' }])));
return searchSrv.search({ query: '' }).then(res => {
results = res;
@ -109,15 +113,17 @@ describe('SearchSrv', () => {
let results: any;
beforeEach(() => {
backendSrvMock.search = jest
.fn()
.mockReturnValueOnce(
Promise.resolve([
{ id: 1, title: 'starred and recent', isStarred: true },
{ id: 2, title: 'recent' },
])
)
.mockReturnValue(Promise.resolve([{ id: 1, title: 'starred and recent' }]));
searchMock.mockImplementation(
jest
.fn()
.mockReturnValueOnce(
Promise.resolve([
{ id: 1, title: 'starred and recent', isStarred: true },
{ id: 2, title: 'recent' },
])
)
.mockReturnValue(Promise.resolve([{ id: 1, title: 'starred and recent' }]))
);
impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([1, 2]);
return searchSrv.search({ query: '' }).then(res => {
@ -140,35 +146,37 @@ describe('SearchSrv', () => {
let results: any;
beforeEach(() => {
backendSrvMock.search = jest
.fn()
.mockReturnValueOnce(Promise.resolve([]))
.mockReturnValue(
Promise.resolve([
{
title: 'folder1',
type: 'dash-folder',
id: 1,
},
{
title: 'dash with no folder',
type: 'dash-db',
id: 2,
},
{
title: 'dash in folder1 1',
type: 'dash-db',
id: 3,
folderId: 1,
},
{
title: 'dash in folder1 2',
type: 'dash-db',
id: 4,
folderId: 1,
},
])
);
searchMock.mockImplementation(
jest
.fn()
.mockReturnValueOnce(Promise.resolve([]))
.mockReturnValue(
Promise.resolve([
{
title: 'folder1',
type: 'dash-folder',
id: 1,
},
{
title: 'dash with no folder',
type: 'dash-db',
id: 2,
},
{
title: 'dash in folder1 1',
type: 'dash-db',
id: 3,
folderId: 1,
},
{
title: 'dash in folder1 2',
type: 'dash-db',
id: 4,
folderId: 1,
},
])
)
);
return searchSrv.search({ query: '' }).then(res => {
results = res;
@ -188,23 +196,25 @@ describe('SearchSrv', () => {
let results: any;
beforeEach(() => {
backendSrvMock.search = jest.fn().mockReturnValue(
Promise.resolve([
{
id: 2,
title: 'dash with no folder',
type: 'dash-db',
},
{
id: 3,
title: 'dash in folder1 1',
type: 'dash-db',
folderId: 1,
folderUid: 'uid',
folderTitle: 'folder1',
folderUrl: '/dashboards/f/uid/folder1',
},
])
searchMock.mockImplementation(
jest.fn().mockReturnValue(
Promise.resolve([
{
id: 2,
title: 'dash with no folder',
type: 'dash-db',
},
{
id: 3,
title: 'dash in folder1 1',
type: 'dash-db',
folderId: 1,
folderUid: 'uid',
folderTitle: 'folder1',
folderUrl: '/dashboards/f/uid/folder1',
},
])
)
);
return searchSrv.search({ query: 'search' }).then(res => {
@ -213,7 +223,7 @@ describe('SearchSrv', () => {
});
it('should not specify folder ids', () => {
expect(backendSrvMock.search.mock.calls[0][0].folderIds).toHaveLength(0);
expect(searchMock.mock.calls[0][0].folderIds).toHaveLength(0);
});
it('should group results by folder', () => {
@ -228,27 +238,25 @@ describe('SearchSrv', () => {
describe('with tags', () => {
beforeEach(() => {
backendSrvMock.search = jest.fn();
backendSrvMock.search.mockReturnValue(Promise.resolve([]));
searchMock.mockImplementation(jest.fn().mockReturnValue(Promise.resolve([])));
return searchSrv.search({ tag: ['atag'] }).then(() => {});
});
it('should send tags query to backend search', () => {
expect(backendSrvMock.search.mock.calls[0][0].tag).toHaveLength(1);
expect(searchMock.mock.calls[0][0].tag).toHaveLength(1);
});
});
describe('with starred', () => {
beforeEach(() => {
backendSrvMock.search = jest.fn();
backendSrvMock.search.mockReturnValue(Promise.resolve([]));
searchMock.mockImplementation(jest.fn().mockReturnValue(Promise.resolve([])));
return searchSrv.search({ starred: true }).then(() => {});
});
it('should send starred query to backend search', () => {
expect(backendSrvMock.search.mock.calls[0][0].starred).toEqual(true);
expect(searchMock.mock.calls[0][0].starred).toEqual(true);
});
});
@ -256,8 +264,7 @@ describe('SearchSrv', () => {
let getRecentDashboardsCalled = false;
beforeEach(() => {
backendSrvMock.search = jest.fn();
backendSrvMock.search.mockReturnValue(Promise.resolve([]));
searchMock.mockImplementation(jest.fn().mockReturnValue(Promise.resolve([])));
searchSrv['getRecentDashboards'] = () => {
getRecentDashboardsCalled = true;
@ -276,8 +283,7 @@ describe('SearchSrv', () => {
let getStarredCalled = false;
beforeEach(() => {
backendSrvMock.search = jest.fn();
backendSrvMock.search.mockReturnValue(Promise.resolve([]));
searchMock.mockImplementation(jest.fn().mockReturnValue(Promise.resolve([])));
impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([]);
searchSrv['getStarred'] = () => {

View File

@ -1,28 +1,32 @@
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { NavModelSrv } from 'app/core/core';
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
export default class AdminEditOrgCtrl {
/** @ngInject */
constructor($scope: any, $routeParams: any, backendSrv: BackendSrv, $location: any, navModelSrv: NavModelSrv) {
constructor($scope: any, $routeParams: any, $location: any, navModelSrv: NavModelSrv) {
$scope.init = () => {
$scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0);
if ($routeParams.id) {
$scope.getOrg($routeParams.id);
$scope.getOrgUsers($routeParams.id);
promiseToDigest($scope)(Promise.all([$scope.getOrg($routeParams.id), $scope.getOrgUsers($routeParams.id)]));
}
};
$scope.getOrg = (id: number) => {
backendSrv.get('/api/orgs/' + id).then((org: any) => {
$scope.org = org;
});
return getBackendSrv()
.get('/api/orgs/' + id)
.then((org: any) => {
$scope.org = org;
});
};
$scope.getOrgUsers = (id: number) => {
backendSrv.get('/api/orgs/' + id + '/users').then((orgUsers: any) => {
$scope.orgUsers = orgUsers;
});
return getBackendSrv()
.get('/api/orgs/' + id + '/users')
.then((orgUsers: any) => {
$scope.orgUsers = orgUsers;
});
};
$scope.update = () => {
@ -30,19 +34,25 @@ export default class AdminEditOrgCtrl {
return;
}
backendSrv.put('/api/orgs/' + $scope.org.id, $scope.org).then(() => {
$location.path('/admin/orgs');
});
promiseToDigest($scope)(
getBackendSrv()
.put('/api/orgs/' + $scope.org.id, $scope.org)
.then(() => {
$location.path('/admin/orgs');
})
);
};
$scope.updateOrgUser = (orgUser: any) => {
backendSrv.patch('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId, orgUser);
getBackendSrv().patch('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId, orgUser);
};
$scope.removeOrgUser = (orgUser: any) => {
backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId).then(() => {
$scope.getOrgUsers($scope.org.id);
});
promiseToDigest($scope)(
getBackendSrv()
.delete('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId)
.then(() => $scope.getOrgUsers($scope.org.id))
);
};
$scope.init();

View File

@ -1,19 +1,14 @@
import _ from 'lodash';
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { NavModelSrv } from 'app/core/core';
import { User } from 'app/core/services/context_srv';
import { UserSession, Scope, CoreEvents, AppEventEmitter } from 'app/types';
import { dateTime } from '@grafana/data';
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
export default class AdminEditUserCtrl {
/** @ngInject */
constructor(
$scope: Scope & AppEventEmitter,
$routeParams: any,
backendSrv: BackendSrv,
$location: any,
navModelSrv: NavModelSrv
) {
constructor($scope: Scope & AppEventEmitter, $routeParams: any, $location: any, navModelSrv: NavModelSrv) {
$scope.user = {};
$scope.sessions = [];
$scope.newOrg = { name: '', role: 'Editor' };
@ -22,60 +17,74 @@ export default class AdminEditUserCtrl {
$scope.init = () => {
if ($routeParams.id) {
$scope.getUser($routeParams.id);
$scope.getUserSessions($routeParams.id);
$scope.getUserOrgs($routeParams.id);
promiseToDigest($scope)(
Promise.all([
$scope.getUser($routeParams.id),
$scope.getUserSessions($routeParams.id),
$scope.getUserOrgs($routeParams.id),
])
);
}
};
$scope.getUser = (id: number) => {
backendSrv.get('/api/users/' + id).then((user: User) => {
$scope.user = user;
$scope.user_id = id;
$scope.permissions.isGrafanaAdmin = user.isGrafanaAdmin;
});
return getBackendSrv()
.get('/api/users/' + id)
.then((user: User) => {
$scope.user = user;
$scope.user_id = id;
$scope.permissions.isGrafanaAdmin = user.isGrafanaAdmin;
});
};
$scope.getUserSessions = (id: number) => {
backendSrv.get('/api/admin/users/' + id + '/auth-tokens').then((sessions: UserSession[]) => {
sessions.reverse();
return getBackendSrv()
.get('/api/admin/users/' + id + '/auth-tokens')
.then((sessions: UserSession[]) => {
sessions.reverse();
$scope.sessions = sessions.map((session: UserSession) => {
return {
id: session.id,
isActive: session.isActive,
seenAt: dateTime(session.seenAt).fromNow(),
createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'),
clientIp: session.clientIp,
browser: session.browser,
browserVersion: session.browserVersion,
os: session.os,
osVersion: session.osVersion,
device: session.device,
};
});
});
};
$scope.revokeUserSession = (tokenId: number) => {
backendSrv
.post('/api/admin/users/' + $scope.user_id + '/revoke-auth-token', {
authTokenId: tokenId,
})
.then(() => {
$scope.sessions = $scope.sessions.filter((session: UserSession) => {
if (session.id === tokenId) {
return false;
}
return true;
$scope.sessions = sessions.map((session: UserSession) => {
return {
id: session.id,
isActive: session.isActive,
seenAt: dateTime(session.seenAt).fromNow(),
createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'),
clientIp: session.clientIp,
browser: session.browser,
browserVersion: session.browserVersion,
os: session.os,
osVersion: session.osVersion,
device: session.device,
};
});
});
};
$scope.revokeUserSession = (tokenId: number) => {
promiseToDigest($scope)(
getBackendSrv()
.post('/api/admin/users/' + $scope.user_id + '/revoke-auth-token', {
authTokenId: tokenId,
})
.then(() => {
$scope.sessions = $scope.sessions.filter((session: UserSession) => {
if (session.id === tokenId) {
return false;
}
return true;
});
})
);
};
$scope.revokeAllUserSessions = (tokenId: number) => {
backendSrv.post('/api/admin/users/' + $scope.user_id + '/logout').then(() => {
$scope.sessions = [];
});
promiseToDigest($scope)(
getBackendSrv()
.post('/api/admin/users/' + $scope.user_id + '/logout')
.then(() => {
$scope.sessions = [];
})
);
};
$scope.setPassword = () => {
@ -84,15 +93,19 @@ export default class AdminEditUserCtrl {
}
const payload = { password: $scope.password };
backendSrv.put('/api/admin/users/' + $scope.user_id + '/password', payload).then(() => {
$location.path('/admin/users');
});
promiseToDigest($scope)(
getBackendSrv()
.put('/api/admin/users/' + $scope.user_id + '/password', payload)
.then(() => {
$location.path('/admin/users');
})
);
};
$scope.updatePermissions = () => {
const payload = $scope.permissions;
backendSrv.put('/api/admin/users/' + $scope.user_id + '/permissions', payload);
getBackendSrv().put('/api/admin/users/' + $scope.user_id + '/permissions', payload);
};
$scope.create = () => {
@ -100,15 +113,21 @@ export default class AdminEditUserCtrl {
return;
}
backendSrv.post('/api/admin/users', $scope.user).then(() => {
$location.path('/admin/users');
});
promiseToDigest($scope)(
getBackendSrv()
.post('/api/admin/users', $scope.user)
.then(() => {
$location.path('/admin/users');
})
);
};
$scope.getUserOrgs = (id: number) => {
backendSrv.get('/api/users/' + id + '/orgs').then((orgs: any) => {
$scope.orgs = orgs;
});
return getBackendSrv()
.get('/api/users/' + id + '/orgs')
.then((orgs: any) => {
$scope.orgs = orgs;
});
};
$scope.update = () => {
@ -116,20 +135,27 @@ export default class AdminEditUserCtrl {
return;
}
backendSrv.put('/api/users/' + $scope.user_id, $scope.user).then(() => {
$location.path('/admin/users');
});
promiseToDigest($scope)(
getBackendSrv()
.put('/api/users/' + $scope.user_id, $scope.user)
.then(() => {
$location.path('/admin/users');
})
);
};
$scope.updateOrgUser = (orgUser: { orgId: string }) => {
backendSrv.patch('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id, orgUser).then(() => {});
promiseToDigest($scope)(
getBackendSrv().patch('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id, orgUser)
);
};
$scope.removeOrgUser = (orgUser: { orgId: string }) => {
backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id).then(() => {
$scope.getUser($scope.user_id);
$scope.getUserOrgs($scope.user_id);
});
promiseToDigest($scope)(
getBackendSrv()
.delete('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id)
.then(() => Promise.all([$scope.getUser($scope.user_id), $scope.getUserOrgs($scope.user_id)]))
);
};
$scope.orgsSearchCache = [];
@ -140,10 +166,14 @@ export default class AdminEditUserCtrl {
return;
}
backendSrv.get('/api/orgs', { query: '' }).then((result: any) => {
$scope.orgsSearchCache = result;
callback(_.map(result, 'name'));
});
promiseToDigest($scope)(
getBackendSrv()
.get('/api/orgs', { query: '' })
.then((result: any) => {
$scope.orgsSearchCache = result;
callback(_.map(result, 'name'));
})
);
};
$scope.addOrgUser = () => {
@ -161,10 +191,11 @@ export default class AdminEditUserCtrl {
$scope.newOrg.loginOrEmail = $scope.user.login;
backendSrv.post('/api/orgs/' + orgInfo.id + '/users/', $scope.newOrg).then(() => {
$scope.getUser($scope.user_id);
$scope.getUserOrgs($scope.user_id);
});
promiseToDigest($scope)(
getBackendSrv()
.post('/api/orgs/' + orgInfo.id + '/users/', $scope.newOrg)
.then(() => Promise.all([$scope.getUser($scope.user_id), $scope.getUserOrgs($scope.user_id)]))
);
};
$scope.deleteUser = (user: any) => {
@ -174,9 +205,13 @@ export default class AdminEditUserCtrl {
icon: 'fa-trash',
yesText: 'Delete',
onConfirm: () => {
backendSrv.delete('/api/admin/users/' + user.id).then(() => {
$location.path('/admin/users');
});
promiseToDigest($scope)(
getBackendSrv()
.delete('/api/admin/users/' + user.id)
.then(() => {
$location.path('/admin/users');
})
);
},
});
};
@ -192,9 +227,10 @@ export default class AdminEditUserCtrl {
}
const actionEndpoint = user.isDisabled ? '/enable' : '/disable';
backendSrv.post('/api/admin/users/' + user.id + actionEndpoint).then(() => {
$scope.init();
});
getBackendSrv()
.post('/api/admin/users/' + user.id + actionEndpoint)
.then(() => $scope.init());
};
$scope.init();

View File

@ -1,19 +1,19 @@
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { NavModelSrv } from 'app/core/core';
import { Scope, CoreEvents, AppEventEmitter } from 'app/types';
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
export default class AdminListOrgsCtrl {
/** @ngInject */
constructor($scope: Scope & AppEventEmitter, backendSrv: BackendSrv, navModelSrv: NavModelSrv) {
$scope.init = () => {
constructor($scope: Scope & AppEventEmitter, navModelSrv: NavModelSrv) {
$scope.init = async () => {
$scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0);
$scope.getOrgs();
await $scope.getOrgs();
};
$scope.getOrgs = () => {
backendSrv.get('/api/orgs').then((orgs: any) => {
$scope.orgs = orgs;
});
$scope.getOrgs = async () => {
const orgs = await promiseToDigest($scope)(getBackendSrv().get('/api/orgs'));
$scope.orgs = orgs;
};
$scope.deleteOrg = (org: any) => {
@ -23,10 +23,9 @@ export default class AdminListOrgsCtrl {
text2: 'All dashboards for this organization will be removed!',
icon: 'fa-trash',
yesText: 'Delete',
onConfirm: () => {
backendSrv.delete('/api/orgs/' + org.id).then(() => {
$scope.getOrgs();
});
onConfirm: async () => {
await getBackendSrv().delete('/api/orgs/' + org.id);
await $scope.getOrgs();
},
});
};

View File

@ -1,6 +1,8 @@
import { getTagColorsFromName } from '@grafana/ui';
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { NavModelSrv } from 'app/core/core';
import { Scope } from 'app/types/angular';
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
export default class AdminListUsersCtrl {
users: any;
@ -13,29 +15,31 @@ export default class AdminListUsersCtrl {
navModel: any;
/** @ngInject */
constructor(private backendSrv: BackendSrv, navModelSrv: NavModelSrv) {
constructor(private $scope: Scope, navModelSrv: NavModelSrv) {
this.navModel = navModelSrv.getNav('admin', 'global-users', 0);
this.query = '';
this.getUsers();
}
getUsers() {
this.backendSrv
.get(`/api/users/search?perpage=${this.perPage}&page=${this.page}&query=${this.query}`)
.then((result: any) => {
this.users = result.users;
this.page = result.page;
this.perPage = result.perPage;
this.totalPages = Math.ceil(result.totalCount / result.perPage);
this.showPaging = this.totalPages > 1;
this.pages = [];
promiseToDigest(this.$scope)(
getBackendSrv()
.get(`/api/users/search?perpage=${this.perPage}&page=${this.page}&query=${this.query}`)
.then((result: any) => {
this.users = result.users;
this.page = result.page;
this.perPage = result.perPage;
this.totalPages = Math.ceil(result.totalCount / result.perPage);
this.showPaging = this.totalPages > 1;
this.pages = [];
for (let i = 1; i < this.totalPages + 1; i++) {
this.pages.push({ page: i, current: i === this.page });
}
for (let i = 1; i < this.totalPages + 1; i++) {
this.pages.push({ page: i, current: i === this.page });
}
this.addUsersAuthLabels();
});
this.addUsersAuthLabels();
})
);
}
navigateToPage(page: any) {

View File

@ -9,8 +9,6 @@ import { StoreState } from 'app/types';
import { getNavModel } from 'app/core/selectors/navModel';
import Page from 'app/core/components/Page/Page';
const backendSrv = getBackendSrv();
type Settings = { [key: string]: { [key: string]: string } };
interface Props {
@ -29,7 +27,7 @@ export class AdminSettings extends React.PureComponent<Props, State> {
};
async componentDidMount() {
const settings: Settings = await backendSrv.get('/api/admin/settings');
const settings: Settings = await getBackendSrv().get('/api/admin/settings');
this.setState({
settings,
isLoading: false,

View File

@ -5,7 +5,7 @@ import { QueryPart } from 'app/core/components/query_part/query_part';
import alertDef from './state/alertDef';
import config from 'app/core/config';
import appEvents from 'app/core/app_events';
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { DashboardSrv } from '../dashboard/services/DashboardSrv';
import DatasourceSrv from '../plugins/datasource_srv';
import { DataQuery, DataSourceApi } from '@grafana/data';
@ -13,6 +13,7 @@ import { PanelModel } from 'app/features/dashboard/state';
import { getDefaultCondition } from './getAlertingValidationMessage';
import { CoreEvents } from 'app/types';
import kbn from 'app/core/utils/kbn';
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
export class AlertTabCtrl {
panel: PanelModel;
@ -39,7 +40,6 @@ export class AlertTabCtrl {
/** @ngInject */
constructor(
private $scope: any,
private backendSrv: BackendSrv,
private dashboardSrv: DashboardSrv,
private uiSegmentSrv: any,
private datasourceSrv: DatasourceSrv
@ -78,25 +78,31 @@ export class AlertTabCtrl {
this.alertNotifications = [];
this.alertHistory = [];
return this.backendSrv.get('/api/alert-notifications/lookup').then((res: any) => {
this.notifications = res;
return promiseToDigest(this.$scope)(
getBackendSrv()
.get('/api/alert-notifications/lookup')
.then((res: any) => {
this.notifications = res;
this.initModel();
this.validateModel();
});
this.initModel();
this.validateModel();
})
);
}
getAlertHistory() {
this.backendSrv
.get(`/api/annotations?dashboardId=${this.panelCtrl.dashboard.id}&panelId=${this.panel.id}&limit=50&type=alert`)
.then((res: any) => {
this.alertHistory = _.map(res, ah => {
ah.time = this.dashboardSrv.getCurrent().formatDate(ah.time, 'MMM D, YYYY HH:mm:ss');
ah.stateModel = alertDef.getStateDisplayModel(ah.newState);
ah.info = alertDef.getAlertAnnotationInfo(ah);
return ah;
});
});
promiseToDigest(this.$scope)(
getBackendSrv()
.get(`/api/annotations?dashboardId=${this.panelCtrl.dashboard.id}&panelId=${this.panel.id}&limit=50&type=alert`)
.then((res: any) => {
this.alertHistory = _.map(res, ah => {
ah.time = this.dashboardSrv.getCurrent().formatDate(ah.time, 'MMM D, YYYY HH:mm:ss');
ah.stateModel = alertDef.getStateDisplayModel(ah.newState);
ah.info = alertDef.getAlertAnnotationInfo(ah);
return ah;
});
})
);
}
getNotificationIcon(type: string): string {
@ -459,15 +465,17 @@ export class AlertTabCtrl {
icon: 'fa-trash',
yesText: 'Yes',
onConfirm: () => {
this.backendSrv
.post('/api/annotations/mass-delete', {
dashboardId: this.panelCtrl.dashboard.id,
panelId: this.panel.id,
})
.then(() => {
this.alertHistory = [];
this.panelCtrl.refresh();
});
promiseToDigest(this.$scope)(
getBackendSrv()
.post('/api/annotations/mass-delete', {
dashboardId: this.panelCtrl.dashboard.id,
panelId: this.panel.id,
})
.then(() => {
this.alertHistory = [];
this.panelCtrl.refresh();
})
);
},
});
}

View File

@ -1,7 +1,9 @@
import _ from 'lodash';
import { appEvents, coreModule, NavModelSrv } from 'app/core/core';
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { AppEvents } from '@grafana/data';
import { IScope } from 'angular';
import { promiseToDigest } from '../../core/utils/promiseToDigest';
export class AlertNotificationEditCtrl {
theForm: any;
@ -28,8 +30,8 @@ export class AlertNotificationEditCtrl {
/** @ngInject */
constructor(
private $scope: IScope,
private $routeParams: any,
private backendSrv: BackendSrv,
private $location: any,
private $templateCache: any,
navModelSrv: NavModelSrv
@ -41,33 +43,37 @@ export class AlertNotificationEditCtrl {
return ['1m', '5m', '10m', '15m', '30m', '1h'];
};
this.backendSrv
.get(`/api/alert-notifiers`)
.then((notifiers: any) => {
this.notifiers = notifiers;
promiseToDigest(this.$scope)(
getBackendSrv()
.get(`/api/alert-notifiers`)
.then((notifiers: any) => {
this.notifiers = notifiers;
// add option templates
for (const notifier of this.notifiers) {
this.$templateCache.put(this.getNotifierTemplateId(notifier.type), notifier.optionsTemplate);
}
// add option templates
for (const notifier of this.notifiers) {
this.$templateCache.put(this.getNotifierTemplateId(notifier.type), notifier.optionsTemplate);
}
if (!this.$routeParams.id) {
this.navModel.breadcrumbs.push({ text: 'New channel' });
this.navModel.node = { text: 'New channel' };
return _.defaults(this.model, this.defaults);
}
if (!this.$routeParams.id) {
this.navModel.breadcrumbs.push({ text: 'New channel' });
this.navModel.node = { text: 'New channel' };
return _.defaults(this.model, this.defaults);
}
return this.backendSrv.get(`/api/alert-notifications/${this.$routeParams.id}`).then((result: any) => {
this.navModel.breadcrumbs.push({ text: result.name });
this.navModel.node = { text: result.name };
result.settings = _.defaults(result.settings, this.defaults.settings);
return result;
});
})
.then((model: any) => {
this.model = model;
this.notifierTemplateId = this.getNotifierTemplateId(this.model.type);
});
return getBackendSrv()
.get(`/api/alert-notifications/${this.$routeParams.id}`)
.then((result: any) => {
this.navModel.breadcrumbs.push({ text: result.name });
this.navModel.node = { text: result.name };
result.settings = _.defaults(result.settings, this.defaults.settings);
return result;
});
})
.then((model: any) => {
this.model = model;
this.notifierTemplateId = this.getNotifierTemplateId(this.model.type);
})
);
}
save() {
@ -76,37 +82,45 @@ export class AlertNotificationEditCtrl {
}
if (this.model.id) {
this.backendSrv
.put(`/api/alert-notifications/${this.model.id}`, this.model)
.then((res: any) => {
this.model = res;
appEvents.emit(AppEvents.alertSuccess, ['Notification updated']);
})
.catch((err: any) => {
if (err.data && err.data.error) {
appEvents.emit(AppEvents.alertError, [err.data.error]);
}
});
promiseToDigest(this.$scope)(
getBackendSrv()
.put(`/api/alert-notifications/${this.model.id}`, this.model)
.then((res: any) => {
this.model = res;
appEvents.emit(AppEvents.alertSuccess, ['Notification updated']);
})
.catch((err: any) => {
if (err.data && err.data.error) {
appEvents.emit(AppEvents.alertError, [err.data.error]);
}
})
);
} else {
this.backendSrv
.post(`/api/alert-notifications`, this.model)
.then((res: any) => {
appEvents.emit(AppEvents.alertSuccess, ['Notification created']);
this.$location.path('alerting/notifications');
})
.catch((err: any) => {
if (err.data && err.data.error) {
appEvents.emit(AppEvents.alertError, [err.data.error]);
}
});
promiseToDigest(this.$scope)(
getBackendSrv()
.post(`/api/alert-notifications`, this.model)
.then((res: any) => {
appEvents.emit(AppEvents.alertSuccess, ['Notification created']);
this.$location.path('alerting/notifications');
})
.catch((err: any) => {
if (err.data && err.data.error) {
appEvents.emit(AppEvents.alertError, [err.data.error]);
}
})
);
}
}
deleteNotification() {
this.backendSrv.delete(`/api/alert-notifications/${this.model.id}`).then((res: any) => {
this.model = res;
this.$location.path('alerting/notifications');
});
promiseToDigest(this.$scope)(
getBackendSrv()
.delete(`/api/alert-notifications/${this.model.id}`)
.then((res: any) => {
this.model = res;
this.$location.path('alerting/notifications');
})
);
}
getNotifierTemplateId(type: string) {
@ -130,7 +144,7 @@ export class AlertNotificationEditCtrl {
settings: this.model.settings,
};
this.backendSrv.post(`/api/alert-notifications/test`, payload);
promiseToDigest(this.$scope)(getBackendSrv().post(`/api/alert-notifications/test`, payload));
}
}

View File

@ -1,28 +1,39 @@
import { IScope } from 'angular';
import { getBackendSrv } from '@grafana/runtime';
import { coreModule, NavModelSrv } from 'app/core/core';
import { BackendSrv } from 'app/core/services/backend_srv';
import { promiseToDigest } from '../../core/utils/promiseToDigest';
export class AlertNotificationsListCtrl {
notifications: any;
navModel: any;
/** @ngInject */
constructor(private backendSrv: BackendSrv, navModelSrv: NavModelSrv) {
constructor(private $scope: IScope, navModelSrv: NavModelSrv) {
this.loadNotifications();
this.navModel = navModelSrv.getNav('alerting', 'channels', 0);
}
loadNotifications() {
this.backendSrv.get(`/api/alert-notifications`).then((result: any) => {
this.notifications = result;
});
promiseToDigest(this.$scope)(
getBackendSrv()
.get(`/api/alert-notifications`)
.then((result: any) => {
this.notifications = result;
})
);
}
deleteNotification(id: number) {
this.backendSrv.delete(`/api/alert-notifications/${id}`).then(() => {
this.notifications = this.notifications.filter((notification: any) => {
return notification.id !== id;
});
});
promiseToDigest(this.$scope)(
getBackendSrv()
.delete(`/api/alert-notifications/${id}`)
.then(() => {
this.notifications = this.notifications.filter((notification: any) => {
return notification.id !== id;
});
})
);
}
}

View File

@ -3,11 +3,16 @@ import { TestRuleResult, Props } from './TestRuleResult';
import { DashboardModel } from '../dashboard/state';
import { shallow } from 'enzyme';
jest.mock('@grafana/runtime/src/services/backendSrv', () => ({
getBackendSrv: () => ({
post: jest.fn(),
}),
}));
jest.mock('@grafana/runtime', () => {
const original = jest.requireActual('@grafana/runtime');
return {
...original,
getBackendSrv: () => ({
post: jest.fn(),
}),
};
});
const setup = (propOverrides?: object) => {
const props: Props = {

View File

@ -4,7 +4,7 @@ import { LoadingPlaceholder, JSONFormatter } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
import { DashboardModel } from '../dashboard/state/DashboardModel';
import { getBackendSrv, BackendSrv } from '@grafana/runtime';
import { getBackendSrv } from '@grafana/runtime';
import { AppEvents } from '@grafana/data';
export interface Props {
@ -27,12 +27,6 @@ export class TestRuleResult extends PureComponent<Props, State> {
formattedJson: any;
clipboard: any;
backendSrv: BackendSrv = null;
constructor(props: Props) {
super(props);
this.backendSrv = getBackendSrv();
}
componentDidMount() {
this.testRule();
@ -43,7 +37,7 @@ export class TestRuleResult extends PureComponent<Props, State> {
const payload = { dashboard: dashboard.getSaveModelClone(), panelId };
this.setState({ isLoading: true });
const testRuleResponse = await this.backendSrv.post(`/api/alerts/test`, payload);
const testRuleResponse = await getBackendSrv().post(`/api/alerts/test`, payload);
this.setState({ isLoading: false, testRuleResponse });
}

View File

@ -1,6 +1,6 @@
// Libaries
import angular from 'angular';
import _ from 'lodash';
import flattenDeep from 'lodash/flattenDeep';
import cloneDeep from 'lodash/cloneDeep';
// Components
import './editor_ctrl';
@ -11,25 +11,16 @@ import { dedupAnnotations } from './events_processing';
// Types
import { DashboardModel } from '../dashboard/state/DashboardModel';
import DatasourceSrv from '../plugins/datasource_srv';
import { BackendSrv } from 'app/core/services/backend_srv';
import { TimeSrv } from '../dashboard/services/TimeSrv';
import { DataSourceApi, PanelEvents, AnnotationEvent, AppEvents, PanelModel, TimeRange } from '@grafana/data';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { appEvents } from 'app/core/core';
import { getBackendSrv, getDataSourceSrv } from '@grafana/runtime';
import { getTimeSrv } from '../dashboard/services/TimeSrv';
export class AnnotationsSrv {
globalAnnotationsPromise: any;
alertStatesPromise: any;
datasourcePromises: any;
/** @ngInject */
constructor(
private $rootScope: GrafanaRootScope,
private datasourceSrv: DatasourceSrv,
private backendSrv: BackendSrv,
private timeSrv: TimeSrv
) {}
init(dashboard: DashboardModel) {
// always clearPromiseCaches when loading new dashboard
this.clearPromiseCaches();
@ -47,10 +38,10 @@ export class AnnotationsSrv {
return Promise.all([this.getGlobalAnnotations(options), this.getAlertStates(options)])
.then(results => {
// combine the annotations and flatten results
let annotations: AnnotationEvent[] = _.flattenDeep(results[0]);
let annotations: AnnotationEvent[] = flattenDeep(results[0]);
// filter out annotations that do not belong to requesting panel
annotations = _.filter(annotations, item => {
annotations = annotations.filter(item => {
// if event has panel id and query is of type dashboard then panel and requesting panel id must match
if (item.panelId && item.source.type === 'dashboard') {
return item.panelId === options.panel.id;
@ -61,7 +52,7 @@ export class AnnotationsSrv {
annotations = dedupAnnotations(annotations);
// look for alert state for this panel
const alertState: any = _.find(results[1], { panelId: options.panel.id });
const alertState: any = results[1].find((res: any) => res.panelId === options.panel.id);
return {
annotations: annotations,
@ -73,7 +64,7 @@ export class AnnotationsSrv {
err.message = err.data.message;
}
console.log('AnnotationSrv.query error', err);
this.$rootScope.appEvent(AppEvents.alertError, ['Annotation Query Failed', err.message || err]);
appEvents.emit(AppEvents.alertError, ['Annotation Query Failed', err.message || err]);
return [];
});
}
@ -96,7 +87,7 @@ export class AnnotationsSrv {
return this.alertStatesPromise;
}
this.alertStatesPromise = this.backendSrv.get('/api/alerts/states-for-dashboard', {
this.alertStatesPromise = getBackendSrv().get('/api/alerts/states-for-dashboard', {
dashboardId: options.dashboard.id,
});
return this.alertStatesPromise;
@ -109,7 +100,7 @@ export class AnnotationsSrv {
return this.globalAnnotationsPromise;
}
const range = this.timeSrv.timeRange();
const range = getTimeSrv().timeRange();
const promises = [];
const dsPromises = [];
@ -121,7 +112,7 @@ export class AnnotationsSrv {
if (annotation.snapshotData) {
return this.translateQueryResult(annotation, annotation.snapshotData);
}
const datasourcePromise = this.datasourceSrv.get(annotation.datasource);
const datasourcePromise = getDataSourceSrv().get(annotation.datasource);
dsPromises.push(datasourcePromise);
promises.push(
datasourcePromise
@ -137,7 +128,7 @@ export class AnnotationsSrv {
.then(results => {
// store response in annotation object if this is a snapshot call
if (dashboard.snapshot) {
annotation.snapshotData = angular.copy(results);
annotation.snapshotData = cloneDeep(results);
}
// translate result
return this.translateQueryResult(annotation, results);
@ -151,26 +142,26 @@ export class AnnotationsSrv {
saveAnnotationEvent(annotation: AnnotationEvent) {
this.globalAnnotationsPromise = null;
return this.backendSrv.post('/api/annotations', annotation);
return getBackendSrv().post('/api/annotations', annotation);
}
updateAnnotationEvent(annotation: AnnotationEvent) {
this.globalAnnotationsPromise = null;
return this.backendSrv.put(`/api/annotations/${annotation.id}`, annotation);
return getBackendSrv().put(`/api/annotations/${annotation.id}`, annotation);
}
deleteAnnotationEvent(annotation: AnnotationEvent) {
this.globalAnnotationsPromise = null;
const deleteUrl = `/api/annotations/${annotation.id}`;
return this.backendSrv.delete(deleteUrl);
return getBackendSrv().delete(deleteUrl);
}
translateQueryResult(annotation: any, results: any) {
// if annotation has snapshotData
// make clone and remove it
if (annotation.snapshotData) {
annotation = angular.copy(annotation);
annotation = cloneDeep(annotation);
delete annotation.snapshotData;
}

View File

@ -1,11 +1,7 @@
import { AnnotationsSrv } from '../annotations_srv';
describe('AnnotationsSrv', () => {
const $rootScope: any = {
onAppEvent: jest.fn(),
};
const annotationsSrv = new AnnotationsSrv($rootScope, null, null, null);
const annotationsSrv = new AnnotationsSrv();
describe('When translating the query result', () => {
const annotationSource = {

View File

@ -25,7 +25,7 @@ export function loadApiKeys(includeExpired: boolean): ThunkResult<void> {
export function deleteApiKey(id: number, includeExpired: boolean): ThunkResult<void> {
return async dispatch => {
getBackendSrv()
.delete('/api/auth/keys/' + id)
.then(dispatch(loadApiKeys(includeExpired)));
.delete(`/api/auth/keys/${id}`)
.then(() => dispatch(loadApiKeys(includeExpired)));
};
}

View File

@ -2,11 +2,12 @@ import angular from 'angular';
import _ from 'lodash';
import { iconMap } from './DashLinksEditorCtrl';
import { LinkSrv } from 'app/features/panel/panellinks/link_srv';
import { BackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv';
import { DashboardSrv } from '../../services/DashboardSrv';
import { PanelEvents } from '@grafana/data';
import { CoreEvents } from 'app/types';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
export type DashboardLink = { tags: any; target: string; keepTime: any; includeVars: any };
@ -94,13 +95,7 @@ function dashLink($compile: any, $sanitize: any, linkSrv: LinkSrv) {
export class DashLinksContainerCtrl {
/** @ngInject */
constructor(
$scope: any,
$rootScope: GrafanaRootScope,
backendSrv: BackendSrv,
dashboardSrv: DashboardSrv,
linkSrv: LinkSrv
) {
constructor($scope: any, $rootScope: GrafanaRootScope, dashboardSrv: DashboardSrv, linkSrv: LinkSrv) {
const currentDashId = dashboardSrv.getCurrent().id;
function buildLinks(linkDef: any) {
@ -154,26 +149,28 @@ export class DashLinksContainerCtrl {
}
$scope.searchDashboards = (link: DashboardLink, limit: any) => {
return backendSrv.search({ tag: link.tags, limit: limit }).then(results => {
return _.reduce(
results,
(memo, dash) => {
// do not add current dashboard
if (dash.id !== currentDashId) {
memo.push({
title: dash.title,
url: dash.url,
target: link.target === '_self' ? '' : link.target,
icon: 'fa fa-th-large',
keepTime: link.keepTime,
includeVars: link.includeVars,
});
}
return memo;
},
[]
);
});
return promiseToDigest($scope)(
backendSrv.search({ tag: link.tags, limit: limit }).then(results => {
return _.reduce(
results,
(memo, dash) => {
// do not add current dashboard
if (dash.id !== currentDashId) {
memo.push({
title: dash.title,
url: dash.url,
target: link.target === '_self' ? '' : link.target,
icon: 'fa fa-th-large',
keepTime: link.keepTime,
includeVars: link.includeVars,
});
}
return memo;
},
[]
);
})
);
};
$scope.fillDropdown = (link: { searchHits: any }) => {

View File

@ -1,15 +1,17 @@
import { appEvents, contextSrv, coreModule } from 'app/core/core';
import { DashboardModel } from '../../state/DashboardModel';
import $ from 'jquery';
import _ from 'lodash';
import angular, { ILocationService } from 'angular';
import angular, { ILocationService, IScope } from 'angular';
import { e2e } from '@grafana/e2e';
import { appEvents, contextSrv, coreModule } from 'app/core/core';
import { DashboardModel } from '../../state/DashboardModel';
import config from 'app/core/config';
import { BackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv';
import { DashboardSrv } from '../../services/DashboardSrv';
import { CoreEvents } from 'app/types';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { AppEvents } from '@grafana/data';
import { e2e } from '@grafana/e2e';
import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
export class SettingsCtrl {
dashboard: DashboardModel;
@ -26,11 +28,10 @@ export class SettingsCtrl {
/** @ngInject */
constructor(
private $scope: any,
private $scope: IScope & Record<string, any>,
private $route: any,
private $location: ILocationService,
private $rootScope: GrafanaRootScope,
private backendSrv: BackendSrv,
private dashboardSrv: DashboardSrv
) {
// temp hack for annotations and variables editors
@ -234,10 +235,12 @@ export class SettingsCtrl {
}
deleteDashboardConfirmed() {
this.backendSrv.deleteDashboard(this.dashboard.uid, false).then(() => {
appEvents.emit(AppEvents.alertSuccess, ['Dashboard Deleted', this.dashboard.title + ' has been deleted']);
this.$location.url('/');
});
promiseToDigest(this.$scope)(
backendSrv.deleteDashboard(this.dashboard.uid, false).then(() => {
appEvents.emit(AppEvents.alertSuccess, ['Dashboard Deleted', this.dashboard.title + ' has been deleted']);
this.$location.url('/');
})
);
}
onFolderChange(folder: { id: number; title: string }) {

View File

@ -1,10 +1,13 @@
import _ from 'lodash';
import { IScope } from 'angular';
import { AppEvents } from '@grafana/data';
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import { BackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv';
import { ValidationSrv } from 'app/features/manage-dashboards';
import { ContextSrv } from 'app/core/services/context_srv';
import { AppEvents } from '@grafana/data';
import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
export class FolderPickerCtrl {
initialTitle: string;
@ -28,7 +31,7 @@ export class FolderPickerCtrl {
dashboardId?: number;
/** @ngInject */
constructor(private backendSrv: BackendSrv, private validationSrv: ValidationSrv, private contextSrv: ContextSrv) {
constructor(private validationSrv: ValidationSrv, private contextSrv: ContextSrv, private $scope: IScope) {
this.isEditor = this.contextSrv.isEditor;
if (!this.labelClass) {
@ -45,33 +48,35 @@ export class FolderPickerCtrl {
permission: 'Edit',
};
return this.backendSrv.get('api/search', params).then((result: any) => {
if (
this.isEditor &&
(query === '' ||
query.toLowerCase() === 'g' ||
query.toLowerCase() === 'ge' ||
query.toLowerCase() === 'gen' ||
query.toLowerCase() === 'gene' ||
query.toLowerCase() === 'gener' ||
query.toLowerCase() === 'genera' ||
query.toLowerCase() === 'general')
) {
result.unshift({ title: this.rootName, id: 0 });
}
return promiseToDigest(this.$scope)(
backendSrv.get('api/search', params).then((result: any) => {
if (
this.isEditor &&
(query === '' ||
query.toLowerCase() === 'g' ||
query.toLowerCase() === 'ge' ||
query.toLowerCase() === 'gen' ||
query.toLowerCase() === 'gene' ||
query.toLowerCase() === 'gener' ||
query.toLowerCase() === 'genera' ||
query.toLowerCase() === 'general')
) {
result.unshift({ title: this.rootName, id: 0 });
}
if (this.isEditor && this.enableCreateNew && query === '') {
result.unshift({ title: '-- New Folder --', id: -1 });
}
if (this.isEditor && this.enableCreateNew && query === '') {
result.unshift({ title: '-- New Folder --', id: -1 });
}
if (this.enableReset && query === '' && this.initialTitle !== '') {
result.unshift({ title: this.initialTitle, id: null });
}
if (this.enableReset && query === '' && this.initialTitle !== '') {
result.unshift({ title: this.initialTitle, id: null });
}
return _.map(result, item => {
return { text: item.title, value: item.id };
});
});
return _.map(result, item => {
return { text: item.title, value: item.id };
});
})
);
}
onFolderChange(option: { value: number; text: string }) {
@ -105,13 +110,15 @@ export class FolderPickerCtrl {
evt.preventDefault();
}
return this.backendSrv.createFolder({ title: this.newFolderName }).then((result: { title: string; id: number }) => {
appEvents.emit(AppEvents.alertSuccess, ['Folder Created', 'OK']);
return promiseToDigest(this.$scope)(
backendSrv.createFolder({ title: this.newFolderName }).then((result: { title: string; id: number }) => {
appEvents.emit(AppEvents.alertSuccess, ['Folder Created', 'OK']);
this.closeCreateFolder();
this.folder = { text: result.title, value: result.id };
this.onFolderChange(this.folder);
});
this.closeCreateFolder();
this.folder = { text: result.title, value: result.id };
this.onFolderChange(this.folder);
})
);
}
cancelCreateFolder(evt: any) {

View File

@ -96,7 +96,7 @@ export class SaveDashboardModalCtrl {
this.selectors = e2e.pages.SaveDashboardModal.selectors;
}
save() {
save(): void | Promise<any> {
if (!this.saveForm.$valid) {
return;
}

View File

@ -1,18 +1,19 @@
import angular, { ILocationService } from 'angular';
import angular, { ILocationService, IScope } from 'angular';
import _ from 'lodash';
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { TimeSrv } from '../../services/TimeSrv';
import { DashboardModel } from '../../state/DashboardModel';
import { PanelModel } from '../../state/PanelModel';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
export class ShareSnapshotCtrl {
/** @ngInject */
constructor(
$scope: any,
$scope: IScope & Record<string, any>,
$rootScope: GrafanaRootScope,
$location: ILocationService,
backendSrv: BackendSrv,
$timeout: any,
timeSrv: TimeSrv
) {
@ -38,10 +39,14 @@ export class ShareSnapshotCtrl {
];
$scope.init = () => {
backendSrv.get('/api/snapshot/shared-options').then((options: { [x: string]: any }) => {
$scope.sharingButtonText = options['externalSnapshotName'];
$scope.externalEnabled = options['externalEnabled'];
});
promiseToDigest($scope)(
getBackendSrv()
.get('/api/snapshot/shared-options')
.then((options: { [x: string]: any }) => {
$scope.sharingButtonText = options['externalSnapshotName'];
$scope.externalEnabled = options['externalEnabled'];
})
);
};
$scope.apiUrl = '/api/snapshots';
@ -75,16 +80,20 @@ export class ShareSnapshotCtrl {
external: external,
};
backendSrv.post($scope.apiUrl, cmdData).then(
(results: { deleteUrl: any; url: any }) => {
$scope.loading = false;
$scope.deleteUrl = results.deleteUrl;
$scope.snapshotUrl = results.url;
$scope.step = 2;
},
() => {
$scope.loading = false;
}
promiseToDigest($scope)(
getBackendSrv()
.post($scope.apiUrl, cmdData)
.then(
(results: { deleteUrl: any; url: any }) => {
$scope.loading = false;
$scope.deleteUrl = results.deleteUrl;
$scope.snapshotUrl = results.url;
$scope.step = 2;
},
() => {
$scope.loading = false;
}
)
);
};
@ -152,9 +161,13 @@ export class ShareSnapshotCtrl {
};
$scope.deleteSnapshot = () => {
backendSrv.get($scope.deleteUrl).then(() => {
$scope.step = 3;
});
promiseToDigest($scope)(
getBackendSrv()
.get($scope.deleteUrl)
.then(() => {
$scope.step = 3;
})
);
};
}
}

View File

@ -1,6 +1,8 @@
import _ from 'lodash';
import { IScope } from 'angular';
import { HistoryListCtrl } from './HistoryListCtrl';
import { versions, compare, restore } from './__mocks__/history';
import { compare, restore, versions } from './__mocks__/history';
import { CoreEvents } from 'app/types';
describe('HistoryListCtrl', () => {
@ -12,6 +14,7 @@ describe('HistoryListCtrl', () => {
let historySrv: any;
let $rootScope: any;
const $scope: IScope = ({ $evalAsync: jest.fn() } as any) as IScope;
let historyListCtrl: any;
beforeEach(() => {
historySrv = {
@ -28,7 +31,7 @@ describe('HistoryListCtrl', () => {
beforeEach(() => {
historySrv.getHistoryList = jest.fn(() => Promise.resolve({}));
historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, {});
historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, $scope);
historyListCtrl.dashboard = {
id: 2,
@ -84,7 +87,7 @@ describe('HistoryListCtrl', () => {
beforeEach(async () => {
historySrv.getHistoryList = jest.fn(() => Promise.reject(new Error('HistoryListError')));
historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, {});
historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, $scope);
await historyListCtrl.getLog();
});
@ -127,7 +130,7 @@ describe('HistoryListCtrl', () => {
historySrv.getHistoryList = jest.fn(() => Promise.resolve(versionsResponse));
historySrv.calculateDiff = jest.fn(() => Promise.resolve({}));
historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, {});
historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, $scope);
historyListCtrl.dashboard = {
id: 2,
@ -260,7 +263,7 @@ describe('HistoryListCtrl', () => {
historySrv.getHistoryList = jest.fn(() => Promise.resolve(versionsResponse));
historySrv.restoreDashboard = jest.fn(() => Promise.resolve());
historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, {});
historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, $scope);
historyListCtrl.dashboard = {
id: 1,
@ -279,7 +282,7 @@ describe('HistoryListCtrl', () => {
beforeEach(async () => {
historySrv.getHistoryList = jest.fn(() => Promise.resolve(versionsResponse));
historySrv.restoreDashboard = jest.fn(() => Promise.resolve());
historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, {});
historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, $scope);
historySrv.restoreDashboard = jest.fn(() => Promise.reject(new Error('RestoreError')));
historyListCtrl.restoreConfirm(RESTORE_ID);
await historyListCtrl.getLog();

View File

@ -1,12 +1,13 @@
import _ from 'lodash';
import angular, { ILocationService } from 'angular';
import angular, { ILocationService, IScope } from 'angular';
import locationUtil from 'app/core/utils/location_util';
import { DashboardModel } from '../../state/DashboardModel';
import { HistoryListOpts, RevisionsModel, CalculateDiffOptions, HistorySrv } from './HistorySrv';
import { dateTime, toUtc, DateTimeInput, AppEvents } from '@grafana/data';
import { CalculateDiffOptions, HistoryListOpts, HistorySrv, RevisionsModel } from './HistorySrv';
import { AppEvents, dateTime, DateTimeInput, toUtc } from '@grafana/data';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { CoreEvents } from 'app/types';
import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
export class HistoryListCtrl {
appending: boolean;
@ -30,7 +31,7 @@ export class HistoryListCtrl {
private $rootScope: GrafanaRootScope,
private $location: ILocationService,
private historySrv: HistorySrv,
public $scope: any
public $scope: IScope
) {
this.appending = false;
this.diff = 'basic';
@ -108,18 +109,20 @@ export class HistoryListCtrl {
diffType: diff,
};
return this.historySrv
.calculateDiff(options)
.then((response: any) => {
// @ts-ignore
this.delta[this.diff] = response;
})
.catch(() => {
this.mode = 'list';
})
.finally(() => {
this.loading = false;
});
return promiseToDigest(this.$scope)(
this.historySrv
.calculateDiff(options)
.then((response: any) => {
// @ts-ignore
this.delta[this.diff] = response;
})
.catch(() => {
this.mode = 'list';
})
.finally(() => {
this.loading = false;
})
);
}
getLog(append = false) {
@ -130,25 +133,27 @@ export class HistoryListCtrl {
start: this.start,
};
return this.historySrv
.getHistoryList(this.dashboard, options)
.then((revisions: any) => {
// set formatted dates & default values
for (const rev of revisions) {
rev.createdDateString = this.formatDate(rev.created);
rev.ageString = this.formatBasicDate(rev.created);
rev.checked = false;
}
return promiseToDigest(this.$scope)(
this.historySrv
.getHistoryList(this.dashboard, options)
.then((revisions: any) => {
// set formatted dates & default values
for (const rev of revisions) {
rev.createdDateString = this.formatDate(rev.created);
rev.ageString = this.formatBasicDate(rev.created);
rev.checked = false;
}
this.revisions = append ? this.revisions.concat(revisions) : revisions;
})
.catch((err: any) => {
this.loading = false;
})
.finally(() => {
this.loading = false;
this.appending = false;
});
this.revisions = append ? this.revisions.concat(revisions) : revisions;
})
.catch((err: any) => {
this.loading = false;
})
.finally(() => {
this.loading = false;
this.appending = false;
})
);
}
isLastPage() {
@ -183,17 +188,19 @@ export class HistoryListCtrl {
restoreConfirm(version: number) {
this.loading = true;
return this.historySrv
.restoreDashboard(this.dashboard, version)
.then((response: any) => {
this.$location.url(locationUtil.stripBaseFromUrl(response.url)).replace();
this.$route.reload();
this.$rootScope.appEvent(AppEvents.alertSuccess, ['Dashboard restored', 'Restored from version ' + version]);
})
.catch(() => {
this.mode = 'list';
this.loading = false;
});
return promiseToDigest(this.$scope)(
this.historySrv
.restoreDashboard(this.dashboard, version)
.then((response: any) => {
this.$location.url(locationUtil.stripBaseFromUrl(response.url)).replace();
this.$route.reload();
this.$rootScope.appEvent(AppEvents.alertSuccess, ['Dashboard restored', 'Restored from version ' + version]);
})
.catch(() => {
this.mode = 'list';
this.loading = false;
})
);
}
}

View File

@ -1,27 +1,41 @@
import { versions, restore } from './__mocks__/history';
import { HistorySrv } from './HistorySrv';
import { DashboardModel } from '../../state/DashboardModel';
const getMock = jest.fn().mockResolvedValue({});
const postMock = jest.fn().mockResolvedValue({});
jest.mock('app/core/store');
jest.mock('@grafana/runtime', () => {
const original = jest.requireActual('@grafana/runtime');
return {
...original,
getBackendSrv: () => ({
post: postMock,
get: getMock,
}),
};
});
describe('historySrv', () => {
const versionsResponse = versions();
const restoreResponse = restore;
const backendSrv: any = {
get: jest.fn(() => Promise.resolve({})),
post: jest.fn(() => Promise.resolve({})),
};
let historySrv = new HistorySrv(backendSrv);
let historySrv = new HistorySrv();
const dash = new DashboardModel({ id: 1 });
const emptyDash = new DashboardModel({});
const historyListOpts = { limit: 10, start: 0 };
beforeEach(() => {
jest.clearAllMocks();
});
describe('getHistoryList', () => {
it('should return a versions array for the given dashboard id', () => {
backendSrv.get = jest.fn(() => Promise.resolve(versionsResponse));
historySrv = new HistorySrv(backendSrv);
getMock.mockImplementation(() => Promise.resolve(versionsResponse));
historySrv = new HistorySrv();
return historySrv.getHistoryList(dash, historyListOpts).then((versions: any) => {
expect(versions).toEqual(versionsResponse);
@ -44,15 +58,15 @@ describe('historySrv', () => {
describe('restoreDashboard', () => {
it('should return a success response given valid parameters', () => {
const version = 6;
backendSrv.post = jest.fn(() => Promise.resolve(restoreResponse(version)));
historySrv = new HistorySrv(backendSrv);
postMock.mockImplementation(() => Promise.resolve(restoreResponse(version)));
historySrv = new HistorySrv();
return historySrv.restoreDashboard(dash, version).then((response: any) => {
expect(response).toEqual(restoreResponse(version));
});
});
it('should return an empty object when not given an id', async () => {
historySrv = new HistorySrv(backendSrv);
historySrv = new HistorySrv();
const rsp = await historySrv.restoreDashboard(emptyDash, 6);
expect(rsp).toEqual({});
});

View File

@ -1,7 +1,7 @@
import _ from 'lodash';
import coreModule from 'app/core/core_module';
import { DashboardModel } from '../../state/DashboardModel';
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
export interface HistoryListOpts {
limit: number;
@ -32,23 +32,20 @@ export interface DiffTarget {
}
export class HistorySrv {
/** @ngInject */
constructor(private backendSrv: BackendSrv) {}
getHistoryList(dashboard: DashboardModel, options: HistoryListOpts) {
const id = dashboard && dashboard.id ? dashboard.id : void 0;
return id ? this.backendSrv.get(`api/dashboards/id/${id}/versions`, options) : Promise.resolve([]);
return id ? getBackendSrv().get(`api/dashboards/id/${id}/versions`, options) : Promise.resolve([]);
}
calculateDiff(options: CalculateDiffOptions) {
return this.backendSrv.post('api/dashboards/calculate-diff', options);
return getBackendSrv().post('api/dashboards/calculate-diff', options);
}
restoreDashboard(dashboard: DashboardModel, version: number) {
const id = dashboard && dashboard.id ? dashboard.id : void 0;
const url = `api/dashboards/id/${id}/restore`;
return id && _.isNumber(version) ? this.backendSrv.post(url, { version }) : Promise.resolve({});
return id && _.isNumber(version) ? getBackendSrv().post(url, { version }) : Promise.resolve({});
}
}

View File

@ -11,7 +11,7 @@ import { PanelOptionsGroup, TransformationsEditor, AlphaNotice } from '@grafana/
import { QueryEditorRows } from './QueryEditorRows';
// Services
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv';
import config from 'app/core/config';
// Types
import { PanelModel } from '../state/PanelModel';
@ -48,7 +48,7 @@ interface State {
export class QueriesTab extends PureComponent<Props, State> {
datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources();
backendSrv = getBackendSrv();
backendSrv = backendSrv;
querySubscription: Unsubscribable;
state: State = {

View File

@ -6,7 +6,7 @@ import $ from 'jquery';
import kbn from 'app/core/utils/kbn';
import { dateMath, AppEvents } from '@grafana/data';
import impressionSrv from 'app/core/services/impression_srv';
import { BackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv';
import { DashboardSrv } from './DashboardSrv';
import DatasourceSrv from 'app/features/plugins/datasource_srv';
import { UrlQueryValue } from '@grafana/runtime';
@ -15,7 +15,6 @@ import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
export class DashboardLoaderSrv {
/** @ngInject */
constructor(
private backendSrv: BackendSrv,
private dashboardSrv: DashboardSrv,
private datasourceSrv: DatasourceSrv,
private $http: any,
@ -46,11 +45,11 @@ export class DashboardLoaderSrv {
if (type === 'script') {
promise = this._loadScriptedDashboard(slug);
} else if (type === 'snapshot') {
promise = this.backendSrv.get('/api/snapshots/' + slug).catch(() => {
promise = backendSrv.get('/api/snapshots/' + slug).catch(() => {
return this._dashboardLoadFailed('Snapshot not found', true);
});
} else {
promise = this.backendSrv
promise = backendSrv
.getDashboardByUid(uid)
.then((result: any) => {
if (result.meta.isFolder) {

View File

@ -1,14 +1,15 @@
import { ILocationService } from 'angular';
import { AppEvents, PanelEvents } from '@grafana/data';
import coreModule from 'app/core/core_module';
import { appEvents } from 'app/core/app_events';
import locationUtil from 'app/core/utils/location_util';
import { DashboardModel } from '../state/DashboardModel';
import { removePanel } from '../utils/panel';
import { DashboardMeta, CoreEvents } from 'app/types';
import { CoreEvents, DashboardMeta } from 'app/types';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { BackendSrv } from 'app/core/services/backend_srv';
import { ILocationService } from 'angular';
import { AppEvents } from '@grafana/data';
import { PanelEvents } from '@grafana/data';
import { backendSrv } from 'app/core/services/backend_srv';
import { promiseToDigest } from '../../../core/utils/promiseToDigest';
interface DashboardSaveOptions {
folderId?: number;
@ -21,11 +22,7 @@ export class DashboardSrv {
dashboard: DashboardModel;
/** @ngInject */
constructor(
private backendSrv: BackendSrv,
private $rootScope: GrafanaRootScope,
private $location: ILocationService
) {
constructor(private $rootScope: GrafanaRootScope, private $location: ILocationService) {
appEvents.on(CoreEvents.saveDashboard, this.saveDashboard.bind(this), $rootScope);
appEvents.on(PanelEvents.panelChangeView, this.onPanelChangeView);
appEvents.on(CoreEvents.removePanel, this.onRemovePanel);
@ -167,10 +164,12 @@ export class DashboardSrv {
save(clone: any, options?: DashboardSaveOptions) {
options.folderId = options.folderId >= 0 ? options.folderId : this.dashboard.meta.folderId || clone.folderId;
return this.backendSrv
.saveDashboard(clone, options)
.then((data: any) => this.postSave(data))
.catch(this.handleSaveDashboardError.bind(this, clone, { folderId: options.folderId }));
return promiseToDigest(this.$rootScope)(
backendSrv
.saveDashboard(clone, options)
.then((data: any) => this.postSave(data))
.catch(this.handleSaveDashboardError.bind(this, clone, { folderId: options.folderId }))
);
}
saveDashboard(
@ -228,13 +227,17 @@ export class DashboardSrv {
let promise;
if (isStarred) {
promise = this.backendSrv.delete('/api/user/stars/dashboard/' + dashboardId).then(() => {
return false;
});
promise = promiseToDigest(this.$rootScope)(
backendSrv.delete('/api/user/stars/dashboard/' + dashboardId).then(() => {
return false;
})
);
} else {
promise = this.backendSrv.post('/api/user/stars/dashboard/' + dashboardId).then(() => {
return true;
});
promise = promiseToDigest(this.$rootScope)(
backendSrv.post('/api/user/stars/dashboard/' + dashboardId).then(() => {
return true;
})
);
}
return promise.then((res: boolean) => {

View File

@ -1,6 +1,6 @@
// Services & Utils
import { createErrorNotification } from 'app/core/copy/appNotification';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv';
import { DashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { DashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
@ -38,7 +38,7 @@ export interface InitDashboardArgs {
}
async function redirectToNewUrl(slug: string, dispatch: ThunkDispatch, currentPath: string) {
const res = await getBackendSrv().getDashboardBySlug(slug);
const res = await backendSrv.getDashboardBySlug(slug);
if (res) {
let newUrl = res.meta.url;
@ -62,7 +62,7 @@ async function fetchDashboard(
switch (args.routeInfo) {
case DashboardRouteInfo.Home: {
// load home dash
const dashDTO: DashboardDTO = await getBackendSrv().get('/api/dashboards/home');
const dashDTO: DashboardDTO = await backendSrv.get('/api/dashboards/home');
// if user specified a custom home dashboard redirect to that
if (dashDTO.redirectUri) {

View File

@ -3,7 +3,7 @@ import { Observable, of, timer, merge, from } from 'rxjs';
import { flatten, map as lodashMap, isArray, isString } from 'lodash';
import { map, catchError, takeUntil, mapTo, share, finalize, tap } from 'rxjs/operators';
// Utils & Services
import { getBackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv';
// Types
import {
DataSourceApi,
@ -136,7 +136,7 @@ export function runRequest(datasource: DataSourceApi, request: DataQueryRequest)
function cancelNetworkRequestsOnUnsubscribe(req: DataQueryRequest) {
return () => {
getBackendSrv().resolveCancelerIfExists(req.requestId);
backendSrv.resolveCancelerIfExists(req.requestId);
};
}

View File

@ -11,7 +11,7 @@ import BasicSettings from './BasicSettings';
import ButtonRow from './ButtonRow';
// Services & Utils
import appEvents from 'app/core/app_events';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
// Actions & selectors
import { getDataSource, getDataSourceMeta } from '../state/selectors';
@ -145,7 +145,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
this.setState({ isTesting: true, testingMessage: 'Testing...', testingStatus: 'info' });
getBackendSrv().withNoBackendCache(async () => {
backendSrv.withNoBackendCache(async () => {
try {
const result = await dsApi.testDatasource();

View File

@ -1,10 +1,12 @@
import { ILocationService, IScope } from 'angular';
import { AppEvents } from '@grafana/data';
import appEvents from 'app/core/app_events';
import locationUtil from 'app/core/utils/location_util';
import { BackendSrv } from 'app/core/services/backend_srv';
import { ILocationService } from 'angular';
import { backendSrv } from 'app/core/services/backend_srv';
import { ValidationSrv } from 'app/features/manage-dashboards';
import { NavModelSrv } from 'app/core/nav_model_srv';
import { AppEvents } from '@grafana/data';
import { promiseToDigest } from '../../core/utils/promiseToDigest';
export default class CreateFolderCtrl {
title = '';
@ -15,10 +17,10 @@ export default class CreateFolderCtrl {
/** @ngInject */
constructor(
private backendSrv: BackendSrv,
private $location: ILocationService,
private validationSrv: ValidationSrv,
navModelSrv: NavModelSrv
navModelSrv: NavModelSrv,
private $scope: IScope
) {
this.navModel = navModelSrv.getNav('dashboards', 'manage-dashboards', 0);
}
@ -28,23 +30,27 @@ export default class CreateFolderCtrl {
return;
}
return this.backendSrv.createFolder({ title: this.title }).then((result: any) => {
appEvents.emit(AppEvents.alertSuccess, ['Folder Created', 'OK']);
this.$location.url(locationUtil.stripBaseFromUrl(result.url));
});
promiseToDigest(this.$scope)(
backendSrv.createFolder({ title: this.title }).then((result: any) => {
appEvents.emit(AppEvents.alertSuccess, ['Folder Created', 'OK']);
this.$location.url(locationUtil.stripBaseFromUrl(result.url));
})
);
}
titleChanged() {
this.titleTouched = true;
this.validationSrv
.validateNewFolderName(this.title)
.then(() => {
this.hasValidationError = false;
})
.catch(err => {
this.hasValidationError = true;
this.validationError = err.message;
});
promiseToDigest(this.$scope)(
this.validationSrv
.validateNewFolderName(this.title)
.then(() => {
this.hasValidationError = false;
})
.catch(err => {
this.hasValidationError = true;
this.validationError = err.message;
})
);
}
}

View File

@ -1,7 +1,9 @@
import { ILocationService, IScope } from 'angular';
import { FolderPageLoader } from './services/FolderPageLoader';
import locationUtil from 'app/core/utils/location_util';
import { NavModelSrv } from 'app/core/core';
import { ILocationService } from 'angular';
import { promiseToDigest } from '../../core/utils/promiseToDigest';
export default class FolderDashboardsCtrl {
navModel: any;
@ -10,23 +12,25 @@ export default class FolderDashboardsCtrl {
/** @ngInject */
constructor(
private backendSrv: any,
navModelSrv: NavModelSrv,
private $routeParams: any,
$location: ILocationService
$location: ILocationService,
private $scope: IScope
) {
if (this.$routeParams.uid) {
this.uid = $routeParams.uid;
const loader = new FolderPageLoader(this.backendSrv);
const loader = new FolderPageLoader();
loader.load(this, this.uid, 'manage-folder-dashboards').then((folder: any) => {
const url = locationUtil.stripBaseFromUrl(folder.url);
promiseToDigest(this.$scope)(
loader.load(this, this.uid, 'manage-folder-dashboards').then((folder: any) => {
const url = locationUtil.stripBaseFromUrl(folder.url);
if (url !== $location.path()) {
$location.path(url).replace();
}
});
if (url !== $location.path()) {
$location.path(url).replace();
}
})
);
}
}
}

View File

@ -1,8 +1,6 @@
import { BackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv';
export class FolderPageLoader {
constructor(private backendSrv: BackendSrv) {}
load(ctrl: any, uid: any, activeChildId: any) {
ctrl.navModel = {
main: {
@ -38,7 +36,7 @@ export class FolderPageLoader {
},
};
return this.backendSrv.getFolderByUid(uid).then((folder: any) => {
return backendSrv.getFolderByUid(uid).then((folder: any) => {
ctrl.folderId = folder.id;
const folderTitle = folder.title;
const folderUrl = folder.url;

View File

@ -1,6 +1,5 @@
import { AppEvents } from '@grafana/data';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv';
import { FolderState, ThunkResult } from 'app/types';
import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel } from 'app/types/acl';
@ -11,7 +10,7 @@ import { loadFolder, loadFolderPermissions } from './reducers';
export function getFolderByUid(uid: string): ThunkResult<void> {
return async dispatch => {
const folder = await getBackendSrv().getFolderByUid(uid);
const folder = await backendSrv.getFolderByUid(uid);
dispatch(loadFolder(folder));
dispatch(updateNavIndex(buildNavModel(folder)));
};
@ -19,7 +18,7 @@ export function getFolderByUid(uid: string): ThunkResult<void> {
export function saveFolder(folder: FolderState): ThunkResult<void> {
return async dispatch => {
const res = await getBackendSrv().put(`/api/folders/${folder.uid}`, {
const res = await backendSrv.put(`/api/folders/${folder.uid}`, {
title: folder.title,
version: folder.version,
});
@ -33,14 +32,14 @@ export function saveFolder(folder: FolderState): ThunkResult<void> {
export function deleteFolder(uid: string): ThunkResult<void> {
return async dispatch => {
await getBackendSrv().deleteFolder(uid, true);
await backendSrv.deleteFolder(uid, true);
dispatch(updateLocation({ path: `dashboards` }));
};
}
export function getFolderPermissions(uid: string): ThunkResult<void> {
return async dispatch => {
const permissions = await getBackendSrv().get(`/api/folders/${uid}/permissions`);
const permissions = await backendSrv.get(`/api/folders/${uid}/permissions`);
dispatch(loadFolderPermissions(permissions));
};
}
@ -74,7 +73,7 @@ export function updateFolderPermission(itemToUpdate: DashboardAcl, level: Permis
itemsToUpdate.push(updated);
}
await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate });
await backendSrv.post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate });
await dispatch(getFolderPermissions(folder.uid));
};
}
@ -91,7 +90,7 @@ export function removeFolderPermission(itemToDelete: DashboardAcl): ThunkResult<
itemsToUpdate.push(toUpdateItem(item));
}
await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate });
await backendSrv.post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate });
await dispatch(getFolderPermissions(folder.uid));
};
}
@ -115,7 +114,7 @@ export function addFolderPermission(newItem: NewDashboardAclItem): ThunkResult<v
permission: newItem.permission,
});
await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate });
await backendSrv.post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate });
await dispatch(getFolderPermissions(folder.uid));
};
}

View File

@ -1,11 +1,14 @@
import { DashboardImportCtrl } from './DashboardImportCtrl';
import config from 'app/core/config';
import { backendSrv } from 'app/core/services/backend_srv';
describe('DashboardImportCtrl', () => {
const ctx: any = {};
jest.spyOn(backendSrv, 'getDashboardByUid').mockImplementation(() => Promise.resolve([]));
jest.spyOn(backendSrv, 'search').mockImplementation(() => Promise.resolve([]));
const getMock = jest.spyOn(backendSrv, 'get');
let navModelSrv: any;
let backendSrv: any;
let validationSrv: any;
beforeEach(() => {
@ -13,17 +16,13 @@ describe('DashboardImportCtrl', () => {
getNav: () => {},
};
backendSrv = {
search: jest.fn().mockReturnValue(Promise.resolve([])),
getDashboardByUid: jest.fn().mockReturnValue(Promise.resolve([])),
get: jest.fn(),
};
validationSrv = {
validateNewDashboardName: jest.fn().mockReturnValue(Promise.resolve()),
};
ctx.ctrl = new DashboardImportCtrl(backendSrv, validationSrv, navModelSrv, {} as any, {} as any);
ctx.ctrl = new DashboardImportCtrl(validationSrv, navModelSrv, {} as any, {} as any);
jest.clearAllMocks();
});
describe('when uploading json', () => {
@ -61,16 +60,12 @@ describe('DashboardImportCtrl', () => {
beforeEach(() => {
ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123';
// setup api mock
backendSrv.get = jest.fn(() => {
return Promise.resolve({
json: {},
});
});
getMock.mockImplementation(() => Promise.resolve({ json: {} }));
return ctx.ctrl.checkGnetDashboard();
});
it('should call gnet api with correct dashboard id', () => {
expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/123');
expect(getMock.mock.calls[0][0]).toBe('api/gnet/dashboards/123');
});
});
@ -78,16 +73,12 @@ describe('DashboardImportCtrl', () => {
beforeEach(() => {
ctx.ctrl.gnetUrl = '2342';
// setup api mock
backendSrv.get = jest.fn(() => {
return Promise.resolve({
json: {},
});
});
getMock.mockImplementation(() => Promise.resolve({ json: {} }));
return ctx.ctrl.checkGnetDashboard();
});
it('should call gnet api with correct dashboard id', () => {
expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/2342');
expect(getMock.mock.calls[0][0]).toBe('api/gnet/dashboards/2342');
});
});
});

View File

@ -1,10 +1,10 @@
import _ from 'lodash';
import config from 'app/core/config';
import locationUtil from 'app/core/utils/location_util';
import { BackendSrv } from '@grafana/runtime';
import { ValidationSrv } from './services/ValidationSrv';
import { NavModelSrv } from 'app/core/core';
import { ILocationService } from 'angular';
import { backendSrv } from 'app/core/services/backend_srv';
export class DashboardImportCtrl {
navModel: any;
@ -32,7 +32,6 @@ export class DashboardImportCtrl {
/** @ngInject */
constructor(
private backendSrv: BackendSrv,
private validationSrv: ValidationSrv,
navModelSrv: NavModelSrv,
private $location: ILocationService,
@ -145,7 +144,7 @@ export class DashboardImportCtrl {
return;
}
this.backendSrv
backendSrv
// @ts-ignore
.getDashboardByUid(this.dash.uid)
.then((res: any) => {
@ -185,7 +184,7 @@ export class DashboardImportCtrl {
};
});
return this.backendSrv
return backendSrv
.post('api/dashboards/import', {
dashboard: this.dash,
overwrite: true,
@ -224,7 +223,7 @@ export class DashboardImportCtrl {
this.gnetError = 'Could not find dashboard';
}
return this.backendSrv
return backendSrv
.get('api/gnet/dashboards/' + dashboardId)
.then(res => {
this.gnetInfo = res;

View File

@ -1,9 +1,11 @@
import _ from 'lodash';
import { ILocationService, IScope } from 'angular';
import { getBackendSrv } from '@grafana/runtime';
import { NavModelSrv } from 'app/core/core';
import { ILocationService } from 'angular';
import { BackendSrv } from '@grafana/runtime';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { CoreEvents } from 'app/types';
import { promiseToDigest } from '../../core/utils/promiseToDigest';
export class SnapshotListCtrl {
navModel: any;
@ -12,27 +14,35 @@ export class SnapshotListCtrl {
/** @ngInject */
constructor(
private $rootScope: GrafanaRootScope,
private backendSrv: BackendSrv,
navModelSrv: NavModelSrv,
private $location: ILocationService
private $location: ILocationService,
private $scope: IScope
) {
this.navModel = navModelSrv.getNav('dashboards', 'snapshots', 0);
this.backendSrv.get('/api/dashboard/snapshots').then((result: any) => {
const baseUrl = this.$location.absUrl().replace($location.url(), '');
this.snapshots = result.map((snapshot: any) => ({
...snapshot,
url: snapshot.externalUrl || `${baseUrl}/dashboard/snapshot/${snapshot.key}`,
}));
});
promiseToDigest(this.$scope)(
getBackendSrv()
.get('/api/dashboard/snapshots')
.then((result: any) => {
const baseUrl = this.$location.absUrl().replace($location.url(), '');
this.snapshots = result.map((snapshot: any) => ({
...snapshot,
url: snapshot.externalUrl || `${baseUrl}/dashboard/snapshot/${snapshot.key}`,
}));
})
);
}
removeSnapshotConfirmed(snapshot: any) {
_.remove(this.snapshots, { key: snapshot.key });
this.backendSrv.delete('/api/snapshots/' + snapshot.key).then(
() => {},
() => {
this.snapshots.push(snapshot);
}
promiseToDigest(this.$scope)(
getBackendSrv()
.delete('/api/snapshots/' + snapshot.key)
.then(
() => {},
() => {
this.snapshots.push(snapshot);
}
)
);
}

View File

@ -1,6 +1,6 @@
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import { BackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv';
import { AppEvents } from '@grafana/data';
export class MoveToFolderCtrl {
@ -10,15 +10,12 @@ export class MoveToFolderCtrl {
afterSave: any;
isValidFolderSelection = true;
/** @ngInject */
constructor(private backendSrv: BackendSrv) {}
onFolderChange(folder: any) {
this.folder = folder;
}
save() {
return this.backendSrv.moveDashboards(this.dashboards, this.folder).then((result: any) => {
return backendSrv.moveDashboards(this.dashboards, this.folder).then((result: any) => {
if (result.successCount > 0) {
const header = `Dashboard${result.successCount === 1 ? '' : 's'} Moved`;
const msg = `${result.successCount} dashboard${result.successCount === 1 ? '' : 's'} moved to ${

View File

@ -1,5 +1,5 @@
import coreModule from 'app/core/core_module';
import { BackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv';
const hitTypes = {
FOLDER: 'dash-folder',
@ -9,9 +9,6 @@ const hitTypes = {
export class ValidationSrv {
rootName = 'general';
/** @ngInject */
constructor(private backendSrv: BackendSrv) {}
validateNewDashboardName(folderId: any, name: string) {
return this.validate(folderId, name, 'A dashboard in this folder with the same name already exists');
}
@ -39,8 +36,8 @@ export class ValidationSrv {
}
const promises = [];
promises.push(this.backendSrv.search({ type: hitTypes.FOLDER, folderIds: [folderId], query: name }));
promises.push(this.backendSrv.search({ type: hitTypes.DASHBOARD, folderIds: [folderId], query: name }));
promises.push(backendSrv.search({ type: hitTypes.FOLDER, folderIds: [folderId], query: name }));
promises.push(backendSrv.search({ type: hitTypes.DASHBOARD, folderIds: [folderId], query: name }));
return Promise.all(promises).then(res => {
let hits: any[] = [];

View File

@ -1,20 +1,24 @@
import angular from 'angular';
import config from 'app/core/config';
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { NavModelSrv } from 'app/core/core';
export class NewOrgCtrl {
/** @ngInject */
constructor($scope: any, $http: any, backendSrv: BackendSrv, navModelSrv: NavModelSrv) {
constructor($scope: any, $http: any, navModelSrv: NavModelSrv) {
$scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0);
$scope.newOrg = { name: '' };
$scope.createOrg = () => {
backendSrv.post('/api/orgs/', $scope.newOrg).then((result: any) => {
backendSrv.post('/api/user/using/' + result.orgId).then(() => {
window.location.href = config.appSubUrl + '/org';
getBackendSrv()
.post('/api/orgs/', $scope.newOrg)
.then((result: any) => {
getBackendSrv()
.post('/api/user/using/' + result.orgId)
.then(() => {
window.location.href = config.appSubUrl + '/org';
});
});
});
};
}
}

View File

@ -1,10 +1,11 @@
import angular from 'angular';
import config from 'app/core/config';
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
export class SelectOrgCtrl {
/** @ngInject */
constructor($scope: any, backendSrv: BackendSrv, contextSrv: any) {
constructor($scope: any, contextSrv: any) {
contextSrv.sidemenu = false;
$scope.navModel = {
@ -20,15 +21,21 @@ export class SelectOrgCtrl {
};
$scope.getUserOrgs = () => {
backendSrv.get('/api/user/orgs').then((orgs: any) => {
$scope.orgs = orgs;
});
promiseToDigest($scope)(
getBackendSrv()
.get('/api/user/orgs')
.then((orgs: any) => {
$scope.orgs = orgs;
})
);
};
$scope.setUsingOrg = (org: any) => {
backendSrv.post('/api/user/using/' + org.orgId).then(() => {
window.location.href = config.appSubUrl + '/';
});
getBackendSrv()
.post('/api/user/using/' + org.orgId)
.then(() => {
window.location.href = config.appSubUrl + '/';
});
};
$scope.init();

View File

@ -1,7 +1,8 @@
import coreModule from 'app/core/core_module';
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { NavModelSrv } from 'app/core/core';
import { ILocationService } from 'angular';
import { ILocationService, IScope } from 'angular';
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
export class UserInviteCtrl {
navModel: any;
@ -9,7 +10,7 @@ export class UserInviteCtrl {
inviteForm: any;
/** @ngInject */
constructor(private backendSrv: BackendSrv, navModelSrv: NavModelSrv, private $location: ILocationService) {
constructor(private $scope: IScope, navModelSrv: NavModelSrv, private $location: ILocationService) {
this.navModel = navModelSrv.getNav('cfg', 'users', 0);
this.invite = {
@ -25,9 +26,13 @@ export class UserInviteCtrl {
return;
}
return this.backendSrv.post('/api/org/invites', this.invite).then(() => {
this.$location.path('org/users/');
});
promiseToDigest(this.$scope)(
getBackendSrv()
.post('/api/org/invites', this.invite)
.then(() => {
this.$location.path('org/users/');
})
);
}
}

View File

@ -1,7 +1,7 @@
import angular from 'angular';
import _ from 'lodash';
import './link_srv';
import { BackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv';
function panelLinksEditor() {
return {
@ -17,7 +17,7 @@ function panelLinksEditor() {
export class PanelLinksEditorCtrl {
/** @ngInject */
constructor($scope: any, backendSrv: BackendSrv) {
constructor($scope: any) {
$scope.panel.links = $scope.panel.links || [];
$scope.addLink = () => {

View File

@ -1,10 +1,11 @@
import _ from 'lodash';
import coreModule from '../../core/core_module';
import { ILocationService } from 'angular';
import { BackendSrv } from 'app/core/services/backend_srv';
import { ILocationService, IScope } from 'angular';
import { getBackendSrv } from '@grafana/runtime';
import { NavModelSrv } from 'app/core/nav_model_srv';
import { AppEventEmitter } from 'app/types';
import { AppEvents } from '@grafana/data';
import { promiseToDigest } from '../../core/utils/promiseToDigest';
export interface PlaylistItem {
value: any;
@ -29,8 +30,7 @@ export class PlaylistEditCtrl {
/** @ngInject */
constructor(
private $scope: AppEventEmitter,
private backendSrv: BackendSrv,
private $scope: IScope & AppEventEmitter,
private $location: ILocationService,
$route: any,
navModelSrv: NavModelSrv
@ -41,13 +41,21 @@ export class PlaylistEditCtrl {
if ($route.current.params.id) {
const playlistId = $route.current.params.id;
backendSrv.get('/api/playlists/' + playlistId).then((result: any) => {
this.playlist = result;
});
promiseToDigest(this.$scope)(
getBackendSrv()
.get('/api/playlists/' + playlistId)
.then((result: any) => {
this.playlist = result;
})
);
backendSrv.get('/api/playlists/' + playlistId + '/items').then((result: any) => {
this.playlistItems = result;
});
promiseToDigest(this.$scope)(
getBackendSrv()
.get('/api/playlists/' + playlistId + '/items')
.then((result: any) => {
this.playlistItems = result;
})
);
}
}
@ -99,8 +107,8 @@ export class PlaylistEditCtrl {
playlist.items = playlistItems;
savePromise = playlist.id
? this.backendSrv.put('/api/playlists/' + playlist.id, playlist)
: this.backendSrv.post('/api/playlists', playlist);
? promiseToDigest(this.$scope)(getBackendSrv().put('/api/playlists/' + playlist.id, playlist))
: promiseToDigest(this.$scope)(getBackendSrv().post('/api/playlists', playlist));
savePromise.then(
() => {

View File

@ -1,5 +1,8 @@
import { IScope, ITimeoutService } from 'angular';
import coreModule from '../../core/core_module';
import { BackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv';
import { promiseToDigest } from '../../core/utils/promiseToDigest';
export class PlaylistSearchCtrl {
query: any;
@ -8,7 +11,7 @@ export class PlaylistSearchCtrl {
searchStarted: any;
/** @ngInject */
constructor($timeout: any, private backendSrv: BackendSrv) {
constructor(private $scope: IScope, $timeout: ITimeoutService) {
this.query = { query: '', tag: [], starred: false, limit: 20 };
$timeout(() => {
@ -22,12 +25,14 @@ export class PlaylistSearchCtrl {
this.tagsMode = false;
const prom: any = {};
prom.promise = this.backendSrv.search(this.query).then(result => {
return {
dashboardResult: result,
tagResult: [],
};
});
prom.promise = promiseToDigest(this.$scope)(
backendSrv.search(this.query).then(result => {
return {
dashboardResult: result,
tagResult: [],
};
})
);
this.searchStarted(prom);
}
@ -52,12 +57,14 @@ export class PlaylistSearchCtrl {
getTags() {
const prom: any = {};
prom.promise = this.backendSrv.get('/api/dashboards/tags').then((result: any) => {
return {
dashboardResult: [],
tagResult: result,
} as any;
});
prom.promise = promiseToDigest(this.$scope)(
backendSrv.get('/api/dashboards/tags').then((result: any) => {
return {
dashboardResult: [],
tagResult: result,
} as any;
})
);
this.searchStarted(prom);
}

View File

@ -9,6 +9,7 @@ import locationUtil from 'app/core/utils/location_util';
import kbn from 'app/core/utils/kbn';
import { store } from 'app/store/store';
import { CoreEvents } from 'app/types';
import { getBackendSrv } from '@grafana/runtime';
export const queryParamsToPreserve: { [key: string]: boolean } = {
kiosk: true,
@ -28,7 +29,7 @@ export class PlaylistSrv {
isPlaying: boolean;
/** @ngInject */
constructor(private $location: any, private $timeout: any, private backendSrv: any) {}
constructor(private $location: any, private $timeout: any) {}
next() {
this.$timeout.cancel(this.cancelPromise);
@ -89,13 +90,17 @@ export class PlaylistSrv {
appEvents.emit(CoreEvents.playlistStarted);
return this.backendSrv.get(`/api/playlists/${playlistId}`).then((playlist: any) => {
return this.backendSrv.get(`/api/playlists/${playlistId}/dashboards`).then((dashboards: any) => {
this.dashboards = dashboards;
this.interval = kbn.interval_to_ms(playlist.interval);
this.next();
return getBackendSrv()
.get(`/api/playlists/${playlistId}`)
.then((playlist: any) => {
return getBackendSrv()
.get(`/api/playlists/${playlistId}/dashboards`)
.then((dashboards: any) => {
this.dashboards = dashboards;
this.interval = kbn.interval_to_ms(playlist.interval);
this.next();
});
});
});
}
stop() {

View File

@ -1,37 +1,47 @@
import { IScope } from 'angular';
import _ from 'lodash';
import coreModule from '../../core/core_module';
import { BackendSrv } from '@grafana/runtime';
import { NavModelSrv } from 'app/core/nav_model_srv';
import { CoreEvents } from 'app/types';
import { AppEvents } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import coreModule from '../../core/core_module';
import { NavModelSrv } from 'app/core/nav_model_srv';
import { AppEventEmitter, CoreEvents } from 'app/types';
import { promiseToDigest } from '../../core/utils/promiseToDigest';
export class PlaylistsCtrl {
playlists: any;
navModel: any;
/** @ngInject */
constructor(private $scope: any, private backendSrv: BackendSrv, navModelSrv: NavModelSrv) {
constructor(private $scope: IScope & AppEventEmitter, navModelSrv: NavModelSrv) {
this.navModel = navModelSrv.getNav('dashboards', 'playlists', 0);
backendSrv.get('/api/playlists').then((result: any) => {
this.playlists = result.map((item: any) => {
item.startUrl = `playlists/play/${item.id}`;
return item;
});
});
promiseToDigest($scope)(
getBackendSrv()
.get('/api/playlists')
.then((result: any) => {
this.playlists = result.map((item: any) => {
item.startUrl = `playlists/play/${item.id}`;
return item;
});
})
);
}
removePlaylistConfirmed(playlist: any) {
_.remove(this.playlists, { id: playlist.id });
this.backendSrv.delete('/api/playlists/' + playlist.id).then(
() => {
this.$scope.appEvent(AppEvents.alertSuccess, ['Playlist deleted']);
},
() => {
this.$scope.appEvent(AppEvents.alertError, ['Unable to delete playlist']);
this.playlists.push(playlist);
}
promiseToDigest(this.$scope)(
getBackendSrv()
.delete('/api/playlists/' + playlist.id)
.then(
() => {
this.$scope.appEvent(AppEvents.alertSuccess, ['Playlist deleted']);
},
() => {
this.$scope.appEvent(AppEvents.alertError, ['Unable to delete playlist']);
this.playlists.push(playlist);
}
)
);
}

View File

@ -10,7 +10,7 @@ describe('PlaylistEditCtrl', () => {
},
};
ctx = new PlaylistEditCtrl(null, null, null, { current: { params: {} } }, navModelSrv);
ctx = new PlaylistEditCtrl(null, null, { current: { params: {} } }, navModelSrv);
ctx.dashboardresult = [
{ id: 2, title: 'dashboard: 2' },

View File

@ -3,6 +3,18 @@ import configureMockStore from 'redux-mock-store';
import { PlaylistSrv } from '../playlist_srv';
import { setStore } from 'app/store/store';
const getMock = jest.fn();
jest.mock('@grafana/runtime', () => {
const original = jest.requireActual('@grafana/runtime');
return {
...original,
getBackendSrv: () => ({
get: getMock,
}),
};
});
const mockStore = configureMockStore<any, any>();
setStore(
@ -14,19 +26,6 @@ setStore(
const dashboards = [{ url: 'dash1' }, { url: 'dash2' }];
const createPlaylistSrv = (): [PlaylistSrv, { url: jest.MockInstance<any, any> }] => {
const mockBackendSrv = {
get: jest.fn(url => {
switch (url) {
case '/api/playlists/1':
return Promise.resolve({ interval: '1s' });
case '/api/playlists/1/dashboards':
return Promise.resolve(dashboards);
default:
throw new Error(`Unexpected url=${url}`);
}
}),
};
const mockLocation = {
url: jest.fn(),
search: () => ({}),
@ -36,7 +35,7 @@ const createPlaylistSrv = (): [PlaylistSrv, { url: jest.MockInstance<any, any> }
const mockTimeout = jest.fn();
(mockTimeout as any).cancel = jest.fn();
return [new PlaylistSrv(mockLocation, mockTimeout, mockBackendSrv), mockLocation];
return [new PlaylistSrv(mockLocation, mockTimeout), mockLocation];
};
const mockWindowLocation = (): [jest.MockInstance<any, any>, () => void] => {
@ -67,6 +66,20 @@ describe('PlaylistSrv', () => {
const initialUrl = 'http://localhost/playlist';
beforeEach(() => {
jest.clearAllMocks();
getMock.mockImplementation(
jest.fn(url => {
switch (url) {
case '/api/playlists/1':
return Promise.resolve({ interval: '1s' });
case '/api/playlists/1/dashboards':
return Promise.resolve(dashboards);
default:
throw new Error(`Unexpected url=${url}`);
}
})
);
[srv] = createPlaylistSrv();
[hrefMock, unmockLocation] = mockWindowLocation();

View File

@ -3,7 +3,7 @@ import React, { PureComponent } from 'react';
import extend from 'lodash/extend';
import { PluginDashboard } from 'app/types';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { appEvents } from 'app/core/core';
import DashboardsTable from 'app/features/datasources/DashboardsTable';
import { AppEvents, PluginMeta, DataSourceApi } from '@grafana/data';

View File

@ -1,4 +1,4 @@
import { getBackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { PluginMeta } from '@grafana/data';
type PluginCache = {

View File

@ -1,5 +1,5 @@
// Libraries
import _ from 'lodash';
import sortBy from 'lodash/sortBy';
import coreModule from 'app/core/core_module';
// Services & Utils
@ -17,7 +17,7 @@ import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
export class DatasourceSrv implements DataSourceService {
datasources: Record<string, DataSourceApi>;
datasources: Record<string, DataSourceApi> = {};
/** @ngInject */
constructor(
@ -38,7 +38,7 @@ export class DatasourceSrv implements DataSourceService {
}
// Interpolation here is to support template variable in data source selection
name = this.templateSrv.replace(name, scopedVars, (value: any[], variable: any) => {
name = this.templateSrv.replace(name, scopedVars, (value: any[]) => {
if (Array.isArray(value)) {
return value[0];
}
@ -56,7 +56,7 @@ export class DatasourceSrv implements DataSourceService {
return this.loadDatasource(name);
}
loadDatasource(name: string): Promise<DataSourceApi<any, any>> {
async loadDatasource(name: string): Promise<DataSourceApi<any, any>> {
// Expression Datasource (not a real datasource)
if (name === expressionDatasource.name) {
this.datasources[name] = expressionDatasource as any;
@ -68,32 +68,31 @@ export class DatasourceSrv implements DataSourceService {
return Promise.reject({ message: `Datasource named ${name} was not found` });
}
return importDataSourcePlugin(dsConfig.meta)
.then(dsPlugin => {
// check if its in cache now
if (this.datasources[name]) {
return this.datasources[name];
}
try {
const dsPlugin = await importDataSourcePlugin(dsConfig.meta);
// check if its in cache now
if (this.datasources[name]) {
return this.datasources[name];
}
// If there is only one constructor argument it is instanceSettings
const useAngular = dsPlugin.DataSourceClass.length !== 1;
const instance: DataSourceApi = useAngular
? this.$injector.instantiate(dsPlugin.DataSourceClass, {
instanceSettings: dsConfig,
})
: new dsPlugin.DataSourceClass(dsConfig);
// If there is only one constructor argument it is instanceSettings
const useAngular = dsPlugin.DataSourceClass.length !== 1;
const instance: DataSourceApi = useAngular
? this.$injector.instantiate(dsPlugin.DataSourceClass, {
instanceSettings: dsConfig,
})
: new dsPlugin.DataSourceClass(dsConfig);
instance.components = dsPlugin.components;
instance.meta = dsConfig.meta;
instance.components = dsPlugin.components;
instance.meta = dsConfig.meta;
// store in instance cache
this.datasources[name] = instance;
return instance;
})
.catch(err => {
this.$rootScope.appEvent(AppEvents.alertError, [dsConfig.name + ' plugin failed', err.toString()]);
return undefined;
});
// store in instance cache
this.datasources[name] = instance;
return instance;
} catch (err) {
this.$rootScope.appEvent(AppEvents.alertError, [dsConfig.name + ' plugin failed', err.toString()]);
return Promise.reject({ message: `Datasource named ${name} was not found` });
}
}
getAll() {
@ -103,7 +102,7 @@ export class DatasourceSrv implements DataSourceService {
getExternal() {
const datasources = this.getAll().filter(ds => !ds.meta.builtIn);
return _.sortBy(datasources, ['name']);
return sortBy(datasources, ['name']);
}
getAnnotationSources() {
@ -111,8 +110,8 @@ export class DatasourceSrv implements DataSourceService {
this.addDataSourceVariables(sources);
_.each(config.datasources, value => {
if (value.meta && value.meta.annotations) {
Object.values(config.datasources).forEach(value => {
if (value.meta?.annotations) {
sources.push(value);
}
});
@ -123,8 +122,8 @@ export class DatasourceSrv implements DataSourceService {
getMetricSources(options?: { skipVariables?: boolean }) {
const metricSources: DataSourceSelectItem[] = [];
_.each(config.datasources, (value, key) => {
if (value.meta && value.meta.metrics) {
Object.entries(config.datasources).forEach(([key, value]) => {
if (value.meta?.metrics) {
let metricSource = { value: key, name: key, meta: value.meta, sort: key };
//Make sure grafana and mixed are sorted at the bottom
@ -164,29 +163,22 @@ export class DatasourceSrv implements DataSourceService {
addDataSourceVariables(list: any[]) {
// look for data source variables
for (let i = 0; i < this.templateSrv.variables.length; i++) {
const variable = this.templateSrv.variables[i];
if (variable.type !== 'datasource') {
continue;
}
this.templateSrv.variables
.filter(variable => variable.type === 'datasource')
.forEach(variable => {
const first = variable.current.value === 'default' ? config.defaultDatasource : variable.current.value;
const ds = config.datasources[first];
let first = variable.current.value;
if (first === 'default') {
first = config.defaultDatasource;
}
const ds = config.datasources[first];
if (ds) {
const key = `$${variable.name}`;
list.push({
name: key,
value: key,
meta: ds.meta,
sort: key,
});
}
}
if (ds) {
const key = `$${variable.name}`;
list.push({
name: key,
value: key,
meta: ds.meta,
sort: key,
});
}
});
}
}

View File

@ -5,9 +5,8 @@ import extend from 'lodash/extend';
import { Button } from '@grafana/ui';
import { PluginMeta, AppPlugin, deprecationWarning } from '@grafana/data';
import { AngularComponent, getAngularLoader, getBackendSrv } from '@grafana/runtime';
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { css } from 'emotion';
interface Props {

View File

@ -1,62 +1,70 @@
import { coreModule, NavModelSrv } from 'app/core/core';
import { dateTime } from '@grafana/data';
import { UserSession } from 'app/types';
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
import { IScope } from 'angular';
export class ProfileCtrl {
sessions: object[] = [];
navModel: any;
/** @ngInject */
constructor(private backendSrv: BackendSrv, navModelSrv: NavModelSrv) {
constructor(private $scope: IScope, navModelSrv: NavModelSrv) {
this.getUserSessions();
this.navModel = navModelSrv.getNav('profile', 'profile-settings', 0);
}
getUserSessions() {
this.backendSrv.get('/api/user/auth-tokens').then((sessions: UserSession[]) => {
sessions.reverse();
promiseToDigest(this.$scope)(
getBackendSrv()
.get('/api/user/auth-tokens')
.then((sessions: UserSession[]) => {
sessions.reverse();
const found = sessions.findIndex((session: UserSession) => {
return session.isActive;
});
const found = sessions.findIndex((session: UserSession) => {
return session.isActive;
});
if (found) {
const now = sessions[found];
sessions.splice(found, found);
sessions.unshift(now);
}
if (found) {
const now = sessions[found];
sessions.splice(found, found);
sessions.unshift(now);
}
this.sessions = sessions.map((session: UserSession) => {
return {
id: session.id,
isActive: session.isActive,
seenAt: dateTime(session.seenAt).fromNow(),
createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'),
clientIp: session.clientIp,
browser: session.browser,
browserVersion: session.browserVersion,
os: session.os,
osVersion: session.osVersion,
device: session.device,
};
});
});
this.sessions = sessions.map((session: UserSession) => {
return {
id: session.id,
isActive: session.isActive,
seenAt: dateTime(session.seenAt).fromNow(),
createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'),
clientIp: session.clientIp,
browser: session.browser,
browserVersion: session.browserVersion,
os: session.os,
osVersion: session.osVersion,
device: session.device,
};
});
})
);
}
revokeUserSession(tokenId: number) {
this.backendSrv
.post('/api/user/revoke-auth-token', {
authTokenId: tokenId,
})
.then(() => {
this.sessions = this.sessions.filter((session: UserSession) => {
if (session.id === tokenId) {
return false;
}
return true;
});
});
promiseToDigest(this.$scope)(
getBackendSrv()
.post('/api/user/revoke-auth-token', {
authTokenId: tokenId,
})
.then(() => {
this.sessions = this.sessions.filter((session: UserSession) => {
if (session.id === tokenId) {
return false;
}
return true;
});
})
);
}
}

View File

@ -11,9 +11,7 @@ import { ConstantVariable } from './constant_variable';
import { AdhocVariable } from './adhoc_variable';
import { TextBoxVariable } from './TextBoxVariable';
coreModule.factory('templateSrv', () => {
return templateSrv;
});
coreModule.factory('templateSrv', () => templateSrv);
export {
VariableSrv,

View File

@ -31,7 +31,7 @@ const setup = () => {
),
]);
const datasource = new CloudWatchDatasource(instanceSettings, {} as any, templateSrv as any, {} as any);
const datasource = new CloudWatchDatasource(instanceSettings, templateSrv as any, {} as any);
datasource.metricFindQuery = async () => [{ value: 'test', label: 'test' }];
const props: Props = {

View File

@ -15,7 +15,7 @@ import {
DataQueryRequest,
DataSourceInstanceSettings,
} from '@grafana/data';
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { ThrottlingErrorMessage } from './components/ThrottlingErrorMessage';
@ -48,7 +48,6 @@ export default class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery,
/** @ngInject */
constructor(
instanceSettings: DataSourceInstanceSettings<CloudWatchJsonData>,
private backendSrv: BackendSrv,
private templateSrv: TemplateSrv,
private timeSrv: TimeSrv
) {
@ -193,7 +192,7 @@ export default class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery,
)}`;
}
performTimeSeriesQuery(request: any, { from, to }: TimeRange) {
performTimeSeriesQuery(request: any, { from, to }: TimeRange): Promise<any> {
return this.awsRequest('/api/tsdb/query', request)
.then((res: any) => {
if (!res.results) {
@ -530,9 +529,11 @@ export default class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery,
data,
};
return this.backendSrv.datasourceRequest(options).then((result: any) => {
return result.data;
});
return getBackendSrv()
.datasourceRequest(options)
.then((result: any) => {
return result.data;
});
}
getDefaultRegion() {

View File

@ -7,10 +7,17 @@ import { CustomVariable } from 'app/features/templating/all';
import _ from 'lodash';
import { CloudWatchQuery } from '../types';
import { DataSourceInstanceSettings } from '@grafana/data';
import { BackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv,
}));
describe('CloudWatchDatasource', () => {
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
const instanceSettings = {
jsonData: { defaultRegion: 'us-east-1' },
name: 'TestDatasource',
@ -29,14 +36,14 @@ describe('CloudWatchDatasource', () => {
};
},
} as TimeSrv;
const backendSrv = {} as BackendSrv;
const ctx = {
backendSrv,
templateSrv,
} as any;
beforeEach(() => {
ctx.ds = new CloudWatchDatasource(instanceSettings, backendSrv, templateSrv, timeSrv);
ctx.ds = new CloudWatchDatasource(instanceSettings, templateSrv, timeSrv);
jest.clearAllMocks();
});
describe('When performing CloudWatch query', () => {
@ -86,7 +93,7 @@ describe('CloudWatchDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = jest.fn(params => {
datasourceRequestMock.mockImplementation(params => {
requestParams = params.data;
return Promise.resolve({ data: response });
});
@ -174,7 +181,7 @@ describe('CloudWatchDatasource', () => {
describe('a correct cloudwatch url should be built for each time series in the response', () => {
beforeEach(() => {
ctx.backendSrv.datasourceRequest = jest.fn(params => {
datasourceRequestMock.mockImplementation(params => {
requestParams = params.data;
return Promise.resolve({ data: response });
});
@ -291,7 +298,7 @@ describe('CloudWatchDatasource', () => {
dispatch: jest.fn(),
} as any);
ctx.backendSrv.datasourceRequest = jest.fn(() => {
datasourceRequestMock.mockImplementation(() => {
return Promise.reject(backendErrorResponse);
});
});
@ -310,10 +317,10 @@ describe('CloudWatchDatasource', () => {
describe('when regions query is used', () => {
beforeEach(() => {
ctx.backendSrv.datasourceRequest = jest.fn(() => {
datasourceRequestMock.mockImplementation(() => {
return Promise.resolve({});
});
ctx.ds = new CloudWatchDatasource(instanceSettings, backendSrv, templateSrv, timeSrv);
ctx.ds = new CloudWatchDatasource(instanceSettings, templateSrv, timeSrv);
ctx.ds.doMetricQueryRequest = jest.fn(() => []);
});
describe('and region param is left out', () => {
@ -441,7 +448,7 @@ describe('CloudWatchDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = jest.fn(params => {
datasourceRequestMock.mockImplementation(params => {
return Promise.resolve({ data: response });
});
});
@ -511,7 +518,7 @@ describe('CloudWatchDatasource', () => {
),
]);
ctx.backendSrv.datasourceRequest = jest.fn(params => {
datasourceRequestMock.mockImplementation(params => {
requestParams = params.data;
return Promise.resolve({ data: {} });
});
@ -643,7 +650,7 @@ describe('CloudWatchDatasource', () => {
scenario.setup = async (setupCallback: any) => {
beforeEach(async () => {
await setupCallback();
ctx.backendSrv.datasourceRequest = jest.fn(args => {
datasourceRequestMock.mockImplementation(args => {
scenario.request = args.data;
return Promise.resolve({ data: scenario.requestResponse });
});

View File

@ -3,16 +3,23 @@ import { dateMath, Field } from '@grafana/data';
import _ from 'lodash';
import { ElasticDatasource } from './datasource';
import { toUtc, dateTime } from '@grafana/data';
import { BackendSrv } from 'app/core/services/backend_srv';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { DataSourceInstanceSettings } from '@grafana/data';
import { ElasticsearchOptions } from './types';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv,
}));
describe('ElasticDatasource', function(this: any) {
const backendSrv: any = {
datasourceRequest: jest.fn(),
};
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
beforeEach(() => {
jest.clearAllMocks();
});
const $rootScope = {
$on: jest.fn(),
@ -45,17 +52,11 @@ describe('ElasticDatasource', function(this: any) {
const ctx = {
$rootScope,
backendSrv,
} as any;
function createDatasource(instanceSettings: DataSourceInstanceSettings<ElasticsearchOptions>) {
instanceSettings.jsonData = instanceSettings.jsonData || ({} as ElasticsearchOptions);
ctx.ds = new ElasticDatasource(
instanceSettings,
backendSrv as BackendSrv,
templateSrv as TemplateSrv,
timeSrv as TimeSrv
);
ctx.ds = new ElasticDatasource(instanceSettings, templateSrv as TemplateSrv, timeSrv as TimeSrv);
}
describe('When testing datasource with index pattern', () => {
@ -69,7 +70,7 @@ describe('ElasticDatasource', function(this: any) {
it('should translate index pattern to current day', () => {
let requestOptions: any;
ctx.backendSrv.datasourceRequest = jest.fn(options => {
datasourceRequestMock.mockImplementation(options => {
requestOptions = options;
return Promise.resolve({ data: {} });
});
@ -91,7 +92,7 @@ describe('ElasticDatasource', function(this: any) {
jsonData: { interval: 'Daily', esVersion: 2 } as ElasticsearchOptions,
} as DataSourceInstanceSettings<ElasticsearchOptions>);
ctx.backendSrv.datasourceRequest = jest.fn(options => {
datasourceRequestMock.mockImplementation(options => {
requestOptions = options;
return Promise.resolve({
data: {
@ -165,7 +166,7 @@ describe('ElasticDatasource', function(this: any) {
} as ElasticsearchOptions,
} as DataSourceInstanceSettings<ElasticsearchOptions>);
ctx.backendSrv.datasourceRequest = jest.fn(options => {
datasourceRequestMock.mockImplementation(options => {
return Promise.resolve(logsResponse);
});
@ -225,7 +226,7 @@ describe('ElasticDatasource', function(this: any) {
jsonData: { esVersion: 2 } as ElasticsearchOptions,
} as DataSourceInstanceSettings<ElasticsearchOptions>);
ctx.backendSrv.datasourceRequest = jest.fn(options => {
datasourceRequestMock.mockImplementation(options => {
requestOptions = options;
return Promise.resolve({ data: { responses: [] } });
});
@ -266,7 +267,7 @@ describe('ElasticDatasource', function(this: any) {
jsonData: { esVersion: 50 } as ElasticsearchOptions,
} as DataSourceInstanceSettings<ElasticsearchOptions>);
ctx.backendSrv.datasourceRequest = jest.fn(options => {
datasourceRequestMock.mockImplementation(options => {
return Promise.resolve({
data: {
metricbeat: {
@ -362,7 +363,7 @@ describe('ElasticDatasource', function(this: any) {
jsonData: { esVersion: 70 } as ElasticsearchOptions,
} as DataSourceInstanceSettings<ElasticsearchOptions>);
ctx.backendSrv.datasourceRequest = jest.fn(options => {
datasourceRequestMock.mockImplementation(options => {
return Promise.resolve({
data: {
'genuine.es7._mapping.response': {
@ -515,7 +516,7 @@ describe('ElasticDatasource', function(this: any) {
jsonData: { esVersion: 5 } as ElasticsearchOptions,
} as DataSourceInstanceSettings<ElasticsearchOptions>);
ctx.backendSrv.datasourceRequest = jest.fn(options => {
datasourceRequestMock.mockImplementation(options => {
requestOptions = options;
return Promise.resolve({ data: { responses: [] } });
});
@ -558,7 +559,7 @@ describe('ElasticDatasource', function(this: any) {
jsonData: { esVersion: 5 } as ElasticsearchOptions,
} as DataSourceInstanceSettings<ElasticsearchOptions>);
ctx.backendSrv.datasourceRequest = jest.fn(options => {
datasourceRequestMock.mockImplementation(options => {
requestOptions = options;
return Promise.resolve({
data: {

View File

@ -12,7 +12,7 @@ import { IndexPattern } from './index_pattern';
import { ElasticQueryBuilder } from './query_builder';
import { toUtc } from '@grafana/data';
import * as queryDef from './query_def';
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { DataLinkConfig, ElasticsearchOptions, ElasticsearchQuery } from './types';
@ -36,7 +36,6 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
/** @ngInject */
constructor(
instanceSettings: DataSourceInstanceSettings<ElasticsearchOptions>,
private backendSrv: BackendSrv,
private templateSrv: TemplateSrv,
private timeSrv: TimeSrv
) {
@ -86,7 +85,7 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
};
}
return this.backendSrv.datasourceRequest(options);
return getBackendSrv().datasourceRequest(options);
}
private get(url: string) {
@ -123,7 +122,7 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
});
}
annotationQuery(options: any) {
annotationQuery(options: any): Promise<any> {
const annotation = options.annotation;
const timeField = annotation.timeField || '@timestamp';
const timeEndField = annotation.timeEndField || null;
@ -511,20 +510,20 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
metricFindQuery(query: any) {
query = angular.fromJson(query);
if (!query) {
return Promise.resolve([]);
if (query) {
if (query.find === 'fields') {
query.field = this.templateSrv.replace(query.field, {}, 'lucene');
return this.getFields(query);
}
if (query.find === 'terms') {
query.field = this.templateSrv.replace(query.field, {}, 'lucene');
query.query = this.templateSrv.replace(query.query || '*', {}, 'lucene');
return this.getTerms(query);
}
}
if (query.find === 'fields') {
query.field = this.templateSrv.replace(query.field, {}, 'lucene');
return this.getFields(query);
}
if (query.find === 'terms') {
query.field = this.templateSrv.replace(query.field, {}, 'lucene');
query.query = this.templateSrv.replace(query.query || '*', {}, 'lucene');
return this.getTerms(query);
}
return Promise.resolve([]);
}
getTagKeys() {

View File

@ -1,20 +1,28 @@
import Datasource from '../datasource';
import { DataFrame, toUtc } from '@grafana/data';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv,
}));
describe('AppInsightsDatasource', () => {
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
const ctx: any = {
backendSrv: {},
templateSrv: new TemplateSrv(),
};
beforeEach(() => {
jest.clearAllMocks();
ctx.instanceSettings = {
jsonData: { appInsightsAppId: '3ad4400f-ea7d-465d-a8fb-43fb20555d85' },
url: 'http://appinsightsapi',
};
ctx.ds = new Datasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv);
ctx.ds = new Datasource(ctx.instanceSettings, ctx.templateSrv);
});
describe('When performing testDatasource', () => {
@ -38,9 +46,9 @@ describe('AppInsightsDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = () => {
datasourceRequestMock.mockImplementation(() => {
return Promise.resolve({ data: response, status: 200 });
};
});
});
it('should return success status', () => {
@ -63,9 +71,9 @@ describe('AppInsightsDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = () => {
datasourceRequestMock.mockImplementation(() => {
return Promise.reject(error);
};
});
});
it('should return error status and a detailed error message', () => {
@ -91,9 +99,9 @@ describe('AppInsightsDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = () => {
datasourceRequestMock.mockImplementation(() => {
return Promise.reject(error);
};
});
});
it('should return error status and a detailed error message', () => {
@ -151,7 +159,7 @@ describe('AppInsightsDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: any) => {
datasourceRequestMock.mockImplementation((options: any) => {
expect(options.url).toContain('/api/tsdb/query');
expect(options.data.queries.length).toBe(1);
expect(options.data.queries[0].refId).toBe('A');
@ -160,7 +168,7 @@ describe('AppInsightsDatasource', () => {
expect(options.data.queries[0].appInsights.valueColumn).toEqual('max');
expect(options.data.queries[0].appInsights.segmentColumn).toBeUndefined();
return Promise.resolve({ data: response, status: 200 });
};
});
});
it('should return a list of datapoints', () => {
@ -194,7 +202,7 @@ describe('AppInsightsDatasource', () => {
beforeEach(() => {
options.targets[0].appInsights.segmentColumn = 'partition';
ctx.backendSrv.datasourceRequest = (options: any) => {
datasourceRequestMock.mockImplementation((options: any) => {
expect(options.url).toContain('/api/tsdb/query');
expect(options.data.queries.length).toBe(1);
expect(options.data.queries[0].refId).toBe('A');
@ -203,7 +211,7 @@ describe('AppInsightsDatasource', () => {
expect(options.data.queries[0].appInsights.valueColumn).toEqual('max');
expect(options.data.queries[0].appInsights.segmentColumn).toEqual('partition');
return Promise.resolve({ data: response, status: 200 });
};
});
});
it('should return a list of datapoints', () => {
@ -257,14 +265,14 @@ describe('AppInsightsDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: any) => {
datasourceRequestMock.mockImplementation((options: any) => {
expect(options.url).toContain('/api/tsdb/query');
expect(options.data.queries.length).toBe(1);
expect(options.data.queries[0].refId).toBe('A');
expect(options.data.queries[0].appInsights.rawQueryString).toBeUndefined();
expect(options.data.queries[0].appInsights.metricName).toBe('exceptions/server');
return Promise.resolve({ data: response, status: 200 });
};
});
});
it('should return a single datapoint', () => {
@ -300,14 +308,14 @@ describe('AppInsightsDatasource', () => {
beforeEach(() => {
options.targets[0].appInsights.timeGrain = 'PT30M';
ctx.backendSrv.datasourceRequest = (options: any) => {
datasourceRequestMock.mockImplementation((options: any) => {
expect(options.url).toContain('/api/tsdb/query');
expect(options.data.queries[0].refId).toBe('A');
expect(options.data.queries[0].appInsights.rawQueryString).toBeUndefined();
expect(options.data.queries[0].appInsights.metricName).toBe('exceptions/server');
expect(options.data.queries[0].appInsights.timeGrain).toBe('PT30M');
return Promise.resolve({ data: response, status: 200 });
};
});
});
it('should return a list of datapoints', () => {
@ -355,13 +363,13 @@ describe('AppInsightsDatasource', () => {
beforeEach(() => {
options.targets[0].appInsights.dimension = 'client/city';
ctx.backendSrv.datasourceRequest = (options: any) => {
datasourceRequestMock.mockImplementation((options: any) => {
expect(options.url).toContain('/api/tsdb/query');
expect(options.data.queries[0].appInsights.rawQueryString).toBeUndefined();
expect(options.data.queries[0].appInsights.metricName).toBe('exceptions/server');
expect(options.data.queries[0].appInsights.dimension).toBe('client/city');
return Promise.resolve({ data: response, status: 200 });
};
});
});
it('should return a list of datapoints', () => {
@ -397,10 +405,10 @@ describe('AppInsightsDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
expect(options.url).toContain('/metrics/metadata');
return Promise.resolve({ data: response, status: 200 });
};
});
});
it('should return a list of metric names', () => {
@ -435,10 +443,10 @@ describe('AppInsightsDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
expect(options.url).toContain('/metrics/metadata');
return Promise.resolve({ data: response, status: 200 });
};
});
});
it('should return a list of group bys', () => {
@ -463,10 +471,10 @@ describe('AppInsightsDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
expect(options.url).toContain('/metrics/metadata');
return Promise.resolve({ data: response, status: 200 });
};
});
});
it('should return a list of metric names', () => {
@ -501,10 +509,10 @@ describe('AppInsightsDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
expect(options.url).toContain('/metrics/metadata');
return Promise.resolve({ data: response, status: 200 });
};
});
});
it('should return a list of group bys', () => {

View File

@ -1,6 +1,6 @@
import { TimeSeries, toDataFrame } from '@grafana/data';
import { DataQueryRequest, DataQueryResponseData, DataSourceInstanceSettings } from '@grafana/data';
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { TemplateSrv } from 'app/features/templating/template_srv';
import _ from 'lodash';
@ -21,11 +21,7 @@ export default class AppInsightsDatasource {
logAnalyticsColumns: { [key: string]: LogAnalyticsColumn[] } = {};
/** @ngInject */
constructor(
instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>,
private backendSrv: BackendSrv,
private templateSrv: TemplateSrv
) {
constructor(instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>, private templateSrv: TemplateSrv) {
this.id = instanceSettings.id;
this.applicationId = instanceSettings.jsonData.appInsightsAppId;
this.baseUrl = `/appinsights/${this.version}/apps/${this.applicationId}`;
@ -119,7 +115,7 @@ export default class AppInsightsDatasource {
return;
}
const { data } = await this.backendSrv.datasourceRequest({
const { data } = await getBackendSrv().datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
data: {
@ -228,8 +224,8 @@ export default class AppInsightsDatasource {
});
}
doRequest(url: any, maxRetries = 1) {
return this.backendSrv
doRequest(url: any, maxRetries = 1): Promise<any> {
return getBackendSrv()
.datasourceRequest({
url: this.url + url,
method: 'GET',

View File

@ -4,10 +4,22 @@ import FakeSchemaData from './__mocks__/schema';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { KustoSchema } from '../types';
import { toUtc } from '@grafana/data';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv,
}));
describe('AzureLogAnalyticsDatasource', () => {
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
beforeEach(() => {
jest.clearAllMocks();
datasourceRequestMock.mockImplementation(jest.fn());
});
const ctx: any = {
backendSrv: {},
templateSrv: new TemplateSrv(),
};
@ -17,7 +29,7 @@ describe('AzureLogAnalyticsDatasource', () => {
url: 'http://azureloganalyticsapi',
};
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv);
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.templateSrv);
});
describe('When the config option "Same as Azure Monitor" has been chosen', () => {
@ -56,9 +68,9 @@ describe('AzureLogAnalyticsDatasource', () => {
ctx.instanceSettings.jsonData.tenantId = 'xxx';
ctx.instanceSettings.jsonData.clientId = 'xxx';
ctx.instanceSettings.jsonData.azureLogAnalyticsSameAs = true;
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv);
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.templateSrv);
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
if (options.url.indexOf('Microsoft.OperationalInsights/workspaces') > -1) {
workspacesUrl = options.url;
return Promise.resolve({ data: workspaceResponse, status: 200 });
@ -66,7 +78,7 @@ describe('AzureLogAnalyticsDatasource', () => {
azureLogAnalyticsUrl = options.url;
return Promise.resolve({ data: tableResponseWithOneColumn, status: 200 });
}
};
});
await ctx.ds.metricFindQuery('workspace("aworkspace").AzureActivity | distinct Category');
});
@ -94,9 +106,7 @@ describe('AzureLogAnalyticsDatasource', () => {
ctx.instanceSettings.jsonData.logAnalyticsSubscriptionId = 'xxx';
ctx.instanceSettings.jsonData.logAnalyticsTenantId = 'xxx';
ctx.instanceSettings.jsonData.logAnalyticsClientId = 'xxx';
ctx.backendSrv.datasourceRequest = () => {
return Promise.reject(error);
};
datasourceRequestMock.mockImplementation(() => Promise.reject(error));
});
it('should return error status and a detailed error message', () => {
@ -166,10 +176,10 @@ describe('AzureLogAnalyticsDatasource', () => {
describe('in time series format', () => {
describe('and the data is valid (has time, metric and value columns)', () => {
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
expect(options.url).toContain('query=AzureActivity');
return Promise.resolve({ data: response, status: 200 });
};
});
});
it('should return a list of datapoints', () => {
@ -205,10 +215,11 @@ describe('AzureLogAnalyticsDatasource', () => {
},
],
};
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
expect(options.url).toContain('query=AzureActivity');
return Promise.resolve({ data: invalidResponse, status: 200 });
};
});
});
it('should throw an exception', () => {
@ -222,10 +233,10 @@ describe('AzureLogAnalyticsDatasource', () => {
describe('in tableformat', () => {
beforeEach(() => {
options.targets[0].azureLogAnalytics.resultFormat = 'table';
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
expect(options.url).toContain('query=AzureActivity');
return Promise.resolve({ data: response, status: 200 });
};
});
});
it('should return a list of columns and rows', () => {
@ -249,10 +260,10 @@ describe('AzureLogAnalyticsDatasource', () => {
describe('When performing getSchema', () => {
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
expect(options.url).toContain('metadata');
return Promise.resolve({ data: FakeSchemaData.getlogAnalyticsFakeMetadata(), status: 200 });
};
});
});
it('should return a schema with a table and rows', () => {
@ -302,13 +313,13 @@ describe('AzureLogAnalyticsDatasource', () => {
let queryResults: any[];
beforeEach(async () => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
if (options.url.indexOf('Microsoft.OperationalInsights/workspaces') > -1) {
return Promise.resolve({ data: workspaceResponse, status: 200 });
} else {
return Promise.resolve({ data: tableResponseWithOneColumn, status: 200 });
}
};
});
queryResults = await ctx.ds.metricFindQuery('workspace("aworkspace").AzureActivity | distinct Category');
});
@ -364,13 +375,13 @@ describe('AzureLogAnalyticsDatasource', () => {
let annotationResults: any[];
beforeEach(async () => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
if (options.url.indexOf('Microsoft.OperationalInsights/workspaces') > -1) {
return Promise.resolve({ data: workspaceResponse, status: 200 });
} else {
return Promise.resolve({ data: tableResponse, status: 200 });
}
};
});
annotationResults = await ctx.ds.annotationQuery({
annotation: {

View File

@ -3,7 +3,7 @@ import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder
import ResponseParser from './response_parser';
import { AzureMonitorQuery, AzureDataSourceJsonData } from '../types';
import { DataQueryRequest, DataSourceInstanceSettings } from '@grafana/data';
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { TemplateSrv } from 'app/features/templating/template_srv';
export default class AzureLogAnalyticsDatasource {
@ -18,7 +18,6 @@ export default class AzureLogAnalyticsDatasource {
/** @ngInject */
constructor(
private instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>,
private backendSrv: BackendSrv,
private templateSrv: TemplateSrv
) {
this.id = instanceSettings.id;
@ -230,8 +229,8 @@ export default class AzureLogAnalyticsDatasource {
});
}
doRequest(url: string, maxRetries = 1) {
return this.backendSrv
doRequest(url: string, maxRetries = 1): Promise<any> {
return getBackendSrv()
.datasourceRequest({
url: this.url + url,
method: 'GET',

View File

@ -2,21 +2,28 @@ import AzureMonitorDatasource from '../datasource';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { toUtc, DataFrame } from '@grafana/data';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv,
}));
describe('AzureMonitorDatasource', () => {
const ctx: any = {
backendSrv: {},
templateSrv: new TemplateSrv(),
};
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
beforeEach(() => {
jest.clearAllMocks();
ctx.instanceSettings = {
url: 'http://azuremonitor.com',
jsonData: { subscriptionId: '9935389e-9122-4ef9-95f9-1513dd24753f' },
cloudName: 'azuremonitor',
};
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv);
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.templateSrv);
});
describe('When performing testDatasource', () => {
@ -35,9 +42,7 @@ describe('AzureMonitorDatasource', () => {
beforeEach(() => {
ctx.instanceSettings.jsonData.tenantId = 'xxx';
ctx.instanceSettings.jsonData.clientId = 'xxx';
ctx.backendSrv.datasourceRequest = () => {
return Promise.reject(error);
};
datasourceRequestMock.mockImplementation(() => Promise.reject(error));
});
it('should return error status and a detailed error message', () => {
@ -62,9 +67,7 @@ describe('AzureMonitorDatasource', () => {
beforeEach(() => {
ctx.instanceSettings.jsonData.tenantId = 'xxx';
ctx.instanceSettings.jsonData.clientId = 'xxx';
ctx.backendSrv.datasourceRequest = () => {
return Promise.resolve({ data: response, status: 200 });
};
datasourceRequestMock.mockImplementation(() => Promise.resolve({ data: response, status: 200 }));
});
it('should return success status', () => {
@ -124,10 +127,10 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
expect(options.url).toContain('/api/tsdb/query');
return Promise.resolve({ data: response, status: 200 });
};
});
});
it('should return a list of datapoints', () => {
@ -157,9 +160,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = () => {
return Promise.resolve(response);
};
datasourceRequestMock.mockImplementation((options: { url: string }) => Promise.resolve(response));
});
it('should return a list of subscriptions', () => {
@ -183,9 +184,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = () => {
return Promise.resolve(response);
};
datasourceRequestMock.mockImplementation(() => Promise.resolve(response));
});
it('should return a list of resource groups', () => {
@ -209,10 +208,10 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
expect(options.url).toContain('11112222-eeee-4949-9b2d-9106972f9123');
return Promise.resolve(response);
};
});
});
it('should return a list of resource groups', () => {
@ -243,12 +242,12 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01');
return Promise.resolve(response);
};
});
});
it('should return a list of namespaces', () => {
@ -277,12 +276,12 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01');
return Promise.resolve(response);
};
});
});
it('should return a list of namespaces', () => {
@ -315,12 +314,12 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01');
return Promise.resolve(response);
};
});
});
it('should return a list of resource names', () => {
@ -353,12 +352,12 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01');
return Promise.resolve(response);
};
});
});
it('should return a list of resource names', () => {
@ -397,7 +396,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(options.url).toBe(
@ -406,7 +405,7 @@ describe('AzureMonitorDatasource', () => {
'metricdefinitions?api-version=2018-01-01&metricnamespace=default'
);
return Promise.resolve(response);
};
});
});
it('should return a list of metric names', () => {
@ -446,7 +445,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
expect(options.url).toBe(
@ -455,7 +454,7 @@ describe('AzureMonitorDatasource', () => {
'metricdefinitions?api-version=2018-01-01&metricnamespace=default'
);
return Promise.resolve(response);
};
});
});
it('should return a list of metric names', () => {
@ -497,7 +496,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(options.url).toBe(
@ -505,7 +504,7 @@ describe('AzureMonitorDatasource', () => {
'/nodeapp/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview'
);
return Promise.resolve(response);
};
});
});
it('should return a list of metric names', () => {
@ -545,7 +544,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
expect(options.url).toBe(
@ -553,7 +552,7 @@ describe('AzureMonitorDatasource', () => {
'/nodeapp/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview'
);
return Promise.resolve(response);
};
});
});
it('should return a list of metric namespaces', () => {
@ -601,9 +600,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = () => {
return Promise.resolve(response);
};
datasourceRequestMock.mockImplementation(() => Promise.resolve(response));
});
it('should return list of Resource Groups', () => {
@ -625,9 +622,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = () => {
return Promise.resolve(response);
};
datasourceRequestMock.mockImplementation(() => Promise.resolve(response));
});
it('should return list of Resource Groups', () => {
@ -674,12 +669,12 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01');
return Promise.resolve(response);
};
});
});
it('should return list of Metric Definitions with no duplicates and no unsupported namespaces', () => {
@ -725,12 +720,12 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01');
return Promise.resolve(response);
};
});
});
it('should return list of Resource Names', () => {
@ -763,12 +758,12 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01');
return Promise.resolve(response);
};
});
});
it('should return list of Resource Names', () => {
@ -828,7 +823,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
const expected =
@ -837,7 +832,7 @@ describe('AzureMonitorDatasource', () => {
'/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
expect(options.url).toBe(expected);
return Promise.resolve(response);
};
});
});
it('should return list of Metric Definitions', () => {
@ -900,7 +895,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
const expected =
@ -909,7 +904,7 @@ describe('AzureMonitorDatasource', () => {
'/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
expect(options.url).toBe(expected);
return Promise.resolve(response);
};
});
});
it('should return Aggregation metadata for a Metric', () => {
@ -974,7 +969,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
const expected =
@ -983,7 +978,7 @@ describe('AzureMonitorDatasource', () => {
'/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
expect(options.url).toBe(expected);
return Promise.resolve(response);
};
});
});
it('should return dimensions for a Metric that has dimensions', () => {

View File

@ -12,8 +12,8 @@ import {
import { DataQueryRequest, DataQueryResponseData, DataSourceInstanceSettings } from '@grafana/data';
import { TimeSeries, toDataFrame } from '@grafana/data';
import { BackendSrv } from 'app/core/services/backend_srv';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { getBackendSrv } from '@grafana/runtime';
export default class AzureMonitorDatasource {
apiVersion = '2018-01-01';
@ -31,7 +31,6 @@ export default class AzureMonitorDatasource {
/** @ngInject */
constructor(
private instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>,
private backendSrv: BackendSrv,
private templateSrv: TemplateSrv
) {
this.id = instanceSettings.id;
@ -108,7 +107,7 @@ export default class AzureMonitorDatasource {
return Promise.resolve([]);
}
const { data } = await this.backendSrv.datasourceRequest({
const { data } = await getBackendSrv().datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
data: {
@ -434,8 +433,8 @@ export default class AzureMonitorDatasource {
return field && field.length > 0;
}
doRequest(url: string, maxRetries = 1) {
return this.backendSrv
doRequest(url: string, maxRetries = 1): Promise<any> {
return getBackendSrv()
.datasourceRequest({
url: this.url + url,
method: 'GET',

View File

@ -10,7 +10,7 @@ import {
import { MonitorConfig } from './MonitorConfig';
import { AnalyticsConfig } from './AnalyticsConfig';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { InsightsConfig } from './InsightsConfig';
import ResponseParser from '../azure_monitor/response_parser';
import { AzureDataSourceJsonData, AzureDataSourceSecureJsonData, AzureDataSourceSettings } from '../types';
@ -38,7 +38,6 @@ export class ConfigEditor extends PureComponent<Props, State> {
logAnalyticsSubscriptionId: '',
};
this.backendSrv = getBackendSrv();
this.templateSrv = new TemplateSrv();
if (this.props.options.id) {
updateDatasourcePluginOption(this.props, 'url', '/api/datasources/proxy/' + this.props.options.id);
@ -46,7 +45,6 @@ export class ConfigEditor extends PureComponent<Props, State> {
}
initPromise: CancelablePromise<any> = null;
backendSrv: BackendSrv = null;
templateSrv: TemplateSrv = null;
componentDidMount() {
@ -157,7 +155,7 @@ export class ConfigEditor extends PureComponent<Props, State> {
};
onLoadSubscriptions = async (type?: string) => {
await this.backendSrv
await getBackendSrv()
.put(`/api/datasources/${this.props.options.id}`, this.props.options)
.then((result: AzureDataSourceSettings) => {
updateDatasourcePluginOption(this.props, 'version', result.version);
@ -173,7 +171,7 @@ export class ConfigEditor extends PureComponent<Props, State> {
loadSubscriptions = async (route?: string) => {
const url = `/${route || this.props.options.jsonData.cloudName}/subscriptions?api-version=2019-03-01`;
const result = await this.backendSrv.datasourceRequest({
const result = await getBackendSrv().datasourceRequest({
url: this.props.options.url + url,
method: 'GET',
});
@ -198,7 +196,7 @@ export class ConfigEditor extends PureComponent<Props, State> {
azureMonitorUrl +
`/${subscriptionId}/providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview`;
const result = await this.backendSrv.datasourceRequest({
const result = await getBackendSrv().datasourceRequest({
url: this.props.options.url + workspaceListUrl,
method: 'GET',
});

View File

@ -4,7 +4,6 @@ import AppInsightsDatasource from './app_insights/app_insights_datasource';
import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource';
import { AzureMonitorQuery, AzureDataSourceJsonData } from './types';
import { DataSourceApi, DataQueryRequest, DataSourceInstanceSettings } from '@grafana/data';
import { BackendSrv } from 'app/core/services/backend_srv';
import { TemplateSrv } from 'app/features/templating/template_srv';
export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDataSourceJsonData> {
@ -13,20 +12,12 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
azureLogAnalyticsDatasource: AzureLogAnalyticsDatasource;
/** @ngInject */
constructor(
instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>,
private backendSrv: BackendSrv,
private templateSrv: TemplateSrv
) {
constructor(instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>, private templateSrv: TemplateSrv) {
super(instanceSettings);
this.azureMonitorDatasource = new AzureMonitorDatasource(instanceSettings, this.backendSrv, this.templateSrv);
this.appInsightsDatasource = new AppInsightsDatasource(instanceSettings, this.backendSrv, this.templateSrv);
this.azureMonitorDatasource = new AzureMonitorDatasource(instanceSettings, this.templateSrv);
this.appInsightsDatasource = new AppInsightsDatasource(instanceSettings, this.templateSrv);
this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource(
instanceSettings,
this.backendSrv,
this.templateSrv
);
this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource(instanceSettings, this.templateSrv);
}
async query(options: DataQueryRequest<AzureMonitorQuery>) {

View File

@ -1,13 +1,13 @@
import _ from 'lodash';
import { BackendSrv } from 'app/core/services/backend_srv';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { getBackendSrv } from '@grafana/runtime';
class GrafanaDatasource {
/** @ngInject */
constructor(private backendSrv: BackendSrv, private templateSrv: TemplateSrv) {}
constructor(private templateSrv: TemplateSrv) {}
query(options: any) {
return this.backendSrv
return getBackendSrv()
.get('/api/tsdb/testdata/random-walk', {
from: options.range.from.valueOf(),
to: options.range.to.valueOf(),
@ -76,7 +76,7 @@ class GrafanaDatasource {
params.tags = tags;
}
return this.backendSrv.get('/api/annotations', params);
return getBackendSrv().get('/api/annotations', params);
}
}

View File

@ -1,25 +1,37 @@
import { GrafanaDatasource } from '../datasource';
// @ts-ignore
import q from 'q';
import { dateTime } from '@grafana/data';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
import { GrafanaDatasource } from '../datasource';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv,
}));
describe('grafana data source', () => {
const getMock = jest.spyOn(backendSrv, 'get');
beforeEach(() => {
jest.clearAllMocks();
});
describe('when executing an annotations query', () => {
let calledBackendSrvParams: any;
const backendSrvStub = {
get: (url: string, options: any) => {
let templateSrvStub: any;
let ds: GrafanaDatasource;
beforeEach(() => {
getMock.mockImplementation((url: string, options: any) => {
calledBackendSrvParams = options;
return q.resolve([]);
},
};
return Promise.resolve([]);
});
const templateSrvStub = {
replace: (val: string) => {
return val.replace('$var2', 'replaced__delimiter__replaced2').replace('$var', 'replaced');
},
};
templateSrvStub = {
replace: (val: string) => {
return val.replace('$var2', 'replaced__delimiter__replaced2').replace('$var', 'replaced');
},
};
const ds = new GrafanaDatasource(backendSrvStub as any, templateSrvStub as any);
ds = new GrafanaDatasource(templateSrvStub as any);
});
describe('with tags that have template variables', () => {
const options = setupAnnotationQueryOptions({ tags: ['tag1:$var'] });

View File

@ -10,7 +10,7 @@ import {
} from '@grafana/data';
import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version';
import gfunc from './gfunc';
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { TemplateSrv } from 'app/features/templating/template_srv';
//Types
import { GraphiteOptions, GraphiteQuery, GraphiteType } from './types';
@ -30,7 +30,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
_seriesRefLetters: string;
/** @ngInject */
constructor(instanceSettings: any, private backendSrv: BackendSrv, private templateSrv: TemplateSrv) {
constructor(instanceSettings: any, private templateSrv: TemplateSrv) {
super(instanceSettings);
this.basicAuth = instanceSettings.basicAuth;
this.url = instanceSettings.url;
@ -571,7 +571,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
options.url = this.url + options.url;
options.inspect = { type: 'graphite' };
return this.backendSrv.datasourceRequest(options);
return getBackendSrv().datasourceRequest(options);
}
buildGraphiteParams(options: any, scopedVars: ScopedVars): string[] {

View File

@ -3,19 +3,26 @@ import _ from 'lodash';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { dateTime } from '@grafana/data';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv,
}));
describe('graphiteDatasource', () => {
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
const ctx: any = {
backendSrv: {},
// @ts-ignore
templateSrv: new TemplateSrv(),
instanceSettings: { url: 'url', name: 'graphiteProd', jsonData: {} },
};
beforeEach(() => {
jest.clearAllMocks();
ctx.instanceSettings.url = '/api/datasources/proxy/1';
// @ts-ignore
ctx.ds = new GraphiteDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv);
ctx.ds = new GraphiteDatasource(ctx.instanceSettings, ctx.templateSrv);
});
describe('When querying graphite with one target using query editor target spec', () => {
@ -31,7 +38,7 @@ describe('graphiteDatasource', () => {
let requestOptions: any;
beforeEach(async () => {
ctx.backendSrv.datasourceRequest = (options: any) => {
datasourceRequestMock.mockImplementation((options: any) => {
requestOptions = options;
return Promise.resolve({
data: [
@ -44,7 +51,7 @@ describe('graphiteDatasource', () => {
},
],
});
};
});
await ctx.ds.query(query).then((data: any) => {
results = data;
@ -115,10 +122,9 @@ describe('graphiteDatasource', () => {
};
beforeEach(async () => {
ctx.backendSrv.datasourceRequest = (options: any) => {
datasourceRequestMock.mockImplementation((options: any) => {
return Promise.resolve(response);
};
});
await ctx.ds.annotationQuery(options).then((data: any) => {
results = data;
});
@ -145,9 +151,9 @@ describe('graphiteDatasource', () => {
],
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: any) => {
datasourceRequestMock.mockImplementation((options: any) => {
return Promise.resolve(response);
};
});
ctx.ds.annotationQuery(options).then((data: any) => {
results = data;
@ -263,12 +269,12 @@ describe('graphiteDatasource', () => {
let requestOptions: any;
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: any) => {
datasourceRequestMock.mockImplementation((options: any) => {
requestOptions = options;
return Promise.resolve({
data: ['backend_01', 'backend_02'],
});
};
});
});
it('should generate tags query', () => {
@ -390,7 +396,6 @@ describe('graphiteDatasource', () => {
function accessScenario(name: string, url: string, fn: any) {
describe('access scenario ' + name, () => {
const ctx: any = {
backendSrv: {},
// @ts-ignore
templateSrv: new TemplateSrv(),
instanceSettings: { url: 'url', name: 'graphiteProd', jsonData: {} },
@ -405,8 +410,7 @@ function accessScenario(name: string, url: string, fn: any) {
it('tracing headers should be added', () => {
ctx.instanceSettings.url = url;
// @ts-ignore
const ds = new GraphiteDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv);
const ds = new GraphiteDatasource(ctx.instanceSettings, ctx.templateSrv);
ds.addTracingHeaders(httpOptions, options);
fn(httpOptions);
});

View File

@ -6,7 +6,7 @@ import InfluxQueryModel from './influx_query_model';
import ResponseParser from './response_parser';
import { InfluxQueryBuilder } from './query_builder';
import { InfluxQuery, InfluxOptions } from './types';
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { TemplateSrv } from 'app/features/templating/template_srv';
export default class InfluxDatasource extends DataSourceApi<InfluxQuery, InfluxOptions> {
@ -23,11 +23,7 @@ export default class InfluxDatasource extends DataSourceApi<InfluxQuery, InfluxO
httpMode: string;
/** @ngInject */
constructor(
instanceSettings: DataSourceInstanceSettings<InfluxOptions>,
private backendSrv: BackendSrv,
private templateSrv: TemplateSrv
) {
constructor(instanceSettings: DataSourceInstanceSettings<InfluxOptions>, private templateSrv: TemplateSrv) {
super(instanceSettings);
this.type = 'influxdb';
this.urls = _.map(instanceSettings.url.split(','), url => {
@ -208,7 +204,7 @@ export default class InfluxDatasource extends DataSourceApi<InfluxQuery, InfluxO
metricFindQuery(query: string, options?: any) {
const interpolated = this.templateSrv.replace(query, null, 'regex');
return this._seriesQuery(interpolated, options).then(_.curry(this.responseParser.parse)(query));
return this._seriesQuery(interpolated, options).then(() => this.responseParser.parse(query));
}
getTagKeys(options: any = {}) {
@ -320,28 +316,30 @@ export default class InfluxDatasource extends DataSourceApi<InfluxQuery, InfluxO
req.headers['Content-type'] = 'application/x-www-form-urlencoded';
}
return this.backendSrv.datasourceRequest(req).then(
(result: any) => {
return result.data;
},
(err: any) => {
if (err.status !== 0 || err.status >= 300) {
if (err.data && err.data.error) {
throw {
message: 'InfluxDB Error: ' + err.data.error,
data: err.data,
config: err.config,
};
} else {
throw {
message: 'Network Error: ' + err.statusText + '(' + err.status + ')',
data: err.data,
config: err.config,
};
return getBackendSrv()
.datasourceRequest(req)
.then(
(result: any) => {
return result.data;
},
(err: any) => {
if (err.status !== 0 || err.status >= 300) {
if (err.data && err.data.error) {
throw {
message: 'InfluxDB Error: ' + err.data.error,
data: err.data,
config: err.config,
};
} else {
throw {
message: 'Network Error: ' + err.statusText + '(' + err.status + ')',
data: err.data,
config: err.config,
};
}
}
}
}
);
);
}
getTimeFilter(options: any) {

View File

@ -1,18 +1,26 @@
import InfluxDatasource from '../datasource';
import { TemplateSrvStub } from 'test/specs/helpers';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv,
}));
describe('InfluxDataSource', () => {
const ctx: any = {
backendSrv: {},
//@ts-ignore
templateSrv: new TemplateSrvStub(),
instanceSettings: { url: 'url', name: 'influxDb', jsonData: { httpMode: 'GET' } },
};
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
beforeEach(() => {
jest.clearAllMocks();
ctx.instanceSettings.url = '/api/datasources/proxy/1';
ctx.ds = new InfluxDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv);
ctx.ds = new InfluxDatasource(ctx.instanceSettings, ctx.templateSrv);
});
describe('When issuing metricFindQuery', () => {
@ -26,7 +34,7 @@ describe('InfluxDataSource', () => {
let requestQuery: any, requestMethod: any, requestData: any;
beforeEach(async () => {
ctx.backendSrv.datasourceRequest = (req: any) => {
datasourceRequestMock.mockImplementation((req: any) => {
requestMethod = req.method;
requestQuery = req.params.q;
requestData = req.data;
@ -43,7 +51,7 @@ describe('InfluxDataSource', () => {
},
],
});
};
});
await ctx.ds.metricFindQuery(query, queryOptions).then(() => {});
});
@ -60,60 +68,59 @@ describe('InfluxDataSource', () => {
expect(requestData).toBeNull();
});
});
});
describe('InfluxDataSource in POST query mode', () => {
const ctx: any = {
backendSrv: {},
//@ts-ignore
templateSrv: new TemplateSrvStub(),
instanceSettings: { url: 'url', name: 'influxDb', jsonData: { httpMode: 'POST' } },
};
describe('InfluxDataSource in POST query mode', () => {
const ctx: any = {
//@ts-ignore
templateSrv: new TemplateSrvStub(),
instanceSettings: { url: 'url', name: 'influxDb', jsonData: { httpMode: 'POST' } },
};
beforeEach(() => {
ctx.instanceSettings.url = '/api/datasources/proxy/1';
ctx.ds = new InfluxDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv);
});
beforeEach(() => {
ctx.instanceSettings.url = '/api/datasources/proxy/1';
ctx.ds = new InfluxDatasource(ctx.instanceSettings, ctx.templateSrv);
});
describe('When issuing metricFindQuery', () => {
const query = 'SELECT max(value) FROM measurement';
const queryOptions: any = {};
let requestMethod: any, requestQueryParameter: any, queryEncoded: any, requestQuery: any;
describe('When issuing metricFindQuery', () => {
const query = 'SELECT max(value) FROM measurement';
const queryOptions: any = {};
let requestMethod: any, requestQueryParameter: any, queryEncoded: any, requestQuery: any;
beforeEach(async () => {
ctx.backendSrv.datasourceRequest = (req: any) => {
requestMethod = req.method;
requestQueryParameter = req.params;
requestQuery = req.data;
return Promise.resolve({
results: [
{
series: [
{
name: 'measurement',
columns: ['max'],
values: [[1]],
},
],
},
],
beforeEach(async () => {
datasourceRequestMock.mockImplementation((req: any) => {
requestMethod = req.method;
requestQueryParameter = req.params;
requestQuery = req.data;
return Promise.resolve({
results: [
{
series: [
{
name: 'measurement',
columns: ['max'],
values: [[1]],
},
],
},
],
});
});
};
queryEncoded = await ctx.ds.serializeParams({ q: query });
await ctx.ds.metricFindQuery(query, queryOptions).then(() => {});
});
queryEncoded = await ctx.ds.serializeParams({ q: query });
await ctx.ds.metricFindQuery(query, queryOptions).then(() => {});
});
it('should have the query form urlencoded', () => {
expect(requestQuery).toBe(queryEncoded);
});
it('should have the query form urlencoded', () => {
expect(requestQuery).toBe(queryEncoded);
});
it('should use the HTTP POST method', () => {
expect(requestMethod).toBe('POST');
});
it('should use the HTTP POST method', () => {
expect(requestMethod).toBe('POST');
});
it('should not have q as a query parameter', () => {
expect(requestQueryParameter).not.toHaveProperty('q');
it('should not have q as a query parameter', () => {
expect(requestQueryParameter).not.toHaveProperty('q');
});
});
});
});

View File

@ -2,13 +2,20 @@ import LokiDatasource, { RangeQueryOptions } from './datasource';
import { LokiQuery, LokiResultType, LokiResponse, LokiLegacyStreamResponse } from './types';
import { getQueryOptions } from 'test/helpers/getQueryOptions';
import { AnnotationQueryRequest, DataSourceApi, DataFrame, dateTime, TimeRange } from '@grafana/data';
import { BackendSrv } from 'app/core/services/backend_srv';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { CustomVariable } from 'app/features/templating/custom_variable';
import { makeMockLokiDatasource } from './mocks';
import { ExploreMode } from 'app/types';
import { of } from 'rxjs';
import omit from 'lodash/omit';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv,
}));
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
describe('LokiDatasource', () => {
const instanceSettings: any = {
@ -42,8 +49,10 @@ describe('LokiDatasource', () => {
},
};
const backendSrvMock = { datasourceRequest: jest.fn() };
const backendSrv = (backendSrvMock as unknown) as BackendSrv;
beforeEach(() => {
jest.clearAllMocks();
datasourceRequestMock.mockImplementation(() => Promise.resolve());
});
const templateSrvMock = ({
getAdhocFilters: (): any[] => [],
@ -56,7 +65,7 @@ describe('LokiDatasource', () => {
beforeEach(() => {
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
const customSettings = { ...instanceSettings, jsonData: customData };
ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock);
ds = new LokiDatasource(customSettings, templateSrvMock);
adjustIntervalSpy = jest.spyOn(ds, 'adjustInterval');
});
@ -95,8 +104,8 @@ describe('LokiDatasource', () => {
beforeEach(() => {
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
const customSettings = { ...instanceSettings, jsonData: customData };
ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock);
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(legacyTestResp));
ds = new LokiDatasource(customSettings, templateSrvMock);
datasourceRequestMock.mockImplementation(() => Promise.resolve(legacyTestResp));
});
test('should try latest endpoint but fall back to legacy endpoint if it cannot be reached', async () => {
@ -112,14 +121,18 @@ describe('LokiDatasource', () => {
});
describe('when querying', () => {
const testLimit = makeLimitTest(instanceSettings, backendSrvMock, backendSrv, templateSrvMock, legacyTestResp);
let ds: LokiDatasource;
let testLimit: any;
beforeAll(() => {
testLimit = makeLimitTest(instanceSettings, datasourceRequestMock, templateSrvMock, legacyTestResp);
});
beforeEach(() => {
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
const customSettings = { ...instanceSettings, jsonData: customData };
ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock);
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
ds = new LokiDatasource(customSettings, templateSrvMock);
datasourceRequestMock.mockImplementation(() => Promise.resolve(testResp));
});
test('should run instant query and range query when in metrics mode', async () => {
@ -183,11 +196,13 @@ describe('LokiDatasource', () => {
test('should return series data', async () => {
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
const customSettings = { ...instanceSettings, jsonData: customData };
const ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock);
backendSrvMock.datasourceRequest = jest
.fn()
.mockReturnValueOnce(Promise.resolve(legacyTestResp))
.mockReturnValueOnce(Promise.resolve(omit(legacyTestResp, 'status')));
const ds = new LokiDatasource(customSettings, templateSrvMock);
datasourceRequestMock.mockImplementation(
jest
.fn()
.mockReturnValueOnce(Promise.resolve(legacyTestResp))
.mockReturnValueOnce(Promise.resolve(omit(legacyTestResp, 'status')))
);
const options = getQueryOptions<LokiQuery>({
targets: [{ expr: '{job="grafana"} |= "foo"', refId: 'B' }],
@ -209,7 +224,7 @@ describe('LokiDatasource', () => {
beforeEach(() => {
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
const customSettings = { ...instanceSettings, jsonData: customData };
ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock);
ds = new LokiDatasource(customSettings, templateSrvMock);
variable = new CustomVariable({}, {} as any);
});
@ -256,17 +271,15 @@ describe('LokiDatasource', () => {
describe('and call succeeds', () => {
beforeEach(async () => {
const backendSrv = ({
async datasourceRequest() {
return Promise.resolve({
status: 200,
data: {
data: ['avalue'],
},
});
},
} as unknown) as BackendSrv;
ds = new LokiDatasource(instanceSettings, backendSrv, {} as TemplateSrv);
datasourceRequestMock.mockImplementation(async () => {
return Promise.resolve({
status: 200,
data: {
values: ['avalue'],
},
});
});
ds = new LokiDatasource(instanceSettings, {} as TemplateSrv);
result = await ds.testDatasource();
});
@ -278,7 +291,7 @@ describe('LokiDatasource', () => {
describe('and call fails with 401 error', () => {
let ds: LokiDatasource;
beforeEach(() => {
backendSrvMock.datasourceRequest = jest.fn(() =>
datasourceRequestMock.mockImplementation(() =>
Promise.reject({
statusText: 'Unauthorized',
status: 401,
@ -290,7 +303,7 @@ describe('LokiDatasource', () => {
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
const customSettings = { ...instanceSettings, jsonData: customData };
ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock);
ds = new LokiDatasource(customSettings, templateSrvMock);
});
it('should return error status and a detailed error message', async () => {
@ -302,16 +315,14 @@ describe('LokiDatasource', () => {
describe('and call fails with 404 error', () => {
beforeEach(async () => {
const backendSrv = ({
async datasourceRequest() {
return Promise.reject({
statusText: 'Not found',
status: 404,
data: '404 page not found',
});
},
} as unknown) as BackendSrv;
ds = new LokiDatasource(instanceSettings, backendSrv, {} as TemplateSrv);
datasourceRequestMock.mockImplementation(() =>
Promise.reject({
statusText: 'Not found',
status: 404,
data: '404 page not found',
})
);
ds = new LokiDatasource(instanceSettings, {} as TemplateSrv);
result = await ds.testDatasource();
});
@ -323,16 +334,14 @@ describe('LokiDatasource', () => {
describe('and call fails with 502 error', () => {
beforeEach(async () => {
const backendSrv = ({
async datasourceRequest() {
return Promise.reject({
statusText: 'Bad Gateway',
status: 502,
data: '',
});
},
} as unknown) as BackendSrv;
ds = new LokiDatasource(instanceSettings, backendSrv, {} as TemplateSrv);
datasourceRequestMock.mockImplementation(() =>
Promise.reject({
statusText: 'Bad Gateway',
status: 502,
data: '',
})
);
ds = new LokiDatasource(instanceSettings, {} as TemplateSrv);
result = await ds.testDatasource();
});
@ -344,7 +353,7 @@ describe('LokiDatasource', () => {
});
describe('when creating a range query', () => {
const ds = new LokiDatasource(instanceSettings, backendSrv, templateSrvMock);
const ds = new LokiDatasource(instanceSettings, templateSrvMock);
const query: LokiQuery = { expr: 'foo', refId: 'bar' };
// Loki v1 API has an issue with float step parameters, can be removed when API is fixed
@ -362,31 +371,33 @@ describe('LokiDatasource', () => {
describe('annotationQuery', () => {
it('should transform the loki data to annotation response', async () => {
const ds = new LokiDatasource(instanceSettings, backendSrv, templateSrvMock);
backendSrvMock.datasourceRequest = jest
.fn()
.mockReturnValueOnce(
Promise.resolve({
data: [],
status: 404,
})
)
.mockReturnValueOnce(
Promise.resolve({
data: {
streams: [
{
entries: [{ ts: '2019-02-01T10:27:37.498180581Z', line: 'hello' }],
labels: '{label="value"}',
},
{
entries: [{ ts: '2019-02-01T12:27:37.498180581Z', line: 'hello 2' }],
labels: '{label2="value2"}',
},
],
},
})
);
const ds = new LokiDatasource(instanceSettings, templateSrvMock);
datasourceRequestMock.mockImplementation(
jest
.fn()
.mockReturnValueOnce(
Promise.resolve({
data: [],
status: 404,
})
)
.mockReturnValueOnce(
Promise.resolve({
data: {
streams: [
{
entries: [{ ts: '2019-02-01T10:27:37.498180581Z', line: 'hello' }],
labels: '{label="value"}',
},
{
entries: [{ ts: '2019-02-01T12:27:37.498180581Z', line: 'hello 2' }],
labels: '{label2="value2"}',
},
],
},
})
)
);
const query = makeAnnotationQueryRequest();
const res = await ds.annotationQuery(query);
@ -400,7 +411,7 @@ describe('LokiDatasource', () => {
});
describe('metricFindQuery', () => {
const ds = new LokiDatasource(instanceSettings, backendSrv, templateSrvMock);
const ds = new LokiDatasource(instanceSettings, templateSrvMock);
const mocks = makeMetadataAndVersionsMocks();
mocks.forEach((mock, index) => {
@ -456,21 +467,15 @@ type LimitTestArgs = {
maxLines?: number;
expectedLimit: number;
};
function makeLimitTest(
instanceSettings: any,
backendSrvMock: any,
backendSrv: any,
templateSrvMock: any,
testResp: any
) {
function makeLimitTest(instanceSettings: any, datasourceRequestMock: any, templateSrvMock: any, testResp: any) {
return ({ maxDataPoints, maxLines, expectedLimit }: LimitTestArgs) => {
let settings = instanceSettings;
if (Number.isFinite(maxLines)) {
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
settings = { ...instanceSettings, jsonData: customData };
}
const ds = new LokiDatasource(settings, backendSrv, templateSrvMock);
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
const ds = new LokiDatasource(settings, templateSrvMock);
datasourceRequestMock.mockImplementation(() => Promise.resolve(testResp));
const options = getQueryOptions<LokiQuery>({ targets: [{ expr: 'foo', refId: 'B' }] });
if (Number.isFinite(maxDataPoints)) {
@ -482,8 +487,8 @@ function makeLimitTest(
ds.query(options);
expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain(`limit=${expectedLimit}`);
expect(datasourceRequestMock.mock.calls.length).toBe(1);
expect(datasourceRequestMock.mock.calls[0][0].url).toContain(`limit=${expectedLimit}`);
};
}

View File

@ -6,7 +6,8 @@ import { map, filter, catchError, switchMap, mergeMap } from 'rxjs/operators';
// Services & Utils
import { dateMath } from '@grafana/data';
import { addLabelToSelector, keepSelectorFilters } from 'app/plugins/datasource/prometheus/add_label_to_query';
import { BackendSrv, DatasourceRequestOptions } from 'app/core/services/backend_srv';
import { DatasourceRequestOptions } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { safeStringifyValue, convertToWebSocketUrl } from 'app/core/utils/explore';
import {
@ -84,11 +85,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
maxLines: number;
/** @ngInject */
constructor(
private instanceSettings: DataSourceInstanceSettings<LokiOptions>,
private backendSrv: BackendSrv,
private templateSrv: TemplateSrv
) {
constructor(private instanceSettings: DataSourceInstanceSettings<LokiOptions>, private templateSrv: TemplateSrv) {
super(instanceSettings);
this.languageProvider = new LanguageProvider(this);
@ -122,7 +119,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
url,
};
return from(this.backendSrv.datasourceRequest(req));
return from(getBackendSrv().datasourceRequest(req));
}
query(options: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> {

View File

@ -1,6 +1,6 @@
import _ from 'lodash';
import ResponseParser from './response_parser';
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
//Types
@ -13,12 +13,7 @@ export class MssqlDatasource {
interval: string;
/** @ngInject */
constructor(
instanceSettings: any,
private backendSrv: BackendSrv,
private templateSrv: TemplateSrv,
private timeSrv: TimeSrv
) {
constructor(instanceSettings: any, private templateSrv: TemplateSrv, private timeSrv: TimeSrv) {
this.name = instanceSettings.name;
this.id = instanceSettings.id;
this.responseParser = new ResponseParser();
@ -81,7 +76,7 @@ export class MssqlDatasource {
return Promise.resolve({ data: [] });
}
return this.backendSrv
return getBackendSrv()
.datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
@ -106,7 +101,7 @@ export class MssqlDatasource {
format: 'table',
};
return this.backendSrv
return getBackendSrv()
.datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
@ -139,7 +134,7 @@ export class MssqlDatasource {
to: range.to.valueOf().toString(),
};
return this.backendSrv
return getBackendSrv()
.datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
@ -149,7 +144,7 @@ export class MssqlDatasource {
}
testDatasource() {
return this.backendSrv
return getBackendSrv()
.datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',

View File

@ -4,19 +4,26 @@ import { CustomVariable } from 'app/features/templating/custom_variable';
import { dateTime } from '@grafana/data';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv,
}));
describe('MSSQLDatasource', () => {
const templateSrv: TemplateSrv = new TemplateSrv();
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
const ctx: any = {
backendSrv: {},
timeSrv: new TimeSrvStub(),
};
beforeEach(() => {
ctx.instanceSettings = { name: 'mssql' };
jest.clearAllMocks();
ctx.ds = new MssqlDatasource(ctx.instanceSettings, ctx.backendSrv, templateSrv, ctx.timeSrv);
ctx.instanceSettings = { name: 'mssql' };
ctx.ds = new MssqlDatasource(ctx.instanceSettings, templateSrv, ctx.timeSrv);
});
describe('When performing annotationQuery', () => {
@ -54,9 +61,7 @@ describe('MSSQLDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: any) => {
return Promise.resolve({ data: response, status: 200 });
};
datasourceRequestMock.mockImplementation((options: any) => Promise.resolve({ data: response, status: 200 }));
return ctx.ds.annotationQuery(options).then((data: any) => {
results = data;
@ -102,9 +107,7 @@ describe('MSSQLDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: any) => {
return Promise.resolve({ data: response, status: 200 });
};
datasourceRequestMock.mockImplementation((options: any) => Promise.resolve({ data: response, status: 200 }));
return ctx.ds.metricFindQuery(query).then((data: any) => {
results = data;
@ -143,9 +146,7 @@ describe('MSSQLDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: any) => {
return Promise.resolve({ data: response, status: 200 });
};
datasourceRequestMock.mockImplementation((options: any) => Promise.resolve({ data: response, status: 200 }));
return ctx.ds.metricFindQuery(query).then((data: any) => {
results = data;
@ -186,10 +187,7 @@ describe('MSSQLDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: any) => {
return Promise.resolve({ data: response, status: 200 });
};
datasourceRequestMock.mockImplementation((options: any) => Promise.resolve({ data: response, status: 200 }));
return ctx.ds.metricFindQuery(query).then((data: any) => {
results = data;
});
@ -229,10 +227,10 @@ describe('MSSQLDatasource', () => {
beforeEach(() => {
ctx.timeSrv.setTime(time);
ctx.backendSrv.datasourceRequest = (options: any) => {
datasourceRequestMock.mockImplementation((options: any) => {
results = options.data;
return Promise.resolve({ data: response, status: 200 });
};
});
return ctx.ds.metricFindQuery(query);
});

View File

@ -1,7 +1,7 @@
import _ from 'lodash';
import ResponseParser from './response_parser';
import MysqlQuery from 'app/plugins/datasource/mysql/mysql_query';
import { BackendSrv } from 'app/core/services/backend_srv';
import { getBackendSrv } from '@grafana/runtime';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
//Types
@ -16,12 +16,7 @@ export class MysqlDatasource {
interval: string;
/** @ngInject */
constructor(
instanceSettings: any,
private backendSrv: BackendSrv,
private templateSrv: TemplateSrv,
private timeSrv: TimeSrv
) {
constructor(instanceSettings: any, private templateSrv: TemplateSrv, private timeSrv: TimeSrv) {
this.name = instanceSettings.name;
this.id = instanceSettings.id;
this.responseParser = new ResponseParser();
@ -83,7 +78,7 @@ export class MysqlDatasource {
return Promise.resolve({ data: [] });
}
return this.backendSrv
return getBackendSrv()
.datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
@ -110,7 +105,7 @@ export class MysqlDatasource {
format: 'table',
};
return this.backendSrv
return getBackendSrv()
.datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
@ -156,7 +151,7 @@ export class MysqlDatasource {
data['to'] = optionalOptions.range.to.valueOf().toString();
}
return this.backendSrv
return getBackendSrv()
.datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
@ -166,7 +161,7 @@ export class MysqlDatasource {
}
testDatasource() {
return this.backendSrv
return getBackendSrv()
.datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',

Some files were not shown because too many files have changed in this diff Show More