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", "redux-thunk": "2.3.0",
"reselect": "4.0.0", "reselect": "4.0.0",
"rst2html": "github:thoward/rst2html#990cb89", "rst2html": "github:thoward/rst2html#990cb89",
"rxjs": "6.4.0", "rxjs": "6.5.4",
"search-query-parser": "1.5.2", "search-query-parser": "1.5.2",
"slate": "0.47.8", "slate": "0.47.8",
"slate-plain-serializer": "0.7.10", "slate-plain-serializer": "0.7.10",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import { BackendSrv } from '../services/backend_srv'; import { backendSrv } from '../services/backend_srv';
const template = ` const template = `
<select class="gf-form-input" ng-model="ctrl.model" ng-options="f.value as f.text for f in ctrl.options"></select> <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; model: any;
options: any; options: any;
/** @ngInject */
constructor(private backendSrv: BackendSrv) {}
$onInit() { $onInit() {
this.options = [{ value: 0, text: 'Default' }]; this.options = [{ value: 0, text: 'Default' }];
return this.backendSrv.search({ starred: true }).then(res => { return backendSrv.search({ starred: true }).then(res => {
res.forEach(dash => { res.forEach(dash => {
this.options.push({ value: dash.id, text: dash.title }); 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 coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { SearchSrv } from 'app/core/services/search_srv'; 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 { ContextSrv } from 'app/core/services/context_srv';
import { CoreEvents } from 'app/types'; import { CoreEvents } from 'app/types';
import { promiseToDigest } from '../../utils/promiseToDigest';
export interface Section { export interface Section {
id: number; id: number;
@@ -69,12 +70,7 @@ export class ManageDashboardsCtrl {
hasEditPermissionInFolders: boolean; hasEditPermissionInFolders: boolean;
/** @ngInject */ /** @ngInject */
constructor( constructor(private $scope: IScope, private searchSrv: SearchSrv, private contextSrv: ContextSrv) {
private $scope: IScope,
private backendSrv: BackendSrv,
private searchSrv: SearchSrv,
private contextSrv: ContextSrv
) {
this.isEditor = this.contextSrv.isEditor; this.isEditor = this.contextSrv.isEditor;
this.hasEditPermissionInFolders = this.contextSrv.hasEditPermissionInFolders; this.hasEditPermissionInFolders = this.contextSrv.hasEditPermissionInFolders;
@@ -108,10 +104,10 @@ export class ManageDashboardsCtrl {
.then(() => { .then(() => {
if (!this.folderUid) { if (!this.folderUid) {
this.$scope.$digest(); 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; this.canSave = folder.canSave;
if (!this.canSave) { if (!this.canSave) {
this.hasEditPermissionInFolders = false; this.hasEditPermissionInFolders = false;
@@ -216,9 +212,11 @@ export class ManageDashboardsCtrl {
} }
private deleteFoldersAndDashboards(folderUids: string[], dashboardUids: string[]) { private deleteFoldersAndDashboards(folderUids: string[], dashboardUids: string[]) {
this.backendSrv.deleteFoldersAndDashboards(folderUids, dashboardUids).then(() => { promiseToDigest(this.$scope)(
this.refreshList(); backendSrv.deleteFoldersAndDashboards(folderUids, dashboardUids).then(() => {
}); this.refreshList();
})
);
} }
getDashboardsToMove() { getDashboardsToMove() {

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
import config from 'app/core/config'; import config from 'app/core/config';
import coreModule from '../core_module'; import coreModule from '../core_module';
import { getBackendSrv } from '@grafana/runtime/src/services';
import { promiseToDigest } from '../utils/promiseToDigest';
export class SignUpCtrl { export class SignUpCtrl {
/** @ngInject */ /** @ngInject */
constructor(private $scope: any, private backendSrv: any, $location: any, contextSrv: any) { constructor(private $scope: any, $location: any, contextSrv: any) {
contextSrv.sidemenu = false; contextSrv.sidemenu = false;
$scope.ctrl = this; $scope.ctrl = this;
@@ -34,10 +36,14 @@ export class SignUpCtrl {
}, },
}; };
backendSrv.get('/api/user/signup/options').then((options: any) => { promiseToDigest($scope)(
$scope.verifyEmailEnabled = options.verifyEmailEnabled; getBackendSrv()
$scope.autoAssignOrg = options.autoAssignOrg; .get('/api/user/signup/options')
}); .then((options: any) => {
$scope.verifyEmailEnabled = options.verifyEmailEnabled;
$scope.autoAssignOrg = options.autoAssignOrg;
})
);
} }
submit() { submit() {
@@ -45,13 +51,15 @@ export class SignUpCtrl {
return; return;
} }
this.backendSrv.post('/api/user/signup/step2', this.$scope.formModel).then((rsp: any) => { getBackendSrv()
if (rsp.code === 'redirect-to-select-org') { .post('/api/user/signup/step2', this.$scope.formModel)
window.location.href = config.appSubUrl + '/profile/select-org?signup=1'; .then((rsp: any) => {
} else { if (rsp.code === 'redirect-to-select-org') {
window.location.href = config.appSubUrl + '/'; 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 omitBy from 'lodash/omitBy';
import angular from 'angular'; import { from, merge, MonoTypeOperatorFunction, Observable, Subject, throwError } from 'rxjs';
import coreModule from 'app/core/core_module'; 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 appEvents from 'app/core/app_events';
import config from 'app/core/config'; import config from 'app/core/config';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { DashboardSearchHit } from 'app/types/search'; import { DashboardSearchHit } from 'app/types/search';
import { ContextSrv } from './context_srv'; import { CoreEvents, DashboardDTO, FolderInfo } from 'app/types';
import { FolderInfo, DashboardDTO, CoreEvents } from 'app/types'; import { ContextSrv, contextSrv } from './context_srv';
import { BackendSrv as BackendService, getBackendSrv as getBackendService, BackendSrvRequest } from '@grafana/runtime'; import { coreModule } from 'app/core/core_module';
import { AppEvents } from '@grafana/data'; import { Emitter } from '../utils/emitter';
export interface DatasourceRequestOptions { export interface DatasourceRequestOptions {
retry?: number; retry?: number;
method?: string; method?: string;
requestId?: string; requestId?: string;
timeout?: angular.IPromise<any>; timeout?: Promise<any>;
url?: string; url?: string;
headers?: { [key: string]: any }; headers?: Record<string, any>;
silent?: boolean; 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 { 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 HTTP_REQUEST_CANCELED = -1;
private noBackendCache: boolean; private noBackendCache: boolean;
private dependencies: BackendSrvDependencies = {
fromFetch: fromFetch,
appEvents: appEvents,
contextSrv: contextSrv,
logout: () => {
window.location.href = config.appSubUrl + '/logout';
},
};
/** @ngInject */ constructor(deps?: BackendSrvDependencies) {
constructor( if (deps) {
private $http: any, this.dependencies = {
private $q: angular.IQService, ...this.dependencies,
private $timeout: angular.ITimeoutService, ...deps,
private contextSrv: ContextSrv };
) {} }
get(url: string, params?: any) {
return this.request({ method: 'GET', url, params });
} }
delete(url: string) { async get(url: string, params?: any) {
return this.request({ method: 'DELETE', url }); return await this.request({ method: 'GET', url, params });
} }
post(url: string, data?: any) { async delete(url: string) {
return this.request({ method: 'POST', url, data }); return await this.request({ method: 'DELETE', url });
} }
patch(url: string, data: any) { async post(url: string, data?: any) {
return this.request({ method: 'PATCH', url, data }); return await this.request({ method: 'POST', url, data });
} }
put(url: string, data: any) { async patch(url: string, data: any) {
return this.request({ method: 'PUT', url, data }); 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) { withNoBackendCache(callback: any) {
@@ -61,18 +123,18 @@ export class BackendSrv implements BackendService {
}); });
} }
requestErrorHandler(err: any) { requestErrorHandler = (err: ErrorResponse) => {
if (err.isHandled) { if (err.isHandled) {
return; return;
} }
let data = err.data || { message: 'Unexpected error' }; let data = err.data ?? { message: 'Unexpected error' };
if (_.isString(data)) { if (typeof data === 'string') {
data = { message: data }; data = { message: data };
} }
if (err.status === 422) { if (err.status === 422) {
appEvents.emit(AppEvents.alertWarning, ['Validation failed', data.message]); this.dependencies.appEvents.emit(AppEvents.alertWarning, ['Validation failed', data.message]);
throw data; throw data;
} }
@@ -84,170 +146,133 @@ export class BackendSrv implements BackendService {
message = 'Error'; 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; throw data;
} };
request(options: BackendSrvRequest) { async request(options: BackendSrvRequest): Promise<any> {
options.retry = options.retry || 0; options = this.parseRequestOptions(options, this.dependencies.contextSrv.user?.orgId);
const requestIsLocal = !options.url.match(/^http/);
const firstAttempt = options.retry === 0;
if (requestIsLocal) { const fromFetchStream = this.getFromFetchStream(options);
if (this.contextSrv.user && this.contextSrv.user.orgId) { const failureStream = fromFetchStream.pipe(this.toFailureStream(options));
options.headers = options.headers || {}; const successStream = fromFetchStream.pipe(
options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId; filter(response => response.ok === true),
} map(response => {
const fetchSuccessResponse: SuccessResponse = response.data;
if (options.url.indexOf('/') === 0) { return fetchSuccessResponse;
options.url = options.url.substring(1); }),
} tap(response => {
} if (options.method !== 'GET' && response?.message && options.showSuccessAlert !== false) {
this.dependencies.appEvents.emit(AppEvents.alertSuccess, [response.message]);
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]);
}
}
} }
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>) { return merge(successStream, failureStream)
if (requestId in this.inFlightRequests) { .pipe(
this.inFlightRequests[requestId].push(canceler); catchError((err: ErrorResponse) => {
} else { if (err.status === 401) {
this.inFlightRequests[requestId] = [canceler]; 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) { resolveCancelerIfExists(requestId: string) {
const cancelers = this.inFlightRequests[requestId]; this.inFlightRequests.next(requestId);
if (!_.isUndefined(cancelers) && cancelers.length) {
cancelers[0].resolve();
}
} }
datasourceRequest(options: BackendSrvRequest) { async datasourceRequest(options: BackendSrvRequest): Promise<any> {
let canceler: angular.IDeferred<any> = null; // A requestId is provided by the datasource as a unique identifier for a
options.retry = options.retry || 0; // 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
// A requestID is provided by the datasource as a unique identifier for a if (options.requestId) {
// particular query. If the requestID exists, the promise it is keyed to this.inFlightRequests.next(options.requestId);
// 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);
} }
const requestIsLocal = !options.url.match(/^http/); options = this.parseDataSourceRequestOptions(
const firstAttempt = options.retry === 0; options,
this.dependencies.contextSrv.user?.orgId,
this.noBackendCache
);
if (requestIsLocal) { const fromFetchStream = this.getFromFetchStream(options);
if (this.contextSrv.user && this.contextSrv.user.orgId) { const failureStream = fromFetchStream.pipe(this.toFailureStream(options));
options.headers = options.headers || {}; const successStream = fromFetchStream.pipe(
options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId; filter(response => response.ok === true),
} map(response => {
const { data } = response;
if (options.url.indexOf('/') === 0) { const fetchSuccessResponse: DataSourceSuccessResponse = { data };
options.url = options.url.substring(1); return fetchSuccessResponse;
} }),
tap(res => {
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) => {
if (!options.silent) { 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 return merge(successStream, failureStream)
if (requestIsLocal && firstAttempt && err.status === 401) { .pipe(
return this.loginPing() catchError((err: ErrorResponse) => {
.then(() => { if (err.status === this.HTTP_REQUEST_CANCELED) {
options.retry = 1; return throwError({
if (canceler) { err,
canceler.resolve(); cancelled: true,
}
return this.datasourceRequest(options);
})
.catch((err: any) => {
if (err.status === 401) {
window.location.href = config.appSubUrl + '/logout';
throw err;
}
}); });
} }
// populate error obj on Internal Error if (err.status === 401) {
if (_.isString(err.data) && err.status === 500) { this.dependencies.logout();
err.data = { return throwError(err);
error: err.statusText, }
response: err.data,
};
}
// for Prometheus // populate error obj on Internal Error
if (err.data && !err.data.message && _.isString(err.data.error)) { if (typeof err.data === 'string' && err.status === 500) {
err.data.message = err.data.error; err.data = {
} error: err.statusText,
if (!options.silent) { response: err.data,
appEvents.emit(CoreEvents.dsRequestError, err); };
} }
throw err;
}) // for Prometheus
.finally(() => { if (err.data && !err.data.message && typeof err.data.error === 'string') {
// clean up err.data.message = err.data.error;
if (options.requestId) { }
this.inFlightRequests[options.requestId].shift();
} 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() { loginPing() {
@@ -309,7 +334,7 @@ export class BackendSrv implements BackendService {
tasks.push(this.createTask(this.deleteDashboard.bind(this), true, dashboardUid, true)); tasks.push(this.createTask(this.deleteDashboard.bind(this), true, dashboardUid, true));
} }
return this.executeInOrder(tasks, []); return this.executeInOrder(tasks);
} }
moveDashboards(dashboardUids: string[], toFolder: FolderInfo) { 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)); 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 { return {
totalCount: result.length, totalCount: result.length,
successCount: _.filter(result, { succeeded: true }).length, successCount: result.filter((res: any) => res.succeeded).length,
alreadyInFolderCount: _.filter(result, { alreadyInFolder: true }).length, alreadyInFolderCount: result.filter((res: any) => res.alreadyInFolder).length,
}; };
}); });
} }
private moveDashboard(uid: string, toFolder: FolderInfo) { private async moveDashboard(uid: string, toFolder: FolderInfo) {
const deferred = this.$q.defer(); const fullDash: DashboardDTO = await this.getDashboardByUid(uid);
const model = new DashboardModel(fullDash.dashboard, fullDash.meta);
this.getDashboardByUid(uid).then((fullDash: DashboardDTO) => { if ((!fullDash.meta.folderId && toFolder.id === 0) || fullDash.meta.folderId === toFolder.id) {
const model = new DashboardModel(fullDash.dashboard, fullDash.meta); return { alreadyInFolder: true };
}
if ((!fullDash.meta.folderId && toFolder.id === 0) || fullDash.meta.folderId === toFolder.id) { const clone = model.getSaveModelClone();
deferred.resolve({ alreadyInFolder: true }); const options = {
return; 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(); err.isHandled = true;
const options = { options.overwrite = true;
folderId: toFolder.id,
overwrite: false,
};
this.saveDashboard(clone, options) try {
.then(() => { await this.saveDashboard(clone, options);
deferred.resolve({ succeeded: true }); return { succeeded: true };
}) } catch (e) {
.catch((err: any) => { return { succeeded: false };
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;
} }
private createTask(fn: Function, ignoreRejections: boolean, ...args: any[]) { private createTask(fn: (...args: any[]) => Promise<any>, ignoreRejections: boolean, ...args: any[]) {
return (result: any) => { return async (result: any) => {
return fn try {
.apply(null, args) const res = await fn(...args);
.then((res: any) => { return Array.prototype.concat(result, [res]);
return Array.prototype.concat(result, [res]); } catch (err) {
}) if (ignoreRejections) {
.catch((err: any) => { return result;
if (ignoreRejections) { }
return result;
}
throw err; throw err;
}); }
}; };
} }
private executeInOrder(tasks: any[], initialValue: any[]) { private executeInOrder(tasks: any[]) {
return tasks.reduce(this.$q.when, initialValue); 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 // Used for testing and things that really need BackendSrv
export function getBackendSrv(): BackendSrv { export const backendSrv = new BackendSrv();
return getBackendService() as 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 // TODO: Enable backend request when we have metrics API
// if (this.options.url) { // 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 impressionSrv from 'app/core/services/impression_srv';
import store from 'app/core/store'; import store from 'app/core/store';
import { contextSrv } from 'app/core/services/context_srv'; 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 { Section } from '../components/manage_dashboards/manage_dashboards';
import { DashboardSearchHit } from 'app/types/search'; import { DashboardSearchHit } from 'app/types/search';
@@ -16,8 +16,7 @@ export class SearchSrv {
recentIsOpen: boolean; recentIsOpen: boolean;
starredIsOpen: boolean; starredIsOpen: boolean;
/** @ngInject */ constructor() {
constructor(private backendSrv: BackendSrv) {
this.recentIsOpen = store.getBool('search.sections.recent', true); this.recentIsOpen = store.getBool('search.sections.recent', true);
this.starredIsOpen = store.getBool('search.sections.starred', true); this.starredIsOpen = store.getBool('search.sections.starred', true);
} }
@@ -44,7 +43,7 @@ export class SearchSrv {
return Promise.resolve([]); return Promise.resolve([]);
} }
return this.backendSrv.search({ dashboardIds: dashIds }).then(result => { return backendSrv.search({ dashboardIds: dashIds }).then(result => {
return dashIds return dashIds
.map(orderId => { .map(orderId => {
return _.find(result, { id: orderId }); return _.find(result, { id: orderId });
@@ -78,7 +77,7 @@ export class SearchSrv {
return Promise.resolve(); 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) { if (result.length > 0) {
sections['starred'] = { sections['starred'] = {
title: 'Starred', title: 'Starred',
@@ -116,7 +115,7 @@ export class SearchSrv {
} }
promises.push( promises.push(
this.backendSrv.search(query).then(results => { backendSrv.search(query).then(results => {
return this.handleSearchResult(sections, results); return this.handleSearchResult(sections, results);
}) })
); );
@@ -197,14 +196,14 @@ export class SearchSrv {
folderIds: [section.id], folderIds: [section.id],
}; };
return this.backendSrv.search(query).then(results => { return backendSrv.search(query).then(results => {
section.items = results; section.items = results;
return Promise.resolve(section); return Promise.resolve(section);
}); });
} }
getDashboardTags() { getDashboardTags() {
return this.backendSrv.get('/api/dashboards/tags'); return backendSrv.get('/api/dashboards/tags');
} }
} }

View File

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

View File

@@ -1,32 +1,532 @@
import angular from 'angular'; import { BackendSrv, getBackendSrv } from '../services/backend_srv';
import { BackendSrv } from 'app/core/services/backend_srv'; import { Emitter } from '../utils/emitter';
import { ContextSrv } from '../services/context_srv'; import { ContextSrv, User } from '../services/context_srv';
jest.mock('app/core/store'); import { Observable, of } from 'rxjs';
import { AppEvents } from '@grafana/data';
import { CoreEvents } from '../../types';
import { delay } from 'rxjs/operators';
describe('backend_srv', () => { const getTestContext = (overides?: object) => {
const _httpBackend = (options: any) => { const defaults = {
if (options.url === 'gateway-error') { data: { test: 'hello world' },
return Promise.reject({ status: 502 }); ok: true,
} status: 200,
return Promise.resolve({}); 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( const expectRequestCallChain = (options: any) => {
_httpBackend, expect(parseRequestOptionsMock).toHaveBeenCalledTimes(1);
{} as angular.IQService, expect(parseRequestOptionsMock).toHaveBeenCalledWith(options, 1337);
{} as angular.ITimeoutService, expectCallChain(options);
{} as ContextSrv };
);
describe('when handling errors', () => { const expectDataSourceRequestCallChain = (options: any) => {
it('should return the http status code', async () => { expect(parseDataSourceRequestOptionsMock).toHaveBeenCalledTimes(1);
try { expect(parseDataSourceRequestOptionsMock).toHaveBeenCalledWith(options, 1337, undefined);
await _backendSrv.datasourceRequest({ expectCallChain(options);
url: 'gateway-error', };
});
} catch (err) { return {
expect(err.status).toBe(502); 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, FoldersAndDashboardUids,
} from 'app/core/components/manage_dashboards/manage_dashboards'; } from 'app/core/components/manage_dashboards/manage_dashboards';
import { SearchSrv } from 'app/core/services/search_srv'; import { SearchSrv } from 'app/core/services/search_srv';
import { BackendSrv } from '../services/backend_srv';
import { ContextSrv } from '../services/context_srv'; import { ContextSrv } from '../services/context_srv';
const mockSection = (overides?: object): Section => { const mockSection = (overides?: object): Section => {
@@ -593,7 +592,6 @@ function createCtrlWithStubs(searchResponse: any, tags?: any) {
return new ManageDashboardsCtrl( return new ManageDashboardsCtrl(
{ $digest: jest.fn() } as any, { $digest: jest.fn() } as any,
{} as BackendSrv,
searchSrvStub as SearchSrv, searchSrvStub as SearchSrv,
{ isEditor: true } as ContextSrv { isEditor: true } as ContextSrv
); );

View File

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

View File

@@ -1,9 +1,8 @@
import { SearchSrv } from 'app/core/services/search_srv'; import { SearchSrv } from 'app/core/services/search_srv';
import { BackendSrvMock } from 'test/mocks/backend_srv';
import impressionSrv from 'app/core/services/impression_srv'; import impressionSrv from 'app/core/services/impression_srv';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { beforeEach } from 'test/lib/common'; import { beforeEach } from 'test/lib/common';
import { BackendSrv } from '../services/backend_srv'; import { backendSrv } from '../services/backend_srv';
jest.mock('app/core/store', () => { jest.mock('app/core/store', () => {
return { return {
@@ -19,29 +18,32 @@ jest.mock('app/core/services/impression_srv', () => {
}); });
describe('SearchSrv', () => { describe('SearchSrv', () => {
let searchSrv: SearchSrv, backendSrvMock: BackendSrvMock; let searchSrv: SearchSrv;
const searchMock = jest.spyOn(backendSrv, 'search'); // will use the mock in __mocks__
beforeEach(() => { beforeEach(() => {
backendSrvMock = new BackendSrvMock(); searchSrv = new SearchSrv();
searchSrv = new SearchSrv(backendSrvMock as BackendSrv);
contextSrv.isSignedIn = true; contextSrv.isSignedIn = true;
impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([]); impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([]);
jest.clearAllMocks();
}); });
describe('With recent dashboards', () => { describe('With recent dashboards', () => {
let results: any; let results: any;
beforeEach(() => { beforeEach(() => {
backendSrvMock.search = jest searchMock.mockImplementation(
.fn() jest
.mockReturnValueOnce( .fn()
Promise.resolve([ .mockReturnValueOnce(
{ id: 2, title: 'second but first' }, Promise.resolve([
{ id: 1, title: 'first but second' }, { id: 2, title: 'second but first' },
]) { id: 1, title: 'first but second' },
) ])
.mockReturnValue(Promise.resolve([])); )
.mockReturnValue(Promise.resolve([]))
);
impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([1, 2]); impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([1, 2]);
@@ -63,15 +65,17 @@ describe('SearchSrv', () => {
let results: any; let results: any;
beforeEach(() => { beforeEach(() => {
backendSrvMock.search = jest searchMock.mockImplementation(
.fn() jest
.mockReturnValueOnce( .fn()
Promise.resolve([ .mockReturnValueOnce(
{ id: 2, title: 'two' }, Promise.resolve([
{ id: 1, title: 'one' }, { id: 2, title: 'two' },
]) { id: 1, title: 'one' },
) ])
.mockReturnValue(Promise.resolve([])); )
.mockReturnValue(Promise.resolve([]))
);
impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([4, 5, 1, 2, 3]); impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([4, 5, 1, 2, 3]);
@@ -92,7 +96,7 @@ describe('SearchSrv', () => {
let results: any; let results: any;
beforeEach(() => { 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 => { return searchSrv.search({ query: '' }).then(res => {
results = res; results = res;
@@ -109,15 +113,17 @@ describe('SearchSrv', () => {
let results: any; let results: any;
beforeEach(() => { beforeEach(() => {
backendSrvMock.search = jest searchMock.mockImplementation(
.fn() jest
.mockReturnValueOnce( .fn()
Promise.resolve([ .mockReturnValueOnce(
{ id: 1, title: 'starred and recent', isStarred: true }, Promise.resolve([
{ id: 2, title: 'recent' }, { id: 1, title: 'starred and recent', isStarred: true },
]) { id: 2, title: 'recent' },
) ])
.mockReturnValue(Promise.resolve([{ id: 1, title: 'starred and recent' }])); )
.mockReturnValue(Promise.resolve([{ id: 1, title: 'starred and recent' }]))
);
impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([1, 2]); impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([1, 2]);
return searchSrv.search({ query: '' }).then(res => { return searchSrv.search({ query: '' }).then(res => {
@@ -140,35 +146,37 @@ describe('SearchSrv', () => {
let results: any; let results: any;
beforeEach(() => { beforeEach(() => {
backendSrvMock.search = jest searchMock.mockImplementation(
.fn() jest
.mockReturnValueOnce(Promise.resolve([])) .fn()
.mockReturnValue( .mockReturnValueOnce(Promise.resolve([]))
Promise.resolve([ .mockReturnValue(
{ Promise.resolve([
title: 'folder1', {
type: 'dash-folder', title: 'folder1',
id: 1, type: 'dash-folder',
}, id: 1,
{ },
title: 'dash with no folder', {
type: 'dash-db', title: 'dash with no folder',
id: 2, type: 'dash-db',
}, id: 2,
{ },
title: 'dash in folder1 1', {
type: 'dash-db', title: 'dash in folder1 1',
id: 3, type: 'dash-db',
folderId: 1, id: 3,
}, folderId: 1,
{ },
title: 'dash in folder1 2', {
type: 'dash-db', title: 'dash in folder1 2',
id: 4, type: 'dash-db',
folderId: 1, id: 4,
}, folderId: 1,
]) },
); ])
)
);
return searchSrv.search({ query: '' }).then(res => { return searchSrv.search({ query: '' }).then(res => {
results = res; results = res;
@@ -188,23 +196,25 @@ describe('SearchSrv', () => {
let results: any; let results: any;
beforeEach(() => { beforeEach(() => {
backendSrvMock.search = jest.fn().mockReturnValue( searchMock.mockImplementation(
Promise.resolve([ jest.fn().mockReturnValue(
{ Promise.resolve([
id: 2, {
title: 'dash with no folder', id: 2,
type: 'dash-db', title: 'dash with no folder',
}, type: 'dash-db',
{ },
id: 3, {
title: 'dash in folder1 1', id: 3,
type: 'dash-db', title: 'dash in folder1 1',
folderId: 1, type: 'dash-db',
folderUid: 'uid', folderId: 1,
folderTitle: 'folder1', folderUid: 'uid',
folderUrl: '/dashboards/f/uid/folder1', folderTitle: 'folder1',
}, folderUrl: '/dashboards/f/uid/folder1',
]) },
])
)
); );
return searchSrv.search({ query: 'search' }).then(res => { return searchSrv.search({ query: 'search' }).then(res => {
@@ -213,7 +223,7 @@ describe('SearchSrv', () => {
}); });
it('should not specify folder ids', () => { 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', () => { it('should group results by folder', () => {
@@ -228,27 +238,25 @@ describe('SearchSrv', () => {
describe('with tags', () => { describe('with tags', () => {
beforeEach(() => { beforeEach(() => {
backendSrvMock.search = jest.fn(); searchMock.mockImplementation(jest.fn().mockReturnValue(Promise.resolve([])));
backendSrvMock.search.mockReturnValue(Promise.resolve([]));
return searchSrv.search({ tag: ['atag'] }).then(() => {}); return searchSrv.search({ tag: ['atag'] }).then(() => {});
}); });
it('should send tags query to backend search', () => { 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', () => { describe('with starred', () => {
beforeEach(() => { beforeEach(() => {
backendSrvMock.search = jest.fn(); searchMock.mockImplementation(jest.fn().mockReturnValue(Promise.resolve([])));
backendSrvMock.search.mockReturnValue(Promise.resolve([]));
return searchSrv.search({ starred: true }).then(() => {}); return searchSrv.search({ starred: true }).then(() => {});
}); });
it('should send starred query to backend search', () => { 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; let getRecentDashboardsCalled = false;
beforeEach(() => { beforeEach(() => {
backendSrvMock.search = jest.fn(); searchMock.mockImplementation(jest.fn().mockReturnValue(Promise.resolve([])));
backendSrvMock.search.mockReturnValue(Promise.resolve([]));
searchSrv['getRecentDashboards'] = () => { searchSrv['getRecentDashboards'] = () => {
getRecentDashboardsCalled = true; getRecentDashboardsCalled = true;
@@ -276,8 +283,7 @@ describe('SearchSrv', () => {
let getStarredCalled = false; let getStarredCalled = false;
beforeEach(() => { beforeEach(() => {
backendSrvMock.search = jest.fn(); searchMock.mockImplementation(jest.fn().mockReturnValue(Promise.resolve([])));
backendSrvMock.search.mockReturnValue(Promise.resolve([]));
impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([]); impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([]);
searchSrv['getStarred'] = () => { 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 { NavModelSrv } from 'app/core/core';
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
export default class AdminEditOrgCtrl { export default class AdminEditOrgCtrl {
/** @ngInject */ /** @ngInject */
constructor($scope: any, $routeParams: any, backendSrv: BackendSrv, $location: any, navModelSrv: NavModelSrv) { constructor($scope: any, $routeParams: any, $location: any, navModelSrv: NavModelSrv) {
$scope.init = () => { $scope.init = () => {
$scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0); $scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0);
if ($routeParams.id) { if ($routeParams.id) {
$scope.getOrg($routeParams.id); promiseToDigest($scope)(Promise.all([$scope.getOrg($routeParams.id), $scope.getOrgUsers($routeParams.id)]));
$scope.getOrgUsers($routeParams.id);
} }
}; };
$scope.getOrg = (id: number) => { $scope.getOrg = (id: number) => {
backendSrv.get('/api/orgs/' + id).then((org: any) => { return getBackendSrv()
$scope.org = org; .get('/api/orgs/' + id)
}); .then((org: any) => {
$scope.org = org;
});
}; };
$scope.getOrgUsers = (id: number) => { $scope.getOrgUsers = (id: number) => {
backendSrv.get('/api/orgs/' + id + '/users').then((orgUsers: any) => { return getBackendSrv()
$scope.orgUsers = orgUsers; .get('/api/orgs/' + id + '/users')
}); .then((orgUsers: any) => {
$scope.orgUsers = orgUsers;
});
}; };
$scope.update = () => { $scope.update = () => {
@@ -30,19 +34,25 @@ export default class AdminEditOrgCtrl {
return; return;
} }
backendSrv.put('/api/orgs/' + $scope.org.id, $scope.org).then(() => { promiseToDigest($scope)(
$location.path('/admin/orgs'); getBackendSrv()
}); .put('/api/orgs/' + $scope.org.id, $scope.org)
.then(() => {
$location.path('/admin/orgs');
})
);
}; };
$scope.updateOrgUser = (orgUser: any) => { $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) => { $scope.removeOrgUser = (orgUser: any) => {
backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId).then(() => { promiseToDigest($scope)(
$scope.getOrgUsers($scope.org.id); getBackendSrv()
}); .delete('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId)
.then(() => $scope.getOrgUsers($scope.org.id))
);
}; };
$scope.init(); $scope.init();

View File

@@ -1,19 +1,14 @@
import _ from 'lodash'; import _ from 'lodash';
import { BackendSrv } from 'app/core/services/backend_srv'; import { getBackendSrv } from '@grafana/runtime';
import { NavModelSrv } from 'app/core/core'; import { NavModelSrv } from 'app/core/core';
import { User } from 'app/core/services/context_srv'; import { User } from 'app/core/services/context_srv';
import { UserSession, Scope, CoreEvents, AppEventEmitter } from 'app/types'; import { UserSession, Scope, CoreEvents, AppEventEmitter } from 'app/types';
import { dateTime } from '@grafana/data'; import { dateTime } from '@grafana/data';
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
export default class AdminEditUserCtrl { export default class AdminEditUserCtrl {
/** @ngInject */ /** @ngInject */
constructor( constructor($scope: Scope & AppEventEmitter, $routeParams: any, $location: any, navModelSrv: NavModelSrv) {
$scope: Scope & AppEventEmitter,
$routeParams: any,
backendSrv: BackendSrv,
$location: any,
navModelSrv: NavModelSrv
) {
$scope.user = {}; $scope.user = {};
$scope.sessions = []; $scope.sessions = [];
$scope.newOrg = { name: '', role: 'Editor' }; $scope.newOrg = { name: '', role: 'Editor' };
@@ -22,60 +17,74 @@ export default class AdminEditUserCtrl {
$scope.init = () => { $scope.init = () => {
if ($routeParams.id) { if ($routeParams.id) {
$scope.getUser($routeParams.id); promiseToDigest($scope)(
$scope.getUserSessions($routeParams.id); Promise.all([
$scope.getUserOrgs($routeParams.id); $scope.getUser($routeParams.id),
$scope.getUserSessions($routeParams.id),
$scope.getUserOrgs($routeParams.id),
])
);
} }
}; };
$scope.getUser = (id: number) => { $scope.getUser = (id: number) => {
backendSrv.get('/api/users/' + id).then((user: User) => { return getBackendSrv()
$scope.user = user; .get('/api/users/' + id)
$scope.user_id = id; .then((user: User) => {
$scope.permissions.isGrafanaAdmin = user.isGrafanaAdmin; $scope.user = user;
}); $scope.user_id = id;
$scope.permissions.isGrafanaAdmin = user.isGrafanaAdmin;
});
}; };
$scope.getUserSessions = (id: number) => { $scope.getUserSessions = (id: number) => {
backendSrv.get('/api/admin/users/' + id + '/auth-tokens').then((sessions: UserSession[]) => { return getBackendSrv()
sessions.reverse(); .get('/api/admin/users/' + id + '/auth-tokens')
.then((sessions: UserSession[]) => {
sessions.reverse();
$scope.sessions = sessions.map((session: UserSession) => { $scope.sessions = sessions.map((session: UserSession) => {
return { return {
id: session.id, id: session.id,
isActive: session.isActive, isActive: session.isActive,
seenAt: dateTime(session.seenAt).fromNow(), seenAt: dateTime(session.seenAt).fromNow(),
createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'), createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'),
clientIp: session.clientIp, clientIp: session.clientIp,
browser: session.browser, browser: session.browser,
browserVersion: session.browserVersion, browserVersion: session.browserVersion,
os: session.os, os: session.os,
osVersion: session.osVersion, osVersion: session.osVersion,
device: session.device, 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.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) => { $scope.revokeAllUserSessions = (tokenId: number) => {
backendSrv.post('/api/admin/users/' + $scope.user_id + '/logout').then(() => { promiseToDigest($scope)(
$scope.sessions = []; getBackendSrv()
}); .post('/api/admin/users/' + $scope.user_id + '/logout')
.then(() => {
$scope.sessions = [];
})
);
}; };
$scope.setPassword = () => { $scope.setPassword = () => {
@@ -84,15 +93,19 @@ export default class AdminEditUserCtrl {
} }
const payload = { password: $scope.password }; 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 = () => { $scope.updatePermissions = () => {
const payload = $scope.permissions; const payload = $scope.permissions;
getBackendSrv().put('/api/admin/users/' + $scope.user_id + '/permissions', payload);
backendSrv.put('/api/admin/users/' + $scope.user_id + '/permissions', payload);
}; };
$scope.create = () => { $scope.create = () => {
@@ -100,15 +113,21 @@ export default class AdminEditUserCtrl {
return; return;
} }
backendSrv.post('/api/admin/users', $scope.user).then(() => { promiseToDigest($scope)(
$location.path('/admin/users'); getBackendSrv()
}); .post('/api/admin/users', $scope.user)
.then(() => {
$location.path('/admin/users');
})
);
}; };
$scope.getUserOrgs = (id: number) => { $scope.getUserOrgs = (id: number) => {
backendSrv.get('/api/users/' + id + '/orgs').then((orgs: any) => { return getBackendSrv()
$scope.orgs = orgs; .get('/api/users/' + id + '/orgs')
}); .then((orgs: any) => {
$scope.orgs = orgs;
});
}; };
$scope.update = () => { $scope.update = () => {
@@ -116,20 +135,27 @@ export default class AdminEditUserCtrl {
return; return;
} }
backendSrv.put('/api/users/' + $scope.user_id, $scope.user).then(() => { promiseToDigest($scope)(
$location.path('/admin/users'); getBackendSrv()
}); .put('/api/users/' + $scope.user_id, $scope.user)
.then(() => {
$location.path('/admin/users');
})
);
}; };
$scope.updateOrgUser = (orgUser: { orgId: string }) => { $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 }) => { $scope.removeOrgUser = (orgUser: { orgId: string }) => {
backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id).then(() => { promiseToDigest($scope)(
$scope.getUser($scope.user_id); getBackendSrv()
$scope.getUserOrgs($scope.user_id); .delete('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id)
}); .then(() => Promise.all([$scope.getUser($scope.user_id), $scope.getUserOrgs($scope.user_id)]))
);
}; };
$scope.orgsSearchCache = []; $scope.orgsSearchCache = [];
@@ -140,10 +166,14 @@ export default class AdminEditUserCtrl {
return; return;
} }
backendSrv.get('/api/orgs', { query: '' }).then((result: any) => { promiseToDigest($scope)(
$scope.orgsSearchCache = result; getBackendSrv()
callback(_.map(result, 'name')); .get('/api/orgs', { query: '' })
}); .then((result: any) => {
$scope.orgsSearchCache = result;
callback(_.map(result, 'name'));
})
);
}; };
$scope.addOrgUser = () => { $scope.addOrgUser = () => {
@@ -161,10 +191,11 @@ export default class AdminEditUserCtrl {
$scope.newOrg.loginOrEmail = $scope.user.login; $scope.newOrg.loginOrEmail = $scope.user.login;
backendSrv.post('/api/orgs/' + orgInfo.id + '/users/', $scope.newOrg).then(() => { promiseToDigest($scope)(
$scope.getUser($scope.user_id); getBackendSrv()
$scope.getUserOrgs($scope.user_id); .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) => { $scope.deleteUser = (user: any) => {
@@ -174,9 +205,13 @@ export default class AdminEditUserCtrl {
icon: 'fa-trash', icon: 'fa-trash',
yesText: 'Delete', yesText: 'Delete',
onConfirm: () => { onConfirm: () => {
backendSrv.delete('/api/admin/users/' + user.id).then(() => { promiseToDigest($scope)(
$location.path('/admin/users'); 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'; 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(); $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 { NavModelSrv } from 'app/core/core';
import { Scope, CoreEvents, AppEventEmitter } from 'app/types'; import { Scope, CoreEvents, AppEventEmitter } from 'app/types';
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
export default class AdminListOrgsCtrl { export default class AdminListOrgsCtrl {
/** @ngInject */ /** @ngInject */
constructor($scope: Scope & AppEventEmitter, backendSrv: BackendSrv, navModelSrv: NavModelSrv) { constructor($scope: Scope & AppEventEmitter, navModelSrv: NavModelSrv) {
$scope.init = () => { $scope.init = async () => {
$scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0); $scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0);
$scope.getOrgs(); await $scope.getOrgs();
}; };
$scope.getOrgs = () => { $scope.getOrgs = async () => {
backendSrv.get('/api/orgs').then((orgs: any) => { const orgs = await promiseToDigest($scope)(getBackendSrv().get('/api/orgs'));
$scope.orgs = orgs; $scope.orgs = orgs;
});
}; };
$scope.deleteOrg = (org: any) => { $scope.deleteOrg = (org: any) => {
@@ -23,10 +23,9 @@ export default class AdminListOrgsCtrl {
text2: 'All dashboards for this organization will be removed!', text2: 'All dashboards for this organization will be removed!',
icon: 'fa-trash', icon: 'fa-trash',
yesText: 'Delete', yesText: 'Delete',
onConfirm: () => { onConfirm: async () => {
backendSrv.delete('/api/orgs/' + org.id).then(() => { await getBackendSrv().delete('/api/orgs/' + org.id);
$scope.getOrgs(); await $scope.getOrgs();
});
}, },
}); });
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
// Libaries // Libaries
import angular from 'angular'; import flattenDeep from 'lodash/flattenDeep';
import _ from 'lodash'; import cloneDeep from 'lodash/cloneDeep';
// Components // Components
import './editor_ctrl'; import './editor_ctrl';
@@ -11,25 +11,16 @@ import { dedupAnnotations } from './events_processing';
// Types // Types
import { DashboardModel } from '../dashboard/state/DashboardModel'; 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 { 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 { export class AnnotationsSrv {
globalAnnotationsPromise: any; globalAnnotationsPromise: any;
alertStatesPromise: any; alertStatesPromise: any;
datasourcePromises: any; datasourcePromises: any;
/** @ngInject */
constructor(
private $rootScope: GrafanaRootScope,
private datasourceSrv: DatasourceSrv,
private backendSrv: BackendSrv,
private timeSrv: TimeSrv
) {}
init(dashboard: DashboardModel) { init(dashboard: DashboardModel) {
// always clearPromiseCaches when loading new dashboard // always clearPromiseCaches when loading new dashboard
this.clearPromiseCaches(); this.clearPromiseCaches();
@@ -47,10 +38,10 @@ export class AnnotationsSrv {
return Promise.all([this.getGlobalAnnotations(options), this.getAlertStates(options)]) return Promise.all([this.getGlobalAnnotations(options), this.getAlertStates(options)])
.then(results => { .then(results => {
// combine the annotations and flatten 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 // 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 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') { if (item.panelId && item.source.type === 'dashboard') {
return item.panelId === options.panel.id; return item.panelId === options.panel.id;
@@ -61,7 +52,7 @@ export class AnnotationsSrv {
annotations = dedupAnnotations(annotations); annotations = dedupAnnotations(annotations);
// look for alert state for this panel // 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 { return {
annotations: annotations, annotations: annotations,
@@ -73,7 +64,7 @@ export class AnnotationsSrv {
err.message = err.data.message; err.message = err.data.message;
} }
console.log('AnnotationSrv.query error', err); 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 []; return [];
}); });
} }
@@ -96,7 +87,7 @@ export class AnnotationsSrv {
return this.alertStatesPromise; 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, dashboardId: options.dashboard.id,
}); });
return this.alertStatesPromise; return this.alertStatesPromise;
@@ -109,7 +100,7 @@ export class AnnotationsSrv {
return this.globalAnnotationsPromise; return this.globalAnnotationsPromise;
} }
const range = this.timeSrv.timeRange(); const range = getTimeSrv().timeRange();
const promises = []; const promises = [];
const dsPromises = []; const dsPromises = [];
@@ -121,7 +112,7 @@ export class AnnotationsSrv {
if (annotation.snapshotData) { if (annotation.snapshotData) {
return this.translateQueryResult(annotation, annotation.snapshotData); return this.translateQueryResult(annotation, annotation.snapshotData);
} }
const datasourcePromise = this.datasourceSrv.get(annotation.datasource); const datasourcePromise = getDataSourceSrv().get(annotation.datasource);
dsPromises.push(datasourcePromise); dsPromises.push(datasourcePromise);
promises.push( promises.push(
datasourcePromise datasourcePromise
@@ -137,7 +128,7 @@ export class AnnotationsSrv {
.then(results => { .then(results => {
// store response in annotation object if this is a snapshot call // store response in annotation object if this is a snapshot call
if (dashboard.snapshot) { if (dashboard.snapshot) {
annotation.snapshotData = angular.copy(results); annotation.snapshotData = cloneDeep(results);
} }
// translate result // translate result
return this.translateQueryResult(annotation, results); return this.translateQueryResult(annotation, results);
@@ -151,26 +142,26 @@ export class AnnotationsSrv {
saveAnnotationEvent(annotation: AnnotationEvent) { saveAnnotationEvent(annotation: AnnotationEvent) {
this.globalAnnotationsPromise = null; this.globalAnnotationsPromise = null;
return this.backendSrv.post('/api/annotations', annotation); return getBackendSrv().post('/api/annotations', annotation);
} }
updateAnnotationEvent(annotation: AnnotationEvent) { updateAnnotationEvent(annotation: AnnotationEvent) {
this.globalAnnotationsPromise = null; this.globalAnnotationsPromise = null;
return this.backendSrv.put(`/api/annotations/${annotation.id}`, annotation); return getBackendSrv().put(`/api/annotations/${annotation.id}`, annotation);
} }
deleteAnnotationEvent(annotation: AnnotationEvent) { deleteAnnotationEvent(annotation: AnnotationEvent) {
this.globalAnnotationsPromise = null; this.globalAnnotationsPromise = null;
const deleteUrl = `/api/annotations/${annotation.id}`; const deleteUrl = `/api/annotations/${annotation.id}`;
return this.backendSrv.delete(deleteUrl); return getBackendSrv().delete(deleteUrl);
} }
translateQueryResult(annotation: any, results: any) { translateQueryResult(annotation: any, results: any) {
// if annotation has snapshotData // if annotation has snapshotData
// make clone and remove it // make clone and remove it
if (annotation.snapshotData) { if (annotation.snapshotData) {
annotation = angular.copy(annotation); annotation = cloneDeep(annotation);
delete annotation.snapshotData; delete annotation.snapshotData;
} }

View File

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

View File

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

View File

@@ -2,11 +2,12 @@ import angular from 'angular';
import _ from 'lodash'; import _ from 'lodash';
import { iconMap } from './DashLinksEditorCtrl'; import { iconMap } from './DashLinksEditorCtrl';
import { LinkSrv } from 'app/features/panel/panellinks/link_srv'; 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 { DashboardSrv } from '../../services/DashboardSrv';
import { PanelEvents } from '@grafana/data'; import { PanelEvents } from '@grafana/data';
import { CoreEvents } from 'app/types'; import { CoreEvents } from 'app/types';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
export type DashboardLink = { tags: any; target: string; keepTime: any; includeVars: any }; 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 { export class DashLinksContainerCtrl {
/** @ngInject */ /** @ngInject */
constructor( constructor($scope: any, $rootScope: GrafanaRootScope, dashboardSrv: DashboardSrv, linkSrv: LinkSrv) {
$scope: any,
$rootScope: GrafanaRootScope,
backendSrv: BackendSrv,
dashboardSrv: DashboardSrv,
linkSrv: LinkSrv
) {
const currentDashId = dashboardSrv.getCurrent().id; const currentDashId = dashboardSrv.getCurrent().id;
function buildLinks(linkDef: any) { function buildLinks(linkDef: any) {
@@ -154,26 +149,28 @@ export class DashLinksContainerCtrl {
} }
$scope.searchDashboards = (link: DashboardLink, limit: any) => { $scope.searchDashboards = (link: DashboardLink, limit: any) => {
return backendSrv.search({ tag: link.tags, limit: limit }).then(results => { return promiseToDigest($scope)(
return _.reduce( backendSrv.search({ tag: link.tags, limit: limit }).then(results => {
results, return _.reduce(
(memo, dash) => { results,
// do not add current dashboard (memo, dash) => {
if (dash.id !== currentDashId) { // do not add current dashboard
memo.push({ if (dash.id !== currentDashId) {
title: dash.title, memo.push({
url: dash.url, title: dash.title,
target: link.target === '_self' ? '' : link.target, url: dash.url,
icon: 'fa fa-th-large', target: link.target === '_self' ? '' : link.target,
keepTime: link.keepTime, icon: 'fa fa-th-large',
includeVars: link.includeVars, keepTime: link.keepTime,
}); includeVars: link.includeVars,
} });
return memo; }
}, return memo;
[] },
); []
}); );
})
);
}; };
$scope.fillDropdown = (link: { searchHits: any }) => { $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 'jquery';
import _ from 'lodash'; 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 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 { DashboardSrv } from '../../services/DashboardSrv';
import { CoreEvents } from 'app/types'; import { CoreEvents } from 'app/types';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { AppEvents } from '@grafana/data'; import { AppEvents } from '@grafana/data';
import { e2e } from '@grafana/e2e'; import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
export class SettingsCtrl { export class SettingsCtrl {
dashboard: DashboardModel; dashboard: DashboardModel;
@@ -26,11 +28,10 @@ export class SettingsCtrl {
/** @ngInject */ /** @ngInject */
constructor( constructor(
private $scope: any, private $scope: IScope & Record<string, any>,
private $route: any, private $route: any,
private $location: ILocationService, private $location: ILocationService,
private $rootScope: GrafanaRootScope, private $rootScope: GrafanaRootScope,
private backendSrv: BackendSrv,
private dashboardSrv: DashboardSrv private dashboardSrv: DashboardSrv
) { ) {
// temp hack for annotations and variables editors // temp hack for annotations and variables editors
@@ -234,10 +235,12 @@ export class SettingsCtrl {
} }
deleteDashboardConfirmed() { deleteDashboardConfirmed() {
this.backendSrv.deleteDashboard(this.dashboard.uid, false).then(() => { promiseToDigest(this.$scope)(
appEvents.emit(AppEvents.alertSuccess, ['Dashboard Deleted', this.dashboard.title + ' has been deleted']); backendSrv.deleteDashboard(this.dashboard.uid, false).then(() => {
this.$location.url('/'); appEvents.emit(AppEvents.alertSuccess, ['Dashboard Deleted', this.dashboard.title + ' has been deleted']);
}); this.$location.url('/');
})
);
} }
onFolderChange(folder: { id: number; title: string }) { onFolderChange(folder: { id: number; title: string }) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import _ from 'lodash'; import _ from 'lodash';
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import { DashboardModel } from '../../state/DashboardModel'; import { DashboardModel } from '../../state/DashboardModel';
import { BackendSrv } from 'app/core/services/backend_srv'; import { getBackendSrv } from '@grafana/runtime';
export interface HistoryListOpts { export interface HistoryListOpts {
limit: number; limit: number;
@@ -32,23 +32,20 @@ export interface DiffTarget {
} }
export class HistorySrv { export class HistorySrv {
/** @ngInject */
constructor(private backendSrv: BackendSrv) {}
getHistoryList(dashboard: DashboardModel, options: HistoryListOpts) { getHistoryList(dashboard: DashboardModel, options: HistoryListOpts) {
const id = dashboard && dashboard.id ? dashboard.id : void 0; 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) { 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) { restoreDashboard(dashboard: DashboardModel, version: number) {
const id = dashboard && dashboard.id ? dashboard.id : void 0; const id = dashboard && dashboard.id ? dashboard.id : void 0;
const url = `api/dashboards/id/${id}/restore`; 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'; import { QueryEditorRows } from './QueryEditorRows';
// Services // Services
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; 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'; import config from 'app/core/config';
// Types // Types
import { PanelModel } from '../state/PanelModel'; import { PanelModel } from '../state/PanelModel';
@@ -48,7 +48,7 @@ interface State {
export class QueriesTab extends PureComponent<Props, State> { export class QueriesTab extends PureComponent<Props, State> {
datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources(); datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources();
backendSrv = getBackendSrv(); backendSrv = backendSrv;
querySubscription: Unsubscribable; querySubscription: Unsubscribable;
state: State = { state: State = {

View File

@@ -6,7 +6,7 @@ import $ from 'jquery';
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
import { dateMath, AppEvents } from '@grafana/data'; import { dateMath, AppEvents } from '@grafana/data';
import impressionSrv from 'app/core/services/impression_srv'; 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 { DashboardSrv } from './DashboardSrv';
import DatasourceSrv from 'app/features/plugins/datasource_srv'; import DatasourceSrv from 'app/features/plugins/datasource_srv';
import { UrlQueryValue } from '@grafana/runtime'; import { UrlQueryValue } from '@grafana/runtime';
@@ -15,7 +15,6 @@ import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
export class DashboardLoaderSrv { export class DashboardLoaderSrv {
/** @ngInject */ /** @ngInject */
constructor( constructor(
private backendSrv: BackendSrv,
private dashboardSrv: DashboardSrv, private dashboardSrv: DashboardSrv,
private datasourceSrv: DatasourceSrv, private datasourceSrv: DatasourceSrv,
private $http: any, private $http: any,
@@ -46,11 +45,11 @@ export class DashboardLoaderSrv {
if (type === 'script') { if (type === 'script') {
promise = this._loadScriptedDashboard(slug); promise = this._loadScriptedDashboard(slug);
} else if (type === 'snapshot') { } 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); return this._dashboardLoadFailed('Snapshot not found', true);
}); });
} else { } else {
promise = this.backendSrv promise = backendSrv
.getDashboardByUid(uid) .getDashboardByUid(uid)
.then((result: any) => { .then((result: any) => {
if (result.meta.isFolder) { 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 coreModule from 'app/core/core_module';
import { appEvents } from 'app/core/app_events'; import { appEvents } from 'app/core/app_events';
import locationUtil from 'app/core/utils/location_util'; import locationUtil from 'app/core/utils/location_util';
import { DashboardModel } from '../state/DashboardModel'; import { DashboardModel } from '../state/DashboardModel';
import { removePanel } from '../utils/panel'; import { removePanel } from '../utils/panel';
import { DashboardMeta, CoreEvents } from 'app/types'; import { CoreEvents, DashboardMeta } from 'app/types';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { BackendSrv } from 'app/core/services/backend_srv'; import { backendSrv } from 'app/core/services/backend_srv';
import { ILocationService } from 'angular'; import { promiseToDigest } from '../../../core/utils/promiseToDigest';
import { AppEvents } from '@grafana/data';
import { PanelEvents } from '@grafana/data';
interface DashboardSaveOptions { interface DashboardSaveOptions {
folderId?: number; folderId?: number;
@@ -21,11 +22,7 @@ export class DashboardSrv {
dashboard: DashboardModel; dashboard: DashboardModel;
/** @ngInject */ /** @ngInject */
constructor( constructor(private $rootScope: GrafanaRootScope, private $location: ILocationService) {
private backendSrv: BackendSrv,
private $rootScope: GrafanaRootScope,
private $location: ILocationService
) {
appEvents.on(CoreEvents.saveDashboard, this.saveDashboard.bind(this), $rootScope); appEvents.on(CoreEvents.saveDashboard, this.saveDashboard.bind(this), $rootScope);
appEvents.on(PanelEvents.panelChangeView, this.onPanelChangeView); appEvents.on(PanelEvents.panelChangeView, this.onPanelChangeView);
appEvents.on(CoreEvents.removePanel, this.onRemovePanel); appEvents.on(CoreEvents.removePanel, this.onRemovePanel);
@@ -167,10 +164,12 @@ export class DashboardSrv {
save(clone: any, options?: DashboardSaveOptions) { save(clone: any, options?: DashboardSaveOptions) {
options.folderId = options.folderId >= 0 ? options.folderId : this.dashboard.meta.folderId || clone.folderId; options.folderId = options.folderId >= 0 ? options.folderId : this.dashboard.meta.folderId || clone.folderId;
return this.backendSrv return promiseToDigest(this.$rootScope)(
.saveDashboard(clone, options) backendSrv
.then((data: any) => this.postSave(data)) .saveDashboard(clone, options)
.catch(this.handleSaveDashboardError.bind(this, clone, { folderId: options.folderId })); .then((data: any) => this.postSave(data))
.catch(this.handleSaveDashboardError.bind(this, clone, { folderId: options.folderId }))
);
} }
saveDashboard( saveDashboard(
@@ -228,13 +227,17 @@ export class DashboardSrv {
let promise; let promise;
if (isStarred) { if (isStarred) {
promise = this.backendSrv.delete('/api/user/stars/dashboard/' + dashboardId).then(() => { promise = promiseToDigest(this.$rootScope)(
return false; backendSrv.delete('/api/user/stars/dashboard/' + dashboardId).then(() => {
}); return false;
})
);
} else { } else {
promise = this.backendSrv.post('/api/user/stars/dashboard/' + dashboardId).then(() => { promise = promiseToDigest(this.$rootScope)(
return true; backendSrv.post('/api/user/stars/dashboard/' + dashboardId).then(() => {
}); return true;
})
);
} }
return promise.then((res: boolean) => { return promise.then((res: boolean) => {

View File

@@ -1,6 +1,6 @@
// Services & Utils // Services & Utils
import { createErrorNotification } from 'app/core/copy/appNotification'; 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 { DashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { DashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { DashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
@@ -38,7 +38,7 @@ export interface InitDashboardArgs {
} }
async function redirectToNewUrl(slug: string, dispatch: ThunkDispatch, currentPath: string) { async function redirectToNewUrl(slug: string, dispatch: ThunkDispatch, currentPath: string) {
const res = await getBackendSrv().getDashboardBySlug(slug); const res = await backendSrv.getDashboardBySlug(slug);
if (res) { if (res) {
let newUrl = res.meta.url; let newUrl = res.meta.url;
@@ -62,7 +62,7 @@ async function fetchDashboard(
switch (args.routeInfo) { switch (args.routeInfo) {
case DashboardRouteInfo.Home: { case DashboardRouteInfo.Home: {
// load home dash // 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 user specified a custom home dashboard redirect to that
if (dashDTO.redirectUri) { 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 { flatten, map as lodashMap, isArray, isString } from 'lodash';
import { map, catchError, takeUntil, mapTo, share, finalize, tap } from 'rxjs/operators'; import { map, catchError, takeUntil, mapTo, share, finalize, tap } from 'rxjs/operators';
// Utils & Services // Utils & Services
import { getBackendSrv } from 'app/core/services/backend_srv'; import { backendSrv } from 'app/core/services/backend_srv';
// Types // Types
import { import {
DataSourceApi, DataSourceApi,
@@ -136,7 +136,7 @@ export function runRequest(datasource: DataSourceApi, request: DataQueryRequest)
function cancelNetworkRequestsOnUnsubscribe(req: DataQueryRequest) { function cancelNetworkRequestsOnUnsubscribe(req: DataQueryRequest) {
return () => { return () => {
getBackendSrv().resolveCancelerIfExists(req.requestId); backendSrv.resolveCancelerIfExists(req.requestId);
}; };
} }

View File

@@ -11,7 +11,7 @@ import BasicSettings from './BasicSettings';
import ButtonRow from './ButtonRow'; import ButtonRow from './ButtonRow';
// Services & Utils // Services & Utils
import appEvents from 'app/core/app_events'; 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'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
// Actions & selectors // Actions & selectors
import { getDataSource, getDataSourceMeta } from '../state/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' }); this.setState({ isTesting: true, testingMessage: 'Testing...', testingStatus: 'info' });
getBackendSrv().withNoBackendCache(async () => { backendSrv.withNoBackendCache(async () => {
try { try {
const result = await dsApi.testDatasource(); 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 appEvents from 'app/core/app_events';
import locationUtil from 'app/core/utils/location_util'; import locationUtil from 'app/core/utils/location_util';
import { BackendSrv } from 'app/core/services/backend_srv'; import { backendSrv } from 'app/core/services/backend_srv';
import { ILocationService } from 'angular';
import { ValidationSrv } from 'app/features/manage-dashboards'; import { ValidationSrv } from 'app/features/manage-dashboards';
import { NavModelSrv } from 'app/core/nav_model_srv'; import { NavModelSrv } from 'app/core/nav_model_srv';
import { AppEvents } from '@grafana/data'; import { promiseToDigest } from '../../core/utils/promiseToDigest';
export default class CreateFolderCtrl { export default class CreateFolderCtrl {
title = ''; title = '';
@@ -15,10 +17,10 @@ export default class CreateFolderCtrl {
/** @ngInject */ /** @ngInject */
constructor( constructor(
private backendSrv: BackendSrv,
private $location: ILocationService, private $location: ILocationService,
private validationSrv: ValidationSrv, private validationSrv: ValidationSrv,
navModelSrv: NavModelSrv navModelSrv: NavModelSrv,
private $scope: IScope
) { ) {
this.navModel = navModelSrv.getNav('dashboards', 'manage-dashboards', 0); this.navModel = navModelSrv.getNav('dashboards', 'manage-dashboards', 0);
} }
@@ -28,23 +30,27 @@ export default class CreateFolderCtrl {
return; return;
} }
return this.backendSrv.createFolder({ title: this.title }).then((result: any) => { promiseToDigest(this.$scope)(
appEvents.emit(AppEvents.alertSuccess, ['Folder Created', 'OK']); backendSrv.createFolder({ title: this.title }).then((result: any) => {
this.$location.url(locationUtil.stripBaseFromUrl(result.url)); appEvents.emit(AppEvents.alertSuccess, ['Folder Created', 'OK']);
}); this.$location.url(locationUtil.stripBaseFromUrl(result.url));
})
);
} }
titleChanged() { titleChanged() {
this.titleTouched = true; this.titleTouched = true;
this.validationSrv promiseToDigest(this.$scope)(
.validateNewFolderName(this.title) this.validationSrv
.then(() => { .validateNewFolderName(this.title)
this.hasValidationError = false; .then(() => {
}) this.hasValidationError = false;
.catch(err => { })
this.hasValidationError = true; .catch(err => {
this.validationError = err.message; this.hasValidationError = true;
}); this.validationError = err.message;
})
);
} }
} }

View File

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

View File

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

View File

@@ -1,11 +1,14 @@
import { DashboardImportCtrl } from './DashboardImportCtrl'; import { DashboardImportCtrl } from './DashboardImportCtrl';
import config from 'app/core/config'; import config from 'app/core/config';
import { backendSrv } from 'app/core/services/backend_srv';
describe('DashboardImportCtrl', () => { describe('DashboardImportCtrl', () => {
const ctx: any = {}; 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 navModelSrv: any;
let backendSrv: any;
let validationSrv: any; let validationSrv: any;
beforeEach(() => { beforeEach(() => {
@@ -13,17 +16,13 @@ describe('DashboardImportCtrl', () => {
getNav: () => {}, getNav: () => {},
}; };
backendSrv = {
search: jest.fn().mockReturnValue(Promise.resolve([])),
getDashboardByUid: jest.fn().mockReturnValue(Promise.resolve([])),
get: jest.fn(),
};
validationSrv = { validationSrv = {
validateNewDashboardName: jest.fn().mockReturnValue(Promise.resolve()), 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', () => { describe('when uploading json', () => {
@@ -61,16 +60,12 @@ describe('DashboardImportCtrl', () => {
beforeEach(() => { beforeEach(() => {
ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123'; ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123';
// setup api mock // setup api mock
backendSrv.get = jest.fn(() => { getMock.mockImplementation(() => Promise.resolve({ json: {} }));
return Promise.resolve({
json: {},
});
});
return ctx.ctrl.checkGnetDashboard(); return ctx.ctrl.checkGnetDashboard();
}); });
it('should call gnet api with correct dashboard id', () => { 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(() => { beforeEach(() => {
ctx.ctrl.gnetUrl = '2342'; ctx.ctrl.gnetUrl = '2342';
// setup api mock // setup api mock
backendSrv.get = jest.fn(() => { getMock.mockImplementation(() => Promise.resolve({ json: {} }));
return Promise.resolve({
json: {},
});
});
return ctx.ctrl.checkGnetDashboard(); return ctx.ctrl.checkGnetDashboard();
}); });
it('should call gnet api with correct dashboard id', () => { 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 _ from 'lodash';
import config from 'app/core/config'; import config from 'app/core/config';
import locationUtil from 'app/core/utils/location_util'; import locationUtil from 'app/core/utils/location_util';
import { BackendSrv } from '@grafana/runtime';
import { ValidationSrv } from './services/ValidationSrv'; import { ValidationSrv } from './services/ValidationSrv';
import { NavModelSrv } from 'app/core/core'; import { NavModelSrv } from 'app/core/core';
import { ILocationService } from 'angular'; import { ILocationService } from 'angular';
import { backendSrv } from 'app/core/services/backend_srv';
export class DashboardImportCtrl { export class DashboardImportCtrl {
navModel: any; navModel: any;
@@ -32,7 +32,6 @@ export class DashboardImportCtrl {
/** @ngInject */ /** @ngInject */
constructor( constructor(
private backendSrv: BackendSrv,
private validationSrv: ValidationSrv, private validationSrv: ValidationSrv,
navModelSrv: NavModelSrv, navModelSrv: NavModelSrv,
private $location: ILocationService, private $location: ILocationService,
@@ -145,7 +144,7 @@ export class DashboardImportCtrl {
return; return;
} }
this.backendSrv backendSrv
// @ts-ignore // @ts-ignore
.getDashboardByUid(this.dash.uid) .getDashboardByUid(this.dash.uid)
.then((res: any) => { .then((res: any) => {
@@ -185,7 +184,7 @@ export class DashboardImportCtrl {
}; };
}); });
return this.backendSrv return backendSrv
.post('api/dashboards/import', { .post('api/dashboards/import', {
dashboard: this.dash, dashboard: this.dash,
overwrite: true, overwrite: true,
@@ -224,7 +223,7 @@ export class DashboardImportCtrl {
this.gnetError = 'Could not find dashboard'; this.gnetError = 'Could not find dashboard';
} }
return this.backendSrv return backendSrv
.get('api/gnet/dashboards/' + dashboardId) .get('api/gnet/dashboards/' + dashboardId)
.then(res => { .then(res => {
this.gnetInfo = res; this.gnetInfo = res;

View File

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

View File

@@ -1,6 +1,6 @@
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events'; 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'; import { AppEvents } from '@grafana/data';
export class MoveToFolderCtrl { export class MoveToFolderCtrl {
@@ -10,15 +10,12 @@ export class MoveToFolderCtrl {
afterSave: any; afterSave: any;
isValidFolderSelection = true; isValidFolderSelection = true;
/** @ngInject */
constructor(private backendSrv: BackendSrv) {}
onFolderChange(folder: any) { onFolderChange(folder: any) {
this.folder = folder; this.folder = folder;
} }
save() { 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) { if (result.successCount > 0) {
const header = `Dashboard${result.successCount === 1 ? '' : 's'} Moved`; const header = `Dashboard${result.successCount === 1 ? '' : 's'} Moved`;
const msg = `${result.successCount} dashboard${result.successCount === 1 ? '' : 's'} moved to ${ 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 coreModule from 'app/core/core_module';
import { BackendSrv } from 'app/core/services/backend_srv'; import { backendSrv } from 'app/core/services/backend_srv';
const hitTypes = { const hitTypes = {
FOLDER: 'dash-folder', FOLDER: 'dash-folder',
@@ -9,9 +9,6 @@ const hitTypes = {
export class ValidationSrv { export class ValidationSrv {
rootName = 'general'; rootName = 'general';
/** @ngInject */
constructor(private backendSrv: BackendSrv) {}
validateNewDashboardName(folderId: any, name: string) { validateNewDashboardName(folderId: any, name: string) {
return this.validate(folderId, name, 'A dashboard in this folder with the same name already exists'); 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 = []; const promises = [];
promises.push(this.backendSrv.search({ type: hitTypes.FOLDER, folderIds: [folderId], query: name })); promises.push(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.DASHBOARD, folderIds: [folderId], query: name }));
return Promise.all(promises).then(res => { return Promise.all(promises).then(res => {
let hits: any[] = []; let hits: any[] = [];

View File

@@ -1,20 +1,24 @@
import angular from 'angular'; import angular from 'angular';
import config from 'app/core/config'; 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'; import { NavModelSrv } from 'app/core/core';
export class NewOrgCtrl { export class NewOrgCtrl {
/** @ngInject */ /** @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.navModel = navModelSrv.getNav('admin', 'global-orgs', 0);
$scope.newOrg = { name: '' }; $scope.newOrg = { name: '' };
$scope.createOrg = () => { $scope.createOrg = () => {
backendSrv.post('/api/orgs/', $scope.newOrg).then((result: any) => { getBackendSrv()
backendSrv.post('/api/user/using/' + result.orgId).then(() => { .post('/api/orgs/', $scope.newOrg)
window.location.href = config.appSubUrl + '/org'; .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 angular from 'angular';
import config from 'app/core/config'; 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 { export class SelectOrgCtrl {
/** @ngInject */ /** @ngInject */
constructor($scope: any, backendSrv: BackendSrv, contextSrv: any) { constructor($scope: any, contextSrv: any) {
contextSrv.sidemenu = false; contextSrv.sidemenu = false;
$scope.navModel = { $scope.navModel = {
@@ -20,15 +21,21 @@ export class SelectOrgCtrl {
}; };
$scope.getUserOrgs = () => { $scope.getUserOrgs = () => {
backendSrv.get('/api/user/orgs').then((orgs: any) => { promiseToDigest($scope)(
$scope.orgs = orgs; getBackendSrv()
}); .get('/api/user/orgs')
.then((orgs: any) => {
$scope.orgs = orgs;
})
);
}; };
$scope.setUsingOrg = (org: any) => { $scope.setUsingOrg = (org: any) => {
backendSrv.post('/api/user/using/' + org.orgId).then(() => { getBackendSrv()
window.location.href = config.appSubUrl + '/'; .post('/api/user/using/' + org.orgId)
}); .then(() => {
window.location.href = config.appSubUrl + '/';
});
}; };
$scope.init(); $scope.init();

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,8 @@
import { IScope, ITimeoutService } from 'angular';
import coreModule from '../../core/core_module'; 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 { export class PlaylistSearchCtrl {
query: any; query: any;
@@ -8,7 +11,7 @@ export class PlaylistSearchCtrl {
searchStarted: any; searchStarted: any;
/** @ngInject */ /** @ngInject */
constructor($timeout: any, private backendSrv: BackendSrv) { constructor(private $scope: IScope, $timeout: ITimeoutService) {
this.query = { query: '', tag: [], starred: false, limit: 20 }; this.query = { query: '', tag: [], starred: false, limit: 20 };
$timeout(() => { $timeout(() => {
@@ -22,12 +25,14 @@ export class PlaylistSearchCtrl {
this.tagsMode = false; this.tagsMode = false;
const prom: any = {}; const prom: any = {};
prom.promise = this.backendSrv.search(this.query).then(result => { prom.promise = promiseToDigest(this.$scope)(
return { backendSrv.search(this.query).then(result => {
dashboardResult: result, return {
tagResult: [], dashboardResult: result,
}; tagResult: [],
}); };
})
);
this.searchStarted(prom); this.searchStarted(prom);
} }
@@ -52,12 +57,14 @@ export class PlaylistSearchCtrl {
getTags() { getTags() {
const prom: any = {}; const prom: any = {};
prom.promise = this.backendSrv.get('/api/dashboards/tags').then((result: any) => { prom.promise = promiseToDigest(this.$scope)(
return { backendSrv.get('/api/dashboards/tags').then((result: any) => {
dashboardResult: [], return {
tagResult: result, dashboardResult: [],
} as any; tagResult: result,
}); } as any;
})
);
this.searchStarted(prom); 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 kbn from 'app/core/utils/kbn';
import { store } from 'app/store/store'; import { store } from 'app/store/store';
import { CoreEvents } from 'app/types'; import { CoreEvents } from 'app/types';
import { getBackendSrv } from '@grafana/runtime';
export const queryParamsToPreserve: { [key: string]: boolean } = { export const queryParamsToPreserve: { [key: string]: boolean } = {
kiosk: true, kiosk: true,
@@ -28,7 +29,7 @@ export class PlaylistSrv {
isPlaying: boolean; isPlaying: boolean;
/** @ngInject */ /** @ngInject */
constructor(private $location: any, private $timeout: any, private backendSrv: any) {} constructor(private $location: any, private $timeout: any) {}
next() { next() {
this.$timeout.cancel(this.cancelPromise); this.$timeout.cancel(this.cancelPromise);
@@ -89,13 +90,17 @@ export class PlaylistSrv {
appEvents.emit(CoreEvents.playlistStarted); appEvents.emit(CoreEvents.playlistStarted);
return this.backendSrv.get(`/api/playlists/${playlistId}`).then((playlist: any) => { return getBackendSrv()
return this.backendSrv.get(`/api/playlists/${playlistId}/dashboards`).then((dashboards: any) => { .get(`/api/playlists/${playlistId}`)
this.dashboards = dashboards; .then((playlist: any) => {
this.interval = kbn.interval_to_ms(playlist.interval); return getBackendSrv()
this.next(); .get(`/api/playlists/${playlistId}/dashboards`)
.then((dashboards: any) => {
this.dashboards = dashboards;
this.interval = kbn.interval_to_ms(playlist.interval);
this.next();
});
}); });
});
} }
stop() { stop() {

View File

@@ -1,37 +1,47 @@
import { IScope } from 'angular';
import _ from 'lodash'; 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 { 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 { export class PlaylistsCtrl {
playlists: any; playlists: any;
navModel: any; navModel: any;
/** @ngInject */ /** @ngInject */
constructor(private $scope: any, private backendSrv: BackendSrv, navModelSrv: NavModelSrv) { constructor(private $scope: IScope & AppEventEmitter, navModelSrv: NavModelSrv) {
this.navModel = navModelSrv.getNav('dashboards', 'playlists', 0); this.navModel = navModelSrv.getNav('dashboards', 'playlists', 0);
promiseToDigest($scope)(
backendSrv.get('/api/playlists').then((result: any) => { getBackendSrv()
this.playlists = result.map((item: any) => { .get('/api/playlists')
item.startUrl = `playlists/play/${item.id}`; .then((result: any) => {
return item; this.playlists = result.map((item: any) => {
}); item.startUrl = `playlists/play/${item.id}`;
}); return item;
});
})
);
} }
removePlaylistConfirmed(playlist: any) { removePlaylistConfirmed(playlist: any) {
_.remove(this.playlists, { id: playlist.id }); _.remove(this.playlists, { id: playlist.id });
this.backendSrv.delete('/api/playlists/' + playlist.id).then( promiseToDigest(this.$scope)(
() => { getBackendSrv()
this.$scope.appEvent(AppEvents.alertSuccess, ['Playlist deleted']); .delete('/api/playlists/' + playlist.id)
}, .then(
() => { () => {
this.$scope.appEvent(AppEvents.alertError, ['Unable to delete playlist']); this.$scope.appEvent(AppEvents.alertSuccess, ['Playlist deleted']);
this.playlists.push(playlist); },
} () => {
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 = [ ctx.dashboardresult = [
{ id: 2, title: 'dashboard: 2' }, { id: 2, title: 'dashboard: 2' },

View File

@@ -3,6 +3,18 @@ import configureMockStore from 'redux-mock-store';
import { PlaylistSrv } from '../playlist_srv'; import { PlaylistSrv } from '../playlist_srv';
import { setStore } from 'app/store/store'; 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>(); const mockStore = configureMockStore<any, any>();
setStore( setStore(
@@ -14,19 +26,6 @@ setStore(
const dashboards = [{ url: 'dash1' }, { url: 'dash2' }]; const dashboards = [{ url: 'dash1' }, { url: 'dash2' }];
const createPlaylistSrv = (): [PlaylistSrv, { url: jest.MockInstance<any, any> }] => { 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 = { const mockLocation = {
url: jest.fn(), url: jest.fn(),
search: () => ({}), search: () => ({}),
@@ -36,7 +35,7 @@ const createPlaylistSrv = (): [PlaylistSrv, { url: jest.MockInstance<any, any> }
const mockTimeout = jest.fn(); const mockTimeout = jest.fn();
(mockTimeout as any).cancel = 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] => { const mockWindowLocation = (): [jest.MockInstance<any, any>, () => void] => {
@@ -67,6 +66,20 @@ describe('PlaylistSrv', () => {
const initialUrl = 'http://localhost/playlist'; const initialUrl = 'http://localhost/playlist';
beforeEach(() => { 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(); [srv] = createPlaylistSrv();
[hrefMock, unmockLocation] = mockWindowLocation(); [hrefMock, unmockLocation] = mockWindowLocation();

View File

@@ -3,7 +3,7 @@ import React, { PureComponent } from 'react';
import extend from 'lodash/extend'; import extend from 'lodash/extend';
import { PluginDashboard } from 'app/types'; 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 { appEvents } from 'app/core/core';
import DashboardsTable from 'app/features/datasources/DashboardsTable'; import DashboardsTable from 'app/features/datasources/DashboardsTable';
import { AppEvents, PluginMeta, DataSourceApi } from '@grafana/data'; 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'; import { PluginMeta } from '@grafana/data';
type PluginCache = { type PluginCache = {

View File

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

View File

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

View File

@@ -11,9 +11,7 @@ import { ConstantVariable } from './constant_variable';
import { AdhocVariable } from './adhoc_variable'; import { AdhocVariable } from './adhoc_variable';
import { TextBoxVariable } from './TextBoxVariable'; import { TextBoxVariable } from './TextBoxVariable';
coreModule.factory('templateSrv', () => { coreModule.factory('templateSrv', () => templateSrv);
return templateSrv;
});
export { export {
VariableSrv, 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' }]; datasource.metricFindQuery = async () => [{ value: 'test', label: 'test' }];
const props: Props = { const props: Props = {

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ import { IndexPattern } from './index_pattern';
import { ElasticQueryBuilder } from './query_builder'; import { ElasticQueryBuilder } from './query_builder';
import { toUtc } from '@grafana/data'; import { toUtc } from '@grafana/data';
import * as queryDef from './query_def'; 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 { TemplateSrv } from 'app/features/templating/template_srv';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { DataLinkConfig, ElasticsearchOptions, ElasticsearchQuery } from './types'; import { DataLinkConfig, ElasticsearchOptions, ElasticsearchQuery } from './types';
@@ -36,7 +36,6 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
/** @ngInject */ /** @ngInject */
constructor( constructor(
instanceSettings: DataSourceInstanceSettings<ElasticsearchOptions>, instanceSettings: DataSourceInstanceSettings<ElasticsearchOptions>,
private backendSrv: BackendSrv,
private templateSrv: TemplateSrv, private templateSrv: TemplateSrv,
private timeSrv: TimeSrv 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) { 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 annotation = options.annotation;
const timeField = annotation.timeField || '@timestamp'; const timeField = annotation.timeField || '@timestamp';
const timeEndField = annotation.timeEndField || null; const timeEndField = annotation.timeEndField || null;
@@ -511,20 +510,20 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
metricFindQuery(query: any) { metricFindQuery(query: any) {
query = angular.fromJson(query); query = angular.fromJson(query);
if (!query) { if (query) {
return Promise.resolve([]); 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') { return Promise.resolve([]);
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);
}
} }
getTagKeys() { getTagKeys() {

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,21 +2,28 @@ import AzureMonitorDatasource from '../datasource';
import { TemplateSrv } from 'app/features/templating/template_srv'; import { TemplateSrv } from 'app/features/templating/template_srv';
import { toUtc, DataFrame } from '@grafana/data'; 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', () => { describe('AzureMonitorDatasource', () => {
const ctx: any = { const ctx: any = {
backendSrv: {},
templateSrv: new TemplateSrv(), templateSrv: new TemplateSrv(),
}; };
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks();
ctx.instanceSettings = { ctx.instanceSettings = {
url: 'http://azuremonitor.com', url: 'http://azuremonitor.com',
jsonData: { subscriptionId: '9935389e-9122-4ef9-95f9-1513dd24753f' }, jsonData: { subscriptionId: '9935389e-9122-4ef9-95f9-1513dd24753f' },
cloudName: 'azuremonitor', cloudName: 'azuremonitor',
}; };
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv); ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.templateSrv);
}); });
describe('When performing testDatasource', () => { describe('When performing testDatasource', () => {
@@ -35,9 +42,7 @@ describe('AzureMonitorDatasource', () => {
beforeEach(() => { beforeEach(() => {
ctx.instanceSettings.jsonData.tenantId = 'xxx'; ctx.instanceSettings.jsonData.tenantId = 'xxx';
ctx.instanceSettings.jsonData.clientId = 'xxx'; ctx.instanceSettings.jsonData.clientId = 'xxx';
ctx.backendSrv.datasourceRequest = () => { datasourceRequestMock.mockImplementation(() => Promise.reject(error));
return Promise.reject(error);
};
}); });
it('should return error status and a detailed error message', () => { it('should return error status and a detailed error message', () => {
@@ -62,9 +67,7 @@ describe('AzureMonitorDatasource', () => {
beforeEach(() => { beforeEach(() => {
ctx.instanceSettings.jsonData.tenantId = 'xxx'; ctx.instanceSettings.jsonData.tenantId = 'xxx';
ctx.instanceSettings.jsonData.clientId = 'xxx'; ctx.instanceSettings.jsonData.clientId = 'xxx';
ctx.backendSrv.datasourceRequest = () => { datasourceRequestMock.mockImplementation(() => Promise.resolve({ data: response, status: 200 }));
return Promise.resolve({ data: response, status: 200 });
};
}); });
it('should return success status', () => { it('should return success status', () => {
@@ -124,10 +127,10 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => { datasourceRequestMock.mockImplementation((options: { url: string }) => {
expect(options.url).toContain('/api/tsdb/query'); expect(options.url).toContain('/api/tsdb/query');
return Promise.resolve({ data: response, status: 200 }); return Promise.resolve({ data: response, status: 200 });
}; });
}); });
it('should return a list of datapoints', () => { it('should return a list of datapoints', () => {
@@ -157,9 +160,7 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = () => { datasourceRequestMock.mockImplementation((options: { url: string }) => Promise.resolve(response));
return Promise.resolve(response);
};
}); });
it('should return a list of subscriptions', () => { it('should return a list of subscriptions', () => {
@@ -183,9 +184,7 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = () => { datasourceRequestMock.mockImplementation(() => Promise.resolve(response));
return Promise.resolve(response);
};
}); });
it('should return a list of resource groups', () => { it('should return a list of resource groups', () => {
@@ -209,10 +208,10 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => { datasourceRequestMock.mockImplementation((options: { url: string }) => {
expect(options.url).toContain('11112222-eeee-4949-9b2d-9106972f9123'); expect(options.url).toContain('11112222-eeee-4949-9b2d-9106972f9123');
return Promise.resolve(response); return Promise.resolve(response);
}; });
}); });
it('should return a list of resource groups', () => { it('should return a list of resource groups', () => {
@@ -243,12 +242,12 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => { datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl = const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01'); expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01');
return Promise.resolve(response); return Promise.resolve(response);
}; });
}); });
it('should return a list of namespaces', () => { it('should return a list of namespaces', () => {
@@ -277,12 +276,12 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => { datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl = const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups'; 'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01'); expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01');
return Promise.resolve(response); return Promise.resolve(response);
}; });
}); });
it('should return a list of namespaces', () => { it('should return a list of namespaces', () => {
@@ -315,12 +314,12 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => { datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl = const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01'); expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01');
return Promise.resolve(response); return Promise.resolve(response);
}; });
}); });
it('should return a list of resource names', () => { it('should return a list of resource names', () => {
@@ -353,12 +352,12 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => { datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl = const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups'; 'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01'); expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01');
return Promise.resolve(response); return Promise.resolve(response);
}; });
}); });
it('should return a list of resource names', () => { it('should return a list of resource names', () => {
@@ -397,7 +396,7 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => { datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl = const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(options.url).toBe( expect(options.url).toBe(
@@ -406,7 +405,7 @@ describe('AzureMonitorDatasource', () => {
'metricdefinitions?api-version=2018-01-01&metricnamespace=default' 'metricdefinitions?api-version=2018-01-01&metricnamespace=default'
); );
return Promise.resolve(response); return Promise.resolve(response);
}; });
}); });
it('should return a list of metric names', () => { it('should return a list of metric names', () => {
@@ -446,7 +445,7 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => { datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl = const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups'; 'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
expect(options.url).toBe( expect(options.url).toBe(
@@ -455,7 +454,7 @@ describe('AzureMonitorDatasource', () => {
'metricdefinitions?api-version=2018-01-01&metricnamespace=default' 'metricdefinitions?api-version=2018-01-01&metricnamespace=default'
); );
return Promise.resolve(response); return Promise.resolve(response);
}; });
}); });
it('should return a list of metric names', () => { it('should return a list of metric names', () => {
@@ -497,7 +496,7 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => { datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl = const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(options.url).toBe( 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' '/nodeapp/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview'
); );
return Promise.resolve(response); return Promise.resolve(response);
}; });
}); });
it('should return a list of metric names', () => { it('should return a list of metric names', () => {
@@ -545,7 +544,7 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => { datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl = const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups'; 'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
expect(options.url).toBe( 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' '/nodeapp/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview'
); );
return Promise.resolve(response); return Promise.resolve(response);
}; });
}); });
it('should return a list of metric namespaces', () => { it('should return a list of metric namespaces', () => {
@@ -601,9 +600,7 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = () => { datasourceRequestMock.mockImplementation(() => Promise.resolve(response));
return Promise.resolve(response);
};
}); });
it('should return list of Resource Groups', () => { it('should return list of Resource Groups', () => {
@@ -625,9 +622,7 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = () => { datasourceRequestMock.mockImplementation(() => Promise.resolve(response));
return Promise.resolve(response);
};
}); });
it('should return list of Resource Groups', () => { it('should return list of Resource Groups', () => {
@@ -674,12 +669,12 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => { datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl = const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01'); expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01');
return Promise.resolve(response); return Promise.resolve(response);
}; });
}); });
it('should return list of Metric Definitions with no duplicates and no unsupported namespaces', () => { it('should return list of Metric Definitions with no duplicates and no unsupported namespaces', () => {
@@ -725,12 +720,12 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => { datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl = const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01'); expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01');
return Promise.resolve(response); return Promise.resolve(response);
}; });
}); });
it('should return list of Resource Names', () => { it('should return list of Resource Names', () => {
@@ -763,12 +758,12 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => { datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl = const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01'); expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01');
return Promise.resolve(response); return Promise.resolve(response);
}; });
}); });
it('should return list of Resource Names', () => { it('should return list of Resource Names', () => {
@@ -828,7 +823,7 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => { datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl = const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp'; 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
const expected = const expected =
@@ -837,7 +832,7 @@ describe('AzureMonitorDatasource', () => {
'/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default'; '/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
expect(options.url).toBe(expected); expect(options.url).toBe(expected);
return Promise.resolve(response); return Promise.resolve(response);
}; });
}); });
it('should return list of Metric Definitions', () => { it('should return list of Metric Definitions', () => {
@@ -900,7 +895,7 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => { datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl = const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp'; 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
const expected = const expected =
@@ -909,7 +904,7 @@ describe('AzureMonitorDatasource', () => {
'/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default'; '/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
expect(options.url).toBe(expected); expect(options.url).toBe(expected);
return Promise.resolve(response); return Promise.resolve(response);
}; });
}); });
it('should return Aggregation metadata for a Metric', () => { it('should return Aggregation metadata for a Metric', () => {
@@ -974,7 +969,7 @@ describe('AzureMonitorDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => { datasourceRequestMock.mockImplementation((options: { url: string }) => {
const baseUrl = const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp'; 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
const expected = const expected =
@@ -983,7 +978,7 @@ describe('AzureMonitorDatasource', () => {
'/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default'; '/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
expect(options.url).toBe(expected); expect(options.url).toBe(expected);
return Promise.resolve(response); return Promise.resolve(response);
}; });
}); });
it('should return dimensions for a Metric that has dimensions', () => { 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 { DataQueryRequest, DataQueryResponseData, DataSourceInstanceSettings } from '@grafana/data';
import { TimeSeries, toDataFrame } 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 { TemplateSrv } from 'app/features/templating/template_srv';
import { getBackendSrv } from '@grafana/runtime';
export default class AzureMonitorDatasource { export default class AzureMonitorDatasource {
apiVersion = '2018-01-01'; apiVersion = '2018-01-01';
@@ -31,7 +31,6 @@ export default class AzureMonitorDatasource {
/** @ngInject */ /** @ngInject */
constructor( constructor(
private instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>, private instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>,
private backendSrv: BackendSrv,
private templateSrv: TemplateSrv private templateSrv: TemplateSrv
) { ) {
this.id = instanceSettings.id; this.id = instanceSettings.id;
@@ -108,7 +107,7 @@ export default class AzureMonitorDatasource {
return Promise.resolve([]); return Promise.resolve([]);
} }
const { data } = await this.backendSrv.datasourceRequest({ const { data } = await getBackendSrv().datasourceRequest({
url: '/api/tsdb/query', url: '/api/tsdb/query',
method: 'POST', method: 'POST',
data: { data: {
@@ -434,8 +433,8 @@ export default class AzureMonitorDatasource {
return field && field.length > 0; return field && field.length > 0;
} }
doRequest(url: string, maxRetries = 1) { doRequest(url: string, maxRetries = 1): Promise<any> {
return this.backendSrv return getBackendSrv()
.datasourceRequest({ .datasourceRequest({
url: this.url + url, url: this.url + url,
method: 'GET', method: 'GET',

View File

@@ -10,7 +10,7 @@ import {
import { MonitorConfig } from './MonitorConfig'; import { MonitorConfig } from './MonitorConfig';
import { AnalyticsConfig } from './AnalyticsConfig'; import { AnalyticsConfig } from './AnalyticsConfig';
import { TemplateSrv } from 'app/features/templating/template_srv'; 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 { InsightsConfig } from './InsightsConfig';
import ResponseParser from '../azure_monitor/response_parser'; import ResponseParser from '../azure_monitor/response_parser';
import { AzureDataSourceJsonData, AzureDataSourceSecureJsonData, AzureDataSourceSettings } from '../types'; import { AzureDataSourceJsonData, AzureDataSourceSecureJsonData, AzureDataSourceSettings } from '../types';
@@ -38,7 +38,6 @@ export class ConfigEditor extends PureComponent<Props, State> {
logAnalyticsSubscriptionId: '', logAnalyticsSubscriptionId: '',
}; };
this.backendSrv = getBackendSrv();
this.templateSrv = new TemplateSrv(); this.templateSrv = new TemplateSrv();
if (this.props.options.id) { if (this.props.options.id) {
updateDatasourcePluginOption(this.props, 'url', '/api/datasources/proxy/' + 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; initPromise: CancelablePromise<any> = null;
backendSrv: BackendSrv = null;
templateSrv: TemplateSrv = null; templateSrv: TemplateSrv = null;
componentDidMount() { componentDidMount() {
@@ -157,7 +155,7 @@ export class ConfigEditor extends PureComponent<Props, State> {
}; };
onLoadSubscriptions = async (type?: string) => { onLoadSubscriptions = async (type?: string) => {
await this.backendSrv await getBackendSrv()
.put(`/api/datasources/${this.props.options.id}`, this.props.options) .put(`/api/datasources/${this.props.options.id}`, this.props.options)
.then((result: AzureDataSourceSettings) => { .then((result: AzureDataSourceSettings) => {
updateDatasourcePluginOption(this.props, 'version', result.version); updateDatasourcePluginOption(this.props, 'version', result.version);
@@ -173,7 +171,7 @@ export class ConfigEditor extends PureComponent<Props, State> {
loadSubscriptions = async (route?: string) => { loadSubscriptions = async (route?: string) => {
const url = `/${route || this.props.options.jsonData.cloudName}/subscriptions?api-version=2019-03-01`; 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, url: this.props.options.url + url,
method: 'GET', method: 'GET',
}); });
@@ -198,7 +196,7 @@ export class ConfigEditor extends PureComponent<Props, State> {
azureMonitorUrl + azureMonitorUrl +
`/${subscriptionId}/providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview`; `/${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, url: this.props.options.url + workspaceListUrl,
method: 'GET', 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 AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource';
import { AzureMonitorQuery, AzureDataSourceJsonData } from './types'; import { AzureMonitorQuery, AzureDataSourceJsonData } from './types';
import { DataSourceApi, DataQueryRequest, DataSourceInstanceSettings } from '@grafana/data'; import { DataSourceApi, DataQueryRequest, DataSourceInstanceSettings } from '@grafana/data';
import { BackendSrv } from 'app/core/services/backend_srv';
import { TemplateSrv } from 'app/features/templating/template_srv'; import { TemplateSrv } from 'app/features/templating/template_srv';
export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDataSourceJsonData> { export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDataSourceJsonData> {
@@ -13,20 +12,12 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
azureLogAnalyticsDatasource: AzureLogAnalyticsDatasource; azureLogAnalyticsDatasource: AzureLogAnalyticsDatasource;
/** @ngInject */ /** @ngInject */
constructor( constructor(instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>, private templateSrv: TemplateSrv) {
instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>,
private backendSrv: BackendSrv,
private templateSrv: TemplateSrv
) {
super(instanceSettings); super(instanceSettings);
this.azureMonitorDatasource = new AzureMonitorDatasource(instanceSettings, this.backendSrv, this.templateSrv); this.azureMonitorDatasource = new AzureMonitorDatasource(instanceSettings, this.templateSrv);
this.appInsightsDatasource = new AppInsightsDatasource(instanceSettings, this.backendSrv, this.templateSrv); this.appInsightsDatasource = new AppInsightsDatasource(instanceSettings, this.templateSrv);
this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource( this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource(instanceSettings, this.templateSrv);
instanceSettings,
this.backendSrv,
this.templateSrv
);
} }
async query(options: DataQueryRequest<AzureMonitorQuery>) { async query(options: DataQueryRequest<AzureMonitorQuery>) {

View File

@@ -1,13 +1,13 @@
import _ from 'lodash'; import _ from 'lodash';
import { BackendSrv } from 'app/core/services/backend_srv';
import { TemplateSrv } from 'app/features/templating/template_srv'; import { TemplateSrv } from 'app/features/templating/template_srv';
import { getBackendSrv } from '@grafana/runtime';
class GrafanaDatasource { class GrafanaDatasource {
/** @ngInject */ /** @ngInject */
constructor(private backendSrv: BackendSrv, private templateSrv: TemplateSrv) {} constructor(private templateSrv: TemplateSrv) {}
query(options: any) { query(options: any) {
return this.backendSrv return getBackendSrv()
.get('/api/tsdb/testdata/random-walk', { .get('/api/tsdb/testdata/random-walk', {
from: options.range.from.valueOf(), from: options.range.from.valueOf(),
to: options.range.to.valueOf(), to: options.range.to.valueOf(),
@@ -76,7 +76,7 @@ class GrafanaDatasource {
params.tags = tags; 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 { 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', () => { describe('grafana data source', () => {
const getMock = jest.spyOn(backendSrv, 'get');
beforeEach(() => {
jest.clearAllMocks();
});
describe('when executing an annotations query', () => { describe('when executing an annotations query', () => {
let calledBackendSrvParams: any; let calledBackendSrvParams: any;
const backendSrvStub = { let templateSrvStub: any;
get: (url: string, options: any) => { let ds: GrafanaDatasource;
beforeEach(() => {
getMock.mockImplementation((url: string, options: any) => {
calledBackendSrvParams = options; calledBackendSrvParams = options;
return q.resolve([]); return Promise.resolve([]);
}, });
};
const templateSrvStub = { templateSrvStub = {
replace: (val: string) => { replace: (val: string) => {
return val.replace('$var2', 'replaced__delimiter__replaced2').replace('$var', 'replaced'); 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', () => { describe('with tags that have template variables', () => {
const options = setupAnnotationQueryOptions({ tags: ['tag1:$var'] }); const options = setupAnnotationQueryOptions({ tags: ['tag1:$var'] });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,19 +4,26 @@ import { CustomVariable } from 'app/features/templating/custom_variable';
import { dateTime } from '@grafana/data'; import { dateTime } from '@grafana/data';
import { TemplateSrv } from 'app/features/templating/template_srv'; 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', () => { describe('MSSQLDatasource', () => {
const templateSrv: TemplateSrv = new TemplateSrv(); const templateSrv: TemplateSrv = new TemplateSrv();
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
const ctx: any = { const ctx: any = {
backendSrv: {},
timeSrv: new TimeSrvStub(), timeSrv: new TimeSrvStub(),
}; };
beforeEach(() => { 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', () => { describe('When performing annotationQuery', () => {
@@ -54,9 +61,7 @@ describe('MSSQLDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: any) => { datasourceRequestMock.mockImplementation((options: any) => Promise.resolve({ data: response, status: 200 }));
return Promise.resolve({ data: response, status: 200 });
};
return ctx.ds.annotationQuery(options).then((data: any) => { return ctx.ds.annotationQuery(options).then((data: any) => {
results = data; results = data;
@@ -102,9 +107,7 @@ describe('MSSQLDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: any) => { datasourceRequestMock.mockImplementation((options: any) => Promise.resolve({ data: response, status: 200 }));
return Promise.resolve({ data: response, status: 200 });
};
return ctx.ds.metricFindQuery(query).then((data: any) => { return ctx.ds.metricFindQuery(query).then((data: any) => {
results = data; results = data;
@@ -143,9 +146,7 @@ describe('MSSQLDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: any) => { datasourceRequestMock.mockImplementation((options: any) => Promise.resolve({ data: response, status: 200 }));
return Promise.resolve({ data: response, status: 200 });
};
return ctx.ds.metricFindQuery(query).then((data: any) => { return ctx.ds.metricFindQuery(query).then((data: any) => {
results = data; results = data;
@@ -186,10 +187,7 @@ describe('MSSQLDatasource', () => {
}; };
beforeEach(() => { beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: any) => { datasourceRequestMock.mockImplementation((options: any) => Promise.resolve({ data: response, status: 200 }));
return Promise.resolve({ data: response, status: 200 });
};
return ctx.ds.metricFindQuery(query).then((data: any) => { return ctx.ds.metricFindQuery(query).then((data: any) => {
results = data; results = data;
}); });
@@ -229,10 +227,10 @@ describe('MSSQLDatasource', () => {
beforeEach(() => { beforeEach(() => {
ctx.timeSrv.setTime(time); ctx.timeSrv.setTime(time);
ctx.backendSrv.datasourceRequest = (options: any) => { datasourceRequestMock.mockImplementation((options: any) => {
results = options.data; results = options.data;
return Promise.resolve({ data: response, status: 200 }); return Promise.resolve({ data: response, status: 200 });
}; });
return ctx.ds.metricFindQuery(query); return ctx.ds.metricFindQuery(query);
}); });

View File

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

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